From b8fe991ba28eb883a64b1309e3e02c6158fcef32 Mon Sep 17 00:00:00 2001 From: Leopold Schabel Date: Tue, 5 Feb 2019 13:47:24 +0000 Subject: [PATCH 001/245] Improve description text in the "Create Diff" form Summary: The textarea is, in fact, above the description! Test Plan: Description text changed. Reviewers: #blessed_reviewers, epriestley Reviewed By: #blessed_reviewers, epriestley Subscribers: Korvin, epriestley Differential Revision: https://secure.phabricator.com/D20092 --- .../controller/DifferentialDiffCreateController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/applications/differential/controller/DifferentialDiffCreateController.php b/src/applications/differential/controller/DifferentialDiffCreateController.php index 36f0b86202..284edf49d3 100644 --- a/src/applications/differential/controller/DifferentialDiffCreateController.php +++ b/src/applications/differential/controller/DifferentialDiffCreateController.php @@ -112,7 +112,7 @@ final class DifferentialDiffCreateController extends DifferentialController { $arcanist_link, ), pht( - 'You can also paste a diff below, or upload a file '. + 'You can also paste a diff above, or upload a file '. 'containing a diff (for example, from %s, %s or %s).', phutil_tag('tt', array(), 'svn diff'), phutil_tag('tt', array(), 'git diff'), From 4675306615da8848e0f196820af7bcc6f9fa6118 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 4 Feb 2019 16:52:58 -0800 Subject: [PATCH 002/245] Add a "metronome" for spreading service call load Summary: Ref T13244. See D20080. Rather than randomly jittering service calls, we can give each host a "metronome" that ticks every 60 seconds to get load to spread out after one cycle. For example, web001 ticks (and makes a service call) when the second hand points at 0:17, web002 at 0:43, web003 at 0:04, etc. For now I'm just planning to seed the metronomes randomly based on hostname, but we could conceivably give each host an assigned offset some day if we want perfectly smooth service call rates. Test Plan: Ran unit tests. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13244 Differential Revision: https://secure.phabricator.com/D20087 --- src/__phutil_library_map__.php | 4 + .../util/PhabricatorMetronome.php | 92 +++++++++++++++++++ .../PhabricatorMetronomeTestCase.php | 61 ++++++++++++ 3 files changed, 157 insertions(+) create mode 100644 src/infrastructure/util/PhabricatorMetronome.php create mode 100644 src/infrastructure/util/__tests__/PhabricatorMetronomeTestCase.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 324fc3c5de..fe77880417 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -3552,6 +3552,8 @@ phutil_register_library_map(array( 'PhabricatorMetaMTASchemaSpec' => 'applications/metamta/storage/PhabricatorMetaMTASchemaSpec.php', 'PhabricatorMetaMTASendGridReceiveController' => 'applications/metamta/controller/PhabricatorMetaMTASendGridReceiveController.php', 'PhabricatorMetaMTAWorker' => 'applications/metamta/PhabricatorMetaMTAWorker.php', + 'PhabricatorMetronome' => 'infrastructure/util/PhabricatorMetronome.php', + 'PhabricatorMetronomeTestCase' => 'infrastructure/util/__tests__/PhabricatorMetronomeTestCase.php', 'PhabricatorMetronomicTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorMetronomicTriggerClock.php', 'PhabricatorModularTransaction' => 'applications/transactions/storage/PhabricatorModularTransaction.php', 'PhabricatorModularTransactionType' => 'applications/transactions/storage/PhabricatorModularTransactionType.php', @@ -9477,6 +9479,8 @@ phutil_register_library_map(array( 'PhabricatorMetaMTASchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorMetaMTASendGridReceiveController' => 'PhabricatorMetaMTAController', 'PhabricatorMetaMTAWorker' => 'PhabricatorWorker', + 'PhabricatorMetronome' => 'Phobject', + 'PhabricatorMetronomeTestCase' => 'PhabricatorTestCase', 'PhabricatorMetronomicTriggerClock' => 'PhabricatorTriggerClock', 'PhabricatorModularTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorModularTransactionType' => 'Phobject', diff --git a/src/infrastructure/util/PhabricatorMetronome.php b/src/infrastructure/util/PhabricatorMetronome.php new file mode 100644 index 0000000000..24f58127f6 --- /dev/null +++ b/src/infrastructure/util/PhabricatorMetronome.php @@ -0,0 +1,92 @@ +offset = $offset; + + return $this; + } + + public function setFrequency($frequency) { + if (!is_int($frequency)) { + throw new Exception(pht('Metronome frequency must be an integer.')); + } + + if ($frequency < 1) { + throw new Exception(pht('Metronome frequency must be 1 or more.')); + } + + $this->frequency = $frequency; + + return $this; + } + + public function setOffsetFromSeed($seed) { + $offset = PhabricatorHash::digestToRange($seed, 0, PHP_INT_MAX); + return $this->setOffset($offset); + } + + public function getFrequency() { + if ($this->frequency === null) { + throw new PhutilInvalidStateException('setFrequency'); + } + return $this->frequency; + } + + public function getOffset() { + $frequency = $this->getFrequency(); + return ($this->offset % $frequency); + } + + public function getNextTickAfter($epoch) { + $frequency = $this->getFrequency(); + $offset = $this->getOffset(); + + $remainder = ($epoch % $frequency); + + if ($remainder < $offset) { + return ($epoch - $remainder) + $offset; + } else { + return ($epoch - $remainder) + $frequency + $offset; + } + } + + public function didTickBetween($min, $max) { + if ($max < $min) { + throw new Exception( + pht( + 'Maximum tick window must not be smaller than minimum tick window.')); + } + + $next = $this->getNextTickAfter($min); + return ($next <= $max); + } + +} diff --git a/src/infrastructure/util/__tests__/PhabricatorMetronomeTestCase.php b/src/infrastructure/util/__tests__/PhabricatorMetronomeTestCase.php new file mode 100644 index 0000000000..9ad74e2b90 --- /dev/null +++ b/src/infrastructure/util/__tests__/PhabricatorMetronomeTestCase.php @@ -0,0 +1,61 @@ + 44, + 'web002.example.net' => 36, + 'web003.example.net' => 25, + 'web004.example.net' => 25, + 'web005.example.net' => 16, + 'web006.example.net' => 26, + 'web007.example.net' => 35, + 'web008.example.net' => 14, + ); + + $metronome = id(new PhabricatorMetronome()) + ->setFrequency(60); + + foreach ($cases as $input => $expect) { + $metronome->setOffsetFromSeed($input); + + $this->assertEqual( + $expect, + $metronome->getOffset(), + pht('Offset for: %s', $input)); + } + } + + public function testMetronomeTicks() { + $metronome = id(new PhabricatorMetronome()) + ->setFrequency(60) + ->setOffset(13); + + $tick_epoch = strtotime('2000-01-01 11:11:13 AM UTC'); + + // Since the epoch is at "0:13" on the clock, the metronome should tick + // then. + $this->assertEqual( + $tick_epoch, + $metronome->getNextTickAfter($tick_epoch - 1), + pht('Tick at 11:11:13 AM.')); + + // The next tick should be a minute later. + $this->assertEqual( + $tick_epoch + 60, + $metronome->getNextTickAfter($tick_epoch), + pht('Tick at 11:12:13 AM.')); + + + // There's no tick in the next 59 seconds. + $this->assertFalse( + $metronome->didTickBetween($tick_epoch, $tick_epoch + 59)); + + $this->assertTrue( + $metronome->didTickBetween($tick_epoch, $tick_epoch + 60)); + } + + +} From ee24eb60b765c1cf831a97aabff69afc9ca921be Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 4 Feb 2019 19:33:30 -0800 Subject: [PATCH 003/245] In Owners Packages, make the API representation of the "Auditing" field more consistent Summary: Ref T13244. See PHI1047. A while ago, the "Review" field changed from "yes/no" to 20 flavors of "Non-Owner Blocking Under A Full Moon". The sky didn't fall, so we'll probably do this to "Audit" eventually too. The "owners.search" API method anticipates this and returns "none" or "audit" to describe package audit statuses, so it can begin returning "audit-non-owner-reviewers" or whatever in the future. However, the "owners.edit" API method doesn't work the same way, and takes strings, and the strings have to be numbers. This is goofy and confusing and generally bad. Make "owners.edit" take the same strings that "owners.search" emits. For now, continue accepting the old values of "0" and "1". Test Plan: - Edited audit status of packages via API using "none", "audit", "0", "1" (worked), and invalid values like "quack" (helpful error). - Edited audit status of packages via web UI. - Used `owners.search` to retrieve package information. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13244 Differential Revision: https://secure.phabricator.com/D20091 --- .../PhabricatorOwnersPackageEditEngine.php | 6 +-- .../storage/PhabricatorOwnersPackage.php | 14 ++++- ...icatorOwnersPackageAuditingTransaction.php | 53 ++++++++++++++++++- 3 files changed, 67 insertions(+), 6 deletions(-) diff --git a/src/applications/owners/editor/PhabricatorOwnersPackageEditEngine.php b/src/applications/owners/editor/PhabricatorOwnersPackageEditEngine.php index 044cb8beda..c4ee026374 100644 --- a/src/applications/owners/editor/PhabricatorOwnersPackageEditEngine.php +++ b/src/applications/owners/editor/PhabricatorOwnersPackageEditEngine.php @@ -140,11 +140,11 @@ EOTEXT ->setTransactionType( PhabricatorOwnersPackageAuditingTransaction::TRANSACTIONTYPE) ->setIsCopyable(true) - ->setValue($object->getAuditingEnabled()) + ->setValue($object->getAuditingState()) ->setOptions( array( - '' => pht('Disabled'), - '1' => pht('Enabled'), + PhabricatorOwnersPackage::AUDITING_NONE => pht('No Auditing'), + PhabricatorOwnersPackage::AUDITING_AUDIT => pht('Audit Commits'), )), id(new PhabricatorRemarkupEditField()) ->setKey('description') diff --git a/src/applications/owners/storage/PhabricatorOwnersPackage.php b/src/applications/owners/storage/PhabricatorOwnersPackage.php index 564fc8a28b..207e0cb809 100644 --- a/src/applications/owners/storage/PhabricatorOwnersPackage.php +++ b/src/applications/owners/storage/PhabricatorOwnersPackage.php @@ -38,6 +38,9 @@ final class PhabricatorOwnersPackage const AUTOREVIEW_BLOCK = 'block'; const AUTOREVIEW_BLOCK_ALWAYS = 'block-always'; + const AUDITING_NONE = 'none'; + const AUDITING_AUDIT = 'audit'; + const DOMINION_STRONG = 'strong'; const DOMINION_WEAK = 'weak'; @@ -564,6 +567,14 @@ final class PhabricatorOwnersPackage return '/owners/package/'.$this->getID().'/'; } + public function getAuditingState() { + if ($this->getAuditingEnabled()) { + return self::AUDITING_AUDIT; + } else { + return self::AUDITING_NONE; + } + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ @@ -720,11 +731,10 @@ final class PhabricatorOwnersPackage 'label' => $review_label, ); + $audit_value = $this->getAuditingState(); if ($this->getAuditingEnabled()) { - $audit_value = 'audit'; $audit_label = pht('Auditing Enabled'); } else { - $audit_value = 'none'; $audit_label = pht('No Auditing'); } diff --git a/src/applications/owners/xaction/PhabricatorOwnersPackageAuditingTransaction.php b/src/applications/owners/xaction/PhabricatorOwnersPackageAuditingTransaction.php index df4f0feb01..d7ea7093f9 100644 --- a/src/applications/owners/xaction/PhabricatorOwnersPackageAuditingTransaction.php +++ b/src/applications/owners/xaction/PhabricatorOwnersPackageAuditingTransaction.php @@ -10,7 +10,15 @@ final class PhabricatorOwnersPackageAuditingTransaction } public function generateNewValue($object, $value) { - return (int)$value; + switch ($value) { + case PhabricatorOwnersPackage::AUDITING_AUDIT: + return 1; + case '1': + // TODO: Remove, deprecated. + return 1; + default: + return 0; + } } public function applyInternalEffects($object, $value) { @@ -29,4 +37,47 @@ final class PhabricatorOwnersPackageAuditingTransaction } } + public function validateTransactions($object, array $xactions) { + $errors = array(); + + // See PHI1047. This transaction type accepted some weird stuff. Continue + // supporting it for now, but move toward sensible consistency. + + $modern_options = array( + PhabricatorOwnersPackage::AUDITING_NONE => + sprintf('"%s"', PhabricatorOwnersPackage::AUDITING_NONE), + PhabricatorOwnersPackage::AUDITING_AUDIT => + sprintf('"%s"', PhabricatorOwnersPackage::AUDITING_AUDIT), + ); + + $deprecated_options = array( + '0' => '"0"', + '1' => '"1"', + '' => pht('"" (empty string)'), + ); + + foreach ($xactions as $xaction) { + $new_value = $xaction->getNewValue(); + + if (isset($modern_options[$new_value])) { + continue; + } + + if (isset($deprecated_options[$new_value])) { + continue; + } + + $errors[] = $this->newInvalidError( + pht( + 'Package auditing value "%s" is not supported. Supported options '. + 'are: %s. Deprecated options are: %s.', + $new_value, + implode(', ', $modern_options), + implode(', ', $deprecated_options)), + $xaction); + } + + return $errors; + } + } From a58c9d50b2e1f9cbeabb979cbc4372d0c1981e68 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 4 Feb 2019 16:13:04 -0800 Subject: [PATCH 004/245] Slightly update the Diviner documentation Summary: See PHI1050. Although Diviner hasn't received a ton of new development for a while, it's: not exaclty new; and pretty useful for what we need it for. Test Plan: Reading. Reviewers: amckinley Reviewed By: amckinley Subscribers: leoluk Differential Revision: https://secure.phabricator.com/D20086 --- src/docs/user/userguide/diviner.diviner | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/docs/user/userguide/diviner.diviner b/src/docs/user/userguide/diviner.diviner index e94c33d275..01484be14c 100644 --- a/src/docs/user/userguide/diviner.diviner +++ b/src/docs/user/userguide/diviner.diviner @@ -3,17 +3,28 @@ Using Diviner, a documentation generator. -= Overview = +Overview +======== -NOTE: Diviner is new and not yet generally useful. +Diviner is an application for creating technical documentation. -= Generating Documentation = +This article is maintained in a text file in the Phabricator repository and +generated into the display document you are currently reading using Diviner. + +Beyond generating articles, Diviner can also analyze source code and generate +documentation about classes, methods, and other primitives. + + +Generating Documentation +======================== To generate documentation, run: phabricator/ $ ./bin/diviner generate --book -= .book Files = + +Diviner ".book" Files +===================== Diviner documentation books are configured using JSON `.book` files, which look like this: From 5dc650229350ed5c6df44d351e80bb24fc547f84 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 5 Feb 2019 05:11:01 -0800 Subject: [PATCH 005/245] Make the mobile menu available in "/mail/" Summary: Ref T13244. See . Test Plan: {F6184160} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13244 Differential Revision: https://secure.phabricator.com/D20093 --- .../controller/PhabricatorMetaMTAMailListController.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/applications/metamta/controller/PhabricatorMetaMTAMailListController.php b/src/applications/metamta/controller/PhabricatorMetaMTAMailListController.php index 0651068550..01d6f1e218 100644 --- a/src/applications/metamta/controller/PhabricatorMetaMTAMailListController.php +++ b/src/applications/metamta/controller/PhabricatorMetaMTAMailListController.php @@ -27,4 +27,8 @@ final class PhabricatorMetaMTAMailListController return $nav; } + public function buildApplicationMenu() { + return $this->buildSideNav()->getMenu(); + } + } From ab467d52f4dd7be9b137cab533cec7b155547fe9 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 5 Feb 2019 07:16:56 -0800 Subject: [PATCH 006/245] Improve feed rendering of user rename story Summary: Ref T13244. This story publishes to the feed (and I think that's reasonable and desirable) but doesn't render as nicely as it could. Improve the rendering. (See T9233 for some context on why we render stories like this one in this way.) Test Plan: {F6184490} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13244 Differential Revision: https://secure.phabricator.com/D20097 --- .../xaction/PhabricatorUserUsernameTransaction.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/applications/people/xaction/PhabricatorUserUsernameTransaction.php b/src/applications/people/xaction/PhabricatorUserUsernameTransaction.php index b6d23b3511..f2226d0010 100644 --- a/src/applications/people/xaction/PhabricatorUserUsernameTransaction.php +++ b/src/applications/people/xaction/PhabricatorUserUsernameTransaction.php @@ -40,6 +40,15 @@ final class PhabricatorUserUsernameTransaction $this->renderNewValue()); } + public function getTitleForFeed() { + return pht( + '%s renamed %s from %s to %s.', + $this->renderAuthor(), + $this->renderObject(), + $this->renderOldValue(), + $this->renderNewValue()); + } + public function validateTransactions($object, array $xactions) { $actor = $this->getActor(); $errors = array(); From 03eb989fd875215d5ac8c47ed8c171f3bdb86163 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 4 Feb 2019 16:52:58 -0800 Subject: [PATCH 007/245] Give Duo MFA a stronger hint if users continue without answering the challenge Summary: See PHI912. Also, clean up some leftover copy/pastey code here. Test Plan: {F6182333} Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20088 --- .../auth/factor/PhabricatorAuthFactor.php | 29 +++++++--- .../factor/PhabricatorAuthFactorResult.php | 10 ++++ .../auth/factor/PhabricatorDuoAuthFactor.php | 56 ++++++------------- .../auth/storage/PhabricatorAuthChallenge.php | 10 ++++ 4 files changed, 59 insertions(+), 46 deletions(-) diff --git a/src/applications/auth/factor/PhabricatorAuthFactor.php b/src/applications/auth/factor/PhabricatorAuthFactor.php index ec49f7f748..912a2c31c9 100644 --- a/src/applications/auth/factor/PhabricatorAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorAuthFactor.php @@ -123,6 +123,7 @@ abstract class PhabricatorAuthFactor extends Phobject { ->setUserPHID($viewer->getPHID()) ->setSessionPHID($viewer->getSession()->getPHID()) ->setFactorPHID($config->getPHID()) + ->setIsNewChallenge(true) ->setWorkflowKey($engine->getWorkflowKey()); } @@ -283,8 +284,11 @@ abstract class PhabricatorAuthFactor extends Phobject { $error = $result->getErrorMessage(); - $icon = id(new PHUIIconView()) - ->setIcon('fa-clock-o', 'red'); + $icon = $result->getIcon(); + if (!$icon) { + $icon = id(new PHUIIconView()) + ->setIcon('fa-clock-o', 'red'); + } return id(new PHUIFormTimerControl()) ->setIcon($icon) @@ -295,8 +299,11 @@ abstract class PhabricatorAuthFactor extends Phobject { private function newAnsweredControl( PhabricatorAuthFactorResult $result) { - $icon = id(new PHUIIconView()) - ->setIcon('fa-check-circle-o', 'green'); + $icon = $result->getIcon(); + if (!$icon) { + $icon = id(new PHUIIconView()) + ->setIcon('fa-check-circle-o', 'green'); + } return id(new PHUIFormTimerControl()) ->setIcon($icon) @@ -309,8 +316,11 @@ abstract class PhabricatorAuthFactor extends Phobject { $error = $result->getErrorMessage(); - $icon = id(new PHUIIconView()) - ->setIcon('fa-times', 'red'); + $icon = $result->getIcon(); + if (!$icon) { + $icon = id(new PHUIIconView()) + ->setIcon('fa-times', 'red'); + } return id(new PHUIFormTimerControl()) ->setIcon($icon) @@ -323,8 +333,11 @@ abstract class PhabricatorAuthFactor extends Phobject { $error = $result->getErrorMessage(); - $icon = id(new PHUIIconView()) - ->setIcon('fa-commenting', 'green'); + $icon = $result->getIcon(); + if (!$icon) { + $icon = id(new PHUIIconView()) + ->setIcon('fa-commenting', 'green'); + } return id(new PHUIFormTimerControl()) ->setIcon($icon) diff --git a/src/applications/auth/factor/PhabricatorAuthFactorResult.php b/src/applications/auth/factor/PhabricatorAuthFactorResult.php index 2282f162a9..f03c3674da 100644 --- a/src/applications/auth/factor/PhabricatorAuthFactorResult.php +++ b/src/applications/auth/factor/PhabricatorAuthFactorResult.php @@ -10,6 +10,7 @@ final class PhabricatorAuthFactorResult private $errorMessage; private $value; private $issuedChallenges = array(); + private $icon; public function setAnsweredChallenge(PhabricatorAuthChallenge $challenge) { if (!$challenge->getIsAnsweredChallenge()) { @@ -92,4 +93,13 @@ final class PhabricatorAuthFactorResult return $this->issuedChallenges; } + public function setIcon(PHUIIconView $icon) { + $this->icon = $icon; + return $this; + } + + public function getIcon() { + return $this->icon; + } + } diff --git a/src/applications/auth/factor/PhabricatorDuoAuthFactor.php b/src/applications/auth/factor/PhabricatorDuoAuthFactor.php index 187e011953..4be4c15ea8 100644 --- a/src/applications/auth/factor/PhabricatorDuoAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorDuoAuthFactor.php @@ -612,7 +612,22 @@ final class PhabricatorDuoAuthFactor return $this->newResult() ->setAnsweredChallenge($challenge); case 'waiting': - // No result yet, we'll render a default state later on. + // If we didn't just issue this challenge, give the user a stronger + // hint that they need to follow the instructions. + if (!$challenge->getIsNewChallenge()) { + return $this->newResult() + ->setIsContinue(true) + ->setIcon( + id(new PHUIIconView()) + ->setIcon('fa-exclamation-triangle', 'yellow')) + ->setErrorMessage( + pht( + 'You must approve the challenge which was sent to your '. + 'phone. Open the Duo application and confirm the challenge, '. + 'then continue.')); + } + + // Otherwise, we'll construct a default message later on. break; default: case 'deny': @@ -666,8 +681,7 @@ final class PhabricatorDuoAuthFactor public function getRequestHasChallengeResponse( PhabricatorAuthFactorConfig $config, AphrontRequest $request) { - $value = $this->getChallengeResponseFromRequest($config, $request); - return (bool)strlen($value); + return false; } protected function newResultFromChallengeResponse( @@ -675,41 +689,7 @@ final class PhabricatorDuoAuthFactor PhabricatorUser $viewer, AphrontRequest $request, array $challenges) { - - $challenge = $this->getChallengeForCurrentContext( - $config, - $viewer, - $challenges); - - $code = $this->getChallengeResponseFromRequest( - $config, - $request); - - $result = $this->newResult() - ->setValue($code); - - if ($challenge->getIsAnsweredChallenge()) { - return $result->setAnsweredChallenge($challenge); - } - - if (phutil_hashes_are_identical($code, $challenge->getChallengeKey())) { - $ttl = PhabricatorTime::getNow() + phutil_units('15 minutes in seconds'); - - $challenge - ->markChallengeAsAnswered($ttl); - - return $result->setAnsweredChallenge($challenge); - } - - if (strlen($code)) { - $error_message = pht('Invalid'); - } else { - $error_message = pht('Required'); - } - - $result->setErrorMessage($error_message); - - return $result; + return $this->newResult(); } private function newDuoFuture(PhabricatorAuthFactorProvider $provider) { diff --git a/src/applications/auth/storage/PhabricatorAuthChallenge.php b/src/applications/auth/storage/PhabricatorAuthChallenge.php index 8fa07d712f..0b740e5fa7 100644 --- a/src/applications/auth/storage/PhabricatorAuthChallenge.php +++ b/src/applications/auth/storage/PhabricatorAuthChallenge.php @@ -16,6 +16,7 @@ final class PhabricatorAuthChallenge protected $properties = array(); private $responseToken; + private $isNewChallenge; const HTTPKEY = '__hisec.challenges__'; const TOKEN_DIGEST_KEY = 'auth.challenge.token'; @@ -241,6 +242,15 @@ final class PhabricatorAuthChallenge return $this->properties[$key]; } + public function setIsNewChallenge($is_new_challenge) { + $this->isNewChallenge = $is_new_challenge; + return $this; + } + + public function getIsNewChallenge() { + return $this->isNewChallenge; + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ From 6f3bd13cf5da0f62b9b7df8304105c82bb9bd2ee Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 5 Feb 2019 05:22:39 -0800 Subject: [PATCH 008/245] Begin adding more guidance to the "One-Time Login" flow Summary: Ref T13244. See PHI774. If an install does not use password auth, the "one-time login" flow (via "Welcome" email or "bin/auth recover") is pretty rough. Current behavior: - If an install uses passwords, the user is prompted to set a password. - If an install does not use passwords, you're dumped to `/settings/external/` to link an external account. This is pretty sketchy and this UI does not make it clear what users are expected to do (link an account) or why (so they can log in). Instead, improve this flow: - Password reset flow is fine. - (Future Change) If there are external linkable accounts (like Google) and the user doesn't have any linked, I want to give users a flow like a password reset flow that says "link to an external account". - (This Change) If you're an administrator and there are no providers at all, go to "/auth/" so you can set something up. - (This Change) If we don't hit on any other rules, just go home? This may be tweaked a bit as we go, but basically I want to refine the "/settings/external/" case into a more useful flow which gives users more of a chance of surviving it. Test Plan: Logged in with passwords enabled (got password reset), with nothing enabled as an admin (got sent to Auth), and with something other than passwords enabled (got sent home). Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13244 Differential Revision: https://secure.phabricator.com/D20094 --- .../PhabricatorAuthOneTimeLoginController.php | 86 ++++++++++++------- .../PhabricatorAuthProviderConfigQuery.php | 60 ++++--------- 2 files changed, 72 insertions(+), 74 deletions(-) diff --git a/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php b/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php index 0cac95f53d..f51f379d2b 100644 --- a/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php +++ b/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php @@ -119,38 +119,9 @@ final class PhabricatorAuthOneTimeLoginController } unset($unguarded); - $next = '/'; - if (!PhabricatorPasswordAuthProvider::getPasswordProvider()) { - $next = '/settings/panel/external/'; - } else { + $next_uri = $this->getNextStepURI($target_user); - // We're going to let the user reset their password without knowing - // the old one. Generate a one-time token for that. - $key = Filesystem::readRandomCharacters(16); - $password_type = - PhabricatorAuthPasswordResetTemporaryTokenType::TOKENTYPE; - - $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); - id(new PhabricatorAuthTemporaryToken()) - ->setTokenResource($target_user->getPHID()) - ->setTokenType($password_type) - ->setTokenExpires(time() + phutil_units('1 hour in seconds')) - ->setTokenCode(PhabricatorHash::weakDigest($key)) - ->save(); - unset($unguarded); - - $panel_uri = '/auth/password/'; - - $next = (string)id(new PhutilURI($panel_uri)) - ->setQueryParams( - array( - 'key' => $key, - )); - - $request->setTemporaryCookie(PhabricatorCookies::COOKIE_HISEC, 'yes'); - } - - PhabricatorCookies::setNextURICookie($request, $next, $force = true); + PhabricatorCookies::setNextURICookie($request, $next_uri, $force = true); $force_full_session = false; if ($link_type === PhabricatorAuthSessionEngine::ONETIME_RECOVER) { @@ -206,4 +177,57 @@ final class PhabricatorAuthOneTimeLoginController return id(new AphrontDialogResponse())->setDialog($dialog); } + + private function getNextStepURI(PhabricatorUser $user) { + $request = $this->getRequest(); + + // If we have password auth, let the user set or reset their password after + // login. + $have_passwords = PhabricatorPasswordAuthProvider::getPasswordProvider(); + if ($have_passwords) { + // We're going to let the user reset their password without knowing + // the old one. Generate a one-time token for that. + $key = Filesystem::readRandomCharacters(16); + $password_type = + PhabricatorAuthPasswordResetTemporaryTokenType::TOKENTYPE; + + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + id(new PhabricatorAuthTemporaryToken()) + ->setTokenResource($user->getPHID()) + ->setTokenType($password_type) + ->setTokenExpires(time() + phutil_units('1 hour in seconds')) + ->setTokenCode(PhabricatorHash::weakDigest($key)) + ->save(); + unset($unguarded); + + $panel_uri = '/auth/password/'; + + $request->setTemporaryCookie(PhabricatorCookies::COOKIE_HISEC, 'yes'); + + return (string)id(new PhutilURI($panel_uri)) + ->setQueryParams( + array( + 'key' => $key, + )); + } + + $providers = id(new PhabricatorAuthProviderConfigQuery()) + ->setViewer($user) + ->withIsEnabled(true) + ->execute(); + + // If there are no configured providers and the user is an administrator, + // send them to Auth to configure a provider. This is probably where they + // want to go. You can end up in this state by accidentally losing your + // first session during initial setup, or after restoring exported data + // from a hosted instance. + if (!$providers && $user->getIsAdmin()) { + return '/auth/'; + } + + // If we didn't find anywhere better to send them, give up and just send + // them to the home page. + return '/'; + } + } diff --git a/src/applications/auth/query/PhabricatorAuthProviderConfigQuery.php b/src/applications/auth/query/PhabricatorAuthProviderConfigQuery.php index 626c80348f..1d21342e4e 100644 --- a/src/applications/auth/query/PhabricatorAuthProviderConfigQuery.php +++ b/src/applications/auth/query/PhabricatorAuthProviderConfigQuery.php @@ -6,11 +6,7 @@ final class PhabricatorAuthProviderConfigQuery private $ids; private $phids; private $providerClasses; - - const STATUS_ALL = 'status:all'; - const STATUS_ENABLED = 'status:enabled'; - - private $status = self::STATUS_ALL; + private $isEnabled; public function withPHIDs(array $phids) { $this->phids = $phids; @@ -22,40 +18,26 @@ final class PhabricatorAuthProviderConfigQuery return $this; } - public function withStatus($status) { - $this->status = $status; - return $this; - } - public function withProviderClasses(array $classes) { $this->providerClasses = $classes; return $this; } - public static function getStatusOptions() { - return array( - self::STATUS_ALL => pht('All Providers'), - self::STATUS_ENABLED => pht('Enabled Providers'), - ); + public function withIsEnabled($is_enabled) { + $this->isEnabled = $is_enabled; + return $this; + } + + public function newResultObject() { + return new PhabricatorAuthProviderConfig(); } protected function loadPage() { - $table = new PhabricatorAuthProviderConfig(); - $conn_r = $table->establishConnection('r'); - - $data = queryfx_all( - $conn_r, - 'SELECT * FROM %T %Q %Q %Q', - $table->getTableName(), - $this->buildWhereClause($conn_r), - $this->buildOrderClause($conn_r), - $this->buildLimitClause($conn_r)); - - return $table->loadAllFromArray($data); + return $this->loadStandardPage($this->newResultObject()); } - protected function buildWhereClause(AphrontDatabaseConnection $conn) { - $where = array(); + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); if ($this->ids !== null) { $where[] = qsprintf( @@ -78,22 +60,14 @@ final class PhabricatorAuthProviderConfigQuery $this->providerClasses); } - $status = $this->status; - switch ($status) { - case self::STATUS_ALL: - break; - case self::STATUS_ENABLED: - $where[] = qsprintf( - $conn, - 'isEnabled = 1'); - break; - default: - throw new Exception(pht("Unknown status '%s'!", $status)); + if ($this->isEnabled !== null) { + $where[] = qsprintf( + $conn, + 'isEnabled = %d', + (int)$this->isEnabled); } - $where[] = $this->buildPagingClause($conn); - - return $this->formatWhereClause($conn, $where); + return $where; } public function getQueryApplicationClass() { From 8c8d56dc5636df949e7bacf77abfeb9e51be7b65 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 5 Feb 2019 05:50:58 -0800 Subject: [PATCH 009/245] Replace "Add Auth Provider" radio buttons with a more modern "click to select" UI Summary: Depends on D20094. Ref T13244. Ref T6703. See PHI774. Currently, we use an older-style radio-button UI to choose an auth provider type (Google, Password, LDAP, etc). Instead, use a more modern click-to-select UI. Test Plan: {F6184343} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13244, T6703 Differential Revision: https://secure.phabricator.com/D20095 --- .../PhabricatorAuthApplication.php | 3 +- .../config/PhabricatorAuthEditController.php | 6 +- .../config/PhabricatorAuthNewController.php | 127 +++++++----------- .../auth/provider/PhabricatorAuthProvider.php | 6 + 4 files changed, 56 insertions(+), 86 deletions(-) diff --git a/src/applications/auth/application/PhabricatorAuthApplication.php b/src/applications/auth/application/PhabricatorAuthApplication.php index df86595b46..15d753a286 100644 --- a/src/applications/auth/application/PhabricatorAuthApplication.php +++ b/src/applications/auth/application/PhabricatorAuthApplication.php @@ -48,8 +48,7 @@ final class PhabricatorAuthApplication extends PhabricatorApplication { '' => 'PhabricatorAuthListController', 'config/' => array( 'new/' => 'PhabricatorAuthNewController', - 'new/(?P[^/]+)/' => 'PhabricatorAuthEditController', - 'edit/(?P\d+)/' => 'PhabricatorAuthEditController', + 'edit/(?:(?P\d+)/)?' => 'PhabricatorAuthEditController', '(?Penable|disable)/(?P\d+)/' => 'PhabricatorAuthDisableController', ), diff --git a/src/applications/auth/controller/config/PhabricatorAuthEditController.php b/src/applications/auth/controller/config/PhabricatorAuthEditController.php index 6ff0be4383..016fe51b1a 100644 --- a/src/applications/auth/controller/config/PhabricatorAuthEditController.php +++ b/src/applications/auth/controller/config/PhabricatorAuthEditController.php @@ -6,8 +6,9 @@ final class PhabricatorAuthEditController public function handleRequest(AphrontRequest $request) { $this->requireApplicationCapability( AuthManageProvidersCapability::CAPABILITY); - $viewer = $request->getUser(); - $provider_class = $request->getURIData('className'); + + $viewer = $this->getViewer(); + $provider_class = $request->getStr('provider'); $config_id = $request->getURIData('id'); if ($config_id) { @@ -275,6 +276,7 @@ final class PhabricatorAuthEditController $form = id(new AphrontFormView()) ->setUser($viewer) + ->addHiddenInput('provider', $provider_class) ->appendChild( id(new AphrontFormCheckboxControl()) ->setLabel(pht('Allow')) diff --git a/src/applications/auth/controller/config/PhabricatorAuthNewController.php b/src/applications/auth/controller/config/PhabricatorAuthNewController.php index dbd43f9ea8..c8fd0ad8a5 100644 --- a/src/applications/auth/controller/config/PhabricatorAuthNewController.php +++ b/src/applications/auth/controller/config/PhabricatorAuthNewController.php @@ -6,44 +6,12 @@ final class PhabricatorAuthNewController public function handleRequest(AphrontRequest $request) { $this->requireApplicationCapability( AuthManageProvidersCapability::CAPABILITY); - $request = $this->getRequest(); - $viewer = $request->getUser(); + + $viewer = $this->getViewer(); + $cancel_uri = $this->getApplicationURI(); $providers = PhabricatorAuthProvider::getAllBaseProviders(); - $e_provider = null; - $errors = array(); - - if ($request->isFormPost()) { - $provider_string = $request->getStr('provider'); - if (!strlen($provider_string)) { - $e_provider = pht('Required'); - $errors[] = pht('You must select an authentication provider.'); - } else { - $found = false; - foreach ($providers as $provider) { - if (get_class($provider) === $provider_string) { - $found = true; - break; - } - } - if (!$found) { - $e_provider = pht('Invalid'); - $errors[] = pht('You must select a valid provider.'); - } - } - - if (!$errors) { - return id(new AphrontRedirectResponse())->setURI( - $this->getApplicationURI('/config/new/'.$provider_string.'/')); - } - } - - $options = id(new AphrontFormRadioButtonControl()) - ->setLabel(pht('Provider')) - ->setName('provider') - ->setError($e_provider); - $configured = PhabricatorAuthProvider::getAllProviders(); $configured_classes = array(); foreach ($configured as $configured_provider) { @@ -55,57 +23,52 @@ final class PhabricatorAuthNewController $providers = msort($providers, 'getLoginOrder'); $providers = array_diff_key($providers, $configured_classes) + $providers; - foreach ($providers as $provider) { - if (isset($configured_classes[get_class($provider)])) { - $disabled = true; - $description = pht('This provider is already configured.'); + $menu = id(new PHUIObjectItemListView()) + ->setViewer($viewer) + ->setBig(true) + ->setFlush(true); + + foreach ($providers as $provider_key => $provider) { + $provider_class = get_class($provider); + + $provider_uri = id(new PhutilURI('/config/edit/')) + ->setQueryParam('provider', $provider_class); + $provider_uri = $this->getApplicationURI($provider_uri); + + $already_exists = isset($configured_classes[get_class($provider)]); + + $item = id(new PHUIObjectItemView()) + ->setHeader($provider->getNameForCreate()) + ->setImageIcon($provider->newIconView()) + ->addAttribute($provider->getDescriptionForCreate()); + + if (!$already_exists) { + $item + ->setHref($provider_uri) + ->setClickable(true); } else { - $disabled = false; - $description = $provider->getDescriptionForCreate(); + $item->setDisabled(true); } - $options->addButton( - get_class($provider), - $provider->getNameForCreate(), - $description, - $disabled ? 'disabled' : null, - $disabled); + + if ($already_exists) { + $messages = array(); + $messages[] = pht('You already have a provider of this type.'); + + $info = id(new PHUIInfoView()) + ->setSeverity(PHUIInfoView::SEVERITY_WARNING) + ->setErrors($messages); + + $item->appendChild($info); + } + + $menu->addItem($item); } - $form = id(new AphrontFormView()) - ->setUser($viewer) - ->appendChild($options) - ->appendChild( - id(new AphrontFormSubmitControl()) - ->addCancelButton($this->getApplicationURI()) - ->setValue(pht('Continue'))); - - $form_box = id(new PHUIObjectBoxView()) - ->setHeaderText(pht('Provider')) - ->setFormErrors($errors) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->setForm($form); - - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb(pht('Add Provider')); - $crumbs->setBorder(true); - - $title = pht('Add Auth Provider'); - - $header = id(new PHUIHeaderView()) - ->setHeader($title) - ->setHeaderIcon('fa-plus-square'); - - $view = id(new PHUITwoColumnView()) - ->setHeader($header) - ->setFooter(array( - $form_box, - )); - - return $this->newPage() - ->setTitle($title) - ->setCrumbs($crumbs) - ->appendChild($view); - + return $this->newDialog() + ->setTitle(pht('Add Auth Provider')) + ->setWidth(AphrontDialogView::WIDTH_FORM) + ->appendChild($menu) + ->addCancelButton($cancel_uri); } } diff --git a/src/applications/auth/provider/PhabricatorAuthProvider.php b/src/applications/auth/provider/PhabricatorAuthProvider.php index 0525edad54..bcccca5121 100644 --- a/src/applications/auth/provider/PhabricatorAuthProvider.php +++ b/src/applications/auth/provider/PhabricatorAuthProvider.php @@ -311,6 +311,12 @@ abstract class PhabricatorAuthProvider extends Phobject { return 'Generic'; } + public function newIconView() { + return id(new PHUIIconView()) + ->setSpriteSheet(PHUIIconView::SPRITE_LOGIN) + ->setSpriteIcon($this->getLoginIcon()); + } + public function isLoginFormAButton() { return false; } From 4fcb38a2a94cdf9a0abb1723c81ea88211ea2595 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 5 Feb 2019 07:04:47 -0800 Subject: [PATCH 010/245] Move the Auth Provider edit flow toward a more modern layout Summary: Depends on D20095. Ref T13244. Currently, auth providers have a list item view and a single gigantic edit screen complete with a timeline, piles of instructions, supplemental information, etc. As a step toward making this stuff easier to use and more modern, give them a separate view UI with normal actions, similar to basically every other type of object. Move the timeline and "Disable/Enable" to the view page (from the edit page and the list page, respectively). Test Plan: Created, edited, and viewed auth providers. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13244 Differential Revision: https://secure.phabricator.com/D20096 --- src/__phutil_library_map__.php | 2 + .../PhabricatorAuthApplication.php | 1 + .../PhabricatorAuthDisableController.php | 19 ++- .../config/PhabricatorAuthEditController.php | 22 +--- .../config/PhabricatorAuthListController.php | 50 ++------ .../PhabricatorAuthProviderViewController.php | 119 ++++++++++++++++++ .../PhabricatorAuthProviderConfigQuery.php | 13 ++ .../storage/PhabricatorAuthProviderConfig.php | 12 ++ 8 files changed, 171 insertions(+), 67 deletions(-) create mode 100644 src/applications/auth/controller/config/PhabricatorAuthProviderViewController.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index fe77880417..56af6a4374 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2335,6 +2335,7 @@ phutil_register_library_map(array( 'PhabricatorAuthProviderConfigTransaction' => 'applications/auth/storage/PhabricatorAuthProviderConfigTransaction.php', 'PhabricatorAuthProviderConfigTransactionQuery' => 'applications/auth/query/PhabricatorAuthProviderConfigTransactionQuery.php', 'PhabricatorAuthProviderController' => 'applications/auth/controller/config/PhabricatorAuthProviderController.php', + 'PhabricatorAuthProviderViewController' => 'applications/auth/controller/config/PhabricatorAuthProviderViewController.php', 'PhabricatorAuthProvidersGuidanceContext' => 'applications/auth/guidance/PhabricatorAuthProvidersGuidanceContext.php', 'PhabricatorAuthProvidersGuidanceEngineExtension' => 'applications/auth/guidance/PhabricatorAuthProvidersGuidanceEngineExtension.php', 'PhabricatorAuthQueryPublicKeysConduitAPIMethod' => 'applications/auth/conduit/PhabricatorAuthQueryPublicKeysConduitAPIMethod.php', @@ -8094,6 +8095,7 @@ phutil_register_library_map(array( 'PhabricatorAuthProviderConfigTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorAuthProviderConfigTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorAuthProviderController' => 'PhabricatorAuthController', + 'PhabricatorAuthProviderViewController' => 'PhabricatorAuthProviderConfigController', 'PhabricatorAuthProvidersGuidanceContext' => 'PhabricatorGuidanceContext', 'PhabricatorAuthProvidersGuidanceEngineExtension' => 'PhabricatorGuidanceEngineExtension', 'PhabricatorAuthQueryPublicKeysConduitAPIMethod' => 'PhabricatorAuthConduitAPIMethod', diff --git a/src/applications/auth/application/PhabricatorAuthApplication.php b/src/applications/auth/application/PhabricatorAuthApplication.php index 15d753a286..307cab61c8 100644 --- a/src/applications/auth/application/PhabricatorAuthApplication.php +++ b/src/applications/auth/application/PhabricatorAuthApplication.php @@ -51,6 +51,7 @@ final class PhabricatorAuthApplication extends PhabricatorApplication { 'edit/(?:(?P\d+)/)?' => 'PhabricatorAuthEditController', '(?Penable|disable)/(?P\d+)/' => 'PhabricatorAuthDisableController', + 'view/(?P\d+)/' => 'PhabricatorAuthProviderViewController', ), 'login/(?P[^/]+)/(?:(?P[^/]+)/)?' => 'PhabricatorAuthLoginController', diff --git a/src/applications/auth/controller/config/PhabricatorAuthDisableController.php b/src/applications/auth/controller/config/PhabricatorAuthDisableController.php index 5863aceca9..252f159ec4 100644 --- a/src/applications/auth/controller/config/PhabricatorAuthDisableController.php +++ b/src/applications/auth/controller/config/PhabricatorAuthDisableController.php @@ -6,7 +6,8 @@ final class PhabricatorAuthDisableController public function handleRequest(AphrontRequest $request) { $this->requireApplicationCapability( AuthManageProvidersCapability::CAPABILITY); - $viewer = $request->getUser(); + + $viewer = $this->getViewer(); $config_id = $request->getURIData('id'); $action = $request->getURIData('action'); @@ -24,6 +25,7 @@ final class PhabricatorAuthDisableController } $is_enable = ($action === 'enable'); + $done_uri = $config->getURI(); if ($request->isDialogFormPost()) { $xactions = array(); @@ -39,8 +41,7 @@ final class PhabricatorAuthDisableController ->setContinueOnNoEffect(true) ->applyTransactions($config, $xactions); - return id(new AphrontRedirectResponse())->setURI( - $this->getApplicationURI()); + return id(new AphrontRedirectResponse())->setURI($done_uri); } if ($is_enable) { @@ -64,8 +65,9 @@ final class PhabricatorAuthDisableController // account and pop a warning like "YOU WILL NO LONGER BE ABLE TO LOGIN // YOU GOOF, YOU PROBABLY DO NOT MEAN TO DO THIS". None of this is // critical and we can wait to see how users manage to shoot themselves - // in the feet. Shortly, `bin/auth` will be able to recover from these - // types of mistakes. + // in the feet. + + // `bin/auth` can recover from these types of mistakes. $title = pht('Disable Provider?'); $body = pht( @@ -77,14 +79,11 @@ final class PhabricatorAuthDisableController $button = pht('Disable Provider'); } - $dialog = id(new AphrontDialogView()) - ->setUser($viewer) + return $this->newDialog() ->setTitle($title) ->appendChild($body) - ->addCancelButton($this->getApplicationURI()) + ->addCancelButton($done_uri) ->addSubmitButton($button); - - return id(new AphrontDialogResponse())->setDialog($dialog); } } diff --git a/src/applications/auth/controller/config/PhabricatorAuthEditController.php b/src/applications/auth/controller/config/PhabricatorAuthEditController.php index 016fe51b1a..d3cd2fef98 100644 --- a/src/applications/auth/controller/config/PhabricatorAuthEditController.php +++ b/src/applications/auth/controller/config/PhabricatorAuthEditController.php @@ -156,12 +156,7 @@ final class PhabricatorAuthEditController ->setContinueOnNoEffect(true) ->applyTransactions($config, $xactions); - if ($provider->hasSetupStep() && $is_new) { - $id = $config->getID(); - $next_uri = $this->getApplicationURI('config/edit/'.$id.'/'); - } else { - $next_uri = $this->getApplicationURI(); - } + $next_uri = $config->getURI(); return id(new AphrontRedirectResponse())->setURI($next_uri); } @@ -185,7 +180,7 @@ final class PhabricatorAuthEditController $crumb = pht('Edit Provider'); $title = pht('Edit Auth Provider'); $header_icon = 'fa-pencil'; - $cancel_uri = $this->getApplicationURI(); + $cancel_uri = $config->getURI(); } $header = id(new PHUIHeaderView()) @@ -348,18 +343,6 @@ final class PhabricatorAuthEditController $crumbs->addTextCrumb($crumb); $crumbs->setBorder(true); - $timeline = null; - if (!$is_new) { - $timeline = $this->buildTransactionTimeline( - $config, - new PhabricatorAuthProviderConfigTransactionQuery()); - $xactions = $timeline->getTransactions(); - foreach ($xactions as $xaction) { - $xaction->setProvider($provider); - } - $timeline->setShouldTerminate(true); - } - $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Provider')) ->setFormErrors($errors) @@ -371,7 +354,6 @@ final class PhabricatorAuthEditController ->setFooter(array( $form_box, $footer, - $timeline, )); return $this->newPage() diff --git a/src/applications/auth/controller/config/PhabricatorAuthListController.php b/src/applications/auth/controller/config/PhabricatorAuthListController.php index bb118d798e..f4b05e8adf 100644 --- a/src/applications/auth/controller/config/PhabricatorAuthListController.php +++ b/src/applications/auth/controller/config/PhabricatorAuthListController.php @@ -19,31 +19,18 @@ final class PhabricatorAuthListController $id = $config->getID(); - $edit_uri = $this->getApplicationURI('config/edit/'.$id.'/'); - $enable_uri = $this->getApplicationURI('config/enable/'.$id.'/'); - $disable_uri = $this->getApplicationURI('config/disable/'.$id.'/'); + $view_uri = $config->getURI(); $provider = $config->getProvider(); - if ($provider) { - $name = $provider->getProviderName(); - } else { - $name = $config->getProviderType().' ('.$config->getProviderClass().')'; - } + $name = $provider->getProviderName(); - $item->setHeader($name); + $item + ->setHeader($name) + ->setHref($view_uri); - if ($provider) { - $item->setHref($edit_uri); - } else { - $item->addAttribute(pht('Provider Implementation Missing!')); - } - - $domain = null; - if ($provider) { - $domain = $provider->getProviderDomain(); - if ($domain !== 'self') { - $item->addAttribute($domain); - } + $domain = $provider->getProviderDomain(); + if ($domain !== 'self') { + $item->addAttribute($domain); } if ($config->getShouldAllowRegistration()) { @@ -54,21 +41,9 @@ final class PhabricatorAuthListController if ($config->getIsEnabled()) { $item->setStatusIcon('fa-check-circle green'); - $item->addAction( - id(new PHUIListItemView()) - ->setIcon('fa-times') - ->setHref($disable_uri) - ->setDisabled(!$can_manage) - ->addSigil('workflow')); } else { $item->setStatusIcon('fa-ban red'); $item->addIcon('fa-ban grey', pht('Disabled')); - $item->addAction( - id(new PHUIListItemView()) - ->setIcon('fa-plus') - ->setHref($enable_uri) - ->setDisabled(!$can_manage) - ->addSigil('workflow')); } $list->addItem($item); @@ -123,10 +98,11 @@ final class PhabricatorAuthListController $view = id(new PHUITwoColumnView()) ->setHeader($header) - ->setFooter(array( - $guidance, - $list, - )); + ->setFooter( + array( + $guidance, + $list, + )); $nav = $this->newNavigation() ->setCrumbs($crumbs) diff --git a/src/applications/auth/controller/config/PhabricatorAuthProviderViewController.php b/src/applications/auth/controller/config/PhabricatorAuthProviderViewController.php new file mode 100644 index 0000000000..532744001c --- /dev/null +++ b/src/applications/auth/controller/config/PhabricatorAuthProviderViewController.php @@ -0,0 +1,119 @@ +requireApplicationCapability( + AuthManageProvidersCapability::CAPABILITY); + + $viewer = $this->getViewer(); + $id = $request->getURIData('id'); + + $config = id(new PhabricatorAuthProviderConfigQuery()) + ->setViewer($viewer) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->withIDs(array($id)) + ->executeOne(); + if (!$config) { + return new Aphront404Response(); + } + + $header = $this->buildHeaderView($config); + $properties = $this->buildPropertiesView($config); + $curtain = $this->buildCurtain($config); + + $timeline = $this->buildTransactionTimeline( + $config, + new PhabricatorAuthProviderConfigTransactionQuery()); + $timeline->setShouldTerminate(true); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setCurtain($curtain) + ->addPropertySection(pht('Details'), $properties) + ->setMainColumn($timeline); + + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb($config->getObjectName()) + ->setBorder(true); + + return $this->newPage() + ->setTitle(pht('Auth Provider: %s', $config->getDisplayName())) + ->setCrumbs($crumbs) + ->appendChild($view); + } + + private function buildHeaderView(PhabricatorAuthProviderConfig $config) { + $viewer = $this->getViewer(); + + $view = id(new PHUIHeaderView()) + ->setViewer($viewer) + ->setHeader($config->getDisplayName()); + + if ($config->getIsEnabled()) { + $view->setStatus('fa-check', 'bluegrey', pht('Enabled')); + } else { + $view->setStatus('fa-ban', 'red', pht('Disabled')); + } + + return $view; + } + + private function buildCurtain(PhabricatorAuthProviderConfig $config) { + $viewer = $this->getViewer(); + $id = $config->getID(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $config, + PhabricatorPolicyCapability::CAN_EDIT); + + $curtain = $this->newCurtainView($config); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit Auth Provider')) + ->setIcon('fa-pencil') + ->setHref($this->getApplicationURI("config/edit/{$id}/")) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit)); + + if ($config->getIsEnabled()) { + $disable_uri = $this->getApplicationURI('config/disable/'.$id.'/'); + $disable_icon = 'fa-ban'; + $disable_text = pht('Disable Provider'); + } else { + $disable_uri = $this->getApplicationURI('config/enable/'.$id.'/'); + $disable_icon = 'fa-check'; + $disable_text = pht('Enable Provider'); + } + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName($disable_text) + ->setIcon($disable_icon) + ->setHref($disable_uri) + ->setDisabled(!$can_edit) + ->setWorkflow(true)); + + return $curtain; + } + + private function buildPropertiesView(PhabricatorAuthProviderConfig $config) { + $viewer = $this->getViewer(); + + $view = id(new PHUIPropertyListView()) + ->setViewer($viewer); + + $view->addProperty( + pht('Provider Type'), + $config->getProvider()->getProviderName()); + + return $view; + } +} diff --git a/src/applications/auth/query/PhabricatorAuthProviderConfigQuery.php b/src/applications/auth/query/PhabricatorAuthProviderConfigQuery.php index 1d21342e4e..ee073e3ac1 100644 --- a/src/applications/auth/query/PhabricatorAuthProviderConfigQuery.php +++ b/src/applications/auth/query/PhabricatorAuthProviderConfigQuery.php @@ -70,6 +70,19 @@ final class PhabricatorAuthProviderConfigQuery return $where; } + protected function willFilterPage(array $configs) { + + foreach ($configs as $key => $config) { + $provider = $config->getProvider(); + if (!$provider) { + unset($configs[$key]); + continue; + } + } + + return $configs; + } + public function getQueryApplicationClass() { return 'PhabricatorAuthApplication'; } diff --git a/src/applications/auth/storage/PhabricatorAuthProviderConfig.php b/src/applications/auth/storage/PhabricatorAuthProviderConfig.php index 1de34c4077..6a8bbe1a0d 100644 --- a/src/applications/auth/storage/PhabricatorAuthProviderConfig.php +++ b/src/applications/auth/storage/PhabricatorAuthProviderConfig.php @@ -83,6 +83,18 @@ final class PhabricatorAuthProviderConfig return $this->provider; } + public function getURI() { + return '/auth/config/view/'.$this->getID().'/'; + } + + public function getObjectName() { + return pht('Auth Provider %d', $this->getID()); + } + + public function getDisplayName() { + return $this->getProvider()->getProviderName(); + } + /* -( PhabricatorApplicationTransactionInterface )------------------------- */ From 99e5ef84fc252eec7c3eeca82b2d1d3766ee670d Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 5 Feb 2019 08:52:13 -0800 Subject: [PATCH 011/245] Remove obsolete "PhabricatorAuthLoginHandler" Summary: Depends on D20096. Reverts D14057. This was added for Phacility use cases in D14057 but never used. It is obsoleted by {nav Auth > Customize Messages} for non-Phacility use cases. Test Plan: Grepped for removed symbol. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20099 --- src/__phutil_library_map__.php | 2 -- .../PhabricatorAuthStartController.php | 18 ---------- .../handler/PhabricatorAuthLoginHandler.php | 36 ------------------- 3 files changed, 56 deletions(-) delete mode 100644 src/applications/auth/handler/PhabricatorAuthLoginHandler.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 56af6a4374..7bc8670661 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2272,7 +2272,6 @@ phutil_register_library_map(array( 'PhabricatorAuthLinkController' => 'applications/auth/controller/PhabricatorAuthLinkController.php', 'PhabricatorAuthListController' => 'applications/auth/controller/config/PhabricatorAuthListController.php', 'PhabricatorAuthLoginController' => 'applications/auth/controller/PhabricatorAuthLoginController.php', - 'PhabricatorAuthLoginHandler' => 'applications/auth/handler/PhabricatorAuthLoginHandler.php', 'PhabricatorAuthLoginMessageType' => 'applications/auth/message/PhabricatorAuthLoginMessageType.php', 'PhabricatorAuthLogoutConduitAPIMethod' => 'applications/auth/conduit/PhabricatorAuthLogoutConduitAPIMethod.php', 'PhabricatorAuthMFAEditEngineExtension' => 'applications/auth/engineextension/PhabricatorAuthMFAEditEngineExtension.php', @@ -8019,7 +8018,6 @@ phutil_register_library_map(array( 'PhabricatorAuthLinkController' => 'PhabricatorAuthController', 'PhabricatorAuthListController' => 'PhabricatorAuthProviderConfigController', 'PhabricatorAuthLoginController' => 'PhabricatorAuthController', - 'PhabricatorAuthLoginHandler' => 'Phobject', 'PhabricatorAuthLoginMessageType' => 'PhabricatorAuthMessageType', 'PhabricatorAuthLogoutConduitAPIMethod' => 'PhabricatorAuthConduitAPIMethod', 'PhabricatorAuthMFAEditEngineExtension' => 'PhabricatorEditEngineExtension', diff --git a/src/applications/auth/controller/PhabricatorAuthStartController.php b/src/applications/auth/controller/PhabricatorAuthStartController.php index 29fa7e0b9f..848a5bda27 100644 --- a/src/applications/auth/controller/PhabricatorAuthStartController.php +++ b/src/applications/auth/controller/PhabricatorAuthStartController.php @@ -172,23 +172,6 @@ final class PhabricatorAuthStartController $button_columns); } - $handlers = PhabricatorAuthLoginHandler::getAllHandlers(); - - $delegating_controller = $this->getDelegatingController(); - - $header = array(); - foreach ($handlers as $handler) { - $handler = clone $handler; - - $handler->setRequest($request); - - if ($delegating_controller) { - $handler->setDelegatingController($delegating_controller); - } - - $header[] = $handler->getAuthLoginHeaderContent(); - } - $invite_message = null; if ($invite) { $invite_message = $this->renderInviteHeader($invite); @@ -202,7 +185,6 @@ final class PhabricatorAuthStartController $title = pht('Login'); $view = array( - $header, $invite_message, $custom_message, $out, diff --git a/src/applications/auth/handler/PhabricatorAuthLoginHandler.php b/src/applications/auth/handler/PhabricatorAuthLoginHandler.php deleted file mode 100644 index eabbf91843..0000000000 --- a/src/applications/auth/handler/PhabricatorAuthLoginHandler.php +++ /dev/null @@ -1,36 +0,0 @@ -delegatingController = $controller; - return $this; - } - - final public function getDelegatingController() { - return $this->delegatingController; - } - - final public function setRequest(AphrontRequest $request) { - $this->request = $request; - return $this; - } - - final public function getRequest() { - return $this->request; - } - - final public static function getAllHandlers() { - return id(new PhutilClassMapQuery()) - ->setAncestorClass(__CLASS__) - ->execute(); - } -} From 9632c704c69d29134ed18d13282f1ce84f8a092e Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 5 Feb 2019 09:10:26 -0800 Subject: [PATCH 012/245] Always allow users to login via email link, even if an install does not use passwords Summary: Depends on D20099. Ref T13244. See PHI774. When password auth is enabled, we support a standard email-based account recovery mechanism with "Forgot password?". When password auth is not enabled, we disable the self-serve version of this mechanism. You can still get email account login links via "Send Welcome Mail" or "bin/auth recover". There's no real technical, product, or security reason not to let everyone do email login all the time. On the technical front, these links already work and are used in other contexts. On the product front, we just need to tweak a couple of strings. On the security front, there's some argument that this mechanism provides more overall surface area for an attacker, but if we find that argument compelling we should probably provide a way to disable the self-serve pathway in all cases, rather than coupling it to which providers are enabled. Also, inch toward having things iterate over configurations (saved database objects) instead of providers (abstract implementations) so we can some day live in a world where we support multiple configurations of the same provider type (T6703). Test Plan: - With password auth enabled, reset password. - Without password auth enabled, did an email login recovery. {F6184910} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13244 Differential Revision: https://secure.phabricator.com/D20100 --- .../PhabricatorAuthStartController.php | 47 ++++++ .../PhabricatorEmailLoginController.php | 142 ++++++++++-------- 2 files changed, 127 insertions(+), 62 deletions(-) diff --git a/src/applications/auth/controller/PhabricatorAuthStartController.php b/src/applications/auth/controller/PhabricatorAuthStartController.php index 848a5bda27..0b823098d7 100644 --- a/src/applications/auth/controller/PhabricatorAuthStartController.php +++ b/src/applications/auth/controller/PhabricatorAuthStartController.php @@ -75,6 +75,11 @@ final class PhabricatorAuthStartController } } + $configs = array(); + foreach ($providers as $provider) { + $configs[] = $provider->getProviderConfig(); + } + if (!$providers) { if ($this->isFirstTimeSetup()) { // If this is a fresh install, let the user register their admin @@ -179,6 +184,8 @@ final class PhabricatorAuthStartController $custom_message = $this->newCustomStartMessage(); + $email_login = $this->newEmailLoginView($configs); + $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Login')); $crumbs->setBorder(true); @@ -188,6 +195,7 @@ final class PhabricatorAuthStartController $invite_message, $custom_message, $out, + $email_login, ); return $this->newPage() @@ -311,4 +319,43 @@ final class PhabricatorAuthStartController $remarkup_view); } + private function newEmailLoginView(array $configs) { + assert_instances_of($configs, 'PhabricatorAuthProviderConfig'); + + // Check if password auth is enabled. If it is, the password login form + // renders a "Forgot password?" link, so we don't need to provide a + // supplemental link. + + $has_password = false; + foreach ($configs as $config) { + $provider = $config->getProvider(); + if ($provider instanceof PhabricatorPasswordAuthProvider) { + $has_password = true; + } + } + + if ($has_password) { + return null; + } + + $view = array( + pht('Trouble logging in?'), + ' ', + phutil_tag( + 'a', + array( + 'href' => '/login/email/', + ), + pht('Send a login link to your email address.')), + ); + + return phutil_tag( + 'div', + array( + 'class' => 'auth-custom-message', + ), + $view); + } + + } diff --git a/src/applications/auth/controller/PhabricatorEmailLoginController.php b/src/applications/auth/controller/PhabricatorEmailLoginController.php index f57a29b11a..eef30e6989 100644 --- a/src/applications/auth/controller/PhabricatorEmailLoginController.php +++ b/src/applications/auth/controller/PhabricatorEmailLoginController.php @@ -8,17 +8,13 @@ final class PhabricatorEmailLoginController } public function handleRequest(AphrontRequest $request) { - - if (!PhabricatorPasswordAuthProvider::getPasswordProvider()) { - return new Aphront400Response(); - } + $viewer = $this->getViewer(); $e_email = true; $e_captcha = true; $errors = array(); - $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); - + $v_email = $request->getStr('email'); if ($request->isFormPost()) { $e_email = null; $e_captcha = pht('Again'); @@ -29,8 +25,7 @@ final class PhabricatorEmailLoginController $e_captcha = pht('Invalid'); } - $email = $request->getStr('email'); - if (!strlen($email)) { + if (!strlen($v_email)) { $errors[] = pht('You must provide an email address.'); $e_email = pht('Required'); } @@ -42,7 +37,7 @@ final class PhabricatorEmailLoginController $target_email = id(new PhabricatorUserEmail())->loadOneWhere( 'address = %s', - $email); + $v_email); $target_user = null; if ($target_email) { @@ -81,33 +76,10 @@ final class PhabricatorEmailLoginController } if (!$errors) { - $engine = new PhabricatorAuthSessionEngine(); - $uri = $engine->getOneTimeLoginURI( - $target_user, - null, - PhabricatorAuthSessionEngine::ONETIME_RESET); - - if ($is_serious) { - $body = pht( - "You can use this link to reset your Phabricator password:". - "\n\n %s\n", - $uri); - } else { - $body = pht( - "Condolences on forgetting your password. You can use this ". - "link to reset it:\n\n". - " %s\n\n". - "After you set a new password, consider writing it down on a ". - "sticky note and attaching it to your monitor so you don't ". - "forget again! Choosing a very short, easy-to-remember password ". - "like \"cat\" or \"1234\" might also help.\n\n". - "Best Wishes,\nPhabricator\n", - $uri); - - } + $body = $this->newAccountLoginMailBody($target_user); $mail = id(new PhabricatorMetaMTAMail()) - ->setSubject(pht('[Phabricator] Password Reset')) + ->setSubject(pht('[Phabricator] Account Login Link')) ->setForceDelivery(true) ->addRawTos(array($target_email->getAddress())) ->setBody($body) @@ -123,44 +95,90 @@ final class PhabricatorEmailLoginController } } - $error_view = null; - if ($errors) { - $error_view = new PHUIInfoView(); - $error_view->setErrors($errors); + $form = id(new AphrontFormView()) + ->setViewer($viewer); + + if ($this->isPasswordAuthEnabled()) { + $form->appendRemarkupInstructions( + pht( + 'To reset your password, provide your email address. An email '. + 'with a login link will be sent to you.')); + } else { + $form->appendRemarkupInstructions( + pht( + 'To access your account, provide your email address. An email '. + 'with a login link will be sent to you.')); } - $email_auth = new PHUIFormLayoutView(); - $email_auth->appendChild($error_view); - $email_auth - ->setUser($request->getUser()) - ->setFullWidth(true) - ->appendChild( + $form + ->appendControl( id(new AphrontFormTextControl()) - ->setLabel(pht('Email')) + ->setLabel(pht('Email Address')) ->setName('email') - ->setValue($request->getStr('email')) + ->setValue($v_email) ->setError($e_email)) - ->appendChild( + ->appendControl( id(new AphrontFormRecaptchaControl()) ->setLabel(pht('Captcha')) ->setError($e_captcha)); - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb(pht('Reset Password')); - $crumbs->setBorder(true); - - $dialog = new AphrontDialogView(); - $dialog->setUser($request->getUser()); - $dialog->setTitle(pht('Forgot Password / Email Login')); - $dialog->appendChild($email_auth); - $dialog->addSubmitButton(pht('Send Email')); - $dialog->setSubmitURI('/login/email/'); - - return $this->newPage() - ->setTitle(pht('Forgot Password')) - ->setCrumbs($crumbs) - ->appendChild($dialog); + if ($this->isPasswordAuthEnabled()) { + $title = pht('Password Reset'); + } else { + $title = pht('Email Login'); + } + return $this->newDialog() + ->setTitle($title) + ->setErrors($errors) + ->setWidth(AphrontDialogView::WIDTH_FORM) + ->appendForm($form) + ->addCancelButton('/auth/start/') + ->addSubmitButton(pht('Send Email')); } + private function newAccountLoginMailBody(PhabricatorUser $user) { + $engine = new PhabricatorAuthSessionEngine(); + $uri = $engine->getOneTimeLoginURI( + $user, + null, + PhabricatorAuthSessionEngine::ONETIME_RESET); + + $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); + $have_passwords = $this->isPasswordAuthEnabled(); + + if ($have_passwords) { + if ($is_serious) { + $body = pht( + "You can use this link to reset your Phabricator password:". + "\n\n %s\n", + $uri); + } else { + $body = pht( + "Condolences on forgetting your password. You can use this ". + "link to reset it:\n\n". + " %s\n\n". + "After you set a new password, consider writing it down on a ". + "sticky note and attaching it to your monitor so you don't ". + "forget again! Choosing a very short, easy-to-remember password ". + "like \"cat\" or \"1234\" might also help.\n\n". + "Best Wishes,\nPhabricator\n", + $uri); + + } + } else { + $body = pht( + "You can use this login link to regain access to your Phabricator ". + "account:". + "\n\n". + " %s\n", + $uri); + } + + return $body; + } + + private function isPasswordAuthEnabled() { + return (bool)PhabricatorPasswordAuthProvider::getPasswordProvider(); + } } From 113a2773dd5bb41921dedc2205820d528ca42db6 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 5 Feb 2019 10:38:20 -0800 Subject: [PATCH 013/245] Remove one-time login from username change email Summary: Depends on D20100. Ref T7732. Ref T13244. This is a bit of an adventure. Long ago, passwords were digested with usernames as part of the salt. This was a mistake: it meant that your password becomes invalid if your username is changed. (I think very very long ago, some other hashing may also have used usernames -- perhaps session hashing or CSRF hashing?) To work around this, the "username change" email included a one-time login link and some language about resetting your password. This flaw was fixed when passwords were moved to shared infrastructure (they're now salted more cleanly on a per-digest basis), and since D18908 (about a year ago) we've transparently upgraded password digests on use. Although it's still technically possible that a username change could invalidate your password, it requires: - You set the password on a version of Phabricator earlier than ~2018 Week 5 (about a year ago). - You haven't logged into a version of Phabricator newer than that using your password since then. - Your username is changed. This probably affects more than zero users, but I suspect not //many// more than zero. These users can always use "Forgot password?" to recover account access. Since the value of this is almost certainly very near zero now and declining over time, just get rid of it. Also move the actual mail out of `PhabricatorUser`, ala the similar recent change to welcome mail in D19989. Test Plan: Changed a user's username, reviewed resulting mail with `bin/mail show-outbound`. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13244, T7732 Differential Revision: https://secure.phabricator.com/D20102 --- src/__phutil_library_map__.php | 2 + .../PhabricatorPeopleUsernameMailEngine.php | 60 +++++++++++++++++++ .../people/storage/PhabricatorUser.php | 49 --------------- .../PhabricatorUserUsernameTransaction.php | 15 ++++- 4 files changed, 74 insertions(+), 52 deletions(-) create mode 100644 src/applications/people/mail/PhabricatorPeopleUsernameMailEngine.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 7bc8670661..b9d468ea3c 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -3899,6 +3899,7 @@ phutil_register_library_map(array( 'PhabricatorPeopleTransactionQuery' => 'applications/people/query/PhabricatorPeopleTransactionQuery.php', 'PhabricatorPeopleUserFunctionDatasource' => 'applications/people/typeahead/PhabricatorPeopleUserFunctionDatasource.php', 'PhabricatorPeopleUserPHIDType' => 'applications/people/phid/PhabricatorPeopleUserPHIDType.php', + 'PhabricatorPeopleUsernameMailEngine' => 'applications/people/mail/PhabricatorPeopleUsernameMailEngine.php', 'PhabricatorPeopleWelcomeController' => 'applications/people/controller/PhabricatorPeopleWelcomeController.php', 'PhabricatorPeopleWelcomeMailEngine' => 'applications/people/mail/PhabricatorPeopleWelcomeMailEngine.php', 'PhabricatorPhabricatorAuthProvider' => 'applications/auth/provider/PhabricatorPhabricatorAuthProvider.php', @@ -9895,6 +9896,7 @@ phutil_register_library_map(array( 'PhabricatorPeopleTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorPeopleUserFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorPeopleUserPHIDType' => 'PhabricatorPHIDType', + 'PhabricatorPeopleUsernameMailEngine' => 'PhabricatorPeopleMailEngine', 'PhabricatorPeopleWelcomeController' => 'PhabricatorPeopleController', 'PhabricatorPeopleWelcomeMailEngine' => 'PhabricatorPeopleMailEngine', 'PhabricatorPhabricatorAuthProvider' => 'PhabricatorOAuth2AuthProvider', diff --git a/src/applications/people/mail/PhabricatorPeopleUsernameMailEngine.php b/src/applications/people/mail/PhabricatorPeopleUsernameMailEngine.php new file mode 100644 index 0000000000..c954b7c38e --- /dev/null +++ b/src/applications/people/mail/PhabricatorPeopleUsernameMailEngine.php @@ -0,0 +1,60 @@ +newUsername = $new_username; + return $this; + } + + public function getNewUsername() { + return $this->newUsername; + } + + public function setOldUsername($old_username) { + $this->oldUsername = $old_username; + return $this; + } + + public function getOldUsername() { + return $this->oldUsername; + } + + public function validateMail() { + return; + } + + protected function newMail() { + $sender = $this->getSender(); + $recipient = $this->getRecipient(); + + $sender_username = $sender->getUsername(); + $sender_realname = $sender->getRealName(); + + $old_username = $this->getOldUsername(); + $new_username = $this->getNewUsername(); + + $body = sprintf( + "%s\n\n %s\n %s\n", + pht( + '%s (%s) has changed your Phabricator username.', + $sender_username, + $sender_realname), + pht( + 'Old Username: %s', + $old_username), + pht( + 'New Username: %s', + $new_username)); + + return id(new PhabricatorMetaMTAMail()) + ->addTos(array($recipient->getPHID())) + ->setSubject(pht('[Phabricator] Username Changed')) + ->setBody($body); + } + +} diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php index 0b18c292c2..70d2d9fb4e 100644 --- a/src/applications/people/storage/PhabricatorUser.php +++ b/src/applications/people/storage/PhabricatorUser.php @@ -555,55 +555,6 @@ final class PhabricatorUser } } - public function sendUsernameChangeEmail( - PhabricatorUser $admin, - $old_username) { - - $admin_username = $admin->getUserName(); - $admin_realname = $admin->getRealName(); - $new_username = $this->getUserName(); - - $password_instructions = null; - if (PhabricatorPasswordAuthProvider::getPasswordProvider()) { - $engine = new PhabricatorAuthSessionEngine(); - $uri = $engine->getOneTimeLoginURI( - $this, - null, - PhabricatorAuthSessionEngine::ONETIME_USERNAME); - $password_instructions = sprintf( - "%s\n\n %s\n\n%s\n", - pht( - "If you use a password to login, you'll need to reset it ". - "before you can login again. You can reset your password by ". - "following this link:"), - $uri, - pht( - "And, of course, you'll need to use your new username to login ". - "from now on. If you use OAuth to login, nothing should change.")); - } - - $body = sprintf( - "%s\n\n %s\n %s\n\n%s", - pht( - '%s (%s) has changed your Phabricator username.', - $admin_username, - $admin_realname), - pht( - 'Old Username: %s', - $old_username), - pht( - 'New Username: %s', - $new_username), - $password_instructions); - - $mail = id(new PhabricatorMetaMTAMail()) - ->addTos(array($this->getPHID())) - ->setForceDelivery(true) - ->setSubject(pht('[Phabricator] Username Changed')) - ->setBody($body) - ->saveAndSend(); - } - public static function describeValidUsername() { return pht( 'Usernames must contain only numbers, letters, period, underscore and '. diff --git a/src/applications/people/xaction/PhabricatorUserUsernameTransaction.php b/src/applications/people/xaction/PhabricatorUserUsernameTransaction.php index f2226d0010..b436b76716 100644 --- a/src/applications/people/xaction/PhabricatorUserUsernameTransaction.php +++ b/src/applications/people/xaction/PhabricatorUserUsernameTransaction.php @@ -18,18 +18,27 @@ final class PhabricatorUserUsernameTransaction } public function applyExternalEffects($object, $value) { + $actor = $this->getActor(); $user = $object; + $old_username = $this->getOldValue(); + $new_username = $this->getNewValue(); + $this->newUserLog(PhabricatorUserLog::ACTION_CHANGE_USERNAME) - ->setOldValue($this->getOldValue()) - ->setNewValue($value) + ->setOldValue($old_username) + ->setNewValue($new_username) ->save(); // The SSH key cache currently includes usernames, so dirty it. See T12554 // for discussion. PhabricatorAuthSSHKeyQuery::deleteSSHKeyCache(); - $user->sendUsernameChangeEmail($this->getActor(), $this->getOldValue()); + id(new PhabricatorPeopleUsernameMailEngine()) + ->setSender($actor) + ->setRecipient($object) + ->setOldUsername($old_username) + ->setNewUsername($new_username) + ->sendMail(); } public function getTitle() { From 55286d49e8ec74e03b3bdbd5374e50415b2fab48 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 6 Feb 2019 06:19:12 -0800 Subject: [PATCH 014/245] Clarify "metamta.default-address" instructions and lock the option Summary: This option no longer needs to be configured if you configure inbound mail (and that's the easiest setup approach in a lot of cases), so stop telling users they have to set it up. Test Plan: Read documentation and configuration help. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20104 --- .../PhabricatorMetaMTAConfigOptions.php | 23 +++++++++++++- .../configuring_outbound_email.diviner | 31 +++++++++++++------ 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php b/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php index ce24d48ead..7e6978dfd8 100644 --- a/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php +++ b/src/applications/config/option/PhabricatorMetaMTAConfigOptions.php @@ -182,6 +182,25 @@ EODOC $mailers_description = $this->deformat(pht(<<deformat(pht(<<setHidden(true) ->setDescription($mailers_description), $this->newOption('metamta.default-address', 'string', null) - ->setDescription(pht('Default "From" address.')), + ->setLocked(true) + ->setSummary(pht('Default address used when generating mail.')) + ->setDescription($default_description), $this->newOption( 'metamta.one-mail-per-recipient', 'bool', diff --git a/src/docs/user/configuration/configuring_outbound_email.diviner b/src/docs/user/configuration/configuring_outbound_email.diviner index 4d18ba0eb2..6f5212680e 100644 --- a/src/docs/user/configuration/configuring_outbound_email.diviner +++ b/src/docs/user/configuration/configuring_outbound_email.diviner @@ -47,18 +47,31 @@ not. For more information on using daemons, see @{article:Managing Daemons with phd}. -Basics -====== +Outbound "From" and "To" Addresses +================================== -Before configuring outbound mail, you should first set up -`metamta.default-address` in Configuration. This determines where mail is sent -"From" by default. +When Phabricator sends outbound mail, it must select some "From" address to +send mail from, since mailers require this. -If your domain is `example.org`, set this to something -like `noreply@example.org`. +When mail only has "CC" recipients, Phabricator generates a dummy "To" address, +since some mailers require this and some users write mail rules that depend +on whether they appear in the "To" or "CC" line. -Ideally, this should be a valid, deliverable address that doesn't bounce if -users accidentally send mail to it. +In both cases, the address should ideally correspond to a valid, deliverable +mailbox that accepts the mail and then simply discards it. If the address is +not valid, some outbound mail will bounce, and users will receive bounces when +they "Reply All" even if the other recipients for the message are valid. In +contrast, if the address is a real user address, that user will receive a lot +of mail they probably don't want. + +If you plan to configure //inbound// mail later, you usually don't need to do +anything. Phabricator will automatically create a `noreply@` mailbox which +works the right way (accepts and discards all mail it receives) and +automatically use it when generating addresses. + +If you don't plan to configure inbound mail, you may need to configure an +address for Phabricator to use. You can do this by setting +`metamta.default-address`. Configuring Mailers From d6f691cf5d5d839e470db0ba1778ef59220937f7 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 6 Feb 2019 07:23:59 -0800 Subject: [PATCH 015/245] In "External Accounts", replace hard-to-find tiny "link" icon with a nice button with text on it Summary: Ref T6703. Replaces the small "link" icon with a more obvious "Link External Account" button. Moves us toward operating against `$config` objects instead of against `$provider` objects, which is more modern and will some day allow us to resolve T6703. Test Plan: Viewed page, saw a more obvious button. Linked an external account. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T6703 Differential Revision: https://secure.phabricator.com/D20105 --- .../storage/PhabricatorAuthProviderConfig.php | 9 +++++ ...abricatorExternalAccountsSettingsPanel.php | 37 +++++++++++++------ 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/src/applications/auth/storage/PhabricatorAuthProviderConfig.php b/src/applications/auth/storage/PhabricatorAuthProviderConfig.php index 6a8bbe1a0d..876a70c2d0 100644 --- a/src/applications/auth/storage/PhabricatorAuthProviderConfig.php +++ b/src/applications/auth/storage/PhabricatorAuthProviderConfig.php @@ -95,6 +95,15 @@ final class PhabricatorAuthProviderConfig return $this->getProvider()->getProviderName(); } + public function getSortVector() { + return id(new PhutilSortVector()) + ->addString($this->getDisplayName()); + } + + public function newIconView() { + return $this->getProvider()->newIconView(); + } + /* -( PhabricatorApplicationTransactionInterface )------------------------- */ diff --git a/src/applications/settings/panel/PhabricatorExternalAccountsSettingsPanel.php b/src/applications/settings/panel/PhabricatorExternalAccountsSettingsPanel.php index 1215487208..9401cddbef 100644 --- a/src/applications/settings/panel/PhabricatorExternalAccountsSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorExternalAccountsSettingsPanel.php @@ -105,26 +105,39 @@ final class PhabricatorExternalAccountsSettingsPanel $accounts = mpull($accounts, null, 'getProviderKey'); - $providers = PhabricatorAuthProvider::getAllEnabledProviders(); - $providers = msort($providers, 'getProviderName'); - foreach ($providers as $key => $provider) { - if (isset($accounts[$key])) { - continue; - } + $configs = id(new PhabricatorAuthProviderConfigQuery()) + ->setViewer($viewer) + ->withIsEnabled(true) + ->execute(); + $configs = msort($configs, 'getSortVector'); + + foreach ($configs as $config) { + $provider = $config->getProvider(); if (!$provider->shouldAllowAccountLink()) { continue; } + // Don't show the user providers they already have linked. + $provider_key = $config->getProvider()->getProviderKey(); + if (isset($accounts[$provider_key])) { + continue; + } + $link_uri = '/auth/link/'.$provider->getProviderKey().'/'; - $item = id(new PHUIObjectItemView()) - ->setHeader($provider->getProviderName()) + $link_button = id(new PHUIButtonView()) + ->setTag('a') + ->setIcon('fa-link') ->setHref($link_uri) - ->addAction( - id(new PHUIListItemView()) - ->setIcon('fa-link') - ->setHref($link_uri)); + ->setColor(PHUIButtonView::GREY) + ->setText(pht('Link External Account')); + + $item = id(new PHUIObjectItemView()) + ->setHeader($config->getDisplayName()) + ->setHref($link_uri) + ->setImageIcon($config->newIconView()) + ->setSideColumn($link_button); $linkable->addItem($item); } From fc3b90e1d1b1cd64866ad9274594072e6c4c2486 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 5 Feb 2019 11:43:01 -0800 Subject: [PATCH 016/245] Allow users to unlink their last external account with a warning, instead of preventing the action Summary: Depends on D20105. Fixes T7732. T7732 describes a case where a user had their Google credentials swapped and had trouble regaining access to their account. Since we now allow email login even if password auth is disabled, it's okay to let users unlink their final account, and it's even reasonable for users to unlink their final account if it is mis-linked. Just give them a warning that what they're doing is a little sketchy, rather than preventing the workflow. Test Plan: Unlinked my only login account, got a stern warning instead of a dead end. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T7732 Differential Revision: https://secure.phabricator.com/D20106 --- .../PhabricatorAuthUnlinkController.php | 68 +++++++++++-------- ...abricatorExternalAccountsSettingsPanel.php | 9 --- 2 files changed, 38 insertions(+), 39 deletions(-) diff --git a/src/applications/auth/controller/PhabricatorAuthUnlinkController.php b/src/applications/auth/controller/PhabricatorAuthUnlinkController.php index e6e1493e5a..1e3023e5d2 100644 --- a/src/applications/auth/controller/PhabricatorAuthUnlinkController.php +++ b/src/applications/auth/controller/PhabricatorAuthUnlinkController.php @@ -32,8 +32,15 @@ final class PhabricatorAuthUnlinkController } } - // Check that this account isn't the last account which can be used to - // login. We prevent you from removing the last account. + $confirmations = $request->getStrList('confirmations'); + $confirmations = array_fuse($confirmations); + + if (!$request->isFormPost() || !isset($confirmations['unlink'])) { + return $this->renderConfirmDialog($confirmations); + } + + // Check that this account isn't the only account which can be used to + // login. We warn you when you remove your only login account. if ($account->isUsableForLogin()) { $other_accounts = id(new PhabricatorExternalAccount())->loadAllWhere( 'userPHID = %s', @@ -47,22 +54,20 @@ final class PhabricatorAuthUnlinkController } if ($valid_accounts < 2) { - return $this->renderLastUsableAccountErrorDialog(); + if (!isset($confirmations['only'])) { + return $this->renderOnlyUsableAccountConfirmDialog($confirmations); + } } } - if ($request->isDialogFormPost()) { - $account->delete(); + $account->delete(); - id(new PhabricatorAuthSessionEngine())->terminateLoginSessions( - $viewer, - new PhutilOpaqueEnvelope( - $request->getCookie(PhabricatorCookies::COOKIE_SESSION))); + id(new PhabricatorAuthSessionEngine())->terminateLoginSessions( + $viewer, + new PhutilOpaqueEnvelope( + $request->getCookie(PhabricatorCookies::COOKIE_SESSION))); - return id(new AphrontRedirectResponse())->setURI($this->getDoneURI()); - } - - return $this->renderConfirmDialog(); + return id(new AphrontRedirectResponse())->setURI($this->getDoneURI()); } private function getDoneURI() { @@ -97,22 +102,27 @@ final class PhabricatorAuthUnlinkController return id(new AphrontDialogResponse())->setDialog($dialog); } - private function renderLastUsableAccountErrorDialog() { - $dialog = id(new AphrontDialogView()) - ->setUser($this->getRequest()->getUser()) - ->setTitle(pht('Last Valid Account')) - ->appendChild( - pht( - 'You can not unlink this account because you have no other '. - 'valid login accounts. If you removed it, you would be unable '. - 'to log in. Add another authentication method before removing '. - 'this one.')) - ->addCancelButton($this->getDoneURI()); + private function renderOnlyUsableAccountConfirmDialog(array $confirmations) { + $confirmations[] = 'only'; - return id(new AphrontDialogResponse())->setDialog($dialog); + return $this->newDialog() + ->setTitle(pht('Unlink Your Only Login Account?')) + ->addHiddenInput('confirmations', implode(',', $confirmations)) + ->appendParagraph( + pht( + 'This is the only external login account linked to your Phabicator '. + 'account. If you remove it, you may no longer be able to log in.')) + ->appendParagraph( + pht( + 'If you lose access to your account, you can recover access by '. + 'sending yourself an email login link from the login screen.')) + ->addCancelButton($this->getDoneURI()) + ->addSubmitButton(pht('Unlink External Account')); } - private function renderConfirmDialog() { + private function renderConfirmDialog(array $confirmations) { + $confirmations[] = 'unlink'; + $provider_key = $this->providerKey; $provider = PhabricatorAuthProvider::getEnabledProviderByKey($provider_key); @@ -129,9 +139,9 @@ final class PhabricatorAuthUnlinkController 'to Phabricator.'); } - $dialog = id(new AphrontDialogView()) - ->setUser($this->getRequest()->getUser()) + return $this->newDialog() ->setTitle($title) + ->addHiddenInput('confirmations', implode(',', $confirmations)) ->appendParagraph($body) ->appendParagraph( pht( @@ -139,8 +149,6 @@ final class PhabricatorAuthUnlinkController 'other active login sessions.')) ->addSubmitButton(pht('Unlink Account')) ->addCancelButton($this->getDoneURI()); - - return id(new AphrontDialogResponse())->setDialog($dialog); } } diff --git a/src/applications/settings/panel/PhabricatorExternalAccountsSettingsPanel.php b/src/applications/settings/panel/PhabricatorExternalAccountsSettingsPanel.php index 9401cddbef..29ef9fa2c7 100644 --- a/src/applications/settings/panel/PhabricatorExternalAccountsSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorExternalAccountsSettingsPanel.php @@ -41,13 +41,6 @@ final class PhabricatorExternalAccountsSettingsPanel ->setUser($viewer) ->setNoDataString(pht('You have no linked accounts.')); - $login_accounts = 0; - foreach ($accounts as $account) { - if ($account->isUsableForLogin()) { - $login_accounts++; - } - } - foreach ($accounts as $account) { $item = new PHUIObjectItemView(); @@ -72,8 +65,6 @@ final class PhabricatorExternalAccountsSettingsPanel 'account provider).')); } - $can_unlink = $can_unlink && (!$can_login || ($login_accounts > 1)); - $can_refresh = $provider && $provider->shouldAllowAccountRefresh(); if ($can_refresh) { $item->addAction( From a46c25d2baf307f52a5a456f0445ea21dbac3a28 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 6 Feb 2019 08:51:42 -0800 Subject: [PATCH 017/245] Make two ancient migrations fatal if they affect data Summary: Depends on D20106. Ref T6703. Since I plan to change the `ExternalAccount` table, these migrations (which rely on `save()`) will stop working. They could be rewritten to use raw queries, but I suspect few or no installs are affected. At least for now, just make them safe: if they would affect data, fatal and tell the user to perform a more gradual upgrade. Also remove an `ALTER IGNORE TABLE` (this syntax was removed at some point) and fix a `%Q` when adjusting certain types of primary keys. Test Plan: Ran `bin/storage upgrade --no-quickstart --force --namespace test1234` to get a complete migration since the beginning of time. Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T6703 Differential Revision: https://secure.phabricator.com/D20107 --- resources/sql/patches/133.imagemacro.sql | 4 +- .../sql/patches/20130611.migrateoauth.php | 68 +++---------------- resources/sql/patches/20130611.nukeldap.php | 43 +++--------- .../PhabricatorStorageManagementWorkflow.php | 2 +- 4 files changed, 19 insertions(+), 98 deletions(-) diff --git a/resources/sql/patches/133.imagemacro.sql b/resources/sql/patches/133.imagemacro.sql index 01852c6b48..1477fd879f 100644 --- a/resources/sql/patches/133.imagemacro.sql +++ b/resources/sql/patches/133.imagemacro.sql @@ -1,2 +1,2 @@ -ALTER IGNORE TABLE `{$NAMESPACE}_file`.`file_imagemacro` - ADD UNIQUE `name` (`name`); +ALTER TABLE `{$NAMESPACE}_file`.`file_imagemacro` + ADD UNIQUE KEY `name` (`name`); diff --git a/resources/sql/patches/20130611.migrateoauth.php b/resources/sql/patches/20130611.migrateoauth.php index 3622b2772e..92fe854cfd 100644 --- a/resources/sql/patches/20130611.migrateoauth.php +++ b/resources/sql/patches/20130611.migrateoauth.php @@ -1,66 +1,14 @@ establishConnection('w'); $table_name = 'user_oauthinfo'; -$conn_w = $table->establishConnection('w'); -$xaccount = new PhabricatorExternalAccount(); - -echo pht('Migrating OAuth to %s...', 'ExternalAccount')."\n"; - -$domain_map = array( - 'disqus' => 'disqus.com', - 'facebook' => 'facebook.com', - 'github' => 'github.com', - 'google' => 'google.com', -); - -try { - $phabricator_oauth_uri = new PhutilURI( - PhabricatorEnv::getEnvConfig('phabricator.oauth-uri')); - $domain_map['phabricator'] = $phabricator_oauth_uri->getDomain(); -} catch (Exception $ex) { - // Ignore; this likely indicates that we have removed `phabricator.oauth-uri` - // in some future diff. +foreach (new LiskRawMigrationIterator($conn, $table_name) as $row) { + throw new Exception( + pht( + 'Your Phabricator install has ancient OAuth account data and is '. + 'too old to upgrade directly to a modern version of Phabricator. '. + 'Upgrade to a version released between June 2013 and February 2019 '. + 'first, then upgrade to a modern version.')); } - -$rows = queryfx_all( - $conn_w, - 'SELECT * FROM user_oauthinfo'); -foreach ($rows as $row) { - echo pht('Migrating row ID #%d.', $row['id'])."\n"; - $user = id(new PhabricatorUser())->loadOneWhere( - 'id = %d', - $row['userID']); - if (!$user) { - echo pht('Bad user ID!')."\n"; - continue; - } - - $domain = idx($domain_map, $row['oauthProvider']); - if (empty($domain)) { - echo pht('Unknown OAuth provider!')."\n"; - continue; - } - - - $xaccount = id(new PhabricatorExternalAccount()) - ->setUserPHID($user->getPHID()) - ->setAccountType($row['oauthProvider']) - ->setAccountDomain($domain) - ->setAccountID($row['oauthUID']) - ->setAccountURI($row['accountURI']) - ->setUsername($row['accountName']) - ->setDateCreated($row['dateCreated']); - - try { - $xaccount->save(); - } catch (Exception $ex) { - phlog($ex); - } -} - -echo pht('Done.')."\n"; diff --git a/resources/sql/patches/20130611.nukeldap.php b/resources/sql/patches/20130611.nukeldap.php index 3f225cfa84..0f0b976a58 100644 --- a/resources/sql/patches/20130611.nukeldap.php +++ b/resources/sql/patches/20130611.nukeldap.php @@ -1,41 +1,14 @@ establishConnection('w'); $table_name = 'user_ldapinfo'; -$conn_w = $table->establishConnection('w'); -$xaccount = new PhabricatorExternalAccount(); - -echo pht('Migrating LDAP to %s...', 'ExternalAccount')."\n"; - -$rows = queryfx_all($conn_w, 'SELECT * FROM %T', $table_name); -foreach ($rows as $row) { - echo pht('Migrating row ID #%d.', $row['id'])."\n"; - $user = id(new PhabricatorUser())->loadOneWhere( - 'id = %d', - $row['userID']); - if (!$user) { - echo pht('Bad user ID!')."\n"; - continue; - } - - - $xaccount = id(new PhabricatorExternalAccount()) - ->setUserPHID($user->getPHID()) - ->setAccountType('ldap') - ->setAccountDomain('self') - ->setAccountID($row['ldapUsername']) - ->setUsername($row['ldapUsername']) - ->setDateCreated($row['dateCreated']); - - try { - $xaccount->save(); - } catch (Exception $ex) { - phlog($ex); - } +foreach (new LiskRawMigrationIterator($conn, $table_name) as $row) { + throw new Exception( + pht( + 'Your Phabricator install has ancient LDAP account data and is '. + 'too old to upgrade directly to a modern version of Phabricator. '. + 'Upgrade to a version released between June 2013 and February 2019 '. + 'first, then upgrade to a modern version.')); } - -echo pht('Done.')."\n"; diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php index 5bc83972dd..acbbb4fbda 100644 --- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php +++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php @@ -432,7 +432,7 @@ abstract class PhabricatorStorageManagementWorkflow case 'key': if (($phase == 'drop_keys') && $adjust['exists']) { if ($adjust['name'] == 'PRIMARY') { - $key_name = 'PRIMARY KEY'; + $key_name = qsprintf($conn, 'PRIMARY KEY'); } else { $key_name = qsprintf($conn, 'KEY %T', $adjust['name']); } From f2236eb061a6fb7cc8fb645fe21d8194966cbc86 Mon Sep 17 00:00:00 2001 From: Austin McKinley Date: Thu, 7 Feb 2019 11:32:48 -0800 Subject: [PATCH 018/245] Autofocus form control for adding TOTP codes Summary: Ref D20122. This is something I wanted in a bunch of places. Looks like at some point the most-annoying one (autofocus for entering TOTOP codes) already got fixed at some point. Test Plan: Loaded the form, got autofocus as expected. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D20128 --- .../auth/factor/PhabricatorTOTPAuthFactor.php | 1 + src/view/form/control/PHUIFormNumberControl.php | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php b/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php index ba6613c014..7e77dfc11a 100644 --- a/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorTOTPAuthFactor.php @@ -128,6 +128,7 @@ final class PhabricatorTOTPAuthFactor extends PhabricatorAuthFactor { ->setLabel(pht('TOTP Code')) ->setName('totpcode') ->setValue($code) + ->setAutofocus(true) ->setError($e_code)); } diff --git a/src/view/form/control/PHUIFormNumberControl.php b/src/view/form/control/PHUIFormNumberControl.php index 26e7e03955..c577bebbd0 100644 --- a/src/view/form/control/PHUIFormNumberControl.php +++ b/src/view/form/control/PHUIFormNumberControl.php @@ -3,6 +3,7 @@ final class PHUIFormNumberControl extends AphrontFormControl { private $disableAutocomplete; + private $autofocus; public function setDisableAutocomplete($disable_autocomplete) { $this->disableAutocomplete = $disable_autocomplete; @@ -13,6 +14,15 @@ final class PHUIFormNumberControl extends AphrontFormControl { return $this->disableAutocomplete; } + public function setAutofocus($autofocus) { + $this->autofocus = $autofocus; + return $this; + } + + public function getAutofocus() { + return $this->autofocus; + } + protected function getCustomControlClass() { return 'phui-form-number'; } @@ -34,6 +44,7 @@ final class PHUIFormNumberControl extends AphrontFormControl { 'disabled' => $this->getDisabled() ? 'disabled' : null, 'autocomplete' => $autocomplete, 'id' => $this->getID(), + 'autofocus' => ($this->getAutofocus() ? 'autofocus' : null), )); } From 26081594e2f3c1bd01ce785089b5b5a23625e4ef Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 7 Feb 2019 09:26:07 -0800 Subject: [PATCH 019/245] Fix two very, very minor correctness issues in Slowvote Summary: See and . I previously awarded a bounty for so Slowvote is getting "researched" a lot. - Prevent users from undoing their vote by submitting the form with nothing selected. - Prevent users from racing between the `delete()` and `save()` to vote for multiple options in a plurality poll. Test Plan: - Clicked the vote button with nothing selected in plurality and approval polls, got an error now. - Added a `sleep(5)` between `delete()` and `save()`. Submitted different plurality votes in different windows. Before: votes raced, invalid end state. After: votes waited on the lock, arrived in a valid end state. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20125 --- .../PhabricatorSlowvoteVoteController.php | 53 ++++++++++++++----- 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/src/applications/slowvote/controller/PhabricatorSlowvoteVoteController.php b/src/applications/slowvote/controller/PhabricatorSlowvoteVoteController.php index 62913b09e3..e1a1b9df34 100644 --- a/src/applications/slowvote/controller/PhabricatorSlowvoteVoteController.php +++ b/src/applications/slowvote/controller/PhabricatorSlowvoteVoteController.php @@ -37,6 +37,19 @@ final class PhabricatorSlowvoteVoteController $method = $poll->getMethod(); $is_plurality = ($method == PhabricatorSlowvotePoll::METHOD_PLURALITY); + if (!$votes) { + if ($is_plurality) { + $message = pht('You must vote for something.'); + } else { + $message = pht('You must vote for at least one option.'); + } + + return $this->newDialog() + ->setTitle(pht('Stand For Something')) + ->appendParagraph($message) + ->addCancelButton($poll->getURI()); + } + if ($is_plurality && count($votes) > 1) { throw new Exception( pht('In this poll, you may only vote for one option.')); @@ -52,23 +65,39 @@ final class PhabricatorSlowvoteVoteController } } - foreach ($old_votes as $old_vote) { - if (!idx($votes, $old_vote->getOptionID(), false)) { + $poll->openTransaction(); + $poll->beginReadLocking(); + + $poll->reload(); + + $old_votes = id(new PhabricatorSlowvoteChoice())->loadAllWhere( + 'pollID = %d AND authorPHID = %s', + $poll->getID(), + $viewer->getPHID()); + $old_votes = mpull($old_votes, null, 'getOptionID'); + + foreach ($old_votes as $old_vote) { + if (idx($votes, $old_vote->getOptionID())) { + continue; + } + $old_vote->delete(); } - } - foreach ($votes as $vote) { - if (idx($old_votes, $vote, false)) { - continue; + foreach ($votes as $vote) { + if (idx($old_votes, $vote)) { + continue; + } + + id(new PhabricatorSlowvoteChoice()) + ->setAuthorPHID($viewer->getPHID()) + ->setPollID($poll->getID()) + ->setOptionID($vote) + ->save(); } - id(new PhabricatorSlowvoteChoice()) - ->setAuthorPHID($viewer->getPHID()) - ->setPollID($poll->getID()) - ->setOptionID($vote) - ->save(); - } + $poll->endReadLocking(); + $poll->saveTransaction(); return id(new AphrontRedirectResponse()) ->setURI($poll->getURI()); From e25dc2dfe283794222f284bce5c1247f3d3d1100 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 6 Feb 2019 16:17:09 -0800 Subject: [PATCH 020/245] Revert "feed.http-hooks" HTTP request construction to use "http_build_query()" so nested "storyData" is handled correctly Summary: See . This behavior was changed by D20049. I think it's generally good that we not accept/encode nested values in a PHP-specific way, but retain `feed.http-hooks` compatibility for now. Test Plan: {F6190681} Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20114 --- src/applications/feed/worker/FeedPublisherHTTPWorker.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/applications/feed/worker/FeedPublisherHTTPWorker.php b/src/applications/feed/worker/FeedPublisherHTTPWorker.php index 4742e52a29..27a869ddef 100644 --- a/src/applications/feed/worker/FeedPublisherHTTPWorker.php +++ b/src/applications/feed/worker/FeedPublisherHTTPWorker.php @@ -26,6 +26,11 @@ final class FeedPublisherHTTPWorker extends FeedPushWorker { 'epoch' => $data->getEpoch(), ); + // NOTE: We're explicitly using "http_build_query()" here because the + // "storyData" parameter may be a nested object with arbitrary nested + // sub-objects. + $post_data = http_build_query($post_data, '', '&'); + id(new HTTPSFuture($uri, $post_data)) ->setMethod('POST') ->setTimeout(30) From 4fa5a2421ea5e0296c40840ec785f5f9b9252beb Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 6 Feb 2019 16:28:50 -0800 Subject: [PATCH 021/245] Add formal setup guidance warning that "feed.http-hooks" will be removed in a future version of Phabricator Summary: Depends on D20114. This is on the way out, so make that explicitly clear. Test Plan: Read setup issue and configuration option. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20115 --- .../PhabricatorExtraConfigSetupCheck.php | 18 +++++++++++++ .../config/PhabricatorFeedConfigOptions.php | 26 +++++++++---------- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php index 1c8a593a78..932e04db4b 100644 --- a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php +++ b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php @@ -84,6 +84,24 @@ final class PhabricatorExtraConfigSetupCheck extends PhabricatorSetupCheck { $issue->addPhabricatorConfig($key); } } + + + if (PhabricatorEnv::getEnvConfig('feed.http-hooks')) { + $this->newIssue('config.deprecated.feed.http-hooks') + ->setShortName(pht('Feed Hooks Deprecated')) + ->setName(pht('Migrate From "feed.http-hooks" to Webhooks')) + ->addPhabricatorConfig('feed.http-hooks') + ->setMessage( + pht( + 'The "feed.http-hooks" option is deprecated in favor of '. + 'Webhooks. This option will be removed in a future version '. + 'of Phabricator.'. + "\n\n". + 'You can configure Webhooks in Herald.'. + "\n\n". + 'To resolve this issue, remove all URIs from "feed.http-hooks".')); + } + } /** diff --git a/src/applications/feed/config/PhabricatorFeedConfigOptions.php b/src/applications/feed/config/PhabricatorFeedConfigOptions.php index 4b6612f931..29c5a9549b 100644 --- a/src/applications/feed/config/PhabricatorFeedConfigOptions.php +++ b/src/applications/feed/config/PhabricatorFeedConfigOptions.php @@ -20,22 +20,22 @@ final class PhabricatorFeedConfigOptions } public function getOptions() { + $hooks_help = $this->deformat(pht(<<newOption('feed.http-hooks', 'list', array()) ->setLocked(true) - ->setSummary(pht('POST notifications of feed events.')) - ->setDescription( - pht( - "If you set this to a list of HTTP URIs, when a feed story is ". - "published a task will be created for each URI that posts the ". - "story data to the URI. Daemons automagically retry failures 100 ". - "times, waiting `\$fail_count * 60s` between each subsequent ". - "failure. Be sure to keep the daemon console (`%s`) open ". - "while developing and testing your end points. You may need to". - "restart your daemons to start sending HTTP requests.\n\n". - "NOTE: URIs are not validated, the URI must return HTTP status ". - "200 within 30 seconds, and no permission checks are performed.", - '/daemon/')), + ->setSummary(pht('Deprecated.')) + ->setDescription($hooks_help), ); } From 9f5e6bee903740eaf9423f6fd8915aa4f6bff66e Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 6 Feb 2019 19:36:41 -0800 Subject: [PATCH 022/245] Make the default behavior of getApplicationTransactionCommentObject() "return null" instead of "throw" Summary: Depends on D20115. See . Currently, `getApplicationTransactionCommentObject()` throws by default. Subclasses must override it to `return null` to indicate that they don't support comments. This is silly, and leads to a bunch of code that does a `try / catch` around it, and at least some code (here, `transaction.search`) which doesn't `try / catch` and gets the wrong behavior as a result. Just make it `return null` by default, meaning "no support for comments". Then remove the `try / catch` stuff and all the `return null` implementations. Test Plan: - Grepped for `getApplicationTransactionCommentObject()`, fixed each callsite / definition. - Called `transaction.search` on a diff with transactions (i.e., not a sourced-from-commit diff). Reviewers: amckinley Reviewed By: amckinley Subscribers: jbrownEP Differential Revision: https://secure.phabricator.com/D20121 --- .../almanac/storage/AlmanacModularTransaction.php | 4 ---- .../auth/storage/PhabricatorAuthPasswordTransaction.php | 4 ---- .../storage/PhabricatorAuthProviderConfigTransaction.php | 4 ---- .../auth/storage/PhabricatorAuthSSHKeyTransaction.php | 4 ---- .../config/storage/PhabricatorConfigTransaction.php | 4 ---- .../diviner/storage/DivinerLiveBookTransaction.php | 4 ---- src/applications/fund/storage/FundBackerTransaction.php | 4 ---- src/applications/herald/action/HeraldCommentAction.php | 9 +++------ .../herald/storage/HeraldWebhookTransaction.php | 4 ---- .../PhabricatorMetaMTAApplicationEmailTransaction.php | 4 ---- .../storage/PhabricatorOAuthServerTransaction.php | 4 ---- .../storage/PhabricatorOwnersPackageTransaction.php | 4 ---- .../storage/PassphraseCredentialTransaction.php | 4 ---- .../people/storage/PhabricatorUserTransaction.php | 4 ---- src/applications/phame/storage/PhameBlogTransaction.php | 4 ---- src/applications/phlux/storage/PhluxTransaction.php | 4 ---- .../phortune/storage/PhortuneAccountTransaction.php | 4 ---- .../phortune/storage/PhortuneCartTransaction.php | 4 ---- .../phortune/storage/PhortuneMerchantTransaction.php | 4 ---- .../storage/PhortunePaymentProviderConfigTransaction.php | 4 ---- .../project/storage/PhabricatorProjectTransaction.php | 4 ---- .../storage/PhabricatorRepositoryTransaction.php | 4 ---- .../PhabricatorFulltextIndexEngineExtension.php | 8 ++------ ...habricatorProfileMenuItemConfigurationTransaction.php | 4 ---- .../storage/PhabricatorUserPreferencesTransaction.php | 4 ---- .../storage/PhabricatorSpacesNamespaceTransaction.php | 4 ---- .../editor/PhabricatorApplicationTransactionEditor.php | 7 +------ .../PhabricatorCommentEditEngineExtension.php | 7 +------ .../storage/PhabricatorApplicationTransaction.php | 9 ++------- .../PhabricatorEditEngineConfigurationTransaction.php | 4 ---- 30 files changed, 9 insertions(+), 131 deletions(-) diff --git a/src/applications/almanac/storage/AlmanacModularTransaction.php b/src/applications/almanac/storage/AlmanacModularTransaction.php index 6497069241..3e2eb0a3ec 100644 --- a/src/applications/almanac/storage/AlmanacModularTransaction.php +++ b/src/applications/almanac/storage/AlmanacModularTransaction.php @@ -7,8 +7,4 @@ abstract class AlmanacModularTransaction return 'almanac'; } - public function getApplicationTransactionCommentObject() { - return null; - } - } diff --git a/src/applications/auth/storage/PhabricatorAuthPasswordTransaction.php b/src/applications/auth/storage/PhabricatorAuthPasswordTransaction.php index 9d02112dff..a8cb6d10a3 100644 --- a/src/applications/auth/storage/PhabricatorAuthPasswordTransaction.php +++ b/src/applications/auth/storage/PhabricatorAuthPasswordTransaction.php @@ -11,10 +11,6 @@ final class PhabricatorAuthPasswordTransaction return PhabricatorAuthPasswordPHIDType::TYPECONST; } - public function getApplicationTransactionCommentObject() { - return null; - } - public function getBaseTransactionClass() { return 'PhabricatorAuthPasswordTransactionType'; } diff --git a/src/applications/auth/storage/PhabricatorAuthProviderConfigTransaction.php b/src/applications/auth/storage/PhabricatorAuthProviderConfigTransaction.php index e1453b4383..d5a3588d59 100644 --- a/src/applications/auth/storage/PhabricatorAuthProviderConfigTransaction.php +++ b/src/applications/auth/storage/PhabricatorAuthProviderConfigTransaction.php @@ -33,10 +33,6 @@ final class PhabricatorAuthProviderConfigTransaction return PhabricatorAuthAuthProviderPHIDType::TYPECONST; } - public function getApplicationTransactionCommentObject() { - return null; - } - public function getIcon() { $old = $this->getOldValue(); $new = $this->getNewValue(); diff --git a/src/applications/auth/storage/PhabricatorAuthSSHKeyTransaction.php b/src/applications/auth/storage/PhabricatorAuthSSHKeyTransaction.php index bb08310cf3..028be1746d 100644 --- a/src/applications/auth/storage/PhabricatorAuthSSHKeyTransaction.php +++ b/src/applications/auth/storage/PhabricatorAuthSSHKeyTransaction.php @@ -15,10 +15,6 @@ final class PhabricatorAuthSSHKeyTransaction return PhabricatorAuthSSHKeyPHIDType::TYPECONST; } - public function getApplicationTransactionCommentObject() { - return null; - } - public function getTitle() { $author_phid = $this->getAuthorPHID(); diff --git a/src/applications/config/storage/PhabricatorConfigTransaction.php b/src/applications/config/storage/PhabricatorConfigTransaction.php index b7cfb6f495..94272bfb1a 100644 --- a/src/applications/config/storage/PhabricatorConfigTransaction.php +++ b/src/applications/config/storage/PhabricatorConfigTransaction.php @@ -13,10 +13,6 @@ final class PhabricatorConfigTransaction return PhabricatorConfigConfigPHIDType::TYPECONST; } - public function getApplicationTransactionCommentObject() { - return null; - } - public function getTitle() { $author_phid = $this->getAuthorPHID(); diff --git a/src/applications/diviner/storage/DivinerLiveBookTransaction.php b/src/applications/diviner/storage/DivinerLiveBookTransaction.php index ae461e751a..f8eb81d1f3 100644 --- a/src/applications/diviner/storage/DivinerLiveBookTransaction.php +++ b/src/applications/diviner/storage/DivinerLiveBookTransaction.php @@ -11,8 +11,4 @@ final class DivinerLiveBookTransaction return DivinerBookPHIDType::TYPECONST; } - public function getApplicationTransactionCommentObject() { - return null; - } - } diff --git a/src/applications/fund/storage/FundBackerTransaction.php b/src/applications/fund/storage/FundBackerTransaction.php index c24e769eb6..c08958a29a 100644 --- a/src/applications/fund/storage/FundBackerTransaction.php +++ b/src/applications/fund/storage/FundBackerTransaction.php @@ -11,10 +11,6 @@ final class FundBackerTransaction return FundBackerPHIDType::TYPECONST; } - public function getApplicationTransactionCommentObject() { - return null; - } - public function getBaseTransactionClass() { return 'FundBackerTransactionType'; } diff --git a/src/applications/herald/action/HeraldCommentAction.php b/src/applications/herald/action/HeraldCommentAction.php index fa52ba1f5f..f8b8fbe813 100644 --- a/src/applications/herald/action/HeraldCommentAction.php +++ b/src/applications/herald/action/HeraldCommentAction.php @@ -19,12 +19,9 @@ final class HeraldCommentAction extends HeraldAction { } $xaction = $object->getApplicationTransactionTemplate(); - try { - $comment = $xaction->getApplicationTransactionCommentObject(); - if (!$comment) { - return false; - } - } catch (PhutilMethodNotImplementedException $ex) { + + $comment = $xaction->getApplicationTransactionCommentObject(); + if (!$comment) { return false; } diff --git a/src/applications/herald/storage/HeraldWebhookTransaction.php b/src/applications/herald/storage/HeraldWebhookTransaction.php index 03c8cbb776..4f924cd4bb 100644 --- a/src/applications/herald/storage/HeraldWebhookTransaction.php +++ b/src/applications/herald/storage/HeraldWebhookTransaction.php @@ -11,10 +11,6 @@ final class HeraldWebhookTransaction return HeraldWebhookPHIDType::TYPECONST; } - public function getApplicationTransactionCommentObject() { - return null; - } - public function getBaseTransactionClass() { return 'HeraldWebhookTransactionType'; } diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmailTransaction.php b/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmailTransaction.php index 019adb338d..af6a6fbb88 100644 --- a/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmailTransaction.php +++ b/src/applications/metamta/storage/PhabricatorMetaMTAApplicationEmailTransaction.php @@ -16,8 +16,4 @@ final class PhabricatorMetaMTAApplicationEmailTransaction return PhabricatorMetaMTAApplicationEmailPHIDType::TYPECONST; } - public function getApplicationTransactionCommentObject() { - return null; - } - } diff --git a/src/applications/oauthserver/storage/PhabricatorOAuthServerTransaction.php b/src/applications/oauthserver/storage/PhabricatorOAuthServerTransaction.php index b2624dd9a4..acfb88ef48 100644 --- a/src/applications/oauthserver/storage/PhabricatorOAuthServerTransaction.php +++ b/src/applications/oauthserver/storage/PhabricatorOAuthServerTransaction.php @@ -19,10 +19,6 @@ final class PhabricatorOAuthServerTransaction return PhabricatorOAuthServerClientPHIDType::TYPECONST; } - public function getApplicationTransactionCommentObject() { - return null; - } - public function getTitle() { $author_phid = $this->getAuthorPHID(); $old = $this->getOldValue(); diff --git a/src/applications/owners/storage/PhabricatorOwnersPackageTransaction.php b/src/applications/owners/storage/PhabricatorOwnersPackageTransaction.php index 1dfc944f63..66e15b634a 100644 --- a/src/applications/owners/storage/PhabricatorOwnersPackageTransaction.php +++ b/src/applications/owners/storage/PhabricatorOwnersPackageTransaction.php @@ -15,8 +15,4 @@ final class PhabricatorOwnersPackageTransaction return 'PhabricatorOwnersPackageTransactionType'; } - public function getApplicationTransactionCommentObject() { - return null; - } - } diff --git a/src/applications/passphrase/storage/PassphraseCredentialTransaction.php b/src/applications/passphrase/storage/PassphraseCredentialTransaction.php index b7e4f904ef..bbc3b09668 100644 --- a/src/applications/passphrase/storage/PassphraseCredentialTransaction.php +++ b/src/applications/passphrase/storage/PassphraseCredentialTransaction.php @@ -11,10 +11,6 @@ final class PassphraseCredentialTransaction return PassphraseCredentialPHIDType::TYPECONST; } - public function getApplicationTransactionCommentObject() { - return null; - } - public function getBaseTransactionClass() { return 'PassphraseCredentialTransactionType'; } diff --git a/src/applications/people/storage/PhabricatorUserTransaction.php b/src/applications/people/storage/PhabricatorUserTransaction.php index 24edb2f5b5..81ca52a132 100644 --- a/src/applications/people/storage/PhabricatorUserTransaction.php +++ b/src/applications/people/storage/PhabricatorUserTransaction.php @@ -11,10 +11,6 @@ final class PhabricatorUserTransaction return PhabricatorPeopleUserPHIDType::TYPECONST; } - public function getApplicationTransactionCommentObject() { - return null; - } - public function getBaseTransactionClass() { return 'PhabricatorUserTransactionType'; } diff --git a/src/applications/phame/storage/PhameBlogTransaction.php b/src/applications/phame/storage/PhameBlogTransaction.php index d3d6a79d0a..c605510d7d 100644 --- a/src/applications/phame/storage/PhameBlogTransaction.php +++ b/src/applications/phame/storage/PhameBlogTransaction.php @@ -15,10 +15,6 @@ final class PhameBlogTransaction return PhabricatorPhameBlogPHIDType::TYPECONST; } - public function getApplicationTransactionCommentObject() { - return null; - } - public function getBaseTransactionClass() { return 'PhameBlogTransactionType'; } diff --git a/src/applications/phlux/storage/PhluxTransaction.php b/src/applications/phlux/storage/PhluxTransaction.php index 1224caf201..b1624d581a 100644 --- a/src/applications/phlux/storage/PhluxTransaction.php +++ b/src/applications/phlux/storage/PhluxTransaction.php @@ -13,10 +13,6 @@ final class PhluxTransaction extends PhabricatorApplicationTransaction { return PhluxVariablePHIDType::TYPECONST; } - public function getApplicationTransactionCommentObject() { - return null; - } - public function getTitle() { $author_phid = $this->getAuthorPHID(); diff --git a/src/applications/phortune/storage/PhortuneAccountTransaction.php b/src/applications/phortune/storage/PhortuneAccountTransaction.php index 6733cbe879..e333ef4a26 100644 --- a/src/applications/phortune/storage/PhortuneAccountTransaction.php +++ b/src/applications/phortune/storage/PhortuneAccountTransaction.php @@ -11,10 +11,6 @@ final class PhortuneAccountTransaction return PhortuneAccountPHIDType::TYPECONST; } - public function getApplicationTransactionCommentObject() { - return null; - } - public function getBaseTransactionClass() { return 'PhortuneAccountTransactionType'; } diff --git a/src/applications/phortune/storage/PhortuneCartTransaction.php b/src/applications/phortune/storage/PhortuneCartTransaction.php index 41790011a2..c7a1e36e73 100644 --- a/src/applications/phortune/storage/PhortuneCartTransaction.php +++ b/src/applications/phortune/storage/PhortuneCartTransaction.php @@ -19,10 +19,6 @@ final class PhortuneCartTransaction return PhortuneCartPHIDType::TYPECONST; } - public function getApplicationTransactionCommentObject() { - return null; - } - public function shouldHideForMail(array $xactions) { switch ($this->getTransactionType()) { case self::TYPE_CREATED: diff --git a/src/applications/phortune/storage/PhortuneMerchantTransaction.php b/src/applications/phortune/storage/PhortuneMerchantTransaction.php index 3befb12212..976259c534 100644 --- a/src/applications/phortune/storage/PhortuneMerchantTransaction.php +++ b/src/applications/phortune/storage/PhortuneMerchantTransaction.php @@ -11,10 +11,6 @@ final class PhortuneMerchantTransaction return PhortuneMerchantPHIDType::TYPECONST; } - public function getApplicationTransactionCommentObject() { - return null; - } - public function getBaseTransactionClass() { return 'PhortuneMerchantTransactionType'; } diff --git a/src/applications/phortune/storage/PhortunePaymentProviderConfigTransaction.php b/src/applications/phortune/storage/PhortunePaymentProviderConfigTransaction.php index 08872d48fd..9241c7ae04 100644 --- a/src/applications/phortune/storage/PhortunePaymentProviderConfigTransaction.php +++ b/src/applications/phortune/storage/PhortunePaymentProviderConfigTransaction.php @@ -17,10 +17,6 @@ final class PhortunePaymentProviderConfigTransaction return PhortunePaymentProviderPHIDType::TYPECONST; } - public function getApplicationTransactionCommentObject() { - return null; - } - public function getTitle() { $author_phid = $this->getAuthorPHID(); diff --git a/src/applications/project/storage/PhabricatorProjectTransaction.php b/src/applications/project/storage/PhabricatorProjectTransaction.php index a8b2bb0d4a..158c2480c0 100644 --- a/src/applications/project/storage/PhabricatorProjectTransaction.php +++ b/src/applications/project/storage/PhabricatorProjectTransaction.php @@ -19,10 +19,6 @@ final class PhabricatorProjectTransaction return PhabricatorProjectProjectPHIDType::TYPECONST; } - public function getApplicationTransactionCommentObject() { - return null; - } - public function getBaseTransactionClass() { return 'PhabricatorProjectTransactionType'; } diff --git a/src/applications/repository/storage/PhabricatorRepositoryTransaction.php b/src/applications/repository/storage/PhabricatorRepositoryTransaction.php index 85c354ba67..8729e462a3 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryTransaction.php +++ b/src/applications/repository/storage/PhabricatorRepositoryTransaction.php @@ -11,10 +11,6 @@ final class PhabricatorRepositoryTransaction return PhabricatorRepositoryRepositoryPHIDType::TYPECONST; } - public function getApplicationTransactionCommentObject() { - return null; - } - public function getBaseTransactionClass() { return 'PhabricatorRepositoryTransactionType'; } diff --git a/src/applications/search/engineextension/PhabricatorFulltextIndexEngineExtension.php b/src/applications/search/engineextension/PhabricatorFulltextIndexEngineExtension.php index ab4da88420..126f298f1f 100644 --- a/src/applications/search/engineextension/PhabricatorFulltextIndexEngineExtension.php +++ b/src/applications/search/engineextension/PhabricatorFulltextIndexEngineExtension.php @@ -70,12 +70,8 @@ final class PhabricatorFulltextIndexEngineExtension private function getCommentVersion($object) { $xaction = $object->getApplicationTransactionTemplate(); - try { - $comment = $xaction->getApplicationTransactionCommentObject(); - if (!$comment) { - return 'none'; - } - } catch (Exception $ex) { + $comment = $xaction->getApplicationTransactionCommentObject(); + if (!$comment) { return 'none'; } diff --git a/src/applications/search/storage/PhabricatorProfileMenuItemConfigurationTransaction.php b/src/applications/search/storage/PhabricatorProfileMenuItemConfigurationTransaction.php index b1d30a5b9d..4624d6f9af 100644 --- a/src/applications/search/storage/PhabricatorProfileMenuItemConfigurationTransaction.php +++ b/src/applications/search/storage/PhabricatorProfileMenuItemConfigurationTransaction.php @@ -20,8 +20,4 @@ final class PhabricatorProfileMenuItemConfigurationTransaction return PhabricatorProfileMenuItemPHIDType::TYPECONST; } - public function getApplicationTransactionCommentObject() { - return null; - } - } diff --git a/src/applications/settings/storage/PhabricatorUserPreferencesTransaction.php b/src/applications/settings/storage/PhabricatorUserPreferencesTransaction.php index 6378ee29d3..3ef48c01e0 100644 --- a/src/applications/settings/storage/PhabricatorUserPreferencesTransaction.php +++ b/src/applications/settings/storage/PhabricatorUserPreferencesTransaction.php @@ -11,10 +11,6 @@ final class PhabricatorUserPreferencesTransaction return 'user'; } - public function getApplicationTransactionCommentObject() { - return null; - } - public function getApplicationTransactionType() { return PhabricatorUserPreferencesPHIDType::TYPECONST; } diff --git a/src/applications/spaces/storage/PhabricatorSpacesNamespaceTransaction.php b/src/applications/spaces/storage/PhabricatorSpacesNamespaceTransaction.php index 0f50a870f6..bac0ea636f 100644 --- a/src/applications/spaces/storage/PhabricatorSpacesNamespaceTransaction.php +++ b/src/applications/spaces/storage/PhabricatorSpacesNamespaceTransaction.php @@ -11,10 +11,6 @@ final class PhabricatorSpacesNamespaceTransaction return PhabricatorSpacesNamespacePHIDType::TYPECONST; } - public function getApplicationTransactionCommentObject() { - return null; - } - public function getBaseTransactionClass() { return 'PhabricatorSpacesNamespaceTransactionType'; } diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index 91825eb73d..216c1e00a2 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -360,12 +360,7 @@ abstract class PhabricatorApplicationTransactionEditor } if ($template) { - try { - $comment = $template->getApplicationTransactionCommentObject(); - } catch (PhutilMethodNotImplementedException $ex) { - $comment = null; - } - + $comment = $template->getApplicationTransactionCommentObject(); if ($comment) { $types[] = PhabricatorTransactions::TYPE_COMMENT; } diff --git a/src/applications/transactions/engineextension/PhabricatorCommentEditEngineExtension.php b/src/applications/transactions/engineextension/PhabricatorCommentEditEngineExtension.php index 0d20533798..7d80082cb2 100644 --- a/src/applications/transactions/engineextension/PhabricatorCommentEditEngineExtension.php +++ b/src/applications/transactions/engineextension/PhabricatorCommentEditEngineExtension.php @@ -23,12 +23,7 @@ final class PhabricatorCommentEditEngineExtension PhabricatorApplicationTransactionInterface $object) { $xaction = $object->getApplicationTransactionTemplate(); - - try { - $comment = $xaction->getApplicationTransactionCommentObject(); - } catch (PhutilMethodNotImplementedException $ex) { - $comment = null; - } + $comment = $xaction->getApplicationTransactionCommentObject(); return (bool)$comment; } diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php index 6d047fc823..acfc4d0cfa 100644 --- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php +++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php @@ -76,7 +76,7 @@ abstract class PhabricatorApplicationTransaction } public function getApplicationTransactionCommentObject() { - throw new PhutilMethodNotImplementedException(); + return null; } public function getMetadataValue($key, $default = null) { @@ -1731,12 +1731,7 @@ abstract class PhabricatorApplicationTransaction PhabricatorDestructionEngine $engine) { $this->openTransaction(); - $comment_template = null; - try { - $comment_template = $this->getApplicationTransactionCommentObject(); - } catch (Exception $ex) { - // Continue; no comments for these transactions. - } + $comment_template = $this->getApplicationTransactionCommentObject(); if ($comment_template) { $comments = $comment_template->loadAllWhere( diff --git a/src/applications/transactions/storage/PhabricatorEditEngineConfigurationTransaction.php b/src/applications/transactions/storage/PhabricatorEditEngineConfigurationTransaction.php index a1c44cc003..8cf7fe5b48 100644 --- a/src/applications/transactions/storage/PhabricatorEditEngineConfigurationTransaction.php +++ b/src/applications/transactions/storage/PhabricatorEditEngineConfigurationTransaction.php @@ -23,10 +23,6 @@ final class PhabricatorEditEngineConfigurationTransaction return PhabricatorEditEngineConfigurationPHIDType::TYPECONST; } - public function getApplicationTransactionCommentObject() { - return null; - } - public function getTitle() { $author_phid = $this->getAuthorPHID(); From f0364eef8af2c3ce8a527bba677d343ed9e704bf Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 6 Feb 2019 10:28:56 -0800 Subject: [PATCH 023/245] Remove weird integration between Legalpad and the ExternalAccount table Summary: Depends on D20107. Ref T6703. Legalpad currently inserts "email" records into the external account table, but they're never used for anything and nothing else references them. They also aren't necessary for anything important to work, and the only effect they have is making the UI say "External Account" instead of "None" under the "Account" column. In particular, the signatures still record the actual email address. Stop doing this, remove all the references, and destroy all the rows. (Long ago, Maniphest may also have done this, but no longer does. Nuance/Gatekeeper use a more modern and more suitable "ExternalObject" thing that I initially started adapting here before realizing that Legalpad doesn't actually care about this data.) Test Plan: Signed documents with an email address, saw signature reflected properly in UI. Grepped for other callsites. Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T6703 Differential Revision: https://secure.phabricator.com/D20108 --- .../20190206.external.01.legalpad.sql | 2 ++ .../20190206.external.02.email.sql | 2 ++ .../query/PhabricatorExternalAccountQuery.php | 31 ------------------- .../LegalpadDocumentSignController.php | 10 ------ .../LegalpadDocumentSignatureSearchEngine.php | 2 +- 5 files changed, 5 insertions(+), 42 deletions(-) create mode 100644 resources/sql/autopatches/20190206.external.01.legalpad.sql create mode 100644 resources/sql/autopatches/20190206.external.02.email.sql diff --git a/resources/sql/autopatches/20190206.external.01.legalpad.sql b/resources/sql/autopatches/20190206.external.01.legalpad.sql new file mode 100644 index 0000000000..8afa9dd9ff --- /dev/null +++ b/resources/sql/autopatches/20190206.external.01.legalpad.sql @@ -0,0 +1,2 @@ +UPDATE {$NAMESPACE}_legalpad.legalpad_documentsignature + SET signerPHID = NULL WHERE signerPHID LIKE 'PHID-XUSR-%'; diff --git a/resources/sql/autopatches/20190206.external.02.email.sql b/resources/sql/autopatches/20190206.external.02.email.sql new file mode 100644 index 0000000000..14f5f4791f --- /dev/null +++ b/resources/sql/autopatches/20190206.external.02.email.sql @@ -0,0 +1,2 @@ +DELETE FROM {$NAMESPACE}_user.user_externalaccount + WHERE accountType = 'email'; diff --git a/src/applications/auth/query/PhabricatorExternalAccountQuery.php b/src/applications/auth/query/PhabricatorExternalAccountQuery.php index b34199ce60..c4a53c12f8 100644 --- a/src/applications/auth/query/PhabricatorExternalAccountQuery.php +++ b/src/applications/auth/query/PhabricatorExternalAccountQuery.php @@ -168,35 +168,4 @@ final class PhabricatorExternalAccountQuery return 'PhabricatorPeopleApplication'; } - /** - * Attempts to find an external account and if none exists creates a new - * external account with a shiny new ID and PHID. - * - * NOTE: This function assumes the first item in various query parameters is - * the correct value to use in creating a new external account. - */ - public function loadOneOrCreate() { - $account = $this->executeOne(); - if (!$account) { - $account = new PhabricatorExternalAccount(); - if ($this->accountIDs) { - $account->setAccountID(reset($this->accountIDs)); - } - if ($this->accountTypes) { - $account->setAccountType(reset($this->accountTypes)); - } - if ($this->accountDomains) { - $account->setAccountDomain(reset($this->accountDomains)); - } - if ($this->accountSecrets) { - $account->setAccountSecret(reset($this->accountSecrets)); - } - if ($this->userPHIDs) { - $account->setUserPHID(reset($this->userPHIDs)); - } - $account->save(); - } - return $account; - } - } diff --git a/src/applications/legalpad/controller/LegalpadDocumentSignController.php b/src/applications/legalpad/controller/LegalpadDocumentSignController.php index ab98c0bb78..f09d95af29 100644 --- a/src/applications/legalpad/controller/LegalpadDocumentSignController.php +++ b/src/applications/legalpad/controller/LegalpadDocumentSignController.php @@ -364,16 +364,6 @@ final class LegalpadDocumentSignController extends LegalpadController { if ($email_obj) { return $this->signInResponse(); } - $external_account = id(new PhabricatorExternalAccountQuery()) - ->setViewer($viewer) - ->withAccountTypes(array('email')) - ->withAccountDomains(array($email->getDomainName())) - ->withAccountIDs(array($email->getAddress())) - ->loadOneOrCreate(); - if ($external_account->getUserPHID()) { - return $this->signInResponse(); - } - $signer_phid = $external_account->getPHID(); } } break; diff --git a/src/applications/legalpad/query/LegalpadDocumentSignatureSearchEngine.php b/src/applications/legalpad/query/LegalpadDocumentSignatureSearchEngine.php index 9df8d2478d..ea14fd4a2f 100644 --- a/src/applications/legalpad/query/LegalpadDocumentSignatureSearchEngine.php +++ b/src/applications/legalpad/query/LegalpadDocumentSignatureSearchEngine.php @@ -226,7 +226,7 @@ final class LegalpadDocumentSignatureSearchEngine $handles[$document->getPHID()]->renderLink(), $signer_phid ? $handles[$signer_phid]->renderLink() - : null, + : phutil_tag('em', array(), pht('None')), $name, phutil_tag( 'a', From 8054f7d19156d6a9c336a1f99a5f0e289bc4f3b1 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 6 Feb 2019 12:45:02 -0800 Subject: [PATCH 024/245] Convert a manual query against external accounts into a modern Query Summary: Depends on D20108. Ref T6703. Update this outdated callsite to a more modern appraoch. Test Plan: Destroyed a user with `bin/remove destroy`. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T6703 Differential Revision: https://secure.phabricator.com/D20109 --- src/applications/people/storage/PhabricatorUser.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php index 70d2d9fb4e..055df8b79e 100644 --- a/src/applications/people/storage/PhabricatorUser.php +++ b/src/applications/people/storage/PhabricatorUser.php @@ -1131,9 +1131,10 @@ final class PhabricatorUser $this->openTransaction(); $this->delete(); - $externals = id(new PhabricatorExternalAccount())->loadAllWhere( - 'userPHID = %s', - $this->getPHID()); + $externals = id(new PhabricatorExternalAccountQuery()) + ->setViewer($engine->getViewer()) + ->withUserPHIDs(array($this->getPHID())) + ->execute(); foreach ($externals as $external) { $external->delete(); } From 949afb02fd19b376d219bb6be8539ec0f9329e99 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 6 Feb 2019 20:53:25 -0800 Subject: [PATCH 025/245] On login forms, autofocus the "username" field Summary: Depends on D20120. Fixes T8907. I thought this needed some Javascript nonsense but Safari, Firefox and Chrome all support an `autofocus` attribute. Test Plan: Loaded login page with password auth enabled in Safari, Firefox, and Chrome; saw username field automatically gain focus. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T8907 Differential Revision: https://secure.phabricator.com/D20122 --- .../auth/provider/PhabricatorLDAPAuthProvider.php | 1 + .../auth/provider/PhabricatorPasswordAuthProvider.php | 1 + src/view/form/control/AphrontFormTextControl.php | 11 +++++++++++ 3 files changed, 13 insertions(+) diff --git a/src/applications/auth/provider/PhabricatorLDAPAuthProvider.php b/src/applications/auth/provider/PhabricatorLDAPAuthProvider.php index 44b58b85ff..4a4babcc12 100644 --- a/src/applications/auth/provider/PhabricatorLDAPAuthProvider.php +++ b/src/applications/auth/provider/PhabricatorLDAPAuthProvider.php @@ -112,6 +112,7 @@ final class PhabricatorLDAPAuthProvider extends PhabricatorAuthProvider { id(new AphrontFormTextControl()) ->setLabel(pht('LDAP Username')) ->setName('ldap_username') + ->setAutofocus(true) ->setValue($v_user) ->setError($e_user)) ->appendChild( diff --git a/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php b/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php index d841f091aa..ec5720e078 100644 --- a/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php +++ b/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php @@ -229,6 +229,7 @@ final class PhabricatorPasswordAuthProvider extends PhabricatorAuthProvider { id(new AphrontFormTextControl()) ->setLabel(pht('Username or Email')) ->setName('username') + ->setAutofocus(true) ->setValue($v_user) ->setError($e_user)) ->appendChild( diff --git a/src/view/form/control/AphrontFormTextControl.php b/src/view/form/control/AphrontFormTextControl.php index 581f22682d..f7fd117cfd 100644 --- a/src/view/form/control/AphrontFormTextControl.php +++ b/src/view/form/control/AphrontFormTextControl.php @@ -5,6 +5,7 @@ final class AphrontFormTextControl extends AphrontFormControl { private $disableAutocomplete; private $sigil; private $placeholder; + private $autofocus; public function setDisableAutocomplete($disable) { $this->disableAutocomplete = $disable; @@ -24,6 +25,15 @@ final class AphrontFormTextControl extends AphrontFormControl { return $this; } + public function setAutofocus($autofocus) { + $this->autofocus = $autofocus; + return $this; + } + + public function getAutofocus() { + return $this->autofocus; + } + public function getSigil() { return $this->sigil; } @@ -49,6 +59,7 @@ final class AphrontFormTextControl extends AphrontFormControl { 'id' => $this->getID(), 'sigil' => $this->getSigil(), 'placeholder' => $this->getPlaceholder(), + 'autofocus' => ($this->getAutofocus() ? 'autofocus' : null), )); } From 7469075a8315c7709aad87ccec7cb284d4f71c6e Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 7 Feb 2019 07:06:16 -0800 Subject: [PATCH 026/245] Allow users to be approved from the profile "Manage" page, alongside other similar actions Summary: Depends on D20122. Fixes T8029. Adds an "Approve User" action to the "Manage" page. Users are normally approved from the "Approval Queue", but if you click into a user's profile to check them out in more detail it kind of dead ends you right now. I've occasionally hit this myself, and think this workflow is generally reasonable enough to support upstream. Test Plan: {F6193742} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T8029 Differential Revision: https://secure.phabricator.com/D20123 --- resources/celerity/map.php | 6 +- .../PhabricatorPeopleApplication.php | 3 +- .../PhabricatorPeopleApproveController.php | 10 ++- .../PhabricatorPeopleProfileController.php | 61 +++++++++++-------- ...abricatorPeopleProfileManageController.php | 31 +++++++--- src/view/layout/PhabricatorActionView.php | 1 + webroot/rsrc/css/phui/phui-action-list.css | 17 ++++++ 7 files changed, 93 insertions(+), 36 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 51bc9338d2..537d1f46dc 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,7 +9,7 @@ return array( 'names' => array( 'conpherence.pkg.css' => '3c8a0668', 'conpherence.pkg.js' => '020aebcf', - 'core.pkg.css' => 'e0cb8094', + 'core.pkg.css' => 'eab5ccaf', 'core.pkg.js' => '5c737607', 'differential.pkg.css' => 'b8df73d4', 'differential.pkg.js' => '67c9ea4c', @@ -133,7 +133,7 @@ return array( 'rsrc/css/phui/object-item/phui-oi-flush-ui.css' => '490e2e2e', 'rsrc/css/phui/object-item/phui-oi-list-view.css' => '909f3844', 'rsrc/css/phui/object-item/phui-oi-simple-ui.css' => '6a30fa46', - 'rsrc/css/phui/phui-action-list.css' => 'c1a7631d', + 'rsrc/css/phui/phui-action-list.css' => 'c4972757', 'rsrc/css/phui/phui-action-panel.css' => '6c386cbf', 'rsrc/css/phui/phui-badge.css' => '666e25ad', 'rsrc/css/phui/phui-basic-nav-view.css' => '56ebd66d', @@ -740,7 +740,7 @@ return array( 'path-typeahead' => 'ad486db3', 'people-picture-menu-item-css' => 'fe8e07cf', 'people-profile-css' => '2ea2daa1', - 'phabricator-action-list-view-css' => 'c1a7631d', + 'phabricator-action-list-view-css' => 'c4972757', 'phabricator-busy' => '5202e831', 'phabricator-chatlog-css' => 'abdc76ee', 'phabricator-content-source-view-css' => 'cdf0d579', diff --git a/src/applications/people/application/PhabricatorPeopleApplication.php b/src/applications/people/application/PhabricatorPeopleApplication.php index 9238d8da3b..06740be26e 100644 --- a/src/applications/people/application/PhabricatorPeopleApplication.php +++ b/src/applications/people/application/PhabricatorPeopleApplication.php @@ -51,7 +51,8 @@ final class PhabricatorPeopleApplication extends PhabricatorApplication { 'send/' => 'PhabricatorPeopleInviteSendController', ), - 'approve/(?P[1-9]\d*)/' => 'PhabricatorPeopleApproveController', + 'approve/(?P[1-9]\d*)/(?:via/(?P[^/]+)/)?' + => 'PhabricatorPeopleApproveController', '(?Pdisapprove)/(?P[1-9]\d*)/' => 'PhabricatorPeopleDisableController', '(?Pdisable)/(?P[1-9]\d*)/' diff --git a/src/applications/people/controller/PhabricatorPeopleApproveController.php b/src/applications/people/controller/PhabricatorPeopleApproveController.php index 013f4371f6..af08a6fbdc 100644 --- a/src/applications/people/controller/PhabricatorPeopleApproveController.php +++ b/src/applications/people/controller/PhabricatorPeopleApproveController.php @@ -14,7 +14,15 @@ final class PhabricatorPeopleApproveController return new Aphront404Response(); } - $done_uri = $this->getApplicationURI('query/approval/'); + $via = $request->getURIData('via'); + switch ($via) { + case 'profile': + $done_uri = urisprintf('/people/manage/%d/', $user->getID()); + break; + default: + $done_uri = $this->getApplicationURI('query/approval/'); + break; + } if ($user->getIsApproved()) { return $this->newDialog() diff --git a/src/applications/people/controller/PhabricatorPeopleProfileController.php b/src/applications/people/controller/PhabricatorPeopleProfileController.php index 902b21efcc..91afda123b 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfileController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfileController.php @@ -70,40 +70,53 @@ abstract class PhabricatorPeopleProfileController $profile_icon = PhabricatorPeopleIconSet::getIconIcon($profile->getIcon()); $profile_title = $profile->getDisplayTitle(); - $roles = array(); + + $tag = id(new PHUITagView()) + ->setType(PHUITagView::TYPE_SHADE); + + $tags = array(); if ($user->getIsAdmin()) { - $roles[] = pht('Administrator'); - } - if ($user->getIsDisabled()) { - $roles[] = pht('Disabled'); - } - if (!$user->getIsApproved()) { - $roles[] = pht('Not Approved'); - } - if ($user->getIsSystemAgent()) { - $roles[] = pht('Bot'); - } - if ($user->getIsMailingList()) { - $roles[] = pht('Mailing List'); - } - if (!$user->getIsEmailVerified()) { - $roles[] = pht('Email Not Verified'); + $tags[] = id(clone $tag) + ->setName(pht('Administrator')) + ->setColor('blue'); } - $tag = null; - if ($roles) { - $tag = id(new PHUITagView()) - ->setName(implode(', ', $roles)) - ->addClass('project-view-header-tag') - ->setType(PHUITagView::TYPE_SHADE); + // "Disabled" gets a stronger status tag below. + + if (!$user->getIsApproved()) { + $tags[] = id(clone $tag) + ->setName('Not Approved') + ->setColor('yellow'); + } + + if ($user->getIsSystemAgent()) { + $tags[] = id(clone $tag) + ->setName(pht('Bot')) + ->setColor('orange'); + } + + if ($user->getIsMailingList()) { + $tags[] = id(clone $tag) + ->setName(pht('Mailing List')) + ->setColor('orange'); + } + + if (!$user->getIsEmailVerified()) { + $tags[] = id(clone $tag) + ->setName(pht('Email Not Verified')) + ->setColor('violet'); } $header = id(new PHUIHeaderView()) - ->setHeader(array($user->getFullName(), $tag)) + ->setHeader($user->getFullName()) ->setImage($picture) ->setProfileHeader(true) ->addClass('people-profile-header'); + foreach ($tags as $tag) { + $header->addTag($tag); + } + require_celerity_resource('project-view-css'); if ($user->getIsDisabled()) { diff --git a/src/applications/people/controller/PhabricatorPeopleProfileManageController.php b/src/applications/people/controller/PhabricatorPeopleProfileManageController.php index e9faae3d62..835935f775 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfileManageController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfileManageController.php @@ -92,6 +92,8 @@ final class PhabricatorPeopleProfileManageController PeopleDisableUsersCapability::CAPABILITY); $can_disable = ($has_disable && !$is_self); + $id = $user->getID(); + $welcome_engine = id(new PhabricatorPeopleWelcomeMailEngine()) ->setSender($viewer) ->setRecipient($user); @@ -103,7 +105,7 @@ final class PhabricatorPeopleProfileManageController id(new PhabricatorActionView()) ->setIcon('fa-pencil') ->setName(pht('Edit Profile')) - ->setHref($this->getApplicationURI('editprofile/'.$user->getID().'/')) + ->setHref($this->getApplicationURI('editprofile/'.$id.'/')) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); @@ -111,7 +113,7 @@ final class PhabricatorPeopleProfileManageController id(new PhabricatorActionView()) ->setIcon('fa-picture-o') ->setName(pht('Edit Profile Picture')) - ->setHref($this->getApplicationURI('picture/'.$user->getID().'/')) + ->setHref($this->getApplicationURI('picture/'.$id.'/')) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); @@ -137,7 +139,7 @@ final class PhabricatorPeopleProfileManageController ->setName($empower_name) ->setDisabled(!$can_admin) ->setWorkflow(true) - ->setHref($this->getApplicationURI('empower/'.$user->getID().'/'))); + ->setHref($this->getApplicationURI('empower/'.$id.'/'))); $curtain->addAction( id(new PhabricatorActionView()) @@ -145,7 +147,7 @@ final class PhabricatorPeopleProfileManageController ->setName(pht('Change Username')) ->setDisabled(!$is_admin) ->setWorkflow(true) - ->setHref($this->getApplicationURI('rename/'.$user->getID().'/'))); + ->setHref($this->getApplicationURI('rename/'.$id.'/'))); if ($user->getIsDisabled()) { $disable_icon = 'fa-check-circle-o'; @@ -161,19 +163,34 @@ final class PhabricatorPeopleProfileManageController ->setName(pht('Send Welcome Email')) ->setWorkflow(true) ->setDisabled(!$can_welcome) - ->setHref($this->getApplicationURI('welcome/'.$user->getID().'/'))); + ->setHref($this->getApplicationURI('welcome/'.$id.'/'))); $curtain->addAction( id(new PhabricatorActionView()) ->setType(PhabricatorActionView::TYPE_DIVIDER)); + if (!$user->getIsApproved()) { + $approve_action = id(new PhabricatorActionView()) + ->setIcon('fa-thumbs-up') + ->setName(pht('Approve User')) + ->setWorkflow(true) + ->setDisabled(!$is_admin) + ->setHref("/people/approve/{$id}/via/profile/"); + + if ($is_admin) { + $approve_action->setColor(PhabricatorActionView::GREEN); + } + + $curtain->addAction($approve_action); + } + $curtain->addAction( id(new PhabricatorActionView()) ->setIcon($disable_icon) ->setName($disable_name) ->setDisabled(!$can_disable) ->setWorkflow(true) - ->setHref($this->getApplicationURI('disable/'.$user->getID().'/'))); + ->setHref($this->getApplicationURI('disable/'.$id.'/'))); $curtain->addAction( id(new PhabricatorActionView()) @@ -181,7 +198,7 @@ final class PhabricatorPeopleProfileManageController ->setName(pht('Delete User')) ->setDisabled(!$can_admin) ->setWorkflow(true) - ->setHref($this->getApplicationURI('delete/'.$user->getID().'/'))); + ->setHref($this->getApplicationURI('delete/'.$id.'/'))); $curtain->addAction( id(new PhabricatorActionView()) diff --git a/src/view/layout/PhabricatorActionView.php b/src/view/layout/PhabricatorActionView.php index a1d8fe2664..3de60a2374 100644 --- a/src/view/layout/PhabricatorActionView.php +++ b/src/view/layout/PhabricatorActionView.php @@ -25,6 +25,7 @@ final class PhabricatorActionView extends AphrontView { const TYPE_DIVIDER = 'type-divider'; const TYPE_LABEL = 'label'; const RED = 'action-item-red'; + const GREEN = 'action-item-green'; public function setSelected($selected) { $this->selected = $selected; diff --git a/webroot/rsrc/css/phui/phui-action-list.css b/webroot/rsrc/css/phui/phui-action-list.css index e7ee38a8bf..3df4ff1b78 100644 --- a/webroot/rsrc/css/phui/phui-action-list.css +++ b/webroot/rsrc/css/phui/phui-action-list.css @@ -99,11 +99,20 @@ background-color: {$sh-redbackground}; } +.phabricator-action-view.action-item-green { + background-color: {$sh-greenbackground}; +} + .phabricator-action-view.action-item-red .phabricator-action-view-item, .phabricator-action-view.action-item-red .phabricator-action-view-icon { color: {$sh-redtext}; } +.phabricator-action-view.action-item-green .phabricator-action-view-item, +.phabricator-action-view.action-item-green .phabricator-action-view-icon { + color: {$sh-greentext}; +} + .device-desktop .phabricator-action-view.action-item-red:hover .phabricator-action-view-item, .device-desktop .phabricator-action-view.action-item-red:hover @@ -111,6 +120,14 @@ color: {$red}; } +.device-desktop .phabricator-action-view.action-item-green:hover + .phabricator-action-view-item, +.device-desktop .phabricator-action-view.action-item-green:hover + .phabricator-action-view-icon { + color: {$green}; +} + + .phabricator-action-view-label .phabricator-action-view-item, .phabricator-action-view-type-label .phabricator-action-view-item { font-size: {$smallerfontsize}; From a4bab60ad0aeb002277b52dcd5b67c286819e3e1 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 6 Feb 2019 18:11:36 -0800 Subject: [PATCH 027/245] Don't show "registration might be too open" warnings unless an auth provider actually allows registration Summary: Depends on D20118. Fixes T5351. We possibly raise some warnings about registration (approval queue, email domains), but they aren't relevant if no one can register. Hide these warnings if no providers actually support registration. Test Plan: Viewed the Auth provider list with registration providers and with no registration providers, saw more tailored guidance. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T5351 Differential Revision: https://secure.phabricator.com/D20119 --- ...orAuthProvidersGuidanceEngineExtension.php | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/applications/auth/guidance/PhabricatorAuthProvidersGuidanceEngineExtension.php b/src/applications/auth/guidance/PhabricatorAuthProvidersGuidanceEngineExtension.php index ac3fe4d309..d1f67393ca 100644 --- a/src/applications/auth/guidance/PhabricatorAuthProvidersGuidanceEngineExtension.php +++ b/src/applications/auth/guidance/PhabricatorAuthProvidersGuidanceEngineExtension.php @@ -10,6 +10,26 @@ final class PhabricatorAuthProvidersGuidanceEngineExtension } public function generateGuidance(PhabricatorGuidanceContext $context) { + $configs = id(new PhabricatorAuthProviderConfigQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withIsEnabled(true) + ->execute(); + + $allows_registration = false; + foreach ($configs as $config) { + $provider = $config->getProvider(); + if ($provider->shouldAllowRegistration()) { + $allows_registration = true; + break; + } + } + + // If no provider allows registration, we don't need provide any warnings + // about registration being too open. + if (!$allows_registration) { + return array(); + } + $domains_key = 'auth.email-domains'; $domains_link = $this->renderConfigLink($domains_key); $domains_value = PhabricatorEnv::getEnvConfig($domains_key); From 509fbb6c20e24374506196ffa14bed143f0bc88f Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 7 Feb 2019 11:44:48 -0800 Subject: [PATCH 028/245] When building audit queries, prefilter possible "authorPHID" values Summary: Ref T13244. See PHI1057. Currently, if you're a member of a lot of projects/packages, you can end up with a very large `commit.authorPHID IN (...)` clause in part of the "Active Audits" query, since your `alice` token in "Responsible Users: alice" expands into every package and project you can audit on behalf of. It's impossible for a commit to be authored by anything but a user, and evidence in PHI1057 suggests this giant `IN (...)` list can prevent MySQL from making effective utilization of the `` key on the table. Prefilter the list of PHIDs to only PHIDs which can possibly author a commit. (We'll also eventually need to convert the `authorPHIDs` into `identityPHIDs` anyway, for T12164, and this moves us slightly toward that.) Test Plan: Loaded "Active Audits" before and after change, saw a more streamlined and sensible `authorPHID IN (...)` clause afterwards. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13244 Differential Revision: https://secure.phabricator.com/D20129 --- .../diffusion/query/DiffusionCommitQuery.php | 51 ++++++++++++++++--- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/src/applications/diffusion/query/DiffusionCommitQuery.php b/src/applications/diffusion/query/DiffusionCommitQuery.php index 05072e07c7..03eb0b9edf 100644 --- a/src/applications/diffusion/query/DiffusionCommitQuery.php +++ b/src/applications/diffusion/query/DiffusionCommitQuery.php @@ -202,6 +202,7 @@ final class DiffusionCommitQuery $table = $this->newResultObject(); $conn = $table->establishConnection('r'); + $empty_exception = null; $subqueries = array(); if ($this->responsiblePHIDs) { $base_authors = $this->authorPHIDs; @@ -222,21 +223,33 @@ final class DiffusionCommitQuery $this->authorPHIDs = $all_authors; $this->auditorPHIDs = $base_auditors; - $subqueries[] = $this->buildStandardPageQuery( - $conn, - $table->getTableName()); + try { + $subqueries[] = $this->buildStandardPageQuery( + $conn, + $table->getTableName()); + } catch (PhabricatorEmptyQueryException $ex) { + $empty_exception = $ex; + } $this->authorPHIDs = $base_authors; $this->auditorPHIDs = $all_auditors; - $subqueries[] = $this->buildStandardPageQuery( - $conn, - $table->getTableName()); + try { + $subqueries[] = $this->buildStandardPageQuery( + $conn, + $table->getTableName()); + } catch (PhabricatorEmptyQueryException $ex) { + $empty_exception = $ex; + } } else { $subqueries[] = $this->buildStandardPageQuery( $conn, $table->getTableName()); } + if (!$subqueries) { + throw $empty_exception; + } + if (count($subqueries) > 1) { $unions = null; foreach ($subqueries as $subquery) { @@ -642,10 +655,19 @@ final class DiffusionCommitQuery } if ($this->authorPHIDs !== null) { + $author_phids = $this->authorPHIDs; + if ($author_phids) { + $author_phids = $this->selectPossibleAuthors($author_phids); + if (!$author_phids) { + throw new PhabricatorEmptyQueryException( + pht('Author PHIDs contain no possible authors.')); + } + } + $where[] = qsprintf( $conn, 'commit.authorPHID IN (%Ls)', - $this->authorPHIDs); + $author_phids); } if ($this->epochMin !== null) { @@ -934,5 +956,20 @@ final class DiffusionCommitQuery ) + $parent; } + private function selectPossibleAuthors(array $phids) { + // See PHI1057. Select PHIDs which might possibly be commit authors from + // a larger list of PHIDs. This primarily filters out packages and projects + // from "Responsible Users: ..." queries. Our goal in performing this + // filtering is to improve the performance of the final query. + + foreach ($phids as $key => $phid) { + if (phid_get_type($phid) !== PhabricatorPeopleUserPHIDType::TYPECONST) { + unset($phids[$key]); + } + } + + return $phids; + } + } From 8fab8d8a18ef301d780af44dc920cbcfc50ff083 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 7 Feb 2019 08:00:43 -0800 Subject: [PATCH 029/245] Prepare owners package audit rules to become more flexible Summary: Ref T13244. See PHI1055. (Earlier, see D20091 and PHI1047.) Previously, we expanded the Owners package autoreview rules from "Yes/No" to several "Review (Blocking) If Non-Owner Author Not Subscribed via Package" kinds of rules. The sky didn't fall and this feature didn't turn into "Herald-in-Owners", so I'm comfortable doing something similar to the "Audit" rules. PHI1055 is a request for a way to configure slightly different audit behavior, and expanding the options seems like a good approach to satisfy the use case. Prepare to add more options by moving everything into a class that defines all the behavior of different states, and converting the "0/1" boolean column to a text column. Test Plan: - Created several packages, some with and some without auditing. - Inspected database for: package state; and associated transactions. - Ran the migrations. - Inspected database to confirm that state and transactions migrated correctly. - Reviewed transaction logs. - Created and edited packages and audit state. - Viewed the "Package List" element in Diffusion. - Pulled package information with `owners.search`, got sensible results. - Edited package audit status with `owners.edit`. Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13244 Differential Revision: https://secure.phabricator.com/D20124 --- .../20190207.packages.01.state.sql | 2 + .../20190207.packages.02.migrate.sql | 2 + .../autopatches/20190207.packages.03.drop.sql | 2 + .../20190207.packages.04.xactions.php | 41 +++++++ src/__phutil_library_map__.php | 2 + .../controller/DiffusionBrowseController.php | 7 +- .../constants/PhabricatorOwnersAuditRule.php | 101 ++++++++++++++++++ .../PhabricatorOwnersDetailController.php | 8 +- .../PhabricatorOwnersPackageEditEngine.php | 6 +- .../storage/PhabricatorOwnersPackage.php | 28 ++--- ...icatorOwnersPackageAuditingTransaction.php | 48 +++------ ...habricatorRepositoryCommitOwnersWorker.php | 3 +- 12 files changed, 181 insertions(+), 69 deletions(-) create mode 100644 resources/sql/autopatches/20190207.packages.01.state.sql create mode 100644 resources/sql/autopatches/20190207.packages.02.migrate.sql create mode 100644 resources/sql/autopatches/20190207.packages.03.drop.sql create mode 100644 resources/sql/autopatches/20190207.packages.04.xactions.php create mode 100644 src/applications/owners/constants/PhabricatorOwnersAuditRule.php diff --git a/resources/sql/autopatches/20190207.packages.01.state.sql b/resources/sql/autopatches/20190207.packages.01.state.sql new file mode 100644 index 0000000000..0e74f269ba --- /dev/null +++ b/resources/sql/autopatches/20190207.packages.01.state.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_owners.owners_package + ADD auditingState VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20190207.packages.02.migrate.sql b/resources/sql/autopatches/20190207.packages.02.migrate.sql new file mode 100644 index 0000000000..60bf364ac1 --- /dev/null +++ b/resources/sql/autopatches/20190207.packages.02.migrate.sql @@ -0,0 +1,2 @@ +UPDATE {$NAMESPACE}_owners.owners_package + SET auditingState = IF(auditingEnabled = 0, 'none', 'audit'); diff --git a/resources/sql/autopatches/20190207.packages.03.drop.sql b/resources/sql/autopatches/20190207.packages.03.drop.sql new file mode 100644 index 0000000000..24d0ce1a4f --- /dev/null +++ b/resources/sql/autopatches/20190207.packages.03.drop.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_owners.owners_package + DROP auditingEnabled; diff --git a/resources/sql/autopatches/20190207.packages.04.xactions.php b/resources/sql/autopatches/20190207.packages.04.xactions.php new file mode 100644 index 0000000000..5a8609166e --- /dev/null +++ b/resources/sql/autopatches/20190207.packages.04.xactions.php @@ -0,0 +1,41 @@ +establishConnection('w'); +$iterator = new LiskRawMigrationIterator($conn, $table->getTableName()); + +// Migrate "Auditing State" transactions for Owners Packages from old values +// (which were "0" or "1", as JSON integer literals, without quotes) to new +// values (which are JSON strings, with quotes). + +foreach ($iterator as $row) { + if ($row['transactionType'] !== 'owners.auditing') { + continue; + } + + $old_value = (int)$row['oldValue']; + $new_value = (int)$row['newValue']; + + if (!$old_value) { + $old_value = 'none'; + } else { + $old_value = 'audit'; + } + + if (!$new_value) { + $new_value = 'none'; + } else { + $new_value = 'audit'; + } + + $old_value = phutil_json_encode($old_value); + $new_value = phutil_json_encode($new_value); + + queryfx( + $conn, + 'UPDATE %R SET oldValue = %s, newValue = %s WHERE id = %d', + $table, + $old_value, + $new_value, + $row['id']); +} diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index b9d468ea3c..4f9d7123c1 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -3668,6 +3668,7 @@ phutil_register_library_map(array( 'PhabricatorOwnerPathQuery' => 'applications/owners/query/PhabricatorOwnerPathQuery.php', 'PhabricatorOwnersApplication' => 'applications/owners/application/PhabricatorOwnersApplication.php', 'PhabricatorOwnersArchiveController' => 'applications/owners/controller/PhabricatorOwnersArchiveController.php', + 'PhabricatorOwnersAuditRule' => 'applications/owners/constants/PhabricatorOwnersAuditRule.php', 'PhabricatorOwnersConfigOptions' => 'applications/owners/config/PhabricatorOwnersConfigOptions.php', 'PhabricatorOwnersConfiguredCustomField' => 'applications/owners/customfield/PhabricatorOwnersConfiguredCustomField.php', 'PhabricatorOwnersController' => 'applications/owners/controller/PhabricatorOwnersController.php', @@ -9613,6 +9614,7 @@ phutil_register_library_map(array( 'PhabricatorOwnerPathQuery' => 'Phobject', 'PhabricatorOwnersApplication' => 'PhabricatorApplication', 'PhabricatorOwnersArchiveController' => 'PhabricatorOwnersController', + 'PhabricatorOwnersAuditRule' => 'Phobject', 'PhabricatorOwnersConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorOwnersConfiguredCustomField' => array( 'PhabricatorOwnersCustomField', diff --git a/src/applications/diffusion/controller/DiffusionBrowseController.php b/src/applications/diffusion/controller/DiffusionBrowseController.php index 54be7dd7f1..6a863a4a92 100644 --- a/src/applications/diffusion/controller/DiffusionBrowseController.php +++ b/src/applications/diffusion/controller/DiffusionBrowseController.php @@ -566,11 +566,8 @@ final class DiffusionBrowseController extends DiffusionController { $name = idx($spec, 'name', $auto); $item->addIcon('fa-code', $name); - if ($package->getAuditingEnabled()) { - $item->addIcon('fa-check', pht('Auditing Enabled')); - } else { - $item->addIcon('fa-ban', pht('No Auditing')); - } + $rule = $package->newAuditingRule(); + $item->addIcon($rule->getIconIcon(), $rule->getDisplayName()); if ($package->isArchived()) { $item->setDisabled(true); diff --git a/src/applications/owners/constants/PhabricatorOwnersAuditRule.php b/src/applications/owners/constants/PhabricatorOwnersAuditRule.php new file mode 100644 index 0000000000..1f8fd0b1bd --- /dev/null +++ b/src/applications/owners/constants/PhabricatorOwnersAuditRule.php @@ -0,0 +1,101 @@ +key = $key; + $rule->spec = $spec; + + return $rule; + } + + public function getKey() { + return $this->key; + } + + public function getDisplayName() { + return idx($this->spec, 'name', $this->key); + } + + public function getIconIcon() { + return idx($this->spec, 'icon.icon'); + } + + public static function newSelectControlMap() { + $specs = self::newSpecifications(); + return ipull($specs, 'name'); + } + + public static function getStorageValueFromAPIValue($value) { + $specs = self::newSpecifications(); + + $map = array(); + foreach ($specs as $key => $spec) { + $deprecated = idx($spec, 'deprecated', array()); + if (isset($deprecated[$value])) { + return $key; + } + } + + return $value; + } + + public static function getModernValueMap() { + $specs = self::newSpecifications(); + + $map = array(); + foreach ($specs as $key => $spec) { + $map[$key] = pht('"%s"', $key); + } + + return $map; + } + + public static function getDeprecatedValueMap() { + $specs = self::newSpecifications(); + + $map = array(); + foreach ($specs as $key => $spec) { + $deprecated_map = idx($spec, 'deprecated', array()); + foreach ($deprecated_map as $deprecated_key => $label) { + $map[$deprecated_key] = $label; + } + } + + return $map; + } + + private static function newSpecifications() { + return array( + self::AUDITING_NONE => array( + 'name' => pht('No Auditing'), + 'icon.icon' => 'fa-ban', + 'deprecated' => array( + '' => pht('"" (empty string)'), + '0' => '"0"', + ), + ), + self::AUDITING_AUDIT => array( + 'name' => pht('Audit Commits'), + 'icon.icon' => 'fa-check', + 'deprecated' => array( + '1' => '"1"', + ), + ), + ); + } + + + +} diff --git a/src/applications/owners/controller/PhabricatorOwnersDetailController.php b/src/applications/owners/controller/PhabricatorOwnersDetailController.php index f71009cf19..e28ae2b3bb 100644 --- a/src/applications/owners/controller/PhabricatorOwnersDetailController.php +++ b/src/applications/owners/controller/PhabricatorOwnersDetailController.php @@ -194,12 +194,8 @@ final class PhabricatorOwnersDetailController $name = idx($spec, 'name', $auto); $view->addProperty(pht('Auto Review'), $name); - if ($package->getAuditingEnabled()) { - $auditing = pht('Enabled'); - } else { - $auditing = pht('Disabled'); - } - $view->addProperty(pht('Auditing'), $auditing); + $rule = $package->newAuditingRule(); + $view->addProperty(pht('Auditing'), $rule->getDisplayName()); $ignored = $package->getIgnoredPathAttributes(); $ignored = array_keys($ignored); diff --git a/src/applications/owners/editor/PhabricatorOwnersPackageEditEngine.php b/src/applications/owners/editor/PhabricatorOwnersPackageEditEngine.php index c4ee026374..13f896d3f0 100644 --- a/src/applications/owners/editor/PhabricatorOwnersPackageEditEngine.php +++ b/src/applications/owners/editor/PhabricatorOwnersPackageEditEngine.php @@ -141,11 +141,7 @@ EOTEXT PhabricatorOwnersPackageAuditingTransaction::TRANSACTIONTYPE) ->setIsCopyable(true) ->setValue($object->getAuditingState()) - ->setOptions( - array( - PhabricatorOwnersPackage::AUDITING_NONE => pht('No Auditing'), - PhabricatorOwnersPackage::AUDITING_AUDIT => pht('Audit Commits'), - )), + ->setOptions(PhabricatorOwnersAuditRule::newSelectControlMap()), id(new PhabricatorRemarkupEditField()) ->setKey('description') ->setLabel(pht('Description')) diff --git a/src/applications/owners/storage/PhabricatorOwnersPackage.php b/src/applications/owners/storage/PhabricatorOwnersPackage.php index 207e0cb809..b9e91ef958 100644 --- a/src/applications/owners/storage/PhabricatorOwnersPackage.php +++ b/src/applications/owners/storage/PhabricatorOwnersPackage.php @@ -13,7 +13,6 @@ final class PhabricatorOwnersPackage PhabricatorNgramsInterface { protected $name; - protected $auditingEnabled; protected $autoReview; protected $description; protected $status; @@ -21,6 +20,7 @@ final class PhabricatorOwnersPackage protected $editPolicy; protected $dominion; protected $properties = array(); + protected $auditingState; private $paths = self::ATTACHABLE; private $owners = self::ATTACHABLE; @@ -38,9 +38,6 @@ final class PhabricatorOwnersPackage const AUTOREVIEW_BLOCK = 'block'; const AUTOREVIEW_BLOCK_ALWAYS = 'block-always'; - const AUDITING_NONE = 'none'; - const AUDITING_AUDIT = 'audit'; - const DOMINION_STRONG = 'strong'; const DOMINION_WEAK = 'weak'; @@ -58,7 +55,7 @@ final class PhabricatorOwnersPackage PhabricatorOwnersDefaultEditCapability::CAPABILITY); return id(new PhabricatorOwnersPackage()) - ->setAuditingEnabled(0) + ->setAuditingState(PhabricatorOwnersAuditRule::AUDITING_NONE) ->setAutoReview(self::AUTOREVIEW_NONE) ->setDominion(self::DOMINION_STRONG) ->setViewPolicy($view_policy) @@ -129,7 +126,7 @@ final class PhabricatorOwnersPackage self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'sort', 'description' => 'text', - 'auditingEnabled' => 'bool', + 'auditingState' => 'text32', 'status' => 'text32', 'autoReview' => 'text32', 'dominion' => 'text32', @@ -567,12 +564,8 @@ final class PhabricatorOwnersPackage return '/owners/package/'.$this->getID().'/'; } - public function getAuditingState() { - if ($this->getAuditingEnabled()) { - return self::AUDITING_AUDIT; - } else { - return self::AUDITING_NONE; - } + public function newAuditingRule() { + return PhabricatorOwnersAuditRule::newFromState($this->getAuditingState()); } /* -( PhabricatorPolicyInterface )----------------------------------------- */ @@ -731,16 +724,11 @@ final class PhabricatorOwnersPackage 'label' => $review_label, ); - $audit_value = $this->getAuditingState(); - if ($this->getAuditingEnabled()) { - $audit_label = pht('Auditing Enabled'); - } else { - $audit_label = pht('No Auditing'); - } + $audit_rule = $this->newAuditingRule(); $audit = array( - 'value' => $audit_value, - 'label' => $audit_label, + 'value' => $audit_rule->getKey(), + 'label' => $audit_rule->getDisplayName(), ); $dominion_value = $this->getDominion(); diff --git a/src/applications/owners/xaction/PhabricatorOwnersPackageAuditingTransaction.php b/src/applications/owners/xaction/PhabricatorOwnersPackageAuditingTransaction.php index d7ea7093f9..7c16c850fd 100644 --- a/src/applications/owners/xaction/PhabricatorOwnersPackageAuditingTransaction.php +++ b/src/applications/owners/xaction/PhabricatorOwnersPackageAuditingTransaction.php @@ -6,35 +6,29 @@ final class PhabricatorOwnersPackageAuditingTransaction const TRANSACTIONTYPE = 'owners.auditing'; public function generateOldValue($object) { - return (int)$object->getAuditingEnabled(); + return $object->getAuditingState(); } public function generateNewValue($object, $value) { - switch ($value) { - case PhabricatorOwnersPackage::AUDITING_AUDIT: - return 1; - case '1': - // TODO: Remove, deprecated. - return 1; - default: - return 0; - } + return PhabricatorOwnersAuditRule::getStorageValueFromAPIValue($value); } public function applyInternalEffects($object, $value) { - $object->setAuditingEnabled($value); + $object->setAuditingState($value); } public function getTitle() { - if ($this->getNewValue()) { - return pht( - '%s enabled auditing for this package.', - $this->renderAuthor()); - } else { - return pht( - '%s disabled auditing for this package.', - $this->renderAuthor()); - } + $old_value = $this->getOldValue(); + $new_value = $this->getNewValue(); + + $old_rule = PhabricatorOwnersAuditRule::newFromState($old_value); + $new_rule = PhabricatorOwnersAuditRule::newFromState($new_value); + + return pht( + '%s changed the audit rule for this package from %s to %s.', + $this->renderAuthor(), + $this->renderValue($old_rule->getDisplayName()), + $this->renderValue($new_rule->getDisplayName())); } public function validateTransactions($object, array $xactions) { @@ -43,18 +37,8 @@ final class PhabricatorOwnersPackageAuditingTransaction // See PHI1047. This transaction type accepted some weird stuff. Continue // supporting it for now, but move toward sensible consistency. - $modern_options = array( - PhabricatorOwnersPackage::AUDITING_NONE => - sprintf('"%s"', PhabricatorOwnersPackage::AUDITING_NONE), - PhabricatorOwnersPackage::AUDITING_AUDIT => - sprintf('"%s"', PhabricatorOwnersPackage::AUDITING_AUDIT), - ); - - $deprecated_options = array( - '0' => '"0"', - '1' => '"1"', - '' => pht('"" (empty string)'), - ); + $modern_options = PhabricatorOwnersAuditRule::getModernValueMap(); + $deprecated_options = PhabricatorOwnersAuditRule::getDeprecatedValueMap(); foreach ($xactions as $xaction) { $new_value = $xaction->getNewValue(); diff --git a/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php b/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php index 75ae0c9c14..219314c9d5 100644 --- a/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php +++ b/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php @@ -133,7 +133,8 @@ final class PhabricatorRepositoryCommitOwnersWorker $revision) { // Don't trigger an audit if auditing isn't enabled for the package. - if (!$package->getAuditingEnabled()) { + $rule = $package->newAuditingRule(); + if ($rule->getKey() === PhabricatorOwnersAuditRule::AUDITING_NONE) { return false; } From 31a0ed92d08d52adf88791e3f01da3b65121294f Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 7 Feb 2019 09:16:22 -0800 Subject: [PATCH 030/245] Support a wider range of "Audit" rules for Owners packages Summary: Depends on D20124. Ref T13244. See PHI1055. Add a few more builtin audit behaviors to make Owners more flexible. (At the upper end of flexibility you can trigger audits in a very granular way with Herald, but you tend to need to write one rule per Owners package, and providing a middle ground here has worked reasonably well for "review" rules so far.) Test Plan: - Edited a package to select the various different audit rules. - Used `bin/repository reparse --force --owners ` to trigger package audits under varied conditions. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13244 Differential Revision: https://secure.phabricator.com/D20126 --- .../constants/PhabricatorOwnersAuditRule.php | 22 ++++- ...habricatorRepositoryCommitOwnersWorker.php | 86 +++++++++++++++---- src/docs/user/userguide/owners.diviner | 16 ++-- 3 files changed, 100 insertions(+), 24 deletions(-) diff --git a/src/applications/owners/constants/PhabricatorOwnersAuditRule.php b/src/applications/owners/constants/PhabricatorOwnersAuditRule.php index 1f8fd0b1bd..32a9bb804b 100644 --- a/src/applications/owners/constants/PhabricatorOwnersAuditRule.php +++ b/src/applications/owners/constants/PhabricatorOwnersAuditRule.php @@ -4,7 +4,10 @@ final class PhabricatorOwnersAuditRule extends Phobject { const AUDITING_NONE = 'none'; - const AUDITING_AUDIT = 'audit'; + const AUDITING_NO_OWNER = 'audit'; + const AUDITING_UNREVIEWED = 'unreviewed'; + const AUDITING_NO_OWNER_AND_UNREVIEWED = 'uninvolved-unreviewed'; + const AUDITING_ALL = 'all'; private $key; private $spec; @@ -86,13 +89,26 @@ final class PhabricatorOwnersAuditRule '0' => '"0"', ), ), - self::AUDITING_AUDIT => array( - 'name' => pht('Audit Commits'), + self::AUDITING_UNREVIEWED => array( + 'name' => pht('Audit Unreviewed Commits'), + 'icon.icon' => 'fa-check', + ), + self::AUDITING_NO_OWNER => array( + 'name' => pht('Audit Commits With No Owner Involvement'), 'icon.icon' => 'fa-check', 'deprecated' => array( '1' => '"1"', ), ), + self::AUDITING_NO_OWNER_AND_UNREVIEWED => array( + 'name' => pht( + 'Audit Unreviewed Commits and Commits With No Owner Involvement'), + 'icon.icon' => 'fa-check', + ), + self::AUDITING_ALL => array( + 'name' => pht('Audit All Commits'), + 'icon.icon' => 'fa-check', + ), ); } diff --git a/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php b/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php index 219314c9d5..65434658ab 100644 --- a/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php +++ b/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php @@ -132,29 +132,85 @@ final class PhabricatorRepositoryCommitOwnersWorker $author_phid, $revision) { - // Don't trigger an audit if auditing isn't enabled for the package. + $audit_uninvolved = false; + $audit_unreviewed = false; + $rule = $package->newAuditingRule(); - if ($rule->getKey() === PhabricatorOwnersAuditRule::AUDITING_NONE) { - return false; + switch ($rule->getKey()) { + case PhabricatorOwnersAuditRule::AUDITING_NONE: + return false; + case PhabricatorOwnersAuditRule::AUDITING_ALL: + return true; + case PhabricatorOwnersAuditRule::AUDITING_NO_OWNER: + $audit_uninvolved = true; + break; + case PhabricatorOwnersAuditRule::AUDITING_UNREVIEWED: + $audit_unreviewed = true; + break; + case PhabricatorOwnersAuditRule::AUDITING_NO_OWNER_AND_UNREVIEWED: + $audit_uninvolved = true; + $audit_unreviewed = true; + break; } - // Trigger an audit if we don't recognize the commit's author. - if (!$author_phid) { - return true; + // If auditing is configured to trigger on unreviewed changes, check if + // the revision was "Accepted" when it landed. If not, trigger an audit. + if ($audit_unreviewed) { + $commit_unreviewed = true; + if ($revision) { + $was_accepted = DifferentialRevision::PROPERTY_CLOSED_FROM_ACCEPTED; + if ($revision->isPublished()) { + if ($revision->getProperty($was_accepted)) { + $commit_unreviewed = false; + } + } + } + + if ($commit_unreviewed) { + return true; + } } + // If auditing is configured to trigger on changes with no involved owner, + // check for an owner. If we don't find one, trigger an audit. + if ($audit_uninvolved) { + $commit_uninvolved = $this->isOwnerInvolved( + $commit, + $package, + $author_phid, + $revision); + if ($commit_uninvolved) { + return true; + } + } + + // We can't find any reason to trigger an audit for this commit. + return false; + } + + private function isOwnerInvolved( + PhabricatorRepositoryCommit $commit, + PhabricatorOwnersPackage $package, + $author_phid, + $revision) { + $owner_phids = PhabricatorOwnersOwner::loadAffiliatedUserPHIDs( array( $package->getID(), )); $owner_phids = array_fuse($owner_phids); - // Don't trigger an audit if the author is a package owner. - if (isset($owner_phids[$author_phid])) { - return false; + // If the commit author is identifiable and a package owner, they're + // involved. + if ($author_phid) { + if (isset($owner_phids[$author_phid])) { + return true; + } } - // Trigger an audit of there is no corresponding revision. + // Otherwise, we need to find an owner as a reviewer. + + // If we don't have a revision, this is hopeless: no owners are involved. if (!$revision) { return true; } @@ -174,21 +230,19 @@ final class PhabricatorRepositoryCommitOwnersWorker continue; } - // If this reviewer accepted the revision and owns the package, we're - // all clear and do not need to trigger an audit. + // If this reviewer accepted the revision and owns the package, we've + // found an involved owner. if (isset($accepted_statuses[$reviewer->getReviewerStatus()])) { $found_accept = true; break; } } - // Don't trigger an audit if a package owner already reviewed the - // revision. if ($found_accept) { - return false; + return true; } - return true; + return false; } } diff --git a/src/docs/user/userguide/owners.diviner b/src/docs/user/userguide/owners.diviner index 95a3882552..df74fc6e83 100644 --- a/src/docs/user/userguide/owners.diviner +++ b/src/docs/user/userguide/owners.diviner @@ -114,16 +114,22 @@ Auditing ======== You can automatically trigger audits on unreviewed code by configuring -**Auditing**. The available settings are: +**Auditing**. The available settings allow you to select behavior based on +these conditions: - - **Disabled**: Do not trigger audits. - - **Enabled**: Trigger audits. + - **No Owner Involvement**: Triggers an audit when the commit author is not + a package owner, and no package owner reviewed an associated revision in + Differential. + - **Unreviewed Commits**: Triggers an audit when a commit has no associated + revision in Differential, or the associated revision in Differential landed + without being "Accepted". -When enabled, audits are triggered for commits which: +For example, the **Audit Commits With No Owner Involvement** option triggers +audits for commits which: - affect code owned by the package; - were not authored by a package owner; and - - were not accepted by a package owner. + - were not accepted (in Differential) by a package owner. Audits do not trigger if the package has been archived. From ae54af32c145fec2c4628f360f169d2f6395939f Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 7 Feb 2019 13:41:34 -0800 Subject: [PATCH 031/245] When an Owners package accepts a revision, count that as an "involved owner" for the purposes of audit Summary: Depends on D20129. Ref T13244. See PHI1058. When a revision has an "Accept" from a package, count the owners as "involved" in the change whether or not any actual human owners are actually accepting reviewers. If a user owns "/" and uses "force accept" to cause "/src/javascript" to accept, or a user who legitimately owns "/src/javascript" accepts on behalf of the package but not on behalf of themselves (for whatever reason), it generally makes practical sense that these changes have owners involved in them (i.e., that's what a normal user would expect in both cases) and don't need to trigger audits under "no involvement" rules. Test Plan: Used `bin/repository reparse --force --owners ` to trigger audit logic. Saw a commit owned by `O1` with a revision counted as "involved" when `O1` had accepted the revision, even though no actual human owner had accepted it. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13244 Differential Revision: https://secure.phabricator.com/D20130 --- .../PhabricatorRepositoryCommitOwnersWorker.php | 13 ++++++++++--- src/docs/user/userguide/owners.diviner | 3 ++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php b/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php index 65434658ab..b0c60667a4 100644 --- a/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php +++ b/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php @@ -200,6 +200,12 @@ final class PhabricatorRepositoryCommitOwnersWorker )); $owner_phids = array_fuse($owner_phids); + // For the purposes of deciding whether the owners were involved in the + // revision or not, consider a review by the package itself to count as + // involvement. This can happen when human reviewers force-accept on + // behalf of packages they don't own but have authority over. + $owner_phids[$package->getPHID()] = $package->getPHID(); + // If the commit author is identifiable and a package owner, they're // involved. if ($author_phid) { @@ -225,13 +231,14 @@ final class PhabricatorRepositoryCommitOwnersWorker foreach ($revision->getReviewers() as $reviewer) { $reviewer_phid = $reviewer->getReviewerPHID(); - // If this reviewer isn't a package owner, just ignore them. + // If this reviewer isn't a package owner or the package itself, + // just ignore them. if (empty($owner_phids[$reviewer_phid])) { continue; } - // If this reviewer accepted the revision and owns the package, we've - // found an involved owner. + // If this reviewer accepted the revision and owns the package (or is + // the package), we've found an involved owner. if (isset($accepted_statuses[$reviewer->getReviewerStatus()])) { $found_accept = true; break; diff --git a/src/docs/user/userguide/owners.diviner b/src/docs/user/userguide/owners.diviner index df74fc6e83..11dee8941a 100644 --- a/src/docs/user/userguide/owners.diviner +++ b/src/docs/user/userguide/owners.diviner @@ -129,7 +129,8 @@ audits for commits which: - affect code owned by the package; - were not authored by a package owner; and - - were not accepted (in Differential) by a package owner. + - were not accepted (in Differential) by a package owner or the package + itself. Audits do not trigger if the package has been archived. From 2b718d78bba24f0636842a9281d32fe1d09cc84d Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 8 Feb 2019 07:12:39 -0800 Subject: [PATCH 032/245] Improve UI/UX when users try to add an invalid card with Stripe Summary: Ref T13244. See PHI1052. Our error handling for Stripe errors isn't great right now. We can give users a bit more information, and a less jarring UI. Test Plan: Before (this is in developer mode, production doesn't get a stack trace): {F6197394} After: {F6197397} - Tried all the invalid test codes listed here: https://stripe.com/docs/testing#cards Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13244 Differential Revision: https://secure.phabricator.com/D20132 --- resources/celerity/map.php | 6 +- src/__phutil_library_map__.php | 2 + src/applications/fund/storage/FundBacker.php | 3 + .../PhortunePaymentMethodCreateController.php | 36 ++++--- .../exception/PhortuneDisplayException.php | 15 +++ .../PhortuneStripePaymentProvider.php | 94 ++++++++++++++++++- .../policy/filter/PhabricatorPolicyFilter.php | 7 +- webroot/rsrc/css/aphront/table-view.css | 16 ++-- 8 files changed, 149 insertions(+), 30 deletions(-) create mode 100644 src/applications/phortune/exception/PhortuneDisplayException.php diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 537d1f46dc..8312828c6e 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,7 +9,7 @@ return array( 'names' => array( 'conpherence.pkg.css' => '3c8a0668', 'conpherence.pkg.js' => '020aebcf', - 'core.pkg.css' => 'eab5ccaf', + 'core.pkg.css' => '08baca0c', 'core.pkg.js' => '5c737607', 'differential.pkg.css' => 'b8df73d4', 'differential.pkg.js' => '67c9ea4c', @@ -30,7 +30,7 @@ return array( 'rsrc/css/aphront/notification.css' => '30240bd2', 'rsrc/css/aphront/panel-view.css' => '46923d46', 'rsrc/css/aphront/phabricator-nav-view.css' => 'f8a0c1bf', - 'rsrc/css/aphront/table-view.css' => '76eda3f8', + 'rsrc/css/aphront/table-view.css' => 'daa1f9df', 'rsrc/css/aphront/tokenizer.css' => 'b52d0668', 'rsrc/css/aphront/tooltip.css' => 'e3f2412f', 'rsrc/css/aphront/typeahead-browse.css' => 'b7ed02d2', @@ -519,7 +519,7 @@ return array( 'aphront-list-filter-view-css' => 'feb64255', 'aphront-multi-column-view-css' => 'fbc00ba3', 'aphront-panel-view-css' => '46923d46', - 'aphront-table-view-css' => '76eda3f8', + 'aphront-table-view-css' => 'daa1f9df', 'aphront-tokenizer-control-css' => 'b52d0668', 'aphront-tooltip-css' => 'e3f2412f', 'aphront-typeahead-control-css' => '8779483d', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 4f9d7123c1..b704a7cc2e 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -5015,6 +5015,7 @@ phutil_register_library_map(array( 'PhortuneCurrencySerializer' => 'applications/phortune/currency/PhortuneCurrencySerializer.php', 'PhortuneCurrencyTestCase' => 'applications/phortune/currency/__tests__/PhortuneCurrencyTestCase.php', 'PhortuneDAO' => 'applications/phortune/storage/PhortuneDAO.php', + 'PhortuneDisplayException' => 'applications/phortune/exception/PhortuneDisplayException.php', 'PhortuneErrCode' => 'applications/phortune/constants/PhortuneErrCode.php', 'PhortuneInvoiceView' => 'applications/phortune/view/PhortuneInvoiceView.php', 'PhortuneLandingController' => 'applications/phortune/controller/PhortuneLandingController.php', @@ -11263,6 +11264,7 @@ phutil_register_library_map(array( 'PhortuneCurrencySerializer' => 'PhabricatorLiskSerializer', 'PhortuneCurrencyTestCase' => 'PhabricatorTestCase', 'PhortuneDAO' => 'PhabricatorLiskDAO', + 'PhortuneDisplayException' => 'Exception', 'PhortuneErrCode' => 'PhortuneConstants', 'PhortuneInvoiceView' => 'AphrontTagView', 'PhortuneLandingController' => 'PhortuneController', diff --git a/src/applications/fund/storage/FundBacker.php b/src/applications/fund/storage/FundBacker.php index 87ab342e2a..ebdf39ae17 100644 --- a/src/applications/fund/storage/FundBacker.php +++ b/src/applications/fund/storage/FundBacker.php @@ -76,6 +76,7 @@ final class FundBacker extends FundDAO public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, ); } @@ -91,6 +92,8 @@ final class FundBacker extends FundDAO return $initiative->getPolicy($capability); } return PhabricatorPolicies::POLICY_NOONE; + case PhabricatorPolicyCapability::CAN_EDIT: + return PhabricatorPolicies::POLICY_NOONE; } } diff --git a/src/applications/phortune/controller/payment/PhortunePaymentMethodCreateController.php b/src/applications/phortune/controller/payment/PhortunePaymentMethodCreateController.php index 87bddd4d33..521508e0e8 100644 --- a/src/applications/phortune/controller/payment/PhortunePaymentMethodCreateController.php +++ b/src/applications/phortune/controller/payment/PhortunePaymentMethodCreateController.php @@ -73,6 +73,7 @@ final class PhortunePaymentMethodCreateController $provider = $providers[$provider_id]; $errors = array(); + $display_exception = null; if ($request->isFormPost() && $request->getBool('isProviderForm')) { $method = id(new PhortunePaymentMethod()) ->setAccountPHID($account->getPHID()) @@ -107,14 +108,23 @@ final class PhortunePaymentMethodCreateController } if (!$errors) { - $errors = $provider->createPaymentMethodFromRequest( - $request, - $method, - $client_token); + try { + $provider->createPaymentMethodFromRequest( + $request, + $method, + $client_token); + } catch (PhortuneDisplayException $exception) { + $display_exception = $exception; + } catch (Exception $ex) { + $errors = array( + pht('There was an error adding this payment method:'), + $ex->getMessage(), + ); + } } } - if (!$errors) { + if (!$errors && !$display_exception) { $method->save(); // If we added this method on a cart flow, return to the cart to @@ -133,13 +143,17 @@ final class PhortunePaymentMethodCreateController return id(new AphrontRedirectResponse())->setURI($next_uri); } else { - $dialog = id(new AphrontDialogView()) - ->setUser($viewer) - ->setTitle(pht('Error Adding Payment Method')) - ->appendChild(id(new PHUIInfoView())->setErrors($errors)) - ->addCancelButton($request->getRequestURI()); + if ($display_exception) { + $dialog_body = $display_exception->getView(); + } else { + $dialog_body = id(new PHUIInfoView()) + ->setErrors($errors); + } - return id(new AphrontDialogResponse())->setDialog($dialog); + return $this->newDialog() + ->setTitle(pht('Error Adding Payment Method')) + ->appendChild($dialog_body) + ->addCancelButton($request->getRequestURI()); } } diff --git a/src/applications/phortune/exception/PhortuneDisplayException.php b/src/applications/phortune/exception/PhortuneDisplayException.php new file mode 100644 index 0000000000..7b2bbf6875 --- /dev/null +++ b/src/applications/phortune/exception/PhortuneDisplayException.php @@ -0,0 +1,15 @@ +view = $view; + return $this; + } + + public function getView() { + return $this->view; + } + +} diff --git a/src/applications/phortune/provider/PhortuneStripePaymentProvider.php b/src/applications/phortune/provider/PhortuneStripePaymentProvider.php index bdaa4294b2..0463881016 100644 --- a/src/applications/phortune/provider/PhortuneStripePaymentProvider.php +++ b/src/applications/phortune/provider/PhortuneStripePaymentProvider.php @@ -233,8 +233,6 @@ final class PhortuneStripePaymentProvider extends PhortunePaymentProvider { array $token) { $this->loadStripeAPILibraries(); - $errors = array(); - $secret_key = $this->getSecretKey(); $stripe_token = $token['stripeCardToken']; @@ -253,7 +251,15 @@ final class PhortuneStripePaymentProvider extends PhortunePaymentProvider { // the card more than once. We create one Customer for each card; // they do not map to PhortuneAccounts because we allow an account to // have more than one active card. - $customer = Stripe_Customer::create($params, $secret_key); + try { + $customer = Stripe_Customer::create($params, $secret_key); + } catch (Stripe_CardError $ex) { + $display_exception = $this->newDisplayExceptionFromCardError($ex); + if ($display_exception) { + throw $display_exception; + } + throw $ex; + } $card = $info->card; @@ -267,8 +273,6 @@ final class PhortuneStripePaymentProvider extends PhortunePaymentProvider { 'stripe.customerID' => $customer->id, 'stripe.cardToken' => $stripe_token, )); - - return $errors; } public function renderCreatePaymentMethodForm( @@ -383,4 +387,84 @@ final class PhortuneStripePaymentProvider extends PhortunePaymentProvider { require_once $root.'/externals/stripe-php/lib/Stripe.php'; } + + private function newDisplayExceptionFromCardError(Stripe_CardError $ex) { + $body = $ex->getJSONBody(); + if (!$body) { + return null; + } + + $map = idx($body, 'error'); + if (!$map) { + return null; + } + + $view = array(); + + $message = idx($map, 'message'); + + $view[] = id(new PHUIInfoView()) + ->setErrors(array($message)); + + $view[] = phutil_tag( + 'div', + array( + 'class' => 'mlt mlb', + ), + pht('Additional details about this error:')); + + $rows = array(); + + $rows[] = array( + pht('Error Code'), + idx($map, 'code'), + ); + + $rows[] = array( + pht('Error Type'), + idx($map, 'type'), + ); + + $param = idx($map, 'param'); + if (strlen($param)) { + $rows[] = array( + pht('Error Param'), + $param, + ); + } + + $decline_code = idx($map, 'decline_code'); + if (strlen($decline_code)) { + $rows[] = array( + pht('Decline Code'), + $decline_code, + ); + } + + $doc_url = idx($map, 'doc_url'); + if ($doc_url) { + $rows[] = array( + pht('Learn More'), + phutil_tag( + 'a', + array( + 'href' => $doc_url, + 'target' => '_blank', + ), + $doc_url), + ); + } + + $view[] = id(new AphrontTableView($rows)) + ->setColumnClasses( + array( + 'header', + 'wide', + )); + + return id(new PhortuneDisplayException(get_class($ex))) + ->setView($view); + } + + } diff --git a/src/applications/policy/filter/PhabricatorPolicyFilter.php b/src/applications/policy/filter/PhabricatorPolicyFilter.php index fb03936ec2..a5c9f356f4 100644 --- a/src/applications/policy/filter/PhabricatorPolicyFilter.php +++ b/src/applications/policy/filter/PhabricatorPolicyFilter.php @@ -175,9 +175,10 @@ final class PhabricatorPolicyFilter extends Phobject { if (!in_array($capability, $object_capabilities)) { throw new Exception( pht( - "Testing for capability '%s' on an object which does ". - "not have that capability!", - $capability)); + 'Testing for capability "%s" on an object ("%s") which does '. + 'not support that capability.', + $capability, + get_class($object))); } $policy = $this->getObjectPolicy($object, $capability); diff --git a/webroot/rsrc/css/aphront/table-view.css b/webroot/rsrc/css/aphront/table-view.css index 1a7d2eb215..9bc8536054 100644 --- a/webroot/rsrc/css/aphront/table-view.css +++ b/webroot/rsrc/css/aphront/table-view.css @@ -45,16 +45,20 @@ background: inherit; } -.aphront-table-view th { +.aphront-table-view th, +.aphront-table-view td.header { font-weight: bold; white-space: nowrap; color: {$bluetext}; - text-shadow: 0 1px 0 white; font-weight: bold; - border-bottom: 1px solid {$thinblueborder}; + text-shadow: 0 1px 0 white; background-color: {$lightbluebackground}; } +.aphront-table-view th { + border-bottom: 1px solid {$thinblueborder}; +} + th.aphront-table-view-sortable-selected { background-color: {$greybackground}; } @@ -74,12 +78,8 @@ th.aphront-table-view-sortable-selected { } .aphront-table-view td.header { - padding: 4px 8px; - white-space: nowrap; text-align: right; - color: {$bluetext}; - font-weight: bold; - vertical-align: top; + border-right: 1px solid {$thinblueborder}; } .aphront-table-view td { From a20f108034126ff7dc45604dec80319e1ec76172 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 7 Feb 2019 14:09:53 -0800 Subject: [PATCH 033/245] When an edit overrides an object lock, note it in the transaction record Summary: Ref T13244. See PHI1059. When you lock a task, users who can edit the task can currently override the lock by using "Edit Task" if they confirm that they want to do this. Mark these edits with an emblem, similar to the "MFA" and "Silent" emblems, so it's clear that they may have bent the rules. Also, make the "MFA" and "Silent" emblems more easily visible. Test Plan: Edited a locked task, overrode the lock, got marked for it. {F6195005} Reviewers: amckinley Reviewed By: amckinley Subscribers: aeiser Maniphest Tasks: T13244 Differential Revision: https://secure.phabricator.com/D20131 --- resources/celerity/map.php | 6 ++--- ...habricatorApplicationTransactionEditor.php | 14 +++++++++++ .../PhabricatorApplicationTransaction.php | 14 +++++++++++ .../PhabricatorApplicationTransactionView.php | 3 ++- src/view/phui/PHUIIconView.php | 14 +++++++++++ src/view/phui/PHUITimelineEventView.php | 23 +++++++++++++++++-- webroot/rsrc/css/phui/phui-icon.css | 21 +++++++++++++++++ 7 files changed, 89 insertions(+), 6 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 8312828c6e..cb44dd5585 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,7 +9,7 @@ return array( 'names' => array( 'conpherence.pkg.css' => '3c8a0668', 'conpherence.pkg.js' => '020aebcf', - 'core.pkg.css' => '08baca0c', + 'core.pkg.css' => '7a73ffc5', 'core.pkg.js' => '5c737607', 'differential.pkg.css' => 'b8df73d4', 'differential.pkg.js' => '67c9ea4c', @@ -157,7 +157,7 @@ return array( 'rsrc/css/phui/phui-header-view.css' => '93cea4ec', 'rsrc/css/phui/phui-hovercard.css' => '6ca90fa0', 'rsrc/css/phui/phui-icon-set-selector.css' => '7aa5f3ec', - 'rsrc/css/phui/phui-icon.css' => '281f964d', + 'rsrc/css/phui/phui-icon.css' => '4cbc684a', 'rsrc/css/phui/phui-image-mask.css' => '62c7f4d2', 'rsrc/css/phui/phui-info-view.css' => '37b8d9ce', 'rsrc/css/phui/phui-invisible-character-view.css' => 'c694c4a4', @@ -823,7 +823,7 @@ return array( 'phui-hovercard' => '074f0783', 'phui-hovercard-view-css' => '6ca90fa0', 'phui-icon-set-selector-css' => '7aa5f3ec', - 'phui-icon-view-css' => '281f964d', + 'phui-icon-view-css' => '4cbc684a', 'phui-image-mask-css' => '62c7f4d2', 'phui-info-view-css' => '37b8d9ce', 'phui-inline-comment-view-css' => '48acce5b', diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index 216c1e00a2..bd066e633b 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -1115,6 +1115,16 @@ abstract class PhabricatorApplicationTransactionEditor $transaction_open = true; } + // We can technically test any object for CAN_INTERACT, but we can + // run into some issues in doing so (for example, in project unit tests). + // For now, only test for CAN_INTERACT if the object is explicitly a + // lockable object. + + $was_locked = false; + if ($object instanceof PhabricatorEditEngineLockableInterface) { + $was_locked = !PhabricatorPolicyFilter::canInteract($actor, $object); + } + foreach ($xactions as $xaction) { $this->applyInternalEffects($object, $xaction); } @@ -1132,6 +1142,10 @@ abstract class PhabricatorApplicationTransactionEditor } foreach ($xactions as $xaction) { + if ($was_locked) { + $xaction->setIsLockOverrideTransaction(true); + } + $xaction->setObjectPHID($object->getPHID()); if ($xaction->getComment()) { $xaction->setPHID($xaction->generatePHID()); diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php index acfc4d0cfa..d71728a01f 100644 --- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php +++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php @@ -169,6 +169,14 @@ abstract class PhabricatorApplicationTransaction return (bool)$this->getMetadataValue('core.mfa', false); } + public function setIsLockOverrideTransaction($override) { + return $this->setMetadataValue('core.lock-override', $override); + } + + public function getIsLockOverrideTransaction() { + return (bool)$this->getMetadataValue('core.lock-override', false); + } + public function attachComment( PhabricatorApplicationTransactionComment $comment) { $this->comment = $comment; @@ -1529,6 +1537,12 @@ abstract class PhabricatorApplicationTransaction return false; } } + + // Don't group lock override and non-override transactions together. + $is_override = $this->getIsLockOverrideTransaction(); + if ($is_override != $xaction->getIsLockOverrideTransaction()) { + return false; + } } return true; diff --git a/src/applications/transactions/view/PhabricatorApplicationTransactionView.php b/src/applications/transactions/view/PhabricatorApplicationTransactionView.php index c2b32aa190..4d738877b8 100644 --- a/src/applications/transactions/view/PhabricatorApplicationTransactionView.php +++ b/src/applications/transactions/view/PhabricatorApplicationTransactionView.php @@ -416,7 +416,8 @@ class PhabricatorApplicationTransactionView extends AphrontView { ->setColor($xaction->getColor()) ->setHideCommentOptions($this->getHideCommentOptions()) ->setIsSilent($xaction->getIsSilentTransaction()) - ->setIsMFA($xaction->getIsMFATransaction()); + ->setIsMFA($xaction->getIsMFATransaction()) + ->setIsLockOverride($xaction->getIsLockOverrideTransaction()); list($token, $token_removed) = $xaction->getToken(); if ($token) { diff --git a/src/view/phui/PHUIIconView.php b/src/view/phui/PHUIIconView.php index 8cc61ba2eb..d907cb3343 100644 --- a/src/view/phui/PHUIIconView.php +++ b/src/view/phui/PHUIIconView.php @@ -19,6 +19,7 @@ final class PHUIIconView extends AphrontTagView { private $iconColor; private $iconBackground; private $tooltip; + private $emblemColor; public function setHref($href) { $this->href = $href; @@ -66,6 +67,15 @@ final class PHUIIconView extends AphrontTagView { return $this; } + public function setEmblemColor($emblem_color) { + $this->emblemColor = $emblem_color; + return $this; + } + + public function getEmblemColor() { + return $this->emblemColor; + } + protected function getTagName() { $tag = 'span'; if ($this->href) { @@ -106,6 +116,10 @@ final class PHUIIconView extends AphrontTagView { $this->appendChild($this->text); } + if ($this->emblemColor) { + $classes[] = 'phui-icon-emblem phui-icon-emblem-'.$this->emblemColor; + } + $sigil = null; $meta = array(); if ($this->tooltip) { diff --git a/src/view/phui/PHUITimelineEventView.php b/src/view/phui/PHUITimelineEventView.php index 86628058fe..78a75a2063 100644 --- a/src/view/phui/PHUITimelineEventView.php +++ b/src/view/phui/PHUITimelineEventView.php @@ -31,6 +31,7 @@ final class PHUITimelineEventView extends AphrontView { private $pinboardItems = array(); private $isSilent; private $isMFA; + private $isLockOverride; public function setAuthorPHID($author_phid) { $this->authorPHID = $author_phid; @@ -197,6 +198,15 @@ final class PHUITimelineEventView extends AphrontView { return $this->isMFA; } + public function setIsLockOverride($is_override) { + $this->isLockOverride = $is_override; + return $this; + } + + public function getIsLockOverride() { + return $this->isLockOverride; + } + public function setReallyMajorEvent($me) { $this->reallyMajorEvent = $me; return $this; @@ -597,7 +607,8 @@ final class PHUITimelineEventView extends AphrontView { // not expect to have received any mail or notifications. if ($this->getIsSilent()) { $extra[] = id(new PHUIIconView()) - ->setIcon('fa-bell-slash', 'red') + ->setIcon('fa-bell-slash', 'white') + ->setEmblemColor('red') ->setTooltip(pht('Silent Edit')); } @@ -605,9 +616,17 @@ final class PHUITimelineEventView extends AphrontView { // provide a hint that it was extra authentic. if ($this->getIsMFA()) { $extra[] = id(new PHUIIconView()) - ->setIcon('fa-vcard', 'pink') + ->setIcon('fa-vcard', 'white') + ->setEmblemColor('pink') ->setTooltip(pht('MFA Authenticated')); } + + if ($this->getIsLockOverride()) { + $extra[] = id(new PHUIIconView()) + ->setIcon('fa-chain-broken', 'white') + ->setEmblemColor('violet') + ->setTooltip(pht('Lock Overridden')); + } } $extra = javelin_tag( diff --git a/webroot/rsrc/css/phui/phui-icon.css b/webroot/rsrc/css/phui/phui-icon.css index 4108074b08..5436bb04b1 100644 --- a/webroot/rsrc/css/phui/phui-icon.css +++ b/webroot/rsrc/css/phui/phui-icon.css @@ -183,3 +183,24 @@ a.phui-icon-view.phui-icon-square:hover { text-decoration: none; color: #fff; } + + +.phui-icon-emblem { + border-radius: 4px; +} + +.phui-timeline-extra .phui-icon-emblem { + padding: 4px 6px; +} + +.phui-icon-emblem-violet { + background-color: {$violet}; +} + +.phui-icon-emblem-red { + background-color: {$red}; +} + +.phui-icon-emblem-pink { + background-color: {$pink}; +} From 711871f6bc743a7e91c953695a429c37b6692ca0 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 11 Feb 2019 06:48:13 -0800 Subject: [PATCH 034/245] Allow typeaheads to pass nonscalar data to datasources Summary: Ref T13250. Currently, datasources have a `setParameters(...)` method. This method accepts a dictionary and adds the key/value pairs to the raw HTTP request to the datasource endpoint. Since D20049, this no longer works. Since D20116, it fatals explicitly. In general, the datasource endpoint accepts other values (like `query`, `offset`, and `limit`), and even before these changes, using secret reserved keys in `setParameters(...)` would silently cause program misbehavior. To deal with this, pass parameters as a JSON string named "parameters". This fixes the HTTP query issue (the more pressing issue affecting users today) and prevents the "shadowing reserved keys" issue (a theoretical issue which might affect users some day). (I may revisit the `phutil_build_http_querystring()` behavior and possibly let it make this work again, but I think avoiding the duplicate key issue makes this change desirable even if the querystring behavior changes.) Test Plan: - Used "Land Revision", selected branches. - Configured a custom Maniphest "users" field, used the search typeahead, selected users. - Manually browsed to `/typeahead/class/PhabricatorPeopleDatasource/?query=hi¶meters=xyz` to see the JSON decode exception. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13250 Differential Revision: https://secure.phabricator.com/D20134 --- ...orTypeaheadModularDatasourceController.php | 21 ++++++++++++++++++- .../PhabricatorTypeaheadDatasource.php | 16 ++++++++++++-- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php b/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php index 9e905f8cce..7c2205df46 100644 --- a/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php +++ b/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php @@ -35,7 +35,26 @@ final class PhabricatorTypeaheadModularDatasourceController if (isset($sources[$class])) { $source = $sources[$class]; - $source->setParameters($request->getRequestData()); + + $parameters = array(); + + $raw_parameters = $request->getStr('parameters'); + if (strlen($raw_parameters)) { + try { + $parameters = phutil_json_decode($raw_parameters); + } catch (PhutilJSONParserException $ex) { + return $this->newDialog() + ->setTitle(pht('Invalid Parameters')) + ->appendParagraph( + pht( + 'The HTTP parameter named "parameters" for this request is '. + 'not a valid JSON parameter. JSON is required. Exception: %s', + $ex->getMessage())) + ->addCancelButton('/'); + } + } + + $source->setParameters($parameters); $source->setViewer($viewer); // NOTE: Wrapping the source in a Composite datasource ensures we perform diff --git a/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php b/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php index 2e369a3f67..196ad1b98b 100644 --- a/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php +++ b/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php @@ -100,7 +100,7 @@ abstract class PhabricatorTypeaheadDatasource extends Phobject { public function getDatasourceURI() { $uri = new PhutilURI('/typeahead/class/'.get_class($this).'/'); - $uri->setQueryParams($this->parameters); + $uri->setQueryParams($this->newURIParameters()); return (string)$uri; } @@ -110,10 +110,22 @@ abstract class PhabricatorTypeaheadDatasource extends Phobject { } $uri = new PhutilURI('/typeahead/browse/'.get_class($this).'/'); - $uri->setQueryParams($this->parameters); + $uri->setQueryParams($this->newURIParameters()); return (string)$uri; } + private function newURIParameters() { + if (!$this->parameters) { + return array(); + } + + $map = array( + 'parameters' => phutil_json_encode($this->parameters), + ); + + return $map; + } + abstract public function getPlaceholderText(); public function getBrowseTitle() { From 77247084bd365e9020b942a6ac37b367dad1f4a7 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 11 Feb 2019 06:55:23 -0800 Subject: [PATCH 035/245] Fix inverted check in audit triggers for "uninvolved owner" Summary: See D20126. I was trying to be a little too cute here with the names and ended up confusing myself, then just tested the method behavior. :/ Test Plan: Persudaded by arguments in D20126. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20135 --- .../worker/PhabricatorRepositoryCommitOwnersWorker.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php b/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php index b0c60667a4..d5054a7f18 100644 --- a/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php +++ b/src/applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php @@ -174,12 +174,12 @@ final class PhabricatorRepositoryCommitOwnersWorker // If auditing is configured to trigger on changes with no involved owner, // check for an owner. If we don't find one, trigger an audit. if ($audit_uninvolved) { - $commit_uninvolved = $this->isOwnerInvolved( + $owner_involved = $this->isOwnerInvolved( $commit, $package, $author_phid, $revision); - if ($commit_uninvolved) { + if (!$owner_involved) { return true; } } From 1275326ea6b6ce16ed2ebda895cc8cd0849f242d Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 11 Feb 2019 09:58:38 -0800 Subject: [PATCH 036/245] Use "phutil_string_cast()" in TypeaheadDatasource Summary: Depends on D20138. Ref T13250. This improves exception behavior and gives us a standard page with a stack trace instead of a text fatal with no stack trace. Truly a great day for PHP. (Eventually we may want to replace all `(string)` with `phutil_string_cast()`, but let's let it have some time in the wild first?) Test Plan: Triggered the error, got a more useful exception behavior. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13250 Differential Revision: https://secure.phabricator.com/D20140 --- .../typeahead/datasource/PhabricatorTypeaheadDatasource.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php b/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php index 196ad1b98b..43cc72eaaa 100644 --- a/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php +++ b/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php @@ -101,7 +101,7 @@ abstract class PhabricatorTypeaheadDatasource extends Phobject { public function getDatasourceURI() { $uri = new PhutilURI('/typeahead/class/'.get_class($this).'/'); $uri->setQueryParams($this->newURIParameters()); - return (string)$uri; + return phutil_string_cast($uri); } public function getBrowseURI() { @@ -111,7 +111,7 @@ abstract class PhabricatorTypeaheadDatasource extends Phobject { $uri = new PhutilURI('/typeahead/browse/'.get_class($this).'/'); $uri->setQueryParams($this->newURIParameters()); - return (string)$uri; + return phutil_string_cast($uri); } private function newURIParameters() { From e03079aaaad2ee43637caacb9d6916dce57650af Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 11 Feb 2019 09:32:33 -0800 Subject: [PATCH 037/245] Try harder to present display/rendering exceptions to the user using standard exception handling Summary: Ref T13250. When exceptions occur in display/rendering/writing, they currently go straight to the fallback handler. This is a minimal handler which doesn't show a stack trace or include any debugging details. In some cases, we have to do this: some of these exceptions prevent us from building a normal page. For example, if the menu bar has a hard fatal in it, we aren't going to be able to build a nice exception page with a menu bar no matter how hard we try. However, in many cases the error is mundane: something detected something invalid and raised an exception during rendering. In these cases there's no problem with the page chrome or the rendering pathway itself, just with rendering the page data. When we get a rendering/response exception, try a second time to build a nice normal exception page. This will often work. If it doesn't work, fall back as before. Test Plan: - Forced the error from T13250 by applying D20136 but not D20134. - Before: {F6205001} - After: {F6205002} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13250 Differential Revision: https://secure.phabricator.com/D20137 --- .../AphrontApplicationConfiguration.php | 47 +++++++++++++++++-- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/src/aphront/configuration/AphrontApplicationConfiguration.php b/src/aphront/configuration/AphrontApplicationConfiguration.php index 8d36bbc880..350688d4fb 100644 --- a/src/aphront/configuration/AphrontApplicationConfiguration.php +++ b/src/aphront/configuration/AphrontApplicationConfiguration.php @@ -282,23 +282,62 @@ final class AphrontApplicationConfiguration } } catch (Exception $ex) { $original_exception = $ex; - $response = $this->handleThrowable($ex); } catch (Throwable $ex) { $original_exception = $ex; - $response = $this->handleThrowable($ex); } try { + if ($original_exception) { + $response = $this->handleThrowable($original_exception); + } + $response = $this->produceResponse($request, $response); $response = $controller->willSendResponse($response); $response->setRequest($request); self::writeResponse($sink, $response); - } catch (Exception $ex) { + } catch (Exception $response_exception) { + // If we encountered an exception while building a normal response, then + // encountered another exception while building a response for the first + // exception, just throw the original exception. It is more likely to be + // useful and point at a root cause than the second exception we ran into + // while telling the user about it. if ($original_exception) { throw $original_exception; } - throw $ex; + + // If we built a response successfully and then ran into an exception + // trying to render it, try to handle and present that exception to the + // user using the standard handler. + + // The problem here might be in rendering (more common) or in the actual + // response mechanism (less common). If it's in rendering, we can likely + // still render a nice exception page: the majority of rendering issues + // are in main page content, not content shared with the exception page. + + $handling_exception = null; + try { + $response = $this->handleThrowable($response_exception); + + $response = $this->produceResponse($request, $response); + $response = $controller->willSendResponse($response); + $response->setRequest($request); + + self::writeResponse($sink, $response); + } catch (Exception $ex) { + $handling_exception = $ex; + } catch (Throwable $ex) { + $handling_exception = $ex; + } + + // If we didn't have any luck with that, raise the original response + // exception. As above, this is the root cause exception and more likely + // to be useful. This will go to the fallback error handler at top + // level. + + if ($handling_exception) { + throw $response_exception; + } } return $response; From b9a1260ef5c01bbc912be9a2ff591b29e7d6b13c Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 11 Feb 2019 09:58:45 -0800 Subject: [PATCH 038/245] Improve top-level fatal exception handling in PHP 7+ Summary: Depends on D20137. Ref T13250. Ref T12101. In versions of PHP beyond 7, various engine errors are gradually changing from internal fatals or internal errors to `Throwables`, a superclass of `Exception`. This is generally a good change, but code written against PHP 5.x before `Throwable` was introduced may not catch these errors, even when the code is intended to be a top-level exception handler. (The double-catch pattern here and elsewhere is because `Throwable` does not exist in older PHP, so `catch (Throwable $ex)` catches nothing. The `Exception $ex` clause catches everything in old PHP, the `Throwable $ex` clause catches everything in newer PHP.) Generalize some `Exception` into `Throwable`. Test Plan: - Added a bogus function call to the rendering stack. - Before change: got a blank page. - After change: nice exception page. {F6205012} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13250, T12101 Differential Revision: https://secure.phabricator.com/D20138 --- .../AphrontApplicationConfiguration.php | 9 ++- support/startup/PhabricatorStartup.php | 4 +- webroot/index.php | 55 ++++++++++++++++--- 3 files changed, 58 insertions(+), 10 deletions(-) diff --git a/src/aphront/configuration/AphrontApplicationConfiguration.php b/src/aphront/configuration/AphrontApplicationConfiguration.php index 350688d4fb..8c1b4b710f 100644 --- a/src/aphront/configuration/AphrontApplicationConfiguration.php +++ b/src/aphront/configuration/AphrontApplicationConfiguration.php @@ -286,6 +286,7 @@ final class AphrontApplicationConfiguration $original_exception = $ex; } + $response_exception = null; try { if ($original_exception) { $response = $this->handleThrowable($original_exception); @@ -296,7 +297,13 @@ final class AphrontApplicationConfiguration $response->setRequest($request); self::writeResponse($sink, $response); - } catch (Exception $response_exception) { + } catch (Exception $ex) { + $response_exception = $ex; + } catch (Throwable $ex) { + $response_exception = $ex; + } + + if ($response_exception) { // If we encountered an exception while building a normal response, then // encountered another exception while building a response for the first // exception, just throw the original exception. It is more likely to be diff --git a/support/startup/PhabricatorStartup.php b/support/startup/PhabricatorStartup.php index 1bfb74d886..4c577ca20c 100644 --- a/support/startup/PhabricatorStartup.php +++ b/support/startup/PhabricatorStartup.php @@ -315,7 +315,7 @@ final class PhabricatorStartup { * * @param string Brief description of the exception context, like * `"Rendering Exception"`. - * @param Exception The exception itself. + * @param Throwable The exception itself. * @param bool True if it's okay to show the exception's stack trace * to the user. The trace will always be logged. * @return exit This method **does not return**. @@ -324,7 +324,7 @@ final class PhabricatorStartup { */ public static function didEncounterFatalException( $note, - Exception $ex, + $ex, $show_trace) { $message = '['.$note.'/'.get_class($ex).'] '.$ex->getMessage(); diff --git a/webroot/index.php b/webroot/index.php index 5c7d79bfa1..6c3d66305e 100644 --- a/webroot/index.php +++ b/webroot/index.php @@ -2,6 +2,7 @@ phabricator_startup(); +$fatal_exception = null; try { PhabricatorStartup::beginStartupPhase('libraries'); PhabricatorStartup::loadCoreLibraries(); @@ -12,25 +13,65 @@ try { PhabricatorStartup::beginStartupPhase('sink'); $sink = new AphrontPHPHTTPSink(); + // PHP introduced a "Throwable" interface in PHP 7 and began making more + // runtime errors throw as "Throwable" errors. This is generally good, but + // makes top-level exception handling that is compatible with both PHP 5 + // and PHP 7 a bit tricky. + + // In PHP 5, "Throwable" does not exist, so "catch (Throwable $ex)" catches + // nothing. + + // In PHP 7, various runtime conditions raise an Error which is a Throwable + // but NOT an Exception, so "catch (Exception $ex)" will not catch them. + + // To cover both cases, we "catch (Exception $ex)" to catch everything in + // PHP 5, and most things in PHP 7. Then, we "catch (Throwable $ex)" to catch + // everything else in PHP 7. For the most part, we only need to do this at + // the top level. + + $main_exception = null; try { PhabricatorStartup::beginStartupPhase('run'); AphrontApplicationConfiguration::runHTTPRequest($sink); } catch (Exception $ex) { + $main_exception = $ex; + } catch (Throwable $ex) { + $main_exception = $ex; + } + + if ($main_exception) { + $response_exception = null; try { $response = new AphrontUnhandledExceptionResponse(); - $response->setException($ex); + $response->setException($main_exception); PhabricatorStartup::endOutputCapture(); $sink->writeResponse($response); - } catch (Exception $response_exception) { - // If we hit a rendering exception, ignore it and throw the original - // exception. It is generally more interesting and more likely to be - // the root cause. - throw $ex; + } catch (Exception $ex) { + $response_exception = $ex; + } catch (Throwable $ex) { + $response_exception = $ex; + } + + // If we hit a rendering exception, ignore it and throw the original + // exception. It is generally more interesting and more likely to be + // the root cause. + + if ($response_exception) { + throw $main_exception; } } } catch (Exception $ex) { - PhabricatorStartup::didEncounterFatalException('Core Exception', $ex, false); + $fatal_exception = $ex; +} catch (Throwable $ex) { + $fatal_exception = $ex; +} + +if ($fatal_exception) { + PhabricatorStartup::didEncounterFatalException( + 'Core Exception', + $fatal_exception, + false); } function phabricator_startup() { From 51cca22d07028ec40da61d515095442ee8822d45 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 29 Jan 2019 06:07:43 -0800 Subject: [PATCH 039/245] Support EU domains for Mailgun API Summary: See . Mailgun has a couple of API domains depending on where your account is based. Test Plan: - Sent normal mail successfully with my non-EU account. - Waiting on user confirmation that this works in the EU. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20055 --- .../metamta/adapter/PhabricatorMailMailgunAdapter.php | 6 +++++- .../user/configuration/configuring_outbound_email.diviner | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/applications/metamta/adapter/PhabricatorMailMailgunAdapter.php b/src/applications/metamta/adapter/PhabricatorMailMailgunAdapter.php index 9eb478efc5..8223ee8102 100644 --- a/src/applications/metamta/adapter/PhabricatorMailMailgunAdapter.php +++ b/src/applications/metamta/adapter/PhabricatorMailMailgunAdapter.php @@ -24,6 +24,7 @@ final class PhabricatorMailMailgunAdapter array( 'api-key' => 'string', 'domain' => 'string', + 'api-hostname' => 'string', )); } @@ -31,12 +32,14 @@ final class PhabricatorMailMailgunAdapter return array( 'api-key' => null, 'domain' => null, + 'api-hostname' => 'api.mailgun.net', ); } public function sendMessage(PhabricatorMailExternalMessage $message) { $api_key = $this->getOption('api-key'); $domain = $this->getOption('domain'); + $api_hostname = $this->getOption('api-hostname'); $params = array(); $subject = $message->getSubject(); @@ -92,7 +95,8 @@ final class PhabricatorMailMailgunAdapter } $mailgun_uri = urisprintf( - 'https://api.mailgun.net/v2/%s/messages', + 'https://%s/v2/%s/messages', + $api_hostname, $domain); $future = id(new HTTPSFuture($mailgun_uri, $params)) diff --git a/src/docs/user/configuration/configuring_outbound_email.diviner b/src/docs/user/configuration/configuring_outbound_email.diviner index 6f5212680e..b77d761f80 100644 --- a/src/docs/user/configuration/configuring_outbound_email.diviner +++ b/src/docs/user/configuration/configuring_outbound_email.diviner @@ -227,6 +227,9 @@ To use this mailer, set `type` to `mailgun`, then configure these `options`: - `api-key`: Required string. Your Mailgun API key. - `domain`: Required string. Your Mailgun domain. + - `api-hostname`: Optional string. Defaults to "api.mailgun.net". If your + account is in another region (like the EU), you may need to specify a + different hostname. Consult the Mailgun documentation. Mailer: Amazon SES From 187356fea5155b5895b9e4c07344476c09196dc1 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 11 Feb 2019 13:00:53 -0800 Subject: [PATCH 040/245] Let the top-level exception handler dump a stack trace if we reach debug mode before things go sideways Summary: Depends on D20140. Ref T13250. Currently, the top-level exception handler doesn't dump stacks because we might not be in debug mode, and we might double-extra-super fatal if we call `PhabricatorEnv:...` to try to figure out if we're in debug mode or not. We can get around this by setting a flag on the Sink once we're able to confirm that we're in debug mode. Then it's okay for the top-level error handler to show traces. There's still some small possibility that showing a trace could make us double-super-fatal since we have to call a little more code, but AphrontStackTraceView is pretty conservative about what it does and 99% of the time this is a huge improvement. Test Plan: {F6205122} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13250 Differential Revision: https://secure.phabricator.com/D20142 --- resources/celerity/map.php | 4 +- .../AphrontApplicationConfiguration.php | 6 +++ .../AphrontUnhandledExceptionResponse.php | 43 ++++++++++++++++++- src/aphront/sink/AphrontHTTPSink.php | 14 ++++-- src/view/widget/AphrontStackTraceView.php | 1 - webroot/index.php | 1 + .../config/unhandled-exception.css | 32 +++++++++++++- 7 files changed, 91 insertions(+), 10 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index cb44dd5585..99f3333106 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -46,7 +46,7 @@ return array( 'rsrc/css/application/config/config-options.css' => '16c920ae', 'rsrc/css/application/config/config-template.css' => '20babf50', 'rsrc/css/application/config/setup-issue.css' => '5eed85b2', - 'rsrc/css/application/config/unhandled-exception.css' => '9da8fdab', + 'rsrc/css/application/config/unhandled-exception.css' => '9ecfc00d', 'rsrc/css/application/conpherence/color.css' => 'b17746b0', 'rsrc/css/application/conpherence/durable-column.css' => '2d57072b', 'rsrc/css/application/conpherence/header-pane.css' => 'c9a3db8e', @@ -877,7 +877,7 @@ return array( 'syntax-highlighting-css' => '8a16f91b', 'tokens-css' => 'ce5a50bd', 'typeahead-browse-css' => 'b7ed02d2', - 'unhandled-exception-css' => '9da8fdab', + 'unhandled-exception-css' => '9ecfc00d', ), 'requires' => array( '01384686' => array( diff --git a/src/aphront/configuration/AphrontApplicationConfiguration.php b/src/aphront/configuration/AphrontApplicationConfiguration.php index 8c1b4b710f..8cd27fa62b 100644 --- a/src/aphront/configuration/AphrontApplicationConfiguration.php +++ b/src/aphront/configuration/AphrontApplicationConfiguration.php @@ -118,6 +118,12 @@ final class AphrontApplicationConfiguration $database_exception = $ex; } + // If we're in developer mode, set a flag so that top-level exception + // handlers can add more information. + if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) { + $sink->setShowStackTraces(true); + } + if ($database_exception) { $issue = PhabricatorSetupIssue::newDatabaseConnectionIssue( $database_exception, diff --git a/src/aphront/response/AphrontUnhandledExceptionResponse.php b/src/aphront/response/AphrontUnhandledExceptionResponse.php index efd9d70ead..32d612ca50 100644 --- a/src/aphront/response/AphrontUnhandledExceptionResponse.php +++ b/src/aphront/response/AphrontUnhandledExceptionResponse.php @@ -4,8 +4,20 @@ final class AphrontUnhandledExceptionResponse extends AphrontStandaloneHTMLResponse { private $exception; + private $showStackTraces; + + public function setShowStackTraces($show_stack_traces) { + $this->showStackTraces = $show_stack_traces; + return $this; + } + + public function getShowStackTraces() { + return $this->showStackTraces; + } + + public function setException($exception) { + // NOTE: We accept an Exception or a Throwable. - public function setException(Exception $exception) { // Log the exception unless it's specifically a silent malformed request // exception. @@ -61,10 +73,36 @@ final class AphrontUnhandledExceptionResponse $body = $ex->getMessage(); $body = phutil_escape_html_newlines($body); + $classes = array(); + $classes[] = 'unhandled-exception-detail'; + + $stack = null; + if ($this->getShowStackTraces()) { + try { + $stack = id(new AphrontStackTraceView()) + ->setTrace($ex->getTrace()); + + $stack = hsprintf('%s', $stack); + + $stack = phutil_tag( + 'div', + array( + 'class' => 'unhandled-exception-stack', + ), + $stack); + + $classes[] = 'unhandled-exception-with-stack'; + } catch (Exception $trace_exception) { + $stack = null; + } catch (Throwable $trace_exception) { + $stack = null; + } + } + return phutil_tag( 'div', array( - 'class' => 'unhandled-exception-detail', + 'class' => implode(' ', $classes), ), array( phutil_tag( @@ -79,6 +117,7 @@ final class AphrontUnhandledExceptionResponse 'class' => 'unhandled-exception-body', ), $body), + $stack, )); } diff --git a/src/aphront/sink/AphrontHTTPSink.php b/src/aphront/sink/AphrontHTTPSink.php index 51c54df520..60deba78ca 100644 --- a/src/aphront/sink/AphrontHTTPSink.php +++ b/src/aphront/sink/AphrontHTTPSink.php @@ -5,14 +5,22 @@ * Normally this is just @{class:AphrontPHPHTTPSink}, which uses "echo" and * "header()" to emit responses. * - * Mostly, this class allows us to do install security or metrics hooks in the - * output pipeline. - * * @task write Writing Response Components * @task emit Emitting the Response */ abstract class AphrontHTTPSink extends Phobject { + private $showStackTraces = false; + + final public function setShowStackTraces($show_stack_traces) { + $this->showStackTraces = $show_stack_traces; + return $this; + } + + final public function getShowStackTraces() { + return $this->showStackTraces; + } + /* -( Writing Response Components )---------------------------------------- */ diff --git a/src/view/widget/AphrontStackTraceView.php b/src/view/widget/AphrontStackTraceView.php index 1d0616df3d..edb805af8f 100644 --- a/src/view/widget/AphrontStackTraceView.php +++ b/src/view/widget/AphrontStackTraceView.php @@ -10,7 +10,6 @@ final class AphrontStackTraceView extends AphrontView { } public function render() { - $user = $this->getUser(); $trace = $this->trace; $libraries = PhutilBootloader::getInstance()->getAllLibraries(); diff --git a/webroot/index.php b/webroot/index.php index 6c3d66305e..0014edfa2c 100644 --- a/webroot/index.php +++ b/webroot/index.php @@ -44,6 +44,7 @@ try { try { $response = new AphrontUnhandledExceptionResponse(); $response->setException($main_exception); + $response->setShowStackTraces($sink->getShowStackTraces()); PhabricatorStartup::endOutputCapture(); $sink->writeResponse($response); diff --git a/webroot/rsrc/css/application/config/unhandled-exception.css b/webroot/rsrc/css/application/config/unhandled-exception.css index 831148cdad..cd8ad313dc 100644 --- a/webroot/rsrc/css/application/config/unhandled-exception.css +++ b/webroot/rsrc/css/application/config/unhandled-exception.css @@ -8,12 +8,12 @@ background: #fff; border: 1px solid #c0392b; border-radius: 3px; - padding: 0 8px; + padding: 8px; } .unhandled-exception-detail .unhandled-exception-title { color: #c0392b; - padding: 12px 8px; + padding: 4px 8px 12px; border-bottom: 1px solid #f4dddb; font-size: 16px; font-weight: 500; @@ -23,3 +23,31 @@ .unhandled-exception-detail .unhandled-exception-body { padding: 16px 12px; } + +.unhandled-exception-with-stack { + max-width: 95%; +} + +.unhandled-exception-stack { + background: #fcfcfc; + overflow-x: auto; + overflow-y: hidden; +} + +.unhandled-exception-stack table { + border-spacing: 0; + border-collapse: collapse; + width: 100%; + border: 1px solid #d7d7d7; +} + +.unhandled-exception-stack th { + background: #e7e7e7; + border-bottom: 1px solid #d7d7d7; + padding: 8px; +} + +.unhandled-exception-stack td { + padding: 4px 8px; + white-space: nowrap; +} From 1fd69f788cee6f6a9a5d7c31e54655e176e018c2 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 11 Feb 2019 11:14:57 -0800 Subject: [PATCH 041/245] Replace "getQueryParams()" callsites in Phabricator Summary: See D20136. This method is sort of inherently bad because it is destructive for some inputs (`x=1&x=2`) and had "PHP-flavored" behavior for other inputs (`x[]=1&x[]=2`). Move to explicit `...AsMap` and `...AsPairList` methods. Test Plan: Bit of an adventure, see inlines in a minute. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20141 --- .../auth/provider/PhabricatorAuthProvider.php | 5 ++-- .../DifferentialRevisionViewController.php | 3 ++- .../view/DifferentialChangesetListView.php | 25 ++++++++++++++++--- .../controller/DiffusionBrowseController.php | 2 -- .../oauthserver/PhabricatorOAuthServer.php | 4 +-- .../rule/PhabricatorYoutubeRemarkupRule.php | 16 +++++++++--- 6 files changed, 42 insertions(+), 13 deletions(-) diff --git a/src/applications/auth/provider/PhabricatorAuthProvider.php b/src/applications/auth/provider/PhabricatorAuthProvider.php index bcccca5121..04548d1f0c 100644 --- a/src/applications/auth/provider/PhabricatorAuthProvider.php +++ b/src/applications/auth/provider/PhabricatorAuthProvider.php @@ -438,12 +438,13 @@ abstract class PhabricatorAuthProvider extends Phobject { $uri = $attributes['uri']; $uri = new PhutilURI($uri); - $params = $uri->getQueryParams(); + $params = $uri->getQueryParamsAsPairList(); $uri->setQueryParams(array()); $content = array($button); - foreach ($params as $key => $value) { + foreach ($params as $pair) { + list($key, $value) = $pair; $content[] = phutil_tag( 'input', array( diff --git a/src/applications/differential/controller/DifferentialRevisionViewController.php b/src/applications/differential/controller/DifferentialRevisionViewController.php index 8216c11557..9bc6345576 100644 --- a/src/applications/differential/controller/DifferentialRevisionViewController.php +++ b/src/applications/differential/controller/DifferentialRevisionViewController.php @@ -1098,7 +1098,8 @@ final class DifferentialRevisionViewController // D123.vs123.id123.whitespaceignore-all.diff // lame but nice to include these options $file_name = ltrim($request_uri->getPath(), '/').'.'; - foreach ($request_uri->getQueryParams() as $key => $value) { + foreach ($request_uri->getQueryParamsAsPairList() as $pair) { + list($key, $value) = $pair; if ($key == 'download') { continue; } diff --git a/src/applications/differential/view/DifferentialChangesetListView.php b/src/applications/differential/view/DifferentialChangesetListView.php index 14de553e59..367991497c 100644 --- a/src/applications/differential/view/DifferentialChangesetListView.php +++ b/src/applications/differential/view/DifferentialChangesetListView.php @@ -358,7 +358,7 @@ final class DifferentialChangesetListView extends AphrontView { if ($this->standaloneURI) { $uri = new PhutilURI($this->standaloneURI); - $uri->setQueryParams($uri->getQueryParams() + $qparams); + $uri = $this->appendDefaultQueryParams($uri, $qparams); $meta['standaloneURI'] = (string)$uri; } @@ -381,7 +381,7 @@ final class DifferentialChangesetListView extends AphrontView { if ($this->leftRawFileURI) { if ($change != DifferentialChangeType::TYPE_ADD) { $uri = new PhutilURI($this->leftRawFileURI); - $uri->setQueryParams($uri->getQueryParams() + $qparams); + $uri = $this->appendDefaultQueryParams($uri, $qparams); $meta['leftURI'] = (string)$uri; } } @@ -390,7 +390,7 @@ final class DifferentialChangesetListView extends AphrontView { if ($change != DifferentialChangeType::TYPE_DELETE && $change != DifferentialChangeType::TYPE_MULTICOPY) { $uri = new PhutilURI($this->rightRawFileURI); - $uri->setQueryParams($uri->getQueryParams() + $qparams); + $uri = $this->appendDefaultQueryParams($uri, $qparams); $meta['rightURI'] = (string)$uri; } } @@ -421,4 +421,23 @@ final class DifferentialChangesetListView extends AphrontView { } + private function appendDefaultQueryParams(PhutilURI $uri, array $params) { + // Add these default query parameters to the query string if they do not + // already exist. + + $have = array(); + foreach ($uri->getQueryParamsAsPairList() as $pair) { + list($key, $value) = $pair; + $have[$key] = true; + } + + foreach ($params as $key => $value) { + if (!isset($have[$key])) { + $uri->appendQueryParam($key, $value); + } + } + + return $uri; + } + } diff --git a/src/applications/diffusion/controller/DiffusionBrowseController.php b/src/applications/diffusion/controller/DiffusionBrowseController.php index 6a863a4a92..fcef87b7ef 100644 --- a/src/applications/diffusion/controller/DiffusionBrowseController.php +++ b/src/applications/diffusion/controller/DiffusionBrowseController.php @@ -709,8 +709,6 @@ final class DiffusionBrowseController extends DiffusionController { 'path' => $path, )); - $before_uri->setQueryParams($request->getRequestURI()->getQueryParams()); - $before_uri = $before_uri->alter('before', null); $before_uri = $before_uri->alter('renamed', $renamed); $before_uri = $before_uri->alter('follow', $follow); diff --git a/src/applications/oauthserver/PhabricatorOAuthServer.php b/src/applications/oauthserver/PhabricatorOAuthServer.php index f5c074f4eb..889e960213 100644 --- a/src/applications/oauthserver/PhabricatorOAuthServer.php +++ b/src/applications/oauthserver/PhabricatorOAuthServer.php @@ -256,8 +256,8 @@ final class PhabricatorOAuthServer extends Phobject { // Any query parameters present in the first URI must be exactly present // in the second URI. - $need_params = $primary_uri->getQueryParams(); - $have_params = $secondary_uri->getQueryParams(); + $need_params = $primary_uri->getQueryParamsAsMap(); + $have_params = $secondary_uri->getQueryParamsAsMap(); foreach ($need_params as $key => $value) { if (!array_key_exists($key, $have_params)) { diff --git a/src/infrastructure/markup/rule/PhabricatorYoutubeRemarkupRule.php b/src/infrastructure/markup/rule/PhabricatorYoutubeRemarkupRule.php index cbf322b2d9..9d79d223e0 100644 --- a/src/infrastructure/markup/rule/PhabricatorYoutubeRemarkupRule.php +++ b/src/infrastructure/markup/rule/PhabricatorYoutubeRemarkupRule.php @@ -18,12 +18,22 @@ final class PhabricatorYoutubeRemarkupRule extends PhutilRemarkupRule { return $text; } - $params = $uri->getQueryParams(); - $v_param = idx($params, 'v'); - if (!strlen($v_param)) { + $v_params = array(); + + $params = $uri->getQueryParamsAsPairList(); + foreach ($params as $pair) { + list($k, $v) = $pair; + if ($k === 'v') { + $v_params[] = $v; + } + } + + if (count($v_params) !== 1) { return $text; } + $v_param = head($v_params); + $text_mode = $this->getEngine()->isTextMode(); $mail_mode = $this->getEngine()->isHTMLMailMode(); From 308c4f2407543ae37abdf78653a1c7ea70d02c2a Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 12 Feb 2019 05:31:04 -0800 Subject: [PATCH 042/245] Fix "AphrontRequest->getRequestURI()" for requests with "x[]=1" parameters in the URI Summary: Ref T13250. See PHI1069. This is a small fix for `getRequestURI()` currently not working if the request includes "x[]=..." PHP-flavored array parameters, beacause they're parsed into arrays by `$_GET` and `setQueryParams(...)` no longer accepts nonscalars. Instead, just parse the raw request URI. Test Plan: Visited `/search/hovercard/?phids[]=X`, no more fatal. Dumped the resulting URI, saw it had the right value. Tried `?phids[]=x&x=1&x=1&x=1`, saw the parameters correctly preserved. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13250 Differential Revision: https://secure.phabricator.com/D20147 --- src/aphront/AphrontRequest.php | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/aphront/AphrontRequest.php b/src/aphront/AphrontRequest.php index 78e6dac9d3..a65566e867 100644 --- a/src/aphront/AphrontRequest.php +++ b/src/aphront/AphrontRequest.php @@ -591,10 +591,15 @@ final class AphrontRequest extends Phobject { } public function getRequestURI() { - $get = $_GET; - unset($get['__path__']); + $request_uri = idx($_SERVER, 'REQUEST_URI', '/'); + + $uri = new PhutilURI($request_uri); + $uri->setQueryParam('__path__', null); + $path = phutil_escape_uri($this->getPath()); - return id(new PhutilURI($path))->setQueryParams($get); + $uri->setPath($path); + + return $uri; } public function getAbsoluteRequestURI() { From 00ffb190cc71b173b99ae4ab155b44a0ea9dc95d Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 12 Feb 2019 05:49:59 -0800 Subject: [PATCH 043/245] In Webhooks, label HTTP response codes as "HTTP Status Code", not "HTTP Error" Summary: See PHI1068. We currently show "HTTP Error - 200", which is misleading. Instead, label these results as "HTTP Status Code". Test Plan: {F6206016} Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20148 --- src/applications/herald/storage/HeraldWebhookRequest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/applications/herald/storage/HeraldWebhookRequest.php b/src/applications/herald/storage/HeraldWebhookRequest.php index 3381f6a99c..bc916fd60b 100644 --- a/src/applications/herald/storage/HeraldWebhookRequest.php +++ b/src/applications/herald/storage/HeraldWebhookRequest.php @@ -120,7 +120,7 @@ final class HeraldWebhookRequest public function getErrorTypeForDisplay() { $map = array( self::ERRORTYPE_HOOK => pht('Hook Error'), - self::ERRORTYPE_HTTP => pht('HTTP Error'), + self::ERRORTYPE_HTTP => pht('HTTP Status Code'), self::ERRORTYPE_TIMEOUT => pht('Request Timeout'), ); From fcd85b6d7bedf5c1fc76f5ac309c99b9a73fc584 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 12 Feb 2019 08:07:10 -0800 Subject: [PATCH 044/245] Replace "getRequestURI()->setQueryParams(array())" with "getPath()" Summary: Ref T13250. A handful of callsites are doing `getRequestURI()` + `setQueryParams(array())` to get a bare request path. They can just use `getPath()` instead. Test Plan: See inlines. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13250 Differential Revision: https://secure.phabricator.com/D20150 --- src/aphront/response/AphrontAjaxResponse.php | 12 +++++------- .../PhabricatorChatLogChannelLogController.php | 3 +-- .../PhabricatorMetaMTAApplicationEmailPanel.php | 3 +-- .../panel/PhabricatorEmailAddressesSettingsPanel.php | 3 +-- .../editengine/PhabricatorEditEngine.php | 3 +-- 5 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/aphront/response/AphrontAjaxResponse.php b/src/aphront/response/AphrontAjaxResponse.php index 1ccb3fe97e..2187defc8f 100644 --- a/src/aphront/response/AphrontAjaxResponse.php +++ b/src/aphront/response/AphrontAjaxResponse.php @@ -32,22 +32,21 @@ final class AphrontAjaxResponse extends AphrontResponse { } public function buildResponseString() { + $request = $this->getRequest(); $console = $this->getConsole(); if ($console) { // NOTE: We're stripping query parameters here both for readability and // to mitigate BREACH and similar attacks. The parameters are available // in the "Request" tab, so this should not impact usability. See T3684. - $uri = $this->getRequest()->getRequestURI(); - $uri = new PhutilURI($uri); - $uri->setQueryParams(array()); + $path = $request->getPath(); Javelin::initBehavior( 'dark-console', array( - 'uri' => (string)$uri, - 'key' => $console->getKey($this->getRequest()), + 'uri' => $path, + 'key' => $console->getKey($request), 'color' => $console->getColor(), - 'quicksand' => $this->getRequest()->isQuicksand(), + 'quicksand' => $request->isQuicksand(), )); } @@ -60,7 +59,6 @@ final class AphrontAjaxResponse extends AphrontResponse { $response = CelerityAPI::getStaticResourceResponse(); - $request = $this->getRequest(); if ($request) { $viewer = $request->getViewer(); if ($viewer) { diff --git a/src/applications/chatlog/controller/PhabricatorChatLogChannelLogController.php b/src/applications/chatlog/controller/PhabricatorChatLogChannelLogController.php index 2c6e58da50..b9893f6924 100644 --- a/src/applications/chatlog/controller/PhabricatorChatLogChannelLogController.php +++ b/src/applications/chatlog/controller/PhabricatorChatLogChannelLogController.php @@ -11,8 +11,7 @@ final class PhabricatorChatLogChannelLogController $viewer = $request->getViewer(); $id = $request->getURIData('channelID'); - $uri = clone $request->getRequestURI(); - $uri->setQueryParams(array()); + $uri = new PhutilURI($request->getPath()); $pager = new AphrontCursorPagerView(); $pager->setURI($uri); diff --git a/src/applications/metamta/applicationpanel/PhabricatorMetaMTAApplicationEmailPanel.php b/src/applications/metamta/applicationpanel/PhabricatorMetaMTAApplicationEmailPanel.php index c13835e6f4..2f9ddcf22b 100644 --- a/src/applications/metamta/applicationpanel/PhabricatorMetaMTAApplicationEmailPanel.php +++ b/src/applications/metamta/applicationpanel/PhabricatorMetaMTAApplicationEmailPanel.php @@ -54,8 +54,7 @@ final class PhabricatorMetaMTAApplicationEmailPanel return new Aphront404Response(); } - $uri = $request->getRequestURI(); - $uri->setQueryParams(array()); + $uri = new PhutilURI($request->getPath()); $new = $request->getStr('new'); $edit = $request->getInt('edit'); diff --git a/src/applications/settings/panel/PhabricatorEmailAddressesSettingsPanel.php b/src/applications/settings/panel/PhabricatorEmailAddressesSettingsPanel.php index 1b69adcd62..8f7f633e7e 100644 --- a/src/applications/settings/panel/PhabricatorEmailAddressesSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorEmailAddressesSettingsPanel.php @@ -31,8 +31,7 @@ final class PhabricatorEmailAddressesSettingsPanel $user = $this->getUser(); $editable = PhabricatorEnv::getEnvConfig('account.editable'); - $uri = $request->getRequestURI(); - $uri->setQueryParams(array()); + $uri = new PhutilURI($request->getPath()); if ($editable) { $new = $request->getStr('new'); diff --git a/src/applications/transactions/editengine/PhabricatorEditEngine.php b/src/applications/transactions/editengine/PhabricatorEditEngine.php index feb783e724..d1a2fb72b9 100644 --- a/src/applications/transactions/editengine/PhabricatorEditEngine.php +++ b/src/applications/transactions/editengine/PhabricatorEditEngine.php @@ -1279,8 +1279,7 @@ abstract class PhabricatorEditEngine $fields = $this->willBuildEditForm($object, $fields); - $request_path = $request->getRequestURI() - ->setQueryParams(array()); + $request_path = $request->getPath(); $form = id(new AphrontFormView()) ->setUser($viewer) From 378a43d09c1fdbe7fd88d5bac6609391161aa49e Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 6 Feb 2019 12:46:42 -0800 Subject: [PATCH 045/245] Remove the highly suspect "Import from LDAP" workflow Summary: Depends on D20109. Ref T6703. This flow was contributed in 2012 and I'm not sure it ever worked, or at least ever worked nondestructively. For now, get rid of it. We'll do importing and external sync properly at some point (T3980, T13190). Test Plan: Grepped for `ldap/`, grepped for controller. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T6703 Differential Revision: https://secure.phabricator.com/D20110 --- src/__phutil_library_map__.php | 2 - .../PhabricatorPeopleApplication.php | 1 - .../PhabricatorPeopleController.php | 4 - .../PhabricatorPeopleLdapController.php | 214 ------------------ 4 files changed, 221 deletions(-) delete mode 100644 src/applications/people/controller/PhabricatorPeopleLdapController.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index b704a7cc2e..6c21edc0da 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -3867,7 +3867,6 @@ phutil_register_library_map(array( 'PhabricatorPeopleInviteController' => 'applications/people/controller/PhabricatorPeopleInviteController.php', 'PhabricatorPeopleInviteListController' => 'applications/people/controller/PhabricatorPeopleInviteListController.php', 'PhabricatorPeopleInviteSendController' => 'applications/people/controller/PhabricatorPeopleInviteSendController.php', - 'PhabricatorPeopleLdapController' => 'applications/people/controller/PhabricatorPeopleLdapController.php', 'PhabricatorPeopleListController' => 'applications/people/controller/PhabricatorPeopleListController.php', 'PhabricatorPeopleLogQuery' => 'applications/people/query/PhabricatorPeopleLogQuery.php', 'PhabricatorPeopleLogSearchEngine' => 'applications/people/query/PhabricatorPeopleLogSearchEngine.php', @@ -9866,7 +9865,6 @@ phutil_register_library_map(array( 'PhabricatorPeopleInviteController' => 'PhabricatorPeopleController', 'PhabricatorPeopleInviteListController' => 'PhabricatorPeopleInviteController', 'PhabricatorPeopleInviteSendController' => 'PhabricatorPeopleInviteController', - 'PhabricatorPeopleLdapController' => 'PhabricatorPeopleController', 'PhabricatorPeopleListController' => 'PhabricatorPeopleController', 'PhabricatorPeopleLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorPeopleLogSearchEngine' => 'PhabricatorApplicationSearchEngine', diff --git a/src/applications/people/application/PhabricatorPeopleApplication.php b/src/applications/people/application/PhabricatorPeopleApplication.php index 06740be26e..9cc3607930 100644 --- a/src/applications/people/application/PhabricatorPeopleApplication.php +++ b/src/applications/people/application/PhabricatorPeopleApplication.php @@ -63,7 +63,6 @@ final class PhabricatorPeopleApplication extends PhabricatorApplication { 'welcome/(?P[1-9]\d*)/' => 'PhabricatorPeopleWelcomeController', 'create/' => 'PhabricatorPeopleCreateController', 'new/(?P[^/]+)/' => 'PhabricatorPeopleNewController', - 'ldap/' => 'PhabricatorPeopleLdapController', 'editprofile/(?P[1-9]\d*)/' => 'PhabricatorPeopleProfileEditController', 'badges/(?P[1-9]\d*)/' => diff --git a/src/applications/people/controller/PhabricatorPeopleController.php b/src/applications/people/controller/PhabricatorPeopleController.php index e3b60eff2b..c2c262f9f4 100644 --- a/src/applications/people/controller/PhabricatorPeopleController.php +++ b/src/applications/people/controller/PhabricatorPeopleController.php @@ -28,10 +28,6 @@ abstract class PhabricatorPeopleController extends PhabricatorController { if ($viewer->getIsAdmin()) { $nav->addLabel(pht('User Administration')); - if (PhabricatorLDAPAuthProvider::getLDAPProvider()) { - $nav->addFilter('ldap', pht('Import from LDAP')); - } - $nav->addFilter('logs', pht('Activity Logs')); $nav->addFilter('invite', pht('Email Invitations')); } diff --git a/src/applications/people/controller/PhabricatorPeopleLdapController.php b/src/applications/people/controller/PhabricatorPeopleLdapController.php deleted file mode 100644 index 876bf986ad..0000000000 --- a/src/applications/people/controller/PhabricatorPeopleLdapController.php +++ /dev/null @@ -1,214 +0,0 @@ -requireApplicationCapability( - PeopleCreateUsersCapability::CAPABILITY); - $admin = $request->getUser(); - - $content = array(); - - $form = id(new AphrontFormView()) - ->setAction($request->getRequestURI() - ->alter('search', 'true')->alter('import', null)) - ->setUser($admin) - ->appendChild( - id(new AphrontFormTextControl()) - ->setLabel(pht('LDAP username')) - ->setName('username')) - ->appendChild( - id(new AphrontFormPasswordControl()) - ->setDisableAutocomplete(true) - ->setLabel(pht('Password')) - ->setName('password')) - ->appendChild( - id(new AphrontFormTextControl()) - ->setLabel(pht('LDAP query')) - ->setCaption(pht('A filter such as %s.', '(objectClass=*)')) - ->setName('query')) - ->appendChild( - id(new AphrontFormSubmitControl()) - ->setValue(pht('Search'))); - - $panel = id(new PHUIObjectBoxView()) - ->setHeaderText(pht('Import LDAP Users')) - ->setForm($form); - - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb( - pht('Import LDAP Users'), - $this->getApplicationURI('/ldap/')); - - $nav = $this->buildSideNavView(); - $nav->selectFilter('ldap'); - $nav->appendChild($content); - - if ($request->getStr('import')) { - $nav->appendChild($this->processImportRequest($request)); - } - - $nav->appendChild($panel); - - if ($request->getStr('search')) { - $nav->appendChild($this->processSearchRequest($request)); - } - - return $this->newPage() - ->setTitle(pht('Import LDAP Users')) - ->setCrumbs($crumbs) - ->setNavigation($nav); - } - - private function processImportRequest($request) { - $admin = $request->getUser(); - $usernames = $request->getArr('usernames'); - $emails = $request->getArr('email'); - $names = $request->getArr('name'); - - $notice_view = new PHUIInfoView(); - $notice_view->setSeverity(PHUIInfoView::SEVERITY_NOTICE); - $notice_view->setTitle(pht('Import Successful')); - $notice_view->setErrors(array( - pht('Successfully imported users from LDAP'), - )); - - $list = new PHUIObjectItemListView(); - $list->setNoDataString(pht('No users imported?')); - - foreach ($usernames as $username) { - $user = new PhabricatorUser(); - $user->setUsername($username); - $user->setRealname($names[$username]); - - $email_obj = id(new PhabricatorUserEmail()) - ->setAddress($emails[$username]) - ->setIsVerified(1); - try { - id(new PhabricatorUserEditor()) - ->setActor($admin) - ->createNewUser($user, $email_obj); - - id(new PhabricatorExternalAccount()) - ->setUserPHID($user->getPHID()) - ->setAccountType('ldap') - ->setAccountDomain('self') - ->setAccountID($username) - ->save(); - - $header = pht('Successfully added %s', $username); - $attribute = null; - $color = 'fa-check green'; - } catch (Exception $ex) { - $header = pht('Failed to add %s', $username); - $attribute = $ex->getMessage(); - $color = 'fa-times red'; - } - - $item = id(new PHUIObjectItemView()) - ->setHeader($header) - ->addAttribute($attribute) - ->setStatusIcon($color); - - $list->addItem($item); - } - - return array( - $notice_view, - $list, - ); - - } - - private function processSearchRequest($request) { - $panel = new PHUIBoxView(); - $admin = $request->getUser(); - - $search = $request->getStr('query'); - - $ldap_provider = PhabricatorLDAPAuthProvider::getLDAPProvider(); - if (!$ldap_provider) { - throw new Exception(pht('No LDAP provider enabled!')); - } - - $ldap_adapter = $ldap_provider->getAdapter(); - $ldap_adapter->setLoginUsername($request->getStr('username')); - $ldap_adapter->setLoginPassword( - new PhutilOpaqueEnvelope($request->getStr('password'))); - - // This causes us to connect and bind. - // TODO: Clean up this discard mode stuff. - DarkConsoleErrorLogPluginAPI::enableDiscardMode(); - $ldap_adapter->getAccountID(); - DarkConsoleErrorLogPluginAPI::disableDiscardMode(); - - $results = $ldap_adapter->searchLDAP('%Q', $search); - - foreach ($results as $key => $record) { - $account_id = $ldap_adapter->readLDAPRecordAccountID($record); - if (!$account_id) { - unset($results[$key]); - continue; - } - - $info = array( - $account_id, - $ldap_adapter->readLDAPRecordEmail($record), - $ldap_adapter->readLDAPRecordRealName($record), - ); - $results[$key] = $info; - $results[$key][] = $this->renderUserInputs($info); - } - - $form = id(new AphrontFormView()) - ->setUser($admin); - - $table = new AphrontTableView($results); - $table->setHeaders( - array( - pht('Username'), - pht('Email'), - pht('Real Name'), - pht('Import?'), - )); - $form->appendChild($table); - $form->setAction($request->getRequestURI() - ->alter('import', 'true')->alter('search', null)) - ->appendChild( - id(new AphrontFormSubmitControl()) - ->setValue(pht('Import'))); - - $panel->appendChild($form); - - return $panel; - } - - private function renderUserInputs($user) { - $username = $user[0]; - return hsprintf( - '%s%s%s', - phutil_tag( - 'input', - array( - 'type' => 'checkbox', - 'name' => 'usernames[]', - 'value' => $username, - )), - phutil_tag( - 'input', - array( - 'type' => 'hidden', - 'name' => "email[$username]", - 'value' => $user[1], - )), - phutil_tag( - 'input', - array( - 'type' => 'hidden', - 'name' => "name[$username]", - 'value' => $user[2], - ))); - } - -} From 55c18bc90041f15759f62eaa44e31e5f61359be1 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 6 Feb 2019 12:59:55 -0800 Subject: [PATCH 046/245] During first-time setup, create an administrator account with no authentication instead of weird, detached authentication Summary: Ref T6703. Currently, when you create an account on a new install, we prompt you to select a password. You can't actually use that password unless you set up a password provider, and that password can't be associated with a provider since a password provider won't exist yet. Instead, just don't ask for a password: create an account with a username and an email address only. Setup guidance points you toward Auth. If you lose the session, you can send yourself an email link (if email works yet) or `bin/auth recover` it. This isn't really much different than the pre-change behavior, since you can't use the password you set anyway until you configure password auth. This also makes fixing T9512 more important, which I'll do in a followup. I also plan to add slightly better guideposts toward Auth. Test Plan: Hit first-time setup, created an account. Reviewers: amckinley Reviewed By: amckinley Subscribers: revi Maniphest Tasks: T6703 Differential Revision: https://secure.phabricator.com/D20111 --- .../PhabricatorAuthRegisterController.php | 118 +++++++++--------- .../people/storage/PhabricatorUser.php | 2 +- .../people/storage/PhabricatorUserEmail.php | 5 +- ...figuring_accounts_and_registration.diviner | 32 +++-- 4 files changed, 89 insertions(+), 68 deletions(-) diff --git a/src/applications/auth/controller/PhabricatorAuthRegisterController.php b/src/applications/auth/controller/PhabricatorAuthRegisterController.php index 9e1aef592c..0562a4242c 100644 --- a/src/applications/auth/controller/PhabricatorAuthRegisterController.php +++ b/src/applications/auth/controller/PhabricatorAuthRegisterController.php @@ -21,7 +21,9 @@ final class PhabricatorAuthRegisterController list($account, $provider, $response) = $result; $is_default = false; } else if ($this->isFirstTimeSetup()) { - list($account, $provider, $response) = $this->loadSetupAccount(); + $account = null; + $provider = null; + $response = null; $is_default = true; $is_setup = true; } else { @@ -35,22 +37,24 @@ final class PhabricatorAuthRegisterController $invite = $this->loadInvite(); - if (!$provider->shouldAllowRegistration()) { - if ($invite) { - // If the user has an invite, we allow them to register with any - // provider, even a login-only provider. - } else { - // TODO: This is a routine error if you click "Login" on an external - // auth source which doesn't allow registration. The error should be - // more tailored. + if (!$is_setup) { + if (!$provider->shouldAllowRegistration()) { + if ($invite) { + // If the user has an invite, we allow them to register with any + // provider, even a login-only provider. + } else { + // TODO: This is a routine error if you click "Login" on an external + // auth source which doesn't allow registration. The error should be + // more tailored. - return $this->renderError( - pht( - 'The account you are attempting to register with uses an '. - 'authentication provider ("%s") which does not allow '. - 'registration. An administrator may have recently disabled '. - 'registration with this provider.', - $provider->getProviderName())); + return $this->renderError( + pht( + 'The account you are attempting to register with uses an '. + 'authentication provider ("%s") which does not allow '. + 'registration. An administrator may have recently disabled '. + 'registration with this provider.', + $provider->getProviderName())); + } } } @@ -58,14 +62,19 @@ final class PhabricatorAuthRegisterController $user = new PhabricatorUser(); - $default_username = $account->getUsername(); - $default_realname = $account->getRealName(); + if ($is_setup) { + $default_username = null; + $default_realname = null; + $default_email = null; + } else { + $default_username = $account->getUsername(); + $default_realname = $account->getRealName(); + $default_email = $account->getEmail(); + } $account_type = PhabricatorAuthPassword::PASSWORD_TYPE_ACCOUNT; $content_source = PhabricatorContentSource::newFromRequest($request); - $default_email = $account->getEmail(); - if ($invite) { $default_email = $invite->getEmailAddress(); } @@ -212,7 +221,11 @@ final class PhabricatorAuthRegisterController $can_edit_email = $profile->getCanEditEmail(); $can_edit_realname = $profile->getCanEditRealName(); - $must_set_password = $provider->shouldRequireRegistrationPassword(); + if ($is_setup) { + $must_set_password = false; + } else { + $must_set_password = $provider->shouldRequireRegistrationPassword(); + } $can_edit_anything = $profile->getCanEditAnything() || $must_set_password; $force_verify = $profile->getShouldVerifyEmail(); @@ -334,9 +347,11 @@ final class PhabricatorAuthRegisterController } if (!$errors) { - $image = $this->loadProfilePicture($account); - if ($image) { - $user->setProfileImagePHID($image->getPHID()); + if (!$is_setup) { + $image = $this->loadProfilePicture($account); + if ($image) { + $user->setProfileImagePHID($image->getPHID()); + } } try { @@ -346,17 +361,19 @@ final class PhabricatorAuthRegisterController $verify_email = true; } - if ($value_email === $default_email) { - if ($account->getEmailVerified()) { - $verify_email = true; - } + if (!$is_setup) { + if ($value_email === $default_email) { + if ($account->getEmailVerified()) { + $verify_email = true; + } - if ($provider->shouldTrustEmails()) { - $verify_email = true; - } + if ($provider->shouldTrustEmails()) { + $verify_email = true; + } - if ($invite) { - $verify_email = true; + if ($invite) { + $verify_email = true; + } } } @@ -438,9 +455,11 @@ final class PhabricatorAuthRegisterController $transaction_editor->applyTransactions($user, $xactions); } - $account->setUserPHID($user->getPHID()); - $provider->willRegisterAccount($account); - $account->save(); + if (!$is_setup) { + $account->setUserPHID($user->getPHID()); + $provider->willRegisterAccount($account); + $account->save(); + } $user->saveTransaction(); @@ -501,7 +520,6 @@ final class PhabricatorAuthRegisterController ->setAuthProvider($provider))); } - if ($can_edit_username) { $form->appendChild( id(new AphrontFormTextControl()) @@ -595,7 +613,7 @@ final class PhabricatorAuthRegisterController pht( 'Installation is complete. Register your administrator account '. 'below to log in. You will be able to configure options and add '. - 'other authentication mechanisms (like LDAP or OAuth) later on.')); + 'authentication mechanisms later on.')); } $object_box = id(new PHUIObjectBoxView()) @@ -612,11 +630,12 @@ final class PhabricatorAuthRegisterController $view = id(new PHUITwoColumnView()) ->setHeader($header) - ->setFooter(array( - $welcome_view, - $invite_header, - $object_box, - )); + ->setFooter( + array( + $welcome_view, + $invite_header, + $object_box, + )); return $this->newPage() ->setTitle($title) @@ -657,19 +676,6 @@ final class PhabricatorAuthRegisterController return array($account, $provider, $response); } - private function loadSetupAccount() { - $provider = new PhabricatorPasswordAuthProvider(); - $provider->attachProviderConfig( - id(new PhabricatorAuthProviderConfig()) - ->setShouldAllowRegistration(1) - ->setShouldAllowLogin(1) - ->setIsEnabled(true)); - - $account = $provider->getDefaultExternalAccount(); - $response = null; - return array($account, $provider, $response); - } - private function loadProfilePicture(PhabricatorExternalAccount $account) { $phid = $account->getProfileImagePHID(); if (!$phid) { diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php index 055df8b79e..c675878747 100644 --- a/src/applications/people/storage/PhabricatorUser.php +++ b/src/applications/people/storage/PhabricatorUser.php @@ -557,7 +557,7 @@ final class PhabricatorUser public static function describeValidUsername() { return pht( - 'Usernames must contain only numbers, letters, period, underscore and '. + 'Usernames must contain only numbers, letters, period, underscore, and '. 'hyphen, and can not end with a period. They must have no more than %d '. 'characters.', new PhutilNumber(self::MAXIMUM_USERNAME_LENGTH)); diff --git a/src/applications/people/storage/PhabricatorUserEmail.php b/src/applications/people/storage/PhabricatorUserEmail.php index 42946015de..572c7d6e8b 100644 --- a/src/applications/people/storage/PhabricatorUserEmail.php +++ b/src/applications/people/storage/PhabricatorUserEmail.php @@ -83,9 +83,8 @@ final class PhabricatorUserEmail extends PhabricatorUserDAO { */ public static function describeValidAddresses() { return pht( - "Email addresses should be in the form '%s'. The maximum ". - "length of an email address is %s character(s).", - 'user@domain.com', + 'Email addresses should be in the form "user@domain.com". The maximum '. + 'length of an email address is %s characters.', new PhutilNumber(self::MAX_ADDRESS_LENGTH)); } diff --git a/src/docs/user/configuration/configuring_accounts_and_registration.diviner b/src/docs/user/configuration/configuring_accounts_and_registration.diviner index 05d11b11f3..a56d7377cb 100644 --- a/src/docs/user/configuration/configuring_accounts_and_registration.diviner +++ b/src/docs/user/configuration/configuring_accounts_and_registration.diviner @@ -3,7 +3,8 @@ Describes how to configure user access to Phabricator. -= Overview = +Overview +======== Phabricator supports a number of login systems. You can enable or disable these systems to configure who can register for and access your install, and how users @@ -28,24 +29,37 @@ After you add a provider, you can link it to existing accounts (for example, associate an existing Phabricator account with a GitHub OAuth account) or users can use it to register new accounts (assuming you enable these options). -= Recovering Inaccessible Accounts = + +Recovering Inaccessible Accounts +================================ If you accidentally lock yourself out of Phabricator (for example, by disabling -all authentication providers), you can use the `bin/auth` -script to recover access to an account. To recover access, run: +all authentication providers), you can normally use the "send a login link" +action from the login screen to email yourself a login link and regain access +to your account. - phabricator/ $ ./bin/auth recover +If that isn't working (perhaps because you haven't configured email yet), you +can use the `bin/auth` script to recover access to an account. To recover +access, run: + +``` +phabricator/ $ ./bin/auth recover +``` ...where `` is the account username you want to recover access to. This will generate a link which will log you in as the specified user. -= Managing Accounts with the Web Console = + +Managing Accounts with the Web Console +====================================== To manage accounts from the web, login as an administrator account and go to `/people/` or click "People" on the homepage. Provided you're an admin, you'll see options to create or edit accounts. -= Manually Creating New Accounts = + +Manually Creating New Accounts +============================== There are two ways to manually create new accounts: via the web UI using the "People" application (this is easiest), or via the CLI using the @@ -60,7 +74,9 @@ the CLI. You can also use this script to make a user an administrator (if you accidentally remove your admin flag) or to create an administrative account. -= Next Steps = + +Next Steps +========== Continue by: From 541d794c13df806f916e0829fefa89230c2720cd Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 6 Feb 2019 13:11:34 -0800 Subject: [PATCH 047/245] Give ExternalAccount a providerConfigPHID, tying it to a particular provider Summary: Depends on D20111. Ref T6703. Currently, each ExternalAccount row is tied to a provider by `providerType` + `providerDomain`. This effectively prevents multiple providers of the same type, since, e.g., two LDAP providers may be on different ports on the same domain. The `domain` also isn't really a useful idea anyway because you can move which hostname an LDAP server is on, and LDAP actually uses the value `self` in all cases. Yeah, yikes. Instead, just bind each account to a particular provider. Then we can have an LDAP "alice" on seven different servers on different ports on the same machine and they can all move around and we'll still have a consistent, cohesive view of the world. (On its own, this creates some issues with the link/unlink/refresh flows. Those will be updated in followups, and doing this change in a way with no intermediate breaks would require fixing them to use IDs to reference providerType/providerDomain, then fixing this, then undoing the first fix most of the way.) Test Plan: Ran migrations, sanity-checked database. See followup changes for more comprehensive testing. Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T6703 Differential Revision: https://secure.phabricator.com/D20112 --- .../20190206.external.03.providerphid.sql | 2 ++ .../20190206.external.04.providerlink.php | 36 +++++++++++++++++++ .../PhabricatorAuthRegisterController.php | 2 +- .../auth/provider/PhabricatorAuthProvider.php | 18 +++++++--- .../PhabricatorPasswordAuthProvider.php | 8 ----- .../query/PhabricatorExternalAccountQuery.php | 20 +++++++++++ .../storage/PhabricatorExternalAccount.php | 24 +++++++------ 7 files changed, 85 insertions(+), 25 deletions(-) create mode 100644 resources/sql/autopatches/20190206.external.03.providerphid.sql create mode 100644 resources/sql/autopatches/20190206.external.04.providerlink.php diff --git a/resources/sql/autopatches/20190206.external.03.providerphid.sql b/resources/sql/autopatches/20190206.external.03.providerphid.sql new file mode 100644 index 0000000000..0b2f498e02 --- /dev/null +++ b/resources/sql/autopatches/20190206.external.03.providerphid.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_user.user_externalaccount + ADD providerConfigPHID VARBINARY(64) NOT NULL; diff --git a/resources/sql/autopatches/20190206.external.04.providerlink.php b/resources/sql/autopatches/20190206.external.04.providerlink.php new file mode 100644 index 0000000000..e4a2e2d4bf --- /dev/null +++ b/resources/sql/autopatches/20190206.external.04.providerlink.php @@ -0,0 +1,36 @@ +establishConnection('w'); +$table_name = $account_table->getTableName(); + +$config_table = new PhabricatorAuthProviderConfig(); +$config_conn = $config_table->establishConnection('w'); + +foreach (new LiskRawMigrationIterator($account_conn, $table_name) as $row) { + if (strlen($row['providerConfigPHID'])) { + continue; + } + + $config_row = queryfx_one( + $config_conn, + 'SELECT phid + FROM %R + WHERE providerType = %s AND providerDomain = %s + LIMIT 1', + $config_table, + $row['accountType'], + $row['accountDomain']); + if (!$config_row) { + continue; + } + + queryfx( + $account_conn, + 'UPDATE %R + SET providerConfigPHID = %s + WHERE id = %d', + $account_table, + $config_row['phid'], + $row['id']); +} diff --git a/src/applications/auth/controller/PhabricatorAuthRegisterController.php b/src/applications/auth/controller/PhabricatorAuthRegisterController.php index 0562a4242c..b1b2c86dc4 100644 --- a/src/applications/auth/controller/PhabricatorAuthRegisterController.php +++ b/src/applications/auth/controller/PhabricatorAuthRegisterController.php @@ -671,7 +671,7 @@ final class PhabricatorAuthRegisterController } $provider = head($providers); - $account = $provider->getDefaultExternalAccount(); + $account = $provider->newDefaultExternalAccount(); return array($account, $provider, $response); } diff --git a/src/applications/auth/provider/PhabricatorAuthProvider.php b/src/applications/auth/provider/PhabricatorAuthProvider.php index 04548d1f0c..78aeebd810 100644 --- a/src/applications/auth/provider/PhabricatorAuthProvider.php +++ b/src/applications/auth/provider/PhabricatorAuthProvider.php @@ -220,9 +220,7 @@ abstract class PhabricatorAuthProvider extends Phobject { $adapter->getAdapterDomain(), $account_id); if (!$account) { - $account = id(new PhabricatorExternalAccount()) - ->setAccountType($adapter->getAdapterType()) - ->setAccountDomain($adapter->getAdapterDomain()) + $account = $this->newExternalAccount() ->setAccountID($account_id); } @@ -299,8 +297,18 @@ abstract class PhabricatorAuthProvider extends Phobject { return false; } - public function getDefaultExternalAccount() { - throw new PhutilMethodNotImplementedException(); + public function newDefaultExternalAccount() { + return $this->newExternalAccount(); + } + + protected function newExternalAccount() { + $config = $this->getProviderConfig(); + $adapter = $this->getAdapter(); + + return id(new PhabricatorExternalAccount()) + ->setAccountType($adapter->getAdapterType()) + ->setAccountDomain($adapter->getAdapterDomain()) + ->setProviderConfigPHID($config->getPHID()); } public function getLoginOrder() { diff --git a/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php b/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php index ec5720e078..146e41706a 100644 --- a/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php +++ b/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php @@ -359,14 +359,6 @@ final class PhabricatorPasswordAuthProvider extends PhabricatorAuthProvider { return true; } - public function getDefaultExternalAccount() { - $adapter = $this->getAdapter(); - - return id(new PhabricatorExternalAccount()) - ->setAccountType($adapter->getAdapterType()) - ->setAccountDomain($adapter->getAdapterDomain()); - } - protected function willSaveAccount(PhabricatorExternalAccount $account) { parent::willSaveAccount($account); $account->setUserPHID($account->getAccountID()); diff --git a/src/applications/auth/query/PhabricatorExternalAccountQuery.php b/src/applications/auth/query/PhabricatorExternalAccountQuery.php index c4a53c12f8..f67fb4b581 100644 --- a/src/applications/auth/query/PhabricatorExternalAccountQuery.php +++ b/src/applications/auth/query/PhabricatorExternalAccountQuery.php @@ -71,6 +71,26 @@ final class PhabricatorExternalAccountQuery } protected function willFilterPage(array $accounts) { + $viewer = $this->getViewer(); + + $configs = id(new PhabricatorAuthProviderConfigQuery()) + ->setViewer($viewer) + ->withPHIDs(mpull($accounts, 'getProviderConfigPHID')) + ->execute(); + $configs = mpull($configs, null, 'getPHID'); + + foreach ($accounts as $key => $account) { + $config_phid = $account->getProviderConfigPHID(); + $config = idx($configs, $config_phid); + + if (!$config) { + unset($accounts[$key]); + continue; + } + + $account->attachProviderConfig($config); + } + if ($this->needImages) { $file_phids = mpull($accounts, 'getProfileImagePHID'); $file_phids = array_filter($file_phids); diff --git a/src/applications/people/storage/PhabricatorExternalAccount.php b/src/applications/people/storage/PhabricatorExternalAccount.php index 4bc0fcae98..bde9588339 100644 --- a/src/applications/people/storage/PhabricatorExternalAccount.php +++ b/src/applications/people/storage/PhabricatorExternalAccount.php @@ -16,8 +16,10 @@ final class PhabricatorExternalAccount extends PhabricatorUserDAO protected $accountURI; protected $profileImagePHID; protected $properties = array(); + protected $providerConfigPHID; private $profileImageFile = self::ATTACHABLE; + private $providerConfig = self::ATTACHABLE; public function getProfileImageFile() { return $this->assertAttached($this->profileImageFile); @@ -65,13 +67,6 @@ final class PhabricatorExternalAccount extends PhabricatorUserDAO ) + parent::getConfiguration(); } - public function getPhabricatorUser() { - $tmp_usr = id(new PhabricatorUser()) - ->makeEphemeral() - ->setPHID($this->getPHID()); - return $tmp_usr; - } - public function getProviderKey() { return $this->getAccountType().':'.$this->getAccountDomain(); } @@ -93,13 +88,12 @@ final class PhabricatorExternalAccount extends PhabricatorUserDAO } public function isUsableForLogin() { - $key = $this->getProviderKey(); - $provider = PhabricatorAuthProvider::getEnabledProviderByKey($key); - - if (!$provider) { + $config = $this->getProviderConfig(); + if (!$config->getIsEnabled()) { return false; } + $provider = $config->getProvider(); if (!$provider->shouldAllowLogin()) { return false; } @@ -125,6 +119,14 @@ final class PhabricatorExternalAccount extends PhabricatorUserDAO return idx($map, $type, pht('"%s" User', $type)); } + public function attachProviderConfig(PhabricatorAuthProviderConfig $config) { + $this->providerConfig = $config; + return $this; + } + + public function getProviderConfig() { + return $this->assertAttached($this->providerConfig); + } /* -( PhabricatorPolicyInterface )----------------------------------------- */ From e5ee656fff0cc0a548534912e74bfed00e472b80 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 6 Feb 2019 16:00:40 -0800 Subject: [PATCH 048/245] Make external account unlinking use account IDs, not "providerType + providerDomain" nonsense Summary: Depends on D20112. Ref T6703. When you go to unlink an account, unlink it by ID. Crazy! Test Plan: Unlinked and relinked Google accounts. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T6703 Differential Revision: https://secure.phabricator.com/D20113 --- .../PhabricatorAuthApplication.php | 2 +- .../PhabricatorAuthUnlinkController.php | 117 +++++++----------- ...abricatorExternalAccountsSettingsPanel.php | 2 +- 3 files changed, 50 insertions(+), 71 deletions(-) diff --git a/src/applications/auth/application/PhabricatorAuthApplication.php b/src/applications/auth/application/PhabricatorAuthApplication.php index 307cab61c8..0b3487b71b 100644 --- a/src/applications/auth/application/PhabricatorAuthApplication.php +++ b/src/applications/auth/application/PhabricatorAuthApplication.php @@ -61,7 +61,7 @@ final class PhabricatorAuthApplication extends PhabricatorApplication { 'start/' => 'PhabricatorAuthStartController', 'validate/' => 'PhabricatorAuthValidateController', 'finish/' => 'PhabricatorAuthFinishController', - 'unlink/(?P[^/]+)/' => 'PhabricatorAuthUnlinkController', + 'unlink/(?P\d+)/' => 'PhabricatorAuthUnlinkController', '(?Plink|refresh)/(?P[^/]+)/' => 'PhabricatorAuthLinkController', 'confirmlink/(?P[^/]+)/' diff --git a/src/applications/auth/controller/PhabricatorAuthUnlinkController.php b/src/applications/auth/controller/PhabricatorAuthUnlinkController.php index 1e3023e5d2..004ddf4f9a 100644 --- a/src/applications/auth/controller/PhabricatorAuthUnlinkController.php +++ b/src/applications/auth/controller/PhabricatorAuthUnlinkController.php @@ -3,48 +3,45 @@ final class PhabricatorAuthUnlinkController extends PhabricatorAuthController { - private $providerKey; - public function handleRequest(AphrontRequest $request) { $viewer = $this->getViewer(); - $this->providerKey = $request->getURIData('pkey'); + $id = $request->getURIData('id'); - list($type, $domain) = explode(':', $this->providerKey, 2); - - // Check that this account link actually exists. We don't require the - // provider to exist because we want users to be able to delete links to - // dead accounts if they want. - $account = id(new PhabricatorExternalAccount())->loadOneWhere( - 'accountType = %s AND accountDomain = %s AND userPHID = %s', - $type, - $domain, - $viewer->getPHID()); + $account = id(new PhabricatorExternalAccountQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); if (!$account) { - return $this->renderNoAccountErrorDialog(); + return new Aphront404Response(); } - // Check that the provider (if it exists) allows accounts to be unlinked. - $provider_key = $this->providerKey; - $provider = PhabricatorAuthProvider::getEnabledProviderByKey($provider_key); - if ($provider) { - if (!$provider->shouldAllowAccountUnlink()) { - return $this->renderNotUnlinkableErrorDialog($provider); - } + $done_uri = '/settings/panel/external/'; + + $config = $account->getProviderConfig(); + $provider = $config->getProvider(); + if (!$provider->shouldAllowAccountUnlink()) { + return $this->renderNotUnlinkableErrorDialog($provider, $done_uri); } $confirmations = $request->getStrList('confirmations'); $confirmations = array_fuse($confirmations); if (!$request->isFormPost() || !isset($confirmations['unlink'])) { - return $this->renderConfirmDialog($confirmations); + return $this->renderConfirmDialog($confirmations, $config, $done_uri); } // Check that this account isn't the only account which can be used to // login. We warn you when you remove your only login account. if ($account->isUsableForLogin()) { - $other_accounts = id(new PhabricatorExternalAccount())->loadAllWhere( - 'userPHID = %s', - $viewer->getPHID()); + $other_accounts = id(new PhabricatorExternalAccountQuery()) + ->setViewer($viewer) + ->withUserPHIDs(array($viewer->getPHID())) + ->execute(); $valid_accounts = 0; foreach ($other_accounts as $other_account) { @@ -55,7 +52,9 @@ final class PhabricatorAuthUnlinkController if ($valid_accounts < 2) { if (!isset($confirmations['only'])) { - return $this->renderOnlyUsableAccountConfirmDialog($confirmations); + return $this->renderOnlyUsableAccountConfirmDialog( + $confirmations, + $done_uri); } } } @@ -67,42 +66,27 @@ final class PhabricatorAuthUnlinkController new PhutilOpaqueEnvelope( $request->getCookie(PhabricatorCookies::COOKIE_SESSION))); - return id(new AphrontRedirectResponse())->setURI($this->getDoneURI()); - } - - private function getDoneURI() { - return '/settings/panel/external/'; - } - - private function renderNoAccountErrorDialog() { - $dialog = id(new AphrontDialogView()) - ->setUser($this->getRequest()->getUser()) - ->setTitle(pht('No Such Account')) - ->appendChild( - pht( - 'You can not unlink this account because it is not linked.')) - ->addCancelButton($this->getDoneURI()); - - return id(new AphrontDialogResponse())->setDialog($dialog); + return id(new AphrontRedirectResponse())->setURI($done_uri); } private function renderNotUnlinkableErrorDialog( - PhabricatorAuthProvider $provider) { + PhabricatorAuthProvider $provider, + $done_uri) { - $dialog = id(new AphrontDialogView()) - ->setUser($this->getRequest()->getUser()) + return $this->newDialog() ->setTitle(pht('Permanent Account Link')) ->appendChild( pht( 'You can not unlink this account because the administrator has '. - 'configured Phabricator to make links to %s accounts permanent.', + 'configured Phabricator to make links to "%s" accounts permanent.', $provider->getProviderName())) - ->addCancelButton($this->getDoneURI()); - - return id(new AphrontDialogResponse())->setDialog($dialog); + ->addCancelButton($done_uri); } - private function renderOnlyUsableAccountConfirmDialog(array $confirmations) { + private function renderOnlyUsableAccountConfirmDialog( + array $confirmations, + $done_uri) { + $confirmations[] = 'only'; return $this->newDialog() @@ -116,28 +100,23 @@ final class PhabricatorAuthUnlinkController pht( 'If you lose access to your account, you can recover access by '. 'sending yourself an email login link from the login screen.')) - ->addCancelButton($this->getDoneURI()) + ->addCancelButton($done_uri) ->addSubmitButton(pht('Unlink External Account')); } - private function renderConfirmDialog(array $confirmations) { + private function renderConfirmDialog( + array $confirmations, + PhabricatorAuthProviderConfig $config, + $done_uri) { + $confirmations[] = 'unlink'; + $provider = $config->getProvider(); - $provider_key = $this->providerKey; - $provider = PhabricatorAuthProvider::getEnabledProviderByKey($provider_key); - - if ($provider) { - $title = pht('Unlink "%s" Account?', $provider->getProviderName()); - $body = pht( - 'You will no longer be able to use your %s account to '. - 'log in to Phabricator.', - $provider->getProviderName()); - } else { - $title = pht('Unlink Account?'); - $body = pht( - 'You will no longer be able to use this account to log in '. - 'to Phabricator.'); - } + $title = pht('Unlink "%s" Account?', $provider->getProviderName()); + $body = pht( + 'You will no longer be able to use your %s account to '. + 'log in to Phabricator.', + $provider->getProviderName()); return $this->newDialog() ->setTitle($title) @@ -148,7 +127,7 @@ final class PhabricatorAuthUnlinkController 'Note: Unlinking an authentication provider will terminate any '. 'other active login sessions.')) ->addSubmitButton(pht('Unlink Account')) - ->addCancelButton($this->getDoneURI()); + ->addCancelButton($done_uri); } } diff --git a/src/applications/settings/panel/PhabricatorExternalAccountsSettingsPanel.php b/src/applications/settings/panel/PhabricatorExternalAccountsSettingsPanel.php index 29ef9fa2c7..9904c7369f 100644 --- a/src/applications/settings/panel/PhabricatorExternalAccountsSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorExternalAccountsSettingsPanel.php @@ -78,7 +78,7 @@ final class PhabricatorExternalAccountsSettingsPanel ->setIcon('fa-times') ->setWorkflow(true) ->setDisabled(!$can_unlink) - ->setHref('/auth/unlink/'.$account->getProviderKey().'/')); + ->setHref('/auth/unlink/'.$account->getID().'/')); if ($provider) { $provider->willRenderLinkedAccount($viewer, $item, $account); From d22495a820c67a7e5e156f2a8792620bea44bbdc Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 6 Feb 2019 17:12:12 -0800 Subject: [PATCH 049/245] Make external link/refresh use provider IDs, switch external account MFA to one-shot Summary: Depends on D20113. Ref T6703. Continue moving toward a future where multiple copies of a given type of provider may exist. Switch MFA from session-MFA at the start to one-shot MFA at the actual link action. Add one-shot MFA to the unlink action. This theoretically prevents an attacker from unlinking an account while you're getting coffee, registering `alIce` which they control, adding a copy of your profile picture, and then trying to trick you into writing a private note with your personal secrets or something. Test Plan: Linked and unlinked accounts. Refreshed account. Unlinked, then registered a new account. Unlinked, then relinked to my old account. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T6703 Differential Revision: https://secure.phabricator.com/D20117 --- .../PhabricatorAuthApplication.php | 2 +- .../PhabricatorAuthConfirmLinkController.php | 19 ++++++----- .../controller/PhabricatorAuthController.php | 16 ++++----- .../PhabricatorAuthLinkController.php | 33 ++++++++++--------- .../PhabricatorAuthUnlinkController.php | 10 +++++- .../query/PhabricatorExternalAccountQuery.php | 13 ++++++++ ...bricatorPeopleProfilePictureController.php | 11 +++---- ...abricatorExternalAccountsSettingsPanel.php | 32 ++++++++---------- 8 files changed, 76 insertions(+), 60 deletions(-) diff --git a/src/applications/auth/application/PhabricatorAuthApplication.php b/src/applications/auth/application/PhabricatorAuthApplication.php index 0b3487b71b..52cf01b2aa 100644 --- a/src/applications/auth/application/PhabricatorAuthApplication.php +++ b/src/applications/auth/application/PhabricatorAuthApplication.php @@ -62,7 +62,7 @@ final class PhabricatorAuthApplication extends PhabricatorApplication { 'validate/' => 'PhabricatorAuthValidateController', 'finish/' => 'PhabricatorAuthFinishController', 'unlink/(?P\d+)/' => 'PhabricatorAuthUnlinkController', - '(?Plink|refresh)/(?P[^/]+)/' + '(?Plink|refresh)/(?P\d+)/' => 'PhabricatorAuthLinkController', 'confirmlink/(?P[^/]+)/' => 'PhabricatorAuthConfirmLinkController', diff --git a/src/applications/auth/controller/PhabricatorAuthConfirmLinkController.php b/src/applications/auth/controller/PhabricatorAuthConfirmLinkController.php index 664a97885f..9ceb10df8b 100644 --- a/src/applications/auth/controller/PhabricatorAuthConfirmLinkController.php +++ b/src/applications/auth/controller/PhabricatorAuthConfirmLinkController.php @@ -20,7 +20,15 @@ final class PhabricatorAuthConfirmLinkController $panel_uri = '/settings/panel/external/'; - if ($request->isFormPost()) { + if ($request->isFormOrHisecPost()) { + $workflow_key = sprintf( + 'account.link(%s)', + $account->getPHID()); + + $hisec_token = id(new PhabricatorAuthSessionEngine()) + ->setWorkflowKey($workflow_key) + ->requireHighSecurityToken($viewer, $request, $panel_uri); + $account->setUserPHID($viewer->getPHID()); $account->save(); @@ -31,14 +39,7 @@ final class PhabricatorAuthConfirmLinkController return id(new AphrontRedirectResponse())->setURI($panel_uri); } - // TODO: Provide more information about the external account. Clicking - // through this form blindly is dangerous. - - // TODO: If the user has password authentication, require them to retype - // their password here. - - $dialog = id(new AphrontDialogView()) - ->setUser($viewer) + $dialog = $this->newDialog() ->setTitle(pht('Confirm %s Account Link', $provider->getProviderName())) ->addCancelButton($panel_uri) ->addSubmitButton(pht('Confirm Account Link')); diff --git a/src/applications/auth/controller/PhabricatorAuthController.php b/src/applications/auth/controller/PhabricatorAuthController.php index 9b7267ec96..e81301218c 100644 --- a/src/applications/auth/controller/PhabricatorAuthController.php +++ b/src/applications/auth/controller/PhabricatorAuthController.php @@ -213,19 +213,19 @@ abstract class PhabricatorAuthController extends PhabricatorController { return array($account, $provider, $response); } - $provider = PhabricatorAuthProvider::getEnabledProviderByKey( - $account->getProviderKey()); - - if (!$provider) { + $config = $account->getProviderConfig(); + if (!$config->getIsEnabled()) { $response = $this->renderError( pht( - 'The account you are attempting to register with uses a nonexistent '. - 'or disabled authentication provider (with key "%s"). An '. - 'administrator may have recently disabled this provider.', - $account->getProviderKey())); + 'The account you are attempting to register with uses a disabled '. + 'authentication provider ("%s"). An administrator may have '. + 'recently disabled this provider.', + $config->getDisplayName())); return array($account, $provider, $response); } + $provider = $config->getProvider(); + return array($account, $provider, null); } diff --git a/src/applications/auth/controller/PhabricatorAuthLinkController.php b/src/applications/auth/controller/PhabricatorAuthLinkController.php index 44176a278e..4b127b9ad1 100644 --- a/src/applications/auth/controller/PhabricatorAuthLinkController.php +++ b/src/applications/auth/controller/PhabricatorAuthLinkController.php @@ -6,14 +6,20 @@ final class PhabricatorAuthLinkController public function handleRequest(AphrontRequest $request) { $viewer = $this->getViewer(); $action = $request->getURIData('action'); - $provider_key = $request->getURIData('pkey'); - $provider = PhabricatorAuthProvider::getEnabledProviderByKey( - $provider_key); - if (!$provider) { + $id = $request->getURIData('id'); + + $config = id(new PhabricatorAuthProviderConfigQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->withIsEnabled(true) + ->executeOne(); + if (!$config) { return new Aphront404Response(); } + $provider = $config->getProvider(); + switch ($action) { case 'link': if (!$provider->shouldAllowAccountLink()) { @@ -37,15 +43,15 @@ final class PhabricatorAuthLinkController return new Aphront400Response(); } - $account = id(new PhabricatorExternalAccount())->loadOneWhere( - 'accountType = %s AND accountDomain = %s AND userPHID = %s', - $provider->getProviderType(), - $provider->getProviderDomain(), - $viewer->getPHID()); + $accounts = id(new PhabricatorExternalAccountQuery()) + ->setViewer($viewer) + ->withUserPHIDs(array($viewer->getPHID())) + ->withProviderConfigPHIDs(array($config->getPHID())) + ->execute(); switch ($action) { case 'link': - if ($account) { + if ($accounts) { return $this->renderErrorPage( pht('Account Already Linked'), array( @@ -56,7 +62,7 @@ final class PhabricatorAuthLinkController } break; case 'refresh': - if (!$account) { + if (!$accounts) { return $this->renderErrorPage( pht('No Account Linked'), array( @@ -76,11 +82,6 @@ final class PhabricatorAuthLinkController switch ($action) { case 'link': - id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( - $viewer, - $request, - $panel_uri); - $form = $provider->buildLinkForm($this); break; case 'refresh': diff --git a/src/applications/auth/controller/PhabricatorAuthUnlinkController.php b/src/applications/auth/controller/PhabricatorAuthUnlinkController.php index 004ddf4f9a..43e7b1b362 100644 --- a/src/applications/auth/controller/PhabricatorAuthUnlinkController.php +++ b/src/applications/auth/controller/PhabricatorAuthUnlinkController.php @@ -31,7 +31,7 @@ final class PhabricatorAuthUnlinkController $confirmations = $request->getStrList('confirmations'); $confirmations = array_fuse($confirmations); - if (!$request->isFormPost() || !isset($confirmations['unlink'])) { + if (!$request->isFormOrHisecPost() || !isset($confirmations['unlink'])) { return $this->renderConfirmDialog($confirmations, $config, $done_uri); } @@ -59,6 +59,14 @@ final class PhabricatorAuthUnlinkController } } + $workflow_key = sprintf( + 'account.unlink(%s)', + $account->getPHID()); + + $hisec_token = id(new PhabricatorAuthSessionEngine()) + ->setWorkflowKey($workflow_key) + ->requireHighSecurityToken($viewer, $request, $done_uri); + $account->delete(); id(new PhabricatorAuthSessionEngine())->terminateLoginSessions( diff --git a/src/applications/auth/query/PhabricatorExternalAccountQuery.php b/src/applications/auth/query/PhabricatorExternalAccountQuery.php index f67fb4b581..1c5b3fe14f 100644 --- a/src/applications/auth/query/PhabricatorExternalAccountQuery.php +++ b/src/applications/auth/query/PhabricatorExternalAccountQuery.php @@ -21,6 +21,7 @@ final class PhabricatorExternalAccountQuery private $userPHIDs; private $needImages; private $accountSecrets; + private $providerConfigPHIDs; public function withUserPHIDs(array $user_phids) { $this->userPHIDs = $user_phids; @@ -62,6 +63,11 @@ final class PhabricatorExternalAccountQuery return $this; } + public function withProviderConfigPHIDs(array $phids) { + $this->providerConfigPHIDs = $phids; + return $this; + } + public function newResultObject() { return new PhabricatorExternalAccount(); } @@ -181,6 +187,13 @@ final class PhabricatorExternalAccountQuery $this->accountSecrets); } + if ($this->providerConfigPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'providerConfigPHID IN (%Ls)', + $this->providerConfigPHIDs); + } + return $where; } diff --git a/src/applications/people/controller/PhabricatorPeopleProfilePictureController.php b/src/applications/people/controller/PhabricatorPeopleProfilePictureController.php index 5cab924255..92bb2e0b86 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfilePictureController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfilePictureController.php @@ -157,13 +157,10 @@ final class PhabricatorPeopleProfilePictureController continue; } - $provider = PhabricatorAuthProvider::getEnabledProviderByKey( - $account->getProviderKey()); - if ($provider) { - $tip = pht('Picture From %s', $provider->getProviderName()); - } else { - $tip = pht('Picture From External Account'); - } + $config = $account->getProviderConfig(); + $provider = $config->getProvider(); + + $tip = pht('Picture From %s', $provider->getProviderName()); if ($file->isTransformableImage()) { $images[$file->getPHID()] = array( diff --git a/src/applications/settings/panel/PhabricatorExternalAccountsSettingsPanel.php b/src/applications/settings/panel/PhabricatorExternalAccountsSettingsPanel.php index 9904c7369f..60389e1590 100644 --- a/src/applications/settings/panel/PhabricatorExternalAccountsSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorExternalAccountsSettingsPanel.php @@ -44,17 +44,13 @@ final class PhabricatorExternalAccountsSettingsPanel foreach ($accounts as $account) { $item = new PHUIObjectItemView(); - $provider = idx($providers, $account->getProviderKey()); - if ($provider) { - $item->setHeader($provider->getProviderName()); - $can_unlink = $provider->shouldAllowAccountUnlink(); - if (!$can_unlink) { - $item->addAttribute(pht('Permanently Linked')); - } - } else { - $item->setHeader( - pht('Unknown Account ("%s")', $account->getProviderKey())); - $can_unlink = true; + $config = $account->getProviderConfig(); + $provider = $config->getProvider(); + + $item->setHeader($provider->getProviderName()); + $can_unlink = $provider->shouldAllowAccountUnlink(); + if (!$can_unlink) { + $item->addAttribute(pht('Permanently Linked')); } $can_login = $account->isUsableForLogin(); @@ -65,12 +61,12 @@ final class PhabricatorExternalAccountsSettingsPanel 'account provider).')); } - $can_refresh = $provider && $provider->shouldAllowAccountRefresh(); + $can_refresh = $provider->shouldAllowAccountRefresh(); if ($can_refresh) { $item->addAction( id(new PHUIListItemView()) ->setIcon('fa-refresh') - ->setHref('/auth/refresh/'.$account->getProviderKey().'/')); + ->setHref('/auth/refresh/'.$config->getID().'/')); } $item->addAction( @@ -94,14 +90,15 @@ final class PhabricatorExternalAccountsSettingsPanel ->setNoDataString( pht('Your account is linked with all available providers.')); - $accounts = mpull($accounts, null, 'getProviderKey'); - $configs = id(new PhabricatorAuthProviderConfigQuery()) ->setViewer($viewer) ->withIsEnabled(true) ->execute(); $configs = msort($configs, 'getSortVector'); + $account_map = mgroup($accounts, 'getProviderConfigPHID'); + + foreach ($configs as $config) { $provider = $config->getProvider(); @@ -110,12 +107,11 @@ final class PhabricatorExternalAccountsSettingsPanel } // Don't show the user providers they already have linked. - $provider_key = $config->getProvider()->getProviderKey(); - if (isset($accounts[$provider_key])) { + if (isset($account_map[$config->getPHID()])) { continue; } - $link_uri = '/auth/link/'.$provider->getProviderKey().'/'; + $link_uri = '/auth/link/'.$config->getID().'/'; $link_button = id(new PHUIButtonView()) ->setTag('a') From 3f35c0068ad1e458565fb32e20f23d9a9feef6b4 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 6 Feb 2019 17:55:31 -0800 Subject: [PATCH 050/245] Allow users to register with non-registration providers if they are invited to an instance Summary: Depends on D20117. Fixes T10071. When you're sent an email invitation, it's intended to allow you to register an account even if you otherwise could not (see D11737). Some time between D11737 and today, this stopped working (or perhaps it never worked and I got things wrong in D11737). I think this actually ended up not mattering for us, given the way Phacility auth was ultimately built. This feature generally seems reasonable, though, and probably //should// work. Make it work in the "password" and "oauth" cases, at least. This may //still// not work for LDAP, but testing that is nontrivial. Test Plan: - Enabled only passwords, turned off registration, sent an invite, registered with a password. - Enabled only Google OAuth, turned off registration, sent an invite, registered with Google OAuth. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T10071 Differential Revision: https://secure.phabricator.com/D20118 --- .../PhabricatorAuthLoginController.php | 3 ++- .../PhabricatorAuthRegisterController.php | 19 +++++++++++-------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/applications/auth/controller/PhabricatorAuthLoginController.php b/src/applications/auth/controller/PhabricatorAuthLoginController.php index 54649a6a69..e7dabd9340 100644 --- a/src/applications/auth/controller/PhabricatorAuthLoginController.php +++ b/src/applications/auth/controller/PhabricatorAuthLoginController.php @@ -35,6 +35,7 @@ final class PhabricatorAuthLoginController return $response; } + $invite = $this->loadInvite(); $provider = $this->provider; try { @@ -103,7 +104,7 @@ final class PhabricatorAuthLoginController // The account is not yet attached to a Phabricator user, so this is // either a registration or an account link request. if (!$viewer->isLoggedIn()) { - if ($provider->shouldAllowRegistration()) { + if ($provider->shouldAllowRegistration() || $invite) { return $this->processRegisterUser($account); } else { return $this->renderError( diff --git a/src/applications/auth/controller/PhabricatorAuthRegisterController.php b/src/applications/auth/controller/PhabricatorAuthRegisterController.php index b1b2c86dc4..5a46d0e604 100644 --- a/src/applications/auth/controller/PhabricatorAuthRegisterController.php +++ b/src/applications/auth/controller/PhabricatorAuthRegisterController.php @@ -11,10 +11,12 @@ final class PhabricatorAuthRegisterController $viewer = $this->getViewer(); $account_key = $request->getURIData('akey'); - if ($request->getUser()->isLoggedIn()) { + if ($viewer->isLoggedIn()) { return id(new AphrontRedirectResponse())->setURI('/'); } + $invite = $this->loadInvite(); + $is_setup = false; if (strlen($account_key)) { $result = $this->loadAccountForRegistrationOrLinking($account_key); @@ -27,7 +29,7 @@ final class PhabricatorAuthRegisterController $is_default = true; $is_setup = true; } else { - list($account, $provider, $response) = $this->loadDefaultAccount(); + list($account, $provider, $response) = $this->loadDefaultAccount($invite); $is_default = true; } @@ -35,8 +37,6 @@ final class PhabricatorAuthRegisterController return $response; } - $invite = $this->loadInvite(); - if (!$is_setup) { if (!$provider->shouldAllowRegistration()) { if ($invite) { @@ -643,17 +643,20 @@ final class PhabricatorAuthRegisterController ->appendChild($view); } - private function loadDefaultAccount() { + private function loadDefaultAccount($invite) { $providers = PhabricatorAuthProvider::getAllEnabledProviders(); $account = null; $provider = null; $response = null; foreach ($providers as $key => $candidate_provider) { - if (!$candidate_provider->shouldAllowRegistration()) { - unset($providers[$key]); - continue; + if (!$invite) { + if (!$candidate_provider->shouldAllowRegistration()) { + unset($providers[$key]); + continue; + } } + if (!$candidate_provider->isDefaultRegistrationProvider()) { unset($providers[$key]); } From 5a89da12e2931bc8dd2a787cd2d458a56d25cffe Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 6 Feb 2019 19:08:03 -0800 Subject: [PATCH 051/245] When users have no password on their account, guide them through the "reset password" flow in the guise of "set password" Summary: Depends on D20119. Fixes T9512. When you don't have a password on your account, the "Password" panel in Settings is non-obviously useless: you can't provide an old password, so you can't change your password. The correct remedy is to "Forgot password?" and go through the password reset flow. However, we don't guide you to this and it isn't really self-evident. Instead: - Guide users to the password reset flow. - Make it work when you're already logged in. - Skin it as a "set password" flow. We're still requiring you to prove you own the email associated with your account. This is a pretty weak requirement, but maybe stops attackers who use the computer at the library after you do in some bizarre emergency and forget to log out? It would probably be fine to just let users "set password", this mostly just keeps us from having two different pieces of code responsible for setting passwords. Test Plan: - Set password as a logged-in user. - Reset password on the normal flow as a logged-out user. Reviewers: amckinley Reviewed By: amckinley Subscribers: revi Maniphest Tasks: T9512 Differential Revision: https://secure.phabricator.com/D20120 --- .../PhabricatorAuthOneTimeLoginController.php | 26 ++++- .../PhabricatorEmailLoginController.php | 107 +++++++++++++----- .../PhabricatorPasswordSettingsPanel.php | 46 ++++++-- 3 files changed, 134 insertions(+), 45 deletions(-) diff --git a/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php b/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php index f51f379d2b..26f8785c0a 100644 --- a/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php +++ b/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php @@ -14,11 +14,6 @@ final class PhabricatorAuthOneTimeLoginController $key = $request->getURIData('key'); $email_id = $request->getURIData('emailID'); - if ($request->getUser()->isLoggedIn()) { - return $this->renderError( - pht('You are already logged in.')); - } - $target_user = id(new PhabricatorPeopleQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withIDs(array($id)) @@ -27,6 +22,19 @@ final class PhabricatorAuthOneTimeLoginController return new Aphront404Response(); } + // NOTE: We allow you to use a one-time login link for your own current + // login account. This supports the "Set Password" flow. + + $is_logged_in = false; + if ($viewer->isLoggedIn()) { + if ($viewer->getPHID() !== $target_user->getPHID()) { + return $this->renderError( + pht('You are already logged in.')); + } else { + $is_logged_in = true; + } + } + // NOTE: As a convenience to users, these one-time login URIs may also // be associated with an email address which will be verified when the // URI is used. @@ -100,7 +108,7 @@ final class PhabricatorAuthOneTimeLoginController ->addCancelButton('/'); } - if ($request->isFormPost()) { + if ($request->isFormPost() || $is_logged_in) { // If we have an email bound into this URI, verify email so that clicking // the link in the "Welcome" email is good enough, without requiring users // to go through a second round of email verification. @@ -121,6 +129,12 @@ final class PhabricatorAuthOneTimeLoginController $next_uri = $this->getNextStepURI($target_user); + // If the user is already logged in, we're just doing a "password set" + // flow. Skip directly to the next step. + if ($is_logged_in) { + return id(new AphrontRedirectResponse())->setURI($next_uri); + } + PhabricatorCookies::setNextURICookie($request, $next_uri, $force = true); $force_full_session = false; diff --git a/src/applications/auth/controller/PhabricatorEmailLoginController.php b/src/applications/auth/controller/PhabricatorEmailLoginController.php index eef30e6989..76b288f059 100644 --- a/src/applications/auth/controller/PhabricatorEmailLoginController.php +++ b/src/applications/auth/controller/PhabricatorEmailLoginController.php @@ -9,20 +9,38 @@ final class PhabricatorEmailLoginController public function handleRequest(AphrontRequest $request) { $viewer = $this->getViewer(); + $is_logged_in = $viewer->isLoggedIn(); $e_email = true; $e_captcha = true; $errors = array(); - $v_email = $request->getStr('email'); + if ($is_logged_in) { + if (!$this->isPasswordAuthEnabled()) { + return $this->newDialog() + ->setTitle(pht('No Password Auth')) + ->appendParagraph( + pht( + 'Password authentication is not enabled and you are already '. + 'logged in. There is nothing for you here.')) + ->addCancelButton('/', pht('Continue')); + } + + $v_email = $viewer->loadPrimaryEmailAddress(); + } else { + $v_email = $request->getStr('email'); + } + if ($request->isFormPost()) { $e_email = null; $e_captcha = pht('Again'); - $captcha_ok = AphrontFormRecaptchaControl::processCaptcha($request); - if (!$captcha_ok) { - $errors[] = pht('Captcha response is incorrect, try again.'); - $e_captcha = pht('Invalid'); + if (!$is_logged_in) { + $captcha_ok = AphrontFormRecaptchaControl::processCaptcha($request); + if (!$captcha_ok) { + $errors[] = pht('Captcha response is incorrect, try again.'); + $e_captcha = pht('Invalid'); + } } if (!strlen($v_email)) { @@ -76,10 +94,24 @@ final class PhabricatorEmailLoginController } if (!$errors) { - $body = $this->newAccountLoginMailBody($target_user); + $body = $this->newAccountLoginMailBody( + $target_user, + $is_logged_in); + + if ($is_logged_in) { + $subject = pht('[Phabricator] Account Password Link'); + $instructions = pht( + 'An email has been sent containing a link you can use to set '. + 'a password for your account.'); + } else { + $subject = pht('[Phabricator] Account Login Link'); + $instructions = pht( + 'An email has been sent containing a link you can use to log '. + 'in to your account.'); + } $mail = id(new PhabricatorMetaMTAMail()) - ->setSubject(pht('[Phabricator] Account Login Link')) + ->setSubject($subject) ->setForceDelivery(true) ->addRawTos(array($target_email->getAddress())) ->setBody($body) @@ -88,8 +120,7 @@ final class PhabricatorEmailLoginController return $this->newDialog() ->setTitle(pht('Check Your Email')) ->setShortTitle(pht('Email Sent')) - ->appendParagraph( - pht('An email has been sent with a link you can use to log in.')) + ->appendParagraph($instructions) ->addCancelButton('/', pht('Done')); } } @@ -99,33 +130,47 @@ final class PhabricatorEmailLoginController ->setViewer($viewer); if ($this->isPasswordAuthEnabled()) { - $form->appendRemarkupInstructions( - pht( - 'To reset your password, provide your email address. An email '. - 'with a login link will be sent to you.')); + if ($is_logged_in) { + $title = pht('Set Password'); + $form->appendRemarkupInstructions( + pht( + 'A password reset link will be sent to your primary email '. + 'address. Follow the link to set an account password.')); + } else { + $title = pht('Password Reset'); + $form->appendRemarkupInstructions( + pht( + 'To reset your password, provide your email address. An email '. + 'with a login link will be sent to you.')); + } } else { + $title = pht('Email Login'); $form->appendRemarkupInstructions( pht( 'To access your account, provide your email address. An email '. 'with a login link will be sent to you.')); } + if ($is_logged_in) { + $address_control = new AphrontFormStaticControl(); + } else { + $address_control = id(new AphrontFormTextControl()) + ->setName('email') + ->setError($e_email); + } + + $address_control + ->setLabel(pht('Email Address')) + ->setValue($v_email); + $form - ->appendControl( - id(new AphrontFormTextControl()) - ->setLabel(pht('Email Address')) - ->setName('email') - ->setValue($v_email) - ->setError($e_email)) - ->appendControl( + ->appendControl($address_control); + + if (!$is_logged_in) { + $form->appendControl( id(new AphrontFormRecaptchaControl()) ->setLabel(pht('Captcha')) ->setError($e_captcha)); - - if ($this->isPasswordAuthEnabled()) { - $title = pht('Password Reset'); - } else { - $title = pht('Email Login'); } return $this->newDialog() @@ -137,7 +182,10 @@ final class PhabricatorEmailLoginController ->addSubmitButton(pht('Send Email')); } - private function newAccountLoginMailBody(PhabricatorUser $user) { + private function newAccountLoginMailBody( + PhabricatorUser $user, + $is_logged_in) { + $engine = new PhabricatorAuthSessionEngine(); $uri = $engine->getOneTimeLoginURI( $user, @@ -148,7 +196,12 @@ final class PhabricatorEmailLoginController $have_passwords = $this->isPasswordAuthEnabled(); if ($have_passwords) { - if ($is_serious) { + if ($is_logged_in) { + $body = pht( + 'You can use this link to set a password on your account:'. + "\n\n %s\n", + $uri); + } else if ($is_serious) { $body = pht( "You can use this link to reset your Phabricator password:". "\n\n %s\n", diff --git a/src/applications/settings/panel/PhabricatorPasswordSettingsPanel.php b/src/applications/settings/panel/PhabricatorPasswordSettingsPanel.php index 37393d5d4f..77f32f977d 100644 --- a/src/applications/settings/panel/PhabricatorPasswordSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorPasswordSettingsPanel.php @@ -34,11 +34,6 @@ final class PhabricatorPasswordSettingsPanel extends PhabricatorSettingsPanel { $content_source = PhabricatorContentSource::newFromRequest($request); - $token = id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( - $viewer, - $request, - '/settings/'); - $min_len = PhabricatorEnv::getEnvConfig('account.minimum-password-length'); $min_len = (int)$min_len; @@ -55,20 +50,25 @@ final class PhabricatorPasswordSettingsPanel extends PhabricatorSettingsPanel { ->withPasswordTypes(array($account_type)) ->withIsRevoked(false) ->execute(); - if ($password_objects) { - $password_object = head($password_objects); - } else { - $password_object = PhabricatorAuthPassword::initializeNewPassword( - $user, - $account_type); + if (!$password_objects) { + return $this->newSetPasswordView($request); } + $password_object = head($password_objects); $e_old = true; $e_new = true; $e_conf = true; $errors = array(); - if ($request->isFormPost()) { + if ($request->isFormOrHisecPost()) { + $workflow_key = sprintf( + 'password.change(%s)', + $user->getPHID()); + + $hisec_token = id(new PhabricatorAuthSessionEngine()) + ->setWorkflowKey($workflow_key) + ->requireHighSecurityToken($viewer, $request, '/settings/'); + // Rate limit guesses about the old password. This page requires MFA and // session compromise already, so this is mostly just to stop researchers // from reporting this as a vulnerability. @@ -218,5 +218,27 @@ final class PhabricatorPasswordSettingsPanel extends PhabricatorSettingsPanel { ); } + private function newSetPasswordView(AphrontRequest $request) { + $viewer = $request->getUser(); + $user = $this->getUser(); + + $form = id(new AphrontFormView()) + ->setViewer($viewer) + ->appendRemarkupInstructions( + pht( + 'Your account does not currently have a password set. You can '. + 'choose a password by performing a password reset.')) + ->appendControl( + id(new AphrontFormSubmitControl()) + ->addCancelButton('/login/email/', pht('Reset Password'))); + + $form_box = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Set Password')) + ->setBackground(PHUIObjectBoxView::WHITE_CONFIG) + ->setForm($form); + + return $form_box; + } + } From 7d6d2c128a9a7dbf788bb5d9b541e7e31480f118 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 7 Feb 2019 10:27:17 -0800 Subject: [PATCH 052/245] Make "bin/audit delete" synchronize commit audit status, and improve "bin/audit synchronize" documentation Summary: Depends on D20126. See PHI1056. Ref T13244. - `bin/audit delete` destroys audit requests, but does not update the overall audit state for associated commits. For example, if you destroy all audit requests for a commit, it does not move to "No Audit Required". - `bin/audit synchronize` does this synchronize step, but is poorly documented. Make `bin/audit delete` synchronize affected commits. Document `bin/audit synchronize` better. There's some reasonable argument that `bin/audit synchronize` perhaps shouldn't exist, but it does let you recover from an accidentally (or intentionally) mangled database state. For now, let it live. Test Plan: - Ran `bin/audit delete`, saw audits destroyed and affected commits synchornized. - Ran `bin/audit synchronize`, saw behavior unchanged. - Ran `bin/audit help`, got better help. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13244 Differential Revision: https://secure.phabricator.com/D20127 --- ...abricatorAuditManagementDeleteWorkflow.php | 106 ++++++++++++------ .../PhabricatorAuditManagementWorkflow.php | 35 ++++++ ...atorAuditSynchronizeManagementWorkflow.php | 43 ++----- src/docs/user/userguide/audit.diviner | 9 +- 4 files changed, 120 insertions(+), 73 deletions(-) diff --git a/src/applications/audit/management/PhabricatorAuditManagementDeleteWorkflow.php b/src/applications/audit/management/PhabricatorAuditManagementDeleteWorkflow.php index 23583422fb..7d14df7239 100644 --- a/src/applications/audit/management/PhabricatorAuditManagementDeleteWorkflow.php +++ b/src/applications/audit/management/PhabricatorAuditManagementDeleteWorkflow.php @@ -105,10 +105,10 @@ final class PhabricatorAuditManagementDeleteWorkflow $query->withPHIDs(mpull($commits, 'getPHID')); } - $commits = $query->execute(); - $commits = mpull($commits, null, 'getPHID'); + $commit_iterator = new PhabricatorQueryIterator($query); + $audits = array(); - foreach ($commits as $commit) { + foreach ($commit_iterator as $commit) { $commit_audits = $commit->getAudits(); foreach ($commit_audits as $key => $audit) { if ($id_map && empty($id_map[$audit->getID()])) { @@ -131,51 +131,87 @@ final class PhabricatorAuditManagementDeleteWorkflow continue; } } - $audits[] = $commit_audits; - } - $audits = array_mergev($audits); - $console = PhutilConsole::getConsole(); + if (!$commit_audits) { + continue; + } - if (!$audits) { - $console->writeErr("%s\n", pht('No audits match the query.')); - return 0; - } + $handles = id(new PhabricatorHandleQuery()) + ->setViewer($viewer) + ->withPHIDs(mpull($commit_audits, 'getAuditorPHID')) + ->execute(); - $handles = id(new PhabricatorHandleQuery()) - ->setViewer($this->getViewer()) - ->withPHIDs(mpull($audits, 'getAuditorPHID')) - ->execute(); + foreach ($commit_audits as $audit) { + $audit_id = $audit->getID(); - - foreach ($audits as $audit) { - $commit = $commits[$audit->getCommitPHID()]; - - $console->writeOut( - "%s\n", - sprintf( + $description = sprintf( '%10d %-16s %-16s %s: %s', - $audit->getID(), + $audit_id, $handles[$audit->getAuditorPHID()]->getName(), PhabricatorAuditStatusConstants::getStatusName( $audit->getAuditStatus()), $commit->getRepository()->formatCommitName( $commit->getCommitIdentifier()), - trim($commit->getSummary()))); + trim($commit->getSummary())); + + $audits[] = array( + 'auditID' => $audit_id, + 'commitPHID' => $commit->getPHID(), + 'description' => $description, + ); + } } - if (!$is_dry_run) { - $message = pht( - 'Really delete these %d audit(s)? They will be permanently deleted '. - 'and can not be recovered.', - count($audits)); - if ($console->confirm($message)) { - foreach ($audits as $audit) { - $id = $audit->getID(); - $console->writeOut("%s\n", pht('Deleting audit %d...', $id)); - $audit->delete(); - } + if (!$audits) { + echo tsprintf( + "%s\n", + pht('No audits match the query.')); + return 0; + } + + foreach ($audits as $audit_spec) { + echo tsprintf( + "%s\n", + $audit_spec['description']); + } + + if ($is_dry_run) { + echo tsprintf( + "%s\n", + pht('This is a dry run, so no changes will be made.')); + return 0; + } + + $message = pht( + 'Really delete these %s audit(s)? They will be permanently deleted '. + 'and can not be recovered.', + phutil_count($audits)); + if (!phutil_console_confirm($message)) { + echo tsprintf( + "%s\n", + pht('User aborted the workflow.')); + return 1; + } + + $audits_by_commit = igroup($audits, 'commitPHID'); + foreach ($audits_by_commit as $commit_phid => $audit_specs) { + $audit_ids = ipull($audit_specs, 'auditID'); + + $audits = id(new PhabricatorRepositoryAuditRequest())->loadAllWhere( + 'id IN (%Ld)', + $audit_ids); + + foreach ($audits as $audit) { + $id = $audit->getID(); + + echo tsprintf( + "%s\n", + pht('Deleting audit %d...', $id)); + + $audit->delete(); } + + $this->synchronizeCommitAuditState($commit_phid); } return 0; diff --git a/src/applications/audit/management/PhabricatorAuditManagementWorkflow.php b/src/applications/audit/management/PhabricatorAuditManagementWorkflow.php index 6112a38e1d..b9d90bddc8 100644 --- a/src/applications/audit/management/PhabricatorAuditManagementWorkflow.php +++ b/src/applications/audit/management/PhabricatorAuditManagementWorkflow.php @@ -87,4 +87,39 @@ abstract class PhabricatorAuditManagementWorkflow return $commits; } + protected function synchronizeCommitAuditState($commit_phid) { + $viewer = $this->getViewer(); + + $commit = id(new DiffusionCommitQuery()) + ->setViewer($viewer) + ->withPHIDs(array($commit_phid)) + ->needAuditRequests(true) + ->executeOne(); + if (!$commit) { + return; + } + + $old_status = $commit->getAuditStatusObject(); + $commit->updateAuditStatus($commit->getAudits()); + $new_status = $commit->getAuditStatusObject(); + + if ($old_status->getKey() == $new_status->getKey()) { + echo tsprintf( + "%s\n", + pht( + 'No synchronization changes for "%s".', + $commit->getDisplayName())); + } else { + echo tsprintf( + "%s\n", + pht( + 'Synchronizing "%s": "%s" -> "%s".', + $commit->getDisplayName(), + $old_status->getName(), + $new_status->getName())); + + $commit->save(); + } + } + } diff --git a/src/applications/audit/management/PhabricatorAuditSynchronizeManagementWorkflow.php b/src/applications/audit/management/PhabricatorAuditSynchronizeManagementWorkflow.php index 96d06e65c2..abd0a3c637 100644 --- a/src/applications/audit/management/PhabricatorAuditSynchronizeManagementWorkflow.php +++ b/src/applications/audit/management/PhabricatorAuditSynchronizeManagementWorkflow.php @@ -6,8 +6,16 @@ final class PhabricatorAuditSynchronizeManagementWorkflow protected function didConstruct() { $this ->setName('synchronize') - ->setExamples('**synchronize** ...') - ->setSynopsis(pht('Update audit status for commits.')) + ->setExamples( + "**synchronize** __repository__ ...\n". + "**synchronize** __commit__ ...\n". + "**synchronize** --all") + ->setSynopsis( + pht( + 'Update commits to make their summary audit state reflect the '. + 'state of their actual audit requests. This can fix inconsistencies '. + 'in database state if audit requests have been mangled '. + 'accidentally (or on purpose).')) ->setArguments( array_merge( $this->getCommitConstraintArguments(), @@ -21,36 +29,7 @@ final class PhabricatorAuditSynchronizeManagementWorkflow foreach ($objects as $object) { $commits = $this->loadCommitsForConstraintObject($object); foreach ($commits as $commit) { - $commit = id(new DiffusionCommitQuery()) - ->setViewer($viewer) - ->withPHIDs(array($commit->getPHID())) - ->needAuditRequests(true) - ->executeOne(); - if (!$commit) { - continue; - } - - $old_status = $commit->getAuditStatusObject(); - $commit->updateAuditStatus($commit->getAudits()); - $new_status = $commit->getAuditStatusObject(); - - if ($old_status->getKey() == $new_status->getKey()) { - echo tsprintf( - "%s\n", - pht( - 'No changes for "%s".', - $commit->getDisplayName())); - } else { - echo tsprintf( - "%s\n", - pht( - 'Updating "%s": "%s" -> "%s".', - $commit->getDisplayName(), - $old_status->getName(), - $new_status->getName())); - - $commit->save(); - } + $this->synchronizeCommitAuditState($commit->getPHID()); } } } diff --git a/src/docs/user/userguide/audit.diviner b/src/docs/user/userguide/audit.diviner index 0d10867906..5223f6c969 100644 --- a/src/docs/user/userguide/audit.diviner +++ b/src/docs/user/userguide/audit.diviner @@ -175,16 +175,13 @@ You can use this command to forcibly delete requests which may have triggered incorrectly (for example, because a package or Herald rule was configured in an overbroad way). -After deleting audits, you may want to run `bin/audit synchronize` to -synchronize audit state. - **Synchronize Audit State**: Synchronize the audit state of commits to the current open audit requests with `bin/audit synchronize`. Normally, overall audit state is automatically kept up to date as changes are -made to an audit. However, if you delete audits or manually update the database -to make changes to audit request state, the state of corresponding commits may -no longer be correct. +made to an audit. However, if you manually update the database to make changes +to audit request state, the state of corresponding commits may no longer be +consistent. This command will update commits so their overall audit state reflects the cumulative state of their actual audit requests. From a7bf279fd517bbc914b1116563119c7a7c1a7be2 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 12 Feb 2019 15:24:45 -0800 Subject: [PATCH 053/245] Don't try to publish build results to bare diffs Summary: See . If you run builds against a diff which is not attached to a revision (this is unusual) we still try to publish to the associated revision. This won't work since there is no associated revision. Since bare diffs don't really have a timeline, just publish nowhere for now. Test Plan: - Created a diff. - Did not attach it to a revision. - Created a build plan with "make http request + wait for response". - Manually ran the build plan against the bare diff. - Used `bin/phd debug task` to run the build and hit a "revision not attached" exception during publishing. - Applied patch. - Ran `bin/phd debug task`, got clean (no-op) publish. - Sent build a failure message with "harbormaster.sendmessage", got a failed build. This isn't a real workflow, but shouldn't fail. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20156 --- .../harbormaster/DifferentialBuildableEngine.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/applications/differential/harbormaster/DifferentialBuildableEngine.php b/src/applications/differential/harbormaster/DifferentialBuildableEngine.php index 8554f7be25..8565c2dcad 100644 --- a/src/applications/differential/harbormaster/DifferentialBuildableEngine.php +++ b/src/applications/differential/harbormaster/DifferentialBuildableEngine.php @@ -7,7 +7,11 @@ final class DifferentialBuildableEngine $object = $this->getObject(); if ($object instanceof DifferentialDiff) { - return $object->getRevision(); + if ($object->getRevisionID()) { + return $object->getRevision(); + } else { + return null; + } } return $object; From 991368128e4dc7819f0772f25f7cca4a6a428c41 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 13 Feb 2019 08:56:54 -0800 Subject: [PATCH 054/245] Bump the markup cache version for URI changes Summary: Ref T13250. Ref T13249. Some remarkup rules, including `{image ...}` and `{meme ...}`, may cache URIs as objects because the remarkup cache is `serialize()`-based. URI objects with `query` cached as a key-value map are no longer valid and can raise `__toString()` fatals. Bump the cache version to purge them out of the cache. Test Plan: See PHI1074. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13250, T13249 Differential Revision: https://secure.phabricator.com/D20160 --- src/infrastructure/markup/PhabricatorMarkupEngine.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/infrastructure/markup/PhabricatorMarkupEngine.php b/src/infrastructure/markup/PhabricatorMarkupEngine.php index 868bbc5676..3a63cb97a6 100644 --- a/src/infrastructure/markup/PhabricatorMarkupEngine.php +++ b/src/infrastructure/markup/PhabricatorMarkupEngine.php @@ -42,7 +42,7 @@ final class PhabricatorMarkupEngine extends Phobject { private $objects = array(); private $viewer; private $contextObject; - private $version = 17; + private $version = 18; private $engineCaches = array(); private $auxiliaryConfig = array(); From 9a9fa8bed283043815bf427562922275f93ab5fe Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 13 Feb 2019 05:14:43 -0800 Subject: [PATCH 055/245] Rate limit attempts to add payment methods in Phortune Summary: Ref T13249. See D20132. Although we're probably a poor way to validate a big list of stolen cards in practice in production today (it's very hard to quickly generate a large number of small charges), putting rate limiting on "Add Payment Method" is generally reasonable, can't really hurt anything (no legitimate user will ever hit this limit), and might frustrate attackers in the future if it becomes easier to generate ad-hoc charges (for example, if we run a deal on support pacts and reduce their cost from $1,000 to $1). Test Plan: Reduced limit to 4 / hour, tried to add a card several times, got rate limited. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13249 Differential Revision: https://secure.phabricator.com/D20158 --- src/__phutil_library_map__.php | 2 ++ .../action/PhortuneAddPaymentMethodAction.php | 22 +++++++++++++++++++ .../PhortunePaymentMethodCreateController.php | 9 ++++++++ 3 files changed, 33 insertions(+) create mode 100644 src/applications/phortune/action/PhortuneAddPaymentMethodAction.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 6c21edc0da..8f06586a68 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4986,6 +4986,7 @@ phutil_register_library_map(array( 'PhortuneAccountViewController' => 'applications/phortune/controller/account/PhortuneAccountViewController.php', 'PhortuneAdHocCart' => 'applications/phortune/cart/PhortuneAdHocCart.php', 'PhortuneAdHocProduct' => 'applications/phortune/product/PhortuneAdHocProduct.php', + 'PhortuneAddPaymentMethodAction' => 'applications/phortune/action/PhortuneAddPaymentMethodAction.php', 'PhortuneCart' => 'applications/phortune/storage/PhortuneCart.php', 'PhortuneCartAcceptController' => 'applications/phortune/controller/cart/PhortuneCartAcceptController.php', 'PhortuneCartCancelController' => 'applications/phortune/controller/cart/PhortuneCartCancelController.php', @@ -11227,6 +11228,7 @@ phutil_register_library_map(array( 'PhortuneAccountViewController' => 'PhortuneAccountProfileController', 'PhortuneAdHocCart' => 'PhortuneCartImplementation', 'PhortuneAdHocProduct' => 'PhortuneProductImplementation', + 'PhortuneAddPaymentMethodAction' => 'PhabricatorSystemAction', 'PhortuneCart' => array( 'PhortuneDAO', 'PhabricatorApplicationTransactionInterface', diff --git a/src/applications/phortune/action/PhortuneAddPaymentMethodAction.php b/src/applications/phortune/action/PhortuneAddPaymentMethodAction.php new file mode 100644 index 0000000000..09a8cd2f5d --- /dev/null +++ b/src/applications/phortune/action/PhortuneAddPaymentMethodAction.php @@ -0,0 +1,22 @@ +setProviderPHID($provider->getProviderConfig()->getPHID()) ->setStatus(PhortunePaymentMethod::STATUS_ACTIVE); + // Limit the rate at which you can attempt to add payment methods. This + // is intended as a line of defense against using Phortune to validate a + // large list of stolen credit card numbers. + + PhabricatorSystemActionEngine::willTakeAction( + array($viewer->getPHID()), + new PhortuneAddPaymentMethodAction(), + 1); + if (!$errors) { $errors = $this->processClientErrors( $provider, From 88d5233b7776cae0fcedf85fc9ce22615c036d92 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 13 Feb 2019 04:38:41 -0800 Subject: [PATCH 056/245] Fix specifications of some "Visual Only" elements Summary: See PHI823. These got "visual-only" but should acutally get "aural => false" to pick up "aria-hidden". Test Plan: Viewed page source, saw both "visual-only" and "aria-hidden". Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20157 --- .../view/PhabricatorApplicationTransactionCommentView.php | 5 +++-- src/view/phui/PHUIHeadThingView.php | 5 +++-- src/view/phui/PHUITimelineEventView.php | 5 +++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php b/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php index f6a27d4bcd..1fb850887f 100644 --- a/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php +++ b/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php @@ -254,11 +254,12 @@ final class PhabricatorApplicationTransactionCommentView require_celerity_resource('phui-comment-form-css'); $image_uri = $viewer->getProfileImageURI(); - $image = phutil_tag( + $image = javelin_tag( 'div', array( 'style' => 'background-image: url('.$image_uri.')', - 'class' => 'phui-comment-image visual-only', + 'class' => 'phui-comment-image', + 'aural' => false, )); $wedge = phutil_tag( 'div', diff --git a/src/view/phui/PHUIHeadThingView.php b/src/view/phui/PHUIHeadThingView.php index ab2feee984..219ed28be0 100644 --- a/src/view/phui/PHUIHeadThingView.php +++ b/src/view/phui/PHUIHeadThingView.php @@ -52,12 +52,13 @@ final class PHUIHeadThingView extends AphrontTagView { protected function getTagContent() { - $image = phutil_tag( + $image = javelin_tag( 'a', array( - 'class' => 'phui-head-thing-image visual-only', + 'class' => 'phui-head-thing-image', 'style' => 'background-image: url('.$this->image.');', 'href' => $this->imageHref, + 'aural' => false, )); if ($this->image) { diff --git a/src/view/phui/PHUITimelineEventView.php b/src/view/phui/PHUITimelineEventView.php index 78a75a2063..5013611084 100644 --- a/src/view/phui/PHUITimelineEventView.php +++ b/src/view/phui/PHUITimelineEventView.php @@ -420,12 +420,13 @@ final class PHUITimelineEventView extends AphrontView { $image = null; $badges = null; if ($image_uri) { - $image = phutil_tag( + $image = javelin_tag( ($this->userHandle->getURI()) ? 'a' : 'div', array( 'style' => 'background-image: url('.$image_uri.')', - 'class' => 'phui-timeline-image visual-only', + 'class' => 'phui-timeline-image', 'href' => $this->userHandle->getURI(), + 'aural' => false, ), ''); if ($this->badges && $show_badges) { From eb73cb68ff5b6ae131c9e11bf56d0cfeb6bbd7b7 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 13 Feb 2019 06:37:49 -0800 Subject: [PATCH 057/245] Raise a setup warning when locked configuration has a configuration value stored in the database Summary: Ref T13249. See . Today, when a configuration value is "locked", we prevent //writes// to the database. However, we still perform reads. When you upgrade, we generally don't want a bunch of your configuration to change by surprise. Some day, I'd like to stop reading locked configuration from the database. This would defuse an escalation where an attacker finds a way to write to locked configuration despite safeguards, e.g. through SQL injection or policy bypass. Today, they could write to `cluster.mailers` or similar and substantially escalate access. A better behavior would be to ignore database values for `cluster.mailers` and other locked config, so that these impermissible writes have no effect. Doing this today would break a lot of installs, but we can warn them about it now and then make the change at a later date. Test Plan: - Forced a `phd.taskmasters` config value into the database. - Saw setup warning. - Used `bin/config delete --database phd.taskmasters` to clear the warning. - Reviewed documentation changes. - Reviewed `phd.taskmasters` documentation adjustment. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13249 Differential Revision: https://secure.phabricator.com/D20159 --- .../PhabricatorExtraConfigSetupCheck.php | 101 +++++++++++++++++- .../option/PhabricatorPHDConfigOptions.php | 7 +- .../configuration_locked.diviner | 49 +++++++++ 3 files changed, 153 insertions(+), 4 deletions(-) diff --git a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php index 932e04db4b..a742e3b82b 100644 --- a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php +++ b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php @@ -15,6 +15,9 @@ final class PhabricatorExtraConfigSetupCheck extends PhabricatorSetupCheck { $defined_keys = PhabricatorApplicationConfigOptions::loadAllOptions(); + $stack = PhabricatorEnv::getConfigSourceStack(); + $stack = $stack->getStack(); + foreach ($all_keys as $key) { if (isset($defined_keys[$key])) { continue; @@ -48,9 +51,6 @@ final class PhabricatorExtraConfigSetupCheck extends PhabricatorSetupCheck { ->setName($name) ->setSummary($summary); - $stack = PhabricatorEnv::getConfigSourceStack(); - $stack = $stack->getStack(); - $found = array(); $found_local = false; $found_database = false; @@ -85,6 +85,101 @@ final class PhabricatorExtraConfigSetupCheck extends PhabricatorSetupCheck { } } + $options = PhabricatorApplicationConfigOptions::loadAllOptions(); + foreach ($defined_keys as $key => $value) { + $option = idx($options, $key); + if (!$option) { + continue; + } + + if (!$option->getLocked()) { + continue; + } + + $found_database = false; + foreach ($stack as $source_key => $source) { + $value = $source->getKeys(array($key)); + if ($value) { + if ($source instanceof PhabricatorConfigDatabaseSource) { + $found_database = true; + break; + } + } + } + + if (!$found_database) { + continue; + } + + // NOTE: These are values which we don't let you edit directly, but edit + // via other UI workflows. For now, don't raise this warning about them. + // In the future, before we stop reading database configuration for + // locked values, we either need to add a flag which lets these values + // continue reading from the database or move them to some other storage + // mechanism. + $soft_locks = array( + 'phabricator.uninstalled-applications', + 'phabricator.application-settings', + 'config.ignore-issues', + ); + $soft_locks = array_fuse($soft_locks); + if (isset($soft_locks[$key])) { + continue; + } + + $doc_name = 'Configuration Guide: Locked and Hidden Configuration'; + $doc_href = PhabricatorEnv::getDoclink($doc_name); + + $set_command = phutil_tag( + 'tt', + array(), + csprintf( + 'bin/config set %R ', + $key)); + + $summary = pht( + 'Configuration value "%s" is locked, but has a value in the database.', + $key); + $message = pht( + 'The configuration value "%s" is locked (so it can not be edited '. + 'from the web UI), but has a database value. Usually, this means '. + 'that it was previously not locked, you set it using the web UI, '. + 'and it later became locked.'. + "\n\n". + 'You should copy this configuration value in a local configuration '. + 'source (usually by using %s) and then remove it from the database '. + 'with the command below.'. + "\n\n". + 'For more information on locked and hidden configuration, including '. + 'details about this setup issue, see %s.'. + "\n\n". + 'This database value is currently respected, but a future version '. + 'of Phabricator will stop respecting database values for locked '. + 'configuration options.', + $key, + $set_command, + phutil_tag( + 'a', + array( + 'href' => $doc_href, + 'target' => '_blank', + ), + $doc_name)); + $command = csprintf( + 'phabricator/ $ ./bin/config delete --database %R', + $key); + + $this->newIssue('config.locked.'.$key) + ->setShortName(pht('Deprecated Config Source')) + ->setName( + pht( + 'Locked Configuration Option "%s" Has Database Value', + $key)) + ->setSummary($summary) + ->setMessage($message) + ->addCommand($command) + ->addPhabricatorConfig($key); + } if (PhabricatorEnv::getEnvConfig('feed.http-hooks')) { $this->newIssue('config.deprecated.feed.http-hooks') diff --git a/src/applications/config/option/PhabricatorPHDConfigOptions.php b/src/applications/config/option/PhabricatorPHDConfigOptions.php index 37fae45dfb..e04353876a 100644 --- a/src/applications/config/option/PhabricatorPHDConfigOptions.php +++ b/src/applications/config/option/PhabricatorPHDConfigOptions.php @@ -41,7 +41,12 @@ final class PhabricatorPHDConfigOptions "If you are running a cluster, this limit applies separately ". "to each instance of `phd`. For example, if this limit is set ". "to `4` and you have three hosts running daemons, the effective ". - "global limit will be 12.")), + "global limit will be 12.". + "\n\n". + "After changing this value, you must restart the daemons. Most ". + "configuration changes are picked up by the daemons ". + "automatically, but pool sizes can not be changed without a ". + "restart.")), $this->newOption('phd.verbose', 'bool', false) ->setLocked(true) ->setBoolOptions( diff --git a/src/docs/user/configuration/configuration_locked.diviner b/src/docs/user/configuration/configuration_locked.diviner index 958124c381..f96adc2d82 100644 --- a/src/docs/user/configuration/configuration_locked.diviner +++ b/src/docs/user/configuration/configuration_locked.diviner @@ -111,6 +111,55 @@ phabricator/ $ ./bin/config set ``` +Locked Configuration With Database Values +========================================= + +You may receive a setup issue warning you that a locked configuration key has a +value set in the database. Most commonly, this is because: + + - In some earlier version of Phabricator, this configuration was not locked. + - In the past, you or some other administrator used the web UI to set a + value. This value was written to the database. + - In a later version of the software, the value became locked. + +When Phabricator was originally released, locked configuration did not yet +exist. Locked configuration was introduced later, and then configuration options +were gradually locked for a long time after that. + +In some cases the meaning of a value changed and it became possible to use it +to break an install or the configuration became a security risk. In other +cases, we identified an existing security risk or arrived at some other reason +to lock the value. + +Locking values was more common in the past, and it is now relatively rare for +an unlocked value to become locked: when new values are introduced, they are +generally locked or hidden appropriately. In most cases, this setup issue only +affects installs that have used Phabricator for a long time. + +At time of writing (February 2019), Phabricator currently respects these old +database values. However, some future version of Phabricator will refuse to +read locked configuration from the database, because this improves security if +an attacker manages to find a way to bypass restrictions on editing locked +configuration from the web UI. + +To clear this setup warning and avoid surprise behavioral changes in the future, +you should move these configuration values from the database to a local config +file. Usually, you'll do this by first copying the value from the database: + +``` +phabricator/ $ ./bin/config set +``` + +...and then removing the database value: + +``` +phabricator/ $ ./bin/config delete --database +``` + +See @{Configuration User Guide: Advanced Configuration} for some more detailed +discussion of different configuration sources. + + Next Steps ========== From 4c124201623acfe27487faf715a4045cd72c5d6b Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 12 Feb 2019 10:45:33 -0800 Subject: [PATCH 058/245] Replace "URI->setQueryParams()" after initialization with a constructor argument Summary: Ref T13250. See D20149. In a number of cases, we use `setQueryParams()` immediately after URI construction. To simplify this slightly, let the constructor take parameters, similar to `HTTPSFuture`. Test Plan: See inlines. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13250 Differential Revision: https://secure.phabricator.com/D20151 --- .../almanac/controller/AlmanacController.php | 18 +++------ .../PhabricatorAuthOneTimeLoginController.php | 10 ++--- .../auth/future/PhabricatorDuoFuture.php | 19 +++++---- .../diffusion/view/DiffusionView.php | 12 +++--- .../markup/DivinerSymbolRemarkupRule.php | 18 ++++----- .../herald/controller/HeraldNewController.php | 40 +++++++++---------- .../PhabricatorOwnersDetailController.php | 10 ++--- .../cart/PhortuneCartCheckoutController.php | 12 +++--- .../PhortunePayPalPaymentProvider.php | 14 ++++--- .../provider/PhortunePaymentProvider.php | 3 +- .../storage/PhabricatorRepository.php | 8 +--- .../editengine/PhabricatorEditEngine.php | 3 +- .../PhabricatorTypeaheadDatasource.php | 8 ++-- 13 files changed, 83 insertions(+), 92 deletions(-) diff --git a/src/applications/almanac/controller/AlmanacController.php b/src/applications/almanac/controller/AlmanacController.php index 24a8986766..7fad62a509 100644 --- a/src/applications/almanac/controller/AlmanacController.php +++ b/src/applications/almanac/controller/AlmanacController.php @@ -67,19 +67,13 @@ abstract class AlmanacController $is_builtin = isset($builtins[$key]); $is_persistent = (bool)$property->getID(); - $delete_uri = id(new PhutilURI($delete_base)) - ->setQueryParams( - array( - 'key' => $key, - 'objectPHID' => $object->getPHID(), - )); + $params = array( + 'key' => $key, + 'objectPHID' => $object->getPHID(), + ); - $edit_uri = id(new PhutilURI($edit_base)) - ->setQueryParams( - array( - 'key' => $key, - 'objectPHID' => $object->getPHID(), - )); + $delete_uri = new PhutilURI($delete_base, $params); + $edit_uri = new PhutilURI($edit_base, $params); $delete = javelin_tag( 'a', diff --git a/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php b/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php index 26f8785c0a..353f31562c 100644 --- a/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php +++ b/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php @@ -218,11 +218,11 @@ final class PhabricatorAuthOneTimeLoginController $request->setTemporaryCookie(PhabricatorCookies::COOKIE_HISEC, 'yes'); - return (string)id(new PhutilURI($panel_uri)) - ->setQueryParams( - array( - 'key' => $key, - )); + $params = array( + 'key' => $key, + ); + + return (string)new PhutilURI($panel_uri, $params); } $providers = id(new PhabricatorAuthProviderConfigQuery()) diff --git a/src/applications/auth/future/PhabricatorDuoFuture.php b/src/applications/auth/future/PhabricatorDuoFuture.php index 81a5a2a2b8..1e70ec2a57 100644 --- a/src/applications/auth/future/PhabricatorDuoFuture.php +++ b/src/applications/auth/future/PhabricatorDuoFuture.php @@ -80,11 +80,6 @@ final class PhabricatorDuoFuture $host = $this->apiHostname; $host = phutil_utf8_strtolower($host); - $uri = id(new PhutilURI('')) - ->setProtocol('https') - ->setDomain($host) - ->setPath($path); - $data = $this->parameters; $date = date('r'); @@ -109,11 +104,19 @@ final class PhabricatorDuoFuture $signature = new PhutilOpaqueEnvelope($signature); if ($http_method === 'GET') { - $uri->setQueryParams($data); - $data = array(); + $uri_data = $data; + $body_data = array(); + } else { + $uri_data = array(); + $body_data = $data; } - $future = id(new HTTPSFuture($uri, $data)) + $uri = id(new PhutilURI('', $uri_data)) + ->setProtocol('https') + ->setDomain($host) + ->setPath($path); + + $future = id(new HTTPSFuture($uri, $body_data)) ->setHTTPBasicAuthCredentials($this->integrationKey, $signature) ->setMethod($http_method) ->addHeader('Accept', 'application/json') diff --git a/src/applications/diffusion/view/DiffusionView.php b/src/applications/diffusion/view/DiffusionView.php index eb7f3eb72f..123d22af5e 100644 --- a/src/applications/diffusion/view/DiffusionView.php +++ b/src/applications/diffusion/view/DiffusionView.php @@ -81,12 +81,12 @@ abstract class DiffusionView extends AphrontView { } if (isset($details['external'])) { - $href = id(new PhutilURI('/diffusion/external/')) - ->setQueryParams( - array( - 'uri' => idx($details, 'external'), - 'id' => idx($details, 'hash'), - )); + $params = array( + 'uri' => idx($details, 'external'), + 'id' => idx($details, 'hash'), + ); + + $href = new PhutilURI('/diffusion/external/', $params); $tip = pht('Browse External'); } else { $href = $this->getDiffusionRequest()->generateURI( diff --git a/src/applications/diviner/markup/DivinerSymbolRemarkupRule.php b/src/applications/diviner/markup/DivinerSymbolRemarkupRule.php index 7fdb4cb37e..db68927625 100644 --- a/src/applications/diviner/markup/DivinerSymbolRemarkupRule.php +++ b/src/applications/diviner/markup/DivinerSymbolRemarkupRule.php @@ -111,15 +111,15 @@ final class DivinerSymbolRemarkupRule extends PhutilRemarkupRule { // Here, we're generating comment text or something like that. Just // link to Diviner and let it sort things out. - $href = id(new PhutilURI('/diviner/find/')) - ->setQueryParams( - array( - 'book' => $ref->getBook(), - 'name' => $ref->getName(), - 'type' => $ref->getType(), - 'context' => $ref->getContext(), - 'jump' => true, - )); + $params = array( + 'book' => $ref->getBook(), + 'name' => $ref->getName(), + 'type' => $ref->getType(), + 'context' => $ref->getContext(), + 'jump' => true, + ); + + $href = new PhutilURI('/diviner/find/', $params); } // TODO: This probably is not the best place to do this. Move it somewhere diff --git a/src/applications/herald/controller/HeraldNewController.php b/src/applications/herald/controller/HeraldNewController.php index fbaf1aeb9e..f571aeb395 100644 --- a/src/applications/herald/controller/HeraldNewController.php +++ b/src/applications/herald/controller/HeraldNewController.php @@ -81,13 +81,13 @@ final class HeraldNewController extends HeraldController { } if (!$errors && $done) { - $uri = id(new PhutilURI('edit/')) - ->setQueryParams( - array( - 'content_type' => $content_type, - 'rule_type' => $rule_type, - 'targetPHID' => $target_phid, - )); + $params = array( + 'content_type' => $content_type, + 'rule_type' => $rule_type, + 'targetPHID' => $target_phid, + ); + + $uri = new PhutilURI('edit/', $params); $uri = $this->getApplicationURI($uri); return id(new AphrontRedirectResponse())->setURI($uri); } @@ -126,13 +126,13 @@ final class HeraldNewController extends HeraldController { ->addHiddenInput('step', 2) ->appendChild($rule_types); + $params = array( + 'content_type' => $content_type, + 'step' => '0', + ); + $cancel_text = pht('Back'); - $cancel_uri = id(new PhutilURI('new/')) - ->setQueryParams( - array( - 'content_type' => $content_type, - 'step' => 0, - )); + $cancel_uri = new PhutilURI('new/', $params); $cancel_uri = $this->getApplicationURI($cancel_uri); $title = pht('Create Herald Rule: %s', idx($content_type_map, $content_type)); @@ -173,14 +173,14 @@ final class HeraldNewController extends HeraldController { ->setValue($request->getStr('objectName')) ->setLabel(pht('Object'))); + $params = array( + 'content_type' => $content_type, + 'rule_type' => $rule_type, + 'step' => 1, + ); + $cancel_text = pht('Back'); - $cancel_uri = id(new PhutilURI('new/')) - ->setQueryParams( - array( - 'content_type' => $content_type, - 'rule_type' => $rule_type, - 'step' => 1, - )); + $cancel_uri = new PhutilURI('new/', $params); $cancel_uri = $this->getApplicationURI($cancel_uri); $title = pht('Create Herald Rule: %s', idx($content_type_map, $content_type)); diff --git a/src/applications/owners/controller/PhabricatorOwnersDetailController.php b/src/applications/owners/controller/PhabricatorOwnersDetailController.php index e28ae2b3bb..c458e4dbd1 100644 --- a/src/applications/owners/controller/PhabricatorOwnersDetailController.php +++ b/src/applications/owners/controller/PhabricatorOwnersDetailController.php @@ -65,11 +65,11 @@ final class PhabricatorOwnersDetailController $commit_views = array(); - $commit_uri = id(new PhutilURI('/diffusion/commit/')) - ->setQueryParams( - array( - 'package' => $package->getPHID(), - )); + $params = array( + 'package' => $package->getPHID(), + ); + + $commit_uri = new PhutilURI('/diffusion/commit/', $params); $status_concern = DiffusionCommitAuditStatus::CONCERN_RAISED; diff --git a/src/applications/phortune/controller/cart/PhortuneCartCheckoutController.php b/src/applications/phortune/controller/cart/PhortuneCartCheckoutController.php index dd611ecb7b..874ecf63aa 100644 --- a/src/applications/phortune/controller/cart/PhortuneCartCheckoutController.php +++ b/src/applications/phortune/controller/cart/PhortuneCartCheckoutController.php @@ -134,13 +134,13 @@ final class PhortuneCartCheckoutController $account_id = $account->getID(); + $params = array( + 'merchantID' => $merchant->getID(), + 'cartID' => $cart->getID(), + ); + $payment_method_uri = $this->getApplicationURI("{$account_id}/card/new/"); - $payment_method_uri = new PhutilURI($payment_method_uri); - $payment_method_uri->setQueryParams( - array( - 'merchantID' => $merchant->getID(), - 'cartID' => $cart->getID(), - )); + $payment_method_uri = new PhutilURI($payment_method_uri, $params); $form = id(new AphrontFormView()) ->setUser($viewer) diff --git a/src/applications/phortune/provider/PhortunePayPalPaymentProvider.php b/src/applications/phortune/provider/PhortunePayPalPaymentProvider.php index 078141f8a1..262606ca62 100644 --- a/src/applications/phortune/provider/PhortunePayPalPaymentProvider.php +++ b/src/applications/phortune/provider/PhortunePayPalPaymentProvider.php @@ -348,12 +348,14 @@ final class PhortunePayPalPaymentProvider extends PhortunePaymentProvider { ->setRawPayPalQuery('SetExpressCheckout', $params) ->resolve(); - $uri = new PhutilURI('https://www.sandbox.paypal.com/cgi-bin/webscr'); - $uri->setQueryParams( - array( - 'cmd' => '_express-checkout', - 'token' => $result['TOKEN'], - )); + $params = array( + 'cmd' => '_express-checkout', + 'token' => $result['TOKEN'], + ); + + $uri = new PhutilURI( + 'https://www.sandbox.paypal.com/cgi-bin/webscr', + $params); $cart->setMetadataValue('provider.checkoutURI', (string)$uri); $cart->save(); diff --git a/src/applications/phortune/provider/PhortunePaymentProvider.php b/src/applications/phortune/provider/PhortunePaymentProvider.php index 90e354c5dc..57b2956ecb 100644 --- a/src/applications/phortune/provider/PhortunePaymentProvider.php +++ b/src/applications/phortune/provider/PhortunePaymentProvider.php @@ -273,8 +273,7 @@ abstract class PhortunePaymentProvider extends Phobject { $app = PhabricatorApplication::getByClass('PhabricatorPhortuneApplication'); $path = $app->getBaseURI().'provider/'.$id.'/'.$action.'/'; - $uri = new PhutilURI($path); - $uri->setQueryParams($params); + $uri = new PhutilURI($path, $params); if ($local) { return $uri; diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php index e66fb78afa..d5ad83b6d6 100644 --- a/src/applications/repository/storage/PhabricatorRepository.php +++ b/src/applications/repository/storage/PhabricatorRepository.php @@ -820,8 +820,6 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO return $uri; } - $uri = new PhutilURI($uri); - if (isset($params['lint'])) { $params['params'] = idx($params, 'params', array()) + array( 'lint' => $params['lint'], @@ -830,11 +828,7 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO $query = idx($params, 'params', array()) + $query; - if ($query) { - $uri->setQueryParams($query); - } - - return $uri; + return new PhutilURI($uri, $query); } public function updateURIIndex() { diff --git a/src/applications/transactions/editengine/PhabricatorEditEngine.php b/src/applications/transactions/editengine/PhabricatorEditEngine.php index d1a2fb72b9..353d7ee385 100644 --- a/src/applications/transactions/editengine/PhabricatorEditEngine.php +++ b/src/applications/transactions/editengine/PhabricatorEditEngine.php @@ -1541,8 +1541,7 @@ abstract class PhabricatorEditEngine $config_uri = $config->getCreateURI(); if ($parameters) { - $config_uri = (string)id(new PhutilURI($config_uri)) - ->setQueryParams($parameters); + $config_uri = (string)new PhutilURI($config_uri, $parameters); } $specs[] = array( diff --git a/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php b/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php index 43cc72eaaa..e077d7a7ec 100644 --- a/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php +++ b/src/applications/typeahead/datasource/PhabricatorTypeaheadDatasource.php @@ -99,8 +99,8 @@ abstract class PhabricatorTypeaheadDatasource extends Phobject { } public function getDatasourceURI() { - $uri = new PhutilURI('/typeahead/class/'.get_class($this).'/'); - $uri->setQueryParams($this->newURIParameters()); + $params = $this->newURIParameters(); + $uri = new PhutilURI('/typeahead/class/'.get_class($this).'/', $params); return phutil_string_cast($uri); } @@ -109,8 +109,8 @@ abstract class PhabricatorTypeaheadDatasource extends Phobject { return null; } - $uri = new PhutilURI('/typeahead/browse/'.get_class($this).'/'); - $uri->setQueryParams($this->newURIParameters()); + $params = $this->newURIParameters(); + $uri = new PhutilURI('/typeahead/browse/'.get_class($this).'/', $params); return phutil_string_cast($uri); } From 241f06c9ffc63909ec04e29f67f53cb310fb1953 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 12 Feb 2019 13:18:32 -0800 Subject: [PATCH 059/245] Clean up final `setQueryParams()` callsites Summary: Ref T13250. See D20149. Test Plan: All trivial? Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13250 Differential Revision: https://secure.phabricator.com/D20153 --- src/aphront/AphrontRequest.php | 5 ++++- src/aphront/response/AphrontResponse.php | 2 +- src/applications/auth/provider/PhabricatorAuthProvider.php | 2 +- .../files/controller/PhabricatorFileDataController.php | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/aphront/AphrontRequest.php b/src/aphront/AphrontRequest.php index a65566e867..38eb5a6ffc 100644 --- a/src/aphront/AphrontRequest.php +++ b/src/aphront/AphrontRequest.php @@ -829,7 +829,10 @@ final class AphrontRequest extends Phobject { } $uri->setPath($this->getPath()); - $uri->setQueryParams(self::flattenData($_GET)); + $uri->removeAllQueryParams(); + foreach (self::flattenData($_GET) as $query_key => $query_value) { + $uri->appendQueryParam($query_key, $query_value); + } $input = PhabricatorStartup::getRawInput(); diff --git a/src/aphront/response/AphrontResponse.php b/src/aphront/response/AphrontResponse.php index 0eab91b2af..9cfda0f5c1 100644 --- a/src/aphront/response/AphrontResponse.php +++ b/src/aphront/response/AphrontResponse.php @@ -218,7 +218,7 @@ abstract class AphrontResponse extends Phobject { $uri = id(new PhutilURI($uri)) ->setPath(null) ->setFragment(null) - ->setQueryParams(array()); + ->removeAllQueryParams(); $uri = (string)$uri; if (preg_match('/[ ;\']/', $uri)) { diff --git a/src/applications/auth/provider/PhabricatorAuthProvider.php b/src/applications/auth/provider/PhabricatorAuthProvider.php index 78aeebd810..dfdc7c6a5b 100644 --- a/src/applications/auth/provider/PhabricatorAuthProvider.php +++ b/src/applications/auth/provider/PhabricatorAuthProvider.php @@ -447,7 +447,7 @@ abstract class PhabricatorAuthProvider extends Phobject { $uri = $attributes['uri']; $uri = new PhutilURI($uri); $params = $uri->getQueryParamsAsPairList(); - $uri->setQueryParams(array()); + $uri->removeAllQueryParams(); $content = array($button); diff --git a/src/applications/files/controller/PhabricatorFileDataController.php b/src/applications/files/controller/PhabricatorFileDataController.php index dd5e0adb7d..8ceb1e7907 100644 --- a/src/applications/files/controller/PhabricatorFileDataController.php +++ b/src/applications/files/controller/PhabricatorFileDataController.php @@ -135,7 +135,7 @@ final class PhabricatorFileDataController extends PhabricatorFileController { $request_uri = id(clone $request->getAbsoluteRequestURI()) ->setPath(null) ->setFragment(null) - ->setQueryParams(array()); + ->removeAllQueryParams(); $response->addContentSecurityPolicyURI( 'object-src', From 5892c78986037c3314f4f820decef38902f18166 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 12 Feb 2019 13:35:59 -0800 Subject: [PATCH 060/245] Replace all "setQueryParam()" calls with "remove/replaceQueryParam()" Summary: Ref T13250. See D20149. Mostly: clarify semantics. Partly: remove magic "null" behavior. Test Plan: Poked around, but mostly just inspection since these are pretty much one-for-one. Reviewers: amckinley Reviewed By: amckinley Subscribers: yelirekim Maniphest Tasks: T13250 Differential Revision: https://secure.phabricator.com/D20154 --- src/aphront/AphrontRequest.php | 2 +- .../almanac/controller/AlmanacController.php | 2 +- .../controller/PhabricatorAuthController.php | 2 +- .../PhabricatorAuthStartController.php | 4 +-- .../config/PhabricatorAuthNewController.php | 2 +- ...icatorAuthFactorProviderEditController.php | 2 +- .../PhabricatorAuthMainMenuBarExtension.php | 2 +- ...habricatorCalendarImportViewController.php | 6 ++-- .../check/PhabricatorWebServerSetupCheck.php | 2 +- .../controller/ConpherenceViewController.php | 2 +- ...abricatorDashboardPanelRenderingEngine.php | 4 +-- .../PhabricatorDashboardRenderingEngine.php | 6 ++-- .../DifferentialDiffCreateController.php | 2 +- .../PhabricatorFactHomeController.php | 2 +- .../PhabricatorFileLightboxController.php | 2 +- ...PhabricatorFileTransformListController.php | 2 +- .../markup/PhabricatorImageRemarkupRule.php | 2 +- .../ManiphestTaskDetailController.php | 6 ++-- .../ManiphestTaskSubtaskController.php | 6 ++-- .../maniphest/view/ManiphestTaskListView.php | 2 +- .../controller/MultimeterSampleController.php | 6 ++-- .../PhabricatorNotificationServerRef.php | 2 +- ...PhabricatorNotificationPanelController.php | 2 +- .../PhabricatorNotificationSearchEngine.php | 2 +- .../oauthserver/PhabricatorOAuthResponse.php | 2 +- .../PhabricatorOAuthServerAuthController.php | 2 +- .../pholio/view/PholioMockImagesView.php | 2 +- .../PhortunePaymentMethodCreateController.php | 2 +- .../PhortuneSubscriptionEditController.php | 4 +-- .../ponder/view/PonderAddAnswerView.php | 2 +- .../PhabricatorProjectBoardViewController.php | 30 +++++++++++-------- ...PhabricatorProjectColumnHideController.php | 2 +- .../PhabricatorProjectDefaultController.php | 2 +- ...ephRequestDifferentialCreateController.php | 2 +- ...PhabricatorApplicationSearchController.php | 4 +-- .../PhabricatorMultiFactorSettingsPanel.php | 2 +- ...catorApplicationTransactionCommentView.php | 2 +- ...orTypeaheadModularDatasourceController.php | 12 ++++---- src/infrastructure/env/PhabricatorEnv.php | 16 ++++++---- src/view/phui/PHUITimelineView.php | 4 +-- 40 files changed, 87 insertions(+), 75 deletions(-) diff --git a/src/aphront/AphrontRequest.php b/src/aphront/AphrontRequest.php index 38eb5a6ffc..46d1266b08 100644 --- a/src/aphront/AphrontRequest.php +++ b/src/aphront/AphrontRequest.php @@ -594,7 +594,7 @@ final class AphrontRequest extends Phobject { $request_uri = idx($_SERVER, 'REQUEST_URI', '/'); $uri = new PhutilURI($request_uri); - $uri->setQueryParam('__path__', null); + $uri->removeQueryParam('__path__'); $path = phutil_escape_uri($this->getPath()); $uri->setPath($path); diff --git a/src/applications/almanac/controller/AlmanacController.php b/src/applications/almanac/controller/AlmanacController.php index 7fad62a509..918b3a4ad9 100644 --- a/src/applications/almanac/controller/AlmanacController.php +++ b/src/applications/almanac/controller/AlmanacController.php @@ -137,7 +137,7 @@ abstract class AlmanacController $phid = $object->getPHID(); $add_uri = id(new PhutilURI($edit_base)) - ->setQueryParam('objectPHID', $object->getPHID()); + ->replaceQueryParam('objectPHID', $object->getPHID()); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, diff --git a/src/applications/auth/controller/PhabricatorAuthController.php b/src/applications/auth/controller/PhabricatorAuthController.php index e81301218c..cda56d34b1 100644 --- a/src/applications/auth/controller/PhabricatorAuthController.php +++ b/src/applications/auth/controller/PhabricatorAuthController.php @@ -95,7 +95,7 @@ abstract class PhabricatorAuthController extends PhabricatorController { private function buildLoginValidateResponse(PhabricatorUser $user) { $validate_uri = new PhutilURI($this->getApplicationURI('validate/')); - $validate_uri->setQueryParam('expect', $user->getUsername()); + $validate_uri->replaceQueryParam('expect', $user->getUsername()); return id(new AphrontRedirectResponse())->setURI((string)$validate_uri); } diff --git a/src/applications/auth/controller/PhabricatorAuthStartController.php b/src/applications/auth/controller/PhabricatorAuthStartController.php index 0b823098d7..72cbbea5a8 100644 --- a/src/applications/auth/controller/PhabricatorAuthStartController.php +++ b/src/applications/auth/controller/PhabricatorAuthStartController.php @@ -54,7 +54,7 @@ final class PhabricatorAuthStartController } $redirect_uri = $request->getRequestURI(); - $redirect_uri->setQueryParam('cleared', 1); + $redirect_uri->replaceQueryParam('cleared', 1); return id(new AphrontRedirectResponse())->setURI($redirect_uri); } } @@ -64,7 +64,7 @@ final class PhabricatorAuthStartController // the workflow will continue normally. if ($did_clear) { $redirect_uri = $request->getRequestURI(); - $redirect_uri->setQueryParam('cleared', null); + $redirect_uri->removeQueryParam('cleared'); return id(new AphrontRedirectResponse())->setURI($redirect_uri); } diff --git a/src/applications/auth/controller/config/PhabricatorAuthNewController.php b/src/applications/auth/controller/config/PhabricatorAuthNewController.php index c8fd0ad8a5..770c43208d 100644 --- a/src/applications/auth/controller/config/PhabricatorAuthNewController.php +++ b/src/applications/auth/controller/config/PhabricatorAuthNewController.php @@ -32,7 +32,7 @@ final class PhabricatorAuthNewController $provider_class = get_class($provider); $provider_uri = id(new PhutilURI('/config/edit/')) - ->setQueryParam('provider', $provider_class); + ->replaceQueryParam('provider', $provider_class); $provider_uri = $this->getApplicationURI($provider_uri); $already_exists = isset($configured_classes[get_class($provider)]); diff --git a/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderEditController.php b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderEditController.php index a8d87e2ead..a1636396ac 100644 --- a/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderEditController.php +++ b/src/applications/auth/controller/mfa/PhabricatorAuthFactorProviderEditController.php @@ -45,7 +45,7 @@ final class PhabricatorAuthFactorProviderEditController foreach ($factors as $factor_key => $factor) { $factor_uri = id(new PhutilURI('/mfa/edit/')) - ->setQueryParam('providerFactorKey', $factor_key); + ->replaceQueryParam('providerFactorKey', $factor_key); $factor_uri = $this->getApplicationURI($factor_uri); $is_enabled = $factor->canCreateNewProvider(); diff --git a/src/applications/auth/extension/PhabricatorAuthMainMenuBarExtension.php b/src/applications/auth/extension/PhabricatorAuthMainMenuBarExtension.php index d9fb5d013b..d49a447df6 100644 --- a/src/applications/auth/extension/PhabricatorAuthMainMenuBarExtension.php +++ b/src/applications/auth/extension/PhabricatorAuthMainMenuBarExtension.php @@ -42,7 +42,7 @@ final class PhabricatorAuthMainMenuBarExtension $uri = new PhutilURI('/auth/start/'); if ($controller) { $path = $controller->getRequest()->getPath(); - $uri->setQueryParam('next', $path); + $uri->replaceQueryParam('next', $path); } return id(new PHUIButtonView()) diff --git a/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php b/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php index 9c07d371d0..6d92f00a97 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php @@ -234,7 +234,7 @@ final class PhabricatorCalendarImportViewController $all_uri = $this->getApplicationURI('import/log/'); $all_uri = (string)id(new PhutilURI($all_uri)) - ->setQueryParam('importSourcePHID', $import->getPHID()); + ->replaceQueryParam('importSourcePHID', $import->getPHID()); $all_button = id(new PHUIButtonView()) ->setTag('a') @@ -273,8 +273,8 @@ final class PhabricatorCalendarImportViewController $all_uri = $this->getApplicationURI(); $all_uri = (string)id(new PhutilURI($all_uri)) - ->setQueryParam('importSourcePHID', $import->getPHID()) - ->setQueryParam('display', 'list'); + ->replaceQueryParam('importSourcePHID', $import->getPHID()) + ->replaceQueryParam('display', 'list'); $all_button = id(new PHUIButtonView()) ->setTag('a') diff --git a/src/applications/config/check/PhabricatorWebServerSetupCheck.php b/src/applications/config/check/PhabricatorWebServerSetupCheck.php index 398ebd6376..8f6885e8e8 100644 --- a/src/applications/config/check/PhabricatorWebServerSetupCheck.php +++ b/src/applications/config/check/PhabricatorWebServerSetupCheck.php @@ -40,7 +40,7 @@ final class PhabricatorWebServerSetupCheck extends PhabricatorSetupCheck { $base_uri = id(new PhutilURI($base_uri)) ->setPath($send_path) - ->setQueryParam($expect_key, $expect_value); + ->replaceQueryParam($expect_key, $expect_value); $self_future = id(new HTTPSFuture($base_uri)) ->addHeader('X-Phabricator-SelfCheck', 1) diff --git a/src/applications/conpherence/controller/ConpherenceViewController.php b/src/applications/conpherence/controller/ConpherenceViewController.php index 996417a307..357d07631a 100644 --- a/src/applications/conpherence/controller/ConpherenceViewController.php +++ b/src/applications/conpherence/controller/ConpherenceViewController.php @@ -188,7 +188,7 @@ final class ConpherenceViewController extends } else { // user not logged in so give them a login button. $login_href = id(new PhutilURI('/auth/start/')) - ->setQueryParam('next', '/'.$conpherence->getMonogram()); + ->replaceQueryParam('next', '/'.$conpherence->getMonogram()); return id(new PHUIFormLayoutView()) ->addClass('login-to-participate') ->appendInstructions(pht('Log in to join this room and participate.')) diff --git a/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php b/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php index dfe328933a..fc62c4d5cb 100644 --- a/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php +++ b/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php @@ -287,7 +287,7 @@ final class PhabricatorDashboardPanelRenderingEngine extends Phobject { $edit_uri = "/dashboard/panel/edit/{$panel_id}/"; $edit_uri = new PhutilURI($edit_uri); if ($dashboard_id) { - $edit_uri->setQueryParam('dashboardID', $dashboard_id); + $edit_uri->replaceQueryParam('dashboardID', $dashboard_id); } $action_edit = id(new PHUIIconView()) @@ -303,7 +303,7 @@ final class PhabricatorDashboardPanelRenderingEngine extends Phobject { $remove_uri = "/dashboard/removepanel/{$dashboard_id}/"; $remove_uri = id(new PhutilURI($remove_uri)) - ->setQueryParam('panelPHID', $panel_phid); + ->replaceQueryParam('panelPHID', $panel_phid); $action_remove = id(new PHUIIconView()) ->setIcon('fa-trash-o') diff --git a/src/applications/dashboard/engine/PhabricatorDashboardRenderingEngine.php b/src/applications/dashboard/engine/PhabricatorDashboardRenderingEngine.php index ba6aace971..9f6481c05b 100644 --- a/src/applications/dashboard/engine/PhabricatorDashboardRenderingEngine.php +++ b/src/applications/dashboard/engine/PhabricatorDashboardRenderingEngine.php @@ -113,11 +113,11 @@ final class PhabricatorDashboardRenderingEngine extends Phobject { $dashboard_id = $this->dashboard->getID(); $create_uri = id(new PhutilURI('/dashboard/panel/create/')) - ->setQueryParam('dashboardID', $dashboard_id) - ->setQueryParam('column', $column); + ->replaceQueryParam('dashboardID', $dashboard_id) + ->replaceQueryParam('column', $column); $add_uri = id(new PhutilURI('/dashboard/addpanel/'.$dashboard_id.'/')) - ->setQueryParam('column', $column); + ->replaceQueryParam('column', $column); $create_button = id(new PHUIButtonView()) ->setTag('a') diff --git a/src/applications/differential/controller/DifferentialDiffCreateController.php b/src/applications/differential/controller/DifferentialDiffCreateController.php index 284edf49d3..9aaf407e28 100644 --- a/src/applications/differential/controller/DifferentialDiffCreateController.php +++ b/src/applications/differential/controller/DifferentialDiffCreateController.php @@ -71,7 +71,7 @@ final class DifferentialDiffCreateController extends DifferentialController { $uri = $this->getApplicationURI("diff/{$diff_id}/"); $uri = new PhutilURI($uri); if ($revision) { - $uri->setQueryParam('revisionID', $revision->getID()); + $uri->replaceQueryParam('revisionID', $revision->getID()); } return id(new AphrontRedirectResponse())->setURI($uri); diff --git a/src/applications/fact/controller/PhabricatorFactHomeController.php b/src/applications/fact/controller/PhabricatorFactHomeController.php index 82f6a0905b..56ffe3930b 100644 --- a/src/applications/fact/controller/PhabricatorFactHomeController.php +++ b/src/applications/fact/controller/PhabricatorFactHomeController.php @@ -11,7 +11,7 @@ final class PhabricatorFactHomeController extends PhabricatorFactController { if ($request->isFormPost()) { $uri = new PhutilURI('/fact/chart/'); - $uri->setQueryParam('y1', $request->getStr('y1')); + $uri->replaceQueryParam('y1', $request->getStr('y1')); return id(new AphrontRedirectResponse())->setURI($uri); } diff --git a/src/applications/files/controller/PhabricatorFileLightboxController.php b/src/applications/files/controller/PhabricatorFileLightboxController.php index 1f679d621b..59a826dd42 100644 --- a/src/applications/files/controller/PhabricatorFileLightboxController.php +++ b/src/applications/files/controller/PhabricatorFileLightboxController.php @@ -70,7 +70,7 @@ final class PhabricatorFileLightboxController if (!$viewer->isLoggedIn()) { $login_href = id(new PhutilURI('/auth/start/')) - ->setQueryParam('next', '/'.$file->getMonogram()); + ->replaceQueryParam('next', '/'.$file->getMonogram()); return id(new PHUIFormLayoutView()) ->addClass('phui-comment-panel-empty') ->appendChild( diff --git a/src/applications/files/controller/PhabricatorFileTransformListController.php b/src/applications/files/controller/PhabricatorFileTransformListController.php index ab5322fc1a..7b5bc9299d 100644 --- a/src/applications/files/controller/PhabricatorFileTransformListController.php +++ b/src/applications/files/controller/PhabricatorFileTransformListController.php @@ -61,7 +61,7 @@ final class PhabricatorFileTransformListController $view_href = $file->getURIForTransform($xform); $view_href = new PhutilURI($view_href); - $view_href->setQueryParam('regenerate', 'true'); + $view_href->replaceQueryParam('regenerate', 'true'); $view_text = pht('Regenerate'); diff --git a/src/applications/files/markup/PhabricatorImageRemarkupRule.php b/src/applications/files/markup/PhabricatorImageRemarkupRule.php index 5d1979ed3c..57ad75bbc5 100644 --- a/src/applications/files/markup/PhabricatorImageRemarkupRule.php +++ b/src/applications/files/markup/PhabricatorImageRemarkupRule.php @@ -149,7 +149,7 @@ final class PhabricatorImageRemarkupRule extends PhutilRemarkupRule { )); } else { $src_uri = id(new PhutilURI('/file/imageproxy/')) - ->setQueryParam('uri', $uri); + ->replaceQueryParam('uri', $uri); $img = id(new PHUIRemarkupImageView()) ->setURI($src_uri) diff --git a/src/applications/maniphest/controller/ManiphestTaskDetailController.php b/src/applications/maniphest/controller/ManiphestTaskDetailController.php index 0f96d76b91..ba826e16e8 100644 --- a/src/applications/maniphest/controller/ManiphestTaskDetailController.php +++ b/src/applications/maniphest/controller/ManiphestTaskDetailController.php @@ -300,9 +300,9 @@ final class ManiphestTaskDetailController extends ManiphestController { $subtask_form = head($subtask_options); $form_key = $subtask_form->getIdentifier(); $subtask_uri = id(new PhutilURI("/task/edit/form/{$form_key}/")) - ->setQueryParam('parent', $id) - ->setQueryParam('template', $id) - ->setQueryParam('status', ManiphestTaskStatus::getDefaultStatus()); + ->replaceQueryParam('parent', $id) + ->replaceQueryParam('template', $id) + ->replaceQueryParam('status', ManiphestTaskStatus::getDefaultStatus()); $subtask_workflow = false; } diff --git a/src/applications/maniphest/controller/ManiphestTaskSubtaskController.php b/src/applications/maniphest/controller/ManiphestTaskSubtaskController.php index 5256c1bd26..3105cf661d 100644 --- a/src/applications/maniphest/controller/ManiphestTaskSubtaskController.php +++ b/src/applications/maniphest/controller/ManiphestTaskSubtaskController.php @@ -47,9 +47,9 @@ final class ManiphestTaskSubtaskController $subtype = $subtype_map->getSubtype($subtype_key); $subtask_uri = id(new PhutilURI("/task/edit/form/{$form_key}/")) - ->setQueryParam('parent', $id) - ->setQueryParam('template', $id) - ->setQueryParam('status', ManiphestTaskStatus::getDefaultStatus()); + ->replaceQueryParam('parent', $id) + ->replaceQueryParam('template', $id) + ->replaceQueryParam('status', ManiphestTaskStatus::getDefaultStatus()); $subtask_uri = $this->getApplicationURI($subtask_uri); $item = id(new PHUIObjectItemView()) diff --git a/src/applications/maniphest/view/ManiphestTaskListView.php b/src/applications/maniphest/view/ManiphestTaskListView.php index e7128fb850..6bf5daf29b 100644 --- a/src/applications/maniphest/view/ManiphestTaskListView.php +++ b/src/applications/maniphest/view/ManiphestTaskListView.php @@ -135,7 +135,7 @@ final class ManiphestTaskListView extends ManiphestView { if ($this->showBatchControls) { $href = new PhutilURI('/maniphest/task/edit/'.$task->getID().'/'); if (!$this->showSubpriorityControls) { - $href->setQueryParam('ungrippable', 'true'); + $href->replaceQueryParam('ungrippable', 'true'); } $item->addAction( id(new PHUIListItemView()) diff --git a/src/applications/multimeter/controller/MultimeterSampleController.php b/src/applications/multimeter/controller/MultimeterSampleController.php index da09641d22..73c9ba530e 100644 --- a/src/applications/multimeter/controller/MultimeterSampleController.php +++ b/src/applications/multimeter/controller/MultimeterSampleController.php @@ -302,11 +302,11 @@ final class MultimeterSampleController extends MultimeterController { if (!strlen($group)) { $group = null; } - $uri->setQueryParam('group', $group); + $uri->replaceQueryParam('group', $group); if ($wipe) { foreach ($this->getColumnMap() as $key => $column) { - $uri->setQueryParam($key, null); + $uri->removeQueryParam($key); } } @@ -317,7 +317,7 @@ final class MultimeterSampleController extends MultimeterController { $value = (array)$value; $uri = clone $this->getRequest()->getRequestURI(); - $uri->setQueryParam($key, implode(',', $value)); + $uri->replaceQueryParam($key, implode(',', $value)); return phutil_tag( 'a', diff --git a/src/applications/notification/client/PhabricatorNotificationServerRef.php b/src/applications/notification/client/PhabricatorNotificationServerRef.php index b183221eee..46d03a5c3a 100644 --- a/src/applications/notification/client/PhabricatorNotificationServerRef.php +++ b/src/applications/notification/client/PhabricatorNotificationServerRef.php @@ -153,7 +153,7 @@ final class PhabricatorNotificationServerRef $instance = PhabricatorEnv::getEnvConfig('cluster.instance'); if (strlen($instance)) { - $uri->setQueryParam('instance', $instance); + $uri->replaceQueryParam('instance', $instance); } return $uri; diff --git a/src/applications/notification/controller/PhabricatorNotificationPanelController.php b/src/applications/notification/controller/PhabricatorNotificationPanelController.php index 1e956a60ea..5991e8db7b 100644 --- a/src/applications/notification/controller/PhabricatorNotificationPanelController.php +++ b/src/applications/notification/controller/PhabricatorNotificationPanelController.php @@ -25,7 +25,7 @@ final class PhabricatorNotificationPanelController $notifications_view = $builder->buildView(); $content = $notifications_view->render(); - $clear_uri->setQueryParam( + $clear_uri->replaceQueryParam( 'chronoKey', head($stories)->getChronologicalKey()); } else { diff --git a/src/applications/notification/query/PhabricatorNotificationSearchEngine.php b/src/applications/notification/query/PhabricatorNotificationSearchEngine.php index 0ee7327bfc..c7e1998333 100644 --- a/src/applications/notification/query/PhabricatorNotificationSearchEngine.php +++ b/src/applications/notification/query/PhabricatorNotificationSearchEngine.php @@ -111,7 +111,7 @@ final class PhabricatorNotificationSearchEngine ->setUser($viewer); $view = $builder->buildView(); - $clear_uri->setQueryParam( + $clear_uri->replaceQueryParam( 'chronoKey', head($notifications)->getChronologicalKey()); } else { diff --git a/src/applications/oauthserver/PhabricatorOAuthResponse.php b/src/applications/oauthserver/PhabricatorOAuthResponse.php index 62c0fc9821..e0fca827b4 100644 --- a/src/applications/oauthserver/PhabricatorOAuthResponse.php +++ b/src/applications/oauthserver/PhabricatorOAuthResponse.php @@ -36,7 +36,7 @@ final class PhabricatorOAuthResponse extends AphrontResponse { $base_uri = $this->getClientURI(); $query_params = $this->buildResponseDict(); foreach ($query_params as $key => $value) { - $base_uri->setQueryParam($key, $value); + $base_uri->replaceQueryParam($key, $value); } return $base_uri; } diff --git a/src/applications/oauthserver/controller/PhabricatorOAuthServerAuthController.php b/src/applications/oauthserver/controller/PhabricatorOAuthServerAuthController.php index 745be3e820..2b454e00ef 100644 --- a/src/applications/oauthserver/controller/PhabricatorOAuthServerAuthController.php +++ b/src/applications/oauthserver/controller/PhabricatorOAuthServerAuthController.php @@ -306,7 +306,7 @@ final class PhabricatorOAuthServerAuthController foreach ($params as $key => $value) { if (strlen($value)) { - $full_uri->setQueryParam($key, $value); + $full_uri->replaceQueryParam($key, $value); } } diff --git a/src/applications/pholio/view/PholioMockImagesView.php b/src/applications/pholio/view/PholioMockImagesView.php index 99645c4a91..786de07cfd 100644 --- a/src/applications/pholio/view/PholioMockImagesView.php +++ b/src/applications/pholio/view/PholioMockImagesView.php @@ -133,7 +133,7 @@ final class PholioMockImagesView extends AphrontView { ); $login_uri = id(new PhutilURI('/login/')) - ->setQueryParam('next', (string)$this->getRequestURI()); + ->replaceQueryParam('next', (string)$this->getRequestURI()); $config = array( 'mockID' => $mock->getID(), diff --git a/src/applications/phortune/controller/payment/PhortunePaymentMethodCreateController.php b/src/applications/phortune/controller/payment/PhortunePaymentMethodCreateController.php index 847fbf5716..c068862631 100644 --- a/src/applications/phortune/controller/payment/PhortunePaymentMethodCreateController.php +++ b/src/applications/phortune/controller/payment/PhortunePaymentMethodCreateController.php @@ -143,7 +143,7 @@ final class PhortunePaymentMethodCreateController "cart/{$cart_id}/checkout/?paymentMethodID=".$method->getID()); } else if ($subscription_id) { $next_uri = new PhutilURI($cancel_uri); - $next_uri->setQueryParam('added', true); + $next_uri->replaceQueryParam('added', true); } else { $account_uri = $this->getApplicationURI($account->getID().'/'); $next_uri = new PhutilURI($account_uri); diff --git a/src/applications/phortune/controller/subscription/PhortuneSubscriptionEditController.php b/src/applications/phortune/controller/subscription/PhortuneSubscriptionEditController.php index e7287f3d29..04367a88a0 100644 --- a/src/applications/phortune/controller/subscription/PhortuneSubscriptionEditController.php +++ b/src/applications/phortune/controller/subscription/PhortuneSubscriptionEditController.php @@ -118,8 +118,8 @@ final class PhortuneSubscriptionEditController extends PhortuneController { $uri = $this->getApplicationURI($account->getID().'/card/new/'); $uri = new PhutilURI($uri); - $uri->setQueryParam('merchantID', $merchant->getID()); - $uri->setQueryParam('subscriptionID', $subscription->getID()); + $uri->replaceQueryParam('merchantID', $merchant->getID()); + $uri->replaceQueryParam('subscriptionID', $subscription->getID()); $add_method_button = phutil_tag( 'a', diff --git a/src/applications/ponder/view/PonderAddAnswerView.php b/src/applications/ponder/view/PonderAddAnswerView.php index 43bfd0d6ba..20c52dac8e 100644 --- a/src/applications/ponder/view/PonderAddAnswerView.php +++ b/src/applications/ponder/view/PonderAddAnswerView.php @@ -66,7 +66,7 @@ final class PonderAddAnswerView extends AphrontView { if (!$viewer->isLoggedIn()) { $login_href = id(new PhutilURI('/auth/start/')) - ->setQueryParam('next', '/Q'.$question->getID()); + ->replaceQueryParam('next', '/Q'.$question->getID()); $form = id(new PHUIFormLayoutView()) ->addClass('login-to-participate') ->appendChild( diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index 15e0c5d075..ee239035da 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -284,7 +284,7 @@ final class PhabricatorProjectBoardViewController $query_key = $saved_query->getQueryKey(); $bulk_uri = new PhutilURI("/maniphest/bulk/query/{$query_key}/"); - $bulk_uri->setQueryParam('board', $this->id); + $bulk_uri->replaceQueryParam('board', $this->id); return id(new AphrontRedirectResponse()) ->setURI($bulk_uri); @@ -878,7 +878,7 @@ final class PhabricatorProjectBoardViewController } $uri = $this->getURIWithState($uri) - ->setQueryParam('filter', null); + ->removeQueryParam('filter'); $item->setHref($uri); $items[] = $item; @@ -966,12 +966,12 @@ final class PhabricatorProjectBoardViewController if ($show_hidden) { $hidden_uri = $this->getURIWithState() - ->setQueryParam('hidden', null); + ->removeQueryParam('hidden'); $hidden_icon = 'fa-eye-slash'; $hidden_text = pht('Hide Hidden Columns'); } else { $hidden_uri = $this->getURIWithState() - ->setQueryParam('hidden', 'true'); + ->replaceQueryParam('hidden', 'true'); $hidden_icon = 'fa-eye'; $hidden_text = pht('Show Hidden Columns'); } @@ -999,7 +999,7 @@ final class PhabricatorProjectBoardViewController ->setHref($manage_uri); $batch_edit_uri = $request->getRequestURI(); - $batch_edit_uri->setQueryParam('batch', self::BATCH_EDIT_ALL); + $batch_edit_uri->replaceQueryParam('batch', self::BATCH_EDIT_ALL); $can_batch_edit = PhabricatorPolicyFilter::hasCapability( $viewer, PhabricatorApplication::getByClass('PhabricatorManiphestApplication'), @@ -1090,7 +1090,7 @@ final class PhabricatorProjectBoardViewController } $batch_edit_uri = $request->getRequestURI(); - $batch_edit_uri->setQueryParam('batch', $column->getID()); + $batch_edit_uri->replaceQueryParam('batch', $column->getID()); $can_batch_edit = PhabricatorPolicyFilter::hasCapability( $viewer, PhabricatorApplication::getByClass('PhabricatorManiphestApplication'), @@ -1103,7 +1103,7 @@ final class PhabricatorProjectBoardViewController ->setDisabled(!$can_batch_edit); $batch_move_uri = $request->getRequestURI(); - $batch_move_uri->setQueryParam('move', $column->getID()); + $batch_move_uri->replaceQueryParam('move', $column->getID()); $column_items[] = id(new PhabricatorActionView()) ->setIcon('fa-arrow-right') ->setName(pht('Move Tasks to Column...')) @@ -1111,7 +1111,7 @@ final class PhabricatorProjectBoardViewController ->setWorkflow(true); $query_uri = $request->getRequestURI(); - $query_uri->setQueryParam('queryColumnID', $column->getID()); + $query_uri->replaceQueryParam('queryColumnID', $column->getID()); $column_items[] = id(new PhabricatorActionView()) ->setName(pht('View as Query')) @@ -1188,18 +1188,22 @@ final class PhabricatorProjectBoardViewController $base = new PhutilURI($base); if ($force || ($this->sortKey != $this->getDefaultSort($project))) { - $base->setQueryParam('order', $this->sortKey); + $base->replaceQueryParam('order', $this->sortKey); } else { - $base->setQueryParam('order', null); + $base->removeQueryParam('order'); } if ($force || ($this->queryKey != $this->getDefaultFilter($project))) { - $base->setQueryParam('filter', $this->queryKey); + $base->replaceQueryParam('filter', $this->queryKey); } else { - $base->setQueryParam('filter', null); + $base->removeQueryParam('filter'); } - $base->setQueryParam('hidden', $this->showHidden ? 'true' : null); + if ($this->showHidden) { + $base->replaceQueryParam('hidden', 'true'); + } else { + $base->removeQueryParam('hidden'); + } return $base; } diff --git a/src/applications/project/controller/PhabricatorProjectColumnHideController.php b/src/applications/project/controller/PhabricatorProjectColumnHideController.php index 1dd5e47ecb..fbda2feb1e 100644 --- a/src/applications/project/controller/PhabricatorProjectColumnHideController.php +++ b/src/applications/project/controller/PhabricatorProjectColumnHideController.php @@ -41,7 +41,7 @@ final class PhabricatorProjectColumnHideController $view_uri = $this->getApplicationURI('/board/'.$project_id.'/'); $view_uri = new PhutilURI($view_uri); foreach ($request->getPassthroughRequestData() as $key => $value) { - $view_uri->setQueryParam($key, $value); + $view_uri->replaceQueryParam($key, $value); } if ($column->isDefaultColumn()) { diff --git a/src/applications/project/controller/PhabricatorProjectDefaultController.php b/src/applications/project/controller/PhabricatorProjectDefaultController.php index 8f42ff9736..2c7a47b2df 100644 --- a/src/applications/project/controller/PhabricatorProjectDefaultController.php +++ b/src/applications/project/controller/PhabricatorProjectDefaultController.php @@ -54,7 +54,7 @@ final class PhabricatorProjectDefaultController $view_uri = $this->getApplicationURI("board/{$id}/"); $view_uri = new PhutilURI($view_uri); foreach ($request->getPassthroughRequestData() as $key => $value) { - $view_uri->setQueryParam($key, $value); + $view_uri->replaceQueryParam($key, $value); } if ($request->isFormPost()) { diff --git a/src/applications/releeph/controller/request/ReleephRequestDifferentialCreateController.php b/src/applications/releeph/controller/request/ReleephRequestDifferentialCreateController.php index ffd6388284..1835e6f7f9 100644 --- a/src/applications/releeph/controller/request/ReleephRequestDifferentialCreateController.php +++ b/src/applications/releeph/controller/request/ReleephRequestDifferentialCreateController.php @@ -96,7 +96,7 @@ final class ReleephRequestDifferentialCreateController private function buildReleephRequestURI(ReleephBranch $branch) { $uri = $branch->getURI('request/'); return id(new PhutilURI($uri)) - ->setQueryParam('D', $this->revision->getID()); + ->replaceQueryParam('D', $this->revision->getID()); } } diff --git a/src/applications/search/controller/PhabricatorApplicationSearchController.php b/src/applications/search/controller/PhabricatorApplicationSearchController.php index cf4f95c16d..6b43883c78 100644 --- a/src/applications/search/controller/PhabricatorApplicationSearchController.php +++ b/src/applications/search/controller/PhabricatorApplicationSearchController.php @@ -905,7 +905,7 @@ final class PhabricatorApplicationSearchController $engine = $this->getSearchEngine(); $nux_uri = $engine->getQueryBaseURI(); $nux_uri = id(new PhutilURI($nux_uri)) - ->setQueryParam('nux', true); + ->replaceQueryParam('nux', true); $actions[] = id(new PhabricatorActionView()) ->setIcon('fa-user-plus') @@ -915,7 +915,7 @@ final class PhabricatorApplicationSearchController if ($is_dev) { $overheated_uri = $this->getRequest()->getRequestURI() - ->setQueryParam('overheated', true); + ->replaceQueryParam('overheated', true); $actions[] = id(new PhabricatorActionView()) ->setIcon('fa-fire') diff --git a/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php b/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php index 6809b51334..09193f3c96 100644 --- a/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorMultiFactorSettingsPanel.php @@ -219,7 +219,7 @@ final class PhabricatorMultiFactorSettingsPanel foreach ($providers as $provider_phid => $provider) { $provider_uri = id(new PhutilURI($this->getPanelURI())) - ->setQueryParam('providerPHID', $provider_phid); + ->replaceQueryParam('providerPHID', $provider_phid); $is_enabled = $provider->canCreateNewConfiguration($viewer); diff --git a/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php b/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php index 1fb850887f..115c7b950e 100644 --- a/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php +++ b/src/applications/transactions/view/PhabricatorApplicationTransactionCommentView.php @@ -198,7 +198,7 @@ final class PhabricatorApplicationTransactionCommentView $viewer = $this->getViewer(); if (!$viewer->isLoggedIn()) { $uri = id(new PhutilURI('/login/')) - ->setQueryParam('next', (string)$this->getRequestURI()); + ->replaceQueryParam('next', (string)$this->getRequestURI()); return id(new PHUIObjectBoxView()) ->setFlush(true) ->appendChild( diff --git a/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php b/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php index 7c2205df46..efc9ea5f65 100644 --- a/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php +++ b/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php @@ -126,10 +126,10 @@ final class PhabricatorTypeaheadModularDatasourceController $results = array_slice($results, 0, $limit, $preserve_keys = true); if (($offset + (2 * $limit)) < $hard_limit) { $next_uri = id(new PhutilURI($request->getRequestURI())) - ->setQueryParam('offset', $offset + $limit) - ->setQueryParam('q', $query) - ->setQueryParam('raw', $raw_query) - ->setQueryParam('format', 'html'); + ->replaceQueryParam('offset', $offset + $limit) + ->replaceQueryParam('q', $query) + ->replaceQueryParam('raw', $raw_query) + ->replaceQueryParam('format', 'html'); $next_link = javelin_tag( 'a', @@ -248,7 +248,9 @@ final class PhabricatorTypeaheadModularDatasourceController $parameters = $source->getParameters(); if ($parameters) { $reference_uri = (string)id(new PhutilURI($reference_uri)) - ->setQueryParam('parameters', phutil_json_encode($parameters)); + ->replaceQueryParam( + 'parameters', + phutil_json_encode($parameters)); } $reference_link = phutil_tag( diff --git a/src/infrastructure/env/PhabricatorEnv.php b/src/infrastructure/env/PhabricatorEnv.php index 8b2af38d3d..24fb940c9a 100644 --- a/src/infrastructure/env/PhabricatorEnv.php +++ b/src/infrastructure/env/PhabricatorEnv.php @@ -475,11 +475,17 @@ final class PhabricatorEnv extends Phobject { * @task read */ public static function getDoclink($resource, $type = 'article') { - $uri = new PhutilURI('https://secure.phabricator.com/diviner/find/'); - $uri->setQueryParam('name', $resource); - $uri->setQueryParam('type', $type); - $uri->setQueryParam('jump', true); - return (string)$uri; + $params = array( + 'name' => $resource, + 'type' => $type, + 'jump' => true, + ); + + $uri = new PhutilURI( + 'https://secure.phabricator.com/diviner/find/', + $params); + + return phutil_string_cast($uri); } diff --git a/src/view/phui/PHUITimelineView.php b/src/view/phui/PHUITimelineView.php index d0e942f461..d8c06d6a9f 100644 --- a/src/view/phui/PHUITimelineView.php +++ b/src/view/phui/PHUITimelineView.php @@ -154,8 +154,8 @@ final class PHUITimelineView extends AphrontView { } $uri = $this->getPager()->getNextPageURI(); - $uri->setQueryParam('quoteTargetID', $this->getQuoteTargetID()); - $uri->setQueryParam('quoteRef', $this->getQuoteRef()); + $uri->replaceQueryParam('quoteTargetID', $this->getQuoteTargetID()); + $uri->replaceQueryParam('quoteRef', $this->getQuoteRef()); $events[] = javelin_tag( 'div', array( From be21dd3b52b8b83839d225e39ed9bd37e0a2658a Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 13 Feb 2019 09:13:38 -0800 Subject: [PATCH 061/245] Fix some "URI->alter(X, null)" callsites Summary: Ref T13250. This internally calls `replaceQueryParam(X, null)` now, which fatals if the second parameter is `null`. I hit these legitimately, but I'll look for more callsites and follow up by either allowing this, removing `alter()`, fixing the callsites, or some combination. (I'm not much of a fan of `alter()`.) Test Plan: Browsing a paginated list no longer complains about URI construction. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13250 Differential Revision: https://secure.phabricator.com/D20162 --- src/view/control/AphrontCursorPagerView.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/view/control/AphrontCursorPagerView.php b/src/view/control/AphrontCursorPagerView.php index 4608158481..cdb9562624 100644 --- a/src/view/control/AphrontCursorPagerView.php +++ b/src/view/control/AphrontCursorPagerView.php @@ -99,9 +99,9 @@ final class AphrontCursorPagerView extends AphrontView { return null; } - return $this->uri - ->alter('before', null) - ->alter('after', null); + return id(clone $this->uri) + ->removeQueryParam('after') + ->removeQueryParam('before'); } public function getPrevPageURI() { @@ -113,9 +113,9 @@ final class AphrontCursorPagerView extends AphrontView { return null; } - return $this->uri - ->alter('after', null) - ->alter('before', $this->prevPageID); + return id(clone $this->uri) + ->removeQueryParam('after') + ->replaceQueryParam('before', $this->prevPageID); } public function getNextPageURI() { @@ -127,9 +127,9 @@ final class AphrontCursorPagerView extends AphrontView { return null; } - return $this->uri - ->alter('after', $this->nextPageID) - ->alter('before', null); + return id(clone $this->uri) + ->replaceQueryParam('after', $this->nextPageID) + ->removeQueryParam('before'); } public function render() { From 889eca1af94e3375c7f2b8b0ea673ebd0325700a Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 14 Feb 2019 04:18:46 -0800 Subject: [PATCH 062/245] Allow a DAO object storage namespace to be forced to a particular value Summary: Ref T6703. When we import external data from a third-party install to a Phacility instance, we must link instance accounts to central accounts: either existing central accounts, or newly created central accounts that we send invites for. During this import, or when users register and claim those new accounts, we do a write from `admin.phacility.com` directly into the instance database to link the accounts. This is pretty sketchy, and should almost certainly just be an internal API instead, particularly now that it's relatively stable. However, it's what we use for now. The process has had some issues since the introduction of `%R` (combined database name and table refrence in queries), and now needs to be updated for the new `providerConfigPHID` column in `ExternalAccount`. The problem is that `%R` isn't doing the right thing. We have code like this: ``` $conn = new_connection_to_instance('turtle'); queryf($conn, 'INSERT INTO %R ...', $table); ``` However, the `$table` resolves `%R` using the currently-executing-process information, not anything specific to `$conn`, so it prints `admin_user.user_externalaccount` (the name of the table on `admin.phacility.com`, where the code is running). We want it to print `turtle_user.user_externalaccount` instead: the name of the table on `turtle.phacility.com`, where we're actually writing. To force this to happen, let callers override the namespace part of the database name. Long term: I'd plan to rip this out and replace it with an API call. This "connect directly to the database" stuff is nice for iterating on (only `admin` needs hotfixes) but very very sketchy for maintaining. Test Plan: See next diff. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T6703 Differential Revision: https://secure.phabricator.com/D20167 --- .../storage/lisk/PhabricatorLiskDAO.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php b/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php index e47d701df9..99603da567 100644 --- a/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php +++ b/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php @@ -6,6 +6,7 @@ abstract class PhabricatorLiskDAO extends LiskDAO { private static $namespaceStack = array(); + private $forcedNamespace; const ATTACHABLE = ''; const CONFIG_APPLICATION_SERIALIZERS = 'phabricator/serializers'; @@ -47,6 +48,11 @@ abstract class PhabricatorLiskDAO extends LiskDAO { return $namespace; } + public function setForcedStorageNamespace($namespace) { + $this->forcedNamespace = $namespace; + return $this; + } + /** * @task config */ @@ -188,7 +194,13 @@ abstract class PhabricatorLiskDAO extends LiskDAO { abstract public function getApplicationName(); protected function getDatabaseName() { - return self::getStorageNamespace().'_'.$this->getApplicationName(); + if ($this->forcedNamespace) { + $namespace = $this->forcedNamespace; + } else { + $namespace = self::getStorageNamespace(); + } + + return $namespace.'_'.$this->getApplicationName(); } /** From c5772f51dea721134c129fdcd151a21850d03f72 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 13 Feb 2019 15:25:20 -0800 Subject: [PATCH 063/245] Fix Content-Security-Policy headers on "Email Login" page Summary: In D20100, I changed this page from returning a `newPage()` with a dialog as its content to returning a more modern `newDialog()`. However, the magic to add stuff to the CSP header is actually only on the `newPage()` pathway today, so this accidentally dropped the extra "Content-Security-Policy" rule for Google. Lift the magic up one level so both Dialog and Page responses hit it. Test Plan: - Configured Recaptcha. - Between D20100 and this patch: got a CSP error on the Email Login page. - After this patch: clicked all the pictures of cars / store fronts. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20163 --- src/aphront/sink/AphrontHTTPSink.php | 11 +++++++++++ src/view/page/PhabricatorStandardPageView.php | 7 ------- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/aphront/sink/AphrontHTTPSink.php b/src/aphront/sink/AphrontHTTPSink.php index 60deba78ca..9e43e4a687 100644 --- a/src/aphront/sink/AphrontHTTPSink.php +++ b/src/aphront/sink/AphrontHTTPSink.php @@ -111,6 +111,17 @@ abstract class AphrontHTTPSink extends Phobject { // HTTP headers. $data = $response->getContentIterator(); + // This isn't an exceptionally clean separation of concerns, but we need + // to add CSP headers for all response types (including both web pages + // and dialogs) and can't determine the correct CSP until after we render + // the page (because page elements like Recaptcha may add CSP rules). + $static = CelerityAPI::getStaticResourceResponse(); + foreach ($static->getContentSecurityPolicyURIMap() as $kind => $uris) { + foreach ($uris as $uri) { + $response->addContentSecurityPolicyURI($kind, $uri); + } + } + $all_headers = array_merge( $response->getHeaders(), $response->getCacheHeaders()); diff --git a/src/view/page/PhabricatorStandardPageView.php b/src/view/page/PhabricatorStandardPageView.php index 99143add5f..cfb1b4abbe 100644 --- a/src/view/page/PhabricatorStandardPageView.php +++ b/src/view/page/PhabricatorStandardPageView.php @@ -892,13 +892,6 @@ final class PhabricatorStandardPageView extends PhabricatorBarePageView $response = id(new AphrontWebpageResponse()) ->setContent($content) ->setFrameable($this->getFrameable()); - - $static = CelerityAPI::getStaticResourceResponse(); - foreach ($static->getContentSecurityPolicyURIMap() as $kind => $uris) { - foreach ($uris as $uri) { - $response->addContentSecurityPolicyURI($kind, $uri); - } - } } return $response; From b09cf166a8bde2283b2de2fc3803b605430d8aa3 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 15 Feb 2019 13:57:25 -0800 Subject: [PATCH 064/245] Clean up a couple more URI alter() calls Summary: See . These weren't obviously nullable from a cursory `grep`, but are sometimes nullable in practice. Test Plan: Created, then saved a new Phriction document. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20184 --- .../editor/PhrictionTransactionEditor.php | 11 ++++++++--- src/view/phui/PHUITimelineView.php | 17 +++++++++++++++-- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/applications/phriction/editor/PhrictionTransactionEditor.php b/src/applications/phriction/editor/PhrictionTransactionEditor.php index d09ff5d556..1476b24c46 100644 --- a/src/applications/phriction/editor/PhrictionTransactionEditor.php +++ b/src/applications/phriction/editor/PhrictionTransactionEditor.php @@ -229,9 +229,14 @@ final class PhrictionTransactionEditor foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { case PhrictionDocumentContentTransaction::TRANSACTIONTYPE: - $uri = id(new PhutilURI('/phriction/diff/'.$object->getID().'/')) - ->alter('l', $this->getOldContent()->getVersion()) - ->alter('r', $this->getNewContent()->getVersion()); + $params = array( + 'l' => $this->getOldContent()->getVersion(), + 'r' => $this->getNewContent()->getVersion(), + ); + + $path = '/phriction/diff/'.$object->getID().'/'; + $uri = new PhutilURI($path, $params); + $this->contentDiffURI = (string)$uri; break 2; default: diff --git a/src/view/phui/PHUITimelineView.php b/src/view/phui/PHUITimelineView.php index d8c06d6a9f..2e6d8298c8 100644 --- a/src/view/phui/PHUITimelineView.php +++ b/src/view/phui/PHUITimelineView.php @@ -154,8 +154,21 @@ final class PHUITimelineView extends AphrontView { } $uri = $this->getPager()->getNextPageURI(); - $uri->replaceQueryParam('quoteTargetID', $this->getQuoteTargetID()); - $uri->replaceQueryParam('quoteRef', $this->getQuoteRef()); + + $target_id = $this->getQuoteTargetID(); + if ($target_id === null) { + $uri->removeQueryParam('quoteTargetID'); + } else { + $uri->replaceQueryParam('quoteTargetID', $target_id); + } + + $quote_ref = $this->getQuoteRef(); + if ($quote_ref === null) { + $uri->removeQueryParam('quoteRef'); + } else { + $uri->replaceQueryParam('quoteRef', $quote_ref); + } + $events[] = javelin_tag( 'div', array( From 66060b294bce163037afa2e9352be9336c28cee6 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 15 Feb 2019 04:53:42 -0800 Subject: [PATCH 065/245] Fix a URI construction in remarkup macro/meme rules Summary: Ref T13250. Some of these parameters may be NULL, and `alter()` is no longer happy about that. Test Plan: Ran daemon tasks that happened to render some memes. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13250 Differential Revision: https://secure.phabricator.com/D20176 --- .../macro/engine/PhabricatorMemeEngine.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/applications/macro/engine/PhabricatorMemeEngine.php b/src/applications/macro/engine/PhabricatorMemeEngine.php index 7433a4e8bc..afee0f9b18 100644 --- a/src/applications/macro/engine/PhabricatorMemeEngine.php +++ b/src/applications/macro/engine/PhabricatorMemeEngine.php @@ -47,10 +47,13 @@ final class PhabricatorMemeEngine extends Phobject { } public function getGenerateURI() { - return id(new PhutilURI('/macro/meme/')) - ->alter('macro', $this->getTemplate()) - ->alter('above', $this->getAboveText()) - ->alter('below', $this->getBelowText()); + $params = array( + 'macro' => $this->getTemplate(), + 'above' => $this->getAboveText(), + 'below' => $this->getBelowText(), + ); + + return new PhutilURI('/macro/meme/', $params); } public function newAsset() { From 454a762562289f421b02ed7caf80b9ff399c11bf Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 15 Feb 2019 05:04:30 -0800 Subject: [PATCH 066/245] Queue search indexing tasks at a new PRIORITY_INDEX, not PRIORITY_IMPORT Summary: Depends on D20175. Ref T12425. Ref T13253. Currently, importing commits can stall search index rebuilds, since index rebuilds use an older priority from before T11677 and weren't really updated for D16585. In general, we'd like to complete all indexing tasks before continuing repository imports. A possible exception is if you rebuild an entire index with `bin/search index --rebuild-the-world`, but we could queue those at a separate lower priority if issues arise. Test Plan: Ran some search indexing through the queue. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13253, T12425 Differential Revision: https://secure.phabricator.com/D20177 --- src/applications/search/worker/PhabricatorSearchWorker.php | 2 +- src/infrastructure/daemon/workers/PhabricatorWorker.php | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/applications/search/worker/PhabricatorSearchWorker.php b/src/applications/search/worker/PhabricatorSearchWorker.php index f93df63981..361d9c9b6f 100644 --- a/src/applications/search/worker/PhabricatorSearchWorker.php +++ b/src/applications/search/worker/PhabricatorSearchWorker.php @@ -14,7 +14,7 @@ final class PhabricatorSearchWorker extends PhabricatorWorker { 'parameters' => $parameters, ), array( - 'priority' => parent::PRIORITY_IMPORT, + 'priority' => parent::PRIORITY_INDEX, 'objectPHID' => $phid, )); } diff --git a/src/infrastructure/daemon/workers/PhabricatorWorker.php b/src/infrastructure/daemon/workers/PhabricatorWorker.php index 1b9821b68d..f055544b7b 100644 --- a/src/infrastructure/daemon/workers/PhabricatorWorker.php +++ b/src/infrastructure/daemon/workers/PhabricatorWorker.php @@ -18,6 +18,7 @@ abstract class PhabricatorWorker extends Phobject { const PRIORITY_DEFAULT = 2000; const PRIORITY_COMMIT = 2500; const PRIORITY_BULK = 3000; + const PRIORITY_INDEX = 3500; const PRIORITY_IMPORT = 4000; /** From 2ca316d652d84500c44a7412083714fd313ff932 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 14 Feb 2019 05:06:06 -0800 Subject: [PATCH 067/245] When users confirm Duo MFA in the mobile app, live-update the UI Summary: Ref T13249. Poll for Duo updates in the background so we can automatically update the UI when the user clicks the mobile phone app button. Test Plan: Hit a Duo gate, clicked "Approve" in the mobile app, saw the UI update immediately. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13249 Differential Revision: https://secure.phabricator.com/D20169 --- resources/celerity/map.php | 13 ++- src/__phutil_library_map__.php | 4 + .../PhabricatorAuthApplication.php | 2 + ...abricatorAuthChallengeStatusController.php | 40 +++++++++ .../auth/factor/PhabricatorAuthFactor.php | 23 +++-- .../factor/PhabricatorAuthFactorResult.php | 20 ++--- .../auth/factor/PhabricatorDuoAuthFactor.php | 83 ++++++++++++++++--- .../view/PhabricatorAuthChallengeUpdate.php | 44 ++++++++++ .../form/control/PHUIFormTimerControl.php | 30 ++++++- webroot/rsrc/css/phui/phui-form-view.css | 14 ++++ .../js/phui/behavior-phui-timer-control.js | 41 +++++++++ 11 files changed, 284 insertions(+), 30 deletions(-) create mode 100644 src/applications/auth/controller/mfa/PhabricatorAuthChallengeStatusController.php create mode 100644 src/applications/auth/view/PhabricatorAuthChallengeUpdate.php create mode 100644 webroot/rsrc/js/phui/behavior-phui-timer-control.js diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 99f3333106..2dd5cf0966 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,7 +9,7 @@ return array( 'names' => array( 'conpherence.pkg.css' => '3c8a0668', 'conpherence.pkg.js' => '020aebcf', - 'core.pkg.css' => '7a73ffc5', + 'core.pkg.css' => 'e0f5d66f', 'core.pkg.js' => '5c737607', 'differential.pkg.css' => 'b8df73d4', 'differential.pkg.js' => '67c9ea4c', @@ -151,7 +151,7 @@ return array( 'rsrc/css/phui/phui-document.css' => '52b748a5', 'rsrc/css/phui/phui-feed-story.css' => 'a0c05029', 'rsrc/css/phui/phui-fontkit.css' => '9b714a5e', - 'rsrc/css/phui/phui-form-view.css' => '0807e7ac', + 'rsrc/css/phui/phui-form-view.css' => '01b796c0', 'rsrc/css/phui/phui-form.css' => '159e2d9c', 'rsrc/css/phui/phui-head-thing.css' => 'd7f293df', 'rsrc/css/phui/phui-header-view.css' => '93cea4ec', @@ -502,6 +502,7 @@ return array( 'rsrc/js/phui/behavior-phui-selectable-list.js' => 'b26a41e4', 'rsrc/js/phui/behavior-phui-submenu.js' => 'b5e9bff9', 'rsrc/js/phui/behavior-phui-tab-group.js' => '242aa08b', + 'rsrc/js/phui/behavior-phui-timer-control.js' => 'f84bcbf4', 'rsrc/js/phuix/PHUIXActionListView.js' => 'c68f183f', 'rsrc/js/phuix/PHUIXActionView.js' => 'aaa08f3b', 'rsrc/js/phuix/PHUIXAutocomplete.js' => '58cc4ab8', @@ -650,6 +651,7 @@ return array( 'javelin-behavior-phui-selectable-list' => 'b26a41e4', 'javelin-behavior-phui-submenu' => 'b5e9bff9', 'javelin-behavior-phui-tab-group' => '242aa08b', + 'javelin-behavior-phui-timer-control' => 'f84bcbf4', 'javelin-behavior-phuix-example' => 'c2c500a7', 'javelin-behavior-policy-control' => '0eaa33a9', 'javelin-behavior-policy-rule-editor' => '9347f172', @@ -817,7 +819,7 @@ return array( 'phui-font-icon-base-css' => 'd7994e06', 'phui-fontkit-css' => '9b714a5e', 'phui-form-css' => '159e2d9c', - 'phui-form-view-css' => '0807e7ac', + 'phui-form-view-css' => '01b796c0', 'phui-head-thing-view-css' => 'd7f293df', 'phui-header-view-css' => '93cea4ec', 'phui-hovercard' => '074f0783', @@ -2111,6 +2113,11 @@ return array( 'javelin-stratcom', 'javelin-dom', ), + 'f84bcbf4' => array( + 'javelin-behavior', + 'javelin-stratcom', + 'javelin-dom', + ), 'f8c4e135' => array( 'javelin-install', 'javelin-dom', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 8f06586a68..e3287df74c 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2195,6 +2195,8 @@ phutil_register_library_map(array( 'PhabricatorAuthChallengeGarbageCollector' => 'applications/auth/garbagecollector/PhabricatorAuthChallengeGarbageCollector.php', 'PhabricatorAuthChallengePHIDType' => 'applications/auth/phid/PhabricatorAuthChallengePHIDType.php', 'PhabricatorAuthChallengeQuery' => 'applications/auth/query/PhabricatorAuthChallengeQuery.php', + 'PhabricatorAuthChallengeStatusController' => 'applications/auth/controller/mfa/PhabricatorAuthChallengeStatusController.php', + 'PhabricatorAuthChallengeUpdate' => 'applications/auth/view/PhabricatorAuthChallengeUpdate.php', 'PhabricatorAuthChangePasswordAction' => 'applications/auth/action/PhabricatorAuthChangePasswordAction.php', 'PhabricatorAuthConduitAPIMethod' => 'applications/auth/conduit/PhabricatorAuthConduitAPIMethod.php', 'PhabricatorAuthConduitTokenRevoker' => 'applications/auth/revoker/PhabricatorAuthConduitTokenRevoker.php', @@ -7925,6 +7927,8 @@ phutil_register_library_map(array( 'PhabricatorAuthChallengeGarbageCollector' => 'PhabricatorGarbageCollector', 'PhabricatorAuthChallengePHIDType' => 'PhabricatorPHIDType', 'PhabricatorAuthChallengeQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorAuthChallengeStatusController' => 'PhabricatorAuthController', + 'PhabricatorAuthChallengeUpdate' => 'Phobject', 'PhabricatorAuthChangePasswordAction' => 'PhabricatorSystemAction', 'PhabricatorAuthConduitAPIMethod' => 'ConduitAPIMethod', 'PhabricatorAuthConduitTokenRevoker' => 'PhabricatorAuthRevoker', diff --git a/src/applications/auth/application/PhabricatorAuthApplication.php b/src/applications/auth/application/PhabricatorAuthApplication.php index 52cf01b2aa..4e0baff229 100644 --- a/src/applications/auth/application/PhabricatorAuthApplication.php +++ b/src/applications/auth/application/PhabricatorAuthApplication.php @@ -97,6 +97,8 @@ final class PhabricatorAuthApplication extends PhabricatorApplication { 'PhabricatorAuthFactorProviderViewController', 'message/(?P[1-9]\d*)/' => 'PhabricatorAuthFactorProviderMessageController', + 'challenge/status/(?P[1-9]\d*)/' => + 'PhabricatorAuthChallengeStatusController', ), 'message/' => array( diff --git a/src/applications/auth/controller/mfa/PhabricatorAuthChallengeStatusController.php b/src/applications/auth/controller/mfa/PhabricatorAuthChallengeStatusController.php new file mode 100644 index 0000000000..884bbaad6d --- /dev/null +++ b/src/applications/auth/controller/mfa/PhabricatorAuthChallengeStatusController.php @@ -0,0 +1,40 @@ +getViewer(); + $id = $request->getURIData('id'); + $now = PhabricatorTime::getNow(); + + $result = new PhabricatorAuthChallengeUpdate(); + + $challenge = id(new PhabricatorAuthChallengeQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->withUserPHIDs(array($viewer->getPHID())) + ->withChallengeTTLBetween($now, null) + ->executeOne(); + if ($challenge) { + $config = id(new PhabricatorAuthFactorConfigQuery()) + ->setViewer($viewer) + ->withPHIDs(array($challenge->getFactorPHID())) + ->executeOne(); + if ($config) { + $provider = $config->getFactorProvider(); + $factor = $provider->getFactor(); + + $result = $factor->newChallengeStatusView( + $config, + $provider, + $viewer, + $challenge); + } + } + + return id(new AphrontAjaxResponse()) + ->setContent($result->newContent()); + } + +} diff --git a/src/applications/auth/factor/PhabricatorAuthFactor.php b/src/applications/auth/factor/PhabricatorAuthFactor.php index 912a2c31c9..d7e6e60ecc 100644 --- a/src/applications/auth/factor/PhabricatorAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorAuthFactor.php @@ -80,6 +80,14 @@ abstract class PhabricatorAuthFactor extends Phobject { return array(); } + public function newChallengeStatusView( + PhabricatorAuthFactorConfig $config, + PhabricatorAuthFactorProvider $provider, + PhabricatorUser $viewer, + PhabricatorAuthChallenge $challenge) { + return null; + } + /** * Is this a factor which depends on the user's contact number? * @@ -210,8 +218,6 @@ abstract class PhabricatorAuthFactor extends Phobject { get_class($this))); } - $result->setIssuedChallenges($challenges); - return $result; } @@ -242,8 +248,6 @@ abstract class PhabricatorAuthFactor extends Phobject { get_class($this))); } - $result->setIssuedChallenges($challenges); - return $result; } @@ -339,9 +343,18 @@ abstract class PhabricatorAuthFactor extends Phobject { ->setIcon('fa-commenting', 'green'); } - return id(new PHUIFormTimerControl()) + $control = id(new PHUIFormTimerControl()) ->setIcon($icon) ->appendChild($error); + + $status_challenge = $result->getStatusChallenge(); + if ($status_challenge) { + $id = $status_challenge->getID(); + $uri = "/auth/mfa/challenge/status/{$id}/"; + $control->setUpdateURI($uri); + } + + return $control; } diff --git a/src/applications/auth/factor/PhabricatorAuthFactorResult.php b/src/applications/auth/factor/PhabricatorAuthFactorResult.php index f03c3674da..b5da379545 100644 --- a/src/applications/auth/factor/PhabricatorAuthFactorResult.php +++ b/src/applications/auth/factor/PhabricatorAuthFactorResult.php @@ -11,6 +11,7 @@ final class PhabricatorAuthFactorResult private $value; private $issuedChallenges = array(); private $icon; + private $statusChallenge; public function setAnsweredChallenge(PhabricatorAuthChallenge $challenge) { if (!$challenge->getIsAnsweredChallenge()) { @@ -34,6 +35,15 @@ final class PhabricatorAuthFactorResult return $this->answeredChallenge; } + public function setStatusChallenge(PhabricatorAuthChallenge $challenge) { + $this->statusChallenge = $challenge; + return $this; + } + + public function getStatusChallenge() { + return $this->statusChallenge; + } + public function getIsValid() { return (bool)$this->getAnsweredChallenge(); } @@ -83,16 +93,6 @@ final class PhabricatorAuthFactorResult return $this->value; } - public function setIssuedChallenges(array $issued_challenges) { - assert_instances_of($issued_challenges, 'PhabricatorAuthChallenge'); - $this->issuedChallenges = $issued_challenges; - return $this; - } - - public function getIssuedChallenges() { - return $this->issuedChallenges; - } - public function setIcon(PHUIIconView $icon) { $this->icon = $icon; return $this; diff --git a/src/applications/auth/factor/PhabricatorDuoAuthFactor.php b/src/applications/auth/factor/PhabricatorDuoAuthFactor.php index 4be4c15ea8..66bd7c9ebd 100644 --- a/src/applications/auth/factor/PhabricatorDuoAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorDuoAuthFactor.php @@ -585,7 +585,7 @@ final class PhabricatorDuoAuthFactor $result = $this->newDuoFuture($provider) ->setHTTPMethod('GET') ->setMethod('auth_status', $parameters) - ->setTimeout(5) + ->setTimeout(3) ->resolve(); $state = $result['response']['result']; @@ -661,15 +661,6 @@ final class PhabricatorDuoAuthFactor PhabricatorAuthFactorResult $result) { $control = $this->newAutomaticControl($result); - if (!$control) { - $result = $this->newResult() - ->setIsContinue(true) - ->setErrorMessage( - pht( - 'A challenge has been sent to your phone. Open the Duo '. - 'application and confirm the challenge, then continue.')); - $control = $this->newAutomaticControl($result); - } $control ->setLabel(pht('Duo')) @@ -689,7 +680,27 @@ final class PhabricatorDuoAuthFactor PhabricatorUser $viewer, AphrontRequest $request, array $challenges) { - return $this->newResult(); + + $result = $this->newResult() + ->setIsContinue(true) + ->setErrorMessage( + pht( + 'A challenge has been sent to your phone. Open the Duo '. + 'application and confirm the challenge, then continue.')); + + $challenge = $this->getChallengeForCurrentContext( + $config, + $viewer, + $challenges); + if ($challenge) { + $result + ->setStatusChallenge($challenge) + ->setIcon( + id(new PHUIIconView()) + ->setIcon('fa-refresh', 'green ph-spin')); + } + + return $result; } private function newDuoFuture(PhabricatorAuthFactorProvider $provider) { @@ -790,4 +801,54 @@ final class PhabricatorDuoAuthFactor $hostname)); } + public function newChallengeStatusView( + PhabricatorAuthFactorConfig $config, + PhabricatorAuthFactorProvider $provider, + PhabricatorUser $viewer, + PhabricatorAuthChallenge $challenge) { + + $duo_xaction = $challenge->getChallengeKey(); + + $parameters = array( + 'txid' => $duo_xaction, + ); + + $default_result = id(new PhabricatorAuthChallengeUpdate()) + ->setRetry(true); + + try { + $result = $this->newDuoFuture($provider) + ->setHTTPMethod('GET') + ->setMethod('auth_status', $parameters) + ->setTimeout(5) + ->resolve(); + + $state = $result['response']['result']; + } catch (HTTPFutureCURLResponseStatus $exception) { + // If we failed or timed out, retry. Usually, this is a timeout. + return id(new PhabricatorAuthChallengeUpdate()) + ->setRetry(true); + } + + // For now, don't update the view for anything but an "Allow". Updates + // here are just about providing more visual feedback for user convenience. + if ($state !== 'allow') { + return id(new PhabricatorAuthChallengeUpdate()) + ->setRetry(false); + } + + $icon = id(new PHUIIconView()) + ->setIcon('fa-check-circle-o', 'green'); + + $view = id(new PHUIFormTimerControl()) + ->setIcon($icon) + ->appendChild(pht('You responded to this challenge correctly.')) + ->newTimerView(); + + return id(new PhabricatorAuthChallengeUpdate()) + ->setState('allow') + ->setRetry(false) + ->setMarkup($view); + } + } diff --git a/src/applications/auth/view/PhabricatorAuthChallengeUpdate.php b/src/applications/auth/view/PhabricatorAuthChallengeUpdate.php new file mode 100644 index 0000000000..a8ae5b8825 --- /dev/null +++ b/src/applications/auth/view/PhabricatorAuthChallengeUpdate.php @@ -0,0 +1,44 @@ +retry = $retry; + return $this; + } + + public function getRetry() { + return $this->retry; + } + + public function setState($state) { + $this->state = $state; + return $this; + } + + public function getState() { + return $this->state; + } + + public function setMarkup($markup) { + $this->markup = $markup; + return $this; + } + + public function getMarkup() { + return $this->markup; + } + + public function newContent() { + return array( + 'retry' => $this->getRetry(), + 'state' => $this->getState(), + 'markup' => $this->getMarkup(), + ); + } +} diff --git a/src/view/form/control/PHUIFormTimerControl.php b/src/view/form/control/PHUIFormTimerControl.php index 7229d649e9..090de2c8e4 100644 --- a/src/view/form/control/PHUIFormTimerControl.php +++ b/src/view/form/control/PHUIFormTimerControl.php @@ -3,6 +3,7 @@ final class PHUIFormTimerControl extends AphrontFormControl { private $icon; + private $updateURI; public function setIcon(PHUIIconView $icon) { $this->icon = $icon; @@ -13,11 +14,24 @@ final class PHUIFormTimerControl extends AphrontFormControl { return $this->icon; } + public function setUpdateURI($update_uri) { + $this->updateURI = $update_uri; + return $this; + } + + public function getUpdateURI() { + return $this->updateURI; + } + protected function getCustomControlClass() { return 'phui-form-timer'; } protected function renderInput() { + return $this->newTimerView(); + } + + public function newTimerView() { $icon_cell = phutil_tag( 'td', array( @@ -34,7 +48,21 @@ final class PHUIFormTimerControl extends AphrontFormControl { $row = phutil_tag('tr', array(), array($icon_cell, $content_cell)); - return phutil_tag('table', array(), $row); + $node_id = null; + + $update_uri = $this->getUpdateURI(); + if ($update_uri) { + $node_id = celerity_generate_unique_node_id(); + + Javelin::initBehavior( + 'phui-timer-control', + array( + 'nodeID' => $node_id, + 'uri' => $update_uri, + )); + } + + return phutil_tag('table', array('id' => $node_id), $row); } } diff --git a/webroot/rsrc/css/phui/phui-form-view.css b/webroot/rsrc/css/phui/phui-form-view.css index 3368bcaafb..accce86819 100644 --- a/webroot/rsrc/css/phui/phui-form-view.css +++ b/webroot/rsrc/css/phui/phui-form-view.css @@ -578,3 +578,17 @@ properly, and submit values. */ .mfa-form-enroll-button { text-align: center; } + +.phui-form-timer-updated { + animation: phui-form-timer-fade-in 2s linear; +} + + +@keyframes phui-form-timer-fade-in { + 0% { + background-color: {$lightyellow}; + } + 100% { + background-color: transparent; + } +} diff --git a/webroot/rsrc/js/phui/behavior-phui-timer-control.js b/webroot/rsrc/js/phui/behavior-phui-timer-control.js new file mode 100644 index 0000000000..d5b73a5ee2 --- /dev/null +++ b/webroot/rsrc/js/phui/behavior-phui-timer-control.js @@ -0,0 +1,41 @@ +/** + * @provides javelin-behavior-phui-timer-control + * @requires javelin-behavior + * javelin-stratcom + * javelin-dom + */ + +JX.behavior('phui-timer-control', function(config) { + var node = JX.$(config.nodeID); + var uri = config.uri; + var state = null; + + function onupdate(result) { + var markup = result.markup; + if (markup) { + var new_node = JX.$H(markup).getFragment().firstChild; + JX.DOM.replace(node, new_node); + node = new_node; + + // If the overall state has changed from the previous display state, + // animate the control to draw the user's attention to the state change. + if (result.state !== state) { + state = result.state; + JX.DOM.alterClass(node, 'phui-form-timer-updated', true); + } + } + + var retry = result.retry; + if (retry) { + setTimeout(update, 1000); + } + } + + function update() { + new JX.Request(uri, onupdate) + .setTimeout(10000) + .send(); + } + + update(); +}); From 8f8e863613c0935a0272fdbe7825480f9a0781b0 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 5 Feb 2019 05:22:39 -0800 Subject: [PATCH 068/245] When users follow an email login link but an install does not use passwords, try to get them to link an account Summary: Ref T13249. See PHI774. When users follow an email login link ("Forgot password?", "Send Welcome Email", "Send a login link to your email address.", `bin/auth recover`), we send them to a password reset flow if an install uses passwords. If an install does not use passwords, we previously dumped them unceremoniously into the {nav Settings > External Accounts} UI with no real guidance about what they were supposed to do. Since D20094 we do a slightly better job here in some cases. Continue improving this workflow. This adds a page like "Reset Password" for "Hey, You Should Probably Link An Account, Here's Some Options". Overall, this stuff is still pretty rough in a couple of areas that I imagine addressing in the future: - When you finish linking, we still dump you back in Settings. At least we got you to link things. But better would be to return you here and say "great job, you're a pro". - This UI can become a weird pile of buttons in certain configs and generally looks a little unintentional. This problem is shared among all the "linkable" providers, and the non-login link flow is also weird. So: step forward, but more work to be done. Test Plan: {F6211115} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13249 Differential Revision: https://secure.phabricator.com/D20170 --- resources/celerity/map.php | 6 +- src/__phutil_library_map__.php | 4 + .../PhabricatorAuthApplication.php | 2 + .../PhabricatorAuthOneTimeLoginController.php | 32 ++++- .../PhabricatorAuthSetExternalController.php | 110 ++++++++++++++++++ .../PhabricatorAuthLinkMessageType.php | 18 +++ .../auth/provider/PhabricatorAuthProvider.php | 2 +- .../PhabricatorPasswordAuthProvider.php | 3 +- webroot/rsrc/css/phui/phui-object-box.css | 5 + 9 files changed, 174 insertions(+), 8 deletions(-) create mode 100644 src/applications/auth/controller/PhabricatorAuthSetExternalController.php create mode 100644 src/applications/auth/message/PhabricatorAuthLinkMessageType.php diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 2dd5cf0966..b1a97639c2 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,7 +9,7 @@ return array( 'names' => array( 'conpherence.pkg.css' => '3c8a0668', 'conpherence.pkg.js' => '020aebcf', - 'core.pkg.css' => 'e0f5d66f', + 'core.pkg.css' => '4ed8ce1f', 'core.pkg.js' => '5c737607', 'differential.pkg.css' => 'b8df73d4', 'differential.pkg.js' => '67c9ea4c', @@ -164,7 +164,7 @@ return array( 'rsrc/css/phui/phui-left-right.css' => '68513c34', 'rsrc/css/phui/phui-lightbox.css' => '4ebf22da', 'rsrc/css/phui/phui-list.css' => '470b1adb', - 'rsrc/css/phui/phui-object-box.css' => '9b58483d', + 'rsrc/css/phui/phui-object-box.css' => 'f434b6be', 'rsrc/css/phui/phui-pager.css' => 'd022c7ad', 'rsrc/css/phui/phui-pinboard-view.css' => '1f08f5d8', 'rsrc/css/phui/phui-property-list-view.css' => 'cad62236', @@ -833,7 +833,7 @@ return array( 'phui-left-right-css' => '68513c34', 'phui-lightbox-css' => '4ebf22da', 'phui-list-view-css' => '470b1adb', - 'phui-object-box-css' => '9b58483d', + 'phui-object-box-css' => 'f434b6be', 'phui-oi-big-ui-css' => '9e037c7a', 'phui-oi-color-css' => 'b517bfa0', 'phui-oi-drag-ui-css' => 'da15d3dc', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index e3287df74c..b10c0e109f 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2272,6 +2272,7 @@ phutil_register_library_map(array( 'PhabricatorAuthInviteVerifyException' => 'applications/auth/exception/PhabricatorAuthInviteVerifyException.php', 'PhabricatorAuthInviteWorker' => 'applications/auth/worker/PhabricatorAuthInviteWorker.php', 'PhabricatorAuthLinkController' => 'applications/auth/controller/PhabricatorAuthLinkController.php', + 'PhabricatorAuthLinkMessageType' => 'applications/auth/message/PhabricatorAuthLinkMessageType.php', 'PhabricatorAuthListController' => 'applications/auth/controller/config/PhabricatorAuthListController.php', 'PhabricatorAuthLoginController' => 'applications/auth/controller/PhabricatorAuthLoginController.php', 'PhabricatorAuthLoginMessageType' => 'applications/auth/message/PhabricatorAuthLoginMessageType.php', @@ -2370,6 +2371,7 @@ phutil_register_library_map(array( 'PhabricatorAuthSessionPHIDType' => 'applications/auth/phid/PhabricatorAuthSessionPHIDType.php', 'PhabricatorAuthSessionQuery' => 'applications/auth/query/PhabricatorAuthSessionQuery.php', 'PhabricatorAuthSessionRevoker' => 'applications/auth/revoker/PhabricatorAuthSessionRevoker.php', + 'PhabricatorAuthSetExternalController' => 'applications/auth/controller/PhabricatorAuthSetExternalController.php', 'PhabricatorAuthSetPasswordController' => 'applications/auth/controller/PhabricatorAuthSetPasswordController.php', 'PhabricatorAuthSetupCheck' => 'applications/config/check/PhabricatorAuthSetupCheck.php', 'PhabricatorAuthStartController' => 'applications/auth/controller/PhabricatorAuthStartController.php', @@ -8023,6 +8025,7 @@ phutil_register_library_map(array( 'PhabricatorAuthInviteVerifyException' => 'PhabricatorAuthInviteDialogException', 'PhabricatorAuthInviteWorker' => 'PhabricatorWorker', 'PhabricatorAuthLinkController' => 'PhabricatorAuthController', + 'PhabricatorAuthLinkMessageType' => 'PhabricatorAuthMessageType', 'PhabricatorAuthListController' => 'PhabricatorAuthProviderConfigController', 'PhabricatorAuthLoginController' => 'PhabricatorAuthController', 'PhabricatorAuthLoginMessageType' => 'PhabricatorAuthMessageType', @@ -8142,6 +8145,7 @@ phutil_register_library_map(array( 'PhabricatorAuthSessionPHIDType' => 'PhabricatorPHIDType', 'PhabricatorAuthSessionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorAuthSessionRevoker' => 'PhabricatorAuthRevoker', + 'PhabricatorAuthSetExternalController' => 'PhabricatorAuthController', 'PhabricatorAuthSetPasswordController' => 'PhabricatorAuthController', 'PhabricatorAuthSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorAuthStartController' => 'PhabricatorAuthController', diff --git a/src/applications/auth/application/PhabricatorAuthApplication.php b/src/applications/auth/application/PhabricatorAuthApplication.php index 4e0baff229..3446ad597d 100644 --- a/src/applications/auth/application/PhabricatorAuthApplication.php +++ b/src/applications/auth/application/PhabricatorAuthApplication.php @@ -86,7 +86,9 @@ final class PhabricatorAuthApplication extends PhabricatorApplication { => 'PhabricatorAuthSSHKeyRevokeController', 'view/(?P\d+)/' => 'PhabricatorAuthSSHKeyViewController', ), + 'password/' => 'PhabricatorAuthSetPasswordController', + 'external/' => 'PhabricatorAuthSetExternalController', 'mfa/' => array( $this->getQueryRoutePattern() => diff --git a/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php b/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php index 353f31562c..d176a67119 100644 --- a/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php +++ b/src/applications/auth/controller/PhabricatorAuthOneTimeLoginController.php @@ -225,17 +225,45 @@ final class PhabricatorAuthOneTimeLoginController return (string)new PhutilURI($panel_uri, $params); } - $providers = id(new PhabricatorAuthProviderConfigQuery()) + // Check if the user already has external accounts linked. If they do, + // it's not obvious why they aren't using them to log in, but assume they + // know what they're doing. We won't send them to the link workflow. + $accounts = id(new PhabricatorExternalAccountQuery()) + ->setViewer($user) + ->withUserPHIDs(array($user->getPHID())) + ->execute(); + + $configs = id(new PhabricatorAuthProviderConfigQuery()) ->setViewer($user) ->withIsEnabled(true) ->execute(); + $linkable = array(); + foreach ($configs as $config) { + if (!$config->getShouldAllowLink()) { + continue; + } + + $provider = $config->getProvider(); + if (!$provider->isLoginFormAButton()) { + continue; + } + + $linkable[] = $provider; + } + + // If there's at least one linkable provider, and the user doesn't already + // have accounts, send the user to the link workflow. + if (!$accounts && $linkable) { + return '/auth/external/'; + } + // If there are no configured providers and the user is an administrator, // send them to Auth to configure a provider. This is probably where they // want to go. You can end up in this state by accidentally losing your // first session during initial setup, or after restoring exported data // from a hosted instance. - if (!$providers && $user->getIsAdmin()) { + if (!$configs && $user->getIsAdmin()) { return '/auth/'; } diff --git a/src/applications/auth/controller/PhabricatorAuthSetExternalController.php b/src/applications/auth/controller/PhabricatorAuthSetExternalController.php new file mode 100644 index 0000000000..51dfcab53f --- /dev/null +++ b/src/applications/auth/controller/PhabricatorAuthSetExternalController.php @@ -0,0 +1,110 @@ +getViewer(); + + $configs = id(new PhabricatorAuthProviderConfigQuery()) + ->setViewer($viewer) + ->withIsEnabled(true) + ->execute(); + + $linkable = array(); + foreach ($configs as $config) { + if (!$config->getShouldAllowLink()) { + continue; + } + + // For now, only buttons get to appear here: for example, we can't + // reasonably embed an entire LDAP form into this UI. + $provider = $config->getProvider(); + if (!$provider->isLoginFormAButton()) { + continue; + } + + $linkable[] = $config; + } + + if (!$linkable) { + return $this->newDialog() + ->setTitle(pht('No Linkable External Providers')) + ->appendParagraph( + pht( + 'Currently, there are no configured external auth providers '. + 'which you can link your account to.')) + ->addCancelButton('/'); + } + + $text = PhabricatorAuthMessage::loadMessageText( + $viewer, + PhabricatorAuthLinkMessageType::MESSAGEKEY); + if (!strlen($text)) { + $text = pht( + 'You can link your Phabricator account to an external account to '. + 'allow you to log in more easily in the future. To continue, choose '. + 'an account to link below. If you prefer not to link your account, '. + 'you can skip this step.'); + } + + $remarkup_view = new PHUIRemarkupView($viewer, $text); + $remarkup_view = phutil_tag( + 'div', + array( + 'class' => 'phui-object-box-instructions', + ), + $remarkup_view); + + PhabricatorCookies::setClientIDCookie($request); + + $view = array(); + foreach ($configs as $config) { + $provider = $config->getProvider(); + + $form = $provider->buildLinkForm($this); + + if ($provider->isLoginFormAButton()) { + require_celerity_resource('auth-css'); + $form = phutil_tag( + 'div', + array( + 'class' => 'phabricator-link-button pl', + ), + $form); + } + + $view[] = $form; + } + + $form = id(new AphrontFormView()) + ->setViewer($viewer) + ->appendControl( + id(new AphrontFormSubmitControl()) + ->addCancelButton('/', pht('Skip This Step'))); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Link External Account')); + + $box = id(new PHUIObjectBoxView()) + ->setViewer($viewer) + ->setHeader($header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->appendChild($remarkup_view) + ->appendChild($view) + ->appendChild($form); + + $main_view = id(new PHUITwoColumnView()) + ->setFooter($box); + + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb(pht('Link External Account')) + ->setBorder(true); + + return $this->newPage() + ->setTitle(pht('Link External Account')) + ->setCrumbs($crumbs) + ->appendChild($main_view); + } + +} diff --git a/src/applications/auth/message/PhabricatorAuthLinkMessageType.php b/src/applications/auth/message/PhabricatorAuthLinkMessageType.php new file mode 100644 index 0000000000..17991231c1 --- /dev/null +++ b/src/applications/auth/message/PhabricatorAuthLinkMessageType.php @@ -0,0 +1,18 @@ +renderLoginForm($controller->getRequest(), $mode = 'link'); } diff --git a/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php b/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php index 146e41706a..d39e378798 100644 --- a/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php +++ b/src/applications/auth/provider/PhabricatorPasswordAuthProvider.php @@ -159,8 +159,7 @@ final class PhabricatorPasswordAuthProvider extends PhabricatorAuthProvider { return $dialog; } - public function buildLinkForm( - PhabricatorAuthLinkController $controller) { + public function buildLinkForm($controller) { throw new Exception(pht("Password providers can't be linked.")); } diff --git a/webroot/rsrc/css/phui/phui-object-box.css b/webroot/rsrc/css/phui/phui-object-box.css index 4999a4c2c2..f95e36eedc 100644 --- a/webroot/rsrc/css/phui/phui-object-box.css +++ b/webroot/rsrc/css/phui/phui-object-box.css @@ -158,3 +158,8 @@ div.phui-object-box.phui-object-box-flush { margin-top: 8px; margin-bottom: 8px; } + +.phui-object-box-instructions { + padding: 16px; + border-bottom: 1px solid {$thinblueborder}; +} From 8810cd2f4d1572e536f7aab7e76969bea80fda65 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 13 Feb 2019 17:50:10 -0800 Subject: [PATCH 069/245] Add a standalone view for the Maniphest task graph Summary: See PHI1073. Improve the UX here: - When there are a small number of connected tasks, no changes. - When there are too many total connected tasks, but not too many directly connected tasks, show hint text with a "View Standalone Graph" button to view more of the graph. - When there are too many directly connected tasks, show better hint text with a "View Standalone Graph" button. - Always show a "View Standalone Graph" option in the dropdown menu. - Add a standalone view which works the same way but has a limit of 2,000. - This view doesn't have "View Standalone Graph" links, since they'd just link back to the same page, but is basically the same otherwise. - Increase the main page task limit from 100 to 200. Test Plan: Mobile View: {F6210326} Way too much stuff: {F6210327} New persistent link to the standalone page: {F6210328} Kind of too much stuff: {F6210329} Standalone view: {F6210330} Reviewers: amckinley Reviewed By: amckinley Subscribers: 20after4 Differential Revision: https://secure.phabricator.com/D20164 --- resources/celerity/map.php | 6 +- src/__phutil_library_map__.php | 2 + .../PhabricatorManiphestApplication.php | 1 + .../controller/ManiphestController.php | 98 ++++++++++++++ .../ManiphestTaskDetailController.php | 73 +++++----- .../ManiphestTaskGraphController.php | 125 ++++++++++++++++++ webroot/rsrc/css/aphront/table-view.css | 37 ++++++ 7 files changed, 300 insertions(+), 42 deletions(-) create mode 100644 src/applications/maniphest/controller/ManiphestTaskGraphController.php diff --git a/resources/celerity/map.php b/resources/celerity/map.php index b1a97639c2..2d402dbaa3 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,7 +9,7 @@ return array( 'names' => array( 'conpherence.pkg.css' => '3c8a0668', 'conpherence.pkg.js' => '020aebcf', - 'core.pkg.css' => '4ed8ce1f', + 'core.pkg.css' => '85a1da99', 'core.pkg.js' => '5c737607', 'differential.pkg.css' => 'b8df73d4', 'differential.pkg.js' => '67c9ea4c', @@ -30,7 +30,7 @@ return array( 'rsrc/css/aphront/notification.css' => '30240bd2', 'rsrc/css/aphront/panel-view.css' => '46923d46', 'rsrc/css/aphront/phabricator-nav-view.css' => 'f8a0c1bf', - 'rsrc/css/aphront/table-view.css' => 'daa1f9df', + 'rsrc/css/aphront/table-view.css' => '205053cd', 'rsrc/css/aphront/tokenizer.css' => 'b52d0668', 'rsrc/css/aphront/tooltip.css' => 'e3f2412f', 'rsrc/css/aphront/typeahead-browse.css' => 'b7ed02d2', @@ -520,7 +520,7 @@ return array( 'aphront-list-filter-view-css' => 'feb64255', 'aphront-multi-column-view-css' => 'fbc00ba3', 'aphront-panel-view-css' => '46923d46', - 'aphront-table-view-css' => 'daa1f9df', + 'aphront-table-view-css' => '205053cd', 'aphront-tokenizer-control-css' => 'b52d0668', 'aphront-tooltip-css' => 'e3f2412f', 'aphront-typeahead-control-css' => '8779483d', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index b10c0e109f..3c9a329d99 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1714,6 +1714,7 @@ phutil_register_library_map(array( 'ManiphestTaskFerretEngine' => 'applications/maniphest/search/ManiphestTaskFerretEngine.php', 'ManiphestTaskFulltextEngine' => 'applications/maniphest/search/ManiphestTaskFulltextEngine.php', 'ManiphestTaskGraph' => 'infrastructure/graph/ManiphestTaskGraph.php', + 'ManiphestTaskGraphController' => 'applications/maniphest/controller/ManiphestTaskGraphController.php', 'ManiphestTaskHasCommitEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasCommitEdgeType.php', 'ManiphestTaskHasCommitRelationship' => 'applications/maniphest/relationship/ManiphestTaskHasCommitRelationship.php', 'ManiphestTaskHasDuplicateTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasDuplicateTaskEdgeType.php', @@ -7401,6 +7402,7 @@ phutil_register_library_map(array( 'ManiphestTaskFerretEngine' => 'PhabricatorFerretEngine', 'ManiphestTaskFulltextEngine' => 'PhabricatorFulltextEngine', 'ManiphestTaskGraph' => 'PhabricatorObjectGraph', + 'ManiphestTaskGraphController' => 'ManiphestController', 'ManiphestTaskHasCommitEdgeType' => 'PhabricatorEdgeType', 'ManiphestTaskHasCommitRelationship' => 'ManiphestTaskRelationship', 'ManiphestTaskHasDuplicateTaskEdgeType' => 'PhabricatorEdgeType', diff --git a/src/applications/maniphest/application/PhabricatorManiphestApplication.php b/src/applications/maniphest/application/PhabricatorManiphestApplication.php index 35c1efb6e8..ec732791fa 100644 --- a/src/applications/maniphest/application/PhabricatorManiphestApplication.php +++ b/src/applications/maniphest/application/PhabricatorManiphestApplication.php @@ -55,6 +55,7 @@ final class PhabricatorManiphestApplication extends PhabricatorApplication { 'subtask/(?P[1-9]\d*)/' => 'ManiphestTaskSubtaskController', ), 'subpriority/' => 'ManiphestSubpriorityController', + 'graph/(?P[1-9]\d*)/' => 'ManiphestTaskGraphController', ), ); } diff --git a/src/applications/maniphest/controller/ManiphestController.php b/src/applications/maniphest/controller/ManiphestController.php index c80a1a462a..872d3f7b38 100644 --- a/src/applications/maniphest/controller/ManiphestController.php +++ b/src/applications/maniphest/controller/ManiphestController.php @@ -61,4 +61,102 @@ abstract class ManiphestController extends PhabricatorController { return $view; } + final protected function newTaskGraphDropdownMenu( + ManiphestTask $task, + $has_parents, + $has_subtasks, + $include_standalone) { + $viewer = $this->getViewer(); + + $parents_uri = urisprintf( + '/?subtaskIDs=%d#R', + $task->getID()); + $parents_uri = $this->getApplicationURI($parents_uri); + + $subtasks_uri = urisprintf( + '/?parentIDs=%d#R', + $task->getID()); + $subtasks_uri = $this->getApplicationURI($subtasks_uri); + + $dropdown_menu = id(new PhabricatorActionListView()) + ->setViewer($viewer) + ->addAction( + id(new PhabricatorActionView()) + ->setHref($parents_uri) + ->setName(pht('Search Parent Tasks')) + ->setDisabled(!$has_parents) + ->setIcon('fa-chevron-circle-up')) + ->addAction( + id(new PhabricatorActionView()) + ->setHref($subtasks_uri) + ->setName(pht('Search Subtasks')) + ->setDisabled(!$has_subtasks) + ->setIcon('fa-chevron-circle-down')); + + if ($include_standalone) { + $standalone_uri = urisprintf('/graph/%d/', $task->getID()); + $standalone_uri = $this->getApplicationURI($standalone_uri); + + $dropdown_menu->addAction( + id(new PhabricatorActionView()) + ->setHref($standalone_uri) + ->setName(pht('View Standalone Graph')) + ->setIcon('fa-code-fork')); + } + + $graph_menu = id(new PHUIButtonView()) + ->setTag('a') + ->setIcon('fa-search') + ->setText(pht('Search...')) + ->setDropdownMenu($dropdown_menu); + + return $graph_menu; + } + + final protected function newTaskGraphOverflowView( + ManiphestTask $task, + $overflow_message, + $include_standalone) { + + $id = $task->getID(); + + if ($include_standalone) { + $standalone_uri = $this->getApplicationURI("graph/{$id}/"); + + $standalone_link = id(new PHUIButtonView()) + ->setTag('a') + ->setHref($standalone_uri) + ->setColor(PHUIButtonView::GREY) + ->setIcon('fa-code-fork') + ->setText(pht('View Standalone Graph')); + } else { + $standalone_link = null; + } + + $standalone_icon = id(new PHUIIconView()) + ->setIcon('fa-exclamation-triangle', 'yellow') + ->addClass('object-graph-header-icon'); + + $standalone_view = phutil_tag( + 'div', + array( + 'class' => 'object-graph-header', + ), + array( + $standalone_link, + $standalone_icon, + phutil_tag( + 'div', + array( + 'class' => 'object-graph-header-message', + ), + array( + $overflow_message, + )), + )); + + return $standalone_view; + } + + } diff --git a/src/applications/maniphest/controller/ManiphestTaskDetailController.php b/src/applications/maniphest/controller/ManiphestTaskDetailController.php index ba826e16e8..c5dba7d3b5 100644 --- a/src/applications/maniphest/controller/ManiphestTaskDetailController.php +++ b/src/applications/maniphest/controller/ManiphestTaskDetailController.php @@ -80,7 +80,8 @@ final class ManiphestTaskDetailController extends ManiphestController { $related_tabs = array(); $graph_menu = null; - $graph_limit = 100; + $graph_limit = 200; + $overflow_message = null; $task_graph = id(new ManiphestTaskGraph()) ->setViewer($viewer) ->setSeedPHID($task->getPHID()) @@ -96,61 +97,55 @@ final class ManiphestTaskDetailController extends ManiphestController { $has_parents = (bool)$parent_list; $has_subtasks = (bool)$subtask_list; - $search_text = pht('Search...'); - // First, get a count of direct parent tasks and subtasks. If there // are too many of these, we just don't draw anything. You can use // the search button to browse tasks with the search UI instead. $direct_count = count($parent_list) + count($subtask_list); if ($direct_count > $graph_limit) { - $message = pht( - 'Task graph too large to display (this task is directly connected '. - 'to more than %s other tasks). Use %s to explore connected tasks.', - $graph_limit, - phutil_tag('strong', array(), $search_text)); - $message = phutil_tag('em', array(), $message); - $graph_table = id(new PHUIPropertyListView()) - ->addTextContent($message); + $overflow_message = pht( + 'This task is directly connected to more than %s other tasks. '. + 'Use %s to browse parents or subtasks, or %s to show more of the '. + 'graph.', + new PhutilNumber($graph_limit), + phutil_tag('strong', array(), pht('Search...')), + phutil_tag('strong', array(), pht('View Standalone Graph'))); + + $graph_table = null; } else { // If there aren't too many direct tasks, but there are too many total // tasks, we'll only render directly connected tasks. if ($task_graph->isOverLimit()) { $task_graph->setRenderOnlyAdjacentNodes(true); + + $overflow_message = pht( + 'This task is connected to more than %s other tasks. '. + 'Only direct parents and subtasks are shown here. Use '. + '%s to show more of the graph.', + new PhutilNumber($graph_limit), + phutil_tag('strong', array(), pht('View Standalone Graph'))); } + $graph_table = $task_graph->newGraphTable(); } - $parents_uri = urisprintf( - '/?subtaskIDs=%d#R', - $task->getID()); - $parents_uri = $this->getApplicationURI($parents_uri); + if ($overflow_message) { + $overflow_view = $this->newTaskGraphOverflowView( + $task, + $overflow_message, + true); - $subtasks_uri = urisprintf( - '/?parentIDs=%d#R', - $task->getID()); - $subtasks_uri = $this->getApplicationURI($subtasks_uri); + $graph_table = array( + $overflow_view, + $graph_table, + ); + } - $dropdown_menu = id(new PhabricatorActionListView()) - ->setViewer($viewer) - ->addAction( - id(new PhabricatorActionView()) - ->setHref($parents_uri) - ->setName(pht('Search Parent Tasks')) - ->setDisabled(!$has_parents) - ->setIcon('fa-chevron-circle-up')) - ->addAction( - id(new PhabricatorActionView()) - ->setHref($subtasks_uri) - ->setName(pht('Search Subtasks')) - ->setDisabled(!$has_subtasks) - ->setIcon('fa-chevron-circle-down')); - - $graph_menu = id(new PHUIButtonView()) - ->setTag('a') - ->setIcon('fa-search') - ->setText($search_text) - ->setDropdownMenu($dropdown_menu); + $graph_menu = $this->newTaskGraphDropdownMenu( + $task, + $has_parents, + $has_subtasks, + true); $related_tabs[] = id(new PHUITabView()) ->setName(pht('Task Graph')) diff --git a/src/applications/maniphest/controller/ManiphestTaskGraphController.php b/src/applications/maniphest/controller/ManiphestTaskGraphController.php new file mode 100644 index 0000000000..2f342a2d0f --- /dev/null +++ b/src/applications/maniphest/controller/ManiphestTaskGraphController.php @@ -0,0 +1,125 @@ +getViewer(); + $id = $request->getURIData('id'); + + $task = id(new ManiphestTaskQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->executeOne(); + if (!$task) { + return new Aphront404Response(); + } + + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb($task->getMonogram(), $task->getURI()) + ->addTextCrumb(pht('Graph')) + ->setBorder(true); + + $graph_limit = 2000; + $overflow_message = null; + $task_graph = id(new ManiphestTaskGraph()) + ->setViewer($viewer) + ->setSeedPHID($task->getPHID()) + ->setLimit($graph_limit) + ->loadGraph(); + if (!$task_graph->isEmpty()) { + $parent_type = ManiphestTaskDependedOnByTaskEdgeType::EDGECONST; + $subtask_type = ManiphestTaskDependsOnTaskEdgeType::EDGECONST; + $parent_map = $task_graph->getEdges($parent_type); + $subtask_map = $task_graph->getEdges($subtask_type); + $parent_list = idx($parent_map, $task->getPHID(), array()); + $subtask_list = idx($subtask_map, $task->getPHID(), array()); + $has_parents = (bool)$parent_list; + $has_subtasks = (bool)$subtask_list; + + // First, get a count of direct parent tasks and subtasks. If there + // are too many of these, we just don't draw anything. You can use + // the search button to browse tasks with the search UI instead. + $direct_count = count($parent_list) + count($subtask_list); + + if ($direct_count > $graph_limit) { + $overflow_message = pht( + 'This task is directly connected to more than %s other tasks, '. + 'which is too many tasks to display. Use %s to browse parents '. + 'or subtasks.', + new PhutilNumber($graph_limit), + phutil_tag('strong', array(), pht('Search...'))); + + $graph_table = null; + } else { + // If there aren't too many direct tasks, but there are too many total + // tasks, we'll only render directly connected tasks. + if ($task_graph->isOverLimit()) { + $task_graph->setRenderOnlyAdjacentNodes(true); + + $overflow_message = pht( + 'This task is connected to more than %s other tasks. '. + 'Only direct parents and subtasks are shown here.', + new PhutilNumber($graph_limit)); + } + + $graph_table = $task_graph->newGraphTable(); + } + + $graph_menu = $this->newTaskGraphDropdownMenu( + $task, + $has_parents, + $has_subtasks, + false); + } else { + $graph_menu = null; + $graph_table = null; + + $overflow_message = pht( + 'This task has no parent tasks and no subtasks, so there is no '. + 'graph to draw.'); + } + + if ($overflow_message) { + $overflow_view = $this->newTaskGraphOverflowView( + $task, + $overflow_message, + false); + + $graph_table = array( + $overflow_view, + $graph_table, + ); + } + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Task Graph')); + + if ($graph_menu) { + $header->addActionLink($graph_menu); + } + + $tab_view = id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->appendChild($graph_table); + + $view = id(new PHUITwoColumnView()) + ->setFooter($tab_view); + + return $this->newPage() + ->setTitle( + array( + $task->getMonogram(), + pht('Graph'), + )) + ->setCrumbs($crumbs) + ->appendChild($view); + } + + +} diff --git a/webroot/rsrc/css/aphront/table-view.css b/webroot/rsrc/css/aphront/table-view.css index 9bc8536054..a08674cf14 100644 --- a/webroot/rsrc/css/aphront/table-view.css +++ b/webroot/rsrc/css/aphront/table-view.css @@ -327,3 +327,40 @@ span.single-display-line-content { .phui-object-box .aphront-table-view { border: none; } + +.object-graph-header { + padding: 8px 12px; + overflow: hidden; + background: {$lightyellow}; + border-bottom: 1px solid {$lightblueborder}; + vertical-align: middle; +} + +.object-graph-header .object-graph-header-icon { + float: left; + margin-top: 10px; +} + +.object-graph-header a.button { + float: right; +} + +.object-graph-header-message { + margin: 8px 200px 8px 20px; +} + +.device .object-graph-header .object-graph-header-icon { + display: none; +} + +.device .object-graph-header-message { + clear: both; + margin: 0; +} + +.device .object-graph-header a.button { + margin: 0 auto 12px; + display: block; + width: 180px; + float: none; +} From c5e16f9bd9960310001976b77d0a183778a34724 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 15 Feb 2019 06:42:53 -0800 Subject: [PATCH 070/245] Give HarbormasterBuildUnitMessage a real Query class Summary: Ref T13088. Prepares for putting test names in a separate table to release the 255-character limit. Test Plan: Viewed revisions, buildables, builds, test lists, specific tests. Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13088 Differential Revision: https://secure.phabricator.com/D20179 --- src/__phutil_library_map__.php | 7 +- .../DifferentialChangesetViewController.php | 10 +-- .../controller/DifferentialController.php | 7 +- .../differential/storage/DifferentialDiff.php | 7 +- .../HarbormasterBuildableViewController.php | 7 +- .../HarbormasterUnitMessageListController.php | 7 +- .../HarbormasterUnitMessageViewController.php | 5 +- .../HarbormasterBuildUnitMessageQuery.php | 64 +++++++++++++++++++ .../build/HarbormasterBuildUnitMessage.php | 24 ++++++- 9 files changed, 119 insertions(+), 19 deletions(-) create mode 100644 src/applications/harbormaster/query/HarbormasterBuildUnitMessageQuery.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 3c9a329d99..f82c92d02b 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1365,6 +1365,7 @@ phutil_register_library_map(array( 'HarbormasterBuildTransactionEditor' => 'applications/harbormaster/editor/HarbormasterBuildTransactionEditor.php', 'HarbormasterBuildTransactionQuery' => 'applications/harbormaster/query/HarbormasterBuildTransactionQuery.php', 'HarbormasterBuildUnitMessage' => 'applications/harbormaster/storage/build/HarbormasterBuildUnitMessage.php', + 'HarbormasterBuildUnitMessageQuery' => 'applications/harbormaster/query/HarbormasterBuildUnitMessageQuery.php', 'HarbormasterBuildViewController' => 'applications/harbormaster/controller/HarbormasterBuildViewController.php', 'HarbormasterBuildWorker' => 'applications/harbormaster/worker/HarbormasterBuildWorker.php', 'HarbormasterBuildable' => 'applications/harbormaster/storage/HarbormasterBuildable.php', @@ -6983,7 +6984,11 @@ phutil_register_library_map(array( 'HarbormasterBuildTransaction' => 'PhabricatorApplicationTransaction', 'HarbormasterBuildTransactionEditor' => 'PhabricatorApplicationTransactionEditor', 'HarbormasterBuildTransactionQuery' => 'PhabricatorApplicationTransactionQuery', - 'HarbormasterBuildUnitMessage' => 'HarbormasterDAO', + 'HarbormasterBuildUnitMessage' => array( + 'HarbormasterDAO', + 'PhabricatorPolicyInterface', + ), + 'HarbormasterBuildUnitMessageQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'HarbormasterBuildViewController' => 'HarbormasterController', 'HarbormasterBuildWorker' => 'HarbormasterWorker', 'HarbormasterBuildable' => array( diff --git a/src/applications/differential/controller/DifferentialChangesetViewController.php b/src/applications/differential/controller/DifferentialChangesetViewController.php index c41f951394..35793a2108 100644 --- a/src/applications/differential/controller/DifferentialChangesetViewController.php +++ b/src/applications/differential/controller/DifferentialChangesetViewController.php @@ -420,15 +420,17 @@ final class DifferentialChangesetViewController extends DifferentialController { } private function loadCoverage(DifferentialChangeset $changeset) { + $viewer = $this->getViewer(); + $target_phids = $changeset->getDiff()->getBuildTargetPHIDs(); if (!$target_phids) { return null; } - $unit = id(new HarbormasterBuildUnitMessage())->loadAllWhere( - 'buildTargetPHID IN (%Ls)', - $target_phids); - + $unit = id(new HarbormasterBuildUnitMessageQuery()) + ->setViewer($viewer) + ->withBuildTargetPHIDs($target_phids) + ->execute(); if (!$unit) { return null; } diff --git a/src/applications/differential/controller/DifferentialController.php b/src/applications/differential/controller/DifferentialController.php index 8fe5b5caca..334d46c3cb 100644 --- a/src/applications/differential/controller/DifferentialController.php +++ b/src/applications/differential/controller/DifferentialController.php @@ -192,9 +192,10 @@ abstract class DifferentialController extends PhabricatorController { $all_target_phids = array_mergev($target_map); if ($all_target_phids) { - $unit_messages = id(new HarbormasterBuildUnitMessage())->loadAllWhere( - 'buildTargetPHID IN (%Ls)', - $all_target_phids); + $unit_messages = id(new HarbormasterBuildUnitMessageQuery()) + ->setViewer($viewer) + ->withBuildTargetPHIDs($all_target_phids) + ->execute(); $unit_messages = mgroup($unit_messages, 'getBuildTargetPHID'); } else { $unit_messages = array(); diff --git a/src/applications/differential/storage/DifferentialDiff.php b/src/applications/differential/storage/DifferentialDiff.php index e4c33dc766..a39610c54c 100644 --- a/src/applications/differential/storage/DifferentialDiff.php +++ b/src/applications/differential/storage/DifferentialDiff.php @@ -387,9 +387,10 @@ final class DifferentialDiff return array(); } - $unit = id(new HarbormasterBuildUnitMessage())->loadAllWhere( - 'buildTargetPHID IN (%Ls)', - $target_phids); + $unit = id(new HarbormasterBuildUnitMessageQuery()) + ->setViewer($viewer) + ->withBuildTargetPHIDs($target_phids) + ->execute(); $map = array(); foreach ($unit as $message) { diff --git a/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php b/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php index 1e79ad2b46..40f6587116 100644 --- a/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php @@ -312,9 +312,10 @@ final class HarbormasterBuildableViewController 'buildTargetPHID IN (%Ls)', $target_phids); - $unit_data = id(new HarbormasterBuildUnitMessage())->loadAllWhere( - 'buildTargetPHID IN (%Ls)', - $target_phids); + $unit_data = id(new HarbormasterBuildUnitMessageQuery()) + ->setViewer($viewer) + ->withBuildTargetPHIDs($target_phids) + ->execute(); if ($lint_data) { $lint_table = id(new HarbormasterLintPropertyView()) diff --git a/src/applications/harbormaster/controller/HarbormasterUnitMessageListController.php b/src/applications/harbormaster/controller/HarbormasterUnitMessageListController.php index d548ceac98..a87d17c4fa 100644 --- a/src/applications/harbormaster/controller/HarbormasterUnitMessageListController.php +++ b/src/applications/harbormaster/controller/HarbormasterUnitMessageListController.php @@ -31,9 +31,10 @@ final class HarbormasterUnitMessageListController $unit_data = array(); if ($target_phids) { - $unit_data = id(new HarbormasterBuildUnitMessage())->loadAllWhere( - 'buildTargetPHID IN (%Ls)', - $target_phids); + $unit_data = id(new HarbormasterBuildUnitMessageQuery()) + ->setViewer($viewer) + ->withBuildTargetPHIDs($target_phids) + ->execute(); } else { $unit_data = array(); } diff --git a/src/applications/harbormaster/controller/HarbormasterUnitMessageViewController.php b/src/applications/harbormaster/controller/HarbormasterUnitMessageViewController.php index 5cb33c0c9a..7111db654f 100644 --- a/src/applications/harbormaster/controller/HarbormasterUnitMessageViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterUnitMessageViewController.php @@ -12,7 +12,10 @@ final class HarbormasterUnitMessageViewController $message_id = $request->getURIData('id'); - $message = id(new HarbormasterBuildUnitMessage())->load($message_id); + $message = id(new HarbormasterBuildUnitMessageQuery()) + ->setViewer($viewer) + ->withIDs(array($message_id)) + ->executeOne(); if (!$message) { return new Aphront404Response(); } diff --git a/src/applications/harbormaster/query/HarbormasterBuildUnitMessageQuery.php b/src/applications/harbormaster/query/HarbormasterBuildUnitMessageQuery.php new file mode 100644 index 0000000000..fbd33a5d95 --- /dev/null +++ b/src/applications/harbormaster/query/HarbormasterBuildUnitMessageQuery.php @@ -0,0 +1,64 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withBuildTargetPHIDs(array $target_phids) { + $this->targetPHIDs = $target_phids; + return $this; + } + + public function newResultObject() { + return new HarbormasterBuildUnitMessage(); + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); + + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->phids !== null) { + $where[] = qsprintf( + $conn, + 'phid in (%Ls)', + $this->phids); + } + + if ($this->targetPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'buildTargetPHID in (%Ls)', + $this->targetPHIDs); + } + + return $where; + } + + public function getQueryApplicationClass() { + return 'PhabricatorHarbormasterApplication'; + } + +} diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuildUnitMessage.php b/src/applications/harbormaster/storage/build/HarbormasterBuildUnitMessage.php index b2f566c3eb..1f5e5e1440 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuildUnitMessage.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuildUnitMessage.php @@ -1,7 +1,8 @@ Date: Fri, 15 Feb 2019 04:39:49 -0800 Subject: [PATCH 071/245] Correct schema irregularities (including weird keys) with worker task tables Summary: Ref T13253. Fixes T6615. See that task for discussion. - Remove three keys which serve no real purpose: `dataID` doesn't do anything for us, and the two `leaseOwner` keys are unused. - Rename `leaseOwner_2` to `key_owner`. - Fix an issue where `dataID` was nullable in the active table and non-nullable in the archive table. In practice, //all// workers have data, so all workers have a `dataID`: if they didn't, we'd already fatal when trying to move tasks to the archive table. Just clean this up for consistency, and remove the ancient codepath which imagined tasks with no data. Test Plan: - Ran `bin/storage upgrade`, inspected tables. - Ran `bin/phd debug taskmaster`, worked through a bunch of tasks with no problems. Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13253, T6615 Differential Revision: https://secure.phabricator.com/D20175 --- .../20190215.daemons.01.dropdataid.php | 21 +++++++++++++++++++ .../20190215.daemons.02.nulldataid.sql | 2 ++ .../storage/PhabricatorWorkerActiveTask.php | 18 ++-------------- .../storage/PhabricatorWorkerArchiveTask.php | 3 --- 4 files changed, 25 insertions(+), 19 deletions(-) create mode 100644 resources/sql/autopatches/20190215.daemons.01.dropdataid.php create mode 100644 resources/sql/autopatches/20190215.daemons.02.nulldataid.sql diff --git a/resources/sql/autopatches/20190215.daemons.01.dropdataid.php b/resources/sql/autopatches/20190215.daemons.01.dropdataid.php new file mode 100644 index 0000000000..05cc4adfee --- /dev/null +++ b/resources/sql/autopatches/20190215.daemons.01.dropdataid.php @@ -0,0 +1,21 @@ +establishConnection('w'); + +try { + queryfx( + $conn, + 'ALTER TABLE %R DROP KEY %T', + $table, + 'dataID'); +} catch (AphrontQueryException $ex) { + // Ignore. +} diff --git a/resources/sql/autopatches/20190215.daemons.02.nulldataid.sql b/resources/sql/autopatches/20190215.daemons.02.nulldataid.sql new file mode 100644 index 0000000000..19be602efe --- /dev/null +++ b/resources/sql/autopatches/20190215.daemons.02.nulldataid.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_worker.worker_activetask + CHANGE dataID dataID INT UNSIGNED NOT NULL; diff --git a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php index ed1eeaea63..b6bd462df7 100644 --- a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php +++ b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php @@ -14,35 +14,21 @@ final class PhabricatorWorkerActiveTask extends PhabricatorWorkerTask { self::CONFIG_IDS => self::IDS_COUNTER, self::CONFIG_TIMESTAMPS => false, self::CONFIG_KEY_SCHEMA => array( - 'dataID' => array( - 'columns' => array('dataID'), - 'unique' => true, - ), 'taskClass' => array( 'columns' => array('taskClass'), ), 'leaseExpires' => array( 'columns' => array('leaseExpires'), ), - 'leaseOwner' => array( - 'columns' => array('leaseOwner(16)'), - ), 'key_failuretime' => array( 'columns' => array('failureTime'), ), - 'leaseOwner_2' => array( + 'key_owner' => array( 'columns' => array('leaseOwner', 'priority', 'id'), ), ) + $parent[self::CONFIG_KEY_SCHEMA], ); - $config[self::CONFIG_COLUMN_SCHEMA] = array( - // T6203/NULLABILITY - // This isn't nullable in the archive table, so at a minimum these - // should be the same. - 'dataID' => 'uint32?', - ) + $parent[self::CONFIG_COLUMN_SCHEMA]; - return $config + $parent; } @@ -74,7 +60,7 @@ final class PhabricatorWorkerActiveTask extends PhabricatorWorkerTask { $this->failureCount = 0; } - if ($is_new && ($this->getData() !== null)) { + if ($is_new) { $data = new PhabricatorWorkerTaskData(); $data->setData($this->getData()); $data->save(); diff --git a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerArchiveTask.php b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerArchiveTask.php index fe1164e532..0062d07a84 100644 --- a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerArchiveTask.php +++ b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerArchiveTask.php @@ -28,9 +28,6 @@ final class PhabricatorWorkerArchiveTask extends PhabricatorWorkerTask { 'dateCreated' => array( 'columns' => array('dateCreated'), ), - 'leaseOwner' => array( - 'columns' => array('leaseOwner', 'priority', 'id'), - ), 'key_modified' => array( 'columns' => array('dateModified'), ), From 0b2d25778d8f71a860c4622d2e92e76f1bd47351 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 13 Feb 2019 09:13:46 -0800 Subject: [PATCH 072/245] Add basic, rough support for changing field behavior based on object subtype Summary: Ref T13248. This will probably need quite a bit of refinement, but we can reasonably allow subtype definitions to adjust custom field behavior. Some places where we use fields are global, and always need to show all the fields. For example, on `/maniphest/`, where you can search across all tasks, you need to be able to search across all fields that are present on any task. Likewise, if you "export" a bunch of tasks into a spreadsheet, we need to have columns for every field. However, when you're clearly in the scope of a particular task (like viewing or editing `T123`), there's no reason we can't hide fields based on the task subtype. To start with, allow subtypes to override "disabled" and "name" for custom fields. Test Plan: - Defined several custom fields and several subtypes. - Disabled/renamed some fields for some subtypes. - Viewed/edited tasks of different subtypes, got desired field behavior. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13248 Differential Revision: https://secure.phabricator.com/D20161 --- .../PhabricatorManiphestConfigOptions.php | 23 +++++ .../editengine/PhabricatorEditEngine.php | 19 +++- .../PhabricatorEditEngineSubtype.php | 34 ++++++++ .../field/PhabricatorCustomField.php | 87 +++++++++++++++++++ .../PhabricatorStandardCustomField.php | 14 +++ 5 files changed, 175 insertions(+), 2 deletions(-) diff --git a/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php b/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php index 8f2830908b..05f98a2f62 100644 --- a/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php +++ b/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php @@ -342,6 +342,7 @@ dictionary with these keys: - `icon` //Optional string.// Icon for the subtype. - `children` //Optional map.// Configure options shown to the user when they "Create Subtask". See below. + - `fields` //Optional map.// Configure field behaviors. See below. Each subtype must have a unique key, and you must define a subtype with the key "%s", which is used as a default subtype. @@ -397,6 +398,28 @@ be used when presenting options to the user. If only one option would be presented, the user will be taken directly to the appropriate form instead of being prompted to choose a form. + +The `fields` key can configure the behavior of custom fields on specific +task subtypes. For example: + +``` +{ + ... + "fields": { + "custom.some-field": { + "disabled": true + } + } + ... +} +``` + +Each field supports these options: + + - `disabled` //Optional bool.// Allows you to disable fields on certain + subtypes. + - `name` //Optional string.// Custom name of this field for the subtype. + EOTEXT , $subtype_default_key)); diff --git a/src/applications/transactions/editengine/PhabricatorEditEngine.php b/src/applications/transactions/editengine/PhabricatorEditEngine.php index 353d7ee385..82af45dca8 100644 --- a/src/applications/transactions/editengine/PhabricatorEditEngine.php +++ b/src/applications/transactions/editengine/PhabricatorEditEngine.php @@ -165,14 +165,29 @@ abstract class PhabricatorEditEngine $extensions = array(); } + // See T13248. Create a template object to provide to extensions. We + // adjust the template to have the intended subtype, so that extensions + // may change behavior based on the form subtype. + + $template_object = clone $object; + if ($this->getIsCreate()) { + if ($this->supportsSubtypes()) { + $config = $this->getEditEngineConfiguration(); + $subtype = $config->getSubtype(); + $template_object->setSubtype($subtype); + } + } + foreach ($extensions as $extension) { $extension->setViewer($viewer); - if (!$extension->supportsObject($this, $object)) { + if (!$extension->supportsObject($this, $template_object)) { continue; } - $extension_fields = $extension->buildCustomEditFields($this, $object); + $extension_fields = $extension->buildCustomEditFields( + $this, + $template_object); // TODO: Validate this in more detail with a more tailored error. assert_instances_of($extension_fields, 'PhabricatorEditField'); diff --git a/src/applications/transactions/editengine/PhabricatorEditEngineSubtype.php b/src/applications/transactions/editengine/PhabricatorEditEngineSubtype.php index 9e754a3ca8..6e1d1de115 100644 --- a/src/applications/transactions/editengine/PhabricatorEditEngineSubtype.php +++ b/src/applications/transactions/editengine/PhabricatorEditEngineSubtype.php @@ -13,6 +13,7 @@ final class PhabricatorEditEngineSubtype private $color; private $childSubtypes = array(); private $childIdentifiers = array(); + private $fieldConfiguration = array(); public function setKey($key) { $this->key = $key; @@ -94,6 +95,17 @@ final class PhabricatorEditEngineSubtype return $view; } + public function setSubtypeFieldConfiguration( + $subtype_key, + array $configuration) { + $this->fieldConfiguration[$subtype_key] = $configuration; + return $this; + } + + public function getSubtypeFieldConfiguration($subtype_key) { + return idx($this->fieldConfiguration, $subtype_key); + } + public static function validateSubtypeKey($subtype) { if (strlen($subtype) > 64) { throw new Exception( @@ -139,6 +151,7 @@ final class PhabricatorEditEngineSubtype 'color' => 'optional string', 'icon' => 'optional string', 'children' => 'optional map', + 'fields' => 'optional map', )); $key = $value['key']; @@ -183,6 +196,18 @@ final class PhabricatorEditEngineSubtype 'or the other, but not both.')); } } + + $fields = idx($value, 'fields'); + if ($fields) { + foreach ($fields as $field_key => $configuration) { + PhutilTypeSpec::checkMap( + $configuration, + array( + 'disabled' => 'optional bool', + 'name' => 'optional string', + )); + } + } } if (!isset($map[self::SUBTYPE_DEFAULT])) { @@ -233,6 +258,15 @@ final class PhabricatorEditEngineSubtype $subtype->setChildFormIdentifiers($child_forms); } + $field_configurations = idx($entry, 'fields'); + if ($field_configurations) { + foreach ($field_configurations as $field_key => $field_configuration) { + $subtype->setSubtypeFieldConfiguration( + $field_key, + $field_configuration); + } + } + $map[$key] = $subtype; } diff --git a/src/infrastructure/customfield/field/PhabricatorCustomField.php b/src/infrastructure/customfield/field/PhabricatorCustomField.php index d7df3c5b78..c6c70a9614 100644 --- a/src/infrastructure/customfield/field/PhabricatorCustomField.php +++ b/src/infrastructure/customfield/field/PhabricatorCustomField.php @@ -74,9 +74,22 @@ abstract class PhabricatorCustomField extends Phobject { $spec, $object); + $fields = self::adjustCustomFieldsForObjectSubtype( + $object, + $role, + $fields); + foreach ($fields as $key => $field) { + // NOTE: We perform this filtering in "buildFieldList()", but may need + // to filter again after subtype adjustment. + if (!$field->isFieldEnabled()) { + unset($fields[$key]); + continue; + } + if (!$field->shouldEnableForRole($role)) { unset($fields[$key]); + continue; } } @@ -1622,4 +1635,78 @@ abstract class PhabricatorCustomField extends Phobject { return null; } + private static function adjustCustomFieldsForObjectSubtype( + PhabricatorCustomFieldInterface $object, + $role, + array $fields) { + assert_instances_of($fields, __CLASS__); + + // We only apply subtype adjustment for some roles. For example, when + // writing Herald rules or building a Search interface, we always want to + // show all the fields in their default state, so we do not apply any + // adjustments. + $subtype_roles = array( + self::ROLE_EDITENGINE, + self::ROLE_VIEW, + ); + + $subtype_roles = array_fuse($subtype_roles); + if (!isset($subtype_roles[$role])) { + return $fields; + } + + // If the object doesn't support subtypes, we can't possibly make + // any adjustments based on subtype. + if (!($object instanceof PhabricatorEditEngineSubtypeInterface)) { + return $fields; + } + + $subtype_map = $object->newEditEngineSubtypeMap(); + $subtype_key = $object->getEditEngineSubtype(); + $subtype_object = $subtype_map->getSubtype($subtype_key); + + $map = array(); + foreach ($fields as $field) { + $modern_key = $field->getModernFieldKey(); + if (!strlen($modern_key)) { + continue; + } + + $map[$modern_key] = $field; + } + + foreach ($map as $field_key => $field) { + // For now, only support overriding standard custom fields. In the + // future there's no technical or product reason we couldn't let you + // override (some properites of) other fields like "Title", but they + // don't usually support appropriate "setX()" methods today. + if (!($field instanceof PhabricatorStandardCustomField)) { + // For fields that are proxies on top of StandardCustomField, which + // is how most application custom fields work today, we can reconfigure + // the proxied field instead. + $field = $field->getProxy(); + if (!$field || !($field instanceof PhabricatorStandardCustomField)) { + continue; + } + } + + $subtype_config = $subtype_object->getSubtypeFieldConfiguration( + $field_key); + + if (!$subtype_config) { + continue; + } + + if (isset($subtype_config['disabled'])) { + $field->setIsEnabled(!$subtype_config['disabled']); + } + + if (isset($subtype_config['name'])) { + $field->setFieldName($subtype_config['name']); + } + } + + return $fields; + } + } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php index 5bd6256b73..4c0bce861b 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php @@ -18,6 +18,7 @@ abstract class PhabricatorStandardCustomField private $isCopyable; private $hasStorageValue; private $isBuiltin; + private $isEnabled = true; abstract public function getFieldType(); @@ -175,6 +176,19 @@ abstract class PhabricatorStandardCustomField return $this->rawKey; } + public function setIsEnabled($is_enabled) { + $this->isEnabled = $is_enabled; + return $this; + } + + public function getIsEnabled() { + return $this->isEnabled; + } + + public function isFieldEnabled() { + return $this->getIsEnabled(); + } + /* -( PhabricatorCustomField )--------------------------------------------- */ From 3058cae4b82e104686717f01e16790d38b5360f4 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 8 Feb 2019 06:07:24 -0800 Subject: [PATCH 073/245] Allow task statuses to specify that either "comments" or "edits" are "locked" Summary: Ref T13249. See PHI1059. This allows "locked" in `maniphest.statuses` to specify that either "comments" are locked (current behavior, advisory, overridable by users with edit permission, e.g. for calming discussion on a contentious issue or putting a guard rail on things); or "edits" are locked (hard lock, only task owner can edit things). Roughly, "comments" is a soft/advisory lock. "edits" is a hard/strict lock. (I think both types of locks have reasonable use cases, which is why I'm not just making locks stronger across the board.) When "edits" are locked: - The edit policy looks like "no one" to normal callers. - In one special case, we sneak the real value through a back channel using PolicyCodex in the specific narrow case that you're editing the object. Otherwise, the policy selector control incorrectly switches to "No One". - We also have to do a little more validation around applying a mixture of status + owner transactions that could leave the task uneditable. For now, I'm allowing you to reassign a hard-locked task to someone else. If you get this wrong, we can end up in a state where no one can edit the task. If this is an issue, we could respond in various ways: prevent these edits; prevent assigning to disabled users; provide a `bin/task reassign`; uh maybe have a quorum convene? Test Plan: - Defined "Soft Locked" and "Hard Locked" statues. - "Hard Locked" a task, hit errors (trying to unassign myself, trying to hard lock an unassigned task). - Saw nice new policy guidance icon in header. {F6210362} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13249 Differential Revision: https://secure.phabricator.com/D20165 --- resources/celerity/map.php | 6 +- src/__phutil_library_map__.php | 3 + .../PhabricatorManiphestConfigOptions.php | 5 +- .../constants/ManiphestTaskStatus.php | 38 ++++++++- .../editor/ManiphestTransactionEditor.php | 85 +++++++++++++++++++ .../policy/ManiphestTaskPolicyCodex.php | 70 +++++++++++++++ .../maniphest/storage/ManiphestTask.php | 31 +++++-- .../policy/codex/PhabricatorPolicyCodex.php | 4 + .../PhabricatorPolicyEditEngineExtension.php | 22 ++++- webroot/rsrc/css/phui/phui-header-view.css | 10 +++ 10 files changed, 260 insertions(+), 14 deletions(-) create mode 100644 src/applications/maniphest/policy/ManiphestTaskPolicyCodex.php diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 2d402dbaa3..19f8924555 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,7 +9,7 @@ return array( 'names' => array( 'conpherence.pkg.css' => '3c8a0668', 'conpherence.pkg.js' => '020aebcf', - 'core.pkg.css' => '85a1da99', + 'core.pkg.css' => 'f2319e1f', 'core.pkg.js' => '5c737607', 'differential.pkg.css' => 'b8df73d4', 'differential.pkg.js' => '67c9ea4c', @@ -154,7 +154,7 @@ return array( 'rsrc/css/phui/phui-form-view.css' => '01b796c0', 'rsrc/css/phui/phui-form.css' => '159e2d9c', 'rsrc/css/phui/phui-head-thing.css' => 'd7f293df', - 'rsrc/css/phui/phui-header-view.css' => '93cea4ec', + 'rsrc/css/phui/phui-header-view.css' => '285c9139', 'rsrc/css/phui/phui-hovercard.css' => '6ca90fa0', 'rsrc/css/phui/phui-icon-set-selector.css' => '7aa5f3ec', 'rsrc/css/phui/phui-icon.css' => '4cbc684a', @@ -821,7 +821,7 @@ return array( 'phui-form-css' => '159e2d9c', 'phui-form-view-css' => '01b796c0', 'phui-head-thing-view-css' => 'd7f293df', - 'phui-header-view-css' => '93cea4ec', + 'phui-header-view-css' => '285c9139', 'phui-hovercard' => '074f0783', 'phui-hovercard-view-css' => '6ca90fa0', 'phui-icon-set-selector-css' => '7aa5f3ec', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index f82c92d02b..37be79ec59 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1743,6 +1743,7 @@ phutil_register_library_map(array( 'ManiphestTaskParentTransaction' => 'applications/maniphest/xaction/ManiphestTaskParentTransaction.php', 'ManiphestTaskPoints' => 'applications/maniphest/constants/ManiphestTaskPoints.php', 'ManiphestTaskPointsTransaction' => 'applications/maniphest/xaction/ManiphestTaskPointsTransaction.php', + 'ManiphestTaskPolicyCodex' => 'applications/maniphest/policy/ManiphestTaskPolicyCodex.php', 'ManiphestTaskPriority' => 'applications/maniphest/constants/ManiphestTaskPriority.php', 'ManiphestTaskPriorityDatasource' => 'applications/maniphest/typeahead/ManiphestTaskPriorityDatasource.php', 'ManiphestTaskPriorityHeraldAction' => 'applications/maniphest/herald/ManiphestTaskPriorityHeraldAction.php', @@ -7384,6 +7385,7 @@ phutil_register_library_map(array( 'PhabricatorEditEngineSubtypeInterface', 'PhabricatorEditEngineLockableInterface', 'PhabricatorEditEngineMFAInterface', + 'PhabricatorPolicyCodexInterface', ), 'ManiphestTaskAssignHeraldAction' => 'HeraldAction', 'ManiphestTaskAssignOtherHeraldAction' => 'ManiphestTaskAssignHeraldAction', @@ -7435,6 +7437,7 @@ phutil_register_library_map(array( 'ManiphestTaskParentTransaction' => 'ManiphestTaskTransactionType', 'ManiphestTaskPoints' => 'Phobject', 'ManiphestTaskPointsTransaction' => 'ManiphestTaskTransactionType', + 'ManiphestTaskPolicyCodex' => 'PhabricatorPolicyCodex', 'ManiphestTaskPriority' => 'ManiphestConstants', 'ManiphestTaskPriorityDatasource' => 'PhabricatorTypeaheadDatasource', 'ManiphestTaskPriorityHeraldAction' => 'HeraldAction', diff --git a/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php b/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php index 05f98a2f62..f1916cffec 100644 --- a/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php +++ b/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php @@ -210,8 +210,9 @@ The keys you can provide in a specification are: - `claim` //Optional bool.// By default, closing an unassigned task claims it. You can set this to `false` to disable this behavior for a particular status. - - `locked` //Optional bool.// Lock tasks in this status, preventing users - from commenting. + - `locked` //Optional string.// Lock tasks in this status. Specify "comments" + to lock comments (users who can edit the task may override this lock). + Specify "edits" to prevent anyone except the task owner from making edits. - `mfa` //Optional bool.// Require all edits to this task to be signed with multi-factor authentication. diff --git a/src/applications/maniphest/constants/ManiphestTaskStatus.php b/src/applications/maniphest/constants/ManiphestTaskStatus.php index 4d58816e2a..c040befeed 100644 --- a/src/applications/maniphest/constants/ManiphestTaskStatus.php +++ b/src/applications/maniphest/constants/ManiphestTaskStatus.php @@ -16,6 +16,9 @@ final class ManiphestTaskStatus extends ManiphestConstants { const SPECIAL_CLOSED = 'closed'; const SPECIAL_DUPLICATE = 'duplicate'; + const LOCKED_COMMENTS = 'comments'; + const LOCKED_EDITS = 'edits'; + private static function getStatusConfig() { return PhabricatorEnv::getEnvConfig('maniphest.statuses'); } @@ -156,8 +159,13 @@ final class ManiphestTaskStatus extends ManiphestConstants { return !self::isOpenStatus($status); } - public static function isLockedStatus($status) { - return self::getStatusAttribute($status, 'locked', false); + public static function areCommentsLockedInStatus($status) { + return (bool)self::getStatusAttribute($status, 'locked', false); + } + + public static function areEditsLockedInStatus($status) { + $locked = self::getStatusAttribute($status, 'locked'); + return ($locked === self::LOCKED_EDITS); } public static function isMFAStatus($status) { @@ -285,11 +293,35 @@ final class ManiphestTaskStatus extends ManiphestConstants { 'keywords' => 'optional list', 'disabled' => 'optional bool', 'claim' => 'optional bool', - 'locked' => 'optional bool', + 'locked' => 'optional bool|string', 'mfa' => 'optional bool', )); } + // Supported values are "comments" or "edits". For backward compatibility, + // "true" is an alias of "comments". + + foreach ($config as $key => $value) { + $locked = idx($value, 'locked', false); + if ($locked === true || $locked === false) { + continue; + } + + if ($locked === self::LOCKED_EDITS || + $locked === self::LOCKED_COMMENTS) { + continue; + } + + throw new Exception( + pht( + 'Task status ("%s") has unrecognized value for "locked" '. + 'configuration ("%s"). Supported values are: "%s", "%s".', + $key, + $locked, + self::LOCKED_COMMENTS, + self::LOCKED_EDITS)); + } + $special_map = array(); foreach ($config as $key => $value) { $special = idx($value, 'special'); diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php index 0722e0e27a..1748e5e84e 100644 --- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php +++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php @@ -552,6 +552,10 @@ final class ManiphestTransactionEditor $errors = array_merge($errors, $this->moreValidationErrors); } + foreach ($this->getLockValidationErrors($object, $xactions) as $error) { + $errors[] = $error; + } + return $errors; } @@ -1011,5 +1015,86 @@ final class ManiphestTransactionEditor } + private function getLockValidationErrors($object, array $xactions) { + $errors = array(); + + $old_owner = $object->getOwnerPHID(); + $old_status = $object->getStatus(); + + $new_owner = $old_owner; + $new_status = $old_status; + + $owner_xaction = null; + $status_xaction = null; + + foreach ($xactions as $xaction) { + switch ($xaction->getTransactionType()) { + case ManiphestTaskOwnerTransaction::TRANSACTIONTYPE: + $new_owner = $xaction->getNewValue(); + $owner_xaction = $xaction; + break; + case ManiphestTaskStatusTransaction::TRANSACTIONTYPE: + $new_status = $xaction->getNewValue(); + $status_xaction = $xaction; + break; + } + } + + $actor_phid = $this->getActingAsPHID(); + + $was_locked = ManiphestTaskStatus::areEditsLockedInStatus( + $old_status); + $now_locked = ManiphestTaskStatus::areEditsLockedInStatus( + $new_status); + + if (!$now_locked) { + // If we're not ending in an edit-locked status, everything is good. + } else if ($new_owner !== null) { + // If we ending the edit with some valid owner, this is allowed for + // now. We might need to revisit this. + } else { + // The edits end with the task locked and unowned. No one will be able + // to edit it, so we forbid this. We try to be specific about what the + // user did wrong. + + $owner_changed = ($old_owner && !$new_owner); + $status_changed = ($was_locked !== $now_locked); + $message = null; + + if ($status_changed && $owner_changed) { + $message = pht( + 'You can not lock this task and unassign it at the same time '. + 'because no one will be able to edit it anymore. Lock the task '. + 'or remove the owner, but not both.'); + $problem_xaction = $status_xaction; + } else if ($status_changed) { + $message = pht( + 'You can not lock this task because it does not have an owner. '. + 'No one would be able to edit the task. Assign the task to an '. + 'owner before locking it.'); + $problem_xaction = $status_xaction; + } else if ($owner_changed) { + $message = pht( + 'You can not remove the owner of this task because it is locked '. + 'and no one would be able to edit the task. Reassign the task or '. + 'unlock it before removing the owner.'); + $problem_xaction = $owner_xaction; + } else { + // If the task was already broken, we don't have a transaction to + // complain about so just let it through. In theory, this is + // impossible since policy rules should kick in before we get here. + } + + if ($message) { + $errors[] = new PhabricatorApplicationTransactionValidationError( + $problem_xaction->getTransactionType(), + pht('Lock Error'), + $message, + $problem_xaction); + } + } + + return $errors; + } } diff --git a/src/applications/maniphest/policy/ManiphestTaskPolicyCodex.php b/src/applications/maniphest/policy/ManiphestTaskPolicyCodex.php new file mode 100644 index 0000000000..638d9bfa60 --- /dev/null +++ b/src/applications/maniphest/policy/ManiphestTaskPolicyCodex.php @@ -0,0 +1,70 @@ +getObject(); + + if ($object->areEditsLocked()) { + return pht('Edits Locked'); + } + + return null; + } + + public function getPolicyIcon() { + $object = $this->getObject(); + + if ($object->areEditsLocked()) { + return 'fa-lock'; + } + + return null; + } + + public function getPolicyTagClasses() { + $object = $this->getObject(); + $classes = array(); + + if ($object->areEditsLocked()) { + $classes[] = 'policy-adjusted-locked'; + } + + return $classes; + } + + public function getPolicySpecialRuleDescriptions() { + $object = $this->getObject(); + + $rules = array(); + + $rules[] = $this->newRule() + ->setCapabilities( + array( + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->setIsActive($object->areEditsLocked()) + ->setDescription( + pht( + 'Tasks with edits locked may only be edited by their owner.')); + + return $rules; + } + + public function getPolicyForEdit($capability) { + + // When a task has its edits locked, the effective edit policy is locked + // to "No One". However, the task owner may still bypass the lock and edit + // the task. When they do, we want the control in the UI to have the + // correct value. Return the real value stored on the object. + + switch ($capability) { + case PhabricatorPolicyCapability::CAN_EDIT: + return $this->getObject()->getEditPolicy(); + } + + return parent::getPolicyForEdit($capability); + } + +} diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php index ada537fa4d..0193830b39 100644 --- a/src/applications/maniphest/storage/ManiphestTask.php +++ b/src/applications/maniphest/storage/ManiphestTask.php @@ -20,7 +20,8 @@ final class ManiphestTask extends ManiphestDAO DoorkeeperBridgedObjectInterface, PhabricatorEditEngineSubtypeInterface, PhabricatorEditEngineLockableInterface, - PhabricatorEditEngineMFAInterface { + PhabricatorEditEngineMFAInterface, + PhabricatorPolicyCodexInterface { const MARKUP_FIELD_DESCRIPTION = 'markup:desc'; @@ -217,8 +218,16 @@ final class ManiphestTask extends ManiphestDAO return ManiphestTaskStatus::isClosedStatus($this->getStatus()); } - public function isLocked() { - return ManiphestTaskStatus::isLockedStatus($this->getStatus()); + public function areCommentsLocked() { + if ($this->areEditsLocked()) { + return true; + } + + return ManiphestTaskStatus::areCommentsLockedInStatus($this->getStatus()); + } + + public function areEditsLocked() { + return ManiphestTaskStatus::areEditsLockedInStatus($this->getStatus()); } public function setProperty($key, $value) { @@ -371,13 +380,17 @@ final class ManiphestTask extends ManiphestDAO case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_INTERACT: - if ($this->isLocked()) { + if ($this->areCommentsLocked()) { return PhabricatorPolicies::POLICY_NOONE; } else { return $this->getViewPolicy(); } case PhabricatorPolicyCapability::CAN_EDIT: - return $this->getEditPolicy(); + if ($this->areEditsLocked()) { + return PhabricatorPolicies::POLICY_NOONE; + } else { + return $this->getEditPolicy(); + } } } @@ -628,4 +641,12 @@ final class ManiphestTask extends ManiphestDAO return new ManiphestTaskMFAEngine(); } + +/* -( PhabricatorPolicyCodexInterface )------------------------------------ */ + + + public function newPolicyCodex() { + return new ManiphestTaskPolicyCodex(); + } + } diff --git a/src/applications/policy/codex/PhabricatorPolicyCodex.php b/src/applications/policy/codex/PhabricatorPolicyCodex.php index 060a798e0a..48e6d2f557 100644 --- a/src/applications/policy/codex/PhabricatorPolicyCodex.php +++ b/src/applications/policy/codex/PhabricatorPolicyCodex.php @@ -29,6 +29,10 @@ abstract class PhabricatorPolicyCodex return array(); } + public function getPolicyForEdit($capability) { + return $this->getObject()->getPolicy($capability); + } + public function getDefaultPolicy() { return PhabricatorPolicyQuery::getDefaultPolicyForObject( $this->viewer, diff --git a/src/applications/policy/editor/PhabricatorPolicyEditEngineExtension.php b/src/applications/policy/editor/PhabricatorPolicyEditEngineExtension.php index 568b7bc399..14a4768f21 100644 --- a/src/applications/policy/editor/PhabricatorPolicyEditEngineExtension.php +++ b/src/applications/policy/editor/PhabricatorPolicyEditEngineExtension.php @@ -68,6 +68,14 @@ final class PhabricatorPolicyEditEngineExtension ), ); + if ($object instanceof PhabricatorPolicyCodexInterface) { + $codex = PhabricatorPolicyCodex::newFromObject( + $object, + $viewer); + } else { + $codex = null; + } + $fields = array(); foreach ($map as $type => $spec) { if (empty($types[$type])) { @@ -82,6 +90,18 @@ final class PhabricatorPolicyEditEngineExtension $conduit_description = $spec['description.conduit']; $edit = $spec['edit']; + // Objects may present a policy value to the edit workflow that is + // different from their nominal policy value: for example, when tasks + // are locked, they appear as "Editable By: No One" to other applications + // but we still want to edit the actual policy stored in the database + // when we show the user a form with a policy control in it. + + if ($codex) { + $policy_value = $codex->getPolicyForEdit($capability); + } else { + $policy_value = $object->getPolicy($capability); + } + $policy_field = id(new PhabricatorPolicyEditField()) ->setKey($key) ->setLabel($label) @@ -94,7 +114,7 @@ final class PhabricatorPolicyEditEngineExtension ->setDescription($description) ->setConduitDescription($conduit_description) ->setConduitTypeDescription(pht('New policy PHID or constant.')) - ->setValue($object->getPolicy($capability)); + ->setValue($policy_value); $fields[] = $policy_field; if ($object instanceof PhabricatorSpacesInterface) { diff --git a/webroot/rsrc/css/phui/phui-header-view.css b/webroot/rsrc/css/phui/phui-header-view.css index 6cafb3e330..6a096af76d 100644 --- a/webroot/rsrc/css/phui/phui-header-view.css +++ b/webroot/rsrc/css/phui/phui-header-view.css @@ -249,6 +249,16 @@ body .phui-header-shell.phui-bleed-header color: {$sh-indigotext}; } +.policy-header-callout.policy-adjusted-locked { + background: {$sh-pinkbackground}; +} + +.policy-header-callout.policy-adjusted-locked .policy-link, +.policy-header-callout.policy-adjusted-locked .phui-icon-view { + color: {$sh-pinktext}; +} + + .policy-header-callout .policy-space-container { font-weight: bold; color: {$sh-redtext}; From dbcf41dbea007592cf12bb35fb3c02c9cdf05343 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 16 Feb 2019 07:26:19 -0800 Subject: [PATCH 074/245] Fix a couple more "URI->alter()" callsites in paging code Summary: `grep` had a hard time finding these. Test Plan: Will just hotfix this since I'm still reasonably in the deploy window, this currently fatals: Reviewers: amckinley Differential Revision: https://secure.phabricator.com/D20186 --- src/view/phui/PHUIPagerView.php | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/view/phui/PHUIPagerView.php b/src/view/phui/PHUIPagerView.php index b78efcda96..2bb3a8276e 100644 --- a/src/view/phui/PHUIPagerView.php +++ b/src/view/phui/PHUIPagerView.php @@ -187,9 +187,15 @@ final class PHUIPagerView extends AphrontView { foreach ($pager_index as $key => $index) { if ($index !== null) { $display_index = $this->getDisplayIndex($index); - $pager_links[$key] = (string)$base_uri->alter( - $parameter, - $display_index); + + $uri = id(clone $base_uri); + if ($display_index === null) { + $uri->removeQueryParam($parameter); + } else { + $uri->replaceQueryParam($parameter, $display_index); + } + + $pager_links[$key] = phutil_string_cast($uri); } } Javelin::initBehavior('phabricator-keyboard-pager', $pager_links); @@ -200,10 +206,17 @@ final class PHUIPagerView extends AphrontView { foreach ($links as $link) { list($index, $label, $class) = $link; $display_index = $this->getDisplayIndex($index); - $link = $base_uri->alter($parameter, $display_index); + + $uri = id(clone $base_uri); + if ($display_index === null) { + $uri->removeQueryParam($parameter); + } else { + $uri->replaceQueryParam($parameter, $display_index); + } + $rendered_links[] = id(new PHUIButtonView()) ->setTag('a') - ->setHref($link) + ->setHref($uri) ->setColor(PHUIButtonView::GREY) ->addClass('mml') ->addClass($class) From adab70240390b100fbd64e95d7522bb02079a9a8 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 17 Feb 2019 17:39:34 -0800 Subject: [PATCH 075/245] Fix a PhutilURI issue in Multimeter Fished this out of the `secure` error logs. --- .../multimeter/controller/MultimeterSampleController.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/applications/multimeter/controller/MultimeterSampleController.php b/src/applications/multimeter/controller/MultimeterSampleController.php index 73c9ba530e..190a839f63 100644 --- a/src/applications/multimeter/controller/MultimeterSampleController.php +++ b/src/applications/multimeter/controller/MultimeterSampleController.php @@ -300,9 +300,10 @@ final class MultimeterSampleController extends MultimeterController { $group = implode('.', $group); if (!strlen($group)) { - $group = null; + $uri->removeQueryParam('group'); + } else { + $uri->replaceQueryParam('group', $group); } - $uri->replaceQueryParam('group', $group); if ($wipe) { foreach ($this->getColumnMap() as $key => $column) { From 8cf6c68c9520a8e65856799cfee4b27bf278f693 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 19 Feb 2019 09:01:10 -0800 Subject: [PATCH 076/245] Fix a PhutilURI issue in workboards Ref PHI1082. --- .../PhabricatorProjectBoardViewController.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index ee239035da..f2965892d7 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -1182,19 +1182,27 @@ final class PhabricatorProjectBoardViewController $project = $this->getProject(); if ($base === null) { - $base = $this->getRequest()->getRequestURI(); + $base = $this->getRequest()->getPath(); } $base = new PhutilURI($base); if ($force || ($this->sortKey != $this->getDefaultSort($project))) { - $base->replaceQueryParam('order', $this->sortKey); + if ($this->sortKey !== null) { + $base->replaceQueryParam('order', $this->sortKey); + } else { + $base->removeQueryParam('order'); + } } else { $base->removeQueryParam('order'); } if ($force || ($this->queryKey != $this->getDefaultFilter($project))) { - $base->replaceQueryParam('filter', $this->queryKey); + if ($this->queryKey !== null) { + $base->replaceQueryParam('filter', $this->queryKey); + } else { + $base->removeQueryParam('filter'); + } } else { $base->removeQueryParam('filter'); } From e44b40ca4d83e04bd05070b93f8a42a15da25cb0 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 14 Feb 2019 13:19:20 -0800 Subject: [PATCH 077/245] Make "Subscribe/Unsubscribe" require only "CAN_VIEW", not "CAN_INTERACT" Summary: Ref T13249. See PHI1059. Currently, Subscribe/Unsubscribe require CAN_INTERACT via the web UI and no permissions (i.e., effectively CAN_VIEW) via the API. Weaken the requirements from the web UI so that you do not need "CAN_INTERACT". This is a product change to the effect that it's okay to subscribe/unsubscribe from anything you can see, even hard-locked tasks. This generally seems reasonable. Increase the requirements for the actual transaction, which mostly applies to API changes: - To remove subscribers other than yourself, require CAN_EDIT. - To add subscribers other than yourself, require CAN_EDIT or CAN_INTERACT. You may have CAN_EDIT but not CAN_INTERACT on "soft locked" tasks. It's okay to click "Edit" on these, click "Yes, override lock", then remove subscribers other than yourself. This technically plugs some weird, mostly theoretical holes in the API where "attackers" could sometimes make more subscription changes than they should have been able to. Now that we send you email when you're unsubscribed this could only really be used to be mildly mischievous, but no harm in making the policy enforcement more correct. Test Plan: Against normal, soft-locked, and hard-locked tasks: subscribed, unsubscribed, added and removed subscribers, overrode locks, edited via API. Everything worked like it should and I couldn't find any combination of lock state, policy state, and edit pathway that did anything suspicious. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13249 Differential Revision: https://secure.phabricator.com/D20174 --- ...PhabricatorSubscriptionsEditController.php | 9 ---- ...habricatorSubscriptionsUIEventListener.php | 8 +--- ...habricatorApplicationTransactionEditor.php | 45 +++++++++++++++++-- 3 files changed, 44 insertions(+), 18 deletions(-) diff --git a/src/applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php b/src/applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php index 941c0c5811..747e4e98f8 100644 --- a/src/applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php +++ b/src/applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php @@ -47,15 +47,6 @@ final class PhabricatorSubscriptionsEditController $handle->getURI()); } - if (!PhabricatorPolicyFilter::canInteract($viewer, $object)) { - $lock = PhabricatorEditEngineLock::newForObject($viewer, $object); - - $dialog = $this->newDialog() - ->addCancelButton($handle->getURI()); - - return $lock->willBlockUserInteractionWithDialog($dialog); - } - if ($object instanceof PhabricatorApplicationTransactionInterface) { if ($is_add) { $xaction_value = array( diff --git a/src/applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php b/src/applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php index caf860117e..2077160b7c 100644 --- a/src/applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php +++ b/src/applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php @@ -73,24 +73,20 @@ final class PhabricatorSubscriptionsUIEventListener ->setName(pht('Automatically Subscribed')) ->setIcon('fa-check-circle lightgreytext'); } else { - $can_interact = PhabricatorPolicyFilter::canInteract($user, $object); - if ($is_subscribed) { $sub_action = id(new PhabricatorActionView()) ->setWorkflow(true) ->setRenderAsForm(true) ->setHref('/subscriptions/delete/'.$object->getPHID().'/') ->setName(pht('Unsubscribe')) - ->setIcon('fa-minus-circle') - ->setDisabled(!$can_interact); + ->setIcon('fa-minus-circle'); } else { $sub_action = id(new PhabricatorActionView()) ->setWorkflow(true) ->setRenderAsForm(true) ->setHref('/subscriptions/add/'.$object->getPHID().'/') ->setName(pht('Subscribe')) - ->setIcon('fa-plus-circle') - ->setDisabled(!$can_interact); + ->setIcon('fa-plus-circle'); } if (!$user->isLoggedIn()) { diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index bd066e633b..9460dd3030 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -1648,9 +1648,48 @@ abstract class PhabricatorApplicationTransactionEditor // don't enforce it here. return null; case PhabricatorTransactions::TYPE_SUBSCRIBERS: - // TODO: Removing subscribers other than yourself should probably - // require CAN_EDIT permission. You can do this via the API but - // generally can not via the web interface. + // Anyone can subscribe to or unsubscribe from anything they can view, + // with no other permissions. + + $old = array_fuse($xaction->getOldValue()); + $new = array_fuse($xaction->getNewValue()); + + // To remove users other than yourself, you must be able to edit the + // object. + $rem = array_diff_key($old, $new); + foreach ($rem as $phid) { + if ($phid !== $this->getActingAsPHID()) { + return PhabricatorPolicyCapability::CAN_EDIT; + } + } + + // To add users other than yourself, you must be able to interact. + // This allows "@mentioning" users to work as long as you can comment + // on objects. + + // If you can edit, we return that policy instead so that you can + // override a soft lock and still make edits. + + // TODO: This is a little bit hacky. We really want to be able to say + // "this requires either interact or edit", but there's currently no + // way to specify this kind of requirement. + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $this->getActor(), + $this->object, + PhabricatorPolicyCapability::CAN_EDIT); + + $add = array_diff_key($new, $old); + foreach ($add as $phid) { + if ($phid !== $this->getActingAsPHID()) { + if ($can_edit) { + return PhabricatorPolicyCapability::CAN_EDIT; + } else { + return PhabricatorPolicyCapability::CAN_INTERACT; + } + } + } + return null; case PhabricatorTransactions::TYPE_TOKEN: // TODO: This technically requires CAN_INTERACT, like comments. From 8d348e2eebbe9dbfaf6e8e5bb0188af619516c71 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 15 Feb 2019 13:50:24 -0800 Subject: [PATCH 078/245] Clean up a couple of %Q issues in "Has Parents" task queries Summary: Stragglers from the great "%Q" migration. Test Plan: Ran a query for tasks with parent tasks. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20183 --- src/applications/maniphest/query/ManiphestTaskQuery.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/applications/maniphest/query/ManiphestTaskQuery.php b/src/applications/maniphest/query/ManiphestTaskQuery.php index fc5097f4d3..9fb4ecb68c 100644 --- a/src/applications/maniphest/query/ManiphestTaskQuery.php +++ b/src/applications/maniphest/query/ManiphestTaskQuery.php @@ -618,9 +618,9 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery { $joins = array(); if ($this->hasOpenParents !== null) { if ($this->hasOpenParents) { - $join_type = 'JOIN'; + $join_type = qsprintf($conn, 'JOIN'); } else { - $join_type = 'LEFT JOIN'; + $join_type = qsprintf($conn, 'LEFT JOIN'); } $joins[] = qsprintf( From 92abe3c8fb84bc51b2f845e5bd1fe8da4117e1dd Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 14 Feb 2019 09:54:55 -0800 Subject: [PATCH 079/245] Extract scope line selection logic from the diff rendering engine so it can reasonably be iterated on Summary: Ref T13249. Ref T11738. See PHI985. Currently, we have a crude heuristic for guessing what line in a source file provides the best context. We get it wrong in a lot of cases, sometimes selecting very silly lines like "{". Although we can't always pick the same line a human would pick, we //can// pile on heuristics until this is less frequently completely wrong and perhaps eventually get it to work fairly well most of the time. Pull the logic for this into a separate standalone class and make it testable to prepare for adding heuristics. Test Plan: Ran unit tests, browsed various files in the web UI and saw as-good-or-better context selection. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13249, T11738 Differential Revision: https://secure.phabricator.com/D20171 --- src/__phutil_library_map__.php | 4 + .../parser/DifferentialChangesetParser.php | 49 +----- .../render/DifferentialChangesetRenderer.php | 38 ++++- .../DifferentialChangesetTwoUpRenderer.php | 46 ++++-- .../diff/PhabricatorDiffScopeEngine.php | 156 ++++++++++++++++++ .../PhabricatorDiffScopeEngineTestCase.php | 51 ++++++ .../diff/__tests__/data/zebra.c | 5 + 7 files changed, 286 insertions(+), 63 deletions(-) create mode 100644 src/infrastructure/diff/PhabricatorDiffScopeEngine.php create mode 100644 src/infrastructure/diff/__tests__/PhabricatorDiffScopeEngineTestCase.php create mode 100644 src/infrastructure/diff/__tests__/data/zebra.c diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 37be79ec59..94ba258f53 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2971,6 +2971,8 @@ phutil_register_library_map(array( 'PhabricatorDeveloperPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorDeveloperPreferencesSettingsPanel.php', 'PhabricatorDiffInlineCommentQuery' => 'infrastructure/diff/query/PhabricatorDiffInlineCommentQuery.php', 'PhabricatorDiffPreferencesSettingsPanel' => 'applications/settings/panel/PhabricatorDiffPreferencesSettingsPanel.php', + 'PhabricatorDiffScopeEngine' => 'infrastructure/diff/PhabricatorDiffScopeEngine.php', + 'PhabricatorDiffScopeEngineTestCase' => 'infrastructure/diff/__tests__/PhabricatorDiffScopeEngineTestCase.php', 'PhabricatorDifferenceEngine' => 'infrastructure/diff/PhabricatorDifferenceEngine.php', 'PhabricatorDifferentialApplication' => 'applications/differential/application/PhabricatorDifferentialApplication.php', 'PhabricatorDifferentialAttachCommitWorkflow' => 'applications/differential/management/PhabricatorDifferentialAttachCommitWorkflow.php', @@ -8850,6 +8852,8 @@ phutil_register_library_map(array( 'PhabricatorDeveloperPreferencesSettingsPanel' => 'PhabricatorEditEngineSettingsPanel', 'PhabricatorDiffInlineCommentQuery' => 'PhabricatorApplicationTransactionCommentQuery', 'PhabricatorDiffPreferencesSettingsPanel' => 'PhabricatorEditEngineSettingsPanel', + 'PhabricatorDiffScopeEngine' => 'Phobject', + 'PhabricatorDiffScopeEngineTestCase' => 'PhabricatorTestCase', 'PhabricatorDifferenceEngine' => 'Phobject', 'PhabricatorDifferentialApplication' => 'PhabricatorApplication', 'PhabricatorDifferentialAttachCommitWorkflow' => 'PhabricatorDifferentialManagementWorkflow', diff --git a/src/applications/differential/parser/DifferentialChangesetParser.php b/src/applications/differential/parser/DifferentialChangesetParser.php index e214aa16a4..27d2a2d845 100644 --- a/src/applications/differential/parser/DifferentialChangesetParser.php +++ b/src/applications/differential/parser/DifferentialChangesetParser.php @@ -1173,7 +1173,7 @@ final class DifferentialChangesetParser extends Phobject { } $range_len = min($range_len, $rows - $range_start); - list($gaps, $mask, $depths) = $this->calculateGapsMaskAndDepths( + list($gaps, $mask) = $this->calculateGapsAndMask( $mask_force, $feedback_mask, $range_start, @@ -1181,8 +1181,7 @@ final class DifferentialChangesetParser extends Phobject { $renderer ->setGaps($gaps) - ->setMask($mask) - ->setDepths($depths); + ->setMask($mask); $html = $renderer->renderTextChange( $range_start, @@ -1208,15 +1207,9 @@ final class DifferentialChangesetParser extends Phobject { * "show more"). The $mask returned is a sparsely populated dictionary * of $visible_line_number => true. * - * Depths - compute how indented any given line is. The $depths returned - * is a sparsely populated dictionary of $visible_line_number => $depth. - * - * This function also has the side effect of modifying member variable - * new such that tabs are normalized to spaces for each line of the diff. - * - * @return array($gaps, $mask, $depths) + * @return array($gaps, $mask) */ - private function calculateGapsMaskAndDepths( + private function calculateGapsAndMask( $mask_force, $feedback_mask, $range_start, @@ -1224,7 +1217,6 @@ final class DifferentialChangesetParser extends Phobject { $lines_context = $this->getLinesOfContext(); - // Calculate gaps and mask first $gaps = array(); $gap_start = 0; $in_gap = false; @@ -1253,38 +1245,7 @@ final class DifferentialChangesetParser extends Phobject { $gaps = array_reverse($gaps); $mask = $base_mask; - // Time to calculate depth. - // We need to go backwards to properly indent whitespace in this code: - // - // 0: class C { - // 1: - // 1: function f() { - // 2: - // 2: return; - // 1: - // 1: } - // 0: - // 0: } - // - $depths = array(); - $last_depth = 0; - $range_end = $range_start + $range_len; - if (!isset($this->new[$range_end])) { - $range_end--; - } - for ($ii = $range_end; $ii >= $range_start; $ii--) { - // We need to expand tabs to process mixed indenting and to round - // correctly later. - $line = str_replace("\t", ' ', $this->new[$ii]['text']); - $trimmed = ltrim($line); - if ($trimmed != '') { - // We round down to flatten "/**" and " *". - $last_depth = floor((strlen($line) - strlen($trimmed)) / 2); - } - $depths[$ii] = $last_depth; - } - - return array($gaps, $mask, $depths); + return array($gaps, $mask); } /** diff --git a/src/applications/differential/render/DifferentialChangesetRenderer.php b/src/applications/differential/render/DifferentialChangesetRenderer.php index 450d160e23..f295695286 100644 --- a/src/applications/differential/render/DifferentialChangesetRenderer.php +++ b/src/applications/differential/render/DifferentialChangesetRenderer.php @@ -28,12 +28,12 @@ abstract class DifferentialChangesetRenderer extends Phobject { private $originalNew; private $gaps; private $mask; - private $depths; private $originalCharacterEncoding; private $showEditAndReplyLinks; private $canMarkDone; private $objectOwnerPHID; private $highlightingDisabled; + private $scopeEngine; private $oldFile = false; private $newFile = false; @@ -76,14 +76,6 @@ abstract class DifferentialChangesetRenderer extends Phobject { return $this->isUndershield; } - public function setDepths($depths) { - $this->depths = $depths; - return $this; - } - protected function getDepths() { - return $this->depths; - } - public function setMask($mask) { $this->mask = $mask; return $this; @@ -678,4 +670,32 @@ abstract class DifferentialChangesetRenderer extends Phobject { return $views; } + + final protected function getScopeEngine() { + if (!$this->scopeEngine) { + $line_map = $this->getNewLineTextMap(); + + $scope_engine = id(new PhabricatorDiffScopeEngine()) + ->setLineTextMap($line_map); + + $this->scopeEngine = $scope_engine; + } + + return $this->scopeEngine; + } + + private function getNewLineTextMap() { + $new = $this->getNewLines(); + + $text_map = array(); + foreach ($new as $new_line) { + if (!isset($new_line['line'])) { + continue; + } + $text_map[$new_line['line']] = $new_line['text']; + } + + return $text_map; + } + } diff --git a/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php b/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php index 5d476f5136..f40a7f5e0b 100644 --- a/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php +++ b/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php @@ -3,6 +3,8 @@ final class DifferentialChangesetTwoUpRenderer extends DifferentialChangesetHTMLRenderer { + private $newOffsetMap; + public function isOneUpRenderer() { return false; } @@ -66,9 +68,12 @@ final class DifferentialChangesetTwoUpRenderer $new_render = $this->getNewRender(); $original_left = $this->getOriginalOld(); $original_right = $this->getOriginalNew(); - $depths = $this->getDepths(); $mask = $this->getMask(); + $scope_engine = $this->getScopeEngine(); + + $offset_map = null; + for ($ii = $range_start; $ii < $range_start + $range_len; $ii++) { if (empty($mask[$ii])) { // If we aren't going to show this line, we've just entered a gap. @@ -87,16 +92,19 @@ final class DifferentialChangesetTwoUpRenderer $is_last_block = true; } - $context = null; + $context_text = null; $context_line = null; - if (!$is_last_block && $depths[$ii + $len]) { - for ($l = $ii + $len - 1; $l >= $ii; $l--) { - $line = $new_lines[$l]['text']; - if ($depths[$l] < $depths[$ii + $len] && trim($line) != '') { - $context = $new_render[$l]; - $context_line = $new_lines[$l]['line']; - break; + if (!$is_last_block) { + $target_line = $new_lines[$ii + $len]['line']; + $context_line = $scope_engine->getScopeStart($target_line); + if ($context_line !== null) { + // The scope engine returns a line number in the file. We need + // to map that back to a display offset in the diff. + if (!$offset_map) { + $offset_map = $this->getNewLineToOffsetMap(); } + $offset = $offset_map[$context_line]; + $context_text = $new_render[$offset]; } } @@ -126,7 +134,7 @@ final class DifferentialChangesetTwoUpRenderer 'class' => 'show-context', ), // TODO: [HTML] Escaping model here isn't ideal. - phutil_safe_html($context)), + phutil_safe_html($context_text)), )); $html[] = $container; @@ -386,4 +394,22 @@ final class DifferentialChangesetTwoUpRenderer ->addInlineView($view); } + private function getNewLineToOffsetMap() { + if ($this->newOffsetMap === null) { + $new = $this->getNewLines(); + + $map = array(); + foreach ($new as $offset => $new_line) { + if ($new_line['line'] === null) { + continue; + } + $map[$new_line['line']] = $offset; + } + + $this->newOffsetMap = $map; + } + + return $this->newOffsetMap; + } + } diff --git a/src/infrastructure/diff/PhabricatorDiffScopeEngine.php b/src/infrastructure/diff/PhabricatorDiffScopeEngine.php new file mode 100644 index 0000000000..5ea1ec5021 --- /dev/null +++ b/src/infrastructure/diff/PhabricatorDiffScopeEngine.php @@ -0,0 +1,156 @@ + $value) { + if ($key === $expect) { + $expect++; + continue; + } + + throw new Exception( + pht( + 'ScopeEngine text map must be a contiguous map of '. + 'lines, but is not: found key "%s" where key "%s" was expected.', + $key, + $expect)); + } + + $this->lineTextMap = $map; + + return $this; + } + + public function getLineTextMap() { + if ($this->lineTextMap === null) { + throw new PhutilInvalidStateException('setLineTextMap'); + } + return $this->lineTextMap; + } + + public function getScopeStart($line) { + $text_map = $this->getLineTextMap(); + $depth_map = $this->getLineDepthMap(); + $length = count($text_map); + + // Figure out the effective depth of the line we're getting scope for. + // If the line is just whitespace, it may have no depth on its own. In + // this case, we look for the next line. + $line_depth = null; + for ($ii = $line; $ii <= $length; $ii++) { + if ($depth_map[$ii] !== null) { + $line_depth = $depth_map[$ii]; + break; + } + } + + // If we can't find a line depth for the target line, just bail. + if ($line_depth === null) { + return null; + } + + // Limit the maximum number of lines we'll examine. If a user has a + // million-line diff of nonsense, scanning the whole thing is a waste + // of time. + $search_range = 1000; + $search_until = max(0, $ii - $search_range); + + for ($ii = $line - 1; $ii > $search_until; $ii--) { + $line_text = $text_map[$ii]; + + // This line is in missing context: the diff was diffed with partial + // context, and we ran out of context before finding a good scope line. + // Bail out, we don't want to jump across missing context blocks. + if ($line_text === null) { + return null; + } + + $depth = $depth_map[$ii]; + + // This line is all whitespace. This isn't a possible match. + if ($depth === null) { + continue; + } + + // The depth is the same as (or greater than) the depth we started with, + // so this isn't a possible match. + if ($depth >= $line_depth) { + continue; + } + + // Reject lines which begin with "}" or "{". These lines are probably + // never good matches. + if (preg_match('/^\s*[{}]/i', $line_text)) { + continue; + } + + return $ii; + } + + return null; + } + + private function getLineDepthMap() { + if (!$this->lineDepthMap) { + $this->lineDepthMap = $this->newLineDepthMap(); + } + + return $this->lineDepthMap; + } + + private function newLineDepthMap() { + $text_map = $this->getLineTextMap(); + + // TODO: This should be configurable once we handle tab widths better. + $tab_width = 2; + + $depth_map = array(); + foreach ($text_map as $line_number => $line_text) { + if ($line_text === null) { + $depth_map[$line_number] = null; + continue; + } + + $len = strlen($line_text); + + // If the line has no actual text, don't assign it a depth. + if (!$len || !strlen(trim($line_text))) { + $depth_map[$line_number] = null; + continue; + } + + $count = 0; + for ($ii = 0; $ii < $len; $ii++) { + $c = $line_text[$ii]; + if ($c == ' ') { + $count++; + } else if ($c == "\t") { + $count += $tab_width; + } else { + break; + } + } + + // Round down to cheat our way through the " *" parts of docblock + // comments. This is generally a reasonble heuristic because odd tab + // widths are exceptionally rare. + $depth = ($count >> 1); + + $depth_map[$line_number] = $depth; + } + + return $depth_map; + } + +} diff --git a/src/infrastructure/diff/__tests__/PhabricatorDiffScopeEngineTestCase.php b/src/infrastructure/diff/__tests__/PhabricatorDiffScopeEngineTestCase.php new file mode 100644 index 0000000000..50e23ac31c --- /dev/null +++ b/src/infrastructure/diff/__tests__/PhabricatorDiffScopeEngineTestCase.php @@ -0,0 +1,51 @@ +assertScopeStart('zebra.c', 4, 2); + } + + private function assertScopeStart($file, $line, $expect) { + $engine = $this->getScopeTestEngine($file); + + $actual = $engine->getScopeStart($line); + $this->assertEqual( + $expect, + $actual, + pht( + 'Expect scope for line %s to start on line %s (actual: %s) in "%s".', + $line, + $expect, + $actual, + $file)); + } + + private function getScopeTestEngine($file) { + if (!isset($this->engines[$file])) { + $this->engines[$file] = $this->newScopeTestEngine($file); + } + + return $this->engines[$file]; + } + + private function newScopeTestEngine($file) { + $path = dirname(__FILE__).'/data/'.$file; + $data = Filesystem::readFile($path); + + $lines = phutil_split_lines($data); + $map = array(); + foreach ($lines as $key => $line) { + $map[$key + 1] = $line; + } + + $engine = id(new PhabricatorDiffScopeEngine()) + ->setLineTextMap($map); + + return $engine; + } + +} diff --git a/src/infrastructure/diff/__tests__/data/zebra.c b/src/infrastructure/diff/__tests__/data/zebra.c new file mode 100644 index 0000000000..d587b018a9 --- /dev/null +++ b/src/infrastructure/diff/__tests__/data/zebra.c @@ -0,0 +1,5 @@ +void +ZebraTamer::TameAZebra(nsPoint where, const nsRect& zone, nsAtom* material) +{ + zebra.tame = true; +} From aa470d21549c7928127470e2136adf90f0427ec7 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 14 Feb 2019 11:37:42 -0800 Subject: [PATCH 080/245] Show user availability dots (red = away, orange = busy) in typeaheads, tokenizer tokens, and autocompletes Summary: Ref T13249. See PHI810. We currently show availability dots in some interfaces (timeline, mentions) but not others (typeheads/tokenizers). They're potentially quite useful in tokenizers, e.g. when assigning tasks to someone or requesting reviews. Show them in more places. (The actual rendering here isn't terribly clean, and it would be great to try to unify all these various behaviors some day.) Test Plan: {F6212044} {F6212045} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13249 Differential Revision: https://secure.phabricator.com/D20173 --- resources/celerity/map.php | 52 +++++++++--------- .../typeahead/PhabricatorPeopleDatasource.php | 11 +++- .../storage/PhabricatorTypeaheadResult.php | 11 ++++ .../view/PhabricatorTypeaheadTokenView.php | 55 ++++++++++++++++--- .../control/AphrontFormTokenizerControl.php | 4 ++ webroot/rsrc/css/phui/phui-tag-view.css | 8 +++ webroot/rsrc/js/core/Prefab.js | 29 +++++++++- webroot/rsrc/js/phuix/PHUIXAutocomplete.js | 11 +++- 8 files changed, 141 insertions(+), 40 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 19f8924555..26aec85659 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,8 +9,8 @@ return array( 'names' => array( 'conpherence.pkg.css' => '3c8a0668', 'conpherence.pkg.js' => '020aebcf', - 'core.pkg.css' => 'f2319e1f', - 'core.pkg.js' => '5c737607', + 'core.pkg.css' => '261ee8cf', + 'core.pkg.js' => '5ace8a1e', 'differential.pkg.css' => 'b8df73d4', 'differential.pkg.js' => '67c9ea4c', 'diffusion.pkg.css' => '42c75c37', @@ -172,7 +172,7 @@ return array( 'rsrc/css/phui/phui-segment-bar-view.css' => '5166b370', 'rsrc/css/phui/phui-spacing.css' => 'b05cadc3', 'rsrc/css/phui/phui-status.css' => 'e5ff8be0', - 'rsrc/css/phui/phui-tag-view.css' => 'a42fe34f', + 'rsrc/css/phui/phui-tag-view.css' => '29409667', 'rsrc/css/phui/phui-timeline-view.css' => '1e348e4b', 'rsrc/css/phui/phui-two-column-view.css' => '01e6991e', 'rsrc/css/phui/workboards/phui-workboard-color.css' => 'e86de308', @@ -441,7 +441,7 @@ return array( 'rsrc/js/core/KeyboardShortcutManager.js' => '37b8a04a', 'rsrc/js/core/MultirowRowManager.js' => '5b54c823', 'rsrc/js/core/Notification.js' => 'a9b91e3f', - 'rsrc/js/core/Prefab.js' => 'bf457520', + 'rsrc/js/core/Prefab.js' => '5793d835', 'rsrc/js/core/ShapedRequest.js' => 'abf88db8', 'rsrc/js/core/TextAreaUtils.js' => 'f340a484', 'rsrc/js/core/Title.js' => '43bc9360', @@ -505,7 +505,7 @@ return array( 'rsrc/js/phui/behavior-phui-timer-control.js' => 'f84bcbf4', 'rsrc/js/phuix/PHUIXActionListView.js' => 'c68f183f', 'rsrc/js/phuix/PHUIXActionView.js' => 'aaa08f3b', - 'rsrc/js/phuix/PHUIXAutocomplete.js' => '58cc4ab8', + 'rsrc/js/phuix/PHUIXAutocomplete.js' => '8f139ef0', 'rsrc/js/phuix/PHUIXButtonView.js' => '55a24e84', 'rsrc/js/phuix/PHUIXDropdownMenu.js' => 'bdce4d78', 'rsrc/js/phuix/PHUIXExample.js' => 'c2c500a7', @@ -771,7 +771,7 @@ return array( 'phabricator-notification-menu-css' => 'e6962e89', 'phabricator-object-selector-css' => 'ee77366f', 'phabricator-phtize' => '2f1db1ed', - 'phabricator-prefab' => 'bf457520', + 'phabricator-prefab' => '5793d835', 'phabricator-remarkup-css' => '9e627d41', 'phabricator-search-results-css' => '9ea70ace', 'phabricator-shaped-request' => 'abf88db8', @@ -847,7 +847,7 @@ return array( 'phui-segment-bar-view-css' => '5166b370', 'phui-spacing-css' => 'b05cadc3', 'phui-status-list-view-css' => 'e5ff8be0', - 'phui-tag-view-css' => 'a42fe34f', + 'phui-tag-view-css' => '29409667', 'phui-theme-css' => '35883b37', 'phui-timeline-view-css' => '1e348e4b', 'phui-two-column-view-css' => '01e6991e', @@ -857,7 +857,7 @@ return array( 'phui-workpanel-view-css' => 'bd546a49', 'phuix-action-list-view' => 'c68f183f', 'phuix-action-view' => 'aaa08f3b', - 'phuix-autocomplete' => '58cc4ab8', + 'phuix-autocomplete' => '8f139ef0', 'phuix-button-view' => '55a24e84', 'phuix-dropdown-menu' => 'bdce4d78', 'phuix-form-control-view' => '38c1f3fb', @@ -1354,6 +1354,18 @@ return array( 'javelin-stratcom', 'javelin-dom', ), + '5793d835' => array( + 'javelin-install', + 'javelin-util', + 'javelin-dom', + 'javelin-typeahead', + 'javelin-tokenizer', + 'javelin-typeahead-preloaded-source', + 'javelin-typeahead-ondemand-source', + 'javelin-dom', + 'javelin-stratcom', + 'javelin-util', + ), '5803b9e7' => array( 'javelin-behavior', 'javelin-util', @@ -1362,12 +1374,6 @@ return array( 'javelin-vector', 'javelin-typeahead-static-source', ), - '58cc4ab8' => array( - 'javelin-install', - 'javelin-dom', - 'phuix-icon-view', - 'phabricator-prefab', - ), '5902260c' => array( 'javelin-util', 'javelin-magical-init', @@ -1608,6 +1614,12 @@ return array( '8e2d9a28' => array( 'phui-theme-css', ), + '8f139ef0' => array( + 'javelin-install', + 'javelin-dom', + 'phuix-icon-view', + 'phabricator-prefab', + ), '8f959ad0' => array( 'javelin-behavior', 'javelin-dom', @@ -1895,18 +1907,6 @@ return array( 'javelin-vector', 'javelin-stratcom', ), - 'bf457520' => array( - 'javelin-install', - 'javelin-util', - 'javelin-dom', - 'javelin-typeahead', - 'javelin-tokenizer', - 'javelin-typeahead-preloaded-source', - 'javelin-typeahead-ondemand-source', - 'javelin-dom', - 'javelin-stratcom', - 'javelin-util', - ), 'c03f2fb4' => array( 'javelin-install', ), diff --git a/src/applications/people/typeahead/PhabricatorPeopleDatasource.php b/src/applications/people/typeahead/PhabricatorPeopleDatasource.php index df146808bb..d4a5ad96c7 100644 --- a/src/applications/people/typeahead/PhabricatorPeopleDatasource.php +++ b/src/applications/people/typeahead/PhabricatorPeopleDatasource.php @@ -19,7 +19,8 @@ final class PhabricatorPeopleDatasource $viewer = $this->getViewer(); $query = id(new PhabricatorPeopleQuery()) - ->setOrderVector(array('username')); + ->setOrderVector(array('username')) + ->needAvailability(true); if ($this->getPhase() == self::PHASE_PREFIX) { $prefix = $this->getPrefixQuery(); @@ -96,6 +97,14 @@ final class PhabricatorPeopleDatasource $result->setDisplayType($display_type); } + $until = $user->getAwayUntil(); + if ($until) { + $availability = $user->getDisplayAvailability(); + $color = PhabricatorCalendarEventInvitee::getAvailabilityColor( + $availability); + $result->setAvailabilityColor($color); + } + $results[] = $result; } diff --git a/src/applications/typeahead/storage/PhabricatorTypeaheadResult.php b/src/applications/typeahead/storage/PhabricatorTypeaheadResult.php index 14cbe726dc..b13cf351b1 100644 --- a/src/applications/typeahead/storage/PhabricatorTypeaheadResult.php +++ b/src/applications/typeahead/storage/PhabricatorTypeaheadResult.php @@ -19,6 +19,7 @@ final class PhabricatorTypeaheadResult extends Phobject { private $autocomplete; private $attributes = array(); private $phase; + private $availabilityColor; public function setIcon($icon) { $this->icon = $icon; @@ -156,6 +157,7 @@ final class PhabricatorTypeaheadResult extends Phobject { $this->unique ? 1 : null, $this->autocomplete, $this->phase, + $this->availabilityColor, ); while (end($data) === null) { array_pop($data); @@ -222,4 +224,13 @@ final class PhabricatorTypeaheadResult extends Phobject { return $this->phase; } + public function setAvailabilityColor($availability_color) { + $this->availabilityColor = $availability_color; + return $this; + } + + public function getAvailabilityColor() { + return $this->availabilityColor; + } + } diff --git a/src/applications/typeahead/view/PhabricatorTypeaheadTokenView.php b/src/applications/typeahead/view/PhabricatorTypeaheadTokenView.php index 56867d8278..e0a5270e84 100644 --- a/src/applications/typeahead/view/PhabricatorTypeaheadTokenView.php +++ b/src/applications/typeahead/view/PhabricatorTypeaheadTokenView.php @@ -14,6 +14,7 @@ final class PhabricatorTypeaheadTokenView private $inputName; private $value; private $tokenType = self::TYPE_OBJECT; + private $availabilityColor; public static function newFromTypeaheadResult( PhabricatorTypeaheadResult $result) { @@ -41,6 +42,21 @@ final class PhabricatorTypeaheadTokenView $token->setColor($handle->getTagColor()); } + $availability = $handle->getAvailability(); + $color = null; + switch ($availability) { + case PhabricatorObjectHandle::AVAILABILITY_PARTIAL: + $color = PHUITagView::COLOR_ORANGE; + break; + case PhabricatorObjectHandle::AVAILABILITY_NONE: + $color = PHUITagView::COLOR_RED; + break; + } + + if ($color !== null) { + $token->setAvailabilityColor($color); + } + return $token; } @@ -106,6 +122,15 @@ final class PhabricatorTypeaheadTokenView return 'a'; } + public function setAvailabilityColor($availability_color) { + $this->availabilityColor = $availability_color; + return $this; + } + + public function getAvailabilityColor() { + return $this->availabilityColor; + } + protected function getTagAttributes() { $classes = array(); $classes[] = 'jx-tokenizer-token'; @@ -139,20 +164,32 @@ final class PhabricatorTypeaheadTokenView $value = $this->getValue(); + $availability = null; + $availability_color = $this->getAvailabilityColor(); + if ($availability_color) { + $availability = phutil_tag( + 'span', + array( + 'class' => 'phui-tag-dot phui-tag-color-'.$availability_color, + )); + } + + $icon_view = null; $icon = $this->getIcon(); if ($icon) { - $value = array( - phutil_tag( - 'span', - array( - 'class' => 'phui-icon-view phui-font-fa '.$icon, - )), - $value, - ); + $icon_view = phutil_tag( + 'span', + array( + 'class' => 'phui-icon-view phui-font-fa '.$icon, + )); } return array( - $value, + array( + $icon_view, + $availability, + $value, + ), phutil_tag( 'input', array( diff --git a/src/view/form/control/AphrontFormTokenizerControl.php b/src/view/form/control/AphrontFormTokenizerControl.php index 3d65c4e525..fe80c86f81 100644 --- a/src/view/form/control/AphrontFormTokenizerControl.php +++ b/src/view/form/control/AphrontFormTokenizerControl.php @@ -108,6 +108,10 @@ final class AphrontFormTokenizerControl extends AphrontFormControl { 'icons' => mpull($tokens, 'getIcon', 'getKey'), 'types' => mpull($tokens, 'getTokenType', 'getKey'), 'colors' => mpull($tokens, 'getColor', 'getKey'), + 'availabilityColors' => mpull( + $tokens, + 'getAvailabilityColor', + 'getKey'), 'limit' => $this->limit, 'username' => $username, 'placeholder' => $placeholder, diff --git a/webroot/rsrc/css/phui/phui-tag-view.css b/webroot/rsrc/css/phui/phui-tag-view.css index 73675a44d6..57529645a7 100644 --- a/webroot/rsrc/css/phui/phui-tag-view.css +++ b/webroot/rsrc/css/phui/phui-tag-view.css @@ -54,6 +54,14 @@ a.phui-tag-view:hover { border: 1px solid transparent; } +.tokenizer-result .phui-tag-dot { + margin-right: 6px; +} + +.jx-tokenizer-token .phui-tag-dot { + margin-left: 2px; +} + .phui-tag-type-state { color: #ffffff; text-shadow: rgba(100, 100, 100, 0.40) 0px -1px 1px; diff --git a/webroot/rsrc/js/core/Prefab.js b/webroot/rsrc/js/core/Prefab.js index 979ad3473b..ff4467881b 100644 --- a/webroot/rsrc/js/core/Prefab.js +++ b/webroot/rsrc/js/core/Prefab.js @@ -125,15 +125,18 @@ JX.install('Prefab', { var icon; var type; var color; + var availability_color; if (result) { icon = result.icon; value = result.displayName; type = result.tokenType; color = result.color; + availability_color = result.availabilityColor; } else { icon = (config.icons || {})[key]; type = (config.types || {})[key]; color = (config.colors || {})[key]; + availability_color = (config.availabilityColors || {})[key]; } if (icon) { @@ -147,7 +150,16 @@ JX.install('Prefab', { JX.DOM.alterClass(container, color, true); } - return [icon, value]; + var dot; + if (availability_color) { + dot = JX.$N( + 'span', + { + className: 'phui-tag-dot phui-tag-color-' + availability_color + }); + } + + return [icon, dot, value]; }); if (config.placeholder) { @@ -275,10 +287,20 @@ JX.install('Prefab', { icon_ui = JX.Prefab._renderIcon(icon); } + var availability_ui; + var availability_color = fields[16]; + if (availability_color) { + availability_ui = JX.$N( + 'span', + { + className: 'phui-tag-dot phui-tag-color-' + availability_color + }); + } + var display = JX.$N( 'div', {className: 'tokenizer-result'}, - [icon_ui, fields[4] || fields[0], closed_ui]); + [icon_ui, availability_ui, fields[4] || fields[0], closed_ui]); if (closed) { JX.DOM.alterClass(display, 'tokenizer-result-closed', true); } @@ -300,7 +322,8 @@ JX.install('Prefab', { tokenType: fields[12], unique: fields[13] || false, autocomplete: fields[14], - sort: JX.TypeaheadNormalizer.normalize(fields[0]) + sort: JX.TypeaheadNormalizer.normalize(fields[0]), + availabilityColor: availability_color }; }, diff --git a/webroot/rsrc/js/phuix/PHUIXAutocomplete.js b/webroot/rsrc/js/phuix/PHUIXAutocomplete.js index f46e7666e2..deb9f9d100 100644 --- a/webroot/rsrc/js/phuix/PHUIXAutocomplete.js +++ b/webroot/rsrc/js/phuix/PHUIXAutocomplete.js @@ -185,7 +185,16 @@ JX.install('PHUIXAutocomplete', { .getNode(); } - var display = JX.$N('span', {}, [icon, map.displayName]); + var dot; + if (map.availabilityColor) { + dot = JX.$N( + 'span', + { + className: 'phui-tag-dot phui-tag-color-' + map.availabilityColor + }); + } + + var display = JX.$N('span', {}, [icon, dot, map.displayName]); JX.DOM.alterClass(display, 'tokenizer-result-closed', !!map.closed); map.display = display; From 312ba307148552501dcc6dd8e7f82d61310a8cf9 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 15 Feb 2019 05:24:17 -0800 Subject: [PATCH 081/245] Don't report search indexing errors to the daemon log except from "bin/search index" Summary: Depends on D20177. Fixes T12425. See . Search indexing currently reports failures to load objects to the log. This log is noisy, not concerning, not actionable, and not locally debuggable (it depends on the reporting user's entire state). I think one common, fully legitimate case of this is indexing temporary files: they may fully legitimately be deleted by the time the indexer runs. Instead of sending these errors to the log, eat them. If users don't notice the indexes aren't working: no harm, no foul. If users do notice, we'll run or have them run `bin/search index` as a first diagnostic step anyway, which will now report an actionable/reproducible error. Test Plan: - Faked errors in both cases (initial load, re-load inside the locked section). - Ran indexes in strict/non-strict mode. - Got exception reports from both branches in strict mode. - Got task success without errors in both cases in non-strict mode. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T12425 Differential Revision: https://secure.phabricator.com/D20178 --- ...abricatorSearchManagementIndexWorkflow.php | 9 ++- .../search/worker/PhabricatorSearchWorker.php | 55 +++++++++++++++---- 2 files changed, 51 insertions(+), 13 deletions(-) diff --git a/src/applications/search/management/PhabricatorSearchManagementIndexWorkflow.php b/src/applications/search/management/PhabricatorSearchManagementIndexWorkflow.php index 6b6d25cb6c..99ee3a3123 100644 --- a/src/applications/search/management/PhabricatorSearchManagementIndexWorkflow.php +++ b/src/applications/search/management/PhabricatorSearchManagementIndexWorkflow.php @@ -116,6 +116,10 @@ final class PhabricatorSearchManagementIndexWorkflow // them a hint that they might want to use "--force". $track_skips = (!$is_background && !$is_force); + // Activate "strict" error reporting if we're running in the foreground + // so we'll report a wider range of conditions as errors. + $is_strict = !$is_background; + $count_updated = 0; $count_skipped = 0; @@ -125,7 +129,10 @@ final class PhabricatorSearchManagementIndexWorkflow $old_versions = $this->loadIndexVersions($phid); } - PhabricatorSearchWorker::queueDocumentForIndexing($phid, $parameters); + PhabricatorSearchWorker::queueDocumentForIndexing( + $phid, + $parameters, + $is_strict); if ($track_skips) { $new_versions = $this->loadIndexVersions($phid); diff --git a/src/applications/search/worker/PhabricatorSearchWorker.php b/src/applications/search/worker/PhabricatorSearchWorker.php index 361d9c9b6f..31c68d45c8 100644 --- a/src/applications/search/worker/PhabricatorSearchWorker.php +++ b/src/applications/search/worker/PhabricatorSearchWorker.php @@ -2,7 +2,11 @@ final class PhabricatorSearchWorker extends PhabricatorWorker { - public static function queueDocumentForIndexing($phid, $parameters = null) { + public static function queueDocumentForIndexing( + $phid, + $parameters = null, + $is_strict = false) { + if ($parameters === null) { $parameters = array(); } @@ -12,6 +16,7 @@ final class PhabricatorSearchWorker extends PhabricatorWorker { array( 'documentPHID' => $phid, 'parameters' => $parameters, + 'strict' => $is_strict, ), array( 'priority' => parent::PRIORITY_INDEX, @@ -23,7 +28,25 @@ final class PhabricatorSearchWorker extends PhabricatorWorker { $data = $this->getTaskData(); $object_phid = idx($data, 'documentPHID'); - $object = $this->loadObjectForIndexing($object_phid); + // See T12425. By the time we run an indexing task, the object it indexes + // may have been deleted. This is unusual, but not concerning, and failing + // to index these objects is correct. + + // To avoid showing these non-actionable errors to users, don't report + // indexing exceptions unless we're in "strict" mode. This mode is set by + // the "bin/search index" tool. + + $is_strict = idx($data, 'strict', false); + + try { + $object = $this->loadObjectForIndexing($object_phid); + } catch (PhabricatorWorkerPermanentFailureException $ex) { + if ($is_strict) { + throw $ex; + } else { + return; + } + } $engine = id(new PhabricatorIndexEngine()) ->setObject($object); @@ -35,8 +58,11 @@ final class PhabricatorSearchWorker extends PhabricatorWorker { return; } - $key = "index.{$object_phid}"; - $lock = PhabricatorGlobalLock::newLock($key); + $lock = PhabricatorGlobalLock::newLock( + 'index', + array( + 'objectPHID' => $object_phid, + )); try { $lock->lock(1); @@ -48,29 +74,34 @@ final class PhabricatorSearchWorker extends PhabricatorWorker { throw new PhabricatorWorkerYieldException(15); } + $caught = null; try { // Reload the object now that we have a lock, to make sure we have the // most current version. $object = $this->loadObjectForIndexing($object->getPHID()); $engine->setObject($object); - $engine->indexObject(); } catch (Exception $ex) { - $lock->unlock(); + $caught = $ex; + } - if (!($ex instanceof PhabricatorWorkerPermanentFailureException)) { - $ex = new PhabricatorWorkerPermanentFailureException( + // Release the lock before we deal with the exception. + $lock->unlock(); + + if ($caught) { + if (!($caught instanceof PhabricatorWorkerPermanentFailureException)) { + $caught = new PhabricatorWorkerPermanentFailureException( pht( 'Failed to update search index for document "%s": %s', $object_phid, - $ex->getMessage())); + $caught->getMessage())); } - throw $ex; + if ($is_strict) { + throw $caught; + } } - - $lock->unlock(); } private function loadObjectForIndexing($phid) { From deea2f01f5d285aa0692803b285ff7f46706f0e2 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 15 Feb 2019 06:24:49 -0800 Subject: [PATCH 082/245] Allow unit tests to have arbitrarily long names (>255 characters) Summary: Depends on D20179. Ref T13088. See PHI351. See PHI1018. In various cases, unit tests names are 19 paths mashed together. This is probably not an ideal name, and the test harness should probably pick a better name, but if users are fine with it and don't want to do the work to summarize on their own, accept them. We may summarize with "..." in some cases depending on how this fares in the UI. The actual implementation is a separate "strings" table which is just ``. The unit message table can end up being mostly strings, so this should reduce storage requirements a bit. For now, I'm not forcing a migration: new writes use the new table, existing rows retain the data. I plan to provide a migration tool, recommend migration, then force migration eventually. Prior to that, I'm likely to move at least some other columns to use this table (e.g., lint names), since we have a lot of similar data (arbitrarily long user string constants that we are unlikely to need to search or filter). Test Plan: {F6213819} Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13088 Differential Revision: https://secure.phabricator.com/D20180 --- .../20190215.harbor.01.stringindex.sql | 6 +++ .../20190215.harbor.02.stringcol.sql | 2 + src/__phutil_library_map__.php | 2 + .../HarbormasterBuildUnitMessageQuery.php | 31 +++++++++++ .../storage/HarbormasterString.php | 54 +++++++++++++++++++ .../build/HarbormasterBuildUnitMessage.php | 29 ++++++++++ 6 files changed, 124 insertions(+) create mode 100644 resources/sql/autopatches/20190215.harbor.01.stringindex.sql create mode 100644 resources/sql/autopatches/20190215.harbor.02.stringcol.sql create mode 100644 src/applications/harbormaster/storage/HarbormasterString.php diff --git a/resources/sql/autopatches/20190215.harbor.01.stringindex.sql b/resources/sql/autopatches/20190215.harbor.01.stringindex.sql new file mode 100644 index 0000000000..e94b240bab --- /dev/null +++ b/resources/sql/autopatches/20190215.harbor.01.stringindex.sql @@ -0,0 +1,6 @@ +CREATE TABLE {$NAMESPACE}_harbormaster.harbormaster_string ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + stringIndex BINARY(12) NOT NULL, + stringValue LONGTEXT NOT NULL, + UNIQUE KEY `key_string` (stringIndex) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20190215.harbor.02.stringcol.sql b/resources/sql/autopatches/20190215.harbor.02.stringcol.sql new file mode 100644 index 0000000000..acfdb0f869 --- /dev/null +++ b/resources/sql/autopatches/20190215.harbor.02.stringcol.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_buildunitmessage + ADD nameIndex BINARY(12) NOT NULL; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 94ba258f53..ea3bf7d32d 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1441,6 +1441,7 @@ phutil_register_library_map(array( 'HarbormasterStepDeleteController' => 'applications/harbormaster/controller/HarbormasterStepDeleteController.php', 'HarbormasterStepEditController' => 'applications/harbormaster/controller/HarbormasterStepEditController.php', 'HarbormasterStepViewController' => 'applications/harbormaster/controller/HarbormasterStepViewController.php', + 'HarbormasterString' => 'applications/harbormaster/storage/HarbormasterString.php', 'HarbormasterTargetEngine' => 'applications/harbormaster/engine/HarbormasterTargetEngine.php', 'HarbormasterTargetSearchAPIMethod' => 'applications/harbormaster/conduit/HarbormasterTargetSearchAPIMethod.php', 'HarbormasterTargetWorker' => 'applications/harbormaster/worker/HarbormasterTargetWorker.php', @@ -7070,6 +7071,7 @@ phutil_register_library_map(array( 'HarbormasterStepDeleteController' => 'HarbormasterPlanController', 'HarbormasterStepEditController' => 'HarbormasterPlanController', 'HarbormasterStepViewController' => 'HarbormasterPlanController', + 'HarbormasterString' => 'HarbormasterDAO', 'HarbormasterTargetEngine' => 'Phobject', 'HarbormasterTargetSearchAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'HarbormasterTargetWorker' => 'HarbormasterWorker', diff --git a/src/applications/harbormaster/query/HarbormasterBuildUnitMessageQuery.php b/src/applications/harbormaster/query/HarbormasterBuildUnitMessageQuery.php index fbd33a5d95..f73016a29f 100644 --- a/src/applications/harbormaster/query/HarbormasterBuildUnitMessageQuery.php +++ b/src/applications/harbormaster/query/HarbormasterBuildUnitMessageQuery.php @@ -57,6 +57,37 @@ final class HarbormasterBuildUnitMessageQuery return $where; } + protected function didFilterPage(array $messages) { + $indexes = array(); + foreach ($messages as $message) { + $index = $message->getNameIndex(); + if (strlen($index)) { + $indexes[$index] = $index; + } + } + + if ($indexes) { + $map = HarbormasterString::newIndexMap($indexes); + + foreach ($messages as $message) { + $index = $message->getNameIndex(); + + if (!strlen($index)) { + continue; + } + + $name = idx($map, $index); + if ($name === null) { + $name = pht('Unknown Unit Message ("%s")', $index); + } + + $message->setName($name); + } + } + + return $messages; + } + public function getQueryApplicationClass() { return 'PhabricatorHarbormasterApplication'; } diff --git a/src/applications/harbormaster/storage/HarbormasterString.php b/src/applications/harbormaster/storage/HarbormasterString.php new file mode 100644 index 0000000000..7493e60e21 --- /dev/null +++ b/src/applications/harbormaster/storage/HarbormasterString.php @@ -0,0 +1,54 @@ + false, + self::CONFIG_COLUMN_SCHEMA => array( + 'stringIndex' => 'bytes12', + 'stringValue' => 'text', + ), + self::CONFIG_KEY_SCHEMA => array( + 'key_string' => array( + 'columns' => array('stringIndex'), + 'unique' => true, + ), + ), + ) + parent::getConfiguration(); + } + + public static function newIndex($string) { + $index = PhabricatorHash::digestForIndex($string); + + $table = new self(); + $conn = $table->establishConnection('w'); + + queryfx( + $conn, + 'INSERT IGNORE INTO %R (stringIndex, stringValue) VALUES (%s, %s)', + $table, + $index, + $string); + + return $index; + } + + public static function newIndexMap(array $indexes) { + $table = new self(); + $conn = $table->establishConnection('r'); + + $rows = queryfx_all( + $conn, + 'SELECT stringIndex, stringValue FROM %R WHERE stringIndex IN (%Ls)', + $table, + $indexes); + + return ipull($rows, 'stringValue', 'stringIndex'); + } + +} diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuildUnitMessage.php b/src/applications/harbormaster/storage/build/HarbormasterBuildUnitMessage.php index 1f5e5e1440..9e437efaba 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuildUnitMessage.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuildUnitMessage.php @@ -8,6 +8,7 @@ final class HarbormasterBuildUnitMessage protected $engine; protected $namespace; protected $name; + protected $nameIndex; protected $result; protected $duration; protected $properties = array(); @@ -132,6 +133,7 @@ final class HarbormasterBuildUnitMessage 'engine' => 'text255', 'namespace' => 'text255', 'name' => 'text255', + 'nameIndex' => 'bytes12', 'result' => 'text32', 'duration' => 'double?', ), @@ -260,6 +262,33 @@ final class HarbormasterBuildUnitMessage return implode("\0", $parts); } + public function save() { + if ($this->nameIndex === null) { + $this->nameIndex = HarbormasterString::newIndex($this->getName()); + } + + // See T13088. While we're letting installs do online migrations to avoid + // downtime, don't populate the "name" column for new writes. New writes + // use the "HarbormasterString" table instead. + $old_name = $this->name; + $this->name = ''; + + $caught = null; + try { + $result = parent::save(); + } catch (Exception $ex) { + $caught = $ex; + } + + $this->name = $old_name; + + if ($caught) { + throw $caught; + } + + return $result; + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ From 661c758ff9d1421d2298cac902ca001e3f30a313 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 15 Feb 2019 08:10:56 -0800 Subject: [PATCH 083/245] Render indent depth changes more clearly Summary: Ref T13161. See PHI723. Our whitespace handling is based on whitespace flags like `diff -bw`, mostly just for historical reasons: long ago, the easiest way to minimize the visual impact of indentation changes was to literally use `diff -bw`. However, this approach is very coarse and has a lot of problems, like detecting `"ab" -> "a b"` as "only a whitespace change" even though this is always semantic. It also causes problems in YAML, Python, etc. Over time, we've added a lot of stuff to mitigate the downsides to this approach. We also no longer get any benefits from this approach being simple: we need faithful diffs as the authoritative source, and have to completely rebuild the diff to `diff -bw` it. In the UI, we have a "whitespace mode" flag. We have the "whitespace matters" configuration. I think ReviewBoard generally has a better approach to indent depth changes than we do (see T13161) where it detects them and renders them in a minimal way with low visual impact. This is ultimately what we want: reduce visual clutter for depth-only changes, but preserve whitespace changes in strings, etc. Move toward detecting and rendering indent depth changes. Followup work: - These should get colorblind colors and the design can probably use a little more tweaking. - The OneUp mode is okay, but could be improved. - Whitespace mode can now be removed completely. - I'm trying to handle tabs correctly, but since we currently mangle them into spaces today, it's hard to be sure I actually got it right. Test Plan: {F6214084} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13161 Differential Revision: https://secure.phabricator.com/D20181 --- resources/celerity/map.php | 14 +- .../CelerityDefaultPostprocessor.php | 2 + .../data/whitespace.diff.one.whitespace | 2 +- .../data/whitespace.diff.two.whitespace | 2 +- .../parser/DifferentialChangesetParser.php | 17 +- .../parser/DifferentialHunkParser.php | 171 +++++++++++++++++- .../render/DifferentialChangesetRenderer.php | 10 + .../DifferentialChangesetTestRenderer.php | 4 + .../DifferentialChangesetTwoUpRenderer.php | 24 ++- .../differential/changeset-view.css | 21 +++ webroot/rsrc/image/chevron-in.png | Bin 0 -> 1409 bytes webroot/rsrc/image/chevron-out.png | Bin 0 -> 1417 bytes 12 files changed, 251 insertions(+), 16 deletions(-) create mode 100644 webroot/rsrc/image/chevron-in.png create mode 100644 webroot/rsrc/image/chevron-out.png diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 26aec85659..056535074c 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -11,7 +11,7 @@ return array( 'conpherence.pkg.js' => '020aebcf', 'core.pkg.css' => '261ee8cf', 'core.pkg.js' => '5ace8a1e', - 'differential.pkg.css' => 'b8df73d4', + 'differential.pkg.css' => 'c3f15714', 'differential.pkg.js' => '67c9ea4c', 'diffusion.pkg.css' => '42c75c37', 'diffusion.pkg.js' => '91192d85', @@ -61,7 +61,7 @@ return array( 'rsrc/css/application/dashboard/dashboard.css' => '4267d6c6', 'rsrc/css/application/diff/inline-comment-summary.css' => '81eb368d', 'rsrc/css/application/differential/add-comment.css' => '7e5900d9', - 'rsrc/css/application/differential/changeset-view.css' => '73660575', + 'rsrc/css/application/differential/changeset-view.css' => '783a9206', 'rsrc/css/application/differential/core.css' => 'bdb93065', 'rsrc/css/application/differential/phui-inline-comment.css' => '48acce5b', 'rsrc/css/application/differential/revision-comment.css' => '7dbc8d1d', @@ -275,6 +275,8 @@ return array( 'rsrc/image/checker_dark.png' => '7fc8fa7b', 'rsrc/image/checker_light.png' => '3157a202', 'rsrc/image/checker_lighter.png' => 'c45928c1', + 'rsrc/image/chevron-in.png' => '1aa2f88f', + 'rsrc/image/chevron-out.png' => 'c815e272', 'rsrc/image/controls/checkbox-checked.png' => '1770d7a0', 'rsrc/image/controls/checkbox-unchecked.png' => 'e1deba0a', 'rsrc/image/d5d8e1.png' => '6764616e', @@ -539,7 +541,7 @@ return array( 'conpherence-thread-manager' => 'aec8e38c', 'conpherence-transaction-css' => '3a3f5e7e', 'd3' => 'd67475f5', - 'differential-changeset-view-css' => '73660575', + 'differential-changeset-view-css' => '783a9206', 'differential-core-view-css' => 'bdb93065', 'differential-revision-add-comment-css' => '7e5900d9', 'differential-revision-comment-css' => '7dbc8d1d', @@ -1490,9 +1492,6 @@ return array( 'javelin-dom', 'javelin-uri', ), - 73660575 => array( - 'phui-inline-comment-view-css', - ), '73ecc1f8' => array( 'javelin-behavior', 'javelin-behavior-device', @@ -1514,6 +1513,9 @@ return array( 'javelin-uri', 'javelin-request', ), + '783a9206' => array( + 'phui-inline-comment-view-css', + ), '78bc5d94' => array( 'javelin-behavior', 'javelin-uri', diff --git a/src/applications/celerity/postprocessor/CelerityDefaultPostprocessor.php b/src/applications/celerity/postprocessor/CelerityDefaultPostprocessor.php index 61f6176f15..d848fb81e8 100644 --- a/src/applications/celerity/postprocessor/CelerityDefaultPostprocessor.php +++ b/src/applications/celerity/postprocessor/CelerityDefaultPostprocessor.php @@ -199,8 +199,10 @@ final class CelerityDefaultPostprocessor 'diff.background' => '#fff', 'new-background' => 'rgba(151, 234, 151, .3)', 'new-bright' => 'rgba(151, 234, 151, .6)', + 'new-background-strong' => 'rgba(151, 234, 151, 1)', 'old-background' => 'rgba(251, 175, 175, .3)', 'old-bright' => 'rgba(251, 175, 175, .7)', + 'old-background-strong' => 'rgba(251, 175, 175, 1)', 'move-background' => '#fdf5d4', 'copy-background' => '#f1c40f', diff --git a/src/applications/differential/__tests__/data/whitespace.diff.one.whitespace b/src/applications/differential/__tests__/data/whitespace.diff.one.whitespace index f4a5af6f3e..db43e02158 100644 --- a/src/applications/differential/__tests__/data/whitespace.diff.one.whitespace +++ b/src/applications/differential/__tests__/data/whitespace.diff.one.whitespace @@ -1,2 +1,2 @@ O 1 - -=[-Rocket-Ship>\n~ -N 1 + {( )}-=[-Rocket-Ship>\n~ +N 1 + {> )}-=[-Rocket-Ship>\n~ diff --git a/src/applications/differential/__tests__/data/whitespace.diff.two.whitespace b/src/applications/differential/__tests__/data/whitespace.diff.two.whitespace index f4a5af6f3e..db43e02158 100644 --- a/src/applications/differential/__tests__/data/whitespace.diff.two.whitespace +++ b/src/applications/differential/__tests__/data/whitespace.diff.two.whitespace @@ -1,2 +1,2 @@ O 1 - -=[-Rocket-Ship>\n~ -N 1 + {( )}-=[-Rocket-Ship>\n~ +N 1 + {> )}-=[-Rocket-Ship>\n~ diff --git a/src/applications/differential/parser/DifferentialChangesetParser.php b/src/applications/differential/parser/DifferentialChangesetParser.php index 27d2a2d845..22f31b3e9f 100644 --- a/src/applications/differential/parser/DifferentialChangesetParser.php +++ b/src/applications/differential/parser/DifferentialChangesetParser.php @@ -8,6 +8,7 @@ final class DifferentialChangesetParser extends Phobject { protected $new = array(); protected $old = array(); protected $intra = array(); + protected $depthOnlyLines = array(); protected $newRender = null; protected $oldRender = null; @@ -190,7 +191,7 @@ final class DifferentialChangesetParser extends Phobject { return $this; } - const CACHE_VERSION = 11; + const CACHE_VERSION = 12; const CACHE_MAX_SIZE = 8e6; const ATTR_GENERATED = 'attr:generated'; @@ -224,6 +225,15 @@ final class DifferentialChangesetParser extends Phobject { return $this; } + public function setDepthOnlyLines(array $lines) { + $this->depthOnlyLines = $lines; + return $this; + } + + public function getDepthOnlyLines() { + return $this->depthOnlyLines; + } + public function setVisibileLinesMask(array $mask) { $this->visible = $mask; return $this; @@ -450,6 +460,7 @@ final class DifferentialChangesetParser extends Phobject { 'new', 'old', 'intra', + 'depthOnlyLines', 'newRender', 'oldRender', 'specialAttributes', @@ -754,6 +765,7 @@ final class DifferentialChangesetParser extends Phobject { $this->setOldLines($hunk_parser->getOldLines()); $this->setNewLines($hunk_parser->getNewLines()); $this->setIntraLineDiffs($hunk_parser->getIntraLineDiffs()); + $this->setDepthOnlyLines($hunk_parser->getDepthOnlyLines()); $this->setVisibileLinesMask($hunk_parser->getVisibleLinesMask()); $this->hunkStartLines = $hunk_parser->getHunkStartLines( $changeset->getHunks()); @@ -914,7 +926,8 @@ final class DifferentialChangesetParser extends Phobject { ->setShowEditAndReplyLinks($this->getShowEditAndReplyLinks()) ->setCanMarkDone($this->getCanMarkDone()) ->setObjectOwnerPHID($this->getObjectOwnerPHID()) - ->setHighlightingDisabled($this->highlightingDisabled); + ->setHighlightingDisabled($this->highlightingDisabled) + ->setDepthOnlyLines($this->getDepthOnlyLines()); $shield = null; if ($this->isTopLevel && !$this->comments) { diff --git a/src/applications/differential/parser/DifferentialHunkParser.php b/src/applications/differential/parser/DifferentialHunkParser.php index 5bd98e9012..e7b9ce21a9 100644 --- a/src/applications/differential/parser/DifferentialHunkParser.php +++ b/src/applications/differential/parser/DifferentialHunkParser.php @@ -5,6 +5,7 @@ final class DifferentialHunkParser extends Phobject { private $oldLines; private $newLines; private $intraLineDiffs; + private $depthOnlyLines; private $visibleLinesMask; private $whitespaceMode; @@ -115,6 +116,14 @@ final class DifferentialHunkParser extends Phobject { return $this; } + public function setDepthOnlyLines(array $map) { + $this->depthOnlyLines = $map; + return $this; + } + + public function getDepthOnlyLines() { + return $this->depthOnlyLines; + } public function setWhitespaceMode($white_space_mode) { $this->whitespaceMode = $white_space_mode; @@ -334,6 +343,7 @@ final class DifferentialHunkParser extends Phobject { $new = $this->getNewLines(); $diffs = array(); + $depth_only = array(); foreach ($old as $key => $o) { $n = $new[$key]; @@ -342,13 +352,75 @@ final class DifferentialHunkParser extends Phobject { } if ($o['type'] != $n['type']) { - $diffs[$key] = ArcanistDiffUtils::generateIntralineDiff( - $o['text'], - $n['text']); + $o_segments = array(); + $n_segments = array(); + $tab_width = 2; + + $o_text = $o['text']; + $n_text = $n['text']; + + if ($o_text !== $n_text) { + $o_depth = $this->getIndentDepth($o_text, $tab_width); + $n_depth = $this->getIndentDepth($n_text, $tab_width); + + if ($o_depth < $n_depth) { + $segment_type = '>'; + $segment_width = $this->getCharacterCountForVisualWhitespace( + $n_text, + ($n_depth - $o_depth), + $tab_width); + if ($segment_width) { + $n_text = substr($n_text, $segment_width); + $n_segments[] = array( + $segment_type, + $segment_width, + ); + } + } else if ($o_depth > $n_depth) { + $segment_type = '<'; + $segment_width = $this->getCharacterCountForVisualWhitespace( + $o_text, + ($o_depth - $n_depth), + $tab_width); + if ($segment_width) { + $o_text = substr($o_text, $segment_width); + $o_segments[] = array( + $segment_type, + $segment_width, + ); + } + } + + // If there are no remaining changes to this line after we've marked + // off the indent depth changes, this line was only modified by + // changing the indent depth. Mark it for later so we can change how + // it is displayed. + if ($o_text === $n_text) { + $depth_only[$key] = $segment_type; + } + } + + $intraline_segments = ArcanistDiffUtils::generateIntralineDiff( + $o_text, + $n_text); + + foreach ($intraline_segments[0] as $o_segment) { + $o_segments[] = $o_segment; + } + + foreach ($intraline_segments[1] as $n_segment) { + $n_segments[] = $n_segment; + } + + $diffs[$key] = array( + $o_segments, + $n_segments, + ); } } $this->setIntraLineDiffs($diffs); + $this->setDepthOnlyLines($depth_only); return $this; } @@ -671,4 +743,97 @@ final class DifferentialHunkParser extends Phobject { return $offsets; } + + private function getIndentDepth($text, $tab_width) { + $len = strlen($text); + + $depth = 0; + for ($ii = 0; $ii < $len; $ii++) { + $c = $text[$ii]; + + // If this is a space, increase the indent depth by 1. + if ($c == ' ') { + $depth++; + continue; + } + + // If this is a tab, increase the indent depth to the next tabstop. + + // For example, if the tab width is 4, these sequences both lead us to + // a visual width of 8, i.e. the cursor will be in the 8th column: + // + // + // + + if ($c == "\t") { + $depth = ($depth + $tab_width); + $depth = $depth - ($depth % $tab_width); + continue; + } + + break; + } + + return $depth; + } + + private function getCharacterCountForVisualWhitespace( + $text, + $depth, + $tab_width) { + + // Here, we know the visual indent depth of a line has been increased by + // some amount (for example, 6 characters). + + // We want to find the largest whitespace prefix of the string we can + // which still fits into that amount of visual space. + + // In most cases, this is very easy. For example, if the string has been + // indented by two characters and the string begins with two spaces, that's + // a perfect match. + + // However, if the string has been indented by 7 characters, the tab width + // is 8, and the string begins with "", we can only + // mark the two spaces as an indent change. These cases are unusual. + + $character_depth = 0; + $visual_depth = 0; + + $len = strlen($text); + for ($ii = 0; $ii < $len; $ii++) { + if ($visual_depth >= $depth) { + break; + } + + $c = $text[$ii]; + + if ($c == ' ') { + $character_depth++; + $visual_depth++; + continue; + } + + if ($c == "\t") { + // Figure out how many visual spaces we have until the next tabstop. + $tab_visual = ($visual_depth + $tab_width); + $tab_visual = $tab_visual - ($tab_visual % $tab_width); + $tab_visual = ($tab_visual - $visual_depth); + + // If this tab would take us over the limit, we're all done. + $remaining_depth = ($depth - $visual_depth); + if ($remaining_depth < $tab_visual) { + break; + } + + $character_depth++; + $visual_depth += $tab_visual; + continue; + } + + break; + } + + return $character_depth; + } + } diff --git a/src/applications/differential/render/DifferentialChangesetRenderer.php b/src/applications/differential/render/DifferentialChangesetRenderer.php index f295695286..c5d033a4a9 100644 --- a/src/applications/differential/render/DifferentialChangesetRenderer.php +++ b/src/applications/differential/render/DifferentialChangesetRenderer.php @@ -34,6 +34,7 @@ abstract class DifferentialChangesetRenderer extends Phobject { private $objectOwnerPHID; private $highlightingDisabled; private $scopeEngine; + private $depthOnlyLines; private $oldFile = false; private $newFile = false; @@ -92,6 +93,15 @@ abstract class DifferentialChangesetRenderer extends Phobject { return $this->gaps; } + public function setDepthOnlyLines(array $lines) { + $this->depthOnlyLines = $lines; + return $this; + } + + public function getDepthOnlyLines() { + return $this->depthOnlyLines; + } + public function attachOldFile(PhabricatorFile $old = null) { $this->oldFile = $old; return $this; diff --git a/src/applications/differential/render/DifferentialChangesetTestRenderer.php b/src/applications/differential/render/DifferentialChangesetTestRenderer.php index a0d1fad0eb..c7b35d1fb4 100644 --- a/src/applications/differential/render/DifferentialChangesetTestRenderer.php +++ b/src/applications/differential/render/DifferentialChangesetTestRenderer.php @@ -96,10 +96,14 @@ abstract class DifferentialChangesetTestRenderer array( '', '', + '', + '', ), array( '{(', ')}', + '{<', + '{>', ), $render); diff --git a/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php b/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php index f40a7f5e0b..b4936201e0 100644 --- a/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php +++ b/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php @@ -71,8 +71,8 @@ final class DifferentialChangesetTwoUpRenderer $mask = $this->getMask(); $scope_engine = $this->getScopeEngine(); - $offset_map = null; + $depth_only = $this->getDepthOnlyLines(); for ($ii = $range_start; $ii < $range_start + $range_len; $ii++) { if (empty($mask[$ii])) { @@ -196,11 +196,29 @@ final class DifferentialChangesetTwoUpRenderer } else if (empty($old_lines[$ii])) { $n_class = 'new new-full'; } else { - $n_class = 'new'; + + // NOTE: At least for the moment, I'm intentionally clearing the + // line highlighting only on the right side of the diff when a + // line has only depth changes. When a block depth is decreased, + // this gives us a large color block on the left (to make it easy + // to see the depth change) but a clean diff on the right (to make + // it easy to pick out actual code changes). + + if (isset($depth_only[$ii])) { + $n_class = ''; + } else { + $n_class = 'new'; + } } $n_classes = $n_class; - if ($new_lines[$ii]['type'] == '\\' || !isset($copy_lines[$n_num])) { + $not_copied = + // If this line only changed depth, copy markers are pointless. + (!isset($copy_lines[$n_num])) || + (isset($depth_only[$ii])) || + ($new_lines[$ii]['type'] == '\\'); + + if ($not_copied) { $n_copy = phutil_tag('td', array('class' => "copy {$n_class}")); } else { list($orig_file, $orig_line, $orig_type) = $copy_lines[$n_num]; diff --git a/webroot/rsrc/css/application/differential/changeset-view.css b/webroot/rsrc/css/application/differential/changeset-view.css index b9683dc7c6..2cfa753a42 100644 --- a/webroot/rsrc/css/application/differential/changeset-view.css +++ b/webroot/rsrc/css/application/differential/changeset-view.css @@ -135,12 +135,33 @@ background: {$old-bright}; } + .differential-diff td.new span.bright, .differential-diff td.new-full, .prose-diff span.new { background: {$new-bright}; } +.differential-diff td span.depth-out, +.differential-diff td span.depth-in { + padding: 2px 0; + background-size: 12px 12px; + background-repeat: no-repeat; + background-position: left center; +} + +.differential-diff td span.depth-out { + background-image: url(/rsrc/image/chevron-out.png); + background-color: {$old-background-strong}; +} + +.differential-diff td span.depth-in { + background-position: 2px center; + background-image: url(/rsrc/image/chevron-in.png); + background-color: {$new-background-strong}; +} + + .differential-diff td.copy { min-width: 0.5%; width: 0.5%; diff --git a/webroot/rsrc/image/chevron-in.png b/webroot/rsrc/image/chevron-in.png new file mode 100644 index 0000000000000000000000000000000000000000..373d39cfe1a0b85b7afae5cb8e7e77c63f763f37 GIT binary patch literal 1409 zcmbVMYfKzf6rQDpQUpTQv{lm7={8hAXXmlAj~Q9ZV?kC}3foN}fhzL?rm!>X?9g2( z1jJCgBD6r%SfNcUg%qQrXdop-Y@@XirD{#7T7qd}qroE8HbPVN4zTDC(jQLl%)R&A z^PThE^O#U!!BcTDi7^la#W`~wZZKoo8yy9{#Sx1yg6Uy3yF}e5SEvC-5uglSW(C;k zWhw=?!0>g?T@=zFXhTr+l&B@He43NJMn=;y2E9IjhM=@PK_A0a3o6VCm7-)f%#VF! zfJNSJC^ow=moH1G5_9VnVPAcLhpVsVDBiH=NjNP?0|Q<`W#FK%z|JzjoD}dhgTm1 z@TTw;wA+!h>I>}bhALI{(I^@S1dIWbQC2EZoT4ZcBT#}sfCl2PlT;>%Nd8@63l720 zDWXpmWeL_SGOS#y+6{o|$`!mmmutbqoK}SXbNc zsq+b_Tky-Z3J3C0u`3J)vAe$`O%br6_bMVt3RB~dxmvFvsZNL808Wg&$kU{iCAsN<+*leVL@GQ=n%{*^2 zQ&z(&p8vlQL;)#OE0_P2Pgn$MQd_Py1K6y!lOTZ}Qb4ohPi_4Hf^;XHjto!mySdlj z9X^M}UMP-#p{KW}QQzi>r`j5t9@-kpW;=(whr16+YT!)i+1+I8wY>R3c&y>p^*3TC zZ5>ySPwg&UEP^5n!D#+FZfO5f3x0TMa(uk$j-_lM`TLCAsE*i24)pitZoW5tw^}Y} zxbyMM?<0St>k^U{7Ly))rY{soC^{hee*DDq*(;xR=$iiAm_DD<&i*t!FdjGxU&eKl zUHtPepGePx#>`%hZkyQdPHrXyNb*O)v z?udQU&13k2{%Im~8tVErQx7>uzM6(A9XCgB|J9fJ#TZ24Er&uC?PoH#KRWsioeEtE z#m_C}&(^f|md6cDL+v+`t_61FKhgPG{Vy+d&W#kEY8p;$jz|V$;`mfvvksckpE`Pv S*@kOBMQ3(_l3^w)^1&PVosU-?Y zsp*+{wo31J?^jaDOtDo8H}y5}EpSfF$n>ZxN)4{^3rViZPPR-@vbR&PsjvbXkegbP zs8ErclUHn2VXFi-*9yo63F|8hqhMrUXs&Nys&8PXYhY+)U}0rsr~m~@K--E^(yW49+@N*=dA3R!B_#z``ugSN z<$C4Ddih1^`i7R4mih)p`bI{&Koz>hm3bwJ6}oxF$}kgLQj3#|G7CyF^YauyCMG83 zmzLNn0bL65LT-VtFWlg~VrW1CgG|37u|VHY&pf(~1RD?6IsxA(xEJ)Q4 zN-fSWElLK)N18HBGcfG%TLe-Fbd8mNQ6?}_5_4SglS^|`^GZBjY?XjAdMTMHRwm{q z$;JkjNxGIvX$HC`W`<_ENd^{%x=E=?rsf7lNrs7L#xQfR={K@4Fm^OHb1||sGcz=F zwX|?Ha-hCuU;$XqSVBa{GyQj{2W*+ z2*}7U$uG{xFHmrH2F1FCf`)Hma%LV#P!kkU5P!R*7G;*DrnnX5=PH0h+A0%^E0Piu zjg1XVEOb+nO^kF+5|hkzEi6n@byJcIlT1xhQY=g@&6S|~Q^*ZLeW0WCLCFOv`M`vL zX%fVQX9ge#o}E(jfO)70m{}CP9)>V5FfR9WaSW-r^=77a|6v23hUu3ZwRL25Y%Q!R zN(#O__`BlZ#e=dkGPV&>7Euv5EGMbU&X|0oSJ-!xYW8HS)&GR{Puf{IFMPdD{%Ub? zW&;KgicopK(!S)JZsmFP7wj+Ok83PFIqgQH{)N34)>$=_*PPyx=~!QI{Zs3N1Dx;w z%*wpJFqYZs8q<&WEbiM@CNAbwC|uIuFQXPeyuq8YvCw)@W$^917!!E^1~<`?ZdUNOJRcX12P#2dLRX0GR% z=O$d5(kFHP_@67?x)%H%#SLD+GWeGKIL25}^Y`4H(vw&0(V78r}tV z+*26>=DK+^9$D3EqR;l>ZN#U5dMEdleR~uSy>WOG5dApp(E3jkEtPySiq~H3T>0^l zg2W{~ri!iA3k?ONA|Gzm-=?zgV}rj{*t0cG8y_z}b$7+)dQIK~3_M_@bt<-jk>PUD V61L*pm{3sR=;`X`vd$@?2>`Zs_fh}= literal 0 HcmV?d00001 From 5310f1cdd90ec51331facb79fc1e47a7a824b20e Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 15 Feb 2019 10:23:36 -0800 Subject: [PATCH 084/245] Remove all whitespace options/configuration everywhere Summary: Depends on D20181. Depends on D20182. Fixes T3498. Ref T13161. My claim, at least, is that D20181 can be tweaked to be good enough to throw away this "feature" completely. I think this feature was sort of a mistake, where the ease of access to `diff -bw` shaped behavior a very long time ago and then the train just ran a long way down the tracks in the same direction. Test Plan: Grepped for `whitespace`, deleted almost everything. Poked around the UI a bit. I'm expecting the whitespace changes to get some more iteration this week so I not being hugely pedantic about testing this stuff exhaustively. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13161, T3498 Differential Revision: https://secure.phabricator.com/D20185 --- resources/celerity/map.php | 28 +++--- .../PhabricatorExtraConfigSetupCheck.php | 2 + .../DifferentialParseRenderTestCase.php | 15 +-- .../data/generated.diff.one.unshielded | 3 +- .../data/generated.diff.two.unshielded | 4 +- .../__tests__/data/whitespace.diff.one.expect | 3 +- .../__tests__/data/whitespace.diff.two.expect | 3 +- .../PhabricatorDifferentialConfigOptions.php | 12 --- .../DifferentialRevisionViewController.php | 8 +- .../parser/DifferentialChangesetParser.php | 99 +------------------ .../parser/DifferentialHunkParser.php | 83 ---------------- .../parser/DifferentialLineAdjustmentMap.php | 1 - .../DifferentialChangesetHTMLRenderer.php | 5 - .../render/DifferentialChangesetRenderer.php | 3 - .../storage/DifferentialChangeset.php | 11 --- .../view/DifferentialChangesetDetailView.php | 11 --- .../view/DifferentialChangesetListView.php | 10 +- .../DifferentialRevisionUpdateHistoryView.php | 35 ------- .../controller/DiffusionChangeController.php | 3 - .../controller/DiffusionDiffController.php | 3 - .../user/userguide/differential_faq.diviner | 16 --- .../diff/PhabricatorDifferenceEngine.php | 47 +-------- .../rsrc/js/application/diff/DiffChangeset.js | 3 - 23 files changed, 33 insertions(+), 375 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 056535074c..65eed7916a 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -12,7 +12,7 @@ return array( 'core.pkg.css' => '261ee8cf', 'core.pkg.js' => '5ace8a1e', 'differential.pkg.css' => 'c3f15714', - 'differential.pkg.js' => '67c9ea4c', + 'differential.pkg.js' => 'be031567', 'diffusion.pkg.css' => '42c75c37', 'diffusion.pkg.js' => '91192d85', 'maniphest.pkg.css' => '35995d6d', @@ -374,7 +374,7 @@ return array( 'rsrc/js/application/dashboard/behavior-dashboard-move-panels.js' => '076bd092', 'rsrc/js/application/dashboard/behavior-dashboard-query-panel-select.js' => '1e413dc9', 'rsrc/js/application/dashboard/behavior-dashboard-tab-panel.js' => '9b1cbd76', - 'rsrc/js/application/diff/DiffChangeset.js' => 'e7cf10d6', + 'rsrc/js/application/diff/DiffChangeset.js' => 'd0a85a85', 'rsrc/js/application/diff/DiffChangesetList.js' => 'b91204e9', 'rsrc/js/application/diff/DiffInline.js' => 'a4a14a94', 'rsrc/js/application/diff/behavior-preview-link.js' => 'f51e9c17', @@ -753,7 +753,7 @@ return array( 'phabricator-darklog' => '3b869402', 'phabricator-darkmessage' => '26cd4b73', 'phabricator-dashboard-css' => '4267d6c6', - 'phabricator-diff-changeset' => 'e7cf10d6', + 'phabricator-diff-changeset' => 'd0a85a85', 'phabricator-diff-changeset-list' => 'b91204e9', 'phabricator-diff-inline' => 'a4a14a94', 'phabricator-drag-and-drop-file-upload' => '4370900d', @@ -1973,6 +1973,17 @@ return array( 'javelin-stratcom', 'javelin-util', ), + 'd0a85a85' => array( + 'javelin-dom', + 'javelin-util', + 'javelin-stratcom', + 'javelin-install', + 'javelin-workflow', + 'javelin-router', + 'javelin-behavior-device', + 'javelin-vector', + 'phabricator-diff-inline', + ), 'd12d214f' => array( 'javelin-install', 'javelin-dom', @@ -2038,17 +2049,6 @@ return array( 'javelin-dom', 'phabricator-draggable-list', ), - 'e7cf10d6' => array( - 'javelin-dom', - 'javelin-util', - 'javelin-stratcom', - 'javelin-install', - 'javelin-workflow', - 'javelin-router', - 'javelin-behavior-device', - 'javelin-vector', - 'phabricator-diff-inline', - ), 'e8240b50' => array( 'javelin-behavior', 'javelin-stratcom', diff --git a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php index a742e3b82b..b676063e8c 100644 --- a/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php +++ b/src/applications/config/check/PhabricatorExtraConfigSetupCheck.php @@ -533,6 +533,8 @@ final class PhabricatorExtraConfigSetupCheck extends PhabricatorSetupCheck { 'This ancient extension point has been replaced with other '. 'mechanisms, including "AphrontSite".'), + 'differential.whitespace-matters' => pht( + 'Whitespace rendering is now handled automatically.'), ); return $ancient_config; diff --git a/src/applications/differential/__tests__/DifferentialParseRenderTestCase.php b/src/applications/differential/__tests__/DifferentialParseRenderTestCase.php index c278ff0c67..259bb671de 100644 --- a/src/applications/differential/__tests__/DifferentialParseRenderTestCase.php +++ b/src/applications/differential/__tests__/DifferentialParseRenderTestCase.php @@ -31,7 +31,6 @@ final class DifferentialParseRenderTestCase extends PhabricatorTestCase { foreach (array('one', 'two') as $type) { $this->runParser($type, $data, $file, 'expect'); $this->runParser($type, $data, $file, 'unshielded'); - $this->runParser($type, $data, $file, 'whitespace'); } } } @@ -44,25 +43,20 @@ final class DifferentialParseRenderTestCase extends PhabricatorTestCase { } $unshielded = false; - $whitespace = false; switch ($extension) { case 'unshielded': $unshielded = true; break; - case 'whitespace'; - $unshielded = true; - $whitespace = true; - break; } $parsers = $this->buildChangesetParsers($type, $data, $file); - $actual = $this->renderParsers($parsers, $unshielded, $whitespace); + $actual = $this->renderParsers($parsers, $unshielded); $expect = Filesystem::readFile($test_file); $this->assertEqual($expect, $actual, basename($test_file)); } - private function renderParsers(array $parsers, $unshield, $whitespace) { + private function renderParsers(array $parsers, $unshield) { $result = array(); foreach ($parsers as $parser) { if ($unshield) { @@ -73,11 +67,6 @@ final class DifferentialParseRenderTestCase extends PhabricatorTestCase { $e_range = null; } - if ($whitespace) { - $parser->setWhitespaceMode( - DifferentialChangesetParser::WHITESPACE_SHOW_ALL); - } - $result[] = $parser->render($s_range, $e_range, array()); } return implode(str_repeat('~', 80)."\n", $result); diff --git a/src/applications/differential/__tests__/data/generated.diff.one.unshielded b/src/applications/differential/__tests__/data/generated.diff.one.unshielded index ca4b1b167e..acfa701c8b 100644 --- a/src/applications/differential/__tests__/data/generated.diff.one.unshielded +++ b/src/applications/differential/__tests__/data/generated.diff.one.unshielded @@ -1,6 +1,5 @@ N 1 . @generated\n~ -O 2 - \n~ +N 2 . \n~ O 3 - This is a generated file.\n~ -N 2 + \n~ N 3 + This is a generated file{(, full of generated code)}.\n~ N 4 . \n~ diff --git a/src/applications/differential/__tests__/data/generated.diff.two.unshielded b/src/applications/differential/__tests__/data/generated.diff.two.unshielded index 967f6220de..183a0b6edc 100644 --- a/src/applications/differential/__tests__/data/generated.diff.two.unshielded +++ b/src/applications/differential/__tests__/data/generated.diff.two.unshielded @@ -1,7 +1,7 @@ O 1 . @generated\n~ N 1 . @generated\n~ -O 2 - \n~ -N 2 + \n~ +O 2 . \n~ +N 2 . \n~ O 3 - This is a generated file.\n~ N 3 + This is a generated file{(, full of generated code)}.\n~ O 4 . \n~ diff --git a/src/applications/differential/__tests__/data/whitespace.diff.one.expect b/src/applications/differential/__tests__/data/whitespace.diff.one.expect index 5b229959d3..87ad1dcdd9 100644 --- a/src/applications/differential/__tests__/data/whitespace.diff.one.expect +++ b/src/applications/differential/__tests__/data/whitespace.diff.one.expect @@ -2,4 +2,5 @@ CTYPE 2 1 (unforced) WHITESPACE WHITESPACE - -SHIELD (whitespace) This file was changed only by adding or removing whitespace. +O 1 - -=[-Rocket-Ship>\n~ +N 1 + {> )}-=[-Rocket-Ship>\n~ diff --git a/src/applications/differential/__tests__/data/whitespace.diff.two.expect b/src/applications/differential/__tests__/data/whitespace.diff.two.expect index 5b229959d3..87ad1dcdd9 100644 --- a/src/applications/differential/__tests__/data/whitespace.diff.two.expect +++ b/src/applications/differential/__tests__/data/whitespace.diff.two.expect @@ -2,4 +2,5 @@ CTYPE 2 1 (unforced) WHITESPACE WHITESPACE - -SHIELD (whitespace) This file was changed only by adding or removing whitespace. +O 1 - -=[-Rocket-Ship>\n~ +N 1 + {> )}-=[-Rocket-Ship>\n~ diff --git a/src/applications/differential/config/PhabricatorDifferentialConfigOptions.php b/src/applications/differential/config/PhabricatorDifferentialConfigOptions.php index ec2099f8dd..9634756dac 100644 --- a/src/applications/differential/config/PhabricatorDifferentialConfigOptions.php +++ b/src/applications/differential/config/PhabricatorDifferentialConfigOptions.php @@ -80,18 +80,6 @@ EOHELP "Select and reorder revision fields.\n\n". "NOTE: This feature is under active development and subject ". "to change.")), - $this->newOption( - 'differential.whitespace-matters', - 'list', - array( - '/\.py$/', - '/\.l?hs$/', - '/\.ya?ml$/', - )) - ->setDescription( - pht( - "List of file regexps where whitespace is meaningful and should ". - "not use 'ignore-all' by default")), $this->newOption('differential.require-test-plan-field', 'bool', true) ->setBoolOptions( array( diff --git a/src/applications/differential/controller/DifferentialRevisionViewController.php b/src/applications/differential/controller/DifferentialRevisionViewController.php index 9bc6345576..2ac1d30eca 100644 --- a/src/applications/differential/controller/DifferentialRevisionViewController.php +++ b/src/applications/differential/controller/DifferentialRevisionViewController.php @@ -305,10 +305,6 @@ final class DifferentialRevisionViewController $details = $this->buildDetails($revision, $field_list); $curtain = $this->buildCurtain($revision); - $whitespace = $request->getStr( - 'whitespace', - DifferentialChangesetParser::WHITESPACE_IGNORE_MOST); - $repository = $revision->getRepository(); if ($repository) { $symbol_indexes = $this->buildSymbolIndexes( @@ -383,7 +379,6 @@ final class DifferentialRevisionViewController ->setDiff($target) ->setRenderingReferences($rendering_references) ->setVsMap($vs_map) - ->setWhitespace($whitespace) ->setSymbolIndexes($symbol_indexes) ->setTitle(pht('Diff %s', $target->getID())) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY); @@ -412,7 +407,6 @@ final class DifferentialRevisionViewController ->setDiffUnitStatuses($broken_diffs) ->setSelectedVersusDiffID($diff_vs) ->setSelectedDiffID($target->getID()) - ->setSelectedWhitespace($whitespace) ->setCommitsForLinks($commits_for_links); $local_table = id(new DifferentialLocalCommitsView()) @@ -1095,7 +1089,7 @@ final class DifferentialRevisionViewController // this ends up being something like // D123.diff // or the verbose - // D123.vs123.id123.whitespaceignore-all.diff + // D123.vs123.id123.highlightjs.diff // lame but nice to include these options $file_name = ltrim($request_uri->getPath(), '/').'.'; foreach ($request_uri->getQueryParamsAsPairList() as $pair) { diff --git a/src/applications/differential/parser/DifferentialChangesetParser.php b/src/applications/differential/parser/DifferentialChangesetParser.php index 22f31b3e9f..caa7463672 100644 --- a/src/applications/differential/parser/DifferentialChangesetParser.php +++ b/src/applications/differential/parser/DifferentialChangesetParser.php @@ -19,7 +19,6 @@ final class DifferentialChangesetParser extends Phobject { protected $specialAttributes = array(); protected $changeset; - protected $whitespaceMode = null; protected $renderCacheKey = null; @@ -163,7 +162,6 @@ final class DifferentialChangesetParser extends Phobject { } public function readParametersFromRequest(AphrontRequest $request) { - $this->setWhitespaceMode($request->getStr('whitespace')); $this->setCharacterEncoding($request->getStr('encoding')); $this->setHighlightAs($request->getStr('highlight')); @@ -191,20 +189,14 @@ final class DifferentialChangesetParser extends Phobject { return $this; } - const CACHE_VERSION = 12; + const CACHE_VERSION = 13; const CACHE_MAX_SIZE = 8e6; const ATTR_GENERATED = 'attr:generated'; const ATTR_DELETED = 'attr:deleted'; const ATTR_UNCHANGED = 'attr:unchanged'; - const ATTR_WHITELINES = 'attr:white'; const ATTR_MOVEAWAY = 'attr:moveaway'; - const WHITESPACE_SHOW_ALL = 'show-all'; - const WHITESPACE_IGNORE_TRAILING = 'ignore-trailing'; - const WHITESPACE_IGNORE_MOST = 'ignore-most'; - const WHITESPACE_IGNORE_ALL = 'ignore-all'; - public function setOldLines(array $lines) { $this->old = $lines; return $this; @@ -336,11 +328,6 @@ final class DifferentialChangesetParser extends Phobject { return $this; } - public function setWhitespaceMode($whitespace_mode) { - $this->whitespaceMode = $whitespace_mode; - return $this; - } - public function setRenderingReference($ref) { $this->renderingReference = $ref; return $this; @@ -574,10 +561,6 @@ final class DifferentialChangesetParser extends Phobject { return idx($this->specialAttributes, self::ATTR_UNCHANGED, false); } - public function isWhitespaceOnly() { - return idx($this->specialAttributes, self::ATTR_WHITELINES, false); - } - public function isMoveAway() { return idx($this->specialAttributes, self::ATTR_MOVEAWAY, false); } @@ -624,18 +607,8 @@ final class DifferentialChangesetParser extends Phobject { } private function tryCacheStuff() { - $whitespace_mode = $this->whitespaceMode; - switch ($whitespace_mode) { - case self::WHITESPACE_SHOW_ALL: - case self::WHITESPACE_IGNORE_TRAILING: - case self::WHITESPACE_IGNORE_ALL: - break; - default: - $whitespace_mode = self::WHITESPACE_IGNORE_MOST; - break; - } + $skip_cache = false; - $skip_cache = ($whitespace_mode != self::WHITESPACE_IGNORE_MOST); if ($this->disableCache) { $skip_cache = true; } @@ -648,8 +621,6 @@ final class DifferentialChangesetParser extends Phobject { $skip_cache = true; } - $this->whitespaceMode = $whitespace_mode; - $changeset = $this->changeset; if ($changeset->getFileType() != DifferentialChangeType::FILE_TEXT && @@ -668,71 +639,10 @@ final class DifferentialChangesetParser extends Phobject { } private function process() { - $whitespace_mode = $this->whitespaceMode; $changeset = $this->changeset; - $ignore_all = (($whitespace_mode == self::WHITESPACE_IGNORE_MOST) || - ($whitespace_mode == self::WHITESPACE_IGNORE_ALL)); - - $force_ignore = ($whitespace_mode == self::WHITESPACE_IGNORE_ALL); - - if (!$force_ignore) { - if ($ignore_all && $changeset->getWhitespaceMatters()) { - $ignore_all = false; - } - } - - // The "ignore all whitespace" algorithm depends on rediffing the - // files, and we currently need complete representations of both - // files to do anything reasonable. If we only have parts of the files, - // don't use the "ignore all" algorithm. - if ($ignore_all) { - $hunks = $changeset->getHunks(); - if (count($hunks) !== 1) { - $ignore_all = false; - } else { - $first_hunk = reset($hunks); - if ($first_hunk->getOldOffset() != 1 || - $first_hunk->getNewOffset() != 1) { - $ignore_all = false; - } - } - } - - if ($ignore_all) { - $old_file = $changeset->makeOldFile(); - $new_file = $changeset->makeNewFile(); - if ($old_file == $new_file) { - // If the old and new files are exactly identical, the synthetic - // diff below will give us nonsense and whitespace modes are - // irrelevant anyway. This occurs when you, e.g., copy a file onto - // itself in Subversion (see T271). - $ignore_all = false; - } - } - $hunk_parser = new DifferentialHunkParser(); - $hunk_parser->setWhitespaceMode($whitespace_mode); $hunk_parser->parseHunksForLineData($changeset->getHunks()); - - // Depending on the whitespace mode, we may need to compute a different - // set of changes than the set of changes in the hunk data (specifically, - // we might want to consider changed lines which have only whitespace - // changes as unchanged). - if ($ignore_all) { - $engine = new PhabricatorDifferenceEngine(); - $engine->setIgnoreWhitespace(true); - $no_whitespace_changeset = $engine->generateChangesetFromFileContent( - $old_file, - $new_file); - - $type_parser = new DifferentialHunkParser(); - $type_parser->parseHunksForLineData($no_whitespace_changeset->getHunks()); - - $hunk_parser->setOldLineTypeMap($type_parser->getOldLineTypeMap()); - $hunk_parser->setNewLineTypeMap($type_parser->getNewLineTypeMap()); - } - $hunk_parser->reparseHunksForSpecialAttributes(); $unchanged = false; @@ -753,7 +663,6 @@ final class DifferentialChangesetParser extends Phobject { $this->setSpecialAttributes(array( self::ATTR_UNCHANGED => $unchanged, self::ATTR_DELETED => $hunk_parser->getIsDeleted(), - self::ATTR_WHITELINES => !$hunk_parser->getHasTextChanges(), self::ATTR_MOVEAWAY => $moveaway, )); @@ -971,10 +880,6 @@ final class DifferentialChangesetParser extends Phobject { pht('The contents of this file were not changed.'), $type); } - } else if ($this->isWhitespaceOnly()) { - $shield = $renderer->renderShield( - pht('This file was changed only by adding or removing whitespace.'), - 'whitespace'); } else if ($this->isDeleted()) { $shield = $renderer->renderShield( pht('This file was completely deleted.')); diff --git a/src/applications/differential/parser/DifferentialHunkParser.php b/src/applications/differential/parser/DifferentialHunkParser.php index e7b9ce21a9..d89089a7e7 100644 --- a/src/applications/differential/parser/DifferentialHunkParser.php +++ b/src/applications/differential/parser/DifferentialHunkParser.php @@ -7,7 +7,6 @@ final class DifferentialHunkParser extends Phobject { private $intraLineDiffs; private $depthOnlyLines; private $visibleLinesMask; - private $whitespaceMode; /** * Get a map of lines on which hunks start, other than line 1. This @@ -125,21 +124,6 @@ final class DifferentialHunkParser extends Phobject { return $this->depthOnlyLines; } - public function setWhitespaceMode($white_space_mode) { - $this->whitespaceMode = $white_space_mode; - return $this; - } - - private function getWhitespaceMode() { - if ($this->whitespaceMode === null) { - throw new Exception( - pht( - 'You must %s before accessing this data.', - 'setWhitespaceMode')); - } - return $this->whitespaceMode; - } - public function getIsDeleted() { foreach ($this->getNewLines() as $line) { if ($line) { @@ -159,13 +143,6 @@ final class DifferentialHunkParser extends Phobject { return false; } - /** - * Returns true if the hunks change any text, not just whitespace. - */ - public function getHasTextChanges() { - return $this->getHasChanges('text'); - } - /** * Returns true if the hunks change anything, including whitespace. */ @@ -193,9 +170,6 @@ final class DifferentialHunkParser extends Phobject { } if ($o['type'] !== $n['type']) { - // The types are different, so either the underlying text is actually - // different or whatever whitespace rules we're using consider them - // different. return true; } @@ -278,63 +252,6 @@ final class DifferentialHunkParser extends Phobject { $this->setOldLines($rebuild_old); $this->setNewLines($rebuild_new); - $this->updateChangeTypesForWhitespaceMode(); - - return $this; - } - - private function updateChangeTypesForWhitespaceMode() { - $mode = $this->getWhitespaceMode(); - - $mode_show_all = DifferentialChangesetParser::WHITESPACE_SHOW_ALL; - if ($mode === $mode_show_all) { - // If we're showing all whitespace, we don't need to perform any updates. - return; - } - - $mode_trailing = DifferentialChangesetParser::WHITESPACE_IGNORE_TRAILING; - $is_trailing = ($mode === $mode_trailing); - - $new = $this->getNewLines(); - $old = $this->getOldLines(); - foreach ($old as $key => $o) { - $n = $new[$key]; - - if (!$o || !$n) { - continue; - } - - if ($is_trailing) { - // In "trailing" mode, we need to identify lines which are marked - // changed but differ only by trailing whitespace. We mark these lines - // unchanged. - if ($o['type'] != $n['type']) { - if (rtrim($o['text']) === rtrim($n['text'])) { - $old[$key]['type'] = null; - $new[$key]['type'] = null; - } - } - } else { - // In "ignore most" and "ignore all" modes, we need to identify lines - // which are marked unchanged but have internal whitespace changes. - // We want to ignore leading and trailing whitespace changes only, not - // internal whitespace changes (`diff` doesn't have a mode for this, so - // we have to fix it here). If the text is marked unchanged but the - // old and new text differs by internal space, mark the lines changed. - if ($o['type'] === null && $n['type'] === null) { - if ($o['text'] !== $n['text']) { - if (trim($o['text']) !== trim($n['text'])) { - $old[$key]['type'] = '-'; - $new[$key]['type'] = '+'; - } - } - } - } - } - - $this->setOldLines($old); - $this->setNewLines($new); - return $this; } diff --git a/src/applications/differential/parser/DifferentialLineAdjustmentMap.php b/src/applications/differential/parser/DifferentialLineAdjustmentMap.php index fde8f61f7d..e30f2ca866 100644 --- a/src/applications/differential/parser/DifferentialLineAdjustmentMap.php +++ b/src/applications/differential/parser/DifferentialLineAdjustmentMap.php @@ -359,7 +359,6 @@ final class DifferentialLineAdjustmentMap extends Phobject { } $changeset = id(new PhabricatorDifferenceEngine()) - ->setIgnoreWhitespace(true) ->generateChangesetFromFileContent($u_old, $v_old); $results[$u][$v] = self::newFromHunks( diff --git a/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php b/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php index 30acc71c88..2e760d99bb 100644 --- a/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php +++ b/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php @@ -367,7 +367,6 @@ abstract class DifferentialChangesetHTMLRenderer $reference = $this->getRenderingReference(); if ($force !== 'text' && - $force !== 'whitespace' && $force !== 'none' && $force !== 'default') { throw new Exception( @@ -388,10 +387,6 @@ abstract class DifferentialChangesetHTMLRenderer 'range' => $range, ); - if ($force == 'whitespace') { - $meta['whitespace'] = DifferentialChangesetParser::WHITESPACE_SHOW_ALL; - } - $content = array(); $content[] = $message; if ($force !== 'none') { diff --git a/src/applications/differential/render/DifferentialChangesetRenderer.php b/src/applications/differential/render/DifferentialChangesetRenderer.php index c5d033a4a9..866ae15ac9 100644 --- a/src/applications/differential/render/DifferentialChangesetRenderer.php +++ b/src/applications/differential/render/DifferentialChangesetRenderer.php @@ -406,9 +406,6 @@ abstract class DifferentialChangesetRenderer extends Phobject { * important (e.g., generated code). * - `"text"`: Force the text to be shown. This is probably only relevant * when a file is not changed. - * - `"whitespace"`: Force the text to be shown, and the diff to be - * rendered with all whitespace shown. This is probably only relevant - * when a file is changed only by altering whitespace. * - `"none"`: Don't show the link (e.g., text not available). * * @param string Message explaining why the diff is hidden. diff --git a/src/applications/differential/storage/DifferentialChangeset.php b/src/applications/differential/storage/DifferentialChangeset.php index 00af84bc4e..03400d7a16 100644 --- a/src/applications/differential/storage/DifferentialChangeset.php +++ b/src/applications/differential/storage/DifferentialChangeset.php @@ -249,17 +249,6 @@ final class DifferentialChangeset return $path; } - public function getWhitespaceMatters() { - $config = PhabricatorEnv::getEnvConfig('differential.whitespace-matters'); - foreach ($config as $regexp) { - if (preg_match($regexp, $this->getFilename())) { - return true; - } - } - - return false; - } - public function attachDiff(DifferentialDiff $diff) { $this->diff = $diff; return $this; diff --git a/src/applications/differential/view/DifferentialChangesetDetailView.php b/src/applications/differential/view/DifferentialChangesetDetailView.php index cb697c2e9d..211403c8bd 100644 --- a/src/applications/differential/view/DifferentialChangesetDetailView.php +++ b/src/applications/differential/view/DifferentialChangesetDetailView.php @@ -9,7 +9,6 @@ final class DifferentialChangesetDetailView extends AphrontView { private $id; private $vsChangesetID; private $renderURI; - private $whitespace; private $renderingRef; private $autoload; private $loaded; @@ -42,15 +41,6 @@ final class DifferentialChangesetDetailView extends AphrontView { return $this->renderingRef; } - public function setWhitespace($whitespace) { - $this->whitespace = $whitespace; - return $this; - } - - public function getWhitespace() { - return $this->whitespace; - } - public function setRenderURI($render_uri) { $this->renderURI = $render_uri; return $this; @@ -196,7 +186,6 @@ final class DifferentialChangesetDetailView extends AphrontView { 'left' => $left_id, 'right' => $right_id, 'renderURI' => $this->getRenderURI(), - 'whitespace' => $this->getWhitespace(), 'highlight' => null, 'renderer' => $this->getRenderer(), 'ref' => $this->getRenderingRef(), diff --git a/src/applications/differential/view/DifferentialChangesetListView.php b/src/applications/differential/view/DifferentialChangesetListView.php index 367991497c..67568005fa 100644 --- a/src/applications/differential/view/DifferentialChangesetListView.php +++ b/src/applications/differential/view/DifferentialChangesetListView.php @@ -7,7 +7,6 @@ final class DifferentialChangesetListView extends AphrontView { private $references = array(); private $inlineURI; private $renderURI = '/differential/changeset/'; - private $whitespace; private $background; private $header; private $isStandalone; @@ -100,11 +99,6 @@ final class DifferentialChangesetListView extends AphrontView { return $this; } - public function setWhitespace($whitespace) { - $this->whitespace = $whitespace; - return $this; - } - public function setVsMap(array $vs_map) { $this->vsMap = $vs_map; return $this; @@ -180,7 +174,6 @@ final class DifferentialChangesetListView extends AphrontView { $detail->setRenderingRef($ref); $detail->setRenderURI($this->renderURI); - $detail->setWhitespace($this->whitespace); $detail->setRenderer($renderer); if ($this->getParser()) { @@ -352,8 +345,7 @@ final class DifferentialChangesetListView extends AphrontView { $meta = array(); $qparams = array( - 'ref' => $ref, - 'whitespace' => $this->whitespace, + 'ref' => $ref, ); if ($this->standaloneURI) { diff --git a/src/applications/differential/view/DifferentialRevisionUpdateHistoryView.php b/src/applications/differential/view/DifferentialRevisionUpdateHistoryView.php index a77b320d82..07ca983bc0 100644 --- a/src/applications/differential/view/DifferentialRevisionUpdateHistoryView.php +++ b/src/applications/differential/view/DifferentialRevisionUpdateHistoryView.php @@ -5,7 +5,6 @@ final class DifferentialRevisionUpdateHistoryView extends AphrontView { private $diffs = array(); private $selectedVersusDiffID; private $selectedDiffID; - private $selectedWhitespace; private $commitsForLinks = array(); private $unitStatus = array(); @@ -25,11 +24,6 @@ final class DifferentialRevisionUpdateHistoryView extends AphrontView { return $this; } - public function setSelectedWhitespace($whitespace) { - $this->selectedWhitespace = $whitespace; - return $this; - } - public function setCommitsForLinks(array $commits) { assert_instances_of($commits, 'PhabricatorRepositoryCommit'); $this->commitsForLinks = $commits; @@ -224,28 +218,6 @@ final class DifferentialRevisionUpdateHistoryView extends AphrontView { 'radios' => $radios, )); - $options = array( - DifferentialChangesetParser::WHITESPACE_IGNORE_ALL => pht('Ignore All'), - DifferentialChangesetParser::WHITESPACE_IGNORE_MOST => pht('Ignore Most'), - DifferentialChangesetParser::WHITESPACE_IGNORE_TRAILING => - pht('Ignore Trailing'), - DifferentialChangesetParser::WHITESPACE_SHOW_ALL => pht('Show All'), - ); - - foreach ($options as $value => $label) { - $options[$value] = phutil_tag( - 'option', - array( - 'value' => $value, - 'selected' => ($value == $this->selectedWhitespace) - ? 'selected' - : null, - ), - $label); - } - $select = phutil_tag('select', array('name' => 'whitespace'), $options); - - $table = id(new AphrontTableView($rows)); $table->setHeaders( array( @@ -291,13 +263,6 @@ final class DifferentialRevisionUpdateHistoryView extends AphrontView { 'class' => 'differential-update-history-footer', ), array( - phutil_tag( - 'label', - array(), - array( - pht('Whitespace Changes:'), - $select, - )), phutil_tag( 'button', array(), diff --git a/src/applications/diffusion/controller/DiffusionChangeController.php b/src/applications/diffusion/controller/DiffusionChangeController.php index 90258134c4..0120c978d0 100644 --- a/src/applications/diffusion/controller/DiffusionChangeController.php +++ b/src/applications/diffusion/controller/DiffusionChangeController.php @@ -64,9 +64,6 @@ final class DiffusionChangeController extends DiffusionController { $changeset_view->setRawFileURIs($left_uri, $right_uri); $changeset_view->setRenderURI($repository->getPathURI('diff/')); - - $changeset_view->setWhitespace( - DifferentialChangesetParser::WHITESPACE_SHOW_ALL); $changeset_view->setUser($viewer); $changeset_view->setHeader($changeset_header); diff --git a/src/applications/diffusion/controller/DiffusionDiffController.php b/src/applications/diffusion/controller/DiffusionDiffController.php index 86409c6faa..a0111d001f 100644 --- a/src/applications/diffusion/controller/DiffusionDiffController.php +++ b/src/applications/diffusion/controller/DiffusionDiffController.php @@ -88,9 +88,6 @@ final class DiffusionDiffController extends DiffusionController { ($viewer->getPHID() == $commit->getAuthorPHID())); $parser->setObjectOwnerPHID($commit->getAuthorPHID()); - $parser->setWhitespaceMode( - DifferentialChangesetParser::WHITESPACE_SHOW_ALL); - $inlines = PhabricatorAuditInlineComment::loadDraftAndPublishedComments( $viewer, $commit->getPHID(), diff --git a/src/docs/user/userguide/differential_faq.diviner b/src/docs/user/userguide/differential_faq.diviner index aea49c4ce6..0df1f7b1c9 100644 --- a/src/docs/user/userguide/differential_faq.diviner +++ b/src/docs/user/userguide/differential_faq.diviner @@ -51,22 +51,6 @@ You need to install and configure **Pygments** to highlight anything else than PHP. See the `pygments.enabled` configuration setting. -= What do the whitespace options mean? = - -Most of these are pretty straightforward, but "Ignore Most" is not: - - - **Show All**: Show all whitespace. - - **Ignore Trailing**: Ignore changes which only affect trailing whitespace. - - **Ignore Most**: Ignore changes which only affect leading or trailing - whitespace (but not whitespace changes between non-whitespace characters) - in files which are not marked as having significant whitespace. - In those files, show whitespace changes. By default, Python (.py) and - Haskell (.lhs, .hs) are marked as having significant whitespace, but this - can be changed in the `differential.whitespace-matters` configuration - setting. - - **Ignore All**: Ignore all whitespace changes in all files. - - = What do the very light green and red backgrounds mean? = Differential uses these colors to mark changes coming from rebase: they are diff --git a/src/infrastructure/diff/PhabricatorDifferenceEngine.php b/src/infrastructure/diff/PhabricatorDifferenceEngine.php index 3b4eb55473..90f1d9b10e 100644 --- a/src/infrastructure/diff/PhabricatorDifferenceEngine.php +++ b/src/infrastructure/diff/PhabricatorDifferenceEngine.php @@ -10,7 +10,6 @@ final class PhabricatorDifferenceEngine extends Phobject { - private $ignoreWhitespace; private $oldName; private $newName; @@ -18,19 +17,6 @@ final class PhabricatorDifferenceEngine extends Phobject { /* -( Configuring the Engine )--------------------------------------------- */ - /** - * If true, ignore whitespace when computing differences. - * - * @param bool Ignore whitespace? - * @return this - * @task config - */ - public function setIgnoreWhitespace($ignore_whitespace) { - $this->ignoreWhitespace = $ignore_whitespace; - return $this; - } - - /** * Set the name to identify the old file with. Primarily cosmetic. * @@ -73,9 +59,6 @@ final class PhabricatorDifferenceEngine extends Phobject { public function generateRawDiffFromFileContent($old, $new) { $options = array(); - if ($this->ignoreWhitespace) { - $options[] = '-bw'; - } // Generate diffs with full context. $options[] = '-U65535'; @@ -100,12 +83,10 @@ final class PhabricatorDifferenceEngine extends Phobject { $new_tmp); if (!$err) { - // This indicates that the two files are the same (or, possibly, the - // same modulo whitespace differences, which is why we can't do this - // check trivially before running `diff`). Build a synthetic, changeless - // diff so that we can still render the raw, unchanged file instead of - // being forced to just say "this file didn't change" since we don't have - // the content. + // This indicates that the two files are the same. Build a synthetic, + // changeless diff so that we can still render the raw, unchanged file + // instead of being forced to just say "this file didn't change" since we + // don't have the content. $entire_file = explode("\n", $old); foreach ($entire_file as $k => $line) { @@ -123,26 +104,6 @@ final class PhabricatorDifferenceEngine extends Phobject { "+++ {$new_name}\n". "@@ -1,{$len} +1,{$len} @@\n". $entire_file."\n"; - } else { - if ($this->ignoreWhitespace) { - - // Under "-bw", `diff` is inconsistent about emitting "\ No newline - // at end of file". For instance, a long file with a change in the - // middle will emit a contextless "\ No newline..." at the end if a - // newline is removed, but not if one is added. A file with a change - // at the end will emit the "old" "\ No newline..." block only, even - // if the newline was not removed. Since we're ostensibly ignoring - // whitespace changes, just drop these lines if they appear anywhere - // in the diff. - - $lines = explode("\n", $diff); - foreach ($lines as $key => $line) { - if (isset($line[0]) && $line[0] == '\\') { - unset($lines[$key]); - } - } - $diff = implode("\n", $lines); - } } return $diff; diff --git a/webroot/rsrc/js/application/diff/DiffChangeset.js b/webroot/rsrc/js/application/diff/DiffChangeset.js index 24d734573d..754f3b16e4 100644 --- a/webroot/rsrc/js/application/diff/DiffChangeset.js +++ b/webroot/rsrc/js/application/diff/DiffChangeset.js @@ -22,7 +22,6 @@ JX.install('DiffChangeset', { this._renderURI = data.renderURI; this._ref = data.ref; - this._whitespace = data.whitespace; this._renderer = data.renderer; this._highlight = data.highlight; this._encoding = data.encoding; @@ -46,7 +45,6 @@ JX.install('DiffChangeset', { _renderURI: null, _ref: null, - _whitespace: null, _renderer: null, _highlight: null, _encoding: null, @@ -310,7 +308,6 @@ JX.install('DiffChangeset', { _getViewParameters: function() { return { ref: this._ref, - whitespace: this._whitespace || '', renderer: this.getRenderer() || '', highlight: this._highlight || '', encoding: this._encoding || '' From 3f8eccdaec8f85933bfea6ce5348bde25ced0489 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 16 Feb 2019 07:21:31 -0800 Subject: [PATCH 085/245] Put some whitespace behaviors back, but only for "diff alignment", not display Summary: Depends on D20185. Ref T13161. Fixes T6791. See some discusison in T13161. I want to move to a world where: - whitespace changes are always shown, so users writing YAML and Python are happy without adjusting settings; - the visual impact of indentation-only whitespace changes is significanlty reduced, so indentation changes are easy to read and users writing Javascript or other flavors of Javascript are happy. D20181 needs a little more work, but generally tackles these visual changes and lets us always show whitespace changes, but show them in a very low-impact way when they're relatively unimportant. However, a second aspect to this is how the diff is "aligned". If this file: ``` A ``` ..is changed to this file: ``` X A Y Z ``` ...diff tools will generally produce this diff: ``` + X A + Y + Z ``` This is good, and easy to read, and what humans expect, and it will "align" in two-up like this: ``` 1 X 1 A 2 A 3 Y 4 Z ``` However, if the new file looks like this instead: ``` X A' Y Z ``` ...we get a diff like this: ``` - A + X + A' + Y + Z ``` This one aligns like this: ``` 1 A 1 X 2 A' 3 Y 4 Z ``` This is correct if `A` and `A'` are totally different lines. However, if `A'` is pretty much the same as `A` and it just had a whitespace change, human viewers would prefer this alignment: ``` 1 X 1 A 2 A' 3 Y 4 Z ``` Note that `A` and `A'` are different, but we've aligned them on the same line. `diff`, `git diff`, etc., won't do this automatically, and a `.diff` doesn't have a way to say "these lines are more or less the same even though they're different", although some other visual diff tools will do this. Although `diff` can't do this for us, we can do it ourselves, and already have the code to do it, because we already nearly did this in the changes removed in D20185: in "Ignore All" or "Ignore Most" mode, we pretty much did this already. This mostly just restores a bit of the code from D20185, with some adjustments/simplifications. Here's how it works: - Rebuild the text of the old and new files from the diff we got out of `arc`, `git diff`, etc. - Normalize the files (for example, by removing whitespace from each line). - Diff the normalized files to produce a second diff. - Parse that diff. - Take the "alignment" from the normalized diff (whitespace removed) and the actual text from the original diff (whitespace preserved) to build a new diff with the correct text, but also better diff alignment. Originally, we normalized with `diff -bw`. I've replaced that with `preg_replace()` here mostly just so that we have more control over things. I believe the two behaviors are pretty much identical, but this way lets us see more of the pipeline and possibly add more behaviors in the future to improve diff quality (e.g., normalize case? normalize text encoding?). Test Plan: {F6217133} (Also, fix a unit test.) Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13161, T6791 Differential Revision: https://secure.phabricator.com/D20187 --- .../DifferentialParseRenderTestCase.php | 4 ++ .../__tests__/data/generated.diff | 2 +- .../parser/DifferentialChangesetParser.php | 50 +++++++++++++++ .../parser/DifferentialHunkParser.php | 63 +++++++++++++++++++ .../diff/PhabricatorDifferenceEngine.php | 40 ++++++++++++ 5 files changed, 158 insertions(+), 1 deletion(-) diff --git a/src/applications/differential/__tests__/DifferentialParseRenderTestCase.php b/src/applications/differential/__tests__/DifferentialParseRenderTestCase.php index 259bb671de..ce93bbe65a 100644 --- a/src/applications/differential/__tests__/DifferentialParseRenderTestCase.php +++ b/src/applications/differential/__tests__/DifferentialParseRenderTestCase.php @@ -14,6 +14,10 @@ final class DifferentialParseRenderTestCase extends PhabricatorTestCase { } $data = Filesystem::readFile($dir.$file); + // Strip trailing "~" characters from inputs so they may contain + // trailing whitespace. + $data = preg_replace('/~$/m', '', $data); + $opt_file = $dir.$file.'.options'; if (Filesystem::pathExists($opt_file)) { $options = Filesystem::readFile($opt_file); diff --git a/src/applications/differential/__tests__/data/generated.diff b/src/applications/differential/__tests__/data/generated.diff index 7846c9a494..c130993cf7 100644 --- a/src/applications/differential/__tests__/data/generated.diff +++ b/src/applications/differential/__tests__/data/generated.diff @@ -4,7 +4,7 @@ index 5dcff7f..eff82ef 100644 +++ b/GENERATED @@ -1,4 +1,4 @@ @generated - + ~ -This is a generated file. +This is a generated file, full of generated code. diff --git a/src/applications/differential/parser/DifferentialChangesetParser.php b/src/applications/differential/parser/DifferentialChangesetParser.php index caa7463672..f0f952328a 100644 --- a/src/applications/differential/parser/DifferentialChangesetParser.php +++ b/src/applications/differential/parser/DifferentialChangesetParser.php @@ -643,6 +643,9 @@ final class DifferentialChangesetParser extends Phobject { $hunk_parser = new DifferentialHunkParser(); $hunk_parser->parseHunksForLineData($changeset->getHunks()); + + $this->realignDiff($changeset, $hunk_parser); + $hunk_parser->reparseHunksForSpecialAttributes(); $unchanged = false; @@ -1366,4 +1369,51 @@ final class DifferentialChangesetParser extends Phobject { return $key; } + private function realignDiff( + DifferentialChangeset $changeset, + DifferentialHunkParser $hunk_parser) { + // Normalizing and realigning the diff depends on rediffing the files, and + // we currently need complete representations of both files to do anything + // reasonable. If we only have parts of the files, skip realignment. + + // We have more than one hunk, so we're definitely missing part of the file. + $hunks = $changeset->getHunks(); + if (count($hunks) !== 1) { + return null; + } + + // The first hunk doesn't start at the beginning of the file, so we're + // missing some context. + $first_hunk = head($hunks); + if ($first_hunk->getOldOffset() != 1 || $first_hunk->getNewOffset() != 1) { + return null; + } + + $old_file = $changeset->makeOldFile(); + $new_file = $changeset->makeNewFile(); + if ($old_file === $new_file) { + // If the old and new files are exactly identical, the synthetic + // diff below will give us nonsense and whitespace modes are + // irrelevant anyway. This occurs when you, e.g., copy a file onto + // itself in Subversion (see T271). + return null; + } + + + $engine = id(new PhabricatorDifferenceEngine()) + ->setNormalize(true); + + $normalized_changeset = $engine->generateChangesetFromFileContent( + $old_file, + $new_file); + + $type_parser = new DifferentialHunkParser(); + $type_parser->parseHunksForLineData($normalized_changeset->getHunks()); + + $hunk_parser->setNormalized(true); + $hunk_parser->setOldLineTypeMap($type_parser->getOldLineTypeMap()); + $hunk_parser->setNewLineTypeMap($type_parser->getNewLineTypeMap()); + } + + } diff --git a/src/applications/differential/parser/DifferentialHunkParser.php b/src/applications/differential/parser/DifferentialHunkParser.php index d89089a7e7..d59358bc82 100644 --- a/src/applications/differential/parser/DifferentialHunkParser.php +++ b/src/applications/differential/parser/DifferentialHunkParser.php @@ -7,6 +7,7 @@ final class DifferentialHunkParser extends Phobject { private $intraLineDiffs; private $depthOnlyLines; private $visibleLinesMask; + private $normalized; /** * Get a map of lines on which hunks start, other than line 1. This @@ -124,6 +125,15 @@ final class DifferentialHunkParser extends Phobject { return $this->depthOnlyLines; } + public function setNormalized($normalized) { + $this->normalized = $normalized; + return $this; + } + + public function getNormalized() { + return $this->normalized; + } + public function getIsDeleted() { foreach ($this->getNewLines() as $line) { if ($line) { @@ -252,6 +262,8 @@ final class DifferentialHunkParser extends Phobject { $this->setOldLines($rebuild_old); $this->setNewLines($rebuild_new); + $this->updateChangeTypesForNormalization(); + return $this; } @@ -753,4 +765,55 @@ final class DifferentialHunkParser extends Phobject { return $character_depth; } + private function updateChangeTypesForNormalization() { + if (!$this->getNormalized()) { + return; + } + + // If we've parsed based on a normalized diff alignment, we may currently + // believe some lines are unchanged when they have actually changed. This + // happens when: + // + // - a line changes; + // - the change is a kind of change we normalize away when aligning the + // diff, like an indentation change; + // - we normalize the change away to align the diff; and so + // - the old and new copies of the line are now aligned in the new + // normalized diff. + // + // Then we end up with an alignment where the two lines that differ only + // in some some trivial way are aligned. This is great, and exactly what + // we're trying to accomplish by doing all this alignment stuff in the + // first place. + // + // However, in this case the correctly-aligned lines will be incorrectly + // marked as unchanged because the diff alorithm was fed normalized copies + // of the lines, and these copies truly weren't any different. + // + // When lines are aligned and marked identical, but they're not actually + // identcal, we now mark them as changed. The rest of the processing will + // figure out how to render them appropritely. + + $new = $this->getNewLines(); + $old = $this->getOldLines(); + foreach ($old as $key => $o) { + $n = $new[$key]; + + if (!$o || !$n) { + continue; + } + + if ($o['type'] === null && $n['type'] === null) { + if ($o['text'] !== $n['text']) { + $old[$key]['type'] = '-'; + $new[$key]['type'] = '+'; + } + } + } + + $this->setOldLines($old); + $this->setNewLines($new); + } + + } diff --git a/src/infrastructure/diff/PhabricatorDifferenceEngine.php b/src/infrastructure/diff/PhabricatorDifferenceEngine.php index 90f1d9b10e..84e88ceaaa 100644 --- a/src/infrastructure/diff/PhabricatorDifferenceEngine.php +++ b/src/infrastructure/diff/PhabricatorDifferenceEngine.php @@ -12,6 +12,7 @@ final class PhabricatorDifferenceEngine extends Phobject { private $oldName; private $newName; + private $normalize; /* -( Configuring the Engine )--------------------------------------------- */ @@ -43,6 +44,16 @@ final class PhabricatorDifferenceEngine extends Phobject { } + public function setNormalize($normalize) { + $this->normalize = $normalize; + return $this; + } + + public function getNormalize() { + return $this->normalize; + } + + /* -( Generating Diffs )--------------------------------------------------- */ @@ -71,6 +82,12 @@ final class PhabricatorDifferenceEngine extends Phobject { $options[] = '-L'; $options[] = $new_name; + $normalize = $this->getNormalize(); + if ($normalize) { + $old = $this->normalizeFile($old); + $new = $this->normalizeFile($new); + } + $old_tmp = new TempFile(); $new_tmp = new TempFile(); @@ -129,4 +146,27 @@ final class PhabricatorDifferenceEngine extends Phobject { return head($diff->getChangesets()); } + private function normalizeFile($corpus) { + // We can freely apply any other transformations we want to here: we have + // no constraints on what we need to preserve. If we normalize every line + // to "cat", the diff will still work, the alignment of the "-" / "+" + // lines will just be very hard to read. + + // In general, we'll make the diff better if we normalize two lines that + // humans think are the same. + + // We'll make the diff worse if we normalize two lines that humans think + // are different. + + + // Strip all whitespace present anywhere in the diff, since humans never + // consider whitespace changes to alter the line into a "different line" + // even when they're semantic (e.g., in a string constant). This covers + // indentation changes, trailing whitepspace, and formatting changes + // like "+/-". + $corpus = preg_replace('/[ \t]/', '', $corpus); + + return $corpus; + } + } From 98fe8fae4a9001c17af2e625c64760da55618a49 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 16 Feb 2019 09:50:20 -0800 Subject: [PATCH 086/245] Use `` instead of `3` for line numbers Summary: Ref T13161. Ref T12822. See PHI870. Long ago, the web was simple. You could leave your doors unlocked, you knew all your neighbors, crime hadn't been invented yet, and `3` was a perfectly fine way to render a line number cell containing the number "3". But times have changed! - In PHI870, this isn't good for screenreaders. We can't do much about this, so switch to ``. - In D19349 / T13105 and elsewhere, this `::after { content: attr(data-n); }` approach seems like the least bad general-purpose approach for preventing line numbers from being copied. Although Differential needs even more magic beyond this in the two-up view, this is likely good enough for the one-up view, and is consistent with other views (paste, harbormaster logs, general source display) where this technique is sufficient on its own. The chance this breaks //something// is pretty much 100%, but we've got a week to figure out what it breaks. I couldn't find any issues immediately. Test Plan: - Created, edited, deleted inlines in 1-up and 2-up views. - Replied, keyboard-navigated, keyboard-replied, drag-selected, poked and prodded everything. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13161, T12822 Differential Revision: https://secure.phabricator.com/D20188 --- resources/celerity/map.php | 26 +++++----- .../DifferentialChangesetOneUpRenderer.php | 36 +++++++++----- .../DifferentialChangesetTwoUpRenderer.php | 20 +++++++- .../PHUIDiffOneUpInlineCommentRowScaffold.php | 4 +- .../PHUIDiffTwoUpInlineCommentRowScaffold.php | 4 +- .../differential/changeset-view.css | 49 +++++++++++-------- .../js/application/diff/DiffChangesetList.js | 12 ++--- 7 files changed, 92 insertions(+), 59 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 65eed7916a..441f7108d1 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -11,8 +11,8 @@ return array( 'conpherence.pkg.js' => '020aebcf', 'core.pkg.css' => '261ee8cf', 'core.pkg.js' => '5ace8a1e', - 'differential.pkg.css' => 'c3f15714', - 'differential.pkg.js' => 'be031567', + 'differential.pkg.css' => '801c5653', + 'differential.pkg.js' => '1f211736', 'diffusion.pkg.css' => '42c75c37', 'diffusion.pkg.js' => '91192d85', 'maniphest.pkg.css' => '35995d6d', @@ -61,7 +61,7 @@ return array( 'rsrc/css/application/dashboard/dashboard.css' => '4267d6c6', 'rsrc/css/application/diff/inline-comment-summary.css' => '81eb368d', 'rsrc/css/application/differential/add-comment.css' => '7e5900d9', - 'rsrc/css/application/differential/changeset-view.css' => '783a9206', + 'rsrc/css/application/differential/changeset-view.css' => '8a997ed9', 'rsrc/css/application/differential/core.css' => 'bdb93065', 'rsrc/css/application/differential/phui-inline-comment.css' => '48acce5b', 'rsrc/css/application/differential/revision-comment.css' => '7dbc8d1d', @@ -375,7 +375,7 @@ return array( 'rsrc/js/application/dashboard/behavior-dashboard-query-panel-select.js' => '1e413dc9', 'rsrc/js/application/dashboard/behavior-dashboard-tab-panel.js' => '9b1cbd76', 'rsrc/js/application/diff/DiffChangeset.js' => 'd0a85a85', - 'rsrc/js/application/diff/DiffChangesetList.js' => 'b91204e9', + 'rsrc/js/application/diff/DiffChangesetList.js' => '26fb79ba', 'rsrc/js/application/diff/DiffInline.js' => 'a4a14a94', 'rsrc/js/application/diff/behavior-preview-link.js' => 'f51e9c17', 'rsrc/js/application/differential/behavior-diff-radios.js' => '925fe8cd', @@ -541,7 +541,7 @@ return array( 'conpherence-thread-manager' => 'aec8e38c', 'conpherence-transaction-css' => '3a3f5e7e', 'd3' => 'd67475f5', - 'differential-changeset-view-css' => '783a9206', + 'differential-changeset-view-css' => '8a997ed9', 'differential-core-view-css' => 'bdb93065', 'differential-revision-add-comment-css' => '7e5900d9', 'differential-revision-comment-css' => '7dbc8d1d', @@ -754,7 +754,7 @@ return array( 'phabricator-darkmessage' => '26cd4b73', 'phabricator-dashboard-css' => '4267d6c6', 'phabricator-diff-changeset' => 'd0a85a85', - 'phabricator-diff-changeset-list' => 'b91204e9', + 'phabricator-diff-changeset-list' => '26fb79ba', 'phabricator-diff-inline' => 'a4a14a94', 'phabricator-drag-and-drop-file-upload' => '4370900d', 'phabricator-draggable-list' => '3c6bd549', @@ -1087,6 +1087,10 @@ return array( 'javelin-json', 'phabricator-draggable-list', ), + '26fb79ba' => array( + 'javelin-install', + 'phuix-button-view', + ), '27daef73' => array( 'multirow-row-manager', 'javelin-install', @@ -1513,9 +1517,6 @@ return array( 'javelin-uri', 'javelin-request', ), - '783a9206' => array( - 'phui-inline-comment-view-css', - ), '78bc5d94' => array( 'javelin-behavior', 'javelin-uri', @@ -1586,6 +1587,9 @@ return array( '8a16f91b' => array( 'syntax-default-css', ), + '8a997ed9' => array( + 'phui-inline-comment-view-css', + ), '8ac32fd9' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1895,10 +1899,6 @@ return array( 'javelin-uri', 'phabricator-notification', ), - 'b91204e9' => array( - 'javelin-install', - 'phuix-button-view', - ), 'bd546a49' => array( 'phui-workcard-view-css', ), diff --git a/src/applications/differential/render/DifferentialChangesetOneUpRenderer.php b/src/applications/differential/render/DifferentialChangesetOneUpRenderer.php index 90c3977907..289b802485 100644 --- a/src/applications/differential/render/DifferentialChangesetOneUpRenderer.php +++ b/src/applications/differential/render/DifferentialChangesetOneUpRenderer.php @@ -92,19 +92,23 @@ final class DifferentialChangesetOneUpRenderer $line = $p['line']; $cells[] = phutil_tag( - 'th', + 'td', array( 'id' => $left_id, - 'class' => $class, - ), - $line); + 'class' => $class.' n', + 'data-n' => $line, + )); $render = $p['render']; if ($aural !== null) { $render = array($aural, $render); } - $cells[] = phutil_tag('th', array('class' => $class)); + $cells[] = phutil_tag( + 'td', + array( + 'class' => $class.' n', + )); $cells[] = $no_copy; $cells[] = phutil_tag('td', array('class' => $class), $render); $cells[] = $no_coverage; @@ -115,7 +119,11 @@ final class DifferentialChangesetOneUpRenderer } else { $class = 'right new'; } - $cells[] = phutil_tag('th', array('class' => $class)); + $cells[] = phutil_tag( + 'td', + array( + 'class' => $class.' n', + )); $aural = $aural_plus; } else { $class = 'right'; @@ -127,7 +135,13 @@ final class DifferentialChangesetOneUpRenderer $oline = $p['oline']; - $cells[] = phutil_tag('th', array('id' => $left_id), $oline); + $cells[] = phutil_tag( + 'td', + array( + 'id' => $left_id, + 'class' => 'n', + 'data-n' => $oline, + )); $aural = null; } @@ -144,12 +158,12 @@ final class DifferentialChangesetOneUpRenderer $line = $p['line']; $cells[] = phutil_tag( - 'th', + 'td', array( 'id' => $right_id, - 'class' => $class, - ), - $line); + 'class' => $class.' n', + 'data-n' => $line, + )); $render = $p['render']; if ($aural !== null) { diff --git a/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php b/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php index b4936201e0..baf08aac77 100644 --- a/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php +++ b/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php @@ -306,10 +306,26 @@ final class DifferentialChangesetTwoUpRenderer // clipboard. See the 'phabricator-oncopy' behavior. $zero_space = "\xE2\x80\x8B"; + $old_number = phutil_tag( + 'td', + array( + 'id' => $o_id, + 'class' => $o_classes.' n', + 'data-n' => $o_num, + )); + + $new_number = phutil_tag( + 'td', + array( + 'id' => $n_id, + 'class' => $n_classes.' n', + 'data-n' => $n_num, + )); + $html[] = phutil_tag('tr', array(), array( - phutil_tag('th', array('id' => $o_id, 'class' => $o_classes), $o_num), + $old_number, phutil_tag('td', array('class' => $o_classes), $o_text), - phutil_tag('th', array('id' => $n_id, 'class' => $n_classes), $n_num), + $new_number, $n_copy, phutil_tag( 'td', diff --git a/src/infrastructure/diff/view/PHUIDiffOneUpInlineCommentRowScaffold.php b/src/infrastructure/diff/view/PHUIDiffOneUpInlineCommentRowScaffold.php index 53c2255dc8..1f8e05bc27 100644 --- a/src/infrastructure/diff/view/PHUIDiffOneUpInlineCommentRowScaffold.php +++ b/src/infrastructure/diff/view/PHUIDiffOneUpInlineCommentRowScaffold.php @@ -31,8 +31,8 @@ final class PHUIDiffOneUpInlineCommentRowScaffold } $cells = array( - phutil_tag('th', array(), $left_hidden), - phutil_tag('th', array(), $right_hidden), + phutil_tag('td', array('class' => 'n'), $left_hidden), + phutil_tag('td', array('class' => 'n'), $right_hidden), phutil_tag('td', $attrs, $inline), ); diff --git a/src/infrastructure/diff/view/PHUIDiffTwoUpInlineCommentRowScaffold.php b/src/infrastructure/diff/view/PHUIDiffTwoUpInlineCommentRowScaffold.php index 81b0edaf49..769ad84d1f 100644 --- a/src/infrastructure/diff/view/PHUIDiffTwoUpInlineCommentRowScaffold.php +++ b/src/infrastructure/diff/view/PHUIDiffTwoUpInlineCommentRowScaffold.php @@ -71,9 +71,9 @@ final class PHUIDiffTwoUpInlineCommentRowScaffold ); $cells = array( - phutil_tag('th', array(), $left_hidden), + phutil_tag('td', array('class' => 'n'), $left_hidden), phutil_tag('td', $left_attrs, $left_side), - phutil_tag('th', array(), $right_hidden), + phutil_tag('td', array('class' => 'n'), $right_hidden), phutil_tag('td', $right_attrs, $right_side), ); diff --git a/webroot/rsrc/css/application/differential/changeset-view.css b/webroot/rsrc/css/application/differential/changeset-view.css index 2cfa753a42..a49dd4905b 100644 --- a/webroot/rsrc/css/application/differential/changeset-view.css +++ b/webroot/rsrc/css/application/differential/changeset-view.css @@ -72,23 +72,6 @@ width: 0; } -.differential-diff th { - text-align: right; - padding: 1px 6px 1px 0; - vertical-align: top; - background: {$lightbluebackground}; - color: {$bluetext}; - cursor: pointer; - border-right: 1px solid {$thinblueborder}; - overflow: hidden; - - -moz-user-select: -moz-none; - -khtml-user-select: none; - -webkit-user-select: none; - -ms-user-select: none; - user-select: none; -} - .prose-diff { padding: 12px 0; white-space: pre-wrap; @@ -182,6 +165,34 @@ background: #dddddd; } +.differential-diff .inline > td { + padding: 0; +} + +/* Specify line number behaviors after other behaviors because line numbers +should always have a boring grey background. */ + +.differential-diff td.n { + text-align: right; + padding: 1px 6px 1px 0; + vertical-align: top; + background: {$lightbluebackground}; + color: {$bluetext}; + cursor: pointer; + border-right: 1px solid {$thinblueborder}; + overflow: hidden; + + -moz-user-select: -moz-none; + -khtml-user-select: none; + -webkit-user-select: none; + -ms-user-select: none; + user-select: none; +} + +.differential-diff td.n::before { + content: attr(data-n); +} + .differential-diff td.cov { padding: 0; } @@ -316,10 +327,6 @@ td.cov-I { pointer-events: none; } -.differential-diff .inline > td { - padding: 0; -} - .differential-loading { border-top: 1px solid {$gentle.highlight.border}; border-bottom: 1px solid {$gentle.highlight.border}; diff --git a/webroot/rsrc/js/application/diff/DiffChangesetList.js b/webroot/rsrc/js/application/diff/DiffChangesetList.js index 5ba43a7e6d..8a1306967a 100644 --- a/webroot/rsrc/js/application/diff/DiffChangesetList.js +++ b/webroot/rsrc/js/application/diff/DiffChangesetList.js @@ -70,13 +70,13 @@ JX.install('DiffChangesetList', { var onrangedown = JX.bind(this, this._ifawake, this._onrangedown); JX.Stratcom.listen( 'mousedown', - ['differential-changeset', 'tag:th'], + ['differential-changeset', 'tag:td'], onrangedown); var onrangemove = JX.bind(this, this._ifawake, this._onrangemove); JX.Stratcom.listen( ['mouseover', 'mouseout'], - ['differential-changeset', 'tag:th'], + ['differential-changeset', 'tag:td'], onrangemove); var onrangeup = JX.bind(this, this._ifawake, this._onrangeup); @@ -360,7 +360,7 @@ JX.install('DiffChangesetList', { while (row) { var header = row.firstChild; while (header) { - if (JX.DOM.isType(header, 'th')) { + if (this.getLineNumberFromHeader(header)) { if (header.className.indexOf('old') !== -1) { old_list.push(header); } else if (header.className.indexOf('new') !== -1) { @@ -1247,11 +1247,7 @@ JX.install('DiffChangesetList', { }, getLineNumberFromHeader: function(th) { - try { - return parseInt(th.id.match(/^C\d+[ON]L(\d+)$/)[1], 10); - } catch (x) { - return null; - } + return parseInt(th.getAttribute('data-n')); }, getDisplaySideFromHeader: function(th) { From d4b96bcf6b017678cbf5f966ba7a9ab9a458969f Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 17 Feb 2019 04:06:27 -0800 Subject: [PATCH 087/245] Remove hidden zero-width spaces affecting copy behavior Summary: Ref T13161. Ref T12822. Today, we use invisible Zero-Width Spaces to try to improve copy/paste behavior from Differential. After D20188, we no longer need ZWS characters to avoid copying line numbers. Get rid of these secret invisible semantic ZWS characters completely. This means that both the left-hand and right-hand side of diffs become copyable, which isn't desired. I'll fix that with a hundred thousand lines of Javascript in the next change: this is a step toward everything working better, but doesn't fix everything yet. Test Plan: - Grepped for `zws`, `grep -i zero | grep -i width`. - Copied text out of Differential: got both sides of the diff (not ideal). Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13161, T12822 Differential Revision: https://secure.phabricator.com/D20189 --- resources/celerity/map.php | 30 +++++++++---------- .../DifferentialChangesetTwoUpRenderer.php | 10 +------ .../differential/changeset-view.css | 5 ---- .../repository/repository-crossreference.js | 5 ---- 4 files changed, 16 insertions(+), 34 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 441f7108d1..6b13a2dace 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -11,8 +11,8 @@ return array( 'conpherence.pkg.js' => '020aebcf', 'core.pkg.css' => '261ee8cf', 'core.pkg.js' => '5ace8a1e', - 'differential.pkg.css' => '801c5653', - 'differential.pkg.js' => '1f211736', + 'differential.pkg.css' => 'fcc82bc0', + 'differential.pkg.js' => '0e2b0e2c', 'diffusion.pkg.css' => '42c75c37', 'diffusion.pkg.js' => '91192d85', 'maniphest.pkg.css' => '35995d6d', @@ -61,7 +61,7 @@ return array( 'rsrc/css/application/dashboard/dashboard.css' => '4267d6c6', 'rsrc/css/application/diff/inline-comment-summary.css' => '81eb368d', 'rsrc/css/application/differential/add-comment.css' => '7e5900d9', - 'rsrc/css/application/differential/changeset-view.css' => '8a997ed9', + 'rsrc/css/application/differential/changeset-view.css' => '58236820', 'rsrc/css/application/differential/core.css' => 'bdb93065', 'rsrc/css/application/differential/phui-inline-comment.css' => '48acce5b', 'rsrc/css/application/differential/revision-comment.css' => '7dbc8d1d', @@ -420,7 +420,7 @@ return array( 'rsrc/js/application/releeph/releeph-preview-branch.js' => '75184d68', 'rsrc/js/application/releeph/releeph-request-state-change.js' => '9f081f05', 'rsrc/js/application/releeph/releeph-request-typeahead.js' => 'aa3a100c', - 'rsrc/js/application/repository/repository-crossreference.js' => 'db0c0214', + 'rsrc/js/application/repository/repository-crossreference.js' => 'c15122b4', 'rsrc/js/application/search/behavior-reorder-profile-menu-items.js' => 'e5bdb730', 'rsrc/js/application/search/behavior-reorder-queries.js' => 'b86f297f', 'rsrc/js/application/transactions/behavior-comment-actions.js' => '4dffaeb2', @@ -541,7 +541,7 @@ return array( 'conpherence-thread-manager' => 'aec8e38c', 'conpherence-transaction-css' => '3a3f5e7e', 'd3' => 'd67475f5', - 'differential-changeset-view-css' => '8a997ed9', + 'differential-changeset-view-css' => '58236820', 'differential-core-view-css' => 'bdb93065', 'differential-revision-add-comment-css' => '7e5900d9', 'differential-revision-comment-css' => '7dbc8d1d', @@ -671,7 +671,7 @@ return array( 'javelin-behavior-reorder-applications' => 'aa371860', 'javelin-behavior-reorder-columns' => '8ac32fd9', 'javelin-behavior-reorder-profile-menu-items' => 'e5bdb730', - 'javelin-behavior-repository-crossreference' => 'db0c0214', + 'javelin-behavior-repository-crossreference' => 'c15122b4', 'javelin-behavior-scrollbar' => '92388bae', 'javelin-behavior-search-reorder-queries' => 'b86f297f', 'javelin-behavior-select-content' => 'e8240b50', @@ -1380,6 +1380,9 @@ return array( 'javelin-vector', 'javelin-typeahead-static-source', ), + 58236820 => array( + 'phui-inline-comment-view-css', + ), '5902260c' => array( 'javelin-util', 'javelin-magical-init', @@ -1587,9 +1590,6 @@ return array( '8a16f91b' => array( 'syntax-default-css', ), - '8a997ed9' => array( - 'phui-inline-comment-view-css', - ), '8ac32fd9' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1912,6 +1912,12 @@ return array( 'c03f2fb4' => array( 'javelin-install', ), + 'c15122b4' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-stratcom', + 'javelin-uri', + ), 'c2c500a7' => array( 'javelin-install', 'javelin-dom', @@ -2008,12 +2014,6 @@ return array( 'javelin-uri', 'phabricator-notification', ), - 'db0c0214' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-stratcom', - 'javelin-uri', - ), 'dfa1d313' => array( 'javelin-behavior', 'javelin-dom', diff --git a/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php b/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php index baf08aac77..d975d82522 100644 --- a/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php +++ b/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php @@ -301,11 +301,6 @@ final class DifferentialChangesetTwoUpRenderer } } - // NOTE: This is a unicode zero-width space, which we use as a hint when - // intercepting 'copy' events to make sure sensible text ends up on the - // clipboard. See the 'phabricator-oncopy' behavior. - $zero_space = "\xE2\x80\x8B"; - $old_number = phutil_tag( 'td', array( @@ -330,10 +325,7 @@ final class DifferentialChangesetTwoUpRenderer phutil_tag( 'td', array('class' => $n_classes, 'colspan' => $n_colspan), - array( - phutil_tag('span', array('class' => 'zwsp'), $zero_space), - $n_text, - )), + $n_text), $n_cov, )); diff --git a/webroot/rsrc/css/application/differential/changeset-view.css b/webroot/rsrc/css/application/differential/changeset-view.css index a49dd4905b..59faa8399e 100644 --- a/webroot/rsrc/css/application/differential/changeset-view.css +++ b/webroot/rsrc/css/application/differential/changeset-view.css @@ -67,11 +67,6 @@ padding: 1px 4px; } -.differential-diff td .zwsp { - position: absolute; - width: 0; -} - .prose-diff { padding: 12px 0; white-space: pre-wrap; diff --git a/webroot/rsrc/js/application/repository/repository-crossreference.js b/webroot/rsrc/js/application/repository/repository-crossreference.js index 548ef6173b..d6ff2a06aa 100644 --- a/webroot/rsrc/js/application/repository/repository-crossreference.js +++ b/webroot/rsrc/js/application/repository/repository-crossreference.js @@ -237,11 +237,6 @@ JX.behavior('repository-crossreference', function(config, statics) { } var content = '' + node.textContent; - - // Strip off any ZWS characters. These are marker characters used to - // improve copy/paste behavior. - content = content.replace(/\u200B/g, ''); - char += content.length; } From 3ded5d3e8ca8717ac04cbee179a9e839ec5d7b6c Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 17 Feb 2019 06:24:08 -0800 Subject: [PATCH 088/245] Disable the JSHint "function called before it is defined" and "unused parameter" warnings Summary: Ref T12822. The next change hits these warnings but I think neither is a net positive. The "function called before it is defined" error alerts on this kind of thing: ``` function a() { b(); } function b() { } a(); ``` Here, `b()` is called before it is defined. This code, as written, is completely safe. Although it's possible that this kind of construct may be unsafe, I think the number of programs where there's unsafe behavior here AND the whole thing doesn't immediately break when you run it at all is very very small. Complying with this warning is sometimes impossible -- at least without cheating/restructuring/abuse -- for example, if you have two functions which are mutually recursive. Although compliance is usually possible, it forces you to define all your small utility functions at the top of a behavior. This isn't always the most logical or comprehensible order. I think we also have some older code which did `var a = function() { ... }` to escape this, which I think is just silly/confusing. Bascially, this is almost always a false positive and I think it makes the code worse more often than it makes it better. --- The "unused function parameter" error warns about this: ``` function onevent(e) { do_something(); ``` We aren't using `e`, so this warning is correct. However, when the function is a callback (as here), I think it's generally good hygiene to include the callback parameters in the signature (`onresponse(response)`, `onevent(event)`, etc), even if you aren't using any/all of them. This is a useful hint to future editors that the function is a callback. Although this //can// catch mistakes, I think this is also a situation where the number of cases where it catches a mistake and even the most cursory execution of the code doesn't catch the mistake is vanishingly small. Test Plan: Egregiously violated both rules in the next diff. Before change: complaints. After change: no complaints. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T12822 Differential Revision: https://secure.phabricator.com/D20190 --- support/lint/browser.jshintrc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/support/lint/browser.jshintrc b/support/lint/browser.jshintrc index b88c931eee..2a9c65bdd2 100644 --- a/support/lint/browser.jshintrc +++ b/support/lint/browser.jshintrc @@ -4,12 +4,12 @@ "freeze": true, "immed": true, "indent": 2, - "latedef": true, + "latedef": "nofunc", "newcap": true, "noarg": true, "quotmark": "single", "undef": true, - "unused": true, + "unused": "vars", "expr": true, "loopfunc": true, From 37f12a05ea621788f83a97ef5ab7ed9ffd7c1aaa Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 16 Feb 2019 09:32:13 -0800 Subject: [PATCH 089/245] Behold! Copy text from either side of a diff! MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Ref T12822. Ref T13161. By default, when users select text from a diff and copy it to the clipboard, they get both sides of the diff and all the line numbers. This is usually not what they intended to copy. As of D20188, we use `content: attr(...)` to render line numbers. No browser copies this text, so that fixes line numbers. We can use "user-select" CSS to visually prevent selection of line numbers and other stuff we don't want to copy. In Firefox and Chrome, "user-select" also applies to copied text, so getting "user-select" on the right nodes is largely good enough to do what we want. In Safari, "user-select" is only visual, so we always need to crawl the DOM to figure out what text to pull out of it anyway. In all browsers, we likely want to crawl the DOM anyway because this will let us show one piece of text and copy a different piece of text. We probably want to do this in the future to preserve "\t" tabs, and possibly to let us render certain character codes in one way but copy their original values. For example, we could render "\x07" as "␇". Finally, we have to figure out which side of the diff we're copying from. The rule here is: - If you start the selection by clicking somewhere on the left or right side of the diff, that's what you're copying. - Otherwise, use normal document copy rules. So the overall flow here is: - Listen for clicks. - When the user clicks the left or right side of the diff, store what they clicked. - When a selection starts, and something is actually selected, check if it was initiated by clicking a diff. If it was, apply a visual effect to get "user-select" where it needs to go and show the user what we think they're doing and what we're going to copy. - (Then, try to handle a bunch of degenerate cases where you start a selection and then click inside that selection.) - When a user clicks elsewhere or ends the selection with nothing selected, clear the selection mode. - When a user copies text, if we have an active selection mode, pull all the selected nodes out of the DOM and filter out the ones we don't want to copy, then stitch the text back together. Although I believe this didn't work well in ~2010, it appears to work well today. Test Plan: This mostly seems to work in Safari, Chrome, and Firefox. T12822 has some errata. I haven't tested touch events but am satisfied if the touch event story is anything better than "permanently destroys data". Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13161, T12822 Differential Revision: https://secure.phabricator.com/D20191 --- resources/celerity/map.php | 26 +- .../DifferentialChangesetHTMLRenderer.php | 2 +- .../DifferentialChangesetTwoUpRenderer.php | 14 +- .../differential/changeset-view.css | 36 ++- webroot/rsrc/js/core/behavior-oncopy.js | 299 +++++++++++++++--- 5 files changed, 309 insertions(+), 68 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 6b13a2dace..086cb1b8b3 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -10,8 +10,8 @@ return array( 'conpherence.pkg.css' => '3c8a0668', 'conpherence.pkg.js' => '020aebcf', 'core.pkg.css' => '261ee8cf', - 'core.pkg.js' => '5ace8a1e', - 'differential.pkg.css' => 'fcc82bc0', + 'core.pkg.js' => '5ba0b6d7', + 'differential.pkg.css' => 'd1b29c9c', 'differential.pkg.js' => '0e2b0e2c', 'diffusion.pkg.css' => '42c75c37', 'diffusion.pkg.js' => '91192d85', @@ -61,7 +61,7 @@ return array( 'rsrc/css/application/dashboard/dashboard.css' => '4267d6c6', 'rsrc/css/application/diff/inline-comment-summary.css' => '81eb368d', 'rsrc/css/application/differential/add-comment.css' => '7e5900d9', - 'rsrc/css/application/differential/changeset-view.css' => '58236820', + 'rsrc/css/application/differential/changeset-view.css' => 'e2b81e85', 'rsrc/css/application/differential/core.css' => 'bdb93065', 'rsrc/css/application/differential/phui-inline-comment.css' => '48acce5b', 'rsrc/css/application/differential/revision-comment.css' => '7dbc8d1d', @@ -473,7 +473,7 @@ return array( 'rsrc/js/core/behavior-linked-container.js' => '74446546', 'rsrc/js/core/behavior-more.js' => '506aa3f4', 'rsrc/js/core/behavior-object-selector.js' => 'a4af0b4a', - 'rsrc/js/core/behavior-oncopy.js' => '418f6684', + 'rsrc/js/core/behavior-oncopy.js' => 'f20d66c1', 'rsrc/js/core/behavior-phabricator-nav.js' => 'f166c949', 'rsrc/js/core/behavior-phabricator-remarkup-assist.js' => '2f80333f', 'rsrc/js/core/behavior-read-only-warning.js' => 'b9109f8f', @@ -541,7 +541,7 @@ return array( 'conpherence-thread-manager' => 'aec8e38c', 'conpherence-transaction-css' => '3a3f5e7e', 'd3' => 'd67475f5', - 'differential-changeset-view-css' => '58236820', + 'differential-changeset-view-css' => 'e2b81e85', 'differential-core-view-css' => 'bdb93065', 'differential-revision-add-comment-css' => '7e5900d9', 'differential-revision-comment-css' => '7dbc8d1d', @@ -636,7 +636,7 @@ return array( 'javelin-behavior-phabricator-nav' => 'f166c949', 'javelin-behavior-phabricator-notification-example' => '29819b75', 'javelin-behavior-phabricator-object-selector' => 'a4af0b4a', - 'javelin-behavior-phabricator-oncopy' => '418f6684', + 'javelin-behavior-phabricator-oncopy' => 'f20d66c1', 'javelin-behavior-phabricator-remarkup-assist' => '2f80333f', 'javelin-behavior-phabricator-reveal-content' => 'b105a3a6', 'javelin-behavior-phabricator-search-typeahead' => '1cb7d027', @@ -1222,10 +1222,6 @@ return array( 'javelin-behavior', 'javelin-uri', ), - '418f6684' => array( - 'javelin-behavior', - 'javelin-dom', - ), '42c7a5a7' => array( 'javelin-install', 'javelin-dom', @@ -1380,9 +1376,6 @@ return array( 'javelin-vector', 'javelin-typeahead-static-source', ), - 58236820 => array( - 'phui-inline-comment-view-css', - ), '5902260c' => array( 'javelin-util', 'javelin-magical-init', @@ -2039,6 +2032,9 @@ return array( 'javelin-dom', 'javelin-stratcom', ), + 'e2b81e85' => array( + 'phui-inline-comment-view-css', + ), 'e562708c' => array( 'javelin-install', ), @@ -2090,6 +2086,10 @@ return array( 'javelin-request', 'javelin-util', ), + 'f20d66c1' => array( + 'javelin-behavior', + 'javelin-dom', + ), 'f340a484' => array( 'javelin-install', 'javelin-dom', diff --git a/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php b/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php index 2e760d99bb..e4320b712b 100644 --- a/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php +++ b/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php @@ -436,7 +436,7 @@ abstract class DifferentialChangesetHTMLRenderer 'table', array( 'class' => implode(' ', $classes), - 'sigil' => 'differential-diff', + 'sigil' => 'differential-diff intercept-copy', ), array( $this->renderColgroup(), diff --git a/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php b/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php index d975d82522..290272db49 100644 --- a/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php +++ b/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php @@ -319,12 +319,22 @@ final class DifferentialChangesetTwoUpRenderer $html[] = phutil_tag('tr', array(), array( $old_number, - phutil_tag('td', array('class' => $o_classes), $o_text), + phutil_tag( + 'td', + array( + 'class' => $o_classes, + 'data-copy-mode' => 'copy-l', + ), + $o_text), $new_number, $n_copy, phutil_tag( 'td', - array('class' => $n_classes, 'colspan' => $n_colspan), + array( + 'class' => $n_classes, + 'colspan' => $n_colspan, + 'data-copy-mode' => 'copy-r', + ), $n_text), $n_cov, )); diff --git a/webroot/rsrc/css/application/differential/changeset-view.css b/webroot/rsrc/css/application/differential/changeset-view.css index 59faa8399e..bc1ca8b6ea 100644 --- a/webroot/rsrc/css/application/differential/changeset-view.css +++ b/webroot/rsrc/css/application/differential/changeset-view.css @@ -176,12 +176,6 @@ should always have a boring grey background. */ cursor: pointer; border-right: 1px solid {$thinblueborder}; overflow: hidden; - - -moz-user-select: -moz-none; - -khtml-user-select: none; - -webkit-user-select: none; - -ms-user-select: none; - user-select: none; } .differential-diff td.n::before { @@ -430,3 +424,33 @@ tr.differential-inline-loading { .diff-banner-buttons { float: right; } + +/* In Firefox, making the table unselectable and then making cells selectable +does not work: the cells remain unselectable. Narrowly mark the cells as +unselectable. */ + +.differential-diff.copy-l > tbody > tr > td, +.differential-diff.copy-r > tbody > tr > td { + -moz-user-select: -moz-none; + -khtml-user-select: none; + -ms-user-select: none; + -webkit-user-select: none; + user-select: none; +} + +.differential-diff.copy-l > tbody > tr > td, +.differential-diff.copy-r > tbody > tr > td { + opacity: 0.5; +} + +.differential-diff.copy-l > tbody > tr > td:nth-child(2) { + -webkit-user-select: auto; + user-select: auto; + opacity: 1; +} + +.differential-diff.copy-r > tbody > tr > td:nth-child(5) { + -webkit-user-select: auto; + user-select: auto; + opacity: 1; +} diff --git a/webroot/rsrc/js/core/behavior-oncopy.js b/webroot/rsrc/js/core/behavior-oncopy.js index aa8c684fee..a37474abf9 100644 --- a/webroot/rsrc/js/core/behavior-oncopy.js +++ b/webroot/rsrc/js/core/behavior-oncopy.js @@ -4,62 +4,269 @@ * javelin-dom */ -/** - * Tools like Paste and Differential don't normally respond to the clipboard - * 'copy' operation well, because when a user copies text they'll get line - * numbers and other metadata. - * - * To improve this behavior, applications can embed markers that delimit - * metadata (left of the marker) from content (right of the marker). When - * we get a copy event, we strip out all the metadata and just copy the - * actual text. - */ JX.behavior('phabricator-oncopy', function() { + var copy_root; + var copy_mode; - var zws = '\u200B'; // Unicode Zero-Width Space + function onstartselect(e) { + var target = e.getTarget(); - JX.enableDispatch(document.body, 'copy'); - JX.Stratcom.listen( - ['copy'], - null, - function(e) { + var container; + try { + // NOTE: For now, all elements with custom oncopy behavior are tables, + // so this tag selection will hit everything we need it to. + container = JX.DOM.findAbove(target, 'table', 'intercept-copy'); + } catch (ex) { + container = null; + } - var selection; - var text; - if (window.getSelection) { - selection = window.getSelection(); - text = selection.toString(); - } else { - selection = document.selection; - text = selection.createRange().text; - } + var old_mode = copy_mode; + clear_selection_mode(); - if (text.indexOf(zws) == -1) { - // If there's no marker in the text, just let it copy normally. + if (!container) { + return; + } + + // If the potential selection is starting inside an inline comment, + // don't do anything special. + try { + if (JX.DOM.findAbove(target, 'div', 'differential-inline-comment')) { return; } + } catch (ex) { + // Continue. + } - var result = []; - - // Strip everything before the marker (and the marker itself) out of the - // text. If a line doesn't have the marker, throw it away (the assumption - // is that it's a line number or part of some other meta-text). - var lines = text.split('\n'); - var pos; - for (var ii = 0; ii < lines.length; ii++) { - pos = lines[ii].indexOf(zws); - if (pos == -1 && ii !== 0) { - continue; - } - result.push(lines[ii].substring(pos + 1)); + // Find the row and cell we're copying from. If we don't find anything, + // don't do anything special. + var row; + var cell; + try { + // The target may be the cell we're after, particularly if you click + // in the white area to the right of the text, towards the end of a line. + if (JX.DOM.isType(target, 'td')) { + cell = target; + } else { + cell = JX.DOM.findAbove(target, 'td'); } - result = result.join('\n'); + row = JX.DOM.findAbove(target, 'tr'); + } catch (ex) { + return; + } + + // If the row doesn't have enough nodes, bail out. Note that it's okay + // to begin a selection in the whitespace on the opposite side of an inline + // comment. For example, if there's an inline comment on the right side of + // a diff, it's okay to start selecting the left side of the diff by + // clicking the corresponding empty space on the left side. + if (row.childNodes.length < 4) { + return; + } + + // If the selection's cell is in the "old" diff or the "new" diff, we'll + // activate an appropriate copy mode. + var mode; + if (cell === row.childNodes[1]) { + mode = 'copy-l'; + } else if ((row.childNodes.length >= 4) && (cell === row.childNodes[4])) { + mode = 'copy-r'; + } else { + return; + } + + // We found a copy mode, so set it as the current active mode. + copy_root = container; + copy_mode = mode; + + // If the user makes a selection, then clicks again inside the same + // selection, browsers retain the selection. This is because the user may + // want to drag-and-drop the text to another window. + + // Handle special cases when the click is inside an existing selection. + + var ranges = get_selected_ranges(); + if (ranges.length) { + // We'll have an existing selection if the user selects text on the right + // side of a diff, then clicks the selection on the left side of the + // diff, even if the second click is clicking part of the selection + // range where the selection highlight is currently invisible because + // of CSS rules. + + // This behavior looks and feels glitchy: an invisible selection range + // suddenly pops into existence and there's a bunch of flicker. If we're + // switching selection modes, clear the old selection to avoid this: + // assume the user is not trying to drag-and-drop text which is not + // visually selected. + + if (old_mode !== copy_mode) { + window.getSelection().removeAllRanges(); + } + + // In the more mundane case, if the user selects some text on one side + // of a diff and then clicks that same selection in a normal way (in + // the visible part of the highlighted text), we may either be altering + // the selection range or may be initiating a text drag depending on how + // long they hold the button for. Regardless of what we're doing, we're + // still in a selection mode, so keep the visual hints active. + + JX.DOM.alterClass(copy_root, copy_mode, true); + } + + // We've chosen a mode and saved it now, but we don't actually update to + // apply any visual changes until the user actually starts making some + // kind of selection. + } + + // When the selection range changes, apply CSS classes if the selection is + // nonempty. We don't want to make visual changes to the document immediately + // when the user press the mouse button, since we aren't yet sure that + // they are starting a selection: instead, wait for them to actually select + // something. + function onchangeselect() { + if (!copy_mode) { + return; + } + + var ranges = get_selected_ranges(); + JX.DOM.alterClass(copy_root, copy_mode, !!ranges.length); + } + + // When the user releases the mouse, get rid of the selection mode if we + // don't have a selection. + function onendselect(e) { + if (!copy_mode) { + return; + } + + var ranges = get_selected_ranges(); + if (!ranges.length) { + clear_selection_mode(); + } + } + + function get_selected_ranges() { + var ranges = []; + + if (!window.getSelection) { + return ranges; + } + + var selection = window.getSelection(); + for (var ii = 0; ii < selection.rangeCount; ii++) { + var range = selection.getRangeAt(ii); + if (range.collapsed) { + continue; + } + + ranges.push(range); + } + + return ranges; + } + + function clear_selection_mode() { + if (!copy_root) { + return; + } + + JX.DOM.alterClass(copy_root, copy_mode, false); + copy_root = null; + copy_mode = null; + } + + function oncopy(e) { + // If we aren't in a special copy mode, just fall back to default + // behavior. + if (!copy_mode) { + return; + } + + var ranges = get_selected_ranges(); + if (!ranges.length) { + return; + } + + var text_nodes = []; + for (var ii = 0; ii < ranges.length; ii++) { + var range = ranges[ii]; + + var fragment = range.cloneContents(); + if (!fragment.children.length) { + continue; + } + + // In Chrome and Firefox, because we've already applied "user-select" + // CSS to everything we don't intend to copy, the text in the selection + // range is correct, and the range will include only the correct text + // nodes. + + // However, in Safari, "user-select" does not apply to clipboard + // operations, so we get everything in the document between the beginning + // and end of the selection, even if it isn't visibly selected. + + // Even in Chrome and Firefox, we can get partial empty nodes: for + // example, where a "" is selectable but no content in the node is + // selectable. (We have to leave the "" itself selectable because + // of how Firefox applies "user-select" rules.) + + // The nodes we get here can also start and end more or less anywhere. + + // One saving grace is that we use "content: attr(data-n);" to render + // the line numbers and no browsers copy this content, so we don't have + // to worry about figuring out when text is line numbers. + + for (var jj = 0; jj < fragment.childNodes.length; jj++) { + var node = fragment.childNodes[jj]; + if (JX.DOM.isType(node, 'tr')) { + // This is an inline comment row, so we never want to copy any + // content inside of it. + if (JX.Stratcom.hasSigil(node, 'inline-row')) { + continue; + } + + // Assume anything else is a source code row. Keep only "" cells + // with the correct mode. + for (var kk = 0; kk < node.childNodes.length; kk++) { + var child = node.childNodes[kk]; + + var node_mode = child.getAttribute('data-copy-mode'); + if (node_mode === copy_mode) { + text_nodes.push(child); + } + } + } else { + // For anything else, assume this is a text fragment or part of + // a table cell or something and should be included in the selection + // range. + text_nodes.push(node); + } + } + + var text = []; + for (ii = 0; ii < text_nodes.length; ii++) { + text.push(text_nodes[ii].textContent); + } + text = text.join(''); var rawEvent = e.getRawEvent(); - var clipboardData = 'clipboardData' in rawEvent ? - rawEvent.clipboardData : - window.clipboardData; - clipboardData.setData('Text', result); + var data; + if ('clipboardData' in rawEvent) { + data = rawEvent.clipboardData; + } else { + data = window.clipboardData; + } + data.setData('Text', text); + e.prevent(); - }); + } + } + + JX.enableDispatch(document.body, 'copy'); + JX.enableDispatch(window, 'selectionchange'); + + JX.Stratcom.listen('mousedown', null, onstartselect); + JX.Stratcom.listen('selectionchange', null, onchangeselect); + JX.Stratcom.listen('mouseup', null, onendselect); + + JX.Stratcom.listen('copy', null, oncopy); }); From efccd75ae37174e370a9c88197e60b178fc8e5fe Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 17 Feb 2019 07:58:26 -0800 Subject: [PATCH 090/245] Correct various minor diff copy behaviors Summary: Ref T12822. Fixes a few things: - Firefox selection of weird ranges with an inline between the start and end of the range now works correctly. - "Show More Context" rows now render, highlight, and select properly. - Prepares for nodes to have copy-text which is different from display-text. - Don't do anything too fancy in 1-up/unified mode. We don't copy line numbers after the `content: attr(data-n)` change, but that's as far as we go, because trying to do more than that is kind of weird and not terribly intuitive. Test Plan: - Selected and copied weird ranges in Firefox. - Kept an eye on "Show More Context" rows across select and copy operations. - Generally poked around in Safari/Firefox/Chrome. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T12822 Differential Revision: https://secure.phabricator.com/D20192 --- resources/celerity/map.php | 40 +++--- .../DifferentialChangesetHTMLRenderer.php | 12 +- .../DifferentialChangesetTwoUpRenderer.php | 20 ++- .../differential/changeset-view.css | 31 ++++- .../js/application/diff/DiffChangesetList.js | 22 ++- webroot/rsrc/js/core/behavior-oncopy.js | 127 ++++++++++++------ 6 files changed, 175 insertions(+), 77 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 086cb1b8b3..04036e70e4 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -10,9 +10,9 @@ return array( 'conpherence.pkg.css' => '3c8a0668', 'conpherence.pkg.js' => '020aebcf', 'core.pkg.css' => '261ee8cf', - 'core.pkg.js' => '5ba0b6d7', - 'differential.pkg.css' => 'd1b29c9c', - 'differential.pkg.js' => '0e2b0e2c', + 'core.pkg.js' => 'e368deda', + 'differential.pkg.css' => '249b542d', + 'differential.pkg.js' => '53f8d00c', 'diffusion.pkg.css' => '42c75c37', 'diffusion.pkg.js' => '91192d85', 'maniphest.pkg.css' => '35995d6d', @@ -61,7 +61,7 @@ return array( 'rsrc/css/application/dashboard/dashboard.css' => '4267d6c6', 'rsrc/css/application/diff/inline-comment-summary.css' => '81eb368d', 'rsrc/css/application/differential/add-comment.css' => '7e5900d9', - 'rsrc/css/application/differential/changeset-view.css' => 'e2b81e85', + 'rsrc/css/application/differential/changeset-view.css' => 'cc3fd795', 'rsrc/css/application/differential/core.css' => 'bdb93065', 'rsrc/css/application/differential/phui-inline-comment.css' => '48acce5b', 'rsrc/css/application/differential/revision-comment.css' => '7dbc8d1d', @@ -375,7 +375,7 @@ return array( 'rsrc/js/application/dashboard/behavior-dashboard-query-panel-select.js' => '1e413dc9', 'rsrc/js/application/dashboard/behavior-dashboard-tab-panel.js' => '9b1cbd76', 'rsrc/js/application/diff/DiffChangeset.js' => 'd0a85a85', - 'rsrc/js/application/diff/DiffChangesetList.js' => '26fb79ba', + 'rsrc/js/application/diff/DiffChangesetList.js' => '04023d82', 'rsrc/js/application/diff/DiffInline.js' => 'a4a14a94', 'rsrc/js/application/diff/behavior-preview-link.js' => 'f51e9c17', 'rsrc/js/application/differential/behavior-diff-radios.js' => '925fe8cd', @@ -473,7 +473,7 @@ return array( 'rsrc/js/core/behavior-linked-container.js' => '74446546', 'rsrc/js/core/behavior-more.js' => '506aa3f4', 'rsrc/js/core/behavior-object-selector.js' => 'a4af0b4a', - 'rsrc/js/core/behavior-oncopy.js' => 'f20d66c1', + 'rsrc/js/core/behavior-oncopy.js' => 'de59bf15', 'rsrc/js/core/behavior-phabricator-nav.js' => 'f166c949', 'rsrc/js/core/behavior-phabricator-remarkup-assist.js' => '2f80333f', 'rsrc/js/core/behavior-read-only-warning.js' => 'b9109f8f', @@ -541,7 +541,7 @@ return array( 'conpherence-thread-manager' => 'aec8e38c', 'conpherence-transaction-css' => '3a3f5e7e', 'd3' => 'd67475f5', - 'differential-changeset-view-css' => 'e2b81e85', + 'differential-changeset-view-css' => 'cc3fd795', 'differential-core-view-css' => 'bdb93065', 'differential-revision-add-comment-css' => '7e5900d9', 'differential-revision-comment-css' => '7dbc8d1d', @@ -636,7 +636,7 @@ return array( 'javelin-behavior-phabricator-nav' => 'f166c949', 'javelin-behavior-phabricator-notification-example' => '29819b75', 'javelin-behavior-phabricator-object-selector' => 'a4af0b4a', - 'javelin-behavior-phabricator-oncopy' => 'f20d66c1', + 'javelin-behavior-phabricator-oncopy' => 'de59bf15', 'javelin-behavior-phabricator-remarkup-assist' => '2f80333f', 'javelin-behavior-phabricator-reveal-content' => 'b105a3a6', 'javelin-behavior-phabricator-search-typeahead' => '1cb7d027', @@ -754,7 +754,7 @@ return array( 'phabricator-darkmessage' => '26cd4b73', 'phabricator-dashboard-css' => '4267d6c6', 'phabricator-diff-changeset' => 'd0a85a85', - 'phabricator-diff-changeset-list' => '26fb79ba', + 'phabricator-diff-changeset-list' => '04023d82', 'phabricator-diff-inline' => 'a4a14a94', 'phabricator-drag-and-drop-file-upload' => '4370900d', 'phabricator-draggable-list' => '3c6bd549', @@ -907,6 +907,10 @@ return array( 'javelin-uri', 'javelin-util', ), + '04023d82' => array( + 'javelin-install', + 'phuix-button-view', + ), '04f8a1e3' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1087,10 +1091,6 @@ return array( 'javelin-json', 'phabricator-draggable-list', ), - '26fb79ba' => array( - 'javelin-install', - 'phuix-button-view', - ), '27daef73' => array( 'multirow-row-manager', 'javelin-install', @@ -1961,6 +1961,9 @@ return array( 'javelin-util', 'phabricator-keyboard-shortcut-manager', ), + 'cc3fd795' => array( + 'phui-inline-comment-view-css', + ), 'cf32921f' => array( 'javelin-behavior', 'javelin-dom', @@ -2007,6 +2010,10 @@ return array( 'javelin-uri', 'phabricator-notification', ), + 'de59bf15' => array( + 'javelin-behavior', + 'javelin-dom', + ), 'dfa1d313' => array( 'javelin-behavior', 'javelin-dom', @@ -2032,9 +2039,6 @@ return array( 'javelin-dom', 'javelin-stratcom', ), - 'e2b81e85' => array( - 'phui-inline-comment-view-css', - ), 'e562708c' => array( 'javelin-install', ), @@ -2086,10 +2090,6 @@ return array( 'javelin-request', 'javelin-util', ), - 'f20d66c1' => array( - 'javelin-behavior', - 'javelin-dom', - ), 'f340a484' => array( 'javelin-install', 'javelin-dom', diff --git a/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php b/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php index e4320b712b..df59db9228 100644 --- a/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php +++ b/src/applications/differential/render/DifferentialChangesetHTMLRenderer.php @@ -432,11 +432,17 @@ abstract class DifferentialChangesetHTMLRenderer $classes[] = 'PhabricatorMonospaced'; $classes[] = $this->getRendererTableClass(); + $sigils = array(); + $sigils[] = 'differential-diff'; + foreach ($this->getTableSigils() as $sigil) { + $sigils[] = $sigil; + } + return javelin_tag( 'table', array( 'class' => implode(' ', $classes), - 'sigil' => 'differential-diff intercept-copy', + 'sigil' => implode(' ', $sigils), ), array( $this->renderColgroup(), @@ -444,6 +450,10 @@ abstract class DifferentialChangesetHTMLRenderer )); } + protected function getTableSigils() { + return array(); + } + protected function buildInlineComment( PhabricatorInlineCommentInterface $comment, $on_right = false) { diff --git a/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php b/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php index 290272db49..7c728c1ff7 100644 --- a/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php +++ b/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php @@ -117,16 +117,20 @@ final class DifferentialChangesetTwoUpRenderer phutil_tag( 'td', array( - 'colspan' => 2, + 'class' => 'show-context-line n left-context', + )), + phutil_tag( + 'td', + array( 'class' => 'show-more', ), $contents), phutil_tag( - 'th', + 'td', array( - 'class' => 'show-context-line', - ), - $context_line ? (int)$context_line : null), + 'class' => 'show-context-line n', + 'data-n' => $context_line, + )), phutil_tag( 'td', array( @@ -448,4 +452,10 @@ final class DifferentialChangesetTwoUpRenderer return $this->newOffsetMap; } + protected function getTableSigils() { + return array( + 'intercept-copy', + ); + } + } diff --git a/webroot/rsrc/css/application/differential/changeset-view.css b/webroot/rsrc/css/application/differential/changeset-view.css index bc1ca8b6ea..7d578ccc03 100644 --- a/webroot/rsrc/css/application/differential/changeset-view.css +++ b/webroot/rsrc/css/application/differential/changeset-view.css @@ -87,7 +87,7 @@ color: {$darkgreytext}; } -.differential-changeset-immutable .differential-diff th { +.differential-changeset-immutable .differential-diff td { cursor: auto; } @@ -182,6 +182,10 @@ should always have a boring grey background. */ content: attr(data-n); } +.differential-diff td.show-context-line.n { + cursor: auto; +} + .differential-diff td.cov { padding: 0; } @@ -222,7 +226,7 @@ td.cov-I { } .differential-diff td.show-more, -.differential-diff th.show-context-line, +.differential-diff td.show-context-line, .differential-diff td.show-context, .differential-diff td.differential-shield { background: {$lightbluebackground}; @@ -232,7 +236,7 @@ td.cov-I { } .device .differential-diff td.show-more, -.device .differential-diff th.show-context-line, +.device .differential-diff td.show-context-line, .device .differential-diff td.show-context, .device .differential-diff td.differential-shield { padding: 6px 0; @@ -250,10 +254,14 @@ td.cov-I { color: {$bluetext}; } -.differential-diff th.show-context-line { +.differential-diff td.show-context-line { padding-right: 6px; } +.differential-diff td.show-context-line.left-context { + border-right: none; +} + .differential-diff td.show-context { padding-left: 14px; } @@ -431,8 +439,7 @@ unselectable. */ .differential-diff.copy-l > tbody > tr > td, .differential-diff.copy-r > tbody > tr > td { - -moz-user-select: -moz-none; - -khtml-user-select: none; + -moz-user-select: none; -ms-user-select: none; -webkit-user-select: none; user-select: none; @@ -444,12 +451,24 @@ unselectable. */ } .differential-diff.copy-l > tbody > tr > td:nth-child(2) { + -moz-user-select: auto; + -ms-user-select: auto; -webkit-user-select: auto; user-select: auto; opacity: 1; } +.differential-diff.copy-l > tbody > tr > td.show-more:nth-child(2) { + -moz-user-select: none; + -ms-user-select: none; + -webkit-user-select: none; + user-select: none; + opacity: 0.25; +} + .differential-diff.copy-r > tbody > tr > td:nth-child(5) { + -moz-user-select: auto; + -ms-user-select: auto; -webkit-user-select: auto; user-select: auto; opacity: 1; diff --git a/webroot/rsrc/js/application/diff/DiffChangesetList.js b/webroot/rsrc/js/application/diff/DiffChangesetList.js index 8a1306967a..572faad987 100644 --- a/webroot/rsrc/js/application/diff/DiffChangesetList.js +++ b/webroot/rsrc/js/application/diff/DiffChangesetList.js @@ -1246,8 +1246,24 @@ JX.install('DiffChangesetList', { return changeset.getInlineForRow(inline_row); }, - getLineNumberFromHeader: function(th) { - return parseInt(th.getAttribute('data-n')); + getLineNumberFromHeader: function(node) { + var n = parseInt(node.getAttribute('data-n')); + + if (!n) { + return null; + } + + // If this is a line number that's part of a row showing more context, + // we don't want to let users leave inlines here. + + try { + JX.DOM.findAbove(node, 'tr', 'context-target'); + return null; + } catch (ex) { + // Ignore. + } + + return n; }, getDisplaySideFromHeader: function(th) { @@ -1295,7 +1311,7 @@ JX.install('DiffChangesetList', { }, _updateRange: function(target, is_out) { - // Don't update the range if this "" doesn't correspond to a line + // Don't update the range if this target doesn't correspond to a line // number. For instance, this may be a dead line number, like the empty // line numbers on the left hand side of a newly added file. var number = this.getLineNumberFromHeader(target); diff --git a/webroot/rsrc/js/core/behavior-oncopy.js b/webroot/rsrc/js/core/behavior-oncopy.js index a37474abf9..e7309a1e18 100644 --- a/webroot/rsrc/js/core/behavior-oncopy.js +++ b/webroot/rsrc/js/core/behavior-oncopy.js @@ -186,12 +186,12 @@ JX.behavior('phabricator-oncopy', function() { return; } - var text_nodes = []; + var text = []; for (var ii = 0; ii < ranges.length; ii++) { var range = ranges[ii]; var fragment = range.cloneContents(); - if (!fragment.children.length) { + if (!fragment.childNodes.length) { continue; } @@ -217,48 +217,91 @@ JX.behavior('phabricator-oncopy', function() { for (var jj = 0; jj < fragment.childNodes.length; jj++) { var node = fragment.childNodes[jj]; - if (JX.DOM.isType(node, 'tr')) { - // This is an inline comment row, so we never want to copy any - // content inside of it. - if (JX.Stratcom.hasSigil(node, 'inline-row')) { - continue; - } - - // Assume anything else is a source code row. Keep only "" cells - // with the correct mode. - for (var kk = 0; kk < node.childNodes.length; kk++) { - var child = node.childNodes[kk]; - - var node_mode = child.getAttribute('data-copy-mode'); - if (node_mode === copy_mode) { - text_nodes.push(child); - } - } - } else { - // For anything else, assume this is a text fragment or part of - // a table cell or something and should be included in the selection - // range. - text_nodes.push(node); - } + text.push(extract_text(node)); } - - var text = []; - for (ii = 0; ii < text_nodes.length; ii++) { - text.push(text_nodes[ii].textContent); - } - text = text.join(''); - - var rawEvent = e.getRawEvent(); - var data; - if ('clipboardData' in rawEvent) { - data = rawEvent.clipboardData; - } else { - data = window.clipboardData; - } - data.setData('Text', text); - - e.prevent(); } + + text = flatten_list(text); + text = text.join(''); + + var rawEvent = e.getRawEvent(); + var data; + if ('clipboardData' in rawEvent) { + data = rawEvent.clipboardData; + } else { + data = window.clipboardData; + } + data.setData('Text', text); + + e.prevent(); + } + + function extract_text(node) { + var ii; + var text = []; + + if (JX.DOM.isType(node, 'tr')) { + // This is an inline comment row, so we never want to copy any + // content inside of it. + if (JX.Stratcom.hasSigil(node, 'inline-row')) { + return null; + } + + // This is a "Show More Context" row, so we never want to copy any + // of the content inside. + if (JX.Stratcom.hasSigil(node, 'context-target')) { + return null; + } + + // Assume anything else is a source code row. Keep only "" cells + // with the correct mode. + for (ii = 0; ii < node.childNodes.length; ii++) { + text.push(extract_text(node.childNodes[ii])); + } + + return text; + } + + if (JX.DOM.isType(node, 'td')) { + var node_mode = node.getAttribute('data-copy-mode'); + if (node_mode !== copy_mode) { + return; + } + + // Otherwise, fall through and extract this node's text normally. + } + + if (!node.childNodes || !node.childNodes.length) { + return node.textContent; + } + + for (ii = 0; ii < node.childNodes.length; ii++) { + var child = node.childNodes[ii]; + text.push(extract_text(child)); + } + + return text; + } + + function flatten_list(list) { + var stack = [list]; + var result = []; + while (stack.length) { + var next = stack.pop(); + if (JX.isArray(next)) { + for (var ii = 0; ii < next.length; ii++) { + stack.push(next[ii]); + } + } else if (next === null) { + continue; + } else if (next === undefined) { + continue; + } else { + result.push(next); + } + } + + return result.reverse(); } JX.enableDispatch(document.body, 'copy'); From fe7047d12d6904696a2eb9f2c2b2cbe8831625fe Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 17 Feb 2019 13:39:46 -0800 Subject: [PATCH 091/245] Display some invisible/nonprintable characters in diffs by default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: Ref T12822. Ref T2495. This is the good version of D20193. Currently, we display various nonprintable characters (ZWS, nonbreaking space, various control characters) as themselves, so they're generally invisible. In T12822, one user reports that all their engineers frequently type ZWS characters into source somehow? I don't really believe this (??), and this should be fixed in lint. That said, the only real reason not to show these weird characters in a special way was that it would break copy/paste: if we render ZWS as "🐑", and a user copy-pastes the line including the ZWS, they'll get a sheep. At least, they would have, until D20191. Now that this whole thing is end-to-end Javascript magic, we can copy whatever we want. In particular, we can render any character `X` as `X`, and then copy "Y" instead of "X" when the user copies the node. Limitations: - If users select only "X", they'll get "X" on their clipboard. This seems fine. If you're selecting our ZWS marker *only*, you probably want to copy it? - If "X" is more than one character long, users will get the full "Y" if they select any part of "X". At least here, this only matters when "X" is several spaces and "Y" is a tab. This also seems fine. - We have to be kind of careful because this approach involves editing an HTML blob directly. However, we already do that elsewhere and this isn't really too hard to get right. With those tools in hand: - Replace "\t" (raw text / what gets copied) with the number of spaces to the next tab stop for display. - Replace ZWS and NBSP (raw text) with a special marker for display. - Replace control characters 0x00-0x19 and 0x7F, except for "\t", "\r", and "\n", with the special unicode "control character pictures" reserved for this purpose. Test Plan: - Generated and viewed a file like this one: {F6220422} - Copied text out of it, got authentic raw original source text instead of displayed text. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T12822, T2495 Differential Revision: https://secure.phabricator.com/D20194 --- resources/celerity/map.php | 26 +-- .../parser/DifferentialChangesetParser.php | 190 +++++++++++++++++- .../render/DifferentialChangesetRenderer.php | 14 +- .../DifferentialChangesetTestRenderer.php | 2 +- webroot/rsrc/css/core/syntax.css | 6 + webroot/rsrc/js/core/behavior-oncopy.js | 7 + 6 files changed, 221 insertions(+), 24 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 04036e70e4..98d44804d3 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,8 +9,8 @@ return array( 'names' => array( 'conpherence.pkg.css' => '3c8a0668', 'conpherence.pkg.js' => '020aebcf', - 'core.pkg.css' => '261ee8cf', - 'core.pkg.js' => 'e368deda', + 'core.pkg.css' => 'e3c1a8f2', + 'core.pkg.js' => '2cda17a4', 'differential.pkg.css' => '249b542d', 'differential.pkg.js' => '53f8d00c', 'diffusion.pkg.css' => '42c75c37', @@ -112,7 +112,7 @@ return array( 'rsrc/css/application/uiexample/example.css' => 'b4795059', 'rsrc/css/core/core.css' => '1b29ed61', 'rsrc/css/core/remarkup.css' => '9e627d41', - 'rsrc/css/core/syntax.css' => '8a16f91b', + 'rsrc/css/core/syntax.css' => '4234f572', 'rsrc/css/core/z-index.css' => '99c0f5eb', 'rsrc/css/diviner/diviner-shared.css' => '4bd263b0', 'rsrc/css/font/font-awesome.css' => '3883938a', @@ -473,7 +473,7 @@ return array( 'rsrc/js/core/behavior-linked-container.js' => '74446546', 'rsrc/js/core/behavior-more.js' => '506aa3f4', 'rsrc/js/core/behavior-object-selector.js' => 'a4af0b4a', - 'rsrc/js/core/behavior-oncopy.js' => 'de59bf15', + 'rsrc/js/core/behavior-oncopy.js' => 'ff7b3f22', 'rsrc/js/core/behavior-phabricator-nav.js' => 'f166c949', 'rsrc/js/core/behavior-phabricator-remarkup-assist.js' => '2f80333f', 'rsrc/js/core/behavior-read-only-warning.js' => 'b9109f8f', @@ -636,7 +636,7 @@ return array( 'javelin-behavior-phabricator-nav' => 'f166c949', 'javelin-behavior-phabricator-notification-example' => '29819b75', 'javelin-behavior-phabricator-object-selector' => 'a4af0b4a', - 'javelin-behavior-phabricator-oncopy' => 'de59bf15', + 'javelin-behavior-phabricator-oncopy' => 'ff7b3f22', 'javelin-behavior-phabricator-remarkup-assist' => '2f80333f', 'javelin-behavior-phabricator-reveal-content' => 'b105a3a6', 'javelin-behavior-phabricator-search-typeahead' => '1cb7d027', @@ -878,7 +878,7 @@ return array( 'sprite-login-css' => '18b368a6', 'sprite-tokens-css' => 'f1896dc5', 'syntax-default-css' => '055fc231', - 'syntax-highlighting-css' => '8a16f91b', + 'syntax-highlighting-css' => '4234f572', 'tokens-css' => 'ce5a50bd', 'typeahead-browse-css' => 'b7ed02d2', 'unhandled-exception-css' => '9ecfc00d', @@ -1222,6 +1222,9 @@ return array( 'javelin-behavior', 'javelin-uri', ), + '4234f572' => array( + 'syntax-default-css', + ), '42c7a5a7' => array( 'javelin-install', 'javelin-dom', @@ -1580,9 +1583,6 @@ return array( 'javelin-stratcom', 'javelin-install', ), - '8a16f91b' => array( - 'syntax-default-css', - ), '8ac32fd9' => array( 'javelin-behavior', 'javelin-stratcom', @@ -2010,10 +2010,6 @@ return array( 'javelin-uri', 'phabricator-notification', ), - 'de59bf15' => array( - 'javelin-behavior', - 'javelin-dom', - ), 'dfa1d313' => array( 'javelin-behavior', 'javelin-dom', @@ -2147,6 +2143,10 @@ return array( 'owners-path-editor', 'javelin-behavior', ), + 'ff7b3f22' => array( + 'javelin-behavior', + 'javelin-dom', + ), ), 'packages' => array( 'conpherence.pkg.css' => array( diff --git a/src/applications/differential/parser/DifferentialChangesetParser.php b/src/applications/differential/parser/DifferentialChangesetParser.php index f0f952328a..8ed6d80eed 100644 --- a/src/applications/differential/parser/DifferentialChangesetParser.php +++ b/src/applications/differential/parser/DifferentialChangesetParser.php @@ -189,7 +189,7 @@ final class DifferentialChangesetParser extends Phobject { return $this; } - const CACHE_VERSION = 13; + const CACHE_VERSION = 14; const CACHE_MAX_SIZE = 8e6; const ATTR_GENERATED = 'attr:generated'; @@ -568,11 +568,17 @@ final class DifferentialChangesetParser extends Phobject { private function applyIntraline(&$render, $intra, $corpus) { foreach ($render as $key => $text) { + $result = $text; + if (isset($intra[$key])) { - $render[$key] = ArcanistDiffUtils::applyIntralineDiff( - $text, + $result = ArcanistDiffUtils::applyIntralineDiff( + $result, $intra[$key]); } + + $result = $this->adjustRenderedLineForDisplay($result); + + $render[$key] = $result; } } @@ -1415,5 +1421,183 @@ final class DifferentialChangesetParser extends Phobject { $hunk_parser->setNewLineTypeMap($type_parser->getNewLineTypeMap()); } + private function adjustRenderedLineForDisplay($line) { + // IMPORTANT: We're using "str_replace()" against raw HTML here, which can + // easily become unsafe. The input HTML has already had syntax highlighting + // and intraline diff highlighting applied, so it's full of "" tags. + + static $search; + static $replace; + if ($search === null) { + $rules = $this->newSuspiciousCharacterRules(); + + $map = array(); + foreach ($rules as $key => $spec) { + $tag = phutil_tag( + 'span', + array( + 'data-copy-text' => $key, + 'class' => $spec['class'], + 'title' => $spec['title'], + ), + $spec['replacement']); + $map[$key] = phutil_string_cast($tag); + } + + $search = array_keys($map); + $replace = array_values($map); + } + + $is_html = false; + if ($line instanceof PhutilSafeHTML) { + $is_html = true; + $line = hsprintf('%s', $line); + } + + $line = phutil_string_cast($line); + + if (strpos($line, "\t") !== false) { + $line = $this->replaceTabsWithSpaces($line); + } + $line = str_replace($search, $replace, $line); + + if ($is_html) { + $line = phutil_safe_html($line); + } + + return $line; + } + + private function newSuspiciousCharacterRules() { + // The "title" attributes are cached in the database, so they're + // intentionally not wrapped in "pht(...)". + + $rules = array( + "\xE2\x80\x8B" => array( + 'title' => 'ZWS', + 'class' => 'suspicious-character', + 'replacement' => '!', + ), + "\xC2\xA0" => array( + 'title' => 'NBSP', + 'class' => 'suspicious-character', + 'replacement' => '!', + ), + "\x7F" => array( + 'title' => 'DEL (0x7F)', + 'class' => 'suspicious-character', + 'replacement' => "\xE2\x90\xA1", + ), + ); + + // Unicode defines special pictures for the control characters in the + // range between "0x00" and "0x1F". + + $control = array( + 'NULL', + 'SOH', + 'STX', + 'ETX', + 'EOT', + 'ENQ', + 'ACK', + 'BEL', + 'BS', + null, // "\t" Tab + null, // "\n" New Line + 'VT', + 'FF', + null, // "\r" Carriage Return, + 'SO', + 'SI', + 'DLE', + 'DC1', + 'DC2', + 'DC3', + 'DC4', + 'NAK', + 'SYN', + 'ETB', + 'CAN', + 'EM', + 'SUB', + 'ESC', + 'FS', + 'GS', + 'RS', + 'US', + ); + + foreach ($control as $idx => $label) { + if ($label === null) { + continue; + } + + $rules[chr($idx)] = array( + 'title' => sprintf('%s (0x%02X)', $label, $idx), + 'class' => 'suspicious-character', + 'replacement' => "\xE2\x90".chr(0x80 + $idx), + ); + } + + return $rules; + } + + private function replaceTabsWithSpaces($line) { + // TODO: This should be flexible, eventually. + $tab_width = 2; + + static $tags; + if ($tags === null) { + $tags = array(); + for ($ii = 1; $ii <= $tab_width; $ii++) { + $tag = phutil_tag( + 'span', + array( + 'data-copy-text' => "\t", + ), + str_repeat(' ', $ii)); + $tag = phutil_string_cast($tag); + $tags[$ii] = $tag; + } + } + + // If the line is particularly long, don't try to vectorize it. Use a + // faster approximation of the correct tabstop expansion instead. This + // usually still arrives at the right result. + if (strlen($line) > 256) { + return str_replace("\t", $tags[$tab_width], $line); + } + + $line = phutil_utf8v_combined($line); + $in_tag = false; + $pos = 0; + foreach ($line as $key => $char) { + if ($char === '<') { + $in_tag = true; + continue; + } + + if ($char === '>') { + $in_tag = false; + continue; + } + + if ($in_tag) { + continue; + } + + if ($char === "\t") { + $count = $tab_width - ($pos % $tab_width); + $pos += $count; + $line[$key] = $tags[$count]; + continue; + } + + $pos++; + } + + return implode('', $line); + } } diff --git a/src/applications/differential/render/DifferentialChangesetRenderer.php b/src/applications/differential/render/DifferentialChangesetRenderer.php index 866ae15ac9..b7f78b5b68 100644 --- a/src/applications/differential/render/DifferentialChangesetRenderer.php +++ b/src/applications/differential/render/DifferentialChangesetRenderer.php @@ -363,14 +363,14 @@ abstract class DifferentialChangesetRenderer extends Phobject { $undershield = $this->renderUndershieldHeader(); } - $result = $notice.$props.$undershield.$content; + $result = array( + $notice, + $props, + $undershield, + $content, + ); - // TODO: Let the user customize their tab width / display style. - // TODO: We should possibly post-process "\r" as well. - // TODO: Both these steps should happen earlier. - $result = str_replace("\t", ' ', $result); - - return phutil_safe_html($result); + return hsprintf('%s', $result); } abstract public function isOneUpRenderer(); diff --git a/src/applications/differential/render/DifferentialChangesetTestRenderer.php b/src/applications/differential/render/DifferentialChangesetTestRenderer.php index c7b35d1fb4..e2bd3f53ed 100644 --- a/src/applications/differential/render/DifferentialChangesetTestRenderer.php +++ b/src/applications/differential/render/DifferentialChangesetTestRenderer.php @@ -131,7 +131,7 @@ abstract class DifferentialChangesetTestRenderer } $out = implode("\n", $out)."\n"; - return $out; + return phutil_safe_html($out); } diff --git a/webroot/rsrc/css/core/syntax.css b/webroot/rsrc/css/core/syntax.css index cfc82da09b..90f2981ba6 100644 --- a/webroot/rsrc/css/core/syntax.css +++ b/webroot/rsrc/css/core/syntax.css @@ -29,3 +29,9 @@ span.crossreference-item { color: #222222; background: #dddddd; } + +.suspicious-character { + background: #ff7700; + color: #ffffff; + cursor: default; +} diff --git a/webroot/rsrc/js/core/behavior-oncopy.js b/webroot/rsrc/js/core/behavior-oncopy.js index e7309a1e18..b56e83ab32 100644 --- a/webroot/rsrc/js/core/behavior-oncopy.js +++ b/webroot/rsrc/js/core/behavior-oncopy.js @@ -271,6 +271,13 @@ JX.behavior('phabricator-oncopy', function() { // Otherwise, fall through and extract this node's text normally. } + if (node.getAttribute) { + var copy_text = node.getAttribute('data-copy-text'); + if (copy_text) { + return copy_text; + } + } + if (!node.childNodes || !node.childNodes.length) { return node.textContent; } From cf048f4402feb6eddb6d97dd037bab5f77d845fe Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 19 Feb 2019 14:39:08 -0800 Subject: [PATCH 092/245] Tweak some display behaviors for indent indicators Summary: Ref T13161. - Don't show ">>" when the line indentation changed but the text also changed, this is just "the line changed". - The indicator seems a little cleaner if we just reuse the existing "bright" colors, which already have colorblind colors anyway. Test Plan: Got slightly better rendering for some diffs locally. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13161 Differential Revision: https://secure.phabricator.com/D20195 --- resources/celerity/map.php | 12 ++++++------ .../postprocessor/CelerityDefaultPostprocessor.php | 2 -- .../differential/parser/DifferentialHunkParser.php | 2 +- .../css/application/differential/changeset-view.css | 6 +++--- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 98d44804d3..0fb2a50904 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -11,7 +11,7 @@ return array( 'conpherence.pkg.js' => '020aebcf', 'core.pkg.css' => 'e3c1a8f2', 'core.pkg.js' => '2cda17a4', - 'differential.pkg.css' => '249b542d', + 'differential.pkg.css' => '9f215e54', 'differential.pkg.js' => '53f8d00c', 'diffusion.pkg.css' => '42c75c37', 'diffusion.pkg.js' => '91192d85', @@ -61,7 +61,7 @@ return array( 'rsrc/css/application/dashboard/dashboard.css' => '4267d6c6', 'rsrc/css/application/diff/inline-comment-summary.css' => '81eb368d', 'rsrc/css/application/differential/add-comment.css' => '7e5900d9', - 'rsrc/css/application/differential/changeset-view.css' => 'cc3fd795', + 'rsrc/css/application/differential/changeset-view.css' => 'de570228', 'rsrc/css/application/differential/core.css' => 'bdb93065', 'rsrc/css/application/differential/phui-inline-comment.css' => '48acce5b', 'rsrc/css/application/differential/revision-comment.css' => '7dbc8d1d', @@ -541,7 +541,7 @@ return array( 'conpherence-thread-manager' => 'aec8e38c', 'conpherence-transaction-css' => '3a3f5e7e', 'd3' => 'd67475f5', - 'differential-changeset-view-css' => 'cc3fd795', + 'differential-changeset-view-css' => 'de570228', 'differential-core-view-css' => 'bdb93065', 'differential-revision-add-comment-css' => '7e5900d9', 'differential-revision-comment-css' => '7dbc8d1d', @@ -1961,9 +1961,6 @@ return array( 'javelin-util', 'phabricator-keyboard-shortcut-manager', ), - 'cc3fd795' => array( - 'phui-inline-comment-view-css', - ), 'cf32921f' => array( 'javelin-behavior', 'javelin-dom', @@ -2010,6 +2007,9 @@ return array( 'javelin-uri', 'phabricator-notification', ), + 'de570228' => array( + 'phui-inline-comment-view-css', + ), 'dfa1d313' => array( 'javelin-behavior', 'javelin-dom', diff --git a/src/applications/celerity/postprocessor/CelerityDefaultPostprocessor.php b/src/applications/celerity/postprocessor/CelerityDefaultPostprocessor.php index d848fb81e8..61f6176f15 100644 --- a/src/applications/celerity/postprocessor/CelerityDefaultPostprocessor.php +++ b/src/applications/celerity/postprocessor/CelerityDefaultPostprocessor.php @@ -199,10 +199,8 @@ final class CelerityDefaultPostprocessor 'diff.background' => '#fff', 'new-background' => 'rgba(151, 234, 151, .3)', 'new-bright' => 'rgba(151, 234, 151, .6)', - 'new-background-strong' => 'rgba(151, 234, 151, 1)', 'old-background' => 'rgba(251, 175, 175, .3)', 'old-bright' => 'rgba(251, 175, 175, .7)', - 'old-background-strong' => 'rgba(251, 175, 175, 1)', 'move-background' => '#fdf5d4', 'copy-background' => '#f1c40f', diff --git a/src/applications/differential/parser/DifferentialHunkParser.php b/src/applications/differential/parser/DifferentialHunkParser.php index d59358bc82..8667c032b7 100644 --- a/src/applications/differential/parser/DifferentialHunkParser.php +++ b/src/applications/differential/parser/DifferentialHunkParser.php @@ -288,7 +288,7 @@ final class DifferentialHunkParser extends Phobject { $o_text = $o['text']; $n_text = $n['text']; - if ($o_text !== $n_text) { + if ($o_text !== $n_text && (ltrim($o_text) === ltrim($n_text))) { $o_depth = $this->getIndentDepth($o_text, $tab_width); $n_depth = $this->getIndentDepth($n_text, $tab_width); diff --git a/webroot/rsrc/css/application/differential/changeset-view.css b/webroot/rsrc/css/application/differential/changeset-view.css index 7d578ccc03..55cc5e8fd3 100644 --- a/webroot/rsrc/css/application/differential/changeset-view.css +++ b/webroot/rsrc/css/application/differential/changeset-view.css @@ -130,13 +130,13 @@ .differential-diff td span.depth-out { background-image: url(/rsrc/image/chevron-out.png); - background-color: {$old-background-strong}; + background-color: {$old-bright}; } .differential-diff td span.depth-in { - background-position: 2px center; + background-position: 1px center; background-image: url(/rsrc/image/chevron-in.png); - background-color: {$new-background-strong}; + background-color: {$new-bright}; } From c10b283b927053eaa434e15d1d59551645641d7d Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 20 Feb 2019 09:25:44 -0800 Subject: [PATCH 093/245] Remove some ancient daemon log code Summary: Ref T13253. Long ago, daemon logs were visible in the web UI. They were removed because access to logs generally does not conform to policy rules, and may leak the existence (and sometimes contents) of hidden objects, occasionally leak credentials in certain error messages, etc. These bits and pieces were missed. Test Plan: Grepped for removed symbols. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13253 Differential Revision: https://secure.phabricator.com/D20199 --- src/__phutil_library_map__.php | 4 - .../PhabricatorDaemonsApplication.php | 1 - ...habricatorDaemonLogEventViewController.php | 47 ------- .../view/PhabricatorDaemonLogEventsView.php | 131 ------------------ 4 files changed, 183 deletions(-) delete mode 100644 src/applications/daemon/controller/PhabricatorDaemonLogEventViewController.php delete mode 100644 src/applications/daemon/view/PhabricatorDaemonLogEventsView.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index ea3bf7d32d..f4ee380cc0 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2860,8 +2860,6 @@ phutil_register_library_map(array( 'PhabricatorDaemonLog' => 'applications/daemon/storage/PhabricatorDaemonLog.php', 'PhabricatorDaemonLogEvent' => 'applications/daemon/storage/PhabricatorDaemonLogEvent.php', 'PhabricatorDaemonLogEventGarbageCollector' => 'applications/daemon/garbagecollector/PhabricatorDaemonLogEventGarbageCollector.php', - 'PhabricatorDaemonLogEventViewController' => 'applications/daemon/controller/PhabricatorDaemonLogEventViewController.php', - 'PhabricatorDaemonLogEventsView' => 'applications/daemon/view/PhabricatorDaemonLogEventsView.php', 'PhabricatorDaemonLogGarbageCollector' => 'applications/daemon/garbagecollector/PhabricatorDaemonLogGarbageCollector.php', 'PhabricatorDaemonLogListController' => 'applications/daemon/controller/PhabricatorDaemonLogListController.php', 'PhabricatorDaemonLogListView' => 'applications/daemon/view/PhabricatorDaemonLogListView.php', @@ -8725,8 +8723,6 @@ phutil_register_library_map(array( ), 'PhabricatorDaemonLogEvent' => 'PhabricatorDaemonDAO', 'PhabricatorDaemonLogEventGarbageCollector' => 'PhabricatorGarbageCollector', - 'PhabricatorDaemonLogEventViewController' => 'PhabricatorDaemonController', - 'PhabricatorDaemonLogEventsView' => 'AphrontView', 'PhabricatorDaemonLogGarbageCollector' => 'PhabricatorGarbageCollector', 'PhabricatorDaemonLogListController' => 'PhabricatorDaemonController', 'PhabricatorDaemonLogListView' => 'AphrontView', diff --git a/src/applications/daemon/application/PhabricatorDaemonsApplication.php b/src/applications/daemon/application/PhabricatorDaemonsApplication.php index a0fb77beb0..08e81d5d7e 100644 --- a/src/applications/daemon/application/PhabricatorDaemonsApplication.php +++ b/src/applications/daemon/application/PhabricatorDaemonsApplication.php @@ -45,7 +45,6 @@ final class PhabricatorDaemonsApplication extends PhabricatorApplication { '' => 'PhabricatorDaemonLogListController', '(?P[1-9]\d*)/' => 'PhabricatorDaemonLogViewController', ), - 'event/(?P[1-9]\d*)/' => 'PhabricatorDaemonLogEventViewController', 'bulk/' => array( '(?:query/(?P[^/]+)/)?' => 'PhabricatorDaemonBulkJobListController', diff --git a/src/applications/daemon/controller/PhabricatorDaemonLogEventViewController.php b/src/applications/daemon/controller/PhabricatorDaemonLogEventViewController.php deleted file mode 100644 index 208a20b9a0..0000000000 --- a/src/applications/daemon/controller/PhabricatorDaemonLogEventViewController.php +++ /dev/null @@ -1,47 +0,0 @@ -getURIData('id'); - - $event = id(new PhabricatorDaemonLogEvent())->load($id); - if (!$event) { - return new Aphront404Response(); - } - - $event_view = id(new PhabricatorDaemonLogEventsView()) - ->setEvents(array($event)) - ->setUser($request->getUser()) - ->setCombinedLog(true) - ->setShowFullMessage(true); - - $log_panel = id(new PHUIObjectBoxView()) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->appendChild($event_view); - - $daemon_id = $event->getLogID(); - - $crumbs = $this->buildApplicationCrumbs() - ->addTextCrumb( - pht('Daemon %s', $daemon_id), - $this->getApplicationURI("log/{$daemon_id}/")) - ->addTextCrumb(pht('Event %s', $event->getID())) - ->setBorder(true); - - $header = id(new PHUIHeaderView()) - ->setHeader(pht('Combined Log')) - ->setHeaderIcon('fa-file-text'); - - $view = id(new PHUITwoColumnView()) - ->setHeader($header) - ->setFooter($log_panel); - - return $this->newPage() - ->setTitle(pht('Combined Daemon Log')) - ->appendChild($view); - - } - -} diff --git a/src/applications/daemon/view/PhabricatorDaemonLogEventsView.php b/src/applications/daemon/view/PhabricatorDaemonLogEventsView.php deleted file mode 100644 index 039906e718..0000000000 --- a/src/applications/daemon/view/PhabricatorDaemonLogEventsView.php +++ /dev/null @@ -1,131 +0,0 @@ -showFullMessage = $show_full_message; - return $this; - } - - public function setEvents(array $events) { - assert_instances_of($events, 'PhabricatorDaemonLogEvent'); - $this->events = $events; - return $this; - } - - public function setCombinedLog($is_combined) { - $this->combinedLog = $is_combined; - return $this; - } - - public function render() { - $viewer = $this->getViewer(); - $rows = array(); - - foreach ($this->events as $event) { - - // Limit display log size. If a daemon gets stuck in an output loop this - // page can be like >100MB if we don't truncate stuff. Try to do cheap - // line-based truncation first, and fall back to expensive UTF-8 character - // truncation if that doesn't get things short enough. - - $message = $event->getMessage(); - $more = null; - - if (!$this->showFullMessage) { - $more_lines = null; - $more_chars = null; - $line_limit = 12; - if (substr_count($message, "\n") > $line_limit) { - $message = explode("\n", $message); - $more_lines = count($message) - $line_limit; - $message = array_slice($message, 0, $line_limit); - $message = implode("\n", $message); - } - - $char_limit = 8192; - if (strlen($message) > $char_limit) { - $message = phutil_utf8v($message); - $more_chars = count($message) - $char_limit; - $message = array_slice($message, 0, $char_limit); - $message = implode('', $message); - } - - if ($more_chars) { - $more = new PhutilNumber($more_chars); - $more = pht('Show %d more character(s)...', $more); - } else if ($more_lines) { - $more = new PhutilNumber($more_lines); - $more = pht('Show %d more line(s)...', $more); - } - - if ($more) { - $id = $event->getID(); - $more = array( - "\n...\n", - phutil_tag( - 'a', - array( - 'href' => "/daemon/event/{$id}/", - ), - $more), - ); - } - } - - $row = array( - $event->getLogType(), - phabricator_date($event->getEpoch(), $viewer), - phabricator_time($event->getEpoch(), $viewer), - array( - $message, - $more, - ), - ); - - if ($this->combinedLog) { - array_unshift( - $row, - phutil_tag( - 'a', - array( - 'href' => '/daemon/log/'.$event->getLogID().'/', - ), - pht('Daemon %s', $event->getLogID()))); - } - - $rows[] = $row; - } - - $classes = array( - '', - '', - 'right', - 'wide prewrap', - ); - - $headers = array( - 'Type', - 'Date', - 'Time', - 'Message', - ); - - if ($this->combinedLog) { - array_unshift($classes, 'pri'); - array_unshift($headers, 'Daemon'); - } - - $log_table = new AphrontTableView($rows); - $log_table->setHeaders($headers); - $log_table->setColumnClasses($classes); - - return $log_table->render(); - } - -} From a33409991c2762f78ddc025bb1aff9838f03bcd5 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 19 Feb 2019 20:52:29 -0800 Subject: [PATCH 094/245] Remove an old Differential selection behavior Summary: Ref T12822. Ref PHI878. This is some leftover code from the old selection behavior that prevented visual selection of the left side of a diff if the user clicked on the right -- basically, a much simpler attack on what ultimately landed in D20191. I think the change from `th` to `td` "broke" it so it didn't interfere with the other behavior, which is why I didn't have to remove it earlier. It's no longer necessary, in any case. Test Plan: Grepped for behavior name, selected stuff on both sides of a diff. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T12822 Differential Revision: https://secure.phabricator.com/D20196 --- resources/celerity/map.php | 16 +++------ resources/celerity/packages.php | 1 - .../DifferentialRevisionViewController.php | 2 -- .../css/application/differential/core.css | 8 ----- .../differential/behavior-user-select.js | 33 ------------------- 5 files changed, 4 insertions(+), 56 deletions(-) delete mode 100644 webroot/rsrc/js/application/differential/behavior-user-select.js diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 0fb2a50904..3a5ed56056 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -11,8 +11,8 @@ return array( 'conpherence.pkg.js' => '020aebcf', 'core.pkg.css' => 'e3c1a8f2', 'core.pkg.js' => '2cda17a4', - 'differential.pkg.css' => '9f215e54', - 'differential.pkg.js' => '53f8d00c', + 'differential.pkg.css' => '97e13037', + 'differential.pkg.js' => '67e02996', 'diffusion.pkg.css' => '42c75c37', 'diffusion.pkg.js' => '91192d85', 'maniphest.pkg.css' => '35995d6d', @@ -62,7 +62,7 @@ return array( 'rsrc/css/application/diff/inline-comment-summary.css' => '81eb368d', 'rsrc/css/application/differential/add-comment.css' => '7e5900d9', 'rsrc/css/application/differential/changeset-view.css' => 'de570228', - 'rsrc/css/application/differential/core.css' => 'bdb93065', + 'rsrc/css/application/differential/core.css' => '7300a73e', 'rsrc/css/application/differential/phui-inline-comment.css' => '48acce5b', 'rsrc/css/application/differential/revision-comment.css' => '7dbc8d1d', 'rsrc/css/application/differential/revision-history.css' => '8aa3eac5', @@ -380,7 +380,6 @@ return array( 'rsrc/js/application/diff/behavior-preview-link.js' => 'f51e9c17', 'rsrc/js/application/differential/behavior-diff-radios.js' => '925fe8cd', 'rsrc/js/application/differential/behavior-populate.js' => 'dfa1d313', - 'rsrc/js/application/differential/behavior-user-select.js' => 'e18685c0', 'rsrc/js/application/diffusion/DiffusionLocateFileSource.js' => '94243d89', 'rsrc/js/application/diffusion/behavior-audit-preview.js' => 'b7b73831', 'rsrc/js/application/diffusion/behavior-commit-branches.js' => '4b671572', @@ -542,7 +541,7 @@ return array( 'conpherence-transaction-css' => '3a3f5e7e', 'd3' => 'd67475f5', 'differential-changeset-view-css' => 'de570228', - 'differential-core-view-css' => 'bdb93065', + 'differential-core-view-css' => '7300a73e', 'differential-revision-add-comment-css' => '7e5900d9', 'differential-revision-comment-css' => '7dbc8d1d', 'differential-revision-history-css' => '8aa3eac5', @@ -596,7 +595,6 @@ return array( 'javelin-behavior-diff-preview-link' => 'f51e9c17', 'javelin-behavior-differential-diff-radios' => '925fe8cd', 'javelin-behavior-differential-populate' => 'dfa1d313', - 'javelin-behavior-differential-user-select' => 'e18685c0', 'javelin-behavior-diffusion-commit-branches' => '4b671572', 'javelin-behavior-diffusion-commit-graph' => '1c88f154', 'javelin-behavior-diffusion-locate-file' => '87428eb2', @@ -2030,11 +2028,6 @@ return array( 'javelin-dom', 'javelin-history', ), - 'e18685c0' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-stratcom', - ), 'e562708c' => array( 'javelin-install', ), @@ -2339,7 +2332,6 @@ return array( 'javelin-behavior-aphront-drag-and-drop-textarea', 'javelin-behavior-phabricator-object-selector', 'javelin-behavior-repository-crossreference', - 'javelin-behavior-differential-user-select', 'javelin-behavior-aphront-more', 'phabricator-diff-inline', 'phabricator-diff-changeset', diff --git a/resources/celerity/packages.php b/resources/celerity/packages.php index 4005e064ba..deef3633a8 100644 --- a/resources/celerity/packages.php +++ b/resources/celerity/packages.php @@ -199,7 +199,6 @@ return array( 'javelin-behavior-phabricator-object-selector', 'javelin-behavior-repository-crossreference', - 'javelin-behavior-differential-user-select', 'javelin-behavior-aphront-more', 'phabricator-diff-inline', diff --git a/src/applications/differential/controller/DifferentialRevisionViewController.php b/src/applications/differential/controller/DifferentialRevisionViewController.php index 2ac1d30eca..ba36b1da17 100644 --- a/src/applications/differential/controller/DifferentialRevisionViewController.php +++ b/src/applications/differential/controller/DifferentialRevisionViewController.php @@ -621,8 +621,6 @@ final class DifferentialRevisionViewController ->build($changesets); } - Javelin::initBehavior('differential-user-select'); - $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setSubheader($subheader) diff --git a/webroot/rsrc/css/application/differential/core.css b/webroot/rsrc/css/application/differential/core.css index 2dcc02bb18..893cfb34a8 100644 --- a/webroot/rsrc/css/application/differential/core.css +++ b/webroot/rsrc/css/application/differential/core.css @@ -16,14 +16,6 @@ margin-bottom: 8px; } -.differential-unselectable tr td:nth-of-type(1) { - -moz-user-select: -moz-none; - -khtml-user-select: none; - -webkit-user-select: none; - -ms-user-select: none; - user-select: none; -} - .differential-content-hidden { margin: 0 0 24px 0; } diff --git a/webroot/rsrc/js/application/differential/behavior-user-select.js b/webroot/rsrc/js/application/differential/behavior-user-select.js deleted file mode 100644 index 8db48b704d..0000000000 --- a/webroot/rsrc/js/application/differential/behavior-user-select.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * @provides javelin-behavior-differential-user-select - * @requires javelin-behavior - * javelin-dom - * javelin-stratcom - */ - -JX.behavior('differential-user-select', function() { - - var unselectable; - - function isOnRight(node) { - return node.previousSibling && - node.parentNode.firstChild != node.previousSibling; - } - - JX.Stratcom.listen( - 'mousedown', - null, - function(e) { - var key = 'differential-unselectable'; - if (unselectable) { - JX.DOM.alterClass(unselectable, key, false); - } - var diff = e.getNode('differential-diff'); - var td = e.getNode('tag:td'); - if (diff && td && isOnRight(td)) { - unselectable = diff; - JX.DOM.alterClass(diff, key, true); - } - }); - -}); From f1a035d5c2c655a598a00dbb682d667f891bafe4 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 20 Feb 2019 04:40:06 -0800 Subject: [PATCH 095/245] In Differential, give the "moved/copied from" gutter a more clear visual look Summary: Depends on D20196. See PHI985. When empty, the "moved/copied" gutter currently renders with the same background color as the rest of the line. This can be misleading because it makes code look more indented than it is, especially if you're unfamiliar with the tool: {F6225179} If we remove this misleading coloration, we get a white gap. This is more clear, but looks a little odd: {F6225181} Instead, give this gutter a subtle background fill in all casses, to make it more clear that it's a separate gutter region, not a part of the text diff: {F6225183} Test Plan: See screenshots. Copied text from a diff, added/removed inlines, etc. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20197 --- resources/celerity/map.php | 12 +++++------ .../DifferentialChangesetTwoUpRenderer.php | 5 ++--- .../PHUIDiffOneUpInlineCommentRowScaffold.php | 1 - .../PHUIDiffTwoUpInlineCommentRowScaffold.php | 4 ++-- .../differential/changeset-view.css | 20 ++++++++++++++----- 5 files changed, 25 insertions(+), 17 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 3a5ed56056..3188de0052 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -11,7 +11,7 @@ return array( 'conpherence.pkg.js' => '020aebcf', 'core.pkg.css' => 'e3c1a8f2', 'core.pkg.js' => '2cda17a4', - 'differential.pkg.css' => '97e13037', + 'differential.pkg.css' => 'ab23bd75', 'differential.pkg.js' => '67e02996', 'diffusion.pkg.css' => '42c75c37', 'diffusion.pkg.js' => '91192d85', @@ -61,7 +61,7 @@ return array( 'rsrc/css/application/dashboard/dashboard.css' => '4267d6c6', 'rsrc/css/application/diff/inline-comment-summary.css' => '81eb368d', 'rsrc/css/application/differential/add-comment.css' => '7e5900d9', - 'rsrc/css/application/differential/changeset-view.css' => 'de570228', + 'rsrc/css/application/differential/changeset-view.css' => 'd92bed0d', 'rsrc/css/application/differential/core.css' => '7300a73e', 'rsrc/css/application/differential/phui-inline-comment.css' => '48acce5b', 'rsrc/css/application/differential/revision-comment.css' => '7dbc8d1d', @@ -540,7 +540,7 @@ return array( 'conpherence-thread-manager' => 'aec8e38c', 'conpherence-transaction-css' => '3a3f5e7e', 'd3' => 'd67475f5', - 'differential-changeset-view-css' => 'de570228', + 'differential-changeset-view-css' => 'd92bed0d', 'differential-core-view-css' => '7300a73e', 'differential-revision-add-comment-css' => '7e5900d9', 'differential-revision-comment-css' => '7dbc8d1d', @@ -1997,6 +1997,9 @@ return array( 'javelin-util', 'phabricator-shaped-request', ), + 'd92bed0d' => array( + 'phui-inline-comment-view-css', + ), 'da15d3dc' => array( 'phui-oi-list-view-css', ), @@ -2005,9 +2008,6 @@ return array( 'javelin-uri', 'phabricator-notification', ), - 'de570228' => array( - 'phui-inline-comment-view-css', - ), 'dfa1d313' => array( 'javelin-behavior', 'javelin-dom', diff --git a/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php b/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php index 7c728c1ff7..c37655bb93 100644 --- a/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php +++ b/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php @@ -223,7 +223,7 @@ final class DifferentialChangesetTwoUpRenderer ($new_lines[$ii]['type'] == '\\'); if ($not_copied) { - $n_copy = phutil_tag('td', array('class' => "copy {$n_class}")); + $n_copy = phutil_tag('td', array('class' => 'copy')); } else { list($orig_file, $orig_line, $orig_type) = $copy_lines[$n_num]; $title = ($orig_type == '-' ? 'Moved' : 'Copied').' from '; @@ -243,8 +243,7 @@ final class DifferentialChangesetTwoUpRenderer 'msg' => $title, ), 'class' => 'copy '.$class, - ), - ''); + )); } } } diff --git a/src/infrastructure/diff/view/PHUIDiffOneUpInlineCommentRowScaffold.php b/src/infrastructure/diff/view/PHUIDiffOneUpInlineCommentRowScaffold.php index 1f8e05bc27..fe5cab8622 100644 --- a/src/infrastructure/diff/view/PHUIDiffOneUpInlineCommentRowScaffold.php +++ b/src/infrastructure/diff/view/PHUIDiffOneUpInlineCommentRowScaffold.php @@ -18,7 +18,6 @@ final class PHUIDiffOneUpInlineCommentRowScaffold $attrs = array( 'colspan' => 3, - 'class' => 'right3', 'id' => $inline->getScaffoldCellID(), ); diff --git a/src/infrastructure/diff/view/PHUIDiffTwoUpInlineCommentRowScaffold.php b/src/infrastructure/diff/view/PHUIDiffTwoUpInlineCommentRowScaffold.php index 769ad84d1f..f9bde17bf3 100644 --- a/src/infrastructure/diff/view/PHUIDiffTwoUpInlineCommentRowScaffold.php +++ b/src/infrastructure/diff/view/PHUIDiffTwoUpInlineCommentRowScaffold.php @@ -65,8 +65,7 @@ final class PHUIDiffTwoUpInlineCommentRowScaffold ); $right_attrs = array( - 'colspan' => 3, - 'class' => 'right3', + 'colspan' => 2, 'id' => ($right_side ? $right_side->getScaffoldCellID() : null), ); @@ -74,6 +73,7 @@ final class PHUIDiffTwoUpInlineCommentRowScaffold phutil_tag('td', array('class' => 'n'), $left_hidden), phutil_tag('td', $left_attrs, $left_side), phutil_tag('td', array('class' => 'n'), $right_hidden), + phutil_tag('td', array('class' => 'copy')), phutil_tag('td', $right_attrs, $right_side), ); diff --git a/webroot/rsrc/css/application/differential/changeset-view.css b/webroot/rsrc/css/application/differential/changeset-view.css index 55cc5e8fd3..6ed939a2ee 100644 --- a/webroot/rsrc/css/application/differential/changeset-view.css +++ b/webroot/rsrc/css/application/differential/changeset-view.css @@ -144,6 +144,7 @@ min-width: 0.5%; width: 0.5%; padding: 0; + background: {$lightbluebackground}; } .differential-diff td.new-copy, @@ -178,6 +179,10 @@ should always have a boring grey background. */ overflow: hidden; } +.differential-diff td + td.n { + border-left: 1px solid {$thinblueborder}; +} + .differential-diff td.n::before { content: attr(data-n); } @@ -443,10 +448,6 @@ unselectable. */ -ms-user-select: none; -webkit-user-select: none; user-select: none; -} - -.differential-diff.copy-l > tbody > tr > td, -.differential-diff.copy-r > tbody > tr > td { opacity: 0.5; } @@ -463,7 +464,7 @@ unselectable. */ -ms-user-select: none; -webkit-user-select: none; user-select: none; - opacity: 0.25; + opacity: 0.5; } .differential-diff.copy-r > tbody > tr > td:nth-child(5) { @@ -473,3 +474,12 @@ unselectable. */ user-select: auto; opacity: 1; } + +.differential-diff.copy-l > tbody > tr.inline > td, +.differential-diff.copy-r > tbody > tr.inline > td { + -moz-user-select: none; + -ms-user-select: none; + -webkit-user-select: none; + user-select: none; + opacity: 0.5; +} From 1b832564211258526065aec985c5dd0570ed4df3 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 20 Feb 2019 05:06:42 -0800 Subject: [PATCH 096/245] Don't enable the "ScopeEngine" or try to identify scope context for diffs without context Summary: Depends on D20197. Ref T13161. We currently try to build a "ScopeEngine" even for diffs with no context (e.g., `git diff` instead of `git diff -U9999`). Since we don't have any context, we won't really be able to figure out anything useful about scopes. Also, since ScopeEngine is pretty strict about what it accepts, we crash. In these cases, just don't build a ScopeEngine. Test Plan: Viewed a diff I copy/pasted with `git diff` instead of an `arc diff` / `git diff -U99999`, got a sensible diff with no context instead of a fatal. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13161 Differential Revision: https://secure.phabricator.com/D20198 --- .../render/DifferentialChangesetRenderer.php | 22 ++++++++++++++----- .../DifferentialChangesetTwoUpRenderer.php | 2 +- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/applications/differential/render/DifferentialChangesetRenderer.php b/src/applications/differential/render/DifferentialChangesetRenderer.php index b7f78b5b68..26de5cb53b 100644 --- a/src/applications/differential/render/DifferentialChangesetRenderer.php +++ b/src/applications/differential/render/DifferentialChangesetRenderer.php @@ -33,7 +33,7 @@ abstract class DifferentialChangesetRenderer extends Phobject { private $canMarkDone; private $objectOwnerPHID; private $highlightingDisabled; - private $scopeEngine; + private $scopeEngine = false; private $depthOnlyLines; private $oldFile = false; @@ -677,13 +677,23 @@ abstract class DifferentialChangesetRenderer extends Phobject { return $views; } - final protected function getScopeEngine() { - if (!$this->scopeEngine) { - $line_map = $this->getNewLineTextMap(); + if ($this->scopeEngine === false) { + $hunk_starts = $this->getHunkStartLines(); - $scope_engine = id(new PhabricatorDiffScopeEngine()) - ->setLineTextMap($line_map); + // If this change is missing context, don't try to identify scopes, since + // we won't really be able to get anywhere. + $has_multiple_hunks = (count($hunk_starts) > 1); + $has_offset_hunks = (head_key($hunk_starts) != 1); + $missing_context = ($has_multiple_hunks || $has_offset_hunks); + + if ($missing_context) { + $scope_engine = null; + } else { + $line_map = $this->getNewLineTextMap(); + $scope_engine = id(new PhabricatorDiffScopeEngine()) + ->setLineTextMap($line_map); + } $this->scopeEngine = $scope_engine; } diff --git a/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php b/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php index c37655bb93..7efd29519e 100644 --- a/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php +++ b/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php @@ -94,7 +94,7 @@ final class DifferentialChangesetTwoUpRenderer $context_text = null; $context_line = null; - if (!$is_last_block) { + if (!$is_last_block && $scope_engine) { $target_line = $new_lines[$ii + $len]['line']; $context_line = $scope_engine->getScopeStart($target_line); if ($context_line !== null) { From abc26aa96f4cfa31ecf6ffda83e4c3a948f21eb4 Mon Sep 17 00:00:00 2001 From: Austin McKinley Date: Wed, 20 Feb 2019 10:24:33 -0800 Subject: [PATCH 097/245] Track total time from task creation to task archival Summary: Ref T5401. Depends on D20201. Add timestamps to worker tasks to track task creation, and pass that through to archive tasks. This lets us measure the total time the task spent in the queue, not just the duration it was actually running. Also displays this information in the daemon status console; see screenshot: {F6225726} Test Plan: Stopped daemons, ran `bin/search index --all --background` to create lots of tasks, restarted daemons, observed expected values for `dateCreated` and `epochArchived` in the archive worker table. Also tested the changes to `unarchiveTask` by forcing a search task to permanently fail and then `bin/worker retry`ing it. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin, epriestley, PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T5401 Differential Revision: https://secure.phabricator.com/D20200 --- .../20190220.daemon_worker.completed.01.sql | 2 ++ .../20190220.daemon_worker.completed.02.sql | 3 +++ .../PhabricatorDaemonConsoleController.php | 25 +++++++++++++++++-- .../storage/PhabricatorWorkerActiveTask.php | 5 ++-- .../storage/PhabricatorWorkerArchiveTask.php | 3 +++ 5 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 resources/sql/autopatches/20190220.daemon_worker.completed.01.sql create mode 100644 resources/sql/autopatches/20190220.daemon_worker.completed.02.sql diff --git a/resources/sql/autopatches/20190220.daemon_worker.completed.01.sql b/resources/sql/autopatches/20190220.daemon_worker.completed.01.sql new file mode 100644 index 0000000000..37f5a89bba --- /dev/null +++ b/resources/sql/autopatches/20190220.daemon_worker.completed.01.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_worker.worker_archivetask + ADD archivedEpoch INT UNSIGNED NULL; diff --git a/resources/sql/autopatches/20190220.daemon_worker.completed.02.sql b/resources/sql/autopatches/20190220.daemon_worker.completed.02.sql new file mode 100644 index 0000000000..f0040576a9 --- /dev/null +++ b/resources/sql/autopatches/20190220.daemon_worker.completed.02.sql @@ -0,0 +1,3 @@ +ALTER TABLE {$NAMESPACE}_worker.worker_activetask + ADD dateCreated int unsigned NOT NULL, + ADD dateModified int unsigned NOT NULL; diff --git a/src/applications/daemon/controller/PhabricatorDaemonConsoleController.php b/src/applications/daemon/controller/PhabricatorDaemonConsoleController.php index a70cde04c4..421008082f 100644 --- a/src/applications/daemon/controller/PhabricatorDaemonConsoleController.php +++ b/src/applications/daemon/controller/PhabricatorDaemonConsoleController.php @@ -31,6 +31,7 @@ final class PhabricatorDaemonConsoleController $completed_info[$class] = array( 'n' => 0, 'duration' => 0, + 'queueTime' => 0, ); } $completed_info[$class]['n']++; @@ -41,16 +42,33 @@ final class PhabricatorDaemonConsoleController // compute utilization. $usage_total += $lease_overhead + ($duration / 1000000); $usage_start = min($usage_start, $completed_task->getDateModified()); + + $date_archived = $completed_task->getArchivedEpoch(); + $queue_seconds = $date_archived - $completed_task->getDateCreated(); + + // Don't measure queue time for tasks that completed in the same + // epoch-second they were created in. + if ($queue_seconds > 0) { + $sec_in_us = phutil_units('1 second in microseconds'); + $queue_us = $queue_seconds * $sec_in_us; + $queue_exclusive_us = $queue_us - $duration; + $queue_exclusive_seconds = $queue_exclusive_us / $sec_in_us; + $rounded = floor($queue_exclusive_seconds); + $completed_info[$class]['queueTime'] += $rounded; + } } $completed_info = isort($completed_info, 'n'); $rows = array(); foreach ($completed_info as $class => $info) { + $duration_avg = new PhutilNumber((int)($info['duration'] / $info['n'])); + $queue_avg = new PhutilNumber((int)($info['queueTime'] / $info['n'])); $rows[] = array( $class, number_format($info['n']), - pht('%s us', new PhutilNumber((int)($info['duration'] / $info['n']))), + pht('%s us', $duration_avg), + pht('%s s', $queue_avg), ); } @@ -98,6 +116,7 @@ final class PhabricatorDaemonConsoleController phutil_tag('em', array(), pht('Queue Utilization (Approximate)')), sprintf('%.1f%%', 100 * $used_time), null, + null, ); } @@ -108,13 +127,15 @@ final class PhabricatorDaemonConsoleController array( pht('Class'), pht('Count'), - pht('Avg'), + pht('Average Duration'), + pht('Average Queue Time'), )); $completed_table->setColumnClasses( array( 'wide', 'n', 'n', + 'n', )); $completed_panel = id(new PHUIObjectBoxView()) diff --git a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php index b6bd462df7..7139e39ac3 100644 --- a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php +++ b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerActiveTask.php @@ -12,7 +12,6 @@ final class PhabricatorWorkerActiveTask extends PhabricatorWorkerTask { $config = array( self::CONFIG_IDS => self::IDS_COUNTER, - self::CONFIG_TIMESTAMPS => false, self::CONFIG_KEY_SCHEMA => array( 'taskClass' => array( 'columns' => array('taskClass'), @@ -118,7 +117,9 @@ final class PhabricatorWorkerActiveTask extends PhabricatorWorkerTask { ->setPriority($this->getPriority()) ->setObjectPHID($this->getObjectPHID()) ->setResult($result) - ->setDuration($duration); + ->setDuration($duration) + ->setDateCreated($this->getDateCreated()) + ->setArchivedEpoch(PhabricatorTime::getNow()); // NOTE: This deletes the active task (this object)! $archive->save(); diff --git a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerArchiveTask.php b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerArchiveTask.php index 0062d07a84..25a453b47b 100644 --- a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerArchiveTask.php +++ b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerArchiveTask.php @@ -8,6 +8,7 @@ final class PhabricatorWorkerArchiveTask extends PhabricatorWorkerTask { protected $duration; protected $result; + protected $archivedEpoch; protected function getConfiguration() { $parent = parent::getConfiguration(); @@ -22,6 +23,7 @@ final class PhabricatorWorkerArchiveTask extends PhabricatorWorkerTask { $config[self::CONFIG_COLUMN_SCHEMA] = array( 'result' => 'uint32', 'duration' => 'uint64', + 'archivedEpoch' => 'epoch?', ) + $config[self::CONFIG_COLUMN_SCHEMA]; $config[self::CONFIG_KEY_SCHEMA] = array( @@ -85,6 +87,7 @@ final class PhabricatorWorkerArchiveTask extends PhabricatorWorkerTask { ->setDataID($this->getDataID()) ->setPriority($this->getPriority()) ->setObjectPHID($this->getObjectPHID()) + ->setDateCreated($this->getDateCreated()) ->insert(); $this->setDataID(null); From 90064a350a027eb8b3e37323deb636db96664af6 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 22 Feb 2019 14:54:13 -0800 Subject: [PATCH 098/245] Fix URI construction of typeahead browse "more" pager Summary: Ref T13251. See . Test Plan: - With more than 100 projects (or, set `$limit = 3`)... - Edit a task, then click the "Browse" magnifying glass icon next to the "Tags" typeahead. - Before change: fatal on 'q' being null. - After change: no fatal. Reviewers: amckinley, 20after4 Reviewed By: amckinley Maniphest Tasks: T13251 Differential Revision: https://secure.phabricator.com/D20204 --- ...ricatorTypeaheadModularDatasourceController.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php b/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php index efc9ea5f65..2d55c5f663 100644 --- a/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php +++ b/src/applications/typeahead/controller/PhabricatorTypeaheadModularDatasourceController.php @@ -127,10 +127,20 @@ final class PhabricatorTypeaheadModularDatasourceController if (($offset + (2 * $limit)) < $hard_limit) { $next_uri = id(new PhutilURI($request->getRequestURI())) ->replaceQueryParam('offset', $offset + $limit) - ->replaceQueryParam('q', $query) - ->replaceQueryParam('raw', $raw_query) ->replaceQueryParam('format', 'html'); + if ($query !== null) { + $next_uri->replaceQueryParam('q', $query); + } else { + $next_uri->removeQueryParam('q'); + } + + if ($raw_query !== null) { + $next_uri->replaceQueryParam('raw', $raw_query); + } else { + $next_uri->removeQueryParam('raw'); + } + $next_link = javelin_tag( 'a', array( From 701a9bc339b9d419326a62e85ef13666b08046cd Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 22 Feb 2019 16:28:43 -0800 Subject: [PATCH 099/245] Fix Facebook login on mobile violating CSP after form redirect Summary: Fixes T13254. See that task for details. Test Plan: Used iOS Simulator to do a login locally, didn't get blocked. Verified CSP includes "m.facebook.com". Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13254 Differential Revision: https://secure.phabricator.com/D20206 --- .../PhabricatorFacebookAuthProvider.php | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/applications/auth/provider/PhabricatorFacebookAuthProvider.php b/src/applications/auth/provider/PhabricatorFacebookAuthProvider.php index e3e1fb43e5..67840727e8 100644 --- a/src/applications/auth/provider/PhabricatorFacebookAuthProvider.php +++ b/src/applications/auth/provider/PhabricatorFacebookAuthProvider.php @@ -47,6 +47,14 @@ final class PhabricatorFacebookAuthProvider return 'Facebook'; } + protected function getContentSecurityPolicyFormActions() { + return array( + // See T13254. After login with a mobile device, Facebook may redirect + // to the mobile site. + 'https://m.facebook.com/', + ); + } + public function readFormValuesFromProvider() { $require_secure = $this->getProviderConfig()->getProperty( self::KEY_REQUIRE_SECURE); @@ -114,15 +122,4 @@ final class PhabricatorFacebookAuthProvider return parent::renderConfigPropertyTransactionTitle($xaction); } - public static function getFacebookApplicationID() { - $providers = PhabricatorAuthProvider::getAllProviders(); - $fb_provider = idx($providers, 'facebook:facebook.com'); - if (!$fb_provider) { - return null; - } - - return $fb_provider->getProviderConfig()->getProperty( - self::PROPERTY_APP_ID); - } - } From 01d0fc443afea39e573226c42fbf81f10f4f0f37 Mon Sep 17 00:00:00 2001 From: Ariel Yang Date: Sun, 24 Feb 2019 13:37:14 +0000 Subject: [PATCH 100/245] Fix a typo Summary: Fix a type Test Plan: No need. Reviewers: #blessed_reviewers, amckinley Reviewed By: #blessed_reviewers, amckinley Subscribers: amckinley, epriestley Differential Revision: https://secure.phabricator.com/D20203 --- src/applications/auth/engine/PhabricatorAuthInviteEngine.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/applications/auth/engine/PhabricatorAuthInviteEngine.php b/src/applications/auth/engine/PhabricatorAuthInviteEngine.php index f1cb45483e..70fc03345c 100644 --- a/src/applications/auth/engine/PhabricatorAuthInviteEngine.php +++ b/src/applications/auth/engine/PhabricatorAuthInviteEngine.php @@ -147,7 +147,7 @@ final class PhabricatorAuthInviteEngine extends Phobject { // no address. Users can use password recovery to access the other // account if they really control the address. throw id(new PhabricatorAuthInviteAccountException( - pht('Wrong Acount'), + pht('Wrong Account'), pht( 'You are logged in as %s, but the email address you just '. 'clicked a link from is already the primary email address '. From 66161feb13c47fa93f5e9d812a66c676e497177c Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 25 Feb 2019 04:50:37 -0800 Subject: [PATCH 101/245] Fix a URI construction exception when filtering the Maniphest Burnup chart by project Summary: See . Test Plan: Viewed Maniphest burnup chart, filtered by project: no more URI construction exception. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20207 --- .../controller/ManiphestReportController.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/applications/maniphest/controller/ManiphestReportController.php b/src/applications/maniphest/controller/ManiphestReportController.php index 77bd6c0d59..40498e6e40 100644 --- a/src/applications/maniphest/controller/ManiphestReportController.php +++ b/src/applications/maniphest/controller/ManiphestReportController.php @@ -13,10 +13,19 @@ final class ManiphestReportController extends ManiphestController { $project = head($request->getArr('set_project')); $project = nonempty($project, null); - $uri = $uri->alter('project', $project); + + if ($project !== null) { + $uri->replaceQueryParam('project', $project); + } else { + $uri->removeQueryParam('project'); + } $window = $request->getStr('set_window'); - $uri = $uri->alter('window', $window); + if ($window !== null) { + $uri->replaceQueryParam('window', $window); + } else { + $uri->removeQueryParam('window'); + } return id(new AphrontRedirectResponse())->setURI($uri); } From 767afd1780fd92829073754cc334ebbcc156515d Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 25 Feb 2019 04:58:57 -0800 Subject: [PATCH 102/245] Support an "authorPHIDs" constraint for "transaction.search" Summary: Ref T13255. The "transaction.search" API method currently does not support author constraints, but this is a reasonable thing to support. Test Plan: Queried transactions by author, hit the error cases. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13255 Differential Revision: https://secure.phabricator.com/D20208 --- .../TransactionSearchConduitAPIMethod.php | 59 +++++++++++++------ 1 file changed, 42 insertions(+), 17 deletions(-) diff --git a/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php b/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php index 0edc0b3f5a..0614f1d4ed 100644 --- a/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php +++ b/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php @@ -73,24 +73,8 @@ final class TransactionSearchConduitAPIMethod ->setViewer($viewer); $constraints = $request->getValue('constraints', array()); - PhutilTypeSpec::checkMap( - $constraints, - array( - 'phids' => 'optional list', - )); - $with_phids = idx($constraints, 'phids'); - - if ($with_phids === array()) { - throw new Exception( - pht( - 'Constraint "phids" to "transaction.search" requires nonempty list, '. - 'empty list provided.')); - } - - if ($with_phids) { - $xaction_query->withPHIDs($with_phids); - } + $xaction_query = $this->applyConstraints($constraints, $xaction_query); $xactions = $xaction_query->executeWithCursorPager($pager); @@ -240,4 +224,45 @@ final class TransactionSearchConduitAPIMethod return $this->addPagerResults($results, $pager); } + + private function applyConstraints( + array $constraints, + PhabricatorApplicationTransactionQuery $query) { + + PhutilTypeSpec::checkMap( + $constraints, + array( + 'phids' => 'optional list', + 'authorPHIDs' => 'optional list', + )); + + $with_phids = idx($constraints, 'phids'); + + if ($with_phids === array()) { + throw new Exception( + pht( + 'Constraint "phids" to "transaction.search" requires nonempty list, '. + 'empty list provided.')); + } + + if ($with_phids) { + $query->withPHIDs($with_phids); + } + + $with_authors = idx($constraints, 'authorPHIDs'); + if ($with_authors === array()) { + throw new Exception( + pht( + 'Constraint "authorPHIDs" to "transaction.search" requires '. + 'nonempty list, empty list provided.')); + } + + if ($with_authors) { + $query->withAuthorPHIDs($with_authors); + } + + return $query; + } + + } From f61e825905c1b30a105e74b1c22f290cc3fca0e6 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 25 Feb 2019 06:18:53 -0800 Subject: [PATCH 103/245] Make the Diffusion warning about "svnlook" and PATH more clear Summary: See for discussion. The UI currently shows a misleading warning that looks like "found svnlook; can't find svnlook". It actually means "found svnlook, but when Subversion wipes PATH before executing commit hooks, we will no longer be able to find it". Test Plan: {F6240967} Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20210 --- .../DiffusionRepositoryBasicsManagementPanel.php | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/applications/diffusion/management/DiffusionRepositoryBasicsManagementPanel.php b/src/applications/diffusion/management/DiffusionRepositoryBasicsManagementPanel.php index bcf3ad6d74..d585c5774d 100644 --- a/src/applications/diffusion/management/DiffusionRepositoryBasicsManagementPanel.php +++ b/src/applications/diffusion/management/DiffusionRepositoryBasicsManagementPanel.php @@ -444,13 +444,15 @@ final class DiffusionRepositoryBasicsManagementPanel id(new PHUIStatusItemView()) ->setIcon(PHUIStatusItemView::ICON_WARNING, 'red') ->setTarget( - pht('Missing Binary %s', phutil_tag('tt', array(), $binary))) - ->setNote(pht( - 'Unable to find this binary in `%s`. '. - 'You need to configure %s and include %s.', - 'environment.append-paths', - $this->getEnvConfigLink(), - $path))); + pht('Commit Hooks: %s', phutil_tag('tt', array(), $binary))) + ->setNote( + pht( + 'The directory containing the "svnlook" binary is not '. + 'listed in "environment.append-paths", so commit hooks '. + '(which execute with an empty "PATH") will not be able to '. + 'find "svnlook". Add `%s` to %s.', + $path, + $this->getEnvConfigLink()))); } } } From 83aba7b01cb155d483c3566aa4997c4c996b6745 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 25 Feb 2019 05:32:21 -0800 Subject: [PATCH 104/245] Enrich the "change project tags" transaction in "transaction.search" Summary: Depends on D20208. Ref T13255. See that task for some long-winded discussion and rationale. Short version: - This is a list of operations instead of a list of old/new PHIDs because of scalability issues for large lists (T13056). - This is a fairly verbose list (instead of, for example, the more concise internal map we sometimes use with "+" and "-" as keys) to try to make the structure obvious and extensible in the future. - The "add" and "remove" echo the `*.edit` operations. Test Plan: Called `transaction.search` on an object with project tag changes, saw them in the results. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13255 Differential Revision: https://secure.phabricator.com/D20209 --- .../TransactionSearchConduitAPIMethod.php | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php b/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php index 0614f1d4ed..362b490a42 100644 --- a/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php +++ b/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php @@ -202,6 +202,14 @@ final class TransactionSearchConduitAPIMethod case PhabricatorTransactions::TYPE_CREATE: $type = 'create'; break; + case PhabricatorTransactions::TYPE_EDGE: + switch ($xaction->getMetadataValue('edge:type')) { + case PhabricatorProjectObjectHasProjectEdgeType::EDGECONST: + $type = 'projects'; + $fields = $this->newEdgeTransactionFields($xaction); + break; + } + break; } } @@ -264,5 +272,29 @@ final class TransactionSearchConduitAPIMethod return $query; } + private function newEdgeTransactionFields( + PhabricatorApplicationTransaction $xaction) { + + $record = PhabricatorEdgeChangeRecord::newFromTransaction($xaction); + + $operations = array(); + foreach ($record->getAddedPHIDs() as $phid) { + $operations[] = array( + 'operation' => 'add', + 'phid' => $phid, + ); + } + + foreach ($record->getRemovedPHIDs() as $phid) { + $operations[] = array( + 'operation' => 'remove', + 'phid' => $phid, + ); + } + + return array( + 'operations' => $operations, + ); + } } From d1546209c53613163baa72912ad02fa92a683626 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 25 Feb 2019 07:10:29 -0800 Subject: [PATCH 105/245] Expand documentation for "transaction.search" Summary: Depends on D20209. Ref T13255. It would probably be nice to make this into a "real" `*.search` API method some day, but at least document the features for now. Test Plan: Read documentation. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13255 Differential Revision: https://secure.phabricator.com/D20211 --- .../conduit/method/ConduitAPIMethod.php | 15 +++++ .../PhabricatorSearchEngineAPIMethod.php | 30 +++------- .../TransactionSearchConduitAPIMethod.php | 55 +++++++++++++++++-- 3 files changed, 72 insertions(+), 28 deletions(-) diff --git a/src/applications/conduit/method/ConduitAPIMethod.php b/src/applications/conduit/method/ConduitAPIMethod.php index 05831a782d..0fbfaa2fc3 100644 --- a/src/applications/conduit/method/ConduitAPIMethod.php +++ b/src/applications/conduit/method/ConduitAPIMethod.php @@ -409,4 +409,19 @@ abstract class ConduitAPIMethod $capability); } + final protected function newRemarkupDocumentationView($remarkup) { + $viewer = $this->getViewer(); + + $view = new PHUIRemarkupView($viewer, $remarkup); + + $view->setRemarkupOptions( + array( + PHUIRemarkupView::OPTION_PRESERVE_LINEBREAKS => false, + )); + + return id(new PHUIBoxView()) + ->appendChild($view) + ->addPadding(PHUI::PADDING_LARGE); + } + } diff --git a/src/applications/search/engine/PhabricatorSearchEngineAPIMethod.php b/src/applications/search/engine/PhabricatorSearchEngineAPIMethod.php index 235d74d6f3..510ad91864 100644 --- a/src/applications/search/engine/PhabricatorSearchEngineAPIMethod.php +++ b/src/applications/search/engine/PhabricatorSearchEngineAPIMethod.php @@ -144,7 +144,7 @@ EOTEXT ->setHeaderText(pht('Builtin and Saved Queries')) ->setCollapsed(true) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->appendChild($this->buildRemarkup($info)) + ->appendChild($this->newRemarkupDocumentationView($info)) ->appendChild($table); } @@ -223,7 +223,7 @@ EOTEXT ); if ($constants) { - $constant_lists[] = $this->buildRemarkup( + $constant_lists[] = $this->newRemarkupDocumentationView( pht( 'Constants supported by the `%s` constraint:', 'statuses')); @@ -283,7 +283,7 @@ EOTEXT ->setHeaderText(pht('Custom Query Constraints')) ->setCollapsed(true) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->appendChild($this->buildRemarkup($info)) + ->appendChild($this->newRemarkupDocumentationView($info)) ->appendChild($table) ->appendChild($constant_lists); } @@ -391,9 +391,9 @@ EOTEXT ->setHeaderText(pht('Result Ordering')) ->setCollapsed(true) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->appendChild($this->buildRemarkup($orders_info)) + ->appendChild($this->newRemarkupDocumentationView($orders_info)) ->appendChild($orders_table) - ->appendChild($this->buildRemarkup($columns_info)) + ->appendChild($this->newRemarkupDocumentationView($columns_info)) ->appendChild($columns_table); } @@ -472,7 +472,7 @@ EOTEXT ->setHeaderText(pht('Object Fields')) ->setCollapsed(true) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->appendChild($this->buildRemarkup($info)) + ->appendChild($this->newRemarkupDocumentationView($info)) ->appendChild($table); } @@ -562,7 +562,7 @@ EOTEXT ->setHeaderText(pht('Attachments')) ->setCollapsed(true) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->appendChild($this->buildRemarkup($info)) + ->appendChild($this->newRemarkupDocumentationView($info)) ->appendChild($table); } @@ -633,21 +633,7 @@ EOTEXT ->setHeaderText(pht('Paging and Limits')) ->setCollapsed(true) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->appendChild($this->buildRemarkup($info)); + ->appendChild($this->newRemarkupDocumentationView($info)); } - private function buildRemarkup($remarkup) { - $viewer = $this->getViewer(); - - $view = new PHUIRemarkupView($viewer, $remarkup); - - $view->setRemarkupOptions( - array( - PHUIRemarkupView::OPTION_PRESERVE_LINEBREAKS => false, - )); - - return id(new PHUIBoxView()) - ->appendChild($view) - ->addPadding(PHUI::PADDING_LARGE); - } } diff --git a/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php b/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php index 362b490a42..4ab5de519e 100644 --- a/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php +++ b/src/applications/transactions/conduit/TransactionSearchConduitAPIMethod.php @@ -8,15 +8,58 @@ final class TransactionSearchConduitAPIMethod } public function getMethodDescription() { - return pht('Read transactions for an object.'); + return pht('Read transactions and comments for an object.'); } - public function getMethodStatus() { - return self::METHOD_STATUS_UNSTABLE; - } + public function getMethodDocumentation() { + $markup = pht(<<.// Find specific transactions by PHID. This + is most likely to be useful if you're responding to a webhook notification + and want to inspect only the related events. + - `authorPHIDs` //Optional list.// Find transactions with particular + authors. + +Transaction Format +================== + +Each transaction has custom data describing what the transaction did. The +format varies from transaction to transaction. The easiest way to figure out +exactly what a particular transaction looks like is to make the associated kind +of edit to a test object, then query that object. + +Not all transactions have data: by default, transactions have a `null` "type" +and no additional data. This API does not expose raw transaction data because +some of it is internal, oddly named, misspelled, confusing, not useful, or +could create security or policy problems to expose directly. + +New transactions are exposed (with correctly spelled, comprehensible types and +useful, reasonable fields) as we become aware of use cases for them. + +EOREMARKUP + ); + + $markup = $this->newRemarkupDocumentationView($markup); + + return id(new PHUIObjectBoxView()) + ->setCollapsed(true) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setHeaderText(pht('Method Details')) + ->appendChild($markup); } protected function defineParamTypes() { From dc9aaa0fc2bf41ed01e4beeeca98c442c486fe8e Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 25 Feb 2019 12:41:57 -0800 Subject: [PATCH 106/245] Fix a stray "%Q" warning when hiding/showing inline comments Summary: See PHI1095. Test Plan: Viewed a revision, toggled hide/show on inline comments. Before: warning in logs; after: smooth sailing. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20212 --- .../controller/DifferentialInlineCommentEditController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/applications/differential/controller/DifferentialInlineCommentEditController.php b/src/applications/differential/controller/DifferentialInlineCommentEditController.php index 9741cc93ee..1de156a9b7 100644 --- a/src/applications/differential/controller/DifferentialInlineCommentEditController.php +++ b/src/applications/differential/controller/DifferentialInlineCommentEditController.php @@ -204,9 +204,9 @@ final class DifferentialInlineCommentEditController queryfx( $conn_w, - 'INSERT IGNORE INTO %T (userPHID, commentID) VALUES %Q', + 'INSERT IGNORE INTO %T (userPHID, commentID) VALUES %LQ', $table->getTableName(), - implode(', ', $sql)); + $sql); } protected function showComments(array $ids) { From 814e6d2de927a7b2137d7198e9377382ee8d497e Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 25 Feb 2019 19:13:55 -0800 Subject: [PATCH 107/245] Add more type checking to transactions queued by Herald Summary: See PHI1096. Depends on D20213. An install is reporting a hard-to-reproduce issue where a non-transaction gets queued by Herald somehow. This might be in third-party code. Sprinkle the relevant parts of the code with `final` and type checking to try to catch the problem before it causes a fatal we can't pull a stack trace out of. Test Plan: Poked around locally (e.g., edited revisions to cause Herald to trigger), but hard to know if this will do what it's supposed to or not without deploying and seeing if it catches anything. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20214 --- .../editor/DifferentialDiffEditor.php | 9 -------- .../herald/adapter/HeraldAdapter.php | 21 +++++++++++++++---- ...habricatorApplicationTransactionEditor.php | 9 ++++++-- 3 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/applications/differential/editor/DifferentialDiffEditor.php b/src/applications/differential/editor/DifferentialDiffEditor.php index 261f6f1598..e78e08d808 100644 --- a/src/applications/differential/editor/DifferentialDiffEditor.php +++ b/src/applications/differential/editor/DifferentialDiffEditor.php @@ -208,15 +208,6 @@ final class DifferentialDiffEditor return $adapter; } - protected function didApplyHeraldRules( - PhabricatorLiskDAO $object, - HeraldAdapter $adapter, - HeraldTranscript $transcript) { - - $xactions = array(); - return $xactions; - } - private function updateDiffFromDict(DifferentialDiff $diff, $dict) { $diff ->setSourcePath(idx($dict, 'sourcePath')) diff --git a/src/applications/herald/adapter/HeraldAdapter.php b/src/applications/herald/adapter/HeraldAdapter.php index a266e21f39..69f538afcc 100644 --- a/src/applications/herald/adapter/HeraldAdapter.php +++ b/src/applications/herald/adapter/HeraldAdapter.php @@ -186,15 +186,16 @@ abstract class HeraldAdapter extends Phobject { return $this->appliedTransactions; } - public function queueTransaction($transaction) { + final public function queueTransaction( + PhabricatorApplicationTransaction $transaction) { $this->queuedTransactions[] = $transaction; } - public function getQueuedTransactions() { + final public function getQueuedTransactions() { return $this->queuedTransactions; } - public function newTransaction() { + final public function newTransaction() { $object = $this->newObject(); if (!($object instanceof PhabricatorApplicationTransactionInterface)) { @@ -205,7 +206,19 @@ abstract class HeraldAdapter extends Phobject { 'PhabricatorApplicationTransactionInterface')); } - return $object->getApplicationTransactionTemplate(); + $xaction = $object->getApplicationTransactionTemplate(); + + if (!($xaction instanceof PhabricatorApplicationTransaction)) { + throw new Exception( + pht( + 'Expected object (of class "%s") to return a transaction template '. + '(of class "%s"), but it returned something else ("%s").', + get_class($object), + 'PhabricatorApplicationTransaction', + phutil_describe_type($xaction))); + } + + return $xaction; } diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index 9460dd3030..3a46784a33 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -3779,9 +3779,14 @@ abstract class PhabricatorApplicationTransactionEditor $this->mustEncrypt = $adapter->getMustEncryptReasons(); + $apply_xactions = $this->didApplyHeraldRules($object, $adapter, $xscript); + assert_instances_of($apply_xactions, 'PhabricatorApplicationTransaction'); + + $queue_xactions = $adapter->getQueuedTransactions(); + return array_merge( - $this->didApplyHeraldRules($object, $adapter, $xscript), - $adapter->getQueuedTransactions()); + array_values($apply_xactions), + array_values($queue_xactions)); } protected function didApplyHeraldRules( From b28b05342bccb9ecb3fbbfff9a29b78e4393554e Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 25 Feb 2019 20:02:10 -0800 Subject: [PATCH 108/245] Simplify one "array_keys/range" -> "phutil_is_natural_list()" in "phabricator/" Summary: Depends on D20213. Simplify this idiom. Test Plan: Squinted hard; `grep array_keys | grep range`. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20215 --- src/applications/config/type/PhabricatorSetConfigType.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/applications/config/type/PhabricatorSetConfigType.php b/src/applications/config/type/PhabricatorSetConfigType.php index 805ae50468..553ee614b8 100644 --- a/src/applications/config/type/PhabricatorSetConfigType.php +++ b/src/applications/config/type/PhabricatorSetConfigType.php @@ -43,7 +43,7 @@ final class PhabricatorSetConfigType } if ($value) { - if (array_keys($value) !== range(0, count($value) - 1)) { + if (!phutil_is_natural_list($value)) { throw $this->newException( pht( 'Option "%s" is of type "%s", and should be specified on the '. From 41c03bab39575cae180bf3f5cd3da80c3a02ff58 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 26 Feb 2019 04:32:23 -0800 Subject: [PATCH 109/245] Remove unusual "Created" element from Build Plan curtain UI Summary: Ref T13088. Build Plans currently have a "Created" date in the right-hand "Curtain" UI, but this is unusual and the creation date is evident from the timeline. It's also not obvious why anyone would care. Remove it for simplicity/consistency. I think this may have just been a placeholder during initial implementation. Test Plan: Viewed a build plan, no more "Created" element. Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13088 Differential Revision: https://secure.phabricator.com/D20216 --- .../controller/HarbormasterPlanViewController.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php index 6ebadf7a62..f91c18c5b6 100644 --- a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php @@ -263,11 +263,6 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController { ->setDisabled(!$can_run) ->setIcon('fa-play-circle')); - $curtain->addPanel( - id(new PHUICurtainPanelView()) - ->setHeaderText(pht('Created')) - ->appendChild(phabricator_datetime($plan->getDateCreated(), $viewer))); - return $curtain; } From f6ed873f1728269dc0613b02237c81f3ce18c0e2 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 26 Feb 2019 05:11:46 -0800 Subject: [PATCH 110/245] Move Harbormaster Build Plans to modular transactions Summary: Depends on D20216. Ref T13258. Bland infrastructure update to prepare for bigger things. Test Plan: Created and edited a build plan. Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13258 Differential Revision: https://secure.phabricator.com/D20217 --- src/__phutil_library_map__.php | 8 +- .../HarbormasterPlanDisableController.php | 4 +- .../HarbormasterBuildPlanEditEngine.php | 3 +- .../editor/HarbormasterBuildPlanEditor.php | 93 ++----------------- .../configuration/HarbormasterBuildPlan.php | 10 ++ .../HarbormasterBuildPlanTransaction.php | 68 +------------- .../HarbormasterBuildPlanNameTransaction.php | 46 +++++++++ ...HarbormasterBuildPlanStatusTransaction.php | 67 +++++++++++++ .../HarbormasterBuildPlanTransactionType.php | 4 + 9 files changed, 149 insertions(+), 154 deletions(-) create mode 100644 src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanNameTransaction.php create mode 100644 src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanStatusTransaction.php create mode 100644 src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanTransactionType.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index f4ee380cc0..d4696bde60 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1334,12 +1334,15 @@ phutil_register_library_map(array( 'HarbormasterBuildPlanEditEngine' => 'applications/harbormaster/editor/HarbormasterBuildPlanEditEngine.php', 'HarbormasterBuildPlanEditor' => 'applications/harbormaster/editor/HarbormasterBuildPlanEditor.php', 'HarbormasterBuildPlanNameNgrams' => 'applications/harbormaster/storage/configuration/HarbormasterBuildPlanNameNgrams.php', + 'HarbormasterBuildPlanNameTransaction' => 'applications/harbormaster/xaction/plan/HarbormasterBuildPlanNameTransaction.php', 'HarbormasterBuildPlanPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildPlanPHIDType.php', 'HarbormasterBuildPlanQuery' => 'applications/harbormaster/query/HarbormasterBuildPlanQuery.php', 'HarbormasterBuildPlanSearchAPIMethod' => 'applications/harbormaster/conduit/HarbormasterBuildPlanSearchAPIMethod.php', 'HarbormasterBuildPlanSearchEngine' => 'applications/harbormaster/query/HarbormasterBuildPlanSearchEngine.php', + 'HarbormasterBuildPlanStatusTransaction' => 'applications/harbormaster/xaction/plan/HarbormasterBuildPlanStatusTransaction.php', 'HarbormasterBuildPlanTransaction' => 'applications/harbormaster/storage/configuration/HarbormasterBuildPlanTransaction.php', 'HarbormasterBuildPlanTransactionQuery' => 'applications/harbormaster/query/HarbormasterBuildPlanTransactionQuery.php', + 'HarbormasterBuildPlanTransactionType' => 'applications/harbormaster/xaction/plan/HarbormasterBuildPlanTransactionType.php', 'HarbormasterBuildQuery' => 'applications/harbormaster/query/HarbormasterBuildQuery.php', 'HarbormasterBuildRequest' => 'applications/harbormaster/engine/HarbormasterBuildRequest.php', 'HarbormasterBuildSearchConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterBuildSearchConduitAPIMethod.php', @@ -6943,12 +6946,15 @@ phutil_register_library_map(array( 'HarbormasterBuildPlanEditEngine' => 'PhabricatorEditEngine', 'HarbormasterBuildPlanEditor' => 'PhabricatorApplicationTransactionEditor', 'HarbormasterBuildPlanNameNgrams' => 'PhabricatorSearchNgrams', + 'HarbormasterBuildPlanNameTransaction' => 'HarbormasterBuildPlanTransactionType', 'HarbormasterBuildPlanPHIDType' => 'PhabricatorPHIDType', 'HarbormasterBuildPlanQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'HarbormasterBuildPlanSearchAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'HarbormasterBuildPlanSearchEngine' => 'PhabricatorApplicationSearchEngine', - 'HarbormasterBuildPlanTransaction' => 'PhabricatorApplicationTransaction', + 'HarbormasterBuildPlanStatusTransaction' => 'HarbormasterBuildPlanTransactionType', + 'HarbormasterBuildPlanTransaction' => 'PhabricatorModularTransaction', 'HarbormasterBuildPlanTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'HarbormasterBuildPlanTransactionType' => 'PhabricatorModularTransactionType', 'HarbormasterBuildQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'HarbormasterBuildRequest' => 'Phobject', 'HarbormasterBuildSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', diff --git a/src/applications/harbormaster/controller/HarbormasterPlanDisableController.php b/src/applications/harbormaster/controller/HarbormasterPlanDisableController.php index ccf6b8986f..65a993396d 100644 --- a/src/applications/harbormaster/controller/HarbormasterPlanDisableController.php +++ b/src/applications/harbormaster/controller/HarbormasterPlanDisableController.php @@ -19,11 +19,11 @@ final class HarbormasterPlanDisableController return new Aphront404Response(); } - $plan_uri = $this->getApplicationURI('plan/'.$plan->getID().'/'); + $plan_uri = $plan->getURI(); if ($request->isFormPost()) { - $type_status = HarbormasterBuildPlanTransaction::TYPE_STATUS; + $type_status = HarbormasterBuildPlanStatusTransaction::TRANSACTIONTYPE; $v_status = $plan->isDisabled() ? HarbormasterBuildPlan::STATUS_ACTIVE diff --git a/src/applications/harbormaster/editor/HarbormasterBuildPlanEditEngine.php b/src/applications/harbormaster/editor/HarbormasterBuildPlanEditEngine.php index 11837051c3..35a417d9d4 100644 --- a/src/applications/harbormaster/editor/HarbormasterBuildPlanEditEngine.php +++ b/src/applications/harbormaster/editor/HarbormasterBuildPlanEditEngine.php @@ -82,7 +82,8 @@ final class HarbormasterBuildPlanEditEngine ->setKey('name') ->setLabel(pht('Name')) ->setIsRequired(true) - ->setTransactionType(HarbormasterBuildPlanTransaction::TYPE_NAME) + ->setTransactionType( + HarbormasterBuildPlanNameTransaction::TRANSACTIONTYPE) ->setDescription(pht('The build plan name.')) ->setConduitDescription(pht('Rename the plan.')) ->setConduitTypeDescription(pht('New plan name.')) diff --git a/src/applications/harbormaster/editor/HarbormasterBuildPlanEditor.php b/src/applications/harbormaster/editor/HarbormasterBuildPlanEditor.php index 71c9283ade..1b340b6524 100644 --- a/src/applications/harbormaster/editor/HarbormasterBuildPlanEditor.php +++ b/src/applications/harbormaster/editor/HarbormasterBuildPlanEditor.php @@ -11,100 +11,23 @@ final class HarbormasterBuildPlanEditor return pht('Harbormaster Build Plans'); } + public function getCreateObjectTitle($author, $object) { + return pht('%s created this build plan.', $author); + } + + public function getCreateObjectTitleForFeed($author, $object) { + return pht('%s created %s.', $author, $object); + } + protected function supportsSearch() { return true; } public function getTransactionTypes() { $types = parent::getTransactionTypes(); - $types[] = HarbormasterBuildPlanTransaction::TYPE_NAME; - $types[] = HarbormasterBuildPlanTransaction::TYPE_STATUS; $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; return $types; } - protected function getCustomTransactionOldValue( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - switch ($xaction->getTransactionType()) { - case HarbormasterBuildPlanTransaction::TYPE_NAME: - if ($this->getIsNewObject()) { - return null; - } - return $object->getName(); - case HarbormasterBuildPlanTransaction::TYPE_STATUS: - return $object->getPlanStatus(); - } - - return parent::getCustomTransactionOldValue($object, $xaction); - } - - protected function getCustomTransactionNewValue( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - switch ($xaction->getTransactionType()) { - case HarbormasterBuildPlanTransaction::TYPE_NAME: - return $xaction->getNewValue(); - case HarbormasterBuildPlanTransaction::TYPE_STATUS: - return $xaction->getNewValue(); - } - return parent::getCustomTransactionNewValue($object, $xaction); - } - - protected function applyCustomInternalTransaction( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - switch ($xaction->getTransactionType()) { - case HarbormasterBuildPlanTransaction::TYPE_NAME: - $object->setName($xaction->getNewValue()); - return; - case HarbormasterBuildPlanTransaction::TYPE_STATUS: - $object->setPlanStatus($xaction->getNewValue()); - return; - } - return parent::applyCustomInternalTransaction($object, $xaction); - } - - protected function applyCustomExternalTransaction( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - switch ($xaction->getTransactionType()) { - case HarbormasterBuildPlanTransaction::TYPE_NAME: - case HarbormasterBuildPlanTransaction::TYPE_STATUS: - return; - } - return parent::applyCustomExternalTransaction($object, $xaction); - } - - protected function validateTransaction( - PhabricatorLiskDAO $object, - $type, - array $xactions) { - - $errors = parent::validateTransaction($object, $type, $xactions); - - switch ($type) { - case HarbormasterBuildPlanTransaction::TYPE_NAME: - $missing = $this->validateIsEmptyTextField( - $object->getName(), - $xactions); - - if ($missing) { - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Required'), - pht('You must choose a name for your build plan.'), - last($xactions)); - - $error->setIsMissingFieldError(true); - $errors[] = $error; - } - break; - } - - return $errors; - } - - } diff --git a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php index 2e379aab23..c177c2e7b7 100644 --- a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php +++ b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php @@ -84,6 +84,16 @@ final class HarbormasterBuildPlan extends HarbormasterDAO return ($this->getPlanStatus() == self::STATUS_DISABLED); } + public function getURI() { + return urisprintf( + '/harbormaster/plan/%s/', + $this->getID()); + } + + public function getObjectName() { + return pht('Build Plan %d', $this->getID()); + } + /* -( Autoplans )---------------------------------------------------------- */ diff --git a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlanTransaction.php b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlanTransaction.php index 130471e21b..6cd286343a 100644 --- a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlanTransaction.php +++ b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlanTransaction.php @@ -1,10 +1,7 @@ getOldValue(); - $new = $this->getNewValue(); - - switch ($this->getTransactionType()) { - case self::TYPE_NAME: - if ($old === null) { - return 'fa-plus'; - } - break; - } - - return parent::getIcon(); - } - - public function getColor() { - $old = $this->getOldValue(); - $new = $this->getNewValue(); - - switch ($this->getTransactionType()) { - case self::TYPE_NAME: - if ($old === null) { - return 'green'; - } - break; - } - - return parent::getIcon(); - } - - public function getTitle() { - $old = $this->getOldValue(); - $new = $this->getNewValue(); - $author_handle = $this->renderHandleLink($this->getAuthorPHID()); - - switch ($this->getTransactionType()) { - case self::TYPE_NAME: - if ($old === null) { - return pht( - '%s created this build plan.', - $author_handle); - } else { - return pht( - '%s renamed this build plan from "%s" to "%s".', - $author_handle, - $old, - $new); - } - case self::TYPE_STATUS: - if ($new == HarbormasterBuildPlan::STATUS_DISABLED) { - return pht( - '%s disabled this build plan.', - $author_handle); - } else { - return pht( - '%s enabled this build plan.', - $author_handle); - } - } - - return parent::getTitle(); + public function getBaseTransactionClass() { + return 'HarbormasterBuildPlanTransactionType'; } } diff --git a/src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanNameTransaction.php b/src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanNameTransaction.php new file mode 100644 index 0000000000..30fdbe72ca --- /dev/null +++ b/src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanNameTransaction.php @@ -0,0 +1,46 @@ +getName(); + } + + public function applyInternalEffects($object, $value) { + $object->setName($value); + } + + public function getTitle() { + return pht( + '%s renamed this build plan from "%s" to "%s".', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + if ($this->isEmptyTextTransaction($object->getName(), $xactions)) { + $errors[] = $this->newRequiredError( + pht('You must choose a name for your build plan.')); + } + + return $errors; + } + + public function getTransactionTypeForConduit($xaction) { + return 'name'; + } + + public function getFieldValuesForConduit($xaction, $data) { + return array( + 'old' => $xaction->getOldValue(), + 'new' => $xaction->getNewValue(), + ); + } + +} diff --git a/src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanStatusTransaction.php b/src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanStatusTransaction.php new file mode 100644 index 0000000000..e1c72b4183 --- /dev/null +++ b/src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanStatusTransaction.php @@ -0,0 +1,67 @@ +getPlanStatus(); + } + + public function applyInternalEffects($object, $value) { + $object->setPlanStatus($value); + } + + public function getTitle() { + $new = $this->getNewValue(); + if ($new === HarbormasterBuildPlan::STATUS_DISABLED) { + return pht( + '%s disabled this build plan.', + $this->renderAuthor()); + } else { + return pht( + '%s enabled this build plan.', + $this->renderAuthor()); + } + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + $options = array( + HarbormasterBuildPlan::STATUS_DISABLED, + HarbormasterBuildPlan::STATUS_ACTIVE, + ); + $options = array_fuse($options); + + foreach ($xactions as $xaction) { + $new = $xaction->getNewValue(); + + if (!isset($options[$new])) { + $errors[] = $this->newInvalidError( + pht( + 'Status "%s" is not a valid build plan status. Valid '. + 'statuses are: %s.', + $new, + implode(', ', $options))); + continue; + } + + } + + return $errors; + } + + public function getTransactionTypeForConduit($xaction) { + return 'status'; + } + + public function getFieldValuesForConduit($xaction, $data) { + return array( + 'old' => $xaction->getOldValue(), + 'new' => $xaction->getNewValue(), + ); + } + +} diff --git a/src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanTransactionType.php b/src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanTransactionType.php new file mode 100644 index 0000000000..5545d1de38 --- /dev/null +++ b/src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanTransactionType.php @@ -0,0 +1,4 @@ + Date: Tue, 26 Feb 2019 05:21:35 -0800 Subject: [PATCH 111/245] Provide "harbormaster.buildplan.edit" in the API Summary: Depends on D20217. Ref T13258. Mostly for completeness. You can't edit build steps so this may not be terribly useful, but you can do bulk policy edits or whatever? Test Plan: Edited a build plan via API. Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13258 Differential Revision: https://secure.phabricator.com/D20218 --- src/__phutil_library_map__.php | 2 ++ .../HarbormasterBuildPlanEditAPIMethod.php | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 src/applications/harbormaster/conduit/HarbormasterBuildPlanEditAPIMethod.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index d4696bde60..076344ed3e 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1331,6 +1331,7 @@ phutil_register_library_map(array( 'HarbormasterBuildPlanDatasource' => 'applications/harbormaster/typeahead/HarbormasterBuildPlanDatasource.php', 'HarbormasterBuildPlanDefaultEditCapability' => 'applications/harbormaster/capability/HarbormasterBuildPlanDefaultEditCapability.php', 'HarbormasterBuildPlanDefaultViewCapability' => 'applications/harbormaster/capability/HarbormasterBuildPlanDefaultViewCapability.php', + 'HarbormasterBuildPlanEditAPIMethod' => 'applications/harbormaster/conduit/HarbormasterBuildPlanEditAPIMethod.php', 'HarbormasterBuildPlanEditEngine' => 'applications/harbormaster/editor/HarbormasterBuildPlanEditEngine.php', 'HarbormasterBuildPlanEditor' => 'applications/harbormaster/editor/HarbormasterBuildPlanEditor.php', 'HarbormasterBuildPlanNameNgrams' => 'applications/harbormaster/storage/configuration/HarbormasterBuildPlanNameNgrams.php', @@ -6943,6 +6944,7 @@ phutil_register_library_map(array( 'HarbormasterBuildPlanDatasource' => 'PhabricatorTypeaheadDatasource', 'HarbormasterBuildPlanDefaultEditCapability' => 'PhabricatorPolicyCapability', 'HarbormasterBuildPlanDefaultViewCapability' => 'PhabricatorPolicyCapability', + 'HarbormasterBuildPlanEditAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'HarbormasterBuildPlanEditEngine' => 'PhabricatorEditEngine', 'HarbormasterBuildPlanEditor' => 'PhabricatorApplicationTransactionEditor', 'HarbormasterBuildPlanNameNgrams' => 'PhabricatorSearchNgrams', diff --git a/src/applications/harbormaster/conduit/HarbormasterBuildPlanEditAPIMethod.php b/src/applications/harbormaster/conduit/HarbormasterBuildPlanEditAPIMethod.php new file mode 100644 index 0000000000..5509cf189e --- /dev/null +++ b/src/applications/harbormaster/conduit/HarbormasterBuildPlanEditAPIMethod.php @@ -0,0 +1,20 @@ + Date: Tue, 26 Feb 2019 05:37:51 -0800 Subject: [PATCH 112/245] Add a "Recent Builds" element to the Build Plan UI and tighten up a few odds and ends Summary: Depends on D20218. Ref T13258. It's somewhat cumbersome to get from build plans to related builds but this is a reasonable thing to want to do, so make it a little easier. Also clean up / standardize / hint a few things a little better. Test Plan: {F6244116} Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13258 Differential Revision: https://secure.phabricator.com/D20219 --- src/__phutil_library_map__.php | 2 + .../HarbormasterPlanViewController.php | 92 ++++++++++++++----- .../query/HarbormasterBuildSearchEngine.php | 49 ++-------- .../storage/build/HarbormasterBuild.php | 4 + .../configuration/HarbormasterBuildPlan.php | 2 +- .../view/HarbormasterBuildView.php | 67 ++++++++++++++ 6 files changed, 151 insertions(+), 65 deletions(-) create mode 100644 src/applications/harbormaster/view/HarbormasterBuildView.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 076344ed3e..b80940a25c 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1370,6 +1370,7 @@ phutil_register_library_map(array( 'HarbormasterBuildTransactionQuery' => 'applications/harbormaster/query/HarbormasterBuildTransactionQuery.php', 'HarbormasterBuildUnitMessage' => 'applications/harbormaster/storage/build/HarbormasterBuildUnitMessage.php', 'HarbormasterBuildUnitMessageQuery' => 'applications/harbormaster/query/HarbormasterBuildUnitMessageQuery.php', + 'HarbormasterBuildView' => 'applications/harbormaster/view/HarbormasterBuildView.php', 'HarbormasterBuildViewController' => 'applications/harbormaster/controller/HarbormasterBuildViewController.php', 'HarbormasterBuildWorker' => 'applications/harbormaster/worker/HarbormasterBuildWorker.php', 'HarbormasterBuildable' => 'applications/harbormaster/storage/HarbormasterBuildable.php', @@ -6999,6 +7000,7 @@ phutil_register_library_map(array( 'PhabricatorPolicyInterface', ), 'HarbormasterBuildUnitMessageQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'HarbormasterBuildView' => 'AphrontView', 'HarbormasterBuildViewController' => 'HarbormasterController', 'HarbormasterBuildWorker' => 'HarbormasterWorker', 'HarbormasterBuildable' => array( diff --git a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php index f91c18c5b6..f2ead2c3db 100644 --- a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php @@ -18,11 +18,6 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController { return new Aphront404Response(); } - $timeline = $this->buildTransactionTimeline( - $plan, - new HarbormasterBuildPlanTransactionQuery()); - $timeline->setShouldTerminate(true); - $title = $plan->getName(); $header = id(new PHUIHeaderView()) @@ -33,24 +28,30 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController { $curtain = $this->buildCurtainView($plan); - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb(pht('Plan %d', $id)); - $crumbs->setBorder(true); + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb($plan->getObjectName()) + ->setBorder(true); - list($step_list, $has_any_conflicts, $would_deadlock) = + list($step_list, $has_any_conflicts, $would_deadlock, $steps) = $this->buildStepList($plan); $error = null; - if ($would_deadlock) { - $error = pht('This build plan will deadlock when executed, due to '. - 'circular dependencies present in the build plan. '. - 'Examine the step list and resolve the deadlock.'); + if (!$steps) { + $error = pht( + 'This build plan does not have any build steps yet, so it will '. + 'not do anything when run.'); + } else if ($would_deadlock) { + $error = pht( + 'This build plan will deadlock when executed, due to circular '. + 'dependencies present in the build plan. Examine the step list '. + 'and resolve the deadlock.'); } else if ($has_any_conflicts) { // A deadlocking build will also cause all the artifacts to be // invalid, so we just skip showing this message if that's the // case. - $error = pht('This build plan has conflicts in one or more build steps. '. - 'Examine the step list and resolve the listed errors.'); + $error = pht( + 'This build plan has conflicts in one or more build steps. '. + 'Examine the step list and resolve the listed errors.'); } if ($error) { @@ -59,18 +60,28 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController { ->appendChild($error); } + $builds_view = $this->newBuildsView($plan); + + $timeline = $this->buildTransactionTimeline( + $plan, + new HarbormasterBuildPlanTransactionQuery()); + $timeline->setShouldTerminate(true); + $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setCurtain($curtain) - ->setMainColumn(array( - $error, - $step_list, - $timeline, - )); + ->setMainColumn( + array( + $error, + $step_list, + $builds_view, + $timeline, + )); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) + ->setPageObjectPHIDs(array($plan->getPHID())) ->appendChild($view); } @@ -213,7 +224,7 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController { ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($step_list); - return array($step_box, $has_any_conflicts, $is_deadlocking); + return array($step_box, $has_any_conflicts, $is_deadlocking, $steps); } private function buildCurtainView(HarbormasterBuildPlan $plan) { @@ -376,7 +387,7 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController { array $steps) { $has_conflicts = false; - if (count($step_phids) === 0) { + if (!$step_phids) { return null; } @@ -436,4 +447,41 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController { return array($ui, $has_conflicts); } + + private function newBuildsView(HarbormasterBuildPlan $plan) { + $viewer = $this->getViewer(); + + $builds = id(new HarbormasterBuildQuery()) + ->setViewer($viewer) + ->withBuildPlanPHIDs(array($plan->getPHID())) + ->setLimit(10) + ->execute(); + + $list = id(new HarbormasterBuildView()) + ->setViewer($viewer) + ->setBuilds($builds) + ->newObjectList(); + + $list->setNoDataString(pht('No recent builds.')); + + $more_href = new PhutilURI( + $this->getApplicationURI('/build/'), + array('plan' => $plan->getPHID())); + + $more_link = id(new PHUIButtonView()) + ->setTag('a') + ->setIcon('fa-list-ul') + ->setText(pht('View All Builds')) + ->setHref($more_href); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Recent Builds')) + ->addActionLink($more_link); + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->appendChild($list); + } + } diff --git a/src/applications/harbormaster/query/HarbormasterBuildSearchEngine.php b/src/applications/harbormaster/query/HarbormasterBuildSearchEngine.php index 4cf6a83701..b8140d84f6 100644 --- a/src/applications/harbormaster/query/HarbormasterBuildSearchEngine.php +++ b/src/applications/harbormaster/query/HarbormasterBuildSearchEngine.php @@ -128,49 +128,14 @@ final class HarbormasterBuildSearchEngine $viewer = $this->requireViewer(); - $buildables = mpull($builds, 'getBuildable'); - $object_phids = mpull($buildables, 'getBuildablePHID'); - $initiator_phids = mpull($builds, 'getInitiatorPHID'); - $phids = array_mergev(array($initiator_phids, $object_phids)); - $phids = array_unique(array_filter($phids)); + $list = id(new HarbormasterBuildView()) + ->setViewer($viewer) + ->setBuilds($builds) + ->newObjectList(); - $handles = $viewer->loadHandles($phids); - - $list = new PHUIObjectItemListView(); - foreach ($builds as $build) { - $id = $build->getID(); - $initiator = $handles[$build->getInitiatorPHID()]; - $buildable_object = $handles[$build->getBuildable()->getBuildablePHID()]; - - $item = id(new PHUIObjectItemView()) - ->setViewer($viewer) - ->setObject($build) - ->setObjectName(pht('Build %d', $build->getID())) - ->setHeader($build->getName()) - ->setHref($build->getURI()) - ->setEpoch($build->getDateCreated()) - ->addAttribute($buildable_object->getName()); - - if ($initiator) { - $item->addHandleIcon($initiator, $initiator->getName()); - } - - $status = $build->getBuildStatus(); - - $status_icon = HarbormasterBuildStatus::getBuildStatusIcon($status); - $status_color = HarbormasterBuildStatus::getBuildStatusColor($status); - $status_label = HarbormasterBuildStatus::getBuildStatusName($status); - - $item->setStatusIcon("{$status_icon} {$status_color}", $status_label); - - $list->addItem($item); - } - - $result = new PhabricatorApplicationSearchResultView(); - $result->setObjectList($list); - $result->setNoDataString(pht('No builds found.')); - - return $result; + return id(new PhabricatorApplicationSearchResultView()) + ->setObjectList($list) + ->setNoDataString(pht('No builds found.')); } } diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuild.php b/src/applications/harbormaster/storage/build/HarbormasterBuild.php index 602e388477..84668b79f0 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuild.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuild.php @@ -193,6 +193,10 @@ final class HarbormasterBuild extends HarbormasterDAO return HarbormasterBuildStatus::newBuildStatusObject($status_key); } + public function getObjectName() { + return pht('Build %d', $this->getID()); + } + /* -( Build Commands )----------------------------------------------------- */ diff --git a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php index c177c2e7b7..9d2266e487 100644 --- a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php +++ b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php @@ -91,7 +91,7 @@ final class HarbormasterBuildPlan extends HarbormasterDAO } public function getObjectName() { - return pht('Build Plan %d', $this->getID()); + return pht('Plan %d', $this->getID()); } diff --git a/src/applications/harbormaster/view/HarbormasterBuildView.php b/src/applications/harbormaster/view/HarbormasterBuildView.php new file mode 100644 index 0000000000..54f5abe093 --- /dev/null +++ b/src/applications/harbormaster/view/HarbormasterBuildView.php @@ -0,0 +1,67 @@ +builds = $builds; + return $this; + } + + public function getBuilds() { + return $this->builds; + } + + public function render() { + return $this->newObjectList(); + } + + public function newObjectList() { + $viewer = $this->getViewer(); + $builds = $this->getBuilds(); + + $buildables = mpull($builds, 'getBuildable'); + $object_phids = mpull($buildables, 'getBuildablePHID'); + $initiator_phids = mpull($builds, 'getInitiatorPHID'); + $phids = array_mergev(array($initiator_phids, $object_phids)); + $phids = array_unique(array_filter($phids)); + + $handles = $viewer->loadHandles($phids); + + $list = new PHUIObjectItemListView(); + foreach ($builds as $build) { + $id = $build->getID(); + $initiator = $handles[$build->getInitiatorPHID()]; + $buildable_object = $handles[$build->getBuildable()->getBuildablePHID()]; + + $item = id(new PHUIObjectItemView()) + ->setViewer($viewer) + ->setObject($build) + ->setObjectName($build->getObjectName()) + ->setHeader($build->getName()) + ->setHref($build->getURI()) + ->setEpoch($build->getDateCreated()) + ->addAttribute($buildable_object->getName()); + + if ($initiator) { + $item->addByline($initiator->renderLink()); + } + + $status = $build->getBuildStatus(); + + $status_icon = HarbormasterBuildStatus::getBuildStatusIcon($status); + $status_color = HarbormasterBuildStatus::getBuildStatusColor($status); + $status_label = HarbormasterBuildStatus::getBuildStatusName($status); + + $item->setStatusIcon("{$status_icon} {$status_color}", $status_label); + + $list->addItem($item); + } + + return $list; + } + +} From 4cc556b576abbf6ccfdc0239f30428576d80e7c4 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 28 Feb 2019 05:43:13 -0800 Subject: [PATCH 113/245] Clean up a PhutilURI "alter()" callsite in Diffusion blame Summary: See . Test Plan: Viewed blame, clicked "Skip Past This Commit". Got jumped to the right place instead of a URI exception. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20222 --- .../controller/DiffusionBrowseController.php | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/applications/diffusion/controller/DiffusionBrowseController.php b/src/applications/diffusion/controller/DiffusionBrowseController.php index fcef87b7ef..1493d658e6 100644 --- a/src/applications/diffusion/controller/DiffusionBrowseController.php +++ b/src/applications/diffusion/controller/DiffusionBrowseController.php @@ -709,8 +709,17 @@ final class DiffusionBrowseController extends DiffusionController { 'path' => $path, )); - $before_uri = $before_uri->alter('renamed', $renamed); - $before_uri = $before_uri->alter('follow', $follow); + if ($renamed === null) { + $before_uri->removeQueryParam('renamed'); + } else { + $before_uri->replaceQueryParam('renamed', $renamed); + } + + if ($follow === null) { + $before_uri->removeQueryParam('follow'); + } else { + $before_uri->replaceQueryParam('follow', $follow); + } return id(new AphrontRedirectResponse())->setURI($before_uri); } From 75dfae10118ad4c5630d7ea3046061a11303d8cb Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 28 Feb 2019 08:05:07 -0800 Subject: [PATCH 114/245] Don't require any special capabilities to apply a "closed a subtask" transaction to a parent task Summary: See PHI1059. If you close a task, we apply an "alice closed a subtask: X" transaction to its parents. This transaction is purely informative, but currently requires `CAN_EDIT` permission after T13186. However, we'd prefer to post this transaction anyway, even if: the parent is locked; or the parent is not editable by the acting user. Replace the implicit `CAN_EDIT` requirement with no requirement. (This transaction is only applied internally (by closing a subtask) and can't be applied via the API or any other channel, so this doesn't let attackers spam a bunch of bogus subtask closures all over the place or anything.) Test Plan: - Created a parent task A with subtask B. - Put task A into an "Edits Locked" status. - As a user other than the owner of A, closed B. Then: - Before: Policy exception when trying to apply the "alice closed a subtask: B" transaction to A. - After: B closed, A got a transaction despite being locked. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20223 --- .../xaction/ManiphestTaskUnblockTransaction.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/applications/maniphest/xaction/ManiphestTaskUnblockTransaction.php b/src/applications/maniphest/xaction/ManiphestTaskUnblockTransaction.php index 8833e62b79..cb6c80604d 100644 --- a/src/applications/maniphest/xaction/ManiphestTaskUnblockTransaction.php +++ b/src/applications/maniphest/xaction/ManiphestTaskUnblockTransaction.php @@ -123,4 +123,14 @@ final class ManiphestTaskUnblockTransaction return parent::shouldHideForFeed(); } + public function getRequiredCapabilities( + $object, + PhabricatorApplicationTransaction $xaction) { + + // When you close a task, we want to apply this transaction to its parents + // even if you can not edit (or even see) those parents, so don't require + // any capabilities. See PHI1059. + + return null; + } } From 27ea775fda5f9a158a7e92a20f6b3c6ba7696a39 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 28 Feb 2019 09:34:30 -0800 Subject: [PATCH 115/245] Fix a log warning when searching for ranges on custom "Date" fields Summary: See . It looks like this blames to D19126, which added some more complex constraint logic but overlooked "range" constraints, which are handled separately. Test Plan: - Added a custom "date" field to Maniphest with `"search": true`. - Executed a range query against the field. Then: - Before: Warnings about undefined indexes in the log. - After: No such warnings. Reviewers: amckinley Reviewed By: amckinley Subscribers: jbrownEP Differential Revision: https://secure.phabricator.com/D20225 --- .../query/policy/PhabricatorCursorPagedPolicyAwareQuery.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php index 9f7a69909a..773f78b3a6 100644 --- a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php +++ b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php @@ -1233,6 +1233,8 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery 'index' => $index->getIndexKey(), 'alias' => $alias, 'value' => array($min, $max), + 'data' => null, + 'constraints' => null, ); return $this; From 54006f481729afc6d96c5ae833921dcd62a37d53 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 28 Feb 2019 09:44:20 -0800 Subject: [PATCH 116/245] Stop "Mute Notifications" on Bulk Jobs from fataling Summary: See . Bulk Jobs have an "edge" table but currently do not support edge transactions. Add support. This stops "Mute Notifications" from fataling. The action probably doesn't do what the reporting user expects (it stops edits to the job object from sending notifications; it does not stop the edits the job performs from sending notifications) but I think this change puts us in a better place no matter what, even if we eventually clarify or remove this behavior. Test Plan: Clicked "Mute Notifications" on a bulk job, got an effect instead of a fatal. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20226 --- .../daemon/workers/editor/PhabricatorWorkerBulkJobEditor.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/infrastructure/daemon/workers/editor/PhabricatorWorkerBulkJobEditor.php b/src/infrastructure/daemon/workers/editor/PhabricatorWorkerBulkJobEditor.php index b23c987d6d..e94ca6dc49 100644 --- a/src/infrastructure/daemon/workers/editor/PhabricatorWorkerBulkJobEditor.php +++ b/src/infrastructure/daemon/workers/editor/PhabricatorWorkerBulkJobEditor.php @@ -15,6 +15,7 @@ final class PhabricatorWorkerBulkJobEditor $types = parent::getTransactionTypes(); $types[] = PhabricatorWorkerBulkJobTransaction::TYPE_STATUS; + $types[] = PhabricatorTransactions::TYPE_EDGE; return $types; } From bfe8f43f1a6f4eb3a30f0a74385f2b806c58be53 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 28 Feb 2019 10:36:16 -0800 Subject: [PATCH 117/245] Use "QUERY_STRING", not "REQUEST_URI", to parse raw request parameters Summary: Fixes T13260. "QUERY_STRING" and "REQUEST_URI" are similar for our purposes here, but our nginx documentation tells you to pass "QUERY_STRING" and doesn't tell you to pass "REQUEST_URI". We also use "QUERY_STRING" in a couple of other places already, and already have a setup check for it. Use "QUERY_STRING" instead of "REQUEST_URI". Test Plan: Visited `/oauth/google/?a=b`, got redirected with parameters preserved. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13260 Differential Revision: https://secure.phabricator.com/D20227 --- src/aphront/AphrontRequest.php | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/aphront/AphrontRequest.php b/src/aphront/AphrontRequest.php index 46d1266b08..48004a521f 100644 --- a/src/aphront/AphrontRequest.php +++ b/src/aphront/AphrontRequest.php @@ -591,15 +591,11 @@ final class AphrontRequest extends Phobject { } public function getRequestURI() { - $request_uri = idx($_SERVER, 'REQUEST_URI', '/'); + $uri_path = phutil_escape_uri($this->getPath()); + $uri_query = idx($_SERVER, 'QUERY_STRING', ''); - $uri = new PhutilURI($request_uri); - $uri->removeQueryParam('__path__'); - - $path = phutil_escape_uri($this->getPath()); - $uri->setPath($path); - - return $uri; + return id(new PhutilURI($uri_path.'?'.$uri_query)) + ->removeQueryParam('__path__'); } public function getAbsoluteRequestURI() { From e15fff00a6404319d01e30b0a673a1a2ce289ca9 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 1 Mar 2019 09:45:41 -0800 Subject: [PATCH 118/245] Use "LogLevel=ERROR" to try to improve "ssh" hostkey behavior without doing anything extreme/hacky Summary: Ref T13121. When you connect to a host with SSH, don't already know the host key, and don't have strict host key checking, it prints "Permanently adding host X to known hosts". This is super un-useful. In a perfect world, we'd probably always have strict host key checking, but this is a significant barrier to configuration/setup and I think not hugely important (MITM attacks against SSH hosts are hard/rare and probably not hugely valuable). I'd imagine a more realistic long term approach is likely optional host key checking. For now, try using `LogLevel=ERROR` instead of `LogLevel=quiet` to suppress this error. This should be strictly better (since at least some messages we want to see are ERROR or better), although it may not be perfect (there may be other INFO messages we would still like to see). Test Plan: - Ran `ssh -o LogLevel=... -o 'StrictHostKeyChecking=no' -o 'UserKnownHostsFile=/dev/null'` with bad credentials, for "ERROR", "quiet", and default ("INFO") log levels. - With `INFO`, got a warning about adding the key, then an error about bad credentials (bad: don't want the key warning). - With `quiet`, got nothing (bad: we want the credential error). - With `ERROR`, got no warning but did get an error (good!). Not sure this always gives us exactly what we want, but it seems like an improvement over "quiet". Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13121 Differential Revision: https://secure.phabricator.com/D20240 --- src/applications/diffusion/ssh/DiffusionSSHWorkflow.php | 4 ++-- .../drydock/interface/command/DrydockSSHCommandInterface.php | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php b/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php index b2d1d25f44..57dc83953d 100644 --- a/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php +++ b/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php @@ -127,9 +127,9 @@ abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow { // This is suppressing "added
to the list of known hosts" // messages, which are confusing and irrelevant when they arise from // proxied requests. It might also be suppressing lots of useful errors, - // of course. Ideally, we would enforce host keys eventually. + // of course. Ideally, we would enforce host keys eventually. See T13121. $options[] = '-o'; - $options[] = 'LogLevel=quiet'; + $options[] = 'LogLevel=ERROR'; // NOTE: We prefix the command with "@username", which the far end of the // connection will parse in order to act as the specified user. This diff --git a/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php b/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php index 1aab14b57b..b1eebd92a1 100644 --- a/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php +++ b/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php @@ -30,8 +30,11 @@ final class DrydockSSHCommandInterface extends DrydockCommandInterface { $full_command = call_user_func_array('csprintf', $argv); $flags = array(); + + // See T13121. Attempt to suppress the "Permanently added X to list of + // known hosts" message without suppressing anything important. $flags[] = '-o'; - $flags[] = 'LogLevel=quiet'; + $flags[] = 'LogLevel=ERROR'; $flags[] = '-o'; $flags[] = 'StrictHostKeyChecking=no'; From 9b0b50fbf4694006d622e64643e8f9d4b44cac27 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 4 Mar 2019 16:48:01 -0800 Subject: [PATCH 119/245] Give "bin/worker" flags to repeat and retry tasks Summary: See PHI1063. See PHI1114. Ref T13253. Currently, you can't `bin/worker execute` an archived task and can't `bin/worker retry` a successful task. Although it's good not to do these things by default (particularly, retrying a successful task will double its effects), there are plenty of cases where you want to re-run something for testing/development/debugging and don't care that the effect will repeat (you're in a dev environment, the effect doesn't matter, etc). Test Plan: Ran `bin/worker execute/retry` against archived/successful tasks. Got prompted to add more flags, then got re-execution. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13253 Differential Revision: https://secure.phabricator.com/D20246 --- ...ricatorWorkerManagementExecuteWorkflow.php | 49 +++++++++++++++++-- ...abricatorWorkerManagementRetryWorkflow.php | 32 ++++++++---- 2 files changed, 67 insertions(+), 14 deletions(-) diff --git a/src/infrastructure/daemon/workers/management/PhabricatorWorkerManagementExecuteWorkflow.php b/src/infrastructure/daemon/workers/management/PhabricatorWorkerManagementExecuteWorkflow.php index 2acc8452ea..f3c6be520a 100644 --- a/src/infrastructure/daemon/workers/management/PhabricatorWorkerManagementExecuteWorkflow.php +++ b/src/infrastructure/daemon/workers/management/PhabricatorWorkerManagementExecuteWorkflow.php @@ -11,23 +11,64 @@ final class PhabricatorWorkerManagementExecuteWorkflow pht( 'Execute a task explicitly. This command ignores leases, is '. 'dangerous, and may cause work to be performed twice.')) - ->setArguments($this->getTaskSelectionArguments()); + ->setArguments( + array_merge( + array( + array( + 'name' => 'retry', + 'help' => pht('Retry archived tasks.'), + ), + array( + 'name' => 'repeat', + 'help' => pht('Repeat archived, successful tasks.'), + ), + ), + $this->getTaskSelectionArguments())); } public function execute(PhutilArgumentParser $args) { $console = PhutilConsole::getConsole(); $tasks = $this->loadTasks($args); + $is_retry = $args->getArg('retry'); + $is_repeat = $args->getArg('repeat'); + foreach ($tasks as $task) { $can_execute = !$task->isArchived(); if (!$can_execute) { - $console->writeOut( + if (!$is_retry) { + $console->writeOut( + "** %s ** %s\n", + pht('ARCHIVED'), + pht( + '%s is already archived, and will not be executed. '. + 'Use "--retry" to execute archived tasks.', + $this->describeTask($task))); + continue; + } + + $result_success = PhabricatorWorkerArchiveTask::RESULT_SUCCESS; + if ($task->getResult() == $result_success) { + if (!$is_repeat) { + $console->writeOut( + "** %s ** %s\n", + pht('SUCCEEDED'), + pht( + '%s has already succeeded, and will not be retried. '. + 'Use "--repeat" to repeat successful tasks.', + $this->describeTask($task))); + continue; + } + } + + echo tsprintf( "** %s ** %s\n", pht('ARCHIVED'), pht( - '%s is already archived, and can not be executed.', + 'Unarchiving %s.', $this->describeTask($task))); - continue; + + $task = $task->unarchiveTask(); } // NOTE: This ignores leases, maybe it should respect them without diff --git a/src/infrastructure/daemon/workers/management/PhabricatorWorkerManagementRetryWorkflow.php b/src/infrastructure/daemon/workers/management/PhabricatorWorkerManagementRetryWorkflow.php index 6dbebd168d..538a70add8 100644 --- a/src/infrastructure/daemon/workers/management/PhabricatorWorkerManagementRetryWorkflow.php +++ b/src/infrastructure/daemon/workers/management/PhabricatorWorkerManagementRetryWorkflow.php @@ -10,15 +10,24 @@ final class PhabricatorWorkerManagementRetryWorkflow ->setSynopsis( pht( 'Retry selected tasks which previously failed permanently or '. - 'were cancelled. Only archived, unsuccessful tasks can be '. - 'retried.')) - ->setArguments($this->getTaskSelectionArguments()); + 'were cancelled. Only archived tasks can be retried.')) + ->setArguments( + array_merge( + array( + array( + 'name' => 'repeat', + 'help' => pht( + 'Repeat tasks which already completed successfully.'), + ), + ), + $this->getTaskSelectionArguments())); } public function execute(PhutilArgumentParser $args) { $console = PhutilConsole::getConsole(); $tasks = $this->loadTasks($args); + $is_repeat = $args->getArg('repeat'); foreach ($tasks as $task) { if (!$task->isArchived()) { $console->writeOut( @@ -32,13 +41,16 @@ final class PhabricatorWorkerManagementRetryWorkflow $result_success = PhabricatorWorkerArchiveTask::RESULT_SUCCESS; if ($task->getResult() == $result_success) { - $console->writeOut( - "** %s ** %s\n", - pht('SUCCEEDED'), - pht( - '%s has already succeeded, and can not be retried.', - $this->describeTask($task))); - continue; + if (!$is_repeat) { + $console->writeOut( + "** %s ** %s\n", + pht('SUCCEEDED'), + pht( + '%s has already succeeded, and will not be repeated. '. + 'Use "--repeat" to repeat successful tasks.', + $this->describeTask($task))); + continue; + } } $task->unarchiveTask(); From 34e90d8f5139f3afac2230536933eb812dd428d1 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 2 Mar 2019 06:13:58 -0800 Subject: [PATCH 120/245] Clean up a few "%Q" stragglers in SVN repository browsing code Summary: See . Test Plan: Browed a Subversion repository in Diffusion. These are all reachable from the main landing page if the repository has commits/files, I think. Before change: errors in log; after change: no issues. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20244 --- .../conduit/DiffusionBrowseQueryConduitAPIMethod.php | 12 +++++++----- .../DiffusionHistoryQueryConduitAPIMethod.php | 12 ++++++++---- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/applications/diffusion/conduit/DiffusionBrowseQueryConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionBrowseQueryConduitAPIMethod.php index fe99471b0c..0b6ae19e32 100644 --- a/src/applications/diffusion/conduit/DiffusionBrowseQueryConduitAPIMethod.php +++ b/src/applications/diffusion/conduit/DiffusionBrowseQueryConduitAPIMethod.php @@ -368,9 +368,9 @@ final class DiffusionBrowseQueryConduitAPIMethod } if ($commit) { - $slice_clause = 'AND svnCommit <= '.(int)$commit; + $slice_clause = qsprintf($conn_r, 'AND svnCommit <= %d', $commit); } else { - $slice_clause = ''; + $slice_clause = qsprintf($conn_r, ''); } $index = queryfx_all( @@ -439,9 +439,11 @@ final class DiffusionBrowseQueryConduitAPIMethod $sql = array(); foreach ($index as $row) { - $sql[] = - '(pathID = '.(int)$row['pathID'].' AND '. - 'svnCommit = '.(int)$row['maxCommit'].')'; + $sql[] = qsprintf( + $conn_r, + '(pathID = %d AND svnCommit = %d)', + $row['pathID'], + $row['maxCommit']); } $browse = queryfx_all( diff --git a/src/applications/diffusion/conduit/DiffusionHistoryQueryConduitAPIMethod.php b/src/applications/diffusion/conduit/DiffusionHistoryQueryConduitAPIMethod.php index 4c1d39e8c8..ebce21dd6f 100644 --- a/src/applications/diffusion/conduit/DiffusionHistoryQueryConduitAPIMethod.php +++ b/src/applications/diffusion/conduit/DiffusionHistoryQueryConduitAPIMethod.php @@ -215,13 +215,17 @@ final class DiffusionHistoryQueryConduitAPIMethod return array(); } - $filter_query = ''; + $filter_query = qsprintf($conn_r, ''); if ($need_direct_changes) { if ($need_child_changes) { - $type = DifferentialChangeType::TYPE_CHILD; - $filter_query = 'AND (isDirect = 1 OR changeType = '.$type.')'; + $filter_query = qsprintf( + $conn_r, + 'AND (isDirect = 1 OR changeType = %s)', + DifferentialChangeType::TYPE_CHILD); } else { - $filter_query = 'AND (isDirect = 1)'; + $filter_query = qsprintf( + $conn_r, + 'AND (isDirect = 1)'); } } From ee2bc07c9025695571f35a34fb93a088be533c9e Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 2 Mar 2019 06:05:40 -0800 Subject: [PATCH 121/245] No-op old search indexing migrations which no longer run and have been obsoleted by upgrade "activities" Summary: See T13253. After D20200 (which changed the task schema) these migrations no longer run, since the PHP code will expect a column to exist that won't exist until a `20190220.` migration runs. We don't need these migrations, since anyone upgrading through September 2017 gets a "rebuild search indexes" activity anyway (see T11932). Just no-op them. Test Plan: Grepped for `queueDocumentForIndexing()` in `autopatches/`, removed all of it. Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Differential Revision: https://secure.phabricator.com/D20243 --- .../autopatches/20151221.search.3.reindex.php | 10 +--------- .../20160221.almanac.2.devicei.php | 10 +--------- .../20160221.almanac.4.servicei.php | 10 +--------- .../20160221.almanac.6.networki.php | 10 +--------- .../20160227.harbormaster.2.plani.php | 10 +--------- .../autopatches/20160303.drydock.2.bluei.php | 10 +--------- .../20160308.nuance.04.sourcei.php | 10 +--------- .../autopatches/20160406.badges.ngrams.php | 10 +--------- .../sql/autopatches/20160927.phurl.ngrams.php | 10 +--------- .../20161011.conpherence.ngrams.php | 10 +--------- .../20161216.dashboard.ngram.02.php | 20 +------------------ .../sql/autopatches/20170526.milestones.php | 10 +--------- .../20171026.ferret.05.ponder.index.php | 10 +--------- 13 files changed, 13 insertions(+), 127 deletions(-) diff --git a/resources/sql/autopatches/20151221.search.3.reindex.php b/resources/sql/autopatches/20151221.search.3.reindex.php index 09556d5ea0..623ba7bf6a 100644 --- a/resources/sql/autopatches/20151221.search.3.reindex.php +++ b/resources/sql/autopatches/20151221.search.3.reindex.php @@ -1,11 +1,3 @@ getPHID(), - array( - 'force' => true, - )); -} +// This was an old reindexing migration that has been obsoleted. See T13253. diff --git a/resources/sql/autopatches/20160221.almanac.2.devicei.php b/resources/sql/autopatches/20160221.almanac.2.devicei.php index aea17d0ad6..623ba7bf6a 100644 --- a/resources/sql/autopatches/20160221.almanac.2.devicei.php +++ b/resources/sql/autopatches/20160221.almanac.2.devicei.php @@ -1,11 +1,3 @@ getPHID(), - array( - 'force' => true, - )); -} +// This was an old reindexing migration that has been obsoleted. See T13253. diff --git a/resources/sql/autopatches/20160221.almanac.4.servicei.php b/resources/sql/autopatches/20160221.almanac.4.servicei.php index 97211ca7b5..623ba7bf6a 100644 --- a/resources/sql/autopatches/20160221.almanac.4.servicei.php +++ b/resources/sql/autopatches/20160221.almanac.4.servicei.php @@ -1,11 +1,3 @@ getPHID(), - array( - 'force' => true, - )); -} +// This was an old reindexing migration that has been obsoleted. See T13253. diff --git a/resources/sql/autopatches/20160221.almanac.6.networki.php b/resources/sql/autopatches/20160221.almanac.6.networki.php index 263defbb33..623ba7bf6a 100644 --- a/resources/sql/autopatches/20160221.almanac.6.networki.php +++ b/resources/sql/autopatches/20160221.almanac.6.networki.php @@ -1,11 +1,3 @@ getPHID(), - array( - 'force' => true, - )); -} +// This was an old reindexing migration that has been obsoleted. See T13253. diff --git a/resources/sql/autopatches/20160227.harbormaster.2.plani.php b/resources/sql/autopatches/20160227.harbormaster.2.plani.php index 6dea004c06..623ba7bf6a 100644 --- a/resources/sql/autopatches/20160227.harbormaster.2.plani.php +++ b/resources/sql/autopatches/20160227.harbormaster.2.plani.php @@ -1,11 +1,3 @@ getPHID(), - array( - 'force' => true, - )); -} +// This was an old reindexing migration that has been obsoleted. See T13253. diff --git a/resources/sql/autopatches/20160303.drydock.2.bluei.php b/resources/sql/autopatches/20160303.drydock.2.bluei.php index c0b68c2262..623ba7bf6a 100644 --- a/resources/sql/autopatches/20160303.drydock.2.bluei.php +++ b/resources/sql/autopatches/20160303.drydock.2.bluei.php @@ -1,11 +1,3 @@ getPHID(), - array( - 'force' => true, - )); -} +// This was an old reindexing migration that has been obsoleted. See T13253. diff --git a/resources/sql/autopatches/20160308.nuance.04.sourcei.php b/resources/sql/autopatches/20160308.nuance.04.sourcei.php index eb0d1da113..623ba7bf6a 100644 --- a/resources/sql/autopatches/20160308.nuance.04.sourcei.php +++ b/resources/sql/autopatches/20160308.nuance.04.sourcei.php @@ -1,11 +1,3 @@ getPHID(), - array( - 'force' => true, - )); -} +// This was an old reindexing migration that has been obsoleted. See T13253. diff --git a/resources/sql/autopatches/20160406.badges.ngrams.php b/resources/sql/autopatches/20160406.badges.ngrams.php index ce8d8896ef..623ba7bf6a 100644 --- a/resources/sql/autopatches/20160406.badges.ngrams.php +++ b/resources/sql/autopatches/20160406.badges.ngrams.php @@ -1,11 +1,3 @@ getPHID(), - array( - 'force' => true, - )); -} +// This was an old reindexing migration that has been obsoleted. See T13253. diff --git a/resources/sql/autopatches/20160927.phurl.ngrams.php b/resources/sql/autopatches/20160927.phurl.ngrams.php index 74cf61efa5..623ba7bf6a 100644 --- a/resources/sql/autopatches/20160927.phurl.ngrams.php +++ b/resources/sql/autopatches/20160927.phurl.ngrams.php @@ -1,11 +1,3 @@ getPHID(), - array( - 'force' => true, - )); -} +// This was an old reindexing migration that has been obsoleted. See T13253. diff --git a/resources/sql/autopatches/20161011.conpherence.ngrams.php b/resources/sql/autopatches/20161011.conpherence.ngrams.php index 457143f6c7..623ba7bf6a 100644 --- a/resources/sql/autopatches/20161011.conpherence.ngrams.php +++ b/resources/sql/autopatches/20161011.conpherence.ngrams.php @@ -1,11 +1,3 @@ getPHID(), - array( - 'force' => true, - )); -} +// This was an old reindexing migration that has been obsoleted. See T13253. diff --git a/resources/sql/autopatches/20161216.dashboard.ngram.02.php b/resources/sql/autopatches/20161216.dashboard.ngram.02.php index a7abc99b23..623ba7bf6a 100644 --- a/resources/sql/autopatches/20161216.dashboard.ngram.02.php +++ b/resources/sql/autopatches/20161216.dashboard.ngram.02.php @@ -1,21 +1,3 @@ getPHID(), - array( - 'force' => true, - )); -} - -$table_dbp = new PhabricatorDashboardPanel(); - -foreach (new LiskMigrationIterator($table_dbp) as $panel) { - PhabricatorSearchWorker::queueDocumentForIndexing( - $panel->getPHID(), - array( - 'force' => true, - )); -} +// This was an old reindexing migration that has been obsoleted. See T13253. diff --git a/resources/sql/autopatches/20170526.milestones.php b/resources/sql/autopatches/20170526.milestones.php index 2e30ac4775..623ba7bf6a 100644 --- a/resources/sql/autopatches/20170526.milestones.php +++ b/resources/sql/autopatches/20170526.milestones.php @@ -1,11 +1,3 @@ getPHID(), - array( - 'force' => true, - )); -} +// This was an old reindexing migration that has been obsoleted. See T13253. diff --git a/resources/sql/autopatches/20171026.ferret.05.ponder.index.php b/resources/sql/autopatches/20171026.ferret.05.ponder.index.php index 20489846d2..623ba7bf6a 100644 --- a/resources/sql/autopatches/20171026.ferret.05.ponder.index.php +++ b/resources/sql/autopatches/20171026.ferret.05.ponder.index.php @@ -1,11 +1,3 @@ getPHID(), - array( - 'force' => true, - )); -} +// This was an old reindexing migration that has been obsoleted. See T13253. From d192d04586ecb5153d87a1f961651abb496a8530 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 1 Mar 2019 07:13:16 -0800 Subject: [PATCH 122/245] Make it more visually clear that you can click things in the "Big List of Clickable Things" UI element Summary: Ref T13259. An install provided feedback that it wasn't obvious you could click the buttons in this UI. Make it more clear that these are clickable buttons. Test Plan: {F6251585} {F6251586} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13259 Differential Revision: https://secure.phabricator.com/D20238 --- resources/celerity/map.php | 12 ++++----- .../css/phui/object-item/phui-oi-big-ui.css | 26 ++++++++++++++++--- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 3188de0052..11e7fe4118 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,7 +9,7 @@ return array( 'names' => array( 'conpherence.pkg.css' => '3c8a0668', 'conpherence.pkg.js' => '020aebcf', - 'core.pkg.css' => 'e3c1a8f2', + 'core.pkg.css' => '34ce1741', 'core.pkg.js' => '2cda17a4', 'differential.pkg.css' => 'ab23bd75', 'differential.pkg.js' => '67e02996', @@ -127,7 +127,7 @@ return array( 'rsrc/css/phui/calendar/phui-calendar-list.css' => 'ccd7e4e2', 'rsrc/css/phui/calendar/phui-calendar-month.css' => 'cb758c42', 'rsrc/css/phui/calendar/phui-calendar.css' => 'f11073aa', - 'rsrc/css/phui/object-item/phui-oi-big-ui.css' => '9e037c7a', + 'rsrc/css/phui/object-item/phui-oi-big-ui.css' => '534f1757', 'rsrc/css/phui/object-item/phui-oi-color.css' => 'b517bfa0', 'rsrc/css/phui/object-item/phui-oi-drag-ui.css' => 'da15d3dc', 'rsrc/css/phui/object-item/phui-oi-flush-ui.css' => '490e2e2e', @@ -834,7 +834,7 @@ return array( 'phui-lightbox-css' => '4ebf22da', 'phui-list-view-css' => '470b1adb', 'phui-object-box-css' => 'f434b6be', - 'phui-oi-big-ui-css' => '9e037c7a', + 'phui-oi-big-ui-css' => '534f1757', 'phui-oi-color-css' => 'b517bfa0', 'phui-oi-drag-ui-css' => 'da15d3dc', 'phui-oi-flush-ui-css' => '490e2e2e', @@ -1345,6 +1345,9 @@ return array( 'javelin-dom', 'javelin-fx', ), + '534f1757' => array( + 'phui-oi-list-view-css', + ), '541f81c3' => array( 'javelin-install', ), @@ -1721,9 +1724,6 @@ return array( 'javelin-uri', 'phabricator-textareautils', ), - '9e037c7a' => array( - 'phui-oi-list-view-css', - ), '9f081f05' => array( 'javelin-behavior', 'javelin-dom', diff --git a/webroot/rsrc/css/phui/object-item/phui-oi-big-ui.css b/webroot/rsrc/css/phui/object-item/phui-oi-big-ui.css index a793c018c3..2d2163f9e9 100644 --- a/webroot/rsrc/css/phui/object-item/phui-oi-big-ui.css +++ b/webroot/rsrc/css/phui/object-item/phui-oi-big-ui.css @@ -13,7 +13,12 @@ } .phui-oi-list-big .phui-oi-image-icon { - margin: 8px 2px 12px; + margin: 12px 2px 12px; + text-align: center; +} + +.phui-oi-list-big .phui-oi-image-icon .phui-icon-view { + position: relative; } .phui-oi-list-big a.phui-oi-link { @@ -31,7 +36,7 @@ } .device-desktop .phui-oi-list-big .phui-oi { - margin-bottom: 4px; + margin-bottom: 8px; } .phui-oi-list-big .phui-oi-col0 { @@ -60,13 +65,28 @@ border-radius: 3px; } +.phui-oi-list-big .phui-oi-frame { + padding: 2px 8px; +} + +.phui-oi-list-big .phui-oi-linked-container { + border: 1px solid {$lightblueborder}; + border-radius: 4px; + box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.035); +} + +.phui-oi-list-big .phui-oi-disabled { + border-radius: 4px; + background: {$lightgreybackground}; +} + .device-desktop .phui-oi-linked-container { cursor: pointer; } .device-desktop .phui-oi-linked-container:hover { background-color: {$hoverblue}; - border-radius: 3px; + border-color: {$blueborder}; } .device-desktop .phui-oi-linked-container a:hover { From 920ab13cfb86232c9732e978ff847bdb9d05bf3b Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 28 Feb 2019 19:13:53 -0800 Subject: [PATCH 123/245] Correct a possible fatal in the non-CSRF Duo MFA workflow Summary: Ref T13259. If we miss the separate CSRF step in Duo and proceed directly to prompting, we may fail to build a response which turns into a real control and fatal on `null->setLabel()`. Instead, let MFA providers customize their "bare prompt dialog" response, then make Duo use the same "you have an outstanding request" response for the CSRF and no-CSRF workflows. Test Plan: Hit Duo auth on a non-CSRF workflow (e.g., edit an MFA provider with Duo enabled). Previously: `setLabel()` fatal. After patch: smooth sailing. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13259 Differential Revision: https://secure.phabricator.com/D20234 --- .../engine/PhabricatorAuthSessionEngine.php | 9 ++++- .../auth/factor/PhabricatorAuthFactor.php | 34 +++++++++++++++++++ .../auth/factor/PhabricatorDuoAuthFactor.php | 13 +++++++ 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php index c052805224..38ae2201b8 100644 --- a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php +++ b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php @@ -714,7 +714,14 @@ final class PhabricatorAuthSessionEngine extends Phobject { if (isset($validation_results[$factor_phid])) { continue; } - $validation_results[$factor_phid] = new PhabricatorAuthFactorResult(); + + $issued_challenges = idx($challenge_map, $factor_phid, array()); + + $validation_results[$factor_phid] = $impl->getResultForPrompt( + $factor, + $viewer, + $request, + $issued_challenges); } throw id(new PhabricatorAuthHighSecurityRequiredException()) diff --git a/src/applications/auth/factor/PhabricatorAuthFactor.php b/src/applications/auth/factor/PhabricatorAuthFactor.php index d7e6e60ecc..fefd9b5fd1 100644 --- a/src/applications/auth/factor/PhabricatorAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorAuthFactor.php @@ -221,6 +221,40 @@ abstract class PhabricatorAuthFactor extends Phobject { return $result; } + final public function getResultForPrompt( + PhabricatorAuthFactorConfig $config, + PhabricatorUser $viewer, + AphrontRequest $request, + array $challenges) { + assert_instances_of($challenges, 'PhabricatorAuthChallenge'); + + $result = $this->newResultForPrompt( + $config, + $viewer, + $request, + $challenges); + + if (!$this->isAuthResult($result)) { + throw new Exception( + pht( + 'Expected "newResultForPrompt()" to return an object of class "%s", '. + 'but it returned something else ("%s"; in "%s").', + 'PhabricatorAuthFactorResult', + phutil_describe_type($result), + get_class($this))); + } + + return $result; + } + + protected function newResultForPrompt( + PhabricatorAuthFactorConfig $config, + PhabricatorUser $viewer, + AphrontRequest $request, + array $challenges) { + return $this->newResult(); + } + abstract protected function newResultFromIssuedChallenges( PhabricatorAuthFactorConfig $config, PhabricatorUser $viewer, diff --git a/src/applications/auth/factor/PhabricatorDuoAuthFactor.php b/src/applications/auth/factor/PhabricatorDuoAuthFactor.php index 66bd7c9ebd..a84337a764 100644 --- a/src/applications/auth/factor/PhabricatorDuoAuthFactor.php +++ b/src/applications/auth/factor/PhabricatorDuoAuthFactor.php @@ -681,6 +681,19 @@ final class PhabricatorDuoAuthFactor AphrontRequest $request, array $challenges) { + return $this->getResultForPrompt( + $config, + $viewer, + $request, + $challenges); + } + + protected function newResultForPrompt( + PhabricatorAuthFactorConfig $config, + PhabricatorUser $viewer, + AphrontRequest $request, + array $challenges) { + $result = $this->newResult() ->setIsContinue(true) ->setErrorMessage( From cc8dda62990524c5cd7282ac8b0f38a805ecec38 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 1 Mar 2019 06:54:32 -0800 Subject: [PATCH 124/245] Recognize the official "Go" magic regexp for generated code as generated Summary: See PHI1112. See T784. Although some more general/flexible solution is arriving eventually, adding this rule seems reasonable for now, since it's not a big deal if we remove it later to replace this with some fancier system. Test Plan: Created a diff with the official Go generated marker, saw the changeset marked as generated. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20237 --- .../differential/engine/DifferentialChangesetEngine.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/applications/differential/engine/DifferentialChangesetEngine.php b/src/applications/differential/engine/DifferentialChangesetEngine.php index d72db025ad..23382e6a81 100644 --- a/src/applications/differential/engine/DifferentialChangesetEngine.php +++ b/src/applications/differential/engine/DifferentialChangesetEngine.php @@ -54,6 +54,12 @@ final class DifferentialChangesetEngine extends Phobject { if (strpos($new_data, '@'.'generated') !== false) { return true; } + + // See PHI1112. This is the official pattern for marking Go code as + // generated. + if (preg_match('(^// Code generated .* DO NOT EDIT\.$)m', $new_data)) { + return true; + } } return false; From c116deef6398fc7381306c66f0b92ed9f8c457dc Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 28 Feb 2019 08:25:13 -0800 Subject: [PATCH 125/245] Remove "Effective User" attachment from Repository Identities Summary: See . It's possible for an Idenitity to be bound to user X, then for that user to be deleted, e.g. with `bin/remove destroy X`. In this case, we'll fail to load/attach the user to the identity. Currently, we don't actually use this anywhere, so just stop loading it. Once we start using it (if we ever do), we could figure out what an appropriate policy is in this case. Test Plan: Browsed around Diffusion, grepped for affected "effective user" methods and found no callsites. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20224 --- .../PhabricatorRepositoryIdentityQuery.php | 23 ------------------- .../storage/PhabricatorRepositoryIdentity.php | 11 --------- 2 files changed, 34 deletions(-) diff --git a/src/applications/repository/query/PhabricatorRepositoryIdentityQuery.php b/src/applications/repository/query/PhabricatorRepositoryIdentityQuery.php index ef038f045f..c64b1a296b 100644 --- a/src/applications/repository/query/PhabricatorRepositoryIdentityQuery.php +++ b/src/applications/repository/query/PhabricatorRepositoryIdentityQuery.php @@ -124,29 +124,6 @@ final class PhabricatorRepositoryIdentityQuery return $where; } - protected function didFilterPage(array $identities) { - $user_ids = array_filter( - mpull($identities, 'getCurrentEffectiveUserPHID', 'getID')); - if (!$user_ids) { - return $identities; - } - - $users = id(new PhabricatorPeopleQuery()) - ->withPHIDs($user_ids) - ->setViewer($this->getViewer()) - ->execute(); - $users = mpull($users, null, 'getPHID'); - - foreach ($identities as $identity) { - if ($identity->hasEffectiveUser()) { - $user = idx($users, $identity->getCurrentEffectiveUserPHID()); - $identity->attachEffectiveUser($user); - } - } - - return $identities; - } - public function getQueryApplicationClass() { return 'PhabricatorDiffusionApplication'; } diff --git a/src/applications/repository/storage/PhabricatorRepositoryIdentity.php b/src/applications/repository/storage/PhabricatorRepositoryIdentity.php index 76c6aed9e0..e3833bd10e 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryIdentity.php +++ b/src/applications/repository/storage/PhabricatorRepositoryIdentity.php @@ -14,17 +14,6 @@ final class PhabricatorRepositoryIdentity protected $manuallySetUserPHID; protected $currentEffectiveUserPHID; - private $effectiveUser = self::ATTACHABLE; - - public function attachEffectiveUser(PhabricatorUser $user) { - $this->effectiveUser = $user; - return $this; - } - - public function getEffectiveUser() { - return $this->assertAttached($this->effectiveUser); - } - protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, From ea6c0c9bdebfdbffeb524412651b5ae675c32887 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 28 Feb 2019 19:41:56 -0800 Subject: [PATCH 126/245] Refine the "Mangled Webserver Response" setup check Summary: Ref T13259. In some configurations, making a request to ourselves may return a VPN/Auth response from some LB/appliance layer. If this response begins or ends with whitespace, we currently detect it as "extra whitespace" instead of "bad response". Instead, require that the response be nearly correct (valid JSON with some extra whitespace, instead of literally anything with some extra whitespace) to hit this specialized check. If we don't hit the specialized case, use the generic "mangled" response error, which prints the actual body so you can figure out that it's just your LB/auth thing doing what it's supposed to do. Test Plan: - Rigged responses to add extra whitespace, got "Extra Whitespace" (same as before). - Rigged responses to add extra non-whitespace, got "Mangled Junk" (same as before). - Rigged responses to add extra whitespace and extra non-whitespace, got "Mangled Junk" with a sample of the document body instead of "Extra Whitespace" (improvement). Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13259 Differential Revision: https://secure.phabricator.com/D20235 --- .../AphrontApplicationConfiguration.php | 1 - .../check/PhabricatorWebServerSetupCheck.php | 41 +++++++++---------- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/src/aphront/configuration/AphrontApplicationConfiguration.php b/src/aphront/configuration/AphrontApplicationConfiguration.php index 8cd27fa62b..a479209125 100644 --- a/src/aphront/configuration/AphrontApplicationConfiguration.php +++ b/src/aphront/configuration/AphrontApplicationConfiguration.php @@ -776,7 +776,6 @@ final class AphrontApplicationConfiguration 'filler' => str_repeat('Q', 1024 * 16), ); - return id(new AphrontJSONResponse()) ->setAddJSONShield(false) ->setContent($result); diff --git a/src/applications/config/check/PhabricatorWebServerSetupCheck.php b/src/applications/config/check/PhabricatorWebServerSetupCheck.php index 8f6885e8e8..284b5e2a5f 100644 --- a/src/applications/config/check/PhabricatorWebServerSetupCheck.php +++ b/src/applications/config/check/PhabricatorWebServerSetupCheck.php @@ -129,30 +129,16 @@ final class PhabricatorWebServerSetupCheck extends PhabricatorSetupCheck { } $structure = null; - $caught = null; $extra_whitespace = ($body !== trim($body)); - if (!$extra_whitespace) { - try { - $structure = phutil_json_decode($body); - } catch (Exception $ex) { - $caught = $ex; - } + try { + $structure = phutil_json_decode(trim($body)); + } catch (Exception $ex) { + // Ignore the exception, we only care if the decode worked or not. } - if (!$structure) { - if ($extra_whitespace) { - $message = pht( - 'Phabricator sent itself a test request and expected to get a bare '. - 'JSON response back, but the response had extra whitespace at '. - 'the beginning or end.'. - "\n\n". - 'This usually means you have edited a file and left whitespace '. - 'characters before the opening %s tag, or after a closing %s tag. '. - 'Remove any leading whitespace, and prefer to omit closing tags.', - phutil_tag('tt', array(), '')); - } else { + if (!$structure || $extra_whitespace) { + if (!$structure) { $short = id(new PhutilUTF8StringTruncator()) ->setMaximumGlyphs(1024) ->truncateString($body); @@ -166,6 +152,17 @@ final class PhabricatorWebServerSetupCheck extends PhabricatorSetupCheck { "\n\n". 'Something is misconfigured or otherwise mangling responses.', phutil_tag('pre', array(), $short)); + } else { + $message = pht( + 'Phabricator sent itself a test request and expected to get a bare '. + 'JSON response back. It received a JSON response, but the response '. + 'had extra whitespace at the beginning or end.'. + "\n\n". + 'This usually means you have edited a file and left whitespace '. + 'characters before the opening %s tag, or after a closing %s tag. '. + 'Remove any leading whitespace, and prefer to omit closing tags.', + phutil_tag('tt', array(), '')); } $this->newIssue('webserver.mangle') @@ -174,7 +171,9 @@ final class PhabricatorWebServerSetupCheck extends PhabricatorSetupCheck { ->setMessage($message); // We can't run the other checks if we could not decode the response. - return; + if (!$structure) { + return; + } } $actual_user = idx($structure, 'user'); From d36d0efc35706f4bf1c327f5d4ffc76fbde92426 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 26 Feb 2019 06:26:03 -0800 Subject: [PATCH 127/245] Add behaviors to Build Plans: hold drafts, affect buildables, warn on landing, restartable, runnable Summary: Depends on D20219. Ref T13258. Ref T11415. Installs sometimes have long-running builds or unimportant builds which they may not want to hold up drafts, affect buildable status, or warn during `arc land`. Some builds have side effects (like deployment or merging) and are not idempotent. They can cause problems if restarted. In other cases, builds are isolated and idempotent and generally safe, and it's okay for marketing interns to restart them. To address these cases, add "Behaviors" to Build Plans: - Hold Drafts: Controls how the build affects revision promotion from "Draft". - Warn on Land: Controls the "arc land" warning. - Affects Buildable: Controls whether we care about this build when figuring out if a buildable passed or failed overall. - Restartable: Controls whether this build may restart or not. - Runnable: Allows you to weaken the requirements to run the build if you're confident it's safe to run it on arbitrary old versions of things. NOTE: This only implements UI, none of these options actually do anything yet. Test Plan: Mostly poked around the UI. I'll actually implement these behaviors next, and vet them more thoroughly. {F6244828} {F6244830} Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13258, T11415 Differential Revision: https://secure.phabricator.com/D20220 --- .../20190226.harbor.01.planprops.sql | 2 + .../20190226.harbor.02.planvalue.sql | 2 + src/__phutil_library_map__.php | 8 + .../PhabricatorHarbormasterApplication.php | 2 + .../HarbormasterPlanBehaviorController.php | 92 +++++ .../HarbormasterPlanViewController.php | 73 ++++ .../HarbormasterBuildPlanEditEngine.php | 32 +- .../plan/HarbormasterBuildPlanBehavior.php | 348 ++++++++++++++++++ .../HarbormasterBuildPlanBehaviorOption.php | 57 +++ .../configuration/HarbormasterBuildPlan.php | 13 + ...rbormasterBuildPlanBehaviorTransaction.php | 127 +++++++ 11 files changed, 755 insertions(+), 1 deletion(-) create mode 100644 resources/sql/autopatches/20190226.harbor.01.planprops.sql create mode 100644 resources/sql/autopatches/20190226.harbor.02.planvalue.sql create mode 100644 src/applications/harbormaster/controller/HarbormasterPlanBehaviorController.php create mode 100644 src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php create mode 100644 src/applications/harbormaster/plan/HarbormasterBuildPlanBehaviorOption.php create mode 100644 src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanBehaviorTransaction.php diff --git a/resources/sql/autopatches/20190226.harbor.01.planprops.sql b/resources/sql/autopatches/20190226.harbor.01.planprops.sql new file mode 100644 index 0000000000..324139669e --- /dev/null +++ b/resources/sql/autopatches/20190226.harbor.01.planprops.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_harbormaster.harbormaster_buildplan + ADD properties LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20190226.harbor.02.planvalue.sql b/resources/sql/autopatches/20190226.harbor.02.planvalue.sql new file mode 100644 index 0000000000..b1929abf59 --- /dev/null +++ b/resources/sql/autopatches/20190226.harbor.02.planvalue.sql @@ -0,0 +1,2 @@ +UPDATE {$NAMESPACE}_harbormaster.harbormaster_buildplan + SET properties = '{}' WHERE properties = ''; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index b80940a25c..15c7637e3d 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1328,6 +1328,9 @@ phutil_register_library_map(array( 'HarbormasterBuildMessageQuery' => 'applications/harbormaster/query/HarbormasterBuildMessageQuery.php', 'HarbormasterBuildPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildPHIDType.php', 'HarbormasterBuildPlan' => 'applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php', + 'HarbormasterBuildPlanBehavior' => 'applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php', + 'HarbormasterBuildPlanBehaviorOption' => 'applications/harbormaster/plan/HarbormasterBuildPlanBehaviorOption.php', + 'HarbormasterBuildPlanBehaviorTransaction' => 'applications/harbormaster/xaction/plan/HarbormasterBuildPlanBehaviorTransaction.php', 'HarbormasterBuildPlanDatasource' => 'applications/harbormaster/typeahead/HarbormasterBuildPlanDatasource.php', 'HarbormasterBuildPlanDefaultEditCapability' => 'applications/harbormaster/capability/HarbormasterBuildPlanDefaultEditCapability.php', 'HarbormasterBuildPlanDefaultViewCapability' => 'applications/harbormaster/capability/HarbormasterBuildPlanDefaultViewCapability.php', @@ -1424,6 +1427,7 @@ phutil_register_library_map(array( 'HarbormasterMessageType' => 'applications/harbormaster/engine/HarbormasterMessageType.php', 'HarbormasterObject' => 'applications/harbormaster/storage/HarbormasterObject.php', 'HarbormasterOtherBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterOtherBuildStepGroup.php', + 'HarbormasterPlanBehaviorController' => 'applications/harbormaster/controller/HarbormasterPlanBehaviorController.php', 'HarbormasterPlanController' => 'applications/harbormaster/controller/HarbormasterPlanController.php', 'HarbormasterPlanDisableController' => 'applications/harbormaster/controller/HarbormasterPlanDisableController.php', 'HarbormasterPlanEditController' => 'applications/harbormaster/controller/HarbormasterPlanEditController.php', @@ -6942,6 +6946,9 @@ phutil_register_library_map(array( 'PhabricatorConduitResultInterface', 'PhabricatorProjectInterface', ), + 'HarbormasterBuildPlanBehavior' => 'Phobject', + 'HarbormasterBuildPlanBehaviorOption' => 'Phobject', + 'HarbormasterBuildPlanBehaviorTransaction' => 'HarbormasterBuildPlanTransactionType', 'HarbormasterBuildPlanDatasource' => 'PhabricatorTypeaheadDatasource', 'HarbormasterBuildPlanDefaultEditCapability' => 'PhabricatorPolicyCapability', 'HarbormasterBuildPlanDefaultViewCapability' => 'PhabricatorPolicyCapability', @@ -7057,6 +7064,7 @@ phutil_register_library_map(array( 'HarbormasterMessageType' => 'Phobject', 'HarbormasterObject' => 'HarbormasterDAO', 'HarbormasterOtherBuildStepGroup' => 'HarbormasterBuildStepGroup', + 'HarbormasterPlanBehaviorController' => 'HarbormasterPlanController', 'HarbormasterPlanController' => 'HarbormasterController', 'HarbormasterPlanDisableController' => 'HarbormasterPlanController', 'HarbormasterPlanEditController' => 'HarbormasterPlanController', diff --git a/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php b/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php index 80be90b375..4b369e821e 100644 --- a/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php +++ b/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php @@ -83,6 +83,8 @@ final class PhabricatorHarbormasterApplication extends PhabricatorApplication { => 'HarbormasterPlanEditController', 'order/(?:(?P\d+)/)?' => 'HarbormasterPlanOrderController', 'disable/(?P\d+)/' => 'HarbormasterPlanDisableController', + 'behavior/(?P\d+)/(?P[^/]+)/' => + 'HarbormasterPlanBehaviorController', 'run/(?P\d+)/' => 'HarbormasterPlanRunController', '(?P\d+)/' => 'HarbormasterPlanViewController', ), diff --git a/src/applications/harbormaster/controller/HarbormasterPlanBehaviorController.php b/src/applications/harbormaster/controller/HarbormasterPlanBehaviorController.php new file mode 100644 index 0000000000..8f1fece691 --- /dev/null +++ b/src/applications/harbormaster/controller/HarbormasterPlanBehaviorController.php @@ -0,0 +1,92 @@ +getViewer(); + + $plan = id(new HarbormasterBuildPlanQuery()) + ->setViewer($viewer) + ->withIDs(array($request->getURIData('id'))) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$plan) { + return new Aphront404Response(); + } + + $behavior_key = $request->getURIData('behaviorKey'); + $metadata_key = HarbormasterBuildPlanBehavior::getTransactionMetadataKey(); + + $behaviors = HarbormasterBuildPlanBehavior::newPlanBehaviors(); + $behavior = idx($behaviors, $behavior_key); + if (!$behavior) { + return new Aphront404Response(); + } + + $plan_uri = $plan->getURI(); + + $v_option = $behavior->getPlanOption($plan)->getKey(); + if ($request->isFormPost()) { + $v_option = $request->getStr('option'); + + $xactions = array(); + + $xactions[] = id(new HarbormasterBuildPlanTransaction()) + ->setTransactionType( + HarbormasterBuildPlanBehaviorTransaction::TRANSACTIONTYPE) + ->setMetadataValue($metadata_key, $behavior_key) + ->setNewValue($v_option); + + $editor = id(new HarbormasterBuildPlanEditor()) + ->setActor($viewer) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->setContentSourceFromRequest($request); + + $editor->applyTransactions($plan, $xactions); + + return id(new AphrontRedirectResponse())->setURI($plan_uri); + } + + $select_control = id(new AphrontFormRadioButtonControl()) + ->setName('option') + ->setValue($v_option) + ->setLabel(pht('Option')); + + foreach ($behavior->getOptions() as $option) { + $icon = id(new PHUIIconView()) + ->setIcon($option->getIcon()); + + $select_control->addButton( + $option->getKey(), + array( + $icon, + ' ', + $option->getName(), + ), + $option->getDescription()); + } + + $form = id(new AphrontFormView()) + ->setViewer($viewer) + ->appendInstructions( + pht( + 'Choose a build plan behavior for "%s".', + phutil_tag('strong', array(), $behavior->getName()))) + ->appendRemarkupInstructions($behavior->getEditInstructions()) + ->appendControl($select_control); + + return $this->newDialog() + ->setTitle(pht('Edit Behavior: %s', $behavior->getName())) + ->appendForm($form) + ->setWidth(AphrontDialogView::WIDTH_FORM) + ->addSubmitButton(pht('Save Changes')) + ->addCancelButton($plan_uri); + } + +} diff --git a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php index f2ead2c3db..0ef6162aad 100644 --- a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php @@ -61,6 +61,7 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController { } $builds_view = $this->newBuildsView($plan); + $options_view = $this->newOptionsView($plan); $timeline = $this->buildTransactionTimeline( $plan, @@ -74,6 +75,7 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController { array( $error, $step_list, + $options_view, $builds_view, $timeline, )); @@ -484,4 +486,75 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController { ->appendChild($list); } + + private function newOptionsView(HarbormasterBuildPlan $plan) { + $viewer = $this->getViewer(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $plan, + PhabricatorPolicyCapability::CAN_EDIT); + + $behaviors = HarbormasterBuildPlanBehavior::newPlanBehaviors(); + + $rows = array(); + foreach ($behaviors as $behavior) { + $option = $behavior->getPlanOption($plan); + + $icon = $option->getIcon(); + $icon = id(new PHUIIconView())->setIcon($icon); + + $edit_uri = new PhutilURI( + $this->getApplicationURI( + urisprintf( + 'plan/behavior/%d/%s/', + $plan->getID(), + $behavior->getKey()))); + + $edit_button = id(new PHUIButtonView()) + ->setTag('a') + ->setColor(PHUIButtonView::GREY) + ->setSize(PHUIButtonView::SMALL) + ->setDisabled(!$can_edit) + ->setWorkflow(true) + ->setText(pht('Edit')) + ->setHref($edit_uri); + + $rows[] = array( + $icon, + $behavior->getName(), + $option->getName(), + $option->getDescription(), + $edit_button, + ); + } + + $table = id(new AphrontTableView($rows)) + ->setHeaders( + array( + null, + pht('Name'), + pht('Behavior'), + pht('Details'), + null, + )) + ->setColumnClasses( + array( + null, + 'pri', + null, + 'wide', + null, + )); + + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Plan Behaviors')); + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setTable($table); + } + } diff --git a/src/applications/harbormaster/editor/HarbormasterBuildPlanEditEngine.php b/src/applications/harbormaster/editor/HarbormasterBuildPlanEditEngine.php index 35a417d9d4..c0fa80d71b 100644 --- a/src/applications/harbormaster/editor/HarbormasterBuildPlanEditEngine.php +++ b/src/applications/harbormaster/editor/HarbormasterBuildPlanEditEngine.php @@ -77,7 +77,7 @@ final class HarbormasterBuildPlanEditEngine } protected function buildCustomEditFields($object) { - return array( + $fields = array( id(new PhabricatorTextEditField()) ->setKey('name') ->setLabel(pht('Name')) @@ -89,6 +89,36 @@ final class HarbormasterBuildPlanEditEngine ->setConduitTypeDescription(pht('New plan name.')) ->setValue($object->getName()), ); + + + $metadata_key = HarbormasterBuildPlanBehavior::getTransactionMetadataKey(); + + $behaviors = HarbormasterBuildPlanBehavior::newPlanBehaviors(); + foreach ($behaviors as $behavior) { + $key = $behavior->getKey(); + + // Get the raw key off the object so that we don't reset stuff to + // default values by mistake if a behavior goes missing somehow. + $storage_key = HarbormasterBuildPlanBehavior::getStorageKeyForBehaviorKey( + $key); + $behavior_option = $object->getPlanProperty($storage_key); + + if (!strlen($behavior_option)) { + $behavior_option = $behavior->getPlanOption($object)->getKey(); + } + + $fields[] = id(new PhabricatorSelectEditField()) + ->setIsFormField(false) + ->setKey(sprintf('behavior.%s', $behavior->getKey())) + ->setMetadataValue($metadata_key, $behavior->getKey()) + ->setLabel(pht('Behavior: %s', $behavior->getName())) + ->setTransactionType( + HarbormasterBuildPlanBehaviorTransaction::TRANSACTIONTYPE) + ->setValue($behavior_option) + ->setOptions($behavior->getOptionMap()); + } + + return $fields; } } diff --git a/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php b/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php new file mode 100644 index 0000000000..b0f722a1ca --- /dev/null +++ b/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php @@ -0,0 +1,348 @@ +key = $key; + return $this; + } + + public function getKey() { + return $this->key; + } + + public function setName($name) { + $this->name = $name; + return $this; + } + + public function getName() { + return $this->name; + } + + public function setEditInstructions($edit_instructions) { + $this->editInstructions = $edit_instructions; + return $this; + } + + public function getEditInstructions() { + return $this->editInstructions; + } + + public function getOptionMap() { + return mpull($this->options, 'getName', 'getKey'); + } + + public function setOptions(array $options) { + assert_instances_of($options, 'HarbormasterBuildPlanBehaviorOption'); + + $key_map = array(); + $default = null; + + foreach ($options as $option) { + $key = $option->getKey(); + + if (isset($key_map[$key])) { + throw new Exception( + pht( + 'Multiple behavior options (for behavior "%s") have the same '. + 'key ("%s"). Each option must have a unique key.', + $this->getKey(), + $key)); + } + $key_map[$key] = true; + + if ($option->getIsDefault()) { + if ($default === null) { + $default = $key; + } else { + throw new Exception( + pht( + 'Multiple behavior options (for behavior "%s") are marked as '. + 'default options ("%s" and "%s"). Exactly one option must be '. + 'marked as the default option.', + $this->getKey(), + $default, + $key)); + } + } + } + + if ($default === null) { + throw new Exception( + pht( + 'No behavior option is marked as the default option (for '. + 'behavior "%s"). Exactly one option must be marked as the '. + 'default option.', + $this->getKey())); + } + + $this->options = mpull($options, null, 'getKey'); + $this->defaultKey = $default; + + return $this; + } + + public function getOptions() { + return $this->options; + } + + public function getPlanOption(HarbormasterBuildPlan $plan) { + $behavior_key = $this->getKey(); + $storage_key = self::getStorageKeyForBehaviorKey($behavior_key); + + $plan_value = $plan->getPlanProperty($storage_key); + if (isset($this->options[$plan_value])) { + return $this->options[$plan_value]; + } + + return idx($this->options, $this->defaultKey); + } + + public static function getTransactionMetadataKey() { + return 'behavior-key'; + } + + public static function getStorageKeyForBehaviorKey($behavior_key) { + return sprintf('behavior.%s', $behavior_key); + } + + public static function newPlanBehaviors() { + $draft_options = array( + id(new HarbormasterBuildPlanBehaviorOption()) + ->setKey('always') + ->setIcon('fa-check-circle-o green') + ->setName(pht('Always')) + ->setIsDefault(true) + ->setDescription( + pht( + 'Revisions are not sent for review until the build completes, '. + 'and are returned to the author for updates if the build fails.')), + id(new HarbormasterBuildPlanBehaviorOption()) + ->setKey('building') + ->setIcon('fa-pause-circle-o yellow') + ->setName(pht('If Building')) + ->setDescription( + pht( + 'Revisions are not sent for review until the build completes, '. + 'but they will be sent for review even if it fails.')), + id(new HarbormasterBuildPlanBehaviorOption()) + ->setKey('never') + ->setIcon('fa-circle-o red') + ->setName(pht('Never')) + ->setDescription( + pht( + 'Revisions are sent for review regardless of the status of the '. + 'build.')), + ); + + $land_options = array( + id(new HarbormasterBuildPlanBehaviorOption()) + ->setKey('always') + ->setIcon('fa-check-circle-o green') + ->setName(pht('Always')) + ->setIsDefault(true) + ->setDescription( + pht( + '"arc land" warns if the build is still running or has '. + 'failed.')), + id(new HarbormasterBuildPlanBehaviorOption()) + ->setKey('building') + ->setIcon('fa-pause-circle-o yellow') + ->setName(pht('If Building')) + ->setDescription( + pht( + '"arc land" warns if the build is still running, but ignores '. + 'the build if it has failed.')), + id(new HarbormasterBuildPlanBehaviorOption()) + ->setKey('complete') + ->setIcon('fa-dot-circle-o yellow') + ->setName(pht('If Complete')) + ->setDescription( + pht( + '"arc land" warns if the build has failed, but ignores the '. + 'build if it is still running.')), + id(new HarbormasterBuildPlanBehaviorOption()) + ->setKey('never') + ->setIcon('fa-circle-o red') + ->setName(pht('Never')) + ->setDescription( + pht( + '"arc land" never warns that the build is still running or '. + 'has failed.')), + ); + + $aggregate_options = array( + id(new HarbormasterBuildPlanBehaviorOption()) + ->setKey('always') + ->setIcon('fa-check-circle-o green') + ->setName(pht('Always')) + ->setIsDefault(true) + ->setDescription( + pht( + 'The buildable waits for the build, and fails if the '. + 'build fails.')), + id(new HarbormasterBuildPlanBehaviorOption()) + ->setKey('building') + ->setIcon('fa-pause-circle-o yellow') + ->setName(pht('If Building')) + ->setDescription( + pht( + 'The buildable waits for the build, but does not fail '. + 'if the build fails.')), + id(new HarbormasterBuildPlanBehaviorOption()) + ->setKey('never') + ->setIcon('fa-circle-o red') + ->setName(pht('Never')) + ->setDescription( + pht( + 'The buildable does not wait for the build.')), + ); + + $restart_options = array( + id(new HarbormasterBuildPlanBehaviorOption()) + ->setKey('always') + ->setIcon('fa-repeat green') + ->setName(pht('Always')) + ->setIsDefault(true) + ->setDescription( + pht('The build may be restarted.')), + id(new HarbormasterBuildPlanBehaviorOption()) + ->setKey('never') + ->setIcon('fa-times red') + ->setName(pht('Never')) + ->setDescription( + pht('The build may not be restarted.')), + ); + + $run_options = array( + id(new HarbormasterBuildPlanBehaviorOption()) + ->setKey('edit') + ->setIcon('fa-pencil green') + ->setName(pht('If Editable')) + ->setIsDefault(true) + ->setDescription( + pht('Only users who can edit the plan can run it manually.')), + id(new HarbormasterBuildPlanBehaviorOption()) + ->setKey('view') + ->setIcon('fa-exclamation-triangle yellow') + ->setName(pht('If Viewable')) + ->setDescription( + pht( + 'Any user who can view the plan can run it manually.')), + ); + + $behaviors = array( + id(new self()) + ->setKey('hold-drafts') + ->setName(pht('Hold Drafts')) + ->setEditInstructions( + pht( + 'When users create revisions in Differential, the default '. + 'behavior is to hold them in the "Draft" state until all builds '. + 'pass. Once builds pass, the revisions promote and are sent for '. + 'review, which notifies reviewers.'. + "\n\n". + 'The general intent of this workflow is to make sure reviewers '. + 'are only spending time on review once changes survive automated '. + 'tests. If a change does not pass tests, it usually is not '. + 'really ready for review.'. + "\n\n". + 'If you want to promote revisions out of "Draft" before builds '. + 'pass, or promote revisions even when builds fail, you can '. + 'change the promotion behavior. This may be useful if you have '. + 'very long-running builds, or some builds which are not very '. + 'important.'. + "\n\n". + 'Users may always use "Request Review" to promote a "Draft" '. + 'revision, even if builds have failed or are still in progress.')) + ->setOptions($draft_options), + id(new self()) + ->setKey('arc-land') + ->setName(pht('Warn When Landing')) + ->setEditInstructions( + pht( + 'When a user attempts to `arc land` a revision and that revision '. + 'has ongoing or failed builds, the default behavior of `arc` is '. + 'to warn them about those builds and give them a chance to '. + 'reconsider: they may want to wait for ongoing builds to '. + 'complete, or fix failed builds before landing the change.'. + "\n\n". + 'If you do not want to warn users about this build, you can '. + 'change the warning behavior. This may be useful if the build '. + 'takes a long time to run (so you do not expect users to wait '. + 'for it) or the outcome is not important.'. + "\n\n". + 'This warning is only advisory. Users may always elect to ignore '. + 'this warning and continue, even if builds have failed.')) + ->setOptions($land_options), + id(new self()) + ->setKey('buildable') + ->setEditInstructions( + pht( + 'The overall state of a buildable (like a commit or revision) is '. + 'normally the aggregation of the individual states of all builds '. + 'that have run against it.'. + "\n\n". + 'Buildables are "building" until all builds pass (which changes '. + 'them to "pass"), or any build fails (which changes them to '. + '"fail").'. + "\n\n". + 'You can change this behavior if you do not want to wait for this '. + 'build, or do not care if it fails.')) + ->setName(pht('Affects Buildable')) + ->setOptions($aggregate_options), + id(new self()) + ->setKey('restartable') + ->setEditInstructions( + pht( + 'Usually, builds may be restarted. This may be useful if you '. + 'suspect a build has failed for environmental or circumstantial '. + 'reasons unrelated to the actual code, and want to give it '. + 'another chance at glory.'. + "\n\n". + 'If you want to prevent a build from being restarted, you can '. + 'change the behavior here. This may be useful to prevent '. + 'accidents where a build with a dangerous side effect (like '. + 'deployment) is restarted improperly.')) + ->setName(pht('Restartable')) + ->setOptions($restart_options), + id(new self()) + ->setKey('runnable') + ->setEditInstructions( + pht( + 'To run a build manually, you normally must have permission to '. + 'edit the related build plan. If you would prefer that anyone who '. + 'can see the build plan be able to run and restart the build, you '. + 'can change the behavior here.'. + "\n\n". + 'Note that this affects both {nav Run Plan Manually} and '. + '{nav Restart Build}, since the two actions are largely '. + 'equivalent.'. + "\n\n". + 'WARNING: This may be unsafe, particularly if the build has '. + 'side effects like deployment.'. + "\n\n". + 'If you weaken this policy, an attacker with control of an '. + 'account that has "Can View" permission but not "Can Edit" '. + 'permission can manually run this build against any old version '. + 'of the code, including versions with known security issues.'. + "\n\n". + 'If running the build has a side effect like deploying code, '. + 'they can force deployment of a vulnerable version and then '. + 'escalate into an attack against the deployed service.')) + ->setName(pht('Runnable')) + ->setOptions($run_options), + ); + + return mpull($behaviors, null, 'getKey'); + } + +} diff --git a/src/applications/harbormaster/plan/HarbormasterBuildPlanBehaviorOption.php b/src/applications/harbormaster/plan/HarbormasterBuildPlanBehaviorOption.php new file mode 100644 index 0000000000..65b9662b9f --- /dev/null +++ b/src/applications/harbormaster/plan/HarbormasterBuildPlanBehaviorOption.php @@ -0,0 +1,57 @@ +name = $name; + return $this; + } + + public function getName() { + return $this->name; + } + + public function setKey($key) { + $this->key = $key; + return $this; + } + + public function getKey() { + return $this->key; + } + + public function setDescription($description) { + $this->description = $description; + return $this; + } + + public function getDescription() { + return $this->description; + } + + public function setIsDefault($is_default) { + $this->isDefault = $is_default; + return $this; + } + + public function getIsDefault() { + return $this->isDefault; + } + + public function setIcon($icon) { + $this->icon = $icon; + return $this; + } + + public function getIcon() { + return $this->icon; + } + +} diff --git a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php index 9d2266e487..8f8b0a20a8 100644 --- a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php +++ b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php @@ -17,6 +17,7 @@ final class HarbormasterBuildPlan extends HarbormasterDAO protected $planAutoKey; protected $viewPolicy; protected $editPolicy; + protected $properties = array(); const STATUS_ACTIVE = 'active'; const STATUS_DISABLED = 'disabled'; @@ -45,6 +46,9 @@ final class HarbormasterBuildPlan extends HarbormasterDAO protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, + self::CONFIG_SERIALIZATION => array( + 'properties' => self::SERIALIZATION_JSON, + ), self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'sort128', 'planStatus' => 'text32', @@ -94,6 +98,15 @@ final class HarbormasterBuildPlan extends HarbormasterDAO return pht('Plan %d', $this->getID()); } + public function getPlanProperty($key, $default = null) { + return idx($this->properties, $key, $default); + } + + public function setPlanProperty($key, $value) { + $this->properties[$key] = $value; + return $this; + } + /* -( Autoplans )---------------------------------------------------------- */ diff --git a/src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanBehaviorTransaction.php b/src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanBehaviorTransaction.php new file mode 100644 index 0000000000..7a65eefdfa --- /dev/null +++ b/src/applications/harbormaster/xaction/plan/HarbormasterBuildPlanBehaviorTransaction.php @@ -0,0 +1,127 @@ +getBehavior(); + return $behavior->getPlanOption($object)->getKey(); + } + + public function applyInternalEffects($object, $value) { + $key = $this->getStorageKey(); + return $object->setPlanProperty($key, $value); + } + + public function getTitle() { + $old_value = $this->getOldValue(); + $new_value = $this->getNewValue(); + + $behavior = $this->getBehavior(); + if ($behavior) { + $behavior_name = $behavior->getName(); + + $options = $behavior->getOptions(); + if (isset($options[$old_value])) { + $old_value = $options[$old_value]->getName(); + } + + if (isset($options[$new_value])) { + $new_value = $options[$new_value]->getName(); + } + } else { + $behavior_name = $this->getBehaviorKey(); + } + + return pht( + '%s changed the %s behavior for this plan from %s to %s.', + $this->renderAuthor(), + $this->renderValue($behavior_name), + $this->renderValue($old_value), + $this->renderValue($new_value)); + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + $behaviors = HarbormasterBuildPlanBehavior::newPlanBehaviors(); + $behaviors = mpull($behaviors, null, 'getKey'); + + foreach ($xactions as $xaction) { + $key = $this->getBehaviorKeyForTransaction($xaction); + + if (!isset($behaviors[$key])) { + $errors[] = $this->newInvalidError( + pht( + 'No behavior with key "%s" exists. Valid keys are: %s.', + $key, + implode(', ', array_keys($behaviors))), + $xaction); + continue; + } + + $behavior = $behaviors[$key]; + $options = $behavior->getOptions(); + + $storage_key = HarbormasterBuildPlanBehavior::getStorageKeyForBehaviorKey( + $key); + $old = $object->getPlanProperty($storage_key); + $new = $xaction->getNewValue(); + + if ($old === $new) { + continue; + } + + if (!isset($options[$new])) { + $errors[] = $this->newInvalidError( + pht( + 'Value "%s" is not a valid option for behavior "%s". Valid '. + 'options are: %s.', + $new, + $key, + implode(', ', array_keys($options))), + $xaction); + continue; + } + } + + return $errors; + } + + public function getTransactionTypeForConduit($xaction) { + return 'behavior'; + } + + public function getFieldValuesForConduit($xaction, $data) { + return array( + 'key' => $this->getBehaviorKeyForTransaction($xaction), + 'old' => $xaction->getOldValue(), + 'new' => $xaction->getNewValue(), + ); + } + + private function getBehaviorKeyForTransaction( + PhabricatorApplicationTransaction $xaction) { + $metadata_key = HarbormasterBuildPlanBehavior::getTransactionMetadataKey(); + return $xaction->getMetadataValue($metadata_key); + } + + private function getBehaviorKey() { + $metadata_key = HarbormasterBuildPlanBehavior::getTransactionMetadataKey(); + return $this->getMetadataValue($metadata_key); + } + + private function getBehavior() { + $behavior_key = $this->getBehaviorKey(); + $behaviors = HarbormasterBuildPlanBehavior::newPlanBehaviors(); + return idx($behaviors, $behavior_key); + } + + private function getStorageKey() { + return HarbormasterBuildPlanBehavior::getStorageKeyForBehaviorKey( + $this->getBehaviorKey()); + } + +} From 983cf885e7ce2d3765cd248c9ae43979bd074dcf Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 28 Feb 2019 11:00:20 -0800 Subject: [PATCH 128/245] Expose Build Plan behaviors via "harbormaster.buildplan.search" Summary: Ref T13258. This will support changing behaviors in "arc land". Test Plan: Called "harbormaster.buildplan.search", saw behavior information in results. Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13258 Differential Revision: https://secure.phabricator.com/D20228 --- .../configuration/HarbormasterBuildPlan.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php index 8f8b0a20a8..faecb79a3a 100644 --- a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php +++ b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php @@ -233,15 +233,31 @@ final class HarbormasterBuildPlan extends HarbormasterDAO ->setKey('status') ->setType('map') ->setDescription(pht('The current status of this build plan.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('behaviors') + ->setType('map') + ->setDescription(pht('Behavior configuration for the build plan.')), ); } public function getFieldValuesForConduit() { + $behavior_map = array(); + + $behaviors = HarbormasterBuildPlanBehavior::newPlanBehaviors(); + foreach ($behaviors as $behavior) { + $option = $behavior->getPlanOption($this); + + $behavior_map[$behavior->getKey()] = array( + 'value' => $option->getKey(), + ); + } + return array( 'name' => $this->getName(), 'status' => array( 'value' => $this->getPlanStatus(), ), + 'behaviors' => $behavior_map, ); } From ee0ad4703ebdc47345ba220923ca42609ed7c3a3 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 28 Feb 2019 11:17:04 -0800 Subject: [PATCH 129/245] Make the new Build Plan "Runnable" behavior work Summary: Ref T13258. Fixes T11415. This makes "Runnable" actually do something: - With "Runnable" set to "If Editable" (default): to manually run, pause, resume, abort, or restart a build, you must normally be able to edit the associated build plan. - If you toggle "Runnable" to "If Viewable", anyone who can view the build plan may take these actions. This is pretty straightforward since T9614 already got us pretty close to this ruleset a while ago. Test Plan: - Created a Build Plan, set "Can Edit" to just me, toggled "Runnable" to "If Viewable"/"If Editable", tried to take actions as another user. - With "If Editable", unable to run, pause, resume, abort, or restart as another user. - With "If Viewable", those actions work. Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13258, T11415 Differential Revision: https://secure.phabricator.com/D20229 --- src/__phutil_library_map__.php | 3 ++ .../HarbormasterBuildPlanPolicyCodex.php | 38 ++++++++++++++++ .../HarbormasterPlanRunController.php | 7 +-- .../HarbormasterPlanViewController.php | 2 +- .../plan/HarbormasterBuildPlanBehavior.php | 28 +++++++++--- .../storage/build/HarbormasterBuild.php | 11 +++-- .../configuration/HarbormasterBuildPlan.php | 44 ++++++++++++++++++- 7 files changed, 115 insertions(+), 18 deletions(-) create mode 100644 src/applications/harbormaster/codex/HarbormasterBuildPlanPolicyCodex.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 15c7637e3d..4eedb44b22 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1340,6 +1340,7 @@ phutil_register_library_map(array( 'HarbormasterBuildPlanNameNgrams' => 'applications/harbormaster/storage/configuration/HarbormasterBuildPlanNameNgrams.php', 'HarbormasterBuildPlanNameTransaction' => 'applications/harbormaster/xaction/plan/HarbormasterBuildPlanNameTransaction.php', 'HarbormasterBuildPlanPHIDType' => 'applications/harbormaster/phid/HarbormasterBuildPlanPHIDType.php', + 'HarbormasterBuildPlanPolicyCodex' => 'applications/harbormaster/codex/HarbormasterBuildPlanPolicyCodex.php', 'HarbormasterBuildPlanQuery' => 'applications/harbormaster/query/HarbormasterBuildPlanQuery.php', 'HarbormasterBuildPlanSearchAPIMethod' => 'applications/harbormaster/conduit/HarbormasterBuildPlanSearchAPIMethod.php', 'HarbormasterBuildPlanSearchEngine' => 'applications/harbormaster/query/HarbormasterBuildPlanSearchEngine.php', @@ -6945,6 +6946,7 @@ phutil_register_library_map(array( 'PhabricatorNgramsInterface', 'PhabricatorConduitResultInterface', 'PhabricatorProjectInterface', + 'PhabricatorPolicyCodexInterface', ), 'HarbormasterBuildPlanBehavior' => 'Phobject', 'HarbormasterBuildPlanBehaviorOption' => 'Phobject', @@ -6958,6 +6960,7 @@ phutil_register_library_map(array( 'HarbormasterBuildPlanNameNgrams' => 'PhabricatorSearchNgrams', 'HarbormasterBuildPlanNameTransaction' => 'HarbormasterBuildPlanTransactionType', 'HarbormasterBuildPlanPHIDType' => 'PhabricatorPHIDType', + 'HarbormasterBuildPlanPolicyCodex' => 'PhabricatorPolicyCodex', 'HarbormasterBuildPlanQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'HarbormasterBuildPlanSearchAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'HarbormasterBuildPlanSearchEngine' => 'PhabricatorApplicationSearchEngine', diff --git a/src/applications/harbormaster/codex/HarbormasterBuildPlanPolicyCodex.php b/src/applications/harbormaster/codex/HarbormasterBuildPlanPolicyCodex.php new file mode 100644 index 0000000000..a17f2fb293 --- /dev/null +++ b/src/applications/harbormaster/codex/HarbormasterBuildPlanPolicyCodex.php @@ -0,0 +1,38 @@ +getObject(); + $run_with_view = $object->canRunWithoutEditCapability(); + + $rules = array(); + + $rules[] = $this->newRule() + ->setCapabilities( + array( + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->setIsActive(!$run_with_view) + ->setDescription( + pht( + 'You must have edit permission on this build plan to pause, '. + 'abort, resume, or restart it.')); + + $rules[] = $this->newRule() + ->setCapabilities( + array( + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->setIsActive(!$run_with_view) + ->setDescription( + pht( + 'You must have edit permission on this build plan to run it '. + 'manually.')); + + return $rules; + } + + +} diff --git a/src/applications/harbormaster/controller/HarbormasterPlanRunController.php b/src/applications/harbormaster/controller/HarbormasterPlanRunController.php index fd227ee554..5d80d421aa 100644 --- a/src/applications/harbormaster/controller/HarbormasterPlanRunController.php +++ b/src/applications/harbormaster/controller/HarbormasterPlanRunController.php @@ -9,16 +9,13 @@ final class HarbormasterPlanRunController extends HarbormasterPlanController { $plan = id(new HarbormasterBuildPlanQuery()) ->setViewer($viewer) ->withIDs(array($plan_id)) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) ->executeOne(); if (!$plan) { return new Aphront404Response(); } + $plan->assertHasRunCapability($viewer); + $cancel_uri = $this->getApplicationURI("plan/{$plan_id}/"); if (!$plan->canRunManually()) { diff --git a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php index 0ef6162aad..141f321caa 100644 --- a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php @@ -266,7 +266,7 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController { ->setIcon('fa-ban')); } - $can_run = ($can_edit && $plan->canRunManually()); + $can_run = ($plan->hasRunCapability($viewer) && $plan->canRunManually()); $curtain->addAction( id(new PhabricatorActionView()) diff --git a/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php b/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php index b0f722a1ca..893d351065 100644 --- a/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php +++ b/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php @@ -9,6 +9,10 @@ final class HarbormasterBuildPlanBehavior private $defaultKey; private $editInstructions; + const BEHAVIOR_RUNNABLE = 'runnable'; + const RUNNABLE_IF_VIEWABLE = 'view'; + const RUNNABLE_IF_EDITABLE = 'edit'; + public function setKey($key) { $this->key = $key; return $this; @@ -114,6 +118,19 @@ final class HarbormasterBuildPlanBehavior return sprintf('behavior.%s', $behavior_key); } + public static function getBehavior($key) { + $behaviors = self::newPlanBehaviors(); + + if (!isset($behaviors[$key])) { + throw new Exception( + pht( + 'No build plan behavior with key "%s" exists.', + $key)); + } + + return $behaviors[$key]; + } + public static function newPlanBehaviors() { $draft_options = array( id(new HarbormasterBuildPlanBehaviorOption()) @@ -224,14 +241,14 @@ final class HarbormasterBuildPlanBehavior $run_options = array( id(new HarbormasterBuildPlanBehaviorOption()) - ->setKey('edit') + ->setKey(self::RUNNABLE_IF_EDITABLE) ->setIcon('fa-pencil green') ->setName(pht('If Editable')) ->setIsDefault(true) ->setDescription( pht('Only users who can edit the plan can run it manually.')), id(new HarbormasterBuildPlanBehaviorOption()) - ->setKey('view') + ->setKey(self::RUNNABLE_IF_VIEWABLE) ->setIcon('fa-exclamation-triangle yellow') ->setName(pht('If Viewable')) ->setDescription( @@ -315,7 +332,7 @@ final class HarbormasterBuildPlanBehavior ->setName(pht('Restartable')) ->setOptions($restart_options), id(new self()) - ->setKey('runnable') + ->setKey(self::BEHAVIOR_RUNNABLE) ->setEditInstructions( pht( 'To run a build manually, you normally must have permission to '. @@ -323,9 +340,8 @@ final class HarbormasterBuildPlanBehavior 'can see the build plan be able to run and restart the build, you '. 'can change the behavior here.'. "\n\n". - 'Note that this affects both {nav Run Plan Manually} and '. - '{nav Restart Build}, since the two actions are largely '. - 'equivalent.'. + 'Note that this controls access to all build management actions: '. + '"Run Plan Manually", "Restart", "Abort", "Pause", and "Resume".'. "\n\n". 'WARNING: This may be unsafe, particularly if the build has '. 'side effects like deployment.'. diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuild.php b/src/applications/harbormaster/storage/build/HarbormasterBuild.php index 84668b79f0..9b7b64d06b 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuild.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuild.php @@ -334,14 +334,17 @@ final class HarbormasterBuild extends HarbormasterDAO } public function assertCanIssueCommand(PhabricatorUser $viewer, $command) { - $need_edit = false; + $plan = $this->getBuildPlan(); + + $need_edit = true; switch ($command) { case HarbormasterBuildCommand::COMMAND_RESTART: - break; case HarbormasterBuildCommand::COMMAND_PAUSE: case HarbormasterBuildCommand::COMMAND_RESUME: case HarbormasterBuildCommand::COMMAND_ABORT: - $need_edit = true; + if ($plan->canRunWithoutEditCapability()) { + $need_edit = false; + } break; default: throw new Exception( @@ -355,7 +358,7 @@ final class HarbormasterBuild extends HarbormasterDAO if ($need_edit) { PhabricatorPolicyFilter::requireCapability( $viewer, - $this->getBuildPlan(), + $plan, PhabricatorPolicyCapability::CAN_EDIT); } } diff --git a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php index faecb79a3a..798201f490 100644 --- a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php +++ b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php @@ -10,7 +10,8 @@ final class HarbormasterBuildPlan extends HarbormasterDAO PhabricatorSubscribableInterface, PhabricatorNgramsInterface, PhabricatorConduitResultInterface, - PhabricatorProjectInterface { + PhabricatorProjectInterface, + PhabricatorPolicyCodexInterface { protected $name; protected $planStatus; @@ -133,7 +134,6 @@ final class HarbormasterBuildPlan extends HarbormasterDAO return true; } - public function getName() { $autoplan = $this->getAutoplan(); if ($autoplan) { @@ -143,6 +143,38 @@ final class HarbormasterBuildPlan extends HarbormasterDAO return parent::getName(); } + public function hasRunCapability(PhabricatorUser $viewer) { + try { + $this->assertHasRunCapability($viewer); + return true; + } catch (PhabricatorPolicyException $ex) { + return false; + } + } + + public function canRunWithoutEditCapability() { + $runnable = HarbormasterBuildPlanBehavior::BEHAVIOR_RUNNABLE; + $if_viewable = HarbormasterBuildPlanBehavior::RUNNABLE_IF_VIEWABLE; + + $option = HarbormasterBuildPlanBehavior::getBehavior($runnable) + ->getPlanOption($this); + + return ($option->getKey() === $if_viewable); + } + + public function assertHasRunCapability(PhabricatorUser $viewer) { + if ($this->canRunWithoutEditCapability()) { + $capability = PhabricatorPolicyCapability::CAN_VIEW; + } else { + $capability = PhabricatorPolicyCapability::CAN_EDIT; + } + + PhabricatorPolicyFilter::requireCapability( + $viewer, + $this, + $capability); + } + /* -( PhabricatorSubscribableInterface )----------------------------------- */ @@ -265,4 +297,12 @@ final class HarbormasterBuildPlan extends HarbormasterDAO return array(); } + +/* -( PhabricatorPolicyCodexInterface )------------------------------------ */ + + + public function newPolicyCodex() { + return new HarbormasterBuildPlanPolicyCodex(); + } + } From 578de333dfa5a739fac9326563672689fea99538 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 28 Feb 2019 12:03:28 -0800 Subject: [PATCH 130/245] Make the new Build Plan behavior "Restartable" work Summary: Ref T13258. Implements the "Restartable" behavior, to control whether a build may be restarted or not. This is fairly straightforward because there are already other existing reasons that a build may not be able to be restarted. Test Plan: Restarted a build. Marked it as not restartable, saw "Restart" action become disabled. Tried to restart it anyway, got a useful error message. Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13258 Differential Revision: https://secure.phabricator.com/D20230 --- .../controller/HarbormasterBuildActionController.php | 10 +++++++--- .../plan/HarbormasterBuildPlanBehavior.php | 10 +++++++--- .../harbormaster/storage/build/HarbormasterBuild.php | 5 +++++ .../storage/configuration/HarbormasterBuildPlan.php | 10 ++++++++++ 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/applications/harbormaster/controller/HarbormasterBuildActionController.php b/src/applications/harbormaster/controller/HarbormasterBuildActionController.php index 843ffd4702..b56c7de7f7 100644 --- a/src/applications/harbormaster/controller/HarbormasterBuildActionController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildActionController.php @@ -64,6 +64,11 @@ final class HarbormasterBuildActionController 'restart. Side effects of the build will occur again. Really '. 'restart build?'); $submit = pht('Restart Build'); + } else if (!$build->getBuildPlan()->canRestartBuildPlan()) { + $title = pht('Not Restartable'); + $body = pht( + 'The build plan for this build is not restartable, so you '. + 'can not restart the build.'); } else { $title = pht('Unable to Restart Build'); if ($build->isRestarting()) { @@ -135,8 +140,7 @@ final class HarbormasterBuildActionController break; } - $dialog = id(new AphrontDialogView()) - ->setUser($viewer) + $dialog = $this->newDialog() ->setTitle($title) ->appendChild($body) ->addCancelButton($return_uri); @@ -145,7 +149,7 @@ final class HarbormasterBuildActionController $dialog->addSubmitButton($submit); } - return id(new AphrontDialogResponse())->setDialog($dialog); + return $dialog; } } diff --git a/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php b/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php index 893d351065..66c186a0d1 100644 --- a/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php +++ b/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php @@ -13,6 +13,10 @@ final class HarbormasterBuildPlanBehavior const RUNNABLE_IF_VIEWABLE = 'view'; const RUNNABLE_IF_EDITABLE = 'edit'; + const BEHAVIOR_RESTARTABLE = 'restartable'; + const RESTARTABLE_ALWAYS = 'always'; + const RESTARTABLE_NEVER = 'never'; + public function setKey($key) { $this->key = $key; return $this; @@ -225,14 +229,14 @@ final class HarbormasterBuildPlanBehavior $restart_options = array( id(new HarbormasterBuildPlanBehaviorOption()) - ->setKey('always') + ->setKey(self::RESTARTABLE_ALWAYS) ->setIcon('fa-repeat green') ->setName(pht('Always')) ->setIsDefault(true) ->setDescription( pht('The build may be restarted.')), id(new HarbormasterBuildPlanBehaviorOption()) - ->setKey('never') + ->setKey(self::RESTARTABLE_NEVER) ->setIcon('fa-times red') ->setName(pht('Never')) ->setDescription( @@ -317,7 +321,7 @@ final class HarbormasterBuildPlanBehavior ->setName(pht('Affects Buildable')) ->setOptions($aggregate_options), id(new self()) - ->setKey('restartable') + ->setKey(self::BEHAVIOR_RESTARTABLE) ->setEditInstructions( pht( 'Usually, builds may be restarted. This may be useful if you '. diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuild.php b/src/applications/harbormaster/storage/build/HarbormasterBuild.php index 9b7b64d06b..063f81ff1e 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuild.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuild.php @@ -215,6 +215,11 @@ final class HarbormasterBuild extends HarbormasterDAO return false; } + $plan = $this->getBuildPlan(); + if (!$plan->canRestartBuildPlan()) { + return false; + } + return !$this->isRestarting(); } diff --git a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php index 798201f490..efe62a6f84 100644 --- a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php +++ b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php @@ -175,6 +175,16 @@ final class HarbormasterBuildPlan extends HarbormasterDAO $capability); } + public function canRestartBuildPlan() { + $restartable = HarbormasterBuildPlanBehavior::BEHAVIOR_RESTARTABLE; + $is_restartable = HarbormasterBuildPlanBehavior::RESTARTABLE_ALWAYS; + + $option = HarbormasterBuildPlanBehavior::getBehavior($restartable) + ->getPlanOption($this); + + return ($option->getKey() === $is_restartable); + } + /* -( PhabricatorSubscribableInterface )----------------------------------- */ From 718cdc24471ace13a6f2c9e2d66e5d2019dcb436 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 28 Feb 2019 13:02:14 -0800 Subject: [PATCH 131/245] Implement Build Plan "Hold Drafts" behavior Summary: Ref T13258. Makes the new "Hold Drafts" behavior actually work. Test Plan: - Created a build plan which does "Make HTTP Request" somewhere random and then waits for a message. - Created a Herald rule which "Always" runs this plan. - Created revisions, loaded them, then sent their build targets a "fail" message a short time later. - With "Always": Current behavior. Revision was held as a draft while building, and returned to me for changes when the build failed. - With "If Building": Revision was held as a draft while building, but promoted once the build failed. - With "Never": Revision promoted immediately, ignoring the build completely. Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13258 Differential Revision: https://secure.phabricator.com/D20232 --- .../storage/DifferentialRevision.php | 37 ++++++++++++++++++- .../plan/HarbormasterBuildPlanBehavior.php | 13 +++++-- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/src/applications/differential/storage/DifferentialRevision.php b/src/applications/differential/storage/DifferentialRevision.php index 3397f9cb03..a2a058568b 100644 --- a/src/applications/differential/storage/DifferentialRevision.php +++ b/src/applications/differential/storage/DifferentialRevision.php @@ -877,7 +877,7 @@ final class DifferentialRevision extends DifferentialDAO PhabricatorUser $viewer, array $phids) { - return id(new HarbormasterBuildQuery()) + $builds = id(new HarbormasterBuildQuery()) ->setViewer($viewer) ->withBuildablePHIDs($phids) ->withAutobuilds(false) @@ -893,6 +893,41 @@ final class DifferentialRevision extends DifferentialDAO HarbormasterBuildStatus::STATUS_DEADLOCKED, )) ->execute(); + + // Filter builds based on the "Hold Drafts" behavior of their associated + // build plans. + + $hold_drafts = HarbormasterBuildPlanBehavior::BEHAVIOR_DRAFTS; + $behavior = HarbormasterBuildPlanBehavior::getBehavior($hold_drafts); + + $key_never = HarbormasterBuildPlanBehavior::DRAFTS_NEVER; + $key_building = HarbormasterBuildPlanBehavior::DRAFTS_IF_BUILDING; + + foreach ($builds as $key => $build) { + $plan = $build->getBuildPlan(); + $hold_key = $behavior->getPlanOption($plan)->getKey(); + + $hold_never = ($hold_key === $key_never); + $hold_building = ($hold_key === $key_building); + + // If the build "Never" holds drafts from promoting, we don't care what + // the status is. + if ($hold_never) { + unset($builds[$key]); + continue; + } + + // If the build holds drafts from promoting "While Building", we only + // care about the status until it completes. + if ($hold_building) { + if ($build->isComplete()) { + unset($builds[$key]); + continue; + } + } + } + + return $builds; } diff --git a/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php b/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php index 66c186a0d1..63fc263fcb 100644 --- a/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php +++ b/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php @@ -17,6 +17,11 @@ final class HarbormasterBuildPlanBehavior const RESTARTABLE_ALWAYS = 'always'; const RESTARTABLE_NEVER = 'never'; + const BEHAVIOR_DRAFTS = 'hold-drafts'; + const DRAFTS_ALWAYS = 'always'; + const DRAFTS_IF_BUILDING = 'building'; + const DRAFTS_NEVER = 'never'; + public function setKey($key) { $this->key = $key; return $this; @@ -138,7 +143,7 @@ final class HarbormasterBuildPlanBehavior public static function newPlanBehaviors() { $draft_options = array( id(new HarbormasterBuildPlanBehaviorOption()) - ->setKey('always') + ->setKey(self::DRAFTS_ALWAYS) ->setIcon('fa-check-circle-o green') ->setName(pht('Always')) ->setIsDefault(true) @@ -147,7 +152,7 @@ final class HarbormasterBuildPlanBehavior 'Revisions are not sent for review until the build completes, '. 'and are returned to the author for updates if the build fails.')), id(new HarbormasterBuildPlanBehaviorOption()) - ->setKey('building') + ->setKey(self::DRAFTS_IF_BUILDING) ->setIcon('fa-pause-circle-o yellow') ->setName(pht('If Building')) ->setDescription( @@ -155,7 +160,7 @@ final class HarbormasterBuildPlanBehavior 'Revisions are not sent for review until the build completes, '. 'but they will be sent for review even if it fails.')), id(new HarbormasterBuildPlanBehaviorOption()) - ->setKey('never') + ->setKey(self::DRAFTS_NEVER) ->setIcon('fa-circle-o red') ->setName(pht('Never')) ->setDescription( @@ -262,7 +267,7 @@ final class HarbormasterBuildPlanBehavior $behaviors = array( id(new self()) - ->setKey('hold-drafts') + ->setKey(self::BEHAVIOR_DRAFTS) ->setName(pht('Hold Drafts')) ->setEditInstructions( pht( From f97df9ebea90a240e66efa0ad7ac49c3de57086a Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 28 Feb 2019 13:19:21 -0800 Subject: [PATCH 132/245] Implement Build Plan behavior "Affects Buildable" Summary: Ref T13258. Make the "Affects Buildable" option actually work. Test Plan: - As in previous change, created a "wait for HTTP request" build plan and had it always run against every revision. - Created revisions, waited a bit, then sent the build a "Fail" message, with different values of "Affects Buildable": - "Always": Same behavior as today. Buildable waited for the build, then failed when it failed. - "While Building": Buildable waited for the build, but passed even though it failed (buildable has green checkmark even though build is red): {F6250359} - "Never": Buildable passed immediately (buildable has green checkmark even though build is still running): {F6250360} Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13258 Differential Revision: https://secure.phabricator.com/D20233 --- .../engine/HarbormasterBuildEngine.php | 24 +++++++++++++++++++ .../plan/HarbormasterBuildPlanBehavior.php | 13 ++++++---- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/applications/harbormaster/engine/HarbormasterBuildEngine.php b/src/applications/harbormaster/engine/HarbormasterBuildEngine.php index 170e4c8a5c..447bd53704 100644 --- a/src/applications/harbormaster/engine/HarbormasterBuildEngine.php +++ b/src/applications/harbormaster/engine/HarbormasterBuildEngine.php @@ -497,9 +497,33 @@ final class HarbormasterBuildEngine extends Phobject { // passed everything it needs to. if (!$buildable->isPreparing()) { + $behavior_key = HarbormasterBuildPlanBehavior::BEHAVIOR_BUILDABLE; + $behavior = HarbormasterBuildPlanBehavior::getBehavior($behavior_key); + + $key_never = HarbormasterBuildPlanBehavior::BUILDABLE_NEVER; + $key_building = HarbormasterBuildPlanBehavior::BUILDABLE_IF_BUILDING; + $all_pass = true; $any_fail = false; foreach ($buildable->getBuilds() as $build) { + $plan = $build->getBuildPlan(); + $option = $behavior->getPlanOption($plan); + $option_key = $option->getKey(); + + $is_never = ($option_key === $key_never); + $is_building = ($option_key === $key_building); + + // If this build "Never" affects the buildable, ignore it. + if ($is_never) { + continue; + } + + // If this build affects the buildable "If Building", but is already + // complete, ignore it. + if ($is_building && $build->isComplete()) { + continue; + } + if (!$build->isPassed()) { $all_pass = false; } diff --git a/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php b/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php index 63fc263fcb..690619f56e 100644 --- a/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php +++ b/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php @@ -22,6 +22,11 @@ final class HarbormasterBuildPlanBehavior const DRAFTS_IF_BUILDING = 'building'; const DRAFTS_NEVER = 'never'; + const BEHAVIOR_BUILDABLE = 'buildable'; + const BUILDABLE_ALWAYS = 'always'; + const BUILDABLE_IF_BUILDING = 'building'; + const BUILDABLE_NEVER = 'never'; + public function setKey($key) { $this->key = $key; return $this; @@ -207,7 +212,7 @@ final class HarbormasterBuildPlanBehavior $aggregate_options = array( id(new HarbormasterBuildPlanBehaviorOption()) - ->setKey('always') + ->setKey(self::BUILDABLE_ALWAYS) ->setIcon('fa-check-circle-o green') ->setName(pht('Always')) ->setIsDefault(true) @@ -216,7 +221,7 @@ final class HarbormasterBuildPlanBehavior 'The buildable waits for the build, and fails if the '. 'build fails.')), id(new HarbormasterBuildPlanBehaviorOption()) - ->setKey('building') + ->setKey(self::BUILDABLE_IF_BUILDING) ->setIcon('fa-pause-circle-o yellow') ->setName(pht('If Building')) ->setDescription( @@ -224,7 +229,7 @@ final class HarbormasterBuildPlanBehavior 'The buildable waits for the build, but does not fail '. 'if the build fails.')), id(new HarbormasterBuildPlanBehaviorOption()) - ->setKey('never') + ->setKey(self::BUILDABLE_NEVER) ->setIcon('fa-circle-o red') ->setName(pht('Never')) ->setDescription( @@ -310,7 +315,7 @@ final class HarbormasterBuildPlanBehavior 'this warning and continue, even if builds have failed.')) ->setOptions($land_options), id(new self()) - ->setKey('buildable') + ->setKey(self::BEHAVIOR_BUILDABLE) ->setEditInstructions( pht( 'The overall state of a buildable (like a commit or revision) is '. From 7e468123446742f7b398d55cca525eb53d475b0c Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 1 Mar 2019 07:45:04 -0800 Subject: [PATCH 133/245] Add a warning to revision timelines when changes land with ongoing or failed builds Summary: Ref T13258. The general idea here is "if arc land prompted you and you hit 'y', you get a warning about it on the timeline". This is similar to the existing warning about landing revisions in the wrong state and hitting "y" to get through that. See D18808, previously. These warnings make it easier to catch process issues at a glance, especially because the overall build status is now more complicated (and may legally include some failures on tests which are marked as unimportant). The transaction stores which builds had problems, but I'm not doing anything to render that for now. I think you can usually figure it out from the UI already; if not, we could refine this. Test Plan: - Used `bin/differential attach-commit` to trigger extraction/attachment. - Attached a commit to a revision with various build states, and various build plan "Warn When Landing" flags. - Got sensible warnings and non-warnings based on "Warn When Landing" setting. {F6251631} Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13258 Differential Revision: https://secure.phabricator.com/D20239 --- src/__phutil_library_map__.php | 2 + .../DifferentialDiffExtractionEngine.php | 95 +++++++++++++++++++ ...erentialRevisionWrongBuildsTransaction.php | 37 ++++++++ .../plan/HarbormasterBuildPlanBehavior.php | 21 ++-- 4 files changed, 149 insertions(+), 6 deletions(-) create mode 100644 src/applications/differential/xaction/DifferentialRevisionWrongBuildsTransaction.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 4eedb44b22..48cf3f76fe 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -653,6 +653,7 @@ phutil_register_library_map(array( 'DifferentialRevisionUpdateTransaction' => 'applications/differential/xaction/DifferentialRevisionUpdateTransaction.php', 'DifferentialRevisionViewController' => 'applications/differential/controller/DifferentialRevisionViewController.php', 'DifferentialRevisionVoidTransaction' => 'applications/differential/xaction/DifferentialRevisionVoidTransaction.php', + 'DifferentialRevisionWrongBuildsTransaction' => 'applications/differential/xaction/DifferentialRevisionWrongBuildsTransaction.php', 'DifferentialRevisionWrongStateTransaction' => 'applications/differential/xaction/DifferentialRevisionWrongStateTransaction.php', 'DifferentialSchemaSpec' => 'applications/differential/storage/DifferentialSchemaSpec.php', 'DifferentialSetDiffPropertyConduitAPIMethod' => 'applications/differential/conduit/DifferentialSetDiffPropertyConduitAPIMethod.php', @@ -6181,6 +6182,7 @@ phutil_register_library_map(array( 'DifferentialRevisionUpdateTransaction' => 'DifferentialRevisionTransactionType', 'DifferentialRevisionViewController' => 'DifferentialController', 'DifferentialRevisionVoidTransaction' => 'DifferentialRevisionTransactionType', + 'DifferentialRevisionWrongBuildsTransaction' => 'DifferentialRevisionTransactionType', 'DifferentialRevisionWrongStateTransaction' => 'DifferentialRevisionTransactionType', 'DifferentialSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'DifferentialSetDiffPropertyConduitAPIMethod' => 'DifferentialConduitAPIMethod', diff --git a/src/applications/differential/engine/DifferentialDiffExtractionEngine.php b/src/applications/differential/engine/DifferentialDiffExtractionEngine.php index 861d2ad220..7b94b1958b 100644 --- a/src/applications/differential/engine/DifferentialDiffExtractionEngine.php +++ b/src/applications/differential/engine/DifferentialDiffExtractionEngine.php @@ -285,6 +285,24 @@ final class DifferentialDiffExtractionEngine extends Phobject { ->setNewValue($revision->getModernRevisionStatus()); } + $concerning_builds = $this->loadConcerningBuilds($revision); + if ($concerning_builds) { + $build_list = array(); + foreach ($concerning_builds as $build) { + $build_list[] = array( + 'phid' => $build->getPHID(), + 'status' => $build->getBuildStatus(), + ); + } + + $wrong_builds = + DifferentialRevisionWrongBuildsTransaction::TRANSACTIONTYPE; + + $xactions[] = id(new DifferentialTransaction()) + ->setTransactionType($wrong_builds) + ->setNewValue($build_list); + } + $type_update = DifferentialRevisionUpdateTransaction::TRANSACTIONTYPE; $xactions[] = id(new DifferentialTransaction()) @@ -322,4 +340,81 @@ final class DifferentialDiffExtractionEngine extends Phobject { return $result_data; } + private function loadConcerningBuilds(DifferentialRevision $revision) { + $viewer = $this->getViewer(); + $diff = $revision->getActiveDiff(); + + $buildables = id(new HarbormasterBuildableQuery()) + ->setViewer($viewer) + ->withBuildablePHIDs(array($diff->getPHID())) + ->needBuilds(true) + ->withManualBuildables(false) + ->execute(); + if (!$buildables) { + return array(); + } + + + $land_key = HarbormasterBuildPlanBehavior::BEHAVIOR_LANDWARNING; + $behavior = HarbormasterBuildPlanBehavior::getBehavior($land_key); + + $key_never = HarbormasterBuildPlanBehavior::LANDWARNING_NEVER; + $key_building = HarbormasterBuildPlanBehavior::LANDWARNING_IF_BUILDING; + $key_complete = HarbormasterBuildPlanBehavior::LANDWARNING_IF_COMPLETE; + + $concerning_builds = array(); + foreach ($buildables as $buildable) { + $builds = $buildable->getBuilds(); + foreach ($builds as $build) { + $plan = $build->getBuildPlan(); + $option = $behavior->getPlanOption($plan); + $behavior_value = $option->getKey(); + + $if_never = ($behavior_value === $key_never); + if ($if_never) { + continue; + } + + $if_building = ($behavior_value === $key_building); + if ($if_building && $build->isComplete()) { + continue; + } + + $if_complete = ($behavior_value === $key_complete); + if ($if_complete) { + if (!$build->isComplete()) { + continue; + } + + // TODO: If you "arc land" and a build with "Warn: If Complete" + // is still running, you may not see a warning, and push the revision + // in good faith. The build may then complete before we get here, so + // we now see a completed, failed build. + + // For now, just err on the side of caution and assume these builds + // were in a good state when we prompted the user, even if they're in + // a bad state now. + + // We could refine this with a rule like "if the build finished + // within a couple of minutes before the push happened, assume it was + // in good faith", but we don't currently have an especially + // convenient way to check when the build finished or when the commit + // was pushed or discovered, and this would create some issues in + // cases where the repository is observed and the fetch pipeline + // stalls for a while. + + continue; + } + + if ($build->isPassed()) { + continue; + } + + $concerning_builds[] = $build; + } + } + + return $concerning_builds; + } + } diff --git a/src/applications/differential/xaction/DifferentialRevisionWrongBuildsTransaction.php b/src/applications/differential/xaction/DifferentialRevisionWrongBuildsTransaction.php new file mode 100644 index 0000000000..260813b75b --- /dev/null +++ b/src/applications/differential/xaction/DifferentialRevisionWrongBuildsTransaction.php @@ -0,0 +1,37 @@ +key = $key; return $this; @@ -176,7 +182,7 @@ final class HarbormasterBuildPlanBehavior $land_options = array( id(new HarbormasterBuildPlanBehaviorOption()) - ->setKey('always') + ->setKey(self::LANDWARNING_ALWAYS) ->setIcon('fa-check-circle-o green') ->setName(pht('Always')) ->setIsDefault(true) @@ -185,7 +191,7 @@ final class HarbormasterBuildPlanBehavior '"arc land" warns if the build is still running or has '. 'failed.')), id(new HarbormasterBuildPlanBehaviorOption()) - ->setKey('building') + ->setKey(self::LANDWARNING_IF_BUILDING) ->setIcon('fa-pause-circle-o yellow') ->setName(pht('If Building')) ->setDescription( @@ -193,7 +199,7 @@ final class HarbormasterBuildPlanBehavior '"arc land" warns if the build is still running, but ignores '. 'the build if it has failed.')), id(new HarbormasterBuildPlanBehaviorOption()) - ->setKey('complete') + ->setKey(self::LANDWARNING_IF_COMPLETE) ->setIcon('fa-dot-circle-o yellow') ->setName(pht('If Complete')) ->setDescription( @@ -201,7 +207,7 @@ final class HarbormasterBuildPlanBehavior '"arc land" warns if the build has failed, but ignores the '. 'build if it is still running.')), id(new HarbormasterBuildPlanBehaviorOption()) - ->setKey('never') + ->setKey(self::LANDWARNING_NEVER) ->setIcon('fa-circle-o red') ->setName(pht('Never')) ->setDescription( @@ -296,7 +302,7 @@ final class HarbormasterBuildPlanBehavior 'revision, even if builds have failed or are still in progress.')) ->setOptions($draft_options), id(new self()) - ->setKey('arc-land') + ->setKey(self::BEHAVIOR_LANDWARNING) ->setName(pht('Warn When Landing')) ->setEditInstructions( pht( @@ -312,7 +318,10 @@ final class HarbormasterBuildPlanBehavior 'for it) or the outcome is not important.'. "\n\n". 'This warning is only advisory. Users may always elect to ignore '. - 'this warning and continue, even if builds have failed.')) + 'this warning and continue, even if builds have failed.'. + "\n\n". + 'This setting also affects the warning that is published to '. + 'revisions when commits land with ongoing or failed builds.')) ->setOptions($land_options), id(new self()) ->setKey(self::BEHAVIOR_BUILDABLE) From a3ebaac0f026411268ca1dd2856b6762acebcc23 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 6 Mar 2019 08:30:18 -0800 Subject: [PATCH 134/245] Tweak the visual style of the ">>" / "<<" depth change indicators slightly Summary: Ref T13249. - When a line has only increased in indent depth, don't red-fill highlight the left side of the diff. Since reading a diff //mostly// involves focusing on the right side, indent depth changes are generally visible enough without this extra hint. The extra hint can become distracting in cases where there is a large block of indent depth changes. - Move the markers slightly to the left, to align them with the gutter. - Make them slightly opaque so they're a little less prominent. Test Plan: See screenshots. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13249 Differential Revision: https://secure.phabricator.com/D20251 --- resources/celerity/map.php | 12 ++++----- .../DifferentialChangesetTwoUpRenderer.php | 26 +++++++++++++------ .../differential/changeset-view.css | 3 +++ 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 11e7fe4118..7f9aaa4995 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -11,7 +11,7 @@ return array( 'conpherence.pkg.js' => '020aebcf', 'core.pkg.css' => '34ce1741', 'core.pkg.js' => '2cda17a4', - 'differential.pkg.css' => 'ab23bd75', + 'differential.pkg.css' => '1755a478', 'differential.pkg.js' => '67e02996', 'diffusion.pkg.css' => '42c75c37', 'diffusion.pkg.js' => '91192d85', @@ -61,7 +61,7 @@ return array( 'rsrc/css/application/dashboard/dashboard.css' => '4267d6c6', 'rsrc/css/application/diff/inline-comment-summary.css' => '81eb368d', 'rsrc/css/application/differential/add-comment.css' => '7e5900d9', - 'rsrc/css/application/differential/changeset-view.css' => 'd92bed0d', + 'rsrc/css/application/differential/changeset-view.css' => '4193eeff', 'rsrc/css/application/differential/core.css' => '7300a73e', 'rsrc/css/application/differential/phui-inline-comment.css' => '48acce5b', 'rsrc/css/application/differential/revision-comment.css' => '7dbc8d1d', @@ -540,7 +540,7 @@ return array( 'conpherence-thread-manager' => 'aec8e38c', 'conpherence-transaction-css' => '3a3f5e7e', 'd3' => 'd67475f5', - 'differential-changeset-view-css' => 'd92bed0d', + 'differential-changeset-view-css' => '4193eeff', 'differential-core-view-css' => '7300a73e', 'differential-revision-add-comment-css' => '7e5900d9', 'differential-revision-comment-css' => '7dbc8d1d', @@ -1220,6 +1220,9 @@ return array( 'javelin-behavior', 'javelin-uri', ), + '4193eeff' => array( + 'phui-inline-comment-view-css', + ), '4234f572' => array( 'syntax-default-css', ), @@ -1997,9 +2000,6 @@ return array( 'javelin-util', 'phabricator-shaped-request', ), - 'd92bed0d' => array( - 'phui-inline-comment-view-css', - ), 'da15d3dc' => array( 'phui-oi-list-view-css', ), diff --git a/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php b/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php index 7efd29519e..d803e92c6c 100644 --- a/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php +++ b/src/applications/differential/render/DifferentialChangesetTwoUpRenderer.php @@ -162,7 +162,20 @@ final class DifferentialChangesetTwoUpRenderer } else if (empty($new_lines[$ii])) { $o_class = 'old old-full'; } else { - $o_class = 'old'; + if (isset($depth_only[$ii])) { + if ($depth_only[$ii] == '>') { + // When a line has depth-only change, we only highlight the + // left side of the diff if the depth is decreasing. When the + // depth is increasing, the ">>" marker on the right hand side + // of the diff generally provides enough visibility on its own. + + $o_class = ''; + } else { + $o_class = 'old'; + } + } else { + $o_class = 'old'; + } } $o_classes = $o_class; } @@ -200,13 +213,10 @@ final class DifferentialChangesetTwoUpRenderer } else if (empty($old_lines[$ii])) { $n_class = 'new new-full'; } else { - - // NOTE: At least for the moment, I'm intentionally clearing the - // line highlighting only on the right side of the diff when a - // line has only depth changes. When a block depth is decreased, - // this gives us a large color block on the left (to make it easy - // to see the depth change) but a clean diff on the right (to make - // it easy to pick out actual code changes). + // When a line has a depth-only change, never highlight it on + // the right side. The ">>" marker generally provides enough + // visibility on its own for indent depth increases, and the left + // side is still highlighted for indent depth decreases. if (isset($depth_only[$ii])) { $n_class = ''; diff --git a/webroot/rsrc/css/application/differential/changeset-view.css b/webroot/rsrc/css/application/differential/changeset-view.css index 6ed939a2ee..844690abd3 100644 --- a/webroot/rsrc/css/application/differential/changeset-view.css +++ b/webroot/rsrc/css/application/differential/changeset-view.css @@ -126,6 +126,9 @@ background-size: 12px 12px; background-repeat: no-repeat; background-position: left center; + position: relative; + left: -8px; + opacity: 0.5; } .differential-diff td span.depth-out { From 9918ea1fb7caa6e8abdbc286eccdc329212f2f75 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 6 Mar 2019 16:22:52 -0800 Subject: [PATCH 135/245] Fix an exception with user cache generation in "bin/conduit call --as " Summary: Ref T13249. Using "--as" to call some Conduit methods as a user can currently fatal when trying to access settings/preferences. Allow inline regeneration of user caches. Test Plan: Called `project.edit` to add a member. Before: constructing a policy field tried to access the user's preferences and failed. After: Smooth sailing. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13249 Differential Revision: https://secure.phabricator.com/D20255 --- .../management/PhabricatorConduitCallManagementWorkflow.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/applications/conduit/management/PhabricatorConduitCallManagementWorkflow.php b/src/applications/conduit/management/PhabricatorConduitCallManagementWorkflow.php index f9ba48b372..dc241a04b4 100644 --- a/src/applications/conduit/management/PhabricatorConduitCallManagementWorkflow.php +++ b/src/applications/conduit/management/PhabricatorConduitCallManagementWorkflow.php @@ -58,6 +58,10 @@ final class PhabricatorConduitCallManagementWorkflow 'No such user "%s" exists.', $as)); } + + // Allow inline generation of user caches for the user we're acting + // as, since some calls may read user preferences. + $actor->setAllowInlineCacheGeneration(true); } else { $actor = $viewer; } From bacf1f44e00f33f5e076363b36c7fd86f544540a Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 7 Mar 2019 04:48:41 -0800 Subject: [PATCH 136/245] Modularize HeraldRule transactions Summary: Ref T13249. See PHI1115. I initially wanted to make `bin/policy unlock --owner H123` work to transfer ownership of a Herald rule, although I'm no longer really sure this makes much sense. In any case, this makes things a little better and more modern. I removed the storage table for rule comments. Adding comments to Herald rules doesn't work and probably doesn't make much sense. Test Plan: Created and edited Herald rules, grepped for all the transaction type constants. Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13249 Differential Revision: https://secure.phabricator.com/D20258 --- .../20190307.herald.01.comments.sql | 1 + src/__phutil_library_map__.php | 12 +- .../controller/HeraldDisableController.php | 2 +- .../controller/HeraldRuleController.php | 14 +- .../herald/editor/HeraldRuleEditor.php | 77 ----------- .../herald/storage/HeraldRuleTransaction.php | 121 +----------------- .../storage/HeraldRuleTransactionComment.php | 10 -- .../xaction/HeraldRuleDisableTransaction.php | 32 +++++ .../xaction/HeraldRuleEditTransaction.php | 56 ++++++++ .../xaction/HeraldRuleNameTransaction.php | 48 +++++++ .../xaction/HeraldRuleTransactionType.php | 4 + 11 files changed, 166 insertions(+), 211 deletions(-) create mode 100644 resources/sql/autopatches/20190307.herald.01.comments.sql delete mode 100644 src/applications/herald/storage/HeraldRuleTransactionComment.php create mode 100644 src/applications/herald/xaction/HeraldRuleDisableTransaction.php create mode 100644 src/applications/herald/xaction/HeraldRuleEditTransaction.php create mode 100644 src/applications/herald/xaction/HeraldRuleNameTransaction.php create mode 100644 src/applications/herald/xaction/HeraldRuleTransactionType.php diff --git a/resources/sql/autopatches/20190307.herald.01.comments.sql b/resources/sql/autopatches/20190307.herald.01.comments.sql new file mode 100644 index 0000000000..ff9cb9af88 --- /dev/null +++ b/resources/sql/autopatches/20190307.herald.01.comments.sql @@ -0,0 +1 @@ +DROP TABLE {$NAMESPACE}_herald.herald_ruletransaction_comment; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 48cf3f76fe..6d0b93b1be 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1535,10 +1535,13 @@ phutil_register_library_map(array( 'HeraldRuleAdapterField' => 'applications/herald/field/rule/HeraldRuleAdapterField.php', 'HeraldRuleController' => 'applications/herald/controller/HeraldRuleController.php', 'HeraldRuleDatasource' => 'applications/herald/typeahead/HeraldRuleDatasource.php', + 'HeraldRuleDisableTransaction' => 'applications/herald/xaction/HeraldRuleDisableTransaction.php', + 'HeraldRuleEditTransaction' => 'applications/herald/xaction/HeraldRuleEditTransaction.php', 'HeraldRuleEditor' => 'applications/herald/editor/HeraldRuleEditor.php', 'HeraldRuleField' => 'applications/herald/field/rule/HeraldRuleField.php', 'HeraldRuleFieldGroup' => 'applications/herald/field/rule/HeraldRuleFieldGroup.php', 'HeraldRuleListController' => 'applications/herald/controller/HeraldRuleListController.php', + 'HeraldRuleNameTransaction' => 'applications/herald/xaction/HeraldRuleNameTransaction.php', 'HeraldRulePHIDType' => 'applications/herald/phid/HeraldRulePHIDType.php', 'HeraldRuleQuery' => 'applications/herald/query/HeraldRuleQuery.php', 'HeraldRuleReplyHandler' => 'applications/herald/mail/HeraldRuleReplyHandler.php', @@ -1546,7 +1549,7 @@ phutil_register_library_map(array( 'HeraldRuleSerializer' => 'applications/herald/editor/HeraldRuleSerializer.php', 'HeraldRuleTestCase' => 'applications/herald/storage/__tests__/HeraldRuleTestCase.php', 'HeraldRuleTransaction' => 'applications/herald/storage/HeraldRuleTransaction.php', - 'HeraldRuleTransactionComment' => 'applications/herald/storage/HeraldRuleTransactionComment.php', + 'HeraldRuleTransactionType' => 'applications/herald/xaction/HeraldRuleTransactionType.php', 'HeraldRuleTranscript' => 'applications/herald/storage/transcript/HeraldRuleTranscript.php', 'HeraldRuleTypeConfig' => 'applications/herald/config/HeraldRuleTypeConfig.php', 'HeraldRuleTypeDatasource' => 'applications/herald/typeahead/HeraldRuleTypeDatasource.php', @@ -7188,18 +7191,21 @@ phutil_register_library_map(array( 'HeraldRuleAdapterField' => 'HeraldRuleField', 'HeraldRuleController' => 'HeraldController', 'HeraldRuleDatasource' => 'PhabricatorTypeaheadDatasource', + 'HeraldRuleDisableTransaction' => 'HeraldRuleTransactionType', + 'HeraldRuleEditTransaction' => 'HeraldRuleTransactionType', 'HeraldRuleEditor' => 'PhabricatorApplicationTransactionEditor', 'HeraldRuleField' => 'HeraldField', 'HeraldRuleFieldGroup' => 'HeraldFieldGroup', 'HeraldRuleListController' => 'HeraldController', + 'HeraldRuleNameTransaction' => 'HeraldRuleTransactionType', 'HeraldRulePHIDType' => 'PhabricatorPHIDType', 'HeraldRuleQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'HeraldRuleReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'HeraldRuleSearchEngine' => 'PhabricatorApplicationSearchEngine', 'HeraldRuleSerializer' => 'Phobject', 'HeraldRuleTestCase' => 'PhabricatorTestCase', - 'HeraldRuleTransaction' => 'PhabricatorApplicationTransaction', - 'HeraldRuleTransactionComment' => 'PhabricatorApplicationTransactionComment', + 'HeraldRuleTransaction' => 'PhabricatorModularTransaction', + 'HeraldRuleTransactionType' => 'PhabricatorModularTransactionType', 'HeraldRuleTranscript' => 'Phobject', 'HeraldRuleTypeConfig' => 'Phobject', 'HeraldRuleTypeDatasource' => 'PhabricatorTypeaheadDatasource', diff --git a/src/applications/herald/controller/HeraldDisableController.php b/src/applications/herald/controller/HeraldDisableController.php index def87049f7..765237930c 100644 --- a/src/applications/herald/controller/HeraldDisableController.php +++ b/src/applications/herald/controller/HeraldDisableController.php @@ -31,7 +31,7 @@ final class HeraldDisableController extends HeraldController { if ($request->isFormPost()) { $xaction = id(new HeraldRuleTransaction()) - ->setTransactionType(HeraldRuleTransaction::TYPE_DISABLE) + ->setTransactionType(HeraldRuleDisableTransaction::TRANSACTIONTYPE) ->setNewValue($is_disable); id(new HeraldRuleEditor()) diff --git a/src/applications/herald/controller/HeraldRuleController.php b/src/applications/herald/controller/HeraldRuleController.php index d400f8ae90..d05ed2d525 100644 --- a/src/applications/herald/controller/HeraldRuleController.php +++ b/src/applications/herald/controller/HeraldRuleController.php @@ -359,11 +359,21 @@ final class HeraldRuleController extends HeraldController { $repetition_policy); $xactions = array(); + + // Until this moves to EditEngine, manually add a "CREATE" transaction + // if we're creating a new rule. This improves rendering of the initial + // group of transactions. + $is_new = (bool)(!$rule->getID()); + if ($is_new) { + $xactions[] = id(new HeraldRuleTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_CREATE); + } + $xactions[] = id(new HeraldRuleTransaction()) - ->setTransactionType(HeraldRuleTransaction::TYPE_EDIT) + ->setTransactionType(HeraldRuleEditTransaction::TRANSACTIONTYPE) ->setNewValue($new_state); $xactions[] = id(new HeraldRuleTransaction()) - ->setTransactionType(HeraldRuleTransaction::TYPE_NAME) + ->setTransactionType(HeraldRuleNameTransaction::TRANSACTIONTYPE) ->setNewValue($new_name); try { diff --git a/src/applications/herald/editor/HeraldRuleEditor.php b/src/applications/herald/editor/HeraldRuleEditor.php index 3ba5c4f8ac..8bc3224c77 100644 --- a/src/applications/herald/editor/HeraldRuleEditor.php +++ b/src/applications/herald/editor/HeraldRuleEditor.php @@ -11,82 +11,6 @@ final class HeraldRuleEditor return pht('Herald Rules'); } - public function getTransactionTypes() { - $types = parent::getTransactionTypes(); - - $types[] = PhabricatorTransactions::TYPE_COMMENT; - $types[] = HeraldRuleTransaction::TYPE_EDIT; - $types[] = HeraldRuleTransaction::TYPE_NAME; - $types[] = HeraldRuleTransaction::TYPE_DISABLE; - - return $types; - } - - protected function getCustomTransactionOldValue( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case HeraldRuleTransaction::TYPE_DISABLE: - return (int)$object->getIsDisabled(); - case HeraldRuleTransaction::TYPE_EDIT: - return id(new HeraldRuleSerializer()) - ->serializeRule($object); - case HeraldRuleTransaction::TYPE_NAME: - return $object->getName(); - } - - } - - protected function getCustomTransactionNewValue( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case HeraldRuleTransaction::TYPE_DISABLE: - return (int)$xaction->getNewValue(); - case HeraldRuleTransaction::TYPE_EDIT: - case HeraldRuleTransaction::TYPE_NAME: - return $xaction->getNewValue(); - } - } - - protected function applyCustomInternalTransaction( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case HeraldRuleTransaction::TYPE_DISABLE: - return $object->setIsDisabled($xaction->getNewValue()); - case HeraldRuleTransaction::TYPE_NAME: - return $object->setName($xaction->getNewValue()); - case HeraldRuleTransaction::TYPE_EDIT: - $new_state = id(new HeraldRuleSerializer()) - ->deserializeRuleComponents($xaction->getNewValue()); - $object->setMustMatchAll((int)$new_state['match_all']); - $object->attachConditions($new_state['conditions']); - $object->attachActions($new_state['actions']); - - $new_repetition = $new_state['repetition_policy']; - $object->setRepetitionPolicyStringConstant($new_repetition); - - return $object; - } - - } - - protected function applyCustomExternalTransaction( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - switch ($xaction->getTransactionType()) { - case HeraldRuleTransaction::TYPE_EDIT: - $object->saveConditions($object->getConditions()); - $object->saveActions($object->getActions()); - break; - } - return; - } - protected function shouldApplyHeraldRules( PhabricatorLiskDAO $object, array $xactions) { @@ -137,7 +61,6 @@ final class HeraldRuleEditor return pht('[Herald]'); } - protected function buildMailBody( PhabricatorLiskDAO $object, array $xactions) { diff --git a/src/applications/herald/storage/HeraldRuleTransaction.php b/src/applications/herald/storage/HeraldRuleTransaction.php index b1bd563749..7fa7667ec7 100644 --- a/src/applications/herald/storage/HeraldRuleTransaction.php +++ b/src/applications/herald/storage/HeraldRuleTransaction.php @@ -1,11 +1,9 @@ getOldValue(); - $new = $this->getNewValue(); - - switch ($this->getTransactionType()) { - case self::TYPE_DISABLE: - if ($new) { - return 'red'; - } else { - return 'green'; - } - } - - return parent::getColor(); - } - - public function getActionName() { - $old = $this->getOldValue(); - $new = $this->getNewValue(); - - switch ($this->getTransactionType()) { - case self::TYPE_DISABLE: - if ($new) { - return pht('Disabled'); - } else { - return pht('Enabled'); - } - case self::TYPE_NAME: - return pht('Renamed'); - } - - return parent::getActionName(); - } - - public function getIcon() { - $old = $this->getOldValue(); - $new = $this->getNewValue(); - - switch ($this->getTransactionType()) { - case self::TYPE_DISABLE: - if ($new) { - return 'fa-ban'; - } else { - return 'fa-check'; - } - } - - return parent::getIcon(); - } - - - public function getTitle() { - $author_phid = $this->getAuthorPHID(); - - $old = $this->getOldValue(); - $new = $this->getNewValue(); - - switch ($this->getTransactionType()) { - case self::TYPE_DISABLE: - if ($new) { - return pht( - '%s disabled this rule.', - $this->renderHandleLink($author_phid)); - } else { - return pht( - '%s enabled this rule.', - $this->renderHandleLink($author_phid)); - } - case self::TYPE_NAME: - if ($old == null) { - return pht( - '%s created this rule.', - $this->renderHandleLink($author_phid)); - } else { - return pht( - '%s renamed this rule from "%s" to "%s".', - $this->renderHandleLink($author_phid), - $old, - $new); - } - case self::TYPE_EDIT: - return pht( - '%s edited this rule.', - $this->renderHandleLink($author_phid)); - } - - return parent::getTitle(); - } - - public function hasChangeDetails() { - switch ($this->getTransactionType()) { - case self::TYPE_EDIT: - return true; - } - return parent::hasChangeDetails(); - } - - public function renderChangeDetails(PhabricatorUser $viewer) { - $json = new PhutilJSON(); - switch ($this->getTransactionType()) { - case self::TYPE_EDIT: - return $this->renderTextCorpusChangeDetails( - $viewer, - $json->encodeFormatted($this->getOldValue()), - $json->encodeFormatted($this->getNewValue())); - } - - return $this->renderTextCorpusChangeDetails( - $viewer, - $this->getOldValue(), - $this->getNewValue()); + public function getBaseTransactionClass() { + return 'HeraldRuleTransactionType'; } } diff --git a/src/applications/herald/storage/HeraldRuleTransactionComment.php b/src/applications/herald/storage/HeraldRuleTransactionComment.php deleted file mode 100644 index 56022ef863..0000000000 --- a/src/applications/herald/storage/HeraldRuleTransactionComment.php +++ /dev/null @@ -1,10 +0,0 @@ -getIsDisabled(); + } + + public function generateNewValue($object, $value) { + return (bool)$value; + } + + public function applyInternalEffects($object, $value) { + $object->setIsDisabled((int)$value); + } + + public function getTitle() { + if ($this->getNewValue()) { + return pht( + '%s disabled this rule.', + $this->renderAuthor()); + } else { + return pht( + '%s enabled this rule.', + $this->renderAuthor()); + } + } + +} diff --git a/src/applications/herald/xaction/HeraldRuleEditTransaction.php b/src/applications/herald/xaction/HeraldRuleEditTransaction.php new file mode 100644 index 0000000000..c4b03983fb --- /dev/null +++ b/src/applications/herald/xaction/HeraldRuleEditTransaction.php @@ -0,0 +1,56 @@ +serializeRule($object); + } + + public function applyInternalEffects($object, $value) { + $new_state = id(new HeraldRuleSerializer()) + ->deserializeRuleComponents($value); + + $object->setMustMatchAll((int)$new_state['match_all']); + $object->attachConditions($new_state['conditions']); + $object->attachActions($new_state['actions']); + + $new_repetition = $new_state['repetition_policy']; + $object->setRepetitionPolicyStringConstant($new_repetition); + } + + public function applyExternalEffects($object, $value) { + $object->saveConditions($object->getConditions()); + $object->saveActions($object->getActions()); + } + + public function getTitle() { + return pht( + '%s edited this rule.', + $this->renderAuthor()); + } + + public function hasChangeDetailView() { + return true; + } + + public function newChangeDetailView() { + $viewer = $this->getViewer(); + + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + $json = new PhutilJSON(); + $old_json = $json->encodeFormatted($old); + $new_json = $json->encodeFormatted($new); + + return id(new PhabricatorApplicationTransactionTextDiffDetailView()) + ->setViewer($viewer) + ->setOldText($old_json) + ->setNewText($new_json); + } + +} diff --git a/src/applications/herald/xaction/HeraldRuleNameTransaction.php b/src/applications/herald/xaction/HeraldRuleNameTransaction.php new file mode 100644 index 0000000000..39ce289d34 --- /dev/null +++ b/src/applications/herald/xaction/HeraldRuleNameTransaction.php @@ -0,0 +1,48 @@ +getName(); + } + + public function applyInternalEffects($object, $value) { + $object->setName($value); + } + + public function getTitle() { + return pht( + '%s renamed this rule from %s to %s.', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + if ($this->isEmptyTextTransaction($object->getName(), $xactions)) { + $errors[] = $this->newRequiredError( + pht('Rules must have a name.')); + } + + $max_length = $object->getColumnMaximumByteLength('name'); + foreach ($xactions as $xaction) { + $new_value = $xaction->getNewValue(); + + $new_length = strlen($new_value); + if ($new_length > $max_length) { + $errors[] = $this->newInvalidError( + pht( + 'Rule names can be no longer than %s characters.', + new PhutilNumber($max_length))); + } + } + + return $errors; + } + +} diff --git a/src/applications/herald/xaction/HeraldRuleTransactionType.php b/src/applications/herald/xaction/HeraldRuleTransactionType.php new file mode 100644 index 0000000000..81c6846b1f --- /dev/null +++ b/src/applications/herald/xaction/HeraldRuleTransactionType.php @@ -0,0 +1,4 @@ + Date: Wed, 6 Mar 2019 20:21:51 -0800 Subject: [PATCH 137/245] Update "bin/policy unlock" to be more surgical, flexible, modular, and modern Summary: See PHI1115. Ref T13249. Currently, you can `bin/policy unlock` objects which have become inaccessible through some sort of policy mistake. This script uses a very blunt mechanism to perform unlocks: just manually calling `setXPolicy()` and then trying to `save()` the object. Improve things a bit: - More surgical: allow selection of which policies you want to adjust with "--view", "--edit", and "--owner" (potentially important for some objects like Herald rules which don't have policies, and "edit-locked" tasks which basically ignore the edit policy). - More flexible: Instead of unlocking into "All Users" (which could be bad for stuff like Passphrase credentials, since you create a short window where anyone can access them), take a username as a parameter and set the policy to "just that user". Normally, you'd run this as `bin/policy unlock --view myself --edit myself` or similar, now. - More modular: We can't do "owner" transactions in a generic way, but lay the groundwork for letting applications support providing an owner reassignment mechanism. - More modern: Use transactions, not raw `set()` + `save()`. This previously had some hard-coded logic around unlocking applications. I've removed it, and the new generic stuff doesn't actually work. It probably should be made to work at some point, but I believe it's exceptionally difficult to lock yourself out of applications, and you can unlock them with `bin/config set phabricator.application-settings ...` anyway so I'm not too worried about this. It's also hard to figure out the PHID of an application and no one has ever asked about this so I'd guess the reasonable use rate of `bin/policy unlock` to unlock applications in the wild may be zero. Test Plan: - Used `bin/policy unlock` to unlock some objects, saw sensible transactions. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13249 Differential Revision: https://secure.phabricator.com/D20256 --- src/__phutil_library_map__.php | 4 + ...bricatorPolicyManagementUnlockWorkflow.php | 175 ++++++++++-------- .../engine/PhabricatorDefaultUnlockEngine.php | 4 + .../system/engine/PhabricatorUnlockEngine.php | 75 ++++++++ 4 files changed, 181 insertions(+), 77 deletions(-) create mode 100644 src/applications/system/engine/PhabricatorDefaultUnlockEngine.php create mode 100644 src/applications/system/engine/PhabricatorUnlockEngine.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 6d0b93b1be..a2b9caf3ab 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2974,6 +2974,7 @@ phutil_register_library_map(array( 'PhabricatorDebugController' => 'applications/system/controller/PhabricatorDebugController.php', 'PhabricatorDefaultRequestExceptionHandler' => 'aphront/handler/PhabricatorDefaultRequestExceptionHandler.php', 'PhabricatorDefaultSyntaxStyle' => 'infrastructure/syntax/PhabricatorDefaultSyntaxStyle.php', + 'PhabricatorDefaultUnlockEngine' => 'applications/system/engine/PhabricatorDefaultUnlockEngine.php', 'PhabricatorDestructibleCodex' => 'applications/system/codex/PhabricatorDestructibleCodex.php', 'PhabricatorDestructibleCodexInterface' => 'applications/system/interface/PhabricatorDestructibleCodexInterface.php', 'PhabricatorDestructibleInterface' => 'applications/system/interface/PhabricatorDestructibleInterface.php', @@ -4701,6 +4702,7 @@ phutil_register_library_map(array( 'PhabricatorUnitTestContentSource' => 'infrastructure/contentsource/PhabricatorUnitTestContentSource.php', 'PhabricatorUnitsTestCase' => 'view/__tests__/PhabricatorUnitsTestCase.php', 'PhabricatorUnknownContentSource' => 'infrastructure/contentsource/PhabricatorUnknownContentSource.php', + 'PhabricatorUnlockEngine' => 'applications/system/engine/PhabricatorUnlockEngine.php', 'PhabricatorUnsubscribedFromObjectEdgeType' => 'applications/transactions/edges/PhabricatorUnsubscribedFromObjectEdgeType.php', 'PhabricatorUser' => 'applications/people/storage/PhabricatorUser.php', 'PhabricatorUserApproveTransaction' => 'applications/people/xaction/PhabricatorUserApproveTransaction.php', @@ -8871,6 +8873,7 @@ phutil_register_library_map(array( 'PhabricatorDebugController' => 'PhabricatorController', 'PhabricatorDefaultRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler', 'PhabricatorDefaultSyntaxStyle' => 'PhabricatorSyntaxStyle', + 'PhabricatorDefaultUnlockEngine' => 'PhabricatorUnlockEngine', 'PhabricatorDestructibleCodex' => 'Phobject', 'PhabricatorDestructionEngine' => 'Phobject', 'PhabricatorDestructionEngineExtension' => 'Phobject', @@ -10881,6 +10884,7 @@ phutil_register_library_map(array( 'PhabricatorUnitTestContentSource' => 'PhabricatorContentSource', 'PhabricatorUnitsTestCase' => 'PhabricatorTestCase', 'PhabricatorUnknownContentSource' => 'PhabricatorContentSource', + 'PhabricatorUnlockEngine' => 'Phobject', 'PhabricatorUnsubscribedFromObjectEdgeType' => 'PhabricatorEdgeType', 'PhabricatorUser' => array( 'PhabricatorUserDAO', diff --git a/src/applications/policy/management/PhabricatorPolicyManagementUnlockWorkflow.php b/src/applications/policy/management/PhabricatorPolicyManagementUnlockWorkflow.php index 33f7e209c2..64a32b7186 100644 --- a/src/applications/policy/management/PhabricatorPolicyManagementUnlockWorkflow.php +++ b/src/applications/policy/management/PhabricatorPolicyManagementUnlockWorkflow.php @@ -8,40 +8,72 @@ final class PhabricatorPolicyManagementUnlockWorkflow ->setName('unlock') ->setSynopsis( pht( - 'Unlock an object by setting its policies to allow anyone to view '. - 'and edit it.')) - ->setExamples('**unlock** D123') + 'Unlock an object which has policies that prevent it from being '. + 'viewed or edited.')) + ->setExamples('**unlock** --view __user__ __object__') ->setArguments( array( array( - 'name' => 'objects', - 'wildcard' => true, + 'name' => 'view', + 'param' => 'username', + 'help' => pht( + 'Change the view policy of an object so that the specified '. + 'user may view it.'), + ), + array( + 'name' => 'edit', + 'param' => 'username', + 'help' => pht( + 'Change the edit policy of an object so that the specified '. + 'user may edit it.'), + ), + array( + 'name' => 'owner', + 'param' => 'username', + 'help' => pht( + 'Change the owner of an object to the specified user.'), + ), + array( + 'name' => 'objects', + 'wildcard' => true, ), )); } public function execute(PhutilArgumentParser $args) { - $console = PhutilConsole::getConsole(); $viewer = $this->getViewer(); - $obj_names = $args->getArg('objects'); - if (!$obj_names) { + $object_names = $args->getArg('objects'); + if (!$object_names) { throw new PhutilArgumentUsageException( pht('Specify the name of an object to unlock.')); - } else if (count($obj_names) > 1) { + } else if (count($object_names) > 1) { throw new PhutilArgumentUsageException( pht('Specify the name of exactly one object to unlock.')); } + $object_name = head($object_names); + $object = id(new PhabricatorObjectQuery()) ->setViewer($viewer) - ->withNames($obj_names) + ->withNames(array($object_name)) ->executeOne(); - if (!$object) { - $name = head($obj_names); throw new PhutilArgumentUsageException( - pht("No such object '%s'!", $name)); + pht( + 'Unable to find any object with the specified name ("%s").', + $object_name)); + } + + $view_user = $this->loadUser($args->getArg('view')); + $edit_user = $this->loadUser($args->getArg('edit')); + $owner_user = $this->loadUser($args->getArg('owner')); + + if (!$view_user && !$edit_user && !$owner_user) { + throw new PhutilArgumentUsageException( + pht( + 'Choose which capabilities to unlock with "--view", "--edit", '. + 'or "--owner".')); } $handle = id(new PhabricatorHandleQuery()) @@ -49,84 +81,73 @@ final class PhabricatorPolicyManagementUnlockWorkflow ->withPHIDs(array($object->getPHID())) ->executeOne(); - if ($object instanceof PhabricatorApplication) { - $application = $object; + echo tsprintf( + "** %s ** %s\n", + pht('UNLOCKING'), + pht('Unlocking: %s', $handle->getFullName())); - $console->writeOut( - "%s\n", - pht('Unlocking Application: %s', $handle->getFullName())); + $engine = PhabricatorUnlockEngine::newUnlockEngineForObject($object); - // For applications, we can't unlock them in a normal way and don't want - // to unlock every capability, just view and edit. - $capabilities = array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - ); + $xactions = array(); + if ($view_user) { + $xactions[] = $engine->newUnlockViewTransactions($object, $view_user); + } + if ($edit_user) { + $xactions[] = $engine->newUnlockEditTransactions($object, $edit_user); + } + if ($owner_user) { + $xactions[] = $engine->newUnlockOwnerTransactions($object, $owner_user); + } + $xactions = array_mergev($xactions); - $key = 'phabricator.application-settings'; - $config_entry = PhabricatorConfigEntry::loadConfigEntry($key); - $value = $config_entry->getValue(); + $policy_application = new PhabricatorPolicyApplication(); + $content_source = $this->newContentSource(); - foreach ($capabilities as $capability) { - if ($application->isCapabilityEditable($capability)) { - unset($value[$application->getPHID()]['policy'][$capability]); - } - } + $editor = $object->getApplicationTransactionEditor() + ->setActor($viewer) + ->setActingAsPHID($policy_application->getPHID()) + ->setContinueOnMissingFields(true) + ->setContinueOnNoEffect(true) + ->setContentSource($content_source); - $config_entry->setValue($value); - $config_entry->save(); + $editor->applyTransactions($object, $xactions); - $console->writeOut("%s\n", pht('Saved application.')); + echo tsprintf( + "** %s ** %s\n", + pht('UNLOCKED'), + pht('Modified object policies.')); - return 0; + $uri = $handle->getURI(); + if (strlen($uri)) { + echo tsprintf( + "\n **%s**: __%s__\n\n", + pht('Object URI'), + PhabricatorEnv::getURI($uri)); } - $console->writeOut("%s\n", pht('Unlocking: %s', $handle->getFullName())); + return 0; + } - $updated = false; - foreach ($object->getCapabilities() as $capability) { - switch ($capability) { - case PhabricatorPolicyCapability::CAN_VIEW: - try { - $object->setViewPolicy(PhabricatorPolicies::POLICY_USER); - $console->writeOut("%s\n", pht('Unlocked view policy.')); - $updated = true; - } catch (Exception $ex) { - $console->writeOut("%s\n", pht('View policy is not mutable.')); - } - break; - case PhabricatorPolicyCapability::CAN_EDIT: - try { - $object->setEditPolicy(PhabricatorPolicies::POLICY_USER); - $console->writeOut("%s\n", pht('Unlocked edit policy.')); - $updated = true; - } catch (Exception $ex) { - $console->writeOut("%s\n", pht('Edit policy is not mutable.')); - } - break; - case PhabricatorPolicyCapability::CAN_JOIN: - try { - $object->setJoinPolicy(PhabricatorPolicies::POLICY_USER); - $console->writeOut("%s\n", pht('Unlocked join policy.')); - $updated = true; - } catch (Exception $ex) { - $console->writeOut("%s\n", pht('Join policy is not mutable.')); - } - break; - } + private function loadUser($username) { + $viewer = $this->getViewer(); + + if ($username === null) { + return null; } - if ($updated) { - $object->save(); - $console->writeOut("%s\n", pht('Saved object.')); - } else { - $console->writeOut( - "%s\n", + $user = id(new PhabricatorPeopleQuery()) + ->setViewer($viewer) + ->withUsernames(array($username)) + ->executeOne(); + + if (!$user) { + throw new PhutilArgumentUsageException( pht( - 'Object has no mutable policies. Try unlocking parent/container '. - 'object instead. For example, to gain access to a commit, unlock '. - 'the repository it belongs to.')); + 'No user with username "%s" exists.', + $username)); } + + return $user; } } diff --git a/src/applications/system/engine/PhabricatorDefaultUnlockEngine.php b/src/applications/system/engine/PhabricatorDefaultUnlockEngine.php new file mode 100644 index 0000000000..624191ad21 --- /dev/null +++ b/src/applications/system/engine/PhabricatorDefaultUnlockEngine.php @@ -0,0 +1,4 @@ +canApplyTransactionType($object, $type_view)) { + throw new Exception( + pht( + 'Object view policy can not be unlocked because this object '. + 'does not have a mutable view policy.')); + } + + return array( + $this->newTransaction($object) + ->setTransactionType($type_view) + ->setNewValue($user->getPHID()), + ); + } + + public function newUnlockEditTransactions($object, $user) { + $type_edit = PhabricatorTransactions::TYPE_EDIT_POLICY; + + if (!$this->canApplyTransactionType($object, $type_edit)) { + throw new Exception( + pht( + 'Object edit policy can not be unlocked because this object '. + 'does not have a mutable edit policy.')); + } + + return array( + $this->newTransaction($object) + ->setTransactionType($type_edit) + ->setNewValue($user->getPHID()), + ); + } + + public function newUnlockOwnerTransactions($object, $user) { + throw new Exception( + pht( + 'Object owner can not be unlocked: the unlocking engine ("%s") for '. + 'this object does not implement an owner unlocking mechanism.', + get_class($this))); + } + + final protected function canApplyTransactionType($object, $type) { + $xaction_types = $object->getApplicationTransactionEditor() + ->getTransactionTypesForObject($object); + + $xaction_types = array_fuse($xaction_types); + + return isset($xaction_types[$type]); + } + + final protected function newTransaction($object) { + return $object->getApplicationTransactionTemplate(); + } + + +} From 77221bee72cbe6fb8488d030a121b954c7ef1e77 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 6 Mar 2019 20:48:27 -0800 Subject: [PATCH 138/245] Allow objects to specify custom policy unlocking behavior, and tasks to have owners unlocked Summary: Depends on D20256. Ref T13249. See PHI1115. This primarily makes `bin/policy unlock --owner epriestley T123` work. This is important for "Edit Locked" tasks, since changing the edit policy doesn't really do anything. Test Plan: Hard-locked a task as "alice", reassigned it to myself with `bin/policy unlock --owner epriestley`. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13249 Differential Revision: https://secure.phabricator.com/D20257 --- src/__phutil_library_map__.php | 4 ++++ .../engine/ManiphestTaskUnlockEngine.php | 14 ++++++++++++++ .../maniphest/storage/ManiphestTask.php | 11 ++++++++++- .../system/engine/PhabricatorUnlockEngine.php | 8 +++++++- .../PhabricatorUnlockableInterface.php | 18 ++++++++++++++++++ 5 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 src/applications/maniphest/engine/ManiphestTaskUnlockEngine.php create mode 100644 src/applications/system/interface/PhabricatorUnlockableInterface.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index a2b9caf3ab..def6e0de7d 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1784,6 +1784,7 @@ phutil_register_library_map(array( 'ManiphestTaskTitleTransaction' => 'applications/maniphest/xaction/ManiphestTaskTitleTransaction.php', 'ManiphestTaskTransactionType' => 'applications/maniphest/xaction/ManiphestTaskTransactionType.php', 'ManiphestTaskUnblockTransaction' => 'applications/maniphest/xaction/ManiphestTaskUnblockTransaction.php', + 'ManiphestTaskUnlockEngine' => 'applications/maniphest/engine/ManiphestTaskUnlockEngine.php', 'ManiphestTransaction' => 'applications/maniphest/storage/ManiphestTransaction.php', 'ManiphestTransactionComment' => 'applications/maniphest/storage/ManiphestTransactionComment.php', 'ManiphestTransactionEditor' => 'applications/maniphest/editor/ManiphestTransactionEditor.php', @@ -4703,6 +4704,7 @@ phutil_register_library_map(array( 'PhabricatorUnitsTestCase' => 'view/__tests__/PhabricatorUnitsTestCase.php', 'PhabricatorUnknownContentSource' => 'infrastructure/contentsource/PhabricatorUnknownContentSource.php', 'PhabricatorUnlockEngine' => 'applications/system/engine/PhabricatorUnlockEngine.php', + 'PhabricatorUnlockableInterface' => 'applications/system/interface/PhabricatorUnlockableInterface.php', 'PhabricatorUnsubscribedFromObjectEdgeType' => 'applications/transactions/edges/PhabricatorUnsubscribedFromObjectEdgeType.php', 'PhabricatorUser' => 'applications/people/storage/PhabricatorUser.php', 'PhabricatorUserApproveTransaction' => 'applications/people/xaction/PhabricatorUserApproveTransaction.php', @@ -7419,6 +7421,7 @@ phutil_register_library_map(array( 'PhabricatorEditEngineLockableInterface', 'PhabricatorEditEngineMFAInterface', 'PhabricatorPolicyCodexInterface', + 'PhabricatorUnlockableInterface', ), 'ManiphestTaskAssignHeraldAction' => 'HeraldAction', 'ManiphestTaskAssignOtherHeraldAction' => 'ManiphestTaskAssignHeraldAction', @@ -7496,6 +7499,7 @@ phutil_register_library_map(array( 'ManiphestTaskTitleTransaction' => 'ManiphestTaskTransactionType', 'ManiphestTaskTransactionType' => 'PhabricatorModularTransactionType', 'ManiphestTaskUnblockTransaction' => 'ManiphestTaskTransactionType', + 'ManiphestTaskUnlockEngine' => 'PhabricatorUnlockEngine', 'ManiphestTransaction' => 'PhabricatorModularTransaction', 'ManiphestTransactionComment' => 'PhabricatorApplicationTransactionComment', 'ManiphestTransactionEditor' => 'PhabricatorApplicationTransactionEditor', diff --git a/src/applications/maniphest/engine/ManiphestTaskUnlockEngine.php b/src/applications/maniphest/engine/ManiphestTaskUnlockEngine.php new file mode 100644 index 0000000000..b223724a77 --- /dev/null +++ b/src/applications/maniphest/engine/ManiphestTaskUnlockEngine.php @@ -0,0 +1,14 @@ +newTransaction($object) + ->setTransactionType(ManiphestTaskOwnerTransaction::TRANSACTIONTYPE) + ->setNewValue($user->getPHID()), + ); + } + +} diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php index 0193830b39..400bace650 100644 --- a/src/applications/maniphest/storage/ManiphestTask.php +++ b/src/applications/maniphest/storage/ManiphestTask.php @@ -21,7 +21,8 @@ final class ManiphestTask extends ManiphestDAO PhabricatorEditEngineSubtypeInterface, PhabricatorEditEngineLockableInterface, PhabricatorEditEngineMFAInterface, - PhabricatorPolicyCodexInterface { + PhabricatorPolicyCodexInterface, + PhabricatorUnlockableInterface { const MARKUP_FIELD_DESCRIPTION = 'markup:desc'; @@ -649,4 +650,12 @@ final class ManiphestTask extends ManiphestDAO return new ManiphestTaskPolicyCodex(); } + +/* -( PhabricatorUnlockableInterface )------------------------------------- */ + + + public function newUnlockEngine() { + return new ManiphestTaskUnlockEngine(); + } + } diff --git a/src/applications/system/engine/PhabricatorUnlockEngine.php b/src/applications/system/engine/PhabricatorUnlockEngine.php index a6cd57e30b..8afcc2873e 100644 --- a/src/applications/system/engine/PhabricatorUnlockEngine.php +++ b/src/applications/system/engine/PhabricatorUnlockEngine.php @@ -13,7 +13,13 @@ abstract class PhabricatorUnlockEngine 'PhabricatorApplicationTransactionInterface')); } - return new PhabricatorDefaultUnlockEngine(); + if ($object instanceof PhabricatorUnlockableInterface) { + $engine = $object->newUnlockEngine(); + } else { + $engine = new PhabricatorDefaultUnlockEngine(); + } + + return $engine; } public function newUnlockViewTransactions($object, $user) { diff --git a/src/applications/system/interface/PhabricatorUnlockableInterface.php b/src/applications/system/interface/PhabricatorUnlockableInterface.php new file mode 100644 index 0000000000..1a95215e8c --- /dev/null +++ b/src/applications/system/interface/PhabricatorUnlockableInterface.php @@ -0,0 +1,18 @@ +>>UnlockEngine(); + } + +*/ From 950a7bbb19e3405698f2e92abf6601e1e7649f8a Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 7 Mar 2019 06:10:11 -0800 Subject: [PATCH 139/245] On Harbormaster build plans, show which Herald rules trigger builds Summary: Ref T13258. Provide an easy way to find rules which trigger a particular build plan from the build plan page. The implementation here ends up a little messy: we can't just search for `actionType = 'build' AND targetPHID = ''` since the field is a blob of JSON. Instead, make rules indexable and write a "build plan is affected by rule actions" edge when indexing rules, then search on that edge. For now, only "Run Build Plan: ..." rules actually write this edge, since I think (?) that it doesn't really have meaningful values for other edge types today. Maybe "Call Webhooks", and you could get a link from a hook to rules that trigger it? Reasonable to do in the future. Things end up a little bit rough overall, but I think this panel is pretty useful to add to the Build Plan page. This index needs to be rebuilt with `bin/search index --type HeraldRule`. I'll call this out in the changelog but I'm not planning to explicitly migrate or add an activity, since this is only really important for larger installs and they probably (?) read the changelog. As rules are edited over time, this will converge to the right behavior even if you don't rebuild the index. Test Plan: {F6260095} Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13258 Differential Revision: https://secure.phabricator.com/D20259 --- src/__phutil_library_map__.php | 7 ++ .../HarbormasterPlanViewController.php | 38 ++++++++ .../HarbormasterRunBuildPlansHeraldAction.php | 5 + .../herald/action/HeraldAction.php | 4 + .../HeraldRuleActionAffectsObjectEdgeType.php | 8 ++ .../herald/editor/HeraldRuleEditor.php | 4 + .../HeraldRuleIndexEngineExtension.php | 92 +++++++++++++++++++ .../herald/query/HeraldRuleQuery.php | 28 ++++++ .../herald/query/HeraldRuleSearchEngine.php | 52 +++-------- .../herald/storage/HeraldRule.php | 1 + .../herald/view/HeraldRuleListView.php | 65 +++++++++++++ 11 files changed, 264 insertions(+), 40 deletions(-) create mode 100644 src/applications/herald/edge/HeraldRuleActionAffectsObjectEdgeType.php create mode 100644 src/applications/herald/engineextension/HeraldRuleIndexEngineExtension.php create mode 100644 src/applications/herald/view/HeraldRuleListView.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index def6e0de7d..1a5ab46743 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1531,6 +1531,7 @@ phutil_register_library_map(array( 'HeraldRemarkupFieldValue' => 'applications/herald/value/HeraldRemarkupFieldValue.php', 'HeraldRemarkupRule' => 'applications/herald/remarkup/HeraldRemarkupRule.php', 'HeraldRule' => 'applications/herald/storage/HeraldRule.php', + 'HeraldRuleActionAffectsObjectEdgeType' => 'applications/herald/edge/HeraldRuleActionAffectsObjectEdgeType.php', 'HeraldRuleAdapter' => 'applications/herald/adapter/HeraldRuleAdapter.php', 'HeraldRuleAdapterField' => 'applications/herald/field/rule/HeraldRuleAdapterField.php', 'HeraldRuleController' => 'applications/herald/controller/HeraldRuleController.php', @@ -1540,7 +1541,9 @@ phutil_register_library_map(array( 'HeraldRuleEditor' => 'applications/herald/editor/HeraldRuleEditor.php', 'HeraldRuleField' => 'applications/herald/field/rule/HeraldRuleField.php', 'HeraldRuleFieldGroup' => 'applications/herald/field/rule/HeraldRuleFieldGroup.php', + 'HeraldRuleIndexEngineExtension' => 'applications/herald/engineextension/HeraldRuleIndexEngineExtension.php', 'HeraldRuleListController' => 'applications/herald/controller/HeraldRuleListController.php', + 'HeraldRuleListView' => 'applications/herald/view/HeraldRuleListView.php', 'HeraldRuleNameTransaction' => 'applications/herald/xaction/HeraldRuleNameTransaction.php', 'HeraldRulePHIDType' => 'applications/herald/phid/HeraldRulePHIDType.php', 'HeraldRuleQuery' => 'applications/herald/query/HeraldRuleQuery.php', @@ -7189,8 +7192,10 @@ phutil_register_library_map(array( 'PhabricatorFlaggableInterface', 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', + 'PhabricatorIndexableInterface', 'PhabricatorSubscribableInterface', ), + 'HeraldRuleActionAffectsObjectEdgeType' => 'PhabricatorEdgeType', 'HeraldRuleAdapter' => 'HeraldAdapter', 'HeraldRuleAdapterField' => 'HeraldRuleField', 'HeraldRuleController' => 'HeraldController', @@ -7200,7 +7205,9 @@ phutil_register_library_map(array( 'HeraldRuleEditor' => 'PhabricatorApplicationTransactionEditor', 'HeraldRuleField' => 'HeraldField', 'HeraldRuleFieldGroup' => 'HeraldFieldGroup', + 'HeraldRuleIndexEngineExtension' => 'PhabricatorIndexEngineExtension', 'HeraldRuleListController' => 'HeraldController', + 'HeraldRuleListView' => 'AphrontView', 'HeraldRuleNameTransaction' => 'HeraldRuleTransactionType', 'HeraldRulePHIDType' => 'PhabricatorPHIDType', 'HeraldRuleQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', diff --git a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php index 141f321caa..731c4fa784 100644 --- a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php @@ -62,6 +62,7 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController { $builds_view = $this->newBuildsView($plan); $options_view = $this->newOptionsView($plan); + $rules_view = $this->newRulesView($plan); $timeline = $this->buildTransactionTimeline( $plan, @@ -76,6 +77,7 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController { $error, $step_list, $options_view, + $rules_view, $builds_view, $timeline, )); @@ -486,6 +488,42 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController { ->appendChild($list); } + private function newRulesView(HarbormasterBuildPlan $plan) { + $viewer = $this->getViewer(); + + $rules = id(new HeraldRuleQuery()) + ->setViewer($viewer) + ->withDisabled(false) + ->withAffectedObjectPHIDs(array($plan->getPHID())) + ->needValidateAuthors(true) + ->execute(); + + $list = id(new HeraldRuleListView()) + ->setViewer($viewer) + ->setRules($rules) + ->newObjectList(); + + $list->setNoDataString(pht('No active Herald rules trigger this build.')); + + $more_href = new PhutilURI( + '/herald/', + array('affectedPHID' => $plan->getPHID())); + + $more_link = id(new PHUIButtonView()) + ->setTag('a') + ->setIcon('fa-list-ul') + ->setText(pht('View All Rules')) + ->setHref($more_href); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Run By Herald Rules')) + ->addActionLink($more_link); + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->appendChild($list); + } private function newOptionsView(HarbormasterBuildPlan $plan) { $viewer = $this->getViewer(); diff --git a/src/applications/harbormaster/herald/HarbormasterRunBuildPlansHeraldAction.php b/src/applications/harbormaster/herald/HarbormasterRunBuildPlansHeraldAction.php index 8c718e5f5d..9fc053e8ae 100644 --- a/src/applications/harbormaster/herald/HarbormasterRunBuildPlansHeraldAction.php +++ b/src/applications/harbormaster/herald/HarbormasterRunBuildPlansHeraldAction.php @@ -91,4 +91,9 @@ final class HarbormasterRunBuildPlansHeraldAction 'Run build plans: %s.', $this->renderHandleList($value)); } + + public function getPHIDsAffectedByAction(HeraldActionRecord $record) { + return $record->getTarget(); + } + } diff --git a/src/applications/herald/action/HeraldAction.php b/src/applications/herald/action/HeraldAction.php index 04884a94d4..a9740d1736 100644 --- a/src/applications/herald/action/HeraldAction.php +++ b/src/applications/herald/action/HeraldAction.php @@ -401,4 +401,8 @@ abstract class HeraldAction extends Phobject { return null; } + public function getPHIDsAffectedByAction(HeraldActionRecord $record) { + return array(); + } + } diff --git a/src/applications/herald/edge/HeraldRuleActionAffectsObjectEdgeType.php b/src/applications/herald/edge/HeraldRuleActionAffectsObjectEdgeType.php new file mode 100644 index 0000000000..35a30773ac --- /dev/null +++ b/src/applications/herald/edge/HeraldRuleActionAffectsObjectEdgeType.php @@ -0,0 +1,8 @@ +getPHID(), + $edge_type); + $old_edges = array_fuse($old_edges); + + $new_edges = $this->getPHIDsAffectedByActions($object); + $new_edges = array_fuse($new_edges); + + $add_edges = array_diff_key($new_edges, $old_edges); + $rem_edges = array_diff_key($old_edges, $new_edges); + + if (!$add_edges && !$rem_edges) { + return; + } + + $editor = new PhabricatorEdgeEditor(); + + foreach ($add_edges as $phid) { + $editor->addEdge($object->getPHID(), $edge_type, $phid); + } + + foreach ($rem_edges as $phid) { + $editor->removeEdge($object->getPHID(), $edge_type, $phid); + } + + $editor->save(); + } + + public function getIndexVersion($object) { + $phids = $this->getPHIDsAffectedByActions($object); + sort($phids); + $phids = implode(':', $phids); + return PhabricatorHash::digestForIndex($phids); + } + + private function getPHIDsAffectedByActions(HeraldRule $rule) { + $viewer = $this->getViewer(); + + $rule = id(new HeraldRuleQuery()) + ->setViewer($viewer) + ->withIDs(array($rule->getID())) + ->needConditionsAndActions(true) + ->executeOne(); + if (!$rule) { + return array(); + } + + $phids = array(); + + $actions = HeraldAction::getAllActions(); + foreach ($rule->getActions() as $action_record) { + $action = idx($actions, $action_record->getAction()); + + if (!$action) { + continue; + } + + foreach ($action->getPHIDsAffectedByAction($action_record) as $phid) { + $phids[] = $phid; + } + } + + $phids = array_fuse($phids); + return array_keys($phids); + } + +} diff --git a/src/applications/herald/query/HeraldRuleQuery.php b/src/applications/herald/query/HeraldRuleQuery.php index e6dba43c7a..e346a998d4 100644 --- a/src/applications/herald/query/HeraldRuleQuery.php +++ b/src/applications/herald/query/HeraldRuleQuery.php @@ -11,6 +11,7 @@ final class HeraldRuleQuery extends PhabricatorCursorPagedPolicyAwareQuery { private $active; private $datasourceQuery; private $triggerObjectPHIDs; + private $affectedObjectPHIDs; private $needConditionsAndActions; private $needAppliedToPHIDs; @@ -61,6 +62,11 @@ final class HeraldRuleQuery extends PhabricatorCursorPagedPolicyAwareQuery { return $this; } + public function withAffectedObjectPHIDs(array $phids) { + $this->affectedObjectPHIDs = $phids; + return $this; + } + public function needConditionsAndActions($need) { $this->needConditionsAndActions = $need; return $this; @@ -261,9 +267,31 @@ final class HeraldRuleQuery extends PhabricatorCursorPagedPolicyAwareQuery { $this->triggerObjectPHIDs); } + if ($this->affectedObjectPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'edge_affects.dst IN (%Ls)', + $this->affectedObjectPHIDs); + } + return $where; } + protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { + $joins = parent::buildJoinClauseParts($conn); + + if ($this->affectedObjectPHIDs !== null) { + $joins[] = qsprintf( + $conn, + 'JOIN %T edge_affects ON rule.phid = edge_affects.src + AND edge_affects.type = %d', + PhabricatorEdgeConfig::TABLE_NAME_EDGE, + HeraldRuleActionAffectsObjectEdgeType::EDGECONST); + } + + return $joins; + } + private function validateRuleAuthors(array $rules) { // "Global" and "Object" rules always have valid authors. foreach ($rules as $key => $rule) { diff --git a/src/applications/herald/query/HeraldRuleSearchEngine.php b/src/applications/herald/query/HeraldRuleSearchEngine.php index 47a6832731..95e3079717 100644 --- a/src/applications/herald/query/HeraldRuleSearchEngine.php +++ b/src/applications/herald/query/HeraldRuleSearchEngine.php @@ -55,6 +55,10 @@ final class HeraldRuleSearchEngine extends PhabricatorApplicationSearchEngine { pht('(Show All)'), pht('Show Only Disabled Rules'), pht('Show Only Enabled Rules')), + id(new PhabricatorPHIDsSearchField()) + ->setLabel(pht('Affected Objects')) + ->setKey('affectedPHIDs') + ->setAliases(array('affectedPHID')), ); } @@ -81,6 +85,10 @@ final class HeraldRuleSearchEngine extends PhabricatorApplicationSearchEngine { $query->withActive($map['active']); } + if ($map['affectedPHIDs']) { + $query->withAffectedObjectPHIDs($map['affectedPHIDs']); + } + return $query; } @@ -127,54 +135,18 @@ final class HeraldRuleSearchEngine extends PhabricatorApplicationSearchEngine { PhabricatorSavedQuery $query, array $handles) { assert_instances_of($rules, 'HeraldRule'); - $viewer = $this->requireViewer(); - $handles = $viewer->loadHandles(mpull($rules, 'getAuthorPHID')); - $content_type_map = HeraldAdapter::getEnabledAdapterMap($viewer); - - $list = id(new PHUIObjectItemListView()) - ->setUser($viewer); - foreach ($rules as $rule) { - $monogram = $rule->getMonogram(); - - $item = id(new PHUIObjectItemView()) - ->setObjectName($monogram) - ->setHeader($rule->getName()) - ->setHref("/{$monogram}"); - - if ($rule->isPersonalRule()) { - $item->addIcon('fa-user', pht('Personal Rule')); - $item->addByline( - pht( - 'Authored by %s', - $handles[$rule->getAuthorPHID()]->renderLink())); - } else if ($rule->isObjectRule()) { - $item->addIcon('fa-briefcase', pht('Object Rule')); - } else { - $item->addIcon('fa-globe', pht('Global Rule')); - } - - if ($rule->getIsDisabled()) { - $item->setDisabled(true); - $item->addIcon('fa-lock grey', pht('Disabled')); - } else if (!$rule->hasValidAuthor()) { - $item->setDisabled(true); - $item->addIcon('fa-user grey', pht('Author Not Active')); - } - - $content_type_name = idx($content_type_map, $rule->getContentType()); - $item->addAttribute(pht('Affects: %s', $content_type_name)); - - $list->addItem($item); - } + $list = id(new HeraldRuleListView()) + ->setViewer($viewer) + ->setRules($rules) + ->newObjectList(); $result = new PhabricatorApplicationSearchResultView(); $result->setObjectList($list); $result->setNoDataString(pht('No rules found.')); return $result; - } protected function getNewUserBody() { diff --git a/src/applications/herald/storage/HeraldRule.php b/src/applications/herald/storage/HeraldRule.php index 1b005898cb..a9c131e717 100644 --- a/src/applications/herald/storage/HeraldRule.php +++ b/src/applications/herald/storage/HeraldRule.php @@ -6,6 +6,7 @@ final class HeraldRule extends HeraldDAO PhabricatorFlaggableInterface, PhabricatorPolicyInterface, PhabricatorDestructibleInterface, + PhabricatorIndexableInterface, PhabricatorSubscribableInterface { const TABLE_RULE_APPLIED = 'herald_ruleapplied'; diff --git a/src/applications/herald/view/HeraldRuleListView.php b/src/applications/herald/view/HeraldRuleListView.php new file mode 100644 index 0000000000..150499ce87 --- /dev/null +++ b/src/applications/herald/view/HeraldRuleListView.php @@ -0,0 +1,65 @@ +rules = $rules; + return $this; + } + + public function render() { + return $this->newObjectList(); + } + + public function newObjectList() { + $viewer = $this->getViewer(); + $rules = $this->rules; + + $handles = $viewer->loadHandles(mpull($rules, 'getAuthorPHID')); + + $content_type_map = HeraldAdapter::getEnabledAdapterMap($viewer); + + $list = id(new PHUIObjectItemListView()) + ->setViewer($viewer); + foreach ($rules as $rule) { + $monogram = $rule->getMonogram(); + + $item = id(new PHUIObjectItemView()) + ->setObjectName($monogram) + ->setHeader($rule->getName()) + ->setHref($rule->getURI()); + + if ($rule->isPersonalRule()) { + $item->addIcon('fa-user', pht('Personal Rule')); + $item->addByline( + pht( + 'Authored by %s', + $handles[$rule->getAuthorPHID()]->renderLink())); + } else if ($rule->isObjectRule()) { + $item->addIcon('fa-briefcase', pht('Object Rule')); + } else { + $item->addIcon('fa-globe', pht('Global Rule')); + } + + if ($rule->getIsDisabled()) { + $item->setDisabled(true); + $item->addIcon('fa-lock grey', pht('Disabled')); + } else if (!$rule->hasValidAuthor()) { + $item->setDisabled(true); + $item->addIcon('fa-user grey', pht('Author Not Active')); + } + + $content_type_name = idx($content_type_map, $rule->getContentType()); + $item->addAttribute(pht('Affects: %s', $content_type_name)); + + $list->addItem($item); + } + + return $list; + } + +} From 9913754a2aa25f46f5f85278153a65cc8a2da342 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 7 Mar 2019 08:27:36 -0800 Subject: [PATCH 140/245] Improve utilization of "AuthTemporaryToken" table keys in LFS authentication queries Summary: See PHI1123. The key on this table is `` but we currently query for only ``. This can't use the key. Constrain the query to the resource we expect (the repository) so it can use the key. Test Plan: Pushed files using LFS. See PHI1123 for more, likely. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20261 --- .../controller/DiffusionServeController.php | 32 ++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/src/applications/diffusion/controller/DiffusionServeController.php b/src/applications/diffusion/controller/DiffusionServeController.php index cb4ad0ba95..aea901f100 100644 --- a/src/applications/diffusion/controller/DiffusionServeController.php +++ b/src/applications/diffusion/controller/DiffusionServeController.php @@ -192,7 +192,10 @@ final class DiffusionServeController extends DiffusionController { // Try Git LFS auth first since we can usually reject it without doing // any queries, since the username won't match the one we expect or the // request won't be LFS. - $viewer = $this->authenticateGitLFSUser($username, $password); + $viewer = $this->authenticateGitLFSUser( + $username, + $password, + $identifier); // If that failed, try normal auth. Note that we can use normal auth on // LFS requests, so this isn't strictly an alternative to LFS auth. @@ -655,7 +658,8 @@ final class DiffusionServeController extends DiffusionController { private function authenticateGitLFSUser( $username, - PhutilOpaqueEnvelope $password) { + PhutilOpaqueEnvelope $password, + $identifier) { // Never accept these credentials for requests which aren't LFS requests. if (!$this->getIsGitLFSRequest()) { @@ -668,11 +672,31 @@ final class DiffusionServeController extends DiffusionController { return null; } + // See PHI1123. We need to be able to constrain the token query with + // "withTokenResources(...)" to take advantage of the key on the table. + // In this case, the repository PHID is the "resource" we're after. + + // In normal workflows, we figure out the viewer first, then use the + // viewer to load the repository, but that won't work here. Load the + // repository as the omnipotent viewer, then use the repository PHID to + // look for a token. + + $omnipotent_viewer = PhabricatorUser::getOmnipotentUser(); + + $repository = id(new PhabricatorRepositoryQuery()) + ->setViewer($omnipotent_viewer) + ->withIdentifiers(array($identifier)) + ->executeOne(); + if (!$repository) { + return null; + } + $lfs_pass = $password->openEnvelope(); $lfs_hash = PhabricatorHash::weakDigest($lfs_pass); $token = id(new PhabricatorAuthTemporaryTokenQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->setViewer($omnipotent_viewer) + ->withTokenResources(array($repository->getPHID())) ->withTokenTypes(array(DiffusionGitLFSTemporaryTokenType::TOKENTYPE)) ->withTokenCodes(array($lfs_hash)) ->withExpired(false) @@ -682,7 +706,7 @@ final class DiffusionServeController extends DiffusionController { } $user = id(new PhabricatorPeopleQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->setViewer($omnipotent_viewer) ->withPHIDs(array($token->getUserPHID())) ->executeOne(); From 1d4f6bd44458205a4475399b72461ab1f838886c Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 7 Mar 2019 06:30:02 -0800 Subject: [PATCH 141/245] Index "Call Webhook" in Herald, and show calling rules on the Webhook page Summary: Depends on D20259. Now that we can index Herald rules to affected objects, show callers on the "Webhooks" UI. A few other rule types could get indexes too ("Sign Legalpad Documents", "Add Reviewers", "Add Subscribers"), but I think they're less likely to be useful since those triggers are usually more obvious (the transaction timeline makes it clearer what happened/why). We could revisit this in the future now that it's a possibility. Test Plan: {F6260106} Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Differential Revision: https://secure.phabricator.com/D20260 --- .../HarbormasterPlanViewController.php | 1 + .../herald/action/HeraldCallWebhookAction.php | 4 ++ .../HeraldWebhookViewController.php | 41 +++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php index 731c4fa784..a9af90f2a5 100644 --- a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php @@ -496,6 +496,7 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController { ->withDisabled(false) ->withAffectedObjectPHIDs(array($plan->getPHID())) ->needValidateAuthors(true) + ->setLimit(10) ->execute(); $list = id(new HeraldRuleListView()) diff --git a/src/applications/herald/action/HeraldCallWebhookAction.php b/src/applications/herald/action/HeraldCallWebhookAction.php index 953958e5c6..186a7a741f 100644 --- a/src/applications/herald/action/HeraldCallWebhookAction.php +++ b/src/applications/herald/action/HeraldCallWebhookAction.php @@ -63,4 +63,8 @@ final class HeraldCallWebhookAction extends HeraldAction { return new HeraldWebhookDatasource(); } + public function getPHIDsAffectedByAction(HeraldActionRecord $record) { + return $record->getTarget(); + } + } diff --git a/src/applications/herald/controller/HeraldWebhookViewController.php b/src/applications/herald/controller/HeraldWebhookViewController.php index d8e5eb3c54..5f6be9816c 100644 --- a/src/applications/herald/controller/HeraldWebhookViewController.php +++ b/src/applications/herald/controller/HeraldWebhookViewController.php @@ -73,12 +73,15 @@ final class HeraldWebhookViewController ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setTable($requests_table); + $rules_view = $this->newRulesView($hook); + $hook_view = id(new PHUITwoColumnView()) ->setHeader($header) ->setMainColumn( array( $warnings, $properties_view, + $rules_view, $requests_view, $timeline, )) @@ -194,4 +197,42 @@ final class HeraldWebhookViewController ->appendChild($properties); } + private function newRulesView(HeraldWebhook $hook) { + $viewer = $this->getViewer(); + + $rules = id(new HeraldRuleQuery()) + ->setViewer($viewer) + ->withDisabled(false) + ->withAffectedObjectPHIDs(array($hook->getPHID())) + ->needValidateAuthors(true) + ->setLimit(10) + ->execute(); + + $list = id(new HeraldRuleListView()) + ->setViewer($viewer) + ->setRules($rules) + ->newObjectList(); + + $list->setNoDataString(pht('No active Herald rules call this webhook.')); + + $more_href = new PhutilURI( + '/herald/', + array('affectedPHID' => $hook->getPHID())); + + $more_link = id(new PHUIButtonView()) + ->setTag('a') + ->setIcon('fa-list-ul') + ->setText(pht('View All Rules')) + ->setHref($more_href); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Called By Herald Rules')) + ->addActionLink($more_link); + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->appendChild($list); + } + } From c1bff3b8013e1d2d1293a8a843e15845edfd7194 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 6 Mar 2019 09:32:31 -0800 Subject: [PATCH 142/245] Add an "Restartable: If Failed" behavior to Harbormaster build plans Summary: Ref T13249. Ref T13258. In some cases, builds are not idempotent and should not be restarted casually. If the scary part is at the very end (deploy / provision / whatever), it could be okay to restart them if they previously failed. Also, make the "reasons why you can't restart" and "explanations of why you can't restart" logic a little more cohesive. Test Plan: - Tried to restart builds in various states (failed/not failed, restartable always/if failed/never, already restarted), got appropriate errors or restarts. - (I'm not sure the "Autoplan" error is normally reachable, since you can't edit autoplans to configure things to let you try to restart them.) Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13258, T13249 Differential Revision: https://secure.phabricator.com/D20252 --- src/__phutil_library_map__.php | 2 + .../constants/HarbormasterBuildStatus.php | 4 ++ .../HarbormasterBuildActionController.php | 18 ++---- .../HarbormasterRestartException.php | 33 +++++++++++ .../plan/HarbormasterBuildPlanBehavior.php | 7 +++ .../storage/build/HarbormasterBuild.php | 56 +++++++++++++++++-- .../configuration/HarbormasterBuildPlan.php | 10 ---- 7 files changed, 104 insertions(+), 26 deletions(-) create mode 100644 src/applications/harbormaster/exception/HarbormasterRestartException.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 1a5ab46743..651fad9d12 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1443,6 +1443,7 @@ phutil_register_library_map(array( 'HarbormasterQueryBuildsConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterQueryBuildsConduitAPIMethod.php', 'HarbormasterQueryBuildsSearchEngineAttachment' => 'applications/harbormaster/engineextension/HarbormasterQueryBuildsSearchEngineAttachment.php', 'HarbormasterRemarkupRule' => 'applications/harbormaster/remarkup/HarbormasterRemarkupRule.php', + 'HarbormasterRestartException' => 'applications/harbormaster/exception/HarbormasterRestartException.php', 'HarbormasterRunBuildPlansHeraldAction' => 'applications/harbormaster/herald/HarbormasterRunBuildPlansHeraldAction.php', 'HarbormasterSchemaSpec' => 'applications/harbormaster/storage/HarbormasterSchemaSpec.php', 'HarbormasterScratchTable' => 'applications/harbormaster/storage/HarbormasterScratchTable.php', @@ -7093,6 +7094,7 @@ phutil_register_library_map(array( 'HarbormasterQueryBuildsConduitAPIMethod' => 'HarbormasterConduitAPIMethod', 'HarbormasterQueryBuildsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', 'HarbormasterRemarkupRule' => 'PhabricatorObjectRemarkupRule', + 'HarbormasterRestartException' => 'Exception', 'HarbormasterRunBuildPlansHeraldAction' => 'HeraldAction', 'HarbormasterSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'HarbormasterScratchTable' => 'HarbormasterDAO', diff --git a/src/applications/harbormaster/constants/HarbormasterBuildStatus.php b/src/applications/harbormaster/constants/HarbormasterBuildStatus.php index 7437e48f6c..3ceb8e0686 100644 --- a/src/applications/harbormaster/constants/HarbormasterBuildStatus.php +++ b/src/applications/harbormaster/constants/HarbormasterBuildStatus.php @@ -52,6 +52,10 @@ final class HarbormasterBuildStatus extends Phobject { return ($this->key === self::STATUS_PASSED); } + public function isFailed() { + return ($this->key === self::STATUS_FAILED); + } + /** * Get a human readable name for a build status constant. diff --git a/src/applications/harbormaster/controller/HarbormasterBuildActionController.php b/src/applications/harbormaster/controller/HarbormasterBuildActionController.php index b56c7de7f7..6a4a2b1fee 100644 --- a/src/applications/harbormaster/controller/HarbormasterBuildActionController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildActionController.php @@ -64,19 +64,13 @@ final class HarbormasterBuildActionController 'restart. Side effects of the build will occur again. Really '. 'restart build?'); $submit = pht('Restart Build'); - } else if (!$build->getBuildPlan()->canRestartBuildPlan()) { - $title = pht('Not Restartable'); - $body = pht( - 'The build plan for this build is not restartable, so you '. - 'can not restart the build.'); } else { - $title = pht('Unable to Restart Build'); - if ($build->isRestarting()) { - $body = pht( - 'This build is already restarting. You can not reissue a '. - 'restart command to a restarting build.'); - } else { - $body = pht('You can not restart this build.'); + try { + $build->assertCanRestartBuild(); + throw new Exception(pht('Expected to be unable to restart build.')); + } catch (HarbormasterRestartException $ex) { + $title = $ex->getTitle(); + $body = $ex->getBody(); } } break; diff --git a/src/applications/harbormaster/exception/HarbormasterRestartException.php b/src/applications/harbormaster/exception/HarbormasterRestartException.php new file mode 100644 index 0000000000..bd0b86184a --- /dev/null +++ b/src/applications/harbormaster/exception/HarbormasterRestartException.php @@ -0,0 +1,33 @@ +setTitle($title); + $this->appendParagraph($body); + + parent::__construct($title); + } + + public function setTitle($title) { + $this->title = $title; + return $this; + } + + public function getTitle() { + return $this->title; + } + + public function appendParagraph($description) { + $this->body[] = $description; + return $this; + } + + public function getBody() { + return $this->body; + } + +} diff --git a/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php b/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php index 3c63b3e040..d8e857e711 100644 --- a/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php +++ b/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php @@ -15,6 +15,7 @@ final class HarbormasterBuildPlanBehavior const BEHAVIOR_RESTARTABLE = 'restartable'; const RESTARTABLE_ALWAYS = 'always'; + const RESTARTABLE_IF_FAILED = 'failed'; const RESTARTABLE_NEVER = 'never'; const BEHAVIOR_DRAFTS = 'hold-drafts'; @@ -251,6 +252,12 @@ final class HarbormasterBuildPlanBehavior ->setIsDefault(true) ->setDescription( pht('The build may be restarted.')), + id(new HarbormasterBuildPlanBehaviorOption()) + ->setKey(self::RESTARTABLE_IF_FAILED) + ->setIcon('fa-times-circle-o yellow') + ->setName(pht('If Failed')) + ->setDescription( + pht('The build may be restarted if it has failed.')), id(new HarbormasterBuildPlanBehaviorOption()) ->setKey(self::RESTARTABLE_NEVER) ->setIcon('fa-times red') diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuild.php b/src/applications/harbormaster/storage/build/HarbormasterBuild.php index 063f81ff1e..70c26827ec 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuild.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuild.php @@ -183,6 +183,10 @@ final class HarbormasterBuild extends HarbormasterDAO return $this->getBuildStatusObject()->isPassed(); } + public function isFailed() { + return $this->getBuildStatusObject()->isFailed(); + } + public function getURI() { $id = $this->getID(); return "/harbormaster/build/{$id}/"; @@ -211,16 +215,60 @@ final class HarbormasterBuild extends HarbormasterDAO } public function canRestartBuild() { + try { + $this->assertCanRestartBuild(); + return true; + } catch (HarbormasterRestartException $ex) { + return false; + } + } + + public function assertCanRestartBuild() { if ($this->isAutobuild()) { - return false; + throw new HarbormasterRestartException( + pht('Can Not Restart Autobuild'), + pht( + 'This build can not be restarted because it is an automatic '. + 'build.')); } + $restartable = HarbormasterBuildPlanBehavior::BEHAVIOR_RESTARTABLE; $plan = $this->getBuildPlan(); - if (!$plan->canRestartBuildPlan()) { - return false; + + $option = HarbormasterBuildPlanBehavior::getBehavior($restartable) + ->getPlanOption($plan); + $option_key = $option->getKey(); + + $never_restartable = HarbormasterBuildPlanBehavior::RESTARTABLE_NEVER; + $is_never = ($option_key === $never_restartable); + if ($is_never) { + throw new HarbormasterRestartException( + pht('Build Plan Prevents Restart'), + pht( + 'This build can not be restarted because the build plan is '. + 'configured to prevent the build from restarting.')); } - return !$this->isRestarting(); + $failed_restartable = HarbormasterBuildPlanBehavior::RESTARTABLE_IF_FAILED; + $is_failed = ($option_key === $failed_restartable); + if ($is_failed) { + if (!$this->isFailed()) { + throw new HarbormasterRestartException( + pht('Only Restartable if Failed'), + pht( + 'This build can not be restarted because the build plan is '. + 'configured to prevent the build from restarting unless it '. + 'has failed, and it has not failed.')); + } + } + + if ($this->isRestarting()) { + throw new HarbormasterRestartException( + pht('Already Restarting'), + pht( + 'This build is already restarting. You can not reissue a restart '. + 'command to a restarting build.')); + } } public function canPauseBuild() { diff --git a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php index efe62a6f84..798201f490 100644 --- a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php +++ b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php @@ -175,16 +175,6 @@ final class HarbormasterBuildPlan extends HarbormasterDAO $capability); } - public function canRestartBuildPlan() { - $restartable = HarbormasterBuildPlanBehavior::BEHAVIOR_RESTARTABLE; - $is_restartable = HarbormasterBuildPlanBehavior::RESTARTABLE_ALWAYS; - - $option = HarbormasterBuildPlanBehavior::getBehavior($restartable) - ->getPlanOption($this); - - return ($option->getKey() === $is_restartable); - } - /* -( PhabricatorSubscribableInterface )----------------------------------- */ From 2260738de9e07834a89b45b292920250779190a7 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 1 Mar 2019 15:13:25 -0800 Subject: [PATCH 143/245] When dragging nodes between different columns on an ordered board, don't reorder them by making secondary edits Summary: Ref T10334. When a workboard is ordered by priority, dragging from column "A" to a particular place in column "B" currently means "move this task to column B, and adjust its priority so that it naturally sorts into the location under my mouse cursor". Users frequently find this confusing / undesirable. To begin improving this, make "drag from column A to column B" and "drag from somewhere in column A to somewhere else in column A" into different operations. The first operation, a movement between columns, no longer implies an ordering change. The second action still does. So if you actually want to change the priority of a task, you drag it within its current column. If you just want to move it to a different column, you drag it between columns. This creates some possible problems: - Some users may love the current behavior and just not be very vocal about it. I doubt it, but presumably we'll hear from them if we break it. - If you actualy want to move + reorder, it's a bit more cumbersome now. We could possibly add something like "shift + drag" for this if there's feedback. - The new behavior is probably less surprising, but may not be much more obvious. Future changes (for example, in T10335) should help make it more clear. - When you mouse cursor goes over column B, the card dashed-rectangle preview target thing jumps to the correct position in the column -- but that may not be under your mouse cursor. This feels pretty much fine if the whole column fits on screen. It may not be so great if the column does not fit on screen and the dashed-rectangle-thing has vanished. This is just a UI feedback issue and we could refine this later (scroll/highlight the column). Test Plan: - Created several tasks at different priority levels, sorted a board by priority, dragged tasks between columns. Dragging from "A" to "B" no longer causes a priority edit. - Also, dragged within a column. This still performs priority edits. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T10334 Differential Revision: https://secure.phabricator.com/D20242 --- resources/celerity/map.php | 56 +++++++++---------- .../js/application/projects/WorkboardBoard.js | 4 ++ .../application/projects/WorkboardColumn.js | 27 +++++++-- webroot/rsrc/js/core/DraggableList.js | 32 ++++++++++- 4 files changed, 84 insertions(+), 35 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 7f9aaa4995..bc81717e06 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -10,7 +10,7 @@ return array( 'conpherence.pkg.css' => '3c8a0668', 'conpherence.pkg.js' => '020aebcf', 'core.pkg.css' => '34ce1741', - 'core.pkg.js' => '2cda17a4', + 'core.pkg.js' => '9eb1254b', 'differential.pkg.css' => '1755a478', 'differential.pkg.js' => '67e02996', 'diffusion.pkg.css' => '42c75c37', @@ -409,9 +409,9 @@ return array( 'rsrc/js/application/phortune/phortune-credit-card-form.js' => 'd12d214f', 'rsrc/js/application/policy/behavior-policy-control.js' => '0eaa33a9', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '9347f172', - 'rsrc/js/application/projects/WorkboardBoard.js' => '45d0b2b1', + 'rsrc/js/application/projects/WorkboardBoard.js' => '3a8c42a3', 'rsrc/js/application/projects/WorkboardCard.js' => '9a513421', - 'rsrc/js/application/projects/WorkboardColumn.js' => '8573dc1b', + 'rsrc/js/application/projects/WorkboardColumn.js' => 'b451fd4c', 'rsrc/js/application/projects/WorkboardController.js' => '42c7a5a7', 'rsrc/js/application/projects/behavior-project-boards.js' => '05c74d65', 'rsrc/js/application/projects/behavior-project-create.js' => '34c53422', @@ -434,7 +434,7 @@ return array( 'rsrc/js/application/uiexample/notification-example.js' => '29819b75', 'rsrc/js/core/Busy.js' => '5202e831', 'rsrc/js/core/DragAndDropFileUpload.js' => '4370900d', - 'rsrc/js/core/DraggableList.js' => '3c6bd549', + 'rsrc/js/core/DraggableList.js' => 'd594c805', 'rsrc/js/core/Favicon.js' => '7930776a', 'rsrc/js/core/FileUpload.js' => 'ab85e184', 'rsrc/js/core/Hovercard.js' => '074f0783', @@ -727,9 +727,9 @@ return array( 'javelin-view-renderer' => '9aae2b66', 'javelin-view-visitor' => '308f9fe4', 'javelin-websocket' => 'fdc13e4e', - 'javelin-workboard-board' => '45d0b2b1', + 'javelin-workboard-board' => '3a8c42a3', 'javelin-workboard-card' => '9a513421', - 'javelin-workboard-column' => '8573dc1b', + 'javelin-workboard-column' => 'b451fd4c', 'javelin-workboard-controller' => '42c7a5a7', 'javelin-workflow' => '958e9045', 'maniphest-report-css' => '3d53188b', @@ -755,7 +755,7 @@ return array( 'phabricator-diff-changeset-list' => '04023d82', 'phabricator-diff-inline' => 'a4a14a94', 'phabricator-drag-and-drop-file-upload' => '4370900d', - 'phabricator-draggable-list' => '3c6bd549', + 'phabricator-draggable-list' => 'd594c805', 'phabricator-fatal-config-template-css' => '20babf50', 'phabricator-favicon' => '7930776a', 'phabricator-feed-css' => 'd8b6e3f8', @@ -1188,18 +1188,19 @@ return array( 'javelin-install', 'javelin-dom', ), + '3a8c42a3' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-util', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + 'javelin-workboard-column', + ), '3b4899b0' => array( 'javelin-behavior', 'phabricator-prefab', ), - '3c6bd549' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-stratcom', - 'javelin-util', - 'javelin-vector', - 'javelin-magical-init', - ), '3dc5ad43' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1256,15 +1257,6 @@ return array( '43bc9360' => array( 'javelin-install', ), - '45d0b2b1' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-util', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - 'javelin-workboard-column', - ), '46116c01' => array( 'javelin-request', 'javelin-behavior', @@ -1565,10 +1557,6 @@ return array( 'javelin-workflow', 'phabricator-draggable-list', ), - '8573dc1b' => array( - 'javelin-install', - 'javelin-workboard-card', - ), '87428eb2' => array( 'javelin-behavior', 'javelin-diffusion-locate-file-source', @@ -1855,6 +1843,10 @@ return array( 'b347a301' => array( 'javelin-behavior', ), + 'b451fd4c' => array( + 'javelin-install', + 'javelin-workboard-card', + ), 'b517bfa0' => array( 'phui-oi-list-view-css', ), @@ -1994,6 +1986,14 @@ return array( 'd3799cb4' => array( 'javelin-install', ), + 'd594c805' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-stratcom', + 'javelin-util', + 'javelin-vector', + 'javelin-magical-init', + ), 'd8a86cfb' => array( 'javelin-behavior', 'javelin-dom', diff --git a/webroot/rsrc/js/application/projects/WorkboardBoard.js b/webroot/rsrc/js/application/projects/WorkboardBoard.js index cac35c2d9a..422ee70418 100644 --- a/webroot/rsrc/js/application/projects/WorkboardBoard.js +++ b/webroot/rsrc/js/application/projects/WorkboardBoard.js @@ -118,6 +118,10 @@ JX.install('WorkboardBoard', { .setCanDragX(true) .setHasInfiniteHeight(true); + if (this.getOrder() !== 'natural') { + list.setCompareHandler(JX.bind(column, column.compareHandler)); + } + list.listen('didDrop', JX.bind(this, this._onmovecard, list)); lists.push(list); diff --git a/webroot/rsrc/js/application/projects/WorkboardColumn.js b/webroot/rsrc/js/application/projects/WorkboardColumn.js index 9973648593..28bff862da 100644 --- a/webroot/rsrc/js/application/projects/WorkboardColumn.js +++ b/webroot/rsrc/js/application/projects/WorkboardColumn.js @@ -175,6 +175,19 @@ JX.install('WorkboardColumn', { this._dirty = false; }, + compareHandler: function(src_list, src_node, dst_list, dst_node) { + var board = this.getBoard(); + var order = board.getOrder(); + + var src_phid = JX.Stratcom.getData(src_node).objectPHID; + var dst_phid = JX.Stratcom.getData(dst_node).objectPHID; + + var u_vec = this.getBoard().getOrderVector(src_phid, order); + var v_vec = this.getBoard().getOrderVector(dst_phid, order); + + return this._compareVectors(u_vec, v_vec); + }, + _getCardsSortedNaturally: function() { var list = []; @@ -200,15 +213,19 @@ JX.install('WorkboardColumn', { }, _sortCards: function(order, u, v) { - var ud = this.getBoard().getOrderVector(u.getPHID(), order); - var vd = this.getBoard().getOrderVector(v.getPHID(), order); + var u_vec = this.getBoard().getOrderVector(u.getPHID(), order); + var v_vec = this.getBoard().getOrderVector(v.getPHID(), order); - for (var ii = 0; ii < ud.length; ii++) { - if (ud[ii] > vd[ii]) { + return this._compareVectors(u_vec, v_vec); + }, + + _compareVectors: function(u_vec, v_vec) { + for (var ii = 0; ii < u_vec.length; ii++) { + if (u_vec[ii] > v_vec[ii]) { return 1; } - if (ud[ii] < vd[ii]) { + if (u_vec[ii] < v_vec[ii]) { return -1; } } diff --git a/webroot/rsrc/js/core/DraggableList.js b/webroot/rsrc/js/core/DraggableList.js index a545ed7272..a65489be41 100644 --- a/webroot/rsrc/js/core/DraggableList.js +++ b/webroot/rsrc/js/core/DraggableList.js @@ -39,6 +39,7 @@ JX.install('DraggableList', { properties : { findItemsHandler: null, + compareHandler: null, canDragX: false, outerContainer: null, hasInfiniteHeight: false @@ -367,8 +368,29 @@ JX.install('DraggableList', { return this; }, + _getOrderedTarget: function(src_list, src_node) { + var targets = this._getTargets(); + + // NOTE: The targets are ordered from the bottom of the column to the + // top, so we're looking for the first node that we sort below. If we + // don't find one, we'll sort to the head of the column. + + for (var ii = 0; ii < targets.length; ii++) { + var target = targets[ii]; + if (this._compareTargets(src_list, src_node, target.item) > 0) { + return target.item; + } + } + + return null; + }, + + _compareTargets: function(src_list, src_node, dst_node) { + var dst_list = this; + return this.getCompareHandler()(src_list, src_node, dst_list, dst_node); + }, + _getCurrentTarget : function(p) { - var ghost = this.getGhostNode(); var targets = this._getTargets(); var dragging = this._dragging; @@ -461,9 +483,15 @@ JX.install('DraggableList', { // Compute the size and position of the drop target indicator, because we // need to update our static position computations to account for it. + var compare_handler = this.getCompareHandler(); + var cur_target = false; if (target_list) { - cur_target = target_list._getCurrentTarget(p); + if (compare_handler && (target_list !== this)) { + cur_target = target_list._getOrderedTarget(this, this._dragging); + } else { + cur_target = target_list._getCurrentTarget(p); + } } // If we've selected a new target, update the UI to show where we're From be1e3b2cc0a8773531ad8565a2b0590d0ea75cca Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 2 Mar 2019 07:45:51 -0800 Subject: [PATCH 144/245] When a user drags a card over a column, highlight the column border Summary: Ref T10334. Partly, this just improves visual feedback for all drag operations. After D20242, we can have cases where you (for example) drag a low-priority node to a very tall column on a priority-ordered workboard. In this case, the actual dashed-border-drop-target may not be on screen. We might make the column scroll or put some kind of hint in the UI in this case, but an easy starting point is just to make the "yes, you're targeting this column" state a bit more clear. Test Plan: Dragged tasks between columns, saw the border higlight on the target columns. This is very tricky to take a screenshot of. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T10334 Differential Revision: https://secure.phabricator.com/D20245 --- resources/celerity/map.php | 66 +++++++++---------- .../css/phui/workboards/phui-workpanel.css | 8 +++ .../js/application/projects/WorkboardBoard.js | 3 +- .../application/projects/WorkboardColumn.js | 5 ++ webroot/rsrc/js/core/DraggableList.js | 17 ++++- 5 files changed, 63 insertions(+), 36 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index bc81717e06..5118dc741d 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -10,7 +10,7 @@ return array( 'conpherence.pkg.css' => '3c8a0668', 'conpherence.pkg.js' => '020aebcf', 'core.pkg.css' => '34ce1741', - 'core.pkg.js' => '9eb1254b', + 'core.pkg.js' => 'b96c872e', 'differential.pkg.css' => '1755a478', 'differential.pkg.js' => '67e02996', 'diffusion.pkg.css' => '42c75c37', @@ -178,7 +178,7 @@ return array( 'rsrc/css/phui/workboards/phui-workboard-color.css' => 'e86de308', 'rsrc/css/phui/workboards/phui-workboard.css' => '74fc9d98', 'rsrc/css/phui/workboards/phui-workcard.css' => '8c536f90', - 'rsrc/css/phui/workboards/phui-workpanel.css' => 'bd546a49', + 'rsrc/css/phui/workboards/phui-workpanel.css' => '7e12d43c', 'rsrc/css/sprite-login.css' => '18b368a6', 'rsrc/css/sprite-tokens.css' => 'f1896dc5', 'rsrc/css/syntax/syntax-default.css' => '055fc231', @@ -409,9 +409,9 @@ return array( 'rsrc/js/application/phortune/phortune-credit-card-form.js' => 'd12d214f', 'rsrc/js/application/policy/behavior-policy-control.js' => '0eaa33a9', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '9347f172', - 'rsrc/js/application/projects/WorkboardBoard.js' => '3a8c42a3', + 'rsrc/js/application/projects/WorkboardBoard.js' => 'fd96a6e8', 'rsrc/js/application/projects/WorkboardCard.js' => '9a513421', - 'rsrc/js/application/projects/WorkboardColumn.js' => 'b451fd4c', + 'rsrc/js/application/projects/WorkboardColumn.js' => '1f71e559', 'rsrc/js/application/projects/WorkboardController.js' => '42c7a5a7', 'rsrc/js/application/projects/behavior-project-boards.js' => '05c74d65', 'rsrc/js/application/projects/behavior-project-create.js' => '34c53422', @@ -434,7 +434,7 @@ return array( 'rsrc/js/application/uiexample/notification-example.js' => '29819b75', 'rsrc/js/core/Busy.js' => '5202e831', 'rsrc/js/core/DragAndDropFileUpload.js' => '4370900d', - 'rsrc/js/core/DraggableList.js' => 'd594c805', + 'rsrc/js/core/DraggableList.js' => '8437c663', 'rsrc/js/core/Favicon.js' => '7930776a', 'rsrc/js/core/FileUpload.js' => 'ab85e184', 'rsrc/js/core/Hovercard.js' => '074f0783', @@ -727,9 +727,9 @@ return array( 'javelin-view-renderer' => '9aae2b66', 'javelin-view-visitor' => '308f9fe4', 'javelin-websocket' => 'fdc13e4e', - 'javelin-workboard-board' => '3a8c42a3', + 'javelin-workboard-board' => 'fd96a6e8', 'javelin-workboard-card' => '9a513421', - 'javelin-workboard-column' => 'b451fd4c', + 'javelin-workboard-column' => '1f71e559', 'javelin-workboard-controller' => '42c7a5a7', 'javelin-workflow' => '958e9045', 'maniphest-report-css' => '3d53188b', @@ -755,7 +755,7 @@ return array( 'phabricator-diff-changeset-list' => '04023d82', 'phabricator-diff-inline' => 'a4a14a94', 'phabricator-drag-and-drop-file-upload' => '4370900d', - 'phabricator-draggable-list' => 'd594c805', + 'phabricator-draggable-list' => '8437c663', 'phabricator-fatal-config-template-css' => '20babf50', 'phabricator-favicon' => '7930776a', 'phabricator-feed-css' => 'd8b6e3f8', @@ -854,7 +854,7 @@ return array( 'phui-workboard-color-css' => 'e86de308', 'phui-workboard-view-css' => '74fc9d98', 'phui-workcard-view-css' => '8c536f90', - 'phui-workpanel-view-css' => 'bd546a49', + 'phui-workpanel-view-css' => '7e12d43c', 'phuix-action-list-view' => 'c68f183f', 'phuix-action-view' => 'aaa08f3b', 'phuix-autocomplete' => '8f139ef0', @@ -1034,6 +1034,10 @@ return array( 'javelin-behavior', 'javelin-dom', ), + '1f71e559' => array( + 'javelin-install', + 'javelin-workboard-card', + ), '1ff278aa' => array( 'phui-button-css', ), @@ -1188,15 +1192,6 @@ return array( 'javelin-install', 'javelin-dom', ), - '3a8c42a3' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-util', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - 'javelin-workboard-column', - ), '3b4899b0' => array( 'javelin-behavior', 'phabricator-prefab', @@ -1540,6 +1535,9 @@ return array( 'javelin-install', 'javelin-dom', ), + '7e12d43c' => array( + 'phui-workcard-view-css', + ), '80bff3af' => array( 'javelin-install', 'javelin-typeahead-source', @@ -1557,6 +1555,14 @@ return array( 'javelin-workflow', 'phabricator-draggable-list', ), + '8437c663' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-stratcom', + 'javelin-util', + 'javelin-vector', + 'javelin-magical-init', + ), '87428eb2' => array( 'javelin-behavior', 'javelin-diffusion-locate-file-source', @@ -1843,10 +1849,6 @@ return array( 'b347a301' => array( 'javelin-behavior', ), - 'b451fd4c' => array( - 'javelin-install', - 'javelin-workboard-card', - ), 'b517bfa0' => array( 'phui-oi-list-view-css', ), @@ -1885,9 +1887,6 @@ return array( 'javelin-uri', 'phabricator-notification', ), - 'bd546a49' => array( - 'phui-workcard-view-css', - ), 'bdce4d78' => array( 'javelin-install', 'javelin-util', @@ -1986,14 +1985,6 @@ return array( 'd3799cb4' => array( 'javelin-install', ), - 'd594c805' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-stratcom', - 'javelin-util', - 'javelin-vector', - 'javelin-magical-init', - ), 'd8a86cfb' => array( 'javelin-behavior', 'javelin-dom', @@ -2129,6 +2120,15 @@ return array( 'javelin-magical-init', 'javelin-util', ), + 'fd96a6e8' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-util', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + 'javelin-workboard-column', + ), 'fdc13e4e' => array( 'javelin-install', ), diff --git a/webroot/rsrc/css/phui/workboards/phui-workpanel.css b/webroot/rsrc/css/phui/workboards/phui-workpanel.css index 617ff5aa6d..fb7415ff20 100644 --- a/webroot/rsrc/css/phui/workboards/phui-workpanel.css +++ b/webroot/rsrc/css/phui/workboards/phui-workpanel.css @@ -137,3 +137,11 @@ .phui-workpanel-view.project-panel-over-limit .phui-header-shell { border-color: {$red}; } + +.phui-workpanel-view .phui-box-grey { + border: 1px solid transparent; +} + +.phui-workpanel-view.workboard-column-drop-target .phui-box-grey { + border-color: {$lightblueborder}; +} diff --git a/webroot/rsrc/js/application/projects/WorkboardBoard.js b/webroot/rsrc/js/application/projects/WorkboardBoard.js index 422ee70418..b3f8e585d6 100644 --- a/webroot/rsrc/js/application/projects/WorkboardBoard.js +++ b/webroot/rsrc/js/application/projects/WorkboardBoard.js @@ -116,7 +116,8 @@ JX.install('WorkboardBoard', { .setOuterContainer(this.getRoot()) .setFindItemsHandler(JX.bind(column, column.getCardNodes)) .setCanDragX(true) - .setHasInfiniteHeight(true); + .setHasInfiniteHeight(true) + .setIsDropTargetHandler(JX.bind(column, column.setIsDropTarget)); if (this.getOrder() !== 'natural') { list.setCompareHandler(JX.bind(column, column.compareHandler)); diff --git a/webroot/rsrc/js/application/projects/WorkboardColumn.js b/webroot/rsrc/js/application/projects/WorkboardColumn.js index 28bff862da..a94604a470 100644 --- a/webroot/rsrc/js/application/projects/WorkboardColumn.js +++ b/webroot/rsrc/js/application/projects/WorkboardColumn.js @@ -188,6 +188,11 @@ JX.install('WorkboardColumn', { return this._compareVectors(u_vec, v_vec); }, + setIsDropTarget: function(is_target) { + var node = this.getWorkpanelNode(); + JX.DOM.alterClass(node, 'workboard-column-drop-target', is_target); + }, + _getCardsSortedNaturally: function() { var list = []; diff --git a/webroot/rsrc/js/core/DraggableList.js b/webroot/rsrc/js/core/DraggableList.js index a65489be41..598856581f 100644 --- a/webroot/rsrc/js/core/DraggableList.js +++ b/webroot/rsrc/js/core/DraggableList.js @@ -40,6 +40,7 @@ JX.install('DraggableList', { properties : { findItemsHandler: null, compareHandler: null, + isDropTargetHandler: null, canDragX: false, outerContainer: null, hasInfiniteHeight: false @@ -318,7 +319,7 @@ JX.install('DraggableList', { } } - JX.DOM.alterClass(root, 'drag-target-list', is_target); + group[ii]._setIsDropTarget(is_target); } } else { target_list = this; @@ -368,6 +369,18 @@ JX.install('DraggableList', { return this; }, + _setIsDropTarget: function(is_target) { + var root = this.getRootNode(); + JX.DOM.alterClass(root, 'drag-target-list', is_target); + + var handler = this.getIsDropTargetHandler(); + if (handler) { + handler(is_target); + } + + return this; + }, + _getOrderedTarget: function(src_list, src_node) { var targets = this._getTargets(); @@ -633,7 +646,7 @@ JX.install('DraggableList', { var group = this._group; for (var ii = 0; ii < group.length; ii++) { - JX.DOM.alterClass(group[ii].getRootNode(), 'drag-target-list', false); + group[ii]._setIsDropTarget(false); group[ii]._clearTarget(); } From 14a433c77355fab99e0de71653ef3c7824f8795c Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 5 Mar 2019 06:00:12 -0800 Subject: [PATCH 145/245] Add priority group headers to workboard columns (display only) Summary: Ref T10333. When workboards are ordered (for example, by priority), add headers to the various groups. Major goals are: - Allow users to drag-and-drop to set values that no cards currently have: for example, you can change a card priority to "normal" by dragging it under the "normal" header, even if no other cards in the column are currently "Normal". - Make future orderings more useful, particularly "order by assignee". We don't really have room to put the username on every card and it would create a fair amount of clutter, but we can put usernames in these headers and then reference them with just the profile picture. This also allows you to assign to users who are not currently assigned anything in a given column. - Make the drag-and-drop behavior more obvious by showing what it will do more clearly (see T8135). - Make things a little easier to scan in general: because space on cards is limited, some information isn't conveyed very clearly (for example, priority information is currently conveyed //only// through color, which can be hard to pick out visually and is probably not functional for users who need vision accommodations). - Maybe do "swimlanes": this is pretty much a "swimlanes" UI if we add whitespace at the bottom of each group so that the headers line up across all the columns (e.g., "Normal" is at the same y-axis position in every column as you scroll down the page). Not sold on this being useful, but it's just a UI adjustment if we do want to try it. NOTE: This only makes these headers work for display. They aren't yet recognized as targets by the drag list UI, so you can't drag cards into an empty group. I'll tackle that in a followup. Test Plan: {F6257686} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T10333 Differential Revision: https://secure.phabricator.com/D20247 --- resources/celerity/map.php | 84 ++++++++------- .../maniphest/storage/ManiphestTask.php | 1 + .../PhabricatorProjectBoardViewController.php | 40 +++++++ .../css/phui/workboards/phui-workpanel.css | 13 +++ .../js/application/projects/WorkboardBoard.js | 47 ++++++++ .../js/application/projects/WorkboardCard.js | 4 + .../application/projects/WorkboardColumn.js | 101 +++++++++++++----- .../application/projects/WorkboardHeader.js | 38 +++++++ .../projects/WorkboardHeaderTemplate.js | 28 +++++ .../projects/behavior-project-boards.js | 10 ++ 10 files changed, 306 insertions(+), 60 deletions(-) create mode 100644 webroot/rsrc/js/application/projects/WorkboardHeader.js create mode 100644 webroot/rsrc/js/application/projects/WorkboardHeaderTemplate.js diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 5118dc741d..9bfb432f42 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -178,7 +178,7 @@ return array( 'rsrc/css/phui/workboards/phui-workboard-color.css' => 'e86de308', 'rsrc/css/phui/workboards/phui-workboard.css' => '74fc9d98', 'rsrc/css/phui/workboards/phui-workcard.css' => '8c536f90', - 'rsrc/css/phui/workboards/phui-workpanel.css' => '7e12d43c', + 'rsrc/css/phui/workboards/phui-workpanel.css' => 'bc16cf33', 'rsrc/css/sprite-login.css' => '18b368a6', 'rsrc/css/sprite-tokens.css' => 'f1896dc5', 'rsrc/css/syntax/syntax-default.css' => '055fc231', @@ -409,11 +409,13 @@ return array( 'rsrc/js/application/phortune/phortune-credit-card-form.js' => 'd12d214f', 'rsrc/js/application/policy/behavior-policy-control.js' => '0eaa33a9', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '9347f172', - 'rsrc/js/application/projects/WorkboardBoard.js' => 'fd96a6e8', - 'rsrc/js/application/projects/WorkboardCard.js' => '9a513421', - 'rsrc/js/application/projects/WorkboardColumn.js' => '1f71e559', + 'rsrc/js/application/projects/WorkboardBoard.js' => 'e4e2d107', + 'rsrc/js/application/projects/WorkboardCard.js' => 'c23ddfde', + 'rsrc/js/application/projects/WorkboardColumn.js' => 'fd9cb972', 'rsrc/js/application/projects/WorkboardController.js' => '42c7a5a7', - 'rsrc/js/application/projects/behavior-project-boards.js' => '05c74d65', + 'rsrc/js/application/projects/WorkboardHeader.js' => '354c5c0e', + 'rsrc/js/application/projects/WorkboardHeaderTemplate.js' => '9b86cd0d', + 'rsrc/js/application/projects/behavior-project-boards.js' => 'a3f6b67f', 'rsrc/js/application/projects/behavior-project-create.js' => '34c53422', 'rsrc/js/application/projects/behavior-reorder-columns.js' => '8ac32fd9', 'rsrc/js/application/releeph/releeph-preview-branch.js' => '75184d68', @@ -655,7 +657,7 @@ return array( 'javelin-behavior-phuix-example' => 'c2c500a7', 'javelin-behavior-policy-control' => '0eaa33a9', 'javelin-behavior-policy-rule-editor' => '9347f172', - 'javelin-behavior-project-boards' => '05c74d65', + 'javelin-behavior-project-boards' => 'a3f6b67f', 'javelin-behavior-project-create' => '34c53422', 'javelin-behavior-quicksand-blacklist' => '5a6f6a06', 'javelin-behavior-read-only-warning' => 'b9109f8f', @@ -727,10 +729,12 @@ return array( 'javelin-view-renderer' => '9aae2b66', 'javelin-view-visitor' => '308f9fe4', 'javelin-websocket' => 'fdc13e4e', - 'javelin-workboard-board' => 'fd96a6e8', - 'javelin-workboard-card' => '9a513421', - 'javelin-workboard-column' => '1f71e559', + 'javelin-workboard-board' => 'e4e2d107', + 'javelin-workboard-card' => 'c23ddfde', + 'javelin-workboard-column' => 'fd9cb972', 'javelin-workboard-controller' => '42c7a5a7', + 'javelin-workboard-header' => '354c5c0e', + 'javelin-workboard-header-template' => '9b86cd0d', 'javelin-workflow' => '958e9045', 'maniphest-report-css' => '3d53188b', 'maniphest-task-edit-css' => '272daa84', @@ -854,7 +858,7 @@ return array( 'phui-workboard-color-css' => 'e86de308', 'phui-workboard-view-css' => '74fc9d98', 'phui-workcard-view-css' => '8c536f90', - 'phui-workpanel-view-css' => '7e12d43c', + 'phui-workpanel-view-css' => 'bc16cf33', 'phuix-action-list-view' => 'c68f183f', 'phuix-action-view' => 'aaa08f3b', 'phuix-autocomplete' => '8f139ef0', @@ -915,15 +919,6 @@ return array( 'javelin-dom', 'javelin-workflow', ), - '05c74d65' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-util', - 'javelin-vector', - 'javelin-stratcom', - 'javelin-workflow', - 'javelin-workboard-controller', - ), '05d290ef' => array( 'javelin-install', 'javelin-util', @@ -1034,10 +1029,6 @@ return array( 'javelin-behavior', 'javelin-dom', ), - '1f71e559' => array( - 'javelin-install', - 'javelin-workboard-card', - ), '1ff278aa' => array( 'phui-button-css', ), @@ -1172,6 +1163,9 @@ return array( 'javelin-stratcom', 'javelin-workflow', ), + '354c5c0e' => array( + 'javelin-install', + ), '37b8a04a' => array( 'javelin-install', 'javelin-util', @@ -1535,9 +1529,6 @@ return array( 'javelin-install', 'javelin-dom', ), - '7e12d43c' => array( - 'phui-workcard-view-css', - ), '80bff3af' => array( 'javelin-install', 'javelin-typeahead-source', @@ -1701,9 +1692,6 @@ return array( 'javelin-dom', 'javelin-router', ), - '9a513421' => array( - 'javelin-install', - ), '9aae2b66' => array( 'javelin-install', 'javelin-util', @@ -1713,6 +1701,9 @@ return array( 'javelin-dom', 'javelin-stratcom', ), + '9b86cd0d' => array( + 'javelin-install', + ), '9cec214e' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1737,6 +1728,15 @@ return array( 'a241536a' => array( 'javelin-install', ), + 'a3f6b67f' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-util', + 'javelin-vector', + 'javelin-stratcom', + 'javelin-workflow', + 'javelin-workboard-controller', + ), 'a4356cde' => array( 'javelin-install', 'javelin-dom', @@ -1887,6 +1887,9 @@ return array( 'javelin-uri', 'phabricator-notification', ), + 'bc16cf33' => array( + 'phui-workcard-view-css', + ), 'bdce4d78' => array( 'javelin-install', 'javelin-util', @@ -1903,6 +1906,9 @@ return array( 'javelin-stratcom', 'javelin-uri', ), + 'c23ddfde' => array( + 'javelin-install', + ), 'c2c500a7' => array( 'javelin-install', 'javelin-dom', @@ -2019,6 +2025,16 @@ return array( 'javelin-dom', 'javelin-history', ), + 'e4e2d107' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-util', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + 'javelin-workboard-column', + 'javelin-workboard-header-template', + ), 'e562708c' => array( 'javelin-install', ), @@ -2120,14 +2136,10 @@ return array( 'javelin-magical-init', 'javelin-util', ), - 'fd96a6e8' => array( + 'fd9cb972' => array( 'javelin-install', - 'javelin-dom', - 'javelin-util', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - 'javelin-workboard-column', + 'javelin-workboard-card', + 'javelin-workboard-header', ), 'fdc13e4e' => array( 'javelin-install', diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php index 400bace650..88ade9c35a 100644 --- a/src/applications/maniphest/storage/ManiphestTask.php +++ b/src/applications/maniphest/storage/ManiphestTask.php @@ -306,6 +306,7 @@ final class ManiphestTask extends ManiphestDAO return array( 'status' => $this->getStatus(), 'points' => (double)$this->getPoints(), + 'priority' => $this->getPriority(), ); } diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index f2965892d7..857004caf0 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -621,6 +621,45 @@ final class PhabricatorProjectBoardViewController $board->addPanel($panel); } + // It's possible for tasks to have an invalid/unknown priority in the + // database. We still want to generate a header for these tasks so we + // don't break the workboard. + $priorities = + ManiphestTaskPriority::getTaskPriorityMap() + + mpull($all_tasks, null, 'getPriority'); + $priorities = array_keys($priorities); + + $headers = array(); + foreach ($priorities as $priority) { + $header_key = sprintf('priority(%s)', $priority); + + $priority_name = ManiphestTaskPriority::getTaskPriorityName($priority); + $priority_color = ManiphestTaskPriority::getTaskPriorityColor($priority); + $priority_icon = ManiphestTaskPriority::getTaskPriorityIcon($priority); + + $icon_view = id(new PHUIIconView()) + ->setIcon("{$priority_icon} {$priority_color}"); + + $template = phutil_tag( + 'li', + array( + 'class' => 'workboard-group-header', + ), + array( + $icon_view, + $priority_name, + )); + + $headers[] = array( + 'order' => 'priority', + 'key' => $header_key, + 'template' => hsprintf('%s', $template), + 'vector' => array( + (int)-$priority, + ), + ); + } + $behavior_config = array( 'moveURI' => $this->getApplicationURI('move/'.$project->getID().'/'), 'uploadURI' => '/file/dropupload/', @@ -630,6 +669,7 @@ final class PhabricatorProjectBoardViewController 'boardPHID' => $project->getPHID(), 'order' => $this->sortKey, + 'headers' => $headers, 'templateMap' => $templates, 'columnMaps' => $column_maps, 'orderMaps' => mpull($all_tasks, 'getWorkboardOrderVectors'), diff --git a/webroot/rsrc/css/phui/workboards/phui-workpanel.css b/webroot/rsrc/css/phui/workboards/phui-workpanel.css index fb7415ff20..2dac6b2233 100644 --- a/webroot/rsrc/css/phui/workboards/phui-workpanel.css +++ b/webroot/rsrc/css/phui/workboards/phui-workpanel.css @@ -145,3 +145,16 @@ .phui-workpanel-view.workboard-column-drop-target .phui-box-grey { border-color: {$lightblueborder}; } + +.workboard-group-header { + background: rgba({$alphablue}, 0.10); + padding: 4px 8px; + margin: 0 0 8px -8px; + border-bottom: 1px solid {$lightgreyborder}; + font-weight: bold; + color: {$darkgreytext}; +} + +.workboard-group-header .phui-icon-view { + margin-right: 8px; +} diff --git a/webroot/rsrc/js/application/projects/WorkboardBoard.js b/webroot/rsrc/js/application/projects/WorkboardBoard.js index b3f8e585d6..2ea38b07b2 100644 --- a/webroot/rsrc/js/application/projects/WorkboardBoard.js +++ b/webroot/rsrc/js/application/projects/WorkboardBoard.js @@ -7,6 +7,7 @@ * javelin-workflow * phabricator-draggable-list * javelin-workboard-column + * javelin-workboard-header-template * @javelin */ @@ -20,6 +21,7 @@ JX.install('WorkboardBoard', { this._templates = {}; this._orderMaps = {}; this._propertiesMap = {}; + this._headers = {}; this._buildColumns(); }, @@ -36,6 +38,7 @@ JX.install('WorkboardBoard', { _templates: null, _orderMaps: null, _propertiesMap: null, + _headers: null, getRoot: function() { return this._root; @@ -58,6 +61,36 @@ JX.install('WorkboardBoard', { return this; }, + getHeaderTemplate: function(header_key) { + if (!this._headers[header_key]) { + this._headers[header_key] = new JX.WorkboardHeaderTemplate(header_key); + } + + return this._headers[header_key]; + }, + + getHeaderTemplatesForOrder: function(order) { + var templates = []; + + for (var k in this._headers) { + var header = this._headers[k]; + + if (header.getOrder() !== order) { + continue; + } + + templates.push(header); + } + + templates.sort(JX.bind(this, this._sortHeaderTemplates)); + + return templates; + }, + + _sortHeaderTemplates: function(u, v) { + return this.compareVectors(u.getVector(), v.getVector()); + }, + setObjectProperties: function(phid, properties) { this._propertiesMap[phid] = properties; return this; @@ -84,6 +117,20 @@ JX.install('WorkboardBoard', { return this._orderMaps[phid][key]; }, + compareVectors: function(u_vec, v_vec) { + for (var ii = 0; ii < u_vec.length; ii++) { + if (u_vec[ii] > v_vec[ii]) { + return 1; + } + + if (u_vec[ii] < v_vec[ii]) { + return -1; + } + } + + return 0; + }, + start: function() { this._setupDragHandlers(); diff --git a/webroot/rsrc/js/application/projects/WorkboardCard.js b/webroot/rsrc/js/application/projects/WorkboardCard.js index b506e655c1..753eca40f1 100644 --- a/webroot/rsrc/js/application/projects/WorkboardCard.js +++ b/webroot/rsrc/js/application/projects/WorkboardCard.js @@ -40,6 +40,10 @@ JX.install('WorkboardCard', { return this.getProperties().status; }, + getPriority: function(order) { + return this.getProperties().priority; + }, + getNode: function() { if (!this._root) { var phid = this.getPHID(); diff --git a/webroot/rsrc/js/application/projects/WorkboardColumn.js b/webroot/rsrc/js/application/projects/WorkboardColumn.js index a94604a470..fdb165f589 100644 --- a/webroot/rsrc/js/application/projects/WorkboardColumn.js +++ b/webroot/rsrc/js/application/projects/WorkboardColumn.js @@ -2,6 +2,7 @@ * @provides javelin-workboard-column * @requires javelin-install * javelin-workboard-card + * javelin-workboard-header * @javelin */ @@ -21,6 +22,8 @@ JX.install('WorkboardColumn', { 'column-points-content'); this._cards = {}; + this._headers = {}; + this._objects = []; this._naturalOrder = []; }, @@ -29,11 +32,13 @@ JX.install('WorkboardColumn', { _root: null, _board: null, _cards: null, + _headers: null, _naturalOrder: null, _panel: null, _pointsNode: null, _pointsContentNode: null, _dirty: true, + _objects: null, getPHID: function() { return this._phid; @@ -148,24 +153,85 @@ JX.install('WorkboardColumn', { return this._dirty; }, + getHeader: function(key) { + if (!this._headers[key]) { + this._headers[key] = new JX.WorkboardHeader(this, key); + } + return this._headers[key]; + }, + + _getCardHeaderKey: function(card, order) { + switch (order) { + case 'priority': + return 'priority(' + card.getPriority() + ')'; + default: + return null; + } + }, + redraw: function() { var board = this.getBoard(); var order = board.getOrder(); var list; + var has_headers; if (order == 'natural') { list = this._getCardsSortedNaturally(); + has_headers = false; } else { list = this._getCardsSortedByKey(order); + has_headers = true; } - var content = []; - for (var ii = 0; ii < list.length; ii++) { + var ii; + var objects = []; + + var header_keys = []; + var seen_headers = {}; + if (has_headers) { + var header_templates = board.getHeaderTemplatesForOrder(order); + for (var k in header_templates) { + header_keys.push(header_templates[k].getHeaderKey()); + } + header_keys.reverse(); + } + + for (ii = 0; ii < list.length; ii++) { var card = list[ii]; - var node = card.getNode(); - content.push(node); + // If a column has a "High" priority card and a "Low" priority card, + // we need to add the "Normal" header in between them. This allows + // you to change priority to "Normal" even if there are no "Normal" + // cards in a column. + if (has_headers) { + var header_key = this._getCardHeaderKey(card, order); + if (!seen_headers[header_key]) { + while (header_keys.length) { + var next = header_keys.pop(); + + var header = this.getHeader(next); + objects.push(header); + seen_headers[header_key] = true; + + if (next === header_key) { + break; + } + } + } + } + + objects.push(card); + } + + this._objects = objects; + + var content = []; + for (ii = 0; ii < this._objects.length; ii++) { + var object = this._objects[ii]; + + var node = object.getNode(); + content.push(node); } JX.DOM.setContent(this.getRoot(), content); @@ -182,10 +248,10 @@ JX.install('WorkboardColumn', { var src_phid = JX.Stratcom.getData(src_node).objectPHID; var dst_phid = JX.Stratcom.getData(dst_node).objectPHID; - var u_vec = this.getBoard().getOrderVector(src_phid, order); - var v_vec = this.getBoard().getOrderVector(dst_phid, order); + var u_vec = board.getOrderVector(src_phid, order); + var v_vec = board.getOrderVector(dst_phid, order); - return this._compareVectors(u_vec, v_vec); + return board.compareVectors(u_vec, v_vec); }, setIsDropTarget: function(is_target) { @@ -218,24 +284,11 @@ JX.install('WorkboardColumn', { }, _sortCards: function(order, u, v) { - var u_vec = this.getBoard().getOrderVector(u.getPHID(), order); - var v_vec = this.getBoard().getOrderVector(v.getPHID(), order); + var board = this.getBoard(); + var u_vec = board.getOrderVector(u.getPHID(), order); + var v_vec = board.getOrderVector(v.getPHID(), order); - return this._compareVectors(u_vec, v_vec); - }, - - _compareVectors: function(u_vec, v_vec) { - for (var ii = 0; ii < u_vec.length; ii++) { - if (u_vec[ii] > v_vec[ii]) { - return 1; - } - - if (u_vec[ii] < v_vec[ii]) { - return -1; - } - } - - return 0; + return board.compareVectors(u_vec, v_vec); }, _redrawFrame: function() { diff --git a/webroot/rsrc/js/application/projects/WorkboardHeader.js b/webroot/rsrc/js/application/projects/WorkboardHeader.js new file mode 100644 index 0000000000..d6cfd137d0 --- /dev/null +++ b/webroot/rsrc/js/application/projects/WorkboardHeader.js @@ -0,0 +1,38 @@ +/** + * @provides javelin-workboard-header + * @requires javelin-install + * @javelin + */ + +JX.install('WorkboardHeader', { + + construct: function(column, header_key) { + this._column = column; + this._headerKey = header_key; + }, + + members: { + _root: null, + _column: null, + _headerKey: null, + + getColumn: function() { + return this._column; + }, + + getHeaderKey: function() { + return this._headerKey; + }, + + getNode: function() { + if (!this._root) { + var header_key = this.getHeaderKey(); + var board = this.getColumn().getBoard(); + var template = board.getHeaderTemplate(header_key).getTemplate(); + this._root = JX.$H(template).getFragment().firstChild; + } + return this._root; + } + } + +}); diff --git a/webroot/rsrc/js/application/projects/WorkboardHeaderTemplate.js b/webroot/rsrc/js/application/projects/WorkboardHeaderTemplate.js new file mode 100644 index 0000000000..c08652bed0 --- /dev/null +++ b/webroot/rsrc/js/application/projects/WorkboardHeaderTemplate.js @@ -0,0 +1,28 @@ +/** + * @provides javelin-workboard-header-template + * @requires javelin-install + * @javelin + */ + +JX.install('WorkboardHeaderTemplate', { + + construct: function(header_key) { + this._headerKey = header_key; + }, + + properties: { + template: null, + order: null, + vector: null + }, + + members: { + _headerKey: null, + + getHeaderKey: function() { + return this._headerKey; + } + + } + +}); diff --git a/webroot/rsrc/js/application/projects/behavior-project-boards.js b/webroot/rsrc/js/application/projects/behavior-project-boards.js index 83f41787ab..fd2c1a0fe6 100644 --- a/webroot/rsrc/js/application/projects/behavior-project-boards.js +++ b/webroot/rsrc/js/application/projects/behavior-project-boards.js @@ -105,6 +105,16 @@ JX.behavior('project-boards', function(config, statics) { board.setObjectProperties(property_phid, property_maps[property_phid]); } + var headers = config.headers; + for (var jj = 0; jj < headers.length; jj++) { + var header = headers[jj]; + + board.getHeaderTemplate(header.key) + .setOrder(header.order) + .setTemplate(header.template) + .setVector(header.vector); + } + board.start(); }); From 40af472ff59517258005bd2e8c06ec5ddd68d4d7 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 5 Mar 2019 07:38:35 -0800 Subject: [PATCH 146/245] Make drag-and-drop on workboards interact with priority column headers Summary: Ref T10333. Ref T8135. Depends on D20247. Allow users to drag-and-drop cards on a priority-sorted workboard under headers, even if the header has no other cards. As of D20247, headers show up but they aren't really interactive. Now, you can drag cards directly underneath a header (instead of only between other cards). For example, if a column has only one "Wishlist" task, you may drag it under the "High", "Normal", or "Low" priority headers to select a specific priority. (Some of this code still feels a little rough, but I think it will generalize once other types of sorting are available.) Test Plan: Dragged cards within and between priority groups, saw appropriate priority edits applied in every case I could come up with. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T10333, T8135 Differential Revision: https://secure.phabricator.com/D20248 --- resources/celerity/map.php | 80 +++++++++---------- .../maniphest/storage/ManiphestTask.php | 1 + .../PhabricatorProjectBoardViewController.php | 6 +- .../PhabricatorProjectMoveController.php | 77 ++++++++++++++---- .../storage/PhabricatorProjectColumn.php | 3 + .../js/application/projects/WorkboardBoard.js | 43 ++++++++-- .../js/application/projects/WorkboardCard.js | 4 + .../application/projects/WorkboardColumn.js | 61 +++++++++++--- .../application/projects/WorkboardHeader.js | 6 ++ .../projects/WorkboardHeaderTemplate.js | 3 +- .../projects/behavior-project-boards.js | 3 +- 11 files changed, 208 insertions(+), 79 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 9bfb432f42..00a62f41d6 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -409,13 +409,13 @@ return array( 'rsrc/js/application/phortune/phortune-credit-card-form.js' => 'd12d214f', 'rsrc/js/application/policy/behavior-policy-control.js' => '0eaa33a9', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '9347f172', - 'rsrc/js/application/projects/WorkboardBoard.js' => 'e4e2d107', - 'rsrc/js/application/projects/WorkboardCard.js' => 'c23ddfde', - 'rsrc/js/application/projects/WorkboardColumn.js' => 'fd9cb972', + 'rsrc/js/application/projects/WorkboardBoard.js' => 'a4f1e85d', + 'rsrc/js/application/projects/WorkboardCard.js' => '887ef74f', + 'rsrc/js/application/projects/WorkboardColumn.js' => 'ca444dca', 'rsrc/js/application/projects/WorkboardController.js' => '42c7a5a7', - 'rsrc/js/application/projects/WorkboardHeader.js' => '354c5c0e', - 'rsrc/js/application/projects/WorkboardHeaderTemplate.js' => '9b86cd0d', - 'rsrc/js/application/projects/behavior-project-boards.js' => 'a3f6b67f', + 'rsrc/js/application/projects/WorkboardHeader.js' => '6e75daea', + 'rsrc/js/application/projects/WorkboardHeaderTemplate.js' => '2d641f7d', + 'rsrc/js/application/projects/behavior-project-boards.js' => 'e2730b90', 'rsrc/js/application/projects/behavior-project-create.js' => '34c53422', 'rsrc/js/application/projects/behavior-reorder-columns.js' => '8ac32fd9', 'rsrc/js/application/releeph/releeph-preview-branch.js' => '75184d68', @@ -657,7 +657,7 @@ return array( 'javelin-behavior-phuix-example' => 'c2c500a7', 'javelin-behavior-policy-control' => '0eaa33a9', 'javelin-behavior-policy-rule-editor' => '9347f172', - 'javelin-behavior-project-boards' => 'a3f6b67f', + 'javelin-behavior-project-boards' => 'e2730b90', 'javelin-behavior-project-create' => '34c53422', 'javelin-behavior-quicksand-blacklist' => '5a6f6a06', 'javelin-behavior-read-only-warning' => 'b9109f8f', @@ -729,12 +729,12 @@ return array( 'javelin-view-renderer' => '9aae2b66', 'javelin-view-visitor' => '308f9fe4', 'javelin-websocket' => 'fdc13e4e', - 'javelin-workboard-board' => 'e4e2d107', - 'javelin-workboard-card' => 'c23ddfde', - 'javelin-workboard-column' => 'fd9cb972', + 'javelin-workboard-board' => 'a4f1e85d', + 'javelin-workboard-card' => '887ef74f', + 'javelin-workboard-column' => 'ca444dca', 'javelin-workboard-controller' => '42c7a5a7', - 'javelin-workboard-header' => '354c5c0e', - 'javelin-workboard-header-template' => '9b86cd0d', + 'javelin-workboard-header' => '6e75daea', + 'javelin-workboard-header-template' => '2d641f7d', 'javelin-workflow' => '958e9045', 'maniphest-report-css' => '3d53188b', 'maniphest-task-edit-css' => '272daa84', @@ -1125,6 +1125,9 @@ return array( 'javelin-dom', 'phabricator-keyboard-shortcut', ), + '2d641f7d' => array( + 'javelin-install', + ), '2e255291' => array( 'javelin-install', 'javelin-util', @@ -1163,9 +1166,6 @@ return array( 'javelin-stratcom', 'javelin-workflow', ), - '354c5c0e' => array( - 'javelin-install', - ), '37b8a04a' => array( 'javelin-install', 'javelin-util', @@ -1458,6 +1458,9 @@ return array( 'javelin-install', 'javelin-util', ), + '6e75daea' => array( + 'javelin-install', + ), 70245195 => array( 'javelin-behavior', 'javelin-stratcom', @@ -1566,6 +1569,9 @@ return array( 'javelin-install', 'javelin-dom', ), + '887ef74f' => array( + 'javelin-install', + ), '89a1ae3a' => array( 'javelin-dom', 'javelin-util', @@ -1701,9 +1707,6 @@ return array( 'javelin-dom', 'javelin-stratcom', ), - '9b86cd0d' => array( - 'javelin-install', - ), '9cec214e' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1728,15 +1731,6 @@ return array( 'a241536a' => array( 'javelin-install', ), - 'a3f6b67f' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-util', - 'javelin-vector', - 'javelin-stratcom', - 'javelin-workflow', - 'javelin-workboard-controller', - ), 'a4356cde' => array( 'javelin-install', 'javelin-dom', @@ -1762,6 +1756,16 @@ return array( 'javelin-request', 'javelin-util', ), + 'a4f1e85d' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-util', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + 'javelin-workboard-column', + 'javelin-workboard-header-template', + ), 'a5257c4e' => array( 'javelin-install', 'javelin-dom', @@ -1906,9 +1910,6 @@ return array( 'javelin-stratcom', 'javelin-uri', ), - 'c23ddfde' => array( - 'javelin-install', - ), 'c2c500a7' => array( 'javelin-install', 'javelin-dom', @@ -1959,6 +1960,11 @@ return array( 'javelin-util', 'phabricator-keyboard-shortcut-manager', ), + 'ca444dca' => array( + 'javelin-install', + 'javelin-workboard-card', + 'javelin-workboard-header', + ), 'cf32921f' => array( 'javelin-behavior', 'javelin-dom', @@ -2025,15 +2031,14 @@ return array( 'javelin-dom', 'javelin-history', ), - 'e4e2d107' => array( - 'javelin-install', + 'e2730b90' => array( + 'javelin-behavior', 'javelin-dom', 'javelin-util', + 'javelin-vector', 'javelin-stratcom', 'javelin-workflow', - 'phabricator-draggable-list', - 'javelin-workboard-column', - 'javelin-workboard-header-template', + 'javelin-workboard-controller', ), 'e562708c' => array( 'javelin-install', @@ -2136,11 +2141,6 @@ return array( 'javelin-magical-init', 'javelin-util', ), - 'fd9cb972' => array( - 'javelin-install', - 'javelin-workboard-card', - 'javelin-workboard-header', - ), 'fdc13e4e' => array( 'javelin-install', ), diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php index 88ade9c35a..8d45ed45fa 100644 --- a/src/applications/maniphest/storage/ManiphestTask.php +++ b/src/applications/maniphest/storage/ManiphestTask.php @@ -252,6 +252,7 @@ final class ManiphestTask extends ManiphestDAO return array( PhabricatorProjectColumn::ORDER_PRIORITY => array( (int)-$this->getPriority(), + PhabricatorProjectColumn::NODETYPE_CARD, (double)-$this->getSubpriority(), (int)-$this->getID(), ), diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index 857004caf0..32374ad8be 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -651,11 +651,15 @@ final class PhabricatorProjectBoardViewController )); $headers[] = array( - 'order' => 'priority', + 'order' => PhabricatorProjectColumn::ORDER_PRIORITY, 'key' => $header_key, 'template' => hsprintf('%s', $template), 'vector' => array( (int)-$priority, + PhabricatorProjectColumn::NODETYPE_HEADER, + ), + 'editProperties' => array( + PhabricatorProjectColumn::ORDER_PRIORITY => (int)$priority, ), ); } diff --git a/src/applications/project/controller/PhabricatorProjectMoveController.php b/src/applications/project/controller/PhabricatorProjectMoveController.php index 29b70cfafc..09fca4692d 100644 --- a/src/applications/project/controller/PhabricatorProjectMoveController.php +++ b/src/applications/project/controller/PhabricatorProjectMoveController.php @@ -15,6 +15,14 @@ final class PhabricatorProjectMoveController $before_phid = $request->getStr('beforePHID'); $order = $request->getStr('order', PhabricatorProjectColumn::DEFAULT_ORDER); + $edit_header = null; + $raw_header = $request->getStr('header'); + if (strlen($raw_header)) { + $edit_header = phutil_json_decode($raw_header); + } else { + $edit_header = array(); + } + $project = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->requireCapabilities( @@ -87,10 +95,14 @@ final class PhabricatorProjectMoveController )); if ($order == PhabricatorProjectColumn::ORDER_PRIORITY) { + $header_priority = idx( + $edit_header, + PhabricatorProjectColumn::ORDER_PRIORITY); $priority_xactions = $this->getPriorityTransactions( $object, $after_phid, - $before_phid); + $before_phid, + $header_priority); foreach ($priority_xactions as $xaction) { $xactions[] = $xaction; } @@ -110,13 +122,33 @@ final class PhabricatorProjectMoveController private function getPriorityTransactions( ManiphestTask $task, $after_phid, - $before_phid) { + $before_phid, + $header_priority) { + + $xactions = array(); + $must_move = false; + + if ($header_priority !== null) { + if ($task->getPriority() !== $header_priority) { + $task = id(clone $task) + ->setPriority($header_priority); + + $keyword_map = ManiphestTaskPriority::getTaskPriorityKeywordsMap(); + $keyword = head(idx($keyword_map, $header_priority)); + + $xactions[] = id(new ManiphestTransaction()) + ->setTransactionType( + ManiphestTaskPriorityTransaction::TRANSACTIONTYPE) + ->setNewValue($keyword); + + $must_move = true; + } + } list($after_task, $before_task) = $this->loadPriorityTasks( $after_phid, $before_phid); - $must_move = false; if ($after_task && !$task->isLowerPriorityThan($after_task)) { $must_move = true; } @@ -125,10 +157,10 @@ final class PhabricatorProjectMoveController $must_move = true; } - // The move doesn't require a priority change to be valid, so don't - // change the priority since we are not being forced to. + // The move doesn't require a subpriority change to be valid, so don't + // change the subpriority since we are not being forced to. if (!$must_move) { - return array(); + return $xactions; } $try = array( @@ -139,28 +171,41 @@ final class PhabricatorProjectMoveController $pri = null; $sub = null; foreach ($try as $spec) { - list($task, $is_after) = $spec; + list($nearby_task, $is_after) = $spec; - if (!$task) { + if (!$nearby_task) { continue; } list($pri, $sub) = ManiphestTransactionEditor::getAdjacentSubpriority( - $task, + $nearby_task, $is_after); + // If we drag under a "Low" header between a "Normal" task and a "Low" + // task, we don't want to accept a subpriority assignment which changes + // our priority to "Normal". Only accept a subpriority that keeps us in + // the right primary priority. + if ($header_priority !== null) { + if ($pri !== $header_priority) { + continue; + } + } + // If we find a priority on the first try, don't keep going. break; } - $keyword_map = ManiphestTaskPriority::getTaskPriorityKeywordsMap(); - $keyword = head(idx($keyword_map, $pri)); - - $xactions = array(); if ($pri !== null) { - $xactions[] = id(new ManiphestTransaction()) - ->setTransactionType(ManiphestTaskPriorityTransaction::TRANSACTIONTYPE) - ->setNewValue($keyword); + if ($header_priority === null) { + $keyword_map = ManiphestTaskPriority::getTaskPriorityKeywordsMap(); + $keyword = head(idx($keyword_map, $pri)); + + $xactions[] = id(new ManiphestTransaction()) + ->setTransactionType( + ManiphestTaskPriorityTransaction::TRANSACTIONTYPE) + ->setNewValue($keyword); + } + $xactions[] = id(new ManiphestTransaction()) ->setTransactionType( ManiphestTaskSubpriorityTransaction::TRANSACTIONTYPE) diff --git a/src/applications/project/storage/PhabricatorProjectColumn.php b/src/applications/project/storage/PhabricatorProjectColumn.php index 756c356ee1..03b9cbff70 100644 --- a/src/applications/project/storage/PhabricatorProjectColumn.php +++ b/src/applications/project/storage/PhabricatorProjectColumn.php @@ -16,6 +16,9 @@ final class PhabricatorProjectColumn const ORDER_NATURAL = 'natural'; const ORDER_PRIORITY = 'priority'; + const NODETYPE_HEADER = 0; + const NODETYPE_CARD = 1; + protected $name; protected $status; protected $projectPHID; diff --git a/webroot/rsrc/js/application/projects/WorkboardBoard.js b/webroot/rsrc/js/application/projects/WorkboardBoard.js index 2ea38b07b2..b0bcfe97c6 100644 --- a/webroot/rsrc/js/application/projects/WorkboardBoard.js +++ b/webroot/rsrc/js/application/projects/WorkboardBoard.js @@ -161,11 +161,15 @@ JX.install('WorkboardBoard', { var list = new JX.DraggableList('project-card', column.getRoot()) .setOuterContainer(this.getRoot()) - .setFindItemsHandler(JX.bind(column, column.getCardNodes)) + .setFindItemsHandler(JX.bind(column, column.getDropTargetNodes)) .setCanDragX(true) .setHasInfiniteHeight(true) .setIsDropTargetHandler(JX.bind(column, column.setIsDropTarget)); + var default_handler = list.getGhostHandler(); + list.setGhostHandler( + JX.bind(column, column.handleDragGhost, default_handler)); + if (this.getOrder() !== 'natural') { list.setCompareHandler(JX.bind(column, column.compareHandler)); } @@ -198,16 +202,39 @@ JX.install('WorkboardBoard', { order: this.getOrder() }; - if (after_node) { - data.afterPHID = JX.Stratcom.getData(after_node).objectPHID; + var after_data; + var after_card = after_node; + while (after_card) { + after_data = JX.Stratcom.getData(after_card); + if (after_data.objectPHID) { + break; + } + after_card = after_card.previousSibling; } - var before_node = item.nextSibling; - if (before_node) { - var before_phid = JX.Stratcom.getData(before_node).objectPHID; - if (before_phid) { - data.beforePHID = before_phid; + if (after_data) { + data.afterPHID = after_data.objectPHID; + } + + var before_data; + var before_card = item.nextSibling; + while (before_card) { + before_data = JX.Stratcom.getData(before_card); + if (before_data.objectPHID) { + break; } + before_card = before_card.nextSibling; + } + + if (before_data) { + data.beforePHID = before_data.objectPHID; + } + + var header_key = JX.Stratcom.getData(after_node).headerKey; + if (header_key) { + var properties = this.getHeaderTemplate(header_key) + .getEditProperties(); + data.header = JX.JSON.stringify(properties); } var visible_phids = []; diff --git a/webroot/rsrc/js/application/projects/WorkboardCard.js b/webroot/rsrc/js/application/projects/WorkboardCard.js index 753eca40f1..9da3b7e69f 100644 --- a/webroot/rsrc/js/application/projects/WorkboardCard.js +++ b/webroot/rsrc/js/application/projects/WorkboardCard.js @@ -55,6 +55,10 @@ JX.install('WorkboardCard', { return this._root; }, + isWorkboardHeader: function() { + return false; + }, + redraw: function() { var old_node = this._root; this._root = null; diff --git a/webroot/rsrc/js/application/projects/WorkboardColumn.js b/webroot/rsrc/js/application/projects/WorkboardColumn.js index fdb165f589..262e80c2da 100644 --- a/webroot/rsrc/js/application/projects/WorkboardColumn.js +++ b/webroot/rsrc/js/application/projects/WorkboardColumn.js @@ -52,6 +52,10 @@ JX.install('WorkboardColumn', { return this._cards; }, + _getObjects: function() { + return this._objects; + }, + getCard: function(phid) { return this._cards[phid]; }, @@ -126,12 +130,13 @@ JX.install('WorkboardColumn', { return this; }, - getCardNodes: function() { - var cards = this.getCards(); + getDropTargetNodes: function() { + var objects = this._getObjects(); var nodes = []; - for (var k in cards) { - nodes.push(cards[k].getNode()); + for (var ii = 0; ii < objects.length; ii++) { + var object = objects[ii]; + nodes.push(object.getNode()); } return nodes; @@ -160,6 +165,32 @@ JX.install('WorkboardColumn', { return this._headers[key]; }, + handleDragGhost: function(default_handler, ghost, node) { + // If the column has headers, don't let the user drag a card above + // the topmost header: for example, you can't change a task to have + // a priority higher than the highest possible priority. + + if (this._hasColumnHeaders()) { + if (!node) { + return false; + } + } + + return default_handler(ghost, node); + }, + + _hasColumnHeaders: function() { + var board = this.getBoard(); + var order = board.getOrder(); + + switch (order) { + case 'natural': + return false; + } + + return true; + }, + _getCardHeaderKey: function(card, order) { switch (order) { case 'priority': @@ -174,18 +205,16 @@ JX.install('WorkboardColumn', { var order = board.getOrder(); var list; - var has_headers; if (order == 'natural') { list = this._getCardsSortedNaturally(); - has_headers = false; } else { list = this._getCardsSortedByKey(order); - has_headers = true; } var ii; var objects = []; + var has_headers = this._hasColumnHeaders(); var header_keys = []; var seen_headers = {}; if (has_headers) { @@ -245,15 +274,23 @@ JX.install('WorkboardColumn', { var board = this.getBoard(); var order = board.getOrder(); - var src_phid = JX.Stratcom.getData(src_node).objectPHID; - var dst_phid = JX.Stratcom.getData(dst_node).objectPHID; - - var u_vec = board.getOrderVector(src_phid, order); - var v_vec = board.getOrderVector(dst_phid, order); + var u_vec = this._getNodeOrderVector(src_node, order); + var v_vec = this._getNodeOrderVector(dst_node, order); return board.compareVectors(u_vec, v_vec); }, + _getNodeOrderVector: function(node, order) { + var board = this.getBoard(); + var data = JX.Stratcom.getData(node); + + if (data.objectPHID) { + return board.getOrderVector(data.objectPHID, order); + } + + return board.getHeaderTemplate(data.headerKey).getVector(); + }, + setIsDropTarget: function(is_target) { var node = this.getWorkpanelNode(); JX.DOM.alterClass(node, 'workboard-column-drop-target', is_target); diff --git a/webroot/rsrc/js/application/projects/WorkboardHeader.js b/webroot/rsrc/js/application/projects/WorkboardHeader.js index d6cfd137d0..0a8f4d9681 100644 --- a/webroot/rsrc/js/application/projects/WorkboardHeader.js +++ b/webroot/rsrc/js/application/projects/WorkboardHeader.js @@ -30,8 +30,14 @@ JX.install('WorkboardHeader', { var board = this.getColumn().getBoard(); var template = board.getHeaderTemplate(header_key).getTemplate(); this._root = JX.$H(template).getFragment().firstChild; + + JX.Stratcom.getData(this._root).headerKey = header_key; } return this._root; + }, + + isWorkboardHeader: function() { + return true; } } diff --git a/webroot/rsrc/js/application/projects/WorkboardHeaderTemplate.js b/webroot/rsrc/js/application/projects/WorkboardHeaderTemplate.js index c08652bed0..add37d9c25 100644 --- a/webroot/rsrc/js/application/projects/WorkboardHeaderTemplate.js +++ b/webroot/rsrc/js/application/projects/WorkboardHeaderTemplate.js @@ -13,7 +13,8 @@ JX.install('WorkboardHeaderTemplate', { properties: { template: null, order: null, - vector: null + vector: null, + editProperties: null }, members: { diff --git a/webroot/rsrc/js/application/projects/behavior-project-boards.js b/webroot/rsrc/js/application/projects/behavior-project-boards.js index fd2c1a0fe6..6427e1de4c 100644 --- a/webroot/rsrc/js/application/projects/behavior-project-boards.js +++ b/webroot/rsrc/js/application/projects/behavior-project-boards.js @@ -112,7 +112,8 @@ JX.behavior('project-boards', function(config, statics) { board.getHeaderTemplate(header.key) .setOrder(header.order) .setTemplate(header.template) - .setVector(header.vector); + .setVector(header.vector) + .setEditProperties(header.editProperties); } board.start(); From 46ab71f834dc75d0a37ded7645ea17143a7e3ee1 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 12 Mar 2019 11:52:05 -0700 Subject: [PATCH 147/245] Fix "abou" typo of "about" in SearchEngine API methods Summary: See . Test Plan: Read carefully. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20280 --- .../search/engine/PhabricatorSearchEngineAPIMethod.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/applications/search/engine/PhabricatorSearchEngineAPIMethod.php b/src/applications/search/engine/PhabricatorSearchEngineAPIMethod.php index 510ad91864..f4e2dc918f 100644 --- a/src/applications/search/engine/PhabricatorSearchEngineAPIMethod.php +++ b/src/applications/search/engine/PhabricatorSearchEngineAPIMethod.php @@ -403,7 +403,7 @@ EOTEXT $info = pht(<< Date: Thu, 7 Mar 2019 14:04:27 -0800 Subject: [PATCH 148/245] Remove opacity effects for left-side / right-side diff text selection Summary: These effects feel like they're possibly overkill, since other CSS rules make the selection reticle behave correctly and the implementation is relatively intuitive. Or not, either way. Test Plan: Selected text on either side of a 2-up diff, no more opacity effects. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20264 --- resources/celerity/map.php | 12 ++++++------ .../css/application/differential/changeset-view.css | 5 ----- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 00a62f41d6..0e963428bc 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -11,7 +11,7 @@ return array( 'conpherence.pkg.js' => '020aebcf', 'core.pkg.css' => '34ce1741', 'core.pkg.js' => 'b96c872e', - 'differential.pkg.css' => '1755a478', + 'differential.pkg.css' => '8d8360fb', 'differential.pkg.js' => '67e02996', 'diffusion.pkg.css' => '42c75c37', 'diffusion.pkg.js' => '91192d85', @@ -61,7 +61,7 @@ return array( 'rsrc/css/application/dashboard/dashboard.css' => '4267d6c6', 'rsrc/css/application/diff/inline-comment-summary.css' => '81eb368d', 'rsrc/css/application/differential/add-comment.css' => '7e5900d9', - 'rsrc/css/application/differential/changeset-view.css' => '4193eeff', + 'rsrc/css/application/differential/changeset-view.css' => 'bde53589', 'rsrc/css/application/differential/core.css' => '7300a73e', 'rsrc/css/application/differential/phui-inline-comment.css' => '48acce5b', 'rsrc/css/application/differential/revision-comment.css' => '7dbc8d1d', @@ -542,7 +542,7 @@ return array( 'conpherence-thread-manager' => 'aec8e38c', 'conpherence-transaction-css' => '3a3f5e7e', 'd3' => 'd67475f5', - 'differential-changeset-view-css' => '4193eeff', + 'differential-changeset-view-css' => 'bde53589', 'differential-core-view-css' => '7300a73e', 'differential-revision-add-comment-css' => '7e5900d9', 'differential-revision-comment-css' => '7dbc8d1d', @@ -1210,9 +1210,6 @@ return array( 'javelin-behavior', 'javelin-uri', ), - '4193eeff' => array( - 'phui-inline-comment-view-css', - ), '4234f572' => array( 'syntax-default-css', ), @@ -1901,6 +1898,9 @@ return array( 'javelin-vector', 'javelin-stratcom', ), + 'bde53589' => array( + 'phui-inline-comment-view-css', + ), 'c03f2fb4' => array( 'javelin-install', ), diff --git a/webroot/rsrc/css/application/differential/changeset-view.css b/webroot/rsrc/css/application/differential/changeset-view.css index 844690abd3..233ac4cca5 100644 --- a/webroot/rsrc/css/application/differential/changeset-view.css +++ b/webroot/rsrc/css/application/differential/changeset-view.css @@ -451,7 +451,6 @@ unselectable. */ -ms-user-select: none; -webkit-user-select: none; user-select: none; - opacity: 0.5; } .differential-diff.copy-l > tbody > tr > td:nth-child(2) { @@ -459,7 +458,6 @@ unselectable. */ -ms-user-select: auto; -webkit-user-select: auto; user-select: auto; - opacity: 1; } .differential-diff.copy-l > tbody > tr > td.show-more:nth-child(2) { @@ -467,7 +465,6 @@ unselectable. */ -ms-user-select: none; -webkit-user-select: none; user-select: none; - opacity: 0.5; } .differential-diff.copy-r > tbody > tr > td:nth-child(5) { @@ -475,7 +472,6 @@ unselectable. */ -ms-user-select: auto; -webkit-user-select: auto; user-select: auto; - opacity: 1; } .differential-diff.copy-l > tbody > tr.inline > td, @@ -484,5 +480,4 @@ unselectable. */ -ms-user-select: none; -webkit-user-select: none; user-select: none; - opacity: 0.5; } From 00543f0620d1c9ecfbbf89bfef92fa0d1a002d62 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 7 Mar 2019 10:55:57 -0800 Subject: [PATCH 149/245] Remove the ability to drag tasks up and down on (non-Workboard) priority list views Summary: Ref T13074. Today, in normal task list views in Maniphest (not workboards), you can (sometimes) reorder tasks if the view is priority-sorted. I suspect no one ever does this, few users know it's supported, and that it was basically rendered obsolete the day we shipped workboards. This also means that we need to maintain a global "subpriority" for tasks, which distinguishes between different tasks at the same priority level (e.g., "High") and maintains a consistent ordering on workboards. As we move toward making workboards more flexible (e.g., group by author / owner / custom fields), I'd like to try moving away from "subpriority" and possibly removing it entirely, in favor of "natural order", which basically means "we kind of remember where you put the card and it works a bit like a sticky note". Currently, the "natural order" and "subpriority" systems are sort of similar but also sort of in conflict, and the "subpriority" system can't really be extended while the "natural order / column position" system can. The only real reason to have a global "subpriority" is to support the list-view drag-and-drop. It's possible I'm wrong about this and a bunch of users love this feature, but we can re-evaluate if we get feedback in this vein. (This just removes UI, the actual subpriority system is still intact and still used on workboards.) Test Plan: Viewed task lists, was no longer able to drag stuff. Grepped for affected symbols. Dragged stuff in remaining grippable lists, like "Edit Forms" in EditEngine config. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13074 Differential Revision: https://secure.phabricator.com/D20263 --- resources/celerity/map.php | 28 +++----- resources/celerity/packages.php | 1 - src/__phutil_library_map__.php | 2 - .../PhabricatorManiphestApplication.php | 1 - .../controller/ManiphestController.php | 1 - .../ManiphestSubpriorityController.php | 70 ------------------ .../ManiphestTaskEditController.php | 1 - .../query/ManiphestTaskSearchEngine.php | 3 - .../maniphest/view/ManiphestTaskListView.php | 14 +--- .../view/ManiphestTaskResultListView.php | 30 -------- src/view/phui/PHUIObjectItemView.php | 20 ++---- .../maniphest/behavior-batch-selector.js | 13 ---- .../maniphest/behavior-subpriorityeditor.js | 72 ------------------- 13 files changed, 16 insertions(+), 240 deletions(-) delete mode 100644 src/applications/maniphest/controller/ManiphestSubpriorityController.php delete mode 100644 webroot/rsrc/js/application/maniphest/behavior-subpriorityeditor.js diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 0e963428bc..58aa364de5 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -16,7 +16,7 @@ return array( 'diffusion.pkg.css' => '42c75c37', 'diffusion.pkg.js' => '91192d85', 'maniphest.pkg.css' => '35995d6d', - 'maniphest.pkg.js' => '286955ae', + 'maniphest.pkg.js' => 'c9308721', 'rsrc/audio/basic/alert.mp3' => '17889334', 'rsrc/audio/basic/bing.mp3' => 'a817a0c3', 'rsrc/audio/basic/pock.mp3' => '0fa843d0', @@ -395,10 +395,9 @@ return array( 'rsrc/js/application/herald/HeraldRuleEditor.js' => '27daef73', 'rsrc/js/application/herald/PathTypeahead.js' => 'ad486db3', 'rsrc/js/application/herald/herald-rule-editor.js' => '0922e81d', - 'rsrc/js/application/maniphest/behavior-batch-selector.js' => 'cffd39b4', + 'rsrc/js/application/maniphest/behavior-batch-selector.js' => '139ef688', 'rsrc/js/application/maniphest/behavior-line-chart.js' => 'c8147a20', 'rsrc/js/application/maniphest/behavior-list-edit.js' => 'c687e867', - 'rsrc/js/application/maniphest/behavior-subpriorityeditor.js' => '8400307c', 'rsrc/js/application/owners/OwnersPathEditor.js' => '2a8b62d9', 'rsrc/js/application/owners/owners-path-editor.js' => 'ff688a7a', 'rsrc/js/application/passphrase/passphrase-credential-control.js' => '48fe33d0', @@ -619,9 +618,8 @@ return array( 'javelin-behavior-lightbox-attachments' => 'c7e748bf', 'javelin-behavior-line-chart' => 'c8147a20', 'javelin-behavior-linked-container' => '74446546', - 'javelin-behavior-maniphest-batch-selector' => 'cffd39b4', + 'javelin-behavior-maniphest-batch-selector' => '139ef688', 'javelin-behavior-maniphest-list-editor' => 'c687e867', - 'javelin-behavior-maniphest-subpriority-editor' => '8400307c', 'javelin-behavior-owners-path-editor' => 'ff688a7a', 'javelin-behavior-passphrase-credential-control' => '48fe33d0', 'javelin-behavior-phabricator-active-nav' => '7353f43d', @@ -998,6 +996,12 @@ return array( 'javelin-uri', 'phabricator-keyboard-shortcut', ), + '139ef688' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-stratcom', + 'javelin-util', + ), '1c850a26' => array( 'javelin-install', 'javelin-util', @@ -1539,13 +1543,6 @@ return array( 'javelin-dom', 'javelin-vector', ), - '8400307c' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - ), '8437c663' => array( 'javelin-install', 'javelin-dom', @@ -1970,12 +1967,6 @@ return array( 'javelin-dom', 'javelin-stratcom', ), - 'cffd39b4' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-stratcom', - 'javelin-util', - ), 'd0a85a85' => array( 'javelin-dom', 'javelin-util', @@ -2362,7 +2353,6 @@ return array( ), 'maniphest.pkg.js' => array( 'javelin-behavior-maniphest-batch-selector', - 'javelin-behavior-maniphest-subpriority-editor', 'javelin-behavior-maniphest-list-editor', ), ), diff --git a/resources/celerity/packages.php b/resources/celerity/packages.php index deef3633a8..6dbb662288 100644 --- a/resources/celerity/packages.php +++ b/resources/celerity/packages.php @@ -218,7 +218,6 @@ return array( ), 'maniphest.pkg.js' => array( 'javelin-behavior-maniphest-batch-selector', - 'javelin-behavior-maniphest-subpriority-editor', 'javelin-behavior-maniphest-list-editor', ), ); diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 651fad9d12..3cab91717a 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1709,7 +1709,6 @@ phutil_register_library_map(array( 'ManiphestStatusEmailCommand' => 'applications/maniphest/command/ManiphestStatusEmailCommand.php', 'ManiphestStatusSearchConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestStatusSearchConduitAPIMethod.php', 'ManiphestStatusesConfigType' => 'applications/maniphest/config/ManiphestStatusesConfigType.php', - 'ManiphestSubpriorityController' => 'applications/maniphest/controller/ManiphestSubpriorityController.php', 'ManiphestSubtypesConfigType' => 'applications/maniphest/config/ManiphestSubtypesConfigType.php', 'ManiphestTask' => 'applications/maniphest/storage/ManiphestTask.php', 'ManiphestTaskAssignHeraldAction' => 'applications/maniphest/herald/ManiphestTaskAssignHeraldAction.php', @@ -7406,7 +7405,6 @@ phutil_register_library_map(array( 'ManiphestStatusEmailCommand' => 'ManiphestEmailCommand', 'ManiphestStatusSearchConduitAPIMethod' => 'ManiphestConduitAPIMethod', 'ManiphestStatusesConfigType' => 'PhabricatorJSONConfigType', - 'ManiphestSubpriorityController' => 'ManiphestController', 'ManiphestSubtypesConfigType' => 'PhabricatorJSONConfigType', 'ManiphestTask' => array( 'ManiphestDAO', diff --git a/src/applications/maniphest/application/PhabricatorManiphestApplication.php b/src/applications/maniphest/application/PhabricatorManiphestApplication.php index ec732791fa..8ed20416bb 100644 --- a/src/applications/maniphest/application/PhabricatorManiphestApplication.php +++ b/src/applications/maniphest/application/PhabricatorManiphestApplication.php @@ -54,7 +54,6 @@ final class PhabricatorManiphestApplication extends PhabricatorApplication { => 'ManiphestTaskEditController', 'subtask/(?P[1-9]\d*)/' => 'ManiphestTaskSubtaskController', ), - 'subpriority/' => 'ManiphestSubpriorityController', 'graph/(?P[1-9]\d*)/' => 'ManiphestTaskGraphController', ), ); diff --git a/src/applications/maniphest/controller/ManiphestController.php b/src/applications/maniphest/controller/ManiphestController.php index 872d3f7b38..51a243afac 100644 --- a/src/applications/maniphest/controller/ManiphestController.php +++ b/src/applications/maniphest/controller/ManiphestController.php @@ -53,7 +53,6 @@ abstract class ManiphestController extends PhabricatorController { $view = id(new ManiphestTaskListView()) ->setUser($user) - ->setShowSubpriorityControls(!$request->getStr('ungrippable')) ->setShowBatchControls(true) ->setHandles($handles) ->setTasks(array($task)); diff --git a/src/applications/maniphest/controller/ManiphestSubpriorityController.php b/src/applications/maniphest/controller/ManiphestSubpriorityController.php deleted file mode 100644 index 8869b6a327..0000000000 --- a/src/applications/maniphest/controller/ManiphestSubpriorityController.php +++ /dev/null @@ -1,70 +0,0 @@ -getViewer(); - - if (!$request->validateCSRF()) { - return new Aphront403Response(); - } - - $task = id(new ManiphestTaskQuery()) - ->setViewer($viewer) - ->withIDs(array($request->getInt('task'))) - ->needProjectPHIDs(true) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - if (!$task) { - return new Aphront404Response(); - } - - if ($request->getInt('after')) { - $after_task = id(new ManiphestTaskQuery()) - ->setViewer($viewer) - ->withIDs(array($request->getInt('after'))) - ->executeOne(); - if (!$after_task) { - return new Aphront404Response(); - } - list($pri, $sub) = ManiphestTransactionEditor::getAdjacentSubpriority( - $after_task, - $is_after = true); - } else { - list($pri, $sub) = ManiphestTransactionEditor::getEdgeSubpriority( - $request->getInt('priority'), - $is_end = false); - } - - $keyword_map = ManiphestTaskPriority::getTaskPriorityKeywordsMap(); - $keyword = head(idx($keyword_map, $pri)); - - $xactions = array(); - - $xactions[] = id(new ManiphestTransaction()) - ->setTransactionType(ManiphestTaskPriorityTransaction::TRANSACTIONTYPE) - ->setNewValue($keyword); - - $xactions[] = id(new ManiphestTransaction()) - ->setTransactionType(ManiphestTaskSubpriorityTransaction::TRANSACTIONTYPE) - ->setNewValue($sub); - - $editor = id(new ManiphestTransactionEditor()) - ->setActor($viewer) - ->setContinueOnMissingFields(true) - ->setContinueOnNoEffect(true) - ->setContentSourceFromRequest($request); - - $editor->applyTransactions($task, $xactions); - - return id(new AphrontAjaxResponse())->setContent( - array( - 'tasks' => $this->renderSingleTask($task), - )); - } - -} diff --git a/src/applications/maniphest/controller/ManiphestTaskEditController.php b/src/applications/maniphest/controller/ManiphestTaskEditController.php index 9483529138..341997e325 100644 --- a/src/applications/maniphest/controller/ManiphestTaskEditController.php +++ b/src/applications/maniphest/controller/ManiphestTaskEditController.php @@ -5,7 +5,6 @@ final class ManiphestTaskEditController extends ManiphestController { public function handleRequest(AphrontRequest $request) { return id(new ManiphestEditEngine()) ->setController($this) - ->addContextParameter('ungrippable') ->addContextParameter('responseType') ->addContextParameter('columnPHID') ->addContextParameter('order') diff --git a/src/applications/maniphest/query/ManiphestTaskSearchEngine.php b/src/applications/maniphest/query/ManiphestTaskSearchEngine.php index f4a1f2b344..4c69c604e4 100644 --- a/src/applications/maniphest/query/ManiphestTaskSearchEngine.php +++ b/src/applications/maniphest/query/ManiphestTaskSearchEngine.php @@ -366,10 +366,8 @@ final class ManiphestTaskSearchEngine $viewer = $this->requireViewer(); if ($this->isPanelContext()) { - $can_edit_priority = false; $can_bulk_edit = false; } else { - $can_edit_priority = true; $can_bulk_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $this->getApplication(), @@ -380,7 +378,6 @@ final class ManiphestTaskSearchEngine ->setUser($viewer) ->setTasks($tasks) ->setSavedQuery($saved) - ->setCanEditPriority($can_edit_priority) ->setCanBatchEdit($can_bulk_edit) ->setShowBatchControls($this->showBatchControls); diff --git a/src/applications/maniphest/view/ManiphestTaskListView.php b/src/applications/maniphest/view/ManiphestTaskListView.php index 6bf5daf29b..f9ad9e6046 100644 --- a/src/applications/maniphest/view/ManiphestTaskListView.php +++ b/src/applications/maniphest/view/ManiphestTaskListView.php @@ -5,7 +5,6 @@ final class ManiphestTaskListView extends ManiphestView { private $tasks; private $handles; private $showBatchControls; - private $showSubpriorityControls; private $noDataString; public function setTasks(array $tasks) { @@ -25,11 +24,6 @@ final class ManiphestTaskListView extends ManiphestView { return $this; } - public function setShowSubpriorityControls($show_subpriority_controls) { - $this->showSubpriorityControls = $show_subpriority_controls; - return $this; - } - public function setNoDataString($text) { $this->noDataString = $text; return $this; @@ -102,10 +96,7 @@ final class ManiphestTaskListView extends ManiphestView { phabricator_datetime($task->getDateModified(), $this->getUser())); } - if ($this->showSubpriorityControls) { - $item->setGrippable(true); - } - if ($this->showSubpriorityControls || $this->showBatchControls) { + if ($this->showBatchControls) { $item->addSigil('maniphest-task'); } @@ -134,9 +125,6 @@ final class ManiphestTaskListView extends ManiphestView { if ($this->showBatchControls) { $href = new PhutilURI('/maniphest/task/edit/'.$task->getID().'/'); - if (!$this->showSubpriorityControls) { - $href->replaceQueryParam('ungrippable', 'true'); - } $item->addAction( id(new PHUIListItemView()) ->setIcon('fa-pencil') diff --git a/src/applications/maniphest/view/ManiphestTaskResultListView.php b/src/applications/maniphest/view/ManiphestTaskResultListView.php index 6aafcbdccb..cc2a135292 100644 --- a/src/applications/maniphest/view/ManiphestTaskResultListView.php +++ b/src/applications/maniphest/view/ManiphestTaskResultListView.php @@ -4,7 +4,6 @@ final class ManiphestTaskResultListView extends ManiphestView { private $tasks; private $savedQuery; - private $canEditPriority; private $canBatchEdit; private $showBatchControls; @@ -18,11 +17,6 @@ final class ManiphestTaskResultListView extends ManiphestView { return $this; } - public function setCanEditPriority($can_edit_priority) { - $this->canEditPriority = $can_edit_priority; - return $this; - } - public function setCanBatchEdit($can_batch_edit) { $this->canBatchEdit = $can_batch_edit; return $this; @@ -54,28 +48,12 @@ final class ManiphestTaskResultListView extends ManiphestView { $group_parameter, $handles); - $can_edit_priority = $this->canEditPriority; - - $can_drag = ($order_parameter == 'priority') && - ($can_edit_priority) && - ($group_parameter == 'none' || $group_parameter == 'priority'); - - if (!$viewer->isLoggedIn()) { - // TODO: (T7131) Eventually, we conceivably need to make each task - // draggable individually, since the user may be able to edit some but - // not others. - $can_drag = false; - } - $result = array(); $lists = array(); foreach ($groups as $group => $list) { $task_list = new ManiphestTaskListView(); $task_list->setShowBatchControls($this->showBatchControls); - if ($can_drag) { - $task_list->setShowSubpriorityControls(true); - } $task_list->setUser($viewer); $task_list->setTasks($list); $task_list->setHandles($handles); @@ -91,14 +69,6 @@ final class ManiphestTaskResultListView extends ManiphestView { } - if ($can_drag) { - Javelin::initBehavior( - 'maniphest-subpriority-editor', - array( - 'uri' => '/maniphest/subpriority/', - )); - } - return array( $lists, $this->showBatchControls ? $this->renderBatchEditor($query) : null, diff --git a/src/view/phui/PHUIObjectItemView.php b/src/view/phui/PHUIObjectItemView.php index e1c67c7f32..463f34a2a0 100644 --- a/src/view/phui/PHUIObjectItemView.php +++ b/src/view/phui/PHUIObjectItemView.php @@ -414,25 +414,17 @@ final class PHUIObjectItemView extends AphrontTagView { )); } - // 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-oi-name', ), - javelin_tag( - 'span', - array( - 'sigil' => 'slippery', - ), - array( - $this->headIcons, - $header_name, - $header_link, - $description_tag, - ))); + array( + $this->headIcons, + $header_name, + $header_link, + $description_tag, + )); $icons = array(); if ($this->icons) { diff --git a/webroot/rsrc/js/application/maniphest/behavior-batch-selector.js b/webroot/rsrc/js/application/maniphest/behavior-batch-selector.js index b62f40a589..b64abc3503 100644 --- a/webroot/rsrc/js/application/maniphest/behavior-batch-selector.js +++ b/webroot/rsrc/js/application/maniphest/behavior-batch-selector.js @@ -41,19 +41,6 @@ JX.behavior('maniphest-batch-selector', function(config) { update(); }; - var redraw = function (task) { - var selected = is_selected(task); - change(task, selected); - }; - JX.Stratcom.listen( - 'subpriority-changed', - null, - function (e) { - e.kill(); - var data = e.getData(); - redraw(data.task); - }); - // Change all tasks to some state (used by "select all" / "clear selection" // buttons). diff --git a/webroot/rsrc/js/application/maniphest/behavior-subpriorityeditor.js b/webroot/rsrc/js/application/maniphest/behavior-subpriorityeditor.js deleted file mode 100644 index 82f16854f8..0000000000 --- a/webroot/rsrc/js/application/maniphest/behavior-subpriorityeditor.js +++ /dev/null @@ -1,72 +0,0 @@ -/** - * @provides javelin-behavior-maniphest-subpriority-editor - * @requires javelin-behavior - * javelin-dom - * javelin-stratcom - * javelin-workflow - * phabricator-draggable-list - */ - -JX.behavior('maniphest-subpriority-editor', function(config) { - - var draggable = new JX.DraggableList('maniphest-task') - .setFindItemsHandler(function() { - var tasks = JX.DOM.scry(document.body, 'li', 'maniphest-task'); - var heads = JX.DOM.scry(document.body, 'div', 'task-group'); - return tasks.concat(heads); - }) - .setGhostHandler(function(ghost, target) { - if (!target) { - // The user is trying to drag a task above the first group header; - // don't permit that since it doesn't make sense. - return false; - } - - if (target.nextSibling) { - if (JX.DOM.isType(target, 'div')) { - target.nextSibling.insertBefore(ghost, target.nextSibling.firstChild); - } else { - target.parentNode.insertBefore(ghost, target.nextSibling); - } - } else { - target.parentNode.appendChild(ghost); - } - }); - - draggable.listen('shouldBeginDrag', function(e) { - if (e.getNode('slippery') || e.getNode('maniphest-edit-task')) { - JX.Stratcom.context().kill(); - } - }); - - draggable.listen('didDrop', function(node, after) { - var data = { - task: JX.Stratcom.getData(node).taskID - }; - - if (JX.DOM.isType(after, 'div')) { - data.priority = JX.Stratcom.getData(after).priority; - } else { - data.after = JX.Stratcom.getData(after).taskID; - } - - draggable.lock(); - JX.DOM.alterClass(node, 'drag-sending', true); - - var onresponse = function(r) { - var nodes = JX.$H(r.tasks).getFragment().firstChild; - var task = JX.DOM.find(nodes, 'li', 'maniphest-task'); - JX.DOM.replace(node, task); - draggable.unlock(); - JX.Stratcom.invoke( - 'subpriority-changed', - null, - { 'task' : task }); - }; - - new JX.Workflow(config.uri, data) - .setHandler(onresponse) - .start(); - }); - -}); From 46ed8d4a5ea7adcd5c8000e58891f9b9bae2d4b4 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 10 Mar 2019 08:43:55 -0700 Subject: [PATCH 150/245] On Workboards, sort groups by "natural order", not subpriority Summary: Depends on D20263. Ref T10333. I want to add groups like "Assignee" to workboards. This means you may have several tasks grouped under, say, "Alice". When you drag the bottom-most task under "Alice" to the top, what does that mean? Today, the only grouping is "Priority", and it means "change the task's secret/hidden global subpriority". However, this seems to generally be a somewhat-bad answer, and is quite complex. It also doesn't make much sense for an author grouping, since one task can't really be "more assigned" to Alice than another task. Users likely intend this operation to mean "move it, visually, with no other effects" -- that is, user intent is to shuffle sticky notes around on a board, not edit anything substantive. The meaning is probably something like "this is similar to other nearby tasks" or "maybe this is a good place to start", which we can't really capture with any top-level attribute. We could extend "subpriority" and give tasks a secret/hidden "sub-assignment strength" and so on, but this seems like a bad road to walk down. We'll also run into trouble later when subproject columns may appear on the board, and a user could want to put a task in different positions on different subprojects, conceivably. In the "Natural" order view, we already have what is probably a generally better approach for this: a task display order particular to the column, that just remembers where you put the sticky notes. Move away from "subpriority", and toward a world where we mostly keep sticky notes where you stuck them and move them around only when we have to. With no grouping, we still sort by "natural" order, as before. With priority grouping, we now sort by ``. When you drag stuff around inside a priority group, we update the natural order. This means that moving cards around on a "priority" board will also move them around on a "natural" board, at least somewhat. I think this is okay. If it's not intuitive, we could give every ordering its own separate "natural" view, so we remember where you stuck stuff on the "priority" board but that doesn't affect the "Natural" board. But I suspect we won't need to. Test Plan: - Viewed and dragged a natural board. - Viewed and dragged a priority board. - Dragged within and between groups of 0, 1, and multiple items. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T10333 Differential Revision: https://secure.phabricator.com/D20265 --- resources/celerity/map.php | 38 ++-- .../maniphest/storage/ManiphestTask.php | 3 - .../PhabricatorProjectBoardViewController.php | 8 - .../PhabricatorProjectMoveController.php | 169 +++--------------- .../js/application/projects/WorkboardBoard.js | 44 ++++- .../application/projects/WorkboardColumn.js | 80 ++++++--- 6 files changed, 141 insertions(+), 201 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 58aa364de5..30d6587bac 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -408,9 +408,9 @@ return array( 'rsrc/js/application/phortune/phortune-credit-card-form.js' => 'd12d214f', 'rsrc/js/application/policy/behavior-policy-control.js' => '0eaa33a9', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '9347f172', - 'rsrc/js/application/projects/WorkboardBoard.js' => 'a4f1e85d', + 'rsrc/js/application/projects/WorkboardBoard.js' => '902a1551', 'rsrc/js/application/projects/WorkboardCard.js' => '887ef74f', - 'rsrc/js/application/projects/WorkboardColumn.js' => 'ca444dca', + 'rsrc/js/application/projects/WorkboardColumn.js' => '01ea93b3', 'rsrc/js/application/projects/WorkboardController.js' => '42c7a5a7', 'rsrc/js/application/projects/WorkboardHeader.js' => '6e75daea', 'rsrc/js/application/projects/WorkboardHeaderTemplate.js' => '2d641f7d', @@ -727,9 +727,9 @@ return array( 'javelin-view-renderer' => '9aae2b66', 'javelin-view-visitor' => '308f9fe4', 'javelin-websocket' => 'fdc13e4e', - 'javelin-workboard-board' => 'a4f1e85d', + 'javelin-workboard-board' => '902a1551', 'javelin-workboard-card' => '887ef74f', - 'javelin-workboard-column' => 'ca444dca', + 'javelin-workboard-column' => '01ea93b3', 'javelin-workboard-controller' => '42c7a5a7', 'javelin-workboard-header' => '6e75daea', 'javelin-workboard-header-template' => '2d641f7d', @@ -889,6 +889,11 @@ return array( 'javelin-uri', 'phabricator-notification', ), + '01ea93b3' => array( + 'javelin-install', + 'javelin-workboard-card', + 'javelin-workboard-header', + ), '022516b4' => array( 'javelin-install', 'javelin-util', @@ -1615,6 +1620,16 @@ return array( 'javelin-workflow', 'javelin-stratcom', ), + '902a1551' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-util', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + 'javelin-workboard-column', + 'javelin-workboard-header-template', + ), 91863989 => array( 'javelin-install', 'javelin-stratcom', @@ -1750,16 +1765,6 @@ return array( 'javelin-request', 'javelin-util', ), - 'a4f1e85d' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-util', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - 'javelin-workboard-column', - 'javelin-workboard-header-template', - ), 'a5257c4e' => array( 'javelin-install', 'javelin-dom', @@ -1957,11 +1962,6 @@ return array( 'javelin-util', 'phabricator-keyboard-shortcut-manager', ), - 'ca444dca' => array( - 'javelin-install', - 'javelin-workboard-card', - 'javelin-workboard-header', - ), 'cf32921f' => array( 'javelin-behavior', 'javelin-dom', diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php index 8d45ed45fa..c19e75604c 100644 --- a/src/applications/maniphest/storage/ManiphestTask.php +++ b/src/applications/maniphest/storage/ManiphestTask.php @@ -252,9 +252,6 @@ final class ManiphestTask extends ManiphestDAO return array( PhabricatorProjectColumn::ORDER_PRIORITY => array( (int)-$this->getPriority(), - PhabricatorProjectColumn::NODETYPE_CARD, - (double)-$this->getSubpriority(), - (int)-$this->getID(), ), ); } diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index 32374ad8be..6df43addd9 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -522,13 +522,6 @@ final class PhabricatorProjectBoardViewController $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)); - } - $column_phid = $column->getPHID(); $visible_columns[$column_phid] = $column; @@ -684,7 +677,6 @@ final class PhabricatorProjectBoardViewController ); $this->initBehavior('project-boards', $behavior_config); - $sort_menu = $this->buildSortMenu( $viewer, $project, diff --git a/src/applications/project/controller/PhabricatorProjectMoveController.php b/src/applications/project/controller/PhabricatorProjectMoveController.php index 09fca4692d..7a771ea7e8 100644 --- a/src/applications/project/controller/PhabricatorProjectMoveController.php +++ b/src/applications/project/controller/PhabricatorProjectMoveController.php @@ -71,20 +71,14 @@ final class PhabricatorProjectMoveController ->setObjectPHIDs(array($object_phid)) ->executeLayout(); - $columns = $engine->getObjectColumns($board_phid, $object_phid); - $old_column_phids = mpull($columns, 'getPHID'); - - $xactions = array(); - $order_params = array(); - if ($order == PhabricatorProjectColumn::ORDER_NATURAL) { - if ($after_phid) { - $order_params['afterPHID'] = $after_phid; - } else if ($before_phid) { - $order_params['beforePHID'] = $before_phid; - } + if ($after_phid) { + $order_params['afterPHID'] = $after_phid; + } else if ($before_phid) { + $order_params['beforePHID'] = $before_phid; } + $xactions = array(); $xactions[] = id(new ManiphestTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_COLUMNS) ->setNewValue( @@ -94,18 +88,12 @@ final class PhabricatorProjectMoveController ) + $order_params, )); - if ($order == PhabricatorProjectColumn::ORDER_PRIORITY) { - $header_priority = idx( - $edit_header, - PhabricatorProjectColumn::ORDER_PRIORITY); - $priority_xactions = $this->getPriorityTransactions( - $object, - $after_phid, - $before_phid, - $header_priority); - foreach ($priority_xactions as $xaction) { - $xactions[] = $xaction; - } + $header_xactions = $this->newHeaderTransactions( + $object, + $order, + $edit_header); + foreach ($header_xactions as $header_xaction) { + $xactions[] = $header_xaction; } $editor = id(new ManiphestTransactionEditor()) @@ -119,137 +107,30 @@ final class PhabricatorProjectMoveController return $this->newCardResponse($board_phid, $object_phid); } - private function getPriorityTransactions( + private function newHeaderTransactions( ManiphestTask $task, - $after_phid, - $before_phid, - $header_priority) { + $order, + array $header) { $xactions = array(); - $must_move = false; - if ($header_priority !== null) { - if ($task->getPriority() !== $header_priority) { - $task = id(clone $task) - ->setPriority($header_priority); + switch ($order) { + case PhabricatorProjectColumn::ORDER_PRIORITY: + $new_priority = idx($header, $order); - $keyword_map = ManiphestTaskPriority::getTaskPriorityKeywordsMap(); - $keyword = head(idx($keyword_map, $header_priority)); + if ($task->getPriority() !== $new_priority) { + $keyword_map = ManiphestTaskPriority::getTaskPriorityKeywordsMap(); + $keyword = head(idx($keyword_map, $new_priority)); - $xactions[] = id(new ManiphestTransaction()) - ->setTransactionType( - ManiphestTaskPriorityTransaction::TRANSACTIONTYPE) - ->setNewValue($keyword); - - $must_move = true; - } - } - - list($after_task, $before_task) = $this->loadPriorityTasks( - $after_phid, - $before_phid); - - if ($after_task && !$task->isLowerPriorityThan($after_task)) { - $must_move = true; - } - - if ($before_task && !$task->isHigherPriorityThan($before_task)) { - $must_move = true; - } - - // The move doesn't require a subpriority change to be valid, so don't - // change the subpriority since we are not being forced to. - if (!$must_move) { - return $xactions; - } - - $try = array( - array($after_task, true), - array($before_task, false), - ); - - $pri = null; - $sub = null; - foreach ($try as $spec) { - list($nearby_task, $is_after) = $spec; - - if (!$nearby_task) { - continue; - } - - list($pri, $sub) = ManiphestTransactionEditor::getAdjacentSubpriority( - $nearby_task, - $is_after); - - // If we drag under a "Low" header between a "Normal" task and a "Low" - // task, we don't want to accept a subpriority assignment which changes - // our priority to "Normal". Only accept a subpriority that keeps us in - // the right primary priority. - if ($header_priority !== null) { - if ($pri !== $header_priority) { - continue; + $xactions[] = id(new ManiphestTransaction()) + ->setTransactionType( + ManiphestTaskPriorityTransaction::TRANSACTIONTYPE) + ->setNewValue($keyword); } - } - - // If we find a priority on the first try, don't keep going. - break; - } - - if ($pri !== null) { - if ($header_priority === null) { - $keyword_map = ManiphestTaskPriority::getTaskPriorityKeywordsMap(); - $keyword = head(idx($keyword_map, $pri)); - - $xactions[] = id(new ManiphestTransaction()) - ->setTransactionType( - ManiphestTaskPriorityTransaction::TRANSACTIONTYPE) - ->setNewValue($keyword); - } - - $xactions[] = id(new ManiphestTransaction()) - ->setTransactionType( - ManiphestTaskSubpriorityTransaction::TRANSACTIONTYPE) - ->setNewValue($sub); + break; } return $xactions; } - private function loadPriorityTasks($after_phid, $before_phid) { - $viewer = $this->getViewer(); - - $task_phids = array(); - - if ($after_phid) { - $task_phids[] = $after_phid; - } - if ($before_phid) { - $task_phids[] = $before_phid; - } - - if (!$task_phids) { - return array(null, null); - } - - $tasks = id(new ManiphestTaskQuery()) - ->setViewer($viewer) - ->withPHIDs($task_phids) - ->execute(); - $tasks = mpull($tasks, null, 'getPHID'); - - if ($after_phid) { - $after_task = idx($tasks, $after_phid); - } else { - $after_task = null; - } - - if ($before_phid) { - $before_task = idx($tasks, $before_phid); - } else { - $before_task = null; - } - - return array($after_task, $before_task); - } - } diff --git a/webroot/rsrc/js/application/projects/WorkboardBoard.js b/webroot/rsrc/js/application/projects/WorkboardBoard.js index b0bcfe97c6..c49e4c859f 100644 --- a/webroot/rsrc/js/application/projects/WorkboardBoard.js +++ b/webroot/rsrc/js/application/projects/WorkboardBoard.js @@ -202,6 +202,14 @@ JX.install('WorkboardBoard', { order: this.getOrder() }; + // We're going to send an "afterPHID" and a "beforePHID" if the card + // was dropped immediately adjacent to another card. If a card was + // dropped before or after a header, we don't send a PHID for the card + // on the other side of the header. + + // If the view has headers, we always send the header the card was + // dropped under. + var after_data; var after_card = after_node; while (after_card) { @@ -209,11 +217,16 @@ JX.install('WorkboardBoard', { if (after_data.objectPHID) { break; } + if (after_data.headerKey) { + break; + } after_card = after_card.previousSibling; } if (after_data) { - data.afterPHID = after_data.objectPHID; + if (after_data.objectPHID) { + data.afterPHID = after_data.objectPHID; + } } var before_data; @@ -223,18 +236,35 @@ JX.install('WorkboardBoard', { if (before_data.objectPHID) { break; } + if (before_data.headerKey) { + break; + } before_card = before_card.nextSibling; } if (before_data) { - data.beforePHID = before_data.objectPHID; + if (before_data.objectPHID) { + data.beforePHID = before_data.objectPHID; + } } - var header_key = JX.Stratcom.getData(after_node).headerKey; - if (header_key) { - var properties = this.getHeaderTemplate(header_key) - .getEditProperties(); - data.header = JX.JSON.stringify(properties); + var header_data; + var header_node = after_node; + while (header_node) { + header_data = JX.Stratcom.getData(header_node); + if (header_data.headerKey) { + break; + } + header_node = header_node.previousSibling; + } + + if (header_data) { + var header_key = header_data.headerKey; + if (header_key) { + var properties = this.getHeaderTemplate(header_key) + .getEditProperties(); + data.header = JX.JSON.stringify(properties); + } } var visible_phids = []; diff --git a/webroot/rsrc/js/application/projects/WorkboardColumn.js b/webroot/rsrc/js/application/projects/WorkboardColumn.js index 262e80c2da..e2ec18ae75 100644 --- a/webroot/rsrc/js/application/projects/WorkboardColumn.js +++ b/webroot/rsrc/js/application/projects/WorkboardColumn.js @@ -34,6 +34,7 @@ JX.install('WorkboardColumn', { _cards: null, _headers: null, _naturalOrder: null, + _orderVectors: null, _panel: null, _pointsNode: null, _pointsContentNode: null, @@ -66,6 +67,7 @@ JX.install('WorkboardColumn', { setNaturalOrder: function(order) { this._naturalOrder = order; + this._orderVectors = null; return this; }, @@ -86,6 +88,7 @@ JX.install('WorkboardColumn', { this._cards[phid] = card; this._naturalOrder.push(phid); + this._orderVectors = null; return card; }, @@ -97,6 +100,7 @@ JX.install('WorkboardColumn', { for (var ii = 0; ii < this._naturalOrder.length; ii++) { if (this._naturalOrder[ii] == phid) { this._naturalOrder.splice(ii, 1); + this._orderVectors = null; break; } } @@ -127,6 +131,8 @@ JX.install('WorkboardColumn', { this._naturalOrder.splice(index, 0, phid); } + this._orderVectors = null; + return this; }, @@ -204,12 +210,7 @@ JX.install('WorkboardColumn', { var board = this.getBoard(); var order = board.getOrder(); - var list; - if (order == 'natural') { - list = this._getCardsSortedNaturally(); - } else { - list = this._getCardsSortedByKey(order); - } + var list = this._getCardsSortedByKey(order); var ii; var objects = []; @@ -285,7 +286,7 @@ JX.install('WorkboardColumn', { var data = JX.Stratcom.getData(node); if (data.objectPHID) { - return board.getOrderVector(data.objectPHID, order); + return this._getOrderVector(data.objectPHID, order); } return board.getHeaderTemplate(data.headerKey).getVector(); @@ -296,17 +297,6 @@ JX.install('WorkboardColumn', { JX.DOM.alterClass(node, 'workboard-column-drop-target', is_target); }, - _getCardsSortedNaturally: function() { - var list = []; - - for (var ii = 0; ii < this._naturalOrder.length; ii++) { - var phid = this._naturalOrder[ii]; - list.push(this.getCard(phid)); - } - - return list; - }, - _getCardsSortedByKey: function(order) { var cards = this.getCards(); @@ -322,12 +312,62 @@ JX.install('WorkboardColumn', { _sortCards: function(order, u, v) { var board = this.getBoard(); - var u_vec = board.getOrderVector(u.getPHID(), order); - var v_vec = board.getOrderVector(v.getPHID(), order); + var u_vec = this._getOrderVector(u.getPHID(), order); + var v_vec = this._getOrderVector(v.getPHID(), order); return board.compareVectors(u_vec, v_vec); }, + _getOrderVector: function(phid, order) { + if (!this._orderVectors) { + this._orderVectors = {}; + } + + if (!this._orderVectors[order]) { + var board = this.getBoard(); + var cards = this.getCards(); + var vectors = {}; + + for (var k in cards) { + var card_phid = cards[k].getPHID(); + var vector = board.getOrderVector(card_phid, order); + vectors[card_phid] = [].concat(vector); + + // Push a "card" type, so cards always sort after headers; headers + // have a "0" in this position. + vectors[card_phid].push(1); + } + + for (var ii = 0; ii < this._naturalOrder.length; ii++) { + var natural_phid = this._naturalOrder[ii]; + if (vectors[natural_phid]) { + vectors[natural_phid].push(ii); + } + } + + this._orderVectors[order] = vectors; + } + + if (!this._orderVectors[order][phid]) { + // In this case, we're comparing a card being dragged in from another + // column to the cards already in this column. We're just going to + // build a temporary vector for it. + var incoming_vector = this.getBoard().getOrderVector(phid, order); + incoming_vector = [].concat(incoming_vector); + + // Add a "card" type to sort this after headers. + incoming_vector.push(1); + + // Add a "0" for the natural ordering to put this on top. A new card + // has no natural ordering on a column it isn't part of yet. + incoming_vector.push(0); + + return incoming_vector; + } + + return this._orderVectors[order][phid]; + }, + _redrawFrame: function() { var cards = this.getCards(); var board = this.getBoard(); From 4bad6bc42af9022a648964f1c80718dd8e4fb27e Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 10 Mar 2019 08:44:15 -0700 Subject: [PATCH 151/245] Remove all readers/writers for task "subpriority" Summary: Depends on D20265. Ref T10333. Now that neither task lists nor workboards use subpriority, we can remove all the readers and writers. I'm not actually getting rid of the column data yet, but anticipate doing that in a future change. Note that the subpriority algorithm (removed here) is possibly better than the "natural order" algorithm still in use. It's a bit more clever, and likely performs far fewer writes. I might make the "natural order" code use an algorithm more similar to the "subpriority" algorithm in the future. Test Plan: Grepped for `subpriority`. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T10333 Differential Revision: https://secure.phabricator.com/D20266 --- src/__phutil_library_map__.php | 2 - .../__tests__/ManiphestTaskTestCase.php | 255 ------------------ .../editor/ManiphestTransactionEditor.php | 245 ----------------- ...bricatorManiphestTaskTestDataGenerator.php | 5 - .../maniphest/query/ManiphestTaskQuery.php | 15 +- .../maniphest/storage/ManiphestTask.php | 34 --- .../ManiphestTaskSubpriorityTransaction.php | 7 +- 7 files changed, 5 insertions(+), 558 deletions(-) delete mode 100644 src/applications/maniphest/__tests__/ManiphestTaskTestCase.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 3cab91717a..713d3e5f29 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1782,7 +1782,6 @@ phutil_register_library_map(array( 'ManiphestTaskSubpriorityTransaction' => 'applications/maniphest/xaction/ManiphestTaskSubpriorityTransaction.php', 'ManiphestTaskSubtaskController' => 'applications/maniphest/controller/ManiphestTaskSubtaskController.php', 'ManiphestTaskSubtypeDatasource' => 'applications/maniphest/typeahead/ManiphestTaskSubtypeDatasource.php', - 'ManiphestTaskTestCase' => 'applications/maniphest/__tests__/ManiphestTaskTestCase.php', 'ManiphestTaskTitleHeraldField' => 'applications/maniphest/herald/ManiphestTaskTitleHeraldField.php', 'ManiphestTaskTitleTransaction' => 'applications/maniphest/xaction/ManiphestTaskTitleTransaction.php', 'ManiphestTaskTransactionType' => 'applications/maniphest/xaction/ManiphestTaskTransactionType.php', @@ -7501,7 +7500,6 @@ phutil_register_library_map(array( 'ManiphestTaskSubpriorityTransaction' => 'ManiphestTaskTransactionType', 'ManiphestTaskSubtaskController' => 'ManiphestController', 'ManiphestTaskSubtypeDatasource' => 'PhabricatorTypeaheadDatasource', - 'ManiphestTaskTestCase' => 'PhabricatorTestCase', 'ManiphestTaskTitleHeraldField' => 'ManiphestTaskHeraldField', 'ManiphestTaskTitleTransaction' => 'ManiphestTaskTransactionType', 'ManiphestTaskTransactionType' => 'PhabricatorModularTransactionType', diff --git a/src/applications/maniphest/__tests__/ManiphestTaskTestCase.php b/src/applications/maniphest/__tests__/ManiphestTaskTestCase.php deleted file mode 100644 index 58190f6a89..0000000000 --- a/src/applications/maniphest/__tests__/ManiphestTaskTestCase.php +++ /dev/null @@ -1,255 +0,0 @@ - true, - ); - } - - public function testTaskReordering() { - $viewer = $this->generateNewTestUser(); - - $t1 = $this->newTask($viewer, pht('Task 1')); - $t2 = $this->newTask($viewer, pht('Task 2')); - $t3 = $this->newTask($viewer, pht('Task 3')); - - $auto_base = min(mpull(array($t1, $t2, $t3), 'getID')); - - - // Default order should be reverse creation. - $tasks = $this->loadTasks($viewer, $auto_base); - $t1 = $tasks[1]; - $t2 = $tasks[2]; - $t3 = $tasks[3]; - $this->assertEqual(array(3, 2, 1), array_keys($tasks)); - - - // Move T3 to the middle. - $this->moveTask($viewer, $t3, $t2, true); - $tasks = $this->loadTasks($viewer, $auto_base); - $t1 = $tasks[1]; - $t2 = $tasks[2]; - $t3 = $tasks[3]; - $this->assertEqual(array(2, 3, 1), array_keys($tasks)); - - - // Move T3 to the end. - $this->moveTask($viewer, $t3, $t1, true); - $tasks = $this->loadTasks($viewer, $auto_base); - $t1 = $tasks[1]; - $t2 = $tasks[2]; - $t3 = $tasks[3]; - $this->assertEqual(array(2, 1, 3), array_keys($tasks)); - - - // Repeat the move above, there should be no overall change in order. - $this->moveTask($viewer, $t3, $t1, true); - $tasks = $this->loadTasks($viewer, $auto_base); - $t1 = $tasks[1]; - $t2 = $tasks[2]; - $t3 = $tasks[3]; - $this->assertEqual(array(2, 1, 3), array_keys($tasks)); - - - // Move T3 to the first slot in the priority. - $this->movePriority($viewer, $t3, $t3->getPriority(), false); - $tasks = $this->loadTasks($viewer, $auto_base); - $t1 = $tasks[1]; - $t2 = $tasks[2]; - $t3 = $tasks[3]; - $this->assertEqual(array(3, 2, 1), array_keys($tasks)); - - - // Move T3 to the last slot in the priority. - $this->movePriority($viewer, $t3, $t3->getPriority(), true); - $tasks = $this->loadTasks($viewer, $auto_base); - $t1 = $tasks[1]; - $t2 = $tasks[2]; - $t3 = $tasks[3]; - $this->assertEqual(array(2, 1, 3), array_keys($tasks)); - - - // Move T3 before T2. - $this->moveTask($viewer, $t3, $t2, false); - $tasks = $this->loadTasks($viewer, $auto_base); - $t1 = $tasks[1]; - $t2 = $tasks[2]; - $t3 = $tasks[3]; - $this->assertEqual(array(3, 2, 1), array_keys($tasks)); - - - // Move T3 before T1. - $this->moveTask($viewer, $t3, $t1, false); - $tasks = $this->loadTasks($viewer, $auto_base); - $t1 = $tasks[1]; - $t2 = $tasks[2]; - $t3 = $tasks[3]; - $this->assertEqual(array(2, 3, 1), array_keys($tasks)); - - } - - public function testTaskAdjacentBlocks() { - $viewer = $this->generateNewTestUser(); - - $t = array(); - for ($ii = 1; $ii < 10; $ii++) { - $t[$ii] = $this->newTask($viewer, pht('Task Block %d', $ii)); - - // This makes sure this test remains meaningful if we begin assigning - // subpriorities when tasks are created. - $t[$ii]->setSubpriority(0)->save(); - } - - $auto_base = min(mpull($t, 'getID')); - - $tasks = $this->loadTasks($viewer, $auto_base); - $this->assertEqual( - array(9, 8, 7, 6, 5, 4, 3, 2, 1), - array_keys($tasks)); - - $this->moveTask($viewer, $t[9], $t[8], true); - $tasks = $this->loadTasks($viewer, $auto_base); - $this->assertEqual( - array(8, 9, 7, 6, 5, 4, 3, 2, 1), - array_keys($tasks)); - - // When there is a large block of tasks which all have the same - // subpriority, they should be assigned distinct subpriorities as a - // side effect of having a task moved into the block. - - $subpri = mpull($tasks, 'getSubpriority'); - $unique_subpri = array_unique($subpri); - $this->assertEqual( - 9, - count($subpri), - pht('Expected subpriorities to be distributed.')); - - // Move task 9 to the end. - $this->moveTask($viewer, $t[9], $t[1], true); - $tasks = $this->loadTasks($viewer, $auto_base); - $this->assertEqual( - array(8, 7, 6, 5, 4, 3, 2, 1, 9), - array_keys($tasks)); - - // Move task 3 to the beginning. - $this->moveTask($viewer, $t[3], $t[8], false); - $tasks = $this->loadTasks($viewer, $auto_base); - $this->assertEqual( - array(3, 8, 7, 6, 5, 4, 2, 1, 9), - array_keys($tasks)); - - // Move task 3 to the end. - $this->moveTask($viewer, $t[3], $t[9], true); - $tasks = $this->loadTasks($viewer, $auto_base); - $this->assertEqual( - array(8, 7, 6, 5, 4, 2, 1, 9, 3), - array_keys($tasks)); - - // Move task 5 to before task 4 (this is its current position). - $this->moveTask($viewer, $t[5], $t[4], false); - $tasks = $this->loadTasks($viewer, $auto_base); - $this->assertEqual( - array(8, 7, 6, 5, 4, 2, 1, 9, 3), - array_keys($tasks)); - } - - private function newTask(PhabricatorUser $viewer, $title) { - $task = ManiphestTask::initializeNewTask($viewer); - - $xactions = array(); - - $xactions[] = id(new ManiphestTransaction()) - ->setTransactionType(ManiphestTaskTitleTransaction::TRANSACTIONTYPE) - ->setNewValue($title); - - - $this->applyTaskTransactions($viewer, $task, $xactions); - - return $task; - } - - private function loadTasks(PhabricatorUser $viewer, $auto_base) { - $tasks = id(new ManiphestTaskQuery()) - ->setViewer($viewer) - ->setOrder(ManiphestTaskQuery::ORDER_PRIORITY) - ->execute(); - - // NOTE: AUTO_INCREMENT changes survive ROLLBACK, and we can't throw them - // away without committing the current transaction, so we adjust the - // apparent task IDs as though the first one had been ID 1. This makes the - // tests a little easier to understand. - - $map = array(); - foreach ($tasks as $task) { - $map[($task->getID() - $auto_base) + 1] = $task; - } - - return $map; - } - - private function moveTask(PhabricatorUser $viewer, $src, $dst, $is_after) { - list($pri, $sub) = ManiphestTransactionEditor::getAdjacentSubpriority( - $dst, - $is_after); - - $keyword_map = ManiphestTaskPriority::getTaskPriorityKeywordsMap(); - $keyword = head($keyword_map[$pri]); - - $xactions = array(); - - $xactions[] = id(new ManiphestTransaction()) - ->setTransactionType(ManiphestTaskPriorityTransaction::TRANSACTIONTYPE) - ->setNewValue($keyword); - - $xactions[] = id(new ManiphestTransaction()) - ->setTransactionType(ManiphestTaskSubpriorityTransaction::TRANSACTIONTYPE) - ->setNewValue($sub); - - return $this->applyTaskTransactions($viewer, $src, $xactions); - } - - private function movePriority( - PhabricatorUser $viewer, - $src, - $target_priority, - $is_end) { - - list($pri, $sub) = ManiphestTransactionEditor::getEdgeSubpriority( - $target_priority, - $is_end); - - $keyword_map = ManiphestTaskPriority::getTaskPriorityKeywordsMap(); - $keyword = head($keyword_map[$pri]); - - $xactions = array(); - - $xactions[] = id(new ManiphestTransaction()) - ->setTransactionType(ManiphestTaskPriorityTransaction::TRANSACTIONTYPE) - ->setNewValue($keyword); - - $xactions[] = id(new ManiphestTransaction()) - ->setTransactionType(ManiphestTaskSubpriorityTransaction::TRANSACTIONTYPE) - ->setNewValue($sub); - - return $this->applyTaskTransactions($viewer, $src, $xactions); - } - - private function applyTaskTransactions( - PhabricatorUser $viewer, - ManiphestTask $task, - array $xactions) { - - $content_source = $this->newContentSource(); - - $editor = id(new ManiphestTransactionEditor()) - ->setActor($viewer) - ->setContentSource($content_source) - ->setContinueOnNoEffect(true) - ->applyTransactions($task, $xactions); - - return $task; - } - -} diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php index 1748e5e84e..9a95bbdbb8 100644 --- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php +++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php @@ -297,251 +297,6 @@ final class ManiphestTransactionEditor return $copy; } - /** - * Get priorities for moving a task to a new priority. - */ - public static function getEdgeSubpriority( - $priority, - $is_end) { - - $query = id(new ManiphestTaskQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withPriorities(array($priority)) - ->setLimit(1); - - if ($is_end) { - $query->setOrderVector(array('-priority', '-subpriority', '-id')); - } else { - $query->setOrderVector(array('priority', 'subpriority', 'id')); - } - - $result = $query->executeOne(); - $step = (double)(2 << 32); - - if ($result) { - $base = $result->getSubpriority(); - if ($is_end) { - $sub = ($base - $step); - } else { - $sub = ($base + $step); - } - } else { - $sub = 0; - } - - return array($priority, $sub); - } - - - /** - * Get priorities for moving a task before or after another task. - */ - public static function getAdjacentSubpriority( - ManiphestTask $dst, - $is_after) { - - $query = id(new ManiphestTaskQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->setOrder(ManiphestTaskQuery::ORDER_PRIORITY) - ->withPriorities(array($dst->getPriority())) - ->setLimit(1); - - if ($is_after) { - $query->setAfterID($dst->getID()); - } else { - $query->setBeforeID($dst->getID()); - } - - $adjacent = $query->executeOne(); - - $base = $dst->getSubpriority(); - $step = (double)(2 << 32); - - // If we find an adjacent task, we average the two subpriorities and - // return the result. - if ($adjacent) { - $epsilon = 1.0; - - // If the adjacent task has a subpriority that is identical or very - // close to the task we're looking at, we're going to spread out all - // the nearby tasks. - - $adjacent_sub = $adjacent->getSubpriority(); - if ((abs($adjacent_sub - $base) < $epsilon)) { - $base = self::disperseBlock( - $dst, - $epsilon * 2); - if ($is_after) { - $sub = $base - $epsilon; - } else { - $sub = $base + $epsilon; - } - } else { - $sub = ($adjacent_sub + $base) / 2; - } - } else { - // Otherwise, we take a step away from the target's subpriority and - // use that. - if ($is_after) { - $sub = ($base - $step); - } else { - $sub = ($base + $step); - } - } - - return array($dst->getPriority(), $sub); - } - - /** - * Distribute a cluster of tasks with similar subpriorities. - */ - private static function disperseBlock( - ManiphestTask $task, - $spacing) { - - $conn = $task->establishConnection('w'); - - // Find a block of subpriority space which is, on average, sparse enough - // to hold all the tasks that are inside it with a reasonable level of - // separation between them. - - // We'll start by looking near the target task for a range of numbers - // which has more space available than tasks. For example, if the target - // task has subpriority 33 and we want to separate each task by at least 1, - // we might start by looking in the range [23, 43]. - - // If we find fewer than 20 tasks there, we have room to reassign them - // with the desired level of separation. We space them out, then we're - // done. - - // However: if we find more than 20 tasks, we don't have enough room to - // distribute them. We'll widen our search and look in a bigger range, - // maybe [13, 53]. This range has more space, so if we find fewer than - // 40 tasks in this range we can spread them out. If we still find too - // many tasks, we keep widening the search. - - $base = $task->getSubpriority(); - - $scale = 4.0; - while (true) { - $range = ($spacing * $scale) / 2.0; - $min = ($base - $range); - $max = ($base + $range); - - $result = queryfx_one( - $conn, - 'SELECT COUNT(*) N FROM %T WHERE priority = %d AND - subpriority BETWEEN %f AND %f', - $task->getTableName(), - $task->getPriority(), - $min, - $max); - - $count = $result['N']; - if ($count < $scale) { - // We have found a block which we can make sparse enough, so bail and - // continue below with our selection. - break; - } - - // This block had too many tasks for its size, so try again with a - // bigger block. - $scale *= 2.0; - } - - $rows = queryfx_all( - $conn, - 'SELECT id FROM %T WHERE priority = %d AND - subpriority BETWEEN %f AND %f - ORDER BY priority, subpriority, id', - $task->getTableName(), - $task->getPriority(), - $min, - $max); - - $task_id = $task->getID(); - $result = null; - - // NOTE: In strict mode (which we encourage enabling) we can't structure - // this bulk update as an "INSERT ... ON DUPLICATE KEY UPDATE" unless we - // provide default values for ALL of the columns that don't have defaults. - - // This is gross, but we may be moving enough rows that individual - // queries are unreasonably slow. An alternate construction which might - // be worth evaluating is to use "CASE". Another approach is to disable - // strict mode for this query. - - $default_str = qsprintf($conn, '%s', ''); - $default_int = qsprintf($conn, '%d', 0); - - $extra_columns = array( - 'phid' => $default_str, - 'authorPHID' => $default_str, - 'status' => $default_str, - 'priority' => $default_int, - 'title' => $default_str, - 'description' => $default_str, - 'dateCreated' => $default_int, - 'dateModified' => $default_int, - 'mailKey' => $default_str, - 'viewPolicy' => $default_str, - 'editPolicy' => $default_str, - 'ownerOrdering' => $default_str, - 'spacePHID' => $default_str, - 'bridgedObjectPHID' => $default_str, - 'properties' => $default_str, - 'points' => $default_int, - 'subtype' => $default_str, - ); - - $sql = array(); - $offset = 0; - - // Often, we'll have more room than we need in the range. Distribute the - // tasks evenly over the whole range so that we're less likely to end up - // with tasks spaced exactly the minimum distance apart, which may - // get shifted again later. We have one fewer space to distribute than we - // have tasks. - $divisor = (double)(count($rows) - 1.0); - if ($divisor > 0) { - $available_distance = (($max - $min) / $divisor); - } else { - $available_distance = 0.0; - } - - foreach ($rows as $row) { - $subpriority = $min + ($offset * $available_distance); - - // If this is the task that we're spreading out relative to, keep track - // of where it is ending up so we can return the new subpriority. - $id = $row['id']; - if ($id == $task_id) { - $result = $subpriority; - } - - $sql[] = qsprintf( - $conn, - '(%d, %LQ, %f)', - $id, - $extra_columns, - $subpriority); - - $offset++; - } - - foreach (PhabricatorLiskDAO::chunkSQL($sql) as $chunk) { - queryfx( - $conn, - 'INSERT INTO %T (id, %LC, subpriority) VALUES %LQ - ON DUPLICATE KEY UPDATE subpriority = VALUES(subpriority)', - $task->getTableName(), - array_keys($extra_columns), - $chunk); - } - - return $result; - } - protected function validateAllTransactions( PhabricatorLiskDAO $object, array $xactions) { diff --git a/src/applications/maniphest/lipsum/PhabricatorManiphestTaskTestDataGenerator.php b/src/applications/maniphest/lipsum/PhabricatorManiphestTaskTestDataGenerator.php index 3fc1957b4f..ef0644ddd2 100644 --- a/src/applications/maniphest/lipsum/PhabricatorManiphestTaskTestDataGenerator.php +++ b/src/applications/maniphest/lipsum/PhabricatorManiphestTaskTestDataGenerator.php @@ -14,7 +14,6 @@ final class PhabricatorManiphestTaskTestDataGenerator $author = id(new PhabricatorUser()) ->loadOneWhere('phid = %s', $author_phid); $task = ManiphestTask::initializeNewTask($author) - ->setSubPriority($this->generateTaskSubPriority()) ->setTitle($this->generateTitle()); $content_source = $this->getLipsumContentSource(); @@ -106,10 +105,6 @@ final class PhabricatorManiphestTaskTestDataGenerator return $keyword; } - public function generateTaskSubPriority() { - return rand(2 << 16, 2 << 32); - } - public function generateTaskStatus() { $statuses = array_keys(ManiphestTaskStatus::getTaskStatusMap()); // Make sure 4/5th of all generated Tasks are open diff --git a/src/applications/maniphest/query/ManiphestTaskQuery.php b/src/applications/maniphest/query/ManiphestTaskQuery.php index 9fb4ecb68c..1033bfe333 100644 --- a/src/applications/maniphest/query/ManiphestTaskQuery.php +++ b/src/applications/maniphest/query/ManiphestTaskQuery.php @@ -435,13 +435,6 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery { $this->priorities); } - if ($this->subpriorities !== null) { - $where[] = qsprintf( - $conn, - 'task.subpriority IN (%Lf)', - $this->subpriorities); - } - if ($this->bridgedObjectPHIDs !== null) { $where[] = qsprintf( $conn, @@ -844,7 +837,7 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery { public function getBuiltinOrders() { $orders = array( 'priority' => array( - 'vector' => array('priority', 'subpriority', 'id'), + 'vector' => array('priority', 'id'), 'name' => pht('Priority'), 'aliases' => array(self::ORDER_PRIORITY), ), @@ -919,11 +912,6 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery { 'type' => 'string', 'reverse' => true, ), - 'subpriority' => array( - 'table' => 'task', - 'column' => 'subpriority', - 'type' => 'float', - ), 'updated' => array( 'table' => 'task', 'column' => 'dateModified', @@ -948,7 +936,6 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery { $map = array( 'id' => $task->getID(), 'priority' => $task->getPriority(), - 'subpriority' => $task->getSubpriority(), 'owner' => $task->getOwnerOrdering(), 'status' => $task->getStatus(), 'title' => $task->getTitle(), diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php index c19e75604c..d6e3ec6d6c 100644 --- a/src/applications/maniphest/storage/ManiphestTask.php +++ b/src/applications/maniphest/storage/ManiphestTask.php @@ -267,39 +267,6 @@ final class ManiphestTask extends ManiphestDAO return ManiphestTaskPriority::UNKNOWN_PRIORITY_KEYWORD; } - private function comparePriorityTo(ManiphestTask $other) { - $upri = $this->getPriority(); - $vpri = $other->getPriority(); - - if ($upri != $vpri) { - return ($upri - $vpri); - } - - $usub = $this->getSubpriority(); - $vsub = $other->getSubpriority(); - - if ($usub != $vsub) { - return ($usub - $vsub); - } - - $uid = $this->getID(); - $vid = $other->getID(); - - if ($uid != $vid) { - return ($uid - $vid); - } - - return 0; - } - - public function isLowerPriorityThan(ManiphestTask $other) { - return ($this->comparePriorityTo($other) < 0); - } - - public function isHigherPriorityThan(ManiphestTask $other) { - return ($this->comparePriorityTo($other) > 0); - } - public function getWorkboardProperties() { return array( 'status' => $this->getStatus(), @@ -540,7 +507,6 @@ final class ManiphestTask extends ManiphestDAO $priority_value = (int)$this->getPriority(); $priority_info = array( 'value' => $priority_value, - 'subpriority' => (double)$this->getSubpriority(), 'name' => ManiphestTaskPriority::getTaskPriorityName($priority_value), 'color' => ManiphestTaskPriority::getTaskPriorityColor($priority_value), ); diff --git a/src/applications/maniphest/xaction/ManiphestTaskSubpriorityTransaction.php b/src/applications/maniphest/xaction/ManiphestTaskSubpriorityTransaction.php index 49d227b7f1..c88ee8aa0c 100644 --- a/src/applications/maniphest/xaction/ManiphestTaskSubpriorityTransaction.php +++ b/src/applications/maniphest/xaction/ManiphestTaskSubpriorityTransaction.php @@ -6,16 +6,17 @@ final class ManiphestTaskSubpriorityTransaction const TRANSACTIONTYPE = 'subpriority'; public function generateOldValue($object) { - return $object->getSubpriority(); + return null; } public function applyInternalEffects($object, $value) { - $object->setSubpriority($value); + // This transaction is obsolete, but we're keeping the class around so it + // is hidden from timelines until we destroy the actual transaction data. + throw new PhutilMethodNotImplementedException(); } public function shouldHide() { return true; } - } From 7d849afd16c5e76081f6a8da2a52db374fe4c1c2 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 10 Mar 2019 19:53:25 -0700 Subject: [PATCH 152/245] Add a "WorkboardCardTemplate" class to make workboard client code easier to reason about Summary: Depends on D20266. Boards currently have several `whateverMap stuff>` properties, but we can just move these all down into a `CardTemplate`, similar to the recently introduced `HeaderTemplate`. The `CardTemplate` holds all the global information for a card, and then `Card` is specific for a particular copy in a column. Today, each `CardTemplate` has one `Card`, but a `CardTemplate` may have more than one card in the future (when we add subproject columns). Test Plan: Viewed workboards in different sort orders and dragged stuff around, grepped for all affected symbols. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20267 --- resources/celerity/map.php | 76 ++++++++++--------- .../js/application/projects/WorkboardBoard.js | 70 +++++++---------- .../js/application/projects/WorkboardCard.js | 14 +++- .../projects/WorkboardCardTemplate.js | 47 ++++++++++++ .../application/projects/WorkboardColumn.js | 10 ++- .../projects/behavior-project-boards.js | 11 ++- 6 files changed, 140 insertions(+), 88 deletions(-) create mode 100644 webroot/rsrc/js/application/projects/WorkboardCardTemplate.js diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 30d6587bac..825a5cebc9 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -408,13 +408,14 @@ return array( 'rsrc/js/application/phortune/phortune-credit-card-form.js' => 'd12d214f', 'rsrc/js/application/policy/behavior-policy-control.js' => '0eaa33a9', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '9347f172', - 'rsrc/js/application/projects/WorkboardBoard.js' => '902a1551', - 'rsrc/js/application/projects/WorkboardCard.js' => '887ef74f', - 'rsrc/js/application/projects/WorkboardColumn.js' => '01ea93b3', + 'rsrc/js/application/projects/WorkboardBoard.js' => '2181739b', + 'rsrc/js/application/projects/WorkboardCard.js' => 'bc92741f', + 'rsrc/js/application/projects/WorkboardCardTemplate.js' => 'b0b5f90a', + 'rsrc/js/application/projects/WorkboardColumn.js' => '6461f58b', 'rsrc/js/application/projects/WorkboardController.js' => '42c7a5a7', 'rsrc/js/application/projects/WorkboardHeader.js' => '6e75daea', 'rsrc/js/application/projects/WorkboardHeaderTemplate.js' => '2d641f7d', - 'rsrc/js/application/projects/behavior-project-boards.js' => 'e2730b90', + 'rsrc/js/application/projects/behavior-project-boards.js' => 'cca3f5f8', 'rsrc/js/application/projects/behavior-project-create.js' => '34c53422', 'rsrc/js/application/projects/behavior-reorder-columns.js' => '8ac32fd9', 'rsrc/js/application/releeph/releeph-preview-branch.js' => '75184d68', @@ -655,7 +656,7 @@ return array( 'javelin-behavior-phuix-example' => 'c2c500a7', 'javelin-behavior-policy-control' => '0eaa33a9', 'javelin-behavior-policy-rule-editor' => '9347f172', - 'javelin-behavior-project-boards' => 'e2730b90', + 'javelin-behavior-project-boards' => 'cca3f5f8', 'javelin-behavior-project-create' => '34c53422', 'javelin-behavior-quicksand-blacklist' => '5a6f6a06', 'javelin-behavior-read-only-warning' => 'b9109f8f', @@ -727,9 +728,10 @@ return array( 'javelin-view-renderer' => '9aae2b66', 'javelin-view-visitor' => '308f9fe4', 'javelin-websocket' => 'fdc13e4e', - 'javelin-workboard-board' => '902a1551', - 'javelin-workboard-card' => '887ef74f', - 'javelin-workboard-column' => '01ea93b3', + 'javelin-workboard-board' => '2181739b', + 'javelin-workboard-card' => 'bc92741f', + 'javelin-workboard-card-template' => 'b0b5f90a', + 'javelin-workboard-column' => '6461f58b', 'javelin-workboard-controller' => '42c7a5a7', 'javelin-workboard-header' => '6e75daea', 'javelin-workboard-header-template' => '2d641f7d', @@ -889,11 +891,6 @@ return array( 'javelin-uri', 'phabricator-notification', ), - '01ea93b3' => array( - 'javelin-install', - 'javelin-workboard-card', - 'javelin-workboard-header', - ), '022516b4' => array( 'javelin-install', 'javelin-util', @@ -1051,6 +1048,17 @@ return array( 'javelin-behavior', 'javelin-request', ), + '2181739b' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-util', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + 'javelin-workboard-column', + 'javelin-workboard-header-template', + 'javelin-workboard-card-template', + ), '225bbb98' => array( 'javelin-install', 'javelin-reactor', @@ -1419,6 +1427,11 @@ return array( '60cd9241' => array( 'javelin-behavior', ), + '6461f58b' => array( + 'javelin-install', + 'javelin-workboard-card', + 'javelin-workboard-header', + ), '65bb0011' => array( 'javelin-behavior', 'javelin-dom', @@ -1568,9 +1581,6 @@ return array( 'javelin-install', 'javelin-dom', ), - '887ef74f' => array( - 'javelin-install', - ), '89a1ae3a' => array( 'javelin-dom', 'javelin-util', @@ -1620,16 +1630,6 @@ return array( 'javelin-workflow', 'javelin-stratcom', ), - '902a1551' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-util', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - 'javelin-workboard-column', - 'javelin-workboard-header-template', - ), 91863989 => array( 'javelin-install', 'javelin-stratcom', @@ -1839,6 +1839,9 @@ return array( 'javelin-behavior-device', 'javelin-vector', ), + 'b0b5f90a' => array( + 'javelin-install', + ), 'b105a3a6' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1893,6 +1896,9 @@ return array( 'bc16cf33' => array( 'phui-workcard-view-css', ), + 'bc92741f' => array( + 'javelin-install', + ), 'bdce4d78' => array( 'javelin-install', 'javelin-util', @@ -1962,6 +1968,15 @@ return array( 'javelin-util', 'phabricator-keyboard-shortcut-manager', ), + 'cca3f5f8' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-util', + 'javelin-vector', + 'javelin-stratcom', + 'javelin-workflow', + 'javelin-workboard-controller', + ), 'cf32921f' => array( 'javelin-behavior', 'javelin-dom', @@ -2022,15 +2037,6 @@ return array( 'javelin-dom', 'javelin-history', ), - 'e2730b90' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-util', - 'javelin-vector', - 'javelin-stratcom', - 'javelin-workflow', - 'javelin-workboard-controller', - ), 'e562708c' => array( 'javelin-install', ), diff --git a/webroot/rsrc/js/application/projects/WorkboardBoard.js b/webroot/rsrc/js/application/projects/WorkboardBoard.js index c49e4c859f..a746461325 100644 --- a/webroot/rsrc/js/application/projects/WorkboardBoard.js +++ b/webroot/rsrc/js/application/projects/WorkboardBoard.js @@ -8,6 +8,7 @@ * phabricator-draggable-list * javelin-workboard-column * javelin-workboard-header-template + * javelin-workboard-card-template * @javelin */ @@ -18,10 +19,9 @@ JX.install('WorkboardBoard', { this._phid = phid; this._root = root; - this._templates = {}; - this._orderMaps = {}; - this._propertiesMap = {}; this._headers = {}; + this._cards = {}; + this._buildColumns(); }, @@ -35,10 +35,8 @@ JX.install('WorkboardBoard', { _phid: null, _root: null, _columns: null, - _templates: null, - _orderMaps: null, - _propertiesMap: null, _headers: null, + _cards: null, getRoot: function() { return this._root; @@ -56,9 +54,12 @@ JX.install('WorkboardBoard', { return this._phid; }, - setCardTemplate: function(phid, template) { - this._templates[phid] = template; - return this; + getCardTemplate: function(phid) { + if (!this._cards[phid]) { + this._cards[phid] = new JX.WorkboardCardTemplate(phid); + } + + return this._cards[phid]; }, getHeaderTemplate: function(header_key) { @@ -91,32 +92,10 @@ JX.install('WorkboardBoard', { return this.compareVectors(u.getVector(), v.getVector()); }, - setObjectProperties: function(phid, properties) { - this._propertiesMap[phid] = properties; - return this; - }, - - getObjectProperties: function(phid) { - return this._propertiesMap[phid]; - }, - - getCardTemplate: function(phid) { - return this._templates[phid]; - }, - getController: function() { return this._controller; }, - setOrderMap: function(phid, map) { - this._orderMaps[phid] = map; - return this; - }, - - getOrderVector: function(phid, key) { - return this._orderMaps[phid][key]; - }, - compareVectors: function(u_vec, v_vec) { for (var ii = 0; ii < u_vec.length; ii++) { if (u_vec[ii] > v_vec[ii]) { @@ -310,25 +289,29 @@ JX.install('WorkboardBoard', { var columns = this.getColumns(); var phid = response.objectPHID; + var card = this.getCardTemplate(phid); - if (!this._templates[phid]) { - for (var add_phid in response.columnMaps) { - var target_column = this.getColumn(add_phid); + for (var add_phid in response.columnMaps) { + var target_column = this.getColumn(add_phid); - if (!target_column) { - // If the column isn't visible, don't try to add a card to it. - continue; - } - - target_column.newCard(phid); + if (!target_column) { + // If the column isn't visible, don't try to add a card to it. + continue; } + + target_column.newCard(phid); } - this.setCardTemplate(phid, response.cardHTML); + card.setNodeHTMLTemplate(response.cardHTML); var order_maps = response.orderMaps; for (var order_phid in order_maps) { - this.setOrderMap(order_phid, order_maps[order_phid]); + var card_template = this.getCardTemplate(order_phid); + for (var order_key in order_maps[order_phid]) { + card_template.setSortVector( + order_key, + order_maps[order_phid][order_key]); + } } var column_maps = response.columnMaps; @@ -348,7 +331,8 @@ JX.install('WorkboardBoard', { var property_maps = response.propertyMaps; for (var property_phid in property_maps) { - this.setObjectProperties(property_phid, property_maps[property_phid]); + this.getCardTemplate(property_phid) + .setObjectProperties(property_maps[property_phid]); } for (var column_phid in columns) { diff --git a/webroot/rsrc/js/application/projects/WorkboardCard.js b/webroot/rsrc/js/application/projects/WorkboardCard.js index 9da3b7e69f..e0eab6b53b 100644 --- a/webroot/rsrc/js/application/projects/WorkboardCard.js +++ b/webroot/rsrc/js/application/projects/WorkboardCard.js @@ -29,7 +29,8 @@ JX.install('WorkboardCard', { }, getProperties: function() { - return this.getColumn().getBoard().getObjectProperties(this.getPHID()); + return this.getColumn().getBoard().getCardTemplate(this.getPHID()) + .getObjectProperties(); }, getPoints: function() { @@ -47,11 +48,16 @@ JX.install('WorkboardCard', { getNode: function() { if (!this._root) { var phid = this.getPHID(); - var template = this.getColumn().getBoard().getCardTemplate(phid); - this._root = JX.$H(template).getFragment().firstChild; - JX.Stratcom.getData(this._root).objectPHID = this.getPHID(); + var root = this.getColumn().getBoard() + .getCardTemplate(phid) + .newNode(); + + JX.Stratcom.getData(root).objectPHID = phid; + + this._root = root; } + return this._root; }, diff --git a/webroot/rsrc/js/application/projects/WorkboardCardTemplate.js b/webroot/rsrc/js/application/projects/WorkboardCardTemplate.js new file mode 100644 index 0000000000..70080b5364 --- /dev/null +++ b/webroot/rsrc/js/application/projects/WorkboardCardTemplate.js @@ -0,0 +1,47 @@ +/** + * @provides javelin-workboard-card-template + * @requires javelin-install + * @javelin + */ + +JX.install('WorkboardCardTemplate', { + + construct: function(phid) { + this._phid = phid; + this._vectors = {}; + + this.setObjectProperties({}); + }, + + properties: { + objectProperties: null + }, + + members: { + _phid: null, + _vectors: null, + + getPHID: function() { + return this._phid; + }, + + setNodeHTMLTemplate: function(html) { + this._html = html; + return this; + }, + + setSortVector: function(order, vector) { + this._vectors[order] = vector; + return this; + }, + + getSortVector: function(order) { + return this._vectors[order]; + }, + + newNode: function() { + return JX.$H(this._html).getFragment().firstChild; + } + } + +}); diff --git a/webroot/rsrc/js/application/projects/WorkboardColumn.js b/webroot/rsrc/js/application/projects/WorkboardColumn.js index e2ec18ae75..1c5d6f1a59 100644 --- a/webroot/rsrc/js/application/projects/WorkboardColumn.js +++ b/webroot/rsrc/js/application/projects/WorkboardColumn.js @@ -319,18 +319,21 @@ JX.install('WorkboardColumn', { }, _getOrderVector: function(phid, order) { + var board = this.getBoard(); + if (!this._orderVectors) { this._orderVectors = {}; } if (!this._orderVectors[order]) { - var board = this.getBoard(); var cards = this.getCards(); var vectors = {}; for (var k in cards) { var card_phid = cards[k].getPHID(); - var vector = board.getOrderVector(card_phid, order); + var vector = board.getCardTemplate(card_phid) + .getSortVector(order); + vectors[card_phid] = [].concat(vector); // Push a "card" type, so cards always sort after headers; headers @@ -352,7 +355,8 @@ JX.install('WorkboardColumn', { // In this case, we're comparing a card being dragged in from another // column to the cards already in this column. We're just going to // build a temporary vector for it. - var incoming_vector = this.getBoard().getOrderVector(phid, order); + var incoming_vector = board.getCardTemplate(phid) + .getSortVector(order); incoming_vector = [].concat(incoming_vector); // Add a "card" type to sort this after headers. diff --git a/webroot/rsrc/js/application/projects/behavior-project-boards.js b/webroot/rsrc/js/application/projects/behavior-project-boards.js index 6427e1de4c..da82c02040 100644 --- a/webroot/rsrc/js/application/projects/behavior-project-boards.js +++ b/webroot/rsrc/js/application/projects/behavior-project-boards.js @@ -83,7 +83,8 @@ JX.behavior('project-boards', function(config, statics) { var templates = config.templateMap; for (var k in templates) { - board.setCardTemplate(k, templates[k]); + board.getCardTemplate(k) + .setNodeHTMLTemplate(templates[k]); } var column_maps = config.columnMaps; @@ -97,12 +98,16 @@ JX.behavior('project-boards', function(config, statics) { var order_maps = config.orderMaps; for (var object_phid in order_maps) { - board.setOrderMap(object_phid, order_maps[object_phid]); + var order_card = board.getCardTemplate(object_phid); + for (var order_key in order_maps[object_phid]) { + order_card.setSortVector(order_key, order_maps[object_phid][order_key]); + } } var property_maps = config.propertyMaps; for (var property_phid in property_maps) { - board.setObjectProperties(property_phid, property_maps[property_phid]); + board.getCardTemplate(property_phid) + .setObjectProperties(property_maps[property_phid]); } var headers = config.headers; From 9a8019d4a98bd06868289039496d57080286e03e Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 10 Mar 2019 21:49:12 -0700 Subject: [PATCH 153/245] Modularize workboard column orders Summary: Depends on D20267. Depends on D20268. Ref T10333. Currently, we support "Natural" and "Priority" orders, but a lot of the particulars are pretty hard-coded, including some logic in `ManiphestTask`. Although it's not clear that we'll ever put other types of objects on workboards, it seems generally bad that you need to modify `ManiphestTask` to get a new ordering. Pull the ordering logic out into a `ProjectColumnOrder` hierarchy instead, and let each ordering define the things it needs to work (name, icon, what headers look like, how different objects are sorted, and how to apply an edit when you drop an object under a header). Then move the existing "Natural" and "Priority" orders into this new hierarchy. This has a minor bug where using the "Edit" workflow to change a card's priority on a priority-ordered board doesn't fully refresh card/header order since the response isn't ordering-aware. I'll fix that in an upcoming change. Test Plan: Grouped workboards by "Natural" and "Priority", dragged stuff around within and between columns, grepped for all touched symbols. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T10333 Differential Revision: https://secure.phabricator.com/D20269 --- resources/celerity/map.php | 102 +++++----- src/__phutil_library_map__.php | 8 + .../maniphest/storage/ManiphestTask.php | 16 -- .../PhabricatorProjectBoardViewController.php | 104 ++++------- .../PhabricatorProjectController.php | 17 +- .../PhabricatorProjectMoveController.php | 41 ++-- .../engine/PhabricatorBoardResponseEngine.php | 70 ++++++- .../order/PhabricatorProjectColumnHeader.php | 94 ++++++++++ .../PhabricatorProjectColumnNaturalOrder.php | 12 ++ .../order/PhabricatorProjectColumnOrder.php | 176 ++++++++++++++++++ .../PhabricatorProjectColumnPriorityOrder.php | 91 +++++++++ .../storage/PhabricatorProjectColumn.php | 7 - .../js/application/projects/WorkboardBoard.js | 48 +++-- .../js/application/projects/WorkboardCard.js | 7 +- .../projects/WorkboardCardTemplate.js | 17 ++ .../application/projects/WorkboardColumn.js | 16 +- .../application/projects/WorkboardHeader.js | 12 +- .../projects/WorkboardHeaderTemplate.js | 10 + .../projects/behavior-project-boards.js | 8 +- 19 files changed, 638 insertions(+), 218 deletions(-) create mode 100644 src/applications/project/order/PhabricatorProjectColumnHeader.php create mode 100644 src/applications/project/order/PhabricatorProjectColumnNaturalOrder.php create mode 100644 src/applications/project/order/PhabricatorProjectColumnOrder.php create mode 100644 src/applications/project/order/PhabricatorProjectColumnPriorityOrder.php diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 825a5cebc9..c96d03896c 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -408,14 +408,14 @@ return array( 'rsrc/js/application/phortune/phortune-credit-card-form.js' => 'd12d214f', 'rsrc/js/application/policy/behavior-policy-control.js' => '0eaa33a9', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '9347f172', - 'rsrc/js/application/projects/WorkboardBoard.js' => '2181739b', - 'rsrc/js/application/projects/WorkboardCard.js' => 'bc92741f', - 'rsrc/js/application/projects/WorkboardCardTemplate.js' => 'b0b5f90a', - 'rsrc/js/application/projects/WorkboardColumn.js' => '6461f58b', + 'rsrc/js/application/projects/WorkboardBoard.js' => 'fc1664ff', + 'rsrc/js/application/projects/WorkboardCard.js' => '0392a5d8', + 'rsrc/js/application/projects/WorkboardCardTemplate.js' => '2a61f8d4', + 'rsrc/js/application/projects/WorkboardColumn.js' => '533dd592', 'rsrc/js/application/projects/WorkboardController.js' => '42c7a5a7', - 'rsrc/js/application/projects/WorkboardHeader.js' => '6e75daea', - 'rsrc/js/application/projects/WorkboardHeaderTemplate.js' => '2d641f7d', - 'rsrc/js/application/projects/behavior-project-boards.js' => 'cca3f5f8', + 'rsrc/js/application/projects/WorkboardHeader.js' => '111bfd2d', + 'rsrc/js/application/projects/WorkboardHeaderTemplate.js' => 'b65351bd', + 'rsrc/js/application/projects/behavior-project-boards.js' => '285c337a', 'rsrc/js/application/projects/behavior-project-create.js' => '34c53422', 'rsrc/js/application/projects/behavior-reorder-columns.js' => '8ac32fd9', 'rsrc/js/application/releeph/releeph-preview-branch.js' => '75184d68', @@ -656,7 +656,7 @@ return array( 'javelin-behavior-phuix-example' => 'c2c500a7', 'javelin-behavior-policy-control' => '0eaa33a9', 'javelin-behavior-policy-rule-editor' => '9347f172', - 'javelin-behavior-project-boards' => 'cca3f5f8', + 'javelin-behavior-project-boards' => '285c337a', 'javelin-behavior-project-create' => '34c53422', 'javelin-behavior-quicksand-blacklist' => '5a6f6a06', 'javelin-behavior-read-only-warning' => 'b9109f8f', @@ -728,13 +728,13 @@ return array( 'javelin-view-renderer' => '9aae2b66', 'javelin-view-visitor' => '308f9fe4', 'javelin-websocket' => 'fdc13e4e', - 'javelin-workboard-board' => '2181739b', - 'javelin-workboard-card' => 'bc92741f', - 'javelin-workboard-card-template' => 'b0b5f90a', - 'javelin-workboard-column' => '6461f58b', + 'javelin-workboard-board' => 'fc1664ff', + 'javelin-workboard-card' => '0392a5d8', + 'javelin-workboard-card-template' => '2a61f8d4', + 'javelin-workboard-column' => '533dd592', 'javelin-workboard-controller' => '42c7a5a7', - 'javelin-workboard-header' => '6e75daea', - 'javelin-workboard-header-template' => '2d641f7d', + 'javelin-workboard-header' => '111bfd2d', + 'javelin-workboard-header-template' => 'b65351bd', 'javelin-workflow' => '958e9045', 'maniphest-report-css' => '3d53188b', 'maniphest-task-edit-css' => '272daa84', @@ -909,6 +909,9 @@ return array( 'javelin-uri', 'javelin-util', ), + '0392a5d8' => array( + 'javelin-install', + ), '04023d82' => array( 'javelin-install', 'phuix-button-view', @@ -993,6 +996,9 @@ return array( 'javelin-workflow', 'phuix-icon-view', ), + '111bfd2d' => array( + 'javelin-install', + ), '1325b731' => array( 'javelin-behavior', 'javelin-uri', @@ -1048,17 +1054,6 @@ return array( 'javelin-behavior', 'javelin-request', ), - '2181739b' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-util', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - 'javelin-workboard-column', - 'javelin-workboard-header-template', - 'javelin-workboard-card-template', - ), '225bbb98' => array( 'javelin-install', 'javelin-reactor', @@ -1110,6 +1105,15 @@ return array( 'javelin-json', 'phabricator-prefab', ), + '285c337a' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-util', + 'javelin-vector', + 'javelin-stratcom', + 'javelin-workflow', + 'javelin-workboard-controller', + ), '289bf236' => array( 'javelin-install', 'javelin-util', @@ -1119,6 +1123,9 @@ return array( 'javelin-stratcom', 'javelin-behavior', ), + '2a61f8d4' => array( + 'javelin-install', + ), '2a8b62d9' => array( 'multirow-row-manager', 'javelin-install', @@ -1142,9 +1149,6 @@ return array( 'javelin-dom', 'phabricator-keyboard-shortcut', ), - '2d641f7d' => array( - 'javelin-install', - ), '2e255291' => array( 'javelin-install', 'javelin-util', @@ -1343,6 +1347,11 @@ return array( 'javelin-dom', 'javelin-fx', ), + '533dd592' => array( + 'javelin-install', + 'javelin-workboard-card', + 'javelin-workboard-header', + ), '534f1757' => array( 'phui-oi-list-view-css', ), @@ -1427,11 +1436,6 @@ return array( '60cd9241' => array( 'javelin-behavior', ), - '6461f58b' => array( - 'javelin-install', - 'javelin-workboard-card', - 'javelin-workboard-header', - ), '65bb0011' => array( 'javelin-behavior', 'javelin-dom', @@ -1477,9 +1481,6 @@ return array( 'javelin-install', 'javelin-util', ), - '6e75daea' => array( - 'javelin-install', - ), 70245195 => array( 'javelin-behavior', 'javelin-stratcom', @@ -1839,9 +1840,6 @@ return array( 'javelin-behavior-device', 'javelin-vector', ), - 'b0b5f90a' => array( - 'javelin-install', - ), 'b105a3a6' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1875,6 +1873,9 @@ return array( 'javelin-stratcom', 'javelin-dom', ), + 'b65351bd' => array( + 'javelin-install', + ), 'b7b73831' => array( 'javelin-behavior', 'javelin-dom', @@ -1896,9 +1897,6 @@ return array( 'bc16cf33' => array( 'phui-workcard-view-css', ), - 'bc92741f' => array( - 'javelin-install', - ), 'bdce4d78' => array( 'javelin-install', 'javelin-util', @@ -1968,15 +1966,6 @@ return array( 'javelin-util', 'phabricator-keyboard-shortcut-manager', ), - 'cca3f5f8' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-util', - 'javelin-vector', - 'javelin-stratcom', - 'javelin-workflow', - 'javelin-workboard-controller', - ), 'cf32921f' => array( 'javelin-behavior', 'javelin-dom', @@ -2134,6 +2123,17 @@ return array( 'phabricator-keyboard-shortcut', 'conpherence-thread-manager', ), + 'fc1664ff' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-util', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + 'javelin-workboard-column', + 'javelin-workboard-header-template', + 'javelin-workboard-card-template', + ), 'fce5d170' => array( 'javelin-magical-init', 'javelin-util', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 713d3e5f29..bdb1fee462 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4052,10 +4052,14 @@ phutil_register_library_map(array( 'PhabricatorProjectColumn' => 'applications/project/storage/PhabricatorProjectColumn.php', 'PhabricatorProjectColumnDetailController' => 'applications/project/controller/PhabricatorProjectColumnDetailController.php', 'PhabricatorProjectColumnEditController' => 'applications/project/controller/PhabricatorProjectColumnEditController.php', + 'PhabricatorProjectColumnHeader' => 'applications/project/order/PhabricatorProjectColumnHeader.php', 'PhabricatorProjectColumnHideController' => 'applications/project/controller/PhabricatorProjectColumnHideController.php', + 'PhabricatorProjectColumnNaturalOrder' => 'applications/project/order/PhabricatorProjectColumnNaturalOrder.php', + 'PhabricatorProjectColumnOrder' => 'applications/project/order/PhabricatorProjectColumnOrder.php', 'PhabricatorProjectColumnPHIDType' => 'applications/project/phid/PhabricatorProjectColumnPHIDType.php', 'PhabricatorProjectColumnPosition' => 'applications/project/storage/PhabricatorProjectColumnPosition.php', 'PhabricatorProjectColumnPositionQuery' => 'applications/project/query/PhabricatorProjectColumnPositionQuery.php', + 'PhabricatorProjectColumnPriorityOrder' => 'applications/project/order/PhabricatorProjectColumnPriorityOrder.php', 'PhabricatorProjectColumnQuery' => 'applications/project/query/PhabricatorProjectColumnQuery.php', 'PhabricatorProjectColumnSearchEngine' => 'applications/project/query/PhabricatorProjectColumnSearchEngine.php', 'PhabricatorProjectColumnTransaction' => 'applications/project/storage/PhabricatorProjectColumnTransaction.php', @@ -10132,13 +10136,17 @@ phutil_register_library_map(array( ), 'PhabricatorProjectColumnDetailController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectColumnEditController' => 'PhabricatorProjectBoardController', + 'PhabricatorProjectColumnHeader' => 'Phobject', 'PhabricatorProjectColumnHideController' => 'PhabricatorProjectBoardController', + 'PhabricatorProjectColumnNaturalOrder' => 'PhabricatorProjectColumnOrder', + 'PhabricatorProjectColumnOrder' => 'Phobject', 'PhabricatorProjectColumnPHIDType' => 'PhabricatorPHIDType', 'PhabricatorProjectColumnPosition' => array( 'PhabricatorProjectDAO', 'PhabricatorPolicyInterface', ), 'PhabricatorProjectColumnPositionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorProjectColumnPriorityOrder' => 'PhabricatorProjectColumnOrder', 'PhabricatorProjectColumnQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorProjectColumnSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorProjectColumnTransaction' => 'PhabricatorApplicationTransaction', diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php index d6e3ec6d6c..d2700895ce 100644 --- a/src/applications/maniphest/storage/ManiphestTask.php +++ b/src/applications/maniphest/storage/ManiphestTask.php @@ -248,14 +248,6 @@ final class ManiphestTask extends ManiphestDAO return idx($this->properties, 'cover.thumbnailPHID'); } - public function getWorkboardOrderVectors() { - return array( - PhabricatorProjectColumn::ORDER_PRIORITY => array( - (int)-$this->getPriority(), - ), - ); - } - public function getPriorityKeyword() { $priority = $this->getPriority(); @@ -267,14 +259,6 @@ final class ManiphestTask extends ManiphestDAO return ManiphestTaskPriority::UNKNOWN_PRIORITY_KEYWORD; } - public function getWorkboardProperties() { - return array( - 'status' => $this->getStatus(), - 'points' => (double)$this->getPoints(), - 'priority' => $this->getPriority(), - ); - } - /* -( PhabricatorSubscribableInterface )----------------------------------- */ diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index 6df43addd9..43599fa369 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -614,49 +614,25 @@ final class PhabricatorProjectBoardViewController $board->addPanel($panel); } - // It's possible for tasks to have an invalid/unknown priority in the - // database. We still want to generate a header for these tasks so we - // don't break the workboard. - $priorities = - ManiphestTaskPriority::getTaskPriorityMap() + - mpull($all_tasks, null, 'getPriority'); - $priorities = array_keys($priorities); + $order_key = $this->sortKey; - $headers = array(); - foreach ($priorities as $priority) { - $header_key = sprintf('priority(%s)', $priority); + $ordering_map = PhabricatorProjectColumnOrder::getAllOrders(); + $ordering = id(clone $ordering_map[$order_key]) + ->setViewer($viewer); - $priority_name = ManiphestTaskPriority::getTaskPriorityName($priority); - $priority_color = ManiphestTaskPriority::getTaskPriorityColor($priority); - $priority_icon = ManiphestTaskPriority::getTaskPriorityIcon($priority); + $headers = $ordering->getHeadersForObjects($all_tasks); + $headers = mpull($headers, 'toDictionary'); - $icon_view = id(new PHUIIconView()) - ->setIcon("{$priority_icon} {$priority_color}"); - - $template = phutil_tag( - 'li', - array( - 'class' => 'workboard-group-header', - ), - array( - $icon_view, - $priority_name, - )); - - $headers[] = array( - 'order' => PhabricatorProjectColumn::ORDER_PRIORITY, - 'key' => $header_key, - 'template' => hsprintf('%s', $template), - 'vector' => array( - (int)-$priority, - PhabricatorProjectColumn::NODETYPE_HEADER, - ), - 'editProperties' => array( - PhabricatorProjectColumn::ORDER_PRIORITY => (int)$priority, - ), - ); + $vectors = $ordering->getSortVectorsForObjects($all_tasks); + $vector_map = array(); + foreach ($vectors as $task_phid => $vector) { + $vector_map[$task_phid][$order_key] = $vector; } + $header_keys = $ordering->getHeaderKeysForObjects($all_tasks); + + $properties = array(); + $behavior_config = array( 'moveURI' => $this->getApplicationURI('move/'.$project->getID().'/'), 'uploadURI' => '/file/dropupload/', @@ -667,10 +643,11 @@ final class PhabricatorProjectBoardViewController 'boardPHID' => $project->getPHID(), 'order' => $this->sortKey, 'headers' => $headers, + 'headerKeys' => $header_keys, 'templateMap' => $templates, 'columnMaps' => $column_maps, - 'orderMaps' => mpull($all_tasks, 'getWorkboardOrderVectors'), - 'propertyMaps' => mpull($all_tasks, 'getWorkboardProperties'), + 'orderMaps' => $vector_map, + 'propertyMaps' => $properties, 'boardID' => $board_id, 'projectPHID' => $project->getPHID(), @@ -680,7 +657,8 @@ final class PhabricatorProjectBoardViewController $sort_menu = $this->buildSortMenu( $viewer, $project, - $this->sortKey); + $this->sortKey, + $ordering_map); $filter_menu = $this->buildFilterMenu( $viewer, @@ -775,7 +753,7 @@ final class PhabricatorProjectBoardViewController return $default_sort; } - return PhabricatorProjectColumn::DEFAULT_ORDER; + return PhabricatorProjectColumnNaturalOrder::ORDERKEY; } private function getDefaultFilter(PhabricatorProject $project) { @@ -789,41 +767,37 @@ final class PhabricatorProjectBoardViewController } private function isValidSort($sort) { - switch ($sort) { - case PhabricatorProjectColumn::ORDER_NATURAL: - case PhabricatorProjectColumn::ORDER_PRIORITY: - return true; - } - - return false; + $map = PhabricatorProjectColumnOrder::getAllOrders(); + return isset($map[$sort]); } private function buildSortMenu( PhabricatorUser $viewer, PhabricatorProject $project, - $sort_key) { - - $sort_icon = id(new PHUIIconView()) - ->setIcon('fa-sort-amount-asc bluegrey'); - - $named = array( - PhabricatorProjectColumn::ORDER_NATURAL => pht('Natural'), - PhabricatorProjectColumn::ORDER_PRIORITY => pht('Sort by Priority'), - ); + $sort_key, + array $ordering_map) { $base_uri = $this->getURIWithState(); $items = array(); - foreach ($named as $key => $name) { - $is_selected = ($key == $sort_key); + foreach ($ordering_map as $key => $ordering) { + // TODO: It would be desirable to build a real "PHUIIconView" here, but + // the pathway for threading that through all the view classes ends up + // being fairly complex, since some callers read the icon out of other + // views. For now, just stick with a string. + $ordering_icon = $ordering->getMenuIconIcon(); + $ordering_name = $ordering->getDisplayName(); + + $is_selected = ($key === $sort_key); if ($is_selected) { - $active_order = $name; + $active_name = $ordering_name; + $active_icon = $ordering_icon; } $item = id(new PhabricatorActionView()) - ->setIcon('fa-sort-amount-asc') + ->setIcon($ordering_icon) ->setSelected($is_selected) - ->setName($name); + ->setName($ordering_name); $uri = $base_uri->alter('order', $key); $item->setHref($uri); @@ -856,8 +830,8 @@ final class PhabricatorProjectBoardViewController } $sort_button = id(new PHUIListItemView()) - ->setName($active_order) - ->setIcon('fa-sort-amount-asc') + ->setName($active_name) + ->setIcon($active_icon) ->setHref('#') ->addSigil('boards-dropdown-menu') ->setMetadata( diff --git a/src/applications/project/controller/PhabricatorProjectController.php b/src/applications/project/controller/PhabricatorProjectController.php index 7c344b0b8e..850dfa2268 100644 --- a/src/applications/project/controller/PhabricatorProjectController.php +++ b/src/applications/project/controller/PhabricatorProjectController.php @@ -149,7 +149,11 @@ abstract class PhabricatorProjectController extends PhabricatorController { return $this; } - protected function newCardResponse($board_phid, $object_phid) { + protected function newCardResponse( + $board_phid, + $object_phid, + PhabricatorProjectColumnOrder $ordering = null) { + $viewer = $this->getViewer(); $request = $this->getRequest(); @@ -158,12 +162,17 @@ abstract class PhabricatorProjectController extends PhabricatorController { $visible_phids = array(); } - return id(new PhabricatorBoardResponseEngine()) + $engine = id(new PhabricatorBoardResponseEngine()) ->setViewer($viewer) ->setBoardPHID($board_phid) ->setObjectPHID($object_phid) - ->setVisiblePHIDs($visible_phids) - ->buildResponse(); + ->setVisiblePHIDs($visible_phids); + + if ($ordering) { + $engine->setOrdering($ordering); + } + + return $engine->buildResponse(); } public function renderHashtags(array $tags) { diff --git a/src/applications/project/controller/PhabricatorProjectMoveController.php b/src/applications/project/controller/PhabricatorProjectMoveController.php index 7a771ea7e8..6d7902a733 100644 --- a/src/applications/project/controller/PhabricatorProjectMoveController.php +++ b/src/applications/project/controller/PhabricatorProjectMoveController.php @@ -13,7 +13,15 @@ final class PhabricatorProjectMoveController $object_phid = $request->getStr('objectPHID'); $after_phid = $request->getStr('afterPHID'); $before_phid = $request->getStr('beforePHID'); - $order = $request->getStr('order', PhabricatorProjectColumn::DEFAULT_ORDER); + + $order = $request->getStr('order'); + if (!strlen($order)) { + $order = PhabricatorProjectColumnNaturalOrder::ORDERKEY; + } + + $ordering = PhabricatorProjectColumnOrder::getOrderByKey($order); + $ordering = id(clone $ordering) + ->setViewer($viewer); $edit_header = null; $raw_header = $request->getStr('header'); @@ -88,9 +96,8 @@ final class PhabricatorProjectMoveController ) + $order_params, )); - $header_xactions = $this->newHeaderTransactions( + $header_xactions = $ordering->getColumnTransactions( $object, - $order, $edit_header); foreach ($header_xactions as $header_xaction) { $xactions[] = $header_xaction; @@ -104,33 +111,7 @@ final class PhabricatorProjectMoveController $editor->applyTransactions($object, $xactions); - return $this->newCardResponse($board_phid, $object_phid); - } - - private function newHeaderTransactions( - ManiphestTask $task, - $order, - array $header) { - - $xactions = array(); - - switch ($order) { - case PhabricatorProjectColumn::ORDER_PRIORITY: - $new_priority = idx($header, $order); - - if ($task->getPriority() !== $new_priority) { - $keyword_map = ManiphestTaskPriority::getTaskPriorityKeywordsMap(); - $keyword = head(idx($keyword_map, $new_priority)); - - $xactions[] = id(new ManiphestTransaction()) - ->setTransactionType( - ManiphestTaskPriorityTransaction::TRANSACTIONTYPE) - ->setNewValue($keyword); - } - break; - } - - return $xactions; + return $this->newCardResponse($board_phid, $object_phid, $ordering); } } diff --git a/src/applications/project/engine/PhabricatorBoardResponseEngine.php b/src/applications/project/engine/PhabricatorBoardResponseEngine.php index 969dfa3bc8..36c5e81150 100644 --- a/src/applications/project/engine/PhabricatorBoardResponseEngine.php +++ b/src/applications/project/engine/PhabricatorBoardResponseEngine.php @@ -6,6 +6,7 @@ final class PhabricatorBoardResponseEngine extends Phobject { private $boardPHID; private $objectPHID; private $visiblePHIDs; + private $ordering; public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; @@ -43,10 +44,20 @@ final class PhabricatorBoardResponseEngine extends Phobject { return $this->visiblePHIDs; } + public function setOrdering(PhabricatorProjectColumnOrder $ordering) { + $this->ordering = $ordering; + return $this; + } + + public function getOrdering() { + return $this->ordering; + } + public function buildResponse() { $viewer = $this->getViewer(); $object_phid = $this->getObjectPHID(); $board_phid = $this->getBoardPHID(); + $ordering = $this->getOrdering(); // Load all the other tasks that are visible in the affected columns and // perform layout for them. @@ -74,10 +85,17 @@ final class PhabricatorBoardResponseEngine extends Phobject { ->setViewer($viewer) ->withPHIDs($visible_phids) ->execute(); + $all_visible = mpull($all_visible, null, 'getPHID'); - $order_maps = array(); - foreach ($all_visible as $visible) { - $order_maps[$visible->getPHID()] = $visible->getWorkboardOrderVectors(); + if ($ordering) { + $vectors = $ordering->getSortVectorsForObjects($all_visible); + $header_keys = $ordering->getHeaderKeysForObjects($all_visible); + $headers = $ordering->getHeadersForObjects($all_visible); + $headers = mpull($headers, 'toDictionary'); + } else { + $vectors = array(); + $header_keys = array(); + $headers = array(); } $object = id(new ManiphestTaskQuery()) @@ -91,14 +109,50 @@ final class PhabricatorBoardResponseEngine extends Phobject { $template = $this->buildTemplate($object); + $cards = array(); + foreach ($all_visible as $card_phid => $object) { + $card = array( + 'vectors' => array(), + 'headers' => array(), + 'properties' => array(), + 'nodeHTMLTemplate' => null, + ); + + if ($ordering) { + $order_key = $ordering->getColumnOrderKey(); + + $vector = idx($vectors, $card_phid); + if ($vector !== null) { + $card['vectors'][$order_key] = $vector; + } + + $header = idx($header_keys, $card_phid); + if ($header !== null) { + $card['headers'][$order_key] = $header; + } + + $card['properties'] = array( + 'points' => (double)$object->getPoints(), + 'status' => $object->getStatus(), + ); + } + + if ($card_phid === $object_phid) { + $card['nodeHTMLTemplate'] = hsprintf('%s', $template); + } + + $card['vectors'] = (object)$card['vectors']; + $card['headers'] = (object)$card['headers']; + $card['properties'] = (object)$card['properties']; + + $cards[$card_phid] = $card; + } + $payload = array( 'objectPHID' => $object_phid, - 'cardHTML' => $template, 'columnMaps' => $natural, - 'orderMaps' => $order_maps, - 'propertyMaps' => array( - $object_phid => $object->getWorkboardProperties(), - ), + 'cards' => $cards, + 'headers' => $headers, ); return id(new AphrontAjaxResponse()) diff --git a/src/applications/project/order/PhabricatorProjectColumnHeader.php b/src/applications/project/order/PhabricatorProjectColumnHeader.php new file mode 100644 index 0000000000..432b78279b --- /dev/null +++ b/src/applications/project/order/PhabricatorProjectColumnHeader.php @@ -0,0 +1,94 @@ +orderKey = $order_key; + return $this; + } + + public function getOrderKey() { + return $this->orderKey; + } + + public function setHeaderKey($header_key) { + $this->headerKey = $header_key; + return $this; + } + + public function getHeaderKey() { + return $this->headerKey; + } + + public function setSortVector(array $sort_vector) { + $this->sortVector = $sort_vector; + return $this; + } + + public function getSortVector() { + return $this->sortVector; + } + + public function setName($name) { + $this->name = $name; + return $this; + } + + public function getName() { + return $this->name; + } + + public function setIcon(PHUIIconView$icon) { + $this->icon = $icon; + return $this; + } + + public function getIcon() { + return $this->icon; + } + + public function setEditProperties(array $edit_properties) { + $this->editProperties = $edit_properties; + return $this; + } + + public function getEditProperties() { + return $this->editProperties; + } + + public function toDictionary() { + return array( + 'order' => $this->getOrderKey(), + 'key' => $this->getHeaderKey(), + 'template' => hsprintf('%s', $this->newView()), + 'vector' => $this->getSortVector(), + 'editProperties' => $this->getEditProperties(), + ); + } + + private function newView() { + $icon_view = $this->getIcon(); + $name = $this->getName(); + + $template = phutil_tag( + 'li', + array( + 'class' => 'workboard-group-header', + ), + array( + $icon_view, + $name, + )); + + return $template; + } + +} diff --git a/src/applications/project/order/PhabricatorProjectColumnNaturalOrder.php b/src/applications/project/order/PhabricatorProjectColumnNaturalOrder.php new file mode 100644 index 0000000000..26f4d28601 --- /dev/null +++ b/src/applications/project/order/PhabricatorProjectColumnNaturalOrder.php @@ -0,0 +1,12 @@ +viewer = $viewer; + return $this; + } + + final public function getViewer() { + return $this->viewer; + } + + final public function getColumnOrderKey() { + return $this->getPhobjectClassConstant('ORDERKEY'); + } + + final public static function getAllOrders() { + return id(new PhutilClassMapQuery()) + ->setAncestorClass(__CLASS__) + ->setUniqueMethod('getColumnOrderKey') + ->execute(); + } + + final public static function getOrderByKey($key) { + $map = self::getAllOrders(); + + if (!isset($map[$key])) { + throw new Exception( + pht( + 'No column ordering exists with key "%s".', + $key)); + } + + return $map[$key]; + } + + final public function getColumnTransactions($object, array $header) { + $result = $this->newColumnTransactions($object, $header); + + if (!is_array($result) && !is_null($result)) { + throw new Exception( + pht( + 'Expected "newColumnTransactions()" on "%s" to return "null" or a '. + 'list of transactions, but got "%s".', + get_class($this), + phutil_describe_type($result))); + } + + if ($result === null) { + $result = array(); + } + + assert_instances_of($result, 'PhabricatorApplicationTransaction'); + + return $result; + } + + final public function getMenuIconIcon() { + return $this->newMenuIconIcon(); + } + + protected function newMenuIconIcon() { + return 'fa-sort-amount-asc'; + } + + abstract public function getDisplayName(); + + protected function newColumnTransactions($object, array $header) { + return array(); + } + + final public function getHeadersForObjects(array $objects) { + $headers = $this->newHeadersForObjects($objects); + + if (!is_array($headers)) { + throw new Exception( + pht( + 'Expected "newHeadersForObjects()" on "%s" to return a list '. + 'of headers, but got "%s".', + get_class($this), + phutil_describe_type($headers))); + } + + assert_instances_of($headers, 'PhabricatorProjectColumnHeader'); + + // Add a "0" to the end of each header. This makes them sort above object + // cards in the same group. + foreach ($headers as $header) { + $vector = $header->getSortVector(); + $vector[] = 0; + $header->setSortVector($vector); + } + + return $headers; + } + + protected function newHeadersForObjects(array $objects) { + return array(); + } + + final public function getSortVectorsForObjects(array $objects) { + $vectors = $this->newSortVectorsForObjects($objects); + + if (!is_array($vectors)) { + throw new Exception( + pht( + 'Expected "newSortVectorsForObjects()" on "%s" to return a '. + 'map of vectors, but got "%s".', + get_class($this), + phutil_describe_type($vectors))); + } + + assert_same_keys($objects, $vectors); + + return $vectors; + } + + protected function newSortVectorsForObjects(array $objects) { + $vectors = array(); + + foreach ($objects as $key => $object) { + $vectors[$key] = $this->newSortVectorForObject($object); + } + + return $vectors; + } + + protected function newSortVectorForObject($object) { + return array(); + } + + final public function getHeaderKeysForObjects(array $objects) { + $header_keys = $this->newHeaderKeysForObjects($objects); + + if (!is_array($header_keys)) { + throw new Exception( + pht( + 'Expected "newHeaderKeysForObject()" on "%s" to return a '. + 'map of header keys, but got "%s".', + get_class($this), + phutil_describe_type($header_keys))); + } + + assert_same_keys($objects, $header_keys); + + return $header_keys; + } + + protected function newHeaderKeysForObjects(array $objects) { + $header_keys = array(); + + foreach ($objects as $key => $object) { + $header_keys[$key] = $this->newHeaderKeyForObject($object); + } + + return $header_keys; + } + + protected function newHeaderKeyForObject($object) { + return null; + } + + final protected function newTransaction($object) { + return $object->getApplicationTransactionTemplate(); + } + + final protected function newHeader() { + return id(new PhabricatorProjectColumnHeader()) + ->setOrderKey($this->getColumnOrderKey()); + } + +} diff --git a/src/applications/project/order/PhabricatorProjectColumnPriorityOrder.php b/src/applications/project/order/PhabricatorProjectColumnPriorityOrder.php new file mode 100644 index 0000000000..e08e7c07ef --- /dev/null +++ b/src/applications/project/order/PhabricatorProjectColumnPriorityOrder.php @@ -0,0 +1,91 @@ +newHeaderKeyForPriority($object->getPriority()); + } + + private function newHeaderKeyForPriority($priority) { + return sprintf('priority(%d)', $priority); + } + + protected function newSortVectorForObject($object) { + return $this->newSortVectorForPriority($object->getPriority()); + } + + private function newSortVectorForPriority($priority) { + return array( + (int)-$priority, + ); + } + + protected function newHeadersForObjects(array $objects) { + $priorities = ManiphestTaskPriority::getTaskPriorityMap(); + + // It's possible for tasks to have an invalid/unknown priority in the + // database. We still want to generate a header for these tasks so we + // don't break the workboard. + $priorities = $priorities + mpull($objects, null, 'getPriority'); + + $priorities = array_keys($priorities); + + $headers = array(); + foreach ($priorities as $priority) { + $header_key = $this->newHeaderKeyForPriority($priority); + $sort_vector = $this->newSortVectorForPriority($priority); + + $priority_name = ManiphestTaskPriority::getTaskPriorityName($priority); + $priority_color = ManiphestTaskPriority::getTaskPriorityColor($priority); + $priority_icon = ManiphestTaskPriority::getTaskPriorityIcon($priority); + + $icon_view = id(new PHUIIconView()) + ->setIcon($priority_icon, $priority_color); + + $header = $this->newHeader() + ->setHeaderKey($header_key) + ->setSortVector($sort_vector) + ->setName($priority_name) + ->setIcon($icon_view) + ->setEditProperties( + array( + 'value' => (int)$priority, + )); + + $headers[] = $header; + } + + return $headers; + } + + protected function newColumnTransactions($object, array $header) { + $new_priority = idx($header, 'value'); + + if ($object->getPriority() === $new_priority) { + return null; + } + + $keyword_map = ManiphestTaskPriority::getTaskPriorityKeywordsMap(); + $keyword = head(idx($keyword_map, $new_priority)); + + $xactions = array(); + $xactions[] = $this->newTransaction($object) + ->setTransactionType(ManiphestTaskPriorityTransaction::TRANSACTIONTYPE) + ->setNewValue($keyword); + + return $xactions; + } + + +} diff --git a/src/applications/project/storage/PhabricatorProjectColumn.php b/src/applications/project/storage/PhabricatorProjectColumn.php index 03b9cbff70..febb2eb647 100644 --- a/src/applications/project/storage/PhabricatorProjectColumn.php +++ b/src/applications/project/storage/PhabricatorProjectColumn.php @@ -12,13 +12,6 @@ final class PhabricatorProjectColumn const STATUS_ACTIVE = 0; const STATUS_HIDDEN = 1; - const DEFAULT_ORDER = 'natural'; - const ORDER_NATURAL = 'natural'; - const ORDER_PRIORITY = 'priority'; - - const NODETYPE_HEADER = 0; - const NODETYPE_CARD = 1; - protected $name; protected $status; protected $projectPHID; diff --git a/webroot/rsrc/js/application/projects/WorkboardBoard.js b/webroot/rsrc/js/application/projects/WorkboardBoard.js index a746461325..cda48bde11 100644 --- a/webroot/rsrc/js/application/projects/WorkboardBoard.js +++ b/webroot/rsrc/js/application/projects/WorkboardBoard.js @@ -289,7 +289,6 @@ JX.install('WorkboardBoard', { var columns = this.getColumns(); var phid = response.objectPHID; - var card = this.getCardTemplate(phid); for (var add_phid in response.columnMaps) { var target_column = this.getColumn(add_phid); @@ -302,18 +301,6 @@ JX.install('WorkboardBoard', { target_column.newCard(phid); } - card.setNodeHTMLTemplate(response.cardHTML); - - var order_maps = response.orderMaps; - for (var order_phid in order_maps) { - var card_template = this.getCardTemplate(order_phid); - for (var order_key in order_maps[order_phid]) { - card_template.setSortVector( - order_key, - order_maps[order_phid][order_key]); - } - } - var column_maps = response.columnMaps; var natural_column; for (var natural_phid in column_maps) { @@ -329,10 +316,37 @@ JX.install('WorkboardBoard', { natural_column.setNaturalOrder(column_maps[natural_phid]); } - var property_maps = response.propertyMaps; - for (var property_phid in property_maps) { - this.getCardTemplate(property_phid) - .setObjectProperties(property_maps[property_phid]); + for (var card_phid in response.cards) { + var card_data = response.cards[card_phid]; + var card_template = this.getCardTemplate(card_phid); + + if (card_data.nodeHTMLTemplate) { + card_template.setNodeHTMLTemplate(card_data.nodeHTMLTemplate); + } + + var order; + for (order in card_data.vectors) { + card_template.setSortVector(order, card_data.vectors[order]); + } + + for (order in card_data.headers) { + card_template.setHeaderKey(order, card_data.headers[order]); + } + + for (var key in card_data.properties) { + card_template.setObjectProperty(key, card_data.properties[key]); + } + } + + var headers = response.headers; + for (var jj = 0; jj < headers.length; jj++) { + var header = headers[jj]; + + this.getHeaderTemplate(header.key) + .setOrder(header.order) + .setNodeHTMLTemplate(header.template) + .setVector(header.vector) + .setEditProperties(header.editProperties); } for (var column_phid in columns) { diff --git a/webroot/rsrc/js/application/projects/WorkboardCard.js b/webroot/rsrc/js/application/projects/WorkboardCard.js index e0eab6b53b..4a3be2a51d 100644 --- a/webroot/rsrc/js/application/projects/WorkboardCard.js +++ b/webroot/rsrc/js/application/projects/WorkboardCard.js @@ -29,7 +29,8 @@ JX.install('WorkboardCard', { }, getProperties: function() { - return this.getColumn().getBoard().getCardTemplate(this.getPHID()) + return this.getColumn().getBoard() + .getCardTemplate(this.getPHID()) .getObjectProperties(); }, @@ -41,10 +42,6 @@ JX.install('WorkboardCard', { return this.getProperties().status; }, - getPriority: function(order) { - return this.getProperties().priority; - }, - getNode: function() { if (!this._root) { var phid = this.getPHID(); diff --git a/webroot/rsrc/js/application/projects/WorkboardCardTemplate.js b/webroot/rsrc/js/application/projects/WorkboardCardTemplate.js index 70080b5364..58f3f9e97f 100644 --- a/webroot/rsrc/js/application/projects/WorkboardCardTemplate.js +++ b/webroot/rsrc/js/application/projects/WorkboardCardTemplate.js @@ -9,6 +9,7 @@ JX.install('WorkboardCardTemplate', { construct: function(phid) { this._phid = phid; this._vectors = {}; + this._headerKeys = {}; this.setObjectProperties({}); }, @@ -19,7 +20,9 @@ JX.install('WorkboardCardTemplate', { members: { _phid: null, + _html: null, _vectors: null, + _headerKeys: null, getPHID: function() { return this._phid; @@ -39,8 +42,22 @@ JX.install('WorkboardCardTemplate', { return this._vectors[order]; }, + setHeaderKey: function(order, key) { + this._headerKeys[order] = key; + return this; + }, + + getHeaderKey: function(order) { + return this._headerKeys[order]; + }, + newNode: function() { return JX.$H(this._html).getFragment().firstChild; + }, + + setObjectProperty: function(key, value) { + this.getObjectProperties()[key] = value; + return this; } } diff --git a/webroot/rsrc/js/application/projects/WorkboardColumn.js b/webroot/rsrc/js/application/projects/WorkboardColumn.js index 1c5d6f1a59..a560234a2a 100644 --- a/webroot/rsrc/js/application/projects/WorkboardColumn.js +++ b/webroot/rsrc/js/application/projects/WorkboardColumn.js @@ -189,6 +189,9 @@ JX.install('WorkboardColumn', { var board = this.getBoard(); var order = board.getOrder(); + // TODO: This should be modularized into "ProjectColumnOrder" classes, + // but is currently hard-coded. + switch (order) { case 'natural': return false; @@ -197,15 +200,6 @@ JX.install('WorkboardColumn', { return true; }, - _getCardHeaderKey: function(card, order) { - switch (order) { - case 'priority': - return 'priority(' + card.getPriority() + ')'; - default: - return null; - } - }, - redraw: function() { var board = this.getBoard(); var order = board.getOrder(); @@ -235,7 +229,9 @@ JX.install('WorkboardColumn', { // cards in a column. if (has_headers) { - var header_key = this._getCardHeaderKey(card, order); + var header_key = board.getCardTemplate(card.getPHID()) + .getHeaderKey(order); + if (!seen_headers[header_key]) { while (header_keys.length) { var next = header_keys.pop(); diff --git a/webroot/rsrc/js/application/projects/WorkboardHeader.js b/webroot/rsrc/js/application/projects/WorkboardHeader.js index 0a8f4d9681..a0cbfc13c7 100644 --- a/webroot/rsrc/js/application/projects/WorkboardHeader.js +++ b/webroot/rsrc/js/application/projects/WorkboardHeader.js @@ -27,12 +27,16 @@ JX.install('WorkboardHeader', { getNode: function() { if (!this._root) { var header_key = this.getHeaderKey(); - var board = this.getColumn().getBoard(); - var template = board.getHeaderTemplate(header_key).getTemplate(); - this._root = JX.$H(template).getFragment().firstChild; - JX.Stratcom.getData(this._root).headerKey = header_key; + var root = this.getColumn().getBoard() + .getHeaderTemplate(header_key) + .newNode(); + + JX.Stratcom.getData(root).headerKey = header_key; + + this._root = root; } + return this._root; }, diff --git a/webroot/rsrc/js/application/projects/WorkboardHeaderTemplate.js b/webroot/rsrc/js/application/projects/WorkboardHeaderTemplate.js index add37d9c25..8376359270 100644 --- a/webroot/rsrc/js/application/projects/WorkboardHeaderTemplate.js +++ b/webroot/rsrc/js/application/projects/WorkboardHeaderTemplate.js @@ -19,9 +19,19 @@ JX.install('WorkboardHeaderTemplate', { members: { _headerKey: null, + _html: null, getHeaderKey: function() { return this._headerKey; + }, + + setNodeHTMLTemplate: function(html) { + this._html = html; + return this; + }, + + newNode: function() { + return JX.$H(this._html).getFragment().firstChild; } } diff --git a/webroot/rsrc/js/application/projects/behavior-project-boards.js b/webroot/rsrc/js/application/projects/behavior-project-boards.js index da82c02040..51c067925f 100644 --- a/webroot/rsrc/js/application/projects/behavior-project-boards.js +++ b/webroot/rsrc/js/application/projects/behavior-project-boards.js @@ -116,11 +116,17 @@ JX.behavior('project-boards', function(config, statics) { board.getHeaderTemplate(header.key) .setOrder(header.order) - .setTemplate(header.template) + .setNodeHTMLTemplate(header.template) .setVector(header.vector) .setEditProperties(header.editProperties); } + var header_keys = config.headerKeys; + for (var header_phid in header_keys) { + board.getCardTemplate(header_phid) + .setHeaderKey(config.order, header_keys[header_phid]); + } + board.start(); }); From 2fdab434faf3c5f825985afa071b328a6b0b6bce Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 10 Mar 2019 21:49:18 -0700 Subject: [PATCH 154/245] Implement "Group by Owner" on Workboards Summary: Depends on D20269. Ref T10333. Now that orderings are modularized, this is fairly easy to implement. This isn't super fancy for now (e.g., no profile images) but I'll touch it up in a general polish followup. Test Plan: {F6264596} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T10333 Differential Revision: https://secure.phabricator.com/D20270 --- src/__phutil_library_map__.php | 2 + .../PhabricatorProjectColumnOwnerOrder.php | 163 ++++++++++++++++++ 2 files changed, 165 insertions(+) create mode 100644 src/applications/project/order/PhabricatorProjectColumnOwnerOrder.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index bdb1fee462..e79ae6ea68 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4056,6 +4056,7 @@ phutil_register_library_map(array( 'PhabricatorProjectColumnHideController' => 'applications/project/controller/PhabricatorProjectColumnHideController.php', 'PhabricatorProjectColumnNaturalOrder' => 'applications/project/order/PhabricatorProjectColumnNaturalOrder.php', 'PhabricatorProjectColumnOrder' => 'applications/project/order/PhabricatorProjectColumnOrder.php', + 'PhabricatorProjectColumnOwnerOrder' => 'applications/project/order/PhabricatorProjectColumnOwnerOrder.php', 'PhabricatorProjectColumnPHIDType' => 'applications/project/phid/PhabricatorProjectColumnPHIDType.php', 'PhabricatorProjectColumnPosition' => 'applications/project/storage/PhabricatorProjectColumnPosition.php', 'PhabricatorProjectColumnPositionQuery' => 'applications/project/query/PhabricatorProjectColumnPositionQuery.php', @@ -10140,6 +10141,7 @@ phutil_register_library_map(array( 'PhabricatorProjectColumnHideController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectColumnNaturalOrder' => 'PhabricatorProjectColumnOrder', 'PhabricatorProjectColumnOrder' => 'Phobject', + 'PhabricatorProjectColumnOwnerOrder' => 'PhabricatorProjectColumnOrder', 'PhabricatorProjectColumnPHIDType' => 'PhabricatorPHIDType', 'PhabricatorProjectColumnPosition' => array( 'PhabricatorProjectDAO', diff --git a/src/applications/project/order/PhabricatorProjectColumnOwnerOrder.php b/src/applications/project/order/PhabricatorProjectColumnOwnerOrder.php new file mode 100644 index 0000000000..c98ee61715 --- /dev/null +++ b/src/applications/project/order/PhabricatorProjectColumnOwnerOrder.php @@ -0,0 +1,163 @@ +newHeaderKeyForOwnerPHID($object->getOwnerPHID()); + } + + private function newHeaderKeyForOwnerPHID($owner_phid) { + if ($owner_phid === null) { + $owner_phid = ''; + } + + return sprintf('owner(%s)', $owner_phid); + } + + protected function newSortVectorsForObjects(array $objects) { + $owner_phids = mpull($objects, null, 'getOwnerPHID'); + $owner_phids = array_keys($owner_phids); + $owner_phids = array_filter($owner_phids); + + if ($owner_phids) { + $owner_users = id(new PhabricatorPeopleQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs($owner_phids) + ->execute(); + $owner_users = mpull($owner_users, null, 'getPHID'); + } else { + $owner_users = array(); + } + + $vectors = array(); + foreach ($objects as $vector_key => $object) { + $owner_phid = $object->getOwnerPHID(); + if (!$owner_phid) { + $vector = $this->newSortVectorForUnowned(); + } else { + $owner = idx($owner_users, $owner_phid); + if ($owner) { + $vector = $this->newSortVectorForOwner($owner); + } else { + $vector = $this->newSortVectorForOwnerPHID($owner_phid); + } + } + + $vectors[$vector_key] = $vector; + } + + return $vectors; + } + + private function newSortVectorForUnowned() { + // Always put unasssigned tasks at the top. + return array( + 0, + ); + } + + private function newSortVectorForOwner(PhabricatorUser $user) { + // Put assigned tasks with a valid owner after "Unassigned", but above + // assigned tasks with an invalid owner. Sort these tasks by the owner's + // username. + return array( + 1, + $user->getUsername(), + ); + } + + private function newSortVectorForOwnerPHID($owner_phid) { + // If we have tasks with a nonempty owner but can't load the associated + // "User" object, move them to the bottom. We can only sort these by the + // PHID. + return array( + 2, + $owner_phid, + ); + } + + protected function newHeadersForObjects(array $objects) { + $owner_phids = mpull($objects, null, 'getOwnerPHID'); + $owner_phids = array_keys($owner_phids); + $owner_phids = array_filter($owner_phids); + + if ($owner_phids) { + $owner_users = id(new PhabricatorPeopleQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs($owner_phids) + ->execute(); + $owner_users = mpull($owner_users, null, 'getPHID'); + } else { + $owner_users = array(); + } + + array_unshift($owner_phids, null); + + $headers = array(); + foreach ($owner_phids as $owner_phid) { + $header_key = $this->newHeaderKeyForOwnerPHID($owner_phid); + + if ($owner_phid === null) { + $owner = null; + $sort_vector = $this->newSortVectorForUnowned(); + $owner_name = pht('Not Assigned'); + } else { + $owner = idx($owner_users, $owner_phid); + if ($owner) { + $sort_vector = $this->newSortVectorForOwner($owner); + $owner_name = $owner->getUsername(); + } else { + $sort_vector = $this->newSortVectorForOwnerPHID($owner_phid); + $owner_name = pht('Unknown User ("%s")', $owner_phid); + } + } + + $owner_icon = 'fa-user'; + $owner_color = 'bluegrey'; + + $icon_view = id(new PHUIIconView()) + ->setIcon($owner_icon, $owner_color); + + $header = $this->newHeader() + ->setHeaderKey($header_key) + ->setSortVector($sort_vector) + ->setName($owner_name) + ->setIcon($icon_view) + ->setEditProperties( + array( + 'value' => $owner_phid, + )); + + $headers[] = $header; + } + + return $headers; + } + + protected function newColumnTransactions($object, array $header) { + $new_owner = idx($header, 'value'); + + if ($object->getOwnerPHID() === $new_owner) { + return null; + } + + $xactions = array(); + $xactions[] = $this->newTransaction($object) + ->setTransactionType(ManiphestTaskOwnerTransaction::TRANSACTIONTYPE) + ->setNewValue($new_owner); + + return $xactions; + } + +} From 21dd79b35af2c07b3f689092822c4237bd8a65f5 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 10 Mar 2019 22:09:23 -0700 Subject: [PATCH 155/245] When creating or editing a card on a sorted/grouped workboard, adjust headers appropriately Summary: Depends on D20270. Ref T10333. If you create a task with a new owner, or edit a task and change the priority/owner, we want to move it (and possibly create a new header) when the response comes back. Make sure the response includes the appropriate details about the object's header and position. Test Plan: - Grouped by Owner. - Created a new task with a new owner, saw the header appear. - Edited a task and changed it to give it a new owner, saw the header appear. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T10333 Differential Revision: https://secure.phabricator.com/D20271 --- .../controller/ManiphestController.php | 23 ---------- .../maniphest/editor/ManiphestEditEngine.php | 44 +++++++++---------- 2 files changed, 22 insertions(+), 45 deletions(-) diff --git a/src/applications/maniphest/controller/ManiphestController.php b/src/applications/maniphest/controller/ManiphestController.php index 51a243afac..970095009b 100644 --- a/src/applications/maniphest/controller/ManiphestController.php +++ b/src/applications/maniphest/controller/ManiphestController.php @@ -37,29 +37,6 @@ abstract class ManiphestController extends PhabricatorController { return $crumbs; } - public function renderSingleTask(ManiphestTask $task) { - $request = $this->getRequest(); - $user = $request->getUser(); - - $phids = $task->getProjectPHIDs(); - if ($task->getOwnerPHID()) { - $phids[] = $task->getOwnerPHID(); - } - - $handles = id(new PhabricatorHandleQuery()) - ->setViewer($user) - ->withPHIDs($phids) - ->execute(); - - $view = id(new ManiphestTaskListView()) - ->setUser($user) - ->setShowBatchControls(true) - ->setHandles($handles) - ->setTasks(array($task)); - - return $view; - } - final protected function newTaskGraphDropdownMenu( ManiphestTask $task, $has_parents, diff --git a/src/applications/maniphest/editor/ManiphestEditEngine.php b/src/applications/maniphest/editor/ManiphestEditEngine.php index dc9c56f840..76c2276df0 100644 --- a/src/applications/maniphest/editor/ManiphestEditEngine.php +++ b/src/applications/maniphest/editor/ManiphestEditEngine.php @@ -379,7 +379,10 @@ EODOCS $object, array $xactions) { - if ($request->isAjax()) { + $response_type = $request->getStr('responseType'); + $is_card = ($response_type === 'card'); + + if ($is_card) { // Reload the task to make sure we pick up the final task state. $viewer = $this->getViewer(); $task = id(new ManiphestTaskQuery()) @@ -389,29 +392,12 @@ EODOCS ->needProjectPHIDs(true) ->executeOne(); - switch ($request->getStr('responseType')) { - case 'card': - return $this->buildCardResponse($task); - default: - return $this->buildListResponse($task); - } - + return $this->buildCardResponse($task); } return parent::newEditResponse($request, $object, $xactions); } - private function buildListResponse(ManiphestTask $task) { - $controller = $this->getController(); - - $payload = array( - 'tasks' => $controller->renderSingleTask($task), - 'data' => array(), - ); - - return id(new AphrontAjaxResponse())->setContent($payload); - } - private function buildCardResponse(ManiphestTask $task) { $controller = $this->getController(); $request = $controller->getRequest(); @@ -435,12 +421,26 @@ EODOCS $board_phid = $column->getProjectPHID(); $object_phid = $task->getPHID(); - return id(new PhabricatorBoardResponseEngine()) + $order = $request->getStr('order'); + if ($order) { + $ordering = PhabricatorProjectColumnOrder::getOrderByKey($order); + $ordering = id(clone $ordering) + ->setViewer($viewer); + } else { + $ordering = null; + } + + $engine = id(new PhabricatorBoardResponseEngine()) ->setViewer($viewer) ->setBoardPHID($board_phid) ->setObjectPHID($object_phid) - ->setVisiblePHIDs($visible_phids) - ->buildResponse(); + ->setVisiblePHIDs($visible_phids); + + if ($ordering) { + $engine->setOrdering($ordering); + } + + return $engine->buildResponse(); } private function getColumnMap(ManiphestTask $task) { From 1bdf446a802602a156c9bfd2decdfd5cdaf7ee51 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 10 Mar 2019 22:22:59 -0700 Subject: [PATCH 156/245] Improve rendering of empty workboard columns in header views Summary: Depends on D20271. Ref T10333. When a column is empty but a board is grouped (by priority, owner, etc) render the headers properly. When a column has headers, don't apply the "empty" style even if it has no cards. This style just makes some empty space so you can drag-and-drop more easily, but headers do the same thing. Test Plan: {F6264611} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T10333 Differential Revision: https://secure.phabricator.com/D20272 --- resources/celerity/map.php | 14 ++++----- .../application/projects/WorkboardColumn.js | 31 +++++++++++++++++-- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index c96d03896c..efa2e80f17 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -411,7 +411,7 @@ return array( 'rsrc/js/application/projects/WorkboardBoard.js' => 'fc1664ff', 'rsrc/js/application/projects/WorkboardCard.js' => '0392a5d8', 'rsrc/js/application/projects/WorkboardCardTemplate.js' => '2a61f8d4', - 'rsrc/js/application/projects/WorkboardColumn.js' => '533dd592', + 'rsrc/js/application/projects/WorkboardColumn.js' => 'fd4c2069', 'rsrc/js/application/projects/WorkboardController.js' => '42c7a5a7', 'rsrc/js/application/projects/WorkboardHeader.js' => '111bfd2d', 'rsrc/js/application/projects/WorkboardHeaderTemplate.js' => 'b65351bd', @@ -731,7 +731,7 @@ return array( 'javelin-workboard-board' => 'fc1664ff', 'javelin-workboard-card' => '0392a5d8', 'javelin-workboard-card-template' => '2a61f8d4', - 'javelin-workboard-column' => '533dd592', + 'javelin-workboard-column' => 'fd4c2069', 'javelin-workboard-controller' => '42c7a5a7', 'javelin-workboard-header' => '111bfd2d', 'javelin-workboard-header-template' => 'b65351bd', @@ -1347,11 +1347,6 @@ return array( 'javelin-dom', 'javelin-fx', ), - '533dd592' => array( - 'javelin-install', - 'javelin-workboard-card', - 'javelin-workboard-header', - ), '534f1757' => array( 'phui-oi-list-view-css', ), @@ -2138,6 +2133,11 @@ return array( 'javelin-magical-init', 'javelin-util', ), + 'fd4c2069' => array( + 'javelin-install', + 'javelin-workboard-card', + 'javelin-workboard-header', + ), 'fdc13e4e' => array( 'javelin-install', ), diff --git a/webroot/rsrc/js/application/projects/WorkboardColumn.js b/webroot/rsrc/js/application/projects/WorkboardColumn.js index a560234a2a..271bed467c 100644 --- a/webroot/rsrc/js/application/projects/WorkboardColumn.js +++ b/webroot/rsrc/js/application/projects/WorkboardColumn.js @@ -220,6 +220,8 @@ JX.install('WorkboardColumn', { header_keys.reverse(); } + var header_key; + var next; for (ii = 0; ii < list.length; ii++) { var card = list[ii]; @@ -229,12 +231,12 @@ JX.install('WorkboardColumn', { // cards in a column. if (has_headers) { - var header_key = board.getCardTemplate(card.getPHID()) + header_key = board.getCardTemplate(card.getPHID()) .getHeaderKey(order); if (!seen_headers[header_key]) { while (header_keys.length) { - var next = header_keys.pop(); + next = header_keys.pop(); var header = this.getHeader(next); objects.push(header); @@ -250,6 +252,20 @@ JX.install('WorkboardColumn', { objects.push(card); } + // Add any leftover headers at the bottom of the column which don't have + // any cards in them. In particular, empty columns don't have any cards + // but should still have headers. + + while (header_keys.length) { + next = header_keys.pop(); + + if (seen_headers[next]) { + continue; + } + + objects.push(this.getHeader(next)); + } + this._objects = objects; var content = []; @@ -431,9 +447,18 @@ JX.install('WorkboardColumn', { JX.DOM.setContent(content_node, display_value); - var is_empty = !this.getCardPHIDs().length; + // Only put the "empty" style on the column (which just adds some empty + // space so it's easier to drop cards into an empty column) if it has no + // cards and no headers. + + var is_empty = + (!this.getCardPHIDs().length) && + (!this._hasColumnHeaders()); + var panel = JX.DOM.findAbove(this.getRoot(), 'div', 'workpanel'); JX.DOM.alterClass(panel, 'project-panel-empty', is_empty); + + JX.DOM.alterClass(panel, 'project-panel-over-limit', over_limit); var color_map = { From 74de153e59be0f14f0cd1e49a81660e7e72f82af Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 10 Mar 2019 22:31:35 -0700 Subject: [PATCH 157/245] Allow MFA task edits to go through on workboards Summary: Depends on D20272. Ref T13074. When a task requires MFA to edit, you currently get a fatal. Provide a cancel URI so the prompt works and the edit can go through. Test Plan: - Locked a task, dragged it on a workboard. - Before: fatal trying to build an MFA gate. - After: got MFA gated, answered prompt, action went through. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13074 Differential Revision: https://secure.phabricator.com/D20273 --- .../controller/PhabricatorProjectMoveController.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/applications/project/controller/PhabricatorProjectMoveController.php b/src/applications/project/controller/PhabricatorProjectMoveController.php index 6d7902a733..3cfd94894b 100644 --- a/src/applications/project/controller/PhabricatorProjectMoveController.php +++ b/src/applications/project/controller/PhabricatorProjectMoveController.php @@ -43,6 +43,13 @@ final class PhabricatorProjectMoveController return new Aphront404Response(); } + $cancel_uri = $this->getApplicationURI( + new PhutilURI( + urisprintf('board/%d/', $project->getID()), + array( + 'order' => $order, + ))); + $board_phid = $project->getPHID(); $object = id(new ManiphestTaskQuery()) @@ -107,7 +114,8 @@ final class PhabricatorProjectMoveController ->setActor($viewer) ->setContinueOnMissingFields(true) ->setContinueOnNoEffect(true) - ->setContentSourceFromRequest($request); + ->setContentSourceFromRequest($request) + ->setCancelURI($cancel_uri); $editor->applyTransactions($object, $xactions); From 804be81f5dbbe08059c2ebe1a6ee96b89c32968f Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 10 Mar 2019 22:56:54 -0700 Subject: [PATCH 158/245] Provide better UI feedback about cards that can't be dragged or edited Summary: Depends on D20273. Fixes T10722. Currently, we don't make it very clear when a card can't be edited. Long ago, some code made a weak attempt to do this (by hiding the "grip" on the card), but later UI changes hid the "grip" unconditionally so that mooted things. Instead: - Replace the edit pencil with a red lock. - Provide cursor hints for grabbable / not grabbable. - Don't let users pick up cards they can't edit. Test Plan: On a workboard with a mixture of editable and not-editable cards, hovered over the different cards and was able to figure out which ones I could drag or not drag pretty easily. Picked up cards I could pick up, wasn't able to drag cards I can't edit. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T10722 Differential Revision: https://secure.phabricator.com/D20274 --- resources/celerity/map.php | 52 +++++++++---------- .../project/view/ProjectBoardTaskCard.php | 30 +++++++---- .../css/phui/workboards/phui-workcard.css | 35 ++++++++++--- .../js/application/projects/WorkboardBoard.js | 2 +- webroot/rsrc/js/core/DraggableList.js | 2 + 5 files changed, 77 insertions(+), 44 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index efa2e80f17..33d8b09bc8 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -10,7 +10,7 @@ return array( 'conpherence.pkg.css' => '3c8a0668', 'conpherence.pkg.js' => '020aebcf', 'core.pkg.css' => '34ce1741', - 'core.pkg.js' => 'b96c872e', + 'core.pkg.js' => '200a0a61', 'differential.pkg.css' => '8d8360fb', 'differential.pkg.js' => '67e02996', 'diffusion.pkg.css' => '42c75c37', @@ -177,7 +177,7 @@ return array( 'rsrc/css/phui/phui-two-column-view.css' => '01e6991e', 'rsrc/css/phui/workboards/phui-workboard-color.css' => 'e86de308', 'rsrc/css/phui/workboards/phui-workboard.css' => '74fc9d98', - 'rsrc/css/phui/workboards/phui-workcard.css' => '8c536f90', + 'rsrc/css/phui/workboards/phui-workcard.css' => '9e9eb0df', 'rsrc/css/phui/workboards/phui-workpanel.css' => 'bc16cf33', 'rsrc/css/sprite-login.css' => '18b368a6', 'rsrc/css/sprite-tokens.css' => 'f1896dc5', @@ -408,7 +408,7 @@ return array( 'rsrc/js/application/phortune/phortune-credit-card-form.js' => 'd12d214f', 'rsrc/js/application/policy/behavior-policy-control.js' => '0eaa33a9', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '9347f172', - 'rsrc/js/application/projects/WorkboardBoard.js' => 'fc1664ff', + 'rsrc/js/application/projects/WorkboardBoard.js' => 'eb55f7e8', 'rsrc/js/application/projects/WorkboardCard.js' => '0392a5d8', 'rsrc/js/application/projects/WorkboardCardTemplate.js' => '2a61f8d4', 'rsrc/js/application/projects/WorkboardColumn.js' => 'fd4c2069', @@ -436,7 +436,7 @@ return array( 'rsrc/js/application/uiexample/notification-example.js' => '29819b75', 'rsrc/js/core/Busy.js' => '5202e831', 'rsrc/js/core/DragAndDropFileUpload.js' => '4370900d', - 'rsrc/js/core/DraggableList.js' => '8437c663', + 'rsrc/js/core/DraggableList.js' => '91f40fbf', 'rsrc/js/core/Favicon.js' => '7930776a', 'rsrc/js/core/FileUpload.js' => 'ab85e184', 'rsrc/js/core/Hovercard.js' => '074f0783', @@ -728,7 +728,7 @@ return array( 'javelin-view-renderer' => '9aae2b66', 'javelin-view-visitor' => '308f9fe4', 'javelin-websocket' => 'fdc13e4e', - 'javelin-workboard-board' => 'fc1664ff', + 'javelin-workboard-board' => 'eb55f7e8', 'javelin-workboard-card' => '0392a5d8', 'javelin-workboard-card-template' => '2a61f8d4', 'javelin-workboard-column' => 'fd4c2069', @@ -759,7 +759,7 @@ return array( 'phabricator-diff-changeset-list' => '04023d82', 'phabricator-diff-inline' => 'a4a14a94', 'phabricator-drag-and-drop-file-upload' => '4370900d', - 'phabricator-draggable-list' => '8437c663', + 'phabricator-draggable-list' => '91f40fbf', 'phabricator-fatal-config-template-css' => '20babf50', 'phabricator-favicon' => '7930776a', 'phabricator-feed-css' => 'd8b6e3f8', @@ -857,7 +857,7 @@ return array( 'phui-two-column-view-css' => '01e6991e', 'phui-workboard-color-css' => 'e86de308', 'phui-workboard-view-css' => '74fc9d98', - 'phui-workcard-view-css' => '8c536f90', + 'phui-workcard-view-css' => '9e9eb0df', 'phui-workpanel-view-css' => 'bc16cf33', 'phuix-action-list-view' => 'c68f183f', 'phuix-action-view' => 'aaa08f3b', @@ -1557,14 +1557,6 @@ return array( 'javelin-dom', 'javelin-vector', ), - '8437c663' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-stratcom', - 'javelin-util', - 'javelin-vector', - 'javelin-magical-init', - ), '87428eb2' => array( 'javelin-behavior', 'javelin-diffusion-locate-file-source', @@ -1643,6 +1635,14 @@ return array( 'javelin-workflow', 'javelin-stratcom', ), + '91f40fbf' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-stratcom', + 'javelin-util', + 'javelin-vector', + 'javelin-magical-init', + ), '92388bae' => array( 'javelin-behavior', 'javelin-scrollbar', @@ -2051,6 +2051,17 @@ return array( 'javelin-install', 'javelin-event', ), + 'eb55f7e8' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-util', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + 'javelin-workboard-column', + 'javelin-workboard-header-template', + 'javelin-workboard-card-template', + ), 'ec4e31c0' => array( 'phui-timeline-view-css', ), @@ -2118,17 +2129,6 @@ return array( 'phabricator-keyboard-shortcut', 'conpherence-thread-manager', ), - 'fc1664ff' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-util', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - 'javelin-workboard-column', - 'javelin-workboard-header-template', - 'javelin-workboard-card-template', - ), 'fce5d170' => array( 'javelin-magical-init', 'javelin-util', diff --git a/src/applications/project/view/ProjectBoardTaskCard.php b/src/applications/project/view/ProjectBoardTaskCard.php index 3a7016ca74..bb1c8ca8c5 100644 --- a/src/applications/project/view/ProjectBoardTaskCard.php +++ b/src/applications/project/view/ProjectBoardTaskCard.php @@ -82,20 +82,32 @@ final class ProjectBoardTaskCard extends Phobject { $card = id(new PHUIObjectItemView()) ->setObject($task) ->setUser($viewer) - ->setObjectName('T'.$task->getID()) + ->setObjectName($task->getMonogram()) ->setHeader($task->getTitle()) - ->setGrippable($can_edit) - ->setHref('/T'.$task->getID()) + ->setHref($task->getURI()) ->addSigil('project-card') ->setDisabled($task->isClosed()) - ->addAction( - id(new PHUIListItemView()) - ->setName(pht('Edit')) - ->setIcon('fa-pencil') - ->addSigil('edit-project-card') - ->setHref('/maniphest/task/edit/'.$task->getID().'/')) ->setBarColor($bar_color); + if ($can_edit) { + $card + ->addSigil('draggable-card') + ->addClass('draggable-card'); + $edit_icon = 'fa-pencil'; + } else { + $card + ->addClass('not-editable') + ->addClass('undraggable-card'); + $edit_icon = 'fa-lock red'; + } + + $card->addAction( + id(new PHUIListItemView()) + ->setName(pht('Edit')) + ->setIcon($edit_icon) + ->addSigil('edit-project-card') + ->setHref('/maniphest/task/edit/'.$task->getID().'/')); + if ($owner) { $card->addHandleIcon($owner, $owner->getName()); } diff --git a/webroot/rsrc/css/phui/workboards/phui-workcard.css b/webroot/rsrc/css/phui/workboards/phui-workcard.css index e137e962bc..3c6a798fc8 100644 --- a/webroot/rsrc/css/phui/workboards/phui-workcard.css +++ b/webroot/rsrc/css/phui/workboards/phui-workcard.css @@ -59,14 +59,6 @@ vertical-align: top; } -.phui-workcard.phui-oi-grippable .phui-oi-frame { - padding-left: 0; -} - -.phui-workcard .phui-oi-grip { - display: none; -} - .device-desktop .phui-workcard .phui-list-item-icon { display: none; } @@ -88,6 +80,33 @@ opacity: 1; } +.device-desktop .phui-workcard.draggable-card { + cursor: grab; +} + +.jx-dragging .phui-workcard.draggable-card { + cursor: grabbing; +} + +.device-desktop .phui-workcard.undraggable-card { + cursor: not-allowed; +} + +.device-desktop .phui-workcard.phui-oi.not-editable:hover { + background: {$sh-redbackground}; +} + +.device-desktop .phui-workcard.phui-oi.not-editable:hover + .phui-list-item-href { + border-radius: 3px; + background: {$red}; +} + +.device-desktop .phui-workcard.phui-oi.not-editable:hover + .phui-list-item-href .phui-icon-view { + color: #fff; +} + .phui-workcard.phui-oi:hover .phui-list-item-icon { display: block; } diff --git a/webroot/rsrc/js/application/projects/WorkboardBoard.js b/webroot/rsrc/js/application/projects/WorkboardBoard.js index cda48bde11..a05777e2f2 100644 --- a/webroot/rsrc/js/application/projects/WorkboardBoard.js +++ b/webroot/rsrc/js/application/projects/WorkboardBoard.js @@ -138,7 +138,7 @@ JX.install('WorkboardBoard', { for (var k in columns) { var column = columns[k]; - var list = new JX.DraggableList('project-card', column.getRoot()) + var list = new JX.DraggableList('draggable-card', column.getRoot()) .setOuterContainer(this.getRoot()) .setFindItemsHandler(JX.bind(column, column.getDropTargetNodes)) .setCanDragX(true) diff --git a/webroot/rsrc/js/core/DraggableList.js b/webroot/rsrc/js/core/DraggableList.js index 598856581f..1c7ca766f2 100644 --- a/webroot/rsrc/js/core/DraggableList.js +++ b/webroot/rsrc/js/core/DraggableList.js @@ -240,6 +240,7 @@ JX.install('DraggableList', { frame.appendChild(clone); document.body.appendChild(frame); + JX.DOM.alterClass(document.body, 'jx-dragging', true); this._dragging = drag; this._clone = clone; @@ -618,6 +619,7 @@ JX.install('DraggableList', { this._autoscroller = null; JX.DOM.remove(this._frame); + JX.DOM.alterClass(document.body, 'jx-dragging', false); this._frame = null; this._clone = null; From c020f027bbe6c17c1ba4a38150a4935b7fb9668d Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 11 Mar 2019 08:58:46 -0700 Subject: [PATCH 159/245] Add an "Sort by Creation Date" filter to workboards and modularize remaining order behaviors Summary: Depends on D20274. Ref T10578. This is en route to an ordering by points, it's just a simpler half-step on the way there. Allow columns to be sorted by creation date, so the newest tasks rise to the top. In this ordering you can never reposition cards, since editing a creation date by dragging makes no sense. This will be true of the "points" ordering too (although we could imagine doing something like prompting the user, some day). Test Plan: Viewed boards by "natural" (allows reordering both when dragging within and between columns), "priority" (reorder only within columns), and "creation date" (reorder never). Dragged cards around between and within columns, got apparently sensible behavior. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T10578 Differential Revision: https://secure.phabricator.com/D20275 --- resources/celerity/map.php | 90 ++++++++++--------- src/__phutil_library_map__.php | 2 + .../PhabricatorProjectBoardViewController.php | 4 + .../PhabricatorProjectColumnCreatedOrder.php | 31 +++++++ .../PhabricatorProjectColumnNaturalOrder.php | 8 ++ .../order/PhabricatorProjectColumnOrder.php | 10 +++ .../PhabricatorProjectColumnOwnerOrder.php | 8 ++ .../PhabricatorProjectColumnPriorityOrder.php | 8 ++ .../js/application/projects/WorkboardBoard.js | 31 ++++++- .../application/projects/WorkboardColumn.js | 10 +-- .../projects/WorkboardOrderTemplate.js | 27 ++++++ .../projects/behavior-project-boards.js | 16 +++- webroot/rsrc/js/core/DraggableList.js | 25 +++++- 13 files changed, 212 insertions(+), 58 deletions(-) create mode 100644 src/applications/project/order/PhabricatorProjectColumnCreatedOrder.php create mode 100644 webroot/rsrc/js/application/projects/WorkboardOrderTemplate.js diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 33d8b09bc8..014839c14a 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -10,7 +10,7 @@ return array( 'conpherence.pkg.css' => '3c8a0668', 'conpherence.pkg.js' => '020aebcf', 'core.pkg.css' => '34ce1741', - 'core.pkg.js' => '200a0a61', + 'core.pkg.js' => 'f9c2509b', 'differential.pkg.css' => '8d8360fb', 'differential.pkg.js' => '67e02996', 'diffusion.pkg.css' => '42c75c37', @@ -408,14 +408,15 @@ return array( 'rsrc/js/application/phortune/phortune-credit-card-form.js' => 'd12d214f', 'rsrc/js/application/policy/behavior-policy-control.js' => '0eaa33a9', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '9347f172', - 'rsrc/js/application/projects/WorkboardBoard.js' => 'eb55f7e8', + 'rsrc/js/application/projects/WorkboardBoard.js' => '9d59f098', 'rsrc/js/application/projects/WorkboardCard.js' => '0392a5d8', 'rsrc/js/application/projects/WorkboardCardTemplate.js' => '2a61f8d4', - 'rsrc/js/application/projects/WorkboardColumn.js' => 'fd4c2069', + 'rsrc/js/application/projects/WorkboardColumn.js' => 'ec5c5ce0', 'rsrc/js/application/projects/WorkboardController.js' => '42c7a5a7', 'rsrc/js/application/projects/WorkboardHeader.js' => '111bfd2d', 'rsrc/js/application/projects/WorkboardHeaderTemplate.js' => 'b65351bd', - 'rsrc/js/application/projects/behavior-project-boards.js' => '285c337a', + 'rsrc/js/application/projects/WorkboardOrderTemplate.js' => '03e8891f', + 'rsrc/js/application/projects/behavior-project-boards.js' => '412af9d4', 'rsrc/js/application/projects/behavior-project-create.js' => '34c53422', 'rsrc/js/application/projects/behavior-reorder-columns.js' => '8ac32fd9', 'rsrc/js/application/releeph/releeph-preview-branch.js' => '75184d68', @@ -436,7 +437,7 @@ return array( 'rsrc/js/application/uiexample/notification-example.js' => '29819b75', 'rsrc/js/core/Busy.js' => '5202e831', 'rsrc/js/core/DragAndDropFileUpload.js' => '4370900d', - 'rsrc/js/core/DraggableList.js' => '91f40fbf', + 'rsrc/js/core/DraggableList.js' => '8bc7d797', 'rsrc/js/core/Favicon.js' => '7930776a', 'rsrc/js/core/FileUpload.js' => 'ab85e184', 'rsrc/js/core/Hovercard.js' => '074f0783', @@ -656,7 +657,7 @@ return array( 'javelin-behavior-phuix-example' => 'c2c500a7', 'javelin-behavior-policy-control' => '0eaa33a9', 'javelin-behavior-policy-rule-editor' => '9347f172', - 'javelin-behavior-project-boards' => '285c337a', + 'javelin-behavior-project-boards' => '412af9d4', 'javelin-behavior-project-create' => '34c53422', 'javelin-behavior-quicksand-blacklist' => '5a6f6a06', 'javelin-behavior-read-only-warning' => 'b9109f8f', @@ -728,13 +729,14 @@ return array( 'javelin-view-renderer' => '9aae2b66', 'javelin-view-visitor' => '308f9fe4', 'javelin-websocket' => 'fdc13e4e', - 'javelin-workboard-board' => 'eb55f7e8', + 'javelin-workboard-board' => '9d59f098', 'javelin-workboard-card' => '0392a5d8', 'javelin-workboard-card-template' => '2a61f8d4', - 'javelin-workboard-column' => 'fd4c2069', + 'javelin-workboard-column' => 'ec5c5ce0', 'javelin-workboard-controller' => '42c7a5a7', 'javelin-workboard-header' => '111bfd2d', 'javelin-workboard-header-template' => 'b65351bd', + 'javelin-workboard-order-template' => '03e8891f', 'javelin-workflow' => '958e9045', 'maniphest-report-css' => '3d53188b', 'maniphest-task-edit-css' => '272daa84', @@ -759,7 +761,7 @@ return array( 'phabricator-diff-changeset-list' => '04023d82', 'phabricator-diff-inline' => 'a4a14a94', 'phabricator-drag-and-drop-file-upload' => '4370900d', - 'phabricator-draggable-list' => '91f40fbf', + 'phabricator-draggable-list' => '8bc7d797', 'phabricator-fatal-config-template-css' => '20babf50', 'phabricator-favicon' => '7930776a', 'phabricator-feed-css' => 'd8b6e3f8', @@ -912,6 +914,9 @@ return array( '0392a5d8' => array( 'javelin-install', ), + '03e8891f' => array( + 'javelin-install', + ), '04023d82' => array( 'javelin-install', 'phuix-button-view', @@ -1105,15 +1110,6 @@ return array( 'javelin-json', 'phabricator-prefab', ), - '285c337a' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-util', - 'javelin-vector', - 'javelin-stratcom', - 'javelin-workflow', - 'javelin-workboard-controller', - ), '289bf236' => array( 'javelin-install', 'javelin-util', @@ -1231,6 +1227,15 @@ return array( 'javelin-behavior', 'javelin-uri', ), + '412af9d4' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-util', + 'javelin-vector', + 'javelin-stratcom', + 'javelin-workflow', + 'javelin-workboard-controller', + ), '4234f572' => array( 'syntax-default-css', ), @@ -1588,6 +1593,14 @@ return array( 'javelin-dom', 'javelin-typeahead-normalizer', ), + '8bc7d797' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-stratcom', + 'javelin-util', + 'javelin-vector', + 'javelin-magical-init', + ), '8c2ed2bf' => array( 'javelin-behavior', 'javelin-dom', @@ -1635,14 +1648,6 @@ return array( 'javelin-workflow', 'javelin-stratcom', ), - '91f40fbf' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-stratcom', - 'javelin-util', - 'javelin-vector', - 'javelin-magical-init', - ), '92388bae' => array( 'javelin-behavior', 'javelin-scrollbar', @@ -1720,6 +1725,18 @@ return array( 'javelin-uri', 'phabricator-textareautils', ), + '9d59f098' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-util', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + 'javelin-workboard-column', + 'javelin-workboard-header-template', + 'javelin-workboard-card-template', + 'javelin-workboard-order-template', + ), '9f081f05' => array( 'javelin-behavior', 'javelin-dom', @@ -2051,20 +2068,14 @@ return array( 'javelin-install', 'javelin-event', ), - 'eb55f7e8' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-util', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - 'javelin-workboard-column', - 'javelin-workboard-header-template', - 'javelin-workboard-card-template', - ), 'ec4e31c0' => array( 'phui-timeline-view-css', ), + 'ec5c5ce0' => array( + 'javelin-install', + 'javelin-workboard-card', + 'javelin-workboard-header', + ), 'ee77366f' => array( 'aphront-dialog-view-css', ), @@ -2133,11 +2144,6 @@ return array( 'javelin-magical-init', 'javelin-util', ), - 'fd4c2069' => array( - 'javelin-install', - 'javelin-workboard-card', - 'javelin-workboard-header', - ), 'fdc13e4e' => array( 'javelin-install', ), diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index e79ae6ea68..9716e7180e 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4050,6 +4050,7 @@ phutil_register_library_map(array( 'PhabricatorProjectColorTransaction' => 'applications/project/xaction/PhabricatorProjectColorTransaction.php', 'PhabricatorProjectColorsConfigType' => 'applications/project/config/PhabricatorProjectColorsConfigType.php', 'PhabricatorProjectColumn' => 'applications/project/storage/PhabricatorProjectColumn.php', + 'PhabricatorProjectColumnCreatedOrder' => 'applications/project/order/PhabricatorProjectColumnCreatedOrder.php', 'PhabricatorProjectColumnDetailController' => 'applications/project/controller/PhabricatorProjectColumnDetailController.php', 'PhabricatorProjectColumnEditController' => 'applications/project/controller/PhabricatorProjectColumnEditController.php', 'PhabricatorProjectColumnHeader' => 'applications/project/order/PhabricatorProjectColumnHeader.php', @@ -10135,6 +10136,7 @@ phutil_register_library_map(array( 'PhabricatorExtendedPolicyInterface', 'PhabricatorConduitResultInterface', ), + 'PhabricatorProjectColumnCreatedOrder' => 'PhabricatorProjectColumnOrder', 'PhabricatorProjectColumnDetailController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectColumnEditController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectColumnHeader' => 'Phobject', diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index 43599fa369..8784847f37 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -631,6 +631,9 @@ final class PhabricatorProjectBoardViewController $header_keys = $ordering->getHeaderKeysForObjects($all_tasks); + $order_maps = array(); + $order_maps[] = $ordering->toDictionary(); + $properties = array(); $behavior_config = array( @@ -642,6 +645,7 @@ final class PhabricatorProjectBoardViewController 'boardPHID' => $project->getPHID(), 'order' => $this->sortKey, + 'orders' => $order_maps, 'headers' => $headers, 'headerKeys' => $header_keys, 'templateMap' => $templates, diff --git a/src/applications/project/order/PhabricatorProjectColumnCreatedOrder.php b/src/applications/project/order/PhabricatorProjectColumnCreatedOrder.php new file mode 100644 index 0000000000..945445aecf --- /dev/null +++ b/src/applications/project/order/PhabricatorProjectColumnCreatedOrder.php @@ -0,0 +1,31 @@ +getDateCreated(), + (int)-$object->getID(), + ); + } + +} diff --git a/src/applications/project/order/PhabricatorProjectColumnNaturalOrder.php b/src/applications/project/order/PhabricatorProjectColumnNaturalOrder.php index 26f4d28601..b21a554715 100644 --- a/src/applications/project/order/PhabricatorProjectColumnNaturalOrder.php +++ b/src/applications/project/order/PhabricatorProjectColumnNaturalOrder.php @@ -9,4 +9,12 @@ final class PhabricatorProjectColumnNaturalOrder return pht('Natural'); } + public function getHasHeaders() { + return false; + } + + public function getCanReorder() { + return true; + } + } diff --git a/src/applications/project/order/PhabricatorProjectColumnOrder.php b/src/applications/project/order/PhabricatorProjectColumnOrder.php index 815ab19f80..f3a1ed86ca 100644 --- a/src/applications/project/order/PhabricatorProjectColumnOrder.php +++ b/src/applications/project/order/PhabricatorProjectColumnOrder.php @@ -68,6 +68,8 @@ abstract class PhabricatorProjectColumnOrder } abstract public function getDisplayName(); + abstract public function getHasHeaders(); + abstract public function getCanReorder(); protected function newColumnTransactions($object, array $header) { return array(); @@ -173,4 +175,12 @@ abstract class PhabricatorProjectColumnOrder ->setOrderKey($this->getColumnOrderKey()); } + final public function toDictionary() { + return array( + 'orderKey' => $this->getColumnOrderKey(), + 'hasHeaders' => $this->getHasHeaders(), + 'canReorder' => $this->getCanReorder(), + ); + } + } diff --git a/src/applications/project/order/PhabricatorProjectColumnOwnerOrder.php b/src/applications/project/order/PhabricatorProjectColumnOwnerOrder.php index c98ee61715..a41b78a11c 100644 --- a/src/applications/project/order/PhabricatorProjectColumnOwnerOrder.php +++ b/src/applications/project/order/PhabricatorProjectColumnOwnerOrder.php @@ -13,6 +13,14 @@ final class PhabricatorProjectColumnOwnerOrder return 'fa-users'; } + public function getHasHeaders() { + return true; + } + + public function getCanReorder() { + return true; + } + protected function newHeaderKeyForObject($object) { return $this->newHeaderKeyForOwnerPHID($object->getOwnerPHID()); } diff --git a/src/applications/project/order/PhabricatorProjectColumnPriorityOrder.php b/src/applications/project/order/PhabricatorProjectColumnPriorityOrder.php index e08e7c07ef..64a5934e26 100644 --- a/src/applications/project/order/PhabricatorProjectColumnPriorityOrder.php +++ b/src/applications/project/order/PhabricatorProjectColumnPriorityOrder.php @@ -13,6 +13,14 @@ final class PhabricatorProjectColumnPriorityOrder return 'fa-sort-numeric-asc'; } + public function getHasHeaders() { + return true; + } + + public function getCanReorder() { + return true; + } + protected function newHeaderKeyForObject($object) { return $this->newHeaderKeyForPriority($object->getPriority()); } diff --git a/webroot/rsrc/js/application/projects/WorkboardBoard.js b/webroot/rsrc/js/application/projects/WorkboardBoard.js index a05777e2f2..fa10b2a180 100644 --- a/webroot/rsrc/js/application/projects/WorkboardBoard.js +++ b/webroot/rsrc/js/application/projects/WorkboardBoard.js @@ -9,6 +9,7 @@ * javelin-workboard-column * javelin-workboard-header-template * javelin-workboard-card-template + * javelin-workboard-order-template * @javelin */ @@ -21,6 +22,7 @@ JX.install('WorkboardBoard', { this._headers = {}; this._cards = {}; + this._orders = {}; this._buildColumns(); }, @@ -70,6 +72,14 @@ JX.install('WorkboardBoard', { return this._headers[header_key]; }, + getOrderTemplate: function(order_key) { + if (!this._orders[order_key]) { + this._orders[order_key] = new JX.WorkboardOrderTemplate(order_key); + } + + return this._orders[order_key]; + }, + getHeaderTemplatesForOrder: function(order) { var templates = []; @@ -134,6 +144,10 @@ JX.install('WorkboardBoard', { _setupDragHandlers: function() { var columns = this.getColumns(); + var order_template = this.getOrderTemplate(this.getOrder()); + var has_headers = order_template.getHasHeaders(); + var can_reorder = order_template.getCanReorder(); + var lists = []; for (var k in columns) { var column = columns[k]; @@ -149,8 +163,21 @@ JX.install('WorkboardBoard', { list.setGhostHandler( JX.bind(column, column.handleDragGhost, default_handler)); - if (this.getOrder() !== 'natural') { - list.setCompareHandler(JX.bind(column, column.compareHandler)); + // The "compare handler" locks cards into a specific position in the + // column. + list.setCompareHandler(JX.bind(column, column.compareHandler)); + + // If the view has group headers, we lock cards into the right position + // when moving them between columns, but not within a column. + if (has_headers) { + list.setCompareOnMove(true); + } + + // If we can't reorder cards, we always lock them into their current + // position. + if (!can_reorder) { + list.setCompareOnMove(true); + list.setCompareOnReorder(true); } list.listen('didDrop', JX.bind(this, this._onmovecard, list)); diff --git a/webroot/rsrc/js/application/projects/WorkboardColumn.js b/webroot/rsrc/js/application/projects/WorkboardColumn.js index 271bed467c..709c52016a 100644 --- a/webroot/rsrc/js/application/projects/WorkboardColumn.js +++ b/webroot/rsrc/js/application/projects/WorkboardColumn.js @@ -189,15 +189,7 @@ JX.install('WorkboardColumn', { var board = this.getBoard(); var order = board.getOrder(); - // TODO: This should be modularized into "ProjectColumnOrder" classes, - // but is currently hard-coded. - - switch (order) { - case 'natural': - return false; - } - - return true; + return board.getOrderTemplate(order).getHasHeaders(); }, redraw: function() { diff --git a/webroot/rsrc/js/application/projects/WorkboardOrderTemplate.js b/webroot/rsrc/js/application/projects/WorkboardOrderTemplate.js new file mode 100644 index 0000000000..083dc78b50 --- /dev/null +++ b/webroot/rsrc/js/application/projects/WorkboardOrderTemplate.js @@ -0,0 +1,27 @@ +/** + * @provides javelin-workboard-order-template + * @requires javelin-install + * @javelin + */ + +JX.install('WorkboardOrderTemplate', { + + construct: function(order) { + this._orderKey = order; + }, + + properties: { + hasHeaders: false, + canReorder: false + }, + + members: { + _orderKey: null, + + getOrderKey: function() { + return this._orderKey; + } + + } + +}); diff --git a/webroot/rsrc/js/application/projects/behavior-project-boards.js b/webroot/rsrc/js/application/projects/behavior-project-boards.js index 51c067925f..3aa43722c4 100644 --- a/webroot/rsrc/js/application/projects/behavior-project-boards.js +++ b/webroot/rsrc/js/application/projects/behavior-project-boards.js @@ -87,11 +87,12 @@ JX.behavior('project-boards', function(config, statics) { .setNodeHTMLTemplate(templates[k]); } + var ii; var column_maps = config.columnMaps; for (var column_phid in column_maps) { var column = board.getColumn(column_phid); var column_map = column_maps[column_phid]; - for (var ii = 0; ii < column_map.length; ii++) { + for (ii = 0; ii < column_map.length; ii++) { column.newCard(column_map[ii]); } } @@ -111,8 +112,8 @@ JX.behavior('project-boards', function(config, statics) { } var headers = config.headers; - for (var jj = 0; jj < headers.length; jj++) { - var header = headers[jj]; + for (ii = 0; ii < headers.length; ii++) { + var header = headers[ii]; board.getHeaderTemplate(header.key) .setOrder(header.order) @@ -121,6 +122,15 @@ JX.behavior('project-boards', function(config, statics) { .setEditProperties(header.editProperties); } + var orders = config.orders; + for (ii = 0; ii < orders.length; ii++) { + var order = orders[ii]; + + board.getOrderTemplate(order.orderKey) + .setHasHeaders(order.hasHeaders) + .setCanReorder(order.canReorder); + } + var header_keys = config.headerKeys; for (var header_phid in header_keys) { board.getCardTemplate(header_phid) diff --git a/webroot/rsrc/js/core/DraggableList.js b/webroot/rsrc/js/core/DraggableList.js index 1c7ca766f2..64f57503b8 100644 --- a/webroot/rsrc/js/core/DraggableList.js +++ b/webroot/rsrc/js/core/DraggableList.js @@ -43,7 +43,9 @@ JX.install('DraggableList', { isDropTargetHandler: null, canDragX: false, outerContainer: null, - hasInfiniteHeight: false + hasInfiniteHeight: false, + compareOnMove: false, + compareOnReorder: false }, members : { @@ -501,7 +503,26 @@ JX.install('DraggableList', { var cur_target = false; if (target_list) { - if (compare_handler && (target_list !== this)) { + // Determine if we're going to use the compare handler or not: the + // compare hander locks items into a specific place in the list. For + // example, on Workboards, some operations permit the user to drag + // items between lists, but not to reorder items within a list. + + var should_compare = false; + + var is_reorder = (target_list === this); + var is_move = (target_list !== this); + + if (compare_handler) { + if (is_reorder && this.getCompareOnReorder()) { + should_compare = true; + } + if (is_move && this.getCompareOnMove()) { + should_compare = true; + } + } + + if (should_compare) { cur_target = target_list._getOrderedTarget(this, this._dragging); } else { cur_target = target_list._getCurrentTarget(p); From 03b7aca019d18e8056e3c3d90c5c8bdd8496dd24 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 11 Mar 2019 09:38:55 -0700 Subject: [PATCH 160/245] Implement "Sort by Points" on workboards Summary: Depends on D20275. Fixes T10578. This is a static sorting (like "By Date Created") where you can't change point values by dragging. You can still drag cards between columns, or use the "Edit" icon to change point values. Test Plan: {F6265191} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T10578 Differential Revision: https://secure.phabricator.com/D20276 --- src/__phutil_library_map__.php | 2 + .../PhabricatorProjectBoardViewController.php | 16 +++++- .../PhabricatorProjectColumnCreatedOrder.php | 8 ++- .../PhabricatorProjectColumnNaturalOrder.php | 4 ++ .../order/PhabricatorProjectColumnOrder.php | 21 ++++++++ .../PhabricatorProjectColumnOwnerOrder.php | 4 ++ .../PhabricatorProjectColumnPointsOrder.php | 50 +++++++++++++++++++ .../PhabricatorProjectColumnPriorityOrder.php | 6 ++- 8 files changed, 106 insertions(+), 5 deletions(-) create mode 100644 src/applications/project/order/PhabricatorProjectColumnPointsOrder.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 9716e7180e..e3b6af7974 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4059,6 +4059,7 @@ phutil_register_library_map(array( 'PhabricatorProjectColumnOrder' => 'applications/project/order/PhabricatorProjectColumnOrder.php', 'PhabricatorProjectColumnOwnerOrder' => 'applications/project/order/PhabricatorProjectColumnOwnerOrder.php', 'PhabricatorProjectColumnPHIDType' => 'applications/project/phid/PhabricatorProjectColumnPHIDType.php', + 'PhabricatorProjectColumnPointsOrder' => 'applications/project/order/PhabricatorProjectColumnPointsOrder.php', 'PhabricatorProjectColumnPosition' => 'applications/project/storage/PhabricatorProjectColumnPosition.php', 'PhabricatorProjectColumnPositionQuery' => 'applications/project/query/PhabricatorProjectColumnPositionQuery.php', 'PhabricatorProjectColumnPriorityOrder' => 'applications/project/order/PhabricatorProjectColumnPriorityOrder.php', @@ -10145,6 +10146,7 @@ phutil_register_library_map(array( 'PhabricatorProjectColumnOrder' => 'Phobject', 'PhabricatorProjectColumnOwnerOrder' => 'PhabricatorProjectColumnOrder', 'PhabricatorProjectColumnPHIDType' => 'PhabricatorPHIDType', + 'PhabricatorProjectColumnPointsOrder' => 'PhabricatorProjectColumnOrder', 'PhabricatorProjectColumnPosition' => array( 'PhabricatorProjectDAO', 'PhabricatorPolicyInterface', diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index 8784847f37..cc95958bf7 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -616,7 +616,7 @@ final class PhabricatorProjectBoardViewController $order_key = $this->sortKey; - $ordering_map = PhabricatorProjectColumnOrder::getAllOrders(); + $ordering_map = PhabricatorProjectColumnOrder::getEnabledOrders(); $ordering = id(clone $ordering_map[$order_key]) ->setViewer($viewer); @@ -635,6 +635,12 @@ final class PhabricatorProjectBoardViewController $order_maps[] = $ordering->toDictionary(); $properties = array(); + foreach ($all_tasks as $task) { + $properties[$task->getPHID()] = array( + 'points' => (double)$task->getPoints(), + 'status' => $task->getStatus(), + ); + } $behavior_config = array( 'moveURI' => $this->getApplicationURI('move/'.$project->getID().'/'), @@ -771,7 +777,7 @@ final class PhabricatorProjectBoardViewController } private function isValidSort($sort) { - $map = PhabricatorProjectColumnOrder::getAllOrders(); + $map = PhabricatorProjectColumnOrder::getEnabledOrders(); return isset($map[$sort]); } @@ -820,6 +826,9 @@ final class PhabricatorProjectBoardViewController $project, PhabricatorPolicyCapability::CAN_EDIT); + $items[] = id(new PhabricatorActionView()) + ->setType(PhabricatorActionView::TYPE_DIVIDER); + $items[] = id(new PhabricatorActionView()) ->setIcon('fa-floppy-o') ->setName(pht('Save as Default')) @@ -918,6 +927,9 @@ final class PhabricatorProjectBoardViewController $project, PhabricatorPolicyCapability::CAN_EDIT); + $items[] = id(new PhabricatorActionView()) + ->setType(PhabricatorActionView::TYPE_DIVIDER); + $items[] = id(new PhabricatorActionView()) ->setIcon('fa-floppy-o') ->setName(pht('Save as Default')) diff --git a/src/applications/project/order/PhabricatorProjectColumnCreatedOrder.php b/src/applications/project/order/PhabricatorProjectColumnCreatedOrder.php index 945445aecf..9fd2145886 100644 --- a/src/applications/project/order/PhabricatorProjectColumnCreatedOrder.php +++ b/src/applications/project/order/PhabricatorProjectColumnCreatedOrder.php @@ -21,10 +21,14 @@ final class PhabricatorProjectColumnCreatedOrder return false; } + public function getMenuOrder() { + return 3000; + } + protected function newSortVectorForObject($object) { return array( - (int)-$object->getDateCreated(), - (int)-$object->getID(), + -(int)$object->getDateCreated(), + -(int)$object->getID(), ); } diff --git a/src/applications/project/order/PhabricatorProjectColumnNaturalOrder.php b/src/applications/project/order/PhabricatorProjectColumnNaturalOrder.php index b21a554715..be67d28bcc 100644 --- a/src/applications/project/order/PhabricatorProjectColumnNaturalOrder.php +++ b/src/applications/project/order/PhabricatorProjectColumnNaturalOrder.php @@ -17,4 +17,8 @@ final class PhabricatorProjectColumnNaturalOrder return true; } + public function getMenuOrder() { + return 0; + } + } diff --git a/src/applications/project/order/PhabricatorProjectColumnOrder.php b/src/applications/project/order/PhabricatorProjectColumnOrder.php index f3a1ed86ca..c2da400fb2 100644 --- a/src/applications/project/order/PhabricatorProjectColumnOrder.php +++ b/src/applications/project/order/PhabricatorProjectColumnOrder.php @@ -22,9 +22,22 @@ abstract class PhabricatorProjectColumnOrder return id(new PhutilClassMapQuery()) ->setAncestorClass(__CLASS__) ->setUniqueMethod('getColumnOrderKey') + ->setSortMethod('getMenuOrder') ->execute(); } + final public static function getEnabledOrders() { + $map = self::getAllOrders(); + + foreach ($map as $key => $order) { + if (!$order->isEnabled()) { + unset($map[$key]); + } + } + + return $map; + } + final public static function getOrderByKey($key) { $map = self::getAllOrders(); @@ -71,6 +84,14 @@ abstract class PhabricatorProjectColumnOrder abstract public function getHasHeaders(); abstract public function getCanReorder(); + public function getMenuOrder() { + return 9000; + } + + public function isEnabled() { + return true; + } + protected function newColumnTransactions($object, array $header) { return array(); } diff --git a/src/applications/project/order/PhabricatorProjectColumnOwnerOrder.php b/src/applications/project/order/PhabricatorProjectColumnOwnerOrder.php index a41b78a11c..97ae0f24d4 100644 --- a/src/applications/project/order/PhabricatorProjectColumnOwnerOrder.php +++ b/src/applications/project/order/PhabricatorProjectColumnOwnerOrder.php @@ -21,6 +21,10 @@ final class PhabricatorProjectColumnOwnerOrder return true; } + public function getMenuOrder() { + return 2000; + } + protected function newHeaderKeyForObject($object) { return $this->newHeaderKeyForOwnerPHID($object->getOwnerPHID()); } diff --git a/src/applications/project/order/PhabricatorProjectColumnPointsOrder.php b/src/applications/project/order/PhabricatorProjectColumnPointsOrder.php new file mode 100644 index 0000000000..ad75f6135b --- /dev/null +++ b/src/applications/project/order/PhabricatorProjectColumnPointsOrder.php @@ -0,0 +1,50 @@ +getPoints(); + + // Put cards with no points on top. + $has_points = ($points !== null); + if (!$has_points) { + $overall_order = 0; + } else { + $overall_order = 1; + } + + return array( + $overall_order, + -(double)$points, + -(int)$object->getID(), + ); + } + +} diff --git a/src/applications/project/order/PhabricatorProjectColumnPriorityOrder.php b/src/applications/project/order/PhabricatorProjectColumnPriorityOrder.php index 64a5934e26..1a73145148 100644 --- a/src/applications/project/order/PhabricatorProjectColumnPriorityOrder.php +++ b/src/applications/project/order/PhabricatorProjectColumnPriorityOrder.php @@ -21,6 +21,10 @@ final class PhabricatorProjectColumnPriorityOrder return true; } + public function getMenuOrder() { + return 1000; + } + protected function newHeaderKeyForObject($object) { return $this->newHeaderKeyForPriority($object->getPriority()); } @@ -35,7 +39,7 @@ final class PhabricatorProjectColumnPriorityOrder private function newSortVectorForPriority($priority) { return array( - (int)-$priority, + -(int)$priority, ); } From a400d82932a6c730df9a97f1e0b69a450f021c19 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 11 Mar 2019 10:43:29 -0700 Subject: [PATCH 161/245] Add "Group by Status" to Workboards Summary: Depends on D20276. Ref T10333. This one is a little bit rough/experimental, and I'm sort of curious what feedback we get about it. Weird stuff: - All statuses are always shown, even if the filter prevents tasks in that status from appearing (which is the default, since views are "Open Tasks" by default). - Pro: you can close tasks by dragging them to a closed status. - Con: lots of empty groups. - The "Duplicate" status is shown. - Pro: Shows closed duplicate tasks. - Con: Dragging tasks to "Duplicate" works, but is silly. - Since boards show "open tasks" by default, dragging stuff to a closed status and then reloading the board causes it to vanish. This is kind of how everything works, but more obvious/defaulted on "Status". These issues might overwhelm its usefulness, but there isn't much cost to nuking it in the future if feedback is mostly negative/confused. Test Plan: Grouped a workboard by status, dragged stuff around. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T10333 Differential Revision: https://secure.phabricator.com/D20277 --- src/__phutil_library_map__.php | 2 + .../PhabricatorProjectColumnStatusOrder.php | 106 ++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 src/applications/project/order/PhabricatorProjectColumnStatusOrder.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index e3b6af7974..8f776a2948 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4065,6 +4065,7 @@ phutil_register_library_map(array( 'PhabricatorProjectColumnPriorityOrder' => 'applications/project/order/PhabricatorProjectColumnPriorityOrder.php', 'PhabricatorProjectColumnQuery' => 'applications/project/query/PhabricatorProjectColumnQuery.php', 'PhabricatorProjectColumnSearchEngine' => 'applications/project/query/PhabricatorProjectColumnSearchEngine.php', + 'PhabricatorProjectColumnStatusOrder' => 'applications/project/order/PhabricatorProjectColumnStatusOrder.php', 'PhabricatorProjectColumnTransaction' => 'applications/project/storage/PhabricatorProjectColumnTransaction.php', 'PhabricatorProjectColumnTransactionEditor' => 'applications/project/editor/PhabricatorProjectColumnTransactionEditor.php', 'PhabricatorProjectColumnTransactionQuery' => 'applications/project/query/PhabricatorProjectColumnTransactionQuery.php', @@ -10155,6 +10156,7 @@ phutil_register_library_map(array( 'PhabricatorProjectColumnPriorityOrder' => 'PhabricatorProjectColumnOrder', 'PhabricatorProjectColumnQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorProjectColumnSearchEngine' => 'PhabricatorApplicationSearchEngine', + 'PhabricatorProjectColumnStatusOrder' => 'PhabricatorProjectColumnOrder', 'PhabricatorProjectColumnTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorProjectColumnTransactionEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorProjectColumnTransactionQuery' => 'PhabricatorApplicationTransactionQuery', diff --git a/src/applications/project/order/PhabricatorProjectColumnStatusOrder.php b/src/applications/project/order/PhabricatorProjectColumnStatusOrder.php new file mode 100644 index 0000000000..e9570bea05 --- /dev/null +++ b/src/applications/project/order/PhabricatorProjectColumnStatusOrder.php @@ -0,0 +1,106 @@ +newHeaderKeyForStatus($object->getStatus()); + } + + private function newHeaderKeyForStatus($status) { + return sprintf('status(%s)', $status); + } + + protected function newSortVectorsForObjects(array $objects) { + $status_sequence = $this->newStatusSequence(); + + $vectors = array(); + foreach ($objects as $object_key => $object) { + $vectors[$object_key] = array( + (int)idx($status_sequence, $object->getStatus(), 0), + ); + } + + return $vectors; + } + + private function newStatusSequence() { + $statuses = ManiphestTaskStatus::getTaskStatusMap(); + return array_combine( + array_keys($statuses), + range(1, count($statuses))); + } + + protected function newHeadersForObjects(array $objects) { + $headers = array(); + + $statuses = ManiphestTaskStatus::getTaskStatusMap(); + $sequence = $this->newStatusSequence(); + + foreach ($statuses as $status_key => $status_name) { + $header_key = $this->newHeaderKeyForStatus($status_key); + + $sort_vector = array( + (int)idx($sequence, $status_key, 0), + ); + + $status_icon = ManiphestTaskStatus::getStatusIcon($status_key); + $status_color = ManiphestTaskStatus::getStatusColor($status_key); + + $icon_view = id(new PHUIIconView()) + ->setIcon($status_icon, $status_color); + + $header = $this->newHeader() + ->setHeaderKey($header_key) + ->setSortVector($sort_vector) + ->setName($status_name) + ->setIcon($icon_view) + ->setEditProperties( + array( + 'value' => $status_key, + )); + + $headers[] = $header; + } + + return $headers; + } + + protected function newColumnTransactions($object, array $header) { + $new_status = idx($header, 'value'); + + if ($object->getStatus() === $new_status) { + return null; + } + + $xactions = array(); + $xactions[] = $this->newTransaction($object) + ->setTransactionType(ManiphestTaskStatusTransaction::TRANSACTIONTYPE) + ->setNewValue($new_status); + + return $xactions; + } + +} From 8d74492875094abba369ea6de4e61949bf04971f Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 12 Mar 2019 13:05:16 -0700 Subject: [PATCH 162/245] Make workboard sort-order inversions more clear by using "-1 * ..." instead of "(int)-(int)" Summary: Depends on D20279. See D20269. Agreed that explicit `-1` is probably more clear. Test Plan: Viewed boards in each sort/group order. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20281 --- .../project/order/PhabricatorProjectColumnCreatedOrder.php | 4 ++-- .../project/order/PhabricatorProjectColumnPointsOrder.php | 4 ++-- .../project/order/PhabricatorProjectColumnPriorityOrder.php | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/applications/project/order/PhabricatorProjectColumnCreatedOrder.php b/src/applications/project/order/PhabricatorProjectColumnCreatedOrder.php index 9fd2145886..5652a96731 100644 --- a/src/applications/project/order/PhabricatorProjectColumnCreatedOrder.php +++ b/src/applications/project/order/PhabricatorProjectColumnCreatedOrder.php @@ -27,8 +27,8 @@ final class PhabricatorProjectColumnCreatedOrder protected function newSortVectorForObject($object) { return array( - -(int)$object->getDateCreated(), - -(int)$object->getID(), + -1 * (int)$object->getDateCreated(), + -1 * (int)$object->getID(), ); } diff --git a/src/applications/project/order/PhabricatorProjectColumnPointsOrder.php b/src/applications/project/order/PhabricatorProjectColumnPointsOrder.php index ad75f6135b..3cf758cbc8 100644 --- a/src/applications/project/order/PhabricatorProjectColumnPointsOrder.php +++ b/src/applications/project/order/PhabricatorProjectColumnPointsOrder.php @@ -42,8 +42,8 @@ final class PhabricatorProjectColumnPointsOrder return array( $overall_order, - -(double)$points, - -(int)$object->getID(), + -1.0 * (double)$points, + -1 * (int)$object->getID(), ); } diff --git a/src/applications/project/order/PhabricatorProjectColumnPriorityOrder.php b/src/applications/project/order/PhabricatorProjectColumnPriorityOrder.php index 1a73145148..10fcafad76 100644 --- a/src/applications/project/order/PhabricatorProjectColumnPriorityOrder.php +++ b/src/applications/project/order/PhabricatorProjectColumnPriorityOrder.php @@ -39,7 +39,7 @@ final class PhabricatorProjectColumnPriorityOrder private function newSortVectorForPriority($priority) { return array( - -(int)$priority, + -1 * (int)$priority, ); } From a6e17fb702d1de3b6a38ea951bd711f72232f0c2 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 11 Mar 2019 16:01:54 -0700 Subject: [PATCH 163/245] Improve workboard "Owner" grouping, add "Author" grouping and "Title" sort Summary: Depends on D20277. Ref T10333. - Put profile icons on "Group by Owner". - Add a similar "Group by Author". Probably not terribly useful, but cheap to implement now. - Add "Sort by Title". Very likely not terribly useful, but cheap to implement and sort of flexible? Test Plan: {F6265396} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T10333 Differential Revision: https://secure.phabricator.com/D20278 --- resources/celerity/map.php | 10 +- src/__phutil_library_map__.php | 4 + .../PhabricatorProjectColumnAuthorOrder.php | 139 ++++++++++++++++++ .../PhabricatorProjectColumnCreatedOrder.php | 2 +- .../order/PhabricatorProjectColumnHeader.php | 7 +- .../PhabricatorProjectColumnOwnerOrder.php | 12 +- .../PhabricatorProjectColumnPointsOrder.php | 2 +- .../PhabricatorProjectColumnStatusOrder.php | 2 +- .../PhabricatorProjectColumnTitleOrder.php | 34 +++++ .../css/phui/workboards/phui-workpanel.css | 24 ++- 10 files changed, 223 insertions(+), 13 deletions(-) create mode 100644 src/applications/project/order/PhabricatorProjectColumnAuthorOrder.php create mode 100644 src/applications/project/order/PhabricatorProjectColumnTitleOrder.php diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 014839c14a..b80ce31d1a 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -178,7 +178,7 @@ return array( 'rsrc/css/phui/workboards/phui-workboard-color.css' => 'e86de308', 'rsrc/css/phui/workboards/phui-workboard.css' => '74fc9d98', 'rsrc/css/phui/workboards/phui-workcard.css' => '9e9eb0df', - 'rsrc/css/phui/workboards/phui-workpanel.css' => 'bc16cf33', + 'rsrc/css/phui/workboards/phui-workpanel.css' => 'c5b408ad', 'rsrc/css/sprite-login.css' => '18b368a6', 'rsrc/css/sprite-tokens.css' => 'f1896dc5', 'rsrc/css/syntax/syntax-default.css' => '055fc231', @@ -860,7 +860,7 @@ return array( 'phui-workboard-color-css' => 'e86de308', 'phui-workboard-view-css' => '74fc9d98', 'phui-workcard-view-css' => '9e9eb0df', - 'phui-workpanel-view-css' => 'bc16cf33', + 'phui-workpanel-view-css' => 'c5b408ad', 'phuix-action-list-view' => 'c68f183f', 'phuix-action-view' => 'aaa08f3b', 'phuix-autocomplete' => '8f139ef0', @@ -1906,9 +1906,6 @@ return array( 'javelin-uri', 'phabricator-notification', ), - 'bc16cf33' => array( - 'phui-workcard-view-css', - ), 'bdce4d78' => array( 'javelin-install', 'javelin-util', @@ -1939,6 +1936,9 @@ return array( 'phabricator-phtize', 'javelin-dom', ), + 'c5b408ad' => array( + 'phui-workcard-view-css', + ), 'c687e867' => array( 'javelin-behavior', 'javelin-dom', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 8f776a2948..e07a23d733 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4050,6 +4050,7 @@ phutil_register_library_map(array( 'PhabricatorProjectColorTransaction' => 'applications/project/xaction/PhabricatorProjectColorTransaction.php', 'PhabricatorProjectColorsConfigType' => 'applications/project/config/PhabricatorProjectColorsConfigType.php', 'PhabricatorProjectColumn' => 'applications/project/storage/PhabricatorProjectColumn.php', + 'PhabricatorProjectColumnAuthorOrder' => 'applications/project/order/PhabricatorProjectColumnAuthorOrder.php', 'PhabricatorProjectColumnCreatedOrder' => 'applications/project/order/PhabricatorProjectColumnCreatedOrder.php', 'PhabricatorProjectColumnDetailController' => 'applications/project/controller/PhabricatorProjectColumnDetailController.php', 'PhabricatorProjectColumnEditController' => 'applications/project/controller/PhabricatorProjectColumnEditController.php', @@ -4066,6 +4067,7 @@ phutil_register_library_map(array( 'PhabricatorProjectColumnQuery' => 'applications/project/query/PhabricatorProjectColumnQuery.php', 'PhabricatorProjectColumnSearchEngine' => 'applications/project/query/PhabricatorProjectColumnSearchEngine.php', 'PhabricatorProjectColumnStatusOrder' => 'applications/project/order/PhabricatorProjectColumnStatusOrder.php', + 'PhabricatorProjectColumnTitleOrder' => 'applications/project/order/PhabricatorProjectColumnTitleOrder.php', 'PhabricatorProjectColumnTransaction' => 'applications/project/storage/PhabricatorProjectColumnTransaction.php', 'PhabricatorProjectColumnTransactionEditor' => 'applications/project/editor/PhabricatorProjectColumnTransactionEditor.php', 'PhabricatorProjectColumnTransactionQuery' => 'applications/project/query/PhabricatorProjectColumnTransactionQuery.php', @@ -10138,6 +10140,7 @@ phutil_register_library_map(array( 'PhabricatorExtendedPolicyInterface', 'PhabricatorConduitResultInterface', ), + 'PhabricatorProjectColumnAuthorOrder' => 'PhabricatorProjectColumnOrder', 'PhabricatorProjectColumnCreatedOrder' => 'PhabricatorProjectColumnOrder', 'PhabricatorProjectColumnDetailController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectColumnEditController' => 'PhabricatorProjectBoardController', @@ -10157,6 +10160,7 @@ phutil_register_library_map(array( 'PhabricatorProjectColumnQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorProjectColumnSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorProjectColumnStatusOrder' => 'PhabricatorProjectColumnOrder', + 'PhabricatorProjectColumnTitleOrder' => 'PhabricatorProjectColumnOrder', 'PhabricatorProjectColumnTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorProjectColumnTransactionEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorProjectColumnTransactionQuery' => 'PhabricatorApplicationTransactionQuery', diff --git a/src/applications/project/order/PhabricatorProjectColumnAuthorOrder.php b/src/applications/project/order/PhabricatorProjectColumnAuthorOrder.php new file mode 100644 index 0000000000..9d6bac2aff --- /dev/null +++ b/src/applications/project/order/PhabricatorProjectColumnAuthorOrder.php @@ -0,0 +1,139 @@ +newHeaderKeyForAuthorPHID($object->getAuthorPHID()); + } + + private function newHeaderKeyForAuthorPHID($author_phid) { + return sprintf('author(%s)', $author_phid); + } + + protected function newSortVectorsForObjects(array $objects) { + $author_phids = mpull($objects, null, 'getAuthorPHID'); + $author_phids = array_keys($author_phids); + $author_phids = array_filter($author_phids); + + if ($author_phids) { + $author_users = id(new PhabricatorPeopleQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs($author_phids) + ->execute(); + $author_users = mpull($author_users, null, 'getPHID'); + } else { + $author_users = array(); + } + + $vectors = array(); + foreach ($objects as $vector_key => $object) { + $author_phid = $object->getAuthorPHID(); + $author = idx($author_users, $author_phid); + if ($author) { + $vector = $this->newSortVectorForAuthor($author); + } else { + $vector = $this->newSortVectorForAuthorPHID($author_phid); + } + + $vectors[$vector_key] = $vector; + } + + return $vectors; + } + + private function newSortVectorForAuthor(PhabricatorUser $user) { + return array( + 1, + $user->getUsername(), + ); + } + + private function newSortVectorForAuthorPHID($author_phid) { + return array( + 2, + $author_phid, + ); + } + + protected function newHeadersForObjects(array $objects) { + $author_phids = mpull($objects, null, 'getAuthorPHID'); + $author_phids = array_keys($author_phids); + $author_phids = array_filter($author_phids); + + if ($author_phids) { + $author_users = id(new PhabricatorPeopleQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs($author_phids) + ->needProfileImage(true) + ->execute(); + $author_users = mpull($author_users, null, 'getPHID'); + } else { + $author_users = array(); + } + + $headers = array(); + foreach ($author_phids as $author_phid) { + $header_key = $this->newHeaderKeyForAuthorPHID($author_phid); + + $author = idx($author_users, $author_phid); + if ($author) { + $sort_vector = $this->newSortVectorForAuthor($author); + $author_name = $author->getUsername(); + $author_image = $author->getProfileImageURI(); + } else { + $sort_vector = $this->newSortVectorForAuthorPHID($author_phid); + $author_name = pht('Unknown User ("%s")', $author_phid); + $author_image = null; + } + + $author_icon = 'fa-user'; + $author_color = 'bluegrey'; + + $icon_view = id(new PHUIIconView()); + + if ($author_image) { + $icon_view->setImage($author_image); + } else { + $icon_view->setIcon($author_icon, $author_color); + } + + $header = $this->newHeader() + ->setHeaderKey($header_key) + ->setSortVector($sort_vector) + ->setName($author_name) + ->setIcon($icon_view) + ->setEditProperties( + array( + 'value' => $author_phid, + )); + + $headers[] = $header; + } + + return $headers; + } + +} diff --git a/src/applications/project/order/PhabricatorProjectColumnCreatedOrder.php b/src/applications/project/order/PhabricatorProjectColumnCreatedOrder.php index 5652a96731..05f25a3d6a 100644 --- a/src/applications/project/order/PhabricatorProjectColumnCreatedOrder.php +++ b/src/applications/project/order/PhabricatorProjectColumnCreatedOrder.php @@ -22,7 +22,7 @@ final class PhabricatorProjectColumnCreatedOrder } public function getMenuOrder() { - return 3000; + return 5000; } protected function newSortVectorForObject($object) { diff --git a/src/applications/project/order/PhabricatorProjectColumnHeader.php b/src/applications/project/order/PhabricatorProjectColumnHeader.php index 432b78279b..24d1e5c5ec 100644 --- a/src/applications/project/order/PhabricatorProjectColumnHeader.php +++ b/src/applications/project/order/PhabricatorProjectColumnHeader.php @@ -85,7 +85,12 @@ final class PhabricatorProjectColumnHeader ), array( $icon_view, - $name, + phutil_tag( + 'span', + array( + 'class' => 'workboard-group-header-name', + ), + $name), )); return $template; diff --git a/src/applications/project/order/PhabricatorProjectColumnOwnerOrder.php b/src/applications/project/order/PhabricatorProjectColumnOwnerOrder.php index 97ae0f24d4..336411bac5 100644 --- a/src/applications/project/order/PhabricatorProjectColumnOwnerOrder.php +++ b/src/applications/project/order/PhabricatorProjectColumnOwnerOrder.php @@ -108,6 +108,7 @@ final class PhabricatorProjectColumnOwnerOrder $owner_users = id(new PhabricatorPeopleQuery()) ->setViewer($this->getViewer()) ->withPHIDs($owner_phids) + ->needProfileImage(true) ->execute(); $owner_users = mpull($owner_users, null, 'getPHID'); } else { @@ -120,6 +121,7 @@ final class PhabricatorProjectColumnOwnerOrder foreach ($owner_phids as $owner_phid) { $header_key = $this->newHeaderKeyForOwnerPHID($owner_phid); + $owner_image = null; if ($owner_phid === null) { $owner = null; $sort_vector = $this->newSortVectorForUnowned(); @@ -129,6 +131,7 @@ final class PhabricatorProjectColumnOwnerOrder if ($owner) { $sort_vector = $this->newSortVectorForOwner($owner); $owner_name = $owner->getUsername(); + $owner_image = $owner->getProfileImageURI(); } else { $sort_vector = $this->newSortVectorForOwnerPHID($owner_phid); $owner_name = pht('Unknown User ("%s")', $owner_phid); @@ -138,8 +141,13 @@ final class PhabricatorProjectColumnOwnerOrder $owner_icon = 'fa-user'; $owner_color = 'bluegrey'; - $icon_view = id(new PHUIIconView()) - ->setIcon($owner_icon, $owner_color); + $icon_view = id(new PHUIIconView()); + + if ($owner_image) { + $icon_view->setImage($owner_image); + } else { + $icon_view->setIcon($owner_icon, $owner_color); + } $header = $this->newHeader() ->setHeaderKey($header_key) diff --git a/src/applications/project/order/PhabricatorProjectColumnPointsOrder.php b/src/applications/project/order/PhabricatorProjectColumnPointsOrder.php index 3cf758cbc8..2e9be8e4bb 100644 --- a/src/applications/project/order/PhabricatorProjectColumnPointsOrder.php +++ b/src/applications/project/order/PhabricatorProjectColumnPointsOrder.php @@ -26,7 +26,7 @@ final class PhabricatorProjectColumnPointsOrder } public function getMenuOrder() { - return 4000; + return 6000; } protected function newSortVectorForObject($object) { diff --git a/src/applications/project/order/PhabricatorProjectColumnStatusOrder.php b/src/applications/project/order/PhabricatorProjectColumnStatusOrder.php index e9570bea05..e58d05f655 100644 --- a/src/applications/project/order/PhabricatorProjectColumnStatusOrder.php +++ b/src/applications/project/order/PhabricatorProjectColumnStatusOrder.php @@ -22,7 +22,7 @@ final class PhabricatorProjectColumnStatusOrder } public function getMenuOrder() { - return 3000; + return 4000; } protected function newHeaderKeyForObject($object) { diff --git a/src/applications/project/order/PhabricatorProjectColumnTitleOrder.php b/src/applications/project/order/PhabricatorProjectColumnTitleOrder.php new file mode 100644 index 0000000000..a281c75437 --- /dev/null +++ b/src/applications/project/order/PhabricatorProjectColumnTitleOrder.php @@ -0,0 +1,34 @@ +getTitle(), + ); + } + +} diff --git a/webroot/rsrc/css/phui/workboards/phui-workpanel.css b/webroot/rsrc/css/phui/workboards/phui-workpanel.css index 2dac6b2233..95db8021ef 100644 --- a/webroot/rsrc/css/phui/workboards/phui-workpanel.css +++ b/webroot/rsrc/css/phui/workboards/phui-workpanel.css @@ -148,13 +148,33 @@ .workboard-group-header { background: rgba({$alphablue}, 0.10); - padding: 4px 8px; + padding: 6px 8px; margin: 0 0 8px -8px; border-bottom: 1px solid {$lightgreyborder}; font-weight: bold; color: {$darkgreytext}; + position: relative; } .workboard-group-header .phui-icon-view { - margin-right: 8px; + position: absolute; + display: inline-block; + width: 24px; + padding: 5px 0 0 0; + height: 19px; + background-size: 100%; + border-radius: 3px; + background-repeat: no-repeat; + text-align: center; + background-color: {$lightgreybackground}; + border: 1px solid {$lightgreybackground}; +} + +.workboard-group-header .workboard-group-header-name { + display: block; + position: relative; + height: 24px; + line-height: 24px; + margin-left: 36px; + overflow: hidden; } From 04f9e72cbd101b278a9f9374600e10deadc0eafa Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 13 Mar 2019 11:41:22 -0700 Subject: [PATCH 164/245] Don't subscribe bots implicitly when they act on objects, or when they are mentioned Summary: See PHI1098. When users comment on objects, they are automatically subscribed. And when `@alice` mentions `@bailey` on a task, that usually subscribes `@bailey`. These rules make less sense if the user is a bot. There's generally no reason for a bot to automatically subscribe to objects it acts on (it's not going to read email and follow up later), and it can always subscribe itself pretty easily if it wants (since everything is `*.edit` now and supports subscribe transactions). Also, don't subscribe bots when they're mentioned for similar reasons. If users really want to subscribe bots, they can do so explicitly. These rules seem slightly like "bad implicit magic" since it's not immediately obvious why `@abc` subscribes that user but `@xyz` may not, but some of these rules are fairly complicated already (e.g., `@xyz` doesn't subscribe them if they unsubscribed or are implicitly subscribed) and this feels like it gets the right/desired result almost-always. Test Plan: On a fresh task: - Mentioned a bot in a comment with `@bot`. - Before patch: bot got CC'd. - After patch: no CC. - Called `maniphest.edit` via the API to add a comment as a bot. - Before patch: bot got CC'd. - After patch: no CC. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20284 --- ...habricatorApplicationTransactionEditor.php | 67 +++++++++++++++---- 1 file changed, 53 insertions(+), 14 deletions(-) diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index 3a46784a33..24bff2bc57 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -1816,31 +1816,52 @@ abstract class PhabricatorApplicationTransactionEditor $users = mpull($users, null, 'getPHID'); foreach ($phids as $key => $phid) { - // Do not subscribe mentioned users - // who do not have VIEW Permissions - if ($object instanceof PhabricatorPolicyInterface - && !PhabricatorPolicyFilter::hasCapability( - $users[$phid], - $object, - PhabricatorPolicyCapability::CAN_VIEW) - ) { + $user = idx($users, $phid); + + // Don't subscribe invalid users. + if (!$user) { unset($phids[$key]); - } else { - if ($object->isAutomaticallySubscribed($phid)) { + continue; + } + + // Don't subscribe bots that get mentioned. If users truly intend + // to subscribe them, they can add them explicitly, but it's generally + // not useful to subscribe bots to objects. + if ($user->getIsSystemAgent()) { + unset($phids[$key]); + continue; + } + + // Do not subscribe mentioned users who do not have permission to see + // the object. + if ($object instanceof PhabricatorPolicyInterface) { + $can_view = PhabricatorPolicyFilter::hasCapability( + $user, + $object, + PhabricatorPolicyCapability::CAN_VIEW); + if (!$can_view) { unset($phids[$key]); + continue; } } + + // Don't subscribe users who are already automatically subscribed. + if ($object->isAutomaticallySubscribed($phid)) { + unset($phids[$key]); + continue; + } } + $phids = array_values($phids); } - // No else here to properly return null should we unset all subscriber + if (!$phids) { return null; } - $xaction = newv(get_class(head($xactions)), array()); - $xaction->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS); - $xaction->setNewValue(array('+' => $phids)); + $xaction = $object->getApplicationTransactionTemplate() + ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS) + ->setNewValue(array('+' => $phids)); return $xaction; } @@ -2876,6 +2897,24 @@ abstract class PhabricatorApplicationTransactionEditor } } + $actor = $this->getActor(); + + $user = id(new PhabricatorPeopleQuery()) + ->setViewer($actor) + ->withPHIDs(array($actor_phid)) + ->executeOne(); + if (!$user) { + return $xactions; + } + + // When a bot acts (usually via the API), don't automatically subscribe + // them as a side effect. They can always subscribe explicitly if they + // want, and bot subscriptions normally just clutter things up since bots + // usually do not read email. + if ($user->getIsSystemAgent()) { + return $xactions; + } + $xaction = newv(get_class(head($xactions)), array()); $xaction->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS); $xaction->setNewValue(array('+' => array($actor_phid))); From df53d72e794c8f3eed9123b4b78d7b02ace77e8c Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 12 Mar 2019 14:14:52 -0700 Subject: [PATCH 165/245] Allow "Move Tasks to Column..." to prompt for MFA Summary: Ref T13074. Currently, if you "Move Tasks to Column..." on a board and some of the tasks require MFA to edit, the workflow fatals out. After this change, it works properly. You still have to answer a separate MFA prompt for //each// task, which is a little ridiculous, but at least doable. A reasonable future refinement would be to batch these MFA prompts, but this is currently the only use case for that. Test Plan: Set a task to a "Require MFA" status, bulk-moved it with other tasks on a workboard. Was prompted, answered MFA prompt, got a move. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13074 Differential Revision: https://secure.phabricator.com/D20282 --- .../controller/PhabricatorProjectBoardViewController.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index cc95958bf7..a1dcd6ab68 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -328,7 +328,7 @@ final class PhabricatorProjectBoardViewController $columns = null; $errors = array(); - if ($request->isFormPost()) { + if ($request->isFormOrHiSecPost()) { $move_project_phid = head($request->getArr('moveProjectPHID')); if (!$move_project_phid) { $move_project_phid = $request->getStr('moveProjectPHID'); @@ -425,7 +425,8 @@ final class PhabricatorProjectBoardViewController ->setActor($viewer) ->setContinueOnMissingFields(true) ->setContinueOnNoEffect(true) - ->setContentSourceFromRequest($request); + ->setContentSourceFromRequest($request) + ->setCancelURI($cancel_uri); $editor->applyTransactions($move_task, $xactions); } From 492b03628f19118d1828f1e68a693752a4ba665e Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 15 Mar 2019 14:47:14 -0700 Subject: [PATCH 166/245] Fix a typo in Drydock "Land" operations Summary: Misspelling. Test Plan: Careful reading. Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Differential Revision: https://secure.phabricator.com/D20290 --- .../drydock/operation/DrydockLandRepositoryOperation.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/applications/drydock/operation/DrydockLandRepositoryOperation.php b/src/applications/drydock/operation/DrydockLandRepositoryOperation.php index 1ccc82eb7b..acb48f6f0b 100644 --- a/src/applications/drydock/operation/DrydockLandRepositoryOperation.php +++ b/src/applications/drydock/operation/DrydockLandRepositoryOperation.php @@ -401,7 +401,7 @@ final class DrydockLandRepositoryOperation 'body' => pht( 'When this diff was generated, the server was running an older '. 'version of Phabricator which did not support staging areas for '. - 'this version control system, so the chagne was not pushed to '. + 'this version control system, so the change was not pushed to '. 'staging. Changes must be pushed to staging before they can be '. 'landed from the web.'), ); From b469a5134ddd4f6484ebdf9088126d56acf6d4b8 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 14 Mar 2019 06:37:00 -0700 Subject: [PATCH 167/245] Allow "SMTP" and "Sendmail" mailers to have "Message-ID" behavior configured in "cluster.mailers" Summary: Fixes T13265. See that task for discussion. Briefly: - For mailers that use other mailers (SMTP, Sendmail), optionally let administrators set `"message-id": false` to improve threading behavior if their local Postfix is ultimately sending through SES or some other mailer which will replace the "Message-ID" header. Also: - Postmark is currently marked as supporting "Message-ID", but it does not actually support "Message-ID" on `secure.phabricator.com` (mail arrives with a non-Phabricator message ID). I suspect this was just an oversight in building or refactoring the adapter; correct it. - Remove the "encoding" parameter from "sendmail". It think this was just missed in the cleanup a couple months ago; it is no longer used or documented. Test Plan: Added and ran unit tests. (These feel like overkill, but this is super hard to test on real code.) See T13265 for evidence that this overall approach improves behavior. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13265 Differential Revision: https://secure.phabricator.com/D20285 --- src/__phutil_library_map__.php | 2 + .../adapter/PhabricatorMailAdapter.php | 33 +++++++ .../PhabricatorMailAmazonSESAdapter.php | 4 - .../PhabricatorMailPostmarkAdapter.php | 4 - .../adapter/PhabricatorMailSMTPAdapter.php | 6 +- .../PhabricatorMailSendmailAdapter.php | 9 +- .../PhabricatorMailAdapterTestCase.php | 96 +++++++++++++++++++ .../configuring_outbound_email.diviner | 57 ++++++++++- 8 files changed, 196 insertions(+), 15 deletions(-) create mode 100644 src/applications/metamta/adapter/__tests__/PhabricatorMailAdapterTestCase.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index e07a23d733..82972d5a7d 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -3464,6 +3464,7 @@ phutil_register_library_map(array( 'PhabricatorMacroTransactionType' => 'applications/macro/xaction/PhabricatorMacroTransactionType.php', 'PhabricatorMacroViewController' => 'applications/macro/controller/PhabricatorMacroViewController.php', 'PhabricatorMailAdapter' => 'applications/metamta/adapter/PhabricatorMailAdapter.php', + 'PhabricatorMailAdapterTestCase' => 'applications/metamta/adapter/__tests__/PhabricatorMailAdapterTestCase.php', 'PhabricatorMailAmazonSESAdapter' => 'applications/metamta/adapter/PhabricatorMailAmazonSESAdapter.php', 'PhabricatorMailAmazonSNSAdapter' => 'applications/metamta/adapter/PhabricatorMailAmazonSNSAdapter.php', 'PhabricatorMailAttachment' => 'applications/metamta/message/PhabricatorMailAttachment.php', @@ -9425,6 +9426,7 @@ phutil_register_library_map(array( 'PhabricatorMacroTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorMacroViewController' => 'PhabricatorMacroController', 'PhabricatorMailAdapter' => 'Phobject', + 'PhabricatorMailAdapterTestCase' => 'PhabricatorTestCase', 'PhabricatorMailAmazonSESAdapter' => 'PhabricatorMailAdapter', 'PhabricatorMailAmazonSNSAdapter' => 'PhabricatorMailAdapter', 'PhabricatorMailAttachment' => 'Phobject', diff --git a/src/applications/metamta/adapter/PhabricatorMailAdapter.php b/src/applications/metamta/adapter/PhabricatorMailAdapter.php index 4fb262626d..8c1a6c0ba7 100644 --- a/src/applications/metamta/adapter/PhabricatorMailAdapter.php +++ b/src/applications/metamta/adapter/PhabricatorMailAdapter.php @@ -137,4 +137,37 @@ abstract class PhabricatorMailAdapter abstract public function newDefaultOptions(); + final protected function guessIfHostSupportsMessageID($config, $host) { + // See T13265. Mailers like "SMTP" and "sendmail" usually allow us to + // set the "Message-ID" header to a value we choose, but we may not be + // able to if the mailer is being used as API glue and the outbound + // pathway ends up routing to a service with an SMTP API that selects + // its own "Message-ID" header, like Amazon SES. + + // If users configured a behavior explicitly, use that behavior. + if ($config !== null) { + return $config; + } + + // If the server we're connecting to is part of a service that we know + // does not support "Message-ID", guess that we don't support "Message-ID". + if ($host !== null) { + $host_blocklist = array( + '/\.amazonaws\.com\z/', + '/\.postmarkapp\.com\z/', + '/\.sendgrid\.net\z/', + ); + + $host = phutil_utf8_strtolower($host); + foreach ($host_blocklist as $regexp) { + if (preg_match($regexp, $host)) { + return false; + } + } + } + + return true; + } + + } diff --git a/src/applications/metamta/adapter/PhabricatorMailAmazonSESAdapter.php b/src/applications/metamta/adapter/PhabricatorMailAmazonSESAdapter.php index a289e5bc73..793cd56091 100644 --- a/src/applications/metamta/adapter/PhabricatorMailAmazonSESAdapter.php +++ b/src/applications/metamta/adapter/PhabricatorMailAmazonSESAdapter.php @@ -11,10 +11,6 @@ final class PhabricatorMailAmazonSESAdapter ); } - public function supportsMessageIDHeader() { - return false; - } - protected function validateOptions(array $options) { PhutilTypeSpec::checkMap( $options, diff --git a/src/applications/metamta/adapter/PhabricatorMailPostmarkAdapter.php b/src/applications/metamta/adapter/PhabricatorMailPostmarkAdapter.php index d84d8f8bfa..2381ff04bf 100644 --- a/src/applications/metamta/adapter/PhabricatorMailPostmarkAdapter.php +++ b/src/applications/metamta/adapter/PhabricatorMailPostmarkAdapter.php @@ -11,10 +11,6 @@ final class PhabricatorMailPostmarkAdapter ); } - public function supportsMessageIDHeader() { - return true; - } - protected function validateOptions(array $options) { PhutilTypeSpec::checkMap( $options, diff --git a/src/applications/metamta/adapter/PhabricatorMailSMTPAdapter.php b/src/applications/metamta/adapter/PhabricatorMailSMTPAdapter.php index a3c6298279..abbda40146 100644 --- a/src/applications/metamta/adapter/PhabricatorMailSMTPAdapter.php +++ b/src/applications/metamta/adapter/PhabricatorMailSMTPAdapter.php @@ -12,7 +12,9 @@ final class PhabricatorMailSMTPAdapter } public function supportsMessageIDHeader() { - return true; + return $this->guessIfHostSupportsMessageID( + $this->getOption('message-id'), + $this->getOption('host')); } protected function validateOptions(array $options) { @@ -24,6 +26,7 @@ final class PhabricatorMailSMTPAdapter 'user' => 'string|null', 'password' => 'string|null', 'protocol' => 'string|null', + 'message-id' => 'bool|null', )); } @@ -34,6 +37,7 @@ final class PhabricatorMailSMTPAdapter 'user' => null, 'password' => null, 'protocol' => null, + 'message-id' => null, ); } diff --git a/src/applications/metamta/adapter/PhabricatorMailSendmailAdapter.php b/src/applications/metamta/adapter/PhabricatorMailSendmailAdapter.php index 05f3c909aa..a60c0e5a4e 100644 --- a/src/applications/metamta/adapter/PhabricatorMailSendmailAdapter.php +++ b/src/applications/metamta/adapter/PhabricatorMailSendmailAdapter.php @@ -5,7 +5,6 @@ final class PhabricatorMailSendmailAdapter const ADAPTERTYPE = 'sendmail'; - public function getSupportedMessageTypes() { return array( PhabricatorMailEmailMessage::MESSAGETYPE, @@ -13,20 +12,22 @@ final class PhabricatorMailSendmailAdapter } public function supportsMessageIDHeader() { - return true; + return $this->guessIfHostSupportsMessageID( + $this->getOption('message-id'), + null); } protected function validateOptions(array $options) { PhutilTypeSpec::checkMap( $options, array( - 'encoding' => 'string', + 'message-id' => 'bool|null', )); } public function newDefaultOptions() { return array( - 'encoding' => 'base64', + 'message-id' => null, ); } diff --git a/src/applications/metamta/adapter/__tests__/PhabricatorMailAdapterTestCase.php b/src/applications/metamta/adapter/__tests__/PhabricatorMailAdapterTestCase.php new file mode 100644 index 0000000000..9c194f24c2 --- /dev/null +++ b/src/applications/metamta/adapter/__tests__/PhabricatorMailAdapterTestCase.php @@ -0,0 +1,96 @@ + 'test', + 'secret-key' => 'test', + 'endpoint' => 'test', + ), + ), + + array( + pht('Mailgun'), + true, + new PhabricatorMailMailgunAdapter(), + array( + 'api-key' => 'test', + 'domain' => 'test', + 'api-hostname' => 'test', + ), + ), + + array( + pht('Sendmail'), + true, + new PhabricatorMailSendmailAdapter(), + array(), + ), + + array( + pht('Sendmail (Explicit Config)'), + false, + new PhabricatorMailSendmailAdapter(), + array( + 'message-id' => false, + ), + ), + + array( + pht('SMTP (Local)'), + true, + new PhabricatorMailSMTPAdapter(), + array(), + ), + + array( + pht('SMTP (Local + Explicit)'), + false, + new PhabricatorMailSMTPAdapter(), + array( + 'message-id' => false, + ), + ), + + array( + pht('SMTP (AWS)'), + false, + new PhabricatorMailSMTPAdapter(), + array( + 'host' => 'test.amazonaws.com', + ), + ), + + array( + pht('SMTP (AWS + Explicit)'), + true, + new PhabricatorMailSMTPAdapter(), + array( + 'host' => 'test.amazonaws.com', + 'message-id' => true, + ), + ), + + ); + + foreach ($cases as $case) { + list($label, $expect, $mailer, $options) = $case; + + $defaults = $mailer->newDefaultOptions(); + $mailer->setOptions($options + $defaults); + + $actual = $mailer->supportsMessageIDHeader(); + + $this->assertEqual($expect, $actual, pht('Message-ID: %s', $label)); + } + } + + +} diff --git a/src/docs/user/configuration/configuring_outbound_email.diviner b/src/docs/user/configuration/configuring_outbound_email.diviner index b77d761f80..884e4e7fdb 100644 --- a/src/docs/user/configuration/configuring_outbound_email.diviner +++ b/src/docs/user/configuration/configuring_outbound_email.diviner @@ -339,9 +339,11 @@ document. If you can already send outbound email from the command line or know how to configure it, this option is straightforward. If you have no idea how to do any of this, strongly consider using Postmark or Mailgun instead. -To use this mailer, set `type` to `sendmail`. There are no `options` to -configure. +To use this mailer, set `type` to `sendmail`, then configure these `options`: + - `message-id`: Optional bool. Set to `false` if Phabricator will not be + able to select a custom "Message-ID" header when sending mail via this + mailer. See "Message-ID Headers" below. Mailer: SMTP ============ @@ -361,6 +363,9 @@ To use this mailer, set `type` to `smtp`, then configure these `options`: - `password`: Optional string. Password for authentication. - `protocol`: Optional string. Set to `tls` or `ssl` if necessary. Use `ssl` for Gmail. + - `message-id`: Optional bool. Set to `false` if Phabricator will not be + able to select a custom "Message-ID" header when sending mail via this + mailer. See "Message-ID Headers" below. Disable Mail @@ -446,6 +451,54 @@ in any priority group, in the configured order. In this example there is only one such server, so it will try to send via Mailgun. +Message-ID Headers +================== + +Email has a "Message-ID" header which is important for threading messages +correctly in mail clients. Normally, Phabricator is free to select its own +"Message-ID" header values for mail it sends. + +However, some mailers (including Amazon SES) do not allow selection of custom +"Message-ID" values and will ignore or replace the "Message-ID" in mail that +is submitted through them. + +When Phabricator adds other mail headers which affect threading, like +"In-Reply-To", it needs to know if its "Message-ID" headers will be respected +or not to select header values which will produce good threading behavior. If +we guess wrong and think we can set a "Message-ID" header when we can't, you +may get poor threading behavior in mail clients. + +For most mailers (like Postmark, Mailgun, and Amazon SES), the correct setting +will be selected for you automatically, because the behavior of the mailer +is knowable ahead of time. For example, we know Amazon SES will never respect +our "Message-ID" headers. + +However, if you're sending mail indirectly through a mailer like SMTP or +Sendmail, the mail might or might not be routing through some mail service +which will ignore or replace the "Message-ID" header. + +For example, your local mailer might submit mail to Mailgun (so "Message-ID" +will work), or to Amazon SES (so "Message-ID" will not work), or to some other +mail service (which we may not know anything about). We can't make a reliable +guess about whether "Message-ID" will be respected or not based only on +the local mailer configuration. + +By default, we check if the mailer has a hostname we recognize as belonging +to a service which does not allow us to set a "Message-ID" header. If we don't +recognize the hostname (which is very common, since these services are most +often configured against the localhost or some other local machine), we assume +we can set a "Message-ID" header. + +If the outbound pathway does not actually allow selection of a "Message-ID" +header, you can set the `message-id` option on the mailer to `false` to tell +Phabricator that it should not assume it can select a value for this header. + +For example, if you are sending mail via a local Postfix server which then +forwards the mail to Amazon SES (a service which does not allow selection of +a "Message-ID" header), your `smtp` configuration in Phabricator should +specify `"message-id": false`. + + Next Steps ========== From a6fd8f04792da6653f987677e7d855e32c964e1c Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 13 Mar 2019 08:47:31 -0700 Subject: [PATCH 168/245] When performing complex edits, pause sub-editors before they publish to propagate "Must Encrypt" and other state Summary: See PHI1134. Previously, see T13082 and D19969 for some sort-of-related stuff. Currently, edits work roughly like this: - Suppose we're editing object X, and we're also going to edit some other object, Y, because X mentioned Y or the edit is making X a child or parent of Y, or unblocking Y. - Do the actual edit to X, including inverse edits ("alice mentioned Y on X.", "alice added a child revision: X", etc) which apply to Y. - Run Herald rules on X. - Publish the edit to X. The "inverse edits" currently do this whole process inline, in a sub-editor. So the flow expands like this: - Begin editing X. - Update properties on X. - Begin inverse-edge editing Y. - Update properties on Y. - Run (actually, skip) Herald rules on Y. - Publish edits to Y. - Run Herald rules on X. - Publish edits to X. Notably, the "Y" stuff publishes before the "X" Herald rules run. This creates potential problems: - Herald rules may change the name or visibility policy of "X", but we'll publish mail about it via the edits to Y before those edits apply. This is a problem only in theory, we don't ship any upstream rules like this today. - Herald rules may "Require Secure Mail", but we won't know that at the time we're building mail about the indirect change to "Y". This is a problem in practice. Instead, switch to this new flow, where we stop the sub-editors before they publish, then publish everything at the very end once all the edits are complete: - Begin editing X. - Update properties on X. - Begin inverse-edge editing Y. - Update properties on Y. - Skip Herald on Y. - Run Herald rules on X. - Publish X. - Publish all child-editors of X. - Publish Y. Test Plan: - Created "Must Encrypt" Herald rules for Tasks and Revisions. - Edited object "A", an object which the rules applied to directly, and set object "B" (a different object which the rules did not hit) as its parent/child and/or unblocked it. - In `bin/mail list-outbound`, saw: - Mail about object "A" all flagged as "Must Encrypt". - Normal mail from object B not flagged "Must Encrypt". - Mail from object B about changing relationships to object A flagged as "Must Encrypt". Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20283 --- .../editor/ManiphestTransactionEditor.php | 5 +- ...habricatorApplicationTransactionEditor.php | 86 +++++++++++++++---- 2 files changed, 72 insertions(+), 19 deletions(-) diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php index 9a95bbdbb8..5cb7cf91ba 100644 --- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php +++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php @@ -134,10 +134,7 @@ final class ManiphestTransactionEditor $parent_xaction->setMetadataValue('blocker.new', true); } - id(new ManiphestTransactionEditor()) - ->setActor($this->getActor()) - ->setActingAsPHID($this->getActingAsPHID()) - ->setContentSource($this->getContentSource()) + $this->newSubEditor() ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) ->applyTransactions($blocked_task, array($parent_xaction)); diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index 24bff2bc57..3e5e9c23c9 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -72,7 +72,7 @@ abstract class PhabricatorApplicationTransactionEditor private $mailShouldSend = false; private $modularTypes; private $silent; - private $mustEncrypt; + private $mustEncrypt = array(); private $stampTemplates = array(); private $mailStamps = array(); private $oldTo = array(); @@ -90,6 +90,11 @@ abstract class PhabricatorApplicationTransactionEditor private $cancelURI; private $extensions; + private $parentEditor; + private $subEditors = array(); + private $publishableObject; + private $publishableTransactions; + const STORAGE_ENCODING_BINARY = 'binary'; /** @@ -1272,10 +1277,9 @@ abstract class PhabricatorApplicationTransactionEditor $herald_source = PhabricatorContentSource::newForSource( PhabricatorHeraldContentSource::SOURCECONST); - $herald_editor = newv(get_class($this), array()) + $herald_editor = $this->newEditorCopy() ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) - ->setParentMessageID($this->getParentMessageID()) ->setIsHeraldEditor(true) ->setActor($herald_actor) ->setActingAsPHID($herald_phid) @@ -1330,6 +1334,38 @@ abstract class PhabricatorApplicationTransactionEditor } $this->heraldHeader = $herald_header; + // See PHI1134. If we're a subeditor, we don't publish information about + // the edit yet. Our parent editor still needs to finish applying + // transactions and execute Herald, which may change the information we + // publish. + + // For example, Herald actions may change the parent object's title or + // visibility, or Herald may apply rules like "Must Encrypt" that affect + // email. + + // Once the parent finishes work, it will queue its own publish step and + // then queue publish steps for its children. + + $this->publishableObject = $object; + $this->publishableTransactions = $xactions; + if (!$this->parentEditor) { + $this->queuePublishing(); + } + + return $xactions; + } + + final private function queuePublishing() { + $object = $this->publishableObject; + $xactions = $this->publishableTransactions; + + if (!$object) { + throw new Exception( + pht( + 'Editor method "queuePublishing()" was called, but no publishable '. + 'object is present. This Editor is not ready to publish.')); + } + // We're going to compute some of the data we'll use to publish these // transactions here, before queueing a worker. // @@ -1392,9 +1428,11 @@ abstract class PhabricatorApplicationTransactionEditor 'priority' => PhabricatorWorker::PRIORITY_ALERTS, )); - $this->flushTransactionQueue($object); + foreach ($this->subEditors as $sub_editor) { + $sub_editor->queuePublishing(); + } - return $xactions; + $this->flushTransactionQueue($object); } protected function didCatchDuplicateKeyException( @@ -3818,6 +3856,11 @@ abstract class PhabricatorApplicationTransactionEditor $this->mustEncrypt = $adapter->getMustEncryptReasons(); + // See PHI1134. Propagate "Must Encrypt" state to sub-editors. + foreach ($this->subEditors as $sub_editor) { + $sub_editor->mustEncrypt = $this->mustEncrypt; + } + $apply_xactions = $this->didApplyHeraldRules($object, $adapter, $xscript); assert_instances_of($apply_xactions, 'PhabricatorApplicationTransaction'); @@ -4034,15 +4077,10 @@ abstract class PhabricatorApplicationTransactionEditor ->setOldValue($old_phids) ->setNewValue($new_phids); - $editor + $editor = $this->newSubEditor($editor) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) - ->setParentMessageID($this->getParentMessageID()) - ->setIsInverseEdgeEditor(true) - ->setIsSilent($this->getIsSilent()) - ->setActor($this->requireActor()) - ->setActingAsPHID($this->getActingAsPHID()) - ->setContentSource($this->getContentSource()); + ->setIsInverseEdgeEditor(true); $editor->applyTransactions($node, array($template)); } @@ -4551,23 +4589,41 @@ abstract class PhabricatorApplicationTransactionEditor $xactions = $this->transactionQueue; $this->transactionQueue = array(); - $editor = $this->newQueueEditor(); + $editor = $this->newEditorCopy(); return $editor->applyTransactions($object, $xactions); } - private function newQueueEditor() { - $editor = id(newv(get_class($this), array())) + final protected function newSubEditor( + PhabricatorApplicationTransactionEditor $template = null) { + $editor = $this->newEditorCopy($template); + + $editor->parentEditor = $this; + $this->subEditors[] = $editor; + + return $editor; + } + + private function newEditorCopy( + PhabricatorApplicationTransactionEditor $template = null) { + if ($template === null) { + $template = newv(get_class($this), array()); + } + + $editor = id(clone $template) ->setActor($this->getActor()) ->setContentSource($this->getContentSource()) ->setContinueOnNoEffect($this->getContinueOnNoEffect()) ->setContinueOnMissingFields($this->getContinueOnMissingFields()) + ->setParentMessageID($this->getParentMessageID()) ->setIsSilent($this->getIsSilent()); if ($this->actingAsPHID !== null) { $editor->setActingAsPHID($this->actingAsPHID); } + $editor->mustEncrypt = $this->mustEncrypt; + return $editor; } From 1b0ef4391032f5b4911ede255839b92e6d602c66 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 18 Mar 2019 10:39:56 -0700 Subject: [PATCH 169/245] Separate internal and external Query Cursors more cleanly, to fix pagination against broken objects Summary: Ref T13259. (NOTE) This is "infrastructure/guts" only and breaks some stuff in Query subclasses. I'll fix that stuff in a followup, it's just going to be a larger diff that's mostly mechanical. When a user clicks "Next Page" on a tasks view and gets `?after=100`, we want to show them the next 100 //visible// tasks. It's possible that tasks 1-100 are visible, but tasks 101-788 are not, and the next visible task is 789. We load task ID `100` first, to make sure they can actually see it: you aren't allowed to page based on objects you can't see. If we let you, you could use "order=title&after=100", plus creative retitling of tasks, to discover the title of task 100: create tasks named "A", "B", etc., and see which one is returned first "after" task 100. If it's "D", you know task 100 must start with "C". Assume the user can see task 100. We run a query like `id > 100` to get the next 100 tasks. However, it's possible that few (or none) of these tasks can be seen. If the next visible task is 789, none of the tasks in the next page of results will survive policy filtering. So, for queries after the initial query, we need to be able to page based on tasks that the user can not see: we want to be able to issue `id > 100`, then `id > 200`, and so on, until we overheat or find a page of results (if 789-889 are visible, we'll make it there before overheating). Currently, we do this in a not-so-great way: - We pass the external cursor (`100`) directly to the subquery. - We query for that object using `getPagingViewer()`, which is a piece of magic that returns the real viewer on the first page and the omnipotent viewer on the 2nd..nth page. This is very sketchy. - The subquery builds paging values based on that object (`array('id' => 100)`). - We turn the last result from the subquery back into an external cursor (`200`) and save it for the next time. Note that the last step happens BEFORE policy (and other) filtering. The problems with this are: - The phantom-schrodinger's-omnipotent-viewer thing isn't explicity bad, but it's sketchy and generally not good. It feels like it could easily lead to a mistake or bug eventually. - We issue an extra query each time we page results, to convert the external cursor back into a map (`100`, `200`, `300`, etc). - In T13259, there's a new problem: this only works if the object is filtered out for policy reasons and the omnipotent viewer can still see it. It doesn't work if the object is filtered for some other reason. To expand on the third point: in T13259, we hit a case where 100+ consecutive objects are broken (they point to a nonexistent `repositoryID`). These objects get filtered unconditionally. It doesn't matter if the viewer is omnipotent or not. In that case: we set the next external cursor from the raw results (e.g., `200`). Then we try to load it (using the omnipotent viewer) to turn it into a map of values for paging. This fails because the object isn't loadable, even as the omnipotent viewer. --- To fix this stuff, the new approach steps back a little bit. Primarily, I'm separating "external cursors" from "internal cursors". An "External Cursor" is a string that we can pass in `?after=X` URIs. It generally identifies an object which the user can see. An "Internal Cursor" is a raw result from `loadPage()`, i.e. before policy filtering. Usually, (but not always) this is a `LiskDAO` object that doesn't have anything attached yet and hasn't been policy filtered. We now do this, broadly: - Convert the external cursor to an internal cursor. - Execute the query using internal cursors. - If necessary, convert the last visible result back into an external cursor at the very end. This fixes all the problems: - Sketchy Omnipotent Viewer: We no longer ever use an omnipotent viewer. (We pick cursors out of the result set earlier, instead.) - Too Many Queries: We only issue one query at the beginning, when going from "external" to "internal". This query is generally unavoidable since we need to make sure the viewer can see the object and that it's a real / legitimate object. We no longer have to query an extra time for each page. - Total Failure on Invalid Objects: we now page directly with objects out of `loadPage()`, before any filtering, so we can page over invisible or invalid objects without issues. This change switches us over to internal/external cursors, and makes simple cases (ID-based ordering) work correctly. It doesn't work for complex cases yet since subclasses don't know how to get paging values out of an internal cursor yet. I'll update those in a followup. Test Plan: For now, poked around a bit. Some stuff is broken, but normal ID-based lists load correctly and page properly. See next diff for a more detailed test plan. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13259 Differential Revision: https://secure.phabricator.com/D20291 --- src/__phutil_library_map__.php | 2 + ...PhabricatorCursorPagedPolicyAwareQuery.php | 295 ++++++++++++------ .../policy/PhabricatorPolicyAwareQuery.php | 9 +- .../query/policy/PhabricatorQueryCursor.php | 17 + 4 files changed, 221 insertions(+), 102 deletions(-) create mode 100644 src/infrastructure/query/policy/PhabricatorQueryCursor.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 82972d5a7d..661e9812c3 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4195,6 +4195,7 @@ phutil_register_library_map(array( 'PhabricatorPygmentSetupCheck' => 'applications/config/check/PhabricatorPygmentSetupCheck.php', 'PhabricatorQuery' => 'infrastructure/query/PhabricatorQuery.php', 'PhabricatorQueryConstraint' => 'infrastructure/query/constraint/PhabricatorQueryConstraint.php', + 'PhabricatorQueryCursor' => 'infrastructure/query/policy/PhabricatorQueryCursor.php', 'PhabricatorQueryIterator' => 'infrastructure/storage/lisk/PhabricatorQueryIterator.php', 'PhabricatorQueryOrderItem' => 'infrastructure/query/order/PhabricatorQueryOrderItem.php', 'PhabricatorQueryOrderTestCase' => 'infrastructure/query/order/__tests__/PhabricatorQueryOrderTestCase.php', @@ -10294,6 +10295,7 @@ phutil_register_library_map(array( 'PhabricatorPygmentSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorQuery' => 'Phobject', 'PhabricatorQueryConstraint' => 'Phobject', + 'PhabricatorQueryCursor' => 'Phobject', 'PhabricatorQueryIterator' => 'PhutilBufferedIterator', 'PhabricatorQueryOrderItem' => 'Phobject', 'PhabricatorQueryOrderTestCase' => 'PhabricatorTestCase', diff --git a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php index 773f78b3a6..1f9cc9c4d0 100644 --- a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php +++ b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php @@ -4,6 +4,7 @@ * A query class which uses cursor-based paging. This paging is much more * performant than offset-based paging in the presence of policy filtering. * + * @task cursors Query Cursors * @task clauses Building Query Clauses * @task appsearch Integration with ApplicationSearch * @task customfield Integration with CustomField @@ -15,8 +16,10 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery extends PhabricatorPolicyAwareQuery { - private $afterID; - private $beforeID; + private $externalCursorString; + private $internalCursorObject; + private $isQueryOrderReversed = false; + private $applicationSearchConstraints = array(); private $internalPaging; private $orderVector; @@ -33,54 +36,182 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery private $ferretQuery; private $ferretMetadata = array(); - protected function getPageCursors(array $page) { +/* -( Cursors )------------------------------------------------------------ */ + + protected function newExternalCursorStringForResult($object) { + if (!($object instanceof LiskDAO)) { + throw new Exception( + pht( + 'Expected to be passed a result object of class "LiskDAO" in '. + '"newExternalCursorStringForResult()", actually passed "%s". '. + 'Return storage objects from "loadPage()" or override '. + '"newExternalCursorStringForResult()".', + phutil_describe_type($object))); + } + + return (string)$object->getID(); + } + + protected function newInternalCursorFromExternalCursor($cursor) { + return $this->newInternalCursorObjectFromID($cursor); + } + + protected function newPagingMapFromCursorObject( + PhabricatorQueryCursor $cursor, + array $keys) { + + $object = $cursor->getObject(); + return array( - $this->getResultCursor(head($page)), - $this->getResultCursor(last($page)), + 'id' => (int)$object->getID(), ); } - protected function getResultCursor($object) { - if (!is_object($object)) { + final protected function newInternalCursorObjectFromID($id) { + $viewer = $this->getViewer(); + + $query = newv(get_class($this), array()); + + $query + ->setParentQuery($this) + ->setViewer($viewer) + ->withIDs(array((int)$id)); + + // We're copying our order vector to the subquery so that the subquery + // knows it should generate any supplemental information required by the + // ordering. + + // For example, Phriction documents may be ordered by title, but the title + // isn't a column in the "document" table: the query must JOIN the + // "content" table to perform the ordering. Passing the ordering to the + // subquery tells it that we need it to do that JOIN and attach relevant + // paging information to the internal cursor object. + + // We only expect to load a single result, so the actual result order does + // not matter. We only want the internal cursor for that result to look + // like a cursor this parent query would generate. + $query->setOrderVector($this->getOrderVector()); + + // We're executing the subquery normally to make sure the viewer can + // actually see the object, and that it's a completely valid object which + // passes all filtering and policy checks. You aren't allowed to use an + // object you can't see as a cursor, since this can leak information. + $result = $query->executeOne(); + if (!$result) { + // TODO: Raise a more tailored exception here and make the UI a little + // prettier? throw new Exception( pht( - 'Expected object, got "%s".', - gettype($object))); + 'Cursor "%s" does not identify a valid object in query "%s".', + $id, + get_class($this))); } - return $object->getID(); + // Now that we made sure the viewer can actually see the object the + // external cursor identifies, return the internal cursor the query + // generated as a side effect while loading the object. + return $query->getInternalCursorObject(); } - protected function nextPage(array $page) { - // See getPagingViewer() for a description of this flag. - $this->internalPaging = true; + final private function getExternalCursorStringForResult($object) { + $cursor = $this->newExternalCursorStringForResult($object); - if ($this->beforeID !== null) { - $page = array_reverse($page, $preserve_keys = true); - list($before, $after) = $this->getPageCursors($page); - $this->beforeID = $before; - } else { - list($before, $after) = $this->getPageCursors($page); - $this->afterID = $after; + if (!is_string($cursor)) { + throw new Exception( + pht( + 'Expected "newExternalCursorStringForResult()" in class "%s" to '. + 'return a string, but got "%s".', + get_class($this), + phutil_describe_type($cursor))); } + + return $cursor; } - final public function setAfterID($object_id) { - $this->afterID = $object_id; + final private function getExternalCursorString() { + return $this->externalCursorString; + } + + final private function setExternalCursorString($external_cursor) { + $this->externalCursorString = $external_cursor; return $this; } - final protected function getAfterID() { - return $this->afterID; + final private function getIsQueryOrderReversed() { + return $this->isQueryOrderReversed; } - final public function setBeforeID($object_id) { - $this->beforeID = $object_id; + final private function setIsQueryOrderReversed($is_reversed) { + $this->isQueryOrderReversed = $is_reversed; return $this; } - final protected function getBeforeID() { - return $this->beforeID; + final private function getInternalCursorObject() { + return $this->internalCursorObject; + } + + final private function setInternalCursorObject( + PhabricatorQueryCursor $cursor) { + $this->internalCursorObject = $cursor; + return $this; + } + + final private function getInternalCursorFromExternalCursor( + $cursor_string) { + + $cursor_object = $this->newInternalCursorFromExternalCursor($cursor_string); + + if (!($cursor_object instanceof PhabricatorQueryCursor)) { + throw new Exception( + pht( + 'Expected "newInternalCursorFromExternalCursor()" to return an '. + 'object of class "PhabricatorQueryCursor", but got "%s" (in '. + 'class "%s").', + phutil_describe_type($cursor_object), + get_class($this))); + } + + return $cursor_object; + } + + final private function getPagingMapFromCursorObject( + PhabricatorQueryCursor $cursor, + array $keys) { + + $map = $this->newPagingMapFromCursorObject($cursor, $keys); + + if (!is_array($map)) { + throw new Exception( + pht( + 'Expected "newPagingMapFromCursorObject()" to return a map of '. + 'paging values, but got "%s" (in class "%s").', + phutil_describe_type($map), + get_class($this))); + } + + foreach ($keys as $key) { + if (!array_key_exists($key, $map)) { + throw new Exception( + pht( + 'Map returned by "newPagingMapFromCursorObject()" in class "%s" '. + 'omits required key "%s".', + get_class($this), + $key)); + } + } + + return $map; + } + + final protected function nextPage(array $page) { + if (!$page) { + return; + } + + $cursor = id(new PhabricatorQueryCursor()) + ->setObject(last($page)); + + $this->setInternalCursorObject($cursor); } final public function getFerretMetadata() { @@ -218,7 +349,7 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery } final protected function didLoadResults(array $results) { - if ($this->beforeID) { + if ($this->getIsQueryOrderReversed()) { $results = array_reverse($results, $preserve_keys = true); } @@ -230,10 +361,11 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery $this->setLimit($limit + 1); - if ($pager->getAfterID()) { - $this->setAfterID($pager->getAfterID()); + if (strlen($pager->getAfterID())) { + $this->setExternalCursorString($pager->getAfterID()); } else if ($pager->getBeforeID()) { - $this->setBeforeID($pager->getBeforeID()); + $this->setExternalCursorString($pager->getBeforeID()); + $this->setIsQueryOrderReversed(true); } $results = $this->execute(); @@ -241,15 +373,22 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery $sliced_results = $pager->sliceResults($results); if ($sliced_results) { - list($before, $after) = $this->getPageCursors($sliced_results); + + // If we have results, generate external-facing cursors from the visible + // results. This stops us from leaking any internal details about objects + // which we loaded but which were not visible to the viewer. if ($pager->getBeforeID() || ($count > $limit)) { - $pager->setNextPageID($after); + $last_object = last($sliced_results); + $cursor = $this->getExternalCursorStringForResult($last_object); + $pager->setNextPageID($cursor); } if ($pager->getAfterID() || ($pager->getBeforeID() && ($count > $limit))) { - $pager->setPrevPageID($before); + $head_object = head($sliced_results); + $cursor = $this->getExternalCursorStringForResult($head_object); + $pager->setPrevPageID($cursor); } } @@ -423,38 +562,39 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery $orderable = $this->getOrderableColumns(); $vector = $this->getOrderVector(); - if ($this->beforeID !== null) { - $cursor = $this->beforeID; - $reversed = true; - } else if ($this->afterID !== null) { - $cursor = $this->afterID; - $reversed = false; - } else { - // No paging is being applied to this query so we do not need to - // construct a paging clause. + // If we don't have a cursor object yet, it means we're trying to load + // the first result page. We may need to build a cursor object from the + // external string, or we may not need a paging clause yet. + $cursor_object = $this->getInternalCursorObject(); + if (!$cursor_object) { + $external_cursor = $this->getExternalCursorString(); + if ($external_cursor !== null) { + $cursor_object = $this->getInternalCursorFromExternalCursor( + $external_cursor); + } + } + + // If we still don't have a cursor object, this is the first result page + // and we aren't paging it. We don't need to build a paging clause. + if (!$cursor_object) { return qsprintf($conn, ''); } + $reversed = $this->getIsQueryOrderReversed(); + $keys = array(); foreach ($vector as $order) { $keys[] = $order->getOrderKey(); } - $value_map = $this->getPagingValueMap($cursor, $keys); + $value_map = $this->getPagingMapFromCursorObject( + $cursor_object, + $keys); $columns = array(); foreach ($vector as $order) { $key = $order->getOrderKey(); - if (!array_key_exists($key, $value_map)) { - throw new Exception( - pht( - 'Query "%s" failed to return a value from getPagingValueMap() '. - 'for column "%s".', - get_class($this), - $key)); - } - $column = $orderable[$key]; $column['value'] = $value_map[$key]; @@ -476,48 +616,6 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery } - /** - * @task paging - */ - protected function getPagingValueMap($cursor, array $keys) { - return array( - 'id' => $cursor, - ); - } - - - /** - * @task paging - */ - protected function loadCursorObject($cursor) { - $query = newv(get_class($this), array()) - ->setViewer($this->getPagingViewer()) - ->withIDs(array((int)$cursor)); - - $this->willExecuteCursorQuery($query); - - $object = $query->executeOne(); - if (!$object) { - throw new Exception( - pht( - 'Cursor "%s" does not identify a valid object in query "%s".', - $cursor, - get_class($this))); - } - - return $object; - } - - - /** - * @task paging - */ - protected function willExecuteCursorQuery( - PhabricatorCursorPagedPolicyAwareQuery $query) { - return; - } - - /** * Simplifies the task of constructing a paging clause across multiple * columns. In the general case, this looks like: @@ -1072,10 +1170,7 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery array $parts, $for_union = false) { - $is_query_reversed = false; - if ($this->getBeforeID()) { - $is_query_reversed = !$is_query_reversed; - } + $is_query_reversed = $this->getIsQueryOrderReversed(); $sql = array(); foreach ($parts as $key => $part) { diff --git a/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php index 7aa0f28dfe..5561f374fb 100644 --- a/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php +++ b/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php @@ -282,6 +282,13 @@ abstract class PhabricatorPolicyAwareQuery extends PhabricatorOffsetPagedQuery { $this->didFilterResults($removed); + // NOTE: We call "nextPage()" before checking if we've found enough + // results because we want to build the internal cursor object even + // if we don't need to execute another query: the internal cursor may + // be used by a parent query that is using this query to translate an + // external cursor into an internal cursor. + $this->nextPage($page); + foreach ($visible as $key => $result) { ++$count; @@ -312,8 +319,6 @@ abstract class PhabricatorPolicyAwareQuery extends PhabricatorOffsetPagedQuery { break; } - $this->nextPage($page); - if ($overheat_limit && ($total_seen >= $overheat_limit)) { $this->isOverheated = true; break; diff --git a/src/infrastructure/query/policy/PhabricatorQueryCursor.php b/src/infrastructure/query/policy/PhabricatorQueryCursor.php new file mode 100644 index 0000000000..f2d183ff12 --- /dev/null +++ b/src/infrastructure/query/policy/PhabricatorQueryCursor.php @@ -0,0 +1,17 @@ +object = $object; + return $this; + } + + public function getObject() { + return $this->object; + } + +} From d4847c3eeb787a80c67f297720d79a2b7281bb8e Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 18 Mar 2019 11:44:54 -0700 Subject: [PATCH 170/245] Convert simple query subclasses to use internal cursors Summary: Depends on D20291. Ref T13259. Move all the simple cases (where paging depends only on the partial object and does not depend on keys) to a simple wrapper. This leaves a smaller set of more complex cases where we care about external data or which keys were requested that I'll convert in followups. Test Plan: Poked at things, but a lot of stuff is still broken until everything is converted. Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13259 Differential Revision: https://secure.phabricator.com/D20292 --- .../almanac/query/AlmanacDeviceQuery.php | 7 +++---- .../almanac/query/AlmanacNamespaceQuery.php | 7 +++---- .../almanac/query/AlmanacServiceQuery.php | 7 +++---- .../badges/query/PhabricatorBadgesQuery.php | 8 ++++---- .../calendar/query/PhabricatorCalendarEventQuery.php | 7 +++---- .../countdown/query/PhabricatorCountdownQuery.php | 7 +++---- .../differential/query/DifferentialRevisionQuery.php | 7 +++---- .../diffusion/query/DiffusionCommitQuery.php | 7 +++---- src/applications/diviner/query/DivinerBookQuery.php | 7 +++---- .../query/HarbormasterBuildPlanQuery.php | 7 +++---- .../macro/query/PhabricatorMacroQuery.php | 7 +++---- .../owners/query/PhabricatorOwnersPackageQuery.php | 7 +++---- .../people/query/PhabricatorPeopleQuery.php | 7 +++---- src/applications/phame/query/PhamePostQuery.php | 12 ++++-------- src/applications/phlux/query/PhluxVariableQuery.php | 4 ++-- .../phrequent/query/PhrequentUserTimeQuery.php | 9 ++++----- .../phurl/query/PhabricatorPhurlURLQuery.php | 7 ------- .../project/query/PhabricatorProjectQuery.php | 9 ++++----- .../releeph/query/ReleephProductQuery.php | 8 +++----- .../PhabricatorCursorPagedPolicyAwareQuery.php | 4 ++++ 20 files changed, 61 insertions(+), 84 deletions(-) diff --git a/src/applications/almanac/query/AlmanacDeviceQuery.php b/src/applications/almanac/query/AlmanacDeviceQuery.php index 0d38070e0b..21f8c952ef 100644 --- a/src/applications/almanac/query/AlmanacDeviceQuery.php +++ b/src/applications/almanac/query/AlmanacDeviceQuery.php @@ -122,11 +122,10 @@ final class AlmanacDeviceQuery ); } - protected function getPagingValueMap($cursor, array $keys) { - $device = $this->loadCursorObject($cursor); + protected function newPagingMapFromPartialObject($object) { return array( - 'id' => $device->getID(), - 'name' => $device->getName(), + 'id' => (int)$object->getID(), + 'name' => $object->getName(), ); } diff --git a/src/applications/almanac/query/AlmanacNamespaceQuery.php b/src/applications/almanac/query/AlmanacNamespaceQuery.php index 81332cf03b..d4378e17c7 100644 --- a/src/applications/almanac/query/AlmanacNamespaceQuery.php +++ b/src/applications/almanac/query/AlmanacNamespaceQuery.php @@ -79,11 +79,10 @@ final class AlmanacNamespaceQuery ); } - protected function getPagingValueMap($cursor, array $keys) { - $namespace = $this->loadCursorObject($cursor); + protected function newPagingMapFromPartialObject($object) { return array( - 'id' => $namespace->getID(), - 'name' => $namespace->getName(), + 'id' => (int)$object->getID(), + 'name' => $object->getName(), ); } diff --git a/src/applications/almanac/query/AlmanacServiceQuery.php b/src/applications/almanac/query/AlmanacServiceQuery.php index 3374413e5b..edc55276a9 100644 --- a/src/applications/almanac/query/AlmanacServiceQuery.php +++ b/src/applications/almanac/query/AlmanacServiceQuery.php @@ -206,11 +206,10 @@ final class AlmanacServiceQuery ); } - protected function getPagingValueMap($cursor, array $keys) { - $service = $this->loadCursorObject($cursor); + protected function newPagingMapFromPartialObject($object) { return array( - 'id' => $service->getID(), - 'name' => $service->getName(), + 'id' => (int)$object->getID(), + 'name' => $object->getName(), ); } diff --git a/src/applications/badges/query/PhabricatorBadgesQuery.php b/src/applications/badges/query/PhabricatorBadgesQuery.php index c977e3f826..dcadf881fe 100644 --- a/src/applications/badges/query/PhabricatorBadgesQuery.php +++ b/src/applications/badges/query/PhabricatorBadgesQuery.php @@ -108,11 +108,11 @@ final class PhabricatorBadgesQuery ) + parent::getOrderableColumns(); } - protected function getPagingValueMap($cursor, array $keys) { - $badge = $this->loadCursorObject($cursor); + + protected function newPagingMapFromPartialObject($object) { return array( - 'quality' => $badge->getQuality(), - 'id' => $badge->getID(), + 'id' => (int)$object->getID(), + 'quality' => $object->getQuality(), ); } diff --git a/src/applications/calendar/query/PhabricatorCalendarEventQuery.php b/src/applications/calendar/query/PhabricatorCalendarEventQuery.php index fc1399fdb3..db50bb4d77 100644 --- a/src/applications/calendar/query/PhabricatorCalendarEventQuery.php +++ b/src/applications/calendar/query/PhabricatorCalendarEventQuery.php @@ -140,11 +140,10 @@ final class PhabricatorCalendarEventQuery ) + parent::getOrderableColumns(); } - protected function getPagingValueMap($cursor, array $keys) { - $event = $this->loadCursorObject($cursor); + protected function newPagingMapFromPartialObject($object) { return array( - 'start' => $event->getStartDateTimeEpoch(), - 'id' => $event->getID(), + 'id' => (int)$object->getID(), + 'start' => (int)$object->getStartDateTimeEpoch(), ); } diff --git a/src/applications/countdown/query/PhabricatorCountdownQuery.php b/src/applications/countdown/query/PhabricatorCountdownQuery.php index e6c410ee49..67a2f3a9e3 100644 --- a/src/applications/countdown/query/PhabricatorCountdownQuery.php +++ b/src/applications/countdown/query/PhabricatorCountdownQuery.php @@ -97,11 +97,10 @@ final class PhabricatorCountdownQuery ) + parent::getOrderableColumns(); } - protected function getPagingValueMap($cursor, array $keys) { - $countdown = $this->loadCursorObject($cursor); + protected function newPagingMapFromPartialObject($object) { return array( - 'epoch' => $countdown->getEpoch(), - 'id' => $countdown->getID(), + 'id' => (int)$object->getID(), + 'epoch' => (int)$object->getEpoch(), ); } diff --git a/src/applications/differential/query/DifferentialRevisionQuery.php b/src/applications/differential/query/DifferentialRevisionQuery.php index fdd4904bee..a385fc5252 100644 --- a/src/applications/differential/query/DifferentialRevisionQuery.php +++ b/src/applications/differential/query/DifferentialRevisionQuery.php @@ -800,11 +800,10 @@ final class DifferentialRevisionQuery ) + parent::getOrderableColumns(); } - protected function getPagingValueMap($cursor, array $keys) { - $revision = $this->loadCursorObject($cursor); + protected function newPagingMapFromPartialObject($object) { return array( - 'id' => $revision->getID(), - 'updated' => $revision->getDateModified(), + 'id' => (int)$object->getID(), + 'updated' => (int)$object->getDateModified(), ); } diff --git a/src/applications/diffusion/query/DiffusionCommitQuery.php b/src/applications/diffusion/query/DiffusionCommitQuery.php index 03eb0b9edf..bc555a4fb4 100644 --- a/src/applications/diffusion/query/DiffusionCommitQuery.php +++ b/src/applications/diffusion/query/DiffusionCommitQuery.php @@ -924,11 +924,10 @@ final class DiffusionCommitQuery ); } - protected function getPagingValueMap($cursor, array $keys) { - $commit = $this->loadCursorObject($cursor); + protected function newPagingMapFromPartialObject($object) { return array( - 'id' => $commit->getID(), - 'epoch' => $commit->getEpoch(), + 'id' => (int)$object->getID(), + 'epoch' => (int)$object->getEpoch(), ); } diff --git a/src/applications/diviner/query/DivinerBookQuery.php b/src/applications/diviner/query/DivinerBookQuery.php index d540d971b0..2d6527ec96 100644 --- a/src/applications/diviner/query/DivinerBookQuery.php +++ b/src/applications/diviner/query/DivinerBookQuery.php @@ -181,11 +181,10 @@ final class DivinerBookQuery extends PhabricatorCursorPagedPolicyAwareQuery { ); } - protected function getPagingValueMap($cursor, array $keys) { - $book = $this->loadCursorObject($cursor); - + protected function newPagingMapFromPartialObject($object) { return array( - 'name' => $book->getName(), + 'id' => (int)$object->getID(), + 'name' => $object->getName(), ); } diff --git a/src/applications/harbormaster/query/HarbormasterBuildPlanQuery.php b/src/applications/harbormaster/query/HarbormasterBuildPlanQuery.php index 4058325140..c903fbb37f 100644 --- a/src/applications/harbormaster/query/HarbormasterBuildPlanQuery.php +++ b/src/applications/harbormaster/query/HarbormasterBuildPlanQuery.php @@ -133,11 +133,10 @@ final class HarbormasterBuildPlanQuery ); } - protected function getPagingValueMap($cursor, array $keys) { - $plan = $this->loadCursorObject($cursor); + protected function newPagingMapFromPartialObject($object) { return array( - 'id' => $plan->getID(), - 'name' => $plan->getName(), + 'id' => (int)$object->getID(), + 'name' => $object->getName(), ); } diff --git a/src/applications/macro/query/PhabricatorMacroQuery.php b/src/applications/macro/query/PhabricatorMacroQuery.php index 3ba30502d5..7635b68b73 100644 --- a/src/applications/macro/query/PhabricatorMacroQuery.php +++ b/src/applications/macro/query/PhabricatorMacroQuery.php @@ -249,11 +249,10 @@ final class PhabricatorMacroQuery ); } - protected function getPagingValueMap($cursor, array $keys) { - $macro = $this->loadCursorObject($cursor); + protected function newPagingMapFromPartialObject($object) { return array( - 'id' => $macro->getID(), - 'name' => $macro->getName(), + 'id' => (int)$object->getID(), + 'name' => $object->getName(), ); } diff --git a/src/applications/owners/query/PhabricatorOwnersPackageQuery.php b/src/applications/owners/query/PhabricatorOwnersPackageQuery.php index 6d6ccb2ed2..67b4836a5a 100644 --- a/src/applications/owners/query/PhabricatorOwnersPackageQuery.php +++ b/src/applications/owners/query/PhabricatorOwnersPackageQuery.php @@ -267,11 +267,10 @@ final class PhabricatorOwnersPackageQuery ); } - protected function getPagingValueMap($cursor, array $keys) { - $package = $this->loadCursorObject($cursor); + protected function newPagingMapFromPartialObject($object) { return array( - 'id' => $package->getID(), - 'name' => $package->getName(), + 'id' => (int)$object->getID(), + 'name' => $object->getName(), ); } diff --git a/src/applications/people/query/PhabricatorPeopleQuery.php b/src/applications/people/query/PhabricatorPeopleQuery.php index 542b685e29..5e737aaf90 100644 --- a/src/applications/people/query/PhabricatorPeopleQuery.php +++ b/src/applications/people/query/PhabricatorPeopleQuery.php @@ -379,11 +379,10 @@ final class PhabricatorPeopleQuery ); } - protected function getPagingValueMap($cursor, array $keys) { - $user = $this->loadCursorObject($cursor); + protected function newPagingMapFromPartialObject($object) { return array( - 'id' => $user->getID(), - 'username' => $user->getUsername(), + 'id' => (int)$object->getID(), + 'username' => $object->getUsername(), ); } diff --git a/src/applications/phame/query/PhamePostQuery.php b/src/applications/phame/query/PhamePostQuery.php index 85ef470cea..d7396e553f 100644 --- a/src/applications/phame/query/PhamePostQuery.php +++ b/src/applications/phame/query/PhamePostQuery.php @@ -171,15 +171,11 @@ final class PhamePostQuery extends PhabricatorCursorPagedPolicyAwareQuery { ); } - protected function getPagingValueMap($cursor, array $keys) { - $post = $this->loadCursorObject($cursor); - - $map = array( - 'datePublished' => $post->getDatePublished(), - 'id' => $post->getID(), + protected function newPagingMapFromPartialObject($object) { + return array( + 'id' => (int)$object->getID(), + 'datePublished' => (int)$object->getDatePublished(), ); - - return $map; } public function getQueryApplicationClass() { diff --git a/src/applications/phlux/query/PhluxVariableQuery.php b/src/applications/phlux/query/PhluxVariableQuery.php index 75abd044d0..8ec4bc9334 100644 --- a/src/applications/phlux/query/PhluxVariableQuery.php +++ b/src/applications/phlux/query/PhluxVariableQuery.php @@ -81,9 +81,9 @@ final class PhluxVariableQuery ); } - protected function getPagingValueMap($cursor, array $keys) { - $object = $this->loadCursorObject($cursor); + protected function newPagingMapFromPartialObject($object) { return array( + 'id' => (int)$object->getID(), 'key' => $object->getVariableKey(), ); } diff --git a/src/applications/phrequent/query/PhrequentUserTimeQuery.php b/src/applications/phrequent/query/PhrequentUserTimeQuery.php index cf5122c020..6400771a00 100644 --- a/src/applications/phrequent/query/PhrequentUserTimeQuery.php +++ b/src/applications/phrequent/query/PhrequentUserTimeQuery.php @@ -133,12 +133,11 @@ final class PhrequentUserTimeQuery ); } - protected function getPagingValueMap($cursor, array $keys) { - $usertime = $this->loadCursorObject($cursor); + protected function newPagingMapFromPartialObject($object) { return array( - 'id' => $usertime->getID(), - 'start' => $usertime->getDateStarted(), - 'end' => $usertime->getDateEnded(), + 'id' => (int)$object->getID(), + 'start' => (int)$object->getDateStarted(), + 'end' => (int)$object->getDateEnded(), ); } diff --git a/src/applications/phurl/query/PhabricatorPhurlURLQuery.php b/src/applications/phurl/query/PhabricatorPhurlURLQuery.php index 74cced0771..6efbbd5b4c 100644 --- a/src/applications/phurl/query/PhabricatorPhurlURLQuery.php +++ b/src/applications/phurl/query/PhabricatorPhurlURLQuery.php @@ -50,13 +50,6 @@ final class PhabricatorPhurlURLQuery return $this; } - protected function getPagingValueMap($cursor, array $keys) { - $url = $this->loadCursorObject($cursor); - return array( - 'id' => $url->getID(), - ); - } - protected function loadPage() { return $this->loadStandardPage($this->newResultObject()); } diff --git a/src/applications/project/query/PhabricatorProjectQuery.php b/src/applications/project/query/PhabricatorProjectQuery.php index f6087f7d2a..b08a58501f 100644 --- a/src/applications/project/query/PhabricatorProjectQuery.php +++ b/src/applications/project/query/PhabricatorProjectQuery.php @@ -201,12 +201,11 @@ final class PhabricatorProjectQuery ); } - protected function getPagingValueMap($cursor, array $keys) { - $project = $this->loadCursorObject($cursor); + protected function newPagingMapFromPartialObject($object) { return array( - 'id' => $project->getID(), - 'name' => $project->getName(), - 'status' => $project->getStatus(), + 'id' => (int)$object->getID(), + 'name' => $object->getName(), + 'status' => $object->getStatus(), ); } diff --git a/src/applications/releeph/query/ReleephProductQuery.php b/src/applications/releeph/query/ReleephProductQuery.php index c039950379..118b9919a8 100644 --- a/src/applications/releeph/query/ReleephProductQuery.php +++ b/src/applications/releeph/query/ReleephProductQuery.php @@ -130,12 +130,10 @@ final class ReleephProductQuery ); } - protected function getPagingValueMap($cursor, array $keys) { - $product = $this->loadCursorObject($cursor); - + protected function newPagingMapFromPartialObject($object) { return array( - 'id' => $product->getID(), - 'name' => $product->getName(), + 'id' => (int)$object->getID(), + 'name' => $object->getName(), ); } diff --git a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php index 1f9cc9c4d0..def502e11f 100644 --- a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php +++ b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php @@ -62,6 +62,10 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery $object = $cursor->getObject(); + return $this->newPagingMapFromPartialObject($object); + } + + protected function newPagingMapFromPartialObject($object) { return array( 'id' => (int)$object->getID(), ); From 8449c1793aced4bb22ddfbe021e56096b4eb0eb3 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 18 Mar 2019 13:55:57 -0700 Subject: [PATCH 171/245] Convert complex query subclasses to use internal cursors Summary: Depends on D20292. Ref T13259. This converts the rest of the `getPagingValueMap()` callsites to operate on internal cursors instead. These are pretty one-off for the most part, so I'll annotate them inline. Test Plan: - Grouped tasks by project, sorted by title, paged through them, saw consistent outcomes. - Queried edges with "edge.search", paged through them using the "after" cursor. - Poked around the other stuff without catching any brokenness. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13259 Differential Revision: https://secure.phabricator.com/D20293 --- .../almanac/query/AlmanacInterfaceQuery.php | 25 ++- .../feed/query/PhabricatorFeedQuery.php | 22 +-- .../maniphest/query/ManiphestTaskQuery.php | 138 ++++++++++++---- .../query/PhrictionDocumentQuery.php | 44 +++--- .../query/PhabricatorRepositoryQuery.php | 47 ++---- .../edges/conduit/PhabricatorEdgeObject.php | 21 ++- .../query/PhabricatorEdgeObjectQuery.php | 53 +++++-- ...PhabricatorCursorPagedPolicyAwareQuery.php | 147 ++++++++---------- .../query/policy/PhabricatorQueryCursor.php | 30 ++++ 9 files changed, 316 insertions(+), 211 deletions(-) diff --git a/src/applications/almanac/query/AlmanacInterfaceQuery.php b/src/applications/almanac/query/AlmanacInterfaceQuery.php index d5886761c1..5738108ffc 100644 --- a/src/applications/almanac/query/AlmanacInterfaceQuery.php +++ b/src/applications/almanac/query/AlmanacInterfaceQuery.php @@ -78,6 +78,16 @@ final class AlmanacInterfaceQuery return $interfaces; } + protected function buildSelectClauseParts(AphrontDatabaseConnection $conn) { + $select = parent::buildSelectClauseParts($conn); + + if ($this->shouldJoinDeviceTable()) { + $select[] = qsprintf($conn, 'device.name'); + } + + return $select; + } + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); @@ -186,15 +196,16 @@ final class AlmanacInterfaceQuery ); } - protected function getPagingValueMap($cursor, array $keys) { - $interface = $this->loadCursorObject($cursor); + protected function newPagingMapFromCursorObject( + PhabricatorQueryCursor $cursor, + array $keys) { - $map = array( - 'id' => $interface->getID(), - 'name' => $interface->getDevice()->getName(), + $interface = $cursor->getObject(); + + return array( + 'id' => (int)$interface->getID(), + 'name' => $cursor->getRawRowProperty('device.name'), ); - - return $map; } } diff --git a/src/applications/feed/query/PhabricatorFeedQuery.php b/src/applications/feed/query/PhabricatorFeedQuery.php index a35f14da57..8302af20c1 100644 --- a/src/applications/feed/query/PhabricatorFeedQuery.php +++ b/src/applications/feed/query/PhabricatorFeedQuery.php @@ -147,17 +147,21 @@ final class PhabricatorFeedQuery ); } - protected function getPagingValueMap($cursor, array $keys) { - return array( - 'key' => $cursor, - ); + protected function applyExternalCursorConstraintsToQuery( + PhabricatorCursorPagedPolicyAwareQuery $subquery, + $cursor) { + $subquery->withChronologicalKeys(array($cursor)); } - protected function getResultCursor($item) { - if ($item instanceof PhabricatorFeedStory) { - return $item->getChronologicalKey(); - } - return $item['chronologicalKey']; + protected function newExternalCursorStringForResult($object) { + return $object->getChronologicalKey(); + } + + protected function newPagingMapFromPartialObject($object) { + // This query is unusual, and the "object" is a raw result row. + return array( + 'key' => $object['chronologicalKey'], + ); } protected function getPrimaryTableAlias() { diff --git a/src/applications/maniphest/query/ManiphestTaskQuery.php b/src/applications/maniphest/query/ManiphestTaskQuery.php index 1033bfe333..9e58728cff 100644 --- a/src/applications/maniphest/query/ManiphestTaskQuery.php +++ b/src/applications/maniphest/query/ManiphestTaskQuery.php @@ -27,6 +27,7 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery { private $closedEpochMax; private $closerPHIDs; private $columnPHIDs; + private $specificGroupByProjectPHID; private $status = 'status-any'; const STATUS_ANY = 'status-any'; @@ -227,6 +228,11 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery { return $this; } + public function withSpecificGroupByProjectPHID($project_phid) { + $this->specificGroupByProjectPHID = $project_phid; + return $this; + } + public function newResultObject() { return new ManiphestTask(); } @@ -534,6 +540,13 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery { $select_phids); } + if ($this->specificGroupByProjectPHID !== null) { + $where[] = qsprintf( + $conn, + 'projectGroupName.indexedObjectPHID = %s', + $this->specificGroupByProjectPHID); + } + return $where; } @@ -824,16 +837,6 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery { return array_mergev($phids); } - protected function getResultCursor($result) { - $id = $result->getID(); - - if ($this->groupBy == self::GROUP_PROJECT) { - return rtrim($id.'.'.$result->getGroupByProjectPHID(), '.'); - } - - return $id; - } - public function getBuiltinOrders() { $orders = array( 'priority' => array( @@ -926,39 +929,37 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery { ); } - protected function getPagingValueMap($cursor, array $keys) { - $cursor_parts = explode('.', $cursor, 2); - $task_id = $cursor_parts[0]; - $group_id = idx($cursor_parts, 1); + protected function newPagingMapFromCursorObject( + PhabricatorQueryCursor $cursor, + array $keys) { - $task = $this->loadCursorObject($task_id); + $task = $cursor->getObject(); $map = array( - 'id' => $task->getID(), - 'priority' => $task->getPriority(), + 'id' => (int)$task->getID(), + 'priority' => (int)$task->getPriority(), 'owner' => $task->getOwnerOrdering(), 'status' => $task->getStatus(), 'title' => $task->getTitle(), - 'updated' => $task->getDateModified(), + 'updated' => (int)$task->getDateModified(), 'closed' => $task->getClosedEpoch(), ); - foreach ($keys as $key) { - switch ($key) { - case 'project': - $value = null; - if ($group_id) { - $paging_projects = id(new PhabricatorProjectQuery()) - ->setViewer($this->getViewer()) - ->withPHIDs(array($group_id)) - ->execute(); - if ($paging_projects) { - $value = head($paging_projects)->getName(); - } - } - $map[$key] = $value; - break; + if (isset($keys['project'])) { + $value = null; + + $group_phid = $task->getGroupByProjectPHID(); + if ($group_phid) { + $paging_projects = id(new PhabricatorProjectQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs(array($group_phid)) + ->execute(); + if ($paging_projects) { + $value = head($paging_projects)->getName(); + } } + + $map['project'] = $value; } foreach ($keys as $key) { @@ -971,6 +972,77 @@ final class ManiphestTaskQuery extends PhabricatorCursorPagedPolicyAwareQuery { return $map; } + protected function newExternalCursorStringForResult($object) { + $id = $object->getID(); + + if ($this->groupBy == self::GROUP_PROJECT) { + return rtrim($id.'.'.$object->getGroupByProjectPHID(), '.'); + } + + return $id; + } + + protected function newInternalCursorFromExternalCursor($cursor) { + list($task_id, $group_phid) = $this->parseCursor($cursor); + + $cursor_object = parent::newInternalCursorFromExternalCursor($cursor); + + if ($group_phid !== null) { + $project = id(new PhabricatorProjectQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs(array($group_phid)) + ->execute(); + + if (!$project) { + $this->throwCursorException( + pht( + 'Group PHID ("%s") component of cursor ("%s") is not valid.', + $group_phid, + $cursor)); + } + + $cursor_object->getObject()->attachGroupByProjectPHID($group_phid); + } + + return $cursor_object; + } + + protected function applyExternalCursorConstraintsToQuery( + PhabricatorCursorPagedPolicyAwareQuery $subquery, + $cursor) { + list($task_id, $group_phid) = $this->parseCursor($cursor); + + $subquery->withIDs(array($task_id)); + + if ($group_phid) { + $subquery->setGroupBy(self::GROUP_PROJECT); + + // The subquery needs to return exactly one result. If a task is in + // several projects, the query may naturally return several results. + // Specify that we want only the particular instance of the task in + // the specified project. + $subquery->withSpecificGroupByProjectPHID($group_phid); + } + } + + + private function parseCursor($cursor) { + // Split a "123.PHID-PROJ-abcd" cursor into a "Task ID" part and a + // "Project PHID" part. + + $parts = explode('.', $cursor, 2); + + if (count($parts) < 2) { + $parts[] = null; + } + + if (!strlen($parts[1])) { + $parts[1] = null; + } + + return $parts; + } + protected function getPrimaryTableAlias() { return 'task'; } diff --git a/src/applications/phriction/query/PhrictionDocumentQuery.php b/src/applications/phriction/query/PhrictionDocumentQuery.php index 5f508ad804..f05d67c4f3 100644 --- a/src/applications/phriction/query/PhrictionDocumentQuery.php +++ b/src/applications/phriction/query/PhrictionDocumentQuery.php @@ -168,10 +168,20 @@ final class PhrictionDocumentQuery return $documents; } + protected function buildSelectClauseParts(AphrontDatabaseConnection $conn) { + $select = parent::buildSelectClauseParts($conn); + + if ($this->shouldJoinContentTable()) { + $select[] = qsprintf($conn, 'c.title'); + } + + return $select; + } + protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { $joins = parent::buildJoinClauseParts($conn); - if ($this->getOrderVector()->containsKey('updated')) { + if ($this->shouldJoinContentTable()) { $content_dao = new PhrictionContent(); $joins[] = qsprintf( $conn, @@ -182,6 +192,10 @@ final class PhrictionDocumentQuery return $joins; } + private function shouldJoinContentTable() { + return $this->getOrderVector()->containsKey('updated'); + } + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); @@ -354,35 +368,25 @@ final class PhrictionDocumentQuery ); } - protected function getPagingValueMap($cursor, array $keys) { - $document = $this->loadCursorObject($cursor); + protected function newPagingMapFromCursorObject( + PhabricatorQueryCursor $cursor, + array $keys) { + + $document = $cursor->getObject(); $map = array( - 'id' => $document->getID(), + 'id' => (int)$document->getID(), 'depth' => $document->getDepth(), - 'updated' => $document->getEditedEpoch(), + 'updated' => (int)$document->getEditedEpoch(), ); - foreach ($keys as $key) { - switch ($key) { - case 'title': - $map[$key] = $document->getContent()->getTitle(); - break; - } + if (isset($keys['title'])) { + $map['title'] = $cursor->getRawRowProperty('c.title'); } return $map; } - protected function willExecuteCursorQuery( - PhabricatorCursorPagedPolicyAwareQuery $query) { - $vector = $this->getOrderVector(); - - if ($vector->containsKey('title')) { - $query->needContent(true); - } - } - protected function getPrimaryTableAlias() { return 'd'; } diff --git a/src/applications/repository/query/PhabricatorRepositoryQuery.php b/src/applications/repository/query/PhabricatorRepositoryQuery.php index 56c62cc8fb..e960cf888b 100644 --- a/src/applications/repository/query/PhabricatorRepositoryQuery.php +++ b/src/applications/repository/query/PhabricatorRepositoryQuery.php @@ -442,47 +442,24 @@ final class PhabricatorRepositoryQuery ); } - protected function willExecuteCursorQuery( - PhabricatorCursorPagedPolicyAwareQuery $query) { - $vector = $this->getOrderVector(); + protected function newPagingMapFromCursorObject( + PhabricatorQueryCursor $cursor, + array $keys) { - if ($vector->containsKey('committed')) { - $query->needMostRecentCommits(true); - } - - if ($vector->containsKey('size')) { - $query->needCommitCounts(true); - } - } - - protected function getPagingValueMap($cursor, array $keys) { - $repository = $this->loadCursorObject($cursor); + $repository = $cursor->getObject(); $map = array( - 'id' => $repository->getID(), + 'id' => (int)$repository->getID(), 'callsign' => $repository->getCallsign(), 'name' => $repository->getName(), ); - foreach ($keys as $key) { - switch ($key) { - case 'committed': - $commit = $repository->getMostRecentCommit(); - if ($commit) { - $map[$key] = $commit->getEpoch(); - } else { - $map[$key] = null; - } - break; - case 'size': - $count = $repository->getCommitCount(); - if ($count) { - $map[$key] = $count; - } else { - $map[$key] = null; - } - break; - } + if (isset($keys['committed'])) { + $map['committed'] = $cursor->getRawRowProperty('epoch'); + } + + if (isset($keys['size'])) { + $map['size'] = $cursor->getRawRowProperty('size'); } return $map; @@ -491,8 +468,6 @@ final class PhabricatorRepositoryQuery protected function buildSelectClauseParts(AphrontDatabaseConnection $conn) { $parts = parent::buildSelectClauseParts($conn); - $parts[] = qsprintf($conn, 'r.*'); - if ($this->shouldJoinSummaryTable()) { $parts[] = qsprintf($conn, 's.*'); } diff --git a/src/infrastructure/edges/conduit/PhabricatorEdgeObject.php b/src/infrastructure/edges/conduit/PhabricatorEdgeObject.php index ec5f84e59f..3183185631 100644 --- a/src/infrastructure/edges/conduit/PhabricatorEdgeObject.php +++ b/src/infrastructure/edges/conduit/PhabricatorEdgeObject.php @@ -8,14 +8,18 @@ final class PhabricatorEdgeObject private $src; private $dst; private $type; + private $dateCreated; + private $sequence; public static function newFromRow(array $row) { $edge = new self(); - $edge->id = $row['id']; - $edge->src = $row['src']; - $edge->dst = $row['dst']; - $edge->type = $row['type']; + $edge->id = idx($row, 'id'); + $edge->src = idx($row, 'src'); + $edge->dst = idx($row, 'dst'); + $edge->type = idx($row, 'type'); + $edge->dateCreated = idx($row, 'dateCreated'); + $edge->sequence = idx($row, 'seq'); return $edge; } @@ -40,6 +44,15 @@ final class PhabricatorEdgeObject return null; } + public function getDateCreated() { + return $this->dateCreated; + } + + public function getSequence() { + return $this->sequence; + } + + /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/infrastructure/edges/query/PhabricatorEdgeObjectQuery.php b/src/infrastructure/edges/query/PhabricatorEdgeObjectQuery.php index 1385215569..048a2a9fb4 100644 --- a/src/infrastructure/edges/query/PhabricatorEdgeObjectQuery.php +++ b/src/infrastructure/edges/query/PhabricatorEdgeObjectQuery.php @@ -12,7 +12,6 @@ final class PhabricatorEdgeObjectQuery private $edgeTypes; private $destinationPHIDs; - public function withSourcePHIDs(array $source_phids) { $this->sourcePHIDs = $source_phids; return $this; @@ -85,18 +84,6 @@ final class PhabricatorEdgeObjectQuery return $result; } - protected function buildSelectClauseParts(AphrontDatabaseConnection $conn) { - $parts = parent::buildSelectClauseParts($conn); - - // TODO: This is hacky, because we don't have real IDs on this table. - $parts[] = qsprintf( - $conn, - 'CONCAT(dateCreated, %s, seq) AS id', - '_'); - - return $parts; - } - protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $parts = parent::buildWhereClauseParts($conn); @@ -151,13 +138,45 @@ final class PhabricatorEdgeObjectQuery return array('dateCreated', 'sequence'); } - protected function getPagingValueMap($cursor, array $keys) { - $parts = explode('_', $cursor); + protected function newInternalCursorFromExternalCursor($cursor) { + list($epoch, $sequence) = $this->parseCursor($cursor); + // Instead of actually loading an edge, we're just making a fake edge + // with the properties the cursor describes. + + $edge_object = PhabricatorEdgeObject::newFromRow( + array( + 'dateCreated' => $epoch, + 'seq' => $sequence, + )); + + return id(new PhabricatorQueryCursor()) + ->setObject($edge_object); + } + + protected function newPagingMapFromPartialObject($object) { return array( - 'dateCreated' => $parts[0], - 'sequence' => $parts[1], + 'dateCreated' => $object->getDateCreated(), + 'sequence' => $object->getSequence(), ); } + protected function newExternalCursorStringForResult($object) { + return sprintf( + '%d_%d', + $object->getDateCreated(), + $object->getSequence()); + } + + private function parseCursor($cursor) { + if (!preg_match('/^\d+_\d+\z/', $cursor)) { + $this->throwCursorException( + pht( + 'Expected edge cursor in the form "0123_6789", got "%s".', + $cursor)); + } + + return explode('_', $cursor); + } + } diff --git a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php index def502e11f..2fe70b1ceb 100644 --- a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php +++ b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php @@ -19,6 +19,7 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery private $externalCursorString; private $internalCursorObject; private $isQueryOrderReversed = false; + private $rawCursorRow; private $applicationSearchConstraints = array(); private $internalPaging; @@ -53,7 +54,60 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery } protected function newInternalCursorFromExternalCursor($cursor) { - return $this->newInternalCursorObjectFromID($cursor); + $viewer = $this->getViewer(); + + $query = newv(get_class($this), array()); + + $query + ->setParentQuery($this) + ->setViewer($viewer); + + // We're copying our order vector to the subquery so that the subquery + // knows it should generate any supplemental information required by the + // ordering. + + // For example, Phriction documents may be ordered by title, but the title + // isn't a column in the "document" table: the query must JOIN the + // "content" table to perform the ordering. Passing the ordering to the + // subquery tells it that we need it to do that JOIN and attach relevant + // paging information to the internal cursor object. + + // We only expect to load a single result, so the actual result order does + // not matter. We only want the internal cursor for that result to look + // like a cursor this parent query would generate. + $query->setOrderVector($this->getOrderVector()); + + $this->applyExternalCursorConstraintsToQuery($query, $cursor); + + // We're executing the subquery normally to make sure the viewer can + // actually see the object, and that it's a completely valid object which + // passes all filtering and policy checks. You aren't allowed to use an + // object you can't see as a cursor, since this can leak information. + $result = $query->executeOne(); + if (!$result) { + $this->throwCursorException( + pht( + 'Cursor "%s" does not identify a valid object in query "%s".', + $cursor, + get_class($this))); + } + + // Now that we made sure the viewer can actually see the object the + // external cursor identifies, return the internal cursor the query + // generated as a side effect while loading the object. + return $query->getInternalCursorObject(); + } + + final protected function throwCursorException($message) { + // TODO: Raise a more tailored exception here and make the UI a little + // prettier? + throw new Exception($message); + } + + protected function applyExternalCursorConstraintsToQuery( + PhabricatorCursorPagedPolicyAwareQuery $subquery, + $cursor) { + $subquery->withIDs(array($cursor)); } protected function newPagingMapFromCursorObject( @@ -71,51 +125,6 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery ); } - final protected function newInternalCursorObjectFromID($id) { - $viewer = $this->getViewer(); - - $query = newv(get_class($this), array()); - - $query - ->setParentQuery($this) - ->setViewer($viewer) - ->withIDs(array((int)$id)); - - // We're copying our order vector to the subquery so that the subquery - // knows it should generate any supplemental information required by the - // ordering. - - // For example, Phriction documents may be ordered by title, but the title - // isn't a column in the "document" table: the query must JOIN the - // "content" table to perform the ordering. Passing the ordering to the - // subquery tells it that we need it to do that JOIN and attach relevant - // paging information to the internal cursor object. - - // We only expect to load a single result, so the actual result order does - // not matter. We only want the internal cursor for that result to look - // like a cursor this parent query would generate. - $query->setOrderVector($this->getOrderVector()); - - // We're executing the subquery normally to make sure the viewer can - // actually see the object, and that it's a completely valid object which - // passes all filtering and policy checks. You aren't allowed to use an - // object you can't see as a cursor, since this can leak information. - $result = $query->executeOne(); - if (!$result) { - // TODO: Raise a more tailored exception here and make the UI a little - // prettier? - throw new Exception( - pht( - 'Cursor "%s" does not identify a valid object in query "%s".', - $id, - get_class($this))); - } - - // Now that we made sure the viewer can actually see the object the - // external cursor identifies, return the internal cursor the query - // generated as a side effect while loading the object. - return $query->getInternalCursorObject(); - } final private function getExternalCursorStringForResult($object) { $cursor = $this->newExternalCursorStringForResult($object); @@ -215,6 +224,10 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery $cursor = id(new PhabricatorQueryCursor()) ->setObject(last($page)); + if ($this->rawCursorRow) { + $cursor->setRawRow($this->rawCursorRow); + } + $this->setInternalCursorObject($cursor); } @@ -295,46 +308,9 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery } } - return $rows; - } + $this->rawCursorRow = last($rows); - /** - * Get the viewer for making cursor paging queries. - * - * NOTE: You should ONLY use this viewer to load cursor objects while - * building paging queries. - * - * Cursor paging can happen in two ways. First, the user can request a page - * like `/stuff/?after=33`, which explicitly causes paging. Otherwise, we - * can fall back to implicit paging if we filter some results out of a - * result list because the user can't see them and need to go fetch some more - * results to generate a large enough result list. - * - * In the first case, want to use the viewer's policies to load the object. - * This prevents an attacker from figuring out information about an object - * they can't see by executing queries like `/stuff/?after=33&order=name`, - * which would otherwise give them a hint about the name of the object. - * Generally, if a user can't see an object, they can't use it to page. - * - * In the second case, we need to load the object whether the user can see - * it or not, because we need to examine new results. For example, if a user - * loads `/stuff/` and we run a query for the first 100 items that they can - * see, but the first 100 rows in the database aren't visible, we need to - * be able to issue a query for the next 100 results. If we can't load the - * cursor object, we'll fail or issue the same query over and over again. - * So, generally, internal paging must bypass policy controls. - * - * This method returns the appropriate viewer, based on the context in which - * the paging is occurring. - * - * @return PhabricatorUser Viewer for executing paging queries. - */ - final protected function getPagingViewer() { - if ($this->internalPaging) { - return PhabricatorUser::getOmnipotentUser(); - } else { - return $this->getViewer(); - } + return $rows; } final protected function buildLimitClause(AphrontDatabaseConnection $conn) { @@ -590,6 +566,7 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery foreach ($vector as $order) { $keys[] = $order->getOrderKey(); } + $keys = array_fuse($keys); $value_map = $this->getPagingMapFromCursorObject( $cursor_object, diff --git a/src/infrastructure/query/policy/PhabricatorQueryCursor.php b/src/infrastructure/query/policy/PhabricatorQueryCursor.php index f2d183ff12..4ec1263130 100644 --- a/src/infrastructure/query/policy/PhabricatorQueryCursor.php +++ b/src/infrastructure/query/policy/PhabricatorQueryCursor.php @@ -4,6 +4,7 @@ final class PhabricatorQueryCursor extends Phobject { private $object; + private $rawRow; public function setObject($object) { $this->object = $object; @@ -14,4 +15,33 @@ final class PhabricatorQueryCursor return $this->object; } + public function setRawRow(array $raw_row) { + $this->rawRow = $raw_row; + return $this; + } + + public function getRawRow() { + return $this->rawRow; + } + + public function getRawRowProperty($key) { + if (!is_array($this->rawRow)) { + throw new Exception( + pht( + 'Caller is trying to "getRawRowProperty()" with key "%s", but this '. + 'cursor has no raw row.', + $key)); + } + + if (!array_key_exists($key, $this->rawRow)) { + throw new Exception( + pht( + 'Caller is trying to access raw row property "%s", but the row '. + 'does not have this property.', + $key)); + } + + return $this->rawRow[$key]; + } + } From 18b444e427b07a3eebe478f05efb4094570e5648 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 18 Mar 2019 14:29:33 -0700 Subject: [PATCH 172/245] When queries overheat, raise an exception Summary: Ref T13259. Currently, queries set a flag and return a partial result set when they overheat. This is mostly okay: - It's very unusual for queries to overheat if they don't have a real viewer. - Overheating is rare in general. - In most cases where queries can overheat, the context is a SearchEngine UI, which handles this properly. In T13259, we hit a case where a query with an omnipotent viewer can overheat: if you have more than 1,000 consecutive commits in the database with invalid `repositoryID` values, we'll overheat and bail out. This is pretty bad, since we don't process everything. Change this beahvior: - Throw by default, so this stuff doesn't slip through the cracks. - Handle the SearchEngine case explicitly ("it's okay to overheat, we'll handle it"). - Make `QueryIterator` disable overheating behavior: if we're iterating over all objects, we want to hit the whole table even if most of it is garbage. There are some cases where this might cause new exception behavior that we don't necessarily want. For example, in Owners, each package shows "recent commits in this package". If you can't see the first 1,000 recent commits, you'd previously get a slow page with no results. Now you'll probably get an exception. If these crop up, I think the best approach for now is to deal with them on a case-by-case basis and see how far we get. In the "Owners" case, it might be good to query by repositories you can see first, then query by commits in the package in those repositories. That should give us a better outcome than any generic behavior we could implement. Test Plan: - Added 100000 to all repositoryID values for commits on my local install. - Before making changes, ran `bin/repository rebuild-identities --all --trace`. Saw the script process 1,000 rows and exit silently. - Applied the first part ("throw by default") and ran `bin/repository rebuild-identities`. Saw the script process 1,000 rows, then raise an exception. - Applied the second part ("disable for queryiterator") and ran the script again. Saw the script process all 15,000 rows without issues (although none are valid and none actually load). - Viewed Diffusion, saw appropriate NUX / "overheated" UIs. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13259 Differential Revision: https://secure.phabricator.com/D20294 --- ...PhabricatorApplicationSearchController.php | 3 ++ .../policy/PhabricatorPolicyAwareQuery.php | 31 +++++++++++++++++-- .../storage/lisk/PhabricatorQueryIterator.php | 2 ++ 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/applications/search/controller/PhabricatorApplicationSearchController.php b/src/applications/search/controller/PhabricatorApplicationSearchController.php index 6b43883c78..286158c4fb 100644 --- a/src/applications/search/controller/PhabricatorApplicationSearchController.php +++ b/src/applications/search/controller/PhabricatorApplicationSearchController.php @@ -249,6 +249,8 @@ final class PhabricatorApplicationSearchController $pager = $engine->newPagerForSavedQuery($saved_query); $pager->readFromRequest($request); + $query->setReturnPartialResultsOnOverheat(true); + $objects = $engine->executeQuery($query, $pager); $force_nux = $request->getBool('nux'); @@ -798,6 +800,7 @@ final class PhabricatorApplicationSearchController $object = $query ->setViewer(PhabricatorUser::getOmnipotentUser()) ->setLimit(1) + ->setReturnPartialResultsOnOverheat(true) ->execute(); if ($object) { return null; diff --git a/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php index 5561f374fb..8780584f94 100644 --- a/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php +++ b/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php @@ -45,6 +45,8 @@ abstract class PhabricatorPolicyAwareQuery extends PhabricatorOffsetPagedQuery { */ private $raisePolicyExceptions; private $isOverheated; + private $returnPartialResultsOnOverheat; + private $disableOverheating; /* -( Query Configuration )------------------------------------------------ */ @@ -130,6 +132,16 @@ abstract class PhabricatorPolicyAwareQuery extends PhabricatorOffsetPagedQuery { return $this; } + final public function setReturnPartialResultsOnOverheat($bool) { + $this->returnPartialResultsOnOverheat = $bool; + return $this; + } + + final public function setDisableOverheating($disable_overheating) { + $this->disableOverheating = $disable_overheating; + return $this; + } + /* -( Query Execution )---------------------------------------------------- */ @@ -319,9 +331,22 @@ abstract class PhabricatorPolicyAwareQuery extends PhabricatorOffsetPagedQuery { break; } - if ($overheat_limit && ($total_seen >= $overheat_limit)) { - $this->isOverheated = true; - break; + if (!$this->disableOverheating) { + if ($overheat_limit && ($total_seen >= $overheat_limit)) { + $this->isOverheated = true; + + if (!$this->returnPartialResultsOnOverheat) { + throw new Exception( + pht( + 'Query (of class "%s") overheated: examined more than %s '. + 'raw rows without finding %s visible objects.', + get_class($this), + new PhutilNumber($overheat_limit), + new PhutilNumber($need))); + } + + break; + } } } while (true); diff --git a/src/infrastructure/storage/lisk/PhabricatorQueryIterator.php b/src/infrastructure/storage/lisk/PhabricatorQueryIterator.php index 03aaf5707e..648b83863a 100644 --- a/src/infrastructure/storage/lisk/PhabricatorQueryIterator.php +++ b/src/infrastructure/storage/lisk/PhabricatorQueryIterator.php @@ -25,6 +25,8 @@ final class PhabricatorQueryIterator extends PhutilBufferedIterator { $pager = clone $this->pager; $query = clone $this->query; + $query->setDisableOverheating(true); + $results = $query->executeWithCursorPager($pager); // If we got less than a full page of results, this was the last set of From 3940c8e1f43ae58ac11081dae414c9f42ffb68c5 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 18 Mar 2019 14:49:32 -0700 Subject: [PATCH 173/245] Make the UI when you use an invalid cursor ("?after=19874189471232892") a little nicer Summary: Ref T13259. Currently, visiting a page that executes a query with an invalid cursor raises a bare exception that escapes to top level. Catch this a little sooner and tailor the page a bit. Test Plan: Visited `/maniphest/?after=335234234223`, saw a nicer exception page. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13259 Differential Revision: https://secure.phabricator.com/D20295 --- src/__phutil_library_map__.php | 4 +++- .../controller/PhabricatorApplicationSearchController.php | 2 ++ .../query/{ => exception}/PhabricatorEmptyQueryException.php | 0 .../exception/PhabricatorInvalidQueryCursorException.php | 4 ++++ .../query/policy/PhabricatorCursorPagedPolicyAwareQuery.php | 4 +--- 5 files changed, 10 insertions(+), 4 deletions(-) rename src/infrastructure/query/{ => exception}/PhabricatorEmptyQueryException.php (100%) create mode 100644 src/infrastructure/query/exception/PhabricatorInvalidQueryCursorException.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 661e9812c3..5725b5330b 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -3115,7 +3115,7 @@ phutil_register_library_map(array( 'PhabricatorEmojiDatasource' => 'applications/macro/typeahead/PhabricatorEmojiDatasource.php', 'PhabricatorEmojiRemarkupRule' => 'applications/macro/markup/PhabricatorEmojiRemarkupRule.php', 'PhabricatorEmojiTranslation' => 'infrastructure/internationalization/translation/PhabricatorEmojiTranslation.php', - 'PhabricatorEmptyQueryException' => 'infrastructure/query/PhabricatorEmptyQueryException.php', + 'PhabricatorEmptyQueryException' => 'infrastructure/query/exception/PhabricatorEmptyQueryException.php', 'PhabricatorEnumConfigType' => 'applications/config/type/PhabricatorEnumConfigType.php', 'PhabricatorEnv' => 'infrastructure/env/PhabricatorEnv.php', 'PhabricatorEnvTestCase' => 'infrastructure/env/__tests__/PhabricatorEnvTestCase.php', @@ -3392,6 +3392,7 @@ phutil_register_library_map(array( 'PhabricatorInternationalizationManagementExtractWorkflow' => 'infrastructure/internationalization/management/PhabricatorInternationalizationManagementExtractWorkflow.php', 'PhabricatorInternationalizationManagementWorkflow' => 'infrastructure/internationalization/management/PhabricatorInternationalizationManagementWorkflow.php', 'PhabricatorInvalidConfigSetupCheck' => 'applications/config/check/PhabricatorInvalidConfigSetupCheck.php', + 'PhabricatorInvalidQueryCursorException' => 'infrastructure/query/exception/PhabricatorInvalidQueryCursorException.php', 'PhabricatorIteratedMD5PasswordHasher' => 'infrastructure/util/password/PhabricatorIteratedMD5PasswordHasher.php', 'PhabricatorIteratedMD5PasswordHasherTestCase' => 'infrastructure/util/password/__tests__/PhabricatorIteratedMD5PasswordHasherTestCase.php', 'PhabricatorIteratorFileUploadSource' => 'applications/files/uploadsource/PhabricatorIteratorFileUploadSource.php', @@ -9355,6 +9356,7 @@ phutil_register_library_map(array( 'PhabricatorInternationalizationManagementExtractWorkflow' => 'PhabricatorInternationalizationManagementWorkflow', 'PhabricatorInternationalizationManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorInvalidConfigSetupCheck' => 'PhabricatorSetupCheck', + 'PhabricatorInvalidQueryCursorException' => 'Exception', 'PhabricatorIteratedMD5PasswordHasher' => 'PhabricatorPasswordHasher', 'PhabricatorIteratedMD5PasswordHasherTestCase' => 'PhabricatorTestCase', 'PhabricatorIteratorFileUploadSource' => 'PhabricatorFileUploadSource', diff --git a/src/applications/search/controller/PhabricatorApplicationSearchController.php b/src/applications/search/controller/PhabricatorApplicationSearchController.php index 286158c4fb..067b07512a 100644 --- a/src/applications/search/controller/PhabricatorApplicationSearchController.php +++ b/src/applications/search/controller/PhabricatorApplicationSearchController.php @@ -351,6 +351,8 @@ final class PhabricatorApplicationSearchController $exec_errors[] = $ex->getMessage(); } catch (PhabricatorSearchConstraintException $ex) { $exec_errors[] = $ex->getMessage(); + } catch (PhabricatorInvalidQueryCursorException $ex) { + $exec_errors[] = $ex->getMessage(); } // The engine may have encountered additional errors during rendering; diff --git a/src/infrastructure/query/PhabricatorEmptyQueryException.php b/src/infrastructure/query/exception/PhabricatorEmptyQueryException.php similarity index 100% rename from src/infrastructure/query/PhabricatorEmptyQueryException.php rename to src/infrastructure/query/exception/PhabricatorEmptyQueryException.php diff --git a/src/infrastructure/query/exception/PhabricatorInvalidQueryCursorException.php b/src/infrastructure/query/exception/PhabricatorInvalidQueryCursorException.php new file mode 100644 index 0000000000..8a87745f9a --- /dev/null +++ b/src/infrastructure/query/exception/PhabricatorInvalidQueryCursorException.php @@ -0,0 +1,4 @@ + Date: Mon, 18 Mar 2019 15:48:54 -0700 Subject: [PATCH 174/245] Skip Ferret fulltext columns in "ORDER BY" if there's no fulltext query Summary: Ref T13091. If you "Order By: Relevance" but don't actually specify a query, we currently raise a bare exception. This operation is sort of silly/pointless, but it seems like it's probably best to just return the results for the other constraints in the fallback order (usually, by ID). Alternatively, we could raise a non-bare exception here ("You need to provide a fulltext query to order by relevance.") Test Plan: Queried tasks by relevance with no actual query text. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13091 Differential Revision: https://secure.phabricator.com/D20296 --- ...PhabricatorCursorPagedPolicyAwareQuery.php | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php index 009a4e5188..37e1e06ad6 100644 --- a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php +++ b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php @@ -538,7 +538,7 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery */ protected function buildPagingClause(AphrontDatabaseConnection $conn) { $orderable = $this->getOrderableColumns(); - $vector = $this->getOrderVector(); + $vector = $this->getQueryableOrderVector(); // If we don't have a cursor object yet, it means we're trying to load // the first result page. We may need to build a cursor object from the @@ -1099,16 +1099,19 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery 'table' => null, 'column' => '_ft_rank', 'type' => 'int', + 'requires-ferret' => true, ); $columns['fulltext-created'] = array( 'table' => 'ft_doc', 'column' => 'epochCreated', 'type' => 'int', + 'requires-ferret' => true, ); $columns['fulltext-modified'] = array( 'table' => 'ft_doc', 'column' => 'epochModified', 'type' => 'int', + 'requires-ferret' => true, ); } @@ -1126,11 +1129,12 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery $for_union = false) { $orderable = $this->getOrderableColumns(); - $vector = $this->getOrderVector(); + $vector = $this->getQueryableOrderVector(); $parts = array(); foreach ($vector as $order) { $part = $orderable[$order->getOrderKey()]; + if ($order->getIsReversed()) { $part['reverse'] = !idx($part, 'reverse', false); } @@ -1140,6 +1144,31 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery return $this->formatOrderClause($conn, $parts, $for_union); } + /** + * @task order + */ + private function getQueryableOrderVector() { + $vector = $this->getOrderVector(); + $orderable = $this->getOrderableColumns(); + + $keep = array(); + foreach ($vector as $order) { + $column = $orderable[$order->getOrderKey()]; + + // If this is a Ferret fulltext column but the query doesn't actually + // have a fulltext query, we'll skip most of the Ferret stuff and won't + // actually have the columns in the result set. Just skip them. + if (!empty($column['requires-ferret'])) { + if (!$this->getFerretTokens()) { + continue; + } + } + + $keep[] = $order->getAsScalar(); + } + + return PhabricatorQueryOrderVector::newFromVector($keep); + } /** * @task order From b90e02bec2875660183aa14abc56215d135affd4 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 18 Mar 2019 16:03:28 -0700 Subject: [PATCH 175/245] Select Ferret fulltext columns in results so fulltext queries work under UNION Summary: Ref T13091. In Differential, if you provide a query and "Sort by: Relevance", we build a query like this: ``` ((SELECT revision.* FROM ... ORDER BY rank) UNION ALL (SELECT revision.* FROM ... ORDER BY rank)) ORDER BY rank ``` The internal "ORDER BY rank" is technically redundant (probably?), but doesn't hurt anything, and makes construction easier. The problem is that the outer "ORDER BY rank" at the end, which attempts to order the results of the two parts of the UNION, can't actually order them, since `rank` wasn't selected. (The column isn't actually "rank", which //is// selected -- it's the document modified/created subcolumns, which are not.) To fix this, actually select the fulltext columns into the result set. Test Plan: - Ran a non-empty fulltext query in Differential with "Bucket: Required Action" selected so the UNION construction fired. - Ran normal queries in Maniphest and global search. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13091 Differential Revision: https://secure.phabricator.com/D20297 --- ...PhabricatorCursorPagedPolicyAwareQuery.php | 45 ++++++++++++++----- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php index 37e1e06ad6..cf5343dc45 100644 --- a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php +++ b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php @@ -37,6 +37,10 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery private $ferretQuery; private $ferretMetadata = array(); + const FULLTEXT_RANK = '_ft_rank'; + const FULLTEXT_MODIFIED = '_ft_epochModified'; + const FULLTEXT_CREATED = '_ft_epochCreated'; + /* -( Cursors )------------------------------------------------------------ */ protected function newExternalCursorStringForResult($object) { @@ -298,11 +302,13 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery $metadata = id(new PhabricatorFerretMetadata()) ->setPHID($phid) ->setEngine($this->ferretEngine) - ->setRelevance(idx($row, '_ft_rank')); + ->setRelevance(idx($row, self::FULLTEXT_RANK)); $this->ferretMetadata[$phid] = $metadata; - unset($row['_ft_rank']); + unset($row[self::FULLTEXT_RANK]); + unset($row[self::FULLTEXT_MODIFIED]); + unset($row[self::FULLTEXT_CREATED]); } } @@ -1097,19 +1103,19 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery if ($this->supportsFerretEngine()) { $columns['rank'] = array( 'table' => null, - 'column' => '_ft_rank', + 'column' => self::FULLTEXT_RANK, 'type' => 'int', 'requires-ferret' => true, ); $columns['fulltext-created'] = array( - 'table' => 'ft_doc', - 'column' => 'epochCreated', + 'table' => null, + 'column' => self::FULLTEXT_CREATED, 'type' => 'int', 'requires-ferret' => true, ); $columns['fulltext-modified'] = array( - 'table' => 'ft_doc', - 'column' => 'epochModified', + 'table' => null, + 'column' => self::FULLTEXT_MODIFIED, 'type' => 'int', 'requires-ferret' => true, ); @@ -1779,7 +1785,7 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery } if (!$this->ferretEngine) { - $select[] = qsprintf($conn, '0 _ft_rank'); + $select[] = qsprintf($conn, '0 AS %T', self::FULLTEXT_RANK); return $select; } @@ -1858,8 +1864,27 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery $select[] = qsprintf( $conn, - '%Q _ft_rank', - $sum); + '%Q AS %T', + $sum, + self::FULLTEXT_RANK); + + // See D20297. We select these as real columns in the result set so that + // constructions like this will work: + // + // ((SELECT ...) UNION (SELECT ...)) ORDER BY ... + // + // If the columns aren't part of the result set, the final "ORDER BY" can + // not act on them. + + $select[] = qsprintf( + $conn, + 'ft_doc.epochCreated AS %T', + self::FULLTEXT_CREATED); + + $select[] = qsprintf( + $conn, + 'ft_doc.epochModified AS %T', + self::FULLTEXT_MODIFIED); return $select; } From c16a8c7ab200efa78e9876fb81fcef4b596b73ac Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 23 Mar 2019 07:15:40 -0700 Subject: [PATCH 176/245] Fix a content join condition in Phriction After the cursor changes, we may fatal on pages with a large number of children because "c.title" is not a selected column. We currently join the "content" table if "updated" is part of the order vector, but not if "title" is part of the order vector. This isn't right: "updated" is on the primary table, and only "content" is on the joined table. --- src/applications/phriction/query/PhrictionDocumentQuery.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/applications/phriction/query/PhrictionDocumentQuery.php b/src/applications/phriction/query/PhrictionDocumentQuery.php index f05d67c4f3..702d936a2c 100644 --- a/src/applications/phriction/query/PhrictionDocumentQuery.php +++ b/src/applications/phriction/query/PhrictionDocumentQuery.php @@ -193,7 +193,11 @@ final class PhrictionDocumentQuery } private function shouldJoinContentTable() { - return $this->getOrderVector()->containsKey('updated'); + if ($this->getOrderVector()->containsKey('title')) { + return true; + } + + return false; } protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { From aae7bdbbdd5646457b6014210e6442ac665fe6e3 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 23 Mar 2019 07:27:42 -0700 Subject: [PATCH 177/245] Reference raw "title" row in Phriction paging, not "c.title" Trivial mistake -- the query "SELECT x.y ..." selects a column named "y". The other calls to "getRawRowProperty()" get this right. --- src/applications/phriction/query/PhrictionDocumentQuery.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/applications/phriction/query/PhrictionDocumentQuery.php b/src/applications/phriction/query/PhrictionDocumentQuery.php index 702d936a2c..e7b5a0529e 100644 --- a/src/applications/phriction/query/PhrictionDocumentQuery.php +++ b/src/applications/phriction/query/PhrictionDocumentQuery.php @@ -385,7 +385,7 @@ final class PhrictionDocumentQuery ); if (isset($keys['title'])) { - $map['title'] = $cursor->getRawRowProperty('c.title'); + $map['title'] = $cursor->getRawRowProperty('title'); } return $map; From f0ae75c9916a59c5005f4618af2d2b146e56c02f Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 23 Mar 2019 10:41:06 -0700 Subject: [PATCH 178/245] Fix an issue with "nextPage()" on worker trigger queries Ref T13266. We never page these queries, and previously never reached the "nextPage()" method. The call order changed recently and this method is now reachable. For now, just no-op it rather than throwing. --- .../daemon/workers/query/PhabricatorWorkerTriggerQuery.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/infrastructure/daemon/workers/query/PhabricatorWorkerTriggerQuery.php b/src/infrastructure/daemon/workers/query/PhabricatorWorkerTriggerQuery.php index 87c16a48c5..8ca12d60e4 100644 --- a/src/infrastructure/daemon/workers/query/PhabricatorWorkerTriggerQuery.php +++ b/src/infrastructure/daemon/workers/query/PhabricatorWorkerTriggerQuery.php @@ -70,7 +70,11 @@ final class PhabricatorWorkerTriggerQuery protected function nextPage(array $page) { // NOTE: We don't implement paging because we don't currently ever need // it and paging ORDER_EXECUTION is a hassle. - throw new PhutilMethodNotImplementedException(); + + // (Before T13266, we raised an exception here, but since "nextPage()" is + // now called even if we don't page we can't do that anymore. Just do + // nothing instead.) + return null; } protected function loadPage() { From b8255707349291f63cae791c05be41c2adab82ce Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 24 Mar 2019 07:45:39 -0700 Subject: [PATCH 179/245] Fix transaction queries failing on "withIDs()" after clicking "Show Older" Summary: See . Before T13266, this query got away without having real paging because it used simple ID paging only and results are never actually hidden (today, you can always see all transactions on an object). Provide `withIDs()` so the new, slightly stricter paging works. Test Plan: On an object with "Show Older" in the transaction record, clicked the link. Before: exception in paging code (see Discourse link above). After: transactions loaded cleanly. Reviewers: amckinley, avivey Reviewed By: avivey Differential Revision: https://secure.phabricator.com/D20317 --- .../PhabricatorApplicationTransactionQuery.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/applications/transactions/query/PhabricatorApplicationTransactionQuery.php b/src/applications/transactions/query/PhabricatorApplicationTransactionQuery.php index f15522a087..1db622163d 100644 --- a/src/applications/transactions/query/PhabricatorApplicationTransactionQuery.php +++ b/src/applications/transactions/query/PhabricatorApplicationTransactionQuery.php @@ -3,6 +3,7 @@ abstract class PhabricatorApplicationTransactionQuery extends PhabricatorCursorPagedPolicyAwareQuery { + private $ids; private $phids; private $objectPHIDs; private $authorPHIDs; @@ -35,6 +36,11 @@ abstract class PhabricatorApplicationTransactionQuery abstract public function getTemplateApplicationTransaction(); + public function withIDs(array $ids) { + $this->ids = $ids; + return $this; + } + public function withPHIDs(array $phids) { $this->phids = $phids; return $this; @@ -157,6 +163,13 @@ abstract class PhabricatorApplicationTransactionQuery protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'x.id IN (%Ld)', + $this->ids); + } + if ($this->phids !== null) { $where[] = qsprintf( $conn, From f047b90d938358c262cda7b755ec6da4162ce19f Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 22 Mar 2019 09:57:12 -0700 Subject: [PATCH 180/245] Don't draw the task graph line image on devices by default Summary: See downstream . On mobile, the task graph can take up most of the screen. Hide it on devices. Keep it on the standalone view if you're really dedicated and willing to rotate your phone or whatever to see the lines. Test Plan: Dragged window real narrow, saw graph hide. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20313 --- .../ManiphestTaskGraphController.php | 1 + .../graph/ManiphestTaskGraph.php | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/src/applications/maniphest/controller/ManiphestTaskGraphController.php b/src/applications/maniphest/controller/ManiphestTaskGraphController.php index 2f342a2d0f..f4655d1835 100644 --- a/src/applications/maniphest/controller/ManiphestTaskGraphController.php +++ b/src/applications/maniphest/controller/ManiphestTaskGraphController.php @@ -30,6 +30,7 @@ final class ManiphestTaskGraphController ->setViewer($viewer) ->setSeedPHID($task->getPHID()) ->setLimit($graph_limit) + ->setIsStandalone(true) ->loadGraph(); if (!$task_graph->isEmpty()) { $parent_type = ManiphestTaskDependedOnByTaskEdgeType::EDGECONST; diff --git a/src/infrastructure/graph/ManiphestTaskGraph.php b/src/infrastructure/graph/ManiphestTaskGraph.php index 99191760dd..74a1fe8701 100644 --- a/src/infrastructure/graph/ManiphestTaskGraph.php +++ b/src/infrastructure/graph/ManiphestTaskGraph.php @@ -4,6 +4,7 @@ final class ManiphestTaskGraph extends PhabricatorObjectGraph { private $seedMaps = array(); + private $isStandalone; protected function getEdgeTypes() { return array( @@ -24,6 +25,15 @@ final class ManiphestTaskGraph return $object->isClosed(); } + public function setIsStandalone($is_standalone) { + $this->isStandalone = $is_standalone; + return $this; + } + + public function getIsStandalone() { + return $this->isStandalone; + } + protected function newTableRow($phid, $object, $trace) { $viewer = $this->getViewer(); @@ -132,6 +142,14 @@ final class ManiphestTaskGraph array( true, !$this->getRenderOnlyAdjacentNodes(), + )) + ->setDeviceVisibility( + array( + true, + + // On mobile, we only show the actual graph drawing if we're on the + // standalone page, since it can take over the screen otherwise. + $this->getIsStandalone(), )); } From 7f90570636af8b0fa57d0cab06913e99b2ac27ef Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 19 Mar 2019 10:34:51 -0700 Subject: [PATCH 181/245] When paging by Ferret "rank", page using "HAVING rank > ...", not "WHERE rank > ..." Summary: Ref T13091. The Ferret "rank" column is a function of the query text and looks something like `SELECT ..., 2 + 2 AS rank, ...`. You can't apply conditions to this kind of dynamic column with a WHERE clause: you get a slightly unhelpful error like "column rank unknown in where clause". You must use HAVING: ``` mysql> SELECT 2 + 2 AS x WHERE x = 4; ERROR 1054 (42S22): Unknown column 'x' in 'where clause' mysql> SELECT 2 + 2 AS x HAVING x = 4; +---+ | x | +---+ | 4 | +---+ 1 row in set (0.00 sec) ``` Add a flag to paging column definitions to let them specify that they must be applied with HAVING, then apply the whole paging clause with HAVING if any column requires HAVING. Test Plan: - In Maniphest, ran a fulltext search matching more than 100 results, ordered by "Relevance", then clicked "Next Page". - Before patch: query with `... WHERE rank > 123 OR ...` caused MySQL error because `rank` is not a WHERE-able column. - After patch: query builds as `... HAVING rank > 123 OR ...`, pages properly, no MySQL error. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13091 Differential Revision: https://secure.phabricator.com/D20298 --- ...PhabricatorCursorPagedPolicyAwareQuery.php | 69 ++++++++++++++++++- 1 file changed, 66 insertions(+), 3 deletions(-) diff --git a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php index cf5343dc45..378c282e14 100644 --- a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php +++ b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php @@ -83,6 +83,13 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery $this->applyExternalCursorConstraintsToQuery($query, $cursor); + // If we have a Ferret fulltext query, copy it to the subquery so that we + // generate ranking columns appropriately, and compute the correct object + // ranking score for the current query. + if ($this->ferretEngine) { + $query->withFerretConstraint($this->ferretEngine, $this->ferretTokens); + } + // We're executing the subquery normally to make sure the viewer can // actually see the object, and that it's a completely valid object which // passes all filtering and policy checks. You aren't allowed to use an @@ -204,6 +211,19 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery get_class($this))); } + if ($this->supportsFerretEngine()) { + if ($this->getFerretTokens()) { + $map += array( + 'rank' => + $cursor->getRawRowProperty(self::FULLTEXT_RANK), + 'fulltext-modified' => + $cursor->getRawRowProperty(self::FULLTEXT_MODIFIED), + 'fulltext-created' => + $cursor->getRawRowProperty(self::FULLTEXT_CREATED), + ); + } + } + foreach ($keys as $key) { if (!array_key_exists($key, $map)) { throw new Exception( @@ -295,6 +315,8 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery } protected function didLoadRawRows(array $rows) { + $this->rawCursorRow = last($rows); + if ($this->ferretEngine) { foreach ($rows as $row) { $phid = $row['phid']; @@ -312,8 +334,6 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery } } - $this->rawCursorRow = last($rows); - return $rows; } @@ -467,7 +487,7 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery */ protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = array(); - $where[] = $this->buildPagingClause($conn); + $where[] = $this->buildPagingWhereClause($conn); $where[] = $this->buildEdgeLogicWhereClause($conn); $where[] = $this->buildSpacesWhereClause($conn); $where[] = $this->buildNgramsWhereClause($conn); @@ -482,6 +502,7 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery */ protected function buildHavingClause(AphrontDatabaseConnection $conn) { $having = $this->buildHavingClauseParts($conn); + $having[] = $this->buildPagingHavingClause($conn); return $this->formatHavingClause($conn, $having); } @@ -539,6 +560,45 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery /* -( Paging )------------------------------------------------------------- */ + private function buildPagingWhereClause(AphrontDatabaseConnection $conn) { + if ($this->shouldPageWithHavingClause()) { + return null; + } + + return $this->buildPagingClause($conn); + } + + private function buildPagingHavingClause(AphrontDatabaseConnection $conn) { + if (!$this->shouldPageWithHavingClause()) { + return null; + } + + return $this->buildPagingClause($conn); + } + + private function shouldPageWithHavingClause() { + // If any of the paging conditions reference dynamic columns, we need to + // put the paging conditions in a "HAVING" clause instead of a "WHERE" + // clause. + + // For example, this happens when paging on the Ferret "rank" column, + // since the "rank" value is computed dynamically in the SELECT statement. + + $orderable = $this->getOrderableColumns(); + $vector = $this->getOrderVector(); + + foreach ($vector as $order) { + $key = $order->getOrderKey(); + $column = $orderable[$key]; + + if (!empty($column['having'])) { + return true; + } + } + + return false; + } + /** * @task paging */ @@ -655,6 +715,8 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery 'reverse' => 'optional bool', 'unique' => 'optional bool', 'null' => 'optional string|null', + 'requires-ferret' => 'optional bool', + 'having' => 'optional bool', )); } @@ -1106,6 +1168,7 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery 'column' => self::FULLTEXT_RANK, 'type' => 'int', 'requires-ferret' => true, + 'having' => true, ); $columns['fulltext-created'] = array( 'table' => null, From 2ebe675ae5ec280a80cbe5e13cefb21af75837be Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 24 Mar 2019 13:56:35 -0700 Subject: [PATCH 182/245] Fix an unusual internal cursor in Conpherence Summary: See . Conpherence calls `setAfterID()` and `setBeforeID()` directly on a subquery, but these methods no longer exist. Use a pager instead. This code probably shouldn't exist (we should use some other approach to fetch this data in most cases) but that's a larger change. Test Plan: Sent messages in a Conpherence thread. Before: fatal; after: success. Viewed the Conphrence menu, loaded threads, etc. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20318 --- .../query/ConpherenceThreadQuery.php | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/applications/conpherence/query/ConpherenceThreadQuery.php b/src/applications/conpherence/query/ConpherenceThreadQuery.php index 5cd6489d65..9c6682a8a7 100644 --- a/src/applications/conpherence/query/ConpherenceThreadQuery.php +++ b/src/applications/conpherence/query/ConpherenceThreadQuery.php @@ -285,23 +285,35 @@ final class ConpherenceThreadQuery } private function loadTransactionsAndHandles(array $conpherences) { - $query = id(new ConpherenceTransactionQuery()) - ->setViewer($this->getViewer()) - ->withObjectPHIDs(array_keys($conpherences)) - ->needHandles(true); + // NOTE: This is older code which has been modernized to the minimum + // standard required by T13266. It probably isn't the best available + // approach to the problems it solves. + + $limit = $this->getTransactionLimit(); + if ($limit) { + // fetch an extra for "show older" scenarios + $limit = $limit + 1; + } else { + $limit = 0xFFFF; + } + + $pager = id(new AphrontCursorPagerView()) + ->setPageSize($limit); // We have to flip these for the underlying query class. The semantics of // paging are tricky business. if ($this->afterTransactionID) { - $query->setBeforeID($this->afterTransactionID); + $pager->setBeforeID($this->afterTransactionID); } else if ($this->beforeTransactionID) { - $query->setAfterID($this->beforeTransactionID); + $pager->setAfterID($this->beforeTransactionID); } - if ($this->getTransactionLimit()) { - // fetch an extra for "show older" scenarios - $query->setLimit($this->getTransactionLimit() + 1); - } - $transactions = $query->execute(); + + $transactions = id(new ConpherenceTransactionQuery()) + ->setViewer($this->getViewer()) + ->withObjectPHIDs(array_keys($conpherences)) + ->needHandles(true) + ->executeWithCursorPager($pager); + $transactions = mgroup($transactions, 'getObjectPHID'); foreach ($conpherences as $phid => $conpherence) { $current_transactions = idx($transactions, $phid, array()); From 1d73ae3b50a6a477c2328c47c60cde5d08b17712 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 22 Mar 2019 15:04:36 -0700 Subject: [PATCH 183/245] Fix two minor timezone display issues Summary: Ref T13263. Two minor issues: - The "reconcile" dialog shows the wrong sign because JS signs differ from normal signs (for example, PST or PDT or whatever we're in right now is shown as "UTC+7", but should be "UTC-7"). - The big dropdown of possible timezones lumps "UTC+X:30" timezones into "UTC+X". Test Plan: - Reconciled "America/Nome", saw negative UTC offsets for "America/Nome" and "America/Los_Angeles" (previously: improperly positive). - Viewed the big timzone list, saw ":30" and ":45" timezones grouped/labeled more accurately. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13263 Differential Revision: https://secure.phabricator.com/D20314 --- .../PhabricatorSettingsTimezoneController.php | 5 +++++ .../settings/setting/PhabricatorTimezoneSetting.php | 13 ++++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/applications/settings/controller/PhabricatorSettingsTimezoneController.php b/src/applications/settings/controller/PhabricatorSettingsTimezoneController.php index 51f1747b9f..6a0ba19d03 100644 --- a/src/applications/settings/controller/PhabricatorSettingsTimezoneController.php +++ b/src/applications/settings/controller/PhabricatorSettingsTimezoneController.php @@ -113,6 +113,11 @@ final class PhabricatorSettingsTimezoneController } private function formatOffset($offset) { + // This controller works with client-side (Javascript) offsets, which have + // the opposite sign we might expect -- for example "UTC-3" is a positive + // offset. Invert the sign before rendering the offset. + $offset = -1 * $offset; + $hours = $offset / 60; // Non-integer number of hours off UTC? if ($offset % 60) { diff --git a/src/applications/settings/setting/PhabricatorTimezoneSetting.php b/src/applications/settings/setting/PhabricatorTimezoneSetting.php index 887e08129b..52fce77428 100644 --- a/src/applications/settings/setting/PhabricatorTimezoneSetting.php +++ b/src/applications/settings/setting/PhabricatorTimezoneSetting.php @@ -57,11 +57,11 @@ final class PhabricatorTimezoneSetting $groups = array(); foreach ($timezones as $timezone) { $zone = new DateTimeZone($timezone); - $offset = -($zone->getOffset($now) / (60 * 60)); + $offset = ($zone->getOffset($now) / 60); $groups[$offset][] = $timezone; } - krsort($groups); + ksort($groups); $option_groups = array( array( @@ -71,10 +71,13 @@ final class PhabricatorTimezoneSetting ); foreach ($groups as $offset => $group) { - if ($offset >= 0) { - $label = pht('UTC-%d', $offset); + $hours = $offset / 60; + $minutes = abs($offset % 60); + + if ($offset % 60) { + $label = pht('UTC%+d:%02d', $hours, $minutes); } else { - $label = pht('UTC+%d', -$offset); + $label = pht('UTC%+d', $hours); } sort($group); From c3563ca15608ba0474aa4713abcdf85ce1a972e7 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 25 Mar 2019 07:27:24 -0700 Subject: [PATCH 184/245] Correct use of the paging API in Phame Summary: Ref T13266. This callsite is using the older API; swap it to use pagers. Test Plan: Viewed a Phame blog post with siblings, saw the previous/next posts linked. Reviewers: amckinley Reviewed By: amckinley Subscribers: nicolast Maniphest Tasks: T13263, T13266 Differential Revision: https://secure.phabricator.com/D20319 --- .../controller/post/PhamePostViewController.php | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/applications/phame/controller/post/PhamePostViewController.php b/src/applications/phame/controller/post/PhamePostViewController.php index 11d94d2f94..4fb01c4def 100644 --- a/src/applications/phame/controller/post/PhamePostViewController.php +++ b/src/applications/phame/controller/post/PhamePostViewController.php @@ -304,6 +304,15 @@ final class PhamePostViewController private function loadAdjacentPosts(PhamePost $post) { $viewer = $this->getViewer(); + $pager = id(new AphrontCursorPagerView()) + ->setPageSize(1); + + $prev_pager = id(clone $pager) + ->setAfterID($post->getID()); + + $next_pager = id(clone $pager) + ->setBeforeID($post->getID()); + $query = id(new PhamePostQuery()) ->setViewer($viewer) ->withVisibility(array(PhameConstants::VISIBILITY_PUBLISHED)) @@ -311,12 +320,10 @@ final class PhamePostViewController ->setLimit(1); $prev = id(clone $query) - ->setAfterID($post->getID()) - ->execute(); + ->executeWithCursorPager($prev_pager); $next = id(clone $query) - ->setBeforeID($post->getID()) - ->execute(); + ->executeWithCursorPager($next_pager); return array(head($prev), head($next)); } From 79e36dc7fa39a050c7d8d708684974de0d39046b Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 22 Mar 2019 06:41:00 -0700 Subject: [PATCH 185/245] Explain the relationship between "Runnable" and "Restartable" more clearly in Build Plans Summary: See PHI1153. The "Runnable" and "Restartable" behaviors interact (to click "restart", you must be able to run the build AND it must be restartable). Make this more clear. Test Plan: {F6301739} Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Differential Revision: https://secure.phabricator.com/D20307 --- .../plan/HarbormasterBuildPlanBehavior.php | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php b/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php index d8e857e711..112926c47c 100644 --- a/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php +++ b/src/applications/harbormaster/plan/HarbormasterBuildPlanBehavior.php @@ -350,15 +350,19 @@ final class HarbormasterBuildPlanBehavior ->setKey(self::BEHAVIOR_RESTARTABLE) ->setEditInstructions( pht( - 'Usually, builds may be restarted. This may be useful if you '. - 'suspect a build has failed for environmental or circumstantial '. - 'reasons unrelated to the actual code, and want to give it '. - 'another chance at glory.'. + 'Usually, builds may be restarted by users who have permission '. + 'to edit the related build plan. (You can change who is allowed '. + 'to restart a build by adjusting the "Runnable" behavior.)'. + "\n\n". + 'Restarting a build may be useful if you suspect it has failed '. + 'for environmental or circumstantial reasons unrelated to the '. + 'actual code, and want to give it another chance at glory.'. "\n\n". 'If you want to prevent a build from being restarted, you can '. - 'change the behavior here. This may be useful to prevent '. - 'accidents where a build with a dangerous side effect (like '. - 'deployment) is restarted improperly.')) + 'change when it may be restarted by adjusting this behavior. '. + 'This may be useful to prevent accidents where a build with a '. + 'dangerous side effect (like deployment) is restarted '. + 'improperly.')) ->setName(pht('Restartable')) ->setOptions($restart_options), id(new self()) From e15b3dd3c610923f198ec11f604bbbfc105d776e Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 22 Mar 2019 09:06:10 -0700 Subject: [PATCH 186/245] When a repository is inactive, mark its handle as "closed" Summary: See downstream . We currently don't mark repository handles as closed. Test Plan: Mentioned two repositories with `R1` (active) and `R2` (inactive). After patch, saw `R2` visually indicated as closed/inactive. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20309 --- .../phid/PhabricatorRepositoryRepositoryPHIDType.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/applications/repository/phid/PhabricatorRepositoryRepositoryPHIDType.php b/src/applications/repository/phid/PhabricatorRepositoryRepositoryPHIDType.php index ba78b0fe7a..6ca67257cf 100644 --- a/src/applications/repository/phid/PhabricatorRepositoryRepositoryPHIDType.php +++ b/src/applications/repository/phid/PhabricatorRepositoryRepositoryPHIDType.php @@ -46,6 +46,10 @@ final class PhabricatorRepositoryRepositoryPHIDType ->setFullName("{$monogram} {$name}") ->setURI($uri) ->setMailStampName($monogram); + + if ($repository->getStatus() !== PhabricatorRepository::STATUS_ACTIVE) { + $handle->setStatus(PhabricatorObjectHandle::STATUS_CLOSED); + } } } From 930cc7a6dd70d5076722d65a1a85da864ddf766f Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 22 Mar 2019 09:22:54 -0700 Subject: [PATCH 187/245] Generalize Legalpad validation logic for "Require Signature" Summary: See downstream . I can't actually reproduce any issue here (we only show this field when creating a document, and only if the viewer is an administrator), so maybe this relied on some changes or was originally reported against older code. Regardless, the validation isn't quite right: it requires administrator privileges to apply this transaction at all, but should only require administrator privileges to change the value. Test Plan: Edited Legalpad documents as an administrator and non-administrator before and after the change, with and without signatures being required. Couldn't reproduce the original issue, but this version is generally more correct/robust. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20311 --- ...padDocumentRequireSignatureTransaction.php | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/applications/legalpad/xaction/LegalpadDocumentRequireSignatureTransaction.php b/src/applications/legalpad/xaction/LegalpadDocumentRequireSignatureTransaction.php index 3819f38a70..9932baab80 100644 --- a/src/applications/legalpad/xaction/LegalpadDocumentRequireSignatureTransaction.php +++ b/src/applications/legalpad/xaction/LegalpadDocumentRequireSignatureTransaction.php @@ -55,11 +55,22 @@ final class LegalpadDocumentRequireSignatureTransaction public function validateTransactions($object, array $xactions) { $errors = array(); - $is_admin = $this->getActor()->getIsAdmin(); + $old = (bool)$object->getRequireSignature(); + foreach ($xactions as $xaction) { + $new = (bool)$xaction->getNewValue(); - if (!$is_admin) { - $errors[] = $this->newInvalidError( - pht('Only admins may require signature.')); + if ($old === $new) { + continue; + } + + $is_admin = $this->getActor()->getIsAdmin(); + if (!$is_admin) { + $errors[] = $this->newInvalidError( + pht( + 'Only administrators may change whether a document '. + 'requires a signature.'), + $xaction); + } } return $errors; From b081053e2694e277b1a0f57b8dd8e33863c26fd0 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 22 Mar 2019 09:32:13 -0700 Subject: [PATCH 188/245] Don't show a "Manage" button in Legalpad if the user is signing a TOS document Summary: When a TOS-like Legalpad document is marked "Require this document to use Phabricator", the login prompt shows a "Manage" button, but that button doesn't work. When we're presenting a document as a session gate, don't show "Manage". Test Plan: Viewed a required document during a session gate (no "Manage" button) and normally (saw "Manage" button). Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20312 --- .../base/controller/PhabricatorController.php | 1 + .../LegalpadDocumentSignController.php | 22 +++++++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/applications/base/controller/PhabricatorController.php b/src/applications/base/controller/PhabricatorController.php index 59df22a8aa..cfd0eaee65 100644 --- a/src/applications/base/controller/PhabricatorController.php +++ b/src/applications/base/controller/PhabricatorController.php @@ -608,6 +608,7 @@ abstract class PhabricatorController extends AphrontController { $this->setCurrentApplication($application); $controller = new LegalpadDocumentSignController(); + $controller->setIsSessionGate(true); return $this->delegateToController($controller); } diff --git a/src/applications/legalpad/controller/LegalpadDocumentSignController.php b/src/applications/legalpad/controller/LegalpadDocumentSignController.php index f09d95af29..fb15e2af8f 100644 --- a/src/applications/legalpad/controller/LegalpadDocumentSignController.php +++ b/src/applications/legalpad/controller/LegalpadDocumentSignController.php @@ -2,6 +2,8 @@ final class LegalpadDocumentSignController extends LegalpadController { + private $isSessionGate; + public function shouldAllowPublic() { return true; } @@ -10,6 +12,15 @@ final class LegalpadDocumentSignController extends LegalpadController { return true; } + public function setIsSessionGate($is_session_gate) { + $this->isSessionGate = $is_session_gate; + return $this; + } + + public function getIsSessionGate() { + return $this->isSessionGate; + } + public function handleRequest(AphrontRequest $request) { $viewer = $request->getUser(); @@ -251,8 +262,14 @@ final class LegalpadDocumentSignController extends LegalpadController { $header = id(new PHUIHeaderView()) ->setHeader($title) ->setUser($viewer) - ->setEpoch($content_updated) - ->addActionLink( + ->setEpoch($content_updated); + + // If we're showing the user this document because it's required to use + // Phabricator and they haven't signed it, don't show the "Manage" button, + // since it won't work. + $is_gate = $this->getIsSessionGate(); + if (!$is_gate) { + $header->addActionLink( id(new PHUIButtonView()) ->setTag('a') ->setIcon('fa-pencil') @@ -260,6 +277,7 @@ final class LegalpadDocumentSignController extends LegalpadController { ->setHref($manage_uri) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); + } $preamble_box = null; if (strlen($document->getPreamble())) { From a5226366d3155ede9bcbd3a2a62a6eac6aaae20f Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 22 Mar 2019 09:11:53 -0700 Subject: [PATCH 189/245] Make notifications visually clearer, like Feed Summary: See downstream . The notifications menu is missing some CSS to color and style values in stories like "renamed task from X to Y". Test Plan: Before: {F6302123} After: {F6302122} Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20310 --- resources/celerity/map.php | 6 +++--- webroot/rsrc/css/application/base/notification-menu.css | 8 +++++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index b80ce31d1a..08bcbff8bf 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,7 +9,7 @@ return array( 'names' => array( 'conpherence.pkg.css' => '3c8a0668', 'conpherence.pkg.js' => '020aebcf', - 'core.pkg.css' => '34ce1741', + 'core.pkg.css' => 'b797945d', 'core.pkg.js' => 'f9c2509b', 'differential.pkg.css' => '8d8360fb', 'differential.pkg.js' => '67e02996', @@ -38,7 +38,7 @@ return array( 'rsrc/css/application/almanac/almanac.css' => '2e050f4f', 'rsrc/css/application/auth/auth.css' => 'add92fd8', 'rsrc/css/application/base/main-menu-view.css' => '8e2d9a28', - 'rsrc/css/application/base/notification-menu.css' => 'e6962e89', + 'rsrc/css/application/base/notification-menu.css' => '4df1ee30', 'rsrc/css/application/base/phui-theme.css' => '35883b37', 'rsrc/css/application/base/standard-page-view.css' => '8a295cb9', 'rsrc/css/application/chatlog/chatlog.css' => 'abdc76ee', @@ -774,7 +774,7 @@ return array( 'phabricator-nav-view-css' => 'f8a0c1bf', 'phabricator-notification' => 'a9b91e3f', 'phabricator-notification-css' => '30240bd2', - 'phabricator-notification-menu-css' => 'e6962e89', + 'phabricator-notification-menu-css' => '4df1ee30', 'phabricator-object-selector-css' => 'ee77366f', 'phabricator-phtize' => '2f1db1ed', 'phabricator-prefab' => '5793d835', diff --git a/webroot/rsrc/css/application/base/notification-menu.css b/webroot/rsrc/css/application/base/notification-menu.css index 8db2436891..5886798600 100644 --- a/webroot/rsrc/css/application/base/notification-menu.css +++ b/webroot/rsrc/css/application/base/notification-menu.css @@ -15,6 +15,7 @@ .phabricator-notification { padding: 8px 12px; + color: {$darkgreytext}; } .phabricator-notification-menu-loading { @@ -114,7 +115,7 @@ } .phabricator-notification-header a { - color: {$darkgreytext}; + color: {$anchor}; } .phabricator-notification-header a:hover { @@ -162,3 +163,8 @@ .aphlict-connection-status .connection-status-text { margin-left: 12px; } + +.phabricator-notification .phui-timeline-value { + font-style: italic; + color: #000; +} From 917fedafe69e1999f3bc2cdff88c4746194943fc Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 22 Mar 2019 15:59:55 -0700 Subject: [PATCH 190/245] Support exporting custom "Options/Select" fields to Excel/JSON/CSV/etc Summary: See . In JSON, export both the internal key and the visible value. For other formats, export the visible label. Test Plan: - Added a custom options/select field. - Exported CSV, JSON, Text, got sensible output. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20316 --- src/__phutil_library_map__.php | 2 + .../PhabricatorStandardCustomFieldSelect.php | 5 ++ .../field/PhabricatorOptionExportField.php | 47 +++++++++++++++++++ 3 files changed, 54 insertions(+) create mode 100644 src/infrastructure/export/field/PhabricatorOptionExportField.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 5725b5330b..876892943f 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -3692,6 +3692,7 @@ phutil_register_library_map(array( 'PhabricatorOlderInlinesSetting' => 'applications/settings/setting/PhabricatorOlderInlinesSetting.php', 'PhabricatorOneTimeTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorOneTimeTriggerClock.php', 'PhabricatorOpcodeCacheSpec' => 'applications/cache/spec/PhabricatorOpcodeCacheSpec.php', + 'PhabricatorOptionExportField' => 'infrastructure/export/field/PhabricatorOptionExportField.php', 'PhabricatorOptionGroupSetting' => 'applications/settings/setting/PhabricatorOptionGroupSetting.php', 'PhabricatorOwnerPathQuery' => 'applications/owners/query/PhabricatorOwnerPathQuery.php', 'PhabricatorOwnersApplication' => 'applications/owners/application/PhabricatorOwnersApplication.php', @@ -9687,6 +9688,7 @@ phutil_register_library_map(array( 'PhabricatorOlderInlinesSetting' => 'PhabricatorSelectSetting', 'PhabricatorOneTimeTriggerClock' => 'PhabricatorTriggerClock', 'PhabricatorOpcodeCacheSpec' => 'PhabricatorCacheSpec', + 'PhabricatorOptionExportField' => 'PhabricatorExportField', 'PhabricatorOptionGroupSetting' => 'PhabricatorSetting', 'PhabricatorOwnerPathQuery' => 'Phobject', 'PhabricatorOwnersApplication' => 'PhabricatorApplication', diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldSelect.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldSelect.php index 036b7301a1..5957afe56a 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldSelect.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldSelect.php @@ -153,4 +153,9 @@ final class PhabricatorStandardCustomFieldSelect ->setOptions($this->getOptions()); } + protected function newExportFieldType() { + return id(new PhabricatorOptionExportField()) + ->setOptions($this->getOptions()); + } + } diff --git a/src/infrastructure/export/field/PhabricatorOptionExportField.php b/src/infrastructure/export/field/PhabricatorOptionExportField.php new file mode 100644 index 0000000000..e6d3e9b45b --- /dev/null +++ b/src/infrastructure/export/field/PhabricatorOptionExportField.php @@ -0,0 +1,47 @@ +options = $options; + return $this; + } + + public function getOptions() { + return $this->options; + } + + public function getNaturalValue($value) { + if ($value === null) { + return $value; + } + + if (!strlen($value)) { + return null; + } + + $options = $this->getOptions(); + + return array( + 'value' => (string)$value, + 'name' => (string)idx($options, $value, $value), + ); + } + + public function getTextValue($value) { + $natural_value = $this->getNaturalValue($value); + if ($natural_value === null) { + return null; + } + + return $natural_value['name']; + } + + public function getPHPExcelValue($value) { + return $this->getTextValue($value); + } + +} From 252b6f2260561a77564d175f15ee75f43999f6c7 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 12 Mar 2019 08:29:14 -0700 Subject: [PATCH 191/245] Provide basic scaffolding for workboard column triggers Summary: Depends on D20278. Ref T5474. This change creates some new empty objects that do nothing, and some new views for looking at those objects. There's no actual useful behavior yet. The "Edit" controller is custom instead of being driven by "EditEngine" because I expect it to be a Herald-style "add new rules" UI, and EditEngine isn't a clean match for those today (although maybe I'll try to move it over). The general idea here is: - Triggers are "real" objects with a real PHID. - Each trigger has a name and a collection of rules, like "Change status to: X" or "Play sound: Y". - Each column may be bound to a trigger. - Multiple columns may share the same trigger. - Later UI refinements will make the cases around "copy trigger" vs "reference the same trigger" vs "create a new ad-hoc trigger" more clear. - Triggers have their own edit policy. - Triggers are always world-visible, like Herald rules. Test Plan: Poked around, created some empty trigger objects, and nothing exploded. This doesn't actually do anything useful yet since triggers can't have any rule behavior and columns can't actually be bound to triggers. Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T5474 Differential Revision: https://secure.phabricator.com/D20279 --- .../20190312.triggers.01.trigger.sql | 9 + .../20190312.triggers.02.xaction.sql | 19 ++ .../20190312.triggers.03.triggerphid.sql | 2 + src/__phutil_library_map__.php | 31 +++ .../PhabricatorProjectApplication.php | 8 + .../PhabricatorProjectBoardViewController.php | 40 +++- .../PhabricatorProjectTriggerController.php | 16 ++ ...habricatorProjectTriggerEditController.php | 197 ++++++++++++++++++ ...habricatorProjectTriggerListController.php | 16 ++ ...habricatorProjectTriggerViewController.php | 168 +++++++++++++++ .../PhabricatorProjectTriggerEditor.php | 30 +++ .../engine/PhabricatorBoardLayoutEngine.php | 1 + .../PhabricatorProjectTriggerPHIDType.php | 45 ++++ .../query/PhabricatorProjectColumnQuery.php | 55 +++++ .../query/PhabricatorProjectTriggerQuery.php | 51 +++++ .../PhabricatorProjectTriggerSearchEngine.php | 75 +++++++ ...bricatorProjectTriggerTransactionQuery.php | 10 + .../storage/PhabricatorProjectColumn.php | 39 ++++ .../storage/PhabricatorProjectTrigger.php | 108 ++++++++++ .../PhabricatorProjectTriggerTransaction.php | 18 ++ ...abricatorProjectTriggerNameTransaction.php | 58 ++++++ ...abricatorProjectTriggerTransactionType.php | 4 + ...PhabricatorApplicationSearchController.php | 5 +- 23 files changed, 999 insertions(+), 6 deletions(-) create mode 100644 resources/sql/autopatches/20190312.triggers.01.trigger.sql create mode 100644 resources/sql/autopatches/20190312.triggers.02.xaction.sql create mode 100644 resources/sql/autopatches/20190312.triggers.03.triggerphid.sql create mode 100644 src/applications/project/controller/trigger/PhabricatorProjectTriggerController.php create mode 100644 src/applications/project/controller/trigger/PhabricatorProjectTriggerEditController.php create mode 100644 src/applications/project/controller/trigger/PhabricatorProjectTriggerListController.php create mode 100644 src/applications/project/controller/trigger/PhabricatorProjectTriggerViewController.php create mode 100644 src/applications/project/editor/PhabricatorProjectTriggerEditor.php create mode 100644 src/applications/project/phid/PhabricatorProjectTriggerPHIDType.php create mode 100644 src/applications/project/query/PhabricatorProjectTriggerQuery.php create mode 100644 src/applications/project/query/PhabricatorProjectTriggerSearchEngine.php create mode 100644 src/applications/project/query/PhabricatorProjectTriggerTransactionQuery.php create mode 100644 src/applications/project/storage/PhabricatorProjectTrigger.php create mode 100644 src/applications/project/storage/PhabricatorProjectTriggerTransaction.php create mode 100644 src/applications/project/xaction/trigger/PhabricatorProjectTriggerNameTransaction.php create mode 100644 src/applications/project/xaction/trigger/PhabricatorProjectTriggerTransactionType.php diff --git a/resources/sql/autopatches/20190312.triggers.01.trigger.sql b/resources/sql/autopatches/20190312.triggers.01.trigger.sql new file mode 100644 index 0000000000..301a3a62cd --- /dev/null +++ b/resources/sql/autopatches/20190312.triggers.01.trigger.sql @@ -0,0 +1,9 @@ +CREATE TABLE {$NAMESPACE}_project.project_trigger ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + name VARCHAR(255) NOT NULL COLLATE {$COLLATE_TEXT}, + editPolicy VARBINARY(64) NOT NULL, + ruleset LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20190312.triggers.02.xaction.sql b/resources/sql/autopatches/20190312.triggers.02.xaction.sql new file mode 100644 index 0000000000..1a6034c4b1 --- /dev/null +++ b/resources/sql/autopatches/20190312.triggers.02.xaction.sql @@ -0,0 +1,19 @@ +CREATE TABLE {$NAMESPACE}_project.project_triggertransaction ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + authorPHID VARBINARY(64) NOT NULL, + objectPHID VARBINARY(64) NOT NULL, + viewPolicy VARBINARY(64) NOT NULL, + editPolicy VARBINARY(64) NOT NULL, + commentPHID VARBINARY(64) DEFAULT NULL, + commentVersion INT UNSIGNED NOT NULL, + transactionType VARCHAR(32) NOT NULL, + oldValue LONGTEXT NOT NULL, + newValue LONGTEXT NOT NULL, + contentSource LONGTEXT NOT NULL, + metadata LONGTEXT NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20190312.triggers.03.triggerphid.sql b/resources/sql/autopatches/20190312.triggers.03.triggerphid.sql new file mode 100644 index 0000000000..271d679cfa --- /dev/null +++ b/resources/sql/autopatches/20190312.triggers.03.triggerphid.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_project.project_column + ADD triggerPHID VARBINARY(64); diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 876892943f..258ebf1f8f 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4166,6 +4166,19 @@ phutil_register_library_map(array( 'PhabricatorProjectTransactionEditor' => 'applications/project/editor/PhabricatorProjectTransactionEditor.php', 'PhabricatorProjectTransactionQuery' => 'applications/project/query/PhabricatorProjectTransactionQuery.php', 'PhabricatorProjectTransactionType' => 'applications/project/xaction/PhabricatorProjectTransactionType.php', + 'PhabricatorProjectTrigger' => 'applications/project/storage/PhabricatorProjectTrigger.php', + 'PhabricatorProjectTriggerController' => 'applications/project/controller/trigger/PhabricatorProjectTriggerController.php', + 'PhabricatorProjectTriggerEditController' => 'applications/project/controller/trigger/PhabricatorProjectTriggerEditController.php', + 'PhabricatorProjectTriggerEditor' => 'applications/project/editor/PhabricatorProjectTriggerEditor.php', + 'PhabricatorProjectTriggerListController' => 'applications/project/controller/trigger/PhabricatorProjectTriggerListController.php', + 'PhabricatorProjectTriggerNameTransaction' => 'applications/project/xaction/trigger/PhabricatorProjectTriggerNameTransaction.php', + 'PhabricatorProjectTriggerPHIDType' => 'applications/project/phid/PhabricatorProjectTriggerPHIDType.php', + 'PhabricatorProjectTriggerQuery' => 'applications/project/query/PhabricatorProjectTriggerQuery.php', + 'PhabricatorProjectTriggerSearchEngine' => 'applications/project/query/PhabricatorProjectTriggerSearchEngine.php', + 'PhabricatorProjectTriggerTransaction' => 'applications/project/storage/PhabricatorProjectTriggerTransaction.php', + 'PhabricatorProjectTriggerTransactionQuery' => 'applications/project/query/PhabricatorProjectTriggerTransactionQuery.php', + 'PhabricatorProjectTriggerTransactionType' => 'applications/project/xaction/trigger/PhabricatorProjectTriggerTransactionType.php', + 'PhabricatorProjectTriggerViewController' => 'applications/project/controller/trigger/PhabricatorProjectTriggerViewController.php', 'PhabricatorProjectTypeTransaction' => 'applications/project/xaction/PhabricatorProjectTypeTransaction.php', 'PhabricatorProjectUIEventListener' => 'applications/project/events/PhabricatorProjectUIEventListener.php', 'PhabricatorProjectUpdateController' => 'applications/project/controller/PhabricatorProjectUpdateController.php', @@ -10268,6 +10281,24 @@ phutil_register_library_map(array( 'PhabricatorProjectTransactionEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorProjectTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorProjectTransactionType' => 'PhabricatorModularTransactionType', + 'PhabricatorProjectTrigger' => array( + 'PhabricatorProjectDAO', + 'PhabricatorApplicationTransactionInterface', + 'PhabricatorPolicyInterface', + 'PhabricatorDestructibleInterface', + ), + 'PhabricatorProjectTriggerController' => 'PhabricatorProjectController', + 'PhabricatorProjectTriggerEditController' => 'PhabricatorProjectTriggerController', + 'PhabricatorProjectTriggerEditor' => 'PhabricatorApplicationTransactionEditor', + 'PhabricatorProjectTriggerListController' => 'PhabricatorProjectTriggerController', + 'PhabricatorProjectTriggerNameTransaction' => 'PhabricatorProjectTriggerTransactionType', + 'PhabricatorProjectTriggerPHIDType' => 'PhabricatorPHIDType', + 'PhabricatorProjectTriggerQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorProjectTriggerSearchEngine' => 'PhabricatorApplicationSearchEngine', + 'PhabricatorProjectTriggerTransaction' => 'PhabricatorModularTransaction', + 'PhabricatorProjectTriggerTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'PhabricatorProjectTriggerTransactionType' => 'PhabricatorModularTransactionType', + 'PhabricatorProjectTriggerViewController' => 'PhabricatorProjectTriggerController', 'PhabricatorProjectTypeTransaction' => 'PhabricatorProjectTransactionType', 'PhabricatorProjectUIEventListener' => 'PhabricatorEventListener', 'PhabricatorProjectUpdateController' => 'PhabricatorProjectController', diff --git a/src/applications/project/application/PhabricatorProjectApplication.php b/src/applications/project/application/PhabricatorProjectApplication.php index 0e1a9f37c7..192b40f6cd 100644 --- a/src/applications/project/application/PhabricatorProjectApplication.php +++ b/src/applications/project/application/PhabricatorProjectApplication.php @@ -89,6 +89,14 @@ final class PhabricatorProjectApplication extends PhabricatorApplication { 'background/' => 'PhabricatorProjectBoardBackgroundController', ), + 'trigger/' => array( + $this->getQueryRoutePattern() => + 'PhabricatorProjectTriggerListController', + '(?P[1-9]\d*)/' => + 'PhabricatorProjectTriggerViewController', + $this->getEditRoutePattern('edit/') => + 'PhabricatorProjectTriggerEditController', + ), 'update/(?P[1-9]\d*)/(?P[^/]+)/' => 'PhabricatorProjectUpdateController', 'manage/(?P[1-9]\d*)/' => 'PhabricatorProjectManageController', diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index a1dcd6ab68..9b882c6ccc 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -1111,10 +1111,8 @@ final class PhabricatorProjectBoardViewController )); } - if (count($specs) > 1) { - $column_items[] = id(new PhabricatorActionView()) - ->setType(PhabricatorActionView::TYPE_DIVIDER); - } + $column_items[] = id(new PhabricatorActionView()) + ->setType(PhabricatorActionView::TYPE_DIVIDER); $batch_edit_uri = $request->getRequestURI(); $batch_edit_uri->replaceQueryParam('batch', $column->getID()); @@ -1174,6 +1172,40 @@ final class PhabricatorProjectBoardViewController ->setWorkflow(true); } + if ($column->canHaveTrigger()) { + $column_items[] = id(new PhabricatorActionView()) + ->setType(PhabricatorActionView::TYPE_DIVIDER); + + $trigger = $column->getTrigger(); + if (!$trigger) { + $set_uri = $this->getApplicationURI( + new PhutilURI( + 'trigger/edit/', + array( + 'columnPHID' => $column->getPHID(), + ))); + + $column_items[] = id(new PhabricatorActionView()) + ->setIcon('fa-cogs') + ->setName(pht('New Trigger...')) + ->setHref($set_uri) + ->setDisabled(!$can_edit); + } else { + $column_items[] = id(new PhabricatorActionView()) + ->setIcon('fa-cogs') + ->setName(pht('View Trigger')) + ->setHref($trigger->getURI()) + ->setDisabled(!$can_edit); + } + + $column_items[] = id(new PhabricatorActionView()) + ->setIcon('fa-times') + ->setName(pht('Remove Trigger')) + ->setHref('#') + ->setWorkflow(true) + ->setDisabled(!$can_edit || !$trigger); + } + $column_menu = id(new PhabricatorActionListView()) ->setUser($viewer); foreach ($column_items as $item) { diff --git a/src/applications/project/controller/trigger/PhabricatorProjectTriggerController.php b/src/applications/project/controller/trigger/PhabricatorProjectTriggerController.php new file mode 100644 index 0000000000..ea729e82a4 --- /dev/null +++ b/src/applications/project/controller/trigger/PhabricatorProjectTriggerController.php @@ -0,0 +1,16 @@ +addTextCrumb( + pht('Triggers'), + $this->getApplicationURI('trigger/')); + + return $crumbs; + } + +} diff --git a/src/applications/project/controller/trigger/PhabricatorProjectTriggerEditController.php b/src/applications/project/controller/trigger/PhabricatorProjectTriggerEditController.php new file mode 100644 index 0000000000..86f75225d2 --- /dev/null +++ b/src/applications/project/controller/trigger/PhabricatorProjectTriggerEditController.php @@ -0,0 +1,197 @@ +getRequest(); + $viewer = $request->getViewer(); + + $id = $request->getURIData('id'); + if ($id) { + $trigger = id(new PhabricatorProjectTriggerQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$trigger) { + return new Aphront404Response(); + } + } else { + $trigger = PhabricatorProjectTrigger::initializeNewTrigger(); + } + + $column_phid = $request->getStr('columnPHID'); + if ($column_phid) { + $column = id(new PhabricatorProjectColumnQuery()) + ->setViewer($viewer) + ->withPHIDs(array($column_phid)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$column) { + return new Aphront404Response(); + } + $board_uri = $column->getBoardURI(); + } else { + $column = null; + $board_uri = null; + } + + if ($board_uri) { + $cancel_uri = $board_uri; + } else if ($trigger->getID()) { + $cancel_uri = $trigger->getURI(); + } else { + $cancel_uri = $this->getApplicationURI('trigger/'); + } + + $v_name = $trigger->getName(); + $v_edit = $trigger->getEditPolicy(); + + $e_name = null; + $e_edit = null; + + $validation_exception = null; + if ($request->isFormPost()) { + try { + $v_name = $request->getStr('name'); + $v_edit = $request->getStr('editPolicy'); + + $xactions = array(); + if (!$trigger->getID()) { + $xactions[] = $trigger->getApplicationTransactionTemplate() + ->setTransactionType(PhabricatorTransactions::TYPE_CREATE) + ->setNewValue(true); + } + + $xactions[] = $trigger->getApplicationTransactionTemplate() + ->setTransactionType( + PhabricatorProjectTriggerNameTransaction::TRANSACTIONTYPE) + ->setNewValue($v_name); + + $xactions[] = $trigger->getApplicationTransactionTemplate() + ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY) + ->setNewValue($v_edit); + + $editor = $trigger->getApplicationTransactionEditor() + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true); + + $editor->applyTransactions($trigger, $xactions); + + $next_uri = $trigger->getURI(); + + if ($column) { + $column_xactions = array(); + + // TODO: Modularize column transactions so we can change the column + // trigger here. For now, this does nothing. + + $column_editor = $column->getApplicationTransactionEditor() + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true); + + $column_editor->applyTransactions($column, $column_xactions); + + $next_uri = $column->getBoardURI(); + } + + return id(new AphrontRedirectResponse())->setURI($next_uri); + } catch (PhabricatorApplicationTransactionValidationException $ex) { + $validation_exception = $ex; + + $e_name = $ex->getShortMessage( + PhabricatorProjectTriggerNameTransaction::TRANSACTIONTYPE); + + $e_edit = $ex->getShortMessage( + PhabricatorTransactions::TYPE_EDIT_POLICY); + + $trigger->setEditPolicy($v_edit); + } + } + + if ($trigger->getID()) { + $title = $trigger->getObjectName(); + $submit = pht('Save Trigger'); + $header = pht('Edit Trigger: %s', $trigger->getObjectName()); + } else { + $title = pht('New Trigger'); + $submit = pht('Create Trigger'); + $header = pht('New Trigger'); + } + + $form = id(new AphrontFormView()) + ->setViewer($viewer); + + if ($column) { + $form->addHiddenInput('columnPHID', $column->getPHID()); + } + + $form->appendControl( + id(new AphrontFormTextControl()) + ->setLabel(pht('Name')) + ->setName('name') + ->setValue($v_name) + ->setError($e_name) + ->setPlaceholder($trigger->getDefaultName())); + + $policies = id(new PhabricatorPolicyQuery()) + ->setViewer($viewer) + ->setObject($trigger) + ->execute(); + + $form->appendControl( + id(new AphrontFormPolicyControl()) + ->setName('editPolicy') + ->setPolicyObject($trigger) + ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) + ->setPolicies($policies) + ->setError($e_edit)); + + $form->appendControl( + id(new AphrontFormSubmitControl()) + ->setValue($submit) + ->addCancelButton($cancel_uri)); + + $header = id(new PHUIHeaderView()) + ->setHeader($header); + + $box_view = id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setValidationException($validation_exception) + ->appendChild($form); + + $column_view = id(new PHUITwoColumnView()) + ->setFooter($box_view); + + $crumbs = $this->buildApplicationCrumbs() + ->setBorder(true); + + if ($column) { + $crumbs->addTextCrumb( + pht( + '%s: %s', + $column->getProject()->getDisplayName(), + $column->getName()), + $board_uri); + } + + $crumbs->addTextCrumb($title); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->appendChild($column_view); + } + +} diff --git a/src/applications/project/controller/trigger/PhabricatorProjectTriggerListController.php b/src/applications/project/controller/trigger/PhabricatorProjectTriggerListController.php new file mode 100644 index 0000000000..62e5430f26 --- /dev/null +++ b/src/applications/project/controller/trigger/PhabricatorProjectTriggerListController.php @@ -0,0 +1,16 @@ +setController($this) + ->buildResponse(); + } + +} diff --git a/src/applications/project/controller/trigger/PhabricatorProjectTriggerViewController.php b/src/applications/project/controller/trigger/PhabricatorProjectTriggerViewController.php new file mode 100644 index 0000000000..d1966cf106 --- /dev/null +++ b/src/applications/project/controller/trigger/PhabricatorProjectTriggerViewController.php @@ -0,0 +1,168 @@ +getRequest(); + $viewer = $request->getViewer(); + + $id = $request->getURIData('id'); + + $trigger = id(new PhabricatorProjectTriggerQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->executeOne(); + if (!$trigger) { + return new Aphront404Response(); + } + + $columns_view = $this->newColumnsView($trigger); + + $title = $trigger->getObjectName(); + + $header = id(new PHUIHeaderView()) + ->setHeader($trigger->getDisplayName()); + + $timeline = $this->buildTransactionTimeline( + $trigger, + new PhabricatorProjectTriggerTransactionQuery()); + $timeline->setShouldTerminate(true); + + $curtain = $this->newCurtain($trigger); + + $column_view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setCurtain($curtain) + ->setMainColumn( + array( + $columns_view, + $timeline, + )); + + $crumbs = $this->buildApplicationCrumbs() + ->addTextCrumb($trigger->getObjectName()) + ->setBorder(true); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->appendChild($column_view); + } + + private function newColumnsView(PhabricatorProjectTrigger $trigger) { + $viewer = $this->getViewer(); + + // NOTE: When showing columns which use this trigger, we want to represent + // all columns the trigger is used by: even columns the user can't see. + + // If we hide columns the viewer can't see, they might think that the + // trigger isn't widely used and is safe to edit, when it may actually + // be in use on workboards they don't have access to. + + // Query the columns with the omnipotent viewer first, then pull out their + // PHIDs and throw the actual objects away. Re-query with the real viewer + // so we load only the columns they can actually see, but have a list of + // all the impacted column PHIDs. + + $omnipotent_viewer = PhabricatorUser::getOmnipotentUser(); + $all_columns = id(new PhabricatorProjectColumnQuery()) + ->setViewer($omnipotent_viewer) + ->withTriggerPHIDs(array($trigger->getPHID())) + ->execute(); + $column_phids = mpull($all_columns, 'getPHID'); + + if ($column_phids) { + $visible_columns = id(new PhabricatorProjectColumnQuery()) + ->setViewer($viewer) + ->withPHIDs($column_phids) + ->execute(); + $visible_columns = mpull($visible_columns, null, 'getPHID'); + } else { + $visible_columns = array(); + } + + $rows = array(); + foreach ($column_phids as $column_phid) { + $column = idx($visible_columns, $column_phid); + + if ($column) { + $project = $column->getProject(); + + $project_name = phutil_tag( + 'a', + array( + 'href' => $project->getURI(), + ), + $project->getDisplayName()); + + $column_name = phutil_tag( + 'a', + array( + 'href' => $column->getBoardURI(), + ), + $column->getDisplayName()); + } else { + $project_name = null; + $column_name = phutil_tag('em', array(), pht('Restricted Column')); + } + + $rows[] = array( + $project_name, + $column_name, + ); + } + + $table_view = id(new AphrontTableView($rows)) + ->setNoDataString(pht('This trigger is not used by any columns.')) + ->setHeaders( + array( + pht('Project'), + pht('Column'), + )) + ->setColumnClasses( + array( + null, + 'wide pri', + )); + + $header_view = id(new PHUIHeaderView()) + ->setHeader(pht('Used by Columns')); + + return id(new PHUIObjectBoxView()) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setHeader($header_view) + ->setTable($table_view); + } + + private function newCurtain(PhabricatorProjectTrigger $trigger) { + $viewer = $this->getViewer(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $trigger, + PhabricatorPolicyCapability::CAN_EDIT); + + $curtain = $this->newCurtainView($trigger); + + $edit_uri = $this->getApplicationURI( + urisprintf( + 'trigger/edit/%d/', + $trigger->getID())); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit Trigger')) + ->setIcon('fa-pencil') + ->setHref($edit_uri) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit)); + + return $curtain; + } + +} diff --git a/src/applications/project/editor/PhabricatorProjectTriggerEditor.php b/src/applications/project/editor/PhabricatorProjectTriggerEditor.php new file mode 100644 index 0000000000..20098fa370 --- /dev/null +++ b/src/applications/project/editor/PhabricatorProjectTriggerEditor.php @@ -0,0 +1,30 @@ +setViewer($viewer) ->withProjectPHIDs(array_keys($boards)) + ->needTriggers(true) ->execute(); $columns = msort($columns, 'getOrderingKey'); $columns = mpull($columns, null, 'getPHID'); diff --git a/src/applications/project/phid/PhabricatorProjectTriggerPHIDType.php b/src/applications/project/phid/PhabricatorProjectTriggerPHIDType.php new file mode 100644 index 0000000000..346b0e69fa --- /dev/null +++ b/src/applications/project/phid/PhabricatorProjectTriggerPHIDType.php @@ -0,0 +1,45 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $trigger = $objects[$phid]; + + $handle->setName($trigger->getDisplayName()); + $handle->setURI($trigger->getURI()); + } + } + +} diff --git a/src/applications/project/query/PhabricatorProjectColumnQuery.php b/src/applications/project/query/PhabricatorProjectColumnQuery.php index 441c33e8cb..03169a7827 100644 --- a/src/applications/project/query/PhabricatorProjectColumnQuery.php +++ b/src/applications/project/query/PhabricatorProjectColumnQuery.php @@ -9,6 +9,8 @@ final class PhabricatorProjectColumnQuery private $proxyPHIDs; private $statuses; private $isProxyColumn; + private $triggerPHIDs; + private $needTriggers; public function withIDs(array $ids) { $this->ids = $ids; @@ -40,6 +42,16 @@ final class PhabricatorProjectColumnQuery return $this; } + public function withTriggerPHIDs(array $trigger_phids) { + $this->triggerPHIDs = $trigger_phids; + return $this; + } + + public function needTriggers($need_triggers) { + $this->needTriggers = true; + return $this; + } + public function newResultObject() { return new PhabricatorProjectColumn(); } @@ -121,6 +133,42 @@ final class PhabricatorProjectColumnQuery $column->attachProxy($proxy); } + if ($this->needTriggers) { + $trigger_phids = array(); + foreach ($page as $column) { + if ($column->canHaveTrigger()) { + $trigger_phid = $column->getTriggerPHID(); + if ($trigger_phid) { + $trigger_phids[] = $trigger_phid; + } + } + } + + if ($trigger_phids) { + $triggers = id(new PhabricatorProjectTriggerQuery()) + ->setViewer($this->getViewer()) + ->setParentQuery($this) + ->withPHIDs(array($this->getPHID())) + ->execute(); + $triggers = mpull($triggers, null, 'getPHID'); + } else { + $triggers = array(); + } + + foreach ($page as $column) { + $trigger = null; + + if ($column->canHaveTrigger()) { + $trigger_phid = $column->getTriggerPHID(); + if ($trigger_phid) { + $trigger = idx($triggers, $trigger_phid); + } + } + + $column->attachTrigger($trigger); + } + } + return $page; } @@ -162,6 +210,13 @@ final class PhabricatorProjectColumnQuery $this->statuses); } + if ($this->triggerPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'triggerPHID IN (%Ls)', + $this->triggerPHIDs); + } + if ($this->isProxyColumn !== null) { if ($this->isProxyColumn) { $where[] = qsprintf($conn, 'proxyPHID IS NOT NULL'); diff --git a/src/applications/project/query/PhabricatorProjectTriggerQuery.php b/src/applications/project/query/PhabricatorProjectTriggerQuery.php new file mode 100644 index 0000000000..e3fab5b3d0 --- /dev/null +++ b/src/applications/project/query/PhabricatorProjectTriggerQuery.php @@ -0,0 +1,51 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function newResultObject() { + return new PhabricatorProjectTrigger(); + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); + + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->phids !== null) { + $where[] = qsprintf( + $conn, + 'phid IN (%Ls)', + $this->phids); + } + + return $where; + } + + public function getQueryApplicationClass() { + return 'PhabricatorProjectApplication'; + } + +} diff --git a/src/applications/project/query/PhabricatorProjectTriggerSearchEngine.php b/src/applications/project/query/PhabricatorProjectTriggerSearchEngine.php new file mode 100644 index 0000000000..6c6a417723 --- /dev/null +++ b/src/applications/project/query/PhabricatorProjectTriggerSearchEngine.php @@ -0,0 +1,75 @@ +newQuery(); + + return $query; + } + + protected function getURI($path) { + return '/project/trigger/'.$path; + } + + protected function getBuiltinQueryNames() { + $names = array(); + + $names['all'] = pht('All'); + + return $names; + } + + public function buildSavedQueryFromBuiltin($query_key) { + $query = $this->newSavedQuery(); + $query->setQueryKey($query_key); + + switch ($query_key) { + case 'all': + return $query; + } + + return parent::buildSavedQueryFromBuiltin($query_key); + } + + protected function renderResultList( + array $triggers, + PhabricatorSavedQuery $query, + array $handles) { + assert_instances_of($triggers, 'PhabricatorProjectTrigger'); + $viewer = $this->requireViewer(); + + $list = id(new PHUIObjectItemListView()) + ->setViewer($viewer); + foreach ($triggers as $trigger) { + $item = id(new PHUIObjectItemView()) + ->setObjectName($trigger->getObjectName()) + ->setHeader($trigger->getDisplayName()) + ->setHref($trigger->getURI()); + + $list->addItem($item); + } + + return id(new PhabricatorApplicationSearchResultView()) + ->setObjectList($list) + ->setNoDataString(pht('No triggers found.')); + } + +} diff --git a/src/applications/project/query/PhabricatorProjectTriggerTransactionQuery.php b/src/applications/project/query/PhabricatorProjectTriggerTransactionQuery.php new file mode 100644 index 0000000000..9ec4d4a53b --- /dev/null +++ b/src/applications/project/query/PhabricatorProjectTriggerTransactionQuery.php @@ -0,0 +1,10 @@ + 'uint32', 'sequence' => 'uint32', 'proxyPHID' => 'phid?', + 'triggerPHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( 'key_status' => array( @@ -52,6 +55,9 @@ final class PhabricatorProjectColumn 'columns' => array('projectPHID', 'proxyPHID'), 'unique' => true, ), + 'key_trigger' => array( + 'columns' => array('triggerPHID'), + ), ), ) + parent::getConfiguration(); } @@ -180,6 +186,39 @@ final class PhabricatorProjectColumn return sprintf('%s%012d', $group, $sequence); } + public function attachTrigger(PhabricatorProjectTrigger $trigger = null) { + $this->trigger = $trigger; + return $this; + } + + public function getTrigger() { + return $this->assertAttached($this->trigger); + } + + public function canHaveTrigger() { + // Backlog columns and proxy (subproject / milestone) columns can't have + // triggers because cards routinely end up in these columns through tag + // edits rather than drag-and-drop and it would likely be confusing to + // have these triggers act only a small fraction of the time. + + if ($this->isDefaultColumn()) { + return false; + } + + if ($this->getProxy()) { + return false; + } + + return true; + } + + public function getBoardURI() { + return urisprintf( + '/project/board/%d/', + $this->getProject()->getID()); + } + + /* -( PhabricatorConduitResultInterface )---------------------------------- */ public function getFieldSpecificationsForConduit() { diff --git a/src/applications/project/storage/PhabricatorProjectTrigger.php b/src/applications/project/storage/PhabricatorProjectTrigger.php new file mode 100644 index 0000000000..7730d90529 --- /dev/null +++ b/src/applications/project/storage/PhabricatorProjectTrigger.php @@ -0,0 +1,108 @@ +setName('') + ->setEditPolicy($default_edit); + } + + protected function getConfiguration() { + return array( + self::CONFIG_AUX_PHID => true, + self::CONFIG_SERIALIZATION => array( + 'ruleset' => self::SERIALIZATION_JSON, + ), + self::CONFIG_COLUMN_SCHEMA => array( + 'name' => 'text255', + ), + self::CONFIG_KEY_SCHEMA => array( + ), + ) + parent::getConfiguration(); + } + + public function getPHIDType() { + return PhabricatorProjectTriggerPHIDType::TYPECONST; + } + + public function getDisplayName() { + $name = $this->getName(); + if (strlen($name)) { + return $name; + } + + return $this->getDefaultName(); + } + + public function getDefaultName() { + return pht('Custom Trigger'); + } + + public function getURI() { + return urisprintf( + '/project/trigger/%d/', + $this->getID()); + } + + public function getObjectName() { + return pht('Trigger %d', $this->getID()); + } + + +/* -( PhabricatorApplicationTransactionInterface )------------------------- */ + + + public function getApplicationTransactionEditor() { + return new PhabricatorProjectTriggerEditor(); + } + + public function getApplicationTransactionTemplate() { + return new PhabricatorProjectTriggerTransaction(); + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + ); + } + + public function getPolicy($capability) { + switch ($capability) { + case PhabricatorPolicyCapability::CAN_VIEW: + return PhabricatorPolicies::getMostOpenPolicy(); + case PhabricatorPolicyCapability::CAN_EDIT: + return $this->getEditPolicy(); + } + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return false; + } + + +/* -( PhabricatorDestructibleInterface )----------------------------------- */ + + + public function destroyObjectPermanently( + PhabricatorDestructionEngine $engine) { + $this->delete(); + } + +} diff --git a/src/applications/project/storage/PhabricatorProjectTriggerTransaction.php b/src/applications/project/storage/PhabricatorProjectTriggerTransaction.php new file mode 100644 index 0000000000..fb94bdc364 --- /dev/null +++ b/src/applications/project/storage/PhabricatorProjectTriggerTransaction.php @@ -0,0 +1,18 @@ +getName(); + } + + public function applyInternalEffects($object, $value) { + $object->setName($value); + } + + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + if (strlen($old) && strlen($new)) { + return pht( + '%s renamed this trigger from %s to %s.', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } else if (strlen($new)) { + return pht( + '%s named this trigger %s.', + $this->renderAuthor(), + $this->renderNewValue()); + } else { + return pht( + '%s stripped the name %s from this trigger.', + $this->renderAuthor(), + $this->renderOldValue()); + } + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + $max_length = $object->getColumnMaximumByteLength('name'); + foreach ($xactions as $xaction) { + $new_value = $xaction->getNewValue(); + $new_length = strlen($new_value); + if ($new_length > $max_length) { + $errors[] = $this->newInvalidError( + pht( + 'Trigger names must not be longer than %s characters.', + new PhutilNumber($max_length)), + $xaction); + } + } + + return $errors; + } + +} diff --git a/src/applications/project/xaction/trigger/PhabricatorProjectTriggerTransactionType.php b/src/applications/project/xaction/trigger/PhabricatorProjectTriggerTransactionType.php new file mode 100644 index 0000000000..30222e1e2c --- /dev/null +++ b/src/applications/project/xaction/trigger/PhabricatorProjectTriggerTransactionType.php @@ -0,0 +1,4 @@ +getObjectList()) { From 0204489a526500d9f192eb7103427fc6509254e5 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 14 Mar 2019 08:11:08 -0700 Subject: [PATCH 192/245] Modularize workboard column transactions Summary: Depends on D20279. Ref T5474. Modernize these transactions before I add a new "TriggerTransaction" for setting triggers. Test Plan: Created a column. Edited a column name and point limit. Hid and un-hid a column. Grepped for removed symbols. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T5474 Differential Revision: https://secure.phabricator.com/D20286 --- src/__phutil_library_map__.php | 10 +- ...PhabricatorProjectColumnEditController.php | 5 +- ...PhabricatorProjectColumnHideController.php | 4 +- ...bricatorProjectColumnTransactionEditor.php | 126 +----------------- .../PhabricatorProjectColumnTransaction.php | 70 +--------- ...abricatorProjectColumnLimitTransaction.php | 63 +++++++++ ...habricatorProjectColumnNameTransaction.php | 66 +++++++++ ...bricatorProjectColumnStatusTransaction.php | 55 ++++++++ ...habricatorProjectColumnTransactionType.php | 4 + 9 files changed, 209 insertions(+), 194 deletions(-) create mode 100644 src/applications/project/xaction/column/PhabricatorProjectColumnLimitTransaction.php create mode 100644 src/applications/project/xaction/column/PhabricatorProjectColumnNameTransaction.php create mode 100644 src/applications/project/xaction/column/PhabricatorProjectColumnStatusTransaction.php create mode 100644 src/applications/project/xaction/column/PhabricatorProjectColumnTransactionType.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 258ebf1f8f..110062e861 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4059,6 +4059,8 @@ phutil_register_library_map(array( 'PhabricatorProjectColumnEditController' => 'applications/project/controller/PhabricatorProjectColumnEditController.php', 'PhabricatorProjectColumnHeader' => 'applications/project/order/PhabricatorProjectColumnHeader.php', 'PhabricatorProjectColumnHideController' => 'applications/project/controller/PhabricatorProjectColumnHideController.php', + 'PhabricatorProjectColumnLimitTransaction' => 'applications/project/xaction/column/PhabricatorProjectColumnLimitTransaction.php', + 'PhabricatorProjectColumnNameTransaction' => 'applications/project/xaction/column/PhabricatorProjectColumnNameTransaction.php', 'PhabricatorProjectColumnNaturalOrder' => 'applications/project/order/PhabricatorProjectColumnNaturalOrder.php', 'PhabricatorProjectColumnOrder' => 'applications/project/order/PhabricatorProjectColumnOrder.php', 'PhabricatorProjectColumnOwnerOrder' => 'applications/project/order/PhabricatorProjectColumnOwnerOrder.php', @@ -4070,10 +4072,12 @@ phutil_register_library_map(array( 'PhabricatorProjectColumnQuery' => 'applications/project/query/PhabricatorProjectColumnQuery.php', 'PhabricatorProjectColumnSearchEngine' => 'applications/project/query/PhabricatorProjectColumnSearchEngine.php', 'PhabricatorProjectColumnStatusOrder' => 'applications/project/order/PhabricatorProjectColumnStatusOrder.php', + 'PhabricatorProjectColumnStatusTransaction' => 'applications/project/xaction/column/PhabricatorProjectColumnStatusTransaction.php', 'PhabricatorProjectColumnTitleOrder' => 'applications/project/order/PhabricatorProjectColumnTitleOrder.php', 'PhabricatorProjectColumnTransaction' => 'applications/project/storage/PhabricatorProjectColumnTransaction.php', 'PhabricatorProjectColumnTransactionEditor' => 'applications/project/editor/PhabricatorProjectColumnTransactionEditor.php', 'PhabricatorProjectColumnTransactionQuery' => 'applications/project/query/PhabricatorProjectColumnTransactionQuery.php', + 'PhabricatorProjectColumnTransactionType' => 'applications/project/xaction/column/PhabricatorProjectColumnTransactionType.php', 'PhabricatorProjectConfigOptions' => 'applications/project/config/PhabricatorProjectConfigOptions.php', 'PhabricatorProjectConfiguredCustomField' => 'applications/project/customfield/PhabricatorProjectConfiguredCustomField.php', 'PhabricatorProjectController' => 'applications/project/controller/PhabricatorProjectController.php', @@ -10166,6 +10170,8 @@ phutil_register_library_map(array( 'PhabricatorProjectColumnEditController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectColumnHeader' => 'Phobject', 'PhabricatorProjectColumnHideController' => 'PhabricatorProjectBoardController', + 'PhabricatorProjectColumnLimitTransaction' => 'PhabricatorProjectColumnTransactionType', + 'PhabricatorProjectColumnNameTransaction' => 'PhabricatorProjectColumnTransactionType', 'PhabricatorProjectColumnNaturalOrder' => 'PhabricatorProjectColumnOrder', 'PhabricatorProjectColumnOrder' => 'Phobject', 'PhabricatorProjectColumnOwnerOrder' => 'PhabricatorProjectColumnOrder', @@ -10180,10 +10186,12 @@ phutil_register_library_map(array( 'PhabricatorProjectColumnQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorProjectColumnSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorProjectColumnStatusOrder' => 'PhabricatorProjectColumnOrder', + 'PhabricatorProjectColumnStatusTransaction' => 'PhabricatorProjectColumnTransactionType', 'PhabricatorProjectColumnTitleOrder' => 'PhabricatorProjectColumnOrder', - 'PhabricatorProjectColumnTransaction' => 'PhabricatorApplicationTransaction', + 'PhabricatorProjectColumnTransaction' => 'PhabricatorModularTransaction', 'PhabricatorProjectColumnTransactionEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorProjectColumnTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'PhabricatorProjectColumnTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorProjectConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorProjectConfiguredCustomField' => array( 'PhabricatorProjectStandardCustomField', diff --git a/src/applications/project/controller/PhabricatorProjectColumnEditController.php b/src/applications/project/controller/PhabricatorProjectColumnEditController.php index 94277c92e5..567b923407 100644 --- a/src/applications/project/controller/PhabricatorProjectColumnEditController.php +++ b/src/applications/project/controller/PhabricatorProjectColumnEditController.php @@ -76,8 +76,8 @@ final class PhabricatorProjectColumnEditController $xactions = array(); - $type_name = PhabricatorProjectColumnTransaction::TYPE_NAME; - $type_limit = PhabricatorProjectColumnTransaction::TYPE_LIMIT; + $type_name = PhabricatorProjectColumnNameTransaction::TRANSACTIONTYPE; + $type_limit = PhabricatorProjectColumnLimitTransaction::TRANSACTIONTYPE; if (!$column->getProxy()) { $xactions[] = id(new PhabricatorProjectColumnTransaction()) @@ -93,7 +93,6 @@ final class PhabricatorProjectColumnEditController $editor = id(new PhabricatorProjectColumnTransactionEditor()) ->setActor($viewer) ->setContinueOnNoEffect(true) - ->setContinueOnMissingFields(true) ->setContentSourceFromRequest($request) ->applyTransactions($column, $xactions); return id(new AphrontRedirectResponse())->setURI($view_uri); diff --git a/src/applications/project/controller/PhabricatorProjectColumnHideController.php b/src/applications/project/controller/PhabricatorProjectColumnHideController.php index fbda2feb1e..61811af5c3 100644 --- a/src/applications/project/controller/PhabricatorProjectColumnHideController.php +++ b/src/applications/project/controller/PhabricatorProjectColumnHideController.php @@ -82,7 +82,9 @@ final class PhabricatorProjectColumnHideController $new_status = PhabricatorProjectColumn::STATUS_HIDDEN; } - $type_status = PhabricatorProjectColumnTransaction::TYPE_STATUS; + $type_status = + PhabricatorProjectColumnStatusTransaction::TRANSACTIONTYPE; + $xactions = array( id(new PhabricatorProjectColumnTransaction()) ->setTransactionType($type_status) diff --git a/src/applications/project/editor/PhabricatorProjectColumnTransactionEditor.php b/src/applications/project/editor/PhabricatorProjectColumnTransactionEditor.php index d494767085..e0becc3470 100644 --- a/src/applications/project/editor/PhabricatorProjectColumnTransactionEditor.php +++ b/src/applications/project/editor/PhabricatorProjectColumnTransactionEditor.php @@ -11,130 +11,12 @@ final class PhabricatorProjectColumnTransactionEditor return pht('Workboard Columns'); } - public function getTransactionTypes() { - $types = parent::getTransactionTypes(); - - $types[] = PhabricatorProjectColumnTransaction::TYPE_NAME; - $types[] = PhabricatorProjectColumnTransaction::TYPE_STATUS; - $types[] = PhabricatorProjectColumnTransaction::TYPE_LIMIT; - - return $types; + public function getCreateObjectTitle($author, $object) { + return pht('%s created this column.', $author); } - protected function getCustomTransactionOldValue( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case PhabricatorProjectColumnTransaction::TYPE_NAME: - return $object->getName(); - case PhabricatorProjectColumnTransaction::TYPE_STATUS: - return $object->getStatus(); - case PhabricatorProjectColumnTransaction::TYPE_LIMIT: - return $object->getPointLimit(); - - } - - return parent::getCustomTransactionOldValue($object, $xaction); - } - - protected function getCustomTransactionNewValue( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case PhabricatorProjectColumnTransaction::TYPE_NAME: - case PhabricatorProjectColumnTransaction::TYPE_STATUS: - return $xaction->getNewValue(); - case PhabricatorProjectColumnTransaction::TYPE_LIMIT: - $value = $xaction->getNewValue(); - if (strlen($value)) { - return (int)$xaction->getNewValue(); - } else { - return null; - } - } - - return parent::getCustomTransactionNewValue($object, $xaction); - } - - protected function applyCustomInternalTransaction( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case PhabricatorProjectColumnTransaction::TYPE_NAME: - $object->setName($xaction->getNewValue()); - return; - case PhabricatorProjectColumnTransaction::TYPE_STATUS: - $object->setStatus($xaction->getNewValue()); - return; - case PhabricatorProjectColumnTransaction::TYPE_LIMIT: - $object->setPointLimit($xaction->getNewValue()); - return; - } - - return parent::applyCustomInternalTransaction($object, $xaction); - } - - protected function applyCustomExternalTransaction( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case PhabricatorProjectColumnTransaction::TYPE_NAME: - case PhabricatorProjectColumnTransaction::TYPE_STATUS: - case PhabricatorProjectColumnTransaction::TYPE_LIMIT: - return; - } - - return parent::applyCustomExternalTransaction($object, $xaction); - } - - protected function validateTransaction( - PhabricatorLiskDAO $object, - $type, - array $xactions) { - - $errors = parent::validateTransaction($object, $type, $xactions); - - switch ($type) { - case PhabricatorProjectColumnTransaction::TYPE_LIMIT: - foreach ($xactions as $xaction) { - $value = $xaction->getNewValue(); - if (strlen($value) && !preg_match('/^\d+\z/', $value)) { - $errors[] = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Invalid'), - pht( - 'Column point limit must either be empty or a nonnegative '. - 'integer.'), - $xaction); - } - } - break; - case PhabricatorProjectColumnTransaction::TYPE_NAME: - $missing = $this->validateIsEmptyTextField( - $object->getName(), - $xactions); - - // The default "Backlog" column is allowed to be unnamed, which - // means we use the default name. - - if ($missing && !$object->isDefaultColumn()) { - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Required'), - pht('Column name is required.'), - nonempty(last($xactions), null)); - - $error->setIsMissingFieldError(true); - $errors[] = $error; - } - break; - } - - return $errors; + public function getCreateObjectTitleForFeed($author, $object) { + return pht('%s created %s.', $author, $object); } } diff --git a/src/applications/project/storage/PhabricatorProjectColumnTransaction.php b/src/applications/project/storage/PhabricatorProjectColumnTransaction.php index ed4bfed8a6..35a7461ca2 100644 --- a/src/applications/project/storage/PhabricatorProjectColumnTransaction.php +++ b/src/applications/project/storage/PhabricatorProjectColumnTransaction.php @@ -1,11 +1,7 @@ getOldValue(); - $new = $this->getNewValue(); - $author_handle = $this->renderHandleLink($this->getAuthorPHID()); - - switch ($this->getTransactionType()) { - case self::TYPE_NAME: - if ($old === null) { - return pht( - '%s created this column.', - $author_handle); - } else { - if (!strlen($old)) { - return pht( - '%s named this column "%s".', - $author_handle, - $new); - } else if (strlen($new)) { - return pht( - '%s renamed this column from "%s" to "%s".', - $author_handle, - $old, - $new); - } else { - return pht( - '%s removed the custom name of this column.', - $author_handle); - } - } - case self::TYPE_LIMIT: - if (!$old) { - return pht( - '%s set the point limit for this column to %s.', - $author_handle, - $new); - } else if (!$new) { - return pht( - '%s removed the point limit for this column.', - $author_handle); - } else { - return pht( - '%s changed point limit for this column from %s to %s.', - $author_handle, - $old, - $new); - } - - case self::TYPE_STATUS: - switch ($new) { - case PhabricatorProjectColumn::STATUS_ACTIVE: - return pht( - '%s marked this column visible.', - $author_handle); - case PhabricatorProjectColumn::STATUS_HIDDEN: - return pht( - '%s marked this column hidden.', - $author_handle); - } - break; - } - - return parent::getTitle(); + public function getBaseTransactionClass() { + return 'PhabricatorProjectColumnTransactionType'; } } diff --git a/src/applications/project/xaction/column/PhabricatorProjectColumnLimitTransaction.php b/src/applications/project/xaction/column/PhabricatorProjectColumnLimitTransaction.php new file mode 100644 index 0000000000..8e91ccbe5d --- /dev/null +++ b/src/applications/project/xaction/column/PhabricatorProjectColumnLimitTransaction.php @@ -0,0 +1,63 @@ +getPointLimit(); + } + + public function generateNewValue($object, $value) { + if (strlen($value)) { + return (int)$value; + } else { + return null; + } + } + + public function applyInternalEffects($object, $value) { + $object->setPointLimit($value); + } + + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + if (!$old) { + return pht( + '%s set the point limit for this column to %s.', + $this->renderAuthor(), + $this->renderNewValue()); + } else if (!$new) { + return pht( + '%s removed the point limit for this column.', + $this->renderAuthor()); + } else { + return pht( + '%s changed the point limit for this column from %s to %s.', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + foreach ($xactions as $xaction) { + $value = $xaction->getNewValue(); + if (strlen($value) && !preg_match('/^\d+\z/', $value)) { + $errors[] = $this->newInvalidError( + pht( + 'Column point limit must either be empty or a nonnegative '. + 'integer.'), + $xaction); + } + } + + return $errors; + } + +} diff --git a/src/applications/project/xaction/column/PhabricatorProjectColumnNameTransaction.php b/src/applications/project/xaction/column/PhabricatorProjectColumnNameTransaction.php new file mode 100644 index 0000000000..bff54277de --- /dev/null +++ b/src/applications/project/xaction/column/PhabricatorProjectColumnNameTransaction.php @@ -0,0 +1,66 @@ +getName(); + } + + public function applyInternalEffects($object, $value) { + $object->setName($value); + } + + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + if (!strlen($old)) { + return pht( + '%s named this column %s.', + $this->renderAuthor(), + $this->renderNewValue()); + } else if (strlen($new)) { + return pht( + '%s renamed this column from %s to %s.', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } else { + return pht( + '%s removed the custom name of this column.', + $this->renderAuthor()); + } + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + if ($this->isEmptyTextTransaction($object->getName(), $xactions)) { + // The default "Backlog" column is allowed to be unnamed, which + // means we use the default name. + if (!$object->isDefaultColumn()) { + $errors[] = $this->newRequiredError( + pht('Columns must have a name.')); + } + } + + $max_length = $object->getColumnMaximumByteLength('name'); + foreach ($xactions as $xaction) { + $new_value = $xaction->getNewValue(); + $new_length = strlen($new_value); + if ($new_length > $max_length) { + $errors[] = $this->newInvalidError( + pht( + 'Column names must not be longer than %s characters.', + new PhutilNumber($max_length)), + $xaction); + } + } + + return $errors; + } + +} diff --git a/src/applications/project/xaction/column/PhabricatorProjectColumnStatusTransaction.php b/src/applications/project/xaction/column/PhabricatorProjectColumnStatusTransaction.php new file mode 100644 index 0000000000..7606c72562 --- /dev/null +++ b/src/applications/project/xaction/column/PhabricatorProjectColumnStatusTransaction.php @@ -0,0 +1,55 @@ +getStatus(); + } + + public function applyInternalEffects($object, $value) { + $object->setStatus($value); + } + + public function getTitle() { + $new = $this->getNewValue(); + + switch ($new) { + case PhabricatorProjectColumn::STATUS_ACTIVE: + return pht( + '%s unhid this column.', + $this->renderAuthor()); + case PhabricatorProjectColumn::STATUS_HIDDEN: + return pht( + '%s hid this column.', + $this->renderAuthor()); + } + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + $map = array( + PhabricatorProjectColumn::STATUS_ACTIVE, + PhabricatorProjectColumn::STATUS_HIDDEN, + ); + $map = array_fuse($map); + + foreach ($xactions as $xaction) { + $value = $xaction->getNewValue(); + if (!isset($map[$value])) { + $errors[] = $this->newInvalidError( + pht( + 'Column status "%s" is unrecognized, valid statuses are: %s.', + $value, + implode(', ', array_keys($map))), + $xaction); + } + } + + return $errors; + } + +} diff --git a/src/applications/project/xaction/column/PhabricatorProjectColumnTransactionType.php b/src/applications/project/xaction/column/PhabricatorProjectColumnTransactionType.php new file mode 100644 index 0000000000..1473d3cabb --- /dev/null +++ b/src/applications/project/xaction/column/PhabricatorProjectColumnTransactionType.php @@ -0,0 +1,4 @@ + Date: Thu, 14 Mar 2019 08:36:43 -0700 Subject: [PATCH 193/245] Allow triggers to be attached to and removed from workboard columns Summary: Depends on D20286. Ref T5474. Attaches triggers to columns and makes "Remove Trigger" work. (There's no "pick an existing named trigger from a list" UI yet, but I plan to add that at some point.) Test Plan: Attached and removed triggers, saw column UI update appropriately. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T5474 Differential Revision: https://secure.phabricator.com/D20287 --- src/__phutil_library_map__.php | 4 + .../PhabricatorProjectApplication.php | 4 + .../PhabricatorProjectBoardViewController.php | 120 +++++++++++++----- ...orProjectColumnRemoveTriggerController.php | 60 +++++++++ ...habricatorProjectTriggerEditController.php | 9 +- .../query/PhabricatorProjectColumnQuery.php | 2 +- .../storage/PhabricatorProjectTrigger.php | 20 ++- ...ricatorProjectColumnTriggerTransaction.php | 78 ++++++++++++ 8 files changed, 257 insertions(+), 40 deletions(-) create mode 100644 src/applications/project/controller/PhabricatorProjectColumnRemoveTriggerController.php create mode 100644 src/applications/project/xaction/column/PhabricatorProjectColumnTriggerTransaction.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 110062e861..50df6a7201 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4070,6 +4070,7 @@ phutil_register_library_map(array( 'PhabricatorProjectColumnPositionQuery' => 'applications/project/query/PhabricatorProjectColumnPositionQuery.php', 'PhabricatorProjectColumnPriorityOrder' => 'applications/project/order/PhabricatorProjectColumnPriorityOrder.php', 'PhabricatorProjectColumnQuery' => 'applications/project/query/PhabricatorProjectColumnQuery.php', + 'PhabricatorProjectColumnRemoveTriggerController' => 'applications/project/controller/PhabricatorProjectColumnRemoveTriggerController.php', 'PhabricatorProjectColumnSearchEngine' => 'applications/project/query/PhabricatorProjectColumnSearchEngine.php', 'PhabricatorProjectColumnStatusOrder' => 'applications/project/order/PhabricatorProjectColumnStatusOrder.php', 'PhabricatorProjectColumnStatusTransaction' => 'applications/project/xaction/column/PhabricatorProjectColumnStatusTransaction.php', @@ -4078,6 +4079,7 @@ phutil_register_library_map(array( 'PhabricatorProjectColumnTransactionEditor' => 'applications/project/editor/PhabricatorProjectColumnTransactionEditor.php', 'PhabricatorProjectColumnTransactionQuery' => 'applications/project/query/PhabricatorProjectColumnTransactionQuery.php', 'PhabricatorProjectColumnTransactionType' => 'applications/project/xaction/column/PhabricatorProjectColumnTransactionType.php', + 'PhabricatorProjectColumnTriggerTransaction' => 'applications/project/xaction/column/PhabricatorProjectColumnTriggerTransaction.php', 'PhabricatorProjectConfigOptions' => 'applications/project/config/PhabricatorProjectConfigOptions.php', 'PhabricatorProjectConfiguredCustomField' => 'applications/project/customfield/PhabricatorProjectConfiguredCustomField.php', 'PhabricatorProjectController' => 'applications/project/controller/PhabricatorProjectController.php', @@ -10184,6 +10186,7 @@ phutil_register_library_map(array( 'PhabricatorProjectColumnPositionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorProjectColumnPriorityOrder' => 'PhabricatorProjectColumnOrder', 'PhabricatorProjectColumnQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorProjectColumnRemoveTriggerController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectColumnSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorProjectColumnStatusOrder' => 'PhabricatorProjectColumnOrder', 'PhabricatorProjectColumnStatusTransaction' => 'PhabricatorProjectColumnTransactionType', @@ -10192,6 +10195,7 @@ phutil_register_library_map(array( 'PhabricatorProjectColumnTransactionEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorProjectColumnTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorProjectColumnTransactionType' => 'PhabricatorModularTransactionType', + 'PhabricatorProjectColumnTriggerTransaction' => 'PhabricatorProjectColumnTransactionType', 'PhabricatorProjectConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorProjectConfiguredCustomField' => array( 'PhabricatorProjectStandardCustomField', diff --git a/src/applications/project/application/PhabricatorProjectApplication.php b/src/applications/project/application/PhabricatorProjectApplication.php index 192b40f6cd..46d7558f5b 100644 --- a/src/applications/project/application/PhabricatorProjectApplication.php +++ b/src/applications/project/application/PhabricatorProjectApplication.php @@ -89,6 +89,10 @@ final class PhabricatorProjectApplication extends PhabricatorApplication { 'background/' => 'PhabricatorProjectBoardBackgroundController', ), + 'column/' => array( + 'remove/(?P\d+)/' => + 'PhabricatorProjectColumnRemoveTriggerController', + ), 'trigger/' => array( $this->getQueryRoutePattern() => 'PhabricatorProjectTriggerListController', diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index 9b882c6ccc..dda21c0c43 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -574,6 +574,11 @@ final class PhabricatorProjectBoardViewController $column_menu = $this->buildColumnMenu($project, $column); $panel->addHeaderAction($column_menu); + if ($column->canHaveTrigger()) { + $trigger_menu = $this->buildTriggerMenu($column); + $panel->addHeaderAction($trigger_menu); + } + $count_tag = id(new PHUITagView()) ->setType(PHUITagView::TYPE_SHADE) ->setColor(PHUITagView::COLOR_BLUE) @@ -1172,40 +1177,6 @@ final class PhabricatorProjectBoardViewController ->setWorkflow(true); } - if ($column->canHaveTrigger()) { - $column_items[] = id(new PhabricatorActionView()) - ->setType(PhabricatorActionView::TYPE_DIVIDER); - - $trigger = $column->getTrigger(); - if (!$trigger) { - $set_uri = $this->getApplicationURI( - new PhutilURI( - 'trigger/edit/', - array( - 'columnPHID' => $column->getPHID(), - ))); - - $column_items[] = id(new PhabricatorActionView()) - ->setIcon('fa-cogs') - ->setName(pht('New Trigger...')) - ->setHref($set_uri) - ->setDisabled(!$can_edit); - } else { - $column_items[] = id(new PhabricatorActionView()) - ->setIcon('fa-cogs') - ->setName(pht('View Trigger')) - ->setHref($trigger->getURI()) - ->setDisabled(!$can_edit); - } - - $column_items[] = id(new PhabricatorActionView()) - ->setIcon('fa-times') - ->setName(pht('Remove Trigger')) - ->setHref('#') - ->setWorkflow(true) - ->setDisabled(!$can_edit || !$trigger); - } - $column_menu = id(new PhabricatorActionListView()) ->setUser($viewer); foreach ($column_items as $item) { @@ -1213,7 +1184,7 @@ final class PhabricatorProjectBoardViewController } $column_button = id(new PHUIIconView()) - ->setIcon('fa-caret-down') + ->setIcon('fa-pencil') ->setHref('#') ->addSigil('boards-dropdown-menu') ->setMetadata( @@ -1224,6 +1195,85 @@ final class PhabricatorProjectBoardViewController return $column_button; } + private function buildTriggerMenu(PhabricatorProjectColumn $column) { + $viewer = $this->getViewer(); + $trigger = $column->getTrigger(); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $column, + PhabricatorPolicyCapability::CAN_EDIT); + + $trigger_items = array(); + if (!$trigger) { + $set_uri = $this->getApplicationURI( + new PhutilURI( + 'trigger/edit/', + array( + 'columnPHID' => $column->getPHID(), + ))); + + $trigger_items[] = id(new PhabricatorActionView()) + ->setIcon('fa-cogs') + ->setName(pht('New Trigger...')) + ->setHref($set_uri) + ->setDisabled(!$can_edit); + } else { + $trigger_items[] = id(new PhabricatorActionView()) + ->setIcon('fa-cogs') + ->setName(pht('View Trigger')) + ->setHref($trigger->getURI()) + ->setDisabled(!$can_edit); + } + + $remove_uri = $this->getApplicationURI( + new PhutilURI( + urisprintf( + 'column/remove/%d/', + $column->getID()))); + + $trigger_items[] = id(new PhabricatorActionView()) + ->setIcon('fa-times') + ->setName(pht('Remove Trigger')) + ->setHref($remove_uri) + ->setWorkflow(true) + ->setDisabled(!$can_edit || !$trigger); + + $trigger_menu = id(new PhabricatorActionListView()) + ->setUser($viewer); + foreach ($trigger_items as $item) { + $trigger_menu->addAction($item); + } + + if ($trigger) { + $trigger_icon = 'fa-cogs'; + } else { + $trigger_icon = 'fa-cogs grey'; + } + + if ($trigger) { + $trigger_tip = array( + pht('%s: %s', $trigger->getObjectName(), $trigger->getDisplayName()), + $trigger->getRulesDescription(), + ); + $trigger_tip = implode("\n", $trigger_tip); + } else { + $trigger_tip = pht('No column trigger.'); + } + + $trigger_button = id(new PHUIIconView()) + ->setIcon($trigger_icon) + ->setHref('#') + ->addSigil('boards-dropdown-menu') + ->addSigil('has-tooltip') + ->setMetadata( + array( + 'items' => hsprintf('%s', $trigger_menu), + 'tip' => $trigger_tip, + )); + + return $trigger_button; + } /** * Add current state parameters (like order and the visibility of hidden diff --git a/src/applications/project/controller/PhabricatorProjectColumnRemoveTriggerController.php b/src/applications/project/controller/PhabricatorProjectColumnRemoveTriggerController.php new file mode 100644 index 0000000000..5802449dcb --- /dev/null +++ b/src/applications/project/controller/PhabricatorProjectColumnRemoveTriggerController.php @@ -0,0 +1,60 @@ +getViewer(); + $id = $request->getURIData('id'); + + $column = id(new PhabricatorProjectColumnQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$column) { + return new Aphront404Response(); + } + + $done_uri = $column->getBoardURI(); + + if (!$column->getTriggerPHID()) { + return $this->newDialog() + ->setTitle(pht('No Trigger')) + ->appendParagraph( + pht('This column does not have a trigger.')) + ->addCancelButton($done_uri); + } + + if ($request->isFormPost()) { + $column_xactions = array(); + + $column_xactions[] = $column->getApplicationTransactionTemplate() + ->setTransactionType( + PhabricatorProjectColumnTriggerTransaction::TRANSACTIONTYPE) + ->setNewValue(null); + + $column_editor = $column->getApplicationTransactionEditor() + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true); + + $column_editor->applyTransactions($column, $column_xactions); + + return id(new AphrontRedirectResponse())->setURI($done_uri); + } + + $body = pht('Really remove the trigger from this column?'); + + return $this->newDialog() + ->setTitle(pht('Remove Trigger')) + ->appendParagraph($body) + ->addSubmitButton(pht('Remove Trigger')) + ->addCancelButton($done_uri); + } +} diff --git a/src/applications/project/controller/trigger/PhabricatorProjectTriggerEditController.php b/src/applications/project/controller/trigger/PhabricatorProjectTriggerEditController.php index 86f75225d2..86f0844be3 100644 --- a/src/applications/project/controller/trigger/PhabricatorProjectTriggerEditController.php +++ b/src/applications/project/controller/trigger/PhabricatorProjectTriggerEditController.php @@ -93,13 +93,16 @@ final class PhabricatorProjectTriggerEditController if ($column) { $column_xactions = array(); - // TODO: Modularize column transactions so we can change the column - // trigger here. For now, this does nothing. + $column_xactions[] = $column->getApplicationTransactionTemplate() + ->setTransactionType( + PhabricatorProjectColumnTriggerTransaction::TRANSACTIONTYPE) + ->setNewValue($trigger->getPHID()); $column_editor = $column->getApplicationTransactionEditor() ->setActor($viewer) ->setContentSourceFromRequest($request) - ->setContinueOnNoEffect(true); + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true); $column_editor->applyTransactions($column, $column_xactions); diff --git a/src/applications/project/query/PhabricatorProjectColumnQuery.php b/src/applications/project/query/PhabricatorProjectColumnQuery.php index 03169a7827..380dab5208 100644 --- a/src/applications/project/query/PhabricatorProjectColumnQuery.php +++ b/src/applications/project/query/PhabricatorProjectColumnQuery.php @@ -148,7 +148,7 @@ final class PhabricatorProjectColumnQuery $triggers = id(new PhabricatorProjectTriggerQuery()) ->setViewer($this->getViewer()) ->setParentQuery($this) - ->withPHIDs(array($this->getPHID())) + ->withPHIDs($trigger_phids) ->execute(); $triggers = mpull($triggers, null, 'getPHID'); } else { diff --git a/src/applications/project/storage/PhabricatorProjectTrigger.php b/src/applications/project/storage/PhabricatorProjectTrigger.php index 7730d90529..1df8e935ec 100644 --- a/src/applications/project/storage/PhabricatorProjectTrigger.php +++ b/src/applications/project/storage/PhabricatorProjectTrigger.php @@ -60,6 +60,11 @@ final class PhabricatorProjectTrigger return pht('Trigger %d', $this->getID()); } + public function getRulesDescription() { + // TODO: Summarize the trigger rules in human-readable text. + return pht('Does things.'); + } + /* -( PhabricatorApplicationTransactionInterface )------------------------- */ @@ -102,7 +107,20 @@ final class PhabricatorProjectTrigger public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { - $this->delete(); + + $this->openTransaction(); + $conn = $this->establishConnection('w'); + + // Remove the reference to this trigger from any columns which use it. + queryfx( + $conn, + 'UPDATE %R SET triggerPHID = null WHERE triggerPHID = %s', + new PhabricatorProjectColumn(), + $this->getPHID()); + + $this->delete(); + + $this->saveTransaction(); } } diff --git a/src/applications/project/xaction/column/PhabricatorProjectColumnTriggerTransaction.php b/src/applications/project/xaction/column/PhabricatorProjectColumnTriggerTransaction.php new file mode 100644 index 0000000000..78e2451bd1 --- /dev/null +++ b/src/applications/project/xaction/column/PhabricatorProjectColumnTriggerTransaction.php @@ -0,0 +1,78 @@ +getTriggerPHID(); + } + + public function applyInternalEffects($object, $value) { + $object->setTriggerPHID($value); + } + + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + if (!$old) { + return pht( + '%s set the column trigger to %s.', + $this->renderAuthor(), + $this->renderNewHandle()); + } else if (!$new) { + return pht( + '%s removed the trigger for this column (was %s).', + $this->renderAuthor(), + $this->renderOldHandle()); + } else { + return pht( + '%s changed the trigger for this column from %s to %s.', + $this->renderAuthor(), + $this->renderOldHandle(), + $this->renderNewHandle()); + } + } + + public function validateTransactions($object, array $xactions) { + $actor = $this->getActor(); + $errors = array(); + + foreach ($xactions as $xaction) { + $trigger_phid = $xaction->getNewValue(); + + // You can always remove a trigger. + if (!$trigger_phid) { + continue; + } + + // You can't put a trigger on a column that can't have triggers, like + // a backlog column or a proxy column. + if (!$object->canHaveTrigger()) { + $errors[] = $this->newInvalidError( + pht('This column can not have a trigger.'), + $xaction); + continue; + } + + $trigger = id(new PhabricatorProjectTriggerQuery()) + ->setViewer($actor) + ->withPHIDs(array($trigger_phid)) + ->execute(); + if (!$trigger) { + $errors[] = $this->newInvalidError( + pht( + 'Trigger "%s" is not a valid trigger, or you do not have '. + 'permission to view it.', + $trigger_phid), + $xaction); + continue; + } + } + + return $errors; + } + +} From 149f8cc9595a286854d3bb22624ed6c4da871ce6 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 14 Mar 2019 16:40:18 -0700 Subject: [PATCH 194/245] Hard code a "close task" action on every column Trigger Summary: Depends on D20287. Ref T5474. This hard-codes a storage value for every trigger, with a "Change status to " rule and two bogus rules. Rules may now apply transactions when cards are dropped. Test Plan: Dragged cards to a column with a trigger, saw them close. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T5474 Differential Revision: https://secure.phabricator.com/D20288 --- src/__phutil_library_map__.php | 12 ++ .../PhabricatorProjectBoardViewController.php | 1 + .../PhabricatorProjectMoveController.php | 14 ++ ...catorProjectTriggerCorruptionException.php | 4 + .../storage/PhabricatorProjectTrigger.php | 179 +++++++++++++++++- .../PhabricatorProjectTriggerInvalidRule.php | 22 +++ ...catorProjectTriggerManiphestStatusRule.php | 41 ++++ .../trigger/PhabricatorProjectTriggerRule.php | 89 +++++++++ .../PhabricatorProjectTriggerRuleRecord.php | 27 +++ .../PhabricatorProjectTriggerUnknownRule.php | 22 +++ 10 files changed, 408 insertions(+), 3 deletions(-) create mode 100644 src/applications/project/exception/PhabricatorProjectTriggerCorruptionException.php create mode 100644 src/applications/project/trigger/PhabricatorProjectTriggerInvalidRule.php create mode 100644 src/applications/project/trigger/PhabricatorProjectTriggerManiphestStatusRule.php create mode 100644 src/applications/project/trigger/PhabricatorProjectTriggerRule.php create mode 100644 src/applications/project/trigger/PhabricatorProjectTriggerRuleRecord.php create mode 100644 src/applications/project/trigger/PhabricatorProjectTriggerUnknownRule.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 50df6a7201..b042b017e2 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4174,16 +4174,22 @@ phutil_register_library_map(array( 'PhabricatorProjectTransactionType' => 'applications/project/xaction/PhabricatorProjectTransactionType.php', 'PhabricatorProjectTrigger' => 'applications/project/storage/PhabricatorProjectTrigger.php', 'PhabricatorProjectTriggerController' => 'applications/project/controller/trigger/PhabricatorProjectTriggerController.php', + 'PhabricatorProjectTriggerCorruptionException' => 'applications/project/exception/PhabricatorProjectTriggerCorruptionException.php', 'PhabricatorProjectTriggerEditController' => 'applications/project/controller/trigger/PhabricatorProjectTriggerEditController.php', 'PhabricatorProjectTriggerEditor' => 'applications/project/editor/PhabricatorProjectTriggerEditor.php', + 'PhabricatorProjectTriggerInvalidRule' => 'applications/project/trigger/PhabricatorProjectTriggerInvalidRule.php', 'PhabricatorProjectTriggerListController' => 'applications/project/controller/trigger/PhabricatorProjectTriggerListController.php', + 'PhabricatorProjectTriggerManiphestStatusRule' => 'applications/project/trigger/PhabricatorProjectTriggerManiphestStatusRule.php', 'PhabricatorProjectTriggerNameTransaction' => 'applications/project/xaction/trigger/PhabricatorProjectTriggerNameTransaction.php', 'PhabricatorProjectTriggerPHIDType' => 'applications/project/phid/PhabricatorProjectTriggerPHIDType.php', 'PhabricatorProjectTriggerQuery' => 'applications/project/query/PhabricatorProjectTriggerQuery.php', + 'PhabricatorProjectTriggerRule' => 'applications/project/trigger/PhabricatorProjectTriggerRule.php', + 'PhabricatorProjectTriggerRuleRecord' => 'applications/project/trigger/PhabricatorProjectTriggerRuleRecord.php', 'PhabricatorProjectTriggerSearchEngine' => 'applications/project/query/PhabricatorProjectTriggerSearchEngine.php', 'PhabricatorProjectTriggerTransaction' => 'applications/project/storage/PhabricatorProjectTriggerTransaction.php', 'PhabricatorProjectTriggerTransactionQuery' => 'applications/project/query/PhabricatorProjectTriggerTransactionQuery.php', 'PhabricatorProjectTriggerTransactionType' => 'applications/project/xaction/trigger/PhabricatorProjectTriggerTransactionType.php', + 'PhabricatorProjectTriggerUnknownRule' => 'applications/project/trigger/PhabricatorProjectTriggerUnknownRule.php', 'PhabricatorProjectTriggerViewController' => 'applications/project/controller/trigger/PhabricatorProjectTriggerViewController.php', 'PhabricatorProjectTypeTransaction' => 'applications/project/xaction/PhabricatorProjectTypeTransaction.php', 'PhabricatorProjectUIEventListener' => 'applications/project/events/PhabricatorProjectUIEventListener.php', @@ -10300,16 +10306,22 @@ phutil_register_library_map(array( 'PhabricatorDestructibleInterface', ), 'PhabricatorProjectTriggerController' => 'PhabricatorProjectController', + 'PhabricatorProjectTriggerCorruptionException' => 'Exception', 'PhabricatorProjectTriggerEditController' => 'PhabricatorProjectTriggerController', 'PhabricatorProjectTriggerEditor' => 'PhabricatorApplicationTransactionEditor', + 'PhabricatorProjectTriggerInvalidRule' => 'PhabricatorProjectTriggerRule', 'PhabricatorProjectTriggerListController' => 'PhabricatorProjectTriggerController', + 'PhabricatorProjectTriggerManiphestStatusRule' => 'PhabricatorProjectTriggerRule', 'PhabricatorProjectTriggerNameTransaction' => 'PhabricatorProjectTriggerTransactionType', 'PhabricatorProjectTriggerPHIDType' => 'PhabricatorPHIDType', 'PhabricatorProjectTriggerQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorProjectTriggerRule' => 'Phobject', + 'PhabricatorProjectTriggerRuleRecord' => 'Phobject', 'PhabricatorProjectTriggerSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorProjectTriggerTransaction' => 'PhabricatorModularTransaction', 'PhabricatorProjectTriggerTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorProjectTriggerTransactionType' => 'PhabricatorModularTransactionType', + 'PhabricatorProjectTriggerUnknownRule' => 'PhabricatorProjectTriggerRule', 'PhabricatorProjectTriggerViewController' => 'PhabricatorProjectTriggerController', 'PhabricatorProjectTypeTransaction' => 'PhabricatorProjectTransactionType', 'PhabricatorProjectUIEventListener' => 'PhabricatorEventListener', diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index dda21c0c43..3e702b981b 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -1270,6 +1270,7 @@ final class PhabricatorProjectBoardViewController array( 'items' => hsprintf('%s', $trigger_menu), 'tip' => $trigger_tip, + 'size' => 300, )); return $trigger_button; diff --git a/src/applications/project/controller/PhabricatorProjectMoveController.php b/src/applications/project/controller/PhabricatorProjectMoveController.php index 3cfd94894b..71588754c0 100644 --- a/src/applications/project/controller/PhabricatorProjectMoveController.php +++ b/src/applications/project/controller/PhabricatorProjectMoveController.php @@ -70,6 +70,7 @@ final class PhabricatorProjectMoveController $columns = id(new PhabricatorProjectColumnQuery()) ->setViewer($viewer) ->withProjectPHIDs(array($project->getPHID())) + ->needTriggers(true) ->execute(); $columns = mpull($columns, null, 'getPHID'); @@ -110,6 +111,19 @@ final class PhabricatorProjectMoveController $xactions[] = $header_xaction; } + if ($column->canHaveTrigger()) { + $trigger = $column->getTrigger(); + if ($trigger) { + $trigger_xactions = $trigger->newDropTransactions( + $viewer, + $column, + $object); + foreach ($trigger_xactions as $trigger_xaction) { + $xactions[] = $trigger_xaction; + } + } + } + $editor = id(new ManiphestTransactionEditor()) ->setActor($viewer) ->setContinueOnMissingFields(true) diff --git a/src/applications/project/exception/PhabricatorProjectTriggerCorruptionException.php b/src/applications/project/exception/PhabricatorProjectTriggerCorruptionException.php new file mode 100644 index 0000000000..c235fe7357 --- /dev/null +++ b/src/applications/project/exception/PhabricatorProjectTriggerCorruptionException.php @@ -0,0 +1,4 @@ +getID()); } - public function getRulesDescription() { - // TODO: Summarize the trigger rules in human-readable text. - return pht('Does things.'); + public function getTriggerRules() { + if ($this->triggerRules === null) { + + // TODO: Temporary hard-coded rule specification. + $rule_specifications = array( + array( + 'type' => 'status', + 'value' => ManiphestTaskStatus::getDefaultClosedStatus(), + ), + // This is an intentionally unknown rule. + array( + 'type' => 'quack', + 'value' => 'aaa', + ), + // This is an intentionally invalid rule. + array( + 'type' => 'status', + 'value' => 'quack', + ), + ); + + // NOTE: We're trying to preserve the database state in the rule + // structure, even if it includes rule types we don't have implementations + // for, or rules with invalid rule values. + + // If an administrator adds or removes extensions which add rules, or + // an upgrade affects rule validity, existing rules may become invalid. + // When they do, we still want the UI to reflect the ruleset state + // accurately and "Edit" + "Save" shouldn't destroy data unless the + // user explicitly modifies the ruleset. + + // When we run into rules which are structured correctly but which have + // types we don't know about, we replace them with "Unknown Rules". If + // we know about the type of a rule but the value doesn't validate, we + // replace it with "Invalid Rules". These two rule types don't take any + // actions when a card is dropped into the column, but they show the user + // what's wrong with the ruleset and can be saved without causing any + // collateral damage. + + $rule_map = PhabricatorProjectTriggerRule::getAllTriggerRules(); + + // If the stored rule data isn't a list of rules (or we encounter other + // fundamental structural problems, below), there isn't much we can do + // to try to represent the state. + if (!is_array($rule_specifications)) { + throw new PhabricatorProjectTriggerCorruptionException( + pht( + 'Trigger ("%s") has a corrupt ruleset: expected a list of '. + 'rule specifications, found "%s".', + $this->getPHID(), + phutil_describe_type($rule_specifications))); + } + + $trigger_rules = array(); + foreach ($rule_specifications as $key => $rule) { + if (!is_array($rule)) { + throw new PhabricatorProjectTriggerCorruptionException( + pht( + 'Trigger ("%s") has a corrupt ruleset: rule (with key "%s") '. + 'should be a rule specification, but is actually "%s".', + $this->getPHID(), + $key, + phutil_describe_type($rule))); + } + + try { + PhutilTypeSpec::checkMap( + $rule, + array( + 'type' => 'string', + 'value' => 'wild', + )); + } catch (PhutilTypeCheckException $ex) { + throw new PhabricatorProjectTriggerCorruptionException( + pht( + 'Trigger ("%s") has a corrupt ruleset: rule (with key "%s") '. + 'is not a valid rule specification: %s', + $this->getPHID(), + $key, + $ex->getMessage())); + } + + $record = id(new PhabricatorProjectTriggerRuleRecord()) + ->setType(idx($rule, 'type')) + ->setValue(idx($rule, 'value')); + + if (!isset($rule_map[$record->getType()])) { + $rule = new PhabricatorProjectTriggerUnknownRule(); + } else { + $rule = clone $rule_map[$record->getType()]; + } + + try { + $rule->setRecord($record); + } catch (Exception $ex) { + $rule = id(new PhabricatorProjectTriggerInvalidRule()) + ->setRecord($record); + } + + $trigger_rules[] = $rule; + } + + $this->triggerRules = $trigger_rules; + } + + return $this->triggerRules; } + public function getRulesDescription() { + $rules = $this->getTriggerRules(); + if (!$rules) { + return pht('Does nothing.'); + } + + $things = array(); + + $count = count($rules); + $limit = 3; + + if ($count > $limit) { + $show_rules = array_slice($rules, 0, ($limit - 1)); + } else { + $show_rules = $rules; + } + + foreach ($show_rules as $rule) { + $things[] = $rule->getDescription(); + } + + if ($count > $limit) { + $things[] = pht( + '(Applies %s more actions.)', + new PhutilNumber($count - $limit)); + } + + return implode("\n", $things); + } + + public function newDropTransactions( + PhabricatorUser $viewer, + PhabricatorProjectColumn $column, + $object) { + + $trigger_xactions = array(); + foreach ($this->getTriggerRules() as $rule) { + $rule + ->setViewer($viewer) + ->setTrigger($this) + ->setColumn($column) + ->setObject($object); + + $xactions = $rule->getDropTransactions( + $object, + $rule->getRecord()->getValue()); + + if (!is_array($xactions)) { + throw new Exception( + pht( + 'Expected trigger rule (of class "%s") to return a list of '. + 'transactions from "newDropTransactions()", but got "%s".', + get_class($rule), + phutil_describe_type($xactions))); + } + + $expect_type = get_class($object->getApplicationTransactionTemplate()); + assert_instances_of($xactions, $expect_type); + + foreach ($xactions as $xaction) { + $trigger_xactions[] = $xaction; + } + } + + return $trigger_xactions; + } + + /* -( PhabricatorApplicationTransactionInterface )------------------------- */ diff --git a/src/applications/project/trigger/PhabricatorProjectTriggerInvalidRule.php b/src/applications/project/trigger/PhabricatorProjectTriggerInvalidRule.php new file mode 100644 index 0000000000..55e91e9136 --- /dev/null +++ b/src/applications/project/trigger/PhabricatorProjectTriggerInvalidRule.php @@ -0,0 +1,22 @@ +getRecord()->getType()); + } + + protected function assertValidRuleValue($value) { + return; + } + + protected function newDropTransactions($object, $value) { + return array(); + } + +} diff --git a/src/applications/project/trigger/PhabricatorProjectTriggerManiphestStatusRule.php b/src/applications/project/trigger/PhabricatorProjectTriggerManiphestStatusRule.php new file mode 100644 index 0000000000..ef630f89d9 --- /dev/null +++ b/src/applications/project/trigger/PhabricatorProjectTriggerManiphestStatusRule.php @@ -0,0 +1,41 @@ +getValue(); + + return pht( + 'Changes status to "%s".', + ManiphestTaskStatus::getTaskStatusName($value)); + } + + protected function assertValidRuleValue($value) { + if (!is_string($value)) { + throw new Exception( + pht( + 'Status rule value should be a string, but is not (value is "%s").', + phutil_describe_type($value))); + } + + $map = ManiphestTaskStatus::getTaskStatusMap(); + if (!isset($map[$value])) { + throw new Exception( + pht( + 'Rule value ("%s") is not a valid task status.', + $value)); + } + } + + protected function newDropTransactions($object, $value) { + return array( + $this->newTransaction() + ->setTransactionType(ManiphestTaskStatusTransaction::TRANSACTIONTYPE) + ->setNewValue($value), + ); + } + +} diff --git a/src/applications/project/trigger/PhabricatorProjectTriggerRule.php b/src/applications/project/trigger/PhabricatorProjectTriggerRule.php new file mode 100644 index 0000000000..b311c05ff8 --- /dev/null +++ b/src/applications/project/trigger/PhabricatorProjectTriggerRule.php @@ -0,0 +1,89 @@ +getPhobjectClassConstant('TRIGGERTYPE', 64); + } + + final public static function getAllTriggerRules() { + return id(new PhutilClassMapQuery()) + ->setAncestorClass(__CLASS__) + ->setUniqueMethod('getTriggerType') + ->execute(); + } + + final public function setRecord(PhabricatorProjectTriggerRuleRecord $record) { + $value = $record->getValue(); + + $this->assertValidRuleValue($value); + + $this->record = $record; + return $this; + } + + final public function getRecord() { + return $this->record; + } + + final protected function getValue() { + return $this->getRecord()->getValue(); + } + + abstract public function getDescription(); + abstract protected function assertValidRuleValue($value); + abstract protected function newDropTransactions($object, $value); + + final public function getDropTransactions($object, $value) { + return $this->newDropTransactions($object, $value); + } + + final public function setViewer(PhabricatorUser $viewer) { + $this->viewer = $viewer; + return $this; + } + + final public function getViewer() { + return $this->viewer; + } + + final public function setColumn(PhabricatorProjectColumn $column) { + $this->column = $column; + return $this; + } + + final public function getColumn() { + return $this->column; + } + + final public function setTrigger(PhabricatorProjectTrigger $trigger) { + $this->trigger = $trigger; + return $this; + } + + final public function getTrigger() { + return $this->trigger; + } + + final public function setObject( + PhabricatorApplicationTransactionInterface $object) { + $this->object = $object; + return $this; + } + + final public function getObject() { + return $this->object; + } + + final protected function newTransaction() { + return $this->getObject()->getApplicationTransactionTemplate(); + } + +} diff --git a/src/applications/project/trigger/PhabricatorProjectTriggerRuleRecord.php b/src/applications/project/trigger/PhabricatorProjectTriggerRuleRecord.php new file mode 100644 index 0000000000..da36d9a4d8 --- /dev/null +++ b/src/applications/project/trigger/PhabricatorProjectTriggerRuleRecord.php @@ -0,0 +1,27 @@ +type = $type; + return $this; + } + + public function getType() { + return $this->type; + } + + public function setValue($value) { + $this->value = $value; + return $this; + } + + public function getValue() { + return $this->value; + } + +} diff --git a/src/applications/project/trigger/PhabricatorProjectTriggerUnknownRule.php b/src/applications/project/trigger/PhabricatorProjectTriggerUnknownRule.php new file mode 100644 index 0000000000..881a6652f4 --- /dev/null +++ b/src/applications/project/trigger/PhabricatorProjectTriggerUnknownRule.php @@ -0,0 +1,22 @@ +getRecord()->getType()); + } + + protected function assertValidRuleValue($value) { + return; + } + + protected function newDropTransactions($object, $value) { + return array(); + } + +} From 5dca1569b5772996333a5ecc091bc534016706e0 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 19 Mar 2019 15:27:21 -0700 Subject: [PATCH 195/245] Preview the effects of a drag-and-drop operation on workboards Summary: Ref T10335. Ref T5474. When you drag-and-drop a card on a workboard, show a UI hint which lists all the things that the operation will do. This shows: column moves; changes because of dragging a card to a different header; and changes which will be caused by triggers. Not implemented here: - Actions are currently shown even if they have no effect. For example, if you drag a "Normal" task to a different column, it says "Change priority to Normal.". I plan to hide actions which have no effect, but figuring this out is a little bit tricky. - I'd like to make "trigger effects" vs "non-trigger effects" a little more clear in the future, probably. Test Plan: Dragged stuff between columns and headers, and into columns with triggers. Got appropriate preview text hints previewing what the action would do in the UI. (This is tricky to take a screenshot of since it only shows up while the mouse cursor is down.) Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T10335, T5474 Differential Revision: https://secure.phabricator.com/D20299 --- resources/celerity/map.php | 113 ++++++------ src/__phutil_library_map__.php | 2 + .../PhabricatorProjectBoardViewController.php | 16 +- .../icon/PhabricatorProjectDropEffect.php | 45 +++++ .../order/PhabricatorProjectColumnHeader.php | 11 ++ .../order/PhabricatorProjectColumnOrder.php | 4 + .../PhabricatorProjectColumnOwnerOrder.php | 15 ++ .../PhabricatorProjectColumnPriorityOrder.php | 11 +- .../PhabricatorProjectColumnStatusOrder.php | 11 +- .../storage/PhabricatorProjectColumn.php | 35 ++++ .../storage/PhabricatorProjectTrigger.php | 13 ++ .../PhabricatorProjectTriggerInvalidRule.php | 4 + ...catorProjectTriggerManiphestStatusRule.php | 17 ++ .../trigger/PhabricatorProjectTriggerRule.php | 9 + .../PhabricatorProjectTriggerUnknownRule.php | 4 + .../css/phui/workboards/phui-workpanel.css | 36 ++++ .../js/application/projects/WorkboardBoard.js | 172 ++++++++++++++---- .../application/projects/WorkboardColumn.js | 11 ++ .../projects/WorkboardDropEffect.js | 35 ++++ .../projects/WorkboardHeaderTemplate.js | 3 +- .../projects/behavior-project-boards.js | 35 +++- webroot/rsrc/js/core/DraggableList.js | 21 ++- 22 files changed, 522 insertions(+), 101 deletions(-) create mode 100644 src/applications/project/icon/PhabricatorProjectDropEffect.php create mode 100644 webroot/rsrc/js/application/projects/WorkboardDropEffect.js diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 08bcbff8bf..4d095ad2b1 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -10,7 +10,7 @@ return array( 'conpherence.pkg.css' => '3c8a0668', 'conpherence.pkg.js' => '020aebcf', 'core.pkg.css' => 'b797945d', - 'core.pkg.js' => 'f9c2509b', + 'core.pkg.js' => 'eaca003c', 'differential.pkg.css' => '8d8360fb', 'differential.pkg.js' => '67e02996', 'diffusion.pkg.css' => '42c75c37', @@ -178,7 +178,7 @@ return array( 'rsrc/css/phui/workboards/phui-workboard-color.css' => 'e86de308', 'rsrc/css/phui/workboards/phui-workboard.css' => '74fc9d98', 'rsrc/css/phui/workboards/phui-workcard.css' => '9e9eb0df', - 'rsrc/css/phui/workboards/phui-workpanel.css' => 'c5b408ad', + 'rsrc/css/phui/workboards/phui-workpanel.css' => 'e5461a51', 'rsrc/css/sprite-login.css' => '18b368a6', 'rsrc/css/sprite-tokens.css' => 'f1896dc5', 'rsrc/css/syntax/syntax-default.css' => '055fc231', @@ -408,15 +408,16 @@ return array( 'rsrc/js/application/phortune/phortune-credit-card-form.js' => 'd12d214f', 'rsrc/js/application/policy/behavior-policy-control.js' => '0eaa33a9', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '9347f172', - 'rsrc/js/application/projects/WorkboardBoard.js' => '9d59f098', + 'rsrc/js/application/projects/WorkboardBoard.js' => 'ba6e36b0', 'rsrc/js/application/projects/WorkboardCard.js' => '0392a5d8', 'rsrc/js/application/projects/WorkboardCardTemplate.js' => '2a61f8d4', - 'rsrc/js/application/projects/WorkboardColumn.js' => 'ec5c5ce0', + 'rsrc/js/application/projects/WorkboardColumn.js' => 'c344eb3c', 'rsrc/js/application/projects/WorkboardController.js' => '42c7a5a7', + 'rsrc/js/application/projects/WorkboardDropEffect.js' => '101121be', 'rsrc/js/application/projects/WorkboardHeader.js' => '111bfd2d', - 'rsrc/js/application/projects/WorkboardHeaderTemplate.js' => 'b65351bd', + 'rsrc/js/application/projects/WorkboardHeaderTemplate.js' => 'ebe83a6b', 'rsrc/js/application/projects/WorkboardOrderTemplate.js' => '03e8891f', - 'rsrc/js/application/projects/behavior-project-boards.js' => '412af9d4', + 'rsrc/js/application/projects/behavior-project-boards.js' => 'cd7c9d4f', 'rsrc/js/application/projects/behavior-project-create.js' => '34c53422', 'rsrc/js/application/projects/behavior-reorder-columns.js' => '8ac32fd9', 'rsrc/js/application/releeph/releeph-preview-branch.js' => '75184d68', @@ -437,7 +438,7 @@ return array( 'rsrc/js/application/uiexample/notification-example.js' => '29819b75', 'rsrc/js/core/Busy.js' => '5202e831', 'rsrc/js/core/DragAndDropFileUpload.js' => '4370900d', - 'rsrc/js/core/DraggableList.js' => '8bc7d797', + 'rsrc/js/core/DraggableList.js' => 'c9ad6f70', 'rsrc/js/core/Favicon.js' => '7930776a', 'rsrc/js/core/FileUpload.js' => 'ab85e184', 'rsrc/js/core/Hovercard.js' => '074f0783', @@ -657,7 +658,7 @@ return array( 'javelin-behavior-phuix-example' => 'c2c500a7', 'javelin-behavior-policy-control' => '0eaa33a9', 'javelin-behavior-policy-rule-editor' => '9347f172', - 'javelin-behavior-project-boards' => '412af9d4', + 'javelin-behavior-project-boards' => 'cd7c9d4f', 'javelin-behavior-project-create' => '34c53422', 'javelin-behavior-quicksand-blacklist' => '5a6f6a06', 'javelin-behavior-read-only-warning' => 'b9109f8f', @@ -729,13 +730,14 @@ return array( 'javelin-view-renderer' => '9aae2b66', 'javelin-view-visitor' => '308f9fe4', 'javelin-websocket' => 'fdc13e4e', - 'javelin-workboard-board' => '9d59f098', + 'javelin-workboard-board' => 'ba6e36b0', 'javelin-workboard-card' => '0392a5d8', 'javelin-workboard-card-template' => '2a61f8d4', - 'javelin-workboard-column' => 'ec5c5ce0', + 'javelin-workboard-column' => 'c344eb3c', 'javelin-workboard-controller' => '42c7a5a7', + 'javelin-workboard-drop-effect' => '101121be', 'javelin-workboard-header' => '111bfd2d', - 'javelin-workboard-header-template' => 'b65351bd', + 'javelin-workboard-header-template' => 'ebe83a6b', 'javelin-workboard-order-template' => '03e8891f', 'javelin-workflow' => '958e9045', 'maniphest-report-css' => '3d53188b', @@ -761,7 +763,7 @@ return array( 'phabricator-diff-changeset-list' => '04023d82', 'phabricator-diff-inline' => 'a4a14a94', 'phabricator-drag-and-drop-file-upload' => '4370900d', - 'phabricator-draggable-list' => '8bc7d797', + 'phabricator-draggable-list' => 'c9ad6f70', 'phabricator-fatal-config-template-css' => '20babf50', 'phabricator-favicon' => '7930776a', 'phabricator-feed-css' => 'd8b6e3f8', @@ -860,7 +862,7 @@ return array( 'phui-workboard-color-css' => 'e86de308', 'phui-workboard-view-css' => '74fc9d98', 'phui-workcard-view-css' => '9e9eb0df', - 'phui-workpanel-view-css' => 'c5b408ad', + 'phui-workpanel-view-css' => 'e5461a51', 'phuix-action-list-view' => 'c68f183f', 'phuix-action-view' => 'aaa08f3b', 'phuix-autocomplete' => '8f139ef0', @@ -1001,6 +1003,10 @@ return array( 'javelin-workflow', 'phuix-icon-view', ), + '101121be' => array( + 'javelin-install', + 'javelin-dom', + ), '111bfd2d' => array( 'javelin-install', ), @@ -1227,15 +1233,6 @@ return array( 'javelin-behavior', 'javelin-uri', ), - '412af9d4' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-util', - 'javelin-vector', - 'javelin-stratcom', - 'javelin-workflow', - 'javelin-workboard-controller', - ), '4234f572' => array( 'syntax-default-css', ), @@ -1593,14 +1590,6 @@ return array( 'javelin-dom', 'javelin-typeahead-normalizer', ), - '8bc7d797' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-stratcom', - 'javelin-util', - 'javelin-vector', - 'javelin-magical-init', - ), '8c2ed2bf' => array( 'javelin-behavior', 'javelin-dom', @@ -1725,18 +1714,6 @@ return array( 'javelin-uri', 'phabricator-textareautils', ), - '9d59f098' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-util', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - 'javelin-workboard-column', - 'javelin-workboard-header-template', - 'javelin-workboard-card-template', - 'javelin-workboard-order-template', - ), '9f081f05' => array( 'javelin-behavior', 'javelin-dom', @@ -1885,9 +1862,6 @@ return array( 'javelin-stratcom', 'javelin-dom', ), - 'b65351bd' => array( - 'javelin-install', - ), 'b7b73831' => array( 'javelin-behavior', 'javelin-dom', @@ -1906,6 +1880,18 @@ return array( 'javelin-uri', 'phabricator-notification', ), + 'ba6e36b0' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-util', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + 'javelin-workboard-column', + 'javelin-workboard-header-template', + 'javelin-workboard-card-template', + 'javelin-workboard-order-template', + ), 'bdce4d78' => array( 'javelin-install', 'javelin-util', @@ -1930,15 +1916,17 @@ return array( 'javelin-dom', 'phuix-button-view', ), + 'c344eb3c' => array( + 'javelin-install', + 'javelin-workboard-card', + 'javelin-workboard-header', + ), 'c3703a16' => array( 'javelin-behavior', 'javelin-aphlict', 'phabricator-phtize', 'javelin-dom', ), - 'c5b408ad' => array( - 'phui-workcard-view-css', - ), 'c687e867' => array( 'javelin-behavior', 'javelin-dom', @@ -1978,6 +1966,24 @@ return array( 'javelin-util', 'phabricator-keyboard-shortcut-manager', ), + 'c9ad6f70' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-stratcom', + 'javelin-util', + 'javelin-vector', + 'javelin-magical-init', + ), + 'cd7c9d4f' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-util', + 'javelin-vector', + 'javelin-stratcom', + 'javelin-workflow', + 'javelin-workboard-controller', + 'javelin-workboard-drop-effect', + ), 'cf32921f' => array( 'javelin-behavior', 'javelin-dom', @@ -2038,6 +2044,9 @@ return array( 'javelin-dom', 'javelin-history', ), + 'e5461a51' => array( + 'phui-workcard-view-css', + ), 'e562708c' => array( 'javelin-install', ), @@ -2068,14 +2077,12 @@ return array( 'javelin-install', 'javelin-event', ), + 'ebe83a6b' => array( + 'javelin-install', + ), 'ec4e31c0' => array( 'phui-timeline-view-css', ), - 'ec5c5ce0' => array( - 'javelin-install', - 'javelin-workboard-card', - 'javelin-workboard-header', - ), 'ee77366f' => array( 'aphront-dialog-view-css', ), diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index b042b017e2..ebe7c2b9c3 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4094,6 +4094,7 @@ phutil_register_library_map(array( 'PhabricatorProjectDefaultController' => 'applications/project/controller/PhabricatorProjectDefaultController.php', 'PhabricatorProjectDescriptionField' => 'applications/project/customfield/PhabricatorProjectDescriptionField.php', 'PhabricatorProjectDetailsProfileMenuItem' => 'applications/project/menuitem/PhabricatorProjectDetailsProfileMenuItem.php', + 'PhabricatorProjectDropEffect' => 'applications/project/icon/PhabricatorProjectDropEffect.php', 'PhabricatorProjectEditController' => 'applications/project/controller/PhabricatorProjectEditController.php', 'PhabricatorProjectEditEngine' => 'applications/project/engine/PhabricatorProjectEditEngine.php', 'PhabricatorProjectEditPictureController' => 'applications/project/controller/PhabricatorProjectEditPictureController.php', @@ -10219,6 +10220,7 @@ phutil_register_library_map(array( 'PhabricatorProjectDefaultController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectDescriptionField' => 'PhabricatorProjectStandardCustomField', 'PhabricatorProjectDetailsProfileMenuItem' => 'PhabricatorProfileMenuItem', + 'PhabricatorProjectDropEffect' => 'Phobject', 'PhabricatorProjectEditController' => 'PhabricatorProjectController', 'PhabricatorProjectEditEngine' => 'PhabricatorEditEngine', 'PhabricatorProjectEditPictureController' => 'PhabricatorProjectController', diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index 3e702b981b..41981a5522 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -540,8 +540,8 @@ final class PhabricatorProjectBoardViewController ->setExcludedProjectPHIDs($select_phids); $templates = array(); - $column_maps = array(); $all_tasks = array(); + $column_templates = array(); foreach ($visible_columns as $column_phid => $column) { $column_tasks = $column_phids[$column_phid]; @@ -606,18 +606,28 @@ final class PhabricatorProjectBoardViewController 'pointLimit' => $column->getPointLimit(), )); + $card_phids = array(); foreach ($column_tasks as $task) { $object_phid = $task->getPHID(); $card = $rendering_engine->renderCard($object_phid); $templates[$object_phid] = hsprintf('%s', $card->getItem()); - $column_maps[$column_phid][] = $object_phid; + $card_phids[] = $object_phid; $all_tasks[$object_phid] = $task; } $panel->setCards($cards); $board->addPanel($panel); + + $drop_effects = $column->getDropEffects(); + $drop_effects = mpull($drop_effects, 'toDictionary'); + + $column_templates[] = array( + 'columnPHID' => $column_phid, + 'effects' => $drop_effects, + 'cardPHIDs' => $card_phids, + ); } $order_key = $this->sortKey; @@ -661,9 +671,9 @@ final class PhabricatorProjectBoardViewController 'headers' => $headers, 'headerKeys' => $header_keys, 'templateMap' => $templates, - 'columnMaps' => $column_maps, 'orderMaps' => $vector_map, 'propertyMaps' => $properties, + 'columnTemplates' => $column_templates, 'boardID' => $board_id, 'projectPHID' => $project->getPHID(), diff --git a/src/applications/project/icon/PhabricatorProjectDropEffect.php b/src/applications/project/icon/PhabricatorProjectDropEffect.php new file mode 100644 index 0000000000..33145eb039 --- /dev/null +++ b/src/applications/project/icon/PhabricatorProjectDropEffect.php @@ -0,0 +1,45 @@ +icon = $icon; + return $this; + } + + public function getIcon() { + return $this->icon; + } + + public function setColor($color) { + $this->color = $color; + return $this; + } + + public function getColor() { + return $this->color; + } + + public function setContent($content) { + $this->content = $content; + return $this; + } + + public function getContent() { + return $this->content; + } + + public function toDictionary() { + return array( + 'icon' => $this->getIcon(), + 'color' => $this->getColor(), + 'content' => hsprintf('%s', $this->getContent()), + ); + } + +} diff --git a/src/applications/project/order/PhabricatorProjectColumnHeader.php b/src/applications/project/order/PhabricatorProjectColumnHeader.php index 24d1e5c5ec..898d9b0222 100644 --- a/src/applications/project/order/PhabricatorProjectColumnHeader.php +++ b/src/applications/project/order/PhabricatorProjectColumnHeader.php @@ -9,6 +9,7 @@ final class PhabricatorProjectColumnHeader private $name; private $icon; private $editProperties; + private $dropEffects = array(); public function setOrderKey($order_key) { $this->orderKey = $order_key; @@ -64,6 +65,15 @@ final class PhabricatorProjectColumnHeader return $this->editProperties; } + public function addDropEffect(PhabricatorProjectDropEffect $effect) { + $this->dropEffects[] = $effect; + return $this; + } + + public function getDropEffects() { + return $this->dropEffects; + } + public function toDictionary() { return array( 'order' => $this->getOrderKey(), @@ -71,6 +81,7 @@ final class PhabricatorProjectColumnHeader 'template' => hsprintf('%s', $this->newView()), 'vector' => $this->getSortVector(), 'editProperties' => $this->getEditProperties(), + 'effects' => mpull($this->getDropEffects(), 'toDictionary'), ); } diff --git a/src/applications/project/order/PhabricatorProjectColumnOrder.php b/src/applications/project/order/PhabricatorProjectColumnOrder.php index c2da400fb2..430d9ef472 100644 --- a/src/applications/project/order/PhabricatorProjectColumnOrder.php +++ b/src/applications/project/order/PhabricatorProjectColumnOrder.php @@ -196,6 +196,10 @@ abstract class PhabricatorProjectColumnOrder ->setOrderKey($this->getColumnOrderKey()); } + final protected function newEffect() { + return new PhabricatorProjectDropEffect(); + } + final public function toDictionary() { return array( 'orderKey' => $this->getColumnOrderKey(), diff --git a/src/applications/project/order/PhabricatorProjectColumnOwnerOrder.php b/src/applications/project/order/PhabricatorProjectColumnOwnerOrder.php index 336411bac5..920e87c9b7 100644 --- a/src/applications/project/order/PhabricatorProjectColumnOwnerOrder.php +++ b/src/applications/project/order/PhabricatorProjectColumnOwnerOrder.php @@ -122,16 +122,23 @@ final class PhabricatorProjectColumnOwnerOrder $header_key = $this->newHeaderKeyForOwnerPHID($owner_phid); $owner_image = null; + $effect_content = null; if ($owner_phid === null) { $owner = null; $sort_vector = $this->newSortVectorForUnowned(); $owner_name = pht('Not Assigned'); + + $effect_content = pht('Remove task assignee.'); } else { $owner = idx($owner_users, $owner_phid); if ($owner) { $sort_vector = $this->newSortVectorForOwner($owner); $owner_name = $owner->getUsername(); $owner_image = $owner->getProfileImageURI(); + + $effect_content = pht( + 'Assign task to %s.', + phutil_tag('strong', array(), $owner_name)); } else { $sort_vector = $this->newSortVectorForOwnerPHID($owner_phid); $owner_name = pht('Unknown User ("%s")', $owner_phid); @@ -159,6 +166,14 @@ final class PhabricatorProjectColumnOwnerOrder 'value' => $owner_phid, )); + if ($effect_content !== null) { + $header->addDropEffect( + $this->newEffect() + ->setIcon($owner_icon) + ->setColor($owner_color) + ->setContent($effect_content)); + } + $headers[] = $header; } diff --git a/src/applications/project/order/PhabricatorProjectColumnPriorityOrder.php b/src/applications/project/order/PhabricatorProjectColumnPriorityOrder.php index 10fcafad76..8cffab91ae 100644 --- a/src/applications/project/order/PhabricatorProjectColumnPriorityOrder.php +++ b/src/applications/project/order/PhabricatorProjectColumnPriorityOrder.php @@ -65,6 +65,14 @@ final class PhabricatorProjectColumnPriorityOrder $icon_view = id(new PHUIIconView()) ->setIcon($priority_icon, $priority_color); + $drop_effect = $this->newEffect() + ->setIcon($priority_icon) + ->setColor($priority_color) + ->setContent( + pht( + 'Change priority to %s.', + phutil_tag('strong', array(), $priority_name))); + $header = $this->newHeader() ->setHeaderKey($header_key) ->setSortVector($sort_vector) @@ -73,7 +81,8 @@ final class PhabricatorProjectColumnPriorityOrder ->setEditProperties( array( 'value' => (int)$priority, - )); + )) + ->addDropEffect($drop_effect); $headers[] = $header; } diff --git a/src/applications/project/order/PhabricatorProjectColumnStatusOrder.php b/src/applications/project/order/PhabricatorProjectColumnStatusOrder.php index e58d05f655..419d7062c7 100644 --- a/src/applications/project/order/PhabricatorProjectColumnStatusOrder.php +++ b/src/applications/project/order/PhabricatorProjectColumnStatusOrder.php @@ -72,6 +72,14 @@ final class PhabricatorProjectColumnStatusOrder $icon_view = id(new PHUIIconView()) ->setIcon($status_icon, $status_color); + $drop_effect = $this->newEffect() + ->setIcon($status_icon) + ->setColor($status_color) + ->setContent( + pht( + 'Change status to %s.', + phutil_tag('strong', array(), $status_name))); + $header = $this->newHeader() ->setHeaderKey($header_key) ->setSortVector($sort_vector) @@ -80,7 +88,8 @@ final class PhabricatorProjectColumnStatusOrder ->setEditProperties( array( 'value' => $status_key, - )); + )) + ->addDropEffect($drop_effect); $headers[] = $header; } diff --git a/src/applications/project/storage/PhabricatorProjectColumn.php b/src/applications/project/storage/PhabricatorProjectColumn.php index 5eb9826309..731c2a15fb 100644 --- a/src/applications/project/storage/PhabricatorProjectColumn.php +++ b/src/applications/project/storage/PhabricatorProjectColumn.php @@ -218,6 +218,41 @@ final class PhabricatorProjectColumn $this->getProject()->getID()); } + public function getDropEffects() { + $effects = array(); + + $proxy = $this->getProxy(); + if ($proxy && $proxy->isMilestone()) { + $effects[] = id(new PhabricatorProjectDropEffect()) + ->setIcon($proxy->getProxyColumnIcon()) + ->setColor('violet') + ->setContent( + pht( + 'Move to milestone %s.', + phutil_tag('strong', array(), $this->getDisplayName()))); + } else { + $effects[] = id(new PhabricatorProjectDropEffect()) + ->setIcon('fa-columns') + ->setColor('blue') + ->setContent( + pht( + 'Move to column %s.', + phutil_tag('strong', array(), $this->getDisplayName()))); + } + + + if ($this->canHaveTrigger()) { + $trigger = $this->getTrigger(); + if ($trigger) { + foreach ($trigger->getDropEffects() as $trigger_effect) { + $effects[] = $trigger_effect; + } + } + } + + return $effects; + } + /* -( PhabricatorConduitResultInterface )---------------------------------- */ diff --git a/src/applications/project/storage/PhabricatorProjectTrigger.php b/src/applications/project/storage/PhabricatorProjectTrigger.php index 415d3dbb9e..b02e1ce107 100644 --- a/src/applications/project/storage/PhabricatorProjectTrigger.php +++ b/src/applications/project/storage/PhabricatorProjectTrigger.php @@ -170,6 +170,19 @@ final class PhabricatorProjectTrigger return $this->triggerRules; } + public function getDropEffects() { + $effects = array(); + + $rules = $this->getTriggerRules(); + foreach ($rules as $rule) { + foreach ($rule->getDropEffects() as $effect) { + $effects[] = $effect; + } + } + + return $effects; + } + public function getRulesDescription() { $rules = $this->getTriggerRules(); if (!$rules) { diff --git a/src/applications/project/trigger/PhabricatorProjectTriggerInvalidRule.php b/src/applications/project/trigger/PhabricatorProjectTriggerInvalidRule.php index 55e91e9136..4157ec8e9a 100644 --- a/src/applications/project/trigger/PhabricatorProjectTriggerInvalidRule.php +++ b/src/applications/project/trigger/PhabricatorProjectTriggerInvalidRule.php @@ -19,4 +19,8 @@ final class PhabricatorProjectTriggerInvalidRule return array(); } + protected function newDropEffects($value) { + return array(); + } + } diff --git a/src/applications/project/trigger/PhabricatorProjectTriggerManiphestStatusRule.php b/src/applications/project/trigger/PhabricatorProjectTriggerManiphestStatusRule.php index ef630f89d9..575f51cd2b 100644 --- a/src/applications/project/trigger/PhabricatorProjectTriggerManiphestStatusRule.php +++ b/src/applications/project/trigger/PhabricatorProjectTriggerManiphestStatusRule.php @@ -38,4 +38,21 @@ final class PhabricatorProjectTriggerManiphestStatusRule ); } + protected function newDropEffects($value) { + $status_name = ManiphestTaskStatus::getTaskStatusName($value); + $status_icon = ManiphestTaskStatus::getStatusIcon($value); + $status_color = ManiphestTaskStatus::getStatusColor($value); + + $content = pht( + 'Change status to %s.', + phutil_tag('strong', array(), $status_name)); + + return array( + $this->newEffect() + ->setIcon($status_icon) + ->setColor($status_color) + ->setContent($content), + ); + } + } diff --git a/src/applications/project/trigger/PhabricatorProjectTriggerRule.php b/src/applications/project/trigger/PhabricatorProjectTriggerRule.php index b311c05ff8..6caf9ee0be 100644 --- a/src/applications/project/trigger/PhabricatorProjectTriggerRule.php +++ b/src/applications/project/trigger/PhabricatorProjectTriggerRule.php @@ -40,6 +40,7 @@ abstract class PhabricatorProjectTriggerRule abstract public function getDescription(); abstract protected function assertValidRuleValue($value); abstract protected function newDropTransactions($object, $value); + abstract protected function newDropEffects($value); final public function getDropTransactions($object, $value) { return $this->newDropTransactions($object, $value); @@ -86,4 +87,12 @@ abstract class PhabricatorProjectTriggerRule return $this->getObject()->getApplicationTransactionTemplate(); } + final public function getDropEffects() { + return $this->newDropEffects($this->getValue()); + } + + final protected function newEffect() { + return new PhabricatorProjectDropEffect(); + } + } diff --git a/src/applications/project/trigger/PhabricatorProjectTriggerUnknownRule.php b/src/applications/project/trigger/PhabricatorProjectTriggerUnknownRule.php index 881a6652f4..8d796686de 100644 --- a/src/applications/project/trigger/PhabricatorProjectTriggerUnknownRule.php +++ b/src/applications/project/trigger/PhabricatorProjectTriggerUnknownRule.php @@ -19,4 +19,8 @@ final class PhabricatorProjectTriggerUnknownRule return array(); } + protected function newDropEffects($value) { + return array(); + } + } diff --git a/webroot/rsrc/css/phui/workboards/phui-workpanel.css b/webroot/rsrc/css/phui/workboards/phui-workpanel.css index 95db8021ef..ce0e7885a8 100644 --- a/webroot/rsrc/css/phui/workboards/phui-workpanel.css +++ b/webroot/rsrc/css/phui/workboards/phui-workpanel.css @@ -178,3 +178,39 @@ margin-left: 36px; overflow: hidden; } + +.workboard-drop-preview { + pointer-events: none; + position: absolute; + bottom: 12px; + right: 12px; + width: 300px; + border-radius: 3px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15); + border: 1px solid {$lightblueborder}; + padding: 4px 0; +} + +.workboard-drop-preview:hover { + opacity: 0.25; +} + +.workboard-drop-preview li { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + margin: 4px 8px; + color: {$greytext}; +} + +.workboard-drop-preview li .phui-icon-view { + position: relative; + display: inline-block; + text-align: center; + width: 24px; + height: 18px; + padding-top: 6px; + border-radius: 3px; + background: {$bluebackground}; + margin-right: 6px; +} diff --git a/webroot/rsrc/js/application/projects/WorkboardBoard.js b/webroot/rsrc/js/application/projects/WorkboardBoard.js index fa10b2a180..6fab227c84 100644 --- a/webroot/rsrc/js/application/projects/WorkboardBoard.js +++ b/webroot/rsrc/js/application/projects/WorkboardBoard.js @@ -39,6 +39,8 @@ JX.install('WorkboardBoard', { _columns: null, _headers: null, _cards: null, + _dropPreviewNode: null, + _dropPreviewListNode: null, getRoot: function() { return this._root; @@ -180,6 +182,8 @@ JX.install('WorkboardBoard', { list.setCompareOnReorder(true); } + list.setTargetChangeHandler(JX.bind(this, this._didChangeDropTarget)); + list.listen('didDrop', JX.bind(this, this._onmovecard, list)); lists.push(list); @@ -190,23 +194,89 @@ JX.install('WorkboardBoard', { } }, + _didChangeDropTarget: function(src_list, src_node, dst_list, dst_node) { + var node = this._getDropPreviewNode(); + + if (!dst_list) { + // The card is being dragged into a dead area, like the left menu. + JX.DOM.remove(node); + return; + } + + if (dst_node === false) { + // The card is being dragged over itself, so dropping it won't + // affect anything. + JX.DOM.remove(node); + return; + } + + var src_phid = JX.Stratcom.getData(src_list.getRootNode()).columnPHID; + var dst_phid = JX.Stratcom.getData(dst_list.getRootNode()).columnPHID; + + var src_column = this.getColumn(src_phid); + var dst_column = this.getColumn(dst_phid); + + var effects = []; + + if (src_column !== dst_column) { + effects = effects.concat(dst_column.getDropEffects()); + } + + var context = this._getDropContext(dst_node); + if (context.headerKey) { + var header = this.getHeaderTemplate(context.headerKey); + effects = effects.concat(header.getDropEffects()); + } + + if (!effects.length) { + JX.DOM.remove(node); + return; + } + + var items = []; + for (var ii = 0; ii < effects.length; ii++) { + var effect = effects[ii]; + items.push(effect.newNode()); + } + + JX.DOM.setContent(this._getDropPreviewListNode(), items); + + document.body.appendChild(node); + }, + + _getDropPreviewNode: function() { + if (!this._dropPreviewNode) { + var attributes = { + className: 'workboard-drop-preview' + }; + + var content = [ + this._getDropPreviewListNode() + ]; + + this._dropPreviewNode = JX.$N('div', attributes, content); + } + + return this._dropPreviewNode; + }, + + _getDropPreviewListNode: function() { + if (!this._dropPreviewListNode) { + var attributes = {}; + this._dropPreviewListNode = JX.$N('ul', attributes); + } + + return this._dropPreviewListNode; + }, + _findCardsInColumn: function(column_node) { return JX.DOM.scry(column_node, 'li', 'project-card'); }, - _onmovecard: function(list, item, after_node, src_list) { - list.lock(); - JX.DOM.alterClass(item, 'drag-sending', true); - - var src_phid = JX.Stratcom.getData(src_list.getRootNode()).columnPHID; - var dst_phid = JX.Stratcom.getData(list.getRootNode()).columnPHID; - - var item_phid = JX.Stratcom.getData(item).objectPHID; - var data = { - objectPHID: item_phid, - columnPHID: dst_phid, - order: this.getOrder() - }; + _getDropContext: function(after_node, item) { + var header_key; + var before_phid; + var after_phid; // We're going to send an "afterPHID" and a "beforePHID" if the card // was dropped immediately adjacent to another card. If a card was @@ -231,26 +301,28 @@ JX.install('WorkboardBoard', { if (after_data) { if (after_data.objectPHID) { - data.afterPHID = after_data.objectPHID; + after_phid = after_data.objectPHID; } } - var before_data; - var before_card = item.nextSibling; - while (before_card) { - before_data = JX.Stratcom.getData(before_card); - if (before_data.objectPHID) { - break; + if (item) { + var before_data; + var before_card = item.nextSibling; + while (before_card) { + before_data = JX.Stratcom.getData(before_card); + if (before_data.objectPHID) { + break; + } + if (before_data.headerKey) { + break; + } + before_card = before_card.nextSibling; } - if (before_data.headerKey) { - break; - } - before_card = before_card.nextSibling; - } - if (before_data) { - if (before_data.objectPHID) { - data.beforePHID = before_data.objectPHID; + if (before_data) { + if (before_data.objectPHID) { + before_phid = before_data.objectPHID; + } } } @@ -265,12 +337,44 @@ JX.install('WorkboardBoard', { } if (header_data) { - var header_key = header_data.headerKey; - if (header_key) { - var properties = this.getHeaderTemplate(header_key) - .getEditProperties(); - data.header = JX.JSON.stringify(properties); - } + header_key = header_data.headerKey; + } + + return { + headerKey: header_key, + afterPHID: after_phid, + beforePHID: before_phid + }; + }, + + _onmovecard: function(list, item, after_node, src_list) { + list.lock(); + JX.DOM.alterClass(item, 'drag-sending', true); + + var src_phid = JX.Stratcom.getData(src_list.getRootNode()).columnPHID; + var dst_phid = JX.Stratcom.getData(list.getRootNode()).columnPHID; + + var item_phid = JX.Stratcom.getData(item).objectPHID; + var data = { + objectPHID: item_phid, + columnPHID: dst_phid, + order: this.getOrder() + }; + + var context = this._getDropContext(after_node); + + if (context.afterPHID) { + data.afterPHID = context.afterPHID; + } + + if (context.beforePHID) { + data.beforePHID = context.beforePHID; + } + + if (context.headerKey) { + var properties = this.getHeaderTemplate(context.headerKey) + .getEditProperties(); + data.header = JX.JSON.stringify(properties); } var visible_phids = []; diff --git a/webroot/rsrc/js/application/projects/WorkboardColumn.js b/webroot/rsrc/js/application/projects/WorkboardColumn.js index 709c52016a..593afea776 100644 --- a/webroot/rsrc/js/application/projects/WorkboardColumn.js +++ b/webroot/rsrc/js/application/projects/WorkboardColumn.js @@ -25,6 +25,7 @@ JX.install('WorkboardColumn', { this._headers = {}; this._objects = []; this._naturalOrder = []; + this._dropEffects = []; }, members: { @@ -40,6 +41,7 @@ JX.install('WorkboardColumn', { _pointsContentNode: null, _dirty: true, _objects: null, + _dropEffects: null, getPHID: function() { return this._phid; @@ -71,6 +73,15 @@ JX.install('WorkboardColumn', { return this; }, + setDropEffects: function(effects) { + this._dropEffects = effects; + return this; + }, + + getDropEffects: function() { + return this._dropEffects; + }, + getPointsNode: function() { return this._pointsNode; }, diff --git a/webroot/rsrc/js/application/projects/WorkboardDropEffect.js b/webroot/rsrc/js/application/projects/WorkboardDropEffect.js new file mode 100644 index 0000000000..ecd18d0015 --- /dev/null +++ b/webroot/rsrc/js/application/projects/WorkboardDropEffect.js @@ -0,0 +1,35 @@ +/** + * @provides javelin-workboard-drop-effect + * @requires javelin-install + * javelin-dom + * @javelin + */ + +JX.install('WorkboardDropEffect', { + + properties: { + icon: null, + color: null, + content: null + }, + + statics: { + newFromDictionary: function(map) { + return new JX.WorkboardDropEffect() + .setIcon(map.icon) + .setColor(map.color) + .setContent(JX.$H(map.content)); + } + }, + + members: { + newNode: function() { + var icon = new JX.PHUIXIconView() + .setIcon(this.getIcon()) + .setColor(this.getColor()) + .getNode(); + + return JX.$N('li', {}, [icon, this.getContent()]); + } + } +}); diff --git a/webroot/rsrc/js/application/projects/WorkboardHeaderTemplate.js b/webroot/rsrc/js/application/projects/WorkboardHeaderTemplate.js index 8376359270..d64a56dd29 100644 --- a/webroot/rsrc/js/application/projects/WorkboardHeaderTemplate.js +++ b/webroot/rsrc/js/application/projects/WorkboardHeaderTemplate.js @@ -14,7 +14,8 @@ JX.install('WorkboardHeaderTemplate', { template: null, order: null, vector: null, - editProperties: null + editProperties: null, + dropEffects: [] }, members: { diff --git a/webroot/rsrc/js/application/projects/behavior-project-boards.js b/webroot/rsrc/js/application/projects/behavior-project-boards.js index 3aa43722c4..f25599391f 100644 --- a/webroot/rsrc/js/application/projects/behavior-project-boards.js +++ b/webroot/rsrc/js/application/projects/behavior-project-boards.js @@ -7,6 +7,7 @@ * javelin-stratcom * javelin-workflow * javelin-workboard-controller + * javelin-workboard-drop-effect */ JX.behavior('project-boards', function(config, statics) { @@ -88,12 +89,24 @@ JX.behavior('project-boards', function(config, statics) { } var ii; - var column_maps = config.columnMaps; - for (var column_phid in column_maps) { - var column = board.getColumn(column_phid); - var column_map = column_maps[column_phid]; - for (ii = 0; ii < column_map.length; ii++) { - column.newCard(column_map[ii]); + var jj; + var effects; + + for (ii = 0; ii < config.columnTemplates.length; ii++) { + var spec = config.columnTemplates[ii]; + + var column = board.getColumn(spec.columnPHID); + + effects = []; + for (jj = 0; jj < spec.effects.length; jj++) { + effects.push( + JX.WorkboardDropEffect.newFromDictionary( + spec.effects[jj])); + } + column.setDropEffects(effects); + + for (jj = 0; jj < spec.cardPHIDs.length; jj++) { + column.newCard(spec.cardPHIDs[jj]); } } @@ -115,11 +128,19 @@ JX.behavior('project-boards', function(config, statics) { for (ii = 0; ii < headers.length; ii++) { var header = headers[ii]; + effects = []; + for (jj = 0; jj < header.effects.length; jj++) { + effects.push( + JX.WorkboardDropEffect.newFromDictionary( + header.effects[jj])); + } + board.getHeaderTemplate(header.key) .setOrder(header.order) .setNodeHTMLTemplate(header.template) .setVector(header.vector) - .setEditProperties(header.editProperties); + .setEditProperties(header.editProperties) + .setDropEffects(effects); } var orders = config.orders; diff --git a/webroot/rsrc/js/core/DraggableList.js b/webroot/rsrc/js/core/DraggableList.js index 64f57503b8..5f19b7061d 100644 --- a/webroot/rsrc/js/core/DraggableList.js +++ b/webroot/rsrc/js/core/DraggableList.js @@ -45,7 +45,8 @@ JX.install('DraggableList', { outerContainer: null, hasInfiniteHeight: false, compareOnMove: false, - compareOnReorder: false + compareOnReorder: false, + targetChangeHandler: null }, members : { @@ -53,6 +54,7 @@ JX.install('DraggableList', { _dragging : null, _locked : 0, _target : null, + _lastTarget: null, _targets : null, _ghostHandler : null, _ghostNode : null, @@ -372,6 +374,19 @@ JX.install('DraggableList', { return this; }, + _didChangeTarget: function(dst_list, dst_node) { + if (dst_node === this._lastTarget) { + return; + } + + this._lastTarget = dst_node; + + var handler = this.getTargetChangeHandler(); + if (handler) { + handler(this, this._dragging, dst_list, dst_node); + } + }, + _setIsDropTarget: function(is_target) { var root = this.getRootNode(); JX.DOM.alterClass(root, 'drag-target-list', is_target); @@ -540,6 +555,8 @@ JX.install('DraggableList', { } } + this._didChangeTarget(target_list, cur_target); + this._updateAutoscroll(this._cursorPosition); var f = JX.$V(this._frame); @@ -673,6 +690,8 @@ JX.install('DraggableList', { group[ii]._clearTarget(); } + this._didChangeTarget(null, null); + JX.DOM.alterClass(dragging, 'drag-dragging', false); JX.Tooltip.unlock(); From a5b3e33e3c2649d7c11a73c9c74e2729a66b8743 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 19 Mar 2019 19:29:33 -0700 Subject: [PATCH 196/245] Don't show workboard action previews if the action won't have any effect Summary: Ref T10335. When you (for example) drag a "Resolved" task into a column with "Trigger: change status to resolved.", don't show a hint that the action will "Change status to resolved." since this isn't helpful and is somewhat confusing. For now, the only visibility operator is "!=" since all current actions are simple field comparisons, but some actions in the future (like "add subscriber" or "remove project") might need other conditions. Test Plan: Dragged cards in ways that previously provided useless hints: move from column A to column B on a "Group by Priority" board; drag a resolved task to a "Trigger: change status to as resolved" column. Saw a more accurate preview in both cases. Drags which actually cause effects still show the effects correctly. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T10335 Differential Revision: https://secure.phabricator.com/D20300 --- resources/celerity/map.php | 40 +++++++++---------- .../PhabricatorProjectBoardViewController.php | 2 + .../icon/PhabricatorProjectDropEffect.php | 16 ++++++++ .../PhabricatorProjectColumnOwnerOrder.php | 1 + .../PhabricatorProjectColumnPriorityOrder.php | 1 + .../PhabricatorProjectColumnStatusOrder.php | 1 + ...catorProjectTriggerManiphestStatusRule.php | 1 + .../js/application/projects/WorkboardBoard.js | 11 +++++ .../projects/WorkboardDropEffect.js | 32 ++++++++++++++- 9 files changed, 83 insertions(+), 22 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 4d095ad2b1..4b732de524 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -408,12 +408,12 @@ return array( 'rsrc/js/application/phortune/phortune-credit-card-form.js' => 'd12d214f', 'rsrc/js/application/policy/behavior-policy-control.js' => '0eaa33a9', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '9347f172', - 'rsrc/js/application/projects/WorkboardBoard.js' => 'ba6e36b0', + 'rsrc/js/application/projects/WorkboardBoard.js' => '2f893acd', 'rsrc/js/application/projects/WorkboardCard.js' => '0392a5d8', 'rsrc/js/application/projects/WorkboardCardTemplate.js' => '2a61f8d4', 'rsrc/js/application/projects/WorkboardColumn.js' => 'c344eb3c', 'rsrc/js/application/projects/WorkboardController.js' => '42c7a5a7', - 'rsrc/js/application/projects/WorkboardDropEffect.js' => '101121be', + 'rsrc/js/application/projects/WorkboardDropEffect.js' => 'c808589e', 'rsrc/js/application/projects/WorkboardHeader.js' => '111bfd2d', 'rsrc/js/application/projects/WorkboardHeaderTemplate.js' => 'ebe83a6b', 'rsrc/js/application/projects/WorkboardOrderTemplate.js' => '03e8891f', @@ -730,12 +730,12 @@ return array( 'javelin-view-renderer' => '9aae2b66', 'javelin-view-visitor' => '308f9fe4', 'javelin-websocket' => 'fdc13e4e', - 'javelin-workboard-board' => 'ba6e36b0', + 'javelin-workboard-board' => '2f893acd', 'javelin-workboard-card' => '0392a5d8', 'javelin-workboard-card-template' => '2a61f8d4', 'javelin-workboard-column' => 'c344eb3c', 'javelin-workboard-controller' => '42c7a5a7', - 'javelin-workboard-drop-effect' => '101121be', + 'javelin-workboard-drop-effect' => 'c808589e', 'javelin-workboard-header' => '111bfd2d', 'javelin-workboard-header-template' => 'ebe83a6b', 'javelin-workboard-order-template' => '03e8891f', @@ -1003,10 +1003,6 @@ return array( 'javelin-workflow', 'phuix-icon-view', ), - '101121be' => array( - 'javelin-install', - 'javelin-dom', - ), '111bfd2d' => array( 'javelin-install', ), @@ -1170,6 +1166,18 @@ return array( 'phuix-autocomplete', 'javelin-mask', ), + '2f893acd' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-util', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + 'javelin-workboard-column', + 'javelin-workboard-header-template', + 'javelin-workboard-card-template', + 'javelin-workboard-order-template', + ), '308f9fe4' => array( 'javelin-install', 'javelin-util', @@ -1880,18 +1888,6 @@ return array( 'javelin-uri', 'phabricator-notification', ), - 'ba6e36b0' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-util', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - 'javelin-workboard-column', - 'javelin-workboard-header-template', - 'javelin-workboard-card-template', - 'javelin-workboard-order-template', - ), 'bdce4d78' => array( 'javelin-install', 'javelin-util', @@ -1955,6 +1951,10 @@ return array( 'phuix-icon-view', 'phabricator-busy', ), + 'c808589e' => array( + 'javelin-install', + 'javelin-dom', + ), 'c8147a20' => array( 'javelin-behavior', 'javelin-dom', diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index 41981a5522..6d0c8721fc 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -655,6 +655,8 @@ final class PhabricatorProjectBoardViewController $properties[$task->getPHID()] = array( 'points' => (double)$task->getPoints(), 'status' => $task->getStatus(), + 'priority' => (int)$task->getPriority(), + 'owner' => $task->getOwnerPHID(), ); } diff --git a/src/applications/project/icon/PhabricatorProjectDropEffect.php b/src/applications/project/icon/PhabricatorProjectDropEffect.php index 33145eb039..8e68261fa4 100644 --- a/src/applications/project/icon/PhabricatorProjectDropEffect.php +++ b/src/applications/project/icon/PhabricatorProjectDropEffect.php @@ -6,6 +6,7 @@ final class PhabricatorProjectDropEffect private $icon; private $color; private $content; + private $conditions = array(); public function setIcon($icon) { $this->icon = $icon; @@ -39,7 +40,22 @@ final class PhabricatorProjectDropEffect 'icon' => $this->getIcon(), 'color' => $this->getColor(), 'content' => hsprintf('%s', $this->getContent()), + 'conditions' => $this->getConditions(), ); } + public function addCondition($field, $operator, $value) { + $this->conditions[] = array( + 'field' => $field, + 'operator' => $operator, + 'value' => $value, + ); + + return $this; + } + + public function getConditions() { + return $this->conditions; + } + } diff --git a/src/applications/project/order/PhabricatorProjectColumnOwnerOrder.php b/src/applications/project/order/PhabricatorProjectColumnOwnerOrder.php index 920e87c9b7..48a6c394db 100644 --- a/src/applications/project/order/PhabricatorProjectColumnOwnerOrder.php +++ b/src/applications/project/order/PhabricatorProjectColumnOwnerOrder.php @@ -171,6 +171,7 @@ final class PhabricatorProjectColumnOwnerOrder $this->newEffect() ->setIcon($owner_icon) ->setColor($owner_color) + ->addCondition('owner', '!=', $owner_phid) ->setContent($effect_content)); } diff --git a/src/applications/project/order/PhabricatorProjectColumnPriorityOrder.php b/src/applications/project/order/PhabricatorProjectColumnPriorityOrder.php index 8cffab91ae..42ccf96553 100644 --- a/src/applications/project/order/PhabricatorProjectColumnPriorityOrder.php +++ b/src/applications/project/order/PhabricatorProjectColumnPriorityOrder.php @@ -68,6 +68,7 @@ final class PhabricatorProjectColumnPriorityOrder $drop_effect = $this->newEffect() ->setIcon($priority_icon) ->setColor($priority_color) + ->addCondition('priority', '!=', $priority) ->setContent( pht( 'Change priority to %s.', diff --git a/src/applications/project/order/PhabricatorProjectColumnStatusOrder.php b/src/applications/project/order/PhabricatorProjectColumnStatusOrder.php index 419d7062c7..2cb156aa92 100644 --- a/src/applications/project/order/PhabricatorProjectColumnStatusOrder.php +++ b/src/applications/project/order/PhabricatorProjectColumnStatusOrder.php @@ -75,6 +75,7 @@ final class PhabricatorProjectColumnStatusOrder $drop_effect = $this->newEffect() ->setIcon($status_icon) ->setColor($status_color) + ->addCondition('status', '!=', $status_key) ->setContent( pht( 'Change status to %s.', diff --git a/src/applications/project/trigger/PhabricatorProjectTriggerManiphestStatusRule.php b/src/applications/project/trigger/PhabricatorProjectTriggerManiphestStatusRule.php index 575f51cd2b..58e8c227df 100644 --- a/src/applications/project/trigger/PhabricatorProjectTriggerManiphestStatusRule.php +++ b/src/applications/project/trigger/PhabricatorProjectTriggerManiphestStatusRule.php @@ -51,6 +51,7 @@ final class PhabricatorProjectTriggerManiphestStatusRule $this->newEffect() ->setIcon($status_icon) ->setColor($status_color) + ->addCondition('status', '!=', $value) ->setContent($content), ); } diff --git a/webroot/rsrc/js/application/projects/WorkboardBoard.js b/webroot/rsrc/js/application/projects/WorkboardBoard.js index 6fab227c84..5a55a2d905 100644 --- a/webroot/rsrc/js/application/projects/WorkboardBoard.js +++ b/webroot/rsrc/js/application/projects/WorkboardBoard.js @@ -228,6 +228,17 @@ JX.install('WorkboardBoard', { effects = effects.concat(header.getDropEffects()); } + var card_phid = JX.Stratcom.getData(src_node).objectPHID; + var card = src_column.getCard(card_phid); + + var visible = []; + for (var ii = 0; ii < effects.length; ii++) { + if (effects[ii].isEffectVisibleForCard(card)) { + visible.push(effects[ii]); + } + } + effects = visible; + if (!effects.length) { JX.DOM.remove(node); return; diff --git a/webroot/rsrc/js/application/projects/WorkboardDropEffect.js b/webroot/rsrc/js/application/projects/WorkboardDropEffect.js index ecd18d0015..fc8b2eaa58 100644 --- a/webroot/rsrc/js/application/projects/WorkboardDropEffect.js +++ b/webroot/rsrc/js/application/projects/WorkboardDropEffect.js @@ -10,7 +10,8 @@ JX.install('WorkboardDropEffect', { properties: { icon: null, color: null, - content: null + content: null, + conditions: [] }, statics: { @@ -18,7 +19,8 @@ JX.install('WorkboardDropEffect', { return new JX.WorkboardDropEffect() .setIcon(map.icon) .setColor(map.color) - .setContent(JX.$H(map.content)); + .setContent(JX.$H(map.content)) + .setConditions(map.conditions || []); } }, @@ -30,6 +32,32 @@ JX.install('WorkboardDropEffect', { .getNode(); return JX.$N('li', {}, [icon, this.getContent()]); + }, + + isEffectVisibleForCard: function(card) { + var conditions = this.getConditions(); + + var properties = card.getProperties(); + for (var ii = 0; ii < conditions.length; ii++) { + var condition = conditions[ii]; + + var field = properties[condition.field]; + var value = condition.value; + + var result = true; + switch (condition.operator) { + case '!=': + result = (field !== value); + break; + } + + if (!result) { + return false; + } + } + + return true; } + } }); From 567dea54492b792d52470fa055ffe56d8002a2f4 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 21 Mar 2019 09:32:12 -0700 Subject: [PATCH 197/245] Mostly make the editor UI for triggers work Summary: Ref T5474. This provides a Herald-like UI for editing workboard trigger rules. This probably has some missing pieces and doesn't actually save anything to the database yet, but the basics at least roughly work. Test Plan: {F6299886} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T5474 Differential Revision: https://secure.phabricator.com/D20301 --- resources/celerity/map.php | 28 ++++ ...habricatorProjectTriggerEditController.php | 83 ++++++++++- .../PhabricatorProjectTriggerInvalidRule.php | 36 +++++ ...catorProjectTriggerManiphestStatusRule.php | 21 +++ .../trigger/PhabricatorProjectTriggerRule.php | 48 ++++++ .../PhabricatorProjectTriggerUnknownRule.php | 35 +++++ .../application/project/project-triggers.css | 38 +++++ .../js/application/trigger/TriggerRule.js | 140 ++++++++++++++++++ .../application/trigger/TriggerRuleControl.js | 40 +++++ .../application/trigger/TriggerRuleEditor.js | 137 +++++++++++++++++ .../js/application/trigger/TriggerRuleType.js | 36 +++++ .../trigger/trigger-rule-editor.js | 41 +++++ 12 files changed, 682 insertions(+), 1 deletion(-) create mode 100644 webroot/rsrc/css/application/project/project-triggers.css create mode 100644 webroot/rsrc/js/application/trigger/TriggerRule.js create mode 100644 webroot/rsrc/js/application/trigger/TriggerRuleControl.js create mode 100644 webroot/rsrc/js/application/trigger/TriggerRuleEditor.js create mode 100644 webroot/rsrc/js/application/trigger/TriggerRuleType.js create mode 100644 webroot/rsrc/js/application/trigger/trigger-rule-editor.js diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 4b732de524..c12e141770 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -100,6 +100,7 @@ return array( 'rsrc/css/application/policy/policy.css' => 'ceb56a08', 'rsrc/css/application/ponder/ponder-view.css' => '05a09d0a', 'rsrc/css/application/project/project-card-view.css' => '3b1f7b20', + 'rsrc/css/application/project/project-triggers.css' => 'cb866c2d', 'rsrc/css/application/project/project-view.css' => '567858b3', 'rsrc/css/application/releeph/releeph-core.css' => 'f81ff2db', 'rsrc/css/application/releeph/releeph-preview-branch.css' => '22db5c07', @@ -432,6 +433,11 @@ return array( 'rsrc/js/application/transactions/behavior-show-older-transactions.js' => '600f440c', 'rsrc/js/application/transactions/behavior-transaction-comment-form.js' => '2bdadf1a', 'rsrc/js/application/transactions/behavior-transaction-list.js' => '9cec214e', + 'rsrc/js/application/trigger/TriggerRule.js' => 'e4a816a4', + 'rsrc/js/application/trigger/TriggerRuleControl.js' => '5faf27b9', + 'rsrc/js/application/trigger/TriggerRuleEditor.js' => 'b49fd60c', + 'rsrc/js/application/trigger/TriggerRuleType.js' => '4feea7d3', + 'rsrc/js/application/trigger/trigger-rule-editor.js' => '398fdf13', 'rsrc/js/application/typeahead/behavior-typeahead-browse.js' => '70245195', 'rsrc/js/application/typeahead/behavior-typeahead-search.js' => '7b139193', 'rsrc/js/application/uiexample/gesture-example.js' => '242dedd0', @@ -683,6 +689,7 @@ return array( 'javelin-behavior-time-typeahead' => '5803b9e7', 'javelin-behavior-toggle-class' => 'f5c78ae3', 'javelin-behavior-toggle-widget' => '8f959ad0', + 'javelin-behavior-trigger-rule-editor' => '398fdf13', 'javelin-behavior-typeahead-browse' => '70245195', 'javelin-behavior-typeahead-search' => '7b139193', 'javelin-behavior-user-menu' => '60cd9241', @@ -875,6 +882,7 @@ return array( 'policy-transaction-detail-css' => 'c02b8384', 'ponder-view-css' => '05a09d0a', 'project-card-view-css' => '3b1f7b20', + 'project-triggers-css' => 'cb866c2d', 'project-view-css' => '567858b3', 'releeph-core' => 'f81ff2db', 'releeph-preview-branch' => '22db5c07', @@ -886,6 +894,10 @@ return array( 'syntax-default-css' => '055fc231', 'syntax-highlighting-css' => '4234f572', 'tokens-css' => 'ce5a50bd', + 'trigger-rule' => 'e4a816a4', + 'trigger-rule-control' => '5faf27b9', + 'trigger-rule-editor' => 'b49fd60c', + 'trigger-rule-type' => '4feea7d3', 'typeahead-browse-css' => 'b7ed02d2', 'unhandled-exception-css' => '9ecfc00d', ), @@ -1217,6 +1229,12 @@ return array( 'javelin-install', 'javelin-dom', ), + '398fdf13' => array( + 'javelin-behavior', + 'trigger-rule-editor', + 'trigger-rule', + 'trigger-rule-type', + ), '3b4899b0' => array( 'javelin-behavior', 'phabricator-prefab', @@ -1347,6 +1365,9 @@ return array( 'javelin-sound', 'phabricator-notification', ), + '4feea7d3' => array( + 'trigger-rule-control', + ), '506aa3f4' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1432,6 +1453,9 @@ return array( 'javelin-dom', 'phuix-dropdown-menu', ), + '5faf27b9' => array( + 'phuix-form-control-view', + ), '600f440c' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1850,6 +1874,10 @@ return array( 'b347a301' => array( 'javelin-behavior', ), + 'b49fd60c' => array( + 'multirow-row-manager', + 'trigger-rule', + ), 'b517bfa0' => array( 'phui-oi-list-view-css', ), diff --git a/src/applications/project/controller/trigger/PhabricatorProjectTriggerEditController.php b/src/applications/project/controller/trigger/PhabricatorProjectTriggerEditController.php index 86f0844be3..95d1e9f021 100644 --- a/src/applications/project/controller/trigger/PhabricatorProjectTriggerEditController.php +++ b/src/applications/project/controller/trigger/PhabricatorProjectTriggerEditController.php @@ -65,6 +65,9 @@ final class PhabricatorProjectTriggerEditController $v_name = $request->getStr('name'); $v_edit = $request->getStr('editPolicy'); + $v_rules = $request->getStr('rules'); + $v_rules = phutil_json_decode($v_rules); + $xactions = array(); if (!$trigger->getID()) { $xactions[] = $trigger->getApplicationTransactionTemplate() @@ -81,6 +84,8 @@ final class PhabricatorProjectTriggerEditController ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY) ->setNewValue($v_edit); + // TODO: Actually write the new rules to the database. + $editor = $trigger->getApplicationTransactionEditor() ->setActor($viewer) ->setContentSourceFromRequest($request) @@ -133,8 +138,14 @@ final class PhabricatorProjectTriggerEditController $header = pht('New Trigger'); } + $form_id = celerity_generate_unique_node_id(); + $table_id = celerity_generate_unique_node_id(); + $create_id = celerity_generate_unique_node_id(); + $input_id = celerity_generate_unique_node_id(); + $form = id(new AphrontFormView()) - ->setViewer($viewer); + ->setViewer($viewer) + ->setID($form_id); if ($column) { $form->addHiddenInput('columnPHID', $column->getPHID()); @@ -161,6 +172,46 @@ final class PhabricatorProjectTriggerEditController ->setPolicies($policies) ->setError($e_edit)); + $form->appendChild( + phutil_tag( + 'input', + array( + 'type' => 'hidden', + 'name' => 'rules', + 'id' => $input_id, + ))); + + $form->appendChild( + id(new PHUIFormInsetView()) + ->setTitle(pht('Rules')) + ->setDescription( + pht( + 'When a card is dropped into a column which uses this trigger:')) + ->setRightButton( + javelin_tag( + 'a', + array( + 'href' => '#', + 'class' => 'button button-green', + 'id' => $create_id, + 'mustcapture' => true, + ), + pht('New Rule'))) + ->setContent( + javelin_tag( + 'table', + array( + 'id' => $table_id, + 'class' => 'trigger-rules-table', + )))); + + $this->setupEditorBehavior( + $trigger, + $form_id, + $table_id, + $create_id, + $input_id); + $form->appendControl( id(new AphrontFormSubmitControl()) ->setValue($submit) @@ -197,4 +248,34 @@ final class PhabricatorProjectTriggerEditController ->appendChild($column_view); } + private function setupEditorBehavior( + PhabricatorProjectTrigger $trigger, + $form_id, + $table_id, + $create_id, + $input_id) { + + $rule_list = $trigger->getTriggerRules(); + $rule_list = mpull($rule_list, 'toDictionary'); + $rule_list = array_values($rule_list); + + $type_list = PhabricatorProjectTriggerRule::getAllTriggerRules(); + $type_list = mpull($type_list, 'newTemplate'); + $type_list = array_values($type_list); + + require_celerity_resource('project-triggers-css'); + + Javelin::initBehavior( + 'trigger-rule-editor', + array( + 'formNodeID' => $form_id, + 'tableNodeID' => $table_id, + 'createNodeID' => $create_id, + 'inputNodeID' => $input_id, + + 'rules' => $rule_list, + 'types' => $type_list, + )); + } + } diff --git a/src/applications/project/trigger/PhabricatorProjectTriggerInvalidRule.php b/src/applications/project/trigger/PhabricatorProjectTriggerInvalidRule.php index 4157ec8e9a..0f9fe52abb 100644 --- a/src/applications/project/trigger/PhabricatorProjectTriggerInvalidRule.php +++ b/src/applications/project/trigger/PhabricatorProjectTriggerInvalidRule.php @@ -11,6 +11,14 @@ final class PhabricatorProjectTriggerInvalidRule $this->getRecord()->getType()); } + public function getSelectControlName() { + return pht('(Invalid Rule)'); + } + + protected function isSelectableRule() { + return false; + } + protected function assertValidRuleValue($value) { return; } @@ -23,4 +31,32 @@ final class PhabricatorProjectTriggerInvalidRule return array(); } + protected function isValidRule() { + return false; + } + + protected function newInvalidView() { + return array( + id(new PHUIIconView()) + ->setIcon('fa-exclamation-triangle red'), + ' ', + pht( + 'This is a trigger rule with a valid type ("%s") but an invalid '. + 'value.', + $this->getRecord()->getType()), + ); + } + + protected function getDefaultValue() { + return null; + } + + protected function getPHUIXControlType() { + return null; + } + + protected function getPHUIXControlSpecification() { + return null; + } + } diff --git a/src/applications/project/trigger/PhabricatorProjectTriggerManiphestStatusRule.php b/src/applications/project/trigger/PhabricatorProjectTriggerManiphestStatusRule.php index 58e8c227df..2c40563884 100644 --- a/src/applications/project/trigger/PhabricatorProjectTriggerManiphestStatusRule.php +++ b/src/applications/project/trigger/PhabricatorProjectTriggerManiphestStatusRule.php @@ -13,6 +13,10 @@ final class PhabricatorProjectTriggerManiphestStatusRule ManiphestTaskStatus::getTaskStatusName($value)); } + public function getSelectControlName() { + return pht('Change status to'); + } + protected function assertValidRuleValue($value) { if (!is_string($value)) { throw new Exception( @@ -56,4 +60,21 @@ final class PhabricatorProjectTriggerManiphestStatusRule ); } + protected function getDefaultValue() { + return head_key(ManiphestTaskStatus::getTaskStatusMap()); + } + + protected function getPHUIXControlType() { + return 'select'; + } + + protected function getPHUIXControlSpecification() { + $map = ManiphestTaskStatus::getTaskStatusMap(); + + return array( + 'options' => $map, + 'order' => array_keys($map), + ); + } + } diff --git a/src/applications/project/trigger/PhabricatorProjectTriggerRule.php b/src/applications/project/trigger/PhabricatorProjectTriggerRule.php index 6caf9ee0be..9634086235 100644 --- a/src/applications/project/trigger/PhabricatorProjectTriggerRule.php +++ b/src/applications/project/trigger/PhabricatorProjectTriggerRule.php @@ -38,9 +38,25 @@ abstract class PhabricatorProjectTriggerRule } abstract public function getDescription(); + abstract public function getSelectControlName(); abstract protected function assertValidRuleValue($value); abstract protected function newDropTransactions($object, $value); abstract protected function newDropEffects($value); + abstract protected function getDefaultValue(); + abstract protected function getPHUIXControlType(); + abstract protected function getPHUIXControlSpecification(); + + protected function isSelectableRule() { + return true; + } + + protected function isValidRule() { + return true; + } + + protected function newInvalidView() { + return null; + } final public function getDropTransactions($object, $value) { return $this->newDropTransactions($object, $value); @@ -95,4 +111,36 @@ abstract class PhabricatorProjectTriggerRule return new PhabricatorProjectDropEffect(); } + final public function toDictionary() { + $record = $this->getRecord(); + + $is_valid = $this->isValidRule(); + if (!$is_valid) { + $invalid_view = hsprintf('%s', $this->newInvalidView()); + } else { + $invalid_view = null; + } + + return array( + 'type' => $record->getType(), + 'value' => $record->getValue(), + 'isValidRule' => $is_valid, + 'invalidView' => $invalid_view, + ); + } + + final public function newTemplate() { + return array( + 'type' => $this->getTriggerType(), + 'name' => $this->getSelectControlName(), + 'selectable' => $this->isSelectableRule(), + 'defaultValue' => $this->getDefaultValue(), + 'control' => array( + 'type' => $this->getPHUIXControlType(), + 'specification' => $this->getPHUIXControlSpecification(), + ), + ); + } + + } diff --git a/src/applications/project/trigger/PhabricatorProjectTriggerUnknownRule.php b/src/applications/project/trigger/PhabricatorProjectTriggerUnknownRule.php index 8d796686de..008092061d 100644 --- a/src/applications/project/trigger/PhabricatorProjectTriggerUnknownRule.php +++ b/src/applications/project/trigger/PhabricatorProjectTriggerUnknownRule.php @@ -11,6 +11,14 @@ final class PhabricatorProjectTriggerUnknownRule $this->getRecord()->getType()); } + public function getSelectControlName() { + return pht('(Unknown Rule)'); + } + + protected function isSelectableRule() { + return false; + } + protected function assertValidRuleValue($value) { return; } @@ -23,4 +31,31 @@ final class PhabricatorProjectTriggerUnknownRule return array(); } + protected function isValidRule() { + return false; + } + + protected function newInvalidView() { + return array( + id(new PHUIIconView()) + ->setIcon('fa-exclamation-triangle yellow'), + ' ', + pht( + 'This is a trigger rule with a unknown type ("%s").', + $this->getRecord()->getType()), + ); + } + + protected function getDefaultValue() { + return null; + } + + protected function getPHUIXControlType() { + return null; + } + + protected function getPHUIXControlSpecification() { + return null; + } + } diff --git a/webroot/rsrc/css/application/project/project-triggers.css b/webroot/rsrc/css/application/project/project-triggers.css new file mode 100644 index 0000000000..9b3ce8e462 --- /dev/null +++ b/webroot/rsrc/css/application/project/project-triggers.css @@ -0,0 +1,38 @@ +/** + * @provides project-triggers-css + */ + +.trigger-rules-table { + margin: 16px 0; + border-collapse: separate; + border-spacing: 0 4px; +} + +.trigger-rules-table tr { + background: {$bluebackground}; +} + +.trigger-rules-table td { + padding: 6px 4px; + vertical-align: middle; +} + +.trigger-rules-table td.type-cell { + padding-left: 6px; +} + +.trigger-rules-table td.remove-column { + padding-right: 6px; +} + +.trigger-rules-table td.invalid-cell { + padding-left: 12px; +} + +.trigger-rules-table td.invalid-cell .phui-icon-view { + margin-right: 4px; +} + +.trigger-rules-table td.value-cell { + width: 100%; +} diff --git a/webroot/rsrc/js/application/trigger/TriggerRule.js b/webroot/rsrc/js/application/trigger/TriggerRule.js new file mode 100644 index 0000000000..69feeade18 --- /dev/null +++ b/webroot/rsrc/js/application/trigger/TriggerRule.js @@ -0,0 +1,140 @@ +/** + * @provides trigger-rule + * @javelin + */ + +JX.install('TriggerRule', { + + construct: function() { + }, + + properties: { + rowID: null, + type: null, + value: null, + editor: null, + isValidRule: true, + invalidView: null + }, + + statics: { + newFromDictionary: function(map) { + return new JX.TriggerRule() + .setType(map.type) + .setValue(map.value) + .setIsValidRule(map.isValidRule) + .setInvalidView(map.invalidView); + }, + }, + + members: { + _typeCell: null, + _valueCell: null, + _readValueCallback: null, + + newRowContent: function() { + if (!this.getIsValidRule()) { + var invalid_cell = JX.$N( + 'td', + { + colSpan: 2, + className: 'invalid-cell' + }, + JX.$H(this.getInvalidView())); + + return [invalid_cell]; + } + + var type_cell = this._getTypeCell(); + var value_cell = this._getValueCell(); + + + this._rebuildValueControl(); + + return [type_cell, value_cell]; + }, + + getValueForSubmit: function() { + this._readValueFromControl(); + + return { + type: this.getType(), + value: this.getValue() + }; + }, + + _getTypeCell: function() { + if (!this._typeCell) { + var editor = this.getEditor(); + var types = editor.getTypes(); + + var options = []; + for (var ii = 0; ii < types.length; ii++) { + var type = types[ii]; + + if (!type.getIsSelectable()) { + continue; + } + + options.push( + JX.$N('option', {value: type.getType()}, type.getName())); + } + + var control = JX.$N('select', {}, options); + + control.value = this.getType(); + + var on_change = JX.bind(this, this._onTypeChange); + JX.DOM.listen(control, 'onchange', null, on_change); + + var attributes = { + className: 'type-cell' + }; + + this._typeCell = JX.$N('td', attributes, control); + } + + return this._typeCell; + }, + + _onTypeChange: function() { + var control = this._getTypeCell(); + this.setType(control.value); + + this._rebuildValueControl(); + }, + + _getValueCell: function() { + if (!this._valueCell) { + var attributes = { + className: 'value-cell' + }; + + this._valueCell = JX.$N('td', attributes); + } + + return this._valueCell; + }, + + _rebuildValueControl: function() { + var value_cell = this._getValueCell(); + + var editor = this.getEditor(); + var type = editor.getType(this.getType()); + var control = type.getControl(); + + var input = control.newInput(this); + this._readValueCallback = input.get; + + JX.DOM.setContent(value_cell, input.node); + }, + + _readValueFromControl: function() { + if (this._readValueCallback) { + this.setValue(this._readValueCallback()); + } + } + + } + +}); diff --git a/webroot/rsrc/js/application/trigger/TriggerRuleControl.js b/webroot/rsrc/js/application/trigger/TriggerRuleControl.js new file mode 100644 index 0000000000..a05e740ff9 --- /dev/null +++ b/webroot/rsrc/js/application/trigger/TriggerRuleControl.js @@ -0,0 +1,40 @@ +/** + * @requires phuix-form-control-view + * @provides trigger-rule-control + * @javelin + */ + +JX.install('TriggerRuleControl', { + + construct: function() { + }, + + properties: { + type: null, + specification: null + }, + + statics: { + newFromDictionary: function(map) { + return new JX.TriggerRuleControl() + .setType(map.type) + .setSpecification(map.specification); + }, + }, + + members: { + newInput: function(rule) { + var phuix = new JX.PHUIXFormControl() + .setControl(this.getType(), this.getSpecification()); + + phuix.setValue(rule.getValue()); + + return { + node: phuix.getRawInputNode(), + get: JX.bind(phuix, phuix.getValue) + }; + } + + } + +}); diff --git a/webroot/rsrc/js/application/trigger/TriggerRuleEditor.js b/webroot/rsrc/js/application/trigger/TriggerRuleEditor.js new file mode 100644 index 0000000000..3574a8dbca --- /dev/null +++ b/webroot/rsrc/js/application/trigger/TriggerRuleEditor.js @@ -0,0 +1,137 @@ +/** + * @requires multirow-row-manager + * trigger-rule + * @provides trigger-rule-editor + * @javelin + */ + +JX.install('TriggerRuleEditor', { + + construct: function(form_node) { + this._formNode = form_node; + this._rules = []; + this._types = []; + }, + + members: { + _formNode: null, + _tableNode: null, + _createButtonNode: null, + _inputNode: null, + _rowManager: null, + _rules: null, + _types: null, + + setTableNode: function(table) { + this._tableNode = table; + return this; + }, + + setCreateButtonNode: function(button) { + this._createButtonNode = button; + return this; + }, + + setInputNode: function(input) { + this._inputNode = input; + return this; + }, + + start: function() { + var on_submit = JX.bind(this, this._submitForm); + JX.DOM.listen(this._formNode, 'submit', null, on_submit); + + var manager = new JX.MultirowRowManager(this._tableNode); + this._rowManager = manager; + + var on_remove = JX.bind(this, this._rowRemoved); + manager.listen('row-removed', on_remove); + + var create_button = this._createButtonNode; + var on_create = JX.bind(this, this._createRow); + JX.DOM.listen(create_button, 'click', null, on_create); + }, + + _submitForm: function() { + var values = []; + for (var ii = 0; ii < this._rules.length; ii++) { + var rule = this._rules[ii]; + values.push(rule.getValueForSubmit()); + } + + this._inputNode.value = JX.JSON.stringify(values); + }, + + _createRow: function(e) { + var rule = this.newRule(); + this.addRule(rule); + e.kill(); + }, + + newRule: function() { + // Create new rules with the first valid rule type. + var types = this.getTypes(); + var type; + for (var ii = 0; ii < types.length; ii++) { + type = types[ii]; + if (!type.getIsSelectable()) { + continue; + } + + // If we make it here: this type is valid, so use it. + break; + } + + var default_value = type.getDefaultValue(); + + return new JX.TriggerRule() + .setType(type.getType()) + .setValue(default_value); + }, + + addRule: function(rule) { + rule.setEditor(this); + this._rules.push(rule); + + var manager = this._rowManager; + + var row = manager.addRow([]); + var row_id = manager.getRowID(row); + rule.setRowID(row_id); + + manager.updateRow(row_id, rule.newRowContent()); + }, + + addType: function(type) { + this._types.push(type); + return this; + }, + + getTypes: function() { + return this._types; + }, + + getType: function(type) { + for (var ii = 0; ii < this._types.length; ii++) { + if (this._types[ii].getType() === type) { + return this._types[ii]; + } + } + + return null; + }, + + _rowRemoved: function(row_id) { + for (var ii = 0; ii < this._rules.length; ii++) { + var rule = this._rules[ii]; + + if (rule.getRowID() === row_id) { + this._rules.splice(ii, 1); + break; + } + } + } + + } + +}); diff --git a/webroot/rsrc/js/application/trigger/TriggerRuleType.js b/webroot/rsrc/js/application/trigger/TriggerRuleType.js new file mode 100644 index 0000000000..1075eecedf --- /dev/null +++ b/webroot/rsrc/js/application/trigger/TriggerRuleType.js @@ -0,0 +1,36 @@ +/** + * @requires trigger-rule-control + * @provides trigger-rule-type + * @javelin + */ + +JX.install('TriggerRuleType', { + + construct: function() { + }, + + properties: { + type: null, + name: null, + isSelectable: true, + defaultValue: null, + control: null + }, + + statics: { + newFromDictionary: function(map) { + var control = JX.TriggerRuleControl.newFromDictionary(map.control); + + return new JX.TriggerRuleType() + .setType(map.type) + .setName(map.name) + .setIsSelectable(map.selectable) + .setDefaultValue(map.defaultValue) + .setControl(control); + }, + }, + + members: { + } + +}); diff --git a/webroot/rsrc/js/application/trigger/trigger-rule-editor.js b/webroot/rsrc/js/application/trigger/trigger-rule-editor.js new file mode 100644 index 0000000000..d2741cc337 --- /dev/null +++ b/webroot/rsrc/js/application/trigger/trigger-rule-editor.js @@ -0,0 +1,41 @@ +/** + * @requires javelin-behavior + * trigger-rule-editor + * trigger-rule + * trigger-rule-type + * @provides javelin-behavior-trigger-rule-editor + * @javelin + */ + +JX.behavior('trigger-rule-editor', function(config) { + var form_node = JX.$(config.formNodeID); + var table_node = JX.$(config.tableNodeID); + var create_node = JX.$(config.createNodeID); + var input_node = JX.$(config.inputNodeID); + + var editor = new JX.TriggerRuleEditor(form_node) + .setTableNode(table_node) + .setCreateButtonNode(create_node) + .setInputNode(input_node); + + editor.start(); + + var ii; + + for (ii = 0; ii < config.types.length; ii++) { + var type = JX.TriggerRuleType.newFromDictionary(config.types[ii]); + editor.addType(type); + } + + if (config.rules.length) { + for (ii = 0; ii < config.rules.length; ii++) { + var rule = JX.TriggerRule.newFromDictionary(config.rules[ii]); + editor.addRule(rule); + } + } else { + // If the trigger doesn't have any rules yet, add an empty rule to start + // with, so the user doesn't have to click "New Rule". + editor.addRule(editor.newRule()); + } + +}); From ff128e1b3244dc717a002de76c25f70d72d0a1ee Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 21 Mar 2019 11:48:59 -0700 Subject: [PATCH 198/245] Write workboard trigger rules to the database Summary: Ref T5474. Read and write trigger rules so users can actually edit them. Test Plan: Added, modified, and removed trigger rules. Saved changes, used "Show Details" to review edits. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T5474 Differential Revision: https://secure.phabricator.com/D20302 --- src/__phutil_library_map__.php | 2 + ...habricatorProjectTriggerEditController.php | 20 +- .../storage/PhabricatorProjectTrigger.php | 212 ++++++++++-------- ...icatorProjectTriggerRulesetTransaction.php | 65 ++++++ 4 files changed, 196 insertions(+), 103 deletions(-) create mode 100644 src/applications/project/xaction/trigger/PhabricatorProjectTriggerRulesetTransaction.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index ebe7c2b9c3..a4edf3d246 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4186,6 +4186,7 @@ phutil_register_library_map(array( 'PhabricatorProjectTriggerQuery' => 'applications/project/query/PhabricatorProjectTriggerQuery.php', 'PhabricatorProjectTriggerRule' => 'applications/project/trigger/PhabricatorProjectTriggerRule.php', 'PhabricatorProjectTriggerRuleRecord' => 'applications/project/trigger/PhabricatorProjectTriggerRuleRecord.php', + 'PhabricatorProjectTriggerRulesetTransaction' => 'applications/project/xaction/trigger/PhabricatorProjectTriggerRulesetTransaction.php', 'PhabricatorProjectTriggerSearchEngine' => 'applications/project/query/PhabricatorProjectTriggerSearchEngine.php', 'PhabricatorProjectTriggerTransaction' => 'applications/project/storage/PhabricatorProjectTriggerTransaction.php', 'PhabricatorProjectTriggerTransactionQuery' => 'applications/project/query/PhabricatorProjectTriggerTransactionQuery.php', @@ -10319,6 +10320,7 @@ phutil_register_library_map(array( 'PhabricatorProjectTriggerQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorProjectTriggerRule' => 'Phobject', 'PhabricatorProjectTriggerRuleRecord' => 'Phobject', + 'PhabricatorProjectTriggerRulesetTransaction' => 'PhabricatorProjectTriggerTransactionType', 'PhabricatorProjectTriggerSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorProjectTriggerTransaction' => 'PhabricatorModularTransaction', 'PhabricatorProjectTriggerTransactionQuery' => 'PhabricatorApplicationTransactionQuery', diff --git a/src/applications/project/controller/trigger/PhabricatorProjectTriggerEditController.php b/src/applications/project/controller/trigger/PhabricatorProjectTriggerEditController.php index 95d1e9f021..7189df70ec 100644 --- a/src/applications/project/controller/trigger/PhabricatorProjectTriggerEditController.php +++ b/src/applications/project/controller/trigger/PhabricatorProjectTriggerEditController.php @@ -55,6 +55,7 @@ final class PhabricatorProjectTriggerEditController $v_name = $trigger->getName(); $v_edit = $trigger->getEditPolicy(); + $v_rules = $trigger->getTriggerRules(); $e_name = null; $e_edit = null; @@ -65,8 +66,15 @@ final class PhabricatorProjectTriggerEditController $v_name = $request->getStr('name'); $v_edit = $request->getStr('editPolicy'); - $v_rules = $request->getStr('rules'); - $v_rules = phutil_json_decode($v_rules); + // Read the JSON rules from the request and convert them back into + // "TriggerRule" objects so we can render the correct form state + // if the user is modifying the rules + $raw_rules = $request->getStr('rules'); + $raw_rules = phutil_json_decode($raw_rules); + + $copy = clone $trigger; + $copy->setRuleset($raw_rules); + $v_rules = $copy->getTriggerRules(); $xactions = array(); if (!$trigger->getID()) { @@ -84,7 +92,10 @@ final class PhabricatorProjectTriggerEditController ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY) ->setNewValue($v_edit); - // TODO: Actually write the new rules to the database. + $xactions[] = $trigger->getApplicationTransactionTemplate() + ->setTransactionType( + PhabricatorProjectTriggerRulesetTransaction::TRANSACTIONTYPE) + ->setNewValue($raw_rules); $editor = $trigger->getApplicationTransactionEditor() ->setActor($viewer) @@ -207,6 +218,7 @@ final class PhabricatorProjectTriggerEditController $this->setupEditorBehavior( $trigger, + $v_rules, $form_id, $table_id, $create_id, @@ -250,12 +262,12 @@ final class PhabricatorProjectTriggerEditController private function setupEditorBehavior( PhabricatorProjectTrigger $trigger, + array $rule_list, $form_id, $table_id, $create_id, $input_id) { - $rule_list = $trigger->getTriggerRules(); $rule_list = mpull($rule_list, 'toDictionary'); $rule_list = array_values($rule_list); diff --git a/src/applications/project/storage/PhabricatorProjectTrigger.php b/src/applications/project/storage/PhabricatorProjectTrigger.php index b02e1ce107..a195e9fc44 100644 --- a/src/applications/project/storage/PhabricatorProjectTrigger.php +++ b/src/applications/project/storage/PhabricatorProjectTrigger.php @@ -62,107 +62,19 @@ final class PhabricatorProjectTrigger return pht('Trigger %d', $this->getID()); } + public function setRuleset(array $ruleset) { + // Clear any cached trigger rules, since we're changing the ruleset + // for the trigger. + $this->triggerRules = null; + + parent::setRuleset($ruleset); + } + public function getTriggerRules() { if ($this->triggerRules === null) { - - // TODO: Temporary hard-coded rule specification. - $rule_specifications = array( - array( - 'type' => 'status', - 'value' => ManiphestTaskStatus::getDefaultClosedStatus(), - ), - // This is an intentionally unknown rule. - array( - 'type' => 'quack', - 'value' => 'aaa', - ), - // This is an intentionally invalid rule. - array( - 'type' => 'status', - 'value' => 'quack', - ), - ); - - // NOTE: We're trying to preserve the database state in the rule - // structure, even if it includes rule types we don't have implementations - // for, or rules with invalid rule values. - - // If an administrator adds or removes extensions which add rules, or - // an upgrade affects rule validity, existing rules may become invalid. - // When they do, we still want the UI to reflect the ruleset state - // accurately and "Edit" + "Save" shouldn't destroy data unless the - // user explicitly modifies the ruleset. - - // When we run into rules which are structured correctly but which have - // types we don't know about, we replace them with "Unknown Rules". If - // we know about the type of a rule but the value doesn't validate, we - // replace it with "Invalid Rules". These two rule types don't take any - // actions when a card is dropped into the column, but they show the user - // what's wrong with the ruleset and can be saved without causing any - // collateral damage. - - $rule_map = PhabricatorProjectTriggerRule::getAllTriggerRules(); - - // If the stored rule data isn't a list of rules (or we encounter other - // fundamental structural problems, below), there isn't much we can do - // to try to represent the state. - if (!is_array($rule_specifications)) { - throw new PhabricatorProjectTriggerCorruptionException( - pht( - 'Trigger ("%s") has a corrupt ruleset: expected a list of '. - 'rule specifications, found "%s".', - $this->getPHID(), - phutil_describe_type($rule_specifications))); - } - - $trigger_rules = array(); - foreach ($rule_specifications as $key => $rule) { - if (!is_array($rule)) { - throw new PhabricatorProjectTriggerCorruptionException( - pht( - 'Trigger ("%s") has a corrupt ruleset: rule (with key "%s") '. - 'should be a rule specification, but is actually "%s".', - $this->getPHID(), - $key, - phutil_describe_type($rule))); - } - - try { - PhutilTypeSpec::checkMap( - $rule, - array( - 'type' => 'string', - 'value' => 'wild', - )); - } catch (PhutilTypeCheckException $ex) { - throw new PhabricatorProjectTriggerCorruptionException( - pht( - 'Trigger ("%s") has a corrupt ruleset: rule (with key "%s") '. - 'is not a valid rule specification: %s', - $this->getPHID(), - $key, - $ex->getMessage())); - } - - $record = id(new PhabricatorProjectTriggerRuleRecord()) - ->setType(idx($rule, 'type')) - ->setValue(idx($rule, 'value')); - - if (!isset($rule_map[$record->getType()])) { - $rule = new PhabricatorProjectTriggerUnknownRule(); - } else { - $rule = clone $rule_map[$record->getType()]; - } - - try { - $rule->setRecord($record); - } catch (Exception $ex) { - $rule = id(new PhabricatorProjectTriggerInvalidRule()) - ->setRecord($record); - } - - $trigger_rules[] = $rule; - } + $trigger_rules = self::newTriggerRulesFromRuleSpecifications( + $this->getRuleset(), + $allow_invalid = true); $this->triggerRules = $trigger_rules; } @@ -170,6 +82,108 @@ final class PhabricatorProjectTrigger return $this->triggerRules; } + public static function newTriggerRulesFromRuleSpecifications( + array $list, + $allow_invalid) { + + // NOTE: With "$allow_invalid" set, we're trying to preserve the database + // state in the rule structure, even if it includes rule types we don't + // ha ve implementations for, or rules with invalid rule values. + + // If an administrator adds or removes extensions which add rules, or + // an upgrade affects rule validity, existing rules may become invalid. + // When they do, we still want the UI to reflect the ruleset state + // accurately and "Edit" + "Save" shouldn't destroy data unless the + // user explicitly modifies the ruleset. + + // In this mode, when we run into rules which are structured correctly but + // which have types we don't know about, we replace them with "Unknown + // Rules". If we know about the type of a rule but the value doesn't + // validate, we replace it with "Invalid Rules". These two rule types don't + // take any actions when a card is dropped into the column, but they show + // the user what's wrong with the ruleset and can be saved without causing + // any collateral damage. + + $rule_map = PhabricatorProjectTriggerRule::getAllTriggerRules(); + + // If the stored rule data isn't a list of rules (or we encounter other + // fundamental structural problems, below), there isn't much we can do + // to try to represent the state. + if (!is_array($list)) { + throw new PhabricatorProjectTriggerCorruptionException( + pht( + 'Trigger ruleset is corrupt: expected a list of rule '. + 'specifications, found "%s".', + phutil_describe_type($list))); + } + + $trigger_rules = array(); + foreach ($list as $key => $rule) { + if (!is_array($rule)) { + throw new PhabricatorProjectTriggerCorruptionException( + pht( + 'Trigger ruleset is corrupt: rule (with key "%s") should be a '. + 'rule specification, but is actually "%s".', + $key, + phutil_describe_type($rule))); + } + + try { + PhutilTypeSpec::checkMap( + $rule, + array( + 'type' => 'string', + 'value' => 'wild', + )); + } catch (PhutilTypeCheckException $ex) { + throw new PhabricatorProjectTriggerCorruptionException( + pht( + 'Trigger ruleset is corrupt: rule (with key "%s") is not a '. + 'valid rule specification: %s', + $key, + $ex->getMessage())); + } + + $record = id(new PhabricatorProjectTriggerRuleRecord()) + ->setType(idx($rule, 'type')) + ->setValue(idx($rule, 'value')); + + if (!isset($rule_map[$record->getType()])) { + if (!$allow_invalid) { + throw new PhabricatorProjectTriggerCorruptionException( + pht( + 'Trigger ruleset is corrupt: rule type "%s" is unknown.', + $record->getType())); + } + + $rule = new PhabricatorProjectTriggerUnknownRule(); + } else { + $rule = clone $rule_map[$record->getType()]; + } + + try { + $rule->setRecord($record); + } catch (Exception $ex) { + if (!$allow_invalid) { + throw new PhabricatorProjectTriggerCorruptionException( + pht( + 'Trigger ruleset is corrupt, rule (of type "%s") does not '. + 'validate: %s', + $record->getType(), + $ex->getMessage())); + } + + $rule = id(new PhabricatorProjectTriggerInvalidRule()) + ->setRecord($record); + } + + $trigger_rules[] = $rule; + } + + return $trigger_rules; + } + + public function getDropEffects() { $effects = array(); diff --git a/src/applications/project/xaction/trigger/PhabricatorProjectTriggerRulesetTransaction.php b/src/applications/project/xaction/trigger/PhabricatorProjectTriggerRulesetTransaction.php new file mode 100644 index 0000000000..59c846becf --- /dev/null +++ b/src/applications/project/xaction/trigger/PhabricatorProjectTriggerRulesetTransaction.php @@ -0,0 +1,65 @@ +getRuleset(); + } + + public function applyInternalEffects($object, $value) { + $object->setRuleset($value); + } + + public function getTitle() { + return pht( + '%s updated the ruleset for this trigger.', + $this->renderAuthor()); + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + foreach ($xactions as $xaction) { + $ruleset = $xaction->getNewValue(); + + try { + PhabricatorProjectTrigger::newTriggerRulesFromRuleSpecifications( + $ruleset, + $allow_invalid = false); + } catch (PhabricatorProjectTriggerCorruptionException $ex) { + $errors[] = $this->newInvalidError( + pht( + 'Ruleset specification is not valid. %s', + $ex->getMessage()), + $xaction); + continue; + } + } + + return $errors; + } + + public function hasChangeDetailView() { + return true; + } + + public function newChangeDetailView() { + $viewer = $this->getViewer(); + + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + $json = new PhutilJSON(); + $old_json = $json->encodeAsList($old); + $new_json = $json->encodeAsList($new); + + return id(new PhabricatorApplicationTransactionTextDiffDetailView()) + ->setViewer($viewer) + ->setOldText($old_json) + ->setNewText($new_json); + } + +} From 614f39b806790d6298e0a0b3d11aac95f7a8d8bc Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 21 Mar 2019 12:27:14 -0700 Subject: [PATCH 199/245] Show a trigger rule summary on the rule view page Summary: Ref T5474. When you view the main page for a rule, show what the rule does before you actually edit it. Test Plan: Viewed a real trigger, then faked invalid/unknown rules: {F6300211} {F6300212} {F6300213} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T5474 Differential Revision: https://secure.phabricator.com/D20303 --- ...habricatorProjectTriggerViewController.php | 44 +++++++++++++++++++ .../storage/PhabricatorProjectTrigger.php | 3 +- .../PhabricatorProjectTriggerInvalidRule.php | 37 ++++++++++++++++ ...catorProjectTriggerManiphestStatusRule.php | 21 +++++++++ .../trigger/PhabricatorProjectTriggerRule.php | 3 ++ .../PhabricatorProjectTriggerUnknownRule.php | 16 +++++++ 6 files changed, 123 insertions(+), 1 deletion(-) diff --git a/src/applications/project/controller/trigger/PhabricatorProjectTriggerViewController.php b/src/applications/project/controller/trigger/PhabricatorProjectTriggerViewController.php index d1966cf106..2750adc2ea 100644 --- a/src/applications/project/controller/trigger/PhabricatorProjectTriggerViewController.php +++ b/src/applications/project/controller/trigger/PhabricatorProjectTriggerViewController.php @@ -21,6 +21,7 @@ final class PhabricatorProjectTriggerViewController return new Aphront404Response(); } + $rules_view = $this->newRulesView($trigger); $columns_view = $this->newColumnsView($trigger); $title = $trigger->getObjectName(); @@ -40,6 +41,7 @@ final class PhabricatorProjectTriggerViewController ->setCurtain($curtain) ->setMainColumn( array( + $rules_view, $columns_view, $timeline, )); @@ -139,6 +141,48 @@ final class PhabricatorProjectTriggerViewController ->setTable($table_view); } + private function newRulesView(PhabricatorProjectTrigger $trigger) { + $viewer = $this->getViewer(); + $rules = $trigger->getTriggerRules(); + + $rows = array(); + foreach ($rules as $rule) { + $value = $rule->getRecord()->getValue(); + + $rows[] = array( + $rule->getRuleViewIcon($value), + $rule->getRuleViewLabel(), + $rule->getRuleViewDescription($value), + ); + } + + $table_view = id(new AphrontTableView($rows)) + ->setNoDataString(pht('This trigger has no rules.')) + ->setHeaders( + array( + null, + pht('Rule'), + pht('Action'), + )) + ->setColumnClasses( + array( + null, + 'pri', + 'wide', + )); + + $header_view = id(new PHUIHeaderView()) + ->setHeader(pht('Trigger Rules')) + ->setSubheader( + pht( + 'When a card is dropped into a column that uses this trigger, '. + 'these actions will be taken.')); + + return id(new PHUIObjectBoxView()) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setHeader($header_view) + ->setTable($table_view); + } private function newCurtain(PhabricatorProjectTrigger $trigger) { $viewer = $this->getViewer(); diff --git a/src/applications/project/storage/PhabricatorProjectTrigger.php b/src/applications/project/storage/PhabricatorProjectTrigger.php index a195e9fc44..5029c2caea 100644 --- a/src/applications/project/storage/PhabricatorProjectTrigger.php +++ b/src/applications/project/storage/PhabricatorProjectTrigger.php @@ -174,7 +174,8 @@ final class PhabricatorProjectTrigger } $rule = id(new PhabricatorProjectTriggerInvalidRule()) - ->setRecord($record); + ->setRecord($record) + ->setException($ex); } $trigger_rules[] = $rule; diff --git a/src/applications/project/trigger/PhabricatorProjectTriggerInvalidRule.php b/src/applications/project/trigger/PhabricatorProjectTriggerInvalidRule.php index 0f9fe52abb..184d818aa5 100644 --- a/src/applications/project/trigger/PhabricatorProjectTriggerInvalidRule.php +++ b/src/applications/project/trigger/PhabricatorProjectTriggerInvalidRule.php @@ -5,6 +5,17 @@ final class PhabricatorProjectTriggerInvalidRule const TRIGGERTYPE = 'invalid'; + private $exception; + + public function setException(Exception $exception) { + $this->exception = $exception; + return $this; + } + + public function getException() { + return $this->exception; + } + public function getDescription() { return pht( 'Invalid rule (of type "%s").', @@ -59,4 +70,30 @@ final class PhabricatorProjectTriggerInvalidRule return null; } + public function getRuleViewLabel() { + return pht('Invalid Rule'); + } + + public function getRuleViewDescription($value) { + $record = $this->getRecord(); + $type = $record->getType(); + + $exception = $this->getException(); + if ($exception) { + return pht( + 'This rule (of type "%s") is invalid: %s', + $type, + $exception->getMessage()); + } else { + return pht( + 'This rule (of type "%s") is invalid.', + $type); + } + } + + public function getRuleViewIcon($value) { + return id(new PHUIIconView()) + ->setIcon('fa-exclamation-triangle', 'red'); + } + } diff --git a/src/applications/project/trigger/PhabricatorProjectTriggerManiphestStatusRule.php b/src/applications/project/trigger/PhabricatorProjectTriggerManiphestStatusRule.php index 2c40563884..5b1ad2db36 100644 --- a/src/applications/project/trigger/PhabricatorProjectTriggerManiphestStatusRule.php +++ b/src/applications/project/trigger/PhabricatorProjectTriggerManiphestStatusRule.php @@ -77,4 +77,25 @@ final class PhabricatorProjectTriggerManiphestStatusRule ); } + public function getRuleViewLabel() { + return pht('Change Status'); + } + + public function getRuleViewDescription($value) { + $status_name = ManiphestTaskStatus::getTaskStatusName($value); + + return pht( + 'Change task status to %s.', + phutil_tag('strong', array(), $status_name)); + } + + public function getRuleViewIcon($value) { + $status_icon = ManiphestTaskStatus::getStatusIcon($value); + $status_color = ManiphestTaskStatus::getStatusColor($value); + + return id(new PHUIIconView()) + ->setIcon($status_icon, $status_color); + } + + } diff --git a/src/applications/project/trigger/PhabricatorProjectTriggerRule.php b/src/applications/project/trigger/PhabricatorProjectTriggerRule.php index 9634086235..49fdbf8a93 100644 --- a/src/applications/project/trigger/PhabricatorProjectTriggerRule.php +++ b/src/applications/project/trigger/PhabricatorProjectTriggerRule.php @@ -39,6 +39,9 @@ abstract class PhabricatorProjectTriggerRule abstract public function getDescription(); abstract public function getSelectControlName(); + abstract public function getRuleViewLabel(); + abstract public function getRuleViewDescription($value); + abstract public function getRuleViewIcon($value); abstract protected function assertValidRuleValue($value); abstract protected function newDropTransactions($object, $value); abstract protected function newDropEffects($value); diff --git a/src/applications/project/trigger/PhabricatorProjectTriggerUnknownRule.php b/src/applications/project/trigger/PhabricatorProjectTriggerUnknownRule.php index 008092061d..f71ee44ad7 100644 --- a/src/applications/project/trigger/PhabricatorProjectTriggerUnknownRule.php +++ b/src/applications/project/trigger/PhabricatorProjectTriggerUnknownRule.php @@ -58,4 +58,20 @@ final class PhabricatorProjectTriggerUnknownRule return null; } + public function getRuleViewLabel() { + return pht('Unknown Rule'); + } + + public function getRuleViewDescription($value) { + return pht( + 'This is an unknown rule of type "%s". An administrator may have '. + 'edited or removed an extension which implements this rule type.', + $this->getRecord()->getType()); + } + + public function getRuleViewIcon($value) { + return id(new PHUIIconView()) + ->setIcon('fa-question-circle', 'yellow'); + } + } From 1277db9452585988ad19abc7b883c7387285e4d1 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 21 Mar 2019 13:19:12 -0700 Subject: [PATCH 200/245] When users hover over a column trigger menu, show a "preview" with the rules instead of a tooltip Summary: Ref T5474. The first rough cut of triggers showed some of the trigger rules in a tooltip when you hover over the "add/remove" trigger menu. This isn't great since we don't have much room and it's a bit finnicky / hard to read. Since we have a better way to show effects now in the drop preview, just use that instead. When you hover over the trigger menu, preview the trigger in the "drop effect" element, with a "Trigger: such-and-such" header. Test Plan: - This is pretty tough to screenshot. - Hovered over menu, got a sensible preview of the trigger effects. - Dragged a card over the menu, no preview. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T5474 Differential Revision: https://secure.phabricator.com/D20304 --- resources/celerity/map.php | 74 +++++++-------- .../PhabricatorProjectBoardViewController.php | 33 +++---- .../engine/PhabricatorBoardResponseEngine.php | 14 ++- .../icon/PhabricatorProjectDropEffect.php | 22 +++++ .../storage/PhabricatorProjectTrigger.php | 39 ++------ .../PhabricatorProjectTriggerInvalidRule.php | 6 -- ...catorProjectTriggerManiphestStatusRule.php | 8 -- .../trigger/PhabricatorProjectTriggerRule.php | 4 +- .../PhabricatorProjectTriggerUnknownRule.php | 6 -- .../css/phui/workboards/phui-workpanel.css | 11 +++ .../js/application/projects/WorkboardBoard.js | 95 +++++++++++++++++-- .../application/projects/WorkboardColumn.js | 4 + .../projects/WorkboardDropEffect.js | 12 ++- .../projects/behavior-project-boards.js | 6 ++ 14 files changed, 215 insertions(+), 119 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index c12e141770..ffb8209f41 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -179,7 +179,7 @@ return array( 'rsrc/css/phui/workboards/phui-workboard-color.css' => 'e86de308', 'rsrc/css/phui/workboards/phui-workboard.css' => '74fc9d98', 'rsrc/css/phui/workboards/phui-workcard.css' => '9e9eb0df', - 'rsrc/css/phui/workboards/phui-workpanel.css' => 'e5461a51', + 'rsrc/css/phui/workboards/phui-workpanel.css' => '4e4ec9f0', 'rsrc/css/sprite-login.css' => '18b368a6', 'rsrc/css/sprite-tokens.css' => 'f1896dc5', 'rsrc/css/syntax/syntax-default.css' => '055fc231', @@ -409,16 +409,16 @@ return array( 'rsrc/js/application/phortune/phortune-credit-card-form.js' => 'd12d214f', 'rsrc/js/application/policy/behavior-policy-control.js' => '0eaa33a9', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '9347f172', - 'rsrc/js/application/projects/WorkboardBoard.js' => '2f893acd', + 'rsrc/js/application/projects/WorkboardBoard.js' => '31766c31', 'rsrc/js/application/projects/WorkboardCard.js' => '0392a5d8', 'rsrc/js/application/projects/WorkboardCardTemplate.js' => '2a61f8d4', - 'rsrc/js/application/projects/WorkboardColumn.js' => 'c344eb3c', + 'rsrc/js/application/projects/WorkboardColumn.js' => 'c3d24e63', 'rsrc/js/application/projects/WorkboardController.js' => '42c7a5a7', - 'rsrc/js/application/projects/WorkboardDropEffect.js' => 'c808589e', + 'rsrc/js/application/projects/WorkboardDropEffect.js' => '8e0aa661', 'rsrc/js/application/projects/WorkboardHeader.js' => '111bfd2d', 'rsrc/js/application/projects/WorkboardHeaderTemplate.js' => 'ebe83a6b', 'rsrc/js/application/projects/WorkboardOrderTemplate.js' => '03e8891f', - 'rsrc/js/application/projects/behavior-project-boards.js' => 'cd7c9d4f', + 'rsrc/js/application/projects/behavior-project-boards.js' => '8512e4ea', 'rsrc/js/application/projects/behavior-project-create.js' => '34c53422', 'rsrc/js/application/projects/behavior-reorder-columns.js' => '8ac32fd9', 'rsrc/js/application/releeph/releeph-preview-branch.js' => '75184d68', @@ -664,7 +664,7 @@ return array( 'javelin-behavior-phuix-example' => 'c2c500a7', 'javelin-behavior-policy-control' => '0eaa33a9', 'javelin-behavior-policy-rule-editor' => '9347f172', - 'javelin-behavior-project-boards' => 'cd7c9d4f', + 'javelin-behavior-project-boards' => '8512e4ea', 'javelin-behavior-project-create' => '34c53422', 'javelin-behavior-quicksand-blacklist' => '5a6f6a06', 'javelin-behavior-read-only-warning' => 'b9109f8f', @@ -737,12 +737,12 @@ return array( 'javelin-view-renderer' => '9aae2b66', 'javelin-view-visitor' => '308f9fe4', 'javelin-websocket' => 'fdc13e4e', - 'javelin-workboard-board' => '2f893acd', + 'javelin-workboard-board' => '31766c31', 'javelin-workboard-card' => '0392a5d8', 'javelin-workboard-card-template' => '2a61f8d4', - 'javelin-workboard-column' => 'c344eb3c', + 'javelin-workboard-column' => 'c3d24e63', 'javelin-workboard-controller' => '42c7a5a7', - 'javelin-workboard-drop-effect' => 'c808589e', + 'javelin-workboard-drop-effect' => '8e0aa661', 'javelin-workboard-header' => '111bfd2d', 'javelin-workboard-header-template' => 'ebe83a6b', 'javelin-workboard-order-template' => '03e8891f', @@ -869,7 +869,7 @@ return array( 'phui-workboard-color-css' => 'e86de308', 'phui-workboard-view-css' => '74fc9d98', 'phui-workcard-view-css' => '9e9eb0df', - 'phui-workpanel-view-css' => 'e5461a51', + 'phui-workpanel-view-css' => '4e4ec9f0', 'phuix-action-list-view' => 'c68f183f', 'phuix-action-view' => 'aaa08f3b', 'phuix-autocomplete' => '8f139ef0', @@ -1178,7 +1178,11 @@ return array( 'phuix-autocomplete', 'javelin-mask', ), - '2f893acd' => array( + '308f9fe4' => array( + 'javelin-install', + 'javelin-util', + ), + '31766c31' => array( 'javelin-install', 'javelin-dom', 'javelin-util', @@ -1190,10 +1194,6 @@ return array( 'javelin-workboard-card-template', 'javelin-workboard-order-template', ), - '308f9fe4' => array( - 'javelin-install', - 'javelin-util', - ), '32755edb' => array( 'javelin-install', 'javelin-util', @@ -1351,6 +1351,9 @@ return array( 'phuix-icon-view', 'javelin-behavior-phabricator-gesture', ), + '4e4ec9f0' => array( + 'phui-workcard-view-css', + ), '4e61fa88' => array( 'javelin-behavior', 'javelin-aphlict', @@ -1591,6 +1594,16 @@ return array( 'javelin-dom', 'javelin-vector', ), + '8512e4ea' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-util', + 'javelin-vector', + 'javelin-stratcom', + 'javelin-workflow', + 'javelin-workboard-controller', + 'javelin-workboard-drop-effect', + ), '87428eb2' => array( 'javelin-behavior', 'javelin-diffusion-locate-file-source', @@ -1636,6 +1649,10 @@ return array( 'phabricator-shaped-request', 'conpherence-thread-manager', ), + '8e0aa661' => array( + 'javelin-install', + 'javelin-dom', + ), '8e2d9a28' => array( 'phui-theme-css', ), @@ -1940,17 +1957,17 @@ return array( 'javelin-dom', 'phuix-button-view', ), - 'c344eb3c' => array( - 'javelin-install', - 'javelin-workboard-card', - 'javelin-workboard-header', - ), 'c3703a16' => array( 'javelin-behavior', 'javelin-aphlict', 'phabricator-phtize', 'javelin-dom', ), + 'c3d24e63' => array( + 'javelin-install', + 'javelin-workboard-card', + 'javelin-workboard-header', + ), 'c687e867' => array( 'javelin-behavior', 'javelin-dom', @@ -1979,10 +1996,6 @@ return array( 'phuix-icon-view', 'phabricator-busy', ), - 'c808589e' => array( - 'javelin-install', - 'javelin-dom', - ), 'c8147a20' => array( 'javelin-behavior', 'javelin-dom', @@ -2002,16 +2015,6 @@ return array( 'javelin-vector', 'javelin-magical-init', ), - 'cd7c9d4f' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-util', - 'javelin-vector', - 'javelin-stratcom', - 'javelin-workflow', - 'javelin-workboard-controller', - 'javelin-workboard-drop-effect', - ), 'cf32921f' => array( 'javelin-behavior', 'javelin-dom', @@ -2072,9 +2075,6 @@ return array( 'javelin-dom', 'javelin-history', ), - 'e5461a51' => array( - 'phui-workcard-view-css', - ), 'e562708c' => array( 'javelin-install', ), diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index 6d0c8721fc..4e4ff81b4e 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -623,10 +623,20 @@ final class PhabricatorProjectBoardViewController $drop_effects = $column->getDropEffects(); $drop_effects = mpull($drop_effects, 'toDictionary'); + $preview_effect = null; + if ($column->canHaveTrigger()) { + $trigger = $column->getTrigger(); + if ($trigger) { + $preview_effect = $trigger->getPreviewEffect() + ->toDictionary(); + } + } + $column_templates[] = array( 'columnPHID' => $column_phid, 'effects' => $drop_effects, 'cardPHIDs' => $card_phids, + 'triggerPreviewEffect' => $preview_effect, ); } @@ -652,12 +662,8 @@ final class PhabricatorProjectBoardViewController $properties = array(); foreach ($all_tasks as $task) { - $properties[$task->getPHID()] = array( - 'points' => (double)$task->getPoints(), - 'status' => $task->getStatus(), - 'priority' => (int)$task->getPriority(), - 'owner' => $task->getOwnerPHID(), - ); + $properties[$task->getPHID()] = + PhabricatorBoardResponseEngine::newTaskProperties($task); } $behavior_config = array( @@ -1263,26 +1269,15 @@ final class PhabricatorProjectBoardViewController $trigger_icon = 'fa-cogs grey'; } - if ($trigger) { - $trigger_tip = array( - pht('%s: %s', $trigger->getObjectName(), $trigger->getDisplayName()), - $trigger->getRulesDescription(), - ); - $trigger_tip = implode("\n", $trigger_tip); - } else { - $trigger_tip = pht('No column trigger.'); - } - $trigger_button = id(new PHUIIconView()) ->setIcon($trigger_icon) ->setHref('#') ->addSigil('boards-dropdown-menu') - ->addSigil('has-tooltip') + ->addSigil('trigger-preview') ->setMetadata( array( 'items' => hsprintf('%s', $trigger_menu), - 'tip' => $trigger_tip, - 'size' => 300, + 'columnPHID' => $column->getPHID(), )); return $trigger_button; diff --git a/src/applications/project/engine/PhabricatorBoardResponseEngine.php b/src/applications/project/engine/PhabricatorBoardResponseEngine.php index 36c5e81150..fb5299a857 100644 --- a/src/applications/project/engine/PhabricatorBoardResponseEngine.php +++ b/src/applications/project/engine/PhabricatorBoardResponseEngine.php @@ -131,10 +131,7 @@ final class PhabricatorBoardResponseEngine extends Phobject { $card['headers'][$order_key] = $header; } - $card['properties'] = array( - 'points' => (double)$object->getPoints(), - 'status' => $object->getStatus(), - ); + $card['properties'] = self::newTaskProperties($object); } if ($card_phid === $object_phid) { @@ -159,6 +156,15 @@ final class PhabricatorBoardResponseEngine extends Phobject { ->setContent($payload); } + public static function newTaskProperties($task) { + return array( + 'points' => (double)$task->getPoints(), + 'status' => $task->getStatus(), + 'priority' => (int)$task->getPriority(), + 'owner' => $task->getOwnerPHID(), + ); + } + private function buildTemplate($object) { $viewer = $this->getViewer(); $object_phid = $this->getObjectPHID(); diff --git a/src/applications/project/icon/PhabricatorProjectDropEffect.php b/src/applications/project/icon/PhabricatorProjectDropEffect.php index 8e68261fa4..3d61f9bcef 100644 --- a/src/applications/project/icon/PhabricatorProjectDropEffect.php +++ b/src/applications/project/icon/PhabricatorProjectDropEffect.php @@ -7,6 +7,8 @@ final class PhabricatorProjectDropEffect private $color; private $content; private $conditions = array(); + private $isTriggerEffect; + private $isHeader; public function setIcon($icon) { $this->icon = $icon; @@ -40,6 +42,8 @@ final class PhabricatorProjectDropEffect 'icon' => $this->getIcon(), 'color' => $this->getColor(), 'content' => hsprintf('%s', $this->getContent()), + 'isTriggerEffect' => $this->getIsTriggerEffect(), + 'isHeader' => $this->getIsHeader(), 'conditions' => $this->getConditions(), ); } @@ -58,4 +62,22 @@ final class PhabricatorProjectDropEffect return $this->conditions; } + public function setIsTriggerEffect($is_trigger_effect) { + $this->isTriggerEffect = $is_trigger_effect; + return $this; + } + + public function getIsTriggerEffect() { + return $this->isTriggerEffect; + } + + public function setIsHeader($is_header) { + $this->isHeader = $is_header; + return $this; + } + + public function getIsHeader() { + return $this->isHeader; + } + } diff --git a/src/applications/project/storage/PhabricatorProjectTrigger.php b/src/applications/project/storage/PhabricatorProjectTrigger.php index 5029c2caea..a499f0bcf8 100644 --- a/src/applications/project/storage/PhabricatorProjectTrigger.php +++ b/src/applications/project/storage/PhabricatorProjectTrigger.php @@ -198,36 +198,6 @@ final class PhabricatorProjectTrigger return $effects; } - public function getRulesDescription() { - $rules = $this->getTriggerRules(); - if (!$rules) { - return pht('Does nothing.'); - } - - $things = array(); - - $count = count($rules); - $limit = 3; - - if ($count > $limit) { - $show_rules = array_slice($rules, 0, ($limit - 1)); - } else { - $show_rules = $rules; - } - - foreach ($show_rules as $rule) { - $things[] = $rule->getDescription(); - } - - if ($count > $limit) { - $things[] = pht( - '(Applies %s more actions.)', - new PhutilNumber($count - $limit)); - } - - return implode("\n", $things); - } - public function newDropTransactions( PhabricatorUser $viewer, PhabricatorProjectColumn $column, @@ -265,6 +235,15 @@ final class PhabricatorProjectTrigger return $trigger_xactions; } + public function getPreviewEffect() { + $header = pht('Trigger: %s', $this->getDisplayName()); + + return id(new PhabricatorProjectDropEffect()) + ->setIcon('fa-cogs') + ->setColor('blue') + ->setIsHeader(true) + ->setContent($header); + } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ diff --git a/src/applications/project/trigger/PhabricatorProjectTriggerInvalidRule.php b/src/applications/project/trigger/PhabricatorProjectTriggerInvalidRule.php index 184d818aa5..ba53b77e75 100644 --- a/src/applications/project/trigger/PhabricatorProjectTriggerInvalidRule.php +++ b/src/applications/project/trigger/PhabricatorProjectTriggerInvalidRule.php @@ -16,12 +16,6 @@ final class PhabricatorProjectTriggerInvalidRule return $this->exception; } - public function getDescription() { - return pht( - 'Invalid rule (of type "%s").', - $this->getRecord()->getType()); - } - public function getSelectControlName() { return pht('(Invalid Rule)'); } diff --git a/src/applications/project/trigger/PhabricatorProjectTriggerManiphestStatusRule.php b/src/applications/project/trigger/PhabricatorProjectTriggerManiphestStatusRule.php index 5b1ad2db36..b11d7567de 100644 --- a/src/applications/project/trigger/PhabricatorProjectTriggerManiphestStatusRule.php +++ b/src/applications/project/trigger/PhabricatorProjectTriggerManiphestStatusRule.php @@ -5,14 +5,6 @@ final class PhabricatorProjectTriggerManiphestStatusRule const TRIGGERTYPE = 'task.status'; - public function getDescription() { - $value = $this->getValue(); - - return pht( - 'Changes status to "%s".', - ManiphestTaskStatus::getTaskStatusName($value)); - } - public function getSelectControlName() { return pht('Change status to'); } diff --git a/src/applications/project/trigger/PhabricatorProjectTriggerRule.php b/src/applications/project/trigger/PhabricatorProjectTriggerRule.php index 49fdbf8a93..c75c15a1ab 100644 --- a/src/applications/project/trigger/PhabricatorProjectTriggerRule.php +++ b/src/applications/project/trigger/PhabricatorProjectTriggerRule.php @@ -37,7 +37,6 @@ abstract class PhabricatorProjectTriggerRule return $this->getRecord()->getValue(); } - abstract public function getDescription(); abstract public function getSelectControlName(); abstract public function getRuleViewLabel(); abstract public function getRuleViewDescription($value); @@ -111,7 +110,8 @@ abstract class PhabricatorProjectTriggerRule } final protected function newEffect() { - return new PhabricatorProjectDropEffect(); + return id(new PhabricatorProjectDropEffect()) + ->setIsTriggerEffect(true); } final public function toDictionary() { diff --git a/src/applications/project/trigger/PhabricatorProjectTriggerUnknownRule.php b/src/applications/project/trigger/PhabricatorProjectTriggerUnknownRule.php index f71ee44ad7..925a369bae 100644 --- a/src/applications/project/trigger/PhabricatorProjectTriggerUnknownRule.php +++ b/src/applications/project/trigger/PhabricatorProjectTriggerUnknownRule.php @@ -5,12 +5,6 @@ final class PhabricatorProjectTriggerUnknownRule const TRIGGERTYPE = 'unknown'; - public function getDescription() { - return pht( - 'Unknown rule (of type "%s").', - $this->getRecord()->getType()); - } - public function getSelectControlName() { return pht('(Unknown Rule)'); } diff --git a/webroot/rsrc/css/phui/workboards/phui-workpanel.css b/webroot/rsrc/css/phui/workboards/phui-workpanel.css index ce0e7885a8..97600ff5e6 100644 --- a/webroot/rsrc/css/phui/workboards/phui-workpanel.css +++ b/webroot/rsrc/css/phui/workboards/phui-workpanel.css @@ -201,6 +201,7 @@ text-overflow: ellipsis; margin: 4px 8px; color: {$greytext}; + border-radius: 3px; } .workboard-drop-preview li .phui-icon-view { @@ -214,3 +215,13 @@ background: {$bluebackground}; margin-right: 6px; } + +.workboard-drop-preview .workboard-drop-preview-header { + background: {$sky}; + color: #fff; +} + +.workboard-drop-preview .workboard-drop-preview-header .phui-icon-view { + background: {$blue}; + color: #fff; +} diff --git a/webroot/rsrc/js/application/projects/WorkboardBoard.js b/webroot/rsrc/js/application/projects/WorkboardBoard.js index 5a55a2d905..f96e82fb8b 100644 --- a/webroot/rsrc/js/application/projects/WorkboardBoard.js +++ b/webroot/rsrc/js/application/projects/WorkboardBoard.js @@ -41,6 +41,8 @@ JX.install('WorkboardBoard', { _cards: null, _dropPreviewNode: null, _dropPreviewListNode: null, + _previewPHID: null, + _hidePreivew: false, getRoot: function() { return this._root; @@ -141,6 +143,82 @@ JX.install('WorkboardBoard', { this._columns[phid] = new JX.WorkboardColumn(this, phid, node); } + + var on_over = JX.bind(this, this._showTriggerPreview); + var on_out = JX.bind(this, this._hideTriggerPreview); + JX.Stratcom.listen('mouseover', 'trigger-preview', on_over); + JX.Stratcom.listen('mouseout', 'trigger-preview', on_out); + }, + + _showTriggerPreview: function(e) { + if (this._disablePreview) { + return; + } + + var target = e.getTarget(); + var node = e.getNode('trigger-preview'); + + if (target !== node) { + return; + } + + var phid = JX.Stratcom.getData(node).columnPHID; + var column = this._columns[phid]; + + // Bail out if we don't know anything about this column. + if (!column) { + return; + } + + if (phid === this._previewPHID) { + return; + } + + this._previewPHID = phid; + + var effects = column.getDropEffects(); + + var triggers = []; + for (var ii = 0; ii < effects.length; ii++) { + if (effects[ii].getIsTriggerEffect()) { + triggers.push(effects[ii]); + } + } + + if (triggers.length) { + var header = column.getTriggerPreviewEffect(); + triggers = [header].concat(triggers); + } + + this._showEffects(triggers); + }, + + _hideTriggerPreview: function(e) { + if (this._disablePreview) { + return; + } + + var target = e.getTarget(); + + if (target !== e.getNode('trigger-preview')) { + return; + } + + this._removeTriggerPreview(); + }, + + _removeTriggerPreview: function() { + this._showEffects([]); + this._previewPHID = null; + }, + + _beginDrag: function() { + this._disablePreview = true; + this._showEffects([]); + }, + + _endDrag: function() { + this._disablePreview = false; }, _setupDragHandlers: function() { @@ -186,6 +264,9 @@ JX.install('WorkboardBoard', { list.listen('didDrop', JX.bind(this, this._onmovecard, list)); + list.listen('didBeginDrag', JX.bind(this, this._beginDrag)); + list.listen('didEndDrag', JX.bind(this, this._endDrag)); + lists.push(list); } @@ -195,18 +276,16 @@ JX.install('WorkboardBoard', { }, _didChangeDropTarget: function(src_list, src_node, dst_list, dst_node) { - var node = this._getDropPreviewNode(); - if (!dst_list) { // The card is being dragged into a dead area, like the left menu. - JX.DOM.remove(node); + this._showEffects([]); return; } if (dst_node === false) { // The card is being dragged over itself, so dropping it won't // affect anything. - JX.DOM.remove(node); + this._showEffects([]); return; } @@ -217,7 +296,6 @@ JX.install('WorkboardBoard', { var dst_column = this.getColumn(dst_phid); var effects = []; - if (src_column !== dst_column) { effects = effects.concat(dst_column.getDropEffects()); } @@ -239,6 +317,12 @@ JX.install('WorkboardBoard', { } effects = visible; + this._showEffects(effects); + }, + + _showEffects: function(effects) { + var node = this._getDropPreviewNode(); + if (!effects.length) { JX.DOM.remove(node); return; @@ -251,7 +335,6 @@ JX.install('WorkboardBoard', { } JX.DOM.setContent(this._getDropPreviewListNode(), items); - document.body.appendChild(node); }, diff --git a/webroot/rsrc/js/application/projects/WorkboardColumn.js b/webroot/rsrc/js/application/projects/WorkboardColumn.js index 593afea776..a9bf0f8cc5 100644 --- a/webroot/rsrc/js/application/projects/WorkboardColumn.js +++ b/webroot/rsrc/js/application/projects/WorkboardColumn.js @@ -28,6 +28,10 @@ JX.install('WorkboardColumn', { this._dropEffects = []; }, + properties: { + triggerPreviewEffect: null + }, + members: { _phid: null, _root: null, diff --git a/webroot/rsrc/js/application/projects/WorkboardDropEffect.js b/webroot/rsrc/js/application/projects/WorkboardDropEffect.js index fc8b2eaa58..0c729fc517 100644 --- a/webroot/rsrc/js/application/projects/WorkboardDropEffect.js +++ b/webroot/rsrc/js/application/projects/WorkboardDropEffect.js @@ -11,6 +11,8 @@ JX.install('WorkboardDropEffect', { icon: null, color: null, content: null, + isTriggerEffect: false, + isHeader: false, conditions: [] }, @@ -20,6 +22,8 @@ JX.install('WorkboardDropEffect', { .setIcon(map.icon) .setColor(map.color) .setContent(JX.$H(map.content)) + .setIsTriggerEffect(map.isTriggerEffect) + .setIsHeader(map.isHeader) .setConditions(map.conditions || []); } }, @@ -31,7 +35,13 @@ JX.install('WorkboardDropEffect', { .setColor(this.getColor()) .getNode(); - return JX.$N('li', {}, [icon, this.getContent()]); + var attributes = {}; + + if (this.getIsHeader()) { + attributes.className = 'workboard-drop-preview-header'; + } + + return JX.$N('li', attributes, [icon, this.getContent()]); }, isEffectVisibleForCard: function(card) { diff --git a/webroot/rsrc/js/application/projects/behavior-project-boards.js b/webroot/rsrc/js/application/projects/behavior-project-boards.js index f25599391f..daec59155f 100644 --- a/webroot/rsrc/js/application/projects/behavior-project-boards.js +++ b/webroot/rsrc/js/application/projects/behavior-project-boards.js @@ -108,6 +108,12 @@ JX.behavior('project-boards', function(config, statics) { for (jj = 0; jj < spec.cardPHIDs.length; jj++) { column.newCard(spec.cardPHIDs[jj]); } + + if (spec.triggerPreviewEffect) { + column.setTriggerPreviewEffect( + JX.WorkboardDropEffect.newFromDictionary( + spec.triggerPreviewEffect)); + } } var order_maps = config.orderMaps; From 66c1d623c3406120b65dab78f1bd297195c01a85 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 21 Mar 2019 13:37:43 -0700 Subject: [PATCH 201/245] If the user cancels a workboard drop flow, put things back where they were Summary: Ref T13074. If you hit a prompt on a drop operation (today: MFA; in the future, maybe "add a comment" or "assign this task"), we currently leave the board in a bad semi-frozen state if you cancel the workflow by pressing "Cancel" on the dialog. Instead, put things back the way they were. Test Plan: Dragged an MFA-required card, cancelled the MFA prompt, got a functional board instead of a semi-frozen board I needed to reload. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13074 Differential Revision: https://secure.phabricator.com/D20305 --- resources/celerity/map.php | 28 +++++++++---------- .../js/application/projects/WorkboardBoard.js | 25 +++++++++++++++++ 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index ffb8209f41..62cdf2f1e3 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -409,7 +409,7 @@ return array( 'rsrc/js/application/phortune/phortune-credit-card-form.js' => 'd12d214f', 'rsrc/js/application/policy/behavior-policy-control.js' => '0eaa33a9', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '9347f172', - 'rsrc/js/application/projects/WorkboardBoard.js' => '31766c31', + 'rsrc/js/application/projects/WorkboardBoard.js' => '65afb173', 'rsrc/js/application/projects/WorkboardCard.js' => '0392a5d8', 'rsrc/js/application/projects/WorkboardCardTemplate.js' => '2a61f8d4', 'rsrc/js/application/projects/WorkboardColumn.js' => 'c3d24e63', @@ -737,7 +737,7 @@ return array( 'javelin-view-renderer' => '9aae2b66', 'javelin-view-visitor' => '308f9fe4', 'javelin-websocket' => 'fdc13e4e', - 'javelin-workboard-board' => '31766c31', + 'javelin-workboard-board' => '65afb173', 'javelin-workboard-card' => '0392a5d8', 'javelin-workboard-card-template' => '2a61f8d4', 'javelin-workboard-column' => 'c3d24e63', @@ -1182,18 +1182,6 @@ return array( 'javelin-install', 'javelin-util', ), - '31766c31' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-util', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - 'javelin-workboard-column', - 'javelin-workboard-header-template', - 'javelin-workboard-card-template', - 'javelin-workboard-order-template', - ), '32755edb' => array( 'javelin-install', 'javelin-util', @@ -1468,6 +1456,18 @@ return array( '60cd9241' => array( 'javelin-behavior', ), + '65afb173' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-util', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + 'javelin-workboard-column', + 'javelin-workboard-header-template', + 'javelin-workboard-card-template', + 'javelin-workboard-order-template', + ), '65bb0011' => array( 'javelin-behavior', 'javelin-dom', diff --git a/webroot/rsrc/js/application/projects/WorkboardBoard.js b/webroot/rsrc/js/application/projects/WorkboardBoard.js index f96e82fb8b..a7786a86f4 100644 --- a/webroot/rsrc/js/application/projects/WorkboardBoard.js +++ b/webroot/rsrc/js/application/projects/WorkboardBoard.js @@ -479,6 +479,17 @@ JX.install('WorkboardBoard', { data.visiblePHIDs = visible_phids.join(','); + // If the user cancels the workflow (for example, by hitting an MFA + // prompt that they click "Cancel" on), put the card back where it was + // and reset the UI state. + var on_revert = JX.bind( + this, + this._revertCard, + list, + item, + src_phid, + dst_phid); + var onupdate = JX.bind( this, this._oncardupdate, @@ -489,9 +500,23 @@ JX.install('WorkboardBoard', { new JX.Workflow(this.getController().getMoveURI(), data) .setHandler(onupdate) + .setCloseHandler(on_revert) .start(); }, + _revertCard: function(list, item, src_phid, dst_phid) { + JX.DOM.alterClass(item, 'drag-sending', false); + + var src_column = this.getColumn(src_phid); + var dst_column = this.getColumn(dst_phid); + + src_column.markForRedraw(); + dst_column.markForRedraw(); + this._redrawColumns(); + + list.unlock(); + }, + _oncardupdate: function(list, src_phid, dst_phid, after_phid, response) { var src_column = this.getColumn(src_phid); var dst_column = this.getColumn(dst_phid); From bfa5ffe8a1eaed1f0e503bb2db65e2882b1ed9c4 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 21 Mar 2019 15:22:09 -0700 Subject: [PATCH 202/245] Add a "Play Sound" workboard trigger rule Summary: Ref T5474. Allow columns to play a sound when tasks are dropped. This is a little tricky because Safari has changed somewhat recently to require some gymnastics to play sounds when the user didn't explicitly click something. Preloading the sound on the first mouse interaction, then playing and immediately pausing it seems to work, though. Test Plan: Added a trigger with 5 sounds. In Safari, Chrome, and Firefox, dropped a card into the column. In all browsers, heard a nice sequence of 5 sounds played one after the other. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T5474 Differential Revision: https://secure.phabricator.com/D20306 --- resources/celerity/map.php | 64 ++++----- src/__phutil_library_map__.php | 2 + .../PhabricatorProjectBoardViewController.php | 6 + .../PhabricatorProjectController.php | 6 +- .../PhabricatorProjectMoveController.php | 11 +- .../engine/PhabricatorBoardResponseEngine.php | 11 ++ .../storage/PhabricatorProjectTrigger.php | 12 ++ ...PhabricatorProjectTriggerPlaySoundRule.php | 122 ++++++++++++++++++ .../trigger/PhabricatorProjectTriggerRule.php | 4 + webroot/rsrc/externals/javelin/lib/Sound.js | 50 ++++++- .../js/application/projects/WorkboardBoard.js | 5 + .../projects/behavior-project-boards.js | 12 ++ 12 files changed, 267 insertions(+), 38 deletions(-) create mode 100644 src/applications/project/trigger/PhabricatorProjectTriggerPlaySoundRule.php diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 62cdf2f1e3..5dd506784a 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -10,7 +10,7 @@ return array( 'conpherence.pkg.css' => '3c8a0668', 'conpherence.pkg.js' => '020aebcf', 'core.pkg.css' => 'b797945d', - 'core.pkg.js' => 'eaca003c', + 'core.pkg.js' => 'eb53fc5b', 'differential.pkg.css' => '8d8360fb', 'differential.pkg.js' => '67e02996', 'diffusion.pkg.css' => '42c75c37', @@ -249,7 +249,7 @@ return array( 'rsrc/externals/javelin/lib/Routable.js' => '6a18c42e', 'rsrc/externals/javelin/lib/Router.js' => '32755edb', 'rsrc/externals/javelin/lib/Scrollbar.js' => 'a43ae2ae', - 'rsrc/externals/javelin/lib/Sound.js' => 'e562708c', + 'rsrc/externals/javelin/lib/Sound.js' => 'd4cc2d2a', 'rsrc/externals/javelin/lib/URI.js' => '2e255291', 'rsrc/externals/javelin/lib/Vector.js' => 'e9c80beb', 'rsrc/externals/javelin/lib/WebSocket.js' => 'fdc13e4e', @@ -409,7 +409,7 @@ return array( 'rsrc/js/application/phortune/phortune-credit-card-form.js' => 'd12d214f', 'rsrc/js/application/policy/behavior-policy-control.js' => '0eaa33a9', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '9347f172', - 'rsrc/js/application/projects/WorkboardBoard.js' => '65afb173', + 'rsrc/js/application/projects/WorkboardBoard.js' => '3ba8e6ad', 'rsrc/js/application/projects/WorkboardCard.js' => '0392a5d8', 'rsrc/js/application/projects/WorkboardCardTemplate.js' => '2a61f8d4', 'rsrc/js/application/projects/WorkboardColumn.js' => 'c3d24e63', @@ -418,7 +418,7 @@ return array( 'rsrc/js/application/projects/WorkboardHeader.js' => '111bfd2d', 'rsrc/js/application/projects/WorkboardHeaderTemplate.js' => 'ebe83a6b', 'rsrc/js/application/projects/WorkboardOrderTemplate.js' => '03e8891f', - 'rsrc/js/application/projects/behavior-project-boards.js' => '8512e4ea', + 'rsrc/js/application/projects/behavior-project-boards.js' => 'aad45445', 'rsrc/js/application/projects/behavior-project-create.js' => '34c53422', 'rsrc/js/application/projects/behavior-reorder-columns.js' => '8ac32fd9', 'rsrc/js/application/releeph/releeph-preview-branch.js' => '75184d68', @@ -664,7 +664,7 @@ return array( 'javelin-behavior-phuix-example' => 'c2c500a7', 'javelin-behavior-policy-control' => '0eaa33a9', 'javelin-behavior-policy-rule-editor' => '9347f172', - 'javelin-behavior-project-boards' => '8512e4ea', + 'javelin-behavior-project-boards' => 'aad45445', 'javelin-behavior-project-create' => '34c53422', 'javelin-behavior-quicksand-blacklist' => '5a6f6a06', 'javelin-behavior-read-only-warning' => 'b9109f8f', @@ -718,7 +718,7 @@ return array( 'javelin-routable' => '6a18c42e', 'javelin-router' => '32755edb', 'javelin-scrollbar' => 'a43ae2ae', - 'javelin-sound' => 'e562708c', + 'javelin-sound' => 'd4cc2d2a', 'javelin-stratcom' => '0889b835', 'javelin-tokenizer' => '89a1ae3a', 'javelin-typeahead' => 'a4356cde', @@ -737,7 +737,7 @@ return array( 'javelin-view-renderer' => '9aae2b66', 'javelin-view-visitor' => '308f9fe4', 'javelin-websocket' => 'fdc13e4e', - 'javelin-workboard-board' => '65afb173', + 'javelin-workboard-board' => '3ba8e6ad', 'javelin-workboard-card' => '0392a5d8', 'javelin-workboard-card-template' => '2a61f8d4', 'javelin-workboard-column' => 'c3d24e63', @@ -1227,6 +1227,18 @@ return array( 'javelin-behavior', 'phabricator-prefab', ), + '3ba8e6ad' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-util', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + 'javelin-workboard-column', + 'javelin-workboard-header-template', + 'javelin-workboard-card-template', + 'javelin-workboard-order-template', + ), '3dc5ad43' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1456,18 +1468,6 @@ return array( '60cd9241' => array( 'javelin-behavior', ), - '65afb173' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-util', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - 'javelin-workboard-column', - 'javelin-workboard-header-template', - 'javelin-workboard-card-template', - 'javelin-workboard-order-template', - ), '65bb0011' => array( 'javelin-behavior', 'javelin-dom', @@ -1594,16 +1594,6 @@ return array( 'javelin-dom', 'javelin-vector', ), - '8512e4ea' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-util', - 'javelin-vector', - 'javelin-stratcom', - 'javelin-workflow', - 'javelin-workboard-controller', - 'javelin-workboard-drop-effect', - ), '87428eb2' => array( 'javelin-behavior', 'javelin-diffusion-locate-file-source', @@ -1848,6 +1838,16 @@ return array( 'javelin-dom', 'javelin-util', ), + 'aad45445' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-util', + 'javelin-vector', + 'javelin-stratcom', + 'javelin-workflow', + 'javelin-workboard-controller', + 'javelin-workboard-drop-effect', + ), 'ab85e184' => array( 'javelin-install', 'javelin-dom', @@ -2041,6 +2041,9 @@ return array( 'd3799cb4' => array( 'javelin-install', ), + 'd4cc2d2a' => array( + 'javelin-install', + ), 'd8a86cfb' => array( 'javelin-behavior', 'javelin-dom', @@ -2075,9 +2078,6 @@ return array( 'javelin-dom', 'javelin-history', ), - 'e562708c' => array( - 'javelin-install', - ), 'e5bdb730' => array( 'javelin-behavior', 'javelin-stratcom', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index a4edf3d246..f56c616545 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4183,6 +4183,7 @@ phutil_register_library_map(array( 'PhabricatorProjectTriggerManiphestStatusRule' => 'applications/project/trigger/PhabricatorProjectTriggerManiphestStatusRule.php', 'PhabricatorProjectTriggerNameTransaction' => 'applications/project/xaction/trigger/PhabricatorProjectTriggerNameTransaction.php', 'PhabricatorProjectTriggerPHIDType' => 'applications/project/phid/PhabricatorProjectTriggerPHIDType.php', + 'PhabricatorProjectTriggerPlaySoundRule' => 'applications/project/trigger/PhabricatorProjectTriggerPlaySoundRule.php', 'PhabricatorProjectTriggerQuery' => 'applications/project/query/PhabricatorProjectTriggerQuery.php', 'PhabricatorProjectTriggerRule' => 'applications/project/trigger/PhabricatorProjectTriggerRule.php', 'PhabricatorProjectTriggerRuleRecord' => 'applications/project/trigger/PhabricatorProjectTriggerRuleRecord.php', @@ -10317,6 +10318,7 @@ phutil_register_library_map(array( 'PhabricatorProjectTriggerManiphestStatusRule' => 'PhabricatorProjectTriggerRule', 'PhabricatorProjectTriggerNameTransaction' => 'PhabricatorProjectTriggerTransactionType', 'PhabricatorProjectTriggerPHIDType' => 'PhabricatorPHIDType', + 'PhabricatorProjectTriggerPlaySoundRule' => 'PhabricatorProjectTriggerRule', 'PhabricatorProjectTriggerQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorProjectTriggerRule' => 'Phobject', 'PhabricatorProjectTriggerRuleRecord' => 'Phobject', diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index 4e4ff81b4e..e8a47d362a 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -542,6 +542,7 @@ final class PhabricatorProjectBoardViewController $templates = array(); $all_tasks = array(); $column_templates = array(); + $sounds = array(); foreach ($visible_columns as $column_phid => $column) { $column_tasks = $column_phids[$column_phid]; @@ -629,6 +630,10 @@ final class PhabricatorProjectBoardViewController if ($trigger) { $preview_effect = $trigger->getPreviewEffect() ->toDictionary(); + + foreach ($trigger->getSoundEffects() as $sound) { + $sounds[] = $sound; + } } } @@ -685,6 +690,7 @@ final class PhabricatorProjectBoardViewController 'boardID' => $board_id, 'projectPHID' => $project->getPHID(), + 'preloadSounds' => $sounds, ); $this->initBehavior('project-boards', $behavior_config); diff --git a/src/applications/project/controller/PhabricatorProjectController.php b/src/applications/project/controller/PhabricatorProjectController.php index 850dfa2268..c28ace305c 100644 --- a/src/applications/project/controller/PhabricatorProjectController.php +++ b/src/applications/project/controller/PhabricatorProjectController.php @@ -152,7 +152,8 @@ abstract class PhabricatorProjectController extends PhabricatorController { protected function newCardResponse( $board_phid, $object_phid, - PhabricatorProjectColumnOrder $ordering = null) { + PhabricatorProjectColumnOrder $ordering = null, + $sounds = array()) { $viewer = $this->getViewer(); @@ -166,7 +167,8 @@ abstract class PhabricatorProjectController extends PhabricatorController { ->setViewer($viewer) ->setBoardPHID($board_phid) ->setObjectPHID($object_phid) - ->setVisiblePHIDs($visible_phids); + ->setVisiblePHIDs($visible_phids) + ->setSounds($sounds); if ($ordering) { $engine->setOrdering($ordering); diff --git a/src/applications/project/controller/PhabricatorProjectMoveController.php b/src/applications/project/controller/PhabricatorProjectMoveController.php index 71588754c0..950b1e90cd 100644 --- a/src/applications/project/controller/PhabricatorProjectMoveController.php +++ b/src/applications/project/controller/PhabricatorProjectMoveController.php @@ -111,6 +111,7 @@ final class PhabricatorProjectMoveController $xactions[] = $header_xaction; } + $sounds = array(); if ($column->canHaveTrigger()) { $trigger = $column->getTrigger(); if ($trigger) { @@ -121,6 +122,10 @@ final class PhabricatorProjectMoveController foreach ($trigger_xactions as $trigger_xaction) { $xactions[] = $trigger_xaction; } + + foreach ($trigger->getSoundEffects() as $effect) { + $sounds[] = $effect; + } } } @@ -133,7 +138,11 @@ final class PhabricatorProjectMoveController $editor->applyTransactions($object, $xactions); - return $this->newCardResponse($board_phid, $object_phid, $ordering); + return $this->newCardResponse( + $board_phid, + $object_phid, + $ordering, + $sounds); } } diff --git a/src/applications/project/engine/PhabricatorBoardResponseEngine.php b/src/applications/project/engine/PhabricatorBoardResponseEngine.php index fb5299a857..f22254e43a 100644 --- a/src/applications/project/engine/PhabricatorBoardResponseEngine.php +++ b/src/applications/project/engine/PhabricatorBoardResponseEngine.php @@ -7,6 +7,7 @@ final class PhabricatorBoardResponseEngine extends Phobject { private $objectPHID; private $visiblePHIDs; private $ordering; + private $sounds; public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; @@ -53,6 +54,15 @@ final class PhabricatorBoardResponseEngine extends Phobject { return $this->ordering; } + public function setSounds(array $sounds) { + $this->sounds = $sounds; + return $this; + } + + public function getSounds() { + return $this->sounds; + } + public function buildResponse() { $viewer = $this->getViewer(); $object_phid = $this->getObjectPHID(); @@ -150,6 +160,7 @@ final class PhabricatorBoardResponseEngine extends Phobject { 'columnMaps' => $natural, 'cards' => $cards, 'headers' => $headers, + 'sounds' => $this->getSounds(), ); return id(new AphrontAjaxResponse()) diff --git a/src/applications/project/storage/PhabricatorProjectTrigger.php b/src/applications/project/storage/PhabricatorProjectTrigger.php index a499f0bcf8..bac3927b74 100644 --- a/src/applications/project/storage/PhabricatorProjectTrigger.php +++ b/src/applications/project/storage/PhabricatorProjectTrigger.php @@ -245,6 +245,18 @@ final class PhabricatorProjectTrigger ->setContent($header); } + public function getSoundEffects() { + $sounds = array(); + + foreach ($this->getTriggerRules() as $rule) { + foreach ($rule->getSoundEffects() as $effect) { + $sounds[] = $effect; + } + } + + return $sounds; + } + /* -( PhabricatorApplicationTransactionInterface )------------------------- */ diff --git a/src/applications/project/trigger/PhabricatorProjectTriggerPlaySoundRule.php b/src/applications/project/trigger/PhabricatorProjectTriggerPlaySoundRule.php new file mode 100644 index 0000000000..ef19b504ef --- /dev/null +++ b/src/applications/project/trigger/PhabricatorProjectTriggerPlaySoundRule.php @@ -0,0 +1,122 @@ +newEffect() + ->setIcon($sound_icon) + ->setColor($sound_color) + ->setContent($content), + ); + } + + protected function getDefaultValue() { + return head_key(self::getSoundMap()); + } + + protected function getPHUIXControlType() { + return 'select'; + } + + protected function getPHUIXControlSpecification() { + $map = self::getSoundMap(); + $map = ipull($map, 'name'); + + return array( + 'options' => $map, + 'order' => array_keys($map), + ); + } + + public function getRuleViewLabel() { + return pht('Play Sound'); + } + + public function getRuleViewDescription($value) { + $sound_name = self::getSoundName($value); + + return pht( + 'Play sound %s.', + phutil_tag('strong', array(), $sound_name)); + } + + public function getRuleViewIcon($value) { + $sound_icon = 'fa-volume-up'; + $sound_color = 'blue'; + + return id(new PHUIIconView()) + ->setIcon($sound_icon, $sound_color); + } + + private static function getSoundName($value) { + $map = self::getSoundMap(); + $spec = idx($map, $value, array()); + return idx($spec, 'name', $value); + } + + private static function getSoundMap() { + return array( + 'bing' => array( + 'name' => pht('Bing'), + 'uri' => celerity_get_resource_uri('/rsrc/audio/basic/bing.mp3'), + ), + 'glass' => array( + 'name' => pht('Glass'), + 'uri' => celerity_get_resource_uri('/rsrc/audio/basic/ting.mp3'), + ), + ); + } + + public function getSoundEffects() { + $value = $this->getValue(); + + $map = self::getSoundMap(); + $spec = idx($map, $value, array()); + + $uris = array(); + if (isset($spec['uri'])) { + $uris[] = $spec['uri']; + } + + return $uris; + } + +} diff --git a/src/applications/project/trigger/PhabricatorProjectTriggerRule.php b/src/applications/project/trigger/PhabricatorProjectTriggerRule.php index c75c15a1ab..ae2b3ee092 100644 --- a/src/applications/project/trigger/PhabricatorProjectTriggerRule.php +++ b/src/applications/project/trigger/PhabricatorProjectTriggerRule.php @@ -60,6 +60,10 @@ abstract class PhabricatorProjectTriggerRule return null; } + public function getSoundEffects() { + return array(); + } + final public function getDropTransactions($object, $value) { return $this->newDropTransactions($object, $value); } diff --git a/webroot/rsrc/externals/javelin/lib/Sound.js b/webroot/rsrc/externals/javelin/lib/Sound.js index accbe3d29b..68181560ff 100644 --- a/webroot/rsrc/externals/javelin/lib/Sound.js +++ b/webroot/rsrc/externals/javelin/lib/Sound.js @@ -8,31 +8,75 @@ JX.install('Sound', { statics: { _sounds: {}, + _queue: [], + _playingQueue: false, load: function(uri) { var self = JX.Sound; if (!(uri in self._sounds)) { - self._sounds[uri] = JX.$N( + var audio = JX.$N( 'audio', { src: uri, preload: 'auto' }); + + // In Safari, it isn't good enough to just load a sound in response + // to a click: we must also play it. Once we've played it once, we + // can continue to play it freely. + + // Play the sound, then immediately pause it. This rejects the "play()" + // promise but marks the audio as playable, so our "play()" method will + // work correctly later. + if (window.webkitAudioContext) { + audio.play().then(JX.bag, JX.bag); + audio.pause(); + } + + self._sounds[uri] = audio; } }, - play: function(uri) { + play: function(uri, callback) { var self = JX.Sound; self.load(uri); var sound = self._sounds[uri]; try { - sound.play(); + sound.onended = callback || JX.bag; + sound.play().then(JX.bag, callback || JX.bag); } catch (ex) { JX.log(ex); } + }, + + queue: function(uri) { + var self = JX.Sound; + self._queue.push(uri); + self._playQueue(); + }, + + _playQueue: function() { + var self = JX.Sound; + if (self._playingQueue) { + return; + } + self._playingQueue = true; + self._nextQueue(); + }, + + _nextQueue: function() { + var self = JX.Sound; + if (self._queue.length) { + var next = self._queue[0]; + self._queue.splice(0, 1); + self.play(next, self._nextQueue); + } else { + self._playingQueue = false; + } } + } }); diff --git a/webroot/rsrc/js/application/projects/WorkboardBoard.js b/webroot/rsrc/js/application/projects/WorkboardBoard.js index a7786a86f4..6add658259 100644 --- a/webroot/rsrc/js/application/projects/WorkboardBoard.js +++ b/webroot/rsrc/js/application/projects/WorkboardBoard.js @@ -529,6 +529,11 @@ JX.install('WorkboardBoard', { this.updateCard(response); + var sounds = response.sounds || []; + for (var ii = 0; ii < sounds.length; ii++) { + JX.Sound.queue(sounds[ii]); + } + list.unlock(); }, diff --git a/webroot/rsrc/js/application/projects/behavior-project-boards.js b/webroot/rsrc/js/application/projects/behavior-project-boards.js index daec59155f..bba6db7a49 100644 --- a/webroot/rsrc/js/application/projects/behavior-project-boards.js +++ b/webroot/rsrc/js/application/projects/behavior-project-boards.js @@ -166,4 +166,16 @@ JX.behavior('project-boards', function(config, statics) { board.start(); + // In Safari, we can only play sounds that we've already loaded, and we can + // only load them in response to an explicit user interaction like a click. + var sounds = config.preloadSounds; + var listener = JX.Stratcom.listen('mousedown', null, function() { + for (var ii = 0; ii < sounds.length; ii++) { + JX.Sound.load(sounds[ii]); + } + + // Remove this callback once it has run once. + listener.remove(); + }); + }); From 47856dc93f7ee81b1527c09eb26014c6a0cf7d61 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 22 Mar 2019 07:30:02 -0700 Subject: [PATCH 203/245] Track how many columns use a particular trigger Summary: Ref T5474. In 99% of cases, a separate "archived/active" status for triggers probably doesn't make much sense: there's not much reason to ever disable/archive a trigger explcitly, and the archival rule is really just "is this trigger used by anything?". (The one reason I can think of to disable a trigger manually is because you want to put something in a column and skip trigger rules, but you can already do this from the task detail page anyway, and disabling the trigger globally is a bad way to accomplish this if it's in use by other columns.) Instead of adding a separate "status", just track how many columns a trigger is used by and consider it "inactive" if it is not used by any active columns. Test Plan: This is slightly hard to test exhaustively since you can't share a trigger across multiple columns right now, but: rebuild indexes, poked around the trigger list and trigger details, added/removed triggers. Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T5474 Differential Revision: https://secure.phabricator.com/D20308 --- .../20190322.triggers.01.usage.sql | 8 ++ src/__phutil_library_map__.php | 5 ++ ...habricatorProjectTriggerViewController.php | 27 +++++- .../PhabricatorProjectTriggerEditor.php | 4 + ...rojectTriggerUsageIndexEngineExtension.php | 69 +++++++++++++++ .../query/PhabricatorProjectTriggerQuery.php | 88 ++++++++++++++++++- .../PhabricatorProjectTriggerSearchEngine.php | 88 ++++++++++++++++++- .../storage/PhabricatorProjectTrigger.php | 18 ++++ .../PhabricatorProjectTriggerUsage.php | 28 ++++++ ...bricatorProjectColumnStatusTransaction.php | 9 ++ ...ricatorProjectColumnTriggerTransaction.php | 19 ++++ ...abricatorSearchManagementIndexWorkflow.php | 8 +- 12 files changed, 360 insertions(+), 11 deletions(-) create mode 100644 resources/sql/autopatches/20190322.triggers.01.usage.sql create mode 100644 src/applications/project/engineextension/PhabricatorProjectTriggerUsageIndexEngineExtension.php create mode 100644 src/applications/project/storage/PhabricatorProjectTriggerUsage.php diff --git a/resources/sql/autopatches/20190322.triggers.01.usage.sql b/resources/sql/autopatches/20190322.triggers.01.usage.sql new file mode 100644 index 0000000000..643ebbbfff --- /dev/null +++ b/resources/sql/autopatches/20190322.triggers.01.usage.sql @@ -0,0 +1,8 @@ +CREATE TABLE {$NAMESPACE}_project.project_triggerusage ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + triggerPHID VARBINARY(64) NOT NULL, + examplePHID VARBINARY(64), + columnCount INT UNSIGNED NOT NULL, + activeColumnCount INT UNSIGNED NOT NULL, + UNIQUE KEY `key_trigger` (triggerPHID) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE {$COLLATE_TEXT}; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index f56c616545..ba02f45406 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4193,6 +4193,8 @@ phutil_register_library_map(array( 'PhabricatorProjectTriggerTransactionQuery' => 'applications/project/query/PhabricatorProjectTriggerTransactionQuery.php', 'PhabricatorProjectTriggerTransactionType' => 'applications/project/xaction/trigger/PhabricatorProjectTriggerTransactionType.php', 'PhabricatorProjectTriggerUnknownRule' => 'applications/project/trigger/PhabricatorProjectTriggerUnknownRule.php', + 'PhabricatorProjectTriggerUsage' => 'applications/project/storage/PhabricatorProjectTriggerUsage.php', + 'PhabricatorProjectTriggerUsageIndexEngineExtension' => 'applications/project/engineextension/PhabricatorProjectTriggerUsageIndexEngineExtension.php', 'PhabricatorProjectTriggerViewController' => 'applications/project/controller/trigger/PhabricatorProjectTriggerViewController.php', 'PhabricatorProjectTypeTransaction' => 'applications/project/xaction/PhabricatorProjectTypeTransaction.php', 'PhabricatorProjectUIEventListener' => 'applications/project/events/PhabricatorProjectUIEventListener.php', @@ -10307,6 +10309,7 @@ phutil_register_library_map(array( 'PhabricatorProjectDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', + 'PhabricatorIndexableInterface', 'PhabricatorDestructibleInterface', ), 'PhabricatorProjectTriggerController' => 'PhabricatorProjectController', @@ -10328,6 +10331,8 @@ phutil_register_library_map(array( 'PhabricatorProjectTriggerTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorProjectTriggerTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorProjectTriggerUnknownRule' => 'PhabricatorProjectTriggerRule', + 'PhabricatorProjectTriggerUsage' => 'PhabricatorProjectDAO', + 'PhabricatorProjectTriggerUsageIndexEngineExtension' => 'PhabricatorIndexEngineExtension', 'PhabricatorProjectTriggerViewController' => 'PhabricatorProjectTriggerController', 'PhabricatorProjectTypeTransaction' => 'PhabricatorProjectTransactionType', 'PhabricatorProjectUIEventListener' => 'PhabricatorEventListener', diff --git a/src/applications/project/controller/trigger/PhabricatorProjectTriggerViewController.php b/src/applications/project/controller/trigger/PhabricatorProjectTriggerViewController.php index 2750adc2ea..e18419fedb 100644 --- a/src/applications/project/controller/trigger/PhabricatorProjectTriggerViewController.php +++ b/src/applications/project/controller/trigger/PhabricatorProjectTriggerViewController.php @@ -71,17 +71,23 @@ final class PhabricatorProjectTriggerViewController // so we load only the columns they can actually see, but have a list of // all the impacted column PHIDs. + // (We're also exposing the status of columns the user might not be able + // to see. This technically violates policy, but the trigger usage table + // hints at it anyway and it seems unlikely to ever have any security + // impact, but is useful in assessing whether a trigger is really in use + // or not.) + $omnipotent_viewer = PhabricatorUser::getOmnipotentUser(); $all_columns = id(new PhabricatorProjectColumnQuery()) ->setViewer($omnipotent_viewer) ->withTriggerPHIDs(array($trigger->getPHID())) ->execute(); - $column_phids = mpull($all_columns, 'getPHID'); + $column_map = mpull($all_columns, 'getStatus', 'getPHID'); - if ($column_phids) { + if ($column_map) { $visible_columns = id(new PhabricatorProjectColumnQuery()) ->setViewer($viewer) - ->withPHIDs($column_phids) + ->withPHIDs(array_keys($column_map)) ->execute(); $visible_columns = mpull($visible_columns, null, 'getPHID'); } else { @@ -89,7 +95,7 @@ final class PhabricatorProjectTriggerViewController } $rows = array(); - foreach ($column_phids as $column_phid) { + foreach ($column_map as $column_phid => $column_status) { $column = idx($visible_columns, $column_phid); if ($column) { @@ -113,7 +119,18 @@ final class PhabricatorProjectTriggerViewController $column_name = phutil_tag('em', array(), pht('Restricted Column')); } + if ($column_status == PhabricatorProjectColumn::STATUS_ACTIVE) { + $status_icon = id(new PHUIIconView()) + ->setIcon('fa-columns', 'blue') + ->setTooltip(pht('Active Column')); + } else { + $status_icon = id(new PHUIIconView()) + ->setIcon('fa-eye-slash', 'grey') + ->setTooltip(pht('Hidden Column')); + } + $rows[] = array( + $status_icon, $project_name, $column_name, ); @@ -123,11 +140,13 @@ final class PhabricatorProjectTriggerViewController ->setNoDataString(pht('This trigger is not used by any columns.')) ->setHeaders( array( + null, pht('Project'), pht('Column'), )) ->setColumnClasses( array( + null, null, 'wide pri', )); diff --git a/src/applications/project/editor/PhabricatorProjectTriggerEditor.php b/src/applications/project/editor/PhabricatorProjectTriggerEditor.php index 20098fa370..9014fd6f16 100644 --- a/src/applications/project/editor/PhabricatorProjectTriggerEditor.php +++ b/src/applications/project/editor/PhabricatorProjectTriggerEditor.php @@ -27,4 +27,8 @@ final class PhabricatorProjectTriggerEditor return $types; } + protected function supportsSearch() { + return true; + } + } diff --git a/src/applications/project/engineextension/PhabricatorProjectTriggerUsageIndexEngineExtension.php b/src/applications/project/engineextension/PhabricatorProjectTriggerUsageIndexEngineExtension.php new file mode 100644 index 0000000000..b50c51fba6 --- /dev/null +++ b/src/applications/project/engineextension/PhabricatorProjectTriggerUsageIndexEngineExtension.php @@ -0,0 +1,69 @@ +establishConnection('w'); + + $active_statuses = array( + PhabricatorProjectColumn::STATUS_ACTIVE, + ); + + // Select summary information to populate the usage index. When picking + // an "examplePHID", we try to pick an active column. + $row = queryfx_one( + $conn_w, + 'SELECT phid, COUNT(*) N, SUM(IF(status IN (%Ls), 1, 0)) M FROM %R + WHERE triggerPHID = %s + ORDER BY IF(status IN (%Ls), 1, 0) DESC, id ASC', + $active_statuses, + $column_table, + $object->getPHID(), + $active_statuses); + if ($row) { + $example_phid = $row['phid']; + $column_count = $row['N']; + $active_count = $row['M']; + } else { + $example_phid = null; + $column_count = 0; + $active_count = 0; + } + + queryfx( + $conn_w, + 'INSERT INTO %R (triggerPHID, examplePHID, columnCount, activeColumnCount) + VALUES (%s, %ns, %d, %d) + ON DUPLICATE KEY UPDATE + examplePHID = VALUES(examplePHID), + columnCount = VALUES(columnCount), + activeColumnCount = VALUES(activeColumnCount)', + $usage_table, + $object->getPHID(), + $example_phid, + $column_count, + $active_count); + } + +} diff --git a/src/applications/project/query/PhabricatorProjectTriggerQuery.php b/src/applications/project/query/PhabricatorProjectTriggerQuery.php index e3fab5b3d0..452e3e53f1 100644 --- a/src/applications/project/query/PhabricatorProjectTriggerQuery.php +++ b/src/applications/project/query/PhabricatorProjectTriggerQuery.php @@ -5,6 +5,10 @@ final class PhabricatorProjectTriggerQuery private $ids; private $phids; + private $activeColumnMin; + private $activeColumnMax; + + private $needUsage; public function withIDs(array $ids) { $this->ids = $ids; @@ -16,6 +20,17 @@ final class PhabricatorProjectTriggerQuery return $this; } + public function needUsage($need_usage) { + $this->needUsage = $need_usage; + return $this; + } + + public function withActiveColumnCountBetween($min, $max) { + $this->activeColumnMin = $min; + $this->activeColumnMax = $max; + return $this; + } + public function newResultObject() { return new PhabricatorProjectTrigger(); } @@ -30,22 +45,91 @@ final class PhabricatorProjectTriggerQuery if ($this->ids !== null) { $where[] = qsprintf( $conn, - 'id IN (%Ld)', + 'trigger.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, - 'phid IN (%Ls)', + 'trigger.phid IN (%Ls)', $this->phids); } + if ($this->activeColumnMin !== null) { + $where[] = qsprintf( + $conn, + 'trigger_usage.activeColumnCount >= %d', + $this->activeColumnMin); + } + + if ($this->activeColumnMax !== null) { + $where[] = qsprintf( + $conn, + 'trigger_usage.activeColumnCount <= %d', + $this->activeColumnMax); + } + return $where; } + protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { + $joins = parent::buildJoinClauseParts($conn); + + if ($this->shouldJoinUsageTable()) { + $joins[] = qsprintf( + $conn, + 'JOIN %R trigger_usage ON trigger.phid = trigger_usage.triggerPHID', + new PhabricatorProjectTriggerUsage()); + } + + return $joins; + } + + private function shouldJoinUsageTable() { + if ($this->activeColumnMin !== null) { + return true; + } + + if ($this->activeColumnMax !== null) { + return true; + } + + return false; + } + + protected function didFilterPage(array $triggers) { + if ($this->needUsage) { + $usage_map = id(new PhabricatorProjectTriggerUsage())->loadAllWhere( + 'triggerPHID IN (%Ls)', + mpull($triggers, 'getPHID')); + $usage_map = mpull($usage_map, null, 'getTriggerPHID'); + + foreach ($triggers as $trigger) { + $trigger_phid = $trigger->getPHID(); + + $usage = idx($usage_map, $trigger_phid); + if (!$usage) { + $usage = id(new PhabricatorProjectTriggerUsage()) + ->setTriggerPHID($trigger_phid) + ->setExamplePHID(null) + ->setColumnCount(0) + ->setActiveColumnCount(0); + } + + $trigger->attachUsage($usage); + } + } + + return $triggers; + } + public function getQueryApplicationClass() { return 'PhabricatorProjectApplication'; } + protected function getPrimaryTableAlias() { + return 'trigger'; + } + } diff --git a/src/applications/project/query/PhabricatorProjectTriggerSearchEngine.php b/src/applications/project/query/PhabricatorProjectTriggerSearchEngine.php index 6c6a417723..a178ed3e6c 100644 --- a/src/applications/project/query/PhabricatorProjectTriggerSearchEngine.php +++ b/src/applications/project/query/PhabricatorProjectTriggerSearchEngine.php @@ -12,16 +12,33 @@ final class PhabricatorProjectTriggerSearchEngine } public function newQuery() { - return new PhabricatorProjectTriggerQuery(); + return id(new PhabricatorProjectTriggerQuery()) + ->needUsage(true); } protected function buildCustomSearchFields() { - return array(); + return array( + id(new PhabricatorSearchThreeStateField()) + ->setLabel(pht('Active')) + ->setKey('isActive') + ->setOptions( + pht('(Show All)'), + pht('Show Only Active Triggers'), + pht('Show Only Inactive Triggers')), + ); } protected function buildQueryFromParameters(array $map) { $query = $this->newQuery(); + if ($map['isActive'] !== null) { + if ($map['isActive']) { + $query->withActiveColumnCountBetween(1, null); + } else { + $query->withActiveColumnCountBetween(null, 0); + } + } + return $query; } @@ -32,7 +49,8 @@ final class PhabricatorProjectTriggerSearchEngine protected function getBuiltinQueryNames() { $names = array(); - $names['all'] = pht('All'); + $names['active'] = pht('Active Triggers'); + $names['all'] = pht('All Triggers'); return $names; } @@ -42,6 +60,8 @@ final class PhabricatorProjectTriggerSearchEngine $query->setQueryKey($query_key); switch ($query_key) { + case 'active': + return $query->setParameter('isActive', true); case 'all': return $query; } @@ -56,13 +76,73 @@ final class PhabricatorProjectTriggerSearchEngine assert_instances_of($triggers, 'PhabricatorProjectTrigger'); $viewer = $this->requireViewer(); + $example_phids = array(); + foreach ($triggers as $trigger) { + $example_phid = $trigger->getUsage()->getExamplePHID(); + if ($example_phid) { + $example_phids[] = $example_phid; + } + } + + $handles = $viewer->loadHandles($example_phids); + $list = id(new PHUIObjectItemListView()) ->setViewer($viewer); foreach ($triggers as $trigger) { + $usage = $trigger->getUsage(); + + $column_handle = null; + $have_column = false; + $example_phid = $usage->getExamplePHID(); + if ($example_phid) { + $column_handle = $handles[$example_phid]; + if ($column_handle->isComplete()) { + if (!$column_handle->getPolicyFiltered()) { + $have_column = true; + } + } + } + + $column_count = $usage->getColumnCount(); + $active_count = $usage->getActiveColumnCount(); + + if ($have_column) { + if ($active_count > 1) { + $usage_description = pht( + 'Used on %s and %s other active column(s).', + $column_handle->renderLink(), + new PhutilNumber($active_count - 1)); + } else if ($column_count > 1) { + $usage_description = pht( + 'Used on %s and %s other column(s).', + $column_handle->renderLink(), + new PhutilNumber($column_count - 1)); + } else { + $usage_description = pht( + 'Used on %s.', + $column_handle->renderLink()); + } + } else { + if ($active_count) { + $usage_description = pht( + 'Used on %s active column(s).', + new PhutilNumber($active_count)); + } else if ($column_count) { + $usage_description = pht( + 'Used on %s column(s).', + new PhutilNumber($column_count)); + } else { + $usage_description = pht( + 'Unused trigger.'); + } + } + $item = id(new PHUIObjectItemView()) ->setObjectName($trigger->getObjectName()) ->setHeader($trigger->getDisplayName()) - ->setHref($trigger->getURI()); + ->setHref($trigger->getURI()) + ->addAttribute($usage_description) + ->setDisabled(!$active_count); $list->addItem($item); } diff --git a/src/applications/project/storage/PhabricatorProjectTrigger.php b/src/applications/project/storage/PhabricatorProjectTrigger.php index bac3927b74..625dc7ffd8 100644 --- a/src/applications/project/storage/PhabricatorProjectTrigger.php +++ b/src/applications/project/storage/PhabricatorProjectTrigger.php @@ -5,6 +5,7 @@ final class PhabricatorProjectTrigger implements PhabricatorApplicationTransactionInterface, PhabricatorPolicyInterface, + PhabricatorIndexableInterface, PhabricatorDestructibleInterface { protected $name; @@ -12,6 +13,7 @@ final class PhabricatorProjectTrigger protected $editPolicy; private $triggerRules; + private $usage = self::ATTACHABLE; public static function initializeNewTrigger() { $default_edit = PhabricatorPolicies::POLICY_USER; @@ -257,6 +259,15 @@ final class PhabricatorProjectTrigger return $sounds; } + public function getUsage() { + return $this->assertAttached($this->usage); + } + + public function attachUsage(PhabricatorProjectTriggerUsage $usage) { + $this->usage = $usage; + return $this; + } + /* -( PhabricatorApplicationTransactionInterface )------------------------- */ @@ -310,6 +321,13 @@ final class PhabricatorProjectTrigger new PhabricatorProjectColumn(), $this->getPHID()); + // Remove the usage index row for this trigger, if one exists. + queryfx( + $conn, + 'DELETE FROM %R WHERE triggerPHID = %s', + new PhabricatorProjectTriggerUsage(), + $this->getPHID()); + $this->delete(); $this->saveTransaction(); diff --git a/src/applications/project/storage/PhabricatorProjectTriggerUsage.php b/src/applications/project/storage/PhabricatorProjectTriggerUsage.php new file mode 100644 index 0000000000..982b8ecf55 --- /dev/null +++ b/src/applications/project/storage/PhabricatorProjectTriggerUsage.php @@ -0,0 +1,28 @@ + false, + self::CONFIG_COLUMN_SCHEMA => array( + 'examplePHID' => 'phid?', + 'columnCount' => 'uint32', + 'activeColumnCount' => 'uint32', + ), + self::CONFIG_KEY_SCHEMA => array( + 'key_trigger' => array( + 'columns' => array('triggerPHID'), + 'unique' => true, + ), + ), + ) + parent::getConfiguration(); + } + +} diff --git a/src/applications/project/xaction/column/PhabricatorProjectColumnStatusTransaction.php b/src/applications/project/xaction/column/PhabricatorProjectColumnStatusTransaction.php index 7606c72562..7aab57c8e6 100644 --- a/src/applications/project/xaction/column/PhabricatorProjectColumnStatusTransaction.php +++ b/src/applications/project/xaction/column/PhabricatorProjectColumnStatusTransaction.php @@ -13,6 +13,15 @@ final class PhabricatorProjectColumnStatusTransaction $object->setStatus($value); } + public function applyExternalEffects($object, $value) { + // Update the trigger usage index, which cares about whether columns are + // active or not. + $trigger_phid = $object->getTriggerPHID(); + if ($trigger_phid) { + PhabricatorSearchWorker::queueDocumentForIndexing($trigger_phid); + } + } + public function getTitle() { $new = $this->getNewValue(); diff --git a/src/applications/project/xaction/column/PhabricatorProjectColumnTriggerTransaction.php b/src/applications/project/xaction/column/PhabricatorProjectColumnTriggerTransaction.php index 78e2451bd1..5339699de3 100644 --- a/src/applications/project/xaction/column/PhabricatorProjectColumnTriggerTransaction.php +++ b/src/applications/project/xaction/column/PhabricatorProjectColumnTriggerTransaction.php @@ -13,6 +13,25 @@ final class PhabricatorProjectColumnTriggerTransaction $object->setTriggerPHID($value); } + public function applyExternalEffects($object, $value) { + // After we change the trigger attached to a column, update the search + // indexes for the old and new triggers so we update the usage index. + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + $column_phids = array(); + if ($old) { + $column_phids[] = $old; + } + if ($new) { + $column_phids[] = $new; + } + + foreach ($column_phids as $phid) { + PhabricatorSearchWorker::queueDocumentForIndexing($phid); + } + } + public function getTitle() { $old = $this->getOldValue(); $new = $this->getNewValue(); diff --git a/src/applications/search/management/PhabricatorSearchManagementIndexWorkflow.php b/src/applications/search/management/PhabricatorSearchManagementIndexWorkflow.php index 99ee3a3123..984eeae5fb 100644 --- a/src/applications/search/management/PhabricatorSearchManagementIndexWorkflow.php +++ b/src/applications/search/management/PhabricatorSearchManagementIndexWorkflow.php @@ -136,7 +136,13 @@ final class PhabricatorSearchManagementIndexWorkflow if ($track_skips) { $new_versions = $this->loadIndexVersions($phid); - if ($old_versions !== $new_versions) { + + if (!$old_versions && !$new_versions) { + // If the document doesn't use an index version, both the lists + // of versions will be empty. We still rebuild the index in this + // case. + $count_updated++; + } else if ($old_versions !== $new_versions) { $count_updated++; } else { $count_skipped++; From c53ed72e4cf5bdaf18967d793f8898c709911bbe Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 22 Mar 2019 15:29:47 -0700 Subject: [PATCH 204/245] Provide a clearer UI for "view all results" in partial result panels Summary: In some cases, we show a limited number of one type of object somewhere else, like "Recent Such-And-Such" or "Herald Rules Which Use This" or whatever. We don't do a very good job of communicating that these are partial lists, or how to see all the results. Usually there's a button in the upper right, which is fine, but this could be better. Add an explicit "more stuff" button that shows up where a pager would appear and makes it clear that (a) the list is partial; and (b) you can click the button to see everything. Test Plan: {F6302793} Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Differential Revision: https://secure.phabricator.com/D20315 --- resources/celerity/map.php | 6 ++--- .../HarbormasterPlanViewController.php | 22 +++++++++++++++-- src/view/phui/PHUIObjectItemListView.php | 24 +++++++++++++++++++ .../phui/object-item/phui-oi-list-view.css | 6 +++++ 4 files changed, 53 insertions(+), 5 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 5dd506784a..7b39ee861a 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,7 +9,7 @@ return array( 'names' => array( 'conpherence.pkg.css' => '3c8a0668', 'conpherence.pkg.js' => '020aebcf', - 'core.pkg.css' => 'b797945d', + 'core.pkg.css' => '2dd936d6', 'core.pkg.js' => 'eb53fc5b', 'differential.pkg.css' => '8d8360fb', 'differential.pkg.js' => '67e02996', @@ -132,7 +132,7 @@ return array( 'rsrc/css/phui/object-item/phui-oi-color.css' => 'b517bfa0', 'rsrc/css/phui/object-item/phui-oi-drag-ui.css' => 'da15d3dc', 'rsrc/css/phui/object-item/phui-oi-flush-ui.css' => '490e2e2e', - 'rsrc/css/phui/object-item/phui-oi-list-view.css' => '909f3844', + 'rsrc/css/phui/object-item/phui-oi-list-view.css' => 'a65865a7', 'rsrc/css/phui/object-item/phui-oi-simple-ui.css' => '6a30fa46', 'rsrc/css/phui/phui-action-list.css' => 'c4972757', 'rsrc/css/phui/phui-action-panel.css' => '6c386cbf', @@ -853,7 +853,7 @@ return array( 'phui-oi-color-css' => 'b517bfa0', 'phui-oi-drag-ui-css' => 'da15d3dc', 'phui-oi-flush-ui-css' => '490e2e2e', - 'phui-oi-list-view-css' => '909f3844', + 'phui-oi-list-view-css' => 'a65865a7', 'phui-oi-simple-ui-css' => '6a30fa46', 'phui-pager-css' => 'd022c7ad', 'phui-pinboard-view-css' => '1f08f5d8', diff --git a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php index a9af90f2a5..4f2b70fcaf 100644 --- a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php @@ -455,12 +455,16 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController { private function newBuildsView(HarbormasterBuildPlan $plan) { $viewer = $this->getViewer(); + $limit = 10; $builds = id(new HarbormasterBuildQuery()) ->setViewer($viewer) ->withBuildPlanPHIDs(array($plan->getPHID())) - ->setLimit(10) + ->setLimit($limit + 1) ->execute(); + $more_results = (count($builds) > $limit); + $builds = array_slice($builds, 0, $limit); + $list = id(new HarbormasterBuildView()) ->setViewer($viewer) ->setBuilds($builds) @@ -472,6 +476,11 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController { $this->getApplicationURI('/build/'), array('plan' => $plan->getPHID())); + if ($more_results) { + $list->newTailButton() + ->setHref($more_href); + } + $more_link = id(new PHUIButtonView()) ->setTag('a') ->setIcon('fa-list-ul') @@ -491,14 +500,18 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController { private function newRulesView(HarbormasterBuildPlan $plan) { $viewer = $this->getViewer(); + $limit = 10; $rules = id(new HeraldRuleQuery()) ->setViewer($viewer) ->withDisabled(false) ->withAffectedObjectPHIDs(array($plan->getPHID())) ->needValidateAuthors(true) - ->setLimit(10) + ->setLimit($limit + 1) ->execute(); + $more_results = (count($rules) > $limit); + $rules = array_slice($rules, 0, $limit); + $list = id(new HeraldRuleListView()) ->setViewer($viewer) ->setRules($rules) @@ -510,6 +523,11 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController { '/herald/', array('affectedPHID' => $plan->getPHID())); + if ($more_results) { + $list->newTailButton() + ->setHref($more_href); + } + $more_link = id(new PHUIButtonView()) ->setTag('a') ->setIcon('fa-list-ul') diff --git a/src/view/phui/PHUIObjectItemListView.php b/src/view/phui/PHUIObjectItemListView.php index 53e86382c2..fbc3904586 100644 --- a/src/view/phui/PHUIObjectItemListView.php +++ b/src/view/phui/PHUIObjectItemListView.php @@ -12,6 +12,7 @@ final class PHUIObjectItemListView extends AphrontTagView { private $drag; private $allowEmptyList; private $itemClass = 'phui-oi-standard'; + private $tail = array(); public function setAllowEmptyList($allow_empty_list) { $this->allowEmptyList = $allow_empty_list; @@ -72,6 +73,18 @@ final class PHUIObjectItemListView extends AphrontTagView { return 'ul'; } + public function newTailButton() { + $button = id(new PHUIButtonView()) + ->setTag('a') + ->setColor(PHUIButtonView::GREY) + ->setIcon('fa-chevron-down') + ->setText(pht('View All Results')); + + $this->tail[] = $button; + + return $button; + } + protected function getTagAttributes() { $classes = array(); $classes[] = 'phui-oi-list-view'; @@ -149,9 +162,20 @@ final class PHUIObjectItemListView extends AphrontTagView { $pager = $this->pager; } + $tail = array(); + foreach ($this->tail as $tail_item) { + $tail[] = phutil_tag( + 'li', + array( + 'class' => 'phui-oi-tail', + ), + $tail_item); + } + return array( $header, $items, + $tail, $pager, $this->renderChildren(), ); diff --git a/webroot/rsrc/css/phui/object-item/phui-oi-list-view.css b/webroot/rsrc/css/phui/object-item/phui-oi-list-view.css index 6f2421ca2f..67d0682aa7 100644 --- a/webroot/rsrc/css/phui/object-item/phui-oi-list-view.css +++ b/webroot/rsrc/css/phui/object-item/phui-oi-list-view.css @@ -720,3 +720,9 @@ ul.phui-oi-list-view .phui-oi-selectable .differential-revision-small .phui-icon-view { color: #6699ba; } + +.phui-oi-tail { + text-align: center; + padding: 8px 0; + background: linear-gradient({$lightbluebackground}, #fff 66%, #fff); +} From 686b03a1d59eb2059af3a6075a7092b6156a6135 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 25 Mar 2019 08:04:33 -0700 Subject: [PATCH 205/245] Dim the action drop preview element when the cursor approaches Summary: Depends on D20308. Ref T5474. The element which previews what will happen when you drop a task somewhere can cover the bottom part of the rightmost column on a workboard. To fix this, I'm trying to just fade it out if you put your cursor over it. I tried to do this in a simple way previously (":hover" + "opacity: 0.25") but it doesn't actually work because "pointer-events: none" stops ":hover" from working. Instead, do this in Javascript. This is a little more complicated but: it works; and we can do the fade when you get //near// the element instead of actually over it, which feels a little better. Test Plan: - Shrank window to fairly small size so that the preview could cover up stuff on the workboard. - Dragged a card toward the rightmost column. - Before: drop action preview covered some workboard stuff. - After: preview faded out as my cursor approached. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T5474 Differential Revision: https://secure.phabricator.com/D20320 --- resources/celerity/map.php | 38 +++++++++--------- .../css/phui/workboards/phui-workpanel.css | 20 ++++++++-- .../js/application/projects/WorkboardBoard.js | 40 +++++++++++++++++++ 3 files changed, 75 insertions(+), 23 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 7b39ee861a..586e568f64 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -179,7 +179,7 @@ return array( 'rsrc/css/phui/workboards/phui-workboard-color.css' => 'e86de308', 'rsrc/css/phui/workboards/phui-workboard.css' => '74fc9d98', 'rsrc/css/phui/workboards/phui-workcard.css' => '9e9eb0df', - 'rsrc/css/phui/workboards/phui-workpanel.css' => '4e4ec9f0', + 'rsrc/css/phui/workboards/phui-workpanel.css' => 'f43b8c7f', 'rsrc/css/sprite-login.css' => '18b368a6', 'rsrc/css/sprite-tokens.css' => 'f1896dc5', 'rsrc/css/syntax/syntax-default.css' => '055fc231', @@ -409,7 +409,7 @@ return array( 'rsrc/js/application/phortune/phortune-credit-card-form.js' => 'd12d214f', 'rsrc/js/application/policy/behavior-policy-control.js' => '0eaa33a9', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '9347f172', - 'rsrc/js/application/projects/WorkboardBoard.js' => '3ba8e6ad', + 'rsrc/js/application/projects/WorkboardBoard.js' => '223af34e', 'rsrc/js/application/projects/WorkboardCard.js' => '0392a5d8', 'rsrc/js/application/projects/WorkboardCardTemplate.js' => '2a61f8d4', 'rsrc/js/application/projects/WorkboardColumn.js' => 'c3d24e63', @@ -737,7 +737,7 @@ return array( 'javelin-view-renderer' => '9aae2b66', 'javelin-view-visitor' => '308f9fe4', 'javelin-websocket' => 'fdc13e4e', - 'javelin-workboard-board' => '3ba8e6ad', + 'javelin-workboard-board' => '223af34e', 'javelin-workboard-card' => '0392a5d8', 'javelin-workboard-card-template' => '2a61f8d4', 'javelin-workboard-column' => 'c3d24e63', @@ -869,7 +869,7 @@ return array( 'phui-workboard-color-css' => 'e86de308', 'phui-workboard-view-css' => '74fc9d98', 'phui-workcard-view-css' => '9e9eb0df', - 'phui-workpanel-view-css' => '4e4ec9f0', + 'phui-workpanel-view-css' => 'f43b8c7f', 'phuix-action-list-view' => 'c68f183f', 'phuix-action-view' => 'aaa08f3b', 'phuix-autocomplete' => '8f139ef0', @@ -1073,6 +1073,18 @@ return array( 'javelin-behavior', 'javelin-request', ), + '223af34e' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-util', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + 'javelin-workboard-column', + 'javelin-workboard-header-template', + 'javelin-workboard-card-template', + 'javelin-workboard-order-template', + ), '225bbb98' => array( 'javelin-install', 'javelin-reactor', @@ -1227,18 +1239,6 @@ return array( 'javelin-behavior', 'phabricator-prefab', ), - '3ba8e6ad' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-util', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - 'javelin-workboard-column', - 'javelin-workboard-header-template', - 'javelin-workboard-card-template', - 'javelin-workboard-order-template', - ), '3dc5ad43' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1351,9 +1351,6 @@ return array( 'phuix-icon-view', 'javelin-behavior-phabricator-gesture', ), - '4e4ec9f0' => array( - 'phui-workcard-view-css', - ), '4e61fa88' => array( 'javelin-behavior', 'javelin-aphlict', @@ -2144,6 +2141,9 @@ return array( 'phabricator-darklog', 'phabricator-darkmessage', ), + 'f43b8c7f' => array( + 'phui-workcard-view-css', + ), 'f51e9c17' => 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 97600ff5e6..5c0a62282b 100644 --- a/webroot/rsrc/css/phui/workboards/phui-workpanel.css +++ b/webroot/rsrc/css/phui/workboards/phui-workpanel.css @@ -189,10 +189,7 @@ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15); border: 1px solid {$lightblueborder}; padding: 4px 0; -} - -.workboard-drop-preview:hover { - opacity: 0.25; + background: #fff; } .workboard-drop-preview li { @@ -225,3 +222,18 @@ background: {$blue}; color: #fff; } + +.workboard-drop-preview-fade { + animation: 0.1s workboard-drop-preview-fade-out; + opacity: 0.25; +} + +@keyframes workboard-drop-preview-fade-out { + 0% { + opacity: 1; + } + + 100% { + opacity: 0.25; + } +} diff --git a/webroot/rsrc/js/application/projects/WorkboardBoard.js b/webroot/rsrc/js/application/projects/WorkboardBoard.js index 6add658259..0cd8abb7d2 100644 --- a/webroot/rsrc/js/application/projects/WorkboardBoard.js +++ b/webroot/rsrc/js/application/projects/WorkboardBoard.js @@ -43,6 +43,8 @@ JX.install('WorkboardBoard', { _dropPreviewListNode: null, _previewPHID: null, _hidePreivew: false, + _previewPositionVector: null, + _previewDimState: false, getRoot: function() { return this._root; @@ -148,6 +150,39 @@ JX.install('WorkboardBoard', { var on_out = JX.bind(this, this._hideTriggerPreview); JX.Stratcom.listen('mouseover', 'trigger-preview', on_over); JX.Stratcom.listen('mouseout', 'trigger-preview', on_out); + + var on_move = JX.bind(this, this._dimPreview); + JX.Stratcom.listen('mousemove', null, on_move); + }, + + _dimPreview: function(e) { + var p = this._previewPositionVector; + if (!p) { + return; + } + + // When the mouse cursor gets near the drop preview element, fade it + // out so you can see through it. We can't do this with ":hover" because + // we disable cursor events. + + var cursor = JX.$V(e); + var margin = 64; + + var near_x = (cursor.x > (p.x - margin)); + var near_y = (cursor.y > (p.y - margin)); + var should_dim = (near_x && near_y); + + this._setPreviewDimState(should_dim); + }, + + _setPreviewDimState: function(is_dim) { + if (is_dim === this._previewDimState) { + return; + } + + this._previewDimState = is_dim; + var node = this._getDropPreviewNode(); + JX.DOM.alterClass(node, 'workboard-drop-preview-fade', is_dim); }, _showTriggerPreview: function(e) { @@ -325,6 +360,7 @@ JX.install('WorkboardBoard', { if (!effects.length) { JX.DOM.remove(node); + this._previewPositionVector = null; return; } @@ -336,6 +372,10 @@ JX.install('WorkboardBoard', { JX.DOM.setContent(this._getDropPreviewListNode(), items); document.body.appendChild(node); + + // Undim the drop preview element if it was previously dimmed. + this._setPreviewDimState(false); + this._previewPositionVector = JX.$V(node); }, _getDropPreviewNode: function() { From 6182193cf55a8eeb5fc75e54916dc90ccd944c88 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 25 Mar 2019 14:31:21 -0700 Subject: [PATCH 206/245] Give the "Code" tab in Diffusion more consistent (path-retaining) behavior Summary: Fixes T13270. In Diffusion, the "Code" tab is linked in a weird way that isn't consistent with the other tabs. Particularly, if you navigate to `x/y/z/` and toggle between the "Branches" and "History" tabs (or other tabs), you keep your path. If you click "Code", you lose your path. Instead, retain the path, so you can navigate somewhere and then toggle to/from the "Code" tab to get different views of the same path. Test Plan: Browed into a repository, clicked "History", clicked "Code", ended up back in the place I started. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13270 Differential Revision: https://secure.phabricator.com/D20323 --- src/applications/diffusion/controller/DiffusionController.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/applications/diffusion/controller/DiffusionController.php b/src/applications/diffusion/controller/DiffusionController.php index a220ac05e0..5f4c304ebc 100644 --- a/src/applications/diffusion/controller/DiffusionController.php +++ b/src/applications/diffusion/controller/DiffusionController.php @@ -512,8 +512,7 @@ abstract class DiffusionController extends PhabricatorController { ->setIcon('fa-code') ->setHref($drequest->generateURI( array( - 'action' => 'branch', - 'path' => '/', + 'action' => 'browse', ))) ->setSelected($key == 'code')); From 6138e50962e62316f270a7a25c272eb47a92160a Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 25 Mar 2019 10:12:10 -0700 Subject: [PATCH 207/245] When moving cards on workboards, treat before/after cards as optional hints, not strict requirements Summary: Depends on D20320. Ref T12175. Ref T13074. Currently, when you move a card between columns, the internal transaction takes exactly one `afterPHID` or `beforePHID` and moves the card before or after the specified card. This is a fairly strict interpretation and causes a number of practical issues, mostly because the user/client view of the board may be out of date and the card they're dragging before or after may no longer exist: another user might have moved or hidden it between the last client update and the current time. In T13074, we also run into a more subtle issue where a card that incorrectly appears in multiple columns fatals when dropped before or after itself. In all cases, a better behavior is just to complete the move and accept that the position may not end up exactly like the user specified. We could prompt the user instead: > You tried to drop this card after card X, but that card has moved since you last loaded the board. Reload the board and try again. ...but this is pretty hostile and probably rarely/never what the user wants. Instead, accept a list of before/after PHIDs and just try them until we find one that works, or accept a default position if none work. In essentially all cases, this means that the move "just works" like users expect it to instead of fataling in a confusing/disruptive/undesirable (but "technically correct") way. (A followup will make the client JS send more beforePHIDs/afterPHIDs so this works more often.) We could eventually add a "strict" mode in the API or something if there's some bot/API use case for precise behavior here, but I suspect none exist today or are (ever?) likely to exist in the future. Test Plan: - (T13074) Inserted two conflicting rows to put a card on two columns on the same board. Dropped one version of it underneath the other version. Before: confusing fatal. After: cards merge sensibly into one consistent card. - (T12175) Opened two views of a board. Moved card A to a different column on the first view. On the second view, dropped card B under card A (still showing in the old column). Before: confusing fatal. After: card ended up in the right column in approximately the right place, very reasonably. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13074, T12175 Differential Revision: https://secure.phabricator.com/D20321 --- resources/celerity/map.php | 28 +-- .../maniphest/editor/ManiphestEditEngine.php | 15 +- .../editor/ManiphestTransactionEditor.php | 166 +++++++++--------- .../PhabricatorProjectCoreTestCase.php | 18 +- .../PhabricatorProjectMoveController.php | 18 +- .../engine/PhabricatorBoardLayoutEngine.php | 136 +++++++------- .../js/application/projects/WorkboardBoard.js | 2 +- 7 files changed, 194 insertions(+), 189 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 586e568f64..972a5c2b56 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -409,7 +409,7 @@ return array( 'rsrc/js/application/phortune/phortune-credit-card-form.js' => 'd12d214f', 'rsrc/js/application/policy/behavior-policy-control.js' => '0eaa33a9', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '9347f172', - 'rsrc/js/application/projects/WorkboardBoard.js' => '223af34e', + 'rsrc/js/application/projects/WorkboardBoard.js' => '106d870f', 'rsrc/js/application/projects/WorkboardCard.js' => '0392a5d8', 'rsrc/js/application/projects/WorkboardCardTemplate.js' => '2a61f8d4', 'rsrc/js/application/projects/WorkboardColumn.js' => 'c3d24e63', @@ -737,7 +737,7 @@ return array( 'javelin-view-renderer' => '9aae2b66', 'javelin-view-visitor' => '308f9fe4', 'javelin-websocket' => 'fdc13e4e', - 'javelin-workboard-board' => '223af34e', + 'javelin-workboard-board' => '106d870f', 'javelin-workboard-card' => '0392a5d8', 'javelin-workboard-card-template' => '2a61f8d4', 'javelin-workboard-column' => 'c3d24e63', @@ -1015,6 +1015,18 @@ return array( 'javelin-workflow', 'phuix-icon-view', ), + '106d870f' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-util', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + 'javelin-workboard-column', + 'javelin-workboard-header-template', + 'javelin-workboard-card-template', + 'javelin-workboard-order-template', + ), '111bfd2d' => array( 'javelin-install', ), @@ -1073,18 +1085,6 @@ return array( 'javelin-behavior', 'javelin-request', ), - '223af34e' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-util', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - 'javelin-workboard-column', - 'javelin-workboard-header-template', - 'javelin-workboard-card-template', - 'javelin-workboard-order-template', - ), '225bbb98' => array( 'javelin-install', 'javelin-reactor', diff --git a/src/applications/maniphest/editor/ManiphestEditEngine.php b/src/applications/maniphest/editor/ManiphestEditEngine.php index 76c2276df0..2a8730d5c6 100644 --- a/src/applications/maniphest/editor/ManiphestEditEngine.php +++ b/src/applications/maniphest/editor/ManiphestEditEngine.php @@ -123,22 +123,23 @@ information about the move, including an optional specific position within the column. The target column should be identified as `columnPHID`, and you may select a -position by passing either `beforePHID` or `afterPHID`, specifying the PHID of -a task currently in the column that you want to move this task before or after: +position by passing either `beforePHIDs` or `afterPHIDs`, specifying the PHIDs +of tasks currently in the column that you want to move this task before or +after: ```lang=json [ { "columnPHID": "PHID-PCOL-4444", - "beforePHID": "PHID-TASK-5555" + "beforePHIDs": ["PHID-TASK-5555"] } ] ``` -Note that this affects only the "natural" position of the task. The task -position when the board is sorted by some other attribute (like priority) -depends on that attribute value: change a task's priority to move it on -priority-sorted boards. +When you specify multiple PHIDs, the task will be moved adjacent to the first +valid PHID found in either of the lists. This allows positional moves to +generally work as users expect even if the client view of the board has fallen +out of date and some of the nearby tasks have moved elsewhere. EODOCS ); diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php index 5cb7cf91ba..5198f44572 100644 --- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php +++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php @@ -428,6 +428,7 @@ final class ManiphestTransactionEditor private function buildMoveTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { + $actor = $this->getActor(); $new = $xaction->getNewValue(); if (!is_array($new)) { @@ -435,7 +436,7 @@ final class ManiphestTransactionEditor $new = array($new); } - $nearby_phids = array(); + $relative_phids = array(); foreach ($new as $key => $value) { if (!is_array($value)) { $this->validateColumnPHID($value); @@ -448,35 +449,83 @@ final class ManiphestTransactionEditor $value, array( 'columnPHID' => 'string', + 'beforePHIDs' => 'optional list', + 'afterPHIDs' => 'optional list', + + // Deprecated older variations of "beforePHIDs" and "afterPHIDs". 'beforePHID' => 'optional string', 'afterPHID' => 'optional string', )); - $new[$key] = $value; - - if (!empty($value['beforePHID'])) { - $nearby_phids[] = $value['beforePHID']; - } + $value = $value + array( + 'beforePHIDs' => array(), + 'afterPHIDs' => array(), + ); + // Normalize the legacy keys "beforePHID" and "afterPHID" keys to the + // modern format. if (!empty($value['afterPHID'])) { - $nearby_phids[] = $value['afterPHID']; + if ($value['afterPHIDs']) { + throw new Exception( + pht( + 'Transaction specifies both "afterPHID" and "afterPHIDs". '. + 'Specify only "afterPHIDs".')); + } + $value['afterPHIDs'] = array($value['afterPHID']); + unset($value['afterPHID']); } + + if (isset($value['beforePHID'])) { + if ($value['beforePHIDs']) { + throw new Exception( + pht( + 'Transaction specifies both "beforePHID" and "beforePHIDs". '. + 'Specify only "beforePHIDs".')); + } + $value['beforePHIDs'] = array($value['beforePHID']); + unset($value['beforePHID']); + } + + foreach ($value['beforePHIDs'] as $phid) { + $relative_phids[] = $phid; + } + + foreach ($value['afterPHIDs'] as $phid) { + $relative_phids[] = $phid; + } + + $new[$key] = $value; } - if ($nearby_phids) { - $nearby_objects = id(new PhabricatorObjectQuery()) - ->setViewer($this->getActor()) - ->withPHIDs($nearby_phids) + // We require that objects you specify in "beforePHIDs" or "afterPHIDs" + // are real objects which exist and which you have permission to view. + // If you provide other objects, we remove them from the specification. + + if ($relative_phids) { + $objects = id(new PhabricatorObjectQuery()) + ->setViewer($actor) + ->withPHIDs($relative_phids) ->execute(); - $nearby_objects = mpull($nearby_objects, null, 'getPHID'); + $objects = mpull($objects, null, 'getPHID'); } else { - $nearby_objects = array(); + $objects = array(); + } + + foreach ($new as $key => $value) { + $value['afterPHIDs'] = $this->filterValidPHIDs( + $value['afterPHIDs'], + $objects); + $value['beforePHIDs'] = $this->filterValidPHIDs( + $value['beforePHIDs'], + $objects); + + $new[$key] = $value; } $column_phids = ipull($new, 'columnPHID'); if ($column_phids) { $columns = id(new PhabricatorProjectColumnQuery()) - ->setViewer($this->getActor()) + ->setViewer($actor) ->withPHIDs($column_phids) ->execute(); $columns = mpull($columns, null, 'getPHID'); @@ -487,10 +536,9 @@ final class ManiphestTransactionEditor $board_phids = mpull($columns, 'getProjectPHID'); $object_phid = $object->getPHID(); - $object_phids = $nearby_phids; - // Note that we may not have an object PHID if we're creating a new // object. + $object_phids = array(); if ($object_phid) { $object_phids[] = $object_phid; } @@ -517,49 +565,6 @@ final class ManiphestTransactionEditor $board_phid = $column->getProjectPHID(); - $nearby = array(); - - if (!empty($spec['beforePHID'])) { - $nearby['beforePHID'] = $spec['beforePHID']; - } - - if (!empty($spec['afterPHID'])) { - $nearby['afterPHID'] = $spec['afterPHID']; - } - - if (count($nearby) > 1) { - throw new Exception( - pht( - 'Column move transaction moves object to multiple positions. '. - 'Specify only "beforePHID" or "afterPHID", not both.')); - } - - foreach ($nearby as $where => $nearby_phid) { - if (empty($nearby_objects[$nearby_phid])) { - throw new Exception( - pht( - 'Column move transaction specifies object "%s" as "%s", but '. - 'there is no corresponding object with this PHID.', - $object_phid, - $where)); - } - - $nearby_columns = $layout_engine->getObjectColumns( - $board_phid, - $nearby_phid); - $nearby_columns = mpull($nearby_columns, null, 'getPHID'); - - if (empty($nearby_columns[$column_phid])) { - throw new Exception( - pht( - 'Column move transaction specifies object "%s" as "%s" in '. - 'column "%s", but this object is not in that column!', - $nearby_phid, - $where, - $column_phid)); - } - } - if ($object_phid) { $old_columns = $layout_engine->getObjectColumns( $board_phid, @@ -578,8 +583,8 @@ final class ManiphestTransactionEditor // We can just drop this column change if it has no effect. $from_map = array_fuse($spec['fromColumnPHIDs']); $already_here = isset($from_map[$column_phid]); - $is_reordering = (bool)$nearby; + $is_reordering = ($spec['afterPHIDs'] || $spec['beforePHIDs']); if ($already_here && !$is_reordering) { unset($new[$key]); } else { @@ -677,8 +682,9 @@ final class ManiphestTransactionEditor private function applyBoardMove($object, array $move) { $board_phid = $move['boardPHID']; $column_phid = $move['columnPHID']; - $before_phid = idx($move, 'beforePHID'); - $after_phid = idx($move, 'afterPHID'); + + $before_phids = $move['beforePHIDs']; + $after_phids = $move['afterPHIDs']; $object_phid = $object->getPHID(); @@ -730,24 +736,12 @@ final class ManiphestTransactionEditor $object_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->queueAddPosition( + $board_phid, + $column_phid, + $object_phid, + $after_phids, + $before_phids); $engine->applyPositionUpdates(); } @@ -849,4 +843,16 @@ final class ManiphestTransactionEditor return $errors; } + private function filterValidPHIDs($phid_list, array $object_map) { + foreach ($phid_list as $key => $phid) { + if (isset($object_map[$phid])) { + continue; + } + + unset($phid_list[$key]); + } + + return array_values($phid_list); + } + } diff --git a/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php b/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php index e50c83ab5a..186ac7dea4 100644 --- a/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php +++ b/src/applications/project/__tests__/PhabricatorProjectCoreTestCase.php @@ -1008,29 +1008,32 @@ final class PhabricatorProjectCoreTestCase extends PhabricatorTestCase { $task2->getPHID(), $task1->getPHID(), ); - $this->assertTasksInColumn($expect, $user, $board, $column); + $label = pht('Simple move'); + $this->assertTasksInColumn($expect, $user, $board, $column, $label); // Move the second task after the first task. $options = array( - 'afterPHID' => $task1->getPHID(), + 'afterPHIDs' => array($task1->getPHID()), ); $this->moveToColumn($user, $board, $task2, $column, $column, $options); $expect = array( $task1->getPHID(), $task2->getPHID(), ); - $this->assertTasksInColumn($expect, $user, $board, $column); + $label = pht('With afterPHIDs'); + $this->assertTasksInColumn($expect, $user, $board, $column, $label); // Move the second task before the first task. $options = array( - 'beforePHID' => $task1->getPHID(), + 'beforePHIDs' => array($task1->getPHID()), ); $this->moveToColumn($user, $board, $task2, $column, $column, $options); $expect = array( $task2->getPHID(), $task1->getPHID(), ); - $this->assertTasksInColumn($expect, $user, $board, $column); + $label = pht('With beforePHIDs'); + $this->assertTasksInColumn($expect, $user, $board, $column, $label); } public function testMilestoneMoves() { @@ -1333,7 +1336,8 @@ final class PhabricatorProjectCoreTestCase extends PhabricatorTestCase { array $expect, PhabricatorUser $viewer, PhabricatorProject $board, - PhabricatorProjectColumn $column) { + PhabricatorProjectColumn $column, + $label = null) { $engine = id(new PhabricatorBoardLayoutEngine()) ->setViewer($viewer) @@ -1346,7 +1350,7 @@ final class PhabricatorProjectCoreTestCase extends PhabricatorTestCase { $column->getPHID()); $object_phids = array_values($object_phids); - $this->assertEqual($expect, $object_phids); + $this->assertEqual($expect, $object_phids, $label); } private function addColumn( diff --git a/src/applications/project/controller/PhabricatorProjectMoveController.php b/src/applications/project/controller/PhabricatorProjectMoveController.php index 950b1e90cd..273a068c06 100644 --- a/src/applications/project/controller/PhabricatorProjectMoveController.php +++ b/src/applications/project/controller/PhabricatorProjectMoveController.php @@ -11,9 +11,20 @@ final class PhabricatorProjectMoveController $column_phid = $request->getStr('columnPHID'); $object_phid = $request->getStr('objectPHID'); + $after_phid = $request->getStr('afterPHID'); $before_phid = $request->getStr('beforePHID'); + $after_phids = array(); + if ($after_phid) { + $after_phids[] = $after_phid; + } + + $before_phids = array(); + if ($before_phid) { + $before_phids[] = $before_phid; + } + $order = $request->getStr('order'); if (!strlen($order)) { $order = PhabricatorProjectColumnNaturalOrder::ORDERKEY; @@ -89,9 +100,10 @@ final class PhabricatorProjectMoveController $order_params = array(); if ($after_phid) { - $order_params['afterPHID'] = $after_phid; - } else if ($before_phid) { - $order_params['beforePHID'] = $before_phid; + $order_params['afterPHIDs'] = $after_phids; + } + if ($before_phid) { + $order_params['beforePHIDs'] = $before_phids; } $xactions = array(); diff --git a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php index dcbe2d4ee4..e614ec2f94 100644 --- a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php +++ b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php @@ -135,52 +135,12 @@ final class PhabricatorBoardLayoutEngine extends Phobject { 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) { + array $after_phids, + array $before_phids) { $board_layout = idx($this->boardLayout, $board_phid, array()); $positions = idx($board_layout, $column_phid, array()); @@ -196,54 +156,76 @@ final class PhabricatorBoardLayoutEngine extends Phobject { ->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); + // The user's view of the board may fall out of date, so they might + // try to drop a card under a different card which is no longer where + // they thought it was. + + // When this happens, we perform the move anyway, since this is almost + // certainly what users want when interacting with the UI. We'l try to + // fall back to another nearby card if the client provided us one. If + // we don't find any of the cards the client specified in the column, + // we'll just move the card to the default position. + + $search_phids = array(); + foreach ($after_phids as $after_phid) { + $search_phids[] = array($after_phid, false); + } + + foreach ($before_phids as $before_phid) { + $search_phids[] = array($before_phid, true); + } + + // This makes us fall back to the default position if we fail every + // candidate position. The default position counts as a "before" position + // because we want to put the new card at the top of the column. + $search_phids[] = array(null, true); + + $found = false; + foreach ($search_phids as $search_position) { + list($relative_phid, $is_before) = $search_position; + 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 ($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 ($found) { - $position->setSequence($sequence++); - $this->addQueue[] = $position; + break; } } } - 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; diff --git a/webroot/rsrc/js/application/projects/WorkboardBoard.js b/webroot/rsrc/js/application/projects/WorkboardBoard.js index 0cd8abb7d2..7f1383ac64 100644 --- a/webroot/rsrc/js/application/projects/WorkboardBoard.js +++ b/webroot/rsrc/js/application/projects/WorkboardBoard.js @@ -495,7 +495,7 @@ JX.install('WorkboardBoard', { order: this.getOrder() }; - var context = this._getDropContext(after_node); + var context = this._getDropContext(after_node, item); if (context.afterPHID) { data.afterPHID = context.afterPHID; From 71c89bd0578d8380d7718c4e43bfa80eb5af3bb8 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 25 Mar 2019 09:37:02 -0700 Subject: [PATCH 208/245] Pass all adjacent card PHIDs from the client to the server when moving a card Summary: Depends on D20321. Fixes T12175. Ref T13074. Now that before/after PHIDs are suggestions, we can give the server a more complete view of what the client is trying to do so we're more likely to get a good outcome if the client view is out of date. Instead of passing only the one directly adjacent card PHID, pass all the card PHIDs that the client thinks are in the same group. (For gigantic columns with tens of thousands of tasks this might need some tweaking -- like, slice both lists down to 10 items -- but we can cross that bridge when we come to it.) Test Plan: - Dragged some cards around to top/bottom/middle positions, saw good positioning in all cases. - In two windows, dragged stuff around on the same board. At least at first glance, conflicting simultaneous edits seemed to do reasonable things. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13074, T12175 Differential Revision: https://secure.phabricator.com/D20322 --- resources/celerity/map.php | 28 +++++------ .../PhabricatorProjectMoveController.php | 25 +++------- .../js/application/projects/WorkboardBoard.js | 47 ++++++++----------- 3 files changed, 40 insertions(+), 60 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 972a5c2b56..12c34cd332 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -409,7 +409,7 @@ return array( 'rsrc/js/application/phortune/phortune-credit-card-form.js' => 'd12d214f', 'rsrc/js/application/policy/behavior-policy-control.js' => '0eaa33a9', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '9347f172', - 'rsrc/js/application/projects/WorkboardBoard.js' => '106d870f', + 'rsrc/js/application/projects/WorkboardBoard.js' => 'c02a5497', 'rsrc/js/application/projects/WorkboardCard.js' => '0392a5d8', 'rsrc/js/application/projects/WorkboardCardTemplate.js' => '2a61f8d4', 'rsrc/js/application/projects/WorkboardColumn.js' => 'c3d24e63', @@ -737,7 +737,7 @@ return array( 'javelin-view-renderer' => '9aae2b66', 'javelin-view-visitor' => '308f9fe4', 'javelin-websocket' => 'fdc13e4e', - 'javelin-workboard-board' => '106d870f', + 'javelin-workboard-board' => 'c02a5497', 'javelin-workboard-card' => '0392a5d8', 'javelin-workboard-card-template' => '2a61f8d4', 'javelin-workboard-column' => 'c3d24e63', @@ -1015,18 +1015,6 @@ return array( 'javelin-workflow', 'phuix-icon-view', ), - '106d870f' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-util', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - 'javelin-workboard-column', - 'javelin-workboard-header-template', - 'javelin-workboard-card-template', - 'javelin-workboard-order-template', - ), '111bfd2d' => array( 'javelin-install', ), @@ -1940,6 +1928,18 @@ return array( 'bde53589' => array( 'phui-inline-comment-view-css', ), + 'c02a5497' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-util', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + 'javelin-workboard-column', + 'javelin-workboard-header-template', + 'javelin-workboard-card-template', + 'javelin-workboard-order-template', + ), 'c03f2fb4' => array( 'javelin-install', ), diff --git a/src/applications/project/controller/PhabricatorProjectMoveController.php b/src/applications/project/controller/PhabricatorProjectMoveController.php index 273a068c06..1fd8b3c677 100644 --- a/src/applications/project/controller/PhabricatorProjectMoveController.php +++ b/src/applications/project/controller/PhabricatorProjectMoveController.php @@ -12,18 +12,8 @@ final class PhabricatorProjectMoveController $column_phid = $request->getStr('columnPHID'); $object_phid = $request->getStr('objectPHID'); - $after_phid = $request->getStr('afterPHID'); - $before_phid = $request->getStr('beforePHID'); - - $after_phids = array(); - if ($after_phid) { - $after_phids[] = $after_phid; - } - - $before_phids = array(); - if ($before_phid) { - $before_phids[] = $before_phid; - } + $after_phids = $request->getStrList('afterPHIDs'); + $before_phids = $request->getStrList('beforePHIDs'); $order = $request->getStr('order'); if (!strlen($order)) { @@ -98,13 +88,10 @@ final class PhabricatorProjectMoveController ->setObjectPHIDs(array($object_phid)) ->executeLayout(); - $order_params = array(); - if ($after_phid) { - $order_params['afterPHIDs'] = $after_phids; - } - if ($before_phid) { - $order_params['beforePHIDs'] = $before_phids; - } + $order_params = array( + 'afterPHIDs' => $after_phids, + 'beforePHIDs' => $before_phids, + ); $xactions = array(); $xactions[] = id(new ManiphestTransaction()) diff --git a/webroot/rsrc/js/application/projects/WorkboardBoard.js b/webroot/rsrc/js/application/projects/WorkboardBoard.js index 7f1383ac64..74c0bdf23e 100644 --- a/webroot/rsrc/js/application/projects/WorkboardBoard.js +++ b/webroot/rsrc/js/application/projects/WorkboardBoard.js @@ -409,8 +409,8 @@ JX.install('WorkboardBoard', { _getDropContext: function(after_node, item) { var header_key; - var before_phid; - var after_phid; + var after_phids = []; + var before_phids = []; // We're going to send an "afterPHID" and a "beforePHID" if the card // was dropped immediately adjacent to another card. If a card was @@ -424,19 +424,16 @@ JX.install('WorkboardBoard', { var after_card = after_node; while (after_card) { after_data = JX.Stratcom.getData(after_card); - if (after_data.objectPHID) { - break; - } + if (after_data.headerKey) { break; } - after_card = after_card.previousSibling; - } - if (after_data) { if (after_data.objectPHID) { - after_phid = after_data.objectPHID; + after_phids.push(after_data.objectPHID); } + + after_card = after_card.previousSibling; } if (item) { @@ -444,19 +441,16 @@ JX.install('WorkboardBoard', { var before_card = item.nextSibling; while (before_card) { before_data = JX.Stratcom.getData(before_card); - if (before_data.objectPHID) { - break; - } + if (before_data.headerKey) { break; } - before_card = before_card.nextSibling; - } - if (before_data) { if (before_data.objectPHID) { - before_phid = before_data.objectPHID; + before_phids.push(before_data.objectPHID); } + + before_card = before_card.nextSibling; } } @@ -476,8 +470,8 @@ JX.install('WorkboardBoard', { return { headerKey: header_key, - afterPHID: after_phid, - beforePHID: before_phid + afterPHIDs: after_phids, + beforePHIDs: before_phids }; }, @@ -496,14 +490,8 @@ JX.install('WorkboardBoard', { }; var context = this._getDropContext(after_node, item); - - if (context.afterPHID) { - data.afterPHID = context.afterPHID; - } - - if (context.beforePHID) { - data.beforePHID = context.beforePHID; - } + data.afterPHIDs = context.afterPHIDs.join(','); + data.beforePHIDs = context.beforePHIDs.join(','); if (context.headerKey) { var properties = this.getHeaderTemplate(context.headerKey) @@ -530,13 +518,18 @@ JX.install('WorkboardBoard', { src_phid, dst_phid); + var after_phid = null; + if (data.afterPHIDs.length) { + after_phid = data.afterPHIDs[0]; + } + var onupdate = JX.bind( this, this._oncardupdate, list, src_phid, dst_phid, - data.afterPHID); + after_phid); new JX.Workflow(this.getController().getMoveURI(), data) .setHandler(onupdate) From d347b102a1ede10cf20324e7d70cd4c6615907c6 Mon Sep 17 00:00:00 2001 From: Austin McKinley Date: Tue, 26 Mar 2019 11:30:59 -0700 Subject: [PATCH 209/245] Add workboard trigger rule for changing task priority Summary: This is a copy/paste/find-and-replace-all of the status rule added by D20288. Test Plan: Made some triggers, moved some tasks, edited some triggers. Grepped for the word "status" in the new file. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D20325 --- src/__phutil_library_map__.php | 2 + ...torProjectTriggerManiphestPriorityRule.php | 94 +++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 src/applications/project/trigger/PhabricatorProjectTriggerManiphestPriorityRule.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index ba02f45406..33f2cf4d33 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4180,6 +4180,7 @@ phutil_register_library_map(array( 'PhabricatorProjectTriggerEditor' => 'applications/project/editor/PhabricatorProjectTriggerEditor.php', 'PhabricatorProjectTriggerInvalidRule' => 'applications/project/trigger/PhabricatorProjectTriggerInvalidRule.php', 'PhabricatorProjectTriggerListController' => 'applications/project/controller/trigger/PhabricatorProjectTriggerListController.php', + 'PhabricatorProjectTriggerManiphestPriorityRule' => 'applications/project/trigger/PhabricatorProjectTriggerManiphestPriorityRule.php', 'PhabricatorProjectTriggerManiphestStatusRule' => 'applications/project/trigger/PhabricatorProjectTriggerManiphestStatusRule.php', 'PhabricatorProjectTriggerNameTransaction' => 'applications/project/xaction/trigger/PhabricatorProjectTriggerNameTransaction.php', 'PhabricatorProjectTriggerPHIDType' => 'applications/project/phid/PhabricatorProjectTriggerPHIDType.php', @@ -10318,6 +10319,7 @@ phutil_register_library_map(array( 'PhabricatorProjectTriggerEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorProjectTriggerInvalidRule' => 'PhabricatorProjectTriggerRule', 'PhabricatorProjectTriggerListController' => 'PhabricatorProjectTriggerController', + 'PhabricatorProjectTriggerManiphestPriorityRule' => 'PhabricatorProjectTriggerRule', 'PhabricatorProjectTriggerManiphestStatusRule' => 'PhabricatorProjectTriggerRule', 'PhabricatorProjectTriggerNameTransaction' => 'PhabricatorProjectTriggerTransactionType', 'PhabricatorProjectTriggerPHIDType' => 'PhabricatorPHIDType', diff --git a/src/applications/project/trigger/PhabricatorProjectTriggerManiphestPriorityRule.php b/src/applications/project/trigger/PhabricatorProjectTriggerManiphestPriorityRule.php new file mode 100644 index 0000000000..98a03a1393 --- /dev/null +++ b/src/applications/project/trigger/PhabricatorProjectTriggerManiphestPriorityRule.php @@ -0,0 +1,94 @@ +newTransaction() + ->setTransactionType(ManiphestTaskPriorityTransaction::TRANSACTIONTYPE) + ->setNewValue($value), + ); + } + + protected function newDropEffects($value) { + $priority_name = ManiphestTaskPriority::getTaskPriorityName($value); + $priority_icon = ManiphestTaskPriority::getTaskPriorityIcon($value); + $priority_color = ManiphestTaskPriority::getTaskPriorityColor($value); + + $content = pht( + 'Change priority to %s.', + phutil_tag('strong', array(), $priority_name)); + + return array( + $this->newEffect() + ->setIcon($priority_icon) + ->setColor($priority_color) + ->addCondition('priority', '!=', $value) + ->setContent($content), + ); + } + + protected function getDefaultValue() { + return head_key(ManiphestTaskPriority::getTaskPriorityMap()); + } + + protected function getPHUIXControlType() { + return 'select'; + } + + protected function getPHUIXControlSpecification() { + $map = ManiphestTaskPriority::getTaskPriorityMap(); + + return array( + 'options' => $map, + 'order' => array_keys($map), + ); + } + + public function getRuleViewLabel() { + return pht('Change Priority'); + } + + public function getRuleViewDescription($value) { + $priority_name = ManiphestTaskPriority::getTaskPriorityName($value); + + return pht( + 'Change task priority to %s.', + phutil_tag('strong', array(), $priority_name)); + } + + public function getRuleViewIcon($value) { + $priority_icon = ManiphestTaskPriority::getTaskPriorityIcon($value); + $priority_color = ManiphestTaskPriority::getTaskPriorityColor($value); + + return id(new PHUIIconView()) + ->setIcon($priority_icon, $priority_color); + } + + +} From f6658bf391d3cd654cf25f47bd547bfc17f9457a Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 26 Mar 2019 11:44:55 -0700 Subject: [PATCH 210/245] When changing the trigger type in the trigger editor, properly redraw the control Summary: Ref T13269. I refactored this late in the game to organize things better and add table cells around stuff, and accidentally broke the relationship between the "Rule Type" selector and the value selector. Test Plan: Switched rule type selector from "Change Status" to "Play Sound", saw secondary control update properly. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13269 Differential Revision: https://secure.phabricator.com/D20326 --- resources/celerity/map.php | 4 ++-- webroot/rsrc/js/application/trigger/TriggerRule.js | 8 +++----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 12c34cd332..7f5c8ff45e 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -433,7 +433,7 @@ return array( 'rsrc/js/application/transactions/behavior-show-older-transactions.js' => '600f440c', 'rsrc/js/application/transactions/behavior-transaction-comment-form.js' => '2bdadf1a', 'rsrc/js/application/transactions/behavior-transaction-list.js' => '9cec214e', - 'rsrc/js/application/trigger/TriggerRule.js' => 'e4a816a4', + 'rsrc/js/application/trigger/TriggerRule.js' => '1c60c3fc', 'rsrc/js/application/trigger/TriggerRuleControl.js' => '5faf27b9', 'rsrc/js/application/trigger/TriggerRuleEditor.js' => 'b49fd60c', 'rsrc/js/application/trigger/TriggerRuleType.js' => '4feea7d3', @@ -894,7 +894,7 @@ return array( 'syntax-default-css' => '055fc231', 'syntax-highlighting-css' => '4234f572', 'tokens-css' => 'ce5a50bd', - 'trigger-rule' => 'e4a816a4', + 'trigger-rule' => '1c60c3fc', 'trigger-rule-control' => '5faf27b9', 'trigger-rule-editor' => 'b49fd60c', 'trigger-rule-type' => '4feea7d3', diff --git a/webroot/rsrc/js/application/trigger/TriggerRule.js b/webroot/rsrc/js/application/trigger/TriggerRule.js index 69feeade18..cf117e24d9 100644 --- a/webroot/rsrc/js/application/trigger/TriggerRule.js +++ b/webroot/rsrc/js/application/trigger/TriggerRule.js @@ -84,8 +84,8 @@ JX.install('TriggerRule', { control.value = this.getType(); - var on_change = JX.bind(this, this._onTypeChange); - JX.DOM.listen(control, 'onchange', null, on_change); + var on_change = JX.bind(this, this._onTypeChange, control); + JX.DOM.listen(control, 'change', null, on_change); var attributes = { className: 'type-cell' @@ -97,10 +97,8 @@ JX.install('TriggerRule', { return this._typeCell; }, - _onTypeChange: function() { - var control = this._getTypeCell(); + _onTypeChange: function(control) { this.setType(control.value); - this._rebuildValueControl(); }, From b328af0a1b16bf169d53d607624ec429f7764740 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 26 Mar 2019 14:23:00 -0700 Subject: [PATCH 211/245] Raise a more tailored exception if transform/thumbnail support is missing for cover images Summary: If "GD" doesn't support a particular image type, applying a cover image currently goes through but no-ops. Fail it earlier in the process with a more specific error. Test Plan: Without PNG support locally, dropped a PNG onto a card on a workboard. Got a more useful error. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20328 --- .../ManiphestTaskCoverImageTransaction.php | 36 ++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/src/applications/maniphest/xaction/ManiphestTaskCoverImageTransaction.php b/src/applications/maniphest/xaction/ManiphestTaskCoverImageTransaction.php index eb29c711f8..5e6f63c5c4 100644 --- a/src/applications/maniphest/xaction/ManiphestTaskCoverImageTransaction.php +++ b/src/applications/maniphest/xaction/ManiphestTaskCoverImageTransaction.php @@ -82,17 +82,35 @@ final class ManiphestTaskCoverImageTransaction if (!$file) { $errors[] = $this->newInvalidError( - pht('"%s" is not a valid file PHID.', - $file_phid)); - } else { - if (!$file->isViewableImage()) { - $mime_type = $file->getMimeType(); - $errors[] = $this->newInvalidError( - pht('File mime type of "%s" is not a valid viewable image.', - $mime_type)); - } + pht( + 'File PHID ("%s") is invalid, or you do not have permission '. + 'to view it.', + $file_phid), + $xaction); + continue; } + if (!$file->isViewableImage()) { + $errors[] = $this->newInvalidError( + pht( + 'File ("%s", with MIME type "%s") is not a viewable image file.', + $file_phid, + $file->getMimeType()), + $xaction); + continue; + } + + if (!$file->isTransformableImage()) { + $errors[] = $this->newInvalidError( + pht( + 'File ("%s", with MIME type "%s") can not be transformed into '. + 'a thumbnail. You may be missing support for this file type in '. + 'the "GD" extension.', + $file_phid, + $file->getMimeType()), + $xaction); + continue; + } } return $errors; From 4485482fd4cc7ad8fe100c694446e70a550da1d4 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 26 Mar 2019 15:27:05 -0700 Subject: [PATCH 212/245] Fix task hovercards showing a "Not Editable" state Summary: Ref T13269. These cards really have three states: - Editable: shows a pencil icon edit button. - You Do Not Have Permission To Edit This: shows a "no editing" icon in red. - Hovecard: shouldn't show anything. However, the "hovercard" and "no permission" states are currently the same state, so when I made the "no permission" state more obvious that made the hovercard go all weird. Make these states explicitly separate states. Test Plan: Looked at a... - Editable card on workboard: edit state. - No permission card on workboard: no permission state. - Any hovercard: "not editable, this is a hovercard" state. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13269 Differential Revision: https://secure.phabricator.com/D20330 --- .../ManiphestHovercardEngineExtension.php | 3 +- .../PhabricatorBoardRenderingEngine.php | 1 + .../project/view/ProjectBoardTaskCard.php | 46 ++++++++++++------- 3 files changed, 31 insertions(+), 19 deletions(-) diff --git a/src/applications/maniphest/engineextension/ManiphestHovercardEngineExtension.php b/src/applications/maniphest/engineextension/ManiphestHovercardEngineExtension.php index 2b2ef3c93d..7474f9cfaf 100644 --- a/src/applications/maniphest/engineextension/ManiphestHovercardEngineExtension.php +++ b/src/applications/maniphest/engineextension/ManiphestHovercardEngineExtension.php @@ -47,8 +47,7 @@ final class ManiphestHovercardEngineExtension $card = id(new ProjectBoardTaskCard()) ->setViewer($viewer) - ->setTask($task) - ->setCanEdit(false); + ->setTask($task); $owner_phid = $task->getOwnerPHID(); if ($owner_phid) { diff --git a/src/applications/project/engine/PhabricatorBoardRenderingEngine.php b/src/applications/project/engine/PhabricatorBoardRenderingEngine.php index d76497bc21..f5a81eb9b0 100644 --- a/src/applications/project/engine/PhabricatorBoardRenderingEngine.php +++ b/src/applications/project/engine/PhabricatorBoardRenderingEngine.php @@ -56,6 +56,7 @@ final class PhabricatorBoardRenderingEngine extends Phobject { $card = id(new ProjectBoardTaskCard()) ->setViewer($viewer) ->setTask($object) + ->setShowEditControls(true) ->setCanEdit($this->getCanEdit($phid)); $owner_phid = $object->getOwnerPHID(); diff --git a/src/applications/project/view/ProjectBoardTaskCard.php b/src/applications/project/view/ProjectBoardTaskCard.php index bb1c8ca8c5..d102ac1b11 100644 --- a/src/applications/project/view/ProjectBoardTaskCard.php +++ b/src/applications/project/view/ProjectBoardTaskCard.php @@ -6,6 +6,7 @@ final class ProjectBoardTaskCard extends Phobject { private $projectHandles; private $task; private $owner; + private $showEditControls; private $canEdit; private $coverImageFile; private $hideArchivedProjects; @@ -70,6 +71,15 @@ final class ProjectBoardTaskCard extends Phobject { return $this->canEdit; } + public function setShowEditControls($show_edit_controls) { + $this->showEditControls = $show_edit_controls; + return $this; + } + + public function getShowEditControls() { + return $this->showEditControls; + } + public function getItem() { $task = $this->getTask(); $owner = $this->getOwner(); @@ -89,24 +99,26 @@ final class ProjectBoardTaskCard extends Phobject { ->setDisabled($task->isClosed()) ->setBarColor($bar_color); - if ($can_edit) { - $card - ->addSigil('draggable-card') - ->addClass('draggable-card'); - $edit_icon = 'fa-pencil'; - } else { - $card - ->addClass('not-editable') - ->addClass('undraggable-card'); - $edit_icon = 'fa-lock red'; - } + if ($this->getShowEditControls()) { + if ($can_edit) { + $card + ->addSigil('draggable-card') + ->addClass('draggable-card'); + $edit_icon = 'fa-pencil'; + } else { + $card + ->addClass('not-editable') + ->addClass('undraggable-card'); + $edit_icon = 'fa-lock red'; + } - $card->addAction( - id(new PHUIListItemView()) - ->setName(pht('Edit')) - ->setIcon($edit_icon) - ->addSigil('edit-project-card') - ->setHref('/maniphest/task/edit/'.$task->getID().'/')); + $card->addAction( + id(new PHUIListItemView()) + ->setName(pht('Edit')) + ->setIcon($edit_icon) + ->addSigil('edit-project-card') + ->setHref('/maniphest/task/edit/'.$task->getID().'/')); + } if ($owner) { $card->addHandleIcon($owner, $owner->getName()); From ee54e71ba9aaa917148fe4df052b55f180285777 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 26 Mar 2019 16:30:32 -0700 Subject: [PATCH 213/245] On workboards, link ancestor project breadcrumbs to their workboards Summary: Ref T13269. Currently, if you're on a milestone workboard like this: > Projects > Parent > Milestone > Workboard The "Parent" link goes to the parent profile. More often, I want it to go to the parent workboard. Try doing that? This is kind of one-off but I suspect it's a better rule. Also, consolidate one billion manual constructions of "/board/" URIs. Test Plan: Viewed a milestone workboard, clicked the parent link, ended up on the parent workboard. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13269 Differential Revision: https://secure.phabricator.com/D20331 --- .../editor/ManiphestTransactionEditor.php | 3 +- ...icatorProjectBoardBackgroundController.php | 2 +- ...habricatorProjectBoardManageController.php | 2 +- .../PhabricatorProjectBoardViewController.php | 2 +- ...abricatorProjectColumnDetailController.php | 2 +- ...PhabricatorProjectColumnEditController.php | 3 +- ...PhabricatorProjectColumnHideController.php | 2 +- ...orProjectColumnRemoveTriggerController.php | 2 +- .../PhabricatorProjectController.php | 30 ++++++++++++++++--- ...habricatorProjectTriggerEditController.php | 4 +-- ...habricatorProjectTriggerViewController.php | 2 +- .../PhabricatorProjectsCurtainExtension.php | 2 +- .../PhabricatorProjectUIEventListener.php | 2 +- ...ricatorProjectWorkboardProfileMenuItem.php | 2 +- .../phid/PhabricatorProjectColumnPHIDType.php | 2 +- .../project/storage/PhabricatorProject.php | 4 +++ .../storage/PhabricatorProjectColumn.php | 6 ++-- 17 files changed, 47 insertions(+), 25 deletions(-) diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php index 5198f44572..fd5bfe0cdc 100644 --- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php +++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php @@ -243,8 +243,7 @@ final class ManiphestTransactionEditor foreach ($projects as $project) { $body->addLinkSection( pht('WORKBOARD'), - PhabricatorEnv::getProductionURI( - '/project/board/'.$project->getID().'/')); + PhabricatorEnv::getProductionURI($project->getWorkboardURI())); } } diff --git a/src/applications/project/controller/PhabricatorProjectBoardBackgroundController.php b/src/applications/project/controller/PhabricatorProjectBoardBackgroundController.php index b229f59ecb..c70c211398 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardBackgroundController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardBackgroundController.php @@ -55,7 +55,7 @@ final class PhabricatorProjectBoardBackgroundController $nav = $this->getProfileMenu(); $crumbs = id($this->buildApplicationCrumbs()) - ->addTextCrumb(pht('Workboard'), "/project/board/{$board_id}/") + ->addTextCrumb(pht('Workboard'), $board->getWorkboardURI()) ->addTextCrumb(pht('Manage'), $manage_uri) ->addTextCrumb(pht('Background Color')); diff --git a/src/applications/project/controller/PhabricatorProjectBoardManageController.php b/src/applications/project/controller/PhabricatorProjectBoardManageController.php index 5c71dcfb61..21daf2e654 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardManageController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardManageController.php @@ -34,7 +34,7 @@ final class PhabricatorProjectBoardManageController $curtain = $this->buildCurtainView($board); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb(pht('Workboard'), "/project/board/{$board_id}/"); + $crumbs->addTextCrumb(pht('Workboard'), $board->getWorkboardURI()); $crumbs->addTextCrumb(pht('Manage')); $crumbs->setBorder(true); diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index e8a47d362a..775ff1b61a 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -726,7 +726,7 @@ final class PhabricatorProjectBoardViewController ->setType(PHUIListItemView::TYPE_DIVIDER); $fullscreen = $this->buildFullscreenMenu(); - $crumbs = $this->buildApplicationCrumbs(); + $crumbs = $this->newWorkboardCrumbs(); $crumbs->addTextCrumb(pht('Workboard')); $crumbs->setBorder(true); diff --git a/src/applications/project/controller/PhabricatorProjectColumnDetailController.php b/src/applications/project/controller/PhabricatorProjectColumnDetailController.php index 24efec5ebb..781461a812 100644 --- a/src/applications/project/controller/PhabricatorProjectColumnDetailController.php +++ b/src/applications/project/controller/PhabricatorProjectColumnDetailController.php @@ -47,7 +47,7 @@ final class PhabricatorProjectColumnDetailController $properties = $this->buildPropertyView($column); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb(pht('Workboard'), "/project/board/{$project_id}/"); + $crumbs->addTextCrumb(pht('Workboard'), $project->getWorkboardURI()); $crumbs->addTextCrumb(pht('Column: %s', $title)); $crumbs->setBorder(true); diff --git a/src/applications/project/controller/PhabricatorProjectColumnEditController.php b/src/applications/project/controller/PhabricatorProjectColumnEditController.php index 567b923407..9ddb2b7d8a 100644 --- a/src/applications/project/controller/PhabricatorProjectColumnEditController.php +++ b/src/applications/project/controller/PhabricatorProjectColumnEditController.php @@ -50,8 +50,7 @@ final class PhabricatorProjectColumnEditController $v_name = $column->getName(); $validation_exception = null; - $base_uri = '/board/'.$project_id.'/'; - $view_uri = $this->getApplicationURI($base_uri); + $view_uri = $project->getWorkboardURI(); if ($request->isFormPost()) { $v_name = $request->getStr('name'); diff --git a/src/applications/project/controller/PhabricatorProjectColumnHideController.php b/src/applications/project/controller/PhabricatorProjectColumnHideController.php index 61811af5c3..254beab78c 100644 --- a/src/applications/project/controller/PhabricatorProjectColumnHideController.php +++ b/src/applications/project/controller/PhabricatorProjectColumnHideController.php @@ -38,7 +38,7 @@ final class PhabricatorProjectColumnHideController $column_phid = $column->getPHID(); - $view_uri = $this->getApplicationURI('/board/'.$project_id.'/'); + $view_uri = $project->getWorkboardURI(); $view_uri = new PhutilURI($view_uri); foreach ($request->getPassthroughRequestData() as $key => $value) { $view_uri->replaceQueryParam($key, $value); diff --git a/src/applications/project/controller/PhabricatorProjectColumnRemoveTriggerController.php b/src/applications/project/controller/PhabricatorProjectColumnRemoveTriggerController.php index 5802449dcb..9bb92e5a3a 100644 --- a/src/applications/project/controller/PhabricatorProjectColumnRemoveTriggerController.php +++ b/src/applications/project/controller/PhabricatorProjectColumnRemoveTriggerController.php @@ -20,7 +20,7 @@ final class PhabricatorProjectColumnRemoveTriggerController return new Aphront404Response(); } - $done_uri = $column->getBoardURI(); + $done_uri = $column->getWorkboardURI(); if (!$column->getTriggerPHID()) { return $this->newDialog() diff --git a/src/applications/project/controller/PhabricatorProjectController.php b/src/applications/project/controller/PhabricatorProjectController.php index c28ace305c..63494bf442 100644 --- a/src/applications/project/controller/PhabricatorProjectController.php +++ b/src/applications/project/controller/PhabricatorProjectController.php @@ -109,6 +109,14 @@ abstract class PhabricatorProjectController extends PhabricatorController { } protected function buildApplicationCrumbs() { + return $this->newApplicationCrumbs('profile'); + } + + protected function newWorkboardCrumbs() { + return $this->newApplicationCrumbs('workboard'); + } + + private function newApplicationCrumbs($mode) { $crumbs = parent::buildApplicationCrumbs(); $project = $this->getProject(); @@ -117,10 +125,24 @@ abstract class PhabricatorProjectController extends PhabricatorController { $ancestors = array_reverse($ancestors); $ancestors[] = $project; foreach ($ancestors as $ancestor) { - $crumbs->addTextCrumb( - $ancestor->getName(), - $ancestor->getProfileURI() - ); + if ($ancestor->getPHID() === $project->getPHID()) { + // Link the current project's crumb to its profile no matter what, + // since we're already on the right context page for it and linking + // to the current page isn't helpful. + $crumb_uri = $ancestor->getProfileURI(); + } else { + switch ($mode) { + case 'workboard': + $crumb_uri = $ancestor->getWorkboardURI(); + break; + case 'profile': + default: + $crumb_uri = $ancestor->getProfileURI(); + break; + } + } + + $crumbs->addTextCrumb($ancestor->getName(), $crumb_uri); } } diff --git a/src/applications/project/controller/trigger/PhabricatorProjectTriggerEditController.php b/src/applications/project/controller/trigger/PhabricatorProjectTriggerEditController.php index 7189df70ec..df362efb61 100644 --- a/src/applications/project/controller/trigger/PhabricatorProjectTriggerEditController.php +++ b/src/applications/project/controller/trigger/PhabricatorProjectTriggerEditController.php @@ -39,7 +39,7 @@ final class PhabricatorProjectTriggerEditController if (!$column) { return new Aphront404Response(); } - $board_uri = $column->getBoardURI(); + $board_uri = $column->getWorkboardURI(); } else { $column = null; $board_uri = null; @@ -122,7 +122,7 @@ final class PhabricatorProjectTriggerEditController $column_editor->applyTransactions($column, $column_xactions); - $next_uri = $column->getBoardURI(); + $next_uri = $column->getWorkboardURI(); } return id(new AphrontRedirectResponse())->setURI($next_uri); diff --git a/src/applications/project/controller/trigger/PhabricatorProjectTriggerViewController.php b/src/applications/project/controller/trigger/PhabricatorProjectTriggerViewController.php index e18419fedb..d148c0a421 100644 --- a/src/applications/project/controller/trigger/PhabricatorProjectTriggerViewController.php +++ b/src/applications/project/controller/trigger/PhabricatorProjectTriggerViewController.php @@ -111,7 +111,7 @@ final class PhabricatorProjectTriggerViewController $column_name = phutil_tag( 'a', array( - 'href' => $column->getBoardURI(), + 'href' => $column->getWorkboardURI(), ), $column->getDisplayName()); } else { diff --git a/src/applications/project/engineextension/PhabricatorProjectsCurtainExtension.php b/src/applications/project/engineextension/PhabricatorProjectsCurtainExtension.php index c69e130275..7251323415 100644 --- a/src/applications/project/engineextension/PhabricatorProjectsCurtainExtension.php +++ b/src/applications/project/engineextension/PhabricatorProjectsCurtainExtension.php @@ -55,7 +55,7 @@ final class PhabricatorProjectsCurtainExtension $column_link = phutil_tag( 'a', array( - 'href' => "/project/board/{$project_id}/", + 'href' => $column->getWorkboardURI(), 'class' => 'maniphest-board-link', ), $column_name); diff --git a/src/applications/project/events/PhabricatorProjectUIEventListener.php b/src/applications/project/events/PhabricatorProjectUIEventListener.php index 104084bbf7..25d1ba9f74 100644 --- a/src/applications/project/events/PhabricatorProjectUIEventListener.php +++ b/src/applications/project/events/PhabricatorProjectUIEventListener.php @@ -81,7 +81,7 @@ final class PhabricatorProjectUIEventListener $column_link = phutil_tag( 'a', array( - 'href' => "/project/board/{$project_id}/", + 'href' => $column->getWorkboardURI(), 'class' => 'maniphest-board-link', ), $column_name); diff --git a/src/applications/project/menuitem/PhabricatorProjectWorkboardProfileMenuItem.php b/src/applications/project/menuitem/PhabricatorProjectWorkboardProfileMenuItem.php index 80ec0d835a..38b9632d93 100644 --- a/src/applications/project/menuitem/PhabricatorProjectWorkboardProfileMenuItem.php +++ b/src/applications/project/menuitem/PhabricatorProjectWorkboardProfileMenuItem.php @@ -57,7 +57,7 @@ final class PhabricatorProjectWorkboardProfileMenuItem $project = $config->getProfileObject(); $id = $project->getID(); - $href = "/project/board/{$id}/"; + $href = $project->getWorkboardURI(); $name = $this->getDisplayName($config); $item = $this->newItem() diff --git a/src/applications/project/phid/PhabricatorProjectColumnPHIDType.php b/src/applications/project/phid/PhabricatorProjectColumnPHIDType.php index 07c7f7a0ee..c58bb44671 100644 --- a/src/applications/project/phid/PhabricatorProjectColumnPHIDType.php +++ b/src/applications/project/phid/PhabricatorProjectColumnPHIDType.php @@ -37,7 +37,7 @@ final class PhabricatorProjectColumnPHIDType extends PhabricatorPHIDType { $column = $objects[$phid]; $handle->setName($column->getDisplayName()); - $handle->setURI('/project/board/'.$column->getProject()->getID().'/'); + $handle->setURI($column->getWorkboardURI()); if ($column->isHidden()) { $handle->setStatus(PhabricatorObjectHandle::STATUS_CLOSED); diff --git a/src/applications/project/storage/PhabricatorProject.php b/src/applications/project/storage/PhabricatorProject.php index 5182a941bf..67ab05f5fd 100644 --- a/src/applications/project/storage/PhabricatorProject.php +++ b/src/applications/project/storage/PhabricatorProject.php @@ -392,6 +392,10 @@ final class PhabricatorProject extends PhabricatorProjectDAO return "/project/profile/{$id}/"; } + public function getWorkboardURI() { + return urisprintf('/project/board/%d/', $this->getID()); + } + public function save() { if (!$this->getMailKey()) { $this->setMailKey(Filesystem::readRandomCharacters(20)); diff --git a/src/applications/project/storage/PhabricatorProjectColumn.php b/src/applications/project/storage/PhabricatorProjectColumn.php index 731c2a15fb..49d7f28a9f 100644 --- a/src/applications/project/storage/PhabricatorProjectColumn.php +++ b/src/applications/project/storage/PhabricatorProjectColumn.php @@ -212,10 +212,8 @@ final class PhabricatorProjectColumn return true; } - public function getBoardURI() { - return urisprintf( - '/project/board/%d/', - $this->getProject()->getID()); + public function getWorkboardURI() { + return $this->getProject()->getWorkboardURI(); } public function getDropEffects() { From 3e1ffda85db3fa0abab00f7c198f5cc9449c8e7b Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 27 Mar 2019 07:05:58 -0700 Subject: [PATCH 214/245] Give workboard column header actions a more clickable appearance Summary: Ref T13269. Make it visually more clear that the "Trigger" and "New Task / Edit / Bulk" dropdown menu items are buttons, not status icons or indicators of some kind. Test Plan: {F6313872} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13269 Differential Revision: https://secure.phabricator.com/D20332 --- resources/celerity/map.php | 10 +++++----- .../rsrc/css/phui/workboards/phui-workpanel.css | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 7f5c8ff45e..c1a77bca11 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -179,7 +179,7 @@ return array( 'rsrc/css/phui/workboards/phui-workboard-color.css' => 'e86de308', 'rsrc/css/phui/workboards/phui-workboard.css' => '74fc9d98', 'rsrc/css/phui/workboards/phui-workcard.css' => '9e9eb0df', - 'rsrc/css/phui/workboards/phui-workpanel.css' => 'f43b8c7f', + 'rsrc/css/phui/workboards/phui-workpanel.css' => '3ae89b20', 'rsrc/css/sprite-login.css' => '18b368a6', 'rsrc/css/sprite-tokens.css' => 'f1896dc5', 'rsrc/css/syntax/syntax-default.css' => '055fc231', @@ -869,7 +869,7 @@ return array( 'phui-workboard-color-css' => 'e86de308', 'phui-workboard-view-css' => '74fc9d98', 'phui-workcard-view-css' => '9e9eb0df', - 'phui-workpanel-view-css' => 'f43b8c7f', + 'phui-workpanel-view-css' => '3ae89b20', 'phuix-action-list-view' => 'c68f183f', 'phuix-action-view' => 'aaa08f3b', 'phuix-autocomplete' => '8f139ef0', @@ -1223,6 +1223,9 @@ return array( 'trigger-rule', 'trigger-rule-type', ), + '3ae89b20' => array( + 'phui-workcard-view-css', + ), '3b4899b0' => array( 'javelin-behavior', 'phabricator-prefab', @@ -2141,9 +2144,6 @@ return array( 'phabricator-darklog', 'phabricator-darkmessage', ), - 'f43b8c7f' => array( - 'phui-workcard-view-css', - ), 'f51e9c17' => 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 5c0a62282b..5ee54f2deb 100644 --- a/webroot/rsrc/css/phui/workboards/phui-workpanel.css +++ b/webroot/rsrc/css/phui/workboards/phui-workpanel.css @@ -237,3 +237,18 @@ opacity: 0.25; } } + +.phui-workpanel-view .phui-header-action-item a.phui-icon-view { + width: 24px; + height: 24px; + line-height: 24px; + text-align: center; + border-radius: 3px; + box-shadow: inset -1px -1px 2px rgba(0, 0, 0, 0.05); + border: 1px solid {$lightgreyborder}; + background: {$lightgreybackground}; +} + +.phui-workpanel-view .phui-header-action-item .phui-tag-view { + line-height: 24px; +} From a68b6cfe6541faf75ba80f0a2d15d0f7f76fa071 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 27 Mar 2019 08:07:32 -0700 Subject: [PATCH 215/245] Use real Dashboard Panels to render the default hard-coded homepage, not hacky fake panels Summary: Ref T13272. Currently, the hard-coded default homepage looks like a dashboard but is actually rendered completely manually. This means that various panel rendering improvements we'd like to make (including better "Show More" behavior and better handling of overheated queries) won't work on the home page naturally: we'd have to make these changes twice, once for dashboards and once for the home page. Instead, build the home page out of real panels. This turns out to be significantly simpler (I think the backend part of panels/dashboards is mostly on solid footing, the frontend just needs some work). Test Plan: Loaded the default home page, saw a thing which looked the same as the old thing. Changes I know about / expect: - The headers for these panels are no longer linked, but they weren't colorized before so the links were hard to find. I plan to improve panel behavior for "find/more" in a followup. - I've removed the "follow us on twitter" default NUX if feed is empty, since this seems like unnecessary incidental complexity. - (Internal exception behavior should be better, now.) Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13272 Differential Revision: https://secure.phabricator.com/D20333 --- src/applications/home/view/PHUIHomeView.php | 163 ++++++-------------- 1 file changed, 45 insertions(+), 118 deletions(-) diff --git a/src/applications/home/view/PHUIHomeView.php b/src/applications/home/view/PHUIHomeView.php index d6c3794854..45750f5a93 100644 --- a/src/applications/home/view/PHUIHomeView.php +++ b/src/applications/home/view/PHUIHomeView.php @@ -77,149 +77,76 @@ final class PHUIHomeView return $view; } - private function buildHomepagePanel($title, $href, $view) { - $title = phutil_tag( - 'a', - array( - 'href' => $href, - ), - $title); - - $icon = id(new PHUIIconView()) - ->setIcon('fa-search') - ->setHref($href); - - $header = id(new PHUIHeaderView()) - ->setHeader($title) - ->addActionItem($icon); - - $box = id(new PHUIObjectBoxView()) - ->setHeader($header); - - if ($view->getObjectList()) { - $box->setObjectList($view->getObjectList()); - } - if ($view->getContent()) { - $box->appendChild($view->getContent()); - } - - return $box; - } - private function buildRevisionPanel() { $viewer = $this->getViewer(); if (!$viewer->isLoggedIn()) { return null; } - $engine = new DifferentialRevisionSearchEngine(); - $engine->setViewer($viewer); - $saved = $engine->buildSavedQueryFromBuiltin('active'); - $query = $engine->buildQueryFromSavedQuery($saved); - $pager = $engine->newPagerForSavedQuery($saved); - $pager->setPageSize(15); - $results = $engine->executeQuery($query, $pager); - $view = $engine->renderResults($results, $saved); + $panel = $this->newQueryPanel() + ->setName(pht('Active Revisions')) + ->setProperty('class', 'DifferentialRevisionSearchEngine') + ->setProperty('key', 'active'); - $title = pht('Active Revisions'); - $href = '/differential/query/active/'; - - return $this->buildHomepagePanel($title, $href, $view); + return $this->renderPanel($panel); } private function buildTasksPanel() { $viewer = $this->getViewer(); - $query = 'assigned'; - $title = pht('Assigned Tasks'); - $href = '/maniphest/query/assigned/'; - if (!$viewer->isLoggedIn()) { + if ($viewer->isLoggedIn()) { + $name = pht('Assigned Tasks'); + $query = 'assigned'; + } else { + $name = pht('Open Tasks'); $query = 'open'; - $title = pht('Open Tasks'); - $href = '/maniphest/query/open/'; } - $engine = new ManiphestTaskSearchEngine(); - $engine->setViewer($viewer); - $saved = $engine->buildSavedQueryFromBuiltin($query); - $query = $engine->buildQueryFromSavedQuery($saved); - $pager = $engine->newPagerForSavedQuery($saved); - $pager->setPageSize(15); - $results = $engine->executeQuery($query, $pager); - $view = $engine->renderResults($results, $saved); + $panel = $this->newQueryPanel() + ->setName($name) + ->setProperty('class', 'ManiphestTaskSearchEngine') + ->setProperty('key', $query) + ->setProperty('limit', 15); - return $this->buildHomepagePanel($title, $href, $view); + return $this->renderPanel($panel); } public function buildFeedPanel() { - $viewer = $this->getViewer(); + $panel = $this->newQueryPanel() + ->setName(pht('Recent Activity')) + ->setProperty('class', 'PhabricatorFeedSearchEngine') + ->setProperty('key', 'all') + ->setProperty('limit', 40); - $engine = new PhabricatorFeedSearchEngine(); - $engine->setViewer($viewer); - $saved = $engine->buildSavedQueryFromBuiltin('all'); - $query = $engine->buildQueryFromSavedQuery($saved); - $pager = $engine->newPagerForSavedQuery($saved); - $pager->setPageSize(40); - $results = $engine->executeQuery($query, $pager); - $view = $engine->renderResults($results, $saved); - // Low tech NUX. - if (!$results && ($viewer->getIsAdmin() == 1)) { - $instance = PhabricatorEnv::getEnvConfig('cluster.instance'); - if (!$instance) { - $content = pht(<<setObjectList($list); - } else { - $content = id(new PHUIBoxView()) - ->appendChild(new PHUIRemarkupView($viewer, $content)) - ->addClass('mlt mlb msr msl'); - $view = new PhabricatorApplicationSearchResultView(); - $view->setContent($content); - } - } - - $title = pht('Recent Activity'); - $href = '/feed/'; - - return $this->buildHomepagePanel($title, $href, $view); + return $this->renderPanel($panel); } public function buildRepositoryPanel() { + $panel = $this->newQueryPanel() + ->setName(pht('Active Repositories')) + ->setProperty('class', 'PhabricatorRepositorySearchEngine') + ->setProperty('key', 'active') + ->setProperty('limit', 5); + + return $this->renderPanel($panel); + } + + private function newQueryPanel() { + $panel_type = id(new PhabricatorDashboardQueryPanelType()) + ->getPanelTypeKey(); + + return id(new PhabricatorDashboardPanel()) + ->setPanelType($panel_type); + } + + private function renderPanel(PhabricatorDashboardPanel $panel) { $viewer = $this->getViewer(); - $engine = new PhabricatorRepositorySearchEngine(); - $engine->setViewer($viewer); - $saved = $engine->buildSavedQueryFromBuiltin('active'); - $query = $engine->buildQueryFromSavedQuery($saved); - $pager = $engine->newPagerForSavedQuery($saved); - $pager->setPageSize(5); - $results = $engine->executeQuery($query, $pager); - $view = $engine->renderResults($results, $saved); - - $title = pht('Active Repositories'); - $href = '/diffusion/'; - - return $this->buildHomepagePanel($title, $href, $view); + return id(new PhabricatorDashboardPanelRenderingEngine()) + ->setViewer($viewer) + ->setPanel($panel) + ->setParentPanelPHIDs(array()) + ->renderPanel(); } } From 6b069f26c081c297baeb4c56ba427a2adbc3ff80 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 27 Mar 2019 08:24:15 -0700 Subject: [PATCH 216/245] Give Dashboard query panels a more obvious "View All" button Summary: Depends on D20333. Ref T13272. Currently, dashboard query panels have an aesthetic but hard-to-see icon to view results, with no text label. Instead, provide an easier-to-see button with a text label. Test Plan: {F6314091} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13272 Differential Revision: https://secure.phabricator.com/D20334 --- .../PhabricatorDashboardQueryPanelType.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/applications/dashboard/paneltype/PhabricatorDashboardQueryPanelType.php b/src/applications/dashboard/paneltype/PhabricatorDashboardQueryPanelType.php index 0781d71b16..b8b9c2ceae 100644 --- a/src/applications/dashboard/paneltype/PhabricatorDashboardQueryPanelType.php +++ b/src/applications/dashboard/paneltype/PhabricatorDashboardQueryPanelType.php @@ -120,10 +120,18 @@ final class PhabricatorDashboardQueryPanelType $search_engine = $this->getSearchEngine($panel); $key = $panel->getProperty('key'); $href = $search_engine->getQueryResultsPageURI($key); + $icon = id(new PHUIIconView()) - ->setIcon('fa-search') - ->setHref($href); - $header->addActionItem($icon); + ->setIcon('fa-search'); + + $button = id(new PHUIButtonView()) + ->setTag('a') + ->setText(pht('View All')) + ->setIcon($icon) + ->setHref($href) + ->setColor(PHUIButtonView::GREY); + + $header->addActionLink($button); return $header; } From 2c184bd4cdbf3c1e9e0313227b47ded322ad25ae Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 27 Mar 2019 08:43:18 -0700 Subject: [PATCH 217/245] When query panels overheat, degrade them and show a warning Summary: Depends on D20334. Ref T13272. After recent changes to make overheating queries throw by default, dashboard panels now fail into an error state when they overheat. This is a big step up from the hard-coded homepage panels removed by D20333, but can be improved. Let these panels render partial results when they overheat and show a human-readable warning. Test Plan: {F6314114} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13272 Differential Revision: https://secure.phabricator.com/D20335 --- .../PhabricatorDashboardQueryPanelType.php | 32 +++++++++++++++++-- ...PhabricatorApplicationSearchController.php | 28 +++++++++++----- 2 files changed, 50 insertions(+), 10 deletions(-) diff --git a/src/applications/dashboard/paneltype/PhabricatorDashboardQueryPanelType.php b/src/applications/dashboard/paneltype/PhabricatorDashboardQueryPanelType.php index b8b9c2ceae..3c77da48dc 100644 --- a/src/applications/dashboard/paneltype/PhabricatorDashboardQueryPanelType.php +++ b/src/applications/dashboard/paneltype/PhabricatorDashboardQueryPanelType.php @@ -106,9 +106,37 @@ final class PhabricatorDashboardQueryPanelType } } - $results = $engine->executeQuery($query, $pager); + $query->setReturnPartialResultsOnOverheat(true); - return $engine->renderResults($results, $saved); + $results = $engine->executeQuery($query, $pager); + $results_view = $engine->renderResults($results, $saved); + + $is_overheated = $query->getIsOverheated(); + $overheated_view = null; + if ($is_overheated) { + $content = $results_view->getContent(); + + $overheated_message = + PhabricatorApplicationSearchController::newOverheatedError( + (bool)$results); + + $overheated_warning = id(new PHUIInfoView()) + ->setSeverity(PHUIInfoView::SEVERITY_WARNING) + ->setTitle(pht('Query Overheated')) + ->setErrors( + array( + $overheated_message, + )); + + $overheated_box = id(new PHUIBoxView()) + ->addClass('mmt mmb') + ->appendChild($overheated_warning); + + $content = array($content, $overheated_box); + $results_view->setContent($content); + } + + return $results_view; } public function adjustPanelHeader( diff --git a/src/applications/search/controller/PhabricatorApplicationSearchController.php b/src/applications/search/controller/PhabricatorApplicationSearchController.php index 25cad5ac20..4bf4929f4b 100644 --- a/src/applications/search/controller/PhabricatorApplicationSearchController.php +++ b/src/applications/search/controller/PhabricatorApplicationSearchController.php @@ -850,19 +850,31 @@ final class PhabricatorApplicationSearchController )); } - private function newOverheatedView(array $results) { - if ($results) { + public static function newOverheatedError($has_results) { + $overheated_link = phutil_tag( + 'a', + array( + 'href' => 'https://phurl.io/u/overheated', + 'target' => '_blank', + ), + pht('Learn More')); + + if ($has_results) { $message = pht( - 'Most objects matching your query are not visible to you, so '. - 'filtering results is taking a long time. Only some results are '. - 'shown. Refine your query to find results more quickly.'); + 'This query took too long, so only some results are shown. %s', + $overheated_link); } else { $message = pht( - 'Most objects matching your query are not visible to you, so '. - 'filtering results is taking a long time. Refine your query to '. - 'find results more quickly.'); + 'This query took too long. %s', + $overheated_link); } + return $message; + } + + private function newOverheatedView(array $results) { + $message = self::newOverheatedError((bool)$results); + return id(new PHUIInfoView()) ->setSeverity(PHUIInfoView::SEVERITY_WARNING) ->setFlush(true) From 73feac47c708db45b6fe5322b0dc9c7ddd0a2140 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 27 Mar 2019 10:40:23 -0700 Subject: [PATCH 218/245] When query panels have more results, show a "View All Results" button at the bottom Summary: Depends on D20335. Ref T13263. Ref T13272. See PHI854. Ref T9903. Currently, we don't provide a clear indicator that a query panel is showing a partial result set (UI looks the same whether there are more results or not). We also don't provide any way to get to the full result set (regardless of whether it is the same as the visible set or not) on tab panels, since they don't inherit the header buttons. To (mostly) fix these problems, add a "View All Results" button at the bottom of the list if the panel shows only a subset of results. Test Plan: {F6314560} {F6314562} {F6314564} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13272, T13263, T9903 Differential Revision: https://secure.phabricator.com/D20336 --- .../PhabricatorDashboardQueryPanelType.php | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/applications/dashboard/paneltype/PhabricatorDashboardQueryPanelType.php b/src/applications/dashboard/paneltype/PhabricatorDashboardQueryPanelType.php index 3c77da48dc..a71263b27e 100644 --- a/src/applications/dashboard/paneltype/PhabricatorDashboardQueryPanelType.php +++ b/src/applications/dashboard/paneltype/PhabricatorDashboardQueryPanelType.php @@ -136,6 +136,33 @@ final class PhabricatorDashboardQueryPanelType $results_view->setContent($content); } + if ($pager->getHasMoreResults()) { + $item_list = $results_view->getObjectList(); + + $more_href = $engine->getQueryResultsPageURI($key); + if ($item_list) { + $item_list->newTailButton() + ->setHref($more_href); + } else { + // For search engines that do not return an object list, add a fake + // one to the end so we can render a "View All Results" button that + // looks like it does in normal applications. At time of writing, + // several major applications like Maniphest (which has group headers) + // and Feed (which uses custom rendering) don't return simple lists. + + $content = $results_view->getContent(); + + $more_list = id(new PHUIObjectItemListView()) + ->setAllowEmptyList(true); + + $more_list->newTailButton() + ->setHref($more_href); + + $content = array($content, $more_list); + $results_view->setContent($content); + } + } + return $results_view; } From 6648942bc885175cc59c07ce0be380170a4ce268 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 27 Mar 2019 11:25:27 -0700 Subject: [PATCH 219/245] Don't allow "Conpherence" menu items to be added to editable menus if Conpherence is not installed Summary: Depends on D20336. Ref T13272. Fixes T12745. If you uninstall Conpherence, "Edit Favorites" and other editable menu interfaces still allow you to add "Conpherence" items. Prevent this. Test Plan: - Installed Conpherence: Saw option to add a "Conpherence" link when editing favorites menu. - Uninstalled Conpherence: No more option. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13272, T12745 Differential Revision: https://secure.phabricator.com/D20337 --- .../menuitem/PhabricatorConpherenceProfileMenuItem.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/applications/search/menuitem/PhabricatorConpherenceProfileMenuItem.php b/src/applications/search/menuitem/PhabricatorConpherenceProfileMenuItem.php index 6a91188c8f..542c634958 100644 --- a/src/applications/search/menuitem/PhabricatorConpherenceProfileMenuItem.php +++ b/src/applications/search/menuitem/PhabricatorConpherenceProfileMenuItem.php @@ -17,6 +17,12 @@ final class PhabricatorConpherenceProfileMenuItem } public function canAddToObject($object) { + $application = new PhabricatorConpherenceApplication(); + + if (!$application->isInstalled()) { + return false; + } + return true; } From 6bb9d3ac67a1b9d285ba351af5e5eeffdc8ac682 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 27 Mar 2019 14:09:37 -0700 Subject: [PATCH 220/245] Allow users to add "ProfileMenu" items on mobile Summary: Depends on D20337. Fixes T12167. Ref T13272. On this page ("Favorites > Edit Favorites > Personal", for example) the curtain actions aren't available on mobile. Normally, curtains are built with `Controller->newCurtainView()`, which sets an ID on the action list, which populates the header button. This curtain is built directly because there's no `Controller` handy. To fix the issue, just set an ID. This could probably be cleaner, but that's likely a much more involved change. Test Plan: Edited my favorites, narrowed the window, saw an "Actions" button. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13272, T12167 Differential Revision: https://secure.phabricator.com/D20338 --- .../search/engine/PhabricatorProfileMenuEngine.php | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/applications/search/engine/PhabricatorProfileMenuEngine.php b/src/applications/search/engine/PhabricatorProfileMenuEngine.php index 90f056ac4f..d5cb9ee43b 100644 --- a/src/applications/search/engine/PhabricatorProfileMenuEngine.php +++ b/src/applications/search/engine/PhabricatorProfileMenuEngine.php @@ -917,15 +917,16 @@ abstract class PhabricatorProfileMenuEngine extends Phobject { $list->addItem($view); } - $action_view = id(new PhabricatorActionListView()) - ->setUser($viewer); - $item_types = PhabricatorProfileMenuItem::getAllMenuItems(); $object = $this->getProfileObject(); $action_list = id(new PhabricatorActionListView()) ->setViewer($viewer); + // See T12167. This makes the "Actions" dropdown button show up in the + // page header. + $action_list->setID(celerity_generate_unique_node_id()); + $action_list->addAction( id(new PhabricatorActionView()) ->setLabel(true) @@ -970,9 +971,6 @@ abstract class PhabricatorProfileMenuEngine extends Phobject { ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setObjectList($list); - $panel = id(new PHUICurtainPanelView()) - ->appendChild($action_view); - $curtain = id(new PHUICurtainView()) ->setViewer($viewer) ->setActionList($action_list); From e69b349b1b24c70ebf562359f655c76cd6773bd1 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 28 Mar 2019 08:53:07 -0700 Subject: [PATCH 221/245] Prevent users from removing task titles with "Bulk Edit" Summary: See downstream . The "Bulk Edit" flow works with `setContinueOnMissingFields(true)`, so `newRequiredError()` errors are ignored. This allows you to apply a transaction which changes the title to `""` (the empty string) without actually hitting any errors which the workflow respects. (Normally, `setContinueOnMissingFields(...)` workflows only edit properties that can't be missing, like the status of an object, so this is an unusual flow.) Instead, validate more narrowly: - Transactions which would remove the title get an "invalid" error, which is respected even under "setContinueOnMissingFields()". - Then, we try to raise a "missing/required" error if everything otherwise looks okay. Test Plan: - Edited a task title normally. - Edited a task to remove the title (got an error). - Created a task with no title (disallowed: got an error). - Bulk edited a task to remove its title. - Before change: allowed. - After change: disallowed. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20339 --- .../xaction/ManiphestTaskTitleTransaction.php | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/applications/maniphest/xaction/ManiphestTaskTitleTransaction.php b/src/applications/maniphest/xaction/ManiphestTaskTitleTransaction.php index e4ec2a132f..7dd9217760 100644 --- a/src/applications/maniphest/xaction/ManiphestTaskTitleTransaction.php +++ b/src/applications/maniphest/xaction/ManiphestTaskTitleTransaction.php @@ -64,9 +64,27 @@ final class ManiphestTaskTitleTransaction public function validateTransactions($object, array $xactions) { $errors = array(); - if ($this->isEmptyTextTransaction($object->getTitle(), $xactions)) { - $errors[] = $this->newRequiredError( - pht('Tasks must have a title.')); + // If the user is acting via "Bulk Edit" or another workflow which + // continues on missing fields, they may be applying a transaction which + // removes the task title. Mark these transactions as invalid first, + // then flag the missing field error if we don't find any more specific + // problems. + + foreach ($xactions as $xaction) { + $new = $xaction->getNewValue(); + if (!strlen($new)) { + $errors[] = $this->newInvalidError( + pht('Tasks must have a title.'), + $xaction); + continue; + } + } + + if (!$errors) { + if ($this->isEmptyTextTransaction($object->getTitle(), $xactions)) { + $errors[] = $this->newRequiredError( + pht('Tasks must have a title.')); + } } return $errors; From 15cc475cbd8c60d1c3524f0bda06f5fce7f762e7 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 28 Mar 2019 15:32:23 -0700 Subject: [PATCH 222/245] When a comment was signed with MFA, require MFA to edit it Summary: Ref PHI1173. Currently, you can edit an MFA'd comment without redoing MFA. This is inconsistent with the intent of the MFA badge, since it means an un-MFA'd comment may have an "MFA" badge on it. Instead, implement these rules: - If a comment was signed with MFA, you MUST MFA to edit it. - When removing a comment, add an extra MFA prompt if the user has MFA. This one isn't strictly required, this action is just very hard to undo and seems reasonable to MFA. Test Plan: - Made normal comments and MFA comments. - Edited normal comments and MFA comments (got prompted). - Removed normal comments and MFA comments (prompted in both cases). - Tried to edit an MFA comment without MFA on my account, got a hard "MFA absolutely required" failure. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20340 --- ...cationTransactionCommentEditController.php | 27 +++- ...tionTransactionCommentRemoveController.php | 11 +- ...torApplicationTransactionCommentEditor.php | 121 ++++++++++++++++++ ...habricatorApplicationTransactionEditor.php | 3 +- 4 files changed, 151 insertions(+), 11 deletions(-) diff --git a/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentEditController.php b/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentEditController.php index a93f16a688..1682a7d136 100644 --- a/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentEditController.php +++ b/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentEditController.php @@ -29,7 +29,9 @@ final class PhabricatorApplicationTransactionCommentEditController $handles = $viewer->loadHandles(array($phid)); $obj_handle = $handles[$phid]; - if ($request->isDialogFormPost()) { + $done_uri = $obj_handle->getURI(); + + if ($request->isFormOrHisecPost()) { $text = $request->getStr('text'); $comment = $xaction->getApplicationTransactionCommentObject(); @@ -41,29 +43,42 @@ final class PhabricatorApplicationTransactionCommentEditController $editor = id(new PhabricatorApplicationTransactionCommentEditor()) ->setActor($viewer) ->setContentSource(PhabricatorContentSource::newFromRequest($request)) + ->setRequest($request) + ->setCancelURI($done_uri) ->applyEdit($xaction, $comment); if ($request->isAjax()) { return id(new AphrontAjaxResponse())->setContent(array()); } else { - return id(new AphrontReloadResponse())->setURI($obj_handle->getURI()); + return id(new AphrontReloadResponse())->setURI($done_uri); } } + $errors = array(); + if ($xaction->getIsMFATransaction()) { + $message = pht( + 'This comment was signed with MFA, so you will be required to '. + 'provide MFA credentials to make changes.'); + + $errors[] = id(new PHUIInfoView()) + ->setSeverity(PHUIInfoView::SEVERITY_MFA) + ->setErrors(array($message)); + } + $form = id(new AphrontFormView()) ->setUser($viewer) ->setFullWidth(true) ->appendControl( id(new PhabricatorRemarkupControl()) - ->setName('text') - ->setValue($xaction->getComment()->getContent())); + ->setName('text') + ->setValue($xaction->getComment()->getContent())); return $this->newDialog() ->setTitle(pht('Edit Comment')) - ->addHiddenInput('anchor', $request->getStr('anchor')) + ->appendChild($errors) ->appendForm($form) ->addSubmitButton(pht('Save Changes')) - ->addCancelButton($obj_handle->getURI()); + ->addCancelButton($done_uri); } } diff --git a/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentRemoveController.php b/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentRemoveController.php index c52b087273..381dfe1176 100644 --- a/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentRemoveController.php +++ b/src/applications/transactions/controller/PhabricatorApplicationTransactionCommentRemoveController.php @@ -30,20 +30,24 @@ final class PhabricatorApplicationTransactionCommentRemoveController ->withPHIDs(array($obj_phid)) ->executeOne(); - if ($request->isDialogFormPost()) { + $done_uri = $obj_handle->getURI(); + + if ($request->isFormOrHisecPost()) { $comment = $xaction->getApplicationTransactionCommentObject() ->setContent('') ->setIsRemoved(true); $editor = id(new PhabricatorApplicationTransactionCommentEditor()) ->setActor($viewer) + ->setRequest($request) + ->setCancelURI($done_uri) ->setContentSource(PhabricatorContentSource::newFromRequest($request)) ->applyEdit($xaction, $comment); if ($request->isAjax()) { return id(new AphrontAjaxResponse())->setContent(array()); } else { - return id(new AphrontReloadResponse())->setURI($obj_handle->getURI()); + return id(new AphrontReloadResponse())->setURI($done_uri); } } @@ -54,7 +58,6 @@ final class PhabricatorApplicationTransactionCommentRemoveController ->setTitle(pht('Remove Comment')); $dialog - ->addHiddenInput('anchor', $request->getStr('anchor')) ->appendParagraph( pht( "Removing a comment prevents anyone (including you) from reading ". @@ -65,7 +68,7 @@ final class PhabricatorApplicationTransactionCommentRemoveController $dialog ->addSubmitButton(pht('Remove Comment')) - ->addCancelButton($obj_handle->getURI()); + ->addCancelButton($done_uri); return $dialog; } diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionCommentEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionCommentEditor.php index f9db0e238e..d963ea2ecb 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionCommentEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionCommentEditor.php @@ -5,6 +5,9 @@ final class PhabricatorApplicationTransactionCommentEditor private $contentSource; private $actingAsPHID; + private $request; + private $cancelURI; + private $isNewComment; public function setActingAsPHID($acting_as_phid) { $this->actingAsPHID = $acting_as_phid; @@ -27,6 +30,33 @@ final class PhabricatorApplicationTransactionCommentEditor return $this->contentSource; } + public function setRequest(AphrontRequest $request) { + $this->request = $request; + return $this; + } + + public function getRequest() { + return $this->request; + } + + public function setCancelURI($cancel_uri) { + $this->cancelURI = $cancel_uri; + return $this; + } + + public function getCancelURI() { + return $this->cancelURI; + } + + public function setIsNewComment($is_new) { + $this->isNewComment = $is_new; + return $this; + } + + public function getIsNewComment() { + return $this->isNewComment; + } + /** * Edit a transaction's comment. This method effects the required create, * update or delete to set the transaction's comment to the provided comment. @@ -39,6 +69,8 @@ final class PhabricatorApplicationTransactionCommentEditor $actor = $this->requireActor(); + $this->applyMFAChecks($xaction, $comment); + $comment->setContentSource($this->getContentSource()); $comment->setAuthorPHID($this->getActingAsPHID()); @@ -160,5 +192,94 @@ final class PhabricatorApplicationTransactionCommentEditor } } + private function applyMFAChecks( + PhabricatorApplicationTransaction $xaction, + PhabricatorApplicationTransactionComment $comment) { + $actor = $this->requireActor(); + + // We don't do any MFA checks here when you're creating a comment for the + // first time (the parent editor handles them for us), so we can just bail + // out if this is the creation flow. + if ($this->getIsNewComment()) { + return; + } + + $request = $this->getRequest(); + if (!$request) { + throw new PhutilInvalidStateException('setRequest'); + } + + $cancel_uri = $this->getCancelURI(); + if (!strlen($cancel_uri)) { + throw new PhutilInvalidStateException('setCancelURI'); + } + + // If you're deleting a comment, we try to prompt you for MFA if you have + // it configured, but do not require that you have it configured. In most + // cases, this is administrators removing content. + + // See PHI1173. If you're editing a comment you authored and the original + // comment was signed with MFA, you MUST have MFA on your account and you + // MUST sign the edit with MFA. Otherwise, we can end up with an MFA badge + // on different content than what was signed. + + $want_mfa = false; + $need_mfa = false; + + if ($comment->getIsRemoved()) { + // Try to prompt on removal. + $want_mfa = true; + } + + if ($xaction->getIsMFATransaction()) { + if ($actor->getPHID() === $xaction->getAuthorPHID()) { + // Strictly require MFA if the original transaction was signed and + // you're the author. + $want_mfa = true; + $need_mfa = true; + } + } + + if (!$want_mfa) { + return; + } + + if ($need_mfa) { + $factors = id(new PhabricatorAuthFactorConfigQuery()) + ->setViewer($actor) + ->withUserPHIDs(array($this->getActingAsPHID())) + ->withFactorProviderStatuses( + array( + PhabricatorAuthFactorProviderStatus::STATUS_ACTIVE, + PhabricatorAuthFactorProviderStatus::STATUS_DEPRECATED, + )) + ->execute(); + if (!$factors) { + $error = new PhabricatorApplicationTransactionValidationError( + $xaction->getTransactionType(), + pht('No MFA'), + pht( + 'This comment was signed with MFA, so edits to it must also be '. + 'signed with MFA. You do not have any MFA factors attached to '. + 'your account, so you can not sign this edit. Add MFA to your '. + 'account in Settings.'), + $xaction); + + throw new PhabricatorApplicationTransactionValidationException( + array( + $error, + )); + } + } + + $workflow_key = sprintf( + 'comment.edit(%s, %d)', + $xaction->getPHID(), + $xaction->getComment()->getID()); + + $hisec_token = id(new PhabricatorAuthSessionEngine()) + ->setWorkflowKey($workflow_key) + ->requireHighSecurityToken($actor, $request, $cancel_uri); + } } diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php index 3e5e9c23c9..06c9b43216 100644 --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -1113,7 +1113,8 @@ abstract class PhabricatorApplicationTransactionEditor $comment_editor = id(new PhabricatorApplicationTransactionCommentEditor()) ->setActor($actor) ->setActingAsPHID($this->getActingAsPHID()) - ->setContentSource($this->getContentSource()); + ->setContentSource($this->getContentSource()) + ->setIsNewComment(true); if (!$transaction_open) { $object->openTransaction(); From f1a7eb66da36acc6313002af6e51f092e699ea0f Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 28 Mar 2019 16:14:47 -0700 Subject: [PATCH 223/245] Fix a straggling issue with cursor changes impacting Conpherence thread indexing Summary: Ref T13266. Caught one more of these "directly setting afterID" issues in the logs. Test Plan: Ran `bin/search index --type ConpherenceThread` before and after changes. Before: fatal about a direct call. After: clean index rebuild. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13266 Differential Revision: https://secure.phabricator.com/D20341 --- .../ConpherenceThreadIndexEngineExtension.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/applications/conpherence/engineextension/ConpherenceThreadIndexEngineExtension.php b/src/applications/conpherence/engineextension/ConpherenceThreadIndexEngineExtension.php index d45e347729..740a1d81e2 100644 --- a/src/applications/conpherence/engineextension/ConpherenceThreadIndexEngineExtension.php +++ b/src/applications/conpherence/engineextension/ConpherenceThreadIndexEngineExtension.php @@ -51,13 +51,16 @@ final class ConpherenceThreadIndexEngineExtension ConpherenceThread $thread, ConpherenceTransaction $xaction) { - $previous = id(new ConpherenceTransactionQuery()) + $pager = id(new AphrontCursorPagerView()) + ->setPageSize(1) + ->setAfterID($xaction->getID()); + + $previous_xactions = id(new ConpherenceTransactionQuery()) ->setViewer($this->getViewer()) ->withObjectPHIDs(array($thread->getPHID())) ->withTransactionTypes(array(PhabricatorTransactions::TYPE_COMMENT)) - ->setAfterID($xaction->getID()) - ->setLimit(1) - ->executeOne(); + ->executeWithCursorPager($pager); + $previous = head($previous_xactions); $index = id(new ConpherenceIndex()) ->setThreadPHID($thread->getPHID()) From 953a449305bbd073207602f070b73bd917db86d5 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 28 Mar 2019 16:21:24 -0700 Subject: [PATCH 224/245] Hide "Availability" and "Calendar" on user profiles for disabled users Summary: See downstream . That suggestion is a little light on details, but I basically agree that showing "Availability: Available" on disabled user profiles is kind of questionable/misleading. Just hide event information on disabled profiles, since this doesn't seem worth building a special "Availability: Who Knows, They Are Disabled, Good Luck" disabled state for. Test Plan: Looked at disabled and non-disabled user profiles, saw Calendar stuff only on the former. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20342 --- .../controller/PhabricatorPeopleProfileViewController.php | 6 ++++++ .../people/customfield/PhabricatorUserStatusField.php | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/src/applications/people/controller/PhabricatorPeopleProfileViewController.php b/src/applications/people/controller/PhabricatorPeopleProfileViewController.php index 0c32075932..6a4d68d6aa 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfileViewController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfileViewController.php @@ -175,6 +175,12 @@ final class PhabricatorPeopleProfileViewController return null; } + // Don't show calendar information for disabled users, since it's probably + // not useful or accurate and may be misleading. + if ($user->getIsDisabled()) { + return null; + } + $midnight = PhabricatorTime::getTodayMidnightDateTime($viewer); $week_end = clone $midnight; $week_end = $week_end->modify('+3 days'); diff --git a/src/applications/people/customfield/PhabricatorUserStatusField.php b/src/applications/people/customfield/PhabricatorUserStatusField.php index 2ae9158566..1716e8e198 100644 --- a/src/applications/people/customfield/PhabricatorUserStatusField.php +++ b/src/applications/people/customfield/PhabricatorUserStatusField.php @@ -30,6 +30,12 @@ final class PhabricatorUserStatusField $user = $this->getObject(); $viewer = $this->requireViewer(); + // Don't show availability for disabled users, since this is vaguely + // misleading to say "Availability: Available" and probably not useful. + if ($user->getIsDisabled()) { + return null; + } + return id(new PHUIUserAvailabilityView()) ->setViewer($viewer) ->setAvailableUser($user); From eecee172139eb7e4b94307cb70ca515d09aa5ea7 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 28 Mar 2019 16:49:03 -0700 Subject: [PATCH 225/245] Activate "jx-toggle-class" on click to fix broken mobile behavior Summary: See downstream . Searching for things on mobile is a significant challenge because clicking the "Magnifying Glass" icon shows and then immediately hides the menu. I believe some aspect of iOS event handling has changed since this was originally written. At some point, I'd like to rewrite this to work more cleanly and get rid of `jx-toggle-class`. In particular, it isn't smart enough to know that it should be modal with other menus, so you can get states like this by clicking multiple things: {F6320110} This would also probably just look and work better if it was an inline element that showed up under the header instead of a floating dropdown element. However, I'm having a hard time getting the Safari debugger to actually connect to the iOS simulator, so take a small step toward this bright future and fix the immediate problem for now: toggle on click instead of mousedown/touchstart. This means the menu opens ~100ms later, but actually works. Big improvement! I'd like to move away from "jx-toggle-class" anyway (it usually isn't sophisticated enough to fully describe a behavior) so reducing complexity here seems good. It isn't used in //too// many places so this is unlikely to have any negative effects, I hope. Test Plan: On iOS simulator, clicked the magnifying glass icon in the main menu to get a search input. Before: got a search input for a microsecond. After: actually got a search input. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20343 --- resources/celerity/map.php | 16 ++++++++-------- webroot/rsrc/js/core/behavior-toggle-class.js | 11 +---------- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index c1a77bca11..a08ef54102 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -10,7 +10,7 @@ return array( 'conpherence.pkg.css' => '3c8a0668', 'conpherence.pkg.js' => '020aebcf', 'core.pkg.css' => '2dd936d6', - 'core.pkg.js' => 'eb53fc5b', + 'core.pkg.js' => 'a747b035', 'differential.pkg.css' => '8d8360fb', 'differential.pkg.js' => '67e02996', 'diffusion.pkg.css' => '42c75c37', @@ -498,7 +498,7 @@ return array( 'rsrc/js/core/behavior-select-on-click.js' => '66365ee2', 'rsrc/js/core/behavior-setup-check-https.js' => '01384686', 'rsrc/js/core/behavior-time-typeahead.js' => '5803b9e7', - 'rsrc/js/core/behavior-toggle-class.js' => 'f5c78ae3', + 'rsrc/js/core/behavior-toggle-class.js' => '32db8374', 'rsrc/js/core/behavior-tokenizer.js' => '3b4899b0', 'rsrc/js/core/behavior-tooltip.js' => '73ecc1f8', 'rsrc/js/core/behavior-user-menu.js' => '60cd9241', @@ -687,7 +687,7 @@ return array( 'javelin-behavior-stripe-payment-form' => '02cb4398', 'javelin-behavior-test-payment-form' => '4a7fb02b', 'javelin-behavior-time-typeahead' => '5803b9e7', - 'javelin-behavior-toggle-class' => 'f5c78ae3', + 'javelin-behavior-toggle-class' => '32db8374', 'javelin-behavior-toggle-widget' => '8f959ad0', 'javelin-behavior-trigger-rule-editor' => '398fdf13', 'javelin-behavior-typeahead-browse' => '70245195', @@ -1186,6 +1186,11 @@ return array( 'javelin-install', 'javelin-util', ), + '32db8374' => array( + 'javelin-behavior', + 'javelin-stratcom', + 'javelin-dom', + ), 34450586 => array( 'javelin-color', 'javelin-install', @@ -2149,11 +2154,6 @@ return array( 'javelin-stratcom', 'javelin-dom', ), - 'f5c78ae3' => array( - 'javelin-behavior', - 'javelin-stratcom', - 'javelin-dom', - ), 'f84bcbf4' => array( 'javelin-behavior', 'javelin-stratcom', diff --git a/webroot/rsrc/js/core/behavior-toggle-class.js b/webroot/rsrc/js/core/behavior-toggle-class.js index d4756eb6bb..18663b0487 100644 --- a/webroot/rsrc/js/core/behavior-toggle-class.js +++ b/webroot/rsrc/js/core/behavior-toggle-class.js @@ -17,7 +17,7 @@ JX.behavior('toggle-class', function(config, statics) { function install() { JX.Stratcom.listen( - ['touchstart', 'mousedown'], + 'click', 'jx-toggle-class', function(e) { e.kill(); @@ -29,15 +29,6 @@ JX.behavior('toggle-class', function(config, statics) { } }); - // Swallow the regular click handler event so e.g. Quicksand - // click handler doesn't get a hold of it - JX.Stratcom.listen( - ['click'], - 'jx-toggle-class', - function(e) { - e.kill(); - }); - return true; } From c4856c37e7a6f0cd279418f8ab72750c5f08ebdf Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 28 Mar 2019 17:04:27 -0700 Subject: [PATCH 226/245] Fix content overflow in user hovercards Summary: Fixes T13273. This element is a bit weird, but I think I fixed it without breaking anything. The CSS is used by project hovercards and user hovercards, but they each have a class which builds mostly-shared-but-not-really-identical CSS, instead of having a single `View` class with modes. So I'm not 100% sure I didn't break something obscure, but I couldn't find anything this breaks. The major issue is that all the text content has "position: absolute". Instead, make the image "absolute" and the text actual positioned content. Then fix all the margins/padding/spacing/layout and add overflow. Seems to work? Plus: hide availability for disabled users, for consistency with D20342. Test Plan: Before: {F6320155} After: {F6320156} I think this is pixel-exact except for the overflow behavior. Also: - Viewed some other user hovercards, including a disabled user. They all looked unchanged. - Viewed some project hovercards. They all looked good, too. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13273 Differential Revision: https://secure.phabricator.com/D20344 --- resources/celerity/map.php | 4 ++-- .../people/view/PhabricatorUserCardView.php | 21 +++++++++------- .../application/project/project-card-view.css | 24 +++++++++++++++---- 3 files changed, 33 insertions(+), 16 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index a08ef54102..fce6b89474 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -99,7 +99,7 @@ return array( 'rsrc/css/application/policy/policy-transaction-detail.css' => 'c02b8384', 'rsrc/css/application/policy/policy.css' => 'ceb56a08', 'rsrc/css/application/ponder/ponder-view.css' => '05a09d0a', - 'rsrc/css/application/project/project-card-view.css' => '3b1f7b20', + 'rsrc/css/application/project/project-card-view.css' => '4e7371cd', 'rsrc/css/application/project/project-triggers.css' => 'cb866c2d', 'rsrc/css/application/project/project-view.css' => '567858b3', 'rsrc/css/application/releeph/releeph-core.css' => 'f81ff2db', @@ -881,7 +881,7 @@ return array( 'policy-edit-css' => '8794e2ed', 'policy-transaction-detail-css' => 'c02b8384', 'ponder-view-css' => '05a09d0a', - 'project-card-view-css' => '3b1f7b20', + 'project-card-view-css' => '4e7371cd', 'project-triggers-css' => 'cb866c2d', 'project-view-css' => '567858b3', 'releeph-core' => 'f81ff2db', diff --git a/src/applications/people/view/PhabricatorUserCardView.php b/src/applications/people/view/PhabricatorUserCardView.php index f1fc515f88..21cb468ba8 100644 --- a/src/applications/people/view/PhabricatorUserCardView.php +++ b/src/applications/people/view/PhabricatorUserCardView.php @@ -95,14 +95,17 @@ final class PhabricatorUserCardView extends AphrontTagView { 'fa-user-plus', phabricator_date($user->getDateCreated(), $viewer)); - if (PhabricatorApplication::isClassInstalledForViewer( - 'PhabricatorCalendarApplication', - $viewer)) { - $body[] = $this->addItem( - 'fa-calendar-o', - id(new PHUIUserAvailabilityView()) - ->setViewer($viewer) - ->setAvailableUser($user)); + $has_calendar = PhabricatorApplication::isClassInstalledForViewer( + 'PhabricatorCalendarApplication', + $viewer); + if ($has_calendar) { + if (!$user->getIsDisabled()) { + $body[] = $this->addItem( + 'fa-calendar-o', + id(new PHUIUserAvailabilityView()) + ->setViewer($viewer) + ->setAvailableUser($user)); + } } $classes[] = 'project-card-image'; @@ -150,8 +153,8 @@ final class PhabricatorUserCardView extends AphrontTagView { 'class' => 'project-card-inner', ), array( - $image, $header, + $image, )); return $card; diff --git a/webroot/rsrc/css/application/project/project-card-view.css b/webroot/rsrc/css/application/project/project-card-view.css index b960d55cef..cce4789ef7 100644 --- a/webroot/rsrc/css/application/project/project-card-view.css +++ b/webroot/rsrc/css/application/project/project-card-view.css @@ -36,22 +36,36 @@ } .project-card-view .project-card-image { + position: absolute; height: 140px; width: 140px; - margin: 6px; + top: 6px; + left: 6px; border-radius: 3px; } .project-card-view .project-card-image-href { - display: inline-block; + display: block; } .project-card-view .project-card-item div { display: inline; } +.project-card-inner { + position: relative; +} + +.people-card-view .project-card-inner { + padding: 6px; + min-height: 140px; +} + .project-card-view .project-card-item { margin-bottom: 2px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .project-card-view .project-card-item-text { @@ -63,9 +77,9 @@ } .project-card-view .project-card-header { - position: absolute; - top: 12px; - left: 158px; + margin-top: 6px; + margin-left: 152px; + overflow: hidden; } .project-card-header .project-card-name { From e586ed439a790f2087948ecf701695cad9955700 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 28 Mar 2019 17:39:28 -0700 Subject: [PATCH 227/245] Improve overflow/ellipsis behaivor for very wide task graphs Summary: See downstream . The `T123 Task Name` column in graphs can currently fold down to 0 pixels wide. Although it's visually nice to render this element without a scroll bar when we don't really need one, the current behavior is excessive and not very useful. Instead, tweak the CSS so: - This cell is always at least 320px wide. - After 320px, we'll overflow/ellipsis the cell on small screens. This generally gives us better behavior: - Small screens get a scrollbar to see a reasonable amount of content. - The UI doesn't turn into a total mess if one task has a whole novel of text. Test Plan: Old behavior, note that there's no scrollbar and the cell is so narrow it is useless: {F6320208} New behavior, same default view, has a scrollbar: {F6320209} Scrolling over gives you this: {F6320210} On a wider screen (this wide or better), we don't need to scroll: {F6320211} Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20345 --- resources/celerity/map.php | 6 +++--- webroot/rsrc/css/aphront/table-view.css | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index fce6b89474..4912f91b08 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,7 +9,7 @@ return array( 'names' => array( 'conpherence.pkg.css' => '3c8a0668', 'conpherence.pkg.js' => '020aebcf', - 'core.pkg.css' => '2dd936d6', + 'core.pkg.css' => '7e6e954b', 'core.pkg.js' => 'a747b035', 'differential.pkg.css' => '8d8360fb', 'differential.pkg.js' => '67e02996', @@ -30,7 +30,7 @@ return array( 'rsrc/css/aphront/notification.css' => '30240bd2', 'rsrc/css/aphront/panel-view.css' => '46923d46', 'rsrc/css/aphront/phabricator-nav-view.css' => 'f8a0c1bf', - 'rsrc/css/aphront/table-view.css' => '205053cd', + 'rsrc/css/aphront/table-view.css' => '7dc3a9c2', 'rsrc/css/aphront/tokenizer.css' => 'b52d0668', 'rsrc/css/aphront/tooltip.css' => 'e3f2412f', 'rsrc/css/aphront/typeahead-browse.css' => 'b7ed02d2', @@ -531,7 +531,7 @@ return array( 'aphront-list-filter-view-css' => 'feb64255', 'aphront-multi-column-view-css' => 'fbc00ba3', 'aphront-panel-view-css' => '46923d46', - 'aphront-table-view-css' => '205053cd', + 'aphront-table-view-css' => '7dc3a9c2', 'aphront-tokenizer-control-css' => 'b52d0668', 'aphront-tooltip-css' => 'e3f2412f', 'aphront-typeahead-control-css' => '8779483d', diff --git a/webroot/rsrc/css/aphront/table-view.css b/webroot/rsrc/css/aphront/table-view.css index a08674cf14..fd1a918148 100644 --- a/webroot/rsrc/css/aphront/table-view.css +++ b/webroot/rsrc/css/aphront/table-view.css @@ -229,7 +229,8 @@ span.single-display-line-content { word-wrap: break-word; overflow: hidden; text-overflow: ellipsis; - max-width: 0; + min-width: 320px; + max-width: 320px; } .aphront-table-view tr.closed td.object-link .object-name, From cec779cdabeece5f4a93d746939062ba1f34a577 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 28 Mar 2019 17:49:00 -0700 Subject: [PATCH 228/245] When drawing a very wide graph line diagram, smush it together a bit Summary: Depends on D20345. Use a narrower layout for very large graphs to save some space. Test Plan: Before: {F6320215} After: {F6320216} This does not affect smaller graphs. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20346 --- resources/celerity/map.php | 16 ++++++++-------- .../diffusion/behavior-commit-graph.js | 10 +++++++++- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 4912f91b08..1f36bf03b4 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -14,7 +14,7 @@ return array( 'differential.pkg.css' => '8d8360fb', 'differential.pkg.js' => '67e02996', 'diffusion.pkg.css' => '42c75c37', - 'diffusion.pkg.js' => '91192d85', + 'diffusion.pkg.js' => 'a98c0bf7', 'maniphest.pkg.css' => '35995d6d', 'maniphest.pkg.js' => 'c9308721', 'rsrc/audio/basic/alert.mp3' => '17889334', @@ -384,7 +384,7 @@ return array( 'rsrc/js/application/diffusion/DiffusionLocateFileSource.js' => '94243d89', 'rsrc/js/application/diffusion/behavior-audit-preview.js' => 'b7b73831', 'rsrc/js/application/diffusion/behavior-commit-branches.js' => '4b671572', - 'rsrc/js/application/diffusion/behavior-commit-graph.js' => '1c88f154', + 'rsrc/js/application/diffusion/behavior-commit-graph.js' => 'ef836bf2', 'rsrc/js/application/diffusion/behavior-locate-file.js' => '87428eb2', 'rsrc/js/application/diffusion/behavior-pull-lastmodified.js' => 'c715c123', 'rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js' => '6a85bc5a', @@ -606,7 +606,7 @@ return array( 'javelin-behavior-differential-diff-radios' => '925fe8cd', 'javelin-behavior-differential-populate' => 'dfa1d313', 'javelin-behavior-diffusion-commit-branches' => '4b671572', - 'javelin-behavior-diffusion-commit-graph' => '1c88f154', + 'javelin-behavior-diffusion-commit-graph' => 'ef836bf2', 'javelin-behavior-diffusion-locate-file' => '87428eb2', 'javelin-behavior-diffusion-pull-lastmodified' => 'c715c123', 'javelin-behavior-document-engine' => '243d6c22', @@ -1033,11 +1033,6 @@ return array( 'javelin-install', 'javelin-util', ), - '1c88f154' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-stratcom', - ), '1cab0e9a' => array( 'javelin-behavior', 'javelin-dom', @@ -2124,6 +2119,11 @@ return array( 'phabricator-keyboard-shortcut', 'javelin-stratcom', ), + 'ef836bf2' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-stratcom', + ), 'f166c949' => array( 'javelin-behavior', 'javelin-behavior-device', diff --git a/webroot/rsrc/js/application/diffusion/behavior-commit-graph.js b/webroot/rsrc/js/application/diffusion/behavior-commit-graph.js index 309f972324..5c4591b542 100644 --- a/webroot/rsrc/js/application/diffusion/behavior-commit-graph.js +++ b/webroot/rsrc/js/application/diffusion/behavior-commit-graph.js @@ -44,11 +44,19 @@ JX.behavior('diffusion-commit-graph', function(config) { cxt.stroke(); } + // If the graph is going to be wide, squish it a bit so it doesn't take up + // quite as much space. + var default_width; + if (config.count >= 8) { + default_width = 6; + } else { + default_width = 12; + } for (var ii = 0; ii < nodes.length; ii++) { var data = JX.Stratcom.getData(nodes[ii]); - var cell = 12; // Width of each thread. + var cell = default_width; var xpos = function(col) { return (col * cell) + (cell / 2); }; From 02f94cd7d2885de502d03934af3a0c24453e8e58 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 29 Mar 2019 09:50:50 -0700 Subject: [PATCH 229/245] Fix an issue with Duo not live-updating properly on login gates Summary: See . The "live update Duo status" endpoint currently requires full sessions, and doesn't work from the session upgrade gate on login. Don't require a full session to check the status of an MFA challenge. Test Plan: Went through Duo gate in a new session, got a live update. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20347 --- .../mfa/PhabricatorAuthChallengeStatusController.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/applications/auth/controller/mfa/PhabricatorAuthChallengeStatusController.php b/src/applications/auth/controller/mfa/PhabricatorAuthChallengeStatusController.php index 884bbaad6d..3fbffabc89 100644 --- a/src/applications/auth/controller/mfa/PhabricatorAuthChallengeStatusController.php +++ b/src/applications/auth/controller/mfa/PhabricatorAuthChallengeStatusController.php @@ -3,6 +3,12 @@ final class PhabricatorAuthChallengeStatusController extends PhabricatorAuthController { + public function shouldAllowPartialSessions() { + // We expect that users may request the status of an MFA challenge when + // they hit the session upgrade gate on login. + return true; + } + public function handleRequest(AphrontRequest $request) { $viewer = $this->getViewer(); $id = $request->getURIData('id'); From ea182b6df9ecdb647955f22f6676b43122d553ff Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 29 Mar 2019 16:23:56 -0700 Subject: [PATCH 230/245] When we failover to a replica, log the exception we hit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Summary: See PHI1180. Currently, when we failover to a replica, we may not log the failure. Failovers are serious business and bad news, so emit a log even if we are able to connect to the replica. Test Plan: Configured a bogus master and a good replica: ``` $ ./bin/mail list-outbound [2019-03-29 16:26:09] PHLOG: 'Retrying (attempt 1) after connection failure ("AphrontConnectionQueryException", #2002): Attempt to connect to root@127.0.0.2 failed with error #2002: Operation timed out.' at [/Users/epriestley/dev/core/lib/libphutil/src/aphront/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php:124] [2019-03-29 16:26:19] PHLOG: 'Retrying (attempt 2) after connection failure ("AphrontConnectionQueryException", #2002): Attempt to connect to root@127.0.0.2 failed with error #2002: Operation timed out.' at [/Users/epriestley/dev/core/lib/libphutil/src/aphront/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php:124] [2019-03-29 16:26:29] EXCEPTION: (PhutilProxyException) Failed to connect to master database ("local_config"), failing over into read-only mode. {>} (AphrontConnectionQueryException) Attempt to connect to root@127.0.0.2 failed with error #2002: Operation timed out. at [/src/aphront/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php:362] <...snip backtrace...> 3945 Voided email rP04f9e72cbd10: Don't subscribe bots implicitly when they act on objects, or when they are… 3946 Voided email rPdf53d72e794c: Allow "Move Tasks to Column..." to prompt for MFA 3947 Voided email rP492b03628f19: Fix a typo in Drydock "Land" operations 3948 Voided email rPb469a5134ddd: Allow "SMTP" and "Sendmail" mailers to have "Message-ID" behavior configured in… 3949 Voided email rPa6fd8f04792d: When performing complex edits, pause sub-editors before they publish to… ... ``` Configured a bogus master and a bogus replica: ``` $ ./bin/mail list-outbound [2019-03-29 16:26:57] PHLOG: 'Retrying (attempt 1) after connection failure ("AphrontConnectionQueryException", #2002): Attempt to connect to root@127.0.0.2 failed with error #2002: Operation timed out.' at [/Users/epriestley/dev/core/lib/libphutil/src/aphront/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php:124] [2019-03-29 16:27:07] PHLOG: 'Retrying (attempt 2) after connection failure ("AphrontConnectionQueryException", #2002): Attempt to connect to root@127.0.0.2 failed with error #2002: Operation timed out.' at [/Users/epriestley/dev/core/lib/libphutil/src/aphront/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php:124] [2019-03-29 16:27:27] PHLOG: 'Retrying (attempt 1) after connection failure ("AphrontConnectionQueryException", #2002): Attempt to connect to root@127.0.0.3 failed with error #2002: Operation timed out.' at [/Users/epriestley/dev/core/lib/libphutil/src/aphront/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php:124] [2019-03-29 16:27:37] PHLOG: 'Retrying (attempt 2) after connection failure ("AphrontConnectionQueryException", #2002): Attempt to connect to root@127.0.0.3 failed with error #2002: Operation timed out.' at [/Users/epriestley/dev/core/lib/libphutil/src/aphront/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php:124] [2019-03-29 16:27:47] EXCEPTION: (PhabricatorClusterStrandedException) Unable to establish a connection to any database host (while trying "local_config"). All masters and replicas are completely unreachable. AphrontConnectionQueryException: Attempt to connect to root@127.0.0.2 failed with error #2002: Operation timed out. at [/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php:177] <...snip backtrace...> ``` Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20351 --- .../storage/lisk/PhabricatorLiskDAO.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php b/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php index 99603da567..85adb4fc29 100644 --- a/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php +++ b/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php @@ -110,6 +110,19 @@ abstract class PhabricatorLiskDAO extends LiskDAO { $connection = $replica->newApplicationConnection($database); $connection->setReadOnly(true); if ($replica->isReachable($connection)) { + if ($master_exception) { + // If we ended up here as the result of a failover, log the + // exception. This is seriously bad news even if we are able + // to recover from it. + $proxy_exception = new PhutilProxyException( + pht( + 'Failed to connect to master database ("%s"), failing over '. + 'into read-only mode.', + $database), + $master_exception); + phlog($proxy_exception); + } + return $connection; } } From 00b1c4190c412c365254977e293f5cbbfbc84dd9 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 30 Mar 2019 14:33:19 -0700 Subject: [PATCH 231/245] Correct some straggling Ferret/Cursor interactions Summary: See PHI1182. Ref T13266. The recent fixes didn't quite cover the case where you have a query, but order by something other than relevance, and page forward. Refine the tests around building/selecting these columns and paging values a little bit to be more specific about what they care about. Test Plan: Executed queries, then went to "Next Page", for: - query text, non-relevance order. - query text, relevance order. - no query text, non-relevance order. - no query text, relevance order. Also, made an API call similar to the one in PHI1182. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13266 Differential Revision: https://secure.phabricator.com/D20354 --- ...PhabricatorCursorPagedPolicyAwareQuery.php | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php index 378c282e14..f5586fd90f 100644 --- a/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php +++ b/src/infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php @@ -212,7 +212,7 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery } if ($this->supportsFerretEngine()) { - if ($this->getFerretTokens()) { + if ($this->hasFerretOrder()) { $map += array( 'rank' => $cursor->getRawRowProperty(self::FULLTEXT_RANK), @@ -1840,15 +1840,16 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery return $select; } - $vector = $this->getOrderVector(); - if (!$vector->containsKey('rank')) { - // We only need to SELECT the virtual "_ft_rank" column if we're + if (!$this->hasFerretOrder()) { + // We only need to SELECT the virtual rank/relevance columns if we're // actually sorting the results by rank. return $select; } if (!$this->ferretEngine) { $select[] = qsprintf($conn, '0 AS %T', self::FULLTEXT_RANK); + $select[] = qsprintf($conn, '0 AS %T', self::FULLTEXT_CREATED); + $select[] = qsprintf($conn, '0 AS %T', self::FULLTEXT_MODIFIED); return $select; } @@ -3152,4 +3153,22 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery } } + private function hasFerretOrder() { + $vector = $this->getOrderVector(); + + if ($vector->containsKey('rank')) { + return true; + } + + if ($vector->containsKey('fulltext-created')) { + return true; + } + + if ($vector->containsKey('fulltext-modified')) { + return true; + } + + return false; + } + } From cddbe306f9274b1ea22af86763236ce88572338f Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 1 Apr 2019 10:38:25 -0700 Subject: [PATCH 232/245] Correct a case where a single-hunk diff may incorrectly be identified as multi-hunk by the Scope engine Summary: See PHI985. The layers above this may return `array()` to mean "one hunk with a line-1 offset". Accept either `array()` or `array(1 => ...)` to engage the scope engine. Test Plan: See PHI985. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20363 --- .../differential/render/DifferentialChangesetRenderer.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/applications/differential/render/DifferentialChangesetRenderer.php b/src/applications/differential/render/DifferentialChangesetRenderer.php index 26de5cb53b..4ed77bf041 100644 --- a/src/applications/differential/render/DifferentialChangesetRenderer.php +++ b/src/applications/differential/render/DifferentialChangesetRenderer.php @@ -684,7 +684,12 @@ abstract class DifferentialChangesetRenderer extends Phobject { // If this change is missing context, don't try to identify scopes, since // we won't really be able to get anywhere. $has_multiple_hunks = (count($hunk_starts) > 1); - $has_offset_hunks = (head_key($hunk_starts) != 1); + + $has_offset_hunks = false; + if ($hunk_starts) { + $has_offset_hunks = (head_key($hunk_starts) != 1); + } + $missing_context = ($has_multiple_hunks || $has_offset_hunks); if ($missing_context) { From dba1b107203318ac7b0b0c7b39f0e1fc01717ac4 Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 1 Apr 2019 14:13:36 -0700 Subject: [PATCH 233/245] Deactivate the remarkup autosuggest once text can't match "[[" or "((" rules Summary: See PHI1185, which reports a performance issue with "(" in remarkup in certain contexts. I can't reproduce the performance issue, but I can reproduce the autosuggester incorrectly remaining active and swallowing return characters. When the user types `(` or `[`, we wait for a prefix for the `((` (Phurl) or `[[` (Phriction) rules. We currently continue looking for that prefix until a character is entered that explicitly interrupts the search. For example, typing `(xxx` does not insert a return character, because we're stuck on matching the prefix. Instead, as soon as the user has entered text that we know won't ever match the prefix, deactivate the autocomplete. We can slightly cheat through this by just looking for at least one character of text, since all prefixes are exactly one character long. If we eventually have some kind of `~~@(xyz)` rule we might need to add a more complicated piece of rejection logic. Test Plan: Typed `(xxx`, got a return. Used `((` and `[[` autosuggest rules normally. Used `JX.log()` to sanity check that nothing too crazy seems to be happening. Reviewers: amckinley Reviewed By: amckinley Differential Revision: https://secure.phabricator.com/D20365 --- resources/celerity/map.php | 16 ++++++++-------- webroot/rsrc/js/phuix/PHUIXAutocomplete.js | 7 +++++++ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 1f36bf03b4..20d648f549 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -516,7 +516,7 @@ return array( 'rsrc/js/phui/behavior-phui-timer-control.js' => 'f84bcbf4', 'rsrc/js/phuix/PHUIXActionListView.js' => 'c68f183f', 'rsrc/js/phuix/PHUIXActionView.js' => 'aaa08f3b', - 'rsrc/js/phuix/PHUIXAutocomplete.js' => '8f139ef0', + 'rsrc/js/phuix/PHUIXAutocomplete.js' => '2fbe234d', 'rsrc/js/phuix/PHUIXButtonView.js' => '55a24e84', 'rsrc/js/phuix/PHUIXDropdownMenu.js' => 'bdce4d78', 'rsrc/js/phuix/PHUIXExample.js' => 'c2c500a7', @@ -872,7 +872,7 @@ return array( 'phui-workpanel-view-css' => '3ae89b20', 'phuix-action-list-view' => 'c68f183f', 'phuix-action-view' => 'aaa08f3b', - 'phuix-autocomplete' => '8f139ef0', + 'phuix-autocomplete' => '2fbe234d', 'phuix-button-view' => '55a24e84', 'phuix-dropdown-menu' => 'bdce4d78', 'phuix-form-control-view' => '38c1f3fb', @@ -1173,6 +1173,12 @@ return array( 'phuix-autocomplete', 'javelin-mask', ), + '2fbe234d' => array( + 'javelin-install', + 'javelin-dom', + 'phuix-icon-view', + 'phabricator-prefab', + ), '308f9fe4' => array( 'javelin-install', 'javelin-util', @@ -1634,12 +1640,6 @@ return array( '8e2d9a28' => array( 'phui-theme-css', ), - '8f139ef0' => array( - 'javelin-install', - 'javelin-dom', - 'phuix-icon-view', - 'phabricator-prefab', - ), '8f959ad0' => array( 'javelin-behavior', 'javelin-dom', diff --git a/webroot/rsrc/js/phuix/PHUIXAutocomplete.js b/webroot/rsrc/js/phuix/PHUIXAutocomplete.js index deb9f9d100..2eaa9bafe1 100644 --- a/webroot/rsrc/js/phuix/PHUIXAutocomplete.js +++ b/webroot/rsrc/js/phuix/PHUIXAutocomplete.js @@ -555,6 +555,13 @@ JX.install('PHUIXAutocomplete', { if (prefix) { var pattern = new RegExp(prefix); if (!trim.match(pattern)) { + // If the prefix pattern can not match the text, deactivate. (This + // check might need to be more careful if we have a more varied + // set of prefixes in the future, but for now they're all a single + // prefix character.) + if (trim.length) { + this._deactivate(); + } return; } trim = trim.replace(pattern, ''); From 3e05ff2e99839321376c14eac450d4f4d1eddca9 Mon Sep 17 00:00:00 2001 From: Austin McKinley Date: Tue, 2 Apr 2019 11:53:35 -0700 Subject: [PATCH 234/245] Improve Conpherence behavior for logged out users. Summary: There are two issues here I was trying to fix: * Viewing `/conpherence` by logged out users on `secure` would generate an overheated query on `ConpherenceThreadQuery` `secure` has a ton of wacky threads with bogus names. * When a user views a specific thread that they don't have permission to see, we attempt to fetch the thread's transactions before applying policy filtering. If the thread has more than 1000 comments, that query will also overheat instead of returning a policy exception. I fixed the first problem, but started trying to fix the second by moving the transaction fetch to `didFilterPage` but it broke in strange ways so I gave up. Also fix a dangling `qsprintf` update. Test Plan: Loaded threads and the Conpherence homepage with and without logged in users. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D20375 --- .../conpherence/query/ConpherenceThreadQuery.php | 16 +++++++++++----- .../conpherence/view/ConpherenceLayoutView.php | 8 ++++---- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/applications/conpherence/query/ConpherenceThreadQuery.php b/src/applications/conpherence/query/ConpherenceThreadQuery.php index 9c6682a8a7..99fd18878e 100644 --- a/src/applications/conpherence/query/ConpherenceThreadQuery.php +++ b/src/applications/conpherence/query/ConpherenceThreadQuery.php @@ -136,7 +136,7 @@ final class ConpherenceThreadQuery protected function buildGroupClause(AphrontDatabaseConnection $conn_r) { if ($this->participantPHIDs !== null || strlen($this->fulltext)) { - return 'GROUP BY thread.id'; + return qsprintf($conn_r, 'GROUP BY thread.id'); } else { return $this->buildApplicationSearchGroupClause($conn_r); } @@ -192,18 +192,24 @@ final class ConpherenceThreadQuery if ($can_optimize) { $members_policy = id(new ConpherenceThreadMembersPolicyRule()) ->getObjectPolicyFullKey(); + $policies = array( + $members_policy, + PhabricatorPolicies::POLICY_USER, + PhabricatorPolicies::POLICY_ADMIN, + PhabricatorPolicies::POLICY_NOONE, + ); if ($viewer->isLoggedIn()) { $where[] = qsprintf( $conn, - 'thread.viewPolicy != %s OR vp.participantPHID = %s', - $members_policy, + 'thread.viewPolicy NOT IN (%Ls) OR vp.participantPHID = %s', + $policies, $viewer->getPHID()); } else { $where[] = qsprintf( $conn, - 'thread.viewPolicy != %s', - $members_policy); + 'thread.viewPolicy NOT IN (%Ls)', + $policies); } } diff --git a/src/applications/conpherence/view/ConpherenceLayoutView.php b/src/applications/conpherence/view/ConpherenceLayoutView.php index 7dbc00a325..3382a9ba87 100644 --- a/src/applications/conpherence/view/ConpherenceLayoutView.php +++ b/src/applications/conpherence/view/ConpherenceLayoutView.php @@ -224,12 +224,12 @@ final class ConpherenceLayoutView extends AphrontTagView { private function buildNUXView() { $viewer = $this->getViewer(); - $engine = new ConpherenceThreadSearchEngine(); - $engine->setViewer($viewer); + $engine = id(new ConpherenceThreadSearchEngine()) + ->setViewer($viewer); $saved = $engine->buildSavedQueryFromBuiltin('all'); $query = $engine->buildQueryFromSavedQuery($saved); - $pager = $engine->newPagerForSavedQuery($saved); - $pager->setPageSize(10); + $pager = $engine->newPagerForSavedQuery($saved) + ->setPageSize(10); $results = $engine->executeQuery($query, $pager); $view = $engine->renderResults($results, $saved); From 90df4b2bd15327fcf6ec79e460e66478ab6e1286 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 29 Mar 2019 09:46:17 -0700 Subject: [PATCH 235/245] Add skeleton for Portals, a collection of dashboards and other resources Summary: Ref T13275. Today, you can build a custom page on the home page, on project pages, and in your favorites menu. PHI374 would approximately like to build a completely standalone custom page, and this generally seems like a reasonable capability which we should support, and which should be easy to support if the "custom menu" stuff is built right. In the near future, I'm planning to shore up some of the outstanding issues with profile menus and then build charts (which will have a big dashboard/panel component), so adding Portals now should let me double up on a lot of the testing and maybe make some of it a bit easier. Test Plan: Viewed the list of portals, created a new portal. Everything is currently a pure skeleton with no unique behavior. Here's a glorious portal page: {F6321846} Reviewers: amckinley Reviewed By: amckinley Subscribers: PHID-OPKG-gm6ozazyms6q6i22gyam Maniphest Tasks: T13275 Differential Revision: https://secure.phabricator.com/D20348 --- .../20190329.portals.01.create.sql | 11 ++ .../20190329.portals.02.xaction.sql | 19 ++++ src/__phutil_library_map__.php | 37 +++++++ .../PhabricatorDashboardApplication.php | 8 ++ ...torDashboardPortalEditConduitAPIMethod.php | 19 ++++ ...rDashboardPortalSearchConduitAPIMethod.php | 18 +++ .../PhabricatorDashboardPortalStatus.php | 9 ++ .../PhabricatorDashboardPortalController.php | 14 +++ ...abricatorDashboardPortalEditController.php | 12 ++ ...abricatorDashboardPortalListController.php | 26 +++++ ...abricatorDashboardPortalViewController.php | 34 ++++++ .../PhabricatorDashboardPortalEditEngine.php | 83 ++++++++++++++ .../PhabricatorDashboardPortalEditor.php | 31 ++++++ .../PhabricatorDashboardPortalPHIDType.php | 42 +++++++ .../query/PhabricatorDashboardPortalQuery.php | 64 +++++++++++ ...PhabricatorDashboardPortalSearchEngine.php | 78 +++++++++++++ .../storage/PhabricatorDashboardPortal.php | 104 ++++++++++++++++++ .../PhabricatorDashboardPortalTransaction.php | 18 +++ ...bricatorDashboardPortalNameTransaction.php | 70 ++++++++++++ ...bricatorDashboardPortalTransactionType.php | 4 + 20 files changed, 701 insertions(+) create mode 100644 resources/sql/autopatches/20190329.portals.01.create.sql create mode 100644 resources/sql/autopatches/20190329.portals.02.xaction.sql create mode 100644 src/applications/dashboard/conduit/PhabricatorDashboardPortalEditConduitAPIMethod.php create mode 100644 src/applications/dashboard/conduit/PhabricatorDashboardPortalSearchConduitAPIMethod.php create mode 100644 src/applications/dashboard/constants/PhabricatorDashboardPortalStatus.php create mode 100644 src/applications/dashboard/controller/portal/PhabricatorDashboardPortalController.php create mode 100644 src/applications/dashboard/controller/portal/PhabricatorDashboardPortalEditController.php create mode 100644 src/applications/dashboard/controller/portal/PhabricatorDashboardPortalListController.php create mode 100644 src/applications/dashboard/controller/portal/PhabricatorDashboardPortalViewController.php create mode 100644 src/applications/dashboard/editor/PhabricatorDashboardPortalEditEngine.php create mode 100644 src/applications/dashboard/editor/PhabricatorDashboardPortalEditor.php create mode 100644 src/applications/dashboard/phid/PhabricatorDashboardPortalPHIDType.php create mode 100644 src/applications/dashboard/query/PhabricatorDashboardPortalQuery.php create mode 100644 src/applications/dashboard/query/PhabricatorDashboardPortalSearchEngine.php create mode 100644 src/applications/dashboard/storage/PhabricatorDashboardPortal.php create mode 100644 src/applications/dashboard/storage/PhabricatorDashboardPortalTransaction.php create mode 100644 src/applications/dashboard/xaction/portal/PhabricatorDashboardPortalNameTransaction.php create mode 100644 src/applications/dashboard/xaction/portal/PhabricatorDashboardPortalTransactionType.php diff --git a/resources/sql/autopatches/20190329.portals.01.create.sql b/resources/sql/autopatches/20190329.portals.01.create.sql new file mode 100644 index 0000000000..d7d1e6138f --- /dev/null +++ b/resources/sql/autopatches/20190329.portals.01.create.sql @@ -0,0 +1,11 @@ +CREATE TABLE {$NAMESPACE}_dashboard.dashboard_portal ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + name VARCHAR(255) NOT NULL COLLATE {$COLLATE_TEXT}, + status VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT}, + viewPolicy VARBINARY(64) NOT NULL, + editPolicy VARBINARY(64) NOT NULL, + properties LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20190329.portals.02.xaction.sql b/resources/sql/autopatches/20190329.portals.02.xaction.sql new file mode 100644 index 0000000000..057df69e2d --- /dev/null +++ b/resources/sql/autopatches/20190329.portals.02.xaction.sql @@ -0,0 +1,19 @@ +CREATE TABLE {$NAMESPACE}_dashboard.dashboard_portaltransaction ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + authorPHID VARBINARY(64) NOT NULL, + objectPHID VARBINARY(64) NOT NULL, + viewPolicy VARBINARY(64) NOT NULL, + editPolicy VARBINARY(64) NOT NULL, + commentPHID VARBINARY(64) DEFAULT NULL, + commentVersion INT UNSIGNED NOT NULL, + transactionType VARCHAR(32) NOT NULL, + oldValue LONGTEXT NOT NULL, + newValue LONGTEXT NOT NULL, + contentSource LONGTEXT NOT NULL, + metadata LONGTEXT NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE {$COLLATE_TEXT}; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 33f2cf4d33..f6b439ec6e 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2946,6 +2946,22 @@ phutil_register_library_map(array( 'PhabricatorDashboardPanelTransactionQuery' => 'applications/dashboard/query/PhabricatorDashboardPanelTransactionQuery.php', 'PhabricatorDashboardPanelType' => 'applications/dashboard/paneltype/PhabricatorDashboardPanelType.php', 'PhabricatorDashboardPanelViewController' => 'applications/dashboard/controller/PhabricatorDashboardPanelViewController.php', + 'PhabricatorDashboardPortal' => 'applications/dashboard/storage/PhabricatorDashboardPortal.php', + 'PhabricatorDashboardPortalController' => 'applications/dashboard/controller/portal/PhabricatorDashboardPortalController.php', + 'PhabricatorDashboardPortalEditConduitAPIMethod' => 'applications/dashboard/conduit/PhabricatorDashboardPortalEditConduitAPIMethod.php', + 'PhabricatorDashboardPortalEditController' => 'applications/dashboard/controller/portal/PhabricatorDashboardPortalEditController.php', + 'PhabricatorDashboardPortalEditEngine' => 'applications/dashboard/editor/PhabricatorDashboardPortalEditEngine.php', + 'PhabricatorDashboardPortalEditor' => 'applications/dashboard/editor/PhabricatorDashboardPortalEditor.php', + 'PhabricatorDashboardPortalListController' => 'applications/dashboard/controller/portal/PhabricatorDashboardPortalListController.php', + 'PhabricatorDashboardPortalNameTransaction' => 'applications/dashboard/xaction/portal/PhabricatorDashboardPortalNameTransaction.php', + 'PhabricatorDashboardPortalPHIDType' => 'applications/dashboard/phid/PhabricatorDashboardPortalPHIDType.php', + 'PhabricatorDashboardPortalQuery' => 'applications/dashboard/query/PhabricatorDashboardPortalQuery.php', + 'PhabricatorDashboardPortalSearchConduitAPIMethod' => 'applications/dashboard/conduit/PhabricatorDashboardPortalSearchConduitAPIMethod.php', + 'PhabricatorDashboardPortalSearchEngine' => 'applications/dashboard/query/PhabricatorDashboardPortalSearchEngine.php', + 'PhabricatorDashboardPortalStatus' => 'applications/dashboard/constants/PhabricatorDashboardPortalStatus.php', + 'PhabricatorDashboardPortalTransaction' => 'applications/dashboard/storage/PhabricatorDashboardPortalTransaction.php', + 'PhabricatorDashboardPortalTransactionType' => 'applications/dashboard/xaction/portal/PhabricatorDashboardPortalTransactionType.php', + 'PhabricatorDashboardPortalViewController' => 'applications/dashboard/controller/portal/PhabricatorDashboardPortalViewController.php', 'PhabricatorDashboardProfileController' => 'applications/dashboard/controller/PhabricatorDashboardProfileController.php', 'PhabricatorDashboardProfileMenuItem' => 'applications/search/menuitem/PhabricatorDashboardProfileMenuItem.php', 'PhabricatorDashboardQuery' => 'applications/dashboard/query/PhabricatorDashboardQuery.php', @@ -8896,6 +8912,27 @@ phutil_register_library_map(array( 'PhabricatorDashboardPanelTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorDashboardPanelType' => 'Phobject', 'PhabricatorDashboardPanelViewController' => 'PhabricatorDashboardController', + 'PhabricatorDashboardPortal' => array( + 'PhabricatorDashboardDAO', + 'PhabricatorApplicationTransactionInterface', + 'PhabricatorPolicyInterface', + 'PhabricatorDestructibleInterface', + ), + 'PhabricatorDashboardPortalController' => 'PhabricatorDashboardController', + 'PhabricatorDashboardPortalEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', + 'PhabricatorDashboardPortalEditController' => 'PhabricatorDashboardPortalController', + 'PhabricatorDashboardPortalEditEngine' => 'PhabricatorEditEngine', + 'PhabricatorDashboardPortalEditor' => 'PhabricatorApplicationTransactionEditor', + 'PhabricatorDashboardPortalListController' => 'PhabricatorDashboardPortalController', + 'PhabricatorDashboardPortalNameTransaction' => 'PhabricatorDashboardPortalTransactionType', + 'PhabricatorDashboardPortalPHIDType' => 'PhabricatorPHIDType', + 'PhabricatorDashboardPortalQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorDashboardPortalSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', + 'PhabricatorDashboardPortalSearchEngine' => 'PhabricatorApplicationSearchEngine', + 'PhabricatorDashboardPortalStatus' => 'Phobject', + 'PhabricatorDashboardPortalTransaction' => 'PhabricatorModularTransaction', + 'PhabricatorDashboardPortalTransactionType' => 'PhabricatorModularTransactionType', + 'PhabricatorDashboardPortalViewController' => 'PhabricatorDashboardPortalController', 'PhabricatorDashboardProfileController' => 'PhabricatorController', 'PhabricatorDashboardProfileMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorDashboardQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', diff --git a/src/applications/dashboard/application/PhabricatorDashboardApplication.php b/src/applications/dashboard/application/PhabricatorDashboardApplication.php index d8e2701727..9de38a6d8f 100644 --- a/src/applications/dashboard/application/PhabricatorDashboardApplication.php +++ b/src/applications/dashboard/application/PhabricatorDashboardApplication.php @@ -57,6 +57,14 @@ final class PhabricatorDashboardApplication extends PhabricatorApplication { => 'PhabricatorDashboardPanelArchiveController', ), ), + '/portal/' => array( + $this->getQueryRoutePattern() => + 'PhabricatorDashboardPortalListController', + $this->getEditRoutePattern('edit/') => + 'PhabricatorDashboardPortalEditController', + 'view/(?P\d)/' => + 'PhabricatorDashboardPortalViewController', + ), ); } diff --git a/src/applications/dashboard/conduit/PhabricatorDashboardPortalEditConduitAPIMethod.php b/src/applications/dashboard/conduit/PhabricatorDashboardPortalEditConduitAPIMethod.php new file mode 100644 index 0000000000..489bb21cab --- /dev/null +++ b/src/applications/dashboard/conduit/PhabricatorDashboardPortalEditConduitAPIMethod.php @@ -0,0 +1,19 @@ +addTextCrumb(pht('Portals'), '/portal/'); + + return $crumbs; + } + +} diff --git a/src/applications/dashboard/controller/portal/PhabricatorDashboardPortalEditController.php b/src/applications/dashboard/controller/portal/PhabricatorDashboardPortalEditController.php new file mode 100644 index 0000000000..327d969f34 --- /dev/null +++ b/src/applications/dashboard/controller/portal/PhabricatorDashboardPortalEditController.php @@ -0,0 +1,12 @@ +setController($this) + ->buildResponse(); + } + +} diff --git a/src/applications/dashboard/controller/portal/PhabricatorDashboardPortalListController.php b/src/applications/dashboard/controller/portal/PhabricatorDashboardPortalListController.php new file mode 100644 index 0000000000..3eba0179b3 --- /dev/null +++ b/src/applications/dashboard/controller/portal/PhabricatorDashboardPortalListController.php @@ -0,0 +1,26 @@ +setController($this) + ->buildResponse(); + } + + protected function buildApplicationCrumbs() { + $crumbs = parent::buildApplicationCrumbs(); + + id(new PhabricatorDashboardPortalEditEngine()) + ->setViewer($this->getViewer()) + ->addActionToCrumbs($crumbs); + + return $crumbs; + } + +} diff --git a/src/applications/dashboard/controller/portal/PhabricatorDashboardPortalViewController.php b/src/applications/dashboard/controller/portal/PhabricatorDashboardPortalViewController.php new file mode 100644 index 0000000000..733754565e --- /dev/null +++ b/src/applications/dashboard/controller/portal/PhabricatorDashboardPortalViewController.php @@ -0,0 +1,34 @@ +getViewer(); + $id = $request->getURIData('id'); + + $portal = id(new PhabricatorDashboardPortalQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->executeOne(); + if (!$portal) { + return new Aphront404Response(); + } + + $content = $portal->getObjectName(); + + return $this->newPage() + ->setTitle( + array( + pht('Portal'), + $portal->getName(), + )) + ->setPageObjectPHIDs(array($portal->getPHID())) + ->appendChild($content); + } + +} diff --git a/src/applications/dashboard/editor/PhabricatorDashboardPortalEditEngine.php b/src/applications/dashboard/editor/PhabricatorDashboardPortalEditEngine.php new file mode 100644 index 0000000000..04741c59a2 --- /dev/null +++ b/src/applications/dashboard/editor/PhabricatorDashboardPortalEditEngine.php @@ -0,0 +1,83 @@ +getName()); + } + + protected function getObjectEditShortText($object) { + return pht('Edit Portal'); + } + + protected function getObjectCreateShortText() { + return pht('Create Portal'); + } + + protected function getObjectName() { + return pht('Portal'); + } + + protected function getObjectViewURI($object) { + return $object->getURI(); + } + + protected function getEditorURI() { + return '/portal/edit/'; + } + + protected function buildCustomEditFields($object) { + return array( + id(new PhabricatorTextEditField()) + ->setKey('name') + ->setLabel(pht('Name')) + ->setDescription(pht('Name of the portal.')) + ->setConduitDescription(pht('Rename the portal.')) + ->setConduitTypeDescription(pht('New portal name.')) + ->setTransactionType( + PhabricatorDashboardPortalNameTransaction::TRANSACTIONTYPE) + ->setIsRequired(true) + ->setValue($object->getName()), + ); + } + +} diff --git a/src/applications/dashboard/editor/PhabricatorDashboardPortalEditor.php b/src/applications/dashboard/editor/PhabricatorDashboardPortalEditor.php new file mode 100644 index 0000000000..9989d1e7d5 --- /dev/null +++ b/src/applications/dashboard/editor/PhabricatorDashboardPortalEditor.php @@ -0,0 +1,31 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $portal = $objects[$phid]; + + $handle + ->setName($portal->getName()) + ->setURI($portal->getURI()); + } + } + +} diff --git a/src/applications/dashboard/query/PhabricatorDashboardPortalQuery.php b/src/applications/dashboard/query/PhabricatorDashboardPortalQuery.php new file mode 100644 index 0000000000..d352b99c8f --- /dev/null +++ b/src/applications/dashboard/query/PhabricatorDashboardPortalQuery.php @@ -0,0 +1,64 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withStatuses(array $statuses) { + $this->statuses = $statuses; + return $this; + } + + public function newResultObject() { + return new PhabricatorDashboardPortal(); + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); + + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->phids !== null) { + $where[] = qsprintf( + $conn, + 'phid IN (%Ls)', + $this->phids); + } + + if ($this->statuses !== null) { + $where[] = qsprintf( + $conn, + 'status IN (%Ls)', + $this->statuses); + } + + return $where; + } + + public function getQueryApplicationClass() { + return 'PhabricatorDashboardApplication'; + } + +} diff --git a/src/applications/dashboard/query/PhabricatorDashboardPortalSearchEngine.php b/src/applications/dashboard/query/PhabricatorDashboardPortalSearchEngine.php new file mode 100644 index 0000000000..c1633f2e97 --- /dev/null +++ b/src/applications/dashboard/query/PhabricatorDashboardPortalSearchEngine.php @@ -0,0 +1,78 @@ +newQuery(); + return $query; + } + + protected function buildCustomSearchFields() { + return array(); + } + + protected function getURI($path) { + return '/portal/'.$path; + } + + protected function getBuiltinQueryNames() { + $names = array(); + + $names['all'] = pht('All Portals'); + + return $names; + } + + public function buildSavedQueryFromBuiltin($query_key) { + $query = $this->newSavedQuery(); + $query->setQueryKey($query_key); + $viewer = $this->requireViewer(); + + switch ($query_key) { + case 'all': + return $query; + } + + return parent::buildSavedQueryFromBuiltin($query_key); + } + + protected function renderResultList( + array $portals, + PhabricatorSavedQuery $query, + array $handles) { + + assert_instances_of($portals, 'PhabricatorDashboardPortal'); + + $viewer = $this->requireViewer(); + + $list = new PHUIObjectItemListView(); + $list->setUser($viewer); + foreach ($portals as $portal) { + $item = id(new PHUIObjectItemView()) + ->setObjectName($portal->getObjectName()) + ->setHeader($portal->getName()) + ->setHref($portal->getURI()) + ->setObject($portal); + + $list->addItem($item); + } + + return id(new PhabricatorApplicationSearchResultView()) + ->setObjectList($list) + ->setNoDataString(pht('No portals found.')); + } + +} diff --git a/src/applications/dashboard/storage/PhabricatorDashboardPortal.php b/src/applications/dashboard/storage/PhabricatorDashboardPortal.php new file mode 100644 index 0000000000..1930a23bd4 --- /dev/null +++ b/src/applications/dashboard/storage/PhabricatorDashboardPortal.php @@ -0,0 +1,104 @@ +setName('') + ->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy()) + ->setEditPolicy(PhabricatorPolicies::POLICY_USER) + ->setStatus(PhabricatorDashboardPortalStatus::STATUS_ACTIVE); + } + + protected function getConfiguration() { + return array( + self::CONFIG_AUX_PHID => true, + self::CONFIG_SERIALIZATION => array( + 'properties' => self::SERIALIZATION_JSON, + ), + self::CONFIG_COLUMN_SCHEMA => array( + 'name' => 'text255', + 'status' => 'text32', + ), + ) + parent::getConfiguration(); + } + + public function getPHIDType() { + return PhabricatorDashboardPortalPHIDType::TYPECONST; + } + + public function getPortalProperty($key, $default = null) { + return idx($this->properties, $key, $default); + } + + public function setPortalProperty($key, $value) { + $this->properties[$key] = $value; + return $this; + } + + public function getObjectName() { + return pht('Portal %d', $this->getID()); + } + + public function getURI() { + return '/portal/view/'.$this->getID().'/'; + } + + +/* -( PhabricatorApplicationTransactionInterface )------------------------- */ + + + public function getApplicationTransactionEditor() { + return new PhabricatorDashboardPortalEditor(); + } + + public function getApplicationTransactionTemplate() { + return new PhabricatorDashboardPortalTransaction(); + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + ); + } + + public function getPolicy($capability) { + switch ($capability) { + case PhabricatorPolicyCapability::CAN_VIEW: + return $this->getViewPolicy(); + case PhabricatorPolicyCapability::CAN_EDIT: + return $this->getEditPolicy(); + } + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return false; + } + + +/* -( PhabricatorDestructibleInterface )----------------------------------- */ + + + public function destroyObjectPermanently( + PhabricatorDestructionEngine $engine) { + $this->delete(); + } + + +} diff --git a/src/applications/dashboard/storage/PhabricatorDashboardPortalTransaction.php b/src/applications/dashboard/storage/PhabricatorDashboardPortalTransaction.php new file mode 100644 index 0000000000..7861394b98 --- /dev/null +++ b/src/applications/dashboard/storage/PhabricatorDashboardPortalTransaction.php @@ -0,0 +1,18 @@ +getName(); + } + + public function applyInternalEffects($object, $value) { + $object->setName($value); + } + + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + return pht( + '%s renamed this portal from %s to %s.', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + $max_length = $object->getColumnMaximumByteLength('name'); + foreach ($xactions as $xaction) { + $new = $xaction->getNewValue(); + if (!strlen($new)) { + $errors[] = $this->newInvalidError( + pht('Portals must have a title.'), + $xaction); + continue; + } + + if (strlen($new) > $max_length) { + $errors[] = $this->newInvalidError( + pht( + 'Portal names must not be longer than %s characters.', + $max_length)); + continue; + } + } + + if (!$errors) { + if ($this->isEmptyTextTransaction($object->getName(), $xactions)) { + $errors[] = $this->newRequiredError( + pht('Portals must have a title.')); + } + } + + return $errors; + } + + public function getTransactionTypeForConduit($xaction) { + return 'name'; + } + + public function getFieldValuesForConduit($xaction, $data) { + return array( + 'old' => $xaction->getOldValue(), + 'new' => $xaction->getNewValue(), + ); + } + +} diff --git a/src/applications/dashboard/xaction/portal/PhabricatorDashboardPortalTransactionType.php b/src/applications/dashboard/xaction/portal/PhabricatorDashboardPortalTransactionType.php new file mode 100644 index 0000000000..1855312f26 --- /dev/null +++ b/src/applications/dashboard/xaction/portal/PhabricatorDashboardPortalTransactionType.php @@ -0,0 +1,4 @@ + Date: Fri, 29 Mar 2019 11:00:23 -0700 Subject: [PATCH 236/245] Allow Portals to be edited, and improve empty/blank states Summary: Depends on D20348. Ref T13275. Portals are mostly just a "ProfileMenuEngine" menu, and that code is already relatively modular/flexible, so set that up to start with. The stuff it gets wrong right now is mostly around empty/no-permission states, since the original use cases (project menus) didn't have any of these states: it's not possible to have a project menu with no content. Let the engine render an "empty" state (when there are no items that can render a content page) and try to make some of the empty behavior a little more user-friendly. This mostly makes portals work, more or less. Test Plan: {F6322284} Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13275 Differential Revision: https://secure.phabricator.com/D20349 --- src/__phutil_library_map__.php | 6 + .../PhabricatorDashboardApplication.php | 9 +- ...abricatorDashboardPortalViewController.php | 45 +++++-- .../PhabricatorDashboardPortalEditEngine.php | 6 +- ...icatorDashboardPortalProfileMenuEngine.php | 38 ++++++ .../PhabricatorDashboardPortalMenuItem.php | 117 ++++++++++++++++++ ...ricatorDashboardPortalTransactionQuery.php | 10 ++ .../engine/PhabricatorProfileMenuEngine.php | 61 +++++++-- 8 files changed, 269 insertions(+), 23 deletions(-) create mode 100644 src/applications/dashboard/engine/PhabricatorDashboardPortalProfileMenuEngine.php create mode 100644 src/applications/dashboard/menuitem/PhabricatorDashboardPortalMenuItem.php create mode 100644 src/applications/dashboard/query/PhabricatorDashboardPortalTransactionQuery.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index f6b439ec6e..d93bf72ff4 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2953,13 +2953,16 @@ phutil_register_library_map(array( 'PhabricatorDashboardPortalEditEngine' => 'applications/dashboard/editor/PhabricatorDashboardPortalEditEngine.php', 'PhabricatorDashboardPortalEditor' => 'applications/dashboard/editor/PhabricatorDashboardPortalEditor.php', 'PhabricatorDashboardPortalListController' => 'applications/dashboard/controller/portal/PhabricatorDashboardPortalListController.php', + 'PhabricatorDashboardPortalMenuItem' => 'applications/dashboard/menuitem/PhabricatorDashboardPortalMenuItem.php', 'PhabricatorDashboardPortalNameTransaction' => 'applications/dashboard/xaction/portal/PhabricatorDashboardPortalNameTransaction.php', 'PhabricatorDashboardPortalPHIDType' => 'applications/dashboard/phid/PhabricatorDashboardPortalPHIDType.php', + 'PhabricatorDashboardPortalProfileMenuEngine' => 'applications/dashboard/engine/PhabricatorDashboardPortalProfileMenuEngine.php', 'PhabricatorDashboardPortalQuery' => 'applications/dashboard/query/PhabricatorDashboardPortalQuery.php', 'PhabricatorDashboardPortalSearchConduitAPIMethod' => 'applications/dashboard/conduit/PhabricatorDashboardPortalSearchConduitAPIMethod.php', 'PhabricatorDashboardPortalSearchEngine' => 'applications/dashboard/query/PhabricatorDashboardPortalSearchEngine.php', 'PhabricatorDashboardPortalStatus' => 'applications/dashboard/constants/PhabricatorDashboardPortalStatus.php', 'PhabricatorDashboardPortalTransaction' => 'applications/dashboard/storage/PhabricatorDashboardPortalTransaction.php', + 'PhabricatorDashboardPortalTransactionQuery' => 'applications/dashboard/query/PhabricatorDashboardPortalTransactionQuery.php', 'PhabricatorDashboardPortalTransactionType' => 'applications/dashboard/xaction/portal/PhabricatorDashboardPortalTransactionType.php', 'PhabricatorDashboardPortalViewController' => 'applications/dashboard/controller/portal/PhabricatorDashboardPortalViewController.php', 'PhabricatorDashboardProfileController' => 'applications/dashboard/controller/PhabricatorDashboardProfileController.php', @@ -8924,13 +8927,16 @@ phutil_register_library_map(array( 'PhabricatorDashboardPortalEditEngine' => 'PhabricatorEditEngine', 'PhabricatorDashboardPortalEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorDashboardPortalListController' => 'PhabricatorDashboardPortalController', + 'PhabricatorDashboardPortalMenuItem' => 'PhabricatorProfileMenuItem', 'PhabricatorDashboardPortalNameTransaction' => 'PhabricatorDashboardPortalTransactionType', 'PhabricatorDashboardPortalPHIDType' => 'PhabricatorPHIDType', + 'PhabricatorDashboardPortalProfileMenuEngine' => 'PhabricatorProfileMenuEngine', 'PhabricatorDashboardPortalQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorDashboardPortalSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'PhabricatorDashboardPortalSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorDashboardPortalStatus' => 'Phobject', 'PhabricatorDashboardPortalTransaction' => 'PhabricatorModularTransaction', + 'PhabricatorDashboardPortalTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorDashboardPortalTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorDashboardPortalViewController' => 'PhabricatorDashboardPortalController', 'PhabricatorDashboardProfileController' => 'PhabricatorController', diff --git a/src/applications/dashboard/application/PhabricatorDashboardApplication.php b/src/applications/dashboard/application/PhabricatorDashboardApplication.php index 9de38a6d8f..b44ecc3a38 100644 --- a/src/applications/dashboard/application/PhabricatorDashboardApplication.php +++ b/src/applications/dashboard/application/PhabricatorDashboardApplication.php @@ -27,6 +27,9 @@ final class PhabricatorDashboardApplication extends PhabricatorApplication { } public function getRoutes() { + $menu_rules = $this->getProfileMenuRouting( + 'PhabricatorDashboardPortalViewController'); + return array( '/W(?P\d+)' => 'PhabricatorDashboardPanelViewController', '/dashboard/' => array( @@ -62,8 +65,10 @@ final class PhabricatorDashboardApplication extends PhabricatorApplication { 'PhabricatorDashboardPortalListController', $this->getEditRoutePattern('edit/') => 'PhabricatorDashboardPortalEditController', - 'view/(?P\d)/' => - 'PhabricatorDashboardPortalViewController', + 'view/(?P\d)/' => array( + '' => 'PhabricatorDashboardPortalViewController', + ) + $menu_rules, + ), ); } diff --git a/src/applications/dashboard/controller/portal/PhabricatorDashboardPortalViewController.php b/src/applications/dashboard/controller/portal/PhabricatorDashboardPortalViewController.php index 733754565e..259a06451b 100644 --- a/src/applications/dashboard/controller/portal/PhabricatorDashboardPortalViewController.php +++ b/src/applications/dashboard/controller/portal/PhabricatorDashboardPortalViewController.php @@ -3,13 +3,24 @@ final class PhabricatorDashboardPortalViewController extends PhabricatorDashboardPortalController { + private $portal; + + public function setPortal(PhabricatorDashboardPortal $portal) { + $this->portal = $portal; + return $this; + } + + public function getPortal() { + return $this->portal; + } + public function shouldAllowPublic() { return true; } public function handleRequest(AphrontRequest $request) { $viewer = $this->getViewer(); - $id = $request->getURIData('id'); + $id = $request->getURIData('portalID'); $portal = id(new PhabricatorDashboardPortalQuery()) ->setViewer($viewer) @@ -19,16 +30,30 @@ final class PhabricatorDashboardPortalViewController return new Aphront404Response(); } - $content = $portal->getObjectName(); + $this->setPortal($portal); - return $this->newPage() - ->setTitle( - array( - pht('Portal'), - $portal->getName(), - )) - ->setPageObjectPHIDs(array($portal->getPHID())) - ->appendChild($content); + $engine = id(new PhabricatorDashboardPortalProfileMenuEngine()) + ->setProfileObject($portal) + ->setController($this); + + return $engine->buildResponse(); + } + + protected function buildApplicationCrumbs() { + $crumbs = parent::buildApplicationCrumbs(); + + $portal = $this->getPortal(); + if ($portal) { + $crumbs->addTextCrumb($portal->getName(), $portal->getURI()); + } + + return $crumbs; + } + + public function newTimelineView() { + return $this->buildTransactionTimeline( + $this->getPortal(), + new PhabricatorDashboardPortalTransactionQuery()); } } diff --git a/src/applications/dashboard/editor/PhabricatorDashboardPortalEditEngine.php b/src/applications/dashboard/editor/PhabricatorDashboardPortalEditEngine.php index 04741c59a2..9945ea9d28 100644 --- a/src/applications/dashboard/editor/PhabricatorDashboardPortalEditEngine.php +++ b/src/applications/dashboard/editor/PhabricatorDashboardPortalEditEngine.php @@ -58,7 +58,11 @@ final class PhabricatorDashboardPortalEditEngine } protected function getObjectViewURI($object) { - return $object->getURI(); + if ($this->getIsCreate()) { + return $object->getURI(); + } else { + return '/portal/view/'.$object->getID().'/view/manage/'; + } } protected function getEditorURI() { diff --git a/src/applications/dashboard/engine/PhabricatorDashboardPortalProfileMenuEngine.php b/src/applications/dashboard/engine/PhabricatorDashboardPortalProfileMenuEngine.php new file mode 100644 index 0000000000..18184952e4 --- /dev/null +++ b/src/applications/dashboard/engine/PhabricatorDashboardPortalProfileMenuEngine.php @@ -0,0 +1,38 @@ +getProfileObject(); + + return $portal->getURI().$path; + } + + protected function getBuiltinProfileItems($object) { + $items = array(); + + $items[] = $this->newManageItem(); + + $items[] = $this->newItem() + ->setMenuItemKey(PhabricatorDashboardPortalMenuItem::MENUITEMKEY) + ->setBuiltinKey('manage'); + + return $items; + } + + protected function newNoMenuItemsView() { + return $this->newEmptyView( + pht('New Portal'), + pht('Use "Edit Menu" to add menu items to this portal.')); + } + +} diff --git a/src/applications/dashboard/menuitem/PhabricatorDashboardPortalMenuItem.php b/src/applications/dashboard/menuitem/PhabricatorDashboardPortalMenuItem.php new file mode 100644 index 0000000000..655f67058a --- /dev/null +++ b/src/applications/dashboard/menuitem/PhabricatorDashboardPortalMenuItem.php @@ -0,0 +1,117 @@ +getMenuItemProperty('name'); + + if (strlen($name)) { + return $name; + } + + return $this->getDefaultName(); + } + + public function buildEditEngineFields( + PhabricatorProfileMenuItemConfiguration $config) { + return array( + id(new PhabricatorTextEditField()) + ->setKey('name') + ->setLabel(pht('Name')) + ->setPlaceholder($this->getDefaultName()) + ->setValue($config->getMenuItemProperty('name')), + ); + } + + protected function newNavigationMenuItems( + PhabricatorProfileMenuItemConfiguration $config) { + $viewer = $this->getViewer(); + + if (!$viewer->isLoggedIn()) { + return array(); + } + + $href = $this->getItemViewURI($config); + $name = $this->getDisplayName($config); + $icon = 'fa-pencil'; + + $item = $this->newItem() + ->setHref($href) + ->setName($name) + ->setIcon($icon); + + return array( + $item, + ); + } + + public function newPageContent( + PhabricatorProfileMenuItemConfiguration $config) { + $viewer = $this->getViewer(); + $engine = $this->getEngine(); + $portal = $engine->getProfileObject(); + $controller = $engine->getController(); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Manage Portal')); + + $edit_uri = urisprintf( + '/portal/edit/%d/', + $portal->getID()); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $portal, + PhabricatorPolicyCapability::CAN_EDIT); + + $curtain = $controller->newCurtainView($portal) + ->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit Portal')) + ->setIcon('fa-pencil') + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit) + ->setHref($edit_uri)); + + $timeline = $controller->newTimelineView() + ->setShouldTerminate(true); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setCurtain($curtain) + ->setMainColumn( + array( + $timeline, + )); + + return $view; + } + + +} diff --git a/src/applications/dashboard/query/PhabricatorDashboardPortalTransactionQuery.php b/src/applications/dashboard/query/PhabricatorDashboardPortalTransactionQuery.php new file mode 100644 index 0000000000..f4dff94088 --- /dev/null +++ b/src/applications/dashboard/query/PhabricatorDashboardPortalTransactionQuery.php @@ -0,0 +1,10 @@ +getDisplayName(); + if ($selected_item) { + $page_title = $selected_item->getDisplayName(); + } else { + $page_title = pht('Empty'); + } } switch ($item_action) { case 'view': - $navigation->selectFilter($selected_item->getDefaultMenuItemKey()); + if ($selected_item) { + $navigation->selectFilter($selected_item->getDefaultMenuItemKey()); - try { - $content = $this->buildItemViewContent($selected_item); - } catch (Exception $ex) { - $content = id(new PHUIInfoView()) - ->setTitle(pht('Unable to Render Dashboard')) - ->setErrors(array($ex->getMessage())); + try { + $content = $this->buildItemViewContent($selected_item); + } catch (Exception $ex) { + $content = id(new PHUIInfoView()) + ->setTitle(pht('Unable to Render Dashboard')) + ->setErrors(array($ex->getMessage())); + } + + $crumbs->addTextCrumb($selected_item->getDisplayName()); + } else { + $content = $this->newNoMenuItemsView(); } - $crumbs->addTextCrumb($selected_item->getDisplayName()); if (!$content) { - return new Aphront404Response(); + $content = $this->newEmptyView( + pht('Empty'), + pht('There is nothing here.')); } break; case 'configure': @@ -346,6 +361,7 @@ abstract class PhabricatorProfileMenuEngine extends Phobject { if ($this->navigation) { return $this->navigation; } + $nav = id(new AphrontSideNavFilterView()) ->setIsProfileMenu(true) ->setBaseURI(new PhutilURI($this->getItemURI(''))); @@ -365,6 +381,7 @@ abstract class PhabricatorProfileMenuEngine extends Phobject { $first_item->willBuildNavigationItems($group); } + $has_items = false; foreach ($menu_items as $menu_item) { if ($menu_item->isDisabled()) { continue; @@ -389,9 +406,18 @@ abstract class PhabricatorProfileMenuEngine extends Phobject { foreach ($items as $item) { $nav->addMenuItem($item); + $has_items = true; } } + if (!$has_items) { + // If the navigation menu has no items, add an empty label item to + // force it to render something. + $empty_item = id(new PHUIListItemView()) + ->setType(PHUIListItemView::TYPE_LABEL); + $nav->addMenuItem($empty_item); + } + $nav->selectFilter(null); $this->navigation = $nav; @@ -1319,5 +1345,20 @@ abstract class PhabricatorProfileMenuEngine extends Phobject { return $items; } + final protected function newEmptyView($title, $message) { + return id(new PHUIInfoView()) + ->setTitle($title) + ->setSeverity(PHUIInfoView::SEVERITY_NODATA) + ->setErrors( + array( + $message, + )); + } + + protected function newNoMenuItemsView() { + return $this->newEmptyView( + pht('No Menu Items'), + pht('There are no menu items.')); + } } From 408cbd633cd59f74771b4740bcc7fddea4fb5e45 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 29 Mar 2019 15:37:52 -0700 Subject: [PATCH 237/245] On portals, make the "selected" / "default" logic more straightforward Summary: Depends on D20349. Ref T13275. Currently, a default item is selected as a side effect of generating the full list of items, for absolutely no reason. The logic to pick the currently selected item can also be separated out pretty easily. (And fix a bug in with a weird edge case in projects.) This doesn't really change anything, but it will probably make T12949 a bit easier to fix. Test Plan: Viewed Home / projects / portals, clicked various links, got same default/selection behavior as before. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13275 Differential Revision: https://secure.phabricator.com/D20352 --- .../PhabricatorProjectViewController.php | 2 +- .../engine/PhabricatorProfileMenuEngine.php | 126 +++++++++--------- 2 files changed, 67 insertions(+), 61 deletions(-) diff --git a/src/applications/project/controller/PhabricatorProjectViewController.php b/src/applications/project/controller/PhabricatorProjectViewController.php index 2e53fe7276..c5c536bfa1 100644 --- a/src/applications/project/controller/PhabricatorProjectViewController.php +++ b/src/applications/project/controller/PhabricatorProjectViewController.php @@ -28,7 +28,7 @@ final class PhabricatorProjectViewController $default_key = PhabricatorProject::ITEM_MANAGE; } - switch ($default->getBuiltinKey()) { + switch ($default_key) { case PhabricatorProject::ITEM_WORKBOARD: $controller_object = new PhabricatorProjectBoardViewController(); break; diff --git a/src/applications/search/engine/PhabricatorProfileMenuEngine.php b/src/applications/search/engine/PhabricatorProfileMenuEngine.php index 1e1a7e0b77..f4fde5fdff 100644 --- a/src/applications/search/engine/PhabricatorProfileMenuEngine.php +++ b/src/applications/search/engine/PhabricatorProfileMenuEngine.php @@ -6,7 +6,6 @@ abstract class PhabricatorProfileMenuEngine extends Phobject { private $profileObject; private $customPHID; private $items; - private $defaultItem; private $controller; private $navigation; private $showNavigation = true; @@ -79,8 +78,7 @@ abstract class PhabricatorProfileMenuEngine extends Phobject { } public function getDefaultItem() { - $this->getItems(); - return $this->defaultItem; + return $this->pickDefaultItem($this->getItems()); } public function setShowNavigation($show) { @@ -154,30 +152,10 @@ abstract class PhabricatorProfileMenuEngine extends Phobject { $item_list = $this->getItems(); - $selected_item = null; - if (strlen($item_id)) { - $item_id_int = (int)$item_id; - foreach ($item_list as $item) { - if ($item_id_int) { - if ((int)$item->getID() === $item_id_int) { - $selected_item = $item; - break; - } - } - - $builtin_key = $item->getBuiltinKey(); - if ($builtin_key === (string)$item_id) { - $selected_item = $item; - break; - } - } - } - - if (!$selected_item) { - if ($is_view) { - $selected_item = $this->getDefaultItem(); - } - } + $selected_item = $this->pickSelectedItem( + $item_list, + $item_id, + $is_view); switch ($item_action) { case 'view': @@ -485,39 +463,7 @@ abstract class PhabricatorProfileMenuEngine extends Phobject { } } - $items = $this->arrangeItems($items, $mode); - - // Make sure exactly one valid item is marked as default. - $default = null; - $first = null; - foreach ($items as $item) { - if (!$item->canMakeDefault() || $item->isDisabled()) { - continue; - } - - // If this engine doesn't support pinning items, don't respect any - // setting which might be present in the database. - if ($this->isMenuEnginePinnable()) { - if ($item->isDefault()) { - $default = $item; - break; - } - } - - if ($first === null) { - $first = $item; - } - } - - if (!$default) { - $default = $first; - } - - if ($default) { - $this->setDefaultItem($default); - } - - return $items; + return $this->arrangeItems($items, $mode); } private function loadBuiltinProfileItems($mode) { @@ -1361,4 +1307,64 @@ abstract class PhabricatorProfileMenuEngine extends Phobject { pht('There are no menu items.')); } + private function pickDefaultItem(array $items) { + // Remove all the items which can not be the default item. + foreach ($items as $key => $item) { + if (!$item->canMakeDefault()) { + unset($items[$key]); + continue; + } + + if ($item->isDisabled()) { + unset($items[$key]); + continue; + } + } + + // If this engine supports pinning items and a valid item is pinned, + // pick that item as the default. + if ($this->isMenuEnginePinnable()) { + foreach ($items as $key => $item) { + if ($item->isDefault()) { + return $item; + } + } + } + + // If we have some other valid items, pick the first one as the default. + if ($items) { + return head($items); + } + + return null; + } + + private function pickSelectedItem(array $items, $item_id, $is_view) { + if (strlen($item_id)) { + $item_id_int = (int)$item_id; + foreach ($items as $item) { + if ($item_id_int) { + if ((int)$item->getID() === $item_id_int) { + return $item; + } + } + + $builtin_key = $item->getBuiltinKey(); + if ($builtin_key === (string)$item_id) { + return $item; + } + } + + // Nothing matches the selected item ID, so we don't have a valid + // selection. + return null; + } + + if ($is_view) { + return $this->pickDefaultItem($items); + } + + return null; + } + } From 36a8b4ea1796713e71bee6b1e7187b011460f1dc Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 30 Mar 2019 11:52:29 -0700 Subject: [PATCH 238/245] When a ProfileMenu has a link item that adds URI parameters, highlight it when clicked Summary: Depends on D20352. Fixes T12949. If a user adds a link (for example, to a workboard) that takes you to the same page but with some URI parameters, we'd prefer to highlight the "link" item instead of the default "workboard" item when you click it. For example, you add a `/query/assigned/` "link" item to a workboard, called "Click This To Show Tasks Assigned To Me On This Workboard", i.e. filter the current view. This is a pretty reasonable thing to want to do. When you click it, we'd like to highlight that item to show that you've activated the "Assigned to Me" filter you added. However, we currently highlight the thing actually serving the content, i.e. the "Workboard" item. Instead: - When picking what to highlight, look through all the items for one with a link to the current request URI. - If we find one or more, pick the one that would be the default. - Otherwise, pick the first one. This means that you can have several items like "?a=1", "?a=2", etc., and we will highlight them correctly when you click them. This actual patch has some questionable bits (see some discussion in T13275), but I'd like to wait for stronger motivation to refactor it more extensively. Test Plan: - On a portal, added a `?a=1` link. Saw it highlight properly when clikced. - On a workboard, added a link to the board itself with a different filter. Saw it highlight appropriately when clicked. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T12949 Differential Revision: https://secure.phabricator.com/D20353 --- ...abricatorFavoritesMainMenuBarExtension.php | 5 + .../controller/PhabricatorHomeController.php | 1 + .../PhabricatorProjectBoardController.php | 12 +- .../PhabricatorProjectBoardViewController.php | 16 ++- .../PhabricatorProjectController.php | 4 +- .../engine/PhabricatorProfileMenuEngine.php | 107 +++++++++++++++++- 6 files changed, 125 insertions(+), 20 deletions(-) diff --git a/src/applications/favorites/engineextension/PhabricatorFavoritesMainMenuBarExtension.php b/src/applications/favorites/engineextension/PhabricatorFavoritesMainMenuBarExtension.php index 7b5d6d0720..f26f4603e1 100644 --- a/src/applications/favorites/engineextension/PhabricatorFavoritesMainMenuBarExtension.php +++ b/src/applications/favorites/engineextension/PhabricatorFavoritesMainMenuBarExtension.php @@ -54,6 +54,11 @@ final class PhabricatorFavoritesMainMenuBarExtension ->setProfileObject($favorites) ->setCustomPHID($viewer->getPHID()); + $controller = $this->getController(); + if ($controller) { + $menu_engine->setController($controller); + } + $filter_view = $menu_engine->buildNavigation(); $menu_view = $filter_view->getMenu(); diff --git a/src/applications/home/controller/PhabricatorHomeController.php b/src/applications/home/controller/PhabricatorHomeController.php index 7c46525ee0..9cd3b5b91d 100644 --- a/src/applications/home/controller/PhabricatorHomeController.php +++ b/src/applications/home/controller/PhabricatorHomeController.php @@ -31,6 +31,7 @@ abstract class PhabricatorHomeController extends PhabricatorController { $engine = id(new PhabricatorHomeProfileMenuEngine()) ->setViewer($viewer) + ->setController($this) ->setProfileObject($home) ->setCustomPHID($viewer->getPHID()); diff --git a/src/applications/project/controller/PhabricatorProjectBoardController.php b/src/applications/project/controller/PhabricatorProjectBoardController.php index d0c6abf882..b889bc75da 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardController.php @@ -1,14 +1,4 @@ selectFilter(PhabricatorProject::ITEM_WORKBOARD); - $menu->addClass('project-board-nav'); - - return $menu; - } -} + extends PhabricatorProjectController {} diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index 775ff1b61a..bd14033d3f 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -172,8 +172,7 @@ final class PhabricatorProjectBoardViewController return $content; } - $nav = $this->getProfileMenu(); - $nav->selectFilter(PhabricatorProject::ITEM_WORKBOARD); + $nav = $this->newWorkboardProfileMenu(); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Workboard')); @@ -720,7 +719,7 @@ final class PhabricatorProjectBoardViewController ->appendChild($board) ->addClass('project-board-wrapper'); - $nav = $this->getProfileMenu(); + $nav = $this->newWorkboardProfileMenu(); $divider = id(new PHUIListItemView()) ->setType(PHUIListItemView::TYPE_DIVIDER); @@ -1504,4 +1503,15 @@ final class PhabricatorProjectBoardViewController ->addCancelButton($profile_uri); } + private function newWorkboardProfileMenu() { + $default_item = id(new PhabricatorProfileMenuItemConfiguration()) + ->setBuiltinKey(PhabricatorProject::ITEM_WORKBOARD); + + $menu = parent::getProfileMenu($default_item); + + $menu->addClass('project-board-nav'); + + return $menu; + } + } diff --git a/src/applications/project/controller/PhabricatorProjectController.php b/src/applications/project/controller/PhabricatorProjectController.php index 63494bf442..eb4ab70711 100644 --- a/src/applications/project/controller/PhabricatorProjectController.php +++ b/src/applications/project/controller/PhabricatorProjectController.php @@ -97,11 +97,11 @@ abstract class PhabricatorProjectController extends PhabricatorController { return $menu; } - protected function getProfileMenu() { + protected function getProfileMenu($default_item = null) { if (!$this->profileMenu) { $engine = $this->getProfileMenuEngine(); if ($engine) { - $this->profileMenu = $engine->buildNavigation(); + $this->profileMenu = $engine->buildNavigation($default_item); } } diff --git a/src/applications/search/engine/PhabricatorProfileMenuEngine.php b/src/applications/search/engine/PhabricatorProfileMenuEngine.php index f4fde5fdff..e2439a341b 100644 --- a/src/applications/search/engine/PhabricatorProfileMenuEngine.php +++ b/src/applications/search/engine/PhabricatorProfileMenuEngine.php @@ -183,7 +183,7 @@ abstract class PhabricatorProfileMenuEngine extends Phobject { break; } - $navigation = $this->buildNavigation(); + $navigation = $this->buildNavigation($selected_item); $crumbs = $controller->buildApplicationCrumbsForEditEngine(); @@ -223,8 +223,6 @@ abstract class PhabricatorProfileMenuEngine extends Phobject { switch ($item_action) { case 'view': if ($selected_item) { - $navigation->selectFilter($selected_item->getDefaultMenuItemKey()); - try { $content = $this->buildItemViewContent($selected_item); } catch (Exception $ex) { @@ -335,7 +333,9 @@ abstract class PhabricatorProfileMenuEngine extends Phobject { return $page; } - public function buildNavigation() { + public function buildNavigation( + PhabricatorProfileMenuItemConfiguration $selected_item = null) { + if ($this->navigation) { return $this->navigation; } @@ -398,6 +398,12 @@ abstract class PhabricatorProfileMenuEngine extends Phobject { $nav->selectFilter(null); + $navigation_items = $nav->getMenu()->getItems(); + $select_key = $this->pickHighlightedMenuItem( + $navigation_items, + $selected_item); + $nav->selectFilter($select_key); + $this->navigation = $nav; return $this->navigation; } @@ -1367,4 +1373,97 @@ abstract class PhabricatorProfileMenuEngine extends Phobject { return null; } + private function pickHighlightedMenuItem( + array $items, + PhabricatorProfileMenuItemConfiguration $selected_item = null) { + + assert_instances_of($items, 'PHUIListItemView'); + + $default_key = null; + if ($selected_item) { + $default_key = $selected_item->getDefaultMenuItemKey(); + } + + $controller = $this->getController(); + + // In some rare cases, when like building the "Favorites" menu on a + // 404 page, we may not have a controller. Just accept whatever default + // behavior we'd otherwise end up with. + if (!$controller) { + return $default_key; + } + + $request = $controller->getRequest(); + + // See T12949. If one of the menu items is a link to the same URI that + // the page was accessed with, we want to highlight that item. For example, + // this allows you to add links to a menu that apply filters to a + // workboard. + + $matches = array(); + foreach ($items as $item) { + $href = $item->getHref(); + if ($this->isMatchForRequestURI($request, $href)) { + $matches[] = $item; + } + } + + foreach ($matches as $match) { + if ($match->getKey() === $default_key) { + return $default_key; + } + } + + if ($matches) { + return head($matches)->getKey(); + } + + return $default_key; + } + + private function isMatchForRequestURI(AphrontRequest $request, $item_uri) { + $request_uri = $request->getAbsoluteRequestURI(); + $item_uri = new PhutilURI($item_uri); + + // If the request URI and item URI don't have matching paths, they + // do not match. + if ($request_uri->getPath() !== $item_uri->getPath()) { + return false; + } + + // If the request URI and item URI don't have matching parameters, they + // also do not match. We're specifically trying to let "?filter=X" work + // on Workboards, among other use cases, so this is important. + $request_params = $request_uri->getQueryParamsAsPairList(); + $item_params = $item_uri->getQueryParamsAsPairList(); + if ($request_params !== $item_params) { + return false; + } + + // If the paths and parameters match, the item domain must be: empty; or + // match the request domain; or match the production domain. + + $request_domain = $request_uri->getDomain(); + + $production_uri = PhabricatorEnv::getProductionURI('/'); + $production_domain = id(new PhutilURI($production_uri)) + ->getDomain(); + + $allowed_domains = array( + '', + $request_domain, + $production_domain, + ); + $allowed_domains = array_fuse($allowed_domains); + + $item_domain = $item_uri->getDomain(); + $item_domain = (string)$item_domain; + + if (isset($allowed_domains[$item_domain])) { + return true; + } + + return false; + } + } From 47bf382435eed4ff9b88f972cd4632126f636d1f Mon Sep 17 00:00:00 2001 From: epriestley Date: Sat, 30 Mar 2019 14:22:08 -0700 Subject: [PATCH 239/245] Allow profile menu items to be locked to the top or bottom of the menu Summary: Depends on D20353. Ref T13275. This is just some small quality-of-life fixes: - When you add items to menus, they currently go below the "Edit Menu/Manage Menu" links by default. This isn't a very good place for them. Instead, lock "edit" items to the bottom of the menu. - Lock profile pictures to the top of the menu. This just simplifies things a little. - Show more iconography hints on the "edit menu items" UI. - Add a "drag stuff to do things" hint if some stuff can be dragged. Test Plan: - Added new items to a Portal, they didn't go to the very bottom. Instead, they went above the "Edit/Manage" links; a sensible place for them. - Viewed the "edit menu items" screen, saw more hints and visual richness. - Viewed/edited Home, Projects, Portals, Favorites Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13275 Differential Revision: https://secure.phabricator.com/D20355 --- resources/celerity/map.php | 6 +-- ...icatorDashboardPortalProfileMenuEngine.php | 3 +- .../PhabricatorDashboardPortalMenuItem.php | 2 +- ...PhabricatorHomeLauncherProfileMenuItem.php | 6 ++- .../PhabricatorHomeProfileMenuItem.php | 4 ++ .../PhabricatorProjectProfileMenuEngine.php | 6 ++- ...abricatorProjectDetailsProfileMenuItem.php | 4 ++ ...habricatorProjectManageProfileMenuItem.php | 4 ++ ...abricatorProjectMembersProfileMenuItem.php | 4 ++ ...abricatorProjectPictureProfileMenuItem.php | 4 ++ ...catorProjectSubprojectsProfileMenuItem.php | 4 ++ ...ricatorProjectWorkboardProfileMenuItem.php | 4 ++ .../engine/PhabricatorProfileMenuEngine.php | 45 +++++++++++++++---- .../PhabricatorLabelProfileMenuItem.php | 2 +- .../PhabricatorManageProfileMenuItem.php | 4 ++ ...habricatorProfileMenuItemConfiguration.php | 31 +++++++++++++ src/view/phui/PHUIObjectItemView.php | 12 +++-- .../phui/object-item/phui-oi-list-view.css | 6 ++- 18 files changed, 129 insertions(+), 22 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 20d648f549..ce47a59e07 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,7 +9,7 @@ return array( 'names' => array( 'conpherence.pkg.css' => '3c8a0668', 'conpherence.pkg.js' => '020aebcf', - 'core.pkg.css' => '7e6e954b', + 'core.pkg.css' => 'a1c2d49b', 'core.pkg.js' => 'a747b035', 'differential.pkg.css' => '8d8360fb', 'differential.pkg.js' => '67e02996', @@ -132,7 +132,7 @@ return array( 'rsrc/css/phui/object-item/phui-oi-color.css' => 'b517bfa0', 'rsrc/css/phui/object-item/phui-oi-drag-ui.css' => 'da15d3dc', 'rsrc/css/phui/object-item/phui-oi-flush-ui.css' => '490e2e2e', - 'rsrc/css/phui/object-item/phui-oi-list-view.css' => 'a65865a7', + 'rsrc/css/phui/object-item/phui-oi-list-view.css' => 'f14f2422', 'rsrc/css/phui/object-item/phui-oi-simple-ui.css' => '6a30fa46', 'rsrc/css/phui/phui-action-list.css' => 'c4972757', 'rsrc/css/phui/phui-action-panel.css' => '6c386cbf', @@ -853,7 +853,7 @@ return array( 'phui-oi-color-css' => 'b517bfa0', 'phui-oi-drag-ui-css' => 'da15d3dc', 'phui-oi-flush-ui-css' => '490e2e2e', - 'phui-oi-list-view-css' => 'a65865a7', + 'phui-oi-list-view-css' => 'f14f2422', 'phui-oi-simple-ui-css' => '6a30fa46', 'phui-pager-css' => 'd022c7ad', 'phui-pinboard-view-css' => '1f08f5d8', diff --git a/src/applications/dashboard/engine/PhabricatorDashboardPortalProfileMenuEngine.php b/src/applications/dashboard/engine/PhabricatorDashboardPortalProfileMenuEngine.php index 18184952e4..a5c99a05d0 100644 --- a/src/applications/dashboard/engine/PhabricatorDashboardPortalProfileMenuEngine.php +++ b/src/applications/dashboard/engine/PhabricatorDashboardPortalProfileMenuEngine.php @@ -24,7 +24,8 @@ final class PhabricatorDashboardPortalProfileMenuEngine $items[] = $this->newItem() ->setMenuItemKey(PhabricatorDashboardPortalMenuItem::MENUITEMKEY) - ->setBuiltinKey('manage'); + ->setBuiltinKey('manage') + ->setIsTailItem(true); return $items; } diff --git a/src/applications/dashboard/menuitem/PhabricatorDashboardPortalMenuItem.php b/src/applications/dashboard/menuitem/PhabricatorDashboardPortalMenuItem.php index 655f67058a..da8498891e 100644 --- a/src/applications/dashboard/menuitem/PhabricatorDashboardPortalMenuItem.php +++ b/src/applications/dashboard/menuitem/PhabricatorDashboardPortalMenuItem.php @@ -6,7 +6,7 @@ final class PhabricatorDashboardPortalMenuItem const MENUITEMKEY = 'portal'; public function getMenuItemTypeIcon() { - return 'fa-compass'; + return 'fa-pencil'; } public function getDefaultName() { diff --git a/src/applications/home/menuitem/PhabricatorHomeLauncherProfileMenuItem.php b/src/applications/home/menuitem/PhabricatorHomeLauncherProfileMenuItem.php index a727fbced6..e077ac7ba4 100644 --- a/src/applications/home/menuitem/PhabricatorHomeLauncherProfileMenuItem.php +++ b/src/applications/home/menuitem/PhabricatorHomeLauncherProfileMenuItem.php @@ -13,6 +13,10 @@ final class PhabricatorHomeLauncherProfileMenuItem return pht('More Applications'); } + public function getMenuItemTypeIcon() { + return 'fa-ellipsis-h'; + } + public function canHideMenuItem( PhabricatorProfileMenuItemConfiguration $config) { return false; @@ -50,7 +54,7 @@ final class PhabricatorHomeLauncherProfileMenuItem $viewer = $this->getViewer(); $name = $this->getDisplayName($config); - $icon = 'fa-globe'; + $icon = 'fa-ellipsis-h'; $href = '/applications/'; $item = $this->newItem() diff --git a/src/applications/home/menuitem/PhabricatorHomeProfileMenuItem.php b/src/applications/home/menuitem/PhabricatorHomeProfileMenuItem.php index 8b3eb4fe2d..f4f3cb8733 100644 --- a/src/applications/home/menuitem/PhabricatorHomeProfileMenuItem.php +++ b/src/applications/home/menuitem/PhabricatorHomeProfileMenuItem.php @@ -13,6 +13,10 @@ final class PhabricatorHomeProfileMenuItem return pht('Home'); } + public function getMenuItemTypeIcon() { + return 'fa-home'; + } + public function canMakeDefault( PhabricatorProfileMenuItemConfiguration $config) { return true; diff --git a/src/applications/project/engine/PhabricatorProjectProfileMenuEngine.php b/src/applications/project/engine/PhabricatorProjectProfileMenuEngine.php index 77b69443da..813cd01781 100644 --- a/src/applications/project/engine/PhabricatorProjectProfileMenuEngine.php +++ b/src/applications/project/engine/PhabricatorProjectProfileMenuEngine.php @@ -22,7 +22,8 @@ final class PhabricatorProjectProfileMenuEngine $items[] = $this->newItem() ->setBuiltinKey(PhabricatorProject::ITEM_PICTURE) - ->setMenuItemKey(PhabricatorProjectPictureProfileMenuItem::MENUITEMKEY); + ->setMenuItemKey(PhabricatorProjectPictureProfileMenuItem::MENUITEMKEY) + ->setIsHeadItem(true); $items[] = $this->newItem() ->setBuiltinKey(PhabricatorProject::ITEM_PROFILE) @@ -47,7 +48,8 @@ final class PhabricatorProjectProfileMenuEngine $items[] = $this->newItem() ->setBuiltinKey(PhabricatorProject::ITEM_MANAGE) - ->setMenuItemKey(PhabricatorProjectManageProfileMenuItem::MENUITEMKEY); + ->setMenuItemKey(PhabricatorProjectManageProfileMenuItem::MENUITEMKEY) + ->setIsTailItem(true); return $items; } diff --git a/src/applications/project/menuitem/PhabricatorProjectDetailsProfileMenuItem.php b/src/applications/project/menuitem/PhabricatorProjectDetailsProfileMenuItem.php index b779f4be90..536165e3ef 100644 --- a/src/applications/project/menuitem/PhabricatorProjectDetailsProfileMenuItem.php +++ b/src/applications/project/menuitem/PhabricatorProjectDetailsProfileMenuItem.php @@ -13,6 +13,10 @@ final class PhabricatorProjectDetailsProfileMenuItem return pht('Project Details'); } + public function getMenuItemTypeIcon() { + return 'fa-file-text-o'; + } + public function canHideMenuItem( PhabricatorProfileMenuItemConfiguration $config) { return false; diff --git a/src/applications/project/menuitem/PhabricatorProjectManageProfileMenuItem.php b/src/applications/project/menuitem/PhabricatorProjectManageProfileMenuItem.php index 1bd7e796dc..b20bc777ae 100644 --- a/src/applications/project/menuitem/PhabricatorProjectManageProfileMenuItem.php +++ b/src/applications/project/menuitem/PhabricatorProjectManageProfileMenuItem.php @@ -13,6 +13,10 @@ final class PhabricatorProjectManageProfileMenuItem return pht('Manage'); } + public function getMenuItemTypeIcon() { + return 'fa-cog'; + } + public function canHideMenuItem( PhabricatorProfileMenuItemConfiguration $config) { return false; diff --git a/src/applications/project/menuitem/PhabricatorProjectMembersProfileMenuItem.php b/src/applications/project/menuitem/PhabricatorProjectMembersProfileMenuItem.php index 580aacb635..b13543312d 100644 --- a/src/applications/project/menuitem/PhabricatorProjectMembersProfileMenuItem.php +++ b/src/applications/project/menuitem/PhabricatorProjectMembersProfileMenuItem.php @@ -13,6 +13,10 @@ final class PhabricatorProjectMembersProfileMenuItem return pht('Members'); } + public function getMenuItemTypeIcon() { + return 'fa-users'; + } + public function getDisplayName( PhabricatorProfileMenuItemConfiguration $config) { $name = $config->getMenuItemProperty('name'); diff --git a/src/applications/project/menuitem/PhabricatorProjectPictureProfileMenuItem.php b/src/applications/project/menuitem/PhabricatorProjectPictureProfileMenuItem.php index 2201687c81..b5c203402d 100644 --- a/src/applications/project/menuitem/PhabricatorProjectPictureProfileMenuItem.php +++ b/src/applications/project/menuitem/PhabricatorProjectPictureProfileMenuItem.php @@ -13,6 +13,10 @@ final class PhabricatorProjectPictureProfileMenuItem return pht('Project Picture'); } + public function getMenuItemTypeIcon() { + return 'fa-image'; + } + public function canHideMenuItem( PhabricatorProfileMenuItemConfiguration $config) { return false; diff --git a/src/applications/project/menuitem/PhabricatorProjectSubprojectsProfileMenuItem.php b/src/applications/project/menuitem/PhabricatorProjectSubprojectsProfileMenuItem.php index 6b43210274..da43e30bb8 100644 --- a/src/applications/project/menuitem/PhabricatorProjectSubprojectsProfileMenuItem.php +++ b/src/applications/project/menuitem/PhabricatorProjectSubprojectsProfileMenuItem.php @@ -13,6 +13,10 @@ final class PhabricatorProjectSubprojectsProfileMenuItem return pht('Subprojects'); } + public function getMenuItemTypeIcon() { + return 'fa-sitemap'; + } + public function shouldEnableForObject($object) { if ($object->isMilestone()) { return false; diff --git a/src/applications/project/menuitem/PhabricatorProjectWorkboardProfileMenuItem.php b/src/applications/project/menuitem/PhabricatorProjectWorkboardProfileMenuItem.php index 38b9632d93..1485f1ef8a 100644 --- a/src/applications/project/menuitem/PhabricatorProjectWorkboardProfileMenuItem.php +++ b/src/applications/project/menuitem/PhabricatorProjectWorkboardProfileMenuItem.php @@ -13,6 +13,10 @@ final class PhabricatorProjectWorkboardProfileMenuItem return pht('Workboard'); } + public function getMenuItemTypeIcon() { + return 'fa-columns'; + } + public function canMakeDefault( PhabricatorProfileMenuItemConfiguration $config) { return true; diff --git a/src/applications/search/engine/PhabricatorProfileMenuEngine.php b/src/applications/search/engine/PhabricatorProfileMenuEngine.php index e2439a341b..abb6de789b 100644 --- a/src/applications/search/engine/PhabricatorProfileMenuEngine.php +++ b/src/applications/search/engine/PhabricatorProfileMenuEngine.php @@ -460,6 +460,12 @@ abstract class PhabricatorProfileMenuEngine extends Phobject { // stored config: it corresponds to an out-of-date or uninstalled // item. if (isset($items[$builtin_key])) { + $builtin_item = $items[$builtin_key]; + + // Copy runtime properties from the builtin item to the stored item. + $stored_item->setIsHeadItem($builtin_item->getIsHeadItem()); + $stored_item->setIsTailItem($builtin_item->getIsTailItem()); + $items[$builtin_key] = $stored_item; } else { continue; @@ -802,6 +808,7 @@ abstract class PhabricatorProfileMenuEngine extends Phobject { ->setID($list_id) ->setNoDataString(pht('This menu currently has no items.')); + $any_draggable = false; foreach ($items as $item) { $id = $item->getID(); $builtin_key = $item->getBuiltinKey(); @@ -822,14 +829,25 @@ abstract class PhabricatorProfileMenuEngine extends Phobject { $view->setHeader($name); $view->addAttribute($type); + $icon = $item->getMenuItem()->getMenuItemTypeIcon(); + if ($icon !== null) { + $view->setStatusIcon($icon); + } + if ($can_edit) { - $view - ->setGrippable(true) - ->addSigil('profile-menu-item') - ->setMetadata( - array( - 'key' => nonempty($id, $builtin_key), - )); + $can_move = (!$item->getIsHeadItem() && !$item->getIsTailItem()); + if ($can_move) { + $view + ->setGrippable(true) + ->addSigil('profile-menu-item') + ->setMetadata( + array( + 'key' => nonempty($id, $builtin_key), + )); + $any_draggable = true; + } else { + $view->setGrippable(false); + } if ($id) { $default_uri = $this->getItemURI("default/{$id}/"); @@ -944,8 +962,16 @@ abstract class PhabricatorProfileMenuEngine extends Phobject { ->setHeader(pht('Menu Items')) ->setHeaderIcon('fa-list'); + $list_header = id(new PHUIHeaderView()) + ->setHeader(pht('Current Menu Items')); + + if ($any_draggable) { + $list_header->setSubheader( + pht('Drag items in this list to reorder them.')); + } + $box = id(new PHUIObjectBoxView()) - ->setHeaderText(pht('Current Menu Items')) + ->setHeader($list_header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setObjectList($list); @@ -1190,7 +1216,8 @@ abstract class PhabricatorProfileMenuEngine extends Phobject { protected function newManageItem() { return $this->newItem() ->setBuiltinKey(self::ITEM_MANAGE) - ->setMenuItemKey(PhabricatorManageProfileMenuItem::MENUITEMKEY); + ->setMenuItemKey(PhabricatorManageProfileMenuItem::MENUITEMKEY) + ->setIsTailItem(true); } public function adjustDefault($key) { diff --git a/src/applications/search/menuitem/PhabricatorLabelProfileMenuItem.php b/src/applications/search/menuitem/PhabricatorLabelProfileMenuItem.php index 1f769905d7..605098f216 100644 --- a/src/applications/search/menuitem/PhabricatorLabelProfileMenuItem.php +++ b/src/applications/search/menuitem/PhabricatorLabelProfileMenuItem.php @@ -7,7 +7,7 @@ final class PhabricatorLabelProfileMenuItem const FIELD_NAME = 'name'; public function getMenuItemTypeIcon() { - return 'fa-map-signs'; + return 'fa-tag'; } public function getMenuItemTypeName() { diff --git a/src/applications/search/menuitem/PhabricatorManageProfileMenuItem.php b/src/applications/search/menuitem/PhabricatorManageProfileMenuItem.php index d5de555975..2d882ae2c8 100644 --- a/src/applications/search/menuitem/PhabricatorManageProfileMenuItem.php +++ b/src/applications/search/menuitem/PhabricatorManageProfileMenuItem.php @@ -13,6 +13,10 @@ final class PhabricatorManageProfileMenuItem return pht('Edit Menu'); } + public function getMenuItemTypeIcon() { + return 'fa-pencil'; + } + public function canHideMenuItem( PhabricatorProfileMenuItemConfiguration $config) { return false; diff --git a/src/applications/search/storage/PhabricatorProfileMenuItemConfiguration.php b/src/applications/search/storage/PhabricatorProfileMenuItemConfiguration.php index 8520cac1bd..4a23655d54 100644 --- a/src/applications/search/storage/PhabricatorProfileMenuItemConfiguration.php +++ b/src/applications/search/storage/PhabricatorProfileMenuItemConfiguration.php @@ -17,6 +17,8 @@ final class PhabricatorProfileMenuItemConfiguration private $profileObject = self::ATTACHABLE; private $menuItem = self::ATTACHABLE; + private $isHeadItem = false; + private $isTailItem = false; const VISIBILITY_DEFAULT = 'default'; const VISIBILITY_VISIBLE = 'visible'; @@ -158,6 +160,15 @@ final class PhabricatorProfileMenuItemConfiguration $is_global = 1; } + // Sort "head" items above other items and "tail" items after other items. + if ($this->getIsHeadItem()) { + $force_position = 0; + } else if ($this->getIsTailItem()) { + $force_position = 2; + } else { + $force_position = 1; + } + // Sort items with an explicit order above items without an explicit order, // so any newly created builtins go to the bottom. $order = $this->getMenuItemOrder(); @@ -169,6 +180,7 @@ final class PhabricatorProfileMenuItemConfiguration return id(new PhutilSortVector()) ->addInt($is_global) + ->addInt($force_position) ->addInt($has_order) ->addInt((int)$order) ->addInt((int)$this->getID()); @@ -207,6 +219,25 @@ final class PhabricatorProfileMenuItemConfiguration return $this->getMenuItem()->newPageContent($this); } + public function setIsHeadItem($is_head_item) { + $this->isHeadItem = $is_head_item; + return $this; + } + + public function getIsHeadItem() { + return $this->isHeadItem; + } + + public function setIsTailItem($is_tail_item) { + $this->isTailItem = $is_tail_item; + return $this; + } + + public function getIsTailItem() { + return $this->isTailItem; + } + + /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/view/phui/PHUIObjectItemView.php b/src/view/phui/PHUIObjectItemView.php index 463f34a2a0..40dd8f0a4b 100644 --- a/src/view/phui/PHUIObjectItemView.php +++ b/src/view/phui/PHUIObjectItemView.php @@ -330,8 +330,14 @@ final class PHUIObjectItemView extends AphrontTagView { Javelin::initBehavior('phui-selectable-list'); } - if ($this->getGrippable()) { - $item_classes[] = 'phui-oi-grippable'; + $is_grippable = $this->getGrippable(); + if ($is_grippable !== null) { + $item_classes[] = 'phui-oi-has-grip'; + if ($is_grippable) { + $item_classes[] = 'phui-oi-grippable'; + } else { + $item_classes[] = 'phui-oi-ungrippable'; + } } if ($this->getImageURI()) { @@ -580,7 +586,7 @@ final class PHUIObjectItemView extends AphrontTagView { } $grippable = null; - if ($this->getGrippable()) { + if ($this->getGrippable() !== null) { $grippable = phutil_tag( 'div', array( diff --git a/webroot/rsrc/css/phui/object-item/phui-oi-list-view.css b/webroot/rsrc/css/phui/object-item/phui-oi-list-view.css index 67d0682aa7..d8ac3a8bbb 100644 --- a/webroot/rsrc/css/phui/object-item/phui-oi-list-view.css +++ b/webroot/rsrc/css/phui/object-item/phui-oi-list-view.css @@ -132,11 +132,15 @@ ul.phui-oi-list-view { background: url('/rsrc/image/texture/grip.png') center center no-repeat; } +.phui-oi-ungrippable .phui-oi-grip { + opacity: 0.25; +} + .device .phui-oi-grip { display: none; } -.phui-oi-grippable .phui-oi-frame { +.phui-oi-has-grip .phui-oi-frame { padding-left: 16px; } From 971a272bf679979ef9cc638c3113c98d2fefb186 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 31 Mar 2019 11:48:09 -0700 Subject: [PATCH 240/245] Automatically build mobile menus from navigation, and clean up external ProfileMenu API Summary: Depends on D20355. Ref T13275. Ref T13247. Currently, "Hamburger" menus are not automatically built from navigation menus. However, this is (I'm almost completely sure?) a reasonable and appropriate default behavior, and saves us some code around profile menus. With this rule in place, we can remove `setApplicationMenu()` and `getApplicationMenu()` from `StandardPageView`, since they have no callers. This also updates a lot of profile menu callsites to a new API which is added in the next change. Test Plan: See the next two changes. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13275, T13247 Differential Revision: https://secure.phabricator.com/D20356 --- ...abricatorFavoritesMainMenuBarExtension.php | 5 +- .../controller/PhabricatorHomeController.php | 44 +---------------- ...abricatorPeopleProfileBadgesController.php | 5 +- ...bricatorPeopleProfileCommitsController.php | 5 +- .../PhabricatorPeopleProfileController.php | 49 ++++++++----------- ...abricatorPeopleProfileManageController.php | 5 +- ...icatorPeopleProfileRevisionsController.php | 5 +- ...habricatorPeopleProfileTasksController.php | 5 +- ...PhabricatorPeopleProfileViewController.php | 7 +-- .../PhabricatorProjectBoardViewController.php | 19 +++---- .../PhabricatorProjectController.php | 43 +++++++--------- .../PhabricatorProjectManageController.php | 5 +- ...habricatorProjectMembersViewController.php | 5 +- .../PhabricatorProjectProfileController.php | 5 +- ...habricatorProjectSubprojectsController.php | 5 +- .../PhabricatorProjectViewController.php | 2 +- ...PhabricatorApplicationSearchController.php | 2 - src/view/page/PhabricatorStandardPageView.php | 29 ++++------- 18 files changed, 91 insertions(+), 154 deletions(-) diff --git a/src/applications/favorites/engineextension/PhabricatorFavoritesMainMenuBarExtension.php b/src/applications/favorites/engineextension/PhabricatorFavoritesMainMenuBarExtension.php index f26f4603e1..adc272a125 100644 --- a/src/applications/favorites/engineextension/PhabricatorFavoritesMainMenuBarExtension.php +++ b/src/applications/favorites/engineextension/PhabricatorFavoritesMainMenuBarExtension.php @@ -20,7 +20,7 @@ final class PhabricatorFavoritesMainMenuBarExtension $dropdown = $this->newDropdown($viewer); if (!$dropdown) { - return null; + return array(); } $favorites_menu = id(new PHUIButtonView()) @@ -59,7 +59,8 @@ final class PhabricatorFavoritesMainMenuBarExtension $menu_engine->setController($controller); } - $filter_view = $menu_engine->buildNavigation(); + $filter_view = $menu_engine->newProfileMenuItemViewList() + ->newNavigationView(); $menu_view = $filter_view->getMenu(); $item_views = $menu_view->getItems(); diff --git a/src/applications/home/controller/PhabricatorHomeController.php b/src/applications/home/controller/PhabricatorHomeController.php index 9cd3b5b91d..e47d513946 100644 --- a/src/applications/home/controller/PhabricatorHomeController.php +++ b/src/applications/home/controller/PhabricatorHomeController.php @@ -1,44 +1,4 @@ newApplicationMenu(); - - $profile_menu = $this->getProfileMenu(); - if ($profile_menu) { - $menu->setProfileMenu($profile_menu); - } - - return $menu; - } - - protected function getProfileMenu() { - if (!$this->profileMenu) { - $viewer = $this->getViewer(); - $applications = id(new PhabricatorApplicationQuery()) - ->setViewer($viewer) - ->withClasses(array('PhabricatorHomeApplication')) - ->withInstalled(true) - ->execute(); - $home = head($applications); - if (!$home) { - return null; - } - - $engine = id(new PhabricatorHomeProfileMenuEngine()) - ->setViewer($viewer) - ->setController($this) - ->setProfileObject($home) - ->setCustomPHID($viewer->getPHID()); - - $this->profileMenu = $engine->buildNavigation(); - } - - return $this->profileMenu; - } - -} +abstract class PhabricatorHomeController + extends PhabricatorController {} diff --git a/src/applications/people/controller/PhabricatorPeopleProfileBadgesController.php b/src/applications/people/controller/PhabricatorPeopleProfileBadgesController.php index f3e95eeb66..f98970ef73 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfileBadgesController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfileBadgesController.php @@ -30,8 +30,9 @@ final class PhabricatorPeopleProfileBadgesController $crumbs->addTextCrumb(pht('Badges')); $crumbs->setBorder(true); - $nav = $this->getProfileMenu(); - $nav->selectFilter(PhabricatorPeopleProfileMenuEngine::ITEM_BADGES); + $nav = $this->newNavigation( + $user, + PhabricatorPeopleProfileMenuEngine::ITEM_BADGES); // Best option? $badges = id(new PhabricatorBadgesQuery()) diff --git a/src/applications/people/controller/PhabricatorPeopleProfileCommitsController.php b/src/applications/people/controller/PhabricatorPeopleProfileCommitsController.php index c18c5f4d96..430e11311e 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfileCommitsController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfileCommitsController.php @@ -32,8 +32,9 @@ final class PhabricatorPeopleProfileCommitsController $crumbs->addTextCrumb(pht('Recent Commits')); $crumbs->setBorder(true); - $nav = $this->getProfileMenu(); - $nav->selectFilter(PhabricatorPeopleProfileMenuEngine::ITEM_COMMITS); + $nav = $this->newNavigation( + $user, + PhabricatorPeopleProfileMenuEngine::ITEM_COMMITS); $view = id(new PHUITwoColumnView()) ->setHeader($header) diff --git a/src/applications/people/controller/PhabricatorPeopleProfileController.php b/src/applications/people/controller/PhabricatorPeopleProfileController.php index 91afda123b..1d6f0fc74c 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfileController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfileController.php @@ -4,7 +4,6 @@ abstract class PhabricatorPeopleProfileController extends PhabricatorPeopleController { private $user; - private $profileMenu; public function shouldRequireAdmin() { return false; @@ -19,34 +18,6 @@ abstract class PhabricatorPeopleProfileController return $this->user; } - public function buildApplicationMenu() { - $menu = $this->newApplicationMenu(); - - $profile_menu = $this->getProfileMenu(); - if ($profile_menu) { - $menu->setProfileMenu($profile_menu); - } - - return $menu; - } - - protected function getProfileMenu() { - if (!$this->profileMenu) { - $user = $this->getUser(); - if ($user) { - $viewer = $this->getViewer(); - - $engine = id(new PhabricatorPeopleProfileMenuEngine()) - ->setViewer($viewer) - ->setProfileObject($user); - - $this->profileMenu = $engine->buildNavigation(); - } - } - - return $this->profileMenu; - } - protected function buildApplicationCrumbs() { $crumbs = parent::buildApplicationCrumbs(); @@ -138,4 +109,24 @@ abstract class PhabricatorPeopleProfileController return $header; } + final protected function newNavigation( + PhabricatorUser $user, + $item_identifier) { + + $viewer = $this->getViewer(); + + $engine = id(new PhabricatorPeopleProfileMenuEngine()) + ->setViewer($viewer) + ->setController($this) + ->setProfileObject($user); + + $view_list = $engine->newProfileMenuItemViewList(); + + $view_list->setSelectedViewWithItemIdentifier($item_identifier); + + $navigation = $view_list->newNavigationView(); + + return $navigation; + } + } diff --git a/src/applications/people/controller/PhabricatorPeopleProfileManageController.php b/src/applications/people/controller/PhabricatorPeopleProfileManageController.php index 835935f775..5db38adafb 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfileManageController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfileManageController.php @@ -29,8 +29,9 @@ final class PhabricatorPeopleProfileManageController $properties = $this->buildPropertyView($user); $name = $user->getUsername(); - $nav = $this->getProfileMenu(); - $nav->selectFilter(PhabricatorPeopleProfileMenuEngine::ITEM_MANAGE); + $nav = $this->newNavigation( + $user, + PhabricatorPeopleProfileMenuEngine::ITEM_MANAGE); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Manage')); diff --git a/src/applications/people/controller/PhabricatorPeopleProfileRevisionsController.php b/src/applications/people/controller/PhabricatorPeopleProfileRevisionsController.php index 55baf0140f..0c7b6f6a1f 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfileRevisionsController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfileRevisionsController.php @@ -32,8 +32,9 @@ final class PhabricatorPeopleProfileRevisionsController $crumbs->addTextCrumb(pht('Recent Revisions')); $crumbs->setBorder(true); - $nav = $this->getProfileMenu(); - $nav->selectFilter(PhabricatorPeopleProfileMenuEngine::ITEM_REVISIONS); + $nav = $this->newNavigation( + $user, + PhabricatorPeopleProfileMenuEngine::ITEM_REVISIONS); $view = id(new PHUITwoColumnView()) ->setHeader($header) diff --git a/src/applications/people/controller/PhabricatorPeopleProfileTasksController.php b/src/applications/people/controller/PhabricatorPeopleProfileTasksController.php index b843af8fc7..bc4e1432f1 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfileTasksController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfileTasksController.php @@ -32,8 +32,9 @@ final class PhabricatorPeopleProfileTasksController $crumbs->addTextCrumb(pht('Assigned Tasks')); $crumbs->setBorder(true); - $nav = $this->getProfileMenu(); - $nav->selectFilter(PhabricatorPeopleProfileMenuEngine::ITEM_TASKS); + $nav = $this->newNavigation( + $user, + PhabricatorPeopleProfileMenuEngine::ITEM_TASKS); $view = id(new PHUITwoColumnView()) ->setHeader($header) diff --git a/src/applications/people/controller/PhabricatorPeopleProfileViewController.php b/src/applications/people/controller/PhabricatorPeopleProfileViewController.php index 6a4d68d6aa..b5c0e2b816 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfileViewController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfileViewController.php @@ -64,15 +64,16 @@ final class PhabricatorPeopleProfileViewController $calendar, )); - $nav = $this->getProfileMenu(); - $nav->selectFilter(PhabricatorPeopleProfileMenuEngine::ITEM_PROFILE); + $navigation = $this->newNavigation( + $user, + PhabricatorPeopleProfileMenuEngine::ITEM_PROFILE); $crumbs = $this->buildApplicationCrumbs(); $crumbs->setBorder(true); return $this->newPage() ->setTitle($user->getUsername()) - ->setNavigation($nav) + ->setNavigation($navigation) ->setCrumbs($crumbs) ->setPageObjectPHIDs( array( diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index bd14033d3f..3ee0213daa 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -172,7 +172,9 @@ final class PhabricatorProjectBoardViewController return $content; } - $nav = $this->newWorkboardProfileMenu(); + $nav = $this->newNavigation( + $project, + PhabricatorProject::ITEM_WORKBOARD); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Workboard')); @@ -719,7 +721,9 @@ final class PhabricatorProjectBoardViewController ->appendChild($board) ->addClass('project-board-wrapper'); - $nav = $this->newWorkboardProfileMenu(); + $nav = $this->newNavigation( + $project, + PhabricatorProject::ITEM_WORKBOARD); $divider = id(new PHUIListItemView()) ->setType(PHUIListItemView::TYPE_DIVIDER); @@ -1503,15 +1507,4 @@ final class PhabricatorProjectBoardViewController ->addCancelButton($profile_uri); } - private function newWorkboardProfileMenu() { - $default_item = id(new PhabricatorProfileMenuItemConfiguration()) - ->setBuiltinKey(PhabricatorProject::ITEM_WORKBOARD); - - $menu = parent::getProfileMenu($default_item); - - $menu->addClass('project-board-nav'); - - return $menu; - } - } diff --git a/src/applications/project/controller/PhabricatorProjectController.php b/src/applications/project/controller/PhabricatorProjectController.php index eb4ab70711..7781f4c73b 100644 --- a/src/applications/project/controller/PhabricatorProjectController.php +++ b/src/applications/project/controller/PhabricatorProjectController.php @@ -84,30 +84,6 @@ abstract class PhabricatorProjectController extends PhabricatorController { return null; } - public function buildApplicationMenu() { - $menu = $this->newApplicationMenu(); - - $profile_menu = $this->getProfileMenu(); - if ($profile_menu) { - $menu->setProfileMenu($profile_menu); - } - - $menu->setSearchEngine(new PhabricatorProjectSearchEngine()); - - return $menu; - } - - protected function getProfileMenu($default_item = null) { - if (!$this->profileMenu) { - $engine = $this->getProfileMenuEngine(); - if ($engine) { - $this->profileMenu = $engine->buildNavigation($default_item); - } - } - - return $this->profileMenu; - } - protected function buildApplicationCrumbs() { return $this->newApplicationCrumbs('profile'); } @@ -207,4 +183,23 @@ abstract class PhabricatorProjectController extends PhabricatorController { return implode(', ', $result); } + final protected function newNavigation( + PhabricatorProject $project, + $item_identifier) { + + $engine = $this->getProfileMenuEngine(); + + $view_list = $engine->newProfileMenuItemViewList(); + + $view_list->setSelectedViewWithItemIdentifier($item_identifier); + + $navigation = $view_list->newNavigationView(); + + if ($item_identifier === PhabricatorProject::ITEM_WORKBOARD) { + $navigation->addClass('project-board-nav'); + } + + return $navigation; + } + } diff --git a/src/applications/project/controller/PhabricatorProjectManageController.php b/src/applications/project/controller/PhabricatorProjectManageController.php index 2c76c63606..eadda5bf86 100644 --- a/src/applications/project/controller/PhabricatorProjectManageController.php +++ b/src/applications/project/controller/PhabricatorProjectManageController.php @@ -37,8 +37,9 @@ final class PhabricatorProjectManageController new PhabricatorProjectTransactionQuery()); $timeline->setShouldTerminate(true); - $nav = $this->getProfileMenu(); - $nav->selectFilter(PhabricatorProject::ITEM_MANAGE); + $nav = $this->newNavigation( + $project, + PhabricatorProject::ITEM_MANAGE); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Manage')); diff --git a/src/applications/project/controller/PhabricatorProjectMembersViewController.php b/src/applications/project/controller/PhabricatorProjectMembersViewController.php index ad553eb664..ee0b71cefa 100644 --- a/src/applications/project/controller/PhabricatorProjectMembersViewController.php +++ b/src/applications/project/controller/PhabricatorProjectMembersViewController.php @@ -36,8 +36,9 @@ final class PhabricatorProjectMembersViewController ->setUserPHIDs($project->getWatcherPHIDs()) ->setShowNote(true); - $nav = $this->getProfileMenu(); - $nav->selectFilter(PhabricatorProject::ITEM_MEMBERS); + $nav = $this->newNavigation( + $project, + PhabricatorProject::ITEM_MEMBERS); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Members')); diff --git a/src/applications/project/controller/PhabricatorProjectProfileController.php b/src/applications/project/controller/PhabricatorProjectProfileController.php index 6e1e7677e6..67b94f7fa9 100644 --- a/src/applications/project/controller/PhabricatorProjectProfileController.php +++ b/src/applications/project/controller/PhabricatorProjectProfileController.php @@ -74,8 +74,9 @@ final class PhabricatorProjectProfileController ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setUserPHIDs($project->getWatcherPHIDs()); - $nav = $this->getProfileMenu(); - $nav->selectFilter(PhabricatorProject::ITEM_PROFILE); + $nav = $this->newNavigation( + $project, + PhabricatorProject::ITEM_PROFILE); $stories = id(new PhabricatorFeedQuery()) ->setViewer($viewer) diff --git a/src/applications/project/controller/PhabricatorProjectSubprojectsController.php b/src/applications/project/controller/PhabricatorProjectSubprojectsController.php index 36736c78ba..be787e7a4f 100644 --- a/src/applications/project/controller/PhabricatorProjectSubprojectsController.php +++ b/src/applications/project/controller/PhabricatorProjectSubprojectsController.php @@ -77,8 +77,9 @@ final class PhabricatorProjectSubprojectsController $milestones, $subprojects); - $nav = $this->getProfileMenu(); - $nav->selectFilter(PhabricatorProject::ITEM_SUBPROJECTS); + $nav = $this->newNavigation( + $project, + PhabricatorProject::ITEM_SUBPROJECTS); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Subprojects')); diff --git a/src/applications/project/controller/PhabricatorProjectViewController.php b/src/applications/project/controller/PhabricatorProjectViewController.php index c5c536bfa1..beb622ea50 100644 --- a/src/applications/project/controller/PhabricatorProjectViewController.php +++ b/src/applications/project/controller/PhabricatorProjectViewController.php @@ -18,7 +18,7 @@ final class PhabricatorProjectViewController $project = $this->getProject(); $engine = $this->getProfileMenuEngine(); - $default = $engine->getDefaultItem(); + $default = $engine->getDefaultMenuItemConfiguration(); // If defaults are broken somehow, serve the manage page. See T13033 for // discussion. diff --git a/src/applications/search/controller/PhabricatorApplicationSearchController.php b/src/applications/search/controller/PhabricatorApplicationSearchController.php index 4bf4929f4b..cab5ae61ff 100644 --- a/src/applications/search/controller/PhabricatorApplicationSearchController.php +++ b/src/applications/search/controller/PhabricatorApplicationSearchController.php @@ -387,7 +387,6 @@ final class PhabricatorApplicationSearchController require_celerity_resource('application-search-view-css'); return $this->newPage() - ->setApplicationMenu($this->buildApplicationMenu()) ->setTitle(pht('Query: %s', $title)) ->setCrumbs($crumbs) ->setNavigation($nav) @@ -611,7 +610,6 @@ final class PhabricatorApplicationSearchController ->setFooter($lists); return $this->newPage() - ->setApplicationMenu($this->buildApplicationMenu()) ->setTitle(pht('Saved Queries')) ->setCrumbs($crumbs) ->setNavigation($nav) diff --git a/src/view/page/PhabricatorStandardPageView.php b/src/view/page/PhabricatorStandardPageView.php index cfb1b4abbe..5498a1fb34 100644 --- a/src/view/page/PhabricatorStandardPageView.php +++ b/src/view/page/PhabricatorStandardPageView.php @@ -32,18 +32,6 @@ final class PhabricatorStandardPageView extends PhabricatorBarePageView return $this->showFooter; } - public function setApplicationMenu($application_menu) { - // NOTE: For now, this can either be a PHUIListView or a - // PHUIApplicationMenuView. - - $this->applicationMenu = $application_menu; - return $this; - } - - public function getApplicationMenu() { - return $this->applicationMenu; - } - public function setApplicationName($application_name) { $this->applicationName = $application_name; return $this; @@ -345,7 +333,7 @@ final class PhabricatorStandardPageView extends PhabricatorBarePageView $menu->setController($this->getController()); } - $application_menu = $this->getApplicationMenu(); + $application_menu = $this->applicationMenu; if ($application_menu) { if ($application_menu instanceof PHUIApplicationMenuView) { $crumbs = $this->getCrumbs(); @@ -865,13 +853,6 @@ final class PhabricatorStandardPageView extends PhabricatorBarePageView public function produceAphrontResponse() { $controller = $this->getController(); - if (!$this->getApplicationMenu()) { - $application_menu = $controller->buildApplicationMenu(); - if ($application_menu) { - $this->setApplicationMenu($application_menu); - } - } - $viewer = $this->getUser(); if ($viewer && $viewer->getPHID()) { $object_phids = $this->pageObjects; @@ -887,6 +868,14 @@ final class PhabricatorStandardPageView extends PhabricatorBarePageView $response = id(new AphrontAjaxResponse()) ->setContent($content); } else { + // See T13247. Try to find some navigational menu items to create a + // mobile navigation menu from. + $application_menu = $controller->buildApplicationMenu(); + if (!$application_menu) { + $application_menu = $this->getNavigation()->getMenu(); + } + $this->applicationMenu = $application_menu; + $content = $this->render(); $response = id(new AphrontWebpageResponse()) From 950e9d085b482bc3484e2066591ac07c20154c9a Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 31 Mar 2019 11:48:44 -0700 Subject: [PATCH 241/245] In ProfileMenu, put more structure between "stored/configured items" and "display items" Summary: Depends on D20356. Ref T13275. See also T12871 and T12949. Currently, the whole "ProfileMenu" API operates around //stored// items. However, stored items are allowed to produce zero or more //display// items, and we sometimes want to highlight display item X but render stored item Y (as is the case with "Link" items pointing at `?filter=xyz` on Workboards). For the most part, this either: doesn't work; or works by chance; or is kind of glued together with hope and prayer (as in D20353). Put an actual structural layer in place between "stored/configured item" and "display item" that can link them together more clearly. Now: - The list of `ItemConfiguration` objects (stored/configured items) is used to build an `ItemViewList`. - This handles the selection/highlighting/default state, and knows which display items are related to which stored items. - When we're all done figuring out what we're going to select and what we're going to highlight, it pops out an actual View which can build the HTML. This requires API changes which are not included in this change, see next change. This doesn't really do anything on its own, but builds toward a more satisfying fix for T12871. I'd hoped to avoid doing this for now, but wasn't able to get a patch I felt good about for T12871 built without fixing this first. Test Plan: See next change. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13275 Differential Revision: https://secure.phabricator.com/D20357 --- src/__phutil_library_map__.php | 4 + .../PhabricatorProfileMenuEditEngine.php | 2 +- .../engine/PhabricatorProfileMenuEngine.php | 293 ++++-------------- .../engine/PhabricatorProfileMenuItemView.php | 212 +++++++++++++ .../PhabricatorProfileMenuItemViewList.php | 278 +++++++++++++++++ ...habricatorProfileMenuItemConfiguration.php | 12 +- 6 files changed, 568 insertions(+), 233 deletions(-) create mode 100644 src/applications/search/engine/PhabricatorProfileMenuItemView.php create mode 100644 src/applications/search/engine/PhabricatorProfileMenuItemViewList.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index d93bf72ff4..617939d393 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -4056,6 +4056,8 @@ phutil_register_library_map(array( 'PhabricatorProfileMenuItemConfigurationTransactionQuery' => 'applications/search/query/PhabricatorProfileMenuItemConfigurationTransactionQuery.php', 'PhabricatorProfileMenuItemIconSet' => 'applications/search/menuitem/PhabricatorProfileMenuItemIconSet.php', 'PhabricatorProfileMenuItemPHIDType' => 'applications/search/phidtype/PhabricatorProfileMenuItemPHIDType.php', + 'PhabricatorProfileMenuItemView' => 'applications/search/engine/PhabricatorProfileMenuItemView.php', + 'PhabricatorProfileMenuItemViewList' => 'applications/search/engine/PhabricatorProfileMenuItemViewList.php', 'PhabricatorProject' => 'applications/project/storage/PhabricatorProject.php', 'PhabricatorProjectAddHeraldAction' => 'applications/project/herald/PhabricatorProjectAddHeraldAction.php', 'PhabricatorProjectApplication' => 'applications/project/application/PhabricatorProjectApplication.php', @@ -10184,6 +10186,8 @@ phutil_register_library_map(array( 'PhabricatorProfileMenuItemConfigurationTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorProfileMenuItemIconSet' => 'PhabricatorIconSet', 'PhabricatorProfileMenuItemPHIDType' => 'PhabricatorPHIDType', + 'PhabricatorProfileMenuItemView' => 'Phobject', + 'PhabricatorProfileMenuItemViewList' => 'Phobject', 'PhabricatorProject' => array( 'PhabricatorProjectDAO', 'PhabricatorApplicationTransactionInterface', diff --git a/src/applications/search/editor/PhabricatorProfileMenuEditEngine.php b/src/applications/search/editor/PhabricatorProfileMenuEditEngine.php index b21d03cbd1..b55f43255c 100644 --- a/src/applications/search/editor/PhabricatorProfileMenuEditEngine.php +++ b/src/applications/search/editor/PhabricatorProfileMenuEditEngine.php @@ -109,7 +109,7 @@ final class PhabricatorProfileMenuEditEngine } protected function getObjectEditTitleText($object) { - $object->willBuildNavigationItems(array($object)); + $object->willGetMenuItemViewList(array($object)); return pht('Edit Menu Item: %s', $object->getDisplayName()); } diff --git a/src/applications/search/engine/PhabricatorProfileMenuEngine.php b/src/applications/search/engine/PhabricatorProfileMenuEngine.php index abb6de789b..5e89f22b16 100644 --- a/src/applications/search/engine/PhabricatorProfileMenuEngine.php +++ b/src/applications/search/engine/PhabricatorProfileMenuEngine.php @@ -71,16 +71,6 @@ abstract class PhabricatorProfileMenuEngine extends Phobject { return $this->controller; } - private function setDefaultItem( - PhabricatorProfileMenuItemConfiguration $default_item) { - $this->defaultItem = $default_item; - return $this; - } - - public function getDefaultItem() { - return $this->pickDefaultItem($this->getItems()); - } - public function setShowNavigation($show) { $this->showNavigation = $show; return $this; @@ -150,10 +140,10 @@ abstract class PhabricatorProfileMenuEngine extends Phobject { $item_id = $request->getURIData('id'); } - $item_list = $this->getItems(); + $view_list = $this->newProfileMenuItemViewList(); - $selected_item = $this->pickSelectedItem( - $item_list, + $selected_item = $this->selectItem( + $view_list, $item_id, $is_view); @@ -183,8 +173,7 @@ abstract class PhabricatorProfileMenuEngine extends Phobject { break; } - $navigation = $this->buildNavigation($selected_item); - + $navigation = $view_list->newNavigationView(); $crumbs = $controller->buildApplicationCrumbsForEditEngine(); if (!$is_view) { @@ -288,9 +277,7 @@ abstract class PhabricatorProfileMenuEngine extends Phobject { if (!$this->isMenuEnginePinnable()) { return new Aphront404Response(); } - $content = $this->buildItemDefaultContent( - $selected_item, - $item_list); + $content = $this->buildItemDefaultContent($selected_item); break; case 'edit': $content = $this->buildItemEditContent(); @@ -333,80 +320,7 @@ abstract class PhabricatorProfileMenuEngine extends Phobject { return $page; } - public function buildNavigation( - PhabricatorProfileMenuItemConfiguration $selected_item = null) { - if ($this->navigation) { - return $this->navigation; - } - - $nav = id(new AphrontSideNavFilterView()) - ->setIsProfileMenu(true) - ->setBaseURI(new PhutilURI($this->getItemURI(''))); - - $menu_items = $this->getItems(); - - $filtered_items = array(); - foreach ($menu_items as $menu_item) { - if ($menu_item->isDisabled()) { - continue; - } - $filtered_items[] = $menu_item; - } - $filtered_groups = mgroup($filtered_items, 'getMenuItemKey'); - foreach ($filtered_groups as $group) { - $first_item = head($group); - $first_item->willBuildNavigationItems($group); - } - - $has_items = false; - foreach ($menu_items as $menu_item) { - if ($menu_item->isDisabled()) { - continue; - } - - $items = $menu_item->buildNavigationMenuItems(); - foreach ($items as $item) { - $this->validateNavigationMenuItem($item); - } - - // If the item produced only a single item which does not otherwise - // have a key, try to automatically assign it a reasonable key. This - // makes selecting the correct item simpler. - - if (count($items) == 1) { - $item = head($items); - if ($item->getKey() === null) { - $default_key = $menu_item->getDefaultMenuItemKey(); - $item->setKey($default_key); - } - } - - foreach ($items as $item) { - $nav->addMenuItem($item); - $has_items = true; - } - } - - if (!$has_items) { - // If the navigation menu has no items, add an empty label item to - // force it to render something. - $empty_item = id(new PHUIListItemView()) - ->setType(PHUIListItemView::TYPE_LABEL); - $nav->addMenuItem($empty_item); - } - - $nav->selectFilter(null); - - $navigation_items = $nav->getMenu()->getItems(); - $select_key = $this->pickHighlightedMenuItem( - $navigation_items, - $selected_item); - $nav->selectFilter($select_key); - - $this->navigation = $nav; - return $this->navigation; - } private function getItems() { if ($this->items === null) { @@ -715,7 +629,7 @@ abstract class PhabricatorProfileMenuEngine extends Phobject { * * @return bool True if items may be pinned as default items. */ - protected function isMenuEnginePinnable() { + public function isMenuEnginePinnable() { return !$this->isMenuEnginePersonalizable(); } @@ -779,7 +693,7 @@ abstract class PhabricatorProfileMenuEngine extends Phobject { $filtered_groups = mgroup($items, 'getMenuItemKey'); foreach ($filtered_groups as $group) { $first_item = head($group); - $first_item->willBuildNavigationItems($group); + $first_item->willGetMenuItemViewList($group); } // Users only need to be able to edit the object which this menu appears @@ -1153,8 +1067,7 @@ abstract class PhabricatorProfileMenuEngine extends Phobject { } private function buildItemDefaultContent( - PhabricatorProfileMenuItemConfiguration $configuration, - array $items) { + PhabricatorProfileMenuItemConfiguration $configuration) { $controller = $this->getController(); $request = $controller->getRequest(); @@ -1220,6 +1133,17 @@ abstract class PhabricatorProfileMenuEngine extends Phobject { ->setIsTailItem(true); } + public function getDefaultMenuItemConfiguration() { + $configs = $this->getItems(); + foreach ($configs as $config) { + if ($config->isDefault()) { + return $config; + } + } + + return null; + } + public function adjustDefault($key) { $controller = $this->getController(); $request = $controller->getRequest(); @@ -1340,157 +1264,74 @@ abstract class PhabricatorProfileMenuEngine extends Phobject { pht('There are no menu items.')); } - private function pickDefaultItem(array $items) { - // Remove all the items which can not be the default item. - foreach ($items as $key => $item) { - if (!$item->canMakeDefault()) { - unset($items[$key]); - continue; - } + final public function newProfileMenuItemViewList() { + $items = $this->getItems(); + + // Throw away disabled items: they are not allowed to build any views for + // the menu. + foreach ($items as $key => $item) { if ($item->isDisabled()) { unset($items[$key]); continue; } } - // If this engine supports pinning items and a valid item is pinned, - // pick that item as the default. - if ($this->isMenuEnginePinnable()) { - foreach ($items as $key => $item) { - if ($item->isDefault()) { - return $item; - } - } + // Give each item group a callback so it can load data it needs to render + // views. + $groups = mgroup($items, 'getMenuItemKey'); + foreach ($groups as $group) { + $item = head($group); + $item->willGetMenuItemViewList($group); } - // If we have some other valid items, pick the first one as the default. - if ($items) { - return head($items); - } + $view_list = id(new PhabricatorProfileMenuItemViewList()) + ->setProfileMenuEngine($this); - return null; - } - - private function pickSelectedItem(array $items, $item_id, $is_view) { - if (strlen($item_id)) { - $item_id_int = (int)$item_id; - foreach ($items as $item) { - if ($item_id_int) { - if ((int)$item->getID() === $item_id_int) { - return $item; - } - } - - $builtin_key = $item->getBuiltinKey(); - if ($builtin_key === (string)$item_id) { - return $item; - } - } - - // Nothing matches the selected item ID, so we don't have a valid - // selection. - return null; - } - - if ($is_view) { - return $this->pickDefaultItem($items); - } - - return null; - } - - private function pickHighlightedMenuItem( - array $items, - PhabricatorProfileMenuItemConfiguration $selected_item = null) { - - assert_instances_of($items, 'PHUIListItemView'); - - $default_key = null; - if ($selected_item) { - $default_key = $selected_item->getDefaultMenuItemKey(); - } - - $controller = $this->getController(); - - // In some rare cases, when like building the "Favorites" menu on a - // 404 page, we may not have a controller. Just accept whatever default - // behavior we'd otherwise end up with. - if (!$controller) { - return $default_key; - } - - $request = $controller->getRequest(); - - // See T12949. If one of the menu items is a link to the same URI that - // the page was accessed with, we want to highlight that item. For example, - // this allows you to add links to a menu that apply filters to a - // workboard. - - $matches = array(); foreach ($items as $item) { - $href = $item->getHref(); - if ($this->isMatchForRequestURI($request, $href)) { - $matches[] = $item; + $views = $item->getMenuItemViewList(); + foreach ($views as $view) { + $view_list->addItemView($view); } } - foreach ($matches as $match) { - if ($match->getKey() === $default_key) { - return $default_key; + return $view_list; + } + + private function selectItem( + PhabricatorProfileMenuItemViewList $view_list, + $item_id, + $want_default) { + + // Figure out which view's content we're going to render. In most cases, + // the URI tells us. If we don't have an identifier in the URI, we'll + // render the default view instead if this is a workflow that falls back + // to default rendering. + + $selected_view = null; + if (strlen($item_id)) { + $item_views = $view_list->getViewsWithItemIdentifier($item_id); + if ($item_views) { + $selected_view = head($item_views); + } + } else { + if ($want_default) { + $default_views = $view_list->getDefaultViews(); + if ($default_views) { + $selected_view = head($default_views); + } } } - if ($matches) { - return head($matches)->getKey(); + if ($selected_view) { + $view_list->setSelectedView($selected_view); + $selected_item = $selected_view->getMenuItemConfiguration(); + } else { + $selected_item = null; } - return $default_key; + return $selected_item; } - private function isMatchForRequestURI(AphrontRequest $request, $item_uri) { - $request_uri = $request->getAbsoluteRequestURI(); - $item_uri = new PhutilURI($item_uri); - - // If the request URI and item URI don't have matching paths, they - // do not match. - if ($request_uri->getPath() !== $item_uri->getPath()) { - return false; - } - - // If the request URI and item URI don't have matching parameters, they - // also do not match. We're specifically trying to let "?filter=X" work - // on Workboards, among other use cases, so this is important. - $request_params = $request_uri->getQueryParamsAsPairList(); - $item_params = $item_uri->getQueryParamsAsPairList(); - if ($request_params !== $item_params) { - return false; - } - - // If the paths and parameters match, the item domain must be: empty; or - // match the request domain; or match the production domain. - - $request_domain = $request_uri->getDomain(); - - $production_uri = PhabricatorEnv::getProductionURI('/'); - $production_domain = id(new PhutilURI($production_uri)) - ->getDomain(); - - $allowed_domains = array( - '', - $request_domain, - $production_domain, - ); - $allowed_domains = array_fuse($allowed_domains); - - $item_domain = $item_uri->getDomain(); - $item_domain = (string)$item_domain; - - if (isset($allowed_domains[$item_domain])) { - return true; - } - - return false; - } } diff --git a/src/applications/search/engine/PhabricatorProfileMenuItemView.php b/src/applications/search/engine/PhabricatorProfileMenuItemView.php new file mode 100644 index 0000000000..44e0055a7d --- /dev/null +++ b/src/applications/search/engine/PhabricatorProfileMenuItemView.php @@ -0,0 +1,212 @@ +config = $config; + return $this; + } + + public function getMenuItemConfiguration() { + return $this->config; + } + + public function setURI($uri) { + $this->uri = $uri; + return $this; + } + + public function getURI() { + return $this->uri; + } + + public function setName($name) { + $this->name = $name; + return $this; + } + + public function getName() { + return $this->name; + } + + public function setIcon($icon) { + $this->icon = $icon; + return $this; + } + + public function getIcon() { + return $this->icon; + } + + public function setDisabled($disabled) { + $this->disabled = $disabled; + return $this; + } + + public function getDisabled() { + return $this->disabled; + } + + public function setTooltip($tooltip) { + $this->tooltip = $tooltip; + return $this; + } + + public function getTooltip() { + return $this->tooltip; + } + + public function newAction($uri) { + $this->actions[] = $uri; + return null; + } + + public function newCount($count) { + $this->counts[] = $count; + return null; + } + + public function newProfileImage($src) { + $this->images[] = $src; + return null; + } + + public function newProgressBar($bar) { + $this->progressBars[] = $bar; + return null; + } + + public function setIsExternalLink($is_external) { + $this->isExternalLink = $is_external; + return $this; + } + + public function getIsExternalLink() { + return $this->isExternalLink; + } + + public function setIsLabel($is_label) { + return $this->setSpecialType('label'); + } + + public function getIsLabel() { + return $this->isSpecialType('label'); + } + + public function setIsDivider($is_divider) { + return $this->setSpecialType('divider'); + } + + public function getIsDivider() { + return $this->isSpecialType('divider'); + } + + private function setSpecialType($type) { + $this->specialType = $type; + return $this; + } + + private function isSpecialType($type) { + return ($this->specialType === $type); + } + + public function newListItemView() { + $view = id(new PHUIListItemView()) + ->setName($this->getName()); + + $uri = $this->getURI(); + if (strlen($uri)) { + if ($this->getIsExternalLink()) { + if (!PhabricatorEnv::isValidURIForLink($uri)) { + $uri = '#'; + } + $view->setRel('noreferrer'); + } + + $view->setHref($uri); + } + + $icon = $this->getIcon(); + if ($icon) { + $view->setIcon($icon); + } + + if ($this->getDisabled()) { + $view->setDisabled(true); + } + + if ($this->getIsLabel()) { + $view->setType(PHUIListItemView::TYPE_LABEL); + } + + if ($this->getIsDivider()) { + $view + ->setType(PHUIListItemView::TYPE_DIVIDER) + ->addClass('phui-divider'); + } + + if ($this->images) { + require_celerity_resource('people-picture-menu-item-css'); + foreach ($this->images as $image_src) { + $classes = array(); + $classes[] = 'people-menu-image'; + + if ($this->getDisabled()) { + $classes[] = 'phui-image-disabled'; + } + + $image = phutil_tag( + 'img', + array( + 'src' => $image_src, + 'class' => implode(' ', $classes), + )); + + $image = phutil_tag( + 'div', + array( + 'class' => 'people-menu-image-container', + ), + $image); + + $view->appendChild($image); + } + } + + foreach ($this->counts as $count) { + $view->appendChild( + phutil_tag( + 'span', + array( + 'class' => 'phui-list-item-count', + ), + $count)); + } + + foreach ($this->actions as $action) { + $view->setActionIcon('fa-pencil', $action); + } + + foreach ($this->progressBars as $bar) { + $view->appendChild($bar); + } + + return $view; + } + +} diff --git a/src/applications/search/engine/PhabricatorProfileMenuItemViewList.php b/src/applications/search/engine/PhabricatorProfileMenuItemViewList.php new file mode 100644 index 0000000000..4890657b4e --- /dev/null +++ b/src/applications/search/engine/PhabricatorProfileMenuItemViewList.php @@ -0,0 +1,278 @@ +engine = $engine; + return $this; + } + + public function getProfileMenuEngine() { + return $this->engine; + } + + public function addItemView(PhabricatorProfileMenuItemView $view) { + $this->views[] = $view; + return $this; + } + + public function getItemViews() { + return $this->views; + } + + public function setSelectedView(PhabricatorProfileMenuItemView $view) { + $found = false; + foreach ($this->getItemViews() as $item_view) { + if ($view === $item_view) { + $found = true; + break; + } + } + + if (!$found) { + throw new Exception( + pht( + 'Provided view is not one of the views in the list: you can only '. + 'select a view which appears in the list.')); + } + + $this->selectedView = $view; + + return $this; + } + + public function setSelectedViewWithItemIdentifier($identifier) { + $views = $this->getViewsWithItemIdentifier($identifier); + + if (!$views) { + throw new Exception( + pht( + 'No views match identifier "%s"!', + $identifier)); + } + + return $this->setSelectedView(head($views)); + } + + public function getViewsWithItemIdentifier($identifier) { + $views = $this->getItemViews(); + + if (!strlen($identifier)) { + return array(); + } + + if (ctype_digit($identifier)) { + $identifier_int = (int)$identifier; + } else { + $identifier_int = null; + } + + $identifier_str = (string)$identifier; + + $results = array(); + foreach ($views as $view) { + $config = $view->getMenuItemConfiguration(); + + if ($identifier_int !== null) { + $config_id = (int)$config->getID(); + if ($config_id === $identifier_int) { + $results[] = $view; + continue; + } + } + + if ($config->getBuiltinKey() === $identifier_str) { + $results[] = $view; + continue; + } + } + + return $results; + } + + public function getDefaultViews() { + $engine = $this->getProfileMenuEngine(); + $can_pin = $engine->isMenuEnginePinnable(); + + $views = $this->getItemViews(); + + // Remove all the views which were built by an item that can not be the + // default item. + foreach ($views as $key => $view) { + $config = $view->getMenuItemConfiguration(); + + if (!$config->canMakeDefault()) { + unset($views[$key]); + continue; + } + } + + // If this engine supports pinning items and we have candidate views from a + // valid pinned item, they are the default views. + if ($can_pin) { + $pinned = array(); + + foreach ($views as $key => $view) { + $config = $view->getMenuItemConfiguration(); + + if ($config->isDefault()) { + $pinned[] = $view; + continue; + } + } + + if ($pinned) { + return $pinned; + } + } + + // Return whatever remains that's still valid. + return $views; + } + + public function newNavigationView() { + $engine = $this->getProfileMenuEngine(); + + $base_uri = $engine->getItemURI(''); + $base_uri = new PhutilURI($base_uri); + + $navigation = id(new AphrontSideNavFilterView()) + ->setIsProfileMenu(true) + ->setBaseURI($base_uri); + + $views = $this->getItemViews(); + $selected_item = null; + $item_key = 0; + $items = array(); + foreach ($views as $view) { + $list_item = $view->newListItemView(); + + // Assign unique keys to the list items. These keys are purely internal. + $list_item->setKey(sprintf('item(%d)', $item_key++)); + + if ($this->selectedView) { + if ($this->selectedView === $view) { + $selected_item = $list_item; + } + } + + $navigation->addMenuItem($list_item); + $items[] = $list_item; + } + + if (!$views) { + // If the navigation menu has no items, add an empty label item to + // force it to render something. + $empty_item = id(new PHUIListItemView()) + ->setType(PHUIListItemView::TYPE_LABEL); + $navigation->addMenuItem($empty_item); + } + + $highlight_key = $this->getHighlightedItemKey( + $items, + $selected_item); + $navigation->selectFilter($highlight_key); + + return $navigation; + } + + private function getHighlightedItemKey( + array $items, + PHUIListItemView $selected_item = null) { + + assert_instances_of($items, 'PHUIListItemView'); + + $default_key = null; + if ($selected_item) { + $default_key = $selected_item->getKey(); + } + + $engine = $this->getProfileMenuEngine(); + $controller = $engine->getController(); + + // In some rare cases, when like building the "Favorites" menu on a + // 404 page, we may not have a controller. Just accept whatever default + // behavior we'd otherwise end up with. + if (!$controller) { + return $default_key; + } + + $request = $controller->getRequest(); + + // See T12949. If one of the menu items is a link to the same URI that + // the page was accessed with, we want to highlight that item. For example, + // this allows you to add links to a menu that apply filters to a + // workboard. + + $matches = array(); + foreach ($items as $item) { + $href = $item->getHref(); + if ($this->isMatchForRequestURI($request, $href)) { + $matches[] = $item; + } + } + + foreach ($matches as $match) { + if ($match->getKey() === $default_key) { + return $default_key; + } + } + + if ($matches) { + return head($matches)->getKey(); + } + + return $default_key; + } + + private function isMatchForRequestURI(AphrontRequest $request, $item_uri) { + $request_uri = $request->getAbsoluteRequestURI(); + $item_uri = new PhutilURI($item_uri); + + // If the request URI and item URI don't have matching paths, they + // do not match. + if ($request_uri->getPath() !== $item_uri->getPath()) { + return false; + } + + // If the request URI and item URI don't have matching parameters, they + // also do not match. We're specifically trying to let "?filter=X" work + // on Workboards, among other use cases, so this is important. + $request_params = $request_uri->getQueryParamsAsPairList(); + $item_params = $item_uri->getQueryParamsAsPairList(); + if ($request_params !== $item_params) { + return false; + } + + // If the paths and parameters match, the item domain must be: empty; or + // match the request domain; or match the production domain. + + $request_domain = $request_uri->getDomain(); + + $production_uri = PhabricatorEnv::getProductionURI('/'); + $production_domain = id(new PhutilURI($production_uri)) + ->getDomain(); + + $allowed_domains = array( + '', + $request_domain, + $production_domain, + ); + $allowed_domains = array_fuse($allowed_domains); + + $item_domain = $item_uri->getDomain(); + $item_domain = (string)$item_domain; + + if (isset($allowed_domains[$item_domain])) { + return true; + } + + return false; + } + +} diff --git a/src/applications/search/storage/PhabricatorProfileMenuItemConfiguration.php b/src/applications/search/storage/PhabricatorProfileMenuItemConfiguration.php index 4a23655d54..84736d0990 100644 --- a/src/applications/search/storage/PhabricatorProfileMenuItemConfiguration.php +++ b/src/applications/search/storage/PhabricatorProfileMenuItemConfiguration.php @@ -100,10 +100,6 @@ final class PhabricatorProfileMenuItemConfiguration return idx($this->menuItemProperties, $key, $default); } - public function buildNavigationMenuItems() { - return $this->getMenuItem()->buildNavigationMenuItems($this); - } - public function getMenuItemTypeName() { return $this->getMenuItem()->getMenuItemTypeName(); } @@ -124,8 +120,12 @@ final class PhabricatorProfileMenuItemConfiguration return $this->getMenuItem()->shouldEnableForObject($object); } - public function willBuildNavigationItems(array $items) { - return $this->getMenuItem()->willBuildNavigationItems($items); + public function willGetMenuItemViewList(array $items) { + return $this->getMenuItem()->willGetMenuItemViewList($items); + } + + public function getMenuItemViewList() { + return $this->getMenuItem()->getMenuItemViewList($this); } public function validateTransactions(array $map) { From 5192ae47509ecc4e91bc88aa2c05d956024d0138 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 31 Mar 2019 12:07:05 -0700 Subject: [PATCH 242/245] Update all existing ProfileMenuItems for the more-structured API Summary: Depends on D20357. Ref T13275. Now that there's a stronger layer between "stuff in the database" and "stuff on the screen", these subclasses all need to emit intermediate objects instead of raw, HTML-producing view objects. This update is mostly mechanical. Test Plan: - Viewed Home, Favorites, Portals, User Profiles, Project Profiles. - Clicked each item on each menu/profile type. - Added every (I think?) type of item to a menu and clicked them all. - Grepped for obsolete symbols (`newNavigationMenuItems`, `willBuildNavigationItems`). Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13275 Differential Revision: https://secure.phabricator.com/D20358 --- .../PhabricatorDashboardPortalMenuItem.php | 8 +- ...PhabricatorHomeLauncherProfileMenuItem.php | 8 +- .../PhabricatorHomeProfileMenuItem.php | 8 +- ...PhabricatorPeopleBadgesProfileMenuItem.php | 6 +- ...habricatorPeopleCommitsProfileMenuItem.php | 6 +- ...habricatorPeopleDetailsProfileMenuItem.php | 8 +- ...PhabricatorPeopleManageProfileMenuItem.php | 6 +- ...habricatorPeoplePictureProfileMenuItem.php | 42 +-------- ...bricatorPeopleRevisionsProfileMenuItem.php | 6 +- .../PhabricatorPeopleTasksProfileMenuItem.php | 6 +- ...abricatorProjectDetailsProfileMenuItem.php | 8 +- ...habricatorProjectManageProfileMenuItem.php | 8 +- ...abricatorProjectMembersProfileMenuItem.php | 8 +- ...abricatorProjectPictureProfileMenuItem.php | 30 +----- ...habricatorProjectPointsProfileMenuItem.php | 6 +- ...catorProjectSubprojectsProfileMenuItem.php | 8 +- ...ricatorProjectWorkboardProfileMenuItem.php | 8 +- .../engine/PhabricatorProfileMenuItemView.php | 20 ++++ .../PhabricatorApplicationProfileMenuItem.php | 6 +- .../PhabricatorConpherenceProfileMenuItem.php | 25 ++--- .../PhabricatorDashboardProfileMenuItem.php | 94 ++++++++++++++----- .../PhabricatorDividerProfileMenuItem.php | 7 +- .../PhabricatorEditEngineProfileMenuItem.php | 12 +-- .../PhabricatorLabelProfileMenuItem.php | 6 +- .../PhabricatorLinkProfileMenuItem.php | 20 ++-- .../PhabricatorManageProfileMenuItem.php | 8 +- .../PhabricatorMotivatorProfileMenuItem.php | 4 +- .../menuitem/PhabricatorProfileMenuItem.php | 51 +++++++--- .../PhabricatorProjectProfileMenuItem.php | 12 +-- 29 files changed, 233 insertions(+), 212 deletions(-) diff --git a/src/applications/dashboard/menuitem/PhabricatorDashboardPortalMenuItem.php b/src/applications/dashboard/menuitem/PhabricatorDashboardPortalMenuItem.php index da8498891e..6d47a4c219 100644 --- a/src/applications/dashboard/menuitem/PhabricatorDashboardPortalMenuItem.php +++ b/src/applications/dashboard/menuitem/PhabricatorDashboardPortalMenuItem.php @@ -49,7 +49,7 @@ final class PhabricatorDashboardPortalMenuItem ); } - protected function newNavigationMenuItems( + protected function newMenuItemViewList( PhabricatorProfileMenuItemConfiguration $config) { $viewer = $this->getViewer(); @@ -57,12 +57,12 @@ final class PhabricatorDashboardPortalMenuItem return array(); } - $href = $this->getItemViewURI($config); + $uri = $this->getItemViewURI($config); $name = $this->getDisplayName($config); $icon = 'fa-pencil'; - $item = $this->newItem() - ->setHref($href) + $item = $this->newItemView() + ->setURI($uri) ->setName($name) ->setIcon($icon); diff --git a/src/applications/home/menuitem/PhabricatorHomeLauncherProfileMenuItem.php b/src/applications/home/menuitem/PhabricatorHomeLauncherProfileMenuItem.php index e077ac7ba4..dbf1586366 100644 --- a/src/applications/home/menuitem/PhabricatorHomeLauncherProfileMenuItem.php +++ b/src/applications/home/menuitem/PhabricatorHomeLauncherProfileMenuItem.php @@ -49,16 +49,16 @@ final class PhabricatorHomeLauncherProfileMenuItem ); } - protected function newNavigationMenuItems( + protected function newMenuItemViewList( PhabricatorProfileMenuItemConfiguration $config) { $viewer = $this->getViewer(); $name = $this->getDisplayName($config); $icon = 'fa-ellipsis-h'; - $href = '/applications/'; + $uri = '/applications/'; - $item = $this->newItem() - ->setHref($href) + $item = $this->newItemView() + ->setURI($uri) ->setName($name) ->setIcon($icon); diff --git a/src/applications/home/menuitem/PhabricatorHomeProfileMenuItem.php b/src/applications/home/menuitem/PhabricatorHomeProfileMenuItem.php index f4f3cb8733..a002b59da5 100644 --- a/src/applications/home/menuitem/PhabricatorHomeProfileMenuItem.php +++ b/src/applications/home/menuitem/PhabricatorHomeProfileMenuItem.php @@ -52,16 +52,16 @@ final class PhabricatorHomeProfileMenuItem ); } - protected function newNavigationMenuItems( + protected function newMenuItemViewList( PhabricatorProfileMenuItemConfiguration $config) { $viewer = $this->getViewer(); $name = $this->getDisplayName($config); $icon = 'fa-home'; - $href = $this->getItemViewURI($config); + $uri = $this->getItemViewURI($config); - $item = $this->newItem() - ->setHref($href) + $item = $this->newItemView() + ->setURI($uri) ->setName($name) ->setIcon($icon); diff --git a/src/applications/people/menuitem/PhabricatorPeopleBadgesProfileMenuItem.php b/src/applications/people/menuitem/PhabricatorPeopleBadgesProfileMenuItem.php index 0e4da29b61..71f3aa1392 100644 --- a/src/applications/people/menuitem/PhabricatorPeopleBadgesProfileMenuItem.php +++ b/src/applications/people/menuitem/PhabricatorPeopleBadgesProfileMenuItem.php @@ -40,14 +40,14 @@ final class PhabricatorPeopleBadgesProfileMenuItem ); } - protected function newNavigationMenuItems( + protected function newMenuItemViewList( PhabricatorProfileMenuItemConfiguration $config) { $user = $config->getProfileObject(); $id = $user->getID(); - $item = $this->newItem() - ->setHref("/people/badges/{$id}/") + $item = $this->newItemView() + ->setURI("/people/badges/{$id}/") ->setName($this->getDisplayName($config)) ->setIcon('fa-trophy'); diff --git a/src/applications/people/menuitem/PhabricatorPeopleCommitsProfileMenuItem.php b/src/applications/people/menuitem/PhabricatorPeopleCommitsProfileMenuItem.php index f1d8be1828..b6c1c446cc 100644 --- a/src/applications/people/menuitem/PhabricatorPeopleCommitsProfileMenuItem.php +++ b/src/applications/people/menuitem/PhabricatorPeopleCommitsProfileMenuItem.php @@ -40,14 +40,14 @@ final class PhabricatorPeopleCommitsProfileMenuItem ); } - protected function newNavigationMenuItems( + protected function newMenuItemViewList( PhabricatorProfileMenuItemConfiguration $config) { $user = $config->getProfileObject(); $id = $user->getID(); - $item = $this->newItem() - ->setHref("/people/commits/{$id}/") + $item = $this->newItemView() + ->setURI("/people/commits/{$id}/") ->setName($this->getDisplayName($config)) ->setIcon('fa-code'); diff --git a/src/applications/people/menuitem/PhabricatorPeopleDetailsProfileMenuItem.php b/src/applications/people/menuitem/PhabricatorPeopleDetailsProfileMenuItem.php index d7d36b4ed5..61508ff515 100644 --- a/src/applications/people/menuitem/PhabricatorPeopleDetailsProfileMenuItem.php +++ b/src/applications/people/menuitem/PhabricatorPeopleDetailsProfileMenuItem.php @@ -35,16 +35,16 @@ final class PhabricatorPeopleDetailsProfileMenuItem ); } - protected function newNavigationMenuItems( + protected function newMenuItemViewList( PhabricatorProfileMenuItemConfiguration $config) { $user = $config->getProfileObject(); - $href = urisprintf( + $uri = urisprintf( '/p/%s/', $user->getUsername()); - $item = $this->newItem() - ->setHref($href) + $item = $this->newItemView() + ->setURI($uri) ->setName(pht('Profile')) ->setIcon('fa-user'); diff --git a/src/applications/people/menuitem/PhabricatorPeopleManageProfileMenuItem.php b/src/applications/people/menuitem/PhabricatorPeopleManageProfileMenuItem.php index 78d3dca49d..43d2271a79 100644 --- a/src/applications/people/menuitem/PhabricatorPeopleManageProfileMenuItem.php +++ b/src/applications/people/menuitem/PhabricatorPeopleManageProfileMenuItem.php @@ -40,14 +40,14 @@ final class PhabricatorPeopleManageProfileMenuItem ); } - protected function newNavigationMenuItems( + protected function newMenuItemViewList( PhabricatorProfileMenuItemConfiguration $config) { $user = $config->getProfileObject(); $id = $user->getID(); - $item = $this->newItem() - ->setHref("/people/manage/{$id}/") + $item = $this->newItemView() + ->setURI("/people/manage/{$id}/") ->setName($this->getDisplayName($config)) ->setIcon('fa-gears'); diff --git a/src/applications/people/menuitem/PhabricatorPeoplePictureProfileMenuItem.php b/src/applications/people/menuitem/PhabricatorPeoplePictureProfileMenuItem.php index 938b7cf60a..3e3fc62bf0 100644 --- a/src/applications/people/menuitem/PhabricatorPeoplePictureProfileMenuItem.php +++ b/src/applications/people/menuitem/PhabricatorPeoplePictureProfileMenuItem.php @@ -28,52 +28,18 @@ final class PhabricatorPeoplePictureProfileMenuItem return array(); } - protected function newNavigationMenuItems( + protected function newMenuItemViewList( PhabricatorProfileMenuItemConfiguration $config) { $user = $config->getProfileObject(); - require_celerity_resource('people-picture-menu-item-css'); $picture = $user->getProfileImageURI(); $name = $user->getUsername(); - $classes = array(); - $classes[] = 'people-menu-image'; - if ($user->getIsDisabled()) { - $classes[] = 'phui-image-disabled'; - } + $item = $this->newItemView() + ->setDisabled($user->getIsDisabled()); - $href = urisprintf( - '/p/%s/', - $user->getUsername()); - - $photo = phutil_tag( - 'img', - array( - 'src' => $picture, - 'class' => implode(' ', $classes), - )); - - $can_edit = PhabricatorPolicyFilter::hasCapability( - $this->getViewer(), - $user, - PhabricatorPolicyCapability::CAN_EDIT); - - if ($can_edit) { - $id = $user->getID(); - $href = "/people/picture/{$id}/"; - } - - $view = phutil_tag_div('people-menu-image-container', $photo); - $view = phutil_tag( - 'a', - array( - 'href' => $href, - ), - $view); - - $item = $this->newItem() - ->appendChild($view); + $item->newProfileImage($picture); return array( $item, diff --git a/src/applications/people/menuitem/PhabricatorPeopleRevisionsProfileMenuItem.php b/src/applications/people/menuitem/PhabricatorPeopleRevisionsProfileMenuItem.php index 499fc1d7f4..cfa760fcd6 100644 --- a/src/applications/people/menuitem/PhabricatorPeopleRevisionsProfileMenuItem.php +++ b/src/applications/people/menuitem/PhabricatorPeopleRevisionsProfileMenuItem.php @@ -40,14 +40,14 @@ final class PhabricatorPeopleRevisionsProfileMenuItem ); } - protected function newNavigationMenuItems( + protected function newMenuItemViewList( PhabricatorProfileMenuItemConfiguration $config) { $user = $config->getProfileObject(); $id = $user->getID(); - $item = $this->newItem() - ->setHref("/people/revisions/{$id}/") + $item = $this->newItemView() + ->setURI("/people/revisions/{$id}/") ->setName($this->getDisplayName($config)) ->setIcon('fa-gear'); diff --git a/src/applications/people/menuitem/PhabricatorPeopleTasksProfileMenuItem.php b/src/applications/people/menuitem/PhabricatorPeopleTasksProfileMenuItem.php index c2a5036521..5dea58cb29 100644 --- a/src/applications/people/menuitem/PhabricatorPeopleTasksProfileMenuItem.php +++ b/src/applications/people/menuitem/PhabricatorPeopleTasksProfileMenuItem.php @@ -40,14 +40,14 @@ final class PhabricatorPeopleTasksProfileMenuItem ); } - protected function newNavigationMenuItems( + protected function newMenuItemViewList( PhabricatorProfileMenuItemConfiguration $config) { $user = $config->getProfileObject(); $id = $user->getID(); - $item = $this->newItem() - ->setHref("/people/tasks/{$id}/") + $item = $this->newItemView() + ->setURI("/people/tasks/{$id}/") ->setName($this->getDisplayName($config)) ->setIcon('fa-anchor'); diff --git a/src/applications/project/menuitem/PhabricatorProjectDetailsProfileMenuItem.php b/src/applications/project/menuitem/PhabricatorProjectDetailsProfileMenuItem.php index 536165e3ef..a3021e0239 100644 --- a/src/applications/project/menuitem/PhabricatorProjectDetailsProfileMenuItem.php +++ b/src/applications/project/menuitem/PhabricatorProjectDetailsProfileMenuItem.php @@ -49,7 +49,7 @@ final class PhabricatorProjectDetailsProfileMenuItem ); } - protected function newNavigationMenuItems( + protected function newMenuItemViewList( PhabricatorProfileMenuItemConfiguration $config) { $project = $config->getProfileObject(); @@ -58,10 +58,10 @@ final class PhabricatorProjectDetailsProfileMenuItem $name = $project->getName(); $icon = $project->getDisplayIconIcon(); - $href = "/project/profile/{$id}/"; + $uri = "/project/profile/{$id}/"; - $item = $this->newItem() - ->setHref($href) + $item = $this->newItemView() + ->setURI($uri) ->setName($name) ->setIcon($icon); diff --git a/src/applications/project/menuitem/PhabricatorProjectManageProfileMenuItem.php b/src/applications/project/menuitem/PhabricatorProjectManageProfileMenuItem.php index b20bc777ae..9b8a769318 100644 --- a/src/applications/project/menuitem/PhabricatorProjectManageProfileMenuItem.php +++ b/src/applications/project/menuitem/PhabricatorProjectManageProfileMenuItem.php @@ -49,7 +49,7 @@ final class PhabricatorProjectManageProfileMenuItem ); } - protected function newNavigationMenuItems( + protected function newMenuItemViewList( PhabricatorProfileMenuItemConfiguration $config) { $project = $config->getProfileObject(); @@ -58,10 +58,10 @@ final class PhabricatorProjectManageProfileMenuItem $name = $this->getDisplayName($config); $icon = 'fa-gears'; - $href = "/project/manage/{$id}/"; + $uri = "/project/manage/{$id}/"; - $item = $this->newItem() - ->setHref($href) + $item = $this->newItemView() + ->setURI($uri) ->setName($name) ->setIcon($icon); diff --git a/src/applications/project/menuitem/PhabricatorProjectMembersProfileMenuItem.php b/src/applications/project/menuitem/PhabricatorProjectMembersProfileMenuItem.php index b13543312d..11a57d3a5b 100644 --- a/src/applications/project/menuitem/PhabricatorProjectMembersProfileMenuItem.php +++ b/src/applications/project/menuitem/PhabricatorProjectMembersProfileMenuItem.php @@ -39,7 +39,7 @@ final class PhabricatorProjectMembersProfileMenuItem ); } - protected function newNavigationMenuItems( + protected function newMenuItemViewList( PhabricatorProfileMenuItemConfiguration $config) { $project = $config->getProfileObject(); @@ -48,10 +48,10 @@ final class PhabricatorProjectMembersProfileMenuItem $name = $this->getDisplayName($config); $icon = 'fa-group'; - $href = "/project/members/{$id}/"; + $uri = "/project/members/{$id}/"; - $item = $this->newItem() - ->setHref($href) + $item = $this->newItemView() + ->setURI($uri) ->setName($name) ->setIcon($icon); diff --git a/src/applications/project/menuitem/PhabricatorProjectPictureProfileMenuItem.php b/src/applications/project/menuitem/PhabricatorProjectPictureProfileMenuItem.php index b5c203402d..5a58b3af41 100644 --- a/src/applications/project/menuitem/PhabricatorProjectPictureProfileMenuItem.php +++ b/src/applications/project/menuitem/PhabricatorProjectPictureProfileMenuItem.php @@ -32,38 +32,16 @@ final class PhabricatorProjectPictureProfileMenuItem return array(); } - protected function newNavigationMenuItems( + protected function newMenuItemViewList( PhabricatorProfileMenuItemConfiguration $config) { $project = $config->getProfileObject(); - require_celerity_resource('people-picture-menu-item-css'); - $picture = $project->getProfileImageURI(); - $href = $project->getProfileURI(); - $classes = array(); - $classes[] = 'people-menu-image'; - if ($project->isArchived()) { - $classes[] = 'phui-image-disabled'; - } + $item = $this->newItemView() + ->setDisabled($project->isArchived()); - $photo = phutil_tag( - 'img', - array( - 'src' => $picture, - 'class' => implode(' ', $classes), - )); - - $view = phutil_tag_div('people-menu-image-container', $photo); - $view = phutil_tag( - 'a', - array( - 'href' => $href, - ), - $view); - - $item = $this->newItem() - ->appendChild($view); + $item->newProfileImage($picture); return array( $item, diff --git a/src/applications/project/menuitem/PhabricatorProjectPointsProfileMenuItem.php b/src/applications/project/menuitem/PhabricatorProjectPointsProfileMenuItem.php index b64b4bb7a0..d8c7ee82b1 100644 --- a/src/applications/project/menuitem/PhabricatorProjectPointsProfileMenuItem.php +++ b/src/applications/project/menuitem/PhabricatorProjectPointsProfileMenuItem.php @@ -52,7 +52,7 @@ final class PhabricatorProjectPointsProfileMenuItem ); } - protected function newNavigationMenuItems( + protected function newMenuItemViewList( PhabricatorProfileMenuItemConfiguration $config) { $viewer = $this->getViewer(); $project = $config->getProfileObject(); @@ -165,8 +165,8 @@ final class PhabricatorProjectPointsProfileMenuItem ), $bar); - $item = $this->newItem() - ->appendChild($bar); + $item = $this->newItemView() + ->newProgressBar($bar); return array( $item, diff --git a/src/applications/project/menuitem/PhabricatorProjectSubprojectsProfileMenuItem.php b/src/applications/project/menuitem/PhabricatorProjectSubprojectsProfileMenuItem.php index da43e30bb8..b1782e8f1c 100644 --- a/src/applications/project/menuitem/PhabricatorProjectSubprojectsProfileMenuItem.php +++ b/src/applications/project/menuitem/PhabricatorProjectSubprojectsProfileMenuItem.php @@ -47,7 +47,7 @@ final class PhabricatorProjectSubprojectsProfileMenuItem ); } - protected function newNavigationMenuItems( + protected function newMenuItemViewList( PhabricatorProfileMenuItemConfiguration $config) { $project = $config->getProfileObject(); @@ -55,10 +55,10 @@ final class PhabricatorProjectSubprojectsProfileMenuItem $name = $this->getDisplayName($config); $icon = 'fa-sitemap'; - $href = "/project/subprojects/{$id}/"; + $uri = "/project/subprojects/{$id}/"; - $item = $this->newItem() - ->setHref($href) + $item = $this->newItemView() + ->setURI($uri) ->setName($name) ->setIcon($icon); diff --git a/src/applications/project/menuitem/PhabricatorProjectWorkboardProfileMenuItem.php b/src/applications/project/menuitem/PhabricatorProjectWorkboardProfileMenuItem.php index 1485f1ef8a..34152f85e7 100644 --- a/src/applications/project/menuitem/PhabricatorProjectWorkboardProfileMenuItem.php +++ b/src/applications/project/menuitem/PhabricatorProjectWorkboardProfileMenuItem.php @@ -56,16 +56,16 @@ final class PhabricatorProjectWorkboardProfileMenuItem ); } - protected function newNavigationMenuItems( + protected function newMenuItemViewList( PhabricatorProfileMenuItemConfiguration $config) { $project = $config->getProfileObject(); $id = $project->getID(); - $href = $project->getWorkboardURI(); + $uri = $project->getWorkboardURI(); $name = $this->getDisplayName($config); - $item = $this->newItem() - ->setHref($href) + $item = $this->newItemView() + ->setURI($uri) ->setName($name) ->setIcon('fa-columns'); diff --git a/src/applications/search/engine/PhabricatorProfileMenuItemView.php b/src/applications/search/engine/PhabricatorProfileMenuItemView.php index 44e0055a7d..d947afcba6 100644 --- a/src/applications/search/engine/PhabricatorProfileMenuItemView.php +++ b/src/applications/search/engine/PhabricatorProfileMenuItemView.php @@ -7,6 +7,7 @@ final class PhabricatorProfileMenuItemView private $uri; private $name; private $icon; + private $iconImage; private $disabled; private $tooltip; private $actions = array(); @@ -53,6 +54,15 @@ final class PhabricatorProfileMenuItemView return $this->icon; } + public function setIconImage($icon_image) { + $this->iconImage = $icon_image; + return $this; + } + + public function getIconImage() { + return $this->iconImage; + } + public function setDisabled($disabled) { $this->disabled = $disabled; return $this; @@ -146,6 +156,11 @@ final class PhabricatorProfileMenuItemView $view->setIcon($icon); } + $icon_image = $this->getIconImage(); + if ($icon_image) { + $view->setProfileImage($icon_image); + } + if ($this->getDisabled()) { $view->setDisabled(true); } @@ -160,6 +175,11 @@ final class PhabricatorProfileMenuItemView ->addClass('phui-divider'); } + $tooltip = $this->getTooltip(); + if (strlen($tooltip)) { + $view->setTooltip($tooltip); + } + if ($this->images) { require_celerity_resource('people-picture-menu-item-css'); foreach ($this->images as $image_src) { diff --git a/src/applications/search/menuitem/PhabricatorApplicationProfileMenuItem.php b/src/applications/search/menuitem/PhabricatorApplicationProfileMenuItem.php index aa42d56cfb..040b877368 100644 --- a/src/applications/search/menuitem/PhabricatorApplicationProfileMenuItem.php +++ b/src/applications/search/menuitem/PhabricatorApplicationProfileMenuItem.php @@ -68,7 +68,7 @@ final class PhabricatorApplicationProfileMenuItem return head($apps); } - protected function newNavigationMenuItems( + protected function newMenuItemViewList( PhabricatorProfileMenuItemConfiguration $config) { $viewer = $this->getViewer(); $app = $this->getApplication($config); @@ -83,8 +83,8 @@ final class PhabricatorApplicationProfileMenuItem return array(); } - $item = $this->newItem() - ->setHref($app->getApplicationURI()) + $item = $this->newItemView() + ->setURI($app->getApplicationURI()) ->setName($this->getDisplayName($config)) ->setIcon($app->getIcon()); diff --git a/src/applications/search/menuitem/PhabricatorConpherenceProfileMenuItem.php b/src/applications/search/menuitem/PhabricatorConpherenceProfileMenuItem.php index 542c634958..591dee8604 100644 --- a/src/applications/search/menuitem/PhabricatorConpherenceProfileMenuItem.php +++ b/src/applications/search/menuitem/PhabricatorConpherenceProfileMenuItem.php @@ -41,7 +41,7 @@ final class PhabricatorConpherenceProfileMenuItem return $conpherence; } - public function willBuildNavigationItems(array $items) { + public function willGetMenuItemViewList(array $items) { $viewer = $this->getViewer(); $room_phids = array(); foreach ($items as $item) { @@ -98,7 +98,7 @@ final class PhabricatorConpherenceProfileMenuItem return $config->getMenuItemProperty('name'); } - protected function newNavigationMenuItems( + protected function newMenuItemViewList( PhabricatorProfileMenuItemConfiguration $config) { $viewer = $this->getViewer(); $room = $this->getConpherence($config); @@ -114,21 +114,14 @@ final class PhabricatorConpherenceProfileMenuItem $unread_count = $data['unread_count']; } - $count = null; - if ($unread_count) { - $count = phutil_tag( - 'span', - array( - 'class' => 'phui-list-item-count', - ), - $unread_count); - } - - $item = $this->newItem() - ->setHref('/'.$room->getMonogram()) + $item = $this->newItemView() + ->setURI('/'.$room->getMonogram()) ->setName($this->getDisplayName($config)) - ->setIcon('fa-comments') - ->appendChild($count); + ->setIcon('fa-comments'); + + if ($unread_count) { + $item->newCount($unread_count); + } return array( $item, diff --git a/src/applications/search/menuitem/PhabricatorDashboardProfileMenuItem.php b/src/applications/search/menuitem/PhabricatorDashboardProfileMenuItem.php index 7d4f319b61..af913778ee 100644 --- a/src/applications/search/menuitem/PhabricatorDashboardProfileMenuItem.php +++ b/src/applications/search/menuitem/PhabricatorDashboardProfileMenuItem.php @@ -8,6 +8,7 @@ final class PhabricatorDashboardProfileMenuItem const FIELD_DASHBOARD = 'dashboardPHID'; private $dashboard; + private $dashboardHandle; public function getMenuItemTypeIcon() { return 'fa-dashboard'; @@ -26,21 +27,13 @@ final class PhabricatorDashboardProfileMenuItem return true; } - public function attachDashboard($dashboard) { + private function attachDashboard(PhabricatorDashboard $dashboard = null) { $this->dashboard = $dashboard; return $this; } - public function getDashboard() { - $dashboard = $this->dashboard; - - if (!$dashboard) { - return null; - } else if ($dashboard->isArchived()) { - return null; - } - - return $dashboard; + private function getDashboard() { + return $this->dashboard; } public function newPageContent( @@ -56,7 +49,15 @@ final class PhabricatorDashboardProfileMenuItem ->needPanels(true) ->executeOne(); if (!$dashboard) { - return null; + return $this->newEmptyView( + pht('Invalid Dashboard'), + pht('This dashboard is invalid and could not be loaded.')); + } + + if ($dashboard->isArchived()) { + return $this->newEmptyView( + pht('Archived Dashboard'), + pht('This dashboard has been archived.')); } $engine = id(new PhabricatorDashboardRenderingEngine()) @@ -66,7 +67,7 @@ final class PhabricatorDashboardProfileMenuItem return $engine->renderDashboard(); } - public function willBuildNavigationItems(array $items) { + public function willGetMenuItemViewList(array $items) { $viewer = $this->getViewer(); $dashboard_phids = array(); foreach ($items as $item) { @@ -78,11 +79,18 @@ final class PhabricatorDashboardProfileMenuItem ->withPHIDs($dashboard_phids) ->execute(); + $handles = $viewer->loadHandles($dashboard_phids); + $dashboards = mpull($dashboards, null, 'getPHID'); foreach ($items as $item) { $dashboard_phid = $item->getMenuItemProperty('dashboardPHID'); $dashboard = idx($dashboards, $dashboard_phid, null); - $item->getMenuItem()->attachDashboard($dashboard); + + $menu_item = $item->getMenuItem(); + + $menu_item + ->attachDashboard($dashboard) + ->setDashboardHandle($handles[$dashboard_phid]); } } @@ -91,7 +99,15 @@ final class PhabricatorDashboardProfileMenuItem $dashboard = $this->getDashboard(); if (!$dashboard) { - return pht('(Restricted/Invalid Dashboard)'); + if ($this->getDashboardHandle()->getPolicyFiltered()) { + return pht('Restricted Dashboard'); + } else { + return pht('Invalid Dashboard'); + } + } + + if ($dashboard->isArchived()) { + return pht('Archived Dashboard'); } if (strlen($this->getName($config))) { @@ -122,24 +138,43 @@ final class PhabricatorDashboardProfileMenuItem return $config->getMenuItemProperty('name'); } - protected function newNavigationMenuItems( + protected function newMenuItemViewList( PhabricatorProfileMenuItemConfiguration $config) { + $is_disabled = true; + $action_uri = null; + $dashboard = $this->getDashboard(); - if (!$dashboard) { - return array(); + if ($dashboard) { + if ($dashboard->isArchived()) { + $icon = 'fa-ban'; + $name = $this->getDisplayName($config); + } else { + $icon = $dashboard->getIcon(); + $name = $this->getDisplayName($config); + $is_disabled = false; + $action_uri = '/dashboard/arrange/'.$dashboard->getID().'/'; + } + } else { + $icon = 'fa-ban'; + if ($this->getDashboardHandle()->getPolicyFiltered()) { + $name = pht('Restricted Dashboard'); + } else { + $name = pht('Invalid Dashboard'); + } } - $icon = $dashboard->getIcon(); - $name = $this->getDisplayName($config); - $href = $this->getItemViewURI($config); - $action_href = '/dashboard/arrange/'.$dashboard->getID().'/'; + $uri = $this->getItemViewURI($config); - $item = $this->newItem() - ->setHref($href) + $item = $this->newItemView() + ->setURI($uri) ->setName($name) ->setIcon($icon) - ->setActionIcon('fa-pencil', $action_href); + ->setDisabled($is_disabled); + + if ($action_uri) { + $item->newAction($action_uri); + } return array( $item, @@ -191,4 +226,13 @@ final class PhabricatorDashboardProfileMenuItem return $errors; } + private function getDashboardHandle() { + return $this->dashboardHandle; + } + + private function setDashboardHandle(PhabricatorObjectHandle $handle) { + $this->dashboardHandle = $handle; + return $this; + } + } diff --git a/src/applications/search/menuitem/PhabricatorDividerProfileMenuItem.php b/src/applications/search/menuitem/PhabricatorDividerProfileMenuItem.php index e6a6e608e6..8510418fab 100644 --- a/src/applications/search/menuitem/PhabricatorDividerProfileMenuItem.php +++ b/src/applications/search/menuitem/PhabricatorDividerProfileMenuItem.php @@ -34,12 +34,11 @@ final class PhabricatorDividerProfileMenuItem ); } - protected function newNavigationMenuItems( + protected function newMenuItemViewList( PhabricatorProfileMenuItemConfiguration $config) { - $item = $this->newItem() - ->setType(PHUIListItemView::TYPE_DIVIDER) - ->addClass('phui-divider'); + $item = $this->newItemView() + ->setIsDivider(true); return array( $item, diff --git a/src/applications/search/menuitem/PhabricatorEditEngineProfileMenuItem.php b/src/applications/search/menuitem/PhabricatorEditEngineProfileMenuItem.php index 88749d247f..71e3d7e8a5 100644 --- a/src/applications/search/menuitem/PhabricatorEditEngineProfileMenuItem.php +++ b/src/applications/search/menuitem/PhabricatorEditEngineProfileMenuItem.php @@ -34,7 +34,7 @@ final class PhabricatorEditEngineProfileMenuItem return $form; } - public function willBuildNavigationItems(array $items) { + public function willGetMenuItemViewList(array $items) { $viewer = $this->getViewer(); $engines = PhabricatorEditEngine::getAllEditEngines(); $engine_keys = array_keys($engines); @@ -99,7 +99,7 @@ final class PhabricatorEditEngineProfileMenuItem return $config->getMenuItemProperty('name'); } - protected function newNavigationMenuItems( + protected function newMenuItemViewList( PhabricatorProfileMenuItemConfiguration $config) { $form = $this->getForm(); @@ -110,13 +110,13 @@ final class PhabricatorEditEngineProfileMenuItem $icon = $form->getIcon(); $name = $this->getDisplayName($config); - $href = $form->getCreateURI(); - if ($href === null) { + $uri = $form->getCreateURI(); + if ($uri === null) { return array(); } - $item = $this->newItem() - ->setHref($href) + $item = $this->newItemView() + ->setURI($uri) ->setName($name) ->setIcon($icon); diff --git a/src/applications/search/menuitem/PhabricatorLabelProfileMenuItem.php b/src/applications/search/menuitem/PhabricatorLabelProfileMenuItem.php index 605098f216..a152da5898 100644 --- a/src/applications/search/menuitem/PhabricatorLabelProfileMenuItem.php +++ b/src/applications/search/menuitem/PhabricatorLabelProfileMenuItem.php @@ -39,14 +39,14 @@ final class PhabricatorLabelProfileMenuItem return $config->getMenuItemProperty('name'); } - protected function newNavigationMenuItems( + protected function newMenuItemViewList( PhabricatorProfileMenuItemConfiguration $config) { $name = $this->getLabelName($config); - $item = $this->newItem() + $item = $this->newItemView() ->setName($name) - ->setType(PHUIListItemView::TYPE_LABEL); + ->setIsLabel(true); return array( $item, diff --git a/src/applications/search/menuitem/PhabricatorLinkProfileMenuItem.php b/src/applications/search/menuitem/PhabricatorLinkProfileMenuItem.php index 0b6a2f330e..bba3b01060 100644 --- a/src/applications/search/menuitem/PhabricatorLinkProfileMenuItem.php +++ b/src/applications/search/menuitem/PhabricatorLinkProfileMenuItem.php @@ -71,22 +71,14 @@ final class PhabricatorLinkProfileMenuItem return $config->getMenuItemProperty('tooltip'); } - private function isValidLinkURI($uri) { - return PhabricatorEnv::isValidURIForLink($uri); - } - - protected function newNavigationMenuItems( + protected function newMenuItemViewList( PhabricatorProfileMenuItemConfiguration $config) { $icon = $this->getLinkIcon($config); $name = $this->getLinkName($config); - $href = $this->getLinkURI($config); + $uri = $this->getLinkURI($config); $tooltip = $this->getLinkTooltip($config); - if (!$this->isValidLinkURI($href)) { - $href = '#'; - } - $icon_object = id(new PhabricatorProfileMenuItemIconSet()) ->getIcon($icon); if ($icon_object) { @@ -95,12 +87,12 @@ final class PhabricatorLinkProfileMenuItem $icon_class = 'fa-link'; } - $item = $this->newItem() - ->setHref($href) + $item = $this->newItemView() + ->setURI($uri) ->setName($name) ->setIcon($icon_class) ->setTooltip($tooltip) - ->setRel('noreferrer'); + ->setIsExternalLink(true); return array( $item, @@ -142,7 +134,7 @@ final class PhabricatorLinkProfileMenuItem continue; } - if (!$this->isValidLinkURI($new)) { + if (!PhabricatorEnv::isValidURIForLink($new)) { $errors[] = $this->newInvalidError( pht( 'URI "%s" is not a valid link URI. It should be a full, valid '. diff --git a/src/applications/search/menuitem/PhabricatorManageProfileMenuItem.php b/src/applications/search/menuitem/PhabricatorManageProfileMenuItem.php index 2d882ae2c8..89ac4a5633 100644 --- a/src/applications/search/menuitem/PhabricatorManageProfileMenuItem.php +++ b/src/applications/search/menuitem/PhabricatorManageProfileMenuItem.php @@ -49,7 +49,7 @@ final class PhabricatorManageProfileMenuItem ); } - protected function newNavigationMenuItems( + protected function newMenuItemViewList( PhabricatorProfileMenuItemConfiguration $config) { $viewer = $this->getViewer(); @@ -58,13 +58,13 @@ final class PhabricatorManageProfileMenuItem } $engine = $this->getEngine(); - $href = $engine->getItemURI('configure/'); + $uri = $engine->getItemURI('configure/'); $name = $this->getDisplayName($config); $icon = 'fa-pencil'; - $item = $this->newItem() - ->setHref($href) + $item = $this->newItemView() + ->setURI($uri) ->setName($name) ->setIcon($icon); diff --git a/src/applications/search/menuitem/PhabricatorMotivatorProfileMenuItem.php b/src/applications/search/menuitem/PhabricatorMotivatorProfileMenuItem.php index 071979d391..bf4c61f1e5 100644 --- a/src/applications/search/menuitem/PhabricatorMotivatorProfileMenuItem.php +++ b/src/applications/search/menuitem/PhabricatorMotivatorProfileMenuItem.php @@ -50,7 +50,7 @@ final class PhabricatorMotivatorProfileMenuItem ); } - protected function newNavigationMenuItems( + protected function newMenuItemViewList( PhabricatorProfileMenuItemConfiguration $config) { $source = $config->getMenuItemProperty('source'); @@ -66,7 +66,7 @@ final class PhabricatorMotivatorProfileMenuItem $fact_text = $this->selectFact($facts); - $item = $this->newItem() + $item = $this->newItemView() ->setName($fact_name) ->setIcon($fact_icon) ->setTooltip($fact_text) diff --git a/src/applications/search/menuitem/PhabricatorProfileMenuItem.php b/src/applications/search/menuitem/PhabricatorProfileMenuItem.php index 061afc7fad..118815393d 100644 --- a/src/applications/search/menuitem/PhabricatorProfileMenuItem.php +++ b/src/applications/search/menuitem/PhabricatorProfileMenuItem.php @@ -5,15 +5,6 @@ abstract class PhabricatorProfileMenuItem extends Phobject { private $viewer; private $engine; - final public function buildNavigationMenuItems( - PhabricatorProfileMenuItemConfiguration $config) { - return $this->newNavigationMenuItems($config); - } - - abstract protected function newNavigationMenuItems( - PhabricatorProfileMenuItemConfiguration $config); - - public function willBuildNavigationItems(array $items) {} public function getMenuItemTypeIcon() { return null; @@ -76,10 +67,38 @@ abstract class PhabricatorProfileMenuItem extends Phobject { ->execute(); } - protected function newItem() { - return new PHUIListItemView(); + final protected function newItemView() { + return new PhabricatorProfileMenuItemView(); } + public function willGetMenuItemViewList(array $items) {} + + final public function getMenuItemViewList( + PhabricatorProfileMenuItemConfiguration $config) { + $list = $this->newMenuItemViewList($config); + + if (!is_array($list)) { + throw new Exception( + pht( + 'Expected "newMenuItemViewList()" to return a list (in class "%s"), '. + 'but it returned something else ("%s").', + get_class($this), + phutil_describe_type($list))); + } + + assert_instances_of($list, 'PhabricatorProfileMenuItemView'); + + foreach ($list as $view) { + $view->setMenuItemConfiguration($config); + } + + return $list; + } + + abstract protected function newMenuItemViewList( + PhabricatorProfileMenuItemConfiguration $config); + + public function newPageContent( PhabricatorProfileMenuItemConfiguration $config) { return null; @@ -131,4 +150,14 @@ abstract class PhabricatorProfileMenuItem extends Phobject { return $this->newError(pht('Invalid'), $message, $xaction); } + final protected function newEmptyView($title, $message) { + return id(new PHUIInfoView()) + ->setTitle($title) + ->setSeverity(PHUIInfoView::SEVERITY_NODATA) + ->setErrors( + array( + $message, + )); + } + } diff --git a/src/applications/search/menuitem/PhabricatorProjectProfileMenuItem.php b/src/applications/search/menuitem/PhabricatorProjectProfileMenuItem.php index aadabd179a..efb61f06a1 100644 --- a/src/applications/search/menuitem/PhabricatorProjectProfileMenuItem.php +++ b/src/applications/search/menuitem/PhabricatorProjectProfileMenuItem.php @@ -35,7 +35,7 @@ final class PhabricatorProjectProfileMenuItem return $project; } - public function willBuildNavigationItems(array $items) { + public function willGetMenuItemViewList(array $items) { $viewer = $this->getViewer(); $project_phids = array(); foreach ($items as $item) { @@ -90,7 +90,7 @@ final class PhabricatorProjectProfileMenuItem return $config->getMenuItemProperty('name'); } - protected function newNavigationMenuItems( + protected function newMenuItemViewList( PhabricatorProfileMenuItemConfiguration $config) { $project = $this->getProject(); @@ -100,12 +100,12 @@ final class PhabricatorProjectProfileMenuItem $picture = $project->getProfileImageURI(); $name = $this->getDisplayName($config); - $href = $project->getURI(); + $uri = $project->getURI(); - $item = $this->newItem() - ->setHref($href) + $item = $this->newItemView() + ->setURI($uri) ->setName($name) - ->setProfileImage($picture); + ->setIconImage($picture); return array( $item, From dfe47157d322d3500f5eee204672cdb3d2c32c16 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 31 Mar 2019 13:58:30 -0700 Subject: [PATCH 243/245] When picking a default menu item to render, don't pick disabled items Summary: Depends on D20358. Fixes T12871. After refactoring, we can now tell when a "storage" menu item generated only disabled "display" menu items, and not pick any of them as the default rendering. This means that if you're looking at a portal/menu with several dashboards, but can't see some at the top, you'll get the first one you can see. Also clean up a lot of minor issues with less-common states. Test Plan: - Created a portal with two private dashboards and a public dashboard. - Viewed it as another user, saw the default view show the dashboard I can actually see. - Minor fix: Disabled and enabled the hard-coded "Home" item, now worked cleanly with the right menu state. - Minor fix: added a motivator panel. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T12871 Differential Revision: https://secure.phabricator.com/D20359 --- ...icatorDashboardPortalProfileMenuEngine.php | 19 ++++-- .../engine/PhabricatorProfileMenuEngine.php | 67 +++++++++++++------ .../PhabricatorProfileMenuItemViewList.php | 32 +++------ .../PhabricatorMotivatorProfileMenuItem.php | 2 +- ...habricatorProfileMenuItemConfiguration.php | 18 +++++ 5 files changed, 89 insertions(+), 49 deletions(-) diff --git a/src/applications/dashboard/engine/PhabricatorDashboardPortalProfileMenuEngine.php b/src/applications/dashboard/engine/PhabricatorDashboardPortalProfileMenuEngine.php index a5c99a05d0..5da4760cd3 100644 --- a/src/applications/dashboard/engine/PhabricatorDashboardPortalProfileMenuEngine.php +++ b/src/applications/dashboard/engine/PhabricatorDashboardPortalProfileMenuEngine.php @@ -30,10 +30,21 @@ final class PhabricatorDashboardPortalProfileMenuEngine return $items; } - protected function newNoMenuItemsView() { - return $this->newEmptyView( - pht('New Portal'), - pht('Use "Edit Menu" to add menu items to this portal.')); + protected function newNoMenuItemsView(array $items) { + $object = $this->getProfileObject(); + $builtins = $this->getBuiltinProfileItems($object); + + if (count($items) <= count($builtins)) { + return $this->newEmptyView( + pht('New Portal'), + pht('Use "Edit Menu" to add menu items to this portal.')); + } else { + return $this->newEmptyValue( + pht('No Portal Content'), + pht( + 'None of the visible menu items in this portal can render any '. + 'content.')); + } } } diff --git a/src/applications/search/engine/PhabricatorProfileMenuEngine.php b/src/applications/search/engine/PhabricatorProfileMenuEngine.php index 5e89f22b16..fadb929594 100644 --- a/src/applications/search/engine/PhabricatorProfileMenuEngine.php +++ b/src/applications/search/engine/PhabricatorProfileMenuEngine.php @@ -142,10 +142,14 @@ abstract class PhabricatorProfileMenuEngine extends Phobject { $view_list = $this->newProfileMenuItemViewList(); - $selected_item = $this->selectItem( - $view_list, - $item_id, - $is_view); + if ($is_view) { + $selected_item = $this->selectViewItem($view_list, $item_id); + } else { + if (!strlen($item_id)) { + $item_id = self::ITEM_MANAGE; + } + $selected_item = $this->selectEditItem($view_list, $item_id); + } switch ($item_action) { case 'view': @@ -177,8 +181,6 @@ abstract class PhabricatorProfileMenuEngine extends Phobject { $crumbs = $controller->buildApplicationCrumbsForEditEngine(); if (!$is_view) { - $navigation->selectFilter(self::ITEM_MANAGE); - if ($selected_item) { if ($selected_item->getCustomPHID()) { $edit_mode = 'custom'; @@ -222,7 +224,7 @@ abstract class PhabricatorProfileMenuEngine extends Phobject { $crumbs->addTextCrumb($selected_item->getDisplayName()); } else { - $content = $this->newNoMenuItemsView(); + $content = $this->newNoContentView($this->getItems()); } if (!$content) { @@ -320,8 +322,6 @@ abstract class PhabricatorProfileMenuEngine extends Phobject { return $page; } - - private function getItems() { if ($this->items === null) { $this->items = $this->loadItems(self::MODE_COMBINED); @@ -1258,10 +1258,10 @@ abstract class PhabricatorProfileMenuEngine extends Phobject { )); } - protected function newNoMenuItemsView() { + protected function newNoContentView(array $items) { return $this->newEmptyView( - pht('No Menu Items'), - pht('There are no menu items.')); + pht('No Content'), + pht('No visible menu items can render content.')); } @@ -1298,15 +1298,13 @@ abstract class PhabricatorProfileMenuEngine extends Phobject { return $view_list; } - private function selectItem( + private function selectViewItem( PhabricatorProfileMenuItemViewList $view_list, - $item_id, - $want_default) { + $item_id) { // Figure out which view's content we're going to render. In most cases, // the URI tells us. If we don't have an identifier in the URI, we'll - // render the default view instead if this is a workflow that falls back - // to default rendering. + // render the default view instead. $selected_view = null; if (strlen($item_id)) { @@ -1315,11 +1313,9 @@ abstract class PhabricatorProfileMenuEngine extends Phobject { $selected_view = head($item_views); } } else { - if ($want_default) { - $default_views = $view_list->getDefaultViews(); - if ($default_views) { - $selected_view = head($default_views); - } + $default_views = $view_list->getDefaultViews(); + if ($default_views) { + $selected_view = head($default_views); } } @@ -1333,5 +1329,32 @@ abstract class PhabricatorProfileMenuEngine extends Phobject { return $selected_item; } + private function selectEditItem( + PhabricatorProfileMenuItemViewList $view_list, + $item_id) { + + // First, try to select a visible item using the normal view selection + // pathway. If this works, it also highlights the menu properly. + + if ($item_id) { + $selected_item = $this->selectViewItem($view_list, $item_id); + if ($selected_item) { + return $selected_item; + } + } + + // If we didn't find an item in the view list, we may be enabling an item + // which is currently disabled or editing an item which is not generating + // any actual items in the menu. + + foreach ($this->getItems() as $item) { + if ($item->matchesIdentifier($item_id)) { + return $item; + } + } + + return null; + } + } diff --git a/src/applications/search/engine/PhabricatorProfileMenuItemViewList.php b/src/applications/search/engine/PhabricatorProfileMenuItemViewList.php index 4890657b4e..e5a3fd3d38 100644 --- a/src/applications/search/engine/PhabricatorProfileMenuItemViewList.php +++ b/src/applications/search/engine/PhabricatorProfileMenuItemViewList.php @@ -62,34 +62,15 @@ final class PhabricatorProfileMenuItemViewList public function getViewsWithItemIdentifier($identifier) { $views = $this->getItemViews(); - if (!strlen($identifier)) { - return array(); - } - - if (ctype_digit($identifier)) { - $identifier_int = (int)$identifier; - } else { - $identifier_int = null; - } - - $identifier_str = (string)$identifier; - $results = array(); foreach ($views as $view) { $config = $view->getMenuItemConfiguration(); - if ($identifier_int !== null) { - $config_id = (int)$config->getID(); - if ($config_id === $identifier_int) { - $results[] = $view; - continue; - } - } - - if ($config->getBuiltinKey() === $identifier_str) { - $results[] = $view; + if (!$config->matchesIdentifier($identifier)) { continue; } + + $results[] = $view; } return $results; @@ -112,6 +93,13 @@ final class PhabricatorProfileMenuItemViewList } } + // Remove disabled views. + foreach ($views as $key => $view) { + if ($view->getDisabled()) { + unset($views[$key]); + } + } + // If this engine supports pinning items and we have candidate views from a // valid pinned item, they are the default views. if ($can_pin) { diff --git a/src/applications/search/menuitem/PhabricatorMotivatorProfileMenuItem.php b/src/applications/search/menuitem/PhabricatorMotivatorProfileMenuItem.php index bf4c61f1e5..d85b02b3a2 100644 --- a/src/applications/search/menuitem/PhabricatorMotivatorProfileMenuItem.php +++ b/src/applications/search/menuitem/PhabricatorMotivatorProfileMenuItem.php @@ -70,7 +70,7 @@ final class PhabricatorMotivatorProfileMenuItem ->setName($fact_name) ->setIcon($fact_icon) ->setTooltip($fact_text) - ->setHref('#'); + ->setURI('#'); return array( $item, diff --git a/src/applications/search/storage/PhabricatorProfileMenuItemConfiguration.php b/src/applications/search/storage/PhabricatorProfileMenuItemConfiguration.php index 84736d0990..8ca948cb07 100644 --- a/src/applications/search/storage/PhabricatorProfileMenuItemConfiguration.php +++ b/src/applications/search/storage/PhabricatorProfileMenuItemConfiguration.php @@ -237,6 +237,24 @@ final class PhabricatorProfileMenuItemConfiguration return $this->isTailItem; } + public function matchesIdentifier($identifier) { + if (!strlen($identifier)) { + return false; + } + + if (ctype_digit($identifier)) { + if ((int)$this->getID() === (int)$identifier) { + return true; + } + } + + if ((string)$this->getBuiltinKey() === (string)$identifier) { + return true; + } + + return false; + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ From c9d3fb2ac5b64684d8eba7a892a4c0496486d7cc Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 31 Mar 2019 14:26:05 -0700 Subject: [PATCH 244/245] Fix the incorrect link target for "Create Revision" as a Menu Item Summary: Depends on D20359. Fixes T12098. When you add a new "Form" item and pick "Create Revision", you currently get a bad link. This is because Differential is kind of special and the form isn't usable directly, even though Differential does use EditEngine. Allow EditEngine to specify a different create URI, then specify the web UI paste-a-diff flow to fix this. Test Plan: - Added "Create Revision" to a portal, clicked it, was sensibly put on the diff flow. - Grepped for `getCreateURI()`, the only other real use case is to render the "Create X" dropdowns in the upper right. - Clicked one of those, still worked great. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T12098 Differential Revision: https://secure.phabricator.com/D20360 --- .../editor/DifferentialRevisionEditEngine.php | 4 ++++ .../editengine/PhabricatorEditEngine.php | 12 ++++++++++++ .../storage/PhabricatorEditEngineConfiguration.php | 9 +-------- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/applications/differential/editor/DifferentialRevisionEditEngine.php b/src/applications/differential/editor/DifferentialRevisionEditEngine.php index 9c399036d5..32c82629bb 100644 --- a/src/applications/differential/editor/DifferentialRevisionEditEngine.php +++ b/src/applications/differential/editor/DifferentialRevisionEditEngine.php @@ -63,6 +63,10 @@ final class DifferentialRevisionEditEngine return $object->getMonogram(); } + public function getCreateURI($form_key) { + return '/differential/diff/create/'; + } + protected function getObjectCreateShortText() { return pht('Create Revision'); } diff --git a/src/applications/transactions/editengine/PhabricatorEditEngine.php b/src/applications/transactions/editengine/PhabricatorEditEngine.php index 82af45dca8..0986247454 100644 --- a/src/applications/transactions/editengine/PhabricatorEditEngine.php +++ b/src/applications/transactions/editengine/PhabricatorEditEngine.php @@ -566,6 +566,18 @@ abstract class PhabricatorEditEngine return $this->getObjectViewURI($object); } + /** + * @task uri + */ + public function getCreateURI($form_key) { + try { + $create_uri = $this->getEditURI(null, "form/{$form_key}/"); + } catch (Exception $ex) { + $create_uri = null; + } + + return $create_uri; + } /** * @task uri diff --git a/src/applications/transactions/storage/PhabricatorEditEngineConfiguration.php b/src/applications/transactions/storage/PhabricatorEditEngineConfiguration.php index 6c9f3a50a6..b1919a0ee0 100644 --- a/src/applications/transactions/storage/PhabricatorEditEngineConfiguration.php +++ b/src/applications/transactions/storage/PhabricatorEditEngineConfiguration.php @@ -227,14 +227,7 @@ final class PhabricatorEditEngineConfiguration public function getCreateURI() { $form_key = $this->getIdentifier(); $engine = $this->getEngine(); - - try { - $create_uri = $engine->getEditURI(null, "form/{$form_key}/"); - } catch (Exception $ex) { - $create_uri = null; - } - - return $create_uri; + return $engine->getCreateURI($form_key); } public function getIdentifier() { From 18732a0d2ff82614f5d1181981b7a0d1f6b1b3a3 Mon Sep 17 00:00:00 2001 From: epriestley Date: Sun, 31 Mar 2019 14:48:23 -0700 Subject: [PATCH 245/245] Make Portals reachable without knowing the URI Summary: Depends on D20360. Ref T13275. This makes the "Dashboards" application start on a Drydock-like console page where you pick portals, dashboards, or panels. Probably the "Dashboards" application should either be renamed to "IntelliknowledgePro" or Portals should be split off into a separate application eventually, but let's see how things go like this for now, since restructuring probably breaks some URIs at least a little bit so I'd like more confidence that we're headed in the right direction before we do it. Test Plan: - Visited Dashboards via typeahead, got options for Dashboards/Portals/Panels. - Visited Portals pages, got simplified crumbs. Reviewers: amckinley Reviewed By: amckinley Maniphest Tasks: T13275 Differential Revision: https://secure.phabricator.com/D20361 --- src/__phutil_library_map__.php | 2 + .../PhabricatorDashboardApplication.php | 5 ++ .../PhabricatorDashboardConsoleController.php | 72 +++++++++++++++++++ .../PhabricatorDashboardListController.php | 3 - .../PhabricatorDashboardPortalController.php | 8 ++- src/view/page/PhabricatorStandardPageView.php | 5 +- 6 files changed, 89 insertions(+), 6 deletions(-) create mode 100644 src/applications/dashboard/controller/PhabricatorDashboardConsoleController.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 617939d393..6c810f6d32 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2907,6 +2907,7 @@ phutil_register_library_map(array( 'PhabricatorDashboardApplication' => 'applications/dashboard/application/PhabricatorDashboardApplication.php', 'PhabricatorDashboardArchiveController' => 'applications/dashboard/controller/PhabricatorDashboardArchiveController.php', 'PhabricatorDashboardArrangeController' => 'applications/dashboard/controller/PhabricatorDashboardArrangeController.php', + 'PhabricatorDashboardConsoleController' => 'applications/dashboard/controller/PhabricatorDashboardConsoleController.php', 'PhabricatorDashboardController' => 'applications/dashboard/controller/PhabricatorDashboardController.php', 'PhabricatorDashboardDAO' => 'applications/dashboard/storage/PhabricatorDashboardDAO.php', 'PhabricatorDashboardDashboardHasPanelEdgeType' => 'applications/dashboard/edge/PhabricatorDashboardDashboardHasPanelEdgeType.php', @@ -8867,6 +8868,7 @@ phutil_register_library_map(array( 'PhabricatorDashboardApplication' => 'PhabricatorApplication', 'PhabricatorDashboardArchiveController' => 'PhabricatorDashboardController', 'PhabricatorDashboardArrangeController' => 'PhabricatorDashboardProfileController', + 'PhabricatorDashboardConsoleController' => 'PhabricatorDashboardController', 'PhabricatorDashboardController' => 'PhabricatorController', 'PhabricatorDashboardDAO' => 'PhabricatorLiskDAO', 'PhabricatorDashboardDashboardHasPanelEdgeType' => 'PhabricatorEdgeType', diff --git a/src/applications/dashboard/application/PhabricatorDashboardApplication.php b/src/applications/dashboard/application/PhabricatorDashboardApplication.php index b44ecc3a38..081367effa 100644 --- a/src/applications/dashboard/application/PhabricatorDashboardApplication.php +++ b/src/applications/dashboard/application/PhabricatorDashboardApplication.php @@ -10,6 +10,10 @@ final class PhabricatorDashboardApplication extends PhabricatorApplication { return '/dashboard/'; } + public function getTypeaheadURI() { + return '/dashboard/console/'; + } + public function getShortDescription() { return pht('Create Custom Pages'); } @@ -42,6 +46,7 @@ final class PhabricatorDashboardApplication extends PhabricatorApplication { 'create/' => 'PhabricatorDashboardEditController', 'edit/(?:(?P\d+)/)?' => 'PhabricatorDashboardEditController', 'install/(?:(?P\d+)/)?' => 'PhabricatorDashboardInstallController', + 'console/' => 'PhabricatorDashboardConsoleController', 'addpanel/(?P\d+)/' => 'PhabricatorDashboardAddPanelController', 'movepanel/(?P\d+)/' => 'PhabricatorDashboardMovePanelController', 'removepanel/(?P\d+)/' diff --git a/src/applications/dashboard/controller/PhabricatorDashboardConsoleController.php b/src/applications/dashboard/controller/PhabricatorDashboardConsoleController.php new file mode 100644 index 0000000000..1caceec1e4 --- /dev/null +++ b/src/applications/dashboard/controller/PhabricatorDashboardConsoleController.php @@ -0,0 +1,72 @@ +getViewer(); + + $menu = id(new PHUIObjectItemListView()) + ->setUser($viewer) + ->setBig(true); + + $menu->addItem( + id(new PHUIObjectItemView()) + ->setHeader(pht('Portals')) + ->setImageIcon('fa-compass') + ->setHref('/portal/') + ->setClickable(true) + ->addAttribute( + pht( + 'Portals are collections of dashboards, links, and other '. + 'resources that can provide a high-level overview of a '. + 'project.'))); + + $menu->addItem( + id(new PHUIObjectItemView()) + ->setHeader(pht('Dashboards')) + ->setImageIcon('fa-dashboard') + ->setHref($this->getApplicationURI('/')) + ->setClickable(true) + ->addAttribute( + pht( + 'Dashboards organize panels, creating a cohesive page for '. + 'analysis or action.'))); + + $menu->addItem( + id(new PHUIObjectItemView()) + ->setHeader(pht('Panels')) + ->setImageIcon('fa-line-chart') + ->setHref($this->getApplicationURI('panel/')) + ->setClickable(true) + ->addAttribute( + pht( + 'Panels show queries, charts, and other information to provide '. + 'insight on a particular topic.'))); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb(pht('Console')); + $crumbs->setBorder(true); + + $title = pht('Dashboard Console'); + + $box = id(new PHUIObjectBoxView()) + ->setHeaderText($title) + ->setBackground(PHUIObjectBoxView::WHITE_CONFIG) + ->setObjectList($menu); + + $view = id(new PHUITwoColumnView()) + ->setFixed(true) + ->setFooter($box); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->appendChild($view); + } + +} diff --git a/src/applications/dashboard/controller/PhabricatorDashboardListController.php b/src/applications/dashboard/controller/PhabricatorDashboardListController.php index 60e6d83ab4..93a6af9fbf 100644 --- a/src/applications/dashboard/controller/PhabricatorDashboardListController.php +++ b/src/applications/dashboard/controller/PhabricatorDashboardListController.php @@ -28,9 +28,6 @@ final class PhabricatorDashboardListController ->setViewer($user) ->addNavigationItems($nav->getMenu()); - $nav->addLabel(pht('Panels')); - $nav->addFilter('panel/', pht('Manage Panels')); - $nav->selectFilter(null); return $nav; diff --git a/src/applications/dashboard/controller/portal/PhabricatorDashboardPortalController.php b/src/applications/dashboard/controller/portal/PhabricatorDashboardPortalController.php index a109622e4d..98ac9a7846 100644 --- a/src/applications/dashboard/controller/portal/PhabricatorDashboardPortalController.php +++ b/src/applications/dashboard/controller/portal/PhabricatorDashboardPortalController.php @@ -4,9 +4,13 @@ abstract class PhabricatorDashboardPortalController extends PhabricatorDashboardController { protected function buildApplicationCrumbs() { - $crumbs = parent::buildApplicationCrumbs(); + $crumbs = new PHUICrumbsView(); - $crumbs->addTextCrumb(pht('Portals'), '/portal/'); + $crumbs->addCrumb( + id(new PHUICrumbView()) + ->setHref('/portal/') + ->setName(pht('Portals')) + ->setIcon('fa-compass')); return $crumbs; } diff --git a/src/view/page/PhabricatorStandardPageView.php b/src/view/page/PhabricatorStandardPageView.php index 5498a1fb34..1933f223e5 100644 --- a/src/view/page/PhabricatorStandardPageView.php +++ b/src/view/page/PhabricatorStandardPageView.php @@ -872,7 +872,10 @@ final class PhabricatorStandardPageView extends PhabricatorBarePageView // mobile navigation menu from. $application_menu = $controller->buildApplicationMenu(); if (!$application_menu) { - $application_menu = $this->getNavigation()->getMenu(); + $navigation = $this->getNavigation(); + if ($navigation) { + $application_menu = $navigation->getMenu(); + } } $this->applicationMenu = $application_menu;