From de6349dd67afe3a190607753aa3b3229878a6e6e Mon Sep 17 00:00:00 2001 From: Aviv Eyal Date: Mon, 27 Jun 2016 20:29:47 +0000 Subject: [PATCH 01/35] Revision substate CLOSED_FROM_ACCEPTED Summary: Ref T9838. Add a Properties field to Revision, and update a `wasAcceptedBeforeClose` when closing a revision. Test Plan: A quick run through the obvious steps (Close with commit/manually, with or w/o accept) and calling `differential.query` shows the `wasAcceptedBeforeClose` property was setup correctly. Pushing closed + accepted passes the relevant herald, which was my immediate issue; Pushing un-accepted is blocked. Test the "commit" rule (Different from "pre-commit") by hacking the DB and running the "has accepted revision" rule in a test-console. Reviewers: epriestley, #blessed_reviewers Reviewed By: epriestley, #blessed_reviewers Subscribers: Korvin, epriestley Maniphest Tasks: T9838 Differential Revision: https://secure.phabricator.com/D15085 --- .../20160201.revision.properties.1.sql | 2 ++ .../20160201.revision.properties.2.sql | 2 ++ .../DifferentialQueryConduitAPIMethod.php | 1 + .../editor/DifferentialTransactionEditor.php | 6 +++++ .../storage/DifferentialRevision.php | 17 ++++++++++++ ...usionCommitRevisionAcceptedHeraldField.php | 27 ++++++++++++++++--- ...mmitContentRevisionAcceptedHeraldField.php | 15 ++++++++--- 7 files changed, 63 insertions(+), 7 deletions(-) create mode 100644 resources/sql/autopatches/20160201.revision.properties.1.sql create mode 100644 resources/sql/autopatches/20160201.revision.properties.2.sql diff --git a/resources/sql/autopatches/20160201.revision.properties.1.sql b/resources/sql/autopatches/20160201.revision.properties.1.sql new file mode 100644 index 0000000000..2ab60b2ce9 --- /dev/null +++ b/resources/sql/autopatches/20160201.revision.properties.1.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_differential.differential_revision +ADD properties LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20160201.revision.properties.2.sql b/resources/sql/autopatches/20160201.revision.properties.2.sql new file mode 100644 index 0000000000..41d3234abe --- /dev/null +++ b/resources/sql/autopatches/20160201.revision.properties.2.sql @@ -0,0 +1,2 @@ +UPDATE {$NAMESPACE}_differential.differential_revision +SET properties = '{}' WHERE properties = ''; diff --git a/src/applications/differential/conduit/DifferentialQueryConduitAPIMethod.php b/src/applications/differential/conduit/DifferentialQueryConduitAPIMethod.php index 2bf43d295e..1d4bcc2a8d 100644 --- a/src/applications/differential/conduit/DifferentialQueryConduitAPIMethod.php +++ b/src/applications/differential/conduit/DifferentialQueryConduitAPIMethod.php @@ -206,6 +206,7 @@ final class DifferentialQueryConduitAPIMethod 'statusName' => ArcanistDifferentialRevisionStatus::getNameForRevisionStatus( $revision->getStatus()), + 'properties' => $revision->getProperties(), 'branch' => $diff->getBranch(), 'summary' => $revision->getSummary(), 'testPlan' => $revision->getTestPlan(), diff --git a/src/applications/differential/editor/DifferentialTransactionEditor.php b/src/applications/differential/editor/DifferentialTransactionEditor.php index a87c12c926..bf2df0acee 100644 --- a/src/applications/differential/editor/DifferentialTransactionEditor.php +++ b/src/applications/differential/editor/DifferentialTransactionEditor.php @@ -182,6 +182,7 @@ final class DifferentialTransactionEditor $status_revision = ArcanistDifferentialRevisionStatus::NEEDS_REVISION; $status_plan = ArcanistDifferentialRevisionStatus::CHANGES_PLANNED; $status_abandoned = ArcanistDifferentialRevisionStatus::ABANDONED; + $status_accepted = ArcanistDifferentialRevisionStatus::ACCEPTED; switch ($xaction->getTransactionType()) { case DifferentialTransaction::TYPE_INLINE: @@ -233,7 +234,12 @@ final class DifferentialTransactionEditor $object->setStatus($status_review); return; case DifferentialAction::ACTION_CLOSE: + $old_status = $object->getStatus(); $object->setStatus(ArcanistDifferentialRevisionStatus::CLOSED); + $was_accepted = ($old_status == $status_accepted); + $object->setProperty( + DifferentialRevision::PROPERTY_CLOSED_FROM_ACCEPTED, + $was_accepted); return; case DifferentialAction::ACTION_CLAIM: $object->setAuthorPHID($this->getActingAsPHID()); diff --git a/src/applications/differential/storage/DifferentialRevision.php b/src/applications/differential/storage/DifferentialRevision.php index c2915d92c6..e0c8effada 100644 --- a/src/applications/differential/storage/DifferentialRevision.php +++ b/src/applications/differential/storage/DifferentialRevision.php @@ -35,6 +35,7 @@ final class DifferentialRevision extends DifferentialDAO protected $repositoryPHID; protected $viewPolicy = PhabricatorPolicies::POLICY_USER; protected $editPolicy = PhabricatorPolicies::POLICY_USER; + protected $properties = array(); private $relationships = self::ATTACHABLE; private $commits = self::ATTACHABLE; @@ -53,6 +54,8 @@ final class DifferentialRevision extends DifferentialDAO const RELATION_REVIEWER = 'revw'; const RELATION_SUBSCRIBED = 'subd'; + const PROPERTY_CLOSED_FROM_ACCEPTED = 'wasAcceptedBeforeClose'; + public static function initializeNewRevision(PhabricatorUser $actor) { $app = id(new PhabricatorApplicationQuery()) ->setViewer($actor) @@ -76,6 +79,7 @@ final class DifferentialRevision extends DifferentialDAO self::CONFIG_SERIALIZATION => array( 'attached' => self::SERIALIZATION_JSON, 'unsubscribed' => self::SERIALIZATION_JSON, + 'properties' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'title' => 'text255', @@ -114,6 +118,19 @@ final class DifferentialRevision extends DifferentialDAO ) + parent::getConfiguration(); } + public function setProperty($key, $value) { + $this->properties[$key] = $value; + return $this; + } + + public function getProperty($key, $default = null) { + return idx($this->properties, $key, $default); + } + + public function hasRevisionProperty($key) { + return array_key_exists($key, $this->properties); + } + public function getMonogram() { $id = $this->getID(); return "D{$id}"; diff --git a/src/applications/diffusion/herald/DiffusionCommitRevisionAcceptedHeraldField.php b/src/applications/diffusion/herald/DiffusionCommitRevisionAcceptedHeraldField.php index 10d9c29c92..810b58f2a1 100644 --- a/src/applications/diffusion/herald/DiffusionCommitRevisionAcceptedHeraldField.php +++ b/src/applications/diffusion/herald/DiffusionCommitRevisionAcceptedHeraldField.php @@ -19,10 +19,31 @@ final class DiffusionCommitRevisionAcceptedHeraldField return null; } + $status = $revision->getStatus(); + + switch ($status) { + case ArcanistDifferentialRevisionStatus::ACCEPTED: + return $revision->getPHID(); + case ArcanistDifferentialRevisionStatus::CLOSED: + if ($revision->hasRevisionProperty( + DifferentialRevision::PROPERTY_CLOSED_FROM_ACCEPTED)) { + + if ($revision->getProperty( + DifferentialRevision::PROPERTY_CLOSED_FROM_ACCEPTED)) { + return $revision->getPHID(); + } else { + return null; + } + } else { + // continue on to old-style precommitRevisionStatus + break; + } + default: + return null; + } + $data = $object->getCommitData(); - $status = $data->getCommitDetail( - 'precommitRevisionStatus', - $revision->getStatus()); + $status = $data->getCommitDetail('precommitRevisionStatus'); switch ($status) { case ArcanistDifferentialRevisionStatus::ACCEPTED: diff --git a/src/applications/diffusion/herald/DiffusionPreCommitContentRevisionAcceptedHeraldField.php b/src/applications/diffusion/herald/DiffusionPreCommitContentRevisionAcceptedHeraldField.php index 864cc043af..e19878e627 100644 --- a/src/applications/diffusion/herald/DiffusionPreCommitContentRevisionAcceptedHeraldField.php +++ b/src/applications/diffusion/herald/DiffusionPreCommitContentRevisionAcceptedHeraldField.php @@ -20,12 +20,19 @@ final class DiffusionPreCommitContentRevisionAcceptedHeraldField return null; } - $status_accepted = ArcanistDifferentialRevisionStatus::ACCEPTED; - if ($revision->getStatus() != $status_accepted) { - return null; + switch ($revision->getStatus()) { + case ArcanistDifferentialRevisionStatus::ACCEPTED: + return $revision->getPHID(); + case ArcanistDifferentialRevisionStatus::CLOSED: + if ($revision->getProperty( + DifferentialRevision::PROPERTY_CLOSED_FROM_ACCEPTED)) { + + return $revision->getPHID(); + } + break; } - return $revision->getPHID(); + return null; } protected function getHeraldFieldStandardType() { From 92fc628b042a1d7103461a978cc078826cf5f0b4 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Mon, 27 Jun 2016 17:07:03 -0700 Subject: [PATCH 02/35] Better destruction of PhameBlog, BadgesBadge Summary: Allows proper destruction of Badge Awards and Phame Posts. Test Plan: bin/remove destroy PHID... Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D16182 --- .../badges/storage/PhabricatorBadgesBadge.php | 2 +- src/applications/phame/storage/PhameBlog.php | 8 +++++--- src/applications/phame/storage/PhamePost.php | 4 ++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/applications/badges/storage/PhabricatorBadgesBadge.php b/src/applications/badges/storage/PhabricatorBadgesBadge.php index 91ed3cf34d..be83c8404f 100644 --- a/src/applications/badges/storage/PhabricatorBadgesBadge.php +++ b/src/applications/badges/storage/PhabricatorBadgesBadge.php @@ -186,7 +186,7 @@ final class PhabricatorBadgesBadge extends PhabricatorBadgesDAO ->execute(); foreach ($awards as $award) { - $engine->destroyObjectPermanently($award); + $engine->destroyObject($award); } $this->openTransaction(); diff --git a/src/applications/phame/storage/PhameBlog.php b/src/applications/phame/storage/PhameBlog.php index 6d9c51071a..23ab671552 100644 --- a/src/applications/phame/storage/PhameBlog.php +++ b/src/applications/phame/storage/PhameBlog.php @@ -322,10 +322,12 @@ final class PhameBlog extends PhameDAO $this->openTransaction(); - $posts = id(new PhamePost()) - ->loadAllWhere('blogPHID = %s', $this->getPHID()); + $posts = id(new PhamePostQuery()) + ->setViewer($engine->getViewer()) + ->withBlogPHIDs(array($this->getPHID())) + ->execute(); foreach ($posts as $post) { - $post->delete(); + $engine->destroyObject($post); } $this->delete(); diff --git a/src/applications/phame/storage/PhamePost.php b/src/applications/phame/storage/PhamePost.php index 2d8fc887c5..8078c5055c 100644 --- a/src/applications/phame/storage/PhamePost.php +++ b/src/applications/phame/storage/PhamePost.php @@ -258,15 +258,15 @@ final class PhamePost extends PhameDAO return $timeline; } + /* -( PhabricatorDestructibleInterface )----------------------------------- */ + public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { $this->openTransaction(); - $this->delete(); - $this->saveTransaction(); } From da6c96dfff91c9bbb7ae290e1dbeea2cf57a00e1 Mon Sep 17 00:00:00 2001 From: Austin Seipp Date: Tue, 28 Jun 2016 03:55:31 +0000 Subject: [PATCH 03/35] Fix a busted sentence in the File Encryption documentation Summary: love to wordsmith Test Plan: read it Reviewers: #blessed_reviewers, epriestley Reviewed By: #blessed_reviewers, epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D16183 --- src/docs/user/configuration/configuring_encryption.diviner | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/docs/user/configuration/configuring_encryption.diviner b/src/docs/user/configuration/configuring_encryption.diviner index 1e62071509..dbd0e76314 100644 --- a/src/docs/user/configuration/configuring_encryption.diviner +++ b/src/docs/user/configuration/configuring_encryption.diviner @@ -72,7 +72,7 @@ Each key should have these properties: - `default`: //Optional bool.// Optionally, mark exactly one key as the default key to enable encryption of newly uploaded file data. -The key material is sensitive an an attacker who learns it can decrypt data +The key material is sensitive and an attacker who learns it can decrypt data from the storage engine. From 84ce863116581071f3ce14d3080ba2295a5fa166 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 28 Jun 2016 14:02:06 -0700 Subject: [PATCH 04/35] Select padding for inlines more surgically Summary: Fixes T11225. The primary issue here is that this rule is bleeding down too far. It appears that it's only intended to put space around the inline as a whole. Spacing still isn't //perfect// since a few other rules are bleeding, but it feels reasonable now instead of being clearly broken. Test Plan: - Added "background: red;" to figure out what was being affected. - Before: {F1704081} - After: {F1704084} Reviewers: chad Reviewed By: chad Maniphest Tasks: T11225 Differential Revision: https://secure.phabricator.com/D16184 --- resources/celerity/map.php | 12 ++++++------ .../css/application/differential/changeset-view.css | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index a3aadfe9d7..3b35b902ed 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -10,7 +10,7 @@ return array( 'core.pkg.css' => 'b6b40555', 'core.pkg.js' => 'f2139810', 'darkconsole.pkg.js' => 'e7393ebb', - 'differential.pkg.css' => 'b3eea3f5', + 'differential.pkg.css' => '3e81ae60', 'differential.pkg.js' => '01a010d6', 'diffusion.pkg.css' => '91c5d3a6', 'diffusion.pkg.js' => '3a9a8bfa', @@ -57,7 +57,7 @@ return array( 'rsrc/css/application/dashboard/dashboard.css' => 'bc6f2127', 'rsrc/css/application/diff/inline-comment-summary.css' => '51efda3a', 'rsrc/css/application/differential/add-comment.css' => 'c47f8c40', - 'rsrc/css/application/differential/changeset-view.css' => 'ccfbc869', + 'rsrc/css/application/differential/changeset-view.css' => '37792573', 'rsrc/css/application/differential/core.css' => '5b7b8ff4', 'rsrc/css/application/differential/phui-inline-comment.css' => '5953c28e', 'rsrc/css/application/differential/revision-comment.css' => '14b8565a', @@ -555,7 +555,7 @@ return array( 'conpherence-update-css' => 'faf6be09', 'conpherence-widget-pane-css' => '775eaaba', 'd3' => 'a11a5ff2', - 'differential-changeset-view-css' => 'ccfbc869', + 'differential-changeset-view-css' => '37792573', 'differential-core-view-css' => '5b7b8ff4', 'differential-inline-comment-editor' => '64a5550f', 'differential-revision-add-comment-css' => 'c47f8c40', @@ -1136,6 +1136,9 @@ return array( 'javelin-dom', 'javelin-workflow', ), + 37792573 => array( + 'phui-inline-comment-view-css', + ), '3ab51e2c' => array( 'javelin-behavior', 'javelin-behavior-device', @@ -1931,9 +1934,6 @@ return array( 'javelin-util', 'phabricator-notification-css', ), - 'ccfbc869' => array( - 'phui-inline-comment-view-css', - ), 'cf86d16a' => array( 'javelin-behavior', 'javelin-dom', diff --git a/webroot/rsrc/css/application/differential/changeset-view.css b/webroot/rsrc/css/application/differential/changeset-view.css index 2a72357d4d..2cedb39db5 100644 --- a/webroot/rsrc/css/application/differential/changeset-view.css +++ b/webroot/rsrc/css/application/differential/changeset-view.css @@ -67,7 +67,7 @@ padding: 1px 4px; } -.device .differential-diff .inline td { +.device .differential-diff .inline > td { padding: 4px; } @@ -308,7 +308,7 @@ td.cov-I { box-sizing: border-box; } -.differential-diff .inline td { +.differential-diff .inline > td { padding: 8px 12px; } From ec8581ab629e970aef58d7b7fe44e1d1dd3fe2c8 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 28 Jun 2016 14:22:31 -0700 Subject: [PATCH 05/35] Clean up redirect URIs for "Temporary Tokens" and "API Tokens" settings panels Summary: Fixes T11223. I missed a few of these; most of them kept working anyway because we have redirects in place, but make them a bit more modern/not-hard-coded. Test Plan: - Generated and revoked API tokens for myself. - Generated and revoked API tokens for bots. - Revoked temporary tokens for myself. - Clicked the link to the API tokens panel from the Conduit console. - Clicked all the cancel buttons in all the dialogs, too. In all cases, everything now points at the correct URIs. Previously, some things pointed at the wrong URIs (mostly dealing with stuff for bots). Reviewers: chad Reviewed By: chad Maniphest Tasks: T11223 Differential Revision: https://secure.phabricator.com/D16185 --- .../PhabricatorAuthRevokeTokenController.php | 5 ++++- .../controller/PhabricatorConduitController.php | 9 ++++++++- .../PhabricatorConduitTokenEditController.php | 9 ++++----- .../PhabricatorConduitTokenTerminateController.php | 10 +++++++--- 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/applications/auth/controller/PhabricatorAuthRevokeTokenController.php b/src/applications/auth/controller/PhabricatorAuthRevokeTokenController.php index 6d516916eb..8e063ba565 100644 --- a/src/applications/auth/controller/PhabricatorAuthRevokeTokenController.php +++ b/src/applications/auth/controller/PhabricatorAuthRevokeTokenController.php @@ -24,7 +24,10 @@ final class PhabricatorAuthRevokeTokenController } } - $panel_uri = '/settings/panel/tokens/'; + $panel_uri = id(new PhabricatorTokensSettingsPanel()) + ->setViewer($viewer) + ->setUser($viewer) + ->getPanelURI(); if (!$tokens) { return $this->newDialog() diff --git a/src/applications/conduit/controller/PhabricatorConduitController.php b/src/applications/conduit/controller/PhabricatorConduitController.php index 000d01f888..b29c05f2af 100644 --- a/src/applications/conduit/controller/PhabricatorConduitController.php +++ b/src/applications/conduit/controller/PhabricatorConduitController.php @@ -25,6 +25,8 @@ abstract class PhabricatorConduitController extends PhabricatorController { } protected function renderExampleBox(ConduitAPIMethod $method, $params) { + $viewer = $this->getViewer(); + $arc_example = id(new PHUIPropertyListView()) ->addRawContent($this->renderExample($method, 'arc', $params)); @@ -34,10 +36,15 @@ abstract class PhabricatorConduitController extends PhabricatorController { $php_example = id(new PHUIPropertyListView()) ->addRawContent($this->renderExample($method, 'php', $params)); + $panel_uri = id(new PhabricatorConduitTokensSettingsPanel()) + ->setViewer($viewer) + ->setUser($viewer) + ->getPanelURI(); + $panel_link = phutil_tag( 'a', array( - 'href' => '/settings/panel/apitokens/', + 'href' => $panel_uri, ), pht('Conduit API Tokens')); diff --git a/src/applications/conduit/controller/PhabricatorConduitTokenEditController.php b/src/applications/conduit/controller/PhabricatorConduitTokenEditController.php index 503456d010..7550f92210 100644 --- a/src/applications/conduit/controller/PhabricatorConduitTokenEditController.php +++ b/src/applications/conduit/controller/PhabricatorConduitTokenEditController.php @@ -49,11 +49,10 @@ final class PhabricatorConduitTokenEditController $submit_button = pht('Generate Token'); } - if ($viewer->getPHID() == $object->getPHID()) { - $panel_uri = '/settings/panel/apitokens/'; - } else { - $panel_uri = '/settings/'.$object->getID().'/panel/apitokens/'; - } + $panel_uri = id(new PhabricatorConduitTokensSettingsPanel()) + ->setViewer($viewer) + ->setUser($object) + ->getPanelURI(); id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( $viewer, diff --git a/src/applications/conduit/controller/PhabricatorConduitTokenTerminateController.php b/src/applications/conduit/controller/PhabricatorConduitTokenTerminateController.php index 466089ebeb..9f1ffd2964 100644 --- a/src/applications/conduit/controller/PhabricatorConduitTokenTerminateController.php +++ b/src/applications/conduit/controller/PhabricatorConduitTokenTerminateController.php @@ -31,7 +31,6 @@ final class PhabricatorConduitTokenTerminateController 'Really terminate this token? Any system using this token '. 'will no longer be able to make API requests.'); $submit_button = pht('Terminate Token'); - $panel_uri = '/settings/panel/apitokens/'; } else { $tokens = id(new PhabricatorConduitTokenQuery()) ->setViewer($viewer) @@ -51,7 +50,6 @@ final class PhabricatorConduitTokenTerminateController $submit_button = pht('Terminate Tokens'); } - $panel_uri = '/settings/panel/apitokens/'; if ($object_phid != $viewer->getPHID()) { $object = id(new PhabricatorObjectQuery()) ->setViewer($viewer) @@ -60,9 +58,15 @@ final class PhabricatorConduitTokenTerminateController if (!$object) { return new Aphront404Response(); } - $panel_uri = '/settings/'.$object->getID().'/panel/apitokens/'; + } else { + $object = $viewer; } + $panel_uri = id(new PhabricatorConduitTokensSettingsPanel()) + ->setViewer($viewer) + ->setUser($object) + ->getPanelURI(); + id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( $viewer, $request, From bf1352c0e47b9e8f736b70b7e285d2c7f32447a6 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 28 Jun 2016 14:32:59 -0700 Subject: [PATCH 06/35] Document the "---" rule in Remarkup Summary: Fixes T11228. Test Plan: {F1704113} Reviewers: chad Reviewed By: chad Maniphest Tasks: T11228 Differential Revision: https://secure.phabricator.com/D16186 --- src/docs/user/userguide/remarkup.diviner | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/docs/user/userguide/remarkup.diviner b/src/docs/user/userguide/remarkup.diviner index 8c6c7e1bec..33d8a136df 100644 --- a/src/docs/user/userguide/remarkup.diviner +++ b/src/docs/user/userguide/remarkup.diviner @@ -307,6 +307,28 @@ the rendered result. For example, this callout uses `(NOTE)`: (NOTE) Dr. Egon Spengler is the best resource for additional proton pack questions. + +Dividers +======== + +You can divide sections by putting three or more dashes on a line by +themselves. This creates a divider or horizontal rule similar to an `
` +tag, like this one: + +--- + +The dashes need to appear on their own line and be separated from other +content. For example, like this: + +``` +This section will be visually separated. + +--- + +On an entirely different topic, ... +``` + + = Linking URIs = URIs are automatically linked: http://phabricator.org/ From 2b76785a13df36419959e64395c0494385c2e98f Mon Sep 17 00:00:00 2001 From: Chad Little Date: Tue, 28 Jun 2016 20:17:04 -0700 Subject: [PATCH 07/35] Better 404 for Phame Summary: "Fixes" fatals on phacility.com blog. If post_id is either `0` or any other integer not in the blog system, show a normal Phame 404 with crumbs. Test Plan: http://local.blog.phacility.com/post/0/last_published/, http://local.blog.phacility.com/post/999999/last_published/ Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D16192 --- src/applications/phame/controller/PhameLiveController.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/applications/phame/controller/PhameLiveController.php b/src/applications/phame/controller/PhameLiveController.php index ba2b65d9f8..1287a33f35 100644 --- a/src/applications/phame/controller/PhameLiveController.php +++ b/src/applications/phame/controller/PhameLiveController.php @@ -87,7 +87,7 @@ abstract class PhameLiveController extends PhameController { $this->isExternal = $is_external; $this->isLive = $is_live; - if ($post_id) { + if (strlen($post_id)) { $post_query = id(new PhamePostQuery()) ->setViewer($viewer) ->withIDs(array($post_id)); @@ -104,6 +104,8 @@ abstract class PhameLiveController extends PhameController { $post = $post_query->executeOne(); if (!$post) { + // Not a valid Post + $this->blog = $blog; return new Aphront404Response(); } From 9827cc16227d786f1469376a2f6563c353a1359a Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 29 Jun 2016 11:15:31 -0700 Subject: [PATCH 08/35] Provide a missing timeout on the non-cluster connection pathway Summary: Ref T11232. The cluster connection pathway specifies a timeout when connecting, but this connection pathway does not. (I'm not sure if we just never did or if it got lost at some point.) Soon, T11044 will obsolete this and unify the database connection pathways, but that's a more complicated change. I'm not sure if this will fix T11232, but it can't hurt. Test Plan: Put a `throw` on timeout specifications. Before the change: did not hit it in non-cluster configurations. After the change: hit it. Reviewers: chad Reviewed By: chad Maniphest Tasks: T11232 Differential Revision: https://secure.phabricator.com/D16194 --- src/infrastructure/storage/lisk/PhabricatorLiskDAO.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php b/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php index 1a51467089..99f1bcb864 100644 --- a/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php +++ b/src/infrastructure/storage/lisk/PhabricatorLiskDAO.php @@ -99,6 +99,7 @@ abstract class PhabricatorLiskDAO extends LiskDAO { 'port' => $conf->getPort(), 'database' => $database, 'retries' => 3, + 'timeout' => 10, ), )); } From 25cc90d63247c96c0563600f559fb536f8c5dde8 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 28 Jun 2016 16:05:05 -0700 Subject: [PATCH 09/35] Inch toward using ApplicationSearch to power related objects Summary: Ref T4788. Fixes T9232. This moves the "search for stuff to attach to this object" flow away from hard-coding and legacy constants and toward something more modular and flexible. It also adds an "Edit Commits..." action to Maniphest, resolving T9232. The behavior of the search for commits isn't great right now, but it will improve once these use real ApplicationSearch. Test Plan: Edited a tasks' related commits, mocks, tasks, etc. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4788, T9232 Differential Revision: https://secure.phabricator.com/D16189 --- src/__phutil_library_map__.php | 12 +++ .../ManiphestTaskHasCommitRelationship.php | 11 +-- .../ManiphestTaskHasMockRelationship.php | 4 + .../ManiphestTaskHasParentRelationship.php | 4 + .../ManiphestTaskHasRevisionRelationship.php | 4 + .../ManiphestTaskHasSubtaskRelationship.php | 4 + .../phame/search/PhamePostFulltextEngine.php | 1 - .../search/PholioMockFulltextEngine.php | 8 ++ .../search/DiffusionCommitFulltextEngine.php | 8 ++ .../PhabricatorSearchApplication.php | 2 + .../PhabricatorSearchBaseController.php | 31 +++++++ ...habricatorSearchRelationshipController.php | 38 ++------ ...atorSearchRelationshipSourceController.php | 89 +++++++++++++++++++ ...DifferentialRevisionRelationshipSource.php | 12 +++ .../DiffusionCommitRelationshipSource.php | 12 +++ .../ManiphestTaskRelationshipSource.php | 12 +++ .../PhabricatorObjectRelationship.php | 19 ++++ .../PhabricatorObjectRelationshipSource.php | 7 ++ .../PholioMockRelationshipSource.php | 12 +++ 19 files changed, 249 insertions(+), 41 deletions(-) create mode 100644 src/applications/search/controller/PhabricatorSearchRelationshipSourceController.php create mode 100644 src/applications/search/relationship/DifferentialRevisionRelationshipSource.php create mode 100644 src/applications/search/relationship/DiffusionCommitRelationshipSource.php create mode 100644 src/applications/search/relationship/ManiphestTaskRelationshipSource.php create mode 100644 src/applications/search/relationship/PhabricatorObjectRelationshipSource.php create mode 100644 src/applications/search/relationship/PholioMockRelationshipSource.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 68fab974c6..4fead1dc05 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -536,6 +536,7 @@ phutil_register_library_map(array( 'DifferentialRevisionPackageHeraldField' => 'applications/differential/herald/DifferentialRevisionPackageHeraldField.php', 'DifferentialRevisionPackageOwnerHeraldField' => 'applications/differential/herald/DifferentialRevisionPackageOwnerHeraldField.php', 'DifferentialRevisionQuery' => 'applications/differential/query/DifferentialRevisionQuery.php', + 'DifferentialRevisionRelationshipSource' => 'applications/search/relationship/DifferentialRevisionRelationshipSource.php', 'DifferentialRevisionRepositoryHeraldField' => 'applications/differential/herald/DifferentialRevisionRepositoryHeraldField.php', 'DifferentialRevisionRepositoryProjectsHeraldField' => 'applications/differential/herald/DifferentialRevisionRepositoryProjectsHeraldField.php', 'DifferentialRevisionRequiredActionResultBucket' => 'applications/differential/query/DifferentialRevisionRequiredActionResultBucket.php', @@ -614,6 +615,7 @@ phutil_register_library_map(array( 'DiffusionCommitParentsQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionCommitParentsQueryConduitAPIMethod.php', 'DiffusionCommitQuery' => 'applications/diffusion/query/DiffusionCommitQuery.php', 'DiffusionCommitRef' => 'applications/diffusion/data/DiffusionCommitRef.php', + 'DiffusionCommitRelationshipSource' => 'applications/search/relationship/DiffusionCommitRelationshipSource.php', 'DiffusionCommitRemarkupRule' => 'applications/diffusion/remarkup/DiffusionCommitRemarkupRule.php', 'DiffusionCommitRemarkupRuleTestCase' => 'applications/diffusion/remarkup/__tests__/DiffusionCommitRemarkupRuleTestCase.php', 'DiffusionCommitRepositoryHeraldField' => 'applications/diffusion/herald/DiffusionCommitRepositoryHeraldField.php', @@ -1442,6 +1444,7 @@ phutil_register_library_map(array( 'ManiphestTaskPriorityHeraldField' => 'applications/maniphest/herald/ManiphestTaskPriorityHeraldField.php', 'ManiphestTaskQuery' => 'applications/maniphest/query/ManiphestTaskQuery.php', 'ManiphestTaskRelationship' => 'applications/maniphest/relationship/ManiphestTaskRelationship.php', + 'ManiphestTaskRelationshipSource' => 'applications/search/relationship/ManiphestTaskRelationshipSource.php', 'ManiphestTaskResultListView' => 'applications/maniphest/view/ManiphestTaskResultListView.php', 'ManiphestTaskSearchEngine' => 'applications/maniphest/query/ManiphestTaskSearchEngine.php', 'ManiphestTaskStatus' => 'applications/maniphest/constants/ManiphestTaskStatus.php', @@ -2867,6 +2870,7 @@ phutil_register_library_map(array( 'PhabricatorObjectQuery' => 'applications/phid/query/PhabricatorObjectQuery.php', 'PhabricatorObjectRelationship' => 'applications/search/relationship/PhabricatorObjectRelationship.php', 'PhabricatorObjectRelationshipList' => 'applications/search/relationship/PhabricatorObjectRelationshipList.php', + 'PhabricatorObjectRelationshipSource' => 'applications/search/relationship/PhabricatorObjectRelationshipSource.php', 'PhabricatorObjectRemarkupRule' => 'infrastructure/markup/rule/PhabricatorObjectRemarkupRule.php', 'PhabricatorObjectSelectorDialog' => 'view/control/PhabricatorObjectSelectorDialog.php', 'PhabricatorOffsetPagedQuery' => 'infrastructure/query/PhabricatorOffsetPagedQuery.php', @@ -3397,6 +3401,7 @@ phutil_register_library_map(array( 'PhabricatorSearchOrderField' => 'applications/search/field/PhabricatorSearchOrderField.php', 'PhabricatorSearchRelationship' => 'applications/search/constants/PhabricatorSearchRelationship.php', 'PhabricatorSearchRelationshipController' => 'applications/search/controller/PhabricatorSearchRelationshipController.php', + 'PhabricatorSearchRelationshipSourceController' => 'applications/search/controller/PhabricatorSearchRelationshipSourceController.php', 'PhabricatorSearchResultBucket' => 'applications/search/buckets/PhabricatorSearchResultBucket.php', 'PhabricatorSearchResultBucketGroup' => 'applications/search/buckets/PhabricatorSearchResultBucketGroup.php', 'PhabricatorSearchResultView' => 'applications/search/view/PhabricatorSearchResultView.php', @@ -3870,6 +3875,7 @@ phutil_register_library_map(array( 'PholioMockNameHeraldField' => 'applications/pholio/herald/PholioMockNameHeraldField.php', 'PholioMockPHIDType' => 'applications/pholio/phid/PholioMockPHIDType.php', 'PholioMockQuery' => 'applications/pholio/query/PholioMockQuery.php', + 'PholioMockRelationshipSource' => 'applications/search/relationship/PholioMockRelationshipSource.php', 'PholioMockSearchEngine' => 'applications/pholio/query/PholioMockSearchEngine.php', 'PholioMockThumbGridView' => 'applications/pholio/view/PholioMockThumbGridView.php', 'PholioMockViewController' => 'applications/pholio/controller/PholioMockViewController.php', @@ -4887,6 +4893,7 @@ phutil_register_library_map(array( 'DifferentialRevisionPackageHeraldField' => 'DifferentialRevisionHeraldField', 'DifferentialRevisionPackageOwnerHeraldField' => 'DifferentialRevisionHeraldField', 'DifferentialRevisionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'DifferentialRevisionRelationshipSource' => 'PhabricatorObjectRelationshipSource', 'DifferentialRevisionRepositoryHeraldField' => 'DifferentialRevisionHeraldField', 'DifferentialRevisionRepositoryProjectsHeraldField' => 'DifferentialRevisionHeraldField', 'DifferentialRevisionRequiredActionResultBucket' => 'DifferentialRevisionResultBucket', @@ -4965,6 +4972,7 @@ phutil_register_library_map(array( 'DiffusionCommitParentsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionCommitQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'DiffusionCommitRef' => 'Phobject', + 'DiffusionCommitRelationshipSource' => 'PhabricatorObjectRelationshipSource', 'DiffusionCommitRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'DiffusionCommitRemarkupRuleTestCase' => 'PhabricatorTestCase', 'DiffusionCommitRepositoryHeraldField' => 'DiffusionCommitHeraldField', @@ -5935,6 +5943,7 @@ phutil_register_library_map(array( 'ManiphestTaskPriorityHeraldField' => 'ManiphestTaskHeraldField', 'ManiphestTaskQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'ManiphestTaskRelationship' => 'PhabricatorObjectRelationship', + 'ManiphestTaskRelationshipSource' => 'PhabricatorObjectRelationshipSource', 'ManiphestTaskResultListView' => 'ManiphestView', 'ManiphestTaskSearchEngine' => 'PhabricatorApplicationSearchEngine', 'ManiphestTaskStatus' => 'ManiphestConstants', @@ -7561,6 +7570,7 @@ phutil_register_library_map(array( 'PhabricatorObjectQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorObjectRelationship' => 'Phobject', 'PhabricatorObjectRelationshipList' => 'Phobject', + 'PhabricatorObjectRelationshipSource' => 'Phobject', 'PhabricatorObjectRemarkupRule' => 'PhutilRemarkupRule', 'PhabricatorObjectSelectorDialog' => 'Phobject', 'PhabricatorOffsetPagedQuery' => 'PhabricatorQuery', @@ -8219,6 +8229,7 @@ phutil_register_library_map(array( 'PhabricatorSearchOrderField' => 'PhabricatorSearchField', 'PhabricatorSearchRelationship' => 'Phobject', 'PhabricatorSearchRelationshipController' => 'PhabricatorSearchBaseController', + 'PhabricatorSearchRelationshipSourceController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchResultBucket' => 'Phobject', 'PhabricatorSearchResultBucketGroup' => 'Phobject', 'PhabricatorSearchResultView' => 'AphrontView', @@ -8790,6 +8801,7 @@ phutil_register_library_map(array( 'PholioMockNameHeraldField' => 'PholioMockHeraldField', 'PholioMockPHIDType' => 'PhabricatorPHIDType', 'PholioMockQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PholioMockRelationshipSource' => 'PhabricatorObjectRelationshipSource', 'PholioMockSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PholioMockThumbGridView' => 'AphrontView', 'PholioMockViewController' => 'PholioController', diff --git a/src/applications/maniphest/relationship/ManiphestTaskHasCommitRelationship.php b/src/applications/maniphest/relationship/ManiphestTaskHasCommitRelationship.php index 4fb35e46c9..e21f15a11c 100644 --- a/src/applications/maniphest/relationship/ManiphestTaskHasCommitRelationship.php +++ b/src/applications/maniphest/relationship/ManiphestTaskHasCommitRelationship.php @@ -17,13 +17,6 @@ final class ManiphestTaskHasCommitRelationship return 'fa-code'; } - public function shouldAppearInActionMenu() { - // TODO: For now, the default search for commits is not very good, so - // it is hard to find objects to link to. Until that works better, just - // hide this item. - return false; - } - public function canRelateObjects($src, $dst) { return ($dst instanceof PhabricatorRepositoryCommit); } @@ -40,4 +33,8 @@ final class ManiphestTaskHasCommitRelationship return pht('Save Related Commits'); } + protected function newRelationshipSource() { + return new DiffusionCommitRelationshipSource(); + } + } diff --git a/src/applications/maniphest/relationship/ManiphestTaskHasMockRelationship.php b/src/applications/maniphest/relationship/ManiphestTaskHasMockRelationship.php index 4425d1c3e9..830da4ba01 100644 --- a/src/applications/maniphest/relationship/ManiphestTaskHasMockRelationship.php +++ b/src/applications/maniphest/relationship/ManiphestTaskHasMockRelationship.php @@ -33,4 +33,8 @@ final class ManiphestTaskHasMockRelationship return pht('Save Related Mocks'); } + protected function newRelationshipSource() { + return new PholioMockRelationshipSource(); + } + } diff --git a/src/applications/maniphest/relationship/ManiphestTaskHasParentRelationship.php b/src/applications/maniphest/relationship/ManiphestTaskHasParentRelationship.php index 8d7d3a539f..6210e3c4d4 100644 --- a/src/applications/maniphest/relationship/ManiphestTaskHasParentRelationship.php +++ b/src/applications/maniphest/relationship/ManiphestTaskHasParentRelationship.php @@ -37,4 +37,8 @@ final class ManiphestTaskHasParentRelationship return pht('Save Parent Tasks'); } + protected function newRelationshipSource() { + return new ManiphestTaskRelationshipSource(); + } + } diff --git a/src/applications/maniphest/relationship/ManiphestTaskHasRevisionRelationship.php b/src/applications/maniphest/relationship/ManiphestTaskHasRevisionRelationship.php index 4530940191..f1c8796f50 100644 --- a/src/applications/maniphest/relationship/ManiphestTaskHasRevisionRelationship.php +++ b/src/applications/maniphest/relationship/ManiphestTaskHasRevisionRelationship.php @@ -33,4 +33,8 @@ final class ManiphestTaskHasRevisionRelationship return pht('Save Related Revisions'); } + protected function newRelationshipSource() { + return new DifferentialRevisionRelationshipSource(); + } + } diff --git a/src/applications/maniphest/relationship/ManiphestTaskHasSubtaskRelationship.php b/src/applications/maniphest/relationship/ManiphestTaskHasSubtaskRelationship.php index 9a1f696f6b..335ac03d42 100644 --- a/src/applications/maniphest/relationship/ManiphestTaskHasSubtaskRelationship.php +++ b/src/applications/maniphest/relationship/ManiphestTaskHasSubtaskRelationship.php @@ -37,4 +37,8 @@ final class ManiphestTaskHasSubtaskRelationship return pht('Save Subtasks'); } + protected function newRelationshipSource() { + return new ManiphestTaskRelationshipSource(); + } + } diff --git a/src/applications/phame/search/PhamePostFulltextEngine.php b/src/applications/phame/search/PhamePostFulltextEngine.php index 27a97ae4ba..4b41d5de7c 100644 --- a/src/applications/phame/search/PhamePostFulltextEngine.php +++ b/src/applications/phame/search/PhamePostFulltextEngine.php @@ -28,7 +28,6 @@ final class PhamePostFulltextEngine $post->getPHID(), PhabricatorPhamePostPHIDType::TYPECONST, PhabricatorTime::getNow()); - } } diff --git a/src/applications/pholio/search/PholioMockFulltextEngine.php b/src/applications/pholio/search/PholioMockFulltextEngine.php index 9a162168db..6326cdfb77 100644 --- a/src/applications/pholio/search/PholioMockFulltextEngine.php +++ b/src/applications/pholio/search/PholioMockFulltextEngine.php @@ -20,6 +20,14 @@ final class PholioMockFulltextEngine $mock->getAuthorPHID(), PhabricatorPeopleUserPHIDType::TYPECONST, $mock->getDateCreated()); + + $document->addRelationship( + $mock->isClosed() + ? PhabricatorSearchRelationship::RELATIONSHIP_CLOSED + : PhabricatorSearchRelationship::RELATIONSHIP_OPEN, + $mock->getPHID(), + PholioMockPHIDType::TYPECONST, + PhabricatorTime::getNow()); } } diff --git a/src/applications/repository/search/DiffusionCommitFulltextEngine.php b/src/applications/repository/search/DiffusionCommitFulltextEngine.php index dd87a8bda5..640cf03f43 100644 --- a/src/applications/repository/search/DiffusionCommitFulltextEngine.php +++ b/src/applications/repository/search/DiffusionCommitFulltextEngine.php @@ -47,5 +47,13 @@ final class DiffusionCommitFulltextEngine $repository->getPHID(), PhabricatorRepositoryRepositoryPHIDType::TYPECONST, $date_created); + + $document->addRelationship( + $commit->isUnreachable() + ? PhabricatorSearchRelationship::RELATIONSHIP_CLOSED + : PhabricatorSearchRelationship::RELATIONSHIP_OPEN, + $commit->getPHID(), + PhabricatorRepositoryCommitPHIDType::TYPECONST, + PhabricatorTime::getNow()); } } diff --git a/src/applications/search/application/PhabricatorSearchApplication.php b/src/applications/search/application/PhabricatorSearchApplication.php index 36ad59764c..894e14fe2f 100644 --- a/src/applications/search/application/PhabricatorSearchApplication.php +++ b/src/applications/search/application/PhabricatorSearchApplication.php @@ -43,6 +43,8 @@ final class PhabricatorSearchApplication extends PhabricatorApplication { 'order/(?P[^/]+)/' => 'PhabricatorSearchOrderController', 'rel/(?P[^/]+)/(?P[^/]+)/' => 'PhabricatorSearchRelationshipController', + 'source/(?P[^/]+)/(?P[^/]+)/' + => 'PhabricatorSearchRelationshipSourceController', ), ); } diff --git a/src/applications/search/controller/PhabricatorSearchBaseController.php b/src/applications/search/controller/PhabricatorSearchBaseController.php index 85ac1eb285..189048145e 100644 --- a/src/applications/search/controller/PhabricatorSearchBaseController.php +++ b/src/applications/search/controller/PhabricatorSearchBaseController.php @@ -2,10 +2,41 @@ abstract class PhabricatorSearchBaseController extends PhabricatorController { + const ACTION_ATTACH = 'attach'; const ACTION_MERGE = 'merge'; const ACTION_DEPENDENCIES = 'dependencies'; const ACTION_BLOCKS = 'blocks'; const ACTION_EDGE = 'edge'; + protected function loadRelationshipObject() { + $request = $this->getRequest(); + $viewer = $this->getViewer(); + + $phid = $request->getURIData('sourcePHID'); + + return id(new PhabricatorObjectQuery()) + ->setViewer($viewer) + ->withPHIDs(array($phid)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + } + + protected function loadRelationship($object) { + $request = $this->getRequest(); + $viewer = $this->getViewer(); + + $relationship_key = $request->getURIData('relationshipKey'); + + $list = PhabricatorObjectRelationshipList::newForObject( + $viewer, + $object); + + return $list->getRelationship($relationship_key); + } + } diff --git a/src/applications/search/controller/PhabricatorSearchRelationshipController.php b/src/applications/search/controller/PhabricatorSearchRelationshipController.php index 6af121d1ba..2b2fac9bb7 100644 --- a/src/applications/search/controller/PhabricatorSearchRelationshipController.php +++ b/src/applications/search/controller/PhabricatorSearchRelationshipController.php @@ -6,26 +6,12 @@ final class PhabricatorSearchRelationshipController public function handleRequest(AphrontRequest $request) { $viewer = $this->getViewer(); - $phid = $request->getURIData('sourcePHID'); - $object = id(new PhabricatorObjectQuery()) - ->setViewer($viewer) - ->withPHIDs(array($phid)) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); + $object = $this->loadRelationshipObject(); if (!$object) { return new Aphront404Response(); } - $list = PhabricatorObjectRelationshipList::newForObject( - $viewer, - $object); - - $relationship_key = $request->getURIData('relationshipKey'); - $relationship = $list->getRelationship($relationship_key); + $relationship = $this->loadRelationship($object); if (!$relationship) { return new Aphront404Response(); } @@ -135,21 +121,7 @@ final class PhabricatorSearchRelationshipController $dialog_button = $relationship->getDialogButtonText(); $dialog_instructions = $relationship->getDialogInstructionsText(); - // TODO: Remove this, this is just legacy support. - $legacy_kinds = array( - ManiphestTaskHasCommitEdgeType::EDGECONST => 'CMIT', - ManiphestTaskHasMockEdgeType::EDGECONST => 'MOCK', - ManiphestTaskHasRevisionEdgeType::EDGECONST => 'DREV', - ManiphestTaskDependsOnTaskEdgeType::EDGECONST => 'TASK', - ManiphestTaskDependedOnByTaskEdgeType::EDGECONST => 'TASK', - ); - - $edge_type = $relationship->getEdgeConstant(); - $legacy_kind = idx($legacy_kinds, $edge_type); - if (!$legacy_kind) { - throw new Exception( - pht('Only specific legacy relationships are supported!')); - } + $source_uri = $relationship->getSourceURI($object); return id(new PhabricatorObjectSelectorDialog()) ->setUser($viewer) @@ -157,9 +129,9 @@ final class PhabricatorSearchRelationshipController ->setHandles($handles) ->setFilters($filters) ->setSelectedFilter('created') - ->setExcluded($phid) + ->setExcluded($src_phid) ->setCancelURI($done_uri) - ->setSearchURI("/search/select/{$legacy_kind}/edge/") + ->setSearchURI($source_uri) ->setTitle($dialog_title) ->setHeader($dialog_header) ->setButtonText($dialog_button) diff --git a/src/applications/search/controller/PhabricatorSearchRelationshipSourceController.php b/src/applications/search/controller/PhabricatorSearchRelationshipSourceController.php new file mode 100644 index 0000000000..e783eae7f4 --- /dev/null +++ b/src/applications/search/controller/PhabricatorSearchRelationshipSourceController.php @@ -0,0 +1,89 @@ +getViewer(); + + $object = $this->loadRelationshipObject(); + if (!$object) { + return new Aphront404Response(); + } + + $relationship = $this->loadRelationship($object); + if (!$relationship) { + return new Aphront404Response(); + } + + $source = $relationship->newSource(); + $query = new PhabricatorSavedQuery(); + + $action = $request->getURIData('action'); + $query_str = $request->getStr('query'); + $filter = $request->getStr('filter'); + + $query->setEngineClassName('PhabricatorSearchApplicationSearchEngine'); + $query->setParameter('query', $query_str); + + $types = $source->getResultPHIDTypes(); + $query->setParameter('types', $types); + + $status_open = PhabricatorSearchRelationship::RELATIONSHIP_OPEN; + + switch ($filter) { + case 'assigned': + $query->setParameter('ownerPHIDs', array($viewer->getPHID())); + $query->setParameter('statuses', array($status_open)); + break; + case 'created'; + $query->setParameter('authorPHIDs', array($viewer->getPHID())); + $query->setParameter('statuses', array($status_open)); + break; + case 'open': + $query->setParameter('statuses', array($status_open)); + break; + } + + $query->setParameter('excludePHIDs', array($request->getStr('exclude'))); + + $capabilities = $relationship->getRequiredRelationshipCapabilities(); + + $results = id(new PhabricatorSearchDocumentQuery()) + ->setViewer($viewer) + ->requireObjectCapabilities($capabilities) + ->withSavedQuery($query) + ->setOffset(0) + ->setLimit(100) + ->execute(); + + $phids = array_fill_keys(mpull($results, 'getPHID'), true); + $phids += $this->queryObjectNames($query_str, $capabilities); + + $phids = array_keys($phids); + $handles = $viewer->loadHandles($phids); + + $data = array(); + foreach ($handles as $handle) { + $view = new PhabricatorHandleObjectSelectorDataView($handle); + $data[] = $view->renderData(); + } + + return id(new AphrontAjaxResponse())->setContent($data); + } + + private function queryObjectNames($query, $capabilities) { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $objects = id(new PhabricatorObjectQuery()) + ->setViewer($viewer) + ->requireCapabilities($capabilities) + ->withTypes(array($request->getURIData('type'))) + ->withNames(array($query)) + ->execute(); + + return mpull($objects, 'getPHID'); + } + +} diff --git a/src/applications/search/relationship/DifferentialRevisionRelationshipSource.php b/src/applications/search/relationship/DifferentialRevisionRelationshipSource.php new file mode 100644 index 0000000000..566a822ec3 --- /dev/null +++ b/src/applications/search/relationship/DifferentialRevisionRelationshipSource.php @@ -0,0 +1,12 @@ +newRelationshipSource(); + } + + abstract protected function newRelationshipSource(); + + final public function getSourceURI($object) { + $relationship_key = $this->getRelationshipConstant(); + $object_phid = $object->getPHID(); + + return "/search/source/{$relationship_key}/{$object_phid}/"; + } + final public function newAction($object) { $is_enabled = $this->isActionEnabled($object); $action_uri = $this->getActionURI($object); diff --git a/src/applications/search/relationship/PhabricatorObjectRelationshipSource.php b/src/applications/search/relationship/PhabricatorObjectRelationshipSource.php new file mode 100644 index 0000000000..e0e94c176d --- /dev/null +++ b/src/applications/search/relationship/PhabricatorObjectRelationshipSource.php @@ -0,0 +1,7 @@ + Date: Tue, 28 Jun 2016 18:38:45 -0700 Subject: [PATCH 10/35] Convert all standard relationship-editing actions to modern Relationships code Summary: Ref T4788. This moves everything except "merge" to the new code. Test Plan: - Edited relationships in Differential, Diffusion, and Pholio. - Uninstalled Pholio, made sure "Edit Mocks..." actions vanished. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4788 Differential Revision: https://secure.phabricator.com/D16193 --- src/__phutil_library_map__.php | 28 +++++++++-- .../PhabricatorDifferentialConfigOptions.php | 4 +- .../DifferentialRevisionViewController.php | 48 ++++++++++--------- ...hp => DifferentialChildRevisionsField.php} | 4 +- ...p => DifferentialParentRevisionsField.php} | 4 +- ...fferentialRevisionHasChildRelationship.php | 44 +++++++++++++++++ ...ferentialRevisionHasCommitRelationship.php | 40 ++++++++++++++++ ...ferentialRevisionHasParentRelationship.php | 44 +++++++++++++++++ ...ifferentialRevisionHasTaskRelationship.php | 40 ++++++++++++++++ .../DifferentialRevisionRelationship.php | 19 ++++++++ .../controller/DiffusionCommitController.php | 23 ++++----- ...DiffusionCommitHasRevisionRelationship.php | 40 ++++++++++++++++ .../DiffusionCommitHasTaskRelationship.php | 40 ++++++++++++++++ .../DiffusionCommitRelationship.php | 19 ++++++++ .../controller/PholioMockViewController.php | 15 +++--- .../PholioMockHasTaskRelationship.php | 40 ++++++++++++++++ .../relationships/PholioMockRelationship.php | 19 ++++++++ ...DifferentialRevisionRelationshipSource.php | 8 ++++ .../DiffusionCommitRelationshipSource.php | 8 ++++ .../ManiphestTaskRelationshipSource.php | 8 ++++ .../PhabricatorObjectRelationship.php | 5 +- .../PhabricatorObjectRelationshipList.php | 5 ++ .../PhabricatorObjectRelationshipSource.php | 12 +++++ .../PholioMockRelationshipSource.php | 8 ++++ 24 files changed, 471 insertions(+), 54 deletions(-) rename src/applications/differential/customfield/{DifferentialDependenciesField.php => DifferentialChildRevisionsField.php} (91%) rename src/applications/differential/customfield/{DifferentialDependsOnField.php => DifferentialParentRevisionsField.php} (94%) create mode 100644 src/applications/differential/relationships/DifferentialRevisionHasChildRelationship.php create mode 100644 src/applications/differential/relationships/DifferentialRevisionHasCommitRelationship.php create mode 100644 src/applications/differential/relationships/DifferentialRevisionHasParentRelationship.php create mode 100644 src/applications/differential/relationships/DifferentialRevisionHasTaskRelationship.php create mode 100644 src/applications/differential/relationships/DifferentialRevisionRelationship.php create mode 100644 src/applications/diffusion/relationships/DiffusionCommitHasRevisionRelationship.php create mode 100644 src/applications/diffusion/relationships/DiffusionCommitHasTaskRelationship.php create mode 100644 src/applications/diffusion/relationships/DiffusionCommitRelationship.php create mode 100644 src/applications/pholio/relationships/PholioMockHasTaskRelationship.php create mode 100644 src/applications/pholio/relationships/PholioMockRelationship.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 4fead1dc05..71f520674e 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -381,6 +381,7 @@ phutil_register_library_map(array( 'DifferentialChangesetTwoUpRenderer' => 'applications/differential/render/DifferentialChangesetTwoUpRenderer.php', 'DifferentialChangesetTwoUpTestRenderer' => 'applications/differential/render/DifferentialChangesetTwoUpTestRenderer.php', 'DifferentialChangesetViewController' => 'applications/differential/controller/DifferentialChangesetViewController.php', + 'DifferentialChildRevisionsField' => 'applications/differential/customfield/DifferentialChildRevisionsField.php', 'DifferentialCloseConduitAPIMethod' => 'applications/differential/conduit/DifferentialCloseConduitAPIMethod.php', 'DifferentialCommentPreviewController' => 'applications/differential/controller/DifferentialCommentPreviewController.php', 'DifferentialCommentSaveController' => 'applications/differential/controller/DifferentialCommentSaveController.php', @@ -407,8 +408,6 @@ phutil_register_library_map(array( 'DifferentialCustomFieldStringIndex' => 'applications/differential/storage/DifferentialCustomFieldStringIndex.php', 'DifferentialDAO' => 'applications/differential/storage/DifferentialDAO.php', 'DifferentialDefaultViewCapability' => 'applications/differential/capability/DifferentialDefaultViewCapability.php', - 'DifferentialDependenciesField' => 'applications/differential/customfield/DifferentialDependenciesField.php', - 'DifferentialDependsOnField' => 'applications/differential/customfield/DifferentialDependsOnField.php', 'DifferentialDiff' => 'applications/differential/storage/DifferentialDiff.php', 'DifferentialDiffAffectedFilesHeraldField' => 'applications/differential/herald/DifferentialDiffAffectedFilesHeraldField.php', 'DifferentialDiffAuthorHeraldField' => 'applications/differential/herald/DifferentialDiffAuthorHeraldField.php', @@ -476,6 +475,7 @@ phutil_register_library_map(array( 'DifferentialManiphestTasksField' => 'applications/differential/customfield/DifferentialManiphestTasksField.php', 'DifferentialModernHunk' => 'applications/differential/storage/DifferentialModernHunk.php', 'DifferentialNextStepField' => 'applications/differential/customfield/DifferentialNextStepField.php', + 'DifferentialParentRevisionsField' => 'applications/differential/customfield/DifferentialParentRevisionsField.php', 'DifferentialParseCacheGarbageCollector' => 'applications/differential/garbagecollector/DifferentialParseCacheGarbageCollector.php', 'DifferentialParseCommitMessageConduitAPIMethod' => 'applications/differential/conduit/DifferentialParseCommitMessageConduitAPIMethod.php', 'DifferentialParseRenderTestCase' => 'applications/differential/__tests__/DifferentialParseRenderTestCase.php', @@ -521,9 +521,13 @@ phutil_register_library_map(array( 'DifferentialRevisionDependsOnRevisionEdgeType' => 'applications/differential/edge/DifferentialRevisionDependsOnRevisionEdgeType.php', 'DifferentialRevisionEditController' => 'applications/differential/controller/DifferentialRevisionEditController.php', 'DifferentialRevisionFulltextEngine' => 'applications/differential/search/DifferentialRevisionFulltextEngine.php', + 'DifferentialRevisionHasChildRelationship' => 'applications/differential/relationships/DifferentialRevisionHasChildRelationship.php', 'DifferentialRevisionHasCommitEdgeType' => 'applications/differential/edge/DifferentialRevisionHasCommitEdgeType.php', + 'DifferentialRevisionHasCommitRelationship' => 'applications/differential/relationships/DifferentialRevisionHasCommitRelationship.php', + 'DifferentialRevisionHasParentRelationship' => 'applications/differential/relationships/DifferentialRevisionHasParentRelationship.php', 'DifferentialRevisionHasReviewerEdgeType' => 'applications/differential/edge/DifferentialRevisionHasReviewerEdgeType.php', 'DifferentialRevisionHasTaskEdgeType' => 'applications/differential/edge/DifferentialRevisionHasTaskEdgeType.php', + 'DifferentialRevisionHasTaskRelationship' => 'applications/differential/relationships/DifferentialRevisionHasTaskRelationship.php', 'DifferentialRevisionHeraldField' => 'applications/differential/herald/DifferentialRevisionHeraldField.php', 'DifferentialRevisionHeraldFieldGroup' => 'applications/differential/herald/DifferentialRevisionHeraldFieldGroup.php', 'DifferentialRevisionIDField' => 'applications/differential/customfield/DifferentialRevisionIDField.php', @@ -536,6 +540,7 @@ phutil_register_library_map(array( 'DifferentialRevisionPackageHeraldField' => 'applications/differential/herald/DifferentialRevisionPackageHeraldField.php', 'DifferentialRevisionPackageOwnerHeraldField' => 'applications/differential/herald/DifferentialRevisionPackageOwnerHeraldField.php', 'DifferentialRevisionQuery' => 'applications/differential/query/DifferentialRevisionQuery.php', + 'DifferentialRevisionRelationship' => 'applications/differential/relationships/DifferentialRevisionRelationship.php', 'DifferentialRevisionRelationshipSource' => 'applications/search/relationship/DifferentialRevisionRelationshipSource.php', 'DifferentialRevisionRepositoryHeraldField' => 'applications/differential/herald/DifferentialRevisionRepositoryHeraldField.php', 'DifferentialRevisionRepositoryProjectsHeraldField' => 'applications/differential/herald/DifferentialRevisionRepositoryProjectsHeraldField.php', @@ -601,7 +606,9 @@ phutil_register_library_map(array( 'DiffusionCommitEditController' => 'applications/diffusion/controller/DiffusionCommitEditController.php', 'DiffusionCommitFulltextEngine' => 'applications/repository/search/DiffusionCommitFulltextEngine.php', 'DiffusionCommitHasRevisionEdgeType' => 'applications/diffusion/edge/DiffusionCommitHasRevisionEdgeType.php', + 'DiffusionCommitHasRevisionRelationship' => 'applications/diffusion/relationships/DiffusionCommitHasRevisionRelationship.php', 'DiffusionCommitHasTaskEdgeType' => 'applications/diffusion/edge/DiffusionCommitHasTaskEdgeType.php', + 'DiffusionCommitHasTaskRelationship' => 'applications/diffusion/relationships/DiffusionCommitHasTaskRelationship.php', 'DiffusionCommitHash' => 'applications/diffusion/data/DiffusionCommitHash.php', 'DiffusionCommitHeraldField' => 'applications/diffusion/herald/DiffusionCommitHeraldField.php', 'DiffusionCommitHeraldFieldGroup' => 'applications/diffusion/herald/DiffusionCommitHeraldFieldGroup.php', @@ -615,6 +622,7 @@ phutil_register_library_map(array( 'DiffusionCommitParentsQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionCommitParentsQueryConduitAPIMethod.php', 'DiffusionCommitQuery' => 'applications/diffusion/query/DiffusionCommitQuery.php', 'DiffusionCommitRef' => 'applications/diffusion/data/DiffusionCommitRef.php', + 'DiffusionCommitRelationship' => 'applications/diffusion/relationships/DiffusionCommitRelationship.php', 'DiffusionCommitRelationshipSource' => 'applications/search/relationship/DiffusionCommitRelationshipSource.php', 'DiffusionCommitRemarkupRule' => 'applications/diffusion/remarkup/DiffusionCommitRemarkupRule.php', 'DiffusionCommitRemarkupRuleTestCase' => 'applications/diffusion/remarkup/__tests__/DiffusionCommitRemarkupRuleTestCase.php', @@ -3867,6 +3875,7 @@ phutil_register_library_map(array( 'PholioMockEmbedView' => 'applications/pholio/view/PholioMockEmbedView.php', 'PholioMockFulltextEngine' => 'applications/pholio/search/PholioMockFulltextEngine.php', 'PholioMockHasTaskEdgeType' => 'applications/pholio/edge/PholioMockHasTaskEdgeType.php', + 'PholioMockHasTaskRelationship' => 'applications/pholio/relationships/PholioMockHasTaskRelationship.php', 'PholioMockHeraldField' => 'applications/pholio/herald/PholioMockHeraldField.php', 'PholioMockHeraldFieldGroup' => 'applications/pholio/herald/PholioMockHeraldFieldGroup.php', 'PholioMockImagesView' => 'applications/pholio/view/PholioMockImagesView.php', @@ -3875,6 +3884,7 @@ phutil_register_library_map(array( 'PholioMockNameHeraldField' => 'applications/pholio/herald/PholioMockNameHeraldField.php', 'PholioMockPHIDType' => 'applications/pholio/phid/PholioMockPHIDType.php', 'PholioMockQuery' => 'applications/pholio/query/PholioMockQuery.php', + 'PholioMockRelationship' => 'applications/pholio/relationships/PholioMockRelationship.php', 'PholioMockRelationshipSource' => 'applications/search/relationship/PholioMockRelationshipSource.php', 'PholioMockSearchEngine' => 'applications/pholio/query/PholioMockSearchEngine.php', 'PholioMockThumbGridView' => 'applications/pholio/view/PholioMockThumbGridView.php', @@ -4709,6 +4719,7 @@ phutil_register_library_map(array( 'DifferentialChangesetTwoUpRenderer' => 'DifferentialChangesetHTMLRenderer', 'DifferentialChangesetTwoUpTestRenderer' => 'DifferentialChangesetTestRenderer', 'DifferentialChangesetViewController' => 'DifferentialController', + 'DifferentialChildRevisionsField' => 'DifferentialCustomField', 'DifferentialCloseConduitAPIMethod' => 'DifferentialConduitAPIMethod', 'DifferentialCommentPreviewController' => 'DifferentialController', 'DifferentialCommentSaveController' => 'DifferentialController', @@ -4735,8 +4746,6 @@ phutil_register_library_map(array( 'DifferentialCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage', 'DifferentialDAO' => 'PhabricatorLiskDAO', 'DifferentialDefaultViewCapability' => 'PhabricatorPolicyCapability', - 'DifferentialDependenciesField' => 'DifferentialCustomField', - 'DifferentialDependsOnField' => 'DifferentialCustomField', 'DifferentialDiff' => array( 'DifferentialDAO', 'PhabricatorPolicyInterface', @@ -4817,6 +4826,7 @@ phutil_register_library_map(array( 'DifferentialManiphestTasksField' => 'DifferentialCoreCustomField', 'DifferentialModernHunk' => 'DifferentialHunk', 'DifferentialNextStepField' => 'DifferentialCustomField', + 'DifferentialParentRevisionsField' => 'DifferentialCustomField', 'DifferentialParseCacheGarbageCollector' => 'PhabricatorGarbageCollector', 'DifferentialParseCommitMessageConduitAPIMethod' => 'DifferentialConduitAPIMethod', 'DifferentialParseRenderTestCase' => 'PhabricatorTestCase', @@ -4878,9 +4888,13 @@ phutil_register_library_map(array( 'DifferentialRevisionDependsOnRevisionEdgeType' => 'PhabricatorEdgeType', 'DifferentialRevisionEditController' => 'DifferentialController', 'DifferentialRevisionFulltextEngine' => 'PhabricatorFulltextEngine', + 'DifferentialRevisionHasChildRelationship' => 'DifferentialRevisionRelationship', 'DifferentialRevisionHasCommitEdgeType' => 'PhabricatorEdgeType', + 'DifferentialRevisionHasCommitRelationship' => 'DifferentialRevisionRelationship', + 'DifferentialRevisionHasParentRelationship' => 'DifferentialRevisionRelationship', 'DifferentialRevisionHasReviewerEdgeType' => 'PhabricatorEdgeType', 'DifferentialRevisionHasTaskEdgeType' => 'PhabricatorEdgeType', + 'DifferentialRevisionHasTaskRelationship' => 'DifferentialRevisionRelationship', 'DifferentialRevisionHeraldField' => 'HeraldField', 'DifferentialRevisionHeraldFieldGroup' => 'HeraldFieldGroup', 'DifferentialRevisionIDField' => 'DifferentialCustomField', @@ -4893,6 +4907,7 @@ phutil_register_library_map(array( 'DifferentialRevisionPackageHeraldField' => 'DifferentialRevisionHeraldField', 'DifferentialRevisionPackageOwnerHeraldField' => 'DifferentialRevisionHeraldField', 'DifferentialRevisionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'DifferentialRevisionRelationship' => 'PhabricatorObjectRelationship', 'DifferentialRevisionRelationshipSource' => 'PhabricatorObjectRelationshipSource', 'DifferentialRevisionRepositoryHeraldField' => 'DifferentialRevisionHeraldField', 'DifferentialRevisionRepositoryProjectsHeraldField' => 'DifferentialRevisionHeraldField', @@ -4958,7 +4973,9 @@ phutil_register_library_map(array( 'DiffusionCommitEditController' => 'DiffusionController', 'DiffusionCommitFulltextEngine' => 'PhabricatorFulltextEngine', 'DiffusionCommitHasRevisionEdgeType' => 'PhabricatorEdgeType', + 'DiffusionCommitHasRevisionRelationship' => 'DiffusionCommitRelationship', 'DiffusionCommitHasTaskEdgeType' => 'PhabricatorEdgeType', + 'DiffusionCommitHasTaskRelationship' => 'DiffusionCommitRelationship', 'DiffusionCommitHash' => 'Phobject', 'DiffusionCommitHeraldField' => 'HeraldField', 'DiffusionCommitHeraldFieldGroup' => 'HeraldFieldGroup', @@ -4972,6 +4989,7 @@ phutil_register_library_map(array( 'DiffusionCommitParentsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionCommitQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'DiffusionCommitRef' => 'Phobject', + 'DiffusionCommitRelationship' => 'PhabricatorObjectRelationship', 'DiffusionCommitRelationshipSource' => 'PhabricatorObjectRelationshipSource', 'DiffusionCommitRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'DiffusionCommitRemarkupRuleTestCase' => 'PhabricatorTestCase', @@ -8793,6 +8811,7 @@ phutil_register_library_map(array( 'PholioMockEmbedView' => 'AphrontView', 'PholioMockFulltextEngine' => 'PhabricatorFulltextEngine', 'PholioMockHasTaskEdgeType' => 'PhabricatorEdgeType', + 'PholioMockHasTaskRelationship' => 'PholioMockRelationship', 'PholioMockHeraldField' => 'HeraldField', 'PholioMockHeraldFieldGroup' => 'HeraldFieldGroup', 'PholioMockImagesView' => 'AphrontView', @@ -8801,6 +8820,7 @@ phutil_register_library_map(array( 'PholioMockNameHeraldField' => 'PholioMockHeraldField', 'PholioMockPHIDType' => 'PhabricatorPHIDType', 'PholioMockQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PholioMockRelationship' => 'PhabricatorObjectRelationship', 'PholioMockRelationshipSource' => 'PhabricatorObjectRelationshipSource', 'PholioMockSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PholioMockThumbGridView' => 'AphrontView', diff --git a/src/applications/differential/config/PhabricatorDifferentialConfigOptions.php b/src/applications/differential/config/PhabricatorDifferentialConfigOptions.php index fa0bd0c891..1d7b646270 100644 --- a/src/applications/differential/config/PhabricatorDifferentialConfigOptions.php +++ b/src/applications/differential/config/PhabricatorDifferentialConfigOptions.php @@ -40,8 +40,8 @@ final class PhabricatorDifferentialConfigOptions new DifferentialViewPolicyField(), new DifferentialEditPolicyField(), - new DifferentialDependsOnField(), - new DifferentialDependenciesField(), + new DifferentialParentRevisionsField(), + new DifferentialChildRevisionsField(), new DifferentialManiphestTasksField(), new DifferentialCommitsField(), diff --git a/src/applications/differential/controller/DifferentialRevisionViewController.php b/src/applications/differential/controller/DifferentialRevisionViewController.php index 32f9978e4d..d1fbe3ad05 100644 --- a/src/applications/differential/controller/DifferentialRevisionViewController.php +++ b/src/applications/differential/controller/DifferentialRevisionViewController.php @@ -516,28 +516,6 @@ final class DifferentialRevisionViewController extends DifferentialController { ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); - $this->requireResource('phabricator-object-selector-css'); - $this->requireResource('javelin-behavior-phabricator-object-selector'); - - $curtain->addAction( - id(new PhabricatorActionView()) - ->setIcon('fa-link') - ->setName(pht('Edit Dependencies')) - ->setHref("/search/attach/{$revision_phid}/DREV/dependencies/") - ->setWorkflow(true) - ->setDisabled(!$can_edit)); - - $maniphest = 'PhabricatorManiphestApplication'; - if (PhabricatorApplication::isClassInstalled($maniphest)) { - $curtain->addAction( - id(new PhabricatorActionView()) - ->setIcon('fa-anchor') - ->setName(pht('Edit Maniphest Tasks')) - ->setHref("/search/attach/{$revision_phid}/TASK/") - ->setWorkflow(true) - ->setDisabled(!$can_edit)); - } - $request_uri = $this->getRequest()->getRequestURI(); $curtain->addAction( id(new PhabricatorActionView()) @@ -545,6 +523,32 @@ final class DifferentialRevisionViewController extends DifferentialController { ->setName(pht('Download Raw Diff')) ->setHref($request_uri->alter('download', 'true'))); + $relationship_list = PhabricatorObjectRelationshipList::newForObject( + $viewer, + $revision); + + $parent_key = DifferentialRevisionHasParentRelationship::RELATIONSHIPKEY; + $child_key = DifferentialRevisionHasChildRelationship::RELATIONSHIPKEY; + + $revision_submenu = array(); + + $revision_submenu[] = $relationship_list->getRelationship($parent_key) + ->newAction($revision); + + $revision_submenu[] = $relationship_list->getRelationship($child_key) + ->newAction($revision); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit Related Revisions...')) + ->setIcon('fa-cog') + ->setSubmenu($revision_submenu)); + + $relationship_submenu = $relationship_list->newActionMenu(); + if ($relationship_submenu) { + $curtain->addAction($relationship_submenu); + } + return $curtain; } diff --git a/src/applications/differential/customfield/DifferentialDependenciesField.php b/src/applications/differential/customfield/DifferentialChildRevisionsField.php similarity index 91% rename from src/applications/differential/customfield/DifferentialDependenciesField.php rename to src/applications/differential/customfield/DifferentialChildRevisionsField.php index 3f828b3b55..0965fc2d7c 100644 --- a/src/applications/differential/customfield/DifferentialDependenciesField.php +++ b/src/applications/differential/customfield/DifferentialChildRevisionsField.php @@ -1,6 +1,6 @@ getViewer(); + + $has_app = PhabricatorApplication::isClassInstalledForViewer( + 'PhabricatorDifferentialApplication', + $viewer); + if (!$has_app) { + return false; + } + + return ($object instanceof DifferentialRevision); + } + +} diff --git a/src/applications/diffusion/controller/DiffusionCommitController.php b/src/applications/diffusion/controller/DiffusionCommitController.php index e9ab15287c..1ee2ccedad 100644 --- a/src/applications/diffusion/controller/DiffusionCommitController.php +++ b/src/applications/diffusion/controller/DiffusionCommitController.php @@ -968,26 +968,21 @@ final class DiffusionCommitController extends DiffusionController { ->setWorkflow(!$can_edit); $curtain->addAction($action); - require_celerity_resource('phabricator-object-selector-css'); - require_celerity_resource('javelin-behavior-phabricator-object-selector'); - - $maniphest = 'PhabricatorManiphestApplication'; - if (PhabricatorApplication::isClassInstalled($maniphest)) { - $action = id(new PhabricatorActionView()) - ->setName(pht('Edit Maniphest Tasks')) - ->setIcon('fa-anchor') - ->setHref('/search/attach/'.$commit->getPHID().'/TASK/edge/') - ->setWorkflow(true) - ->setDisabled(!$can_edit); - $curtain->addAction($action); - } - $action = id(new PhabricatorActionView()) ->setName(pht('Download Raw Diff')) ->setHref($request->getRequestURI()->alter('diff', true)) ->setIcon('fa-download'); $curtain->addAction($action); + $relationship_list = PhabricatorObjectRelationshipList::newForObject( + $viewer, + $commit); + + $relationship_submenu = $relationship_list->newActionMenu(); + if ($relationship_submenu) { + $curtain->addAction($relationship_submenu); + } + return $curtain; } diff --git a/src/applications/diffusion/relationships/DiffusionCommitHasRevisionRelationship.php b/src/applications/diffusion/relationships/DiffusionCommitHasRevisionRelationship.php new file mode 100644 index 0000000000..226684c8f2 --- /dev/null +++ b/src/applications/diffusion/relationships/DiffusionCommitHasRevisionRelationship.php @@ -0,0 +1,40 @@ +getViewer(); + + $has_app = PhabricatorApplication::isClassInstalledForViewer( + 'PhabricatorDiffusionApplication', + $viewer); + if (!$has_app) { + return false; + } + + return ($object instanceof PhabricatorRepositoryCommit); + } + +} diff --git a/src/applications/pholio/controller/PholioMockViewController.php b/src/applications/pholio/controller/PholioMockViewController.php index 906454c1f3..a1fb40c86b 100644 --- a/src/applications/pholio/controller/PholioMockViewController.php +++ b/src/applications/pholio/controller/PholioMockViewController.php @@ -150,13 +150,14 @@ final class PholioMockViewController extends PholioController { ->setWorkflow(true)); } - $curtain->addAction( - id(new PhabricatorActionView()) - ->setIcon('fa-anchor') - ->setName(pht('Edit Maniphest Tasks')) - ->setHref("/search/attach/{$mock->getPHID()}/TASK/edge/") - ->setDisabled(!$viewer->isLoggedIn()) - ->setWorkflow(true)); + $relationship_list = PhabricatorObjectRelationshipList::newForObject( + $viewer, + $mock); + + $relationship_submenu = $relationship_list->newActionMenu(); + if ($relationship_submenu) { + $curtain->addAction($relationship_submenu); + } if ($this->getManiphestTaskPHIDs()) { $curtain->newPanel() diff --git a/src/applications/pholio/relationships/PholioMockHasTaskRelationship.php b/src/applications/pholio/relationships/PholioMockHasTaskRelationship.php new file mode 100644 index 0000000000..69ba1616bc --- /dev/null +++ b/src/applications/pholio/relationships/PholioMockHasTaskRelationship.php @@ -0,0 +1,40 @@ +getViewer(); + + $has_app = PhabricatorApplication::isClassInstalledForViewer( + 'PhabricatorPholioApplication', + $viewer); + if (!$has_app) { + return false; + } + + return ($object instanceof PholioMock); + } + +} diff --git a/src/applications/search/relationship/DifferentialRevisionRelationshipSource.php b/src/applications/search/relationship/DifferentialRevisionRelationshipSource.php index 566a822ec3..3a5b274880 100644 --- a/src/applications/search/relationship/DifferentialRevisionRelationshipSource.php +++ b/src/applications/search/relationship/DifferentialRevisionRelationshipSource.php @@ -3,6 +3,14 @@ final class DifferentialRevisionRelationshipSource extends PhabricatorObjectRelationshipSource { + public function isEnabledForObject($object) { + $viewer = $this->getViewer(); + + return PhabricatorApplication::isClassInstalledForViewer( + 'PhabricatorDifferentialApplication', + $viewer); + } + public function getResultPHIDTypes() { return array( DifferentialRevisionPHIDType::TYPECONST, diff --git a/src/applications/search/relationship/DiffusionCommitRelationshipSource.php b/src/applications/search/relationship/DiffusionCommitRelationshipSource.php index 48208a22d1..31fc918011 100644 --- a/src/applications/search/relationship/DiffusionCommitRelationshipSource.php +++ b/src/applications/search/relationship/DiffusionCommitRelationshipSource.php @@ -3,6 +3,14 @@ final class DiffusionCommitRelationshipSource extends PhabricatorObjectRelationshipSource { + public function isEnabledForObject($object) { + $viewer = $this->getViewer(); + + return PhabricatorApplication::isClassInstalledForViewer( + 'PhabricatorDiffusionApplication', + $viewer); + } + public function getResultPHIDTypes() { return array( PhabricatorRepositoryCommitPHIDType::TYPECONST, diff --git a/src/applications/search/relationship/ManiphestTaskRelationshipSource.php b/src/applications/search/relationship/ManiphestTaskRelationshipSource.php index 39135d10f5..e510f73a97 100644 --- a/src/applications/search/relationship/ManiphestTaskRelationshipSource.php +++ b/src/applications/search/relationship/ManiphestTaskRelationshipSource.php @@ -3,6 +3,14 @@ final class ManiphestTaskRelationshipSource extends PhabricatorObjectRelationshipSource { + public function isEnabledForObject($object) { + $viewer = $this->getViewer(); + + return PhabricatorApplication::isClassInstalledForViewer( + 'PhabricatorManiphestApplication', + $viewer); + } + public function getResultPHIDTypes() { return array( ManiphestTaskPHIDType::TYPECONST, diff --git a/src/applications/search/relationship/PhabricatorObjectRelationship.php b/src/applications/search/relationship/PhabricatorObjectRelationship.php index 513ba9ff6b..9711dc1719 100644 --- a/src/applications/search/relationship/PhabricatorObjectRelationship.php +++ b/src/applications/search/relationship/PhabricatorObjectRelationship.php @@ -54,7 +54,10 @@ abstract class PhabricatorObjectRelationship extends Phobject { } final public function newSource() { - return $this->newRelationshipSource(); + $viewer = $this->getViewer(); + + return $this->newRelationshipSource() + ->setViewer($viewer); } abstract protected function newRelationshipSource(); diff --git a/src/applications/search/relationship/PhabricatorObjectRelationshipList.php b/src/applications/search/relationship/PhabricatorObjectRelationshipList.php index a43bbaa571..46f9b6e801 100644 --- a/src/applications/search/relationship/PhabricatorObjectRelationshipList.php +++ b/src/applications/search/relationship/PhabricatorObjectRelationshipList.php @@ -87,6 +87,11 @@ final class PhabricatorObjectRelationshipList extends Phobject { continue; } + $source = $relationship->newSource(); + if (!$source->isEnabledForObject($object)) { + continue; + } + $results[$key] = $relationship; } diff --git a/src/applications/search/relationship/PhabricatorObjectRelationshipSource.php b/src/applications/search/relationship/PhabricatorObjectRelationshipSource.php index e0e94c176d..a1fc9a6370 100644 --- a/src/applications/search/relationship/PhabricatorObjectRelationshipSource.php +++ b/src/applications/search/relationship/PhabricatorObjectRelationshipSource.php @@ -2,6 +2,18 @@ abstract class PhabricatorObjectRelationshipSource extends Phobject { + private $viewer; + + final public function setViewer(PhabricatorUser $viewer) { + $this->viewer = $viewer; + return $this; + } + + final public function getViewer() { + return $this->viewer; + } + + abstract public function isEnabledForObject($object); abstract public function getResultPHIDTypes(); } diff --git a/src/applications/search/relationship/PholioMockRelationshipSource.php b/src/applications/search/relationship/PholioMockRelationshipSource.php index 1f1e432aa4..b378f8ef41 100644 --- a/src/applications/search/relationship/PholioMockRelationshipSource.php +++ b/src/applications/search/relationship/PholioMockRelationshipSource.php @@ -3,6 +3,14 @@ final class PholioMockRelationshipSource extends PhabricatorObjectRelationshipSource { + public function isEnabledForObject($object) { + $viewer = $this->getViewer(); + + return PhabricatorApplication::isClassInstalledForViewer( + 'PhabricatorPholioApplication', + $viewer); + } + public function getResultPHIDTypes() { return array( PholioMockPHIDType::TYPECONST, From fd0a606f79420807d9b916b5484c7afaece08d2a Mon Sep 17 00:00:00 2001 From: Chad Little Date: Wed, 29 Jun 2016 17:53:28 -0700 Subject: [PATCH 11/35] Misc Phame cleanup Summary: Ref T9360. [x] View Live useless on archived blogs [x] Edit Blog Image treatment like profiles [x] Pager next/prev should keep you on whatever view you're on [x] Unset user titles aren't falling back properly [x] Add captions to edit fields for better clarification Test Plan: Archive a blog, Edit a photo, verify pager on live and internal blogs, check empty titles, and view new edit form instructions. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Maniphest Tasks: T9360 Differential Revision: https://secure.phabricator.com/D16197 --- .../controller/blog/PhameBlogManageController.php | 13 ++++++++++++- .../controller/post/PhamePostViewController.php | 10 +++++++--- .../phame/editor/PhameBlogEditEngine.php | 8 ++++++-- src/applications/phame/storage/PhamePost.php | 12 ++++++++++++ 4 files changed, 37 insertions(+), 6 deletions(-) diff --git a/src/applications/phame/controller/blog/PhameBlogManageController.php b/src/applications/phame/controller/blog/PhameBlogManageController.php index 4085d3ef6b..2bdcbf7635 100644 --- a/src/applications/phame/controller/blog/PhameBlogManageController.php +++ b/src/applications/phame/controller/blog/PhameBlogManageController.php @@ -36,7 +36,8 @@ final class PhameBlogManageController extends PhameBlogController { ->setTag('a') ->setText(pht('View Live')) ->setIcon('fa-external-link') - ->setHref($blog->getLiveURI()); + ->setHref($blog->getLiveURI()) + ->setDisabled($blog->isArchived()); $header = id(new PHUIHeaderView()) ->setHeader($blog->getName()) @@ -46,6 +47,16 @@ final class PhameBlogManageController extends PhameBlogController { ->setStatus($header_icon, $header_color, $header_name) ->addActionLink($view); + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $blog, + PhabricatorPolicyCapability::CAN_EDIT); + + if ($can_edit) { + $header->setImageEditURL( + $this->getApplicationURI('blog/picture/'.$blog->getID().'/')); + } + $curtain = $this->buildCurtain($blog); $properties = $this->buildPropertyView($blog); $file = $this->buildFileView($blog); diff --git a/src/applications/phame/controller/post/PhamePostViewController.php b/src/applications/phame/controller/post/PhamePostViewController.php index d28f1b5150..f7dddebe53 100644 --- a/src/applications/phame/controller/post/PhamePostViewController.php +++ b/src/applications/phame/controller/post/PhamePostViewController.php @@ -118,7 +118,7 @@ final class PhamePostViewController array( $user_icon, ' ', - $blogger_profile->getTitle(), + $blogger_profile->getDisplayTitle(), )) ->setImage($blogger->getProfileImageURI()) ->setImageHref($author_uri); @@ -142,12 +142,16 @@ final class PhamePostViewController ->setUser($viewer) ->setObject($post); + $is_live = $this->getIsLive(); + $is_external = $this->getIsExternal(); $next_view = new PhameNextPostView(); if ($next) { - $next_view->setNext($next->getTitle(), $next->getLiveURI()); + $next_view->setNext($next->getTitle(), + $next->getBestURI($is_live, $is_external)); } if ($prev) { - $next_view->setPrevious($prev->getTitle(), $prev->getLiveURI()); + $next_view->setPrevious($prev->getTitle(), + $prev->getBestURI($is_live, $is_external)); } $document->setFoot($next_view); diff --git a/src/applications/phame/editor/PhameBlogEditEngine.php b/src/applications/phame/editor/PhameBlogEditEngine.php index 5f96309e8b..7d6511d55c 100644 --- a/src/applications/phame/editor/PhameBlogEditEngine.php +++ b/src/applications/phame/editor/PhameBlogEditEngine.php @@ -96,6 +96,10 @@ final class PhameBlogEditEngine id(new PhabricatorTextEditField()) ->setKey('domainFullURI') ->setLabel(pht('Full Domain URI')) + ->setControlInstructions(pht('Set Full Domain URI if you plan to '. + 'serve this blog on another hosted domain. Parent Site Name and '. + 'Parent Site URI are optional but helpful since they provide '. + 'a link from the blog back to your parent site.')) ->setDescription(pht('Blog full domain URI.')) ->setConduitDescription(pht('Change the blog full domain URI.')) ->setConduitTypeDescription(pht('New blog full domain URI.')) @@ -103,7 +107,7 @@ final class PhameBlogEditEngine ->setTransactionType(PhameBlogTransaction::TYPE_FULLDOMAIN), id(new PhabricatorTextEditField()) ->setKey('parentSite') - ->setLabel(pht('Parent Site')) + ->setLabel(pht('Parent Site Name')) ->setDescription(pht('Blog parent site name.')) ->setConduitDescription(pht('Change the blog parent site name.')) ->setConduitTypeDescription(pht('New blog parent site name.')) @@ -111,7 +115,7 @@ final class PhameBlogEditEngine ->setTransactionType(PhameBlogTransaction::TYPE_PARENTSITE), id(new PhabricatorTextEditField()) ->setKey('parentDomain') - ->setLabel(pht('Parent Domain')) + ->setLabel(pht('Parent Site URI')) ->setDescription(pht('Blog parent domain name.')) ->setConduitDescription(pht('Change the blog parent domain.')) ->setConduitTypeDescription(pht('New blog parent domain.')) diff --git a/src/applications/phame/storage/PhamePost.php b/src/applications/phame/storage/PhamePost.php index 8078c5055c..4c4acf4dc1 100644 --- a/src/applications/phame/storage/PhamePost.php +++ b/src/applications/phame/storage/PhamePost.php @@ -86,6 +86,18 @@ final class PhamePost extends PhameDAO return "/phame/post/view/{$id}/{$slug}/"; } + public function getBestURI($is_live, $is_external) { + if ($is_live) { + if ($is_external) { + return $this->getExternalLiveURI(); + } else { + return $this->getInternalLiveURI(); + } + } else { + return $this->getViewURI(); + } + } + public function getEditURI() { return '/phame/post/edit/'.$this->getID().'/'; } From 922822bd2dc3df8a59881b435de74752eced49e8 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Wed, 29 Jun 2016 20:27:11 -0700 Subject: [PATCH 12/35] Wrap really long text properly in diffs Summary: Fixes T11236, breaks long words and inlines the spans. Test Plan: Use a diff with super long text like SSH keys, set Font to 24/48 Impact {F1705910} Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Maniphest Tasks: T11236 Differential Revision: https://secure.phabricator.com/D16198 --- resources/celerity/map.php | 12 ++++++------ webroot/rsrc/css/core/syntax.css | 3 ++- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 3b35b902ed..acff94bca3 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,7 +7,7 @@ */ return array( 'names' => array( - 'core.pkg.css' => 'b6b40555', + 'core.pkg.css' => 'ee796570', 'core.pkg.js' => 'f2139810', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '3e81ae60', @@ -105,7 +105,7 @@ return array( 'rsrc/css/application/uiexample/example.css' => '528b19de', 'rsrc/css/core/core.css' => 'd0801452', 'rsrc/css/core/remarkup.css' => '523d34bb', - 'rsrc/css/core/syntax.css' => '9fc496d5', + 'rsrc/css/core/syntax.css' => '769d3498', 'rsrc/css/core/z-index.css' => '5b6fcf3f', 'rsrc/css/diviner/diviner-shared.css' => 'aa3656aa', 'rsrc/css/font/font-aleo.css' => '8bdb2835', @@ -890,7 +890,7 @@ return array( 'sprite-menu-css' => '9dd65b92', 'sprite-tokens-css' => '4f399012', 'syntax-default-css' => '9923583c', - 'syntax-highlighting-css' => '9fc496d5', + 'syntax-highlighting-css' => '769d3498', 'tokens-css' => '3d0f239e', 'typeahead-browse-css' => '8904346a', 'unhandled-exception-css' => '4c96257a', @@ -1463,6 +1463,9 @@ return array( 'javelin-vector', 'javelin-dom', ), + '769d3498' => array( + 'syntax-default-css', + ), '76b9fc3e' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1674,9 +1677,6 @@ return array( 'javelin-dom', 'javelin-vector', ), - '9fc496d5' => array( - 'syntax-default-css', - ), 'a0b57eb8' => array( 'javelin-behavior', 'javelin-dom', diff --git a/webroot/rsrc/css/core/syntax.css b/webroot/rsrc/css/core/syntax.css index 27006f2e4c..5de3b5d4a4 100644 --- a/webroot/rsrc/css/core/syntax.css +++ b/webroot/rsrc/css/core/syntax.css @@ -12,7 +12,8 @@ } .remarkup-code td span { - display: inline-block; + display: inline; + word-break: break-all; } .remarkup-code .rbw_r { color: red; } From 4f8d07594e2af46dfa228a9b7177c7f5b0b47a0b Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 30 Jun 2016 08:22:20 -0700 Subject: [PATCH 13/35] Fix a CSRF issue with adding new email addresses Summary: The first dialog was being given the wrong user (`$user`, should be `$viewer`), leading to a CSRF issue. (The CSRF token it generated was invalid in all validation contexts, so this wasn't a security problem or a way to capture CSRF tokens for other users.) Use `newDialog()` instead. (This seems completely unrelated to the vaguely-similar-looking issues we saw earlier this week.) Test Plan: - Added a new email address. - Clicked "Done" on the last step. - Completed workflow instead of getting a CSRF error. Reviewers: chad, tide Reviewed By: tide Differential Revision: https://secure.phabricator.com/D16200 --- .../panel/PhabricatorEmailAddressesSettingsPanel.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/applications/settings/panel/PhabricatorEmailAddressesSettingsPanel.php b/src/applications/settings/panel/PhabricatorEmailAddressesSettingsPanel.php index ac02fae00e..b5d8ea8617 100644 --- a/src/applications/settings/panel/PhabricatorEmailAddressesSettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorEmailAddressesSettingsPanel.php @@ -227,8 +227,7 @@ final class PhabricatorEmailAddressesSettingsPanel $object->sendVerificationEmail($user); - $dialog = id(new AphrontDialogView()) - ->setUser($user) + $dialog = $this->newDialog() ->addHiddenInput('new', 'verify') ->setTitle(pht('Verification Email Sent')) ->appendChild(phutil_tag('p', array(), pht( @@ -259,8 +258,7 @@ final class PhabricatorEmailAddressesSettingsPanel ->setCaption(PhabricatorUserEmail::describeAllowedAddresses()) ->setError($e_email)); - $dialog = id(new AphrontDialogView()) - ->setUser($viewer) + $dialog = $this->newDialog() ->addHiddenInput('new', 'true') ->setTitle(pht('New Address')) ->appendChild($errors) From 2a7545a452b0336701b5ce95fd5f2d63da42ec11 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 29 Jun 2016 11:09:27 -0700 Subject: [PATCH 14/35] Convert Maniphest merge operations to modern Relationship code Summary: Ref T4788. Fixes T7820. This updates the "Merge Duplicates In" interaction, and adds a "Close as Duplicate" action. These are the last interactions that were using the old code, so it removes that code. Merges are now recorded as real edges, so we can show them in the UI later on (originally from T9390, etc). Also provides more general support for relationships which need EDIT permission, not-undoable relationships like merges, preventing relating an object to itself, and relationship side effects like merges. Finally, fixes a couple of behaviors around typing an exact object name (like `T123`) to find the related object. Test Plan: - Merged tasks into the current task. - Closed the current task as a duplicate of another task. - Edited other relationships. - Searched for tasks, commits, etc., by object monogram. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4788, T7820 Differential Revision: https://secure.phabricator.com/D16196 --- src/__phutil_library_map__.php | 12 +- .../ManiphestTaskDetailController.php | 13 +- .../ManiphestTaskHasDuplicateTaskEdgeType.php | 16 + ...ManiphestTaskIsDuplicateOfTaskEdgeType.php | 16 + ...iphestTaskCloseAsDuplicateRelationship.php | 84 +++++ .../ManiphestTaskMergeInRelationship.php | 75 ++++ .../ManiphestTaskRelationship.php | 48 +++ .../PhabricatorSearchApplication.php | 4 - .../PhabricatorSearchAttachController.php | 330 ------------------ ...habricatorSearchRelationshipController.php | 54 ++- ...atorSearchRelationshipSourceController.php | 14 +- .../PhabricatorSearchSelectController.php | 86 ----- .../PhabricatorObjectRelationship.php | 22 ++ .../PhabricatorApplicationTransaction.php | 2 + 14 files changed, 336 insertions(+), 440 deletions(-) create mode 100644 src/applications/maniphest/edge/ManiphestTaskHasDuplicateTaskEdgeType.php create mode 100644 src/applications/maniphest/edge/ManiphestTaskIsDuplicateOfTaskEdgeType.php create mode 100644 src/applications/maniphest/relationship/ManiphestTaskCloseAsDuplicateRelationship.php create mode 100644 src/applications/maniphest/relationship/ManiphestTaskMergeInRelationship.php delete mode 100644 src/applications/search/controller/PhabricatorSearchAttachController.php delete mode 100644 src/applications/search/controller/PhabricatorSearchSelectController.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 71f520674e..51373419d9 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1420,6 +1420,7 @@ phutil_register_library_map(array( 'ManiphestTaskAssigneeHeraldField' => 'applications/maniphest/herald/ManiphestTaskAssigneeHeraldField.php', 'ManiphestTaskAuthorHeraldField' => 'applications/maniphest/herald/ManiphestTaskAuthorHeraldField.php', 'ManiphestTaskAuthorPolicyRule' => 'applications/maniphest/policyrule/ManiphestTaskAuthorPolicyRule.php', + 'ManiphestTaskCloseAsDuplicateRelationship' => 'applications/maniphest/relationship/ManiphestTaskCloseAsDuplicateRelationship.php', 'ManiphestTaskClosedStatusDatasource' => 'applications/maniphest/typeahead/ManiphestTaskClosedStatusDatasource.php', 'ManiphestTaskDependedOnByTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskDependedOnByTaskEdgeType.php', 'ManiphestTaskDependsOnTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskDependsOnTaskEdgeType.php', @@ -1430,6 +1431,7 @@ phutil_register_library_map(array( 'ManiphestTaskFulltextEngine' => 'applications/maniphest/search/ManiphestTaskFulltextEngine.php', 'ManiphestTaskHasCommitEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasCommitEdgeType.php', 'ManiphestTaskHasCommitRelationship' => 'applications/maniphest/relationship/ManiphestTaskHasCommitRelationship.php', + 'ManiphestTaskHasDuplicateTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasDuplicateTaskEdgeType.php', 'ManiphestTaskHasMockEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasMockEdgeType.php', 'ManiphestTaskHasMockRelationship' => 'applications/maniphest/relationship/ManiphestTaskHasMockRelationship.php', 'ManiphestTaskHasParentRelationship' => 'applications/maniphest/relationship/ManiphestTaskHasParentRelationship.php', @@ -1438,10 +1440,12 @@ phutil_register_library_map(array( 'ManiphestTaskHasSubtaskRelationship' => 'applications/maniphest/relationship/ManiphestTaskHasSubtaskRelationship.php', 'ManiphestTaskHeraldField' => 'applications/maniphest/herald/ManiphestTaskHeraldField.php', 'ManiphestTaskHeraldFieldGroup' => 'applications/maniphest/herald/ManiphestTaskHeraldFieldGroup.php', + 'ManiphestTaskIsDuplicateOfTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskIsDuplicateOfTaskEdgeType.php', 'ManiphestTaskListController' => 'applications/maniphest/controller/ManiphestTaskListController.php', 'ManiphestTaskListHTTPParameterType' => 'applications/maniphest/httpparametertype/ManiphestTaskListHTTPParameterType.php', 'ManiphestTaskListView' => 'applications/maniphest/view/ManiphestTaskListView.php', 'ManiphestTaskMailReceiver' => 'applications/maniphest/mail/ManiphestTaskMailReceiver.php', + 'ManiphestTaskMergeInRelationship' => 'applications/maniphest/relationship/ManiphestTaskMergeInRelationship.php', 'ManiphestTaskOpenStatusDatasource' => 'applications/maniphest/typeahead/ManiphestTaskOpenStatusDatasource.php', 'ManiphestTaskPHIDResolver' => 'applications/maniphest/httpparametertype/ManiphestTaskPHIDResolver.php', 'ManiphestTaskPHIDType' => 'applications/maniphest/phid/ManiphestTaskPHIDType.php', @@ -3372,7 +3376,6 @@ phutil_register_library_map(array( 'PhabricatorSearchApplication' => 'applications/search/application/PhabricatorSearchApplication.php', 'PhabricatorSearchApplicationSearchEngine' => 'applications/search/query/PhabricatorSearchApplicationSearchEngine.php', 'PhabricatorSearchApplicationStorageEnginePanel' => 'applications/search/applicationpanel/PhabricatorSearchApplicationStorageEnginePanel.php', - 'PhabricatorSearchAttachController' => 'applications/search/controller/PhabricatorSearchAttachController.php', 'PhabricatorSearchBaseController' => 'applications/search/controller/PhabricatorSearchBaseController.php', 'PhabricatorSearchCheckboxesField' => 'applications/search/field/PhabricatorSearchCheckboxesField.php', 'PhabricatorSearchConfigOptions' => 'applications/search/config/PhabricatorSearchConfigOptions.php', @@ -3415,7 +3418,6 @@ phutil_register_library_map(array( 'PhabricatorSearchResultView' => 'applications/search/view/PhabricatorSearchResultView.php', 'PhabricatorSearchSchemaSpec' => 'applications/search/storage/PhabricatorSearchSchemaSpec.php', 'PhabricatorSearchScopeSetting' => 'applications/settings/setting/PhabricatorSearchScopeSetting.php', - 'PhabricatorSearchSelectController' => 'applications/search/controller/PhabricatorSearchSelectController.php', 'PhabricatorSearchSelectField' => 'applications/search/field/PhabricatorSearchSelectField.php', 'PhabricatorSearchStringListField' => 'applications/search/field/PhabricatorSearchStringListField.php', 'PhabricatorSearchSubscribersField' => 'applications/search/field/PhabricatorSearchSubscribersField.php', @@ -5929,6 +5931,7 @@ phutil_register_library_map(array( 'ManiphestTaskAssigneeHeraldField' => 'ManiphestTaskHeraldField', 'ManiphestTaskAuthorHeraldField' => 'ManiphestTaskHeraldField', 'ManiphestTaskAuthorPolicyRule' => 'PhabricatorPolicyRule', + 'ManiphestTaskCloseAsDuplicateRelationship' => 'ManiphestTaskRelationship', 'ManiphestTaskClosedStatusDatasource' => 'PhabricatorTypeaheadDatasource', 'ManiphestTaskDependedOnByTaskEdgeType' => 'PhabricatorEdgeType', 'ManiphestTaskDependsOnTaskEdgeType' => 'PhabricatorEdgeType', @@ -5939,6 +5942,7 @@ phutil_register_library_map(array( 'ManiphestTaskFulltextEngine' => 'PhabricatorFulltextEngine', 'ManiphestTaskHasCommitEdgeType' => 'PhabricatorEdgeType', 'ManiphestTaskHasCommitRelationship' => 'ManiphestTaskRelationship', + 'ManiphestTaskHasDuplicateTaskEdgeType' => 'PhabricatorEdgeType', 'ManiphestTaskHasMockEdgeType' => 'PhabricatorEdgeType', 'ManiphestTaskHasMockRelationship' => 'ManiphestTaskRelationship', 'ManiphestTaskHasParentRelationship' => 'ManiphestTaskRelationship', @@ -5947,10 +5951,12 @@ phutil_register_library_map(array( 'ManiphestTaskHasSubtaskRelationship' => 'ManiphestTaskRelationship', 'ManiphestTaskHeraldField' => 'HeraldField', 'ManiphestTaskHeraldFieldGroup' => 'HeraldFieldGroup', + 'ManiphestTaskIsDuplicateOfTaskEdgeType' => 'PhabricatorEdgeType', 'ManiphestTaskListController' => 'ManiphestController', 'ManiphestTaskListHTTPParameterType' => 'AphrontListHTTPParameterType', 'ManiphestTaskListView' => 'ManiphestView', 'ManiphestTaskMailReceiver' => 'PhabricatorObjectMailReceiver', + 'ManiphestTaskMergeInRelationship' => 'ManiphestTaskRelationship', 'ManiphestTaskOpenStatusDatasource' => 'PhabricatorTypeaheadDatasource', 'ManiphestTaskPHIDResolver' => 'PhabricatorPHIDResolver', 'ManiphestTaskPHIDType' => 'PhabricatorPHIDType', @@ -8210,7 +8216,6 @@ phutil_register_library_map(array( 'PhabricatorSearchApplication' => 'PhabricatorApplication', 'PhabricatorSearchApplicationSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorSearchApplicationStorageEnginePanel' => 'PhabricatorApplicationConfigurationPanel', - 'PhabricatorSearchAttachController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchBaseController' => 'PhabricatorController', 'PhabricatorSearchCheckboxesField' => 'PhabricatorSearchField', 'PhabricatorSearchConfigOptions' => 'PhabricatorApplicationConfigOptions', @@ -8253,7 +8258,6 @@ phutil_register_library_map(array( 'PhabricatorSearchResultView' => 'AphrontView', 'PhabricatorSearchSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorSearchScopeSetting' => 'PhabricatorInternalSetting', - 'PhabricatorSearchSelectController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchSelectField' => 'PhabricatorSearchField', 'PhabricatorSearchStringListField' => 'PhabricatorSearchField', 'PhabricatorSearchSubscribersField' => 'PhabricatorSearchTokenizerField', diff --git a/src/applications/maniphest/controller/ManiphestTaskDetailController.php b/src/applications/maniphest/controller/ManiphestTaskDetailController.php index bc862a1122..df95d07c12 100644 --- a/src/applications/maniphest/controller/ManiphestTaskDetailController.php +++ b/src/applications/maniphest/controller/ManiphestTaskDetailController.php @@ -201,6 +201,8 @@ final class ManiphestTaskDetailController extends ManiphestController { $parent_key = ManiphestTaskHasParentRelationship::RELATIONSHIPKEY; $subtask_key = ManiphestTaskHasSubtaskRelationship::RELATIONSHIPKEY; + $merge_key = ManiphestTaskMergeInRelationship::RELATIONSHIPKEY; + $close_key = ManiphestTaskCloseAsDuplicateRelationship::RELATIONSHIPKEY; $task_submenu[] = $relationship_list->getRelationship($parent_key) ->newAction($task); @@ -208,12 +210,11 @@ final class ManiphestTaskDetailController extends ManiphestController { $task_submenu[] = $relationship_list->getRelationship($subtask_key) ->newAction($task); - $task_submenu[] = id(new PhabricatorActionView()) - ->setName(pht('Merge Duplicates In')) - ->setHref("/search/attach/{$phid}/TASK/merge/") - ->setIcon('fa-compress') - ->setDisabled(!$can_edit) - ->setWorkflow(true); + $task_submenu[] = $relationship_list->getRelationship($merge_key) + ->newAction($task); + + $task_submenu[] = $relationship_list->getRelationship($close_key) + ->newAction($task); $curtain->addAction( id(new PhabricatorActionView()) diff --git a/src/applications/maniphest/edge/ManiphestTaskHasDuplicateTaskEdgeType.php b/src/applications/maniphest/edge/ManiphestTaskHasDuplicateTaskEdgeType.php new file mode 100644 index 0000000000..b5f5b44ae3 --- /dev/null +++ b/src/applications/maniphest/edge/ManiphestTaskHasDuplicateTaskEdgeType.php @@ -0,0 +1,16 @@ + 1) { + throw new Exception( + pht( + 'A task can only be closed as a duplicate of exactly one other '. + 'task.')); + } + + $task = head($add); + return $this->newMergeIntoTransactions($task); + } + + public function didUpdateRelationships($object, array $add, array $rem) { + $viewer = $this->getViewer(); + $content_source = $this->getContentSource(); + + $task = head($add); + $xactions = $this->newMergeFromTransactions(array($object)); + + $task->getApplicationTransactionEditor() + ->setActor($viewer) + ->setContentSource($content_source) + ->setContinueOnMissingFields(true) + ->setContinueOnNoEffect(true) + ->applyTransactions($task, $xactions); + } + +} diff --git a/src/applications/maniphest/relationship/ManiphestTaskMergeInRelationship.php b/src/applications/maniphest/relationship/ManiphestTaskMergeInRelationship.php new file mode 100644 index 0000000000..5bf45b7911 --- /dev/null +++ b/src/applications/maniphest/relationship/ManiphestTaskMergeInRelationship.php @@ -0,0 +1,75 @@ +newMergeFromTransactions($add); + } + + public function didUpdateRelationships($object, array $add, array $rem) { + $viewer = $this->getViewer(); + $content_source = $this->getContentSource(); + + foreach ($add as $task) { + $xactions = $this->newMergeIntoTransactions($object); + + $task->getApplicationTransactionEditor() + ->setActor($viewer) + ->setContentSource($content_source) + ->setContinueOnMissingFields(true) + ->setContinueOnNoEffect(true) + ->applyTransactions($task, $xactions); + } + } + +} diff --git a/src/applications/maniphest/relationship/ManiphestTaskRelationship.php b/src/applications/maniphest/relationship/ManiphestTaskRelationship.php index a0383d77f4..928d49e87d 100644 --- a/src/applications/maniphest/relationship/ManiphestTaskRelationship.php +++ b/src/applications/maniphest/relationship/ManiphestTaskRelationship.php @@ -16,4 +16,52 @@ abstract class ManiphestTaskRelationship return ($object instanceof ManiphestTask); } + protected function newMergeIntoTransactions(ManiphestTask $task) { + return array( + id(new ManiphestTransaction()) + ->setTransactionType(ManiphestTransaction::TYPE_MERGED_INTO) + ->setNewValue($task->getPHID()), + ); + } + + protected function newMergeFromTransactions(array $tasks) { + $xactions = array(); + + $subscriber_phids = $this->loadMergeSubscriberPHIDs($tasks); + + $xactions[] = id(new ManiphestTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS) + ->setNewValue(array('+' => $subscriber_phids)); + + $xactions[] = id(new ManiphestTransaction()) + ->setTransactionType(ManiphestTransaction::TYPE_MERGED_FROM) + ->setNewValue(mpull($tasks, 'getPHID')); + + return $xactions; + } + + private function loadMergeSubscriberPHIDs(array $tasks) { + $phids = array(); + + foreach ($tasks as $task) { + $phids[] = $task->getAuthorPHID(); + $phids[] = $task->getOwnerPHID(); + } + + $subscribers = id(new PhabricatorSubscribersQuery()) + ->withObjectPHIDs(mpull($tasks, 'getPHID')) + ->execute(); + + foreach ($subscribers as $phid => $subscriber_list) { + foreach ($subscriber_list as $subscriber) { + $phids[] = $subscriber; + } + } + + $phids = array_unique($phids); + $phids = array_filter($phids); + + return $phids; + } + } diff --git a/src/applications/search/application/PhabricatorSearchApplication.php b/src/applications/search/application/PhabricatorSearchApplication.php index 894e14fe2f..a2a28c7665 100644 --- a/src/applications/search/application/PhabricatorSearchApplication.php +++ b/src/applications/search/application/PhabricatorSearchApplication.php @@ -30,10 +30,6 @@ final class PhabricatorSearchApplication extends PhabricatorApplication { return array( '/search/' => array( '(?:query/(?P[^/]+)/)?' => 'PhabricatorSearchController', - 'attach/(?P[^/]+)/(?P\w+)/(?:(?P\w+)/)?' - => 'PhabricatorSearchAttachController', - 'select/(?P\w+)/(?:(?P\w+)/)?' - => 'PhabricatorSearchSelectController', 'index/(?P[^/]+)/' => 'PhabricatorSearchIndexController', 'hovercard/' => 'PhabricatorSearchHovercardController', diff --git a/src/applications/search/controller/PhabricatorSearchAttachController.php b/src/applications/search/controller/PhabricatorSearchAttachController.php deleted file mode 100644 index a3238cb50d..0000000000 --- a/src/applications/search/controller/PhabricatorSearchAttachController.php +++ /dev/null @@ -1,330 +0,0 @@ -getUser(); - $phid = $request->getURIData('phid'); - $attach_type = $request->getURIData('type'); - $action = $request->getURIData('action', self::ACTION_ATTACH); - - $handle = id(new PhabricatorHandleQuery()) - ->setViewer($user) - ->withPHIDs(array($phid)) - ->executeOne(); - - $object_type = $handle->getType(); - - $object = id(new PhabricatorObjectQuery()) - ->setViewer($user) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->withPHIDs(array($phid)) - ->executeOne(); - - if (!$object) { - return new Aphront404Response(); - } - - $edge_type = null; - switch ($action) { - case self::ACTION_EDGE: - case self::ACTION_DEPENDENCIES: - case self::ACTION_BLOCKS: - case self::ACTION_ATTACH: - $edge_type = $this->getEdgeType($object_type, $attach_type); - break; - } - - if ($request->isFormPost()) { - $phids = explode(';', $request->getStr('phids')); - $phids = array_filter($phids); - $phids = array_values($phids); - - if ($edge_type) { - if (!$object instanceof PhabricatorApplicationTransactionInterface) { - throw new Exception( - pht( - 'Expected object ("%s") to implement interface "%s".', - get_class($object), - 'PhabricatorApplicationTransactionInterface')); - } - - $old_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( - $phid, - $edge_type); - $add_phids = $phids; - $rem_phids = array_diff($old_phids, $add_phids); - - $txn_editor = $object->getApplicationTransactionEditor() - ->setActor($user) - ->setContentSourceFromRequest($request) - ->setContinueOnMissingFields(true) - ->setContinueOnNoEffect(true); - - $txn_template = $object->getApplicationTransactionTemplate() - ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) - ->setMetadataValue('edge:type', $edge_type) - ->setNewValue(array( - '+' => array_fuse($add_phids), - '-' => array_fuse($rem_phids), - )); - - try { - $txn_editor->applyTransactions( - $object->getApplicationTransactionObject(), - array($txn_template)); - } catch (PhabricatorEdgeCycleException $ex) { - $this->raiseGraphCycleException($ex); - } - - return id(new AphrontReloadResponse())->setURI($handle->getURI()); - } else { - return $this->performMerge($object, $handle, $phids); - } - } else { - if ($edge_type) { - $phids = PhabricatorEdgeQuery::loadDestinationPHIDs( - $phid, - $edge_type); - } else { - // This is a merge. - $phids = array(); - } - } - - $strings = $this->getStrings($attach_type, $action); - - $handles = $this->loadViewerHandles($phids); - - $obj_dialog = new PhabricatorObjectSelectorDialog(); - $obj_dialog - ->setUser($user) - ->setHandles($handles) - ->setFilters($this->getFilters($strings, $attach_type)) - ->setSelectedFilter($strings['selected']) - ->setExcluded($phid) - ->setCancelURI($handle->getURI()) - ->setSearchURI('/search/select/'.$attach_type.'/'.$action.'/') - ->setTitle($strings['title']) - ->setHeader($strings['header']) - ->setButtonText($strings['button']) - ->setInstructions($strings['instructions']); - - $dialog = $obj_dialog->buildDialog(); - - return id(new AphrontDialogResponse())->setDialog($dialog); - } - - private function performMerge( - ManiphestTask $task, - PhabricatorObjectHandle $handle, - array $phids) { - - $user = $this->getRequest()->getUser(); - $response = id(new AphrontReloadResponse())->setURI($handle->getURI()); - - $phids = array_fill_keys($phids, true); - unset($phids[$task->getPHID()]); // Prevent merging a task into itself. - - if (!$phids) { - return $response; - } - - $targets = id(new ManiphestTaskQuery()) - ->setViewer($user) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->withPHIDs(array_keys($phids)) - ->needSubscriberPHIDs(true) - ->needProjectPHIDs(true) - ->execute(); - - if (empty($targets)) { - return $response; - } - - $editor = id(new ManiphestTransactionEditor()) - ->setActor($user) - ->setContentSourceFromRequest($this->getRequest()) - ->setContinueOnNoEffect(true) - ->setContinueOnMissingFields(true); - - $cc_vector = array(); - // since we loaded this via a generic object query, go ahead and get the - // attach the subscriber and project phids now - $task->attachSubscriberPHIDs( - PhabricatorSubscribersQuery::loadSubscribersForPHID($task->getPHID())); - $task->attachProjectPHIDs( - PhabricatorEdgeQuery::loadDestinationPHIDs($task->getPHID(), - PhabricatorProjectObjectHasProjectEdgeType::EDGECONST)); - - $cc_vector[] = $task->getSubscriberPHIDs(); - foreach ($targets as $target) { - $cc_vector[] = $target->getSubscriberPHIDs(); - $cc_vector[] = array( - $target->getAuthorPHID(), - $target->getOwnerPHID(), - ); - - $merged_into_txn = id(new ManiphestTransaction()) - ->setTransactionType(ManiphestTransaction::TYPE_MERGED_INTO) - ->setNewValue($task->getPHID()); - - $editor->applyTransactions( - $target, - array($merged_into_txn)); - - } - $all_ccs = array_mergev($cc_vector); - $all_ccs = array_filter($all_ccs); - $all_ccs = array_unique($all_ccs); - - $add_ccs = id(new ManiphestTransaction()) - ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS) - ->setNewValue(array('=' => $all_ccs)); - - $merged_from_txn = id(new ManiphestTransaction()) - ->setTransactionType(ManiphestTransaction::TYPE_MERGED_FROM) - ->setNewValue(mpull($targets, 'getPHID')); - - $editor->applyTransactions( - $task, - array($add_ccs, $merged_from_txn)); - - return $response; - } - - private function getStrings($attach_type, $action) { - switch ($attach_type) { - case DifferentialRevisionPHIDType::TYPECONST: - $noun = pht('Revisions'); - $selected = pht('created'); - break; - case ManiphestTaskPHIDType::TYPECONST: - $noun = pht('Tasks'); - $selected = pht('assigned'); - break; - case PhabricatorRepositoryCommitPHIDType::TYPECONST: - $noun = pht('Commits'); - $selected = pht('created'); - break; - case PholioMockPHIDType::TYPECONST: - $noun = pht('Mocks'); - $selected = pht('created'); - break; - } - - switch ($action) { - case self::ACTION_EDGE: - case self::ACTION_ATTACH: - $dialog_title = pht('Manage Attached %s', $noun); - $header_text = pht('Currently Attached %s', $noun); - $button_text = pht('Save %s', $noun); - $instructions = null; - break; - case self::ACTION_MERGE: - $dialog_title = pht('Merge Duplicate Tasks'); - $header_text = pht('Tasks To Merge'); - $button_text = pht('Merge %s', $noun); - $instructions = pht( - 'These tasks will be merged into the current task and then closed. '. - 'The current task will grow stronger.'); - break; - case self::ACTION_DEPENDENCIES: - $dialog_title = pht('Edit Dependencies'); - $header_text = pht('Current Dependencies'); - $button_text = pht('Save Dependencies'); - $instructions = null; - break; - case self::ACTION_BLOCKS: - $dialog_title = pht('Edit Blocking Tasks'); - $header_text = pht('Current Blocking Tasks'); - $button_text = pht('Save Blocking Tasks'); - $instructions = null; - break; - } - - return array( - 'target_plural_noun' => $noun, - 'selected' => $selected, - 'title' => $dialog_title, - 'header' => $header_text, - 'button' => $button_text, - 'instructions' => $instructions, - ); - } - - private function getFilters(array $strings, $attach_type) { - if ($attach_type == PholioMockPHIDType::TYPECONST) { - $filters = array( - 'created' => pht('Created By Me'), - 'all' => pht('All %s', $strings['target_plural_noun']), - ); - } else { - $filters = array( - 'assigned' => pht('Assigned to Me'), - 'created' => pht('Created By Me'), - 'open' => pht('All Open %s', $strings['target_plural_noun']), - 'all' => pht('All %s', $strings['target_plural_noun']), - ); - } - - return $filters; - } - - private function getEdgeType($src_type, $dst_type) { - $t_cmit = PhabricatorRepositoryCommitPHIDType::TYPECONST; - $t_task = ManiphestTaskPHIDType::TYPECONST; - $t_drev = DifferentialRevisionPHIDType::TYPECONST; - $t_mock = PholioMockPHIDType::TYPECONST; - - $map = array( - $t_cmit => array( - $t_task => DiffusionCommitHasTaskEdgeType::EDGECONST, - ), - $t_task => array( - $t_cmit => ManiphestTaskHasCommitEdgeType::EDGECONST, - $t_task => ManiphestTaskDependsOnTaskEdgeType::EDGECONST, - $t_drev => ManiphestTaskHasRevisionEdgeType::EDGECONST, - $t_mock => ManiphestTaskHasMockEdgeType::EDGECONST, - ), - $t_drev => array( - $t_drev => DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST, - $t_task => DifferentialRevisionHasTaskEdgeType::EDGECONST, - ), - $t_mock => array( - $t_task => PholioMockHasTaskEdgeType::EDGECONST, - ), - ); - - if (empty($map[$src_type][$dst_type])) { - return null; - } - - return $map[$src_type][$dst_type]; - } - - private function raiseGraphCycleException(PhabricatorEdgeCycleException $ex) { - $cycle = $ex->getCycle(); - - $handles = $this->loadViewerHandles($cycle); - $names = array(); - foreach ($cycle as $cycle_phid) { - $names[] = $handles[$cycle_phid]->getFullName(); - } - throw new Exception( - pht( - 'You can not create that dependency, because it would create a '. - 'circular dependency: %s.', - implode(" \xE2\x86\x92 ", $names))); - } - -} diff --git a/src/applications/search/controller/PhabricatorSearchRelationshipController.php b/src/applications/search/controller/PhabricatorSearchRelationshipController.php index 2b2fac9bb7..767c756813 100644 --- a/src/applications/search/controller/PhabricatorSearchRelationshipController.php +++ b/src/applications/search/controller/PhabricatorSearchRelationshipController.php @@ -19,9 +19,16 @@ final class PhabricatorSearchRelationshipController $src_phid = $object->getPHID(); $edge_type = $relationship->getEdgeConstant(); - $dst_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( - $src_phid, - $edge_type); + // If this is a normal relationship, users can remove related objects. If + // it's a special relationship like a merge, we can't undo it, so we won't + // prefill the current related objects. + if ($relationship->canUndoRelationship()) { + $dst_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( + $src_phid, + $edge_type); + } else { + $dst_phids = array(); + } $all_phids = $dst_phids; $all_phids[] = $src_phid; @@ -44,12 +51,16 @@ final class PhabricatorSearchRelationshipController // relationships at the same time don't race and overwrite one another. $add_phids = array_diff($phids, $initial_phids); $rem_phids = array_diff($initial_phids, $phids); + $all_phids = array_merge($add_phids, $rem_phids); - if ($add_phids) { + $capabilities = $relationship->getRequiredRelationshipCapabilities(); + + if ($all_phids) { $dst_objects = id(new PhabricatorObjectQuery()) ->setViewer($viewer) - ->withPHIDs($phids) + ->withPHIDs($all_phids) ->setRaisePolicyExceptions(true) + ->requireCapabilities($capabilities) ->execute(); $dst_objects = mpull($dst_objects, null, 'getPHID'); } else { @@ -67,6 +78,14 @@ final class PhabricatorSearchRelationshipController $add_phid)); } + if ($add_phid == $src_phid) { + throw new Exception( + pht( + 'You can not create a relationship to object "%s" because '. + 'objects can not be related to themselves.', + $add_phid)); + } + if (!$relationship->canRelateObjects($object, $dst_object)) { throw new Exception( pht( @@ -81,9 +100,12 @@ final class PhabricatorSearchRelationshipController return $this->newUnrelatableObjectResponse($ex, $done_uri); } + $content_source = PhabricatorContentSource::newFromRequest($request); + $relationship->setContentSource($content_source); + $editor = $object->getApplicationTransactionEditor() ->setActor($viewer) - ->setContentSourceFromRequest($request) + ->setContentSource($content_source) ->setContinueOnMissingFields(true) ->setContinueOnNoEffect(true); @@ -96,9 +118,29 @@ final class PhabricatorSearchRelationshipController '-' => array_fuse($rem_phids), )); + $add_objects = array_select_keys($dst_objects, $add_phids); + $rem_objects = array_select_keys($dst_objects, $rem_phids); + + if ($add_objects || $rem_objects) { + $more_xactions = $relationship->willUpdateRelationships( + $object, + $add_objects, + $rem_objects); + foreach ($more_xactions as $xaction) { + $xactions[] = $xaction; + } + } + try { $editor->applyTransactions($object, $xactions); + if ($add_objects || $rem_objects) { + $relationship->didUpdateRelationships( + $object, + $add_objects, + $rem_objects); + } + return id(new AphrontRedirectResponse())->setURI($done_uri); } catch (PhabricatorEdgeCycleException $ex) { return $this->newGraphCycleResponse($ex, $done_uri); diff --git a/src/applications/search/controller/PhabricatorSearchRelationshipSourceController.php b/src/applications/search/controller/PhabricatorSearchRelationshipSourceController.php index e783eae7f4..4a96727a29 100644 --- a/src/applications/search/controller/PhabricatorSearchRelationshipSourceController.php +++ b/src/applications/search/controller/PhabricatorSearchRelationshipSourceController.php @@ -58,7 +58,7 @@ final class PhabricatorSearchRelationshipSourceController ->execute(); $phids = array_fill_keys(mpull($results, 'getPHID'), true); - $phids += $this->queryObjectNames($query_str, $capabilities); + $phids = $this->queryObjectNames($query, $capabilities) + $phids; $phids = array_keys($phids); $handles = $viewer->loadHandles($phids); @@ -72,15 +72,21 @@ final class PhabricatorSearchRelationshipSourceController return id(new AphrontAjaxResponse())->setContent($data); } - private function queryObjectNames($query, $capabilities) { + private function queryObjectNames( + PhabricatorSavedQuery $query, + array $capabilities) { + $request = $this->getRequest(); $viewer = $request->getUser(); + $types = $query->getParameter('types'); + $match = $query->getParameter('query'); + $objects = id(new PhabricatorObjectQuery()) ->setViewer($viewer) ->requireCapabilities($capabilities) - ->withTypes(array($request->getURIData('type'))) - ->withNames(array($query)) + ->withTypes($query->getParameter('types')) + ->withNames(array($match)) ->execute(); return mpull($objects, 'getPHID'); diff --git a/src/applications/search/controller/PhabricatorSearchSelectController.php b/src/applications/search/controller/PhabricatorSearchSelectController.php deleted file mode 100644 index f663cd03d7..0000000000 --- a/src/applications/search/controller/PhabricatorSearchSelectController.php +++ /dev/null @@ -1,86 +0,0 @@ -getUser(); - $type = $request->getURIData('type'); - $action = $request->getURIData('action'); - - $query = new PhabricatorSavedQuery(); - $query_str = $request->getStr('query'); - - $query->setEngineClassName('PhabricatorSearchApplicationSearchEngine'); - $query->setParameter('query', $query_str); - $query->setParameter('types', array($type)); - - $status_open = PhabricatorSearchRelationship::RELATIONSHIP_OPEN; - - switch ($request->getStr('filter')) { - case 'assigned': - $query->setParameter('ownerPHIDs', array($user->getPHID())); - $query->setParameter('statuses', array($status_open)); - break; - case 'created'; - $query->setParameter('authorPHIDs', array($user->getPHID())); - // TODO - if / when we allow pholio mocks to be archived, etc - // update this - if ($type != PholioMockPHIDType::TYPECONST) { - $query->setParameter('statuses', array($status_open)); - } - break; - case 'open': - $query->setParameter('statuses', array($status_open)); - break; - } - - $query->setParameter('excludePHIDs', array($request->getStr('exclude'))); - - $capabilities = array(PhabricatorPolicyCapability::CAN_VIEW); - switch ($action) { - case self::ACTION_MERGE: - $capabilities[] = PhabricatorPolicyCapability::CAN_EDIT; - break; - default: - break; - } - - $results = id(new PhabricatorSearchDocumentQuery()) - ->setViewer($user) - ->requireObjectCapabilities($capabilities) - ->withSavedQuery($query) - ->setOffset(0) - ->setLimit(100) - ->execute(); - - $phids = array_fill_keys(mpull($results, 'getPHID'), true); - $phids += $this->queryObjectNames($query_str, $capabilities); - - $phids = array_keys($phids); - $handles = $this->loadViewerHandles($phids); - - $data = array(); - foreach ($handles as $handle) { - $view = new PhabricatorHandleObjectSelectorDataView($handle); - $data[] = $view->renderData(); - } - - return id(new AphrontAjaxResponse())->setContent($data); - } - - private function queryObjectNames($query, $capabilities) { - $request = $this->getRequest(); - $viewer = $request->getUser(); - - $objects = id(new PhabricatorObjectQuery()) - ->setViewer($viewer) - ->requireCapabilities($capabilities) - ->withTypes(array($request->getURIData('type'))) - ->withNames(array($query)) - ->execute(); - - return mpull($objects, 'getPHID'); - } - -} diff --git a/src/applications/search/relationship/PhabricatorObjectRelationship.php b/src/applications/search/relationship/PhabricatorObjectRelationship.php index 9711dc1719..50b5299952 100644 --- a/src/applications/search/relationship/PhabricatorObjectRelationship.php +++ b/src/applications/search/relationship/PhabricatorObjectRelationship.php @@ -3,6 +3,7 @@ abstract class PhabricatorObjectRelationship extends Phobject { private $viewer; + private $contentSource; public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; @@ -13,6 +14,15 @@ abstract class PhabricatorObjectRelationship extends Phobject { return $this->viewer; } + public function setContentSource(PhabricatorContentSource $content_source) { + $this->contentSource = $content_source; + return $this; + } + + public function getContentSource() { + return $this->contentSource; + } + final public function getRelationshipConstant() { return $this->getPhobjectClassConstant('RELATIONSHIPKEY'); } @@ -94,4 +104,16 @@ abstract class PhabricatorObjectRelationship extends Phobject { return "/search/rel/{$type}/{$phid}/"; } + public function canUndoRelationship() { + return true; + } + + public function willUpdateRelationships($object, array $add, array $rem) { + return array(); + } + + public function didUpdateRelationships($object, array $add, array $rem) { + return; + } + } diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php index 53259984b4..e130522bb2 100644 --- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php +++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php @@ -609,6 +609,8 @@ abstract class PhabricatorApplicationTransaction $edge_type = $this->getMetadataValue('edge:type'); switch ($edge_type) { case PhabricatorObjectMentionsObjectEdgeType::EDGECONST: + case ManiphestTaskHasDuplicateTaskEdgeType::EDGECONST: + case ManiphestTaskIsDuplicateOfTaskEdgeType::EDGECONST: return true; break; case PhabricatorObjectMentionedByObjectEdgeType::EDGECONST: From 7574f8dcf51a1839ab9c2c2302dca42db40979bb Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 30 Jun 2016 10:20:56 -0700 Subject: [PATCH 15/35] When all actions in a submenu are disabled, disable the submenu header Summary: Fixes T11240. Also simplify things a little and share a bit more code. Test Plan: - Viewed revisions and tasks, opened submenu. - Viewed as a user without edit permission, saw the menus greyed out. Reviewers: chad Reviewed By: chad Maniphest Tasks: T11240 Differential Revision: https://secure.phabricator.com/D16201 --- .../DifferentialRevisionViewController.php | 22 ++++------- .../ManiphestTaskDetailController.php | 30 +++++---------- .../PhabricatorSearchBaseController.php | 7 ---- .../PhabricatorObjectRelationshipList.php | 37 ++++++++++++++++++- src/view/layout/PhabricatorActionView.php | 4 ++ 5 files changed, 57 insertions(+), 43 deletions(-) diff --git a/src/applications/differential/controller/DifferentialRevisionViewController.php b/src/applications/differential/controller/DifferentialRevisionViewController.php index d1fbe3ad05..dfbcaa74ff 100644 --- a/src/applications/differential/controller/DifferentialRevisionViewController.php +++ b/src/applications/differential/controller/DifferentialRevisionViewController.php @@ -527,22 +527,16 @@ final class DifferentialRevisionViewController extends DifferentialController { $viewer, $revision); - $parent_key = DifferentialRevisionHasParentRelationship::RELATIONSHIPKEY; - $child_key = DifferentialRevisionHasChildRelationship::RELATIONSHIPKEY; + $revision_actions = array( + DifferentialRevisionHasParentRelationship::RELATIONSHIPKEY, + DifferentialRevisionHasChildRelationship::RELATIONSHIPKEY, + ); - $revision_submenu = array(); + $revision_submenu = $relationship_list->newActionSubmenu($revision_actions) + ->setName(pht('Edit Related Revisions...')) + ->setIcon('fa-cog'); - $revision_submenu[] = $relationship_list->getRelationship($parent_key) - ->newAction($revision); - - $revision_submenu[] = $relationship_list->getRelationship($child_key) - ->newAction($revision); - - $curtain->addAction( - id(new PhabricatorActionView()) - ->setName(pht('Edit Related Revisions...')) - ->setIcon('fa-cog') - ->setSubmenu($revision_submenu)); + $curtain->addAction($revision_submenu); $relationship_submenu = $relationship_list->newActionMenu(); if ($relationship_submenu) { diff --git a/src/applications/maniphest/controller/ManiphestTaskDetailController.php b/src/applications/maniphest/controller/ManiphestTaskDetailController.php index df95d07c12..1a59bd2fd8 100644 --- a/src/applications/maniphest/controller/ManiphestTaskDetailController.php +++ b/src/applications/maniphest/controller/ManiphestTaskDetailController.php @@ -199,28 +199,18 @@ final class ManiphestTaskDetailController extends ManiphestController { $viewer, $task); - $parent_key = ManiphestTaskHasParentRelationship::RELATIONSHIPKEY; - $subtask_key = ManiphestTaskHasSubtaskRelationship::RELATIONSHIPKEY; - $merge_key = ManiphestTaskMergeInRelationship::RELATIONSHIPKEY; - $close_key = ManiphestTaskCloseAsDuplicateRelationship::RELATIONSHIPKEY; + $submenu_actions = array( + ManiphestTaskHasParentRelationship::RELATIONSHIPKEY, + ManiphestTaskHasSubtaskRelationship::RELATIONSHIPKEY, + ManiphestTaskMergeInRelationship::RELATIONSHIPKEY, + ManiphestTaskCloseAsDuplicateRelationship::RELATIONSHIPKEY, + ); - $task_submenu[] = $relationship_list->getRelationship($parent_key) - ->newAction($task); + $task_submenu = $relationship_list->newActionSubmenu($submenu_actions) + ->setName(pht('Edit Related Tasks...')) + ->setIcon('fa-anchor'); - $task_submenu[] = $relationship_list->getRelationship($subtask_key) - ->newAction($task); - - $task_submenu[] = $relationship_list->getRelationship($merge_key) - ->newAction($task); - - $task_submenu[] = $relationship_list->getRelationship($close_key) - ->newAction($task); - - $curtain->addAction( - id(new PhabricatorActionView()) - ->setName(pht('Edit Related Tasks...')) - ->setIcon('fa-anchor') - ->setSubmenu($task_submenu)); + $curtain->addAction($task_submenu); $relationship_submenu = $relationship_list->newActionMenu(); if ($relationship_submenu) { diff --git a/src/applications/search/controller/PhabricatorSearchBaseController.php b/src/applications/search/controller/PhabricatorSearchBaseController.php index 189048145e..d3f31c1e6a 100644 --- a/src/applications/search/controller/PhabricatorSearchBaseController.php +++ b/src/applications/search/controller/PhabricatorSearchBaseController.php @@ -2,13 +2,6 @@ abstract class PhabricatorSearchBaseController extends PhabricatorController { - - const ACTION_ATTACH = 'attach'; - const ACTION_MERGE = 'merge'; - const ACTION_DEPENDENCIES = 'dependencies'; - const ACTION_BLOCKS = 'blocks'; - const ACTION_EDGE = 'edge'; - protected function loadRelationshipObject() { $request = $this->getRequest(); $viewer = $this->getViewer(); diff --git a/src/applications/search/relationship/PhabricatorObjectRelationshipList.php b/src/applications/search/relationship/PhabricatorObjectRelationshipList.php index 46f9b6e801..31d05fa939 100644 --- a/src/applications/search/relationship/PhabricatorObjectRelationshipList.php +++ b/src/applications/search/relationship/PhabricatorObjectRelationshipList.php @@ -46,6 +46,26 @@ final class PhabricatorObjectRelationshipList extends Phobject { return $this->relationships; } + public function newActionSubmenu(array $keys) { + $object = $this->getObject(); + + $actions = array(); + + foreach ($keys as $key) { + $relationship = $this->getRelationship($key); + if (!$relationship) { + throw new Exception( + pht( + 'No object relationship of type "%s" exists.', + $key)); + } + + $actions[$key] = $relationship->newAction($object); + } + + return $this->newMenuWithActions($actions); + } + public function newActionMenu() { $relationships = $this->getRelationships(); $object = $this->getObject(); @@ -65,9 +85,22 @@ final class PhabricatorObjectRelationshipList extends Phobject { $actions = msort($actions, 'getName'); - return id(new PhabricatorActionView()) + return $this->newMenuWithActions($actions) ->setName(pht('Edit Related Objects...')) - ->setIcon('fa-link') + ->setIcon('fa-link'); + } + + private function newMenuWithActions(array $actions) { + $any_enabled = false; + foreach ($actions as $action) { + if (!$action->getDisabled()) { + $any_enabled = true; + break; + } + } + + return id(new PhabricatorActionView()) + ->setDisabled(!$any_enabled) ->setSubmenu($actions); } diff --git a/src/view/layout/PhabricatorActionView.php b/src/view/layout/PhabricatorActionView.php index 7a7cc64c3b..493f8c3dbd 100644 --- a/src/view/layout/PhabricatorActionView.php +++ b/src/view/layout/PhabricatorActionView.php @@ -84,6 +84,10 @@ final class PhabricatorActionView extends AphrontView { return $this; } + public function getDisabled() { + return $this->disabled; + } + public function setWorkflow($workflow) { $this->workflow = $workflow; return $this; From 163f2c4262a2f3e6960d8768377275c399a3f81f Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 30 Jun 2016 10:51:14 -0700 Subject: [PATCH 16/35] Refine available filters and defaults for relationship selection Summary: Ref T4788. Fixes T10703. In the longer term I want to put this on top of ApplicationSearch, but that's somewhat complex and we're at a fairly good point to pause this feature for feedback. Inch toward that instead: provide more appropriate filters and defaults without rebuilding the underlying engine. Specifically: - No "assigned" for commits (barely makes sense). - No "assigned" for mocks (does not make sense). - Default to "open" for parent tasks, subtasks, close as duplicate, and merge into. Also, add a key to the `search_document` table to improve the performance of the "all open stuff of type X" query. "All Open Tasks" is about 100x faster on my machine with this key. Test Plan: - Clicked all object relationships, saw more sensible filters and defaults. - Saw "open" query about 100x faster locally (300ms to 3ms). Reviewers: chad Reviewed By: chad Maniphest Tasks: T4788, T10703 Differential Revision: https://secure.phabricator.com/D16202 --- ...iphestTaskCloseAsDuplicateRelationship.php | 3 +- .../ManiphestTaskHasParentRelationship.php | 3 +- .../ManiphestTaskHasSubtaskRelationship.php | 3 +- .../ManiphestTaskMergeInRelationship.php | 3 +- ...habricatorSearchRelationshipController.php | 15 ++++------ .../DiffusionCommitRelationshipSource.php | 6 ++++ .../PhabricatorObjectRelationshipSource.php | 29 +++++++++++++++++++ .../PholioMockRelationshipSource.php | 6 ++++ .../document/PhabricatorSearchDocument.php | 3 ++ 9 files changed, 58 insertions(+), 13 deletions(-) diff --git a/src/applications/maniphest/relationship/ManiphestTaskCloseAsDuplicateRelationship.php b/src/applications/maniphest/relationship/ManiphestTaskCloseAsDuplicateRelationship.php index 696ceb615e..6428c79b55 100644 --- a/src/applications/maniphest/relationship/ManiphestTaskCloseAsDuplicateRelationship.php +++ b/src/applications/maniphest/relationship/ManiphestTaskCloseAsDuplicateRelationship.php @@ -38,7 +38,8 @@ final class ManiphestTaskCloseAsDuplicateRelationship } protected function newRelationshipSource() { - return new ManiphestTaskRelationshipSource(); + return id(new ManiphestTaskRelationshipSource()) + ->setSelectedFilter('open'); } public function getRequiredRelationshipCapabilities() { diff --git a/src/applications/maniphest/relationship/ManiphestTaskHasParentRelationship.php b/src/applications/maniphest/relationship/ManiphestTaskHasParentRelationship.php index 6210e3c4d4..6cfd11ff09 100644 --- a/src/applications/maniphest/relationship/ManiphestTaskHasParentRelationship.php +++ b/src/applications/maniphest/relationship/ManiphestTaskHasParentRelationship.php @@ -38,7 +38,8 @@ final class ManiphestTaskHasParentRelationship } protected function newRelationshipSource() { - return new ManiphestTaskRelationshipSource(); + return id(new ManiphestTaskRelationshipSource()) + ->setSelectedFilter('open'); } } diff --git a/src/applications/maniphest/relationship/ManiphestTaskHasSubtaskRelationship.php b/src/applications/maniphest/relationship/ManiphestTaskHasSubtaskRelationship.php index 335ac03d42..2fb69e90fb 100644 --- a/src/applications/maniphest/relationship/ManiphestTaskHasSubtaskRelationship.php +++ b/src/applications/maniphest/relationship/ManiphestTaskHasSubtaskRelationship.php @@ -38,7 +38,8 @@ final class ManiphestTaskHasSubtaskRelationship } protected function newRelationshipSource() { - return new ManiphestTaskRelationshipSource(); + return id(new ManiphestTaskRelationshipSource()) + ->setSelectedFilter('open'); } } diff --git a/src/applications/maniphest/relationship/ManiphestTaskMergeInRelationship.php b/src/applications/maniphest/relationship/ManiphestTaskMergeInRelationship.php index 5bf45b7911..c8442c428f 100644 --- a/src/applications/maniphest/relationship/ManiphestTaskMergeInRelationship.php +++ b/src/applications/maniphest/relationship/ManiphestTaskMergeInRelationship.php @@ -38,7 +38,8 @@ final class ManiphestTaskMergeInRelationship } protected function newRelationshipSource() { - return new ManiphestTaskRelationshipSource(); + return id(new ManiphestTaskRelationshipSource()) + ->setSelectedFilter('open'); } public function getRequiredRelationshipCapabilities() { diff --git a/src/applications/search/controller/PhabricatorSearchRelationshipController.php b/src/applications/search/controller/PhabricatorSearchRelationshipController.php index 767c756813..14c705ac62 100644 --- a/src/applications/search/controller/PhabricatorSearchRelationshipController.php +++ b/src/applications/search/controller/PhabricatorSearchRelationshipController.php @@ -150,14 +150,6 @@ final class PhabricatorSearchRelationshipController $handles = iterator_to_array($handles); $handles = array_select_keys($handles, $dst_phids); - // TODO: These are hard-coded for now. - $filters = array( - 'assigned' => pht('Assigned to Me'), - 'created' => pht('Created By Me'), - 'open' => pht('All Open Objects'), - 'all' => pht('All Objects'), - ); - $dialog_title = $relationship->getDialogTitleText(); $dialog_header = $relationship->getDialogHeaderText(); $dialog_button = $relationship->getDialogButtonText(); @@ -165,12 +157,17 @@ final class PhabricatorSearchRelationshipController $source_uri = $relationship->getSourceURI($object); + $source = $relationship->newSource(); + + $filters = $source->getFilters(); + $selected_filter = $source->getSelectedFilter(); + return id(new PhabricatorObjectSelectorDialog()) ->setUser($viewer) ->setInitialPHIDs($initial_phids) ->setHandles($handles) ->setFilters($filters) - ->setSelectedFilter('created') + ->setSelectedFilter($selected_filter) ->setExcluded($src_phid) ->setCancelURI($done_uri) ->setSearchURI($source_uri) diff --git a/src/applications/search/relationship/DiffusionCommitRelationshipSource.php b/src/applications/search/relationship/DiffusionCommitRelationshipSource.php index 31fc918011..25c799caf4 100644 --- a/src/applications/search/relationship/DiffusionCommitRelationshipSource.php +++ b/src/applications/search/relationship/DiffusionCommitRelationshipSource.php @@ -17,4 +17,10 @@ final class DiffusionCommitRelationshipSource ); } + public function getFilters() { + $filters = parent::getFilters(); + unset($filters['assigned']); + return $filters; + } + } diff --git a/src/applications/search/relationship/PhabricatorObjectRelationshipSource.php b/src/applications/search/relationship/PhabricatorObjectRelationshipSource.php index a1fc9a6370..9990740b4f 100644 --- a/src/applications/search/relationship/PhabricatorObjectRelationshipSource.php +++ b/src/applications/search/relationship/PhabricatorObjectRelationshipSource.php @@ -3,6 +3,7 @@ abstract class PhabricatorObjectRelationshipSource extends Phobject { private $viewer; + private $selectedFilter; final public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; @@ -16,4 +17,32 @@ abstract class PhabricatorObjectRelationshipSource extends Phobject { abstract public function isEnabledForObject($object); abstract public function getResultPHIDTypes(); + protected function getDefaultFilter() { + return 'created'; + } + + final public function setSelectedFilter($selected_filter) { + $this->selectedFilter = $selected_filter; + return $this; + } + + final public function getSelectedFilter() { + if ($this->selectedFilter === null) { + return $this->getDefaultFilter(); + } + + return $this->selectedFilter; + } + + public function getFilters() { + // TODO: These are hard-coded for now, and all of this will probably be + // rewritten when we move to ApplicationSearch. + return array( + 'assigned' => pht('Assigned to Me'), + 'created' => pht('Created By Me'), + 'open' => pht('All Open Objects'), + 'all' => pht('All Objects'), + ); + } + } diff --git a/src/applications/search/relationship/PholioMockRelationshipSource.php b/src/applications/search/relationship/PholioMockRelationshipSource.php index b378f8ef41..b21fd6624b 100644 --- a/src/applications/search/relationship/PholioMockRelationshipSource.php +++ b/src/applications/search/relationship/PholioMockRelationshipSource.php @@ -17,4 +17,10 @@ final class PholioMockRelationshipSource ); } + public function getFilters() { + $filters = parent::getFilters(); + unset($filters['assigned']); + return $filters; + } + } diff --git a/src/applications/search/storage/document/PhabricatorSearchDocument.php b/src/applications/search/storage/document/PhabricatorSearchDocument.php index 161e791316..3e177c9813 100644 --- a/src/applications/search/storage/document/PhabricatorSearchDocument.php +++ b/src/applications/search/storage/document/PhabricatorSearchDocument.php @@ -26,6 +26,9 @@ final class PhabricatorSearchDocument extends PhabricatorSearchDAO { 'documentCreated' => array( 'columns' => array('documentCreated'), ), + 'key_type' => array( + 'columns' => array('documentType', 'documentCreated'), + ), ), ) + parent::getConfiguration(); } From 23ec515afc1d05b7c0cd4e3a911293225ba49858 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Thu, 30 Jun 2016 12:54:20 -0700 Subject: [PATCH 17/35] Improve PhamePost search options Summary: Ref T9360. This adds ability to search posts by blog(s) and by type better. Test Plan: Create some posts, search for them. {F1705961} Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Maniphest Tasks: T9360 Differential Revision: https://secure.phabricator.com/D16199 --- src/__phutil_library_map__.php | 2 + .../blog/PhameBlogViewController.php | 8 +++ .../phame/query/PhamePostSearchEngine.php | 25 ++++++--- .../phame/typeahead/PhameBlogDatasource.php | 53 +++++++++++++++++++ 4 files changed, 81 insertions(+), 7 deletions(-) create mode 100644 src/applications/phame/typeahead/PhameBlogDatasource.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 51373419d9..95b5de1304 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -3794,6 +3794,7 @@ phutil_register_library_map(array( 'PhameBlogArchiveController' => 'applications/phame/controller/blog/PhameBlogArchiveController.php', 'PhameBlogController' => 'applications/phame/controller/blog/PhameBlogController.php', 'PhameBlogCreateCapability' => 'applications/phame/capability/PhameBlogCreateCapability.php', + 'PhameBlogDatasource' => 'applications/phame/typeahead/PhameBlogDatasource.php', 'PhameBlogEditConduitAPIMethod' => 'applications/phame/conduit/PhameBlogEditConduitAPIMethod.php', 'PhameBlogEditController' => 'applications/phame/controller/blog/PhameBlogEditController.php', 'PhameBlogEditEngine' => 'applications/phame/editor/PhameBlogEditEngine.php', @@ -8698,6 +8699,7 @@ phutil_register_library_map(array( 'PhameBlogArchiveController' => 'PhameBlogController', 'PhameBlogController' => 'PhameController', 'PhameBlogCreateCapability' => 'PhabricatorPolicyCapability', + 'PhameBlogDatasource' => 'PhabricatorTypeaheadDatasource', 'PhameBlogEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'PhameBlogEditController' => 'PhameBlogController', 'PhameBlogEditEngine' => 'PhabricatorEditEngine', diff --git a/src/applications/phame/controller/blog/PhameBlogViewController.php b/src/applications/phame/controller/blog/PhameBlogViewController.php index 1e751c905b..08b23e5a95 100644 --- a/src/applications/phame/controller/blog/PhameBlogViewController.php +++ b/src/applications/phame/controller/blog/PhameBlogViewController.php @@ -139,6 +139,14 @@ final class PhameBlogViewController extends PhameLiveController { ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); + $actions->addAction( + id(new PhabricatorActionView()) + ->setUser($viewer) + ->setIcon('fa-search') + ->setHref( + $this->getApplicationURI('post/?blog='.$blog->getPHID())) + ->setName(pht('Search Posts'))); + $actions->addAction( id(new PhabricatorActionView()) ->setUser($viewer) diff --git a/src/applications/phame/query/PhamePostSearchEngine.php b/src/applications/phame/query/PhamePostSearchEngine.php index dbc248d8b2..8f81c9362e 100644 --- a/src/applications/phame/query/PhamePostSearchEngine.php +++ b/src/applications/phame/query/PhamePostSearchEngine.php @@ -18,25 +18,36 @@ final class PhamePostSearchEngine protected function buildQueryFromParameters(array $map) { $query = $this->newQuery(); - if (strlen($map['visibility'])) { - $query->withVisibility(array($map['visibility'])); + if ($map['visibility']) { + $query->withVisibility($map['visibility']); } + if ($map['blogPHIDs']) { + $query->withBlogPHIDs($map['blogPHIDs']); + } + return $query; } protected function buildCustomSearchFields() { return array( - id(new PhabricatorSearchSelectField()) + id(new PhabricatorSearchCheckboxesField()) ->setKey('visibility') ->setLabel(pht('Visibility')) ->setOptions( array( - '' => pht('All'), PhameConstants::VISIBILITY_PUBLISHED => pht('Published'), PhameConstants::VISIBILITY_DRAFT => pht('Draft'), PhameConstants::VISIBILITY_ARCHIVED => pht('Archived'), )), + id(new PhabricatorSearchDatasourceField()) + ->setLabel(pht('Blogs')) + ->setKey('blogPHIDs') + ->setAliases(array('blog', 'blogs', 'blogPHIDs')) + ->setDescription( + pht('Search for posts within certain blogs.')) + ->setDatasource(new PhameBlogDatasource()), + ); } @@ -63,13 +74,13 @@ final class PhamePostSearchEngine return $query; case 'live': return $query->setParameter( - 'visibility', PhameConstants::VISIBILITY_PUBLISHED); + 'visibility', array(PhameConstants::VISIBILITY_PUBLISHED)); case 'draft': return $query->setParameter( - 'visibility', PhameConstants::VISIBILITY_DRAFT); + 'visibility', array(PhameConstants::VISIBILITY_DRAFT)); case 'archived': return $query->setParameter( - 'visibility', PhameConstants::VISIBILITY_ARCHIVED); + 'visibility', array(PhameConstants::VISIBILITY_ARCHIVED)); } return parent::buildSavedQueryFromBuiltin($query_key); diff --git a/src/applications/phame/typeahead/PhameBlogDatasource.php b/src/applications/phame/typeahead/PhameBlogDatasource.php new file mode 100644 index 0000000000..0658dec3b9 --- /dev/null +++ b/src/applications/phame/typeahead/PhameBlogDatasource.php @@ -0,0 +1,53 @@ +getViewer(); + + $blogs = id(new PhameBlogQuery()) + ->setViewer($viewer) + ->needProfileImage(true) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->execute(); + + $results = array(); + foreach ($blogs as $blog) { + $closed = null; + + $status = $blog->getStatus(); + if ($status === PhabricatorBadgesBadge::STATUS_ARCHIVED) { + $closed = pht('Archived'); + } + + $results[] = id(new PhabricatorTypeaheadResult()) + ->setName($blog->getName()) + ->setClosed($closed) + ->addAttribute(pht('Phame Blog')) + ->setImageURI($blog->getProfileImageURI()) + ->setPHID($blog->getPHID()); + } + + $results = $this->filterResultsAgainstTokens($results); + + return $results; + } + +} From 7a315780b4fe7c8327e4e541b7f5c951cf02cc5e Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 30 Jun 2016 11:37:38 -0700 Subject: [PATCH 18/35] When using the "Close as Duplicate" relationship action, limit the UI to 1 task Summary: Ref T4788. When closing a task as a duplicate of another task, you can only select one task, since it doesn't really make sense to merge one task into several other tasks (this operation is //possible//, but probably not what anyone ever wants to do, I think?). Make the UI understand this: after you select a task, disable all of the "select" buttons in the UI to make this clear. Test Plan: - Used "Close as Duplicate", only allowed to select 1 task. - Used other editors like "Merge Duplicates In", allowed to select lots of tasks. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4788 Differential Revision: https://secure.phabricator.com/D16203 --- resources/celerity/map.php | 18 +-- ...iphestTaskCloseAsDuplicateRelationship.php | 13 +- ...habricatorSearchRelationshipController.php | 13 ++ .../PhabricatorObjectRelationship.php | 4 + .../PhabricatorObjectSelectorDialog.php | 13 ++ .../rsrc/js/core/behavior-object-selector.js | 122 +++++++++++++----- 6 files changed, 130 insertions(+), 53 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index acff94bca3..b6385b9aeb 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -11,7 +11,7 @@ return array( 'core.pkg.js' => 'f2139810', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '3e81ae60', - 'differential.pkg.js' => '01a010d6', + 'differential.pkg.js' => '634399e9', 'diffusion.pkg.css' => '91c5d3a6', 'diffusion.pkg.js' => '3a9a8bfa', 'maniphest.pkg.css' => '4845691a', @@ -495,7 +495,7 @@ return array( 'rsrc/js/core/behavior-lightbox-attachments.js' => 'f8ba29d7', 'rsrc/js/core/behavior-line-linker.js' => '1499a8cb', 'rsrc/js/core/behavior-more.js' => 'a80d0378', - 'rsrc/js/core/behavior-object-selector.js' => '9030ebef', + 'rsrc/js/core/behavior-object-selector.js' => 'e0ec7f2f', 'rsrc/js/core/behavior-oncopy.js' => '2926fff2', 'rsrc/js/core/behavior-phabricator-nav.js' => '56a1ca03', 'rsrc/js/core/behavior-phabricator-remarkup-assist.js' => '116cf19b', @@ -658,7 +658,7 @@ return array( 'javelin-behavior-phabricator-line-linker' => '1499a8cb', 'javelin-behavior-phabricator-nav' => '56a1ca03', 'javelin-behavior-phabricator-notification-example' => '8ce821c5', - 'javelin-behavior-phabricator-object-selector' => '9030ebef', + 'javelin-behavior-phabricator-object-selector' => 'e0ec7f2f', 'javelin-behavior-phabricator-oncopy' => '2926fff2', 'javelin-behavior-phabricator-remarkup-assist' => '116cf19b', 'javelin-behavior-phabricator-reveal-content' => '60821bc7', @@ -1608,12 +1608,6 @@ return array( 'javelin-dom', 'javelin-request', ), - '9030ebef' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-request', - 'javelin-util', - ), '9196fb06' => array( 'javelin-install', 'javelin-dom', @@ -2036,6 +2030,12 @@ return array( 'df5e11d2' => array( 'javelin-install', ), + 'e0ec7f2f' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-request', + 'javelin-util', + ), 'e10f8e18' => array( 'javelin-behavior', 'javelin-dom', diff --git a/src/applications/maniphest/relationship/ManiphestTaskCloseAsDuplicateRelationship.php b/src/applications/maniphest/relationship/ManiphestTaskCloseAsDuplicateRelationship.php index 6428c79b55..e973e86e76 100644 --- a/src/applications/maniphest/relationship/ManiphestTaskCloseAsDuplicateRelationship.php +++ b/src/applications/maniphest/relationship/ManiphestTaskCloseAsDuplicateRelationship.php @@ -53,16 +53,11 @@ final class ManiphestTaskCloseAsDuplicateRelationship return false; } + public function getMaximumSelectionSize() { + return 1; + } + public function willUpdateRelationships($object, array $add, array $rem) { - - // TODO: Communicate this in the UI before users hit this error. - if (count($add) > 1) { - throw new Exception( - pht( - 'A task can only be closed as a duplicate of exactly one other '. - 'task.')); - } - $task = head($add); return $this->newMergeIntoTransactions($task); } diff --git a/src/applications/search/controller/PhabricatorSearchRelationshipController.php b/src/applications/search/controller/PhabricatorSearchRelationshipController.php index 14c705ac62..d254969423 100644 --- a/src/applications/search/controller/PhabricatorSearchRelationshipController.php +++ b/src/applications/search/controller/PhabricatorSearchRelationshipController.php @@ -39,11 +39,23 @@ final class PhabricatorSearchRelationshipController $done_uri = $src_handle->getURI(); $initial_phids = $dst_phids; + $maximum = $relationship->getMaximumSelectionSize(); + if ($request->isFormPost()) { $phids = explode(';', $request->getStr('phids')); $phids = array_filter($phids); $phids = array_values($phids); + // The UI normally enforces this with Javascript, so this is just a + // sanity check and does not need to be particularly user-friendly. + if ($maximum && (count($phids) > $maximum)) { + throw new Exception( + pht( + 'Too many relationships (%s, of type "%s").', + phutil_count($phids), + $relationship->getRelationshipConstant())); + } + $initial_phids = $request->getStrList('initialPHIDs'); // Apply the changes as adds and removes relative to the original state @@ -175,6 +187,7 @@ final class PhabricatorSearchRelationshipController ->setHeader($dialog_header) ->setButtonText($dialog_button) ->setInstructions($dialog_instructions) + ->setMaximumSelectionSize($maximum) ->buildDialog(); } diff --git a/src/applications/search/relationship/PhabricatorObjectRelationship.php b/src/applications/search/relationship/PhabricatorObjectRelationship.php index 50b5299952..0827c1728c 100644 --- a/src/applications/search/relationship/PhabricatorObjectRelationship.php +++ b/src/applications/search/relationship/PhabricatorObjectRelationship.php @@ -104,6 +104,10 @@ abstract class PhabricatorObjectRelationship extends Phobject { return "/search/rel/{$type}/{$phid}/"; } + public function getMaximumSelectionSize() { + return null; + } + public function canUndoRelationship() { return true; } diff --git a/src/view/control/PhabricatorObjectSelectorDialog.php b/src/view/control/PhabricatorObjectSelectorDialog.php index 09d9582e8b..3455cbc8bd 100644 --- a/src/view/control/PhabricatorObjectSelectorDialog.php +++ b/src/view/control/PhabricatorObjectSelectorDialog.php @@ -11,6 +11,7 @@ final class PhabricatorObjectSelectorDialog extends Phobject { private $selectedFilter; private $excluded; private $initialPHIDs; + private $maximumSelectionSize; private $title; private $header; @@ -87,6 +88,15 @@ final class PhabricatorObjectSelectorDialog extends Phobject { return $this->initialPHIDs; } + public function setMaximumSelectionSize($maximum_selection_size) { + $this->maximumSelectionSize = $maximum_selection_size; + return $this; + } + + public function getMaximumSelectionSize() { + return $this->maximumSelectionSize; + } + public function buildDialog() { $user = $this->user; @@ -190,6 +200,8 @@ final class PhabricatorObjectSelectorDialog extends Phobject { $dialog->addHiddenInput('initialPHIDs', $initial_phids); } + $maximum = $this->getMaximumSelectionSize(); + Javelin::initBehavior( 'phabricator-object-selector', array( @@ -202,6 +214,7 @@ final class PhabricatorObjectSelectorDialog extends Phobject { 'exclude' => $this->excluded, 'uri' => $this->searchURI, 'handles' => $handle_views, + 'maximum' => $maximum, )); $dialog->setResizeX(true); diff --git a/webroot/rsrc/js/core/behavior-object-selector.js b/webroot/rsrc/js/core/behavior-object-selector.js index d4a542c840..686f0f8820 100644 --- a/webroot/rsrc/js/core/behavior-object-selector.js +++ b/webroot/rsrc/js/core/behavior-object-selector.js @@ -10,11 +10,13 @@ JX.behavior('phabricator-object-selector', function(config) { var n = 0; var phids = {}; + var display = []; + var handles = config.handles; for (var k in handles) { phids[k] = true; } - var button_list = {}; + var query_timer = null; var query_delay = 50; @@ -41,37 +43,82 @@ JX.behavior('phabricator-object-selector', function(config) { return; } - var display = []; - button_list = {}; + display = []; for (var k in r) { handles[r[k].phid] = r[k]; - display.push(renderHandle(r[k], true)); + display.push({phid: r[k].phid}); } - if (!display.length) { - display = renderNote('No results.'); - } - - JX.DOM.setContent(JX.$(config.results), display); + redrawList(true); } function redrawAttached() { - var display = []; + var attached = []; for (var k in phids) { - display.push(renderHandle(handles[k], false)); + attached.push(renderHandle(handles[k], false).item); } - if (!display.length) { - display = renderNote('Nothing attached.'); + if (!attached.length) { + attached = renderNote('Nothing attached.'); } - JX.DOM.setContent(JX.$(config.current), display); + JX.DOM.setContent(JX.$(config.current), attached); phid_input.value = JX.keys(phids).join(';'); } - function renderHandle(h, attach) { + function redrawList(rebuild) { + var ii; + var content; + if (rebuild) { + if (display.length) { + var handle; + + content = []; + for (ii = 0; ii < display.length; ii++) { + handle = handles[display[ii].phid]; + + display[ii].node = renderHandle(handle, true); + content.push(display[ii].node.item); + } + } else { + content = renderNote('No results.'); + } + + JX.DOM.setContent(JX.$(config.results), content); + } + + var phid; + var is_disabled; + var button; + + var at_maximum = !canSelectMore(); + + for (ii = 0; ii < display.length; ii++) { + phid = display[ii].phid; + + is_disabled = false; + + // If this object is already selected, you can not select it again. + if (phids.hasOwnProperty(phid)) { + is_disabled = true; + } + + // If the maximum number of objects are already selected, you can + // not select more. + if (at_maximum) { + is_disabled = true; + } + + button = display[ii].node.button; + JX.DOM.alterClass(button, 'disabled', is_disabled); + button.disabled = is_disabled; + } + + } + + function renderHandle(h, attach) { var some_icon = JX.$N( 'span', {className: 'phui-icon-view phui-font-fa ' + @@ -111,15 +158,10 @@ JX.behavior('phabricator-object-selector', function(config) { meta: {handle: h, table:table}}, cells)); - if (attach) { - button_list[h.phid] = select_object_button; - if (h.phid in phids) { - JX.DOM.alterClass(select_object_button, 'disabled', true); - select_object_button.disabled = true; - } - } - - return table; + return { + item: table, + button: select_object_button + }; } function renderNote(note) { @@ -138,6 +180,18 @@ JX.behavior('phabricator-object-selector', function(config) { .send(); } + function canSelectMore() { + if (!config.maximum) { + return true; + } + + if (JX.keys(phids).length < config.maximum) { + return true; + } + + return false; + } + JX.DOM.listen( JX.$(config.results), 'click', @@ -151,10 +205,13 @@ JX.behavior('phabricator-object-selector', function(config) { return; } - phids[phid] = true; - JX.DOM.alterClass(button_list[phid], 'disabled', true); - button_list[phid].disabled = true; + if (!canSelectMore()) { + return; + } + phids[phid] = true; + + redrawList(false); redrawAttached(); }); @@ -170,13 +227,7 @@ JX.behavior('phabricator-object-selector', function(config) { delete phids[phid]; - // NOTE: We may not have a button in the button list, if this result is - // not visible in the current search results. - if (button_list[phid]) { - JX.DOM.alterClass(button_list[phid], 'disabled', false); - button_list[phid].disabled = false; - } - + redrawList(false); redrawAttached(); }); @@ -205,6 +256,7 @@ JX.behavior('phabricator-object-selector', function(config) { }); sendQuery(); - redrawAttached(); + redrawList(true); + redrawAttached(); }); From 01862b8f23ac47ae14d464c6005262a3f43d3915 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 30 Jun 2016 13:26:21 -0700 Subject: [PATCH 19/35] Detect the MIME type of large files by examining the first chunk Summary: Fixes T11242. See that task for detailed discussion. Previously, it didn't particularly matter that we don't MIME detect chunked files since they were all just big blobs of junk (PSDs, zips/tarballs, whatever) that we handled uniformly. However, videos are large and the MIME type also matters. - Detect the overall mime type by detecitng the MIME type of the first chunk. This appears to work properly, at least for video. - Skip mime type detection on other chunks, which we were performing and ignoring. This makes uploading chunked files a little faster since we don't need to write stuff to disk. Test Plan: Uploaded a 50MB video locally, saw it as chunks with a "video/mp4" mime type, played it in the browser in Phabricator as an embedded HTML 5 video. {F1706837} Reviewers: chad Reviewed By: chad Maniphest Tasks: T11242 Differential Revision: https://secure.phabricator.com/D16204 --- .../FileUploadChunkConduitAPIMethod.php | 25 ++++++++++++++++++- .../files/storage/PhabricatorFile.php | 6 ++--- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/applications/files/conduit/FileUploadChunkConduitAPIMethod.php b/src/applications/files/conduit/FileUploadChunkConduitAPIMethod.php index e3444d05e2..949bdde28a 100644 --- a/src/applications/files/conduit/FileUploadChunkConduitAPIMethod.php +++ b/src/applications/files/conduit/FileUploadChunkConduitAPIMethod.php @@ -51,6 +51,16 @@ final class FileUploadChunkConduitAPIMethod $start, $start + $length); + // If this is the initial chunk, leave the MIME type unset so we detect + // it and can update the parent file. If this is any other chunk, it has + // no meaningful MIME type. Provide a default type so we can avoid writing + // it to disk to perform MIME type detection. + if (!$start) { + $mime_type = null; + } else { + $mime_type = 'application/octet-stream'; + } + // NOTE: These files have a view policy which prevents normal access. They // are only accessed through the storage engine. $chunk_data = PhabricatorFile::newFromFileData( @@ -58,13 +68,26 @@ final class FileUploadChunkConduitAPIMethod array( 'name' => $file->getMonogram().'.chunk-'.$chunk->getID(), 'viewPolicy' => PhabricatorPolicies::POLICY_NOONE, + 'mime-type' => $mime_type, )); $chunk->setDataFilePHID($chunk_data->getPHID())->save(); + $needs_update = false; + $missing = $this->loadAnyMissingChunk($viewer, $file); if (!$missing) { - $file->setIsPartial(0)->save(); + $file->setIsPartial(0); + $needs_update = true; + } + + if (!$start) { + $file->setMimeType($chunk_data->getMimeType()); + $needs_update = true; + } + + if ($needs_update) { + $file->save(); } return null; diff --git a/src/applications/files/storage/PhabricatorFile.php b/src/applications/files/storage/PhabricatorFile.php index 4e43e3dd18..7e8f4b18fc 100644 --- a/src/applications/files/storage/PhabricatorFile.php +++ b/src/applications/files/storage/PhabricatorFile.php @@ -270,10 +270,8 @@ final class PhabricatorFile extends PhabricatorFileDAO $file->setByteSize($length); - // TODO: We might be able to test the first chunk in order to figure - // this out more reliably, since MIME detection usually examines headers. - // However, enormous files are probably always either actually raw data - // or reasonable to treat like raw data. + // NOTE: Once we receive the first chunk, we'll detect its MIME type and + // update the parent file. This matters for large media files like video. $file->setMimeType('application/octet-stream'); $chunked_hash = idx($params, 'chunkedHash'); From 189910d6157c46af642bbe903a0b884b4a18c5b4 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 30 Jun 2016 15:57:32 -0700 Subject: [PATCH 20/35] Make TabGroups a standalone UI element Summary: Ref T10628. Currently, tabs are part of ObjectBoxes. However, the code is a bit of a mess and I want to use them in some other contexts, notably the "prose diff" dialog to show "old raw, new raw, diff". Pull them out, and update Files to use the new stuff. My plan is: - Update all callsites to this stuff. - Remove the builtin-in ObjectBox integration to simplify ObjectBox a bit. - Move forward with T10628. This is pretty straightforward. A couple of the sigils are a little weird, but I'll update the JS later. For now, the same JS can drive both old and new tabs. Test Plan: Viewed files, everything was unchanged. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10628 Differential Revision: https://secure.phabricator.com/D16205 --- src/__phutil_library_map__.php | 4 + .../PhabricatorFileInfoController.php | 39 +++++++-- src/view/phui/PHUIObjectBoxView.php | 9 +- src/view/phui/PHUITabGroupView.php | 83 +++++++++++++++++++ src/view/phui/PHUITabView.php | 74 +++++++++++++++++ 5 files changed, 202 insertions(+), 7 deletions(-) create mode 100644 src/view/phui/PHUITabGroupView.php create mode 100644 src/view/phui/PHUITabView.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 95b5de1304..5ad020ec06 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1663,6 +1663,8 @@ phutil_register_library_map(array( 'PHUISpacesNamespaceContextView' => 'applications/spaces/view/PHUISpacesNamespaceContextView.php', 'PHUIStatusItemView' => 'view/phui/PHUIStatusItemView.php', 'PHUIStatusListView' => 'view/phui/PHUIStatusListView.php', + 'PHUITabGroupView' => 'view/phui/PHUITabGroupView.php', + 'PHUITabView' => 'view/phui/PHUITabView.php', 'PHUITagExample' => 'applications/uiexample/examples/PHUITagExample.php', 'PHUITagView' => 'view/phui/PHUITagView.php', 'PHUITimelineEventView' => 'view/phui/PHUITimelineEventView.php', @@ -6194,6 +6196,8 @@ phutil_register_library_map(array( 'PHUISpacesNamespaceContextView' => 'AphrontView', 'PHUIStatusItemView' => 'AphrontTagView', 'PHUIStatusListView' => 'AphrontTagView', + 'PHUITabGroupView' => 'AphrontTagView', + 'PHUITabView' => 'AphrontTagView', 'PHUITagExample' => 'PhabricatorUIExample', 'PHUITagView' => 'AphrontTagView', 'PHUITimelineEventView' => 'AphrontView', diff --git a/src/applications/files/controller/PhabricatorFileInfoController.php b/src/applications/files/controller/PhabricatorFileInfoController.php index 9633a3f956..3c822b401c 100644 --- a/src/applications/files/controller/PhabricatorFileInfoController.php +++ b/src/applications/files/controller/PhabricatorFileInfoController.php @@ -182,8 +182,16 @@ final class PhabricatorFileInfoController extends PhabricatorFileController { $request = $this->getRequest(); $viewer = $request->getUser(); + $tab_group = id(new PHUITabGroupView()); + $box->addTabGroup($tab_group); + $properties = id(new PHUIPropertyListView()); - $box->addPropertyList($properties, pht('Details')); + + $tab_group->addTab( + id(new PHUITabView()) + ->setName(pht('Details')) + ->setKey('details') + ->appendChild($properties)); if ($file->getAuthorPHID()) { $properties->addProperty( @@ -195,9 +203,13 @@ final class PhabricatorFileInfoController extends PhabricatorFileController { pht('Created'), phabricator_datetime($file->getDateCreated(), $viewer)); - $finfo = id(new PHUIPropertyListView()); - $box->addPropertyList($finfo, pht('File Info')); + + $tab_group->addTab( + id(new PHUITabView()) + ->setName(pht('File Info')) + ->setKey('info') + ->appendChild($finfo)); $finfo->addProperty( pht('Size'), @@ -262,7 +274,12 @@ final class PhabricatorFileInfoController extends PhabricatorFileController { } $storage_properties = new PHUIPropertyListView(); - $box->addPropertyList($storage_properties, pht('Storage')); + + $tab_group->addTab( + id(new PHUITabView()) + ->setName(pht('Storage')) + ->setKey('storage') + ->appendChild($storage_properties)); $storage_properties->addProperty( pht('Engine'), @@ -285,7 +302,12 @@ final class PhabricatorFileInfoController extends PhabricatorFileController { $phids = $file->getObjectPHIDs(); if ($phids) { $attached = new PHUIPropertyListView(); - $box->addPropertyList($attached, pht('Attached')); + + $tab_group->addTab( + id(new PHUITabView()) + ->setName(pht('Attached')) + ->setKey('attached') + ->appendChild($attached)); $attached->addProperty( pht('Attached To'), @@ -357,7 +379,12 @@ final class PhabricatorFileInfoController extends PhabricatorFileController { if ($engine) { if ($engine->isChunkEngine()) { $chunkinfo = new PHUIPropertyListView(); - $box->addPropertyList($chunkinfo, pht('Chunks')); + + $tab_group->addTab( + id(new PHUITabView()) + ->setName(pht('Chunks')) + ->setKey('chunks') + ->appendChild($chunkinfo)); $chunks = id(new PhabricatorFileChunkQuery()) ->setViewer($viewer) diff --git a/src/view/phui/PHUIObjectBoxView.php b/src/view/phui/PHUIObjectBoxView.php index 9fa280a843..41e176173f 100644 --- a/src/view/phui/PHUIObjectBoxView.php +++ b/src/view/phui/PHUIObjectBoxView.php @@ -5,6 +5,7 @@ final class PHUIObjectBoxView extends AphrontTagView { private $headerText; private $color; private $background; + private $tabGroups = array(); private $formErrors = null; private $formSaved = false; private $infoView; @@ -128,6 +129,11 @@ final class PHUIObjectBoxView extends AphrontTagView { return $this; } + public function addTabGroup(PHUITabGroupView $view) { + $this->tabGroups[] = $view; + return $this; + } + public function setInfoView(PHUIInfoView $view) { $this->infoView = $view; return $this; @@ -211,7 +217,7 @@ final class PHUIObjectBoxView extends AphrontTagView { $i = 0; foreach ($list as $item) { $group->addPropertyList($item); - if ($i > 0) { + if ($i > 0 || $this->tabGroups) { $item->addClass('phui-property-list-section-noninitial'); } $i++; @@ -405,6 +411,7 @@ final class PHUIObjectBoxView extends AphrontTagView { $this->formSaved, $exception_errors, $this->form, + $this->tabGroups, $tabs, $this->tabLists, $showhide, diff --git a/src/view/phui/PHUITabGroupView.php b/src/view/phui/PHUITabGroupView.php new file mode 100644 index 0000000000..1f6739a7c0 --- /dev/null +++ b/src/view/phui/PHUITabGroupView.php @@ -0,0 +1,83 @@ +getKey(); + $tab->lockKey(); + + if (isset($this->tabs[$key])) { + throw new Exception( + pht( + 'Each tab in a tab group must have a unique key; attempting to add '. + 'a second tab with a duplicate key ("%s").', + $key)); + } + + $this->tabs[$key] = $tab; + + return $this; + } + + public function getSelectedTab() { + if (!$this->tabs) { + return null; + } + + return head($this->tabs)->getKey(); + } + + protected function getTagAttributes() { + $tab_map = mpull($this->tabs, 'getContentID', 'getKey'); + + return array( + 'sigil' => 'phui-object-box', + 'meta' => array( + 'tabMap' => $tab_map, + ), + ); + } + + protected function getTagContent() { + Javelin::initBehavior('phui-object-box-tabs'); + + $tabs = id(new PHUIListView()) + ->setType(PHUIListView::NAVBAR_LIST); + $content = array(); + + $selected_tab = $this->getSelectedTab(); + foreach ($this->tabs as $tab) { + $item = $tab->newMenuItem(); + $tab_key = $tab->getKey(); + + if ($tab_key == $selected_tab) { + $item->setSelected(true); + $style = null; + } else { + $style = 'display: none;'; + } + + $tabs->addMenuItem($item); + + $content[] = javelin_tag( + 'div', + array( + 'style' => $style, + 'id' => $tab->getContentID(), + ), + $tab); + } + + return array( + $tabs, + $content, + ); + } + +} diff --git a/src/view/phui/PHUITabView.php b/src/view/phui/PHUITabView.php new file mode 100644 index 0000000000..d037eb0a7b --- /dev/null +++ b/src/view/phui/PHUITabView.php @@ -0,0 +1,74 @@ +keyLocked) { + throw new Exception( + pht( + 'Attempting to change the key of a tab with a locked key ("%s").', + $this->key)); + } + + $this->key = $key; + return $this; + } + + public function hasKey() { + return ($this->key !== null); + } + + public function getKey() { + if (!$this->hasKey()) { + throw new PhutilInvalidStateException('setKey'); + } + + return $this->key; + } + + public function lockKey() { + if (!$this->hasKey()) { + throw new PhutilInvalidStateException('setKey'); + } + + $this->keyLocked = true; + + return $this; + } + + public function setName($name) { + $this->name = $name; + return $this; + } + + public function getName() { + return $this->name; + } + + public function getContentID() { + if ($this->contentID === null) { + $this->contentID = celerity_generate_unique_node_id(); + } + + return $this->contentID; + } + + public function newMenuItem() { + return id(new PHUIListItemView()) + ->setName($this->getName()) + ->setKey($this->getKey()) + ->setType(PHUIListItemView::TYPE_LINK) + ->setHref('#') + ->addSigil('phui-object-box-tab') + ->setMetadata( + array( + 'tabKey' => $this->getKey(), + )); + } + +} From 5a4ecc7a9c5bbf39f905df487e2c0600885882fa Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 30 Jun 2016 16:08:56 -0700 Subject: [PATCH 21/35] Convert "Diff Details" tabs to PHUITabGroup Summary: Ref T10628. Switch this to be nicer and more modern. - When there's only one tab, add an option to hide it. Test Plan: - Viewed normal revisions (no tabs). - Viewed X vs Y revisions (two tabs, rightmost tab selected by default). Reviewers: chad Reviewed By: chad Maniphest Tasks: T10628 Differential Revision: https://secure.phabricator.com/D16206 --- .../DifferentialRevisionViewController.php | 26 ++++++------- src/view/phui/PHUITabGroupView.php | 37 ++++++++++++++++++- 2 files changed, 47 insertions(+), 16 deletions(-) diff --git a/src/applications/differential/controller/DifferentialRevisionViewController.php b/src/applications/differential/controller/DifferentialRevisionViewController.php index dfbcaa74ff..f2078a57de 100644 --- a/src/applications/differential/controller/DifferentialRevisionViewController.php +++ b/src/applications/differential/controller/DifferentialRevisionViewController.php @@ -1031,28 +1031,26 @@ final class DifferentialRevisionViewController extends DifferentialController { ); } - $box = id(new PHUIObjectBoxView()) - ->setHeaderText(pht('Diff Detail')) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->setUser($viewer); + $tab_group = id(new PHUITabGroupView()) + ->setHideSingleTab(true); - $last_tab = null; foreach ($property_lists as $key => $property_list) { list($tab_name, $list_view) = $property_list; - $tab = id(new PHUIListItemView()) + $tab = id(new PHUITabView()) ->setKey($key) - ->setName($tab_name); + ->setName($tab_name) + ->appendChild($list_view); - $box->addPropertyList($list_view, $tab); - $last_tab = $tab; + $tab_group->addTab($tab); + $tab_group->selectTab($key); } - if ($last_tab) { - $last_tab->setSelected(true); - } - - return $box; + return id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Diff Detail')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setUser($viewer) + ->addTabGroup($tab_group); } private function buildDiffPropertyList( diff --git a/src/view/phui/PHUITabGroupView.php b/src/view/phui/PHUITabGroupView.php index 1f6739a7c0..6037343852 100644 --- a/src/view/phui/PHUITabGroupView.php +++ b/src/view/phui/PHUITabGroupView.php @@ -3,11 +3,23 @@ final class PHUITabGroupView extends AphrontTagView { private $tabs = array(); + private $selectedTab; + + private $hideSingleTab; protected function canAppendChild() { return false; } + public function setHideSingleTab($hide_single_tab) { + $this->hideSingleTab = $hide_single_tab; + return $this; + } + + public function getHideSingleTab() { + return $this->hideSingleTab; + } + public function addTab(PHUITabView $tab) { $key = $tab->getKey(); $tab->lockKey(); @@ -25,11 +37,28 @@ final class PHUITabGroupView extends AphrontTagView { return $this; } - public function getSelectedTab() { + public function selectTab($key) { + if (empty($this->tabs[$key])) { + throw new Exception( + pht( + 'Unable to select tab ("%s") which does not exist.', + $key)); + } + + $this->selectedTab = $key; + + return $this; + } + + public function getSelectedTabKey() { if (!$this->tabs) { return null; } + if ($this->selectedTab !== null) { + return $this->selectedTab; + } + return head($this->tabs)->getKey(); } @@ -51,7 +80,7 @@ final class PHUITabGroupView extends AphrontTagView { ->setType(PHUIListView::NAVBAR_LIST); $content = array(); - $selected_tab = $this->getSelectedTab(); + $selected_tab = $this->getSelectedTabKey(); foreach ($this->tabs as $tab) { $item = $tab->newMenuItem(); $tab_key = $tab->getKey(); @@ -74,6 +103,10 @@ final class PHUITabGroupView extends AphrontTagView { $tab); } + if ($this->hideSingleTab && (count($this->tabs) == 1)) { + $tabs = null; + } + return array( $tabs, $content, From 65980ac68347c434ff3f326b8a5b0ab062dfda3d Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 30 Jun 2016 16:28:46 -0700 Subject: [PATCH 22/35] Convert all remaining old tabs to new PHUITabGroupViews Summary: Ref T10628. This moves everything else over. I'll clean up the cruft in the next diff. Test Plan: - Viewed Conduit API page, toggled tabs. - Viewed Harbormaster build, toggled tabs. - Viewed a Drydock lease, swapped tabs. - Viewed a Drydock resource, swapped tabs. - Viewed mail, swapped tabs. - Grepped for `addPropertyList(...)`, looked for any remaining calls with a second argument. - Also checked rSAAS for any calls, but we don't have anything there that uses tabs. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10628 Differential Revision: https://secure.phabricator.com/D16207 --- .../PhabricatorConduitController.php | 21 ++++++-- .../controller/DrydockLeaseViewController.php | 21 ++++++-- .../DrydockResourceViewController.php | 24 ++++++++-- .../HarbormasterBuildViewController.php | 48 ++++++++++++++----- .../PhabricatorMetaMTAMailViewController.php | 27 +++++++++-- .../examples/PHUIPropertyListExample.php | 44 ++++++++--------- 6 files changed, 136 insertions(+), 49 deletions(-) diff --git a/src/applications/conduit/controller/PhabricatorConduitController.php b/src/applications/conduit/controller/PhabricatorConduitController.php index b29c05f2af..6187421e16 100644 --- a/src/applications/conduit/controller/PhabricatorConduitController.php +++ b/src/applications/conduit/controller/PhabricatorConduitController.php @@ -60,13 +60,28 @@ abstract class PhabricatorConduitController extends PhabricatorController { ->setErrors($messages) ->setSeverity(PHUIInfoView::SEVERITY_NOTICE); + $tab_group = id(new PHUITabGroupView()) + ->addTab( + id(new PHUITabView()) + ->setName(pht('arc call-conduit')) + ->setKey('arc') + ->appendChild($arc_example)) + ->addTab( + id(new PHUITabView()) + ->setName(pht('cURL')) + ->setKey('curl') + ->appendChild($curl_example)) + ->addTab( + id(new PHUITabView()) + ->setName(pht('PHP')) + ->setKey('php') + ->appendChild($php_example)); + return id(new PHUIObjectBoxView()) ->setHeaderText(pht('Examples')) ->setInfoView($info_view) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->addPropertyList($arc_example, pht('arc call-conduit')) - ->addPropertyList($curl_example, pht('cURL')) - ->addPropertyList($php_example, pht('PHP')); + ->addTabGroup($tab_group); } private function renderExample( diff --git a/src/applications/drydock/controller/DrydockLeaseViewController.php b/src/applications/drydock/controller/DrydockLeaseViewController.php index 7166b0bef6..1583d28df2 100644 --- a/src/applications/drydock/controller/DrydockLeaseViewController.php +++ b/src/applications/drydock/controller/DrydockLeaseViewController.php @@ -45,12 +45,27 @@ final class DrydockLeaseViewController extends DrydockLeaseController { $locks = $this->buildLocksTab($lease->getPHID()); $commands = $this->buildCommandsTab($lease->getPHID()); + $tab_group = id(new PHUITabGroupView()) + ->addTab( + id(new PHUITabView()) + ->setName(pht('Properties')) + ->setKey('properties') + ->appendChild($properties)) + ->addTab( + id(new PHUITabView()) + ->setName(pht('Slot Locks')) + ->setKey('locks') + ->appendChild($locks)) + ->addTab( + id(new PHUITabView()) + ->setName(pht('Commands')) + ->setKey('commands') + ->appendChild($commands)); + $object_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Properties')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->addPropertyList($properties, pht('Properties')) - ->addPropertyList($locks, pht('Slot Locks')) - ->addPropertyList($commands, pht('Commands')); + ->addTabGroup($tab_group); $view = id(new PHUITwoColumnView()) ->setHeader($header) diff --git a/src/applications/drydock/controller/DrydockResourceViewController.php b/src/applications/drydock/controller/DrydockResourceViewController.php index c2ab4337f5..c6771007ba 100644 --- a/src/applications/drydock/controller/DrydockResourceViewController.php +++ b/src/applications/drydock/controller/DrydockResourceViewController.php @@ -49,14 +49,30 @@ final class DrydockResourceViewController extends DrydockResourceController { $locks = $this->buildLocksTab($resource->getPHID()); $commands = $this->buildCommandsTab($resource->getPHID()); - $lease_box = $this->buildLeaseBox($resource); + + $tab_group = id(new PHUITabGroupView()) + ->addTab( + id(new PHUITabView()) + ->setName(pht('Properties')) + ->setKey('properties') + ->appendChild($properties)) + ->addTab( + id(new PHUITabView()) + ->setName(pht('Slot Locks')) + ->setKey('locks') + ->appendChild($locks)) + ->addTab( + id(new PHUITabView()) + ->setName(pht('Commands')) + ->setKey('commands') + ->appendChild($commands)); $object_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Properties')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->addPropertyList($properties, pht('Properties')) - ->addPropertyList($locks, pht('Slot Locks')) - ->addPropertyList($commands, pht('Commands')); + ->addTabGroup($tab_group); + + $lease_box = $this->buildLeaseBox($resource); $view = id(new PHUITwoColumnView()) ->setHeader($header) diff --git a/src/applications/harbormaster/controller/HarbormasterBuildViewController.php b/src/applications/harbormaster/controller/HarbormasterBuildViewController.php index 6b0e7c1e0d..f78db68149 100644 --- a/src/applications/harbormaster/controller/HarbormasterBuildViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildViewController.php @@ -95,6 +95,9 @@ final class HarbormasterBuildViewController ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setHeader($header); + $tab_group = new PHUITabGroupView(); + $target_box->addTabGroup($tab_group); + $property_list = new PHUIPropertyListView(); $target_artifacts = idx($artifacts, $build_target->getPHID(), array()); @@ -178,7 +181,11 @@ final class HarbormasterBuildViewController $property_list->addProperty(pht('Status'), $status_view); - $target_box->addPropertyList($property_list, pht('Overview')); + $tab_group->addTab( + id(new PHUITabView()) + ->setName(pht('Overview')) + ->setKey('overview') + ->appendChild($property_list)); $step = $build_target->getBuildStep(); @@ -204,22 +211,34 @@ final class HarbormasterBuildViewController foreach ($details as $key => $value) { $property_list->addProperty($key, $value); } - $target_box->addPropertyList($property_list, pht('Configuration')); + $tab_group->addTab( + id(new PHUITabView()) + ->setName(pht('Configuration')) + ->setKey('configuration') + ->appendChild($property_list)); $variables = $build_target->getVariables(); - $property_list = new PHUIPropertyListView(); - $property_list->addRawContent($this->buildProperties($variables)); - $target_box->addPropertyList($property_list, pht('Variables')); + $variables_tab = $this->buildProperties($variables); + $tab_group->addTab( + id(new PHUITabView()) + ->setName(pht('Variables')) + ->setKey('variables') + ->appendChild($variables_tab)); $artifacts_tab = $this->buildArtifacts($build_target, $target_artifacts); - $property_list = new PHUIPropertyListView(); - $property_list->addRawContent($artifacts_tab); - $target_box->addPropertyList($property_list, pht('Artifacts')); + $tab_group->addTab( + id(new PHUITabView()) + ->setName(pht('Artifacts')) + ->setKey('artifacts') + ->appendChild($artifacts_tab)); $build_messages = idx($messages, $build_target->getPHID(), array()); - $property_list = new PHUIPropertyListView(); - $property_list->addRawContent($this->buildMessages($build_messages)); - $target_box->addPropertyList($property_list, pht('Messages')); + $messages_tab = $this->buildMessages($build_messages); + $tab_group->addTab( + id(new PHUITabView()) + ->setName(pht('Messages')) + ->setKey('messages') + ->appendChild($messages_tab)); $property_list = new PHUIPropertyListView(); $property_list->addProperty( @@ -228,7 +247,12 @@ final class HarbormasterBuildViewController $property_list->addProperty( pht('Build Target PHID'), $build_target->getPHID()); - $target_box->addPropertyList($property_list, pht('Metadata')); + + $tab_group->addTab( + id(new PHUITabView()) + ->setName(pht('Metadata')) + ->setKey('metadata') + ->appendChild($property_list)); $targets[] = $target_box; diff --git a/src/applications/metamta/controller/PhabricatorMetaMTAMailViewController.php b/src/applications/metamta/controller/PhabricatorMetaMTAMailViewController.php index fda471b79f..80da535a9e 100644 --- a/src/applications/metamta/controller/PhabricatorMetaMTAMailViewController.php +++ b/src/applications/metamta/controller/PhabricatorMetaMTAMailViewController.php @@ -36,13 +36,32 @@ final class PhabricatorMetaMTAMailViewController ->addTextCrumb(pht('Mail %d', $mail->getID())) ->setBorder(true); + $tab_group = id(new PHUITabGroupView()) + ->addTab( + id(new PHUITabView()) + ->setName(pht('Message')) + ->setKey('message') + ->appendChild($this->buildMessageProperties($mail))) + ->addTab( + id(new PHUITabView()) + ->setName(pht('Headers')) + ->setKey('headers') + ->appendChild($this->buildHeaderProperties($mail))) + ->addTab( + id(new PHUITabView()) + ->setName(pht('Delivery')) + ->setKey('delivery') + ->appendChild($this->buildDeliveryProperties($mail))) + ->addTab( + id(new PHUITabView()) + ->setName(pht('Metadata')) + ->setKey('metadata') + ->appendChild($this->buildMetadataProperties($mail))); + $object_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Mail')) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->addPropertyList($this->buildMessageProperties($mail), pht('Message')) - ->addPropertyList($this->buildHeaderProperties($mail), pht('Headers')) - ->addPropertyList($this->buildDeliveryProperties($mail), pht('Delivery')) - ->addPropertyList($this->buildMetadataProperties($mail), pht('Metadata')); + ->addTabGroup($tab_group); $view = id(new PHUITwoColumnView()) ->setHeader($header) diff --git a/src/applications/uiexample/examples/PHUIPropertyListExample.php b/src/applications/uiexample/examples/PHUIPropertyListExample.php index 76a39774b6..68bc13158b 100644 --- a/src/applications/uiexample/examples/PHUIPropertyListExample.php +++ b/src/applications/uiexample/examples/PHUIPropertyListExample.php @@ -16,24 +16,6 @@ final class PHUIPropertyListExample extends PhabricatorUIExample { $request = $this->getRequest(); $user = $request->getUser(); - $details1 = id(new PHUIListItemView()) - ->setName(pht('Details')) - ->setSelected(true); - - $details2 = id(new PHUIListItemView()) - ->setName(pht('Rainbow Info')) - ->setStatusColor(PHUIListItemView::STATUS_WARN); - - $details3 = id(new PHUIListItemView()) - ->setName(pht('Pasta Haiku')) - ->setStatusColor(PHUIListItemView::STATUS_FAIL); - - $statustabs = id(new PHUIListView()) - ->setType(PHUIListView::NAVBAR_LIST) - ->addMenuItem($details1) - ->addMenuItem($details2) - ->addMenuItem($details3); - $view = new PHUIPropertyListView(); $view->addProperty( @@ -54,7 +36,6 @@ final class PHUIPropertyListExample extends PhabricatorUIExample { 'viverra. Nunc tempus tempor quam id iaculis. Maecenas lectus '. 'velit, aliquam et consequat quis, tincidunt id dolor.'); - $view2 = new PHUIPropertyListView(); $view2->addSectionHeader(pht('Colors of the Rainbow')); @@ -66,7 +47,6 @@ final class PHUIPropertyListExample extends PhabricatorUIExample { $view2->addProperty('I', pht('Indigo')); $view2->addProperty('V', pht('Violet')); - $view3 = new PHUIPropertyListView(); $view3->addSectionHeader(pht('Haiku About Pasta')); @@ -77,11 +57,29 @@ final class PHUIPropertyListExample extends PhabricatorUIExample { pht('haiku. it is very bad.'), pht('what did you expect?'))); + $details1 = id(new PHUITabView()) + ->setName(pht('Details')) + ->setKey('details') + ->appendChild($view); + + $details2 = id(new PHUITabView()) + ->setName(pht('Rainbow Info')) + ->setKey('rainbow') + ->appendChild($view2); + + $details3 = id(new PHUITabView()) + ->setName(pht('Pasta Haiku')) + ->setKey('haiku') + ->appendChild($view3); + + $tab_group = id(new PHUITabGroupView()) + ->addTab($details1) + ->addTab($details2) + ->addTab($details3); + $object_box1 = id(new PHUIObjectBoxView()) ->setHeaderText(pht('%s Stackered', 'PHUIPropertyListView')) - ->addPropertyList($view, $details1) - ->addPropertyList($view2, $details2) - ->addPropertyList($view3, $details3); + ->addTabGroup($tab_group); $edge_cases_view = new PHUIPropertyListView(); From 2c43d055b1919bdc915a0efe6de7b48ac6c38555 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 30 Jun 2016 16:55:28 -0700 Subject: [PATCH 23/35] Remove old ObjectBox tab cruft Summary: Ref T10628. Cleans up remaining weird, unused tab behaviors in ObjectBoxView to simplify ObjectBox. Test Plan: Toggled tabs in Files. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10628 Differential Revision: https://secure.phabricator.com/D16208 --- resources/celerity/map.php | 14 +- src/view/phui/PHUIObjectBoxView.php | 146 ++---------------- src/view/phui/PHUITabGroupView.php | 4 +- src/view/phui/PHUITabView.php | 2 +- ...box-tabs.js => behavior-phui-tab-group.js} | 18 ++- 5 files changed, 37 insertions(+), 147 deletions(-) rename webroot/rsrc/js/phui/{behavior-phui-object-box-tabs.js => behavior-phui-tab-group.js} (55%) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index b6385b9aeb..b53242b6fd 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -518,9 +518,9 @@ return array( 'rsrc/js/core/phtize.js' => 'd254d646', 'rsrc/js/phui/behavior-phui-dropdown-menu.js' => '1aa4c968', 'rsrc/js/phui/behavior-phui-file-upload.js' => 'b003d4fb', - 'rsrc/js/phui/behavior-phui-object-box-tabs.js' => '2bfa2836', 'rsrc/js/phui/behavior-phui-profile-menu.js' => '12884df9', 'rsrc/js/phui/behavior-phui-submenu.js' => 'a6f7a73b', + 'rsrc/js/phui/behavior-phui-tab-group.js' => '0a0b10e9', 'rsrc/js/phuix/PHUIXActionListView.js' => 'b5c256b8', 'rsrc/js/phuix/PHUIXActionView.js' => '8cf6d262', 'rsrc/js/phuix/PHUIXAutocomplete.js' => '9196fb06', @@ -673,9 +673,9 @@ return array( 'javelin-behavior-phui-dropdown-menu' => '1aa4c968', 'javelin-behavior-phui-file-upload' => 'b003d4fb', 'javelin-behavior-phui-hovercards' => 'bcaccd64', - 'javelin-behavior-phui-object-box-tabs' => '2bfa2836', 'javelin-behavior-phui-profile-menu' => '12884df9', 'javelin-behavior-phui-submenu' => 'a6f7a73b', + 'javelin-behavior-phui-tab-group' => '0a0b10e9', 'javelin-behavior-policy-control' => 'd0c516d5', 'javelin-behavior-policy-rule-editor' => '5e9f347c', 'javelin-behavior-project-boards' => '14a1faae', @@ -977,6 +977,11 @@ return array( 'javelin-stratcom', 'javelin-vector', ), + '0a0b10e9' => array( + 'javelin-behavior', + 'javelin-stratcom', + 'javelin-dom', + ), '0a3f3021' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1107,11 +1112,6 @@ return array( 'javelin-install', 'javelin-util', ), - '2bfa2836' => array( - 'javelin-behavior', - 'javelin-stratcom', - 'javelin-dom', - ), '2c426492' => array( 'javelin-behavior', 'javelin-dom', diff --git a/src/view/phui/PHUIObjectBoxView.php b/src/view/phui/PHUIObjectBoxView.php index 41e176173f..5c6cef49a3 100644 --- a/src/view/phui/PHUIObjectBoxView.php +++ b/src/view/phui/PHUIObjectBoxView.php @@ -25,11 +25,7 @@ final class PHUIObjectBoxView extends AphrontTagView { private $showHideContent; private $showHideOpen; - private $tabs = array(); - private $tabMap = null; - private $tabLists = array(); private $propertyLists = array(); - private $propertyList = null; const COLOR_RED = 'red'; const COLOR_BLUE = 'blue'; @@ -40,48 +36,8 @@ final class PHUIObjectBoxView extends AphrontTagView { const BLUE_PROPERTY = 'phui-box-blue-property'; const GREY = 'phui-box-grey'; - public function addPropertyList( - PHUIPropertyListView $property_list, - $tab = null) { - - if (!($tab instanceof PHUIListItemView) && - ($tab !== null)) { - assert_stringlike($tab); - $tab = id(new PHUIListItemView())->setName($tab); - } - - if ($tab) { - if ($tab->getKey()) { - $key = $tab->getKey(); - } else { - $key = 'tab.default.'.spl_object_hash($tab); - $tab->setKey($key); - } - } else { - $key = 'tab.default'; - } - - if ($tab) { - if (empty($this->tabs[$key])) { - $tab->addSigil('phui-object-box-tab'); - $tab->setMetadata( - array( - 'tabKey' => $key, - )); - - if (!$tab->getHref()) { - $tab->setHref('#'); - } - - if (!$tab->getType()) { - $tab->setType(PHUIListItemView::TYPE_LINK); - } - - $this->tabs[$key] = $tab; - } - } - - $this->propertyLists[$key][] = $property_list; + public function addPropertyList(PHUIPropertyListView $property_list) { + $this->propertyLists[] = $property_list; $action_list = $property_list->getActionList(); if ($action_list) { @@ -190,68 +146,6 @@ final class PHUIObjectBoxView extends AphrontTagView { return $this; } - public function willRender() { - $tab_lists = array(); - $property_lists = array(); - $tab_map = array(); - - $default_key = 'tab.default'; - - // Find the selected tab, or select the first tab if none are selected. - if ($this->tabs) { - $selected_tab = null; - foreach ($this->tabs as $key => $tab) { - if ($tab->getSelected()) { - $selected_tab = $key; - break; - } - } - if ($selected_tab === null) { - head($this->tabs)->setSelected(true); - $selected_tab = head_key($this->tabs); - } - } - - foreach ($this->propertyLists as $key => $list) { - $group = new PHUIPropertyGroupView(); - $i = 0; - foreach ($list as $item) { - $group->addPropertyList($item); - if ($i > 0 || $this->tabGroups) { - $item->addClass('phui-property-list-section-noninitial'); - } - $i++; - } - - if ($this->tabs && $key != $default_key) { - $tab_id = celerity_generate_unique_node_id(); - $tab_map[$key] = $tab_id; - - if ($key === $selected_tab) { - $style = null; - } else { - $style = 'display: none'; - } - - $tab_lists[] = phutil_tag( - 'div', - array( - 'style' => $style, - 'id' => $tab_id, - ), - $group); - } else { - if ($this->tabs) { - $group->addClass('phui-property-group-noninitial'); - } - $property_lists[] = $group; - } - $this->propertyList = $property_lists; - $this->tabMap = $tab_map; - $this->tabLists = $tab_lists; - } - } - protected function getTagAttributes() { $classes = array(); $classes[] = 'phui-box'; @@ -275,19 +169,8 @@ final class PHUIObjectBoxView extends AphrontTagView { $classes[] = $this->background; } - $sigil = null; - $metadata = null; - if ($this->tabs) { - $sigil = 'phui-object-box'; - $metadata = array( - 'tabMap' => $this->tabMap, - ); - } - return array( 'class' => implode(' ', $classes), - 'sigil' => $sigil, - 'meta' => $metadata, ); } @@ -393,16 +276,23 @@ final class PHUIObjectBoxView extends AphrontTagView { } } - $tabs = null; - if ($this->tabs) { - $tabs = id(new PHUIListView()) - ->setType(PHUIListView::NAVBAR_LIST); - foreach ($this->tabs as $tab) { - $tabs->addMenuItem($tab); + if ($this->propertyLists) { + $lists = new PHUIPropertyGroupView(); + + $ii = 0; + foreach ($this->propertyLists as $list) { + if ($ii > 0 || $this->tabGroups) { + $list->addClass('phui-property-list-section-noninitial'); + } + + $lists->addPropertyList($list); + $ii++; } - Javelin::initBehavior('phui-object-box-tabs'); + } else { + $lists = null; } + $content = array( ($this->showHideOpen == false ? $this->anchor : null), $header, @@ -412,11 +302,9 @@ final class PHUIObjectBoxView extends AphrontTagView { $exception_errors, $this->form, $this->tabGroups, - $tabs, - $this->tabLists, $showhide, ($this->showHideOpen == true ? $this->anchor : null), - $this->propertyList, + $lists, $this->table, $this->renderChildren(), ); diff --git a/src/view/phui/PHUITabGroupView.php b/src/view/phui/PHUITabGroupView.php index 6037343852..4a1963e050 100644 --- a/src/view/phui/PHUITabGroupView.php +++ b/src/view/phui/PHUITabGroupView.php @@ -66,7 +66,7 @@ final class PHUITabGroupView extends AphrontTagView { $tab_map = mpull($this->tabs, 'getContentID', 'getKey'); return array( - 'sigil' => 'phui-object-box', + 'sigil' => 'phui-tab-group-view', 'meta' => array( 'tabMap' => $tab_map, ), @@ -74,7 +74,7 @@ final class PHUITabGroupView extends AphrontTagView { } protected function getTagContent() { - Javelin::initBehavior('phui-object-box-tabs'); + Javelin::initBehavior('phui-tab-group'); $tabs = id(new PHUIListView()) ->setType(PHUIListView::NAVBAR_LIST); diff --git a/src/view/phui/PHUITabView.php b/src/view/phui/PHUITabView.php index d037eb0a7b..670fc0759a 100644 --- a/src/view/phui/PHUITabView.php +++ b/src/view/phui/PHUITabView.php @@ -64,7 +64,7 @@ final class PHUITabView extends AphrontTagView { ->setKey($this->getKey()) ->setType(PHUIListItemView::TYPE_LINK) ->setHref('#') - ->addSigil('phui-object-box-tab') + ->addSigil('phui-tab-view') ->setMetadata( array( 'tabKey' => $this->getKey(), diff --git a/webroot/rsrc/js/phui/behavior-phui-object-box-tabs.js b/webroot/rsrc/js/phui/behavior-phui-tab-group.js similarity index 55% rename from webroot/rsrc/js/phui/behavior-phui-object-box-tabs.js rename to webroot/rsrc/js/phui/behavior-phui-tab-group.js index 85cdf9345d..23c1ca5b68 100644 --- a/webroot/rsrc/js/phui/behavior-phui-object-box-tabs.js +++ b/webroot/rsrc/js/phui/behavior-phui-tab-group.js @@ -1,23 +1,25 @@ /** - * @provides javelin-behavior-phui-object-box-tabs + * @provides javelin-behavior-phui-tab-group * @requires javelin-behavior * javelin-stratcom * javelin-dom */ -JX.behavior('phui-object-box-tabs', function() { +JX.behavior('phui-tab-group', function() { JX.Stratcom.listen( 'click', - 'phui-object-box-tab', + 'phui-tab-view', function (e) { e.kill(); - var key = e.getNodeData('phui-object-box-tab').tabKey; - var map = e.getNodeData('phui-object-box').tabMap; - var tab = e.getNode('phui-object-box-tab'); - var box = e.getNode('phui-object-box'); - var tabs = JX.DOM.scry(box, 'li', 'phui-object-box-tab'); + var map = e.getNodeData('phui-tab-group-view').tabMap; + var key = e.getNodeData('phui-tab-view').tabKey; + + var group = e.getNode('phui-tab-group-view'); + var tab = e.getNode('phui-tab-view'); + var tabs = JX.DOM.scry(group, 'li', 'phui-tab-view'); + for (var ii = 0; ii < tabs.length; ii++) { JX.DOM.alterClass( tabs[ii], From 6c7e392f8936370d6bcea90a8c3598fe84e9b95b Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 30 Jun 2016 17:15:27 -0700 Subject: [PATCH 24/35] Merge "Table of Contents", "Local Commits", "Update History" and "Similar Revisions" Summary: Ref T10628. Turn these into tabs in a single box, since "local commits" and "similar revisions" are of particularly rare use. Test Plan: {F1707196} Reviewers: chad Reviewed By: chad Maniphest Tasks: T10628 Differential Revision: https://secure.phabricator.com/D16209 --- .../controller/DifferentialController.php | 4 +- .../DifferentialRevisionViewController.php | 55 +++++++++++++++---- .../view/DifferentialLocalCommitsView.php | 5 +- .../DifferentialRevisionUpdateHistoryView.php | 5 +- .../view/PHUIDiffTableOfContentsListView.php | 14 +++++ 5 files changed, 61 insertions(+), 22 deletions(-) diff --git a/src/applications/differential/controller/DifferentialController.php b/src/applications/differential/controller/DifferentialController.php index 43252c07f6..1991580116 100644 --- a/src/applications/differential/controller/DifferentialController.php +++ b/src/applications/differential/controller/DifferentialController.php @@ -28,8 +28,8 @@ abstract class DifferentialController extends PhabricatorController { $viewer = $this->getViewer(); $toc_view = id(new PHUIDiffTableOfContentsListView()) - ->setUser($viewer) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY); + ->setViewer($viewer) + ->setBare(true); $have_owners = PhabricatorApplication::isClassInstalledForViewer( 'PhabricatorOwnersApplication', diff --git a/src/applications/differential/controller/DifferentialRevisionViewController.php b/src/applications/differential/controller/DifferentialRevisionViewController.php index f2078a57de..c66da78035 100644 --- a/src/applications/differential/controller/DifferentialRevisionViewController.php +++ b/src/applications/differential/controller/DifferentialRevisionViewController.php @@ -292,7 +292,7 @@ final class DifferentialRevisionViewController extends DifferentialController { '/differential/comment/inline/edit/'.$revision->getID().'/'); } - $diff_history = id(new DifferentialRevisionUpdateHistoryView()) + $history = id(new DifferentialRevisionUpdateHistoryView()) ->setUser($viewer) ->setDiffs($diffs) ->setSelectedVersusDiffID($diff_vs) @@ -300,7 +300,7 @@ final class DifferentialRevisionViewController extends DifferentialController { ->setSelectedWhitespace($whitespace) ->setCommitsForLinks($commits_for_links); - $local_view = id(new DifferentialLocalCommitsView()) + $local_table = id(new DifferentialLocalCommitsView()) ->setUser($viewer) ->setLocalCommits(idx($props, 'local:commits')) ->setCommitsForLinks($commits_for_links); @@ -324,6 +324,36 @@ final class DifferentialRevisionViewController extends DifferentialController { $visible_changesets, $target->loadCoverageMap($viewer)); + $tab_group = id(new PHUITabGroupView()) + ->addTab( + id(new PHUITabView()) + ->setName(pht('Files')) + ->setKey('files') + ->appendChild($toc_view)) + ->addTab( + id(new PHUITabView()) + ->setName(pht('History')) + ->setKey('history') + ->appendChild($history)) + ->addTab( + id(new PHUITabView()) + ->setName(pht('Commits')) + ->setKey('commits') + ->appendChild($local_table)); + + if ($other_view) { + $tab_group->addTab( + id(new PHUITabView()) + ->setName(pht('Similar')) + ->setKey('similar') + ->appendChild($other_view)); + } + + $tab_view = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Revision Contents')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->addTabGroup($tab_group); + $comment_form = null; if (!$viewer_is_anonymous) { $comment_form = $this->buildCommentForm($revision, $field_list); @@ -348,15 +378,16 @@ final class DifferentialRevisionViewController extends DifferentialController { 'The content of this revision is hidden until the author has '. 'signed all of the required legal agreements.')); } else { - $footer[] = - array( - $diff_history, - $warning, - $local_view, - $toc_view, - $other_view, - $changeset_view, - ); + $anchor = id(new PhabricatorAnchorView()) + ->setAnchorName('toc') + ->setNavigationMarker(true); + + $footer[] = array( + $anchor, + $warning, + $tab_view, + $changeset_view, + ); } if ($comment_form) { @@ -870,9 +901,9 @@ final class DifferentialRevisionViewController extends DifferentialController { ->setHeader(pht('Recent Similar Revisions')); $view = id(new DifferentialRevisionListView()) - ->setHeader($header) ->setRevisions($revisions) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setNoBox(true) ->setUser($viewer); $phids = $view->getRequiredHandlePHIDs(); diff --git a/src/applications/differential/view/DifferentialLocalCommitsView.php b/src/applications/differential/view/DifferentialLocalCommitsView.php index 639b62fc4b..4ac2bc14b3 100644 --- a/src/applications/differential/view/DifferentialLocalCommitsView.php +++ b/src/applications/differential/view/DifferentialLocalCommitsView.php @@ -125,10 +125,7 @@ final class DifferentialLocalCommitsView extends AphrontView { $headers[] = pht('Date'); $table->setHeaders($headers); - return id(new PHUIObjectBoxView()) - ->setHeaderText(pht('Local Commits')) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->setTable($table); + return $table; } private static function formatCommit($commit) { diff --git a/src/applications/differential/view/DifferentialRevisionUpdateHistoryView.php b/src/applications/differential/view/DifferentialRevisionUpdateHistoryView.php index 5ce766afb9..2d311fd0bd 100644 --- a/src/applications/differential/view/DifferentialRevisionUpdateHistoryView.php +++ b/src/applications/differential/view/DifferentialRevisionUpdateHistoryView.php @@ -303,10 +303,7 @@ final class DifferentialRevisionUpdateHistoryView extends AphrontView { $show_diff, )); - return id(new PHUIObjectBoxView()) - ->setHeaderText(pht('Revision Update History')) - ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->setTable($content); + return $content; } const STAR_NONE = 'none'; diff --git a/src/infrastructure/diff/view/PHUIDiffTableOfContentsListView.php b/src/infrastructure/diff/view/PHUIDiffTableOfContentsListView.php index 3330620148..e6ca6cc53d 100644 --- a/src/infrastructure/diff/view/PHUIDiffTableOfContentsListView.php +++ b/src/infrastructure/diff/view/PHUIDiffTableOfContentsListView.php @@ -7,6 +7,7 @@ final class PHUIDiffTableOfContentsListView extends AphrontView { private $header; private $infoView; private $background; + private $bare; public function addItem(PHUIDiffTableOfContentsItemView $item) { $this->items[] = $item; @@ -38,6 +39,15 @@ final class PHUIDiffTableOfContentsListView extends AphrontView { return $this; } + public function setBare($bare) { + $this->bare = $bare; + return $this; + } + + public function getBare() { + return $this->bare; + } + public function render() { $this->requireResource('differential-core-view-css'); $this->requireResource('differential-table-of-contents-css'); @@ -160,6 +170,10 @@ final class PHUIDiffTableOfContentsListView extends AphrontView { ->setAnchorName('toc') ->setNavigationMarker(true); + if ($this->bare) { + return $table; + } + $header = id(new PHUIHeaderView()) ->setHeader(pht('Table of Contents')); From dc37789d530f16d984163d350abbd07ea67df97d Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 30 Jun 2016 17:20:29 -0700 Subject: [PATCH 25/35] Build that thing someone posted a screenshot of on Facebook Summary: Seemed kinda cool. Test Plan: {F1707244} Reviewers: chad Reviewed By: chad Differential Revision: https://secure.phabricator.com/D16210 --- src/__phutil_library_map__.php | 4 + .../DifferentialRevisionViewController.php | 160 ++++++++++++++++ .../DifferentialChildRevisionsField.php | 18 -- .../DifferentialParentRevisionsField.php | 18 -- .../edge/DifferentialStackGraph.php | 58 ++++++ .../storage/DifferentialRevision.php | 29 +++ .../view/DifferentialRevisionListView.php | 31 +--- .../view/DiffusionHistoryTableView.php | 167 +---------------- .../diff/view/PHUIDiffGraphView.php | 171 ++++++++++++++++++ src/view/phui/PHUITabView.php | 19 +- 10 files changed, 450 insertions(+), 225 deletions(-) create mode 100644 src/applications/differential/edge/DifferentialStackGraph.php create mode 100644 src/infrastructure/diff/view/PHUIDiffGraphView.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 5ad020ec06..47e68b31c2 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -556,6 +556,7 @@ phutil_register_library_map(array( 'DifferentialRevisionViewController' => 'applications/differential/controller/DifferentialRevisionViewController.php', 'DifferentialSchemaSpec' => 'applications/differential/storage/DifferentialSchemaSpec.php', 'DifferentialSetDiffPropertyConduitAPIMethod' => 'applications/differential/conduit/DifferentialSetDiffPropertyConduitAPIMethod.php', + 'DifferentialStackGraph' => 'applications/differential/edge/DifferentialStackGraph.php', 'DifferentialStoredCustomField' => 'applications/differential/customfield/DifferentialStoredCustomField.php', 'DifferentialSubscribersField' => 'applications/differential/customfield/DifferentialSubscribersField.php', 'DifferentialSummaryField' => 'applications/differential/customfield/DifferentialSummaryField.php', @@ -1599,6 +1600,7 @@ phutil_register_library_map(array( 'PHUICurtainExtension' => 'view/extension/PHUICurtainExtension.php', 'PHUICurtainPanelView' => 'view/layout/PHUICurtainPanelView.php', 'PHUICurtainView' => 'view/layout/PHUICurtainView.php', + 'PHUIDiffGraphView' => 'infrastructure/diff/view/PHUIDiffGraphView.php', 'PHUIDiffInlineCommentDetailView' => 'infrastructure/diff/view/PHUIDiffInlineCommentDetailView.php', 'PHUIDiffInlineCommentEditView' => 'infrastructure/diff/view/PHUIDiffInlineCommentEditView.php', 'PHUIDiffInlineCommentRowScaffold' => 'infrastructure/diff/view/PHUIDiffInlineCommentRowScaffold.php', @@ -4928,6 +4930,7 @@ phutil_register_library_map(array( 'DifferentialRevisionViewController' => 'DifferentialController', 'DifferentialSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'DifferentialSetDiffPropertyConduitAPIMethod' => 'DifferentialConduitAPIMethod', + 'DifferentialStackGraph' => 'AbstractDirectedGraph', 'DifferentialStoredCustomField' => 'DifferentialCustomField', 'DifferentialSubscribersField' => 'DifferentialCoreCustomField', 'DifferentialSummaryField' => 'DifferentialCoreCustomField', @@ -6132,6 +6135,7 @@ phutil_register_library_map(array( 'PHUICurtainExtension' => 'Phobject', 'PHUICurtainPanelView' => 'AphrontTagView', 'PHUICurtainView' => 'AphrontTagView', + 'PHUIDiffGraphView' => 'Phobject', 'PHUIDiffInlineCommentDetailView' => 'PHUIDiffInlineCommentView', 'PHUIDiffInlineCommentEditView' => 'PHUIDiffInlineCommentView', 'PHUIDiffInlineCommentRowScaffold' => 'AphrontView', diff --git a/src/applications/differential/controller/DifferentialRevisionViewController.php b/src/applications/differential/controller/DifferentialRevisionViewController.php index c66da78035..83c6d10288 100644 --- a/src/applications/differential/controller/DifferentialRevisionViewController.php +++ b/src/applications/differential/controller/DifferentialRevisionViewController.php @@ -341,6 +341,21 @@ final class DifferentialRevisionViewController extends DifferentialController { ->setKey('commits') ->appendChild($local_table)); + $stack_graph = id(new DifferentialStackGraph()) + ->setSeedRevision($revision) + ->loadGraph(); + if (!$stack_graph->isEmpty()) { + $stack_view = $this->renderStackView($revision, $stack_graph); + list($stack_name, $stack_color, $stack_table) = $stack_view; + + $tab_group->addTab( + id(new PHUITabView()) + ->setName($stack_name) + ->setKey('stack') + ->setColor($stack_color) + ->appendChild($stack_table)); + } + if ($other_view) { $tab_group->addTab( id(new PHUITabView()) @@ -1198,4 +1213,149 @@ final class DifferentialRevisionViewController extends DifferentialController { } + private function renderStackView( + DifferentialRevision $current, + DifferentialStackGraph $graph) { + + $ancestry = $graph->getParentEdges(); + $viewer = $this->getViewer(); + + $revisions = id(new DifferentialRevisionQuery()) + ->setViewer($viewer) + ->withPHIDs(array_keys($ancestry)) + ->execute(); + $revisions = mpull($revisions, null, 'getPHID'); + + $order = id(new PhutilDirectedScalarGraph()) + ->addNodes($ancestry) + ->getTopographicallySortedNodes(); + + $ancestry = array_select_keys($ancestry, $order); + + $traces = id(new PHUIDiffGraphView()) + ->renderGraph($ancestry); + + // Load author handles, and also revision handles for any revisions which + // we failed to load (they might be policy restricted). + $handle_phids = mpull($revisions, 'getAuthorPHID'); + foreach ($order as $phid) { + if (empty($revisions[$phid])) { + $handle_phids[] = $phid; + } + } + $handles = $viewer->loadHandles($handle_phids); + + $rows = array(); + $rowc = array(); + + $ii = 0; + $seen = false; + foreach ($ancestry as $phid => $ignored) { + $revision = idx($revisions, $phid); + if ($revision) { + $status_icon = $revision->getStatusIcon(); + $status_name = $revision->getStatusDisplayName(); + + $status = array( + id(new PHUIIconView())->setIcon($status_icon), + ' ', + $status_name, + ); + + $author = $viewer->renderHandle($revision->getAuthorPHID()); + $title = phutil_tag( + 'a', + array( + 'href' => $revision->getURI(), + ), + array( + $revision->getMonogram(), + ' ', + $revision->getTitle(), + )); + } else { + $status = null; + $author = null; + $title = $viewer->renderHandle($phid); + } + + $rows[] = array( + $traces[$ii++], + $status, + $author, + $title, + ); + + if ($phid == $current->getPHID()) { + $rowc[] = 'highlighted'; + } else { + $rowc[] = null; + } + } + + $stack_table = id(new AphrontTableView($rows)) + ->setHeaders( + array( + null, + pht('Status'), + pht('Author'), + pht('Revision'), + )) + ->setRowClasses($rowc) + ->setColumnClasses( + array( + 'threads', + null, + null, + 'wide', + )); + + // Count how many revisions this one depends on that are not yet closed. + $seen = array(); + $look = array($current->getPHID()); + while ($look) { + $phid = array_pop($look); + + $parents = idx($ancestry, $phid, array()); + foreach ($parents as $parent) { + if (isset($seen[$parent])) { + continue; + } + + $seen[$parent] = $parent; + $look[] = $parent; + } + } + + $blocking_count = 0; + foreach ($seen as $parent) { + if ($parent == $current->getPHID()) { + continue; + } + + $revision = idx($revisions, $parent); + if (!$revision) { + continue; + } + + if ($revision->isClosed()) { + continue; + } + + $blocking_count++; + } + + if (!$blocking_count) { + $stack_name = pht('Stack'); + $stack_color = null; + } else { + $stack_name = pht( + 'Stack (%s Open)', + new PhutilNumber($blocking_count)); + $stack_color = PHUIListItemView::STATUS_FAIL; + } + + return array($stack_name, $stack_color, $stack_table); + } + } diff --git a/src/applications/differential/customfield/DifferentialChildRevisionsField.php b/src/applications/differential/customfield/DifferentialChildRevisionsField.php index 0965fc2d7c..1d9406a448 100644 --- a/src/applications/differential/customfield/DifferentialChildRevisionsField.php +++ b/src/applications/differential/customfield/DifferentialChildRevisionsField.php @@ -19,22 +19,4 @@ final class DifferentialChildRevisionsField return pht('Lists revisions this one is depended on by.'); } - public function shouldAppearInPropertyView() { - return true; - } - - public function renderPropertyViewLabel() { - return $this->getFieldName(); - } - - public function getRequiredHandlePHIDsForPropertyView() { - return PhabricatorEdgeQuery::loadDestinationPHIDs( - $this->getObject()->getPHID(), - DifferentialRevisionDependedOnByRevisionEdgeType::EDGECONST); - } - - public function renderPropertyViewValue(array $handles) { - return $this->renderHandleList($handles); - } - } diff --git a/src/applications/differential/customfield/DifferentialParentRevisionsField.php b/src/applications/differential/customfield/DifferentialParentRevisionsField.php index 3654fdc780..30ecea4bf2 100644 --- a/src/applications/differential/customfield/DifferentialParentRevisionsField.php +++ b/src/applications/differential/customfield/DifferentialParentRevisionsField.php @@ -23,24 +23,6 @@ final class DifferentialParentRevisionsField return pht('Lists revisions this one depends on.'); } - public function shouldAppearInPropertyView() { - return true; - } - - public function renderPropertyViewLabel() { - return $this->getFieldName(); - } - - public function getRequiredHandlePHIDsForPropertyView() { - return PhabricatorEdgeQuery::loadDestinationPHIDs( - $this->getObject()->getPHID(), - DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST); - } - - public function renderPropertyViewValue(array $handles) { - return $this->renderHandleList($handles); - } - public function getProTips() { return array( pht( diff --git a/src/applications/differential/edge/DifferentialStackGraph.php b/src/applications/differential/edge/DifferentialStackGraph.php new file mode 100644 index 0000000000..cc7d735d79 --- /dev/null +++ b/src/applications/differential/edge/DifferentialStackGraph.php @@ -0,0 +1,58 @@ +addNodes( + array( + '' => array($revision->getPHID()), + )); + } + + public function isEmpty() { + return (count($this->getNodes()) <= 2); + } + + public function getParentEdges() { + return $this->parentEdges; + } + + protected function loadEdges(array $nodes) { + $query = id(new PhabricatorEdgeQuery()) + ->withSourcePHIDs($nodes) + ->withEdgeTypes( + array( + DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST, + DifferentialRevisionDependedOnByRevisionEdgeType::EDGECONST, + )); + + $query->execute(); + + $map = array(); + foreach ($nodes as $node) { + $parents = $query->getDestinationPHIDs( + array($node), + array( + DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST, + )); + + $children = $query->getDestinationPHIDs( + array($node), + array( + DifferentialRevisionDependedOnByRevisionEdgeType::EDGECONST, + )); + + $this->parentEdges[$node] = $parents; + $this->childEdges[$node] = $children; + + $map[$node] = array_values(array_fuse($parents) + array_fuse($children)); + } + + return $map; + } + +} diff --git a/src/applications/differential/storage/DifferentialRevision.php b/src/applications/differential/storage/DifferentialRevision.php index e0c8effada..97d5a5e581 100644 --- a/src/applications/differential/storage/DifferentialRevision.php +++ b/src/applications/differential/storage/DifferentialRevision.php @@ -136,6 +136,10 @@ final class DifferentialRevision extends DifferentialDAO return "D{$id}"; } + public function getURI() { + return '/'.$this->getMonogram(); + } + public function setTitle($title) { $this->title = $title; if (!$this->getID()) { @@ -426,6 +430,31 @@ final class DifferentialRevision extends DifferentialDAO return DifferentialRevisionStatus::isClosedStatus($this->getStatus()); } + public function getStatusIcon() { + $map = array( + ArcanistDifferentialRevisionStatus::NEEDS_REVIEW + => 'fa-code grey', + ArcanistDifferentialRevisionStatus::NEEDS_REVISION + => 'fa-refresh red', + ArcanistDifferentialRevisionStatus::CHANGES_PLANNED + => 'fa-headphones red', + ArcanistDifferentialRevisionStatus::ACCEPTED + => 'fa-check green', + ArcanistDifferentialRevisionStatus::CLOSED + => 'fa-check-square-o black', + ArcanistDifferentialRevisionStatus::ABANDONED + => 'fa-plane black', + ); + + return idx($map, $this->getStatus()); + } + + public function getStatusDisplayName() { + $status = $this->getStatus(); + return ArcanistDifferentialRevisionStatus::getNameForRevisionStatus( + $status); + } + public function getFlag(PhabricatorUser $viewer) { return $this->assertAttachedKey($this->flags, $viewer->getPHID()); } diff --git a/src/applications/differential/view/DifferentialRevisionListView.php b/src/applications/differential/view/DifferentialRevisionListView.php index 6451bd9d2a..74e3f7633f 100644 --- a/src/applications/differential/view/DifferentialRevisionListView.php +++ b/src/applications/differential/view/DifferentialRevisionListView.php @@ -104,10 +104,6 @@ final class DifferentialRevisionListView extends AphrontView { $modified = $revision->getDateModified(); - $status = $revision->getStatus(); - $status_name = - ArcanistDifferentialRevisionStatus::getNameForRevisionStatus($status); - if (isset($icons['flag'])) { $item->addHeadIcon($icons['flag']); } @@ -155,29 +151,14 @@ final class DifferentialRevisionListView extends AphrontView { $item->addAttribute(pht('Reviewers: %s', $reviewers)); $item->setEpoch($revision->getDateModified()); - switch ($status) { - case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW: - $item->setStatusIcon('fa-code grey', pht('Needs Review')); - break; - case ArcanistDifferentialRevisionStatus::NEEDS_REVISION: - $item->setStatusIcon('fa-refresh red', pht('Needs Revision')); - break; - case ArcanistDifferentialRevisionStatus::CHANGES_PLANNED: - $item->setStatusIcon('fa-headphones red', pht('Changes Planned')); - break; - case ArcanistDifferentialRevisionStatus::ACCEPTED: - $item->setStatusIcon('fa-check green', pht('Accepted')); - break; - case ArcanistDifferentialRevisionStatus::CLOSED: - $item->setDisabled(true); - $item->setStatusIcon('fa-check-square-o black', pht('Closed')); - break; - case ArcanistDifferentialRevisionStatus::ABANDONED: - $item->setDisabled(true); - $item->setStatusIcon('fa-plane black', pht('Abandoned')); - break; + if ($revision->isClosed()) { + $item->setDisabled(true); } + $item->setStatusIcon( + $revision->getStatusIcon(), + $revision->getStatusDisplayName()); + $list->addItem($item); } diff --git a/src/applications/diffusion/view/DiffusionHistoryTableView.php b/src/applications/diffusion/view/DiffusionHistoryTableView.php index a9fb34e683..80e8aacf40 100644 --- a/src/applications/diffusion/view/DiffusionHistoryTableView.php +++ b/src/applications/diffusion/view/DiffusionHistoryTableView.php @@ -82,7 +82,10 @@ final class DiffusionHistoryTableView extends DiffusionView { $graph = null; if ($this->parents) { - $graph = $this->renderGraph(); + $graph = id(new PHUIDiffGraphView()) + ->setIsHead($this->isHead) + ->setIsTail($this->isTail) + ->renderGraph($this->parents); } $show_builds = PhabricatorApplication::isClassInstalledForViewer( @@ -219,166 +222,4 @@ final class DiffusionHistoryTableView extends DiffusionView { return $view->render(); } - /** - * Draw a merge/branch graph from the parent revision data. We're basically - * building up a bunch of strings like this: - * - * ^ - * |^ - * o| - * |o - * o - * - * ...which form an ASCII representation of the graph we eventually want to - * draw. - * - * NOTE: The actual implementation is black magic. - */ - private function renderGraph() { - // This keeps our accumulated information about each line of the - // merge/branch graph. - $graph = array(); - - // This holds the next commit we're looking for in each column of the - // graph. - $threads = array(); - - // This is the largest number of columns any row has, i.e. the width of - // the graph. - $count = 0; - - foreach ($this->history as $key => $history) { - $joins = array(); - $splits = array(); - - $parent_list = $this->parents[$history->getCommitIdentifier()]; - - // Look for some thread which has this commit as the next commit. If - // we find one, this commit goes on that thread. Otherwise, this commit - // goes on a new thread. - - $line = ''; - $found = false; - $pos = count($threads); - for ($n = 0; $n < $count; $n++) { - if (empty($threads[$n])) { - $line .= ' '; - continue; - } - - if ($threads[$n] == $history->getCommitIdentifier()) { - if ($found) { - $line .= ' '; - $joins[] = $n; - unset($threads[$n]); - } else { - $line .= 'o'; - $found = true; - $pos = $n; - } - } else { - - // We render a "|" for any threads which have a commit that we haven't - // seen yet, this is later drawn as a vertical line. - $line .= '|'; - } - } - - // If we didn't find the thread this commit goes on, start a new thread. - // We use "o" to mark the commit for the rendering engine, or "^" to - // indicate that there's nothing after it so the line from the commit - // upward should not be drawn. - - if (!$found) { - if ($this->isHead) { - $line .= '^'; - } else { - $line .= 'o'; - foreach ($graph as $k => $meta) { - // Go back across all the lines we've already drawn and add a - // "|" to the end, since this is connected to some future commit - // we don't know about. - for ($jj = strlen($meta['line']); $jj <= $count; $jj++) { - $graph[$k]['line'] .= '|'; - } - } - } - } - - // Update the next commit on this thread to the commit's first parent. - // This might have the effect of making a new thread. - $threads[$pos] = head($parent_list); - - // If we made a new thread, increase the thread count. - $count = max($pos + 1, $count); - - // Now, deal with splits (merges). I picked this terms opposite to the - // underlying repository term to confuse you. - foreach (array_slice($parent_list, 1) as $parent) { - $found = false; - - // Try to find the other parent(s) in our existing threads. If we find - // them, split to that thread. - - foreach ($threads as $idx => $thread_commit) { - if ($thread_commit == $parent) { - $found = true; - $splits[] = $idx; - } - } - - // If we didn't find the parent, we don't know about it yet. Find the - // first free thread and add it as the "next" commit in that thread. - // This might create a new thread. - - if (!$found) { - for ($n = 0; $n < $count; $n++) { - if (empty($threads[$n])) { - break; - } - } - $threads[$n] = $parent; - $splits[] = $n; - $count = max($n + 1, $count); - } - } - - $graph[] = array( - 'line' => $line, - 'split' => $splits, - 'join' => $joins, - ); - } - - // If this is the last page in history, replace the "o" with an "x" so we - // do not draw a connecting line downward, and replace "^" with an "X" for - // repositories with exactly one commit. - if ($this->isTail && $graph) { - $last = array_pop($graph); - $last['line'] = str_replace('o', 'x', $last['line']); - $last['line'] = str_replace('^', 'X', $last['line']); - $graph[] = $last; - } - - // Render into tags for the behavior. - - foreach ($graph as $k => $meta) { - $graph[$k] = javelin_tag( - 'div', - array( - 'sigil' => 'commit-graph', - 'meta' => $meta, - ), - ''); - } - - Javelin::initBehavior( - 'diffusion-commit-graph', - array( - 'count' => $count, - )); - - return $graph; - } - } diff --git a/src/infrastructure/diff/view/PHUIDiffGraphView.php b/src/infrastructure/diff/view/PHUIDiffGraphView.php new file mode 100644 index 0000000000..02893cc5d4 --- /dev/null +++ b/src/infrastructure/diff/view/PHUIDiffGraphView.php @@ -0,0 +1,171 @@ +isHead = $is_head; + return $this; + } + + public function getIsHead() { + return $this->isHead; + } + + public function setIsTail($is_tail) { + $this->isTail = $is_tail; + return $this; + } + + public function getIsTail() { + return $this->isTail; + } + + public function renderGraph(array $parents) { + // This keeps our accumulated information about each line of the + // merge/branch graph. + $graph = array(); + + // This holds the next commit we're looking for in each column of the + // graph. + $threads = array(); + + // This is the largest number of columns any row has, i.e. the width of + // the graph. + $count = 0; + + foreach ($parents as $cursor => $parent_list) { + $joins = array(); + $splits = array(); + + // Look for some thread which has this commit as the next commit. If + // we find one, this commit goes on that thread. Otherwise, this commit + // goes on a new thread. + + $line = ''; + $found = false; + $pos = count($threads); + for ($n = 0; $n < $count; $n++) { + if (empty($threads[$n])) { + $line .= ' '; + continue; + } + + if ($threads[$n] == $cursor) { + if ($found) { + $line .= ' '; + $joins[] = $n; + unset($threads[$n]); + } else { + $line .= 'o'; + $found = true; + $pos = $n; + } + } else { + + // We render a "|" for any threads which have a commit that we haven't + // seen yet, this is later drawn as a vertical line. + $line .= '|'; + } + } + + // If we didn't find the thread this commit goes on, start a new thread. + // We use "o" to mark the commit for the rendering engine, or "^" to + // indicate that there's nothing after it so the line from the commit + // upward should not be drawn. + + if (!$found) { + if ($this->getIsHead()) { + $line .= '^'; + } else { + $line .= 'o'; + foreach ($graph as $k => $meta) { + // Go back across all the lines we've already drawn and add a + // "|" to the end, since this is connected to some future commit + // we don't know about. + for ($jj = strlen($meta['line']); $jj <= $count; $jj++) { + $graph[$k]['line'] .= '|'; + } + } + } + } + + // Update the next commit on this thread to the commit's first parent. + // This might have the effect of making a new thread. + $threads[$pos] = head($parent_list); + + // If we made a new thread, increase the thread count. + $count = max($pos + 1, $count); + + // Now, deal with splits (merges). I picked this terms opposite to the + // underlying repository term to confuse you. + foreach (array_slice($parent_list, 1) as $parent) { + $found = false; + + // Try to find the other parent(s) in our existing threads. If we find + // them, split to that thread. + + foreach ($threads as $idx => $thread_commit) { + if ($thread_commit == $parent) { + $found = true; + $splits[] = $idx; + } + } + + // If we didn't find the parent, we don't know about it yet. Find the + // first free thread and add it as the "next" commit in that thread. + // This might create a new thread. + + if (!$found) { + for ($n = 0; $n < $count; $n++) { + if (empty($threads[$n])) { + break; + } + } + $threads[$n] = $parent; + $splits[] = $n; + $count = max($n + 1, $count); + } + } + + $graph[] = array( + 'line' => $line, + 'split' => $splits, + 'join' => $joins, + ); + } + + // If this is the last page in history, replace the "o" with an "x" so we + // do not draw a connecting line downward, and replace "^" with an "X" for + // repositories with exactly one commit. + if ($this->getIsTail() && $graph) { + $last = array_pop($graph); + $last['line'] = str_replace('o', 'x', $last['line']); + $last['line'] = str_replace('^', 'X', $last['line']); + $graph[] = $last; + } + + // Render into tags for the behavior. + + foreach ($graph as $k => $meta) { + $graph[$k] = javelin_tag( + 'div', + array( + 'sigil' => 'commit-graph', + 'meta' => $meta, + ), + ''); + } + + Javelin::initBehavior( + 'diffusion-commit-graph', + array( + 'count' => $count, + )); + + return $graph; + } + +} diff --git a/src/view/phui/PHUITabView.php b/src/view/phui/PHUITabView.php index 670fc0759a..bcc80bf774 100644 --- a/src/view/phui/PHUITabView.php +++ b/src/view/phui/PHUITabView.php @@ -6,6 +6,7 @@ final class PHUITabView extends AphrontTagView { private $key; private $keyLocked; private $contentID; + private $color; public function setKey($key) { if ($this->keyLocked) { @@ -58,8 +59,17 @@ final class PHUITabView extends AphrontTagView { return $this->contentID; } + public function setColor($color) { + $this->color = $color; + return $this; + } + + public function getColor() { + return $this->color; + } + public function newMenuItem() { - return id(new PHUIListItemView()) + $item = id(new PHUIListItemView()) ->setName($this->getName()) ->setKey($this->getKey()) ->setType(PHUIListItemView::TYPE_LINK) @@ -69,6 +79,13 @@ final class PHUITabView extends AphrontTagView { array( 'tabKey' => $this->getKey(), )); + + $color = $this->getColor(); + if ($color !== null) { + $item->setStatusColor($color); + } + + return $item; } } From 95b1a89e5cff4ce860a9914aad3f64ffeb65272e Mon Sep 17 00:00:00 2001 From: Chad Little Date: Thu, 30 Jun 2016 21:20:16 -0700 Subject: [PATCH 26/35] New Tokens Summary: New tokens, slightly larger (18x18 vs 16x16). I think these all feel decent, I might tweak the thumbs icons a little more color-wise. Test Plan: Use Tokens. {F1707411} Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Maniphest Tasks: T11244 Differential Revision: https://secure.phabricator.com/D16211 --- resources/celerity/map.php | 14 ++++---- resources/sprite/manifest/tokens.json | 34 +++++++++--------- resources/sprite/tokens_1x/coin-1.png | Bin 638 -> 574 bytes resources/sprite/tokens_1x/coin-2.png | Bin 629 -> 566 bytes resources/sprite/tokens_1x/coin-3.png | Bin 632 -> 551 bytes resources/sprite/tokens_1x/coin-4.png | Bin 791 -> 689 bytes resources/sprite/tokens_1x/heart-1.png | Bin 742 -> 327 bytes resources/sprite/tokens_1x/heart-2.png | Bin 1662 -> 411 bytes resources/sprite/tokens_1x/like-1.png | Bin 659 -> 342 bytes resources/sprite/tokens_1x/like-2.png | Bin 719 -> 353 bytes resources/sprite/tokens_1x/medal-1.png | Bin 657 -> 412 bytes resources/sprite/tokens_1x/medal-2.png | Bin 657 -> 423 bytes resources/sprite/tokens_1x/medal-3.png | Bin 653 -> 424 bytes resources/sprite/tokens_1x/medal-4.png | Bin 1467 -> 666 bytes resources/sprite/tokens_1x/misc-1.png | Bin 683 -> 534 bytes resources/sprite/tokens_1x/misc-2.png | Bin 716 -> 529 bytes resources/sprite/tokens_1x/misc-3.png | Bin 692 -> 433 bytes resources/sprite/tokens_1x/misc-4.png | Bin 826 -> 508 bytes resources/sprite/tokens_2x/coin-1.png | Bin 753 -> 1167 bytes resources/sprite/tokens_2x/coin-2.png | Bin 3431 -> 1197 bytes resources/sprite/tokens_2x/coin-3.png | Bin 3443 -> 1171 bytes resources/sprite/tokens_2x/coin-4.png | Bin 3615 -> 1559 bytes resources/sprite/tokens_2x/heart-1.png | Bin 3608 -> 597 bytes resources/sprite/tokens_2x/heart-2.png | Bin 3745 -> 797 bytes resources/sprite/tokens_2x/like-1.png | Bin 3476 -> 655 bytes resources/sprite/tokens_2x/like-2.png | Bin 3517 -> 650 bytes resources/sprite/tokens_2x/medal-1.png | Bin 3450 -> 803 bytes resources/sprite/tokens_2x/medal-2.png | Bin 3451 -> 859 bytes resources/sprite/tokens_2x/medal-3.png | Bin 3445 -> 841 bytes resources/sprite/tokens_2x/medal-4.png | Bin 3509 -> 1179 bytes resources/sprite/tokens_2x/misc-1.png | Bin 3507 -> 1083 bytes resources/sprite/tokens_2x/misc-2.png | Bin 3545 -> 1019 bytes resources/sprite/tokens_2x/misc-3.png | Bin 3520 -> 794 bytes resources/sprite/tokens_2x/misc-4.png | Bin 3670 -> 1227 bytes .../celerity/CeleritySpriteGenerator.php | 2 +- webroot/rsrc/css/phui/phui-icon.css | 4 +-- webroot/rsrc/css/sprite-tokens.css | 32 ++++++++--------- webroot/rsrc/image/sprite-tokens-X2.png | Bin 8522 -> 13565 bytes webroot/rsrc/image/sprite-tokens.png | Bin 7249 -> 6029 bytes 39 files changed, 43 insertions(+), 43 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index b53242b6fd..b31fe5f6a9 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,7 +7,7 @@ */ return array( 'names' => array( - 'core.pkg.css' => 'ee796570', + 'core.pkg.css' => '425300c6', 'core.pkg.js' => 'f2139810', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '3e81ae60', @@ -139,7 +139,7 @@ return array( 'rsrc/css/phui/phui-header-view.css' => '4c7dd8f5', 'rsrc/css/phui/phui-hovercard.css' => 'de1a2119', 'rsrc/css/phui/phui-icon-set-selector.css' => '1ab67aad', - 'rsrc/css/phui/phui-icon.css' => '3f33ab57', + 'rsrc/css/phui/phui-icon.css' => 'd0534b71', 'rsrc/css/phui/phui-image-mask.css' => 'a8498f9c', 'rsrc/css/phui/phui-info-panel.css' => '27ea50a1', 'rsrc/css/phui/phui-info-view.css' => '28efab79', @@ -163,7 +163,7 @@ return array( 'rsrc/css/phui/workboards/phui-workpanel.css' => '92197373', 'rsrc/css/sprite-login.css' => '60e8560e', 'rsrc/css/sprite-menu.css' => '9dd65b92', - 'rsrc/css/sprite-tokens.css' => '4f399012', + 'rsrc/css/sprite-tokens.css' => 'd7059378', 'rsrc/css/syntax/syntax-default.css' => '9923583c', 'rsrc/externals/d3/d3.min.js' => 'a11a5ff2', 'rsrc/externals/font/aleo/aleo-bold.eot' => 'd3d3bed7', @@ -344,8 +344,8 @@ return array( 'rsrc/image/sprite-login.png' => '03d5af29', 'rsrc/image/sprite-menu-X2.png' => 'cfd8fca5', 'rsrc/image/sprite-menu.png' => 'd7a99faa', - 'rsrc/image/sprite-tokens-X2.png' => '348f1745', - 'rsrc/image/sprite-tokens.png' => 'ce0b62be', + 'rsrc/image/sprite-tokens-X2.png' => 'b92fe16d', + 'rsrc/image/sprite-tokens.png' => '938df9c8', 'rsrc/image/texture/card-gradient.png' => '815f26e8', 'rsrc/image/texture/dark-menu-hover.png' => '5fa7ece8', 'rsrc/image/texture/dark-menu.png' => '7e22296e', @@ -845,7 +845,7 @@ return array( 'phui-hovercard' => '1bd28176', 'phui-hovercard-view-css' => 'de1a2119', 'phui-icon-set-selector-css' => '1ab67aad', - 'phui-icon-view-css' => '3f33ab57', + 'phui-icon-view-css' => 'd0534b71', 'phui-image-mask-css' => 'a8498f9c', 'phui-info-panel-css' => '27ea50a1', 'phui-info-view-css' => '28efab79', @@ -888,7 +888,7 @@ return array( 'setup-issue-css' => 'db7e9c40', 'sprite-login-css' => '60e8560e', 'sprite-menu-css' => '9dd65b92', - 'sprite-tokens-css' => '4f399012', + 'sprite-tokens-css' => 'd7059378', 'syntax-default-css' => '9923583c', 'syntax-highlighting-css' => '769d3498', 'tokens-css' => '3d0f239e', diff --git a/resources/sprite/manifest/tokens.json b/resources/sprite/manifest/tokens.json index eee41c9d78..e4e83bc562 100644 --- a/resources/sprite/manifest/tokens.json +++ b/resources/sprite/manifest/tokens.json @@ -4,88 +4,88 @@ "tokens-coin-1": { "name": "tokens-coin-1", "rule": ".tokens-coin-1", - "hash": "0ec4c7309f8191972340c6789a6b5691" + "hash": "5343d745423994c45c5fc689edc47d05" }, "tokens-coin-2": { "name": "tokens-coin-2", "rule": ".tokens-coin-2", - "hash": "4c85dd4b0c388cfefe0075b7056384fd" + "hash": "9a94b5f925f3e6f8eed673d50fbfe148" }, "tokens-coin-3": { "name": "tokens-coin-3", "rule": ".tokens-coin-3", - "hash": "a2e3770894539957e436a7d5a2be4703" + "hash": "68db03ca248309a76cee97ada64239c6" }, "tokens-coin-4": { "name": "tokens-coin-4", "rule": ".tokens-coin-4", - "hash": "856cb87c5590975c0a25177ca2fd2a8f" + "hash": "75832b7e42df9287b3c35c6afed12a93" }, "tokens-heart-1": { "name": "tokens-heart-1", "rule": ".tokens-heart-1", - "hash": "370228318750a79d93848bdf686444e5" + "hash": "2d4812b2129a8eb05fcdbed1e9654422" }, "tokens-heart-2": { "name": "tokens-heart-2", "rule": ".tokens-heart-2", - "hash": "197144d3987308aaef311e29e3503707" + "hash": "64cbdbfb0dc565f17b6f13b5e41bc000" }, "tokens-like-1": { "name": "tokens-like-1", "rule": ".tokens-like-1", - "hash": "3c5271d6678ad6d217a47779488c9918" + "hash": "cca33a8c4c3bd89adbbc00edf6dd1589" }, "tokens-like-2": { "name": "tokens-like-2", "rule": ".tokens-like-2", - "hash": "b009176baadc3e71786ac24ce8229c5a" + "hash": "faa428c378fd1ad504fd46e2890e6b7b" }, "tokens-medal-1": { "name": "tokens-medal-1", "rule": ".tokens-medal-1", - "hash": "cd897529c6834917da062589ae1a69ae" + "hash": "33d837e703091060c1892c402535eef0" }, "tokens-medal-2": { "name": "tokens-medal-2", "rule": ".tokens-medal-2", - "hash": "d56f106b508c33bca6c0a33e2544d0d6" + "hash": "fa2f3b237d7616a6cb309718ad162d7a" }, "tokens-medal-3": { "name": "tokens-medal-3", "rule": ".tokens-medal-3", - "hash": "d4e7c06cfd39d932a35aa25841d5008c" + "hash": "d7282911ba57373b54b4093986143f3e" }, "tokens-medal-4": { "name": "tokens-medal-4", "rule": ".tokens-medal-4", - "hash": "36f596bd2615e521542ac10a771d6902" + "hash": "a107a334968d57314ec6a71620c45b99" }, "tokens-misc-1": { "name": "tokens-misc-1", "rule": ".tokens-misc-1", - "hash": "8f7575c0176570b30aaffb801bcb2c13" + "hash": "671ce03f62c7b0946482ec92d35b8aa3" }, "tokens-misc-2": { "name": "tokens-misc-2", "rule": ".tokens-misc-2", - "hash": "5c61bc36fd0b5545ebf31b57c6ab5185" + "hash": "872353ba82e41512c3b54e5dc2aa3d43" }, "tokens-misc-3": { "name": "tokens-misc-3", "rule": ".tokens-misc-3", - "hash": "97a383def5eb847077b2b26a1a441c0e" + "hash": "cdf9171ec6397b95ea9abe1edeaab359" }, "tokens-misc-4": { "name": "tokens-misc-4", "rule": ".tokens-misc-4", - "hash": "229c8a28e3b6bb883effbb62689e190f" + "hash": "42bc383165221803c281882141ce4bef" } }, "scales": [ 1, 2 ], - "header": "\/**\n * @provides sprite-tokens-css\n * @generated\n *\/\n\n.sprite-tokens {\n background-image: url(\/rsrc\/image\/sprite-tokens.png);\n background-repeat: no-repeat;\n}\n\n@media\nonly screen and (min-device-pixel-ratio: 1.5),\nonly screen and (-webkit-min-device-pixel-ratio: 1.5),\nonly screen and (min-resolution: 1.5dppx) {\n .sprite-tokens {\n background-image: url(\/rsrc\/image\/sprite-tokens-X2.png);\n background-size: {X}px {Y}px;\n }\n}\n", + "header": "/**\n * @provides sprite-tokens-css\n * @generated\n */\n\n.sprite-tokens {\n background-image: url(/rsrc/image/sprite-tokens.png);\n background-repeat: no-repeat;\n}\n\n@media\nonly screen and (min-device-pixel-ratio: 1.5),\nonly screen and (-webkit-min-device-pixel-ratio: 1.5),\nonly screen and (min-resolution: 1.5dppx) {\n .sprite-tokens {\n background-image: url(/rsrc/image/sprite-tokens-X2.png);\n background-size: {X}px {Y}px;\n }\n}\n", "type": "standard" } diff --git a/resources/sprite/tokens_1x/coin-1.png b/resources/sprite/tokens_1x/coin-1.png index bb981e0e20996ae232287b5020e98fa9ea3c937e..ec7b376f31158fc0032865b0661580315738e9e0 100644 GIT binary patch literal 574 zcmV-E0>S->P)?~{ymVyCqL_r&E1XBbn!B-GO!9uW7 zH24e#yv2z*GiT16*?TXGBx5kfl!71ZX0iD#SReeiQNIt~UYnkBR^QK@J)K#z-x#d{ zVoa$HakU!4ezezc^V;gz2kJF z)U~244O&BFTpx@VY*2~W$XuVh6T3stC3PVbYc01B) zMZ8Z~D_ARdpJ=rr?RG>&Q51&NAAQz))jVw0|Kv^m?(7k!Vw4%9JR%BfA(Y>jz*W;crf5g62gZhyd9(eDjI zybe5=T`Xh=w26%Y=YTUxsTrfyG(b%9O+=P~r@-Uc#iFxkEB`b<0XjA7H*QA0_5c6? M07*qoM6N<$fK@JjWY53$>q-~Ile@Tz_0FLd+Zc>Z2yJTX_89z`-0M@07!(>2sf9S9FF z_8XL_gQc%EtS_4PuPiKIRk3~x4^I^4$7V8fN6#EUa)*xY>L$Icq4C{>V@pUOu~BX# zVdzj8g;g;D2UPaW!)M3PTDD-DHl*vqb0zs!pDT+^suytj4%$@rb zm>@yT7LPx^{Bdbk9z=FBjoh?BB`bZmBv!77v|9q@w@rL`vxKtc-tk9ZTgLpYV(xB= zv-7(r(#Y@K1$`t6mxz>75El|=+d{3ffwe{lO;Zp8x;= diff --git a/resources/sprite/tokens_1x/coin-2.png b/resources/sprite/tokens_1x/coin-2.png index be74169b1f3ec87443454f723c12025e32eccb5c..8a6bdb5dc545c31f3ec2e12ff32dbbf3cd26c22f 100644 GIT binary patch literal 566 zcmV-60?GY}P)Ace4TT4_iQvq1o0Xq2q9R=V&QfM?7RWd!bVY08^JbKT12t+5`sHugt8Lpg=iHrh=P!Og5c>S#&9fv)uA8_cRn$RggAfXU;2q9cTvK7km9MJm>HOm0 z_CDy#$G7XPFfO(!i~QBug$tLb(5Lqschj{0 zIGH(#5R!4ZgKMfkW}%}FQTG@jcd7j;zUmUDR0AtJp)9E9mqB#TsXq$?r8X=^@ zBq_$2hX8xf31wnJ@U({@Q*2y92uKs7wLUe4{$>Iogn&sr62MxEYa4`+C=(xvD3c(B zz}Yd@+AxKRqWEU3j1WB0q_qPP9TD|*!?tx!Q50VRG(c5VkMeBvLI^>e&XV-!aJI(T zF~POKUi}Hm#CX@z6hFvDJJfZ3Z~8ufjrG;%X`0^X_h&Fk7Zr6-%7B=xgn)AmuBowg zNtTVs^ZdzRxOD3mXo6i+N-cN0-Pt5bFvcK+1Og!hthLm2O<9(ibMF3NxU{l&ci&&z zSYPb{SAi=+hy^L-X#ns2RtRAeSOs1VhD+N=w(?KsCq+)<{?8&vqW}N^07*qoM6N<$ Ef&du^y#N3J literal 629 zcmV-*0*d{KP)SxQnV%xp+mn2@3IiJtx?{ci)LZwo9sA*bZCN+zQs-m^OkD=%g>ObpnogO4fM5EDw z8dY(9CIwyB3mnt;z;)dRnM?+IySwOgI-oYdJeQJz^x^xr4U#IL+wEe{{DpKn1xA>@ z2V9Co{qh3p5tEZw>Fe8M4j`WkAu(~qui@nUJ{SxUx%c-J)UXQMvWO8rI7EeKgFz^Y z;)B1euluGl#&sQ=0%zT*)iAE>=s6v{T3+%MnMCF|4s6>dW5bx5x(03R5>{8&AO-?Q z1mx<|rNzZN$lO4DbQF4G904v)I8x`59|x^AK7C%p`*&|)7{;8x0*7N9DV0i(qpEsq zBp%0QO(WS{stcFog21-j!q%oi)7XY(Sucvk;yuDP`g=ezmd$2olgZ?5>N!p4(5a>S z-EOzvtgL)^wy^O0HJyK)Sfs;{DKBzA|FPzbk{m{zZ{>+i{I5UL9{~mca@+C|IjOy} P00000NkvXXu0mjf@>w57 diff --git a/resources/sprite/tokens_1x/coin-3.png b/resources/sprite/tokens_1x/coin-3.png index b0e65082ddc404fb0ea3e098f323e75aac1a2eb9..c89f3d5a536b457b4e0eec72ab03998a7ecf9cd2 100644 GIT binary patch literal 551 zcmV+?0@(eDP)AcJ2U69muwPK>6cK^PY_5W5*D_xNGWKeh!#Pxu~i$vDA);U7Zwc!Qsf(m z!H*!=h?ngB>^W!Vwb)Haj7dttrySn-z05Q4-$wpC_}FP(a+c@L$x38p$rubk2!x{4 zAEnow((~|Iv;FNJ9&3eK%@RlQ1eg{X zIa!KhL6tL+qnIkhi4ZwiI)o11sCuq(A~LJT0OW;|7m6{{E$1ap3?fkE(|d=|U>v>< z2LTN**KkDf=~xsCa}7%fFdPQ@qwod50Sw3CY25d3jX-0;vasmLCxOXC@qW6|S|Ldc zaU%Fq>2^KczGsw$^@C~v*xhWtY1GW!g*i)FF{G)%MS{#GRG}=CEDL1gz<%G;8-`cQ zYn^+?pc!`CS$S5gn8ive#O^qhm&!O(2IDaFUe}k`IvYnyKc3yqW)-*vG)1^>L@ojZ p)$gk80b9Vk<+aZKxxeyH^Ba^B+d3rO*TVn+002ovPDHLkV1n?I`^5kN literal 632 zcmV-;0*C#HP)Ou>V7VF&6%On!9v?2f;KrzfR$9SyrIN z<7n2ZV%Y)1TP8`5NAG|9ECM07C9w2x{JGJWo=Tp`pzBBsT-PB-6j+5E;43f^{jkeB zDF4_*t@!iB*n`D8f-4E7rP+zO)X=G^!Lbo&F$Hz249!TvG1mc?EOHOOwoPbJ6@%j= z5Lm$x+)#k~{!Dga64rJeUef^&d{5eJ9z3@WtCWYp3Xb3^EiX`2xO;Vy zl#n=c1p(m{i(7rg^$F_Af$|NP&#oJEB zfk=&7-Tg5CrS$6QyR`)hZ5(Q(gV9a1kEU^`z%31+Mza`pWoIY;)64W*fB^uwc>Fi! SU03e_00003CvaN~& zm|?m2QOf=_7w+$Tx%c8+=-}7c&l@AXIo?nij+9sls{NiB?{@q5tvMss**cvC`Or71Hm1-?tJ90N}b~du@b5vqcvpan1KlE;r>^NfGD_udH=8i z)UDGT8vv7O_$V~p?9bG-Q8zsl0S@Ph@kof3urXFN2&;GHgIUEWOs0XsP^fET^{#NZ z$fwY8lX>+}`r&FkIGC`GGoC~!vT`9w% zQvF*XTVq)p^JSrF3w2XYp{6lZGiF9g#?ew44U|D8gfmc=ou)N%F>)@ntx>mUpe~n% zG_vL9gGbe*6oi1guicif#~PR>qBM%x)(D)kyN^M`GhADIQJC{)o=1r?!p1jqZB zcpu7?`|izrcMSnQnEB^jDmToOlpR$;sAxc6Br=9oJ<8}9x)@Lm7ZN&UEcO@O<= zHzG_`bd3Nwj(Z~j7_yhRq_Jb!c&)$0v Xd1PmPY4gy!00000NkvXXu0mjfzPe7x literal 791 zcmV+y1L*vTP)`e zIOGr^f*=GzL?j_l&>=)aNktc^kPr=s0#YFmqJfTzk^%|Q5Fv#tD1d}O*hmCKf*r6N z-@92Kd+}#&;-u!UW_EYx|M%a2^M|u-8~n>dbhe%{U9D_8aO=Rc>dOrtFJz=1(uw1CIVQzK6Nh`iOzNm#NcW3!Bk7!#cXQe2*`ry(O;?J;S4DkL^V|@eaJe zZ#hOmz}JNZjKV}HvJ)M#-GnMMlIWC;$^hw7*9!RaU~ycvq}5-}f6hL<^Yr@*TK=`|GBwRhBSIs%t;rfjjg1nGv~x3?9sE~-0RT$F VI=Fu8dEx*7002ovPDHLkV1fs?V}bwx diff --git a/resources/sprite/tokens_1x/heart-1.png b/resources/sprite/tokens_1x/heart-1.png index 53e1013a5859b38253d747ab537246ac1ce7c35a..65c469ea5d9a0cfe225495f18d32a0608c5ee324 100644 GIT binary patch literal 327 zcmV-N0l5B&P)40%A4_69#j;;vH%|nEAc6}+FbWH{iNPSo6U=f1n_zO-1cO2FPiV0W1~OO} z49hnibKAE~pLBSi``qs-hkr^5+Ku`uPEo@eK5&Dx5L0&^rqpiKSMY@O35#B_k)LNW zWi+P%@_BRQJe4WuPo0&b9%^M&3ZSAMDj7O+fJNzKX#InzmEpwSUe!S}3o*T+itZAU zPgFxp?-_)cUa*bc0+Jp!^SiR)G{p3XUG(RaY=xMfhl6ai4KdwgAA=c^K6diO-^s?Q z5YsIVrbq_Z3o$*67r#Wg>>5WCs>2Y|-K0AcMY-(K&p(6X5YyE(zA)`Z{i5BdpD!&~ ZG+()wOVdH)184vM002ovPDHLkV1n@LicSCk literal 742 zcmVUn{>${D5>sUB09)YqM|0c<^BN+SI`Y> z*Il=DXLfgH`n_>oRO|!k#jxt3_Q0&@v zlVs*y*A*0V&cGD$STcRWaqOE${ej-#jV`FQwO)hL>Z(Y5{IHOUMGjKDFW9mWxOfR_ zUEM}KlR@n5P;VjiJ1T73>2JQ=fzs4xQA0(kS_phbQtE>Q5xH0ijv*i zbPS{wU>xOm7>Z$le9FR6Q4UWaaJz>#fc&ec5LD-!LO@kL4e`QDX3|hJUDg;jj--T& z!n;90_kkP!4RNK;s4g{>6D~usR4i*k)#11uH-C`mS#E>I(K5JCD4dHJ~@S4-i|q{hk3sNj=!db$U+kBU&4p^ zNPFAz@Sp76SmGBL-5__dZHle8%NjAnl7_Uj89az4-Xzw|7vJX>B`BatP>`Tv*Esu4phX{rq>O_n>xBi+$d_WAT& z9A?RZ@cBF=pK??qhS*J=awpJGU6S-yuZ+!22mN}Q=du@S{52>PEPRW`Ft(EE>zt)%N2L{agZ>FH Y0MN}G*KUV_XaE2J07*qoM6N<$g5pGASO5S3 diff --git a/resources/sprite/tokens_1x/heart-2.png b/resources/sprite/tokens_1x/heart-2.png index 88bc63982d2d3e766371e1ce216c258bb180a11e..387be5eaf8d6cf5020aaff999d3f9d3862658052 100644 GIT binary patch literal 411 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 literal 659 zcmV;E0&M+>P)jMkLys`Ie6t?| zLrF|syodPU@YUjj$t?a6n-cu_^%M1l=SZKt1a3v%Y?+|Gs-HFM-_Wdn!2T2G?YH+Q zzHf=Z^>j%%Z78P(*=ZnkFuS+dgCt^ZrTM8jt>mmRMF;MfGM3S2_aROO+XFUI=`@kB zh0@^dD3`5_*3!&m0Rgx8Y$kUdQCWfC{1LjWbl^0Yi(SbCS}b|kUlzDaL<3M_ks_J7GN1kS0~HA!`A_+71_TyA}}C?gVy zK>Bh``^I53;>LC6){Xv9)AD;002ovPDHLkV1nk)Froke diff --git a/resources/sprite/tokens_1x/like-2.png b/resources/sprite/tokens_1x/like-2.png index d4069f13eb77e82800a040a5c437ba07b1d57dea..0d292e640c92c6a2ab9a543a46273bda879e3fb7 100644 GIT binary patch literal 353 zcmV-n0iOPeP)Tx-ueW3DXu zU=T#1t_C5x9adskc3n5-fq~(fXMXd-3@8e%(G;gZ4$!C6eE@I3eW`d`6|MHQw;%@W zgdhNkN!v`PQyI}}&wC4W;978aKQpU2f`ia1aMwxo0oIJZpWZZ~_j4qSNW`9X)kjg%?_2bO7!rkvRKvu;cM zC*UXy34(@F|HLwlpD-i{y2QQkoMjr1vmgM!6>>^F-M*b zNtrEZT6id-Q%3)&h#;2suwdO(hz=cO3p@nTu|tQ5PIU?-vmj&-5p;-ddC+1_Q>;Ww zO-)$Wo!!};`R41*?8=&0mmc%xG4Ff7_kQp94U8&u6ajs2Qd(LgwPOuo8};_3$ilV~CbNi3u=XbWoQ>L9WpQNTF!dMJug zVL<)RcHss@W)ikBRZ7WnkfovSRj;iq63|Bisx(=X9{q0?y8i~=bPIo)Y1o)}JA~!5 z_45AELK6S54H38IkCfxD&HHXO4=bg_|JZK<1^_$L8x~`?^yL5m002ovPDHLkV1gZc BOR4|> diff --git a/resources/sprite/tokens_1x/medal-1.png b/resources/sprite/tokens_1x/medal-1.png index 83c06af3ad5cce29baeb80a915343a07bed32c8c..adb1dbfaf98f4a62cef71584f0828bf60de5dbe3 100644 GIT binary patch literal 412 zcmV;N0b~A&P)Ifm`WxtB0^MD-V8;v{1MAtj*X(NMiS%q6fJQjO?VC@%ovKvJ`ASqY_D2Bs zr@6~{x4G-O2FK9=NGa%cExOu1TRwR_`W4hDWHx}SR7zpn1}Xa^j$`QcESk*(z-BtA zUythIJPJHdW7~!>OweleGYk{hw!!l>KoAwhfmR&HAf-U5;lfH;NGWg}1F$j)$O15C zyfIQ6vm_2wlhVRukxXimNsaF-l(Oh_1VAzNjA%3p znR8&z^Yp+Ir2K|lq=WkYM4&I&HgEvs1}6ihbWk6AMu1N=jd1H0iTMe;E0)4}@+s>^h>B zid;HR*TZfrM`}H6t}I^O%l_nNe>}lzWd@&&Q%gSiRj1n1O!s8!C zaCQ#WmBqo-JiABGg^pLSY%D~kF2ek>3>F%La(Eib=xN7zbNv+}QRm4$0y zeFuQ*ZBKCf!$SaE-Q00%3P3RNxv z%tvGKgHD}aMoB~32t)Gv@_?$8P?Zv|FAoSqVn`bx=@t2b5+WpVof-}8el#1}#dT_k zmeoWzpqAaH&!p+u`2lpU9fdG00zvVq3<`v4wKF^K2DI&}Dh=&o^$lR!VN5%W)i=X$k{x&>SES&BDD~7b&nV=ojhU`XT@zt(C@32K+4VY|O2GY@3{M$1Eya5}^cMrfE R6r2D6002ovPDHLkV1na=xNiUe literal 657 zcmV;C0&e|@P)&7;cUD@ED!A~2pf1#96~UDYK}8gFXGNS=(V&Y8O-vh2 z)Haz+ChvLQm?<;Cxo~+i=bdxk{V^c~0^5J_X|>(v8BQ2u46K7ohU6)PB@#;Q<=Kf1 zfks8hr1T z)rU`BiE5)QJOZW5Z;8#^oUKVtSQ9DX((MP6TQ=o06XW~v-Y&qY*C}IxWGV%TdHi)B zan2ESdcAL5W1QYIwhQ+B7bJTODlm;3i3p1cQUAAAn#pP}ppa zxfu)+P?(DM;l3Dn5fGI*qNhCDIp_a7FVBHKnXl8ax)Hwu7G7QpJeMBYVk5&7=(-7z zNGIdFtufKBj{ZS^1KsqX%XT{aU6S^J#py9MGe7BPe6&{U{?X<7w!&S(JWVI#r=6lN zcLb2VTssMw)lJQe$1eT8Po_3`LU}W zl{XjQ@S9HCEuV=V0b79qft2;{S53%qIFj1#2~>xz13SQMV`Lx^j-Zc-Z+4Tisa+pDcIM6Zee>R%w-VANrKdi3xvQiipetD zU74Lq_BjfCCdiNOUW3COlnv7rL64kC>mlMD7x`Ur*uDdSQBDQX96XwPAfwK9&lZDE;i#zT2_c%8{ zaPAa6H}kwhv2t!=S77{{ff8!Vm*&*;xr8z~gL>gPI(v0WzXc^Wg81YE#IYE!Ewjf2 zoj>cbi=~W7_HjM4e{ZHqKV+09^&xMZ**@vf(sx$iyqqw;F%532o?bZf3hX@Eu nlOH?KuV3UwfB5?LSAYQkRy+o73?A*!00000NkvXXu0mjfiD@dM diff --git a/resources/sprite/tokens_1x/medal-4.png b/resources/sprite/tokens_1x/medal-4.png index e471b25647ca7d39821ca15e8fcdcc2aff7cd748..ceb8bd255e28e355e3638010b0dc77dbf52e50d3 100644 GIT binary patch literal 666 zcmV;L0%iS)P)U6vuzh%s8`evoB#Ol6A8r6hY)g-2@hk-eeT)QY0bt0V=`|uwS64Am}bMieQbp zsJkVE3^J%NLo!xKEUAdyo$+Oyb!MI}Tx>P1F1qNzbMc(>KR^EGIS>4AA+{=%in%ku zCE#E^^%}UA&J^zdHK}lLNhCFb zKr(Op>%?YUMNtG+)dL8pGlk`?97O*z#_UBK4 z94=R8nwFf(+L*~iJ00z@>Z!wXR{9Mpl`?3y>^U@5uSqFiIgT|J4kuIV{!!QQ zb5n*9+hfLBEa1rSV18o5GWG+{<`=#uTzBEa$m2U5o31}@wvL*y78~f<2mz?)ddygh z9XDG?8*A%8x*i<|x`zkzV;dn2urZ*!uKoA%19&X+l2ue1F8}}l07*qoM6N<$f-=%8 Axc~qF literal 1467 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`k|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*9U+n3Xa^B1$5BeXNr6bM+EIYV;~{3xK*A7;Nk-3KEmEQ%e+* zQqwc@Y?a>c-mj#PnPRIHZt82`Ti~3Uk?B!Ylp0*+7m{3+ootz+WN)WnQ(*-(AUCxn zQK2F?C$HG5!d3}vt`(3C64qBz04piUwpD^SD#ABF!8yMuRl!uxKsVXI%uvD1M9IxIyg#@@$ndN=gc>^!3Zj z%k|2Q_413-^$jg8E%gnI^o@*kfhu&1EAvVcD|GXUm0>2hq!uR^WfqiV=I1GZOiWD5 zFD$Tv3bSNU;+l1ennz|zM-B0$V)JVzP|XC=H|jx7ncO3BHWAB;NpiyW)Z+ZoqGVvir744~DzI`cN=+=uFAB-e&w+(vKt_H^esM;Afr4|esh*)icxGNo zet9uiy|1s8XI^nhVqS8pr;Du;&;-5A%oHml3u6~U10zdEb5kcnLsvsbBLhoUQ%4IR zHZ%gt!1TK0Czs}?=9R$orXch>;?xUD3b_S9n_W_iGRsm^+=}vZ6~JD$%Eav!bDZWu z^`_u=-`as9%gCZ6wqG3Y7)B|F|lP!<~Pxz^Mz|>v@OxD&H|DI)FVAA(=aSW-r zwIt}icW|K0|2dmq&wZE2$D_M>`3F@d`CCTf52ra^;81FkYm(cxeI9pJ;j?8|?G1g7 zOkXg4#T;*)jwMAEn0^EAqe&e^z(?T&%c+k4pw(wh@h*@C|w z4ES}Rt8&9v6%{p$!@j%+c1r9P75!`Nvm;?j-2shf2aa&%6dygLb8cS%-_DOpJ#UqE z{M`ELWQpah3zA}Puiw3CaMWJg!5z(Yl(?vCo{J>#qc*R6~5qC z1H>h$^;^f2JN2{^I_;;%5*0{Y~31%&y{`{@f;J*7|QfvkijuC&Vtb zDEw+@^GhJUFHWuc{{yF-ws`qZ4@1+que@{V@KcDWW^NApNx}wfP?V%aQ}-TuK#8BGJw9g- zhy6Y-H}l^{mi^ZMXY+TJW^MiT!Q;)j&!X?-=ihz$=V@%sms#vbtbC7Dt4&hg$$PNA kL;4fPbD96b^(+z$zLP#EOkT;g6I7gfy85}Sb4q9e0LPgtP5=M^ diff --git a/resources/sprite/tokens_1x/misc-1.png b/resources/sprite/tokens_1x/misc-1.png index 1d970631faafc27d5c5c24333dd8d000e3d11478..9d7a74199bf79620afbe3c4c2a56871183264d19 100644 GIT binary patch literal 534 zcmV+x0_pvUP)F-KDu`FruwLP@$lxg9eHOZV;zNT{MF@>L3mUUHV_>B1Mrdp^M^BT~1nr z-r&%ohvF{^f@rNjk~WJ?(!_Xo@BMH{h8nJE1P8y<`##T?&%^WOec^wTh$|ZTLI$`G zT-KAxQ2PFK5Ck`D%WVEF$kg@Z#qqIg`L{EV8jms%tB zq194eN-4f~yeEPAdU5z{9B349Uc7$SzUjMRAWih^2azw2xvr9HHq}Gbs#afLS6_La zC;?My*IKuL2|z2aR`^<4K_pVFHC$3Zmt@t|vgt%H4J-nyQ6vH>m6DWVMr?o_20q1w zzi*qz>HT%SHIw6aA3mRm^Jp8+$KwnDmDT`HmWCicQ@R`J%}gDIfYlFZdk;ti+_Wuo zu^-TmO*|igbGBtp_89a>Mn}7PYoQ;^DVloKwoKUz0`13#kuRJ9?$P-&Q5ep9Ek_A` z|3u))!Aj}#+*TmX$QO12kAMb{>D0?a(QUv54sHd3_G(kW0l)?f0HG-2&4M-C58$8c YH_yeWS{b57#sB~S07*qoM6N<$g88}d7ytkO literal 683 zcmV;c0#yBpP)p&WsnAgAS4SAFf{}S zKQ@Z4UENsQ(2msSOF&T%MFQk_6hdSI*)czaSZ*Nywb)-%;O(1NXy4fiOdJ5_e?Z95 zGYdM%N+XDV3s{R2zaKwBW@J24d4)#G*zTSlBvaH~VHz+tLl$NMk%f{eK#WhK82bPr zVSs7xDg1fH+bSK19*;*WiV~g<4dZlQH;(po0>2iJ&n_Yon|s*Od-*0-0pWR`xLhux z#$3w|a{bD2B8EI9_3kR^YHMm*h}=Q)*d~16-;DIY4SX}3mQNv*$$({9m`w&ec`=1+ zcSjNbIrE-wO`FU*7z`RH8h|N#Um~E}UauE6n++C=1(GBo7!0cEi-1U%EVFcgPNUIC0)YUrTCJrwDDym}W!YmL z*@oi1TbNp7`sZN8?RLZG^C23Is_X6L{C&!FwE**A0y?7(I(khNhaCkScz REK&df002ovPDHLkV1k8+DGC4p diff --git a/resources/sprite/tokens_1x/misc-2.png b/resources/sprite/tokens_1x/misc-2.png index 70264e0682a1918ceb072cb85a29a836574a4907..773b38915a43c4436238788704f3b135848a87d5 100644 GIT binary patch literal 529 zcmV+s0`C2ZP)4QF7AThRC$wQ z7Z(v+v`7^z#a5frrm;yJn#7l9Y90K+gLi-ToG<6Rd*0{xb@eH3Q>3rcX0{>7^~G-StUO}Dh0~eR}7;{$BGJ3co=iY z$NgHJa)o-mWpy{IYJpOTf^H&TuCTE9JFzRxp<&YLR@~cS7_Gz;lb~_Pq{;RX?0gh& z3Q>5a5QRNWBWScVfM*Dk8RZJ!R~Z@V2%N|HVi?UgD|*lg0;zzm#M#Ea)e$_3w{{ga zEBsh%Pui1VDAx}8@R{{;%eoK;W#Q)*8=LK*()#w(R1h#aM9KyYP#d3%uZ&n;W08$O{b)*HQ2z# ziJ4NA%t;O;n3yR&#r@{G0fDJbkR3s@li7aTKW0*y#@@eyaM|`9l+<)%?+^U~+1ZFS TH&-!o00000NkvXXu0mjftd;*L literal 716 zcmV;-0yF)IP)!LN8?lVl6Q#Q{#{bjIy=SkFP*p)H`e~tBx`>`jnCDNE-xlaaVEN@y3Q0TK2V~xN{`5nCLi%szP#Qma>eU%1SpK{b}b= z7a0>{D<+Wb61CMqCTE73njgUzP{cpcNwPHfnToQokTx6L+Q4j%AZ8AWn!`J5T^Np4 zR4@4bPNH8k_`LykSG3{wIFTJ$UVX5%80s=Q^8_6O()9lNmS*$lx{!Q6@fn{Q#N~1# z$r%|1lNw&nGxvqK-6ElN&%Arm^$9FA^uEg+_S5aSk zfaxDgs1Ep@)re<53Q%EetnrA%USlZMdM+`=k{-e35yGwrV3~qf^$0CiFmEWiI*YAv z^LY_-YeeGOBC?<}Grk-#O@mFOEHaV)#&TMKWvkn#i8a!Tj1`O6)P9HOHV1o6(?St^b|!CzLNtjCJ-L#fuJ!b7-nBo#INOI|u8a0U zB6n?P<%-Q|eb4;Q}$TZuOmHVfCpyqj49yv0bx@vsL& yxJ`JXptLEdD7oCrUxnMf#t@|={4c>D0R{kZDGud~Q~h@U0000tms5;R@yjmFN30OC6CY3@<8ax1} zkI#$&7nM@wyIs@Q%_yB@LqXAT5Q%t;|905R3~*JgaURrYL1L#Or$_QE3lMKr`11NH z1ut(fJjg;}oViFNVB6=1a?q!HGY~M6|E(#78p*O;teMQzEvT6-{^i|G)$DFRA{s%X8>LiEDP@GJglK9)ymnB*n%&Lf8BmM}aDQ-oKGjbA?Hyh4 bkBxo+8-HtlSri%b00000NkvXXu0mjfJsrv} literal 692 zcmV;l0!#ggP)Ru)N$3-wqZNZ3BUdXqnX`poWF|16)~A zV!=K?S5*!En_e!j6h_N|EwfcOw{l(6)uYjX^fo?*%U8~U8yr)gXO7sMB7fiW(YGOh zNmHiD;SRqPfB6Be#}DD&z##TZ9(;TA1IMHQ*e!NL6ooD-XJ${u3dw6@K4Fs}H6Pdu zZ$}HZyBcufLIe%Y&Dhnx6D;^*i> zF=v#-o~Q|s>?LG@fr$cA!(Y&T(vQXEB{WO>;10UMi8L1({CC!ctN`%d4UXq!?Zw3O zEV?g6k;!E-@@f*lwZ91Y{4n!5&3K)YTRDi|4zsJpUc4qCitr6ez0s0!0~`tib7R#K=tMG?iBax)A(UKhC}P<8V0#{xy`%Fg7tMcOud@gAgj^ z{?R{@cq{XWoph1re-l~R;O~fPl{%IEFMj*DH@~*M$hCcHBXbPy@o;UQ-T5Y2txGYt zPTt&LpzslZQ^O0KKD6*_kx&w@oV|_u$)FQu7}>kb$)QE2PTZsHHi%-Ct5Y{|ZJ+bU zAK?ck7pt@M?rMJ@NPu&rj~O-_bf5M!e)tKF2zcHobEN2DWkVe6GioctwM#cJLYO{w zAFt(b-LL*AsvVspnlGVy7Ja34ln(_eL+^-}3ycpw5J;I? z5h)XBgZ|b=1S;l(0hCD4U9r%8jpStyu^nNo>L}6IA4$xZwPG77le9=q&%Sc27p&+q ziX^h4ZbOst>yGF&vvHIYQq6^m$QQy)Uq)_dG1Pg5=bryL|8xHL7{`A+H>fpzG4EONySIj6 zpcEIaFw}hq_VuDH8`Dtp{&h@T-xKgWK7RdQM}%R-$$_ChE0Xk~R(z;T7Bv;f<+6C0 z3~t&Z?YcOQgYBfSC4p_4lE@;e1sp&56H%nGlDO3{rm^t@np;}PHe^YA9-dfAg1DA} z;bgEZB>_~OdQhGdj7AIAiiJU?8cEg!C#`SM(m_9T>EFldw!WGBznvRbiq4ZFBQ4YDDyJ zE`uB?OWUT>_^C2J(?qdCy__vwLGJlXEH9nuPC&EJB73{olw5YouBEJX6=<4)PiyEY zNU*|{m4AN8{tZj$mluvvjJY$il79+c(DQVFoOnb_#rYv*Rzvu&c&&inf8)Ea{@}E+ zFdK`-T3-#y-tPzh+TF83w9kmvil9>EmrFPJ`AF}&H|FV4!p~YbOfFb`)h7NP7#f`X zM#p+#@=T%^F9zgu_?@Zzp8*!q6yN$cE;jnM<9`AS05hW!cLu_ntpET307*qoM6N<$ Ef_U430ssI2 diff --git a/resources/sprite/tokens_2x/coin-1.png b/resources/sprite/tokens_2x/coin-1.png index a782f94e7678f6793fc12567e1c43b51e930fc66..6f73730ec086a8f571611d15931ba6aa64bb5e82 100644 GIT binary patch literal 1167 zcmV;A1aSL_P)1@T zNgTxZ>cu4~r8pNT`I^@18Li||tz;lX?#6qMu`V;#-Oh~t)mV4MIe+=|>ZW_3KpPjD zOO;T6R;vUr1zPEYBw>s-aWDHW&FuNptDD#F3ut|%A)3b*E;r^XZzw4xfRqpf4y`>( zfs~$pf$$!j6Ictz2(nD#`n9pn`6%vPF~*&{Tg7GqZCq%csMo@)bJcJOKxIeeA zSCS=VnIK6d#^j;8Nw(eY_D-F-wDtGCfqr^t>CyRG<+nz@fd_px07Q;Y&b=L}QTx>q^&|>`$DFpNLcGxf8L%XepeQ;1|?U|pq z!{#?e5Jd{_Jv+Pclc!cUe;?SEDpU#d`%(ykz?Yi0+m-hWGXRL=B9zv-e<1`xpgtP4 zEdhmEodpQP;#N8*==CN}_P|P0iM2&il}a8X4D|Cefr3CS4l*bl-Fs!mm1TvmL9>Ti zHD&@SDFx)sH;~)+qrbOCeGOtrDM}fafSfOycp&lM3RLnnh~dYsYY8aLEI{e35Bf6S zO{1+ah%tyc6(~+p1LeNadiu+IFEFFY4PtbXwB)=5wA=3P=Ef4|3aEPB-Ph6O<7f2d z<}mnvqnk@0?k{FnwX&UvnH|3}ixzF1z1ULCcY_WOH1NwbaXTRV@O zeE29JX|*)fs-sdVdTD&T%#!!e>q(L%AF{?eZfxHsNwXWk+vE0ATXlV<@f7flQu1W; zSc68rnj4FJtOP+`g;|a6{ouzaNYi54M{$p>TP>`0{{k;8uSM7P1=NQ+0elXeovVf% zKT>C|8kTIN^hn9r7nafu1&EWBTe}@P-4x&&@XGR9^yjp=d;O^;a2|LUXoi7ip@9>83=TgkEL<65s>iz2&tinYwW=pb@YN@FwsU@Er7ig<7GNqCWz0 z&SR`4Gj??R+yp)aK3-mnZtfN9z|Yl0K%BBG{f^DgrAx<1Xig`R|v+RABaK>Kb^w zZ)k>d?^GJvTU8s_sI-CUL#RYy0r$Mqut`DMTR-{~j)j0h-2?nQmjkYRT)z%6@hX2+ z2nAHzz8NwUH4m)-aVSfe3n+^0_k+JUaDXw*#B8~zZN|0v(9mBd- zY@(&AhUeYJb|s*?qTi#abXriM!g3inek|q0*k270M}c0>M#~q`alv2n~BUtd>T>xid7v18xoeEW3y9Ooq4gC6X{=uS-N z6y;71m0Wj-Lq>buso^R5_7d~MhZlLQx2;=3DbRl#COL0p61O=cihP=L`v@wK+_?>? zh4KEkC?hgvQ(nNjQODRQX4iq`HM|w87JI~b<=yHu<%5!#?}1(!;Xh|aBPTcyM}ueK zJ>Zd#qsvl&GRAMguu%mG44XaYoG-T?a6bG}eMUX9V!w#`ibhNj^?wXp%B51xxhRQE jvEPIP1O8hBehDxDyRZ~a>1Kn-00000NkvXXu0mjf18!c@ diff --git a/resources/sprite/tokens_2x/coin-2.png b/resources/sprite/tokens_2x/coin-2.png index 646fc4dcfcb0daba33dab3a31fb553dceec2c64f..5c462eb73b686d31824326dbdd8c1833cb2c8fc8 100644 GIT binary patch literal 1197 zcmV;e1XBBnP)0NlgZ4TbMN_ZaVPWDnKWs{h2()*%)Re@-{+k3KF@tG9O4lF zGl=~JH`Z6Bl=3wx<%>$GN0m}FDP<%?nI?pQ_r7q>-E_|V;+*>;gz)*}Pd@AaRYHyR z)l*vQOK}`uR7&anEW$Zg7-KGb?_WRj#530pM5wX8DwR^7rD^(-lu`f)sZddh(lJ75 zq*Tz`Aws}=M{ots=D5NTyaT{{@4MaZrN_=a{pLLp`uT_NPNr%4Qyj;~0i=$J(pglL z+-;E~aD}1hw(zz9z!>v~G3M;q^A|SvOsKKGdMZuR-=Zj*5keBr%%S3REGpn_fp?Y= zJRlHKAZ3Kq(H%K$o>6p~gb*l-Vk^tChtHnBaQ&`?8tbcZnx@yHD5?pmNvrb+srvW5 zE3n-*w&+3#lea|(bX-9vN037;=bgoD-z0cPp6Az%F(<|wUXJI|T7O-VQKMu+An!Ek zZf#)mY&w}W@l%w3L!|U$51-% z7wB$p_WNMJP&y{5&iBi?qKnzSh4(&mI-L{4>KoZ1t@Zmt2vn2|$N+h#dCz13oXyEQ ztpUL(Md}zSrI1p7FdREhs1Bgxnf^8JYuus8&e4tMe%)>l`fpOZx0eGO;SCEko%fE$^tv?+!x9q zce~xjTDvlp5b#N}*~B?ln%NL3sWg?j1)@r|uaMm!q#~*uB|Uoo2pO||yL=y7ORLoa z_;BR8@kDDY%il&(ba7!}0U-oR$HYf!BUS*Vr+Ay=y#?P>rIbjikx@L-z)(uwv~QIY zg7<7}Y+$YZw7$6X>TukoGkCcuiYpr%8xPLU&jUH#tsCfcmMA&Wm#&JEJLl{yii|U`fF0%U1#dGm1POM0lWho*IHAlR7jG9D2jI7p`s`-#*k&%j`_8L zcY*ini%U~Shdq6DtgS3pfLDOGfYZ=BKBbUSB73Uy-s8O=ci+E(kAaWsi%U1}R%>71 zWj%5a0nY>HfHS}e;J)7NE#NxvE6@Ng16S&cOS_Dhhd9I`{#*P5hF1i21~T$m00000 LNkvXXu0mjf9xgrD literal 3431 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 literal 3443 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 JPDHLkV1n&`_aXoQ literal 3615 zcmV+)4&d>LP)KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=0000WV@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F} z0009@Nkl7(Y%Hzp(8|QvKrGbC zXpD_^R+bhf#zte}ioqpBqPQRt1q25eW@CWap2ay&c!t7`_nQ0eeea%o&wu{&pL-3{ zG+D{V#R@+q){9{nwhyl^AKU|A7K`TqFwJAq5CCwyOT|E00Dx>NX-&F4t9n z{oT*sF12qVzv2iO)=n%0dM>uz1gHvyI{|8&A^`X*%Vp<&KLBp8fR`E{5K9H=SJgBE zke;3aVEX$A0EvlTw*U@YczN1U2>j(h&!zUu0IN4_>;!1oz8QeIWK7!BGys*MUE=xA zKAD))EHd%{B&VW+Sh;XxecNsT#`?=UWgR1?OLjX80kcr(0%&RP0AOU zb5R|4c?FTEWI;up4}g4bPR1u^S!(z9m(N`eMZi+TQpxXJc21>A_E@v!uq64Iu-=VZ z40t>MREH%y3I*XpUgw+G?f$F0&Ou;NqKcaLH2}n;F&$QqElzmLED5#g$XqHV`9C@d zKzPe0iI%axDX$mlByV$exO><4Vj$7 zbs(D&1#8^31Ay6?xMV@!SMe~NUM8CtN31I7ud4uHRjag#A2I#QsXAco%c~xYz8BU8 zBWnO?-Y2@?sqiZY@~Z85tpUP;`J^aed_*`fJt&_0s)ReaS*LPfIGVW)&|Fe-3Lvsm zJf4jS2ZrAWA`7YXKcP452$q#82PB*7Hmz0;h%O9_rLF<&bqWGYPsL$?d*}Dv0I09@ zoB#-}U86eCpd9d8Cny^S7Ii>4FryquO(Xz_CKv7k9KX_gM$ZLNXWk2dMu4+NTMhvn z*}m!|Krm3+A+a`n%NkM^6b)k@Ad$&F19;RwbqC=3gKtmu*PqV587+AN+J>~P`cLBO ld6|T^6Sg~})9!|U1_1dMKBLTCFERiC002ovPDHLkV1jl$r5^wQ diff --git a/resources/sprite/tokens_2x/heart-1.png b/resources/sprite/tokens_2x/heart-1.png index 93690575a054e1aa395dd32d9d49548bbc6011dc..baa1024882c588b9814cf74eb8234f93b1adf1b5 100644 GIT binary patch literal 597 zcmV-b0;>IqP)CL3+fWRSJA^SO6V2@9r7Sl zhe+v^Ajo+H_VJ7a_3bK0HveV_Nk%(C;sdCpkc;w}chcM+Hb zDnKuA3HS}H1FwMvW93h)V%LrnpuqPY08fF-E%y5b%or=rn_!$f+8c@<0g_OJj*HH3 z#N_HYx zZ>Zu}6s7{)DWHQ3`lT;tS>E*k*B}CQ<}Ay*{-`5-WP1e^=FKidplXSbFja^^?@EM( zS%L^O4;++d;Xn;Fkg>86UMMN^YB=^Tn*ml!z^v%rKpn_f*#&L^YyTNDqmm3lsQWH}DDQJ|)a{++b6XqOJlTfhz@J zHi7HL%8zCfPCT;u7%Np^2nY(mYyktUFo_^VeFa8%nmSYtgNS9kQ$_@CEzY_m}2@KFlwxP$+AZ#kfPoI;|ZvJ z;GVJaA!~nTkfIiWsV2jRz__vU)}`ACq^LPyI%a6fSb62z>kI-gR-OT~b(bD0wA=)~ j_ayMW$7MUtQ%e2;?=GmXghlVn00000NkvXXu0mjf3SJDF literal 3608 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 eGjh$q&jA2-*A+D=90$Sx0000*C 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 literal 3517 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 diff --git a/resources/sprite/tokens_2x/medal-2.png b/resources/sprite/tokens_2x/medal-2.png index da566050695d54eb6c6972587cb04c39963a15e5..45871415530889960c344ae6e2a00c563fdfafc0 100644 GIT binary patch literal 859 zcmV-h1El)IfcqQ9f%CI7=>HtsDdX1`t9ao!^3@r6O}2 z%b?f#f|QQGbi7b3-Pohnne@CO zl?3R7`w(cEJOGYu(Q7sQU%paO(rYz>%kAa_2XUewKvMv!rXz%F>|WTYrjfy=7DAwU zRrS3#3N#KN)b9=HSka$At7AgB z=d7t9MoKp~#T7bNv9ii&tv1(+bM3rE_G4 z#!yokgrXu86*ZM1Gc<;)f0$vbB^{gY(jw9c zhV7+7v2?pHKpxbez-e&jnZ7BHmdb)yW#JvI*=dF6*k6TW=}eM+)-&zTdC${QIvC4M zjLs6OmRj-p5x)FBOei;2i7Vu*R;^qBZUAF3p>^O|p;)@J9{+U%c~E)W<2wfY0r>B# l3%IR)5BHc_KX>>a=^OVu9aAY0i_QQ5002ovPDHLkV1j1Blv@A* literal 3451 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 diff --git a/resources/sprite/tokens_2x/medal-3.png b/resources/sprite/tokens_2x/medal-3.png index 862bbe4cd532a30ee2c99edffb92bbeb80458652..2aa37023ce8dc586f630d2564441b2d3382214a2 100644 GIT binary patch literal 841 zcmV-P1GfB$P)Ci65^7* zE-*1hRy2{A7*SJ7q0>U!JJ-dXDN~frAPr&Bchls~Ip;q2oOACf95ZP_2BVYdH1O2F z_T%R=LR6aiC3*&l$4>8EK0h~;UTx;52LTqP;eO zLXc4ypclZ0L*u}`xtVl6$S82y1B_0lhk#E&FTmQbWiFhlad}wde7{a2uIx67@O91M zcO*((%~3w0e>NcP)YyR^#S$>#QJd4qRLXUVR`z5pY0GwE_m zfwGHZ+UM1t_ZQCbQs9A=YH8Okn>-2~nO=sKK8m{?YG*J@R=xlzM$>Pxqh zsm%PLfwGHZ9l#1Oq-l`oH8EWnAtc*nhiX+-!?HBGBD-0uRV5qw8d81!1`H=tnWq1J z!(972z!0c1c`!RW4*6V-lI>8fNU9Y{$yRGSJ1Q-vr4fnNw-|C`HB}g(+W=?~jTwMy z+^vG}PTn9XVXGkO&F%R##5yF<2&i&sZh=cB=btcssFK^%ng&)!y)#By0W<(K+$OJQ zn#phD^Brgf(2*((P!7NmyC*iy;Mc-$b6yR*$vBV^B`IqUNcROk$O z$}W!G2c7`|f6>SRPm-z3tAoTpFpvus2PS|Uz!l(>`{_68YR{v}RQ#X#$4Y+yBrqEm TImq}500000NkvXXu0mjfHj;() literal 3445 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 literal 3509 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 B2MGWG literal 3507 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 TZAee*00000NkvXXu0mjfp$e~( diff --git a/resources/sprite/tokens_2x/misc-3.png b/resources/sprite/tokens_2x/misc-3.png index 9a5046cc6ae71bc6fd17c3c329d6c6ee8c1708ea..c40e5a28b3615954b7a1964f7abcdbd2bda8969f 100644 GIT binary patch literal 794 zcmV+#1LgdQP)>)kd%zO#bzI2wUlk-$Vg{J*YGKspV+6Pe zoV1|pz;j^a+Z-2w>%eK5lo_Wwez$^v&w-PiQ54?eJZc}%2lQ11`Jm;Q2Q)WDT<=g< z3sY?|5{gocDd>>Nf^L}evpLwq^Xb-4t@{(2x|h zK$@fNn-zM-h5TYiaMPi)9*F@s=R~mWkVuK!RY4L>VXgqItQGg;z$k#?5^H&gHGpD~ zHJxEHlVujTC6m(hb8zZV|8{}S-4`tn>p62`{&&$|(5;gd3dDh{Sl=&4L5b!tPj_Eh zBG48$Cr0X_qY3<~xqpZA7KqEF+&GpXnUuP!vbjh0foc(Qtab$1&DywFW^&HI1LEfbKx&gC4U}>e)uBzmK z*MMc0+}xpIVDz9pzX8ju#Y-JHQR*`cSXeGz>V{B>YiwM|VY_;^9TF*VO3M>9^92LY z9zQGwiu=?WJtT&WO+q3idX6m!m|9umyaUHIGFiHTk0K>1tT>hz_|DCN!MUI7PmIQ` zPF36XQZ&u|Dba|)Gy)Tkpuw~ZHv{g2z>HF4gkITo=pXL^-t{?%7UC=w?i)g z48sT-hEeN9F-1`{MNzW09H%5V1}+Z*6J7`c#_m6!JpN()7bpvILRpX#%7UCw7WCiY Y502)|A;R5GcK`qY07*qoM6N<$f=eE2>Hq)$ literal 3520 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 literal 3670 zcmV-c4yo~pP)KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=0000WV@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F} z000AlNkl~41V?B=U6$wo~{{Q!etDN2QEYb_!+G$bHZyfN*C zf`YwJq|g?6p*Q{s1)&T5&>JO%=0aL+3n~&SwhdBRtb#-nLyeo}!(?+lym)4{D;A|U zIlecubKW`Uoq6V&nRlI|pPlLl2t#@RBL3%!p6yM?aR6{#z5#Gq;Fa|Kum@oOo^E-T zOaYK^s(p(d1{ziAmB=3f5IxyG3;^(b$>YU=o_HV-grPhO2$wR*2qLEzgtl7zA0fO$AQJ6{!9U0S}SQyKMzj1b5t zWbbJb1H=S@&=8n04!AX9^LB3tfWM+n z%iVndHjRG=z(DXm0IRks_wL{b0L4qc0`PA6Cn1;DcfTA`b@hJmaaD(m0S6p)tWs$L zfHpNQUVB>|t!40OQh*$W=XmWkzAP5y%yEmpMOKoRl%%<9$^gD`-Wz*GHq+y%?t%?-e zX2yEmxT+*yqy0P+R#k3H5SW@QoikV70$_vf{AR50G`*<$ez@;d>@4yvre6>zSZ7o3 zrs*9v1HP6zmDITlj(`I!2l#O-`vBH2=Fp=pa0*KHmEq+8TyK6IfUes`-5ISof+ZH2 z8Yj6`lKx%$+DnpWzvmNxcYfexY!Ild7L)ByGTeQv|7U=mUF&sE@NzcS6+?Me)pFw7 zivWD}VfVWKIP-?d@px3hzkZKBCac!`oj-~I1N~#~0fbTb*nd8CzTn=yqCL9&EDpqq o15aDVa|%zf8#Q_Bxj^8-0KL&0A`l<5NB{r;07*qoM6N<$f*CH=lmGw# diff --git a/src/applications/celerity/CeleritySpriteGenerator.php b/src/applications/celerity/CeleritySpriteGenerator.php index 55346c915c..0dba6c346f 100644 --- a/src/applications/celerity/CeleritySpriteGenerator.php +++ b/src/applications/celerity/CeleritySpriteGenerator.php @@ -61,7 +61,7 @@ final class CeleritySpriteGenerator extends Phobject { '2x' => 2, ); $template = id(new PhutilSprite()) - ->setSourceSize(16, 16); + ->setSourceSize(18, 18); $sprites = array(); $prefix = 'tokens_'; diff --git a/webroot/rsrc/css/phui/phui-icon.css b/webroot/rsrc/css/phui/phui-icon.css index ad789768c8..aa362034ed 100644 --- a/webroot/rsrc/css/phui/phui-icon.css +++ b/webroot/rsrc/css/phui/phui-icon.css @@ -8,8 +8,8 @@ } .phui-icon-view.sprite-tokens { - height: 16px; - width: 16px; + height: 18px; + width: 18px; display: inline-block; vertical-align: top; } diff --git a/webroot/rsrc/css/sprite-tokens.css b/webroot/rsrc/css/sprite-tokens.css index 9a911d40cc..2e51031199 100644 --- a/webroot/rsrc/css/sprite-tokens.css +++ b/webroot/rsrc/css/sprite-tokens.css @@ -14,7 +14,7 @@ only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (min-resolution: 1.5dppx) { .sprite-tokens { background-image: url(/rsrc/image/sprite-tokens-X2.png); - background-size: 68px 68px; + background-size: 76px 76px; } } @@ -24,61 +24,61 @@ only screen and (min-resolution: 1.5dppx) { } .tokens-coin-2 { - background-position: -17px 0px; + background-position: -19px 0px; } .tokens-coin-3 { - background-position: -34px 0px; + background-position: -38px 0px; } .tokens-coin-4 { - background-position: -51px 0px; + background-position: -57px 0px; } .tokens-heart-1 { - background-position: 0px -17px; + background-position: 0px -19px; } .tokens-heart-2 { - background-position: -17px -17px; + background-position: -19px -19px; } .tokens-like-1 { - background-position: -34px -17px; + background-position: -38px -19px; } .tokens-like-2 { - background-position: -51px -17px; + background-position: -57px -19px; } .tokens-medal-1 { - background-position: 0px -34px; + background-position: 0px -38px; } .tokens-medal-2 { - background-position: -17px -34px; + background-position: -19px -38px; } .tokens-medal-3 { - background-position: -34px -34px; + background-position: -38px -38px; } .tokens-medal-4 { - background-position: -51px -34px; + background-position: -57px -38px; } .tokens-misc-1 { - background-position: 0px -51px; + background-position: 0px -57px; } .tokens-misc-2 { - background-position: -17px -51px; + background-position: -19px -57px; } .tokens-misc-3 { - background-position: -34px -51px; + background-position: -38px -57px; } .tokens-misc-4 { - background-position: -51px -51px; + background-position: -57px -57px; } diff --git a/webroot/rsrc/image/sprite-tokens-X2.png b/webroot/rsrc/image/sprite-tokens-X2.png index 84a3407caeff110e237afab4bb073b7663e97b46..d1ebe2e613f73a4705a273a67b1b7c51a61cd209 100644 GIT binary patch literal 13565 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 HCL#X|sBGQe literal 8522 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 Hu0mjf!CIho literal 7249 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 Date: Fri, 1 Jul 2016 09:37:56 -0700 Subject: [PATCH 27/35] Touch up a few tokens Summary: Slightly darker thumbs up/down icon. Slimmer desktop monitor. Test Plan: look at icons Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D16215 --- resources/celerity/map.php | 10 +++++----- resources/sprite/manifest/tokens.json | 6 +++--- resources/sprite/tokens_1x/like-1.png | Bin 342 -> 337 bytes resources/sprite/tokens_1x/like-2.png | Bin 353 -> 328 bytes resources/sprite/tokens_1x/misc-4.png | Bin 508 -> 568 bytes resources/sprite/tokens_2x/like-1.png | Bin 655 -> 636 bytes resources/sprite/tokens_2x/like-2.png | Bin 650 -> 682 bytes resources/sprite/tokens_2x/misc-4.png | Bin 1227 -> 1243 bytes webroot/rsrc/image/sprite-tokens-X2.png | Bin 13565 -> 13542 bytes webroot/rsrc/image/sprite-tokens.png | Bin 6029 -> 6058 bytes 10 files changed, 8 insertions(+), 8 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index b31fe5f6a9..0c513b7dfa 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,7 +7,7 @@ */ return array( 'names' => array( - 'core.pkg.css' => '425300c6', + 'core.pkg.css' => '2cc8508b', 'core.pkg.js' => 'f2139810', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '3e81ae60', @@ -163,7 +163,7 @@ return array( 'rsrc/css/phui/workboards/phui-workpanel.css' => '92197373', 'rsrc/css/sprite-login.css' => '60e8560e', 'rsrc/css/sprite-menu.css' => '9dd65b92', - 'rsrc/css/sprite-tokens.css' => 'd7059378', + 'rsrc/css/sprite-tokens.css' => '72b952bd', 'rsrc/css/syntax/syntax-default.css' => '9923583c', 'rsrc/externals/d3/d3.min.js' => 'a11a5ff2', 'rsrc/externals/font/aleo/aleo-bold.eot' => 'd3d3bed7', @@ -344,8 +344,8 @@ return array( 'rsrc/image/sprite-login.png' => '03d5af29', 'rsrc/image/sprite-menu-X2.png' => 'cfd8fca5', 'rsrc/image/sprite-menu.png' => 'd7a99faa', - 'rsrc/image/sprite-tokens-X2.png' => 'b92fe16d', - 'rsrc/image/sprite-tokens.png' => '938df9c8', + 'rsrc/image/sprite-tokens-X2.png' => 'e991bb40', + 'rsrc/image/sprite-tokens.png' => 'fe69d6ab', 'rsrc/image/texture/card-gradient.png' => '815f26e8', 'rsrc/image/texture/dark-menu-hover.png' => '5fa7ece8', 'rsrc/image/texture/dark-menu.png' => '7e22296e', @@ -888,7 +888,7 @@ return array( 'setup-issue-css' => 'db7e9c40', 'sprite-login-css' => '60e8560e', 'sprite-menu-css' => '9dd65b92', - 'sprite-tokens-css' => 'd7059378', + 'sprite-tokens-css' => '72b952bd', 'syntax-default-css' => '9923583c', 'syntax-highlighting-css' => '769d3498', 'tokens-css' => '3d0f239e', diff --git a/resources/sprite/manifest/tokens.json b/resources/sprite/manifest/tokens.json index e4e83bc562..6096050a30 100644 --- a/resources/sprite/manifest/tokens.json +++ b/resources/sprite/manifest/tokens.json @@ -34,12 +34,12 @@ "tokens-like-1": { "name": "tokens-like-1", "rule": ".tokens-like-1", - "hash": "cca33a8c4c3bd89adbbc00edf6dd1589" + "hash": "1b3966d6e0e5d902b558fe3d76ed8a79" }, "tokens-like-2": { "name": "tokens-like-2", "rule": ".tokens-like-2", - "hash": "faa428c378fd1ad504fd46e2890e6b7b" + "hash": "b74308407fdaa94e08492cfd9b44f2a2" }, "tokens-medal-1": { "name": "tokens-medal-1", @@ -79,7 +79,7 @@ "tokens-misc-4": { "name": "tokens-misc-4", "rule": ".tokens-misc-4", - "hash": "42bc383165221803c281882141ce4bef" + "hash": "7371fa5ecde282e718b7a15b02ca51e8" } }, "scales": [ diff --git a/resources/sprite/tokens_1x/like-1.png b/resources/sprite/tokens_1x/like-1.png index 8404c9d31c6be97964f5f614e741e8ab8c0236be..dfb7cc2a90e34430e88466407747125c587d6eee 100644 GIT binary patch delta 310 zcmV-60m=T>0?`7HB!3u5L_t(IjlGkxN&`Um3l!)VWGZ3iZt3JMJzOOsf4r05tj8ryyWn2X8DKtW`^B$uq2fZ zfJ@*k4E(O0UnDohy8t$T8>#fxI-t)adcdtz+F2FUHqKR`Eq|3RE(87QmQ2tEYKA=q zu7RsC@IUI!R?V(+q|zPW*zh7i00$|J$H2+U^F!ZG2%xcSY(v|?VOqikIvGF<SEc()+-ckvl*ortxc|-o9!zJ5^J(0NeqKv#L*7EtgBjt!5{(5YyVJ5x}!8 z(Vi{Qqb*U%mT1$K=zN}sU!U)IgBN^%1pNM^wf_i$Aou_nymj$M-vr|^EABdim@`RW z*zbF{0J-HnLNsJq$~k|Hj(`Usn;`Ij9`Mwtw|9V(LeZsAjGSD0egHJbP{zw8>o5QS N002ovPDHLkV1hs_jQaop diff --git a/resources/sprite/tokens_1x/like-2.png b/resources/sprite/tokens_1x/like-2.png index 0d292e640c92c6a2ab9a543a46273bda879e3fb7..287a04d55327e1c943aa79366238095cac225b59 100644 GIT binary patch delta 301 zcmV+|0n+~A0>}c8B!3S{L_t(IjjhwKOG9B4#_`WC)6L|MVAx=@xGHG1h=!XiM#1b& zf_jTZP{A zL#9LK15dcaX&i;eODh{Mt)T#O|3F{>1|0S-V(RemG zHtx0Yz2t2hU9HvgxW-hIeS`ft3eO?wi)0gC-E4tuV>gb%&tK#078aKQpU2f`ia1aMwxo0oIJZpWZZ~_j z4qSNW`9X)kjX#ts0|%CAjHaB}_p@$G{U_ik3<-jUQvbvQo5eE+q+Bhx(8 zv-cGz+Q*P27g=)t`|$9-=X3^he?uX?#Bk?5ycHiZf`Pw!K8Vl- zzGWYFY#kBS>Z4UEi_8^e`ig!4^r9bqs}`@5D3*$s4}Tz99Q3LRuC%=uv7sQhB)bOpdo hzyWUms~DlF{08=WumFes-%kb$)QE2PTZsHHi%-Ct5Y{|ZJ+bUAK?ck7pt@M?rMJ@NPu&rj~O-_bf5M!e)tKF z2zcHobEN2DWq(5)>oaOA!?jB{FhZC6CAy0xgpm-xw?LJw9uP>GS`jG|XoLRNMg%J6g8`IC&|R_6eU0R053wC# ztm-Jy*B?pDn6+XXDU-BFPS3t_s~4>3GKwU!qHaTz@muST=rpr&loL|Tg^I`*!c1RA zZfG&tz=-`@_0o)#vZ=5QV-u74u+?4y_H8}WZPTgWz0*wBw5^v?Jk)LTC&@S708YB_ Y1qbJsG%zmiwEzGB07*qoM6N<$f`zy4O#lD@ diff --git a/resources/sprite/tokens_2x/like-1.png b/resources/sprite/tokens_2x/like-1.png index f2d7b45c37977fe61b193c0261b84c82043f9acb..4d4a4d6a9d23c462f768c2a5b771337f558b651a 100644 GIT binary patch delta 611 zcmV-p0-XJi1^fh%B!5FmL_t(oh3%J3Xw*OyfWOq(s`U?PgYME=MG&cHLGkX0fp!C>1;ekCH<{uwDca>@DDB!GneF(cPXbqKHL>-EOt$u5COdQ?u>vCJXsd z=m#NW-kap(4Vjm~7M;^)FPw<&n??I zl&X-=RGaZ(pkUd~;h20PzxF$TA~4z}r3%~vrcEi<5*PBNP6D$)e}}Yrye(qNl;UTi zLcY{RV21Fe{(lJE2A-NyxMNo*B=8{Y{x7BEJ%M550m$V>D3^*XFIU&&szR55SU=VSd=Fj> z4*q?aHBDn+;9yg-655lHObK`w9B4G^O$qf4*Gi}mv&b6o5;$i{;RXW>3uRVUe*;jf zRapG|pB<;2_8m52Q_15G+ZW+Fs5f`5xEi*AvX; zmVO|Fd!Fxe&nr*vIWB>{bdIK4XxYv!;4V-EW=umbLmZ_$>wZ0B$vEr2?x+@ft1ShQ?M`8rgID(GXI_8H(#pDchnXc_R@ z^yMwvxzQ(2PuuIPeKL)}ibdumC(Z4gF`Rpa$v$@E#cNQd$Kb z0`sP!|DMg}o__-eyPf~V(}i>K#za%VZD6cJeFgXndRId>Ac3Cvlxb3H%*;52Zg z8weoYcz*_*RTV zBA}PR@hBk@0iBFg5r}})_T&H}pjVM90uj&^;9HasiGP6Z0Y{^RNCb2>QbiyFig&CU zUk6?!(S71mZAiYo=mr9Vym@>OjC(_v#=cbnCf0$!~Kz(%FSp=pER z$)DmzQk}-XJS{ItWa3j&s*xN z@6{(yGH)?Cg*G&VrfHVU&B(vF^KQNWfkV z4Z0}jAZU9_aS`0k3PtqLK_r8?6huL8sR|;9t6c;UF)B%vNUxmvVUs%KUbrT?ACaVo z(9d$0@Av!O``o<;?_FSr{|pI0(~Ssl2bcjOj(ik|d9<$qRe!(&z5ylR1MmiTt}1dB zAd{Ut1E_v7%elp)qxSs_aIv2!aR^6&J-|LYb^^Ex6g1s9r7H3(AORc+(&31GJ@gMU zek6g%nr@8xRyq(uKL_svzyxrhp9Vtc8Sr_~Ha1>sunQq{4A?t(m;Mn#w`eWS&=EpY zp;|Z)Le)?$9DfL*rBE##2%-DH*Dyr{Lg*o|KSYl3kO2z zB*WV}`agiLHbGLaHMF02cg4&~zggK&A@Z25zW|Y<4CK zg*Bc0jL!28c zz^tms1%H?P1K>%Jv4Xy#d0@9o?h%kx6}jQocsaLd(y_SR81XLwAmb8$4V?Fq=@;Kk zRnv_Lpx0Ea1BX>b9_TqL$QyF|jJY;w83-m+1nM2JRp9QRWgvi%{fEKRj__qwkvDqP s4_(OS67T}}11zYD{BjsFI|Sh`6+n}Sl(pFW01E&B07*qoM6N<$f@{qz0{{R3 delta 625 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

;6n365A8W95HMsNa~*C7kld zA@dseg3-+Q{egb&X`pLo3T7o_PCc($`X3nT VsC_0^6374m002ovPDHLkV1juXKgR$7 diff --git a/webroot/rsrc/image/sprite-tokens-X2.png b/webroot/rsrc/image/sprite-tokens-X2.png index d1ebe2e613f73a4705a273a67b1b7c51a61cd209..3596bf5374d912869b781981138322c0130d1bf2 100644 GIT binary patch literal 13542 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 literal 13565 zcmY*=1yGz#&?XYxfmY%UX**T=Whv{qo`O)F#kL6j!*3Y$f=g2?_&D*Q*-7eyH71kNy)WZjE^{5_E# z@yWG=8g70xj(o^TcarzH>*QaNgMd8O?gQWM1JZDt&Ye2{9T$LFs|=)7T_ZJ_CzeeK z;3l28(Gj%M3Zfx$CH0KlxvXy5TW{HIxQLP&Q_mXr|0K(ix6$O6R(6e1E8F%T1h4D% zIS0J@(}rxQStISa#LTt7f>0b(xz1INaF5xGwqg8s7o^gq3{ZpRQR>|RaEPF`*A&V> z6RhpDtBZhkwow_$r4y^OOc@vkTg=J~i?B(kzzu6s)O+NfdZ}!URKcPTf7^gSCJ0fM zPjihO{c+ZEsU5J!T*wjPUQcGa5=Yc%Zxwm=;~l53>-~1+fzO_lFQnT+9{>Qjfl;jgba7&l{A#+HHD&LYzRrW zmgD+=(5#Yy`Xy_|Lhsx}?^4cg7<{|PEuiQ&dD=>^6;;htCxY^LSPvrR z95?h^oOQ6CKn{k#PM#p5($YS2d4MTJjC)(NRft7=`0S17=Mg`f026mDGYRn2vvrqR=ntCfu ztN;CnOhi_X9j73|_;#=xP5%6~&5ak~zK`%Au#tDOOGX2L{~w^2<3sy-kxc>c`^=#v zGQtq-idG#9exf&~EOOD$+kOyhO|EV9pB9Hopv^M8SpEeNofQut$?a#|jwC&&2B@~y z`5YE{q9XwMX;$hSo?O8|rCu@LMW1{1ma4IpGKiL)F8=WnsBIBz1JJnivG6QDK?RoK zLDF}=t+1uh#@ZzEmq}Bzo{R4l4Slyx>37}eXsWE1CeZ>|Urb+l-E4m%X|Av4>K_m) zI6);d^oi@SXpf0qMkf5`d{yI8JQnqTtZjGY3qtbC6XbFw%vUiXicM@}3D=Y#1X-w! zr0^0clo94%VLbysMW$Z9aEs?1-24UkiX^22eL4KT@bs^!ip8b=mY2o`A^azlyPm3C zPMH5Bdl07>0fCC+mbX#){lOFbXtPc;5zhZ6L2a>l}=#6g|>E$ojpKn(jb2}&x zAKreS%)m#%1qX!=ajWTosy7AXCmo9z3T)Wg#jcFzG{1i4F(8~x%Z(aPAIgQQfcVL~ z73$}NPk!C$82!i$QBm~Mar;-Pt9#V;Xj_Y|H86O1fG+?H&9#h2bW0p&&bX6yoiuzo zx@6gZD)G(pI&8H1nS#qZ?GNP(k~m}0(pV$kmb453U*_=94KptlN;E>O=X)oJ=0;MU z^EB84>^(*OJYJ?OdLFz}!h88JdW^4~NYfLkIueI~KWd1Dve;0{)k6+jC70chR30yj z8x<~cDeDfz#38u1zW2|aGLgXcCJ?G7FFVmtB5RTbqXn4@Ww<3#!{A)gi;~2DJV2^2 ziP0wN`Y=JLRUXQYy?;R!6cx*N>@g)GivsT0ok^(OI3_u#}UJ{_V;UKyEOvUSgw)KSaX^5wBgf_|(%+|9JiQfjyOW0sinVU%zkPb%=7YNgc(r_Qa{#G(K?~v~#9uEYnwnLR=?isy zgnp;!n7`&+jwLFHpz4ILyV0}mnLR}eL=hgI)wk_68x;(wSzFR`qHQOZ-$bmSzlwAC zMpx97m0{|Dm5rrLP>BRLb2{SQ*36i7aV2kzvv%lvo!llJb1$Ohh7E2AM z5|~Sg=G>|o+934Cz8eCR@nSnGST7OpBeX(A;2u-B4NyQiF9X6nc1iS=h^d~sv(xHDz%62u1DvB`J*G}FUTLEbsb zobvE)HUPlIV@`APrc#>E5)@&q#M?qu9=&dDbRt7a)M@ zR@MF+(Y?2Rw(Y$#lR$m0>0Ix~P3Z_Gs&n-S6FhZh^*KsJ@Mv$28!$9&L(no2yIWDqhpXr$XkdTGbo@R>H>C7x?(@Jgt z-hQe1n7Xa+=9s8tA*=`h4`i~$rD?GZuEGPc5=D<%Maks)iAA|}DwomT`~?UJ-6(5q z4GYQz)n|lSq!P-j$@NXd6E&)4L4xr2Qdx_WoOO`-hnU|#;v=$7&}bpKOsT52OjOlU zn=P}xwR~b(p~}PitPZCQUi_8AKYon{vUQuBzWTcW^utH+6HA?B>$1)ob~gAeLl%~> z=FCG?mwV0bH=}Q>hba~xx5Lo(eU`&OUFkR$wPU?X)KafmpLzwvi4MQDC`e<>%5h(g zsx&nPhdeZzs*9F(w#|uWzcS%2N**syy`E#)QztxzI^bc;tf(w1h#nsbWO(f4;QNMa zzu*GOsbOvc*))M{@yWpLHHsfim}q}UmoU>>w4=ugE@~NpOBrktaTwsq(L!Wz|+ru*oqq0E~qg+6PIasV|rLzfJK2m6{P zUMV?i03Df8H#;Dtfy`dkWF-XNjITcIwqKfNmR%d~OX1h&$pdYars4Rh+ELZqkRgt_ zkb3Ow!&W?P5k>(HTeB6o73{lM>LIfcCotStzNc7IImYvsXFX+Z%QJIWSXSpdqZa_o5@ajd|WptP^>%O9=fvzlesC0)H zMfJ~3CA@}z`w;LP~Rejn#JR;0 z{7_tQdpTPpdi%4AT=xkN(0|#oWtXk{xyH?2anfp;Z^jyL)9&u_(?rZH%N#A25ha%) z^rTi!t7B|e21I53V&g}M%Dv*U7?({({AZW8gEhU0u}v_J(m_urjWP}IlmxWo3by2- zMuIQf+eZFa^CBQ2q`bq}Ydv6kkucFswY0DdqD)OQEdJPlzT(UbGQl;fXXS5x8$RGn zW6PQQ36wYMDILsIl}jsyDks4p{kss@j(f_QF!5c>K%(_@SzMmU!J*T{;WkMRAMsMt z>n0w(UuSu3Ew+fky1m?MnE`%Cp2i-R${zJ+1u)LmD+=8wfi23-PgBMOxp#W{Bgmmv zoY`%L{$%TJx1aER*o;BJ%Khk9R8y`wp{_x5+0nO5xS^2_E=}n10(|)kx6dbx2Eook zFZ-2-FYWBRj?cnxW;^GnC-EFRZ*-*xZ{86(ZO)jgXX*B09;NCr=?^Y=7v@qaWHT#q zU>=K;Tt#7>w2zR$^AVKfwY{e_W{t9De@J(tH=3m<98=7%h<9Q4-ZdmWhEMioHS;^a|qFTT2A19+@0=&5p6Zm zxp8Kt-!|I?;`%QyIY16QsD5SGMrC{7DM;%NCQhfpVTn}P|3$|L4F5^bQO;6*xA2l< z51?j?wHb0ICUvi~fA9n=-F?LATVoAdNtPFW?9}@$<8nnbi{>aiYpIS$99RoPMMVfs z$b3q?&(PnM$D!VM9Zf8;A0efp-fLB~?qt^?Swr%pv{>#i-r!c`F-xXRX4=^i`YCm% znrlz13ze0W0wPCO+FJCJr}l!F&O%QT9Lc6bQZ0ObTs1BI9CX7vw(PnIhE=afiP@5u zk&C++Qj8h?)3Wsdn;KxkU_o-p^6Ej^=#9{+;)4LNdTWisXcJVOqANU`F~qO1@2GxzACgB&qHsH;-vDkm$_HBQcda1y?pvI=L*QXf7@#xge0 zG>)kGcvv!5YKy>?2r^#%@3w{?{O|pzX!#Sx2dBj;{aA zxCRb{r5HCFiz98WIs+ig5+uv`%asf(fNA;H+wT@^kN)=+i)c%(E5_^}1Jx%((}!OP zDAb-eGrY8QiJ)6T$4+-UC4C<~lMBj!>ydAwZqAw8_akyh{Z8wK>iK-E#KT(W$?#96 z@UJVESR*SD;h@`(vwVKokH(uvP=S1=9gb`i%|6X#{N^d+jQyXl!o+jmqe8>F--3I- zDF42I6>Dm3u~MI*adc0?Uz`R)4>fmSTJY6yTO;T0E%+hmxgAnf`zd=qM&x*$J3QxM zd|%gn+sAq#swPB48P-o@4-f@i3kyD67^w>;r@pnNT#&>K!2Zm(`Kf>-LJlE>H|aM_ z@NFQ5rC2SDI1-5E{ERc**!AX}`qct(wHWG|oAdhqAsC}t&l!VDxqBJs4tui}x@TbB zYWl>mS(3L*(3D%PZcn!aIdcN%*#d~!`iHjQ?H!iL2Lpi1U+!pM-#3Q_??1ZdgTv`1 zON&T;+aUIhR~S`S&-@|sPUiB;q91<>0nFc#$P1q0zwSRHaXkCBP4R7JOIB+6 zl`4`O!*w3mV*mB3F;kOedKsU6CZDIU;?o5(fI44~6&L8u(5S=n}n_ zyJ|kpQQqX*m+vYnT=5N^$N!b;G@?B{A${`K{nYyq4BX<@kld-vX++V2;q|<&4ARZK zoft00(-^sN9wxpkhfb33wB6DNbpMlblbv3+oKCqAljKO1nz?RIW86y6MUxLdML}HG z2-`$||6PHx(FX|2^I`xslMx&c1E>2BMEvp&iddF!G4GG&VYMslQ2W7rfk8jz56W5J z$oFPd(jduec7Lo4;SQj?vCusmk23cdr=k5o|$;UqNof=IOpaP~m7S?IL}S?9bb zfr#TsH6QZ6Q`d%4)sQlg*{ULuZzJOn?jqc~?~2TCg$z3}44Qix@;BKIib8SD`7qsO z*|pSV`5FP8y@8a2zPjSN28uLI;m;kRN`E3=wn6a_hN!a+A3&{%IHgy4Dmdq5*xg(~uLVw)FGe%;%5 z_TNPRU=}PD>F=QkA`;>O5Oa13t_*V~#f=}(x3D~l4BQs`;a({Tf>=PfRLYwSNokj>yWb5u#=v9L+9F324BxVUdyuZ zm-Ew-${58`u)2g5KsgER$g+Y!s*a?M_2>l}MaDX-D;oE$6!|>}fd$RSbRO;`;s3Bk zV3GVZbw?dSdQHJozH7Se{uQ(#@>NG6Q|S=((h)rKk1@@l;aO3+?+@-cdEA}8>|6P# zM3jN4xX} zhnq-Gd^W5~B>z`ix!&S9On$ z+~Sv}gH)S$ez-SG15*q-CEHhz8Jw=^FR}k)fnDI$pD6JmF%*r&riV6xX30Nu$}yZuQ7}xwYGtO!uH7X z`FOKCI6KpqKKzK_lq}+no(+pqyTdrAtBk>f2o;znShen4&yqeK+~NRUdsQ)bgWVim zzah6_FtTn(@I@+o|2YQO#OB&o*F*-{j%V0RuUE`1m(4(5>lNH)Wnbz5%&Yeg-!>B! z$=%JMXC9UpvP)AKCGEr0*()T?(eQa@)JJ$G=#^_$UdL8{rpWQ{Ch$7dVc_<)pt(eb zCjPQ8Pm^N0^$Od}kN(5DiNiW2o^6wrhQN8A@O#YI5Q})@;avbBv@|u@t7NW))BNFP zTb>r7xSy}01Pn5?Utw)WR5xe+!kQh=o~&({L9i%{lpbQ=4Eb`km$-bhHb`ufBeCQ(6CJ!?)2qYO)5-G}xk5 zUFP*Q&DT!|&C}Oy`EUW@v3K*3C;E756T3K|UP*NB>wI_HGdjcPqvBzc&pU}n`^5oAc`(RxdaiqtAv|xTys~K%{ zrJu3CMerjj>A`Vhan|ONc7XSrit*P)WlL!qz3f>KYcFu^->b*6&0K9zc8eIZCM%&> z8p9J*Wc9^-7YsJbV%sTR@HDW#n7{HWQH+SR55rr!@R{=6#X4qeIlsSdu+uh1wigp> zjYqS3i}}mC@IbPZjeh<5s_^JwKZ&^+1J&4Hb&_lR`q*EapF-%sdVi{>o7`g~m^0vV zckK2(;Bfjf3->^>9FrqZ=I;3+*7jwyt@v;QGV5lPZrGvMwf)-RnK#cJ!1AAdWCVm+ zxEA_|rq;WN)$~j7K}v}AN6ZNJK{WhXt^MOs2rjw5iMtX_O(CZDPBq2jAmdq&c;jVO zSzFsy<;P8dL0!!3USE~x7c?XXQ+lu`#mdXL+|Z=Hx|h;|TS^Sz7!_#?56a_aKYgeC zHlDHAYM*P~2jxU}2JcFltuvooJ_vZIwe)`-cfH6YeAk}yGW0mJ#kyRK(Yoo&PQNQ= zV#_L5L(j# zm|Fx%u#{O??ZUDhwUq{%iS8{#zvZr=c~y9Y=Kk`z(+CWANQ#S1T7oh{(mJlP9a}9u z+tigh!1`pGGv*lJpv#uXnH^Hiz;^VHeQucWwOPcZMD7vud-@7I^LC4DQF{xbAlgP2 zM=6v9xVjzDZ=!51j&;^3AF?-8Xz#d7Ub#q;YW{?!gU{AW~o2s<-39C?F;XCp7Bb6LRT6L3|{fS zUVxm|-@i9tC=Vlhx>?w{64-->DhV=$EcNiBa-lN6U;mP3VSGQgu2nI=Yh@IZu}rOp zFuQCwu!Mh=qEAhNYjl8?AZc-C>Z`u>TGVkQYd0qBSa|6}%SKgrBg3#&nxsQ~&i1&> z`1HhrL1uCtx(gWSj$>aA_ON~lI+xD51h8Wh>Xb!8tIpfkdP1xxU-|J~f>%|z7>yPN z5I!pOIgqgMIl4M=iN}6)QRc!<&)4K)XOji*J7Bgr3`e7x*uP+WyIRc~A%u&!7-f?X zh3gP`rpQ-GI~Krg?;$lH{Km;@b!s)2vcK@b{91k5-({7ZG~3NSMt)$nzCNd(eQtMs zd;dnAa++FSameFmoG|_gv(Wi&%&UFY$R&`rhQ%JK`vy0%C$Ke_;^NTqJtJMhmaQ&u zd@SD6&Gkg`3v>{vV?4z|l)6&Z#45x-wjplStsQif)+MXP&;*&R$|!KoK`A(rne1o5 zc(er-W^-TTOMe+_@<9qsH)UvhlK(8F>%6Dac1=ElhOSZ*aI(*uK3}egw#C&O^D0!so*qu1n388}X~X&*PK##V|jH#wchw z__|3T_)Umb$y=8nGuQCS={q*)yl#UR0^uHm+awk#P(b7DREu0UXaUBuGheFq2JQ~w zE9rB_QrZYcS0$~U)o}H=dTMu$Ovc99Kj>SoQHH%@o3L$8Il&&=^~waXQK9O8{^h=1 zt7RgOS3*`%-08y!=hjB8A~l2V1XS^7(5ulep>&w@{+T4$qaJC?hK;m3 z2D&LxRb387GUlT-dNwE6i=pOO$L8u2?w&@q1gmhlPtt>VcZDiU+1ewE*}*Wiva+rS`& z>e1os)l-QXU=FWS%$s$^2YRn5Vl?ldlBvxhaI#SGgE`uT8V%8ELFA=ry`o43_5tP> z5W4AqfXWp-2?nWR+rn3OGbqI?tx>i@37(W)df}?sLn$MaC7hX5eFobgv2o%1=|8%< zhSC)&yOIVp3)78%N$Z1F#bk5)GAc=Yr+iBoJJ~usn!{I7P1LuUMk+CEl5?M+-V>|$ z0O5@qM>d21x+O*Z>8MF>#yLZ|Qi@uyu$7NT-vEZ#)E>quT#1S6^Q!^=kEJ7liec6N zXNWNeS~D?6FwQuU;G!a$kYe8T;*~QX_RFXHffl9L&^|eCZ+;S_V)@o3A%qA>lEXLj zbgj<(C_iID8H#~mgN2Cdg)%Pnftwgwo|WDb3Wq*#Oe*}xa{Mt~kcb?C)pfmyW;bP~ zx26`F2)jB*{z@ktghHhHVVB`O?~SllVciy5-9J38 zWy&NYTjca>AtWfDHEYTcIGUc&0k5TZ{W+)_O!&O9DG4qQD(;7blPn8Su7phF`ol%3 z`;S&FIT=QFJ$u3pvg$~}MnZQ}MlwSDjt~dyrZjhZ5Bh`4tJFdExB%71EL+ysMTZy@ zA(%o#c=u6#vtCgFV&A!Y=M+^m9l5&EwCp2y1G&mI*yf&O{O^+Zwqq-pZT=Ds^ zyV+kSoQaCfM$aGRe@{JBwBvZhotQ-vD?h2e^vt<<~@BL0sQ}fr=%Juo+Jqz^b1$#Qebh`3P3PZ;I`dO_R7A+zB#TbDj zz!(bpD}>^EPk_m&9X7Y&mIc^1agNhB=tTZvjIAG7m-LsLN6mz}zc4vrPEt^6hl7WX z#$R6n^c@%O9!nC8bYmF;ye5Q>zZ(*w^6$Vr_eZt2M?m!z+l@yC=No$hlntgtI&#Nq zIup|aZ}jnmjToaPaxneiy&PV;^O!%Cv($5u&T2A?zfux zxEeP#gIwm-Zmdxr!K98QrwHJ{*bT_d#?Hgtd=@fWq-+An1H}~D;A)$F(!0=PswIUlpgYvsZ7Vq2j&Zw z<*INBt67nUDGGkZ{Qd|Z1Q1#sEcMLKP?G1Rz}}@RN&y1uc0#Me7J;9OMP3HNmiCHP z_KFwrgH2Z4A6B9 z&DA$@*rAIDC(%P&PztxsvDMGsoIujd0IyyAwz=Y3D975|>kSoLv*t@g%CsJ#nQMF} ztznpMG^Rc|jyxG7)kLVlcqa`hogThGc&o=Gw}O*KVvdud%nRG9UmiPRGC3tQn>EzyA1bj z#sb|h0rXQR`-!I%H&%UW_3xr=ROjU!=#6!ckelW*ubN-cPM>PR@s<4Q;)3e5q{*LV1gDd>U`1A*xhN? z*B2Bl?AwvZr_DdBf;db3{0*Aa@DFyN*bc#9!6-1TX`rqozLf`Ib>4Y$F>aV`8E}7RC-A(rT&tYgi z8)&Rz8Gy^MEE%{q+NTu#dfl_)ocHwuuS+Y699u1n!lWE=#r@3^nakWaT?1^fJi9b80 z26ssie;Hx+@LDuuvTim5-`a>z0y}FxTYZC59F5IKR>GRnw835FDBk?c>qk?=vJO09 znYm8$_BtFLPyYA@2uw~2G<#4dWsRa|)X&;LLpum+8CM5L2O~5VR1U-@@!6oQiMv}> zPI2gZEKjL9d0(haWTJ{SAGEAqUHM-6&H^qfKi8RrpTrjE#*L|@bTAzIm3QS`Cy!Bv z-~A5oa%gE<7%iB2+byTTp0wjol8%uUB!6^2d{gc3lB25eG0uql4;`X*WIG~^Hl=fm z+OBm9vAH5}oT_EQW%luSW_W*)e2*t{?949FU2|0(*;$S8ZnH$V$yN6;or1N|cW+kl zJ0S`F=#qy+nV5DHPW#IjzP0E5c7b_)k?g zld*up*jSX_llb3&BY87FvTraMMhP&kFWt_+_~(HK`pp4F0?F-X&#C_$6>t>xtX4L7 z&BVzKwPve8YBd4I85mP66aL~(CYZ0!2dJToL67K>Slaydz<^b5HOyCgiFa^Tg7`<&06*$Wsk z#fk+!nS5T4va|C$#cH{ZhQ#gA(7(gQvt0;2Jc_Lcj$>MSeH$dTAkG&x z4E>SQgd45?)i`W1pX0gKG#M8Yy59v@tc5$DMOkvVhx>>_e>8pZtII9k=!ov9ZOg`_ z4HsM=Jq?$9dFD0VY3}vD9bc85PXF9ojvE$L%Nl(0t7QFD;x3yKPipzUo9xbZM=>tC zBBIm}f7G^o1P2meJ?uc?{Jj2pW`+zew@@k-Pdtx`nAY-@osfSduJbeeMfEY4B2ex*)KJ$zS&G{ z(jiiN+0LanZW@_7+K~~oQuwt`cD(_O@Po5I2K`#gBI@ab7IJ?RT^Ejj4L$y*MFi2r z(Gk7qj6D&HDf08s_nPaLVJ`gQ_lfZ-o}aM?*(F84ov4_CnCzJXnouWT1OA>zP9nA* z9a}I5Ir2r)y@T!xmGW(P)Kne_;>UUiwGAUXu8@{T&jXa4chYOyqUwPEE=KV?;np1?Ao&4@IgOO% z7YasLU+~QJ_bKXNRuoqKuNM7Utu@c^!VYf+i<4G~9?K1bfq3e{^v`*jK42r(DztEZ z7?OWU4UQfW=SYzLbk%)ly!Xlsee8|%GEwkZ#@F~cR8ZLHB)7cG?>FbL6<+(| zc(I=SV%gd6wFjASE?KI{r2LXY*DyW30h_x(Y{Dgk(p*1gU6nNzsK4)|Co(8rM41eBmv z7mbr>u;p&vFPf;6*_ED$f%Ug87@dfS{Z=58lnUKnxnbfMGVou*s@OlPQO~Sl?C|z$ zg6WW{*NGvYkU#b4X_+5X4NrrVMh|{CPw+mh$d9_<>+xtKAv4q*tEu|40U-d*H8to< zl@$PRz~?$pZ6l9_&m9pp_P(9R(7YrjkbhCwH3Q2gB(H^o7U6c2>ga=WgMNxEnW9XH z4i``v)bTydLpx#`BM}rcKJr(gNFG$RPIf^%&K#Nz0`dv0(Y5}!A~K%UJzqu z3GA9F@?NV8@WIOi+0?}9<6^C4&(A6}iaP{8gV_hg){y6l>+kzQVCZq6F=z_kAdsMq zviT#qBZt#66~`gTZv?yIDNCt)fo56oYbVAa0CmsJ{;MChQQk+7=EwLrn^A@q@Wa1zLZYae+&6+ecy;{@50}eOUh};5G^mX+b^P%>})UiIL_8!Fp3w zpC>bkg;Q(YUfCa`-Cv-*#V8Q8MK2F-0NS#3Cf+(;cIB}|aY_Q#bzk?zoQ}Nh%=fQD zCi}Tau+9Vi>`9Q%vd#u4{H+cewI?>$%A)^MFzOF}$NI(UO|kPq69M}FOfYiN%2G8F HCL#X|sBGQe diff --git a/webroot/rsrc/image/sprite-tokens.png b/webroot/rsrc/image/sprite-tokens.png index cc3ef9d9c8ca81d046567c427e17531b6673fb38..fc02173e40f0e18b8d5e11c7b893d14752cf1df1 100644 GIT binary patch delta 6055 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 Ef`&@4m;e9( From cc7ae60aaf1096d9e230e30cd642b0b5c87d58e6 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 1 Jul 2016 08:06:51 -0700 Subject: [PATCH 28/35] Make the revision graph view more flexible Summary: Ref T4788. This separates the revision graph view into a base class with core logic and a revision class with Differential-specific logic, so I can subclass it in Maniphest, etc., and try using it in other applications to show similar graphs. Not sure if we'll stick with it, but even if we don't this makes the code a bit cleaner and gets custom rendering logic out of the RevisionViewController, which is nice. Test Plan: Viewed revisions, saw the stack UI completely unchanged. Reviewers: chad Reviewed By: chad Maniphest Tasks: T4788 Differential Revision: https://secure.phabricator.com/D16213 --- src/__phutil_library_map__.php | 6 +- .../DifferentialRevisionViewController.php | 171 +++--------------- .../edge/DifferentialStackGraph.php | 58 ------ .../graph/DifferentialRevisionGraph.php | 77 ++++++++ .../graph/PhabricatorObjectGraph.php | 157 ++++++++++++++++ 5 files changed, 259 insertions(+), 210 deletions(-) delete mode 100644 src/applications/differential/edge/DifferentialStackGraph.php create mode 100644 src/infrastructure/graph/DifferentialRevisionGraph.php create mode 100644 src/infrastructure/graph/PhabricatorObjectGraph.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 47e68b31c2..80f5cc54eb 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -521,6 +521,7 @@ phutil_register_library_map(array( 'DifferentialRevisionDependsOnRevisionEdgeType' => 'applications/differential/edge/DifferentialRevisionDependsOnRevisionEdgeType.php', 'DifferentialRevisionEditController' => 'applications/differential/controller/DifferentialRevisionEditController.php', 'DifferentialRevisionFulltextEngine' => 'applications/differential/search/DifferentialRevisionFulltextEngine.php', + 'DifferentialRevisionGraph' => 'infrastructure/graph/DifferentialRevisionGraph.php', 'DifferentialRevisionHasChildRelationship' => 'applications/differential/relationships/DifferentialRevisionHasChildRelationship.php', 'DifferentialRevisionHasCommitEdgeType' => 'applications/differential/edge/DifferentialRevisionHasCommitEdgeType.php', 'DifferentialRevisionHasCommitRelationship' => 'applications/differential/relationships/DifferentialRevisionHasCommitRelationship.php', @@ -556,7 +557,6 @@ phutil_register_library_map(array( 'DifferentialRevisionViewController' => 'applications/differential/controller/DifferentialRevisionViewController.php', 'DifferentialSchemaSpec' => 'applications/differential/storage/DifferentialSchemaSpec.php', 'DifferentialSetDiffPropertyConduitAPIMethod' => 'applications/differential/conduit/DifferentialSetDiffPropertyConduitAPIMethod.php', - 'DifferentialStackGraph' => 'applications/differential/edge/DifferentialStackGraph.php', 'DifferentialStoredCustomField' => 'applications/differential/customfield/DifferentialStoredCustomField.php', 'DifferentialSubscribersField' => 'applications/differential/customfield/DifferentialSubscribersField.php', 'DifferentialSummaryField' => 'applications/differential/customfield/DifferentialSummaryField.php', @@ -2868,6 +2868,7 @@ phutil_register_library_map(array( 'PhabricatorOAuthServerTokenController' => 'applications/oauthserver/controller/PhabricatorOAuthServerTokenController.php', 'PhabricatorOAuthServerTransaction' => 'applications/oauthserver/storage/PhabricatorOAuthServerTransaction.php', 'PhabricatorOAuthServerTransactionQuery' => 'applications/oauthserver/query/PhabricatorOAuthServerTransactionQuery.php', + 'PhabricatorObjectGraph' => 'infrastructure/graph/PhabricatorObjectGraph.php', 'PhabricatorObjectHandle' => 'applications/phid/PhabricatorObjectHandle.php', 'PhabricatorObjectHasAsanaSubtaskEdgeType' => 'applications/doorkeeper/edge/PhabricatorObjectHasAsanaSubtaskEdgeType.php', 'PhabricatorObjectHasAsanaTaskEdgeType' => 'applications/doorkeeper/edge/PhabricatorObjectHasAsanaTaskEdgeType.php', @@ -4895,6 +4896,7 @@ phutil_register_library_map(array( 'DifferentialRevisionDependsOnRevisionEdgeType' => 'PhabricatorEdgeType', 'DifferentialRevisionEditController' => 'DifferentialController', 'DifferentialRevisionFulltextEngine' => 'PhabricatorFulltextEngine', + 'DifferentialRevisionGraph' => 'PhabricatorObjectGraph', 'DifferentialRevisionHasChildRelationship' => 'DifferentialRevisionRelationship', 'DifferentialRevisionHasCommitEdgeType' => 'PhabricatorEdgeType', 'DifferentialRevisionHasCommitRelationship' => 'DifferentialRevisionRelationship', @@ -4930,7 +4932,6 @@ phutil_register_library_map(array( 'DifferentialRevisionViewController' => 'DifferentialController', 'DifferentialSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'DifferentialSetDiffPropertyConduitAPIMethod' => 'DifferentialConduitAPIMethod', - 'DifferentialStackGraph' => 'AbstractDirectedGraph', 'DifferentialStoredCustomField' => 'DifferentialCustomField', 'DifferentialSubscribersField' => 'DifferentialCoreCustomField', 'DifferentialSummaryField' => 'DifferentialCoreCustomField', @@ -7582,6 +7583,7 @@ phutil_register_library_map(array( 'PhabricatorOAuthServerTokenController' => 'PhabricatorOAuthServerController', 'PhabricatorOAuthServerTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorOAuthServerTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'PhabricatorObjectGraph' => 'AbstractDirectedGraph', 'PhabricatorObjectHandle' => array( 'Phobject', 'PhabricatorPolicyInterface', diff --git a/src/applications/differential/controller/DifferentialRevisionViewController.php b/src/applications/differential/controller/DifferentialRevisionViewController.php index 83c6d10288..adf6a42bfd 100644 --- a/src/applications/differential/controller/DifferentialRevisionViewController.php +++ b/src/applications/differential/controller/DifferentialRevisionViewController.php @@ -341,12 +341,29 @@ final class DifferentialRevisionViewController extends DifferentialController { ->setKey('commits') ->appendChild($local_table)); - $stack_graph = id(new DifferentialStackGraph()) - ->setSeedRevision($revision) + $stack_graph = id(new DifferentialRevisionGraph()) + ->setViewer($viewer) + ->setSeedPHID($revision->getPHID()) ->loadGraph(); if (!$stack_graph->isEmpty()) { - $stack_view = $this->renderStackView($revision, $stack_graph); - list($stack_name, $stack_color, $stack_table) = $stack_view; + $stack_table = $stack_graph->newGraphTable(); + + $parent_type = DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST; + $reachable = $stack_graph->getReachableObjects($parent_type); + + foreach ($reachable as $key => $reachable_revision) { + if ($reachable_revision->isClosed()) { + unset($reachable[$key]); + } + } + + if ($reachable) { + $stack_name = pht('Stack (%s Open)', phutil_count($reachable)); + $stack_color = PHUIListItemView::STATUS_FAIL; + } else { + $stack_name = pht('Stack'); + $stack_color = null; + } $tab_group->addTab( id(new PHUITabView()) @@ -1212,150 +1229,4 @@ final class DifferentialRevisionViewController extends DifferentialController { ->setShowViewAll(true); } - - private function renderStackView( - DifferentialRevision $current, - DifferentialStackGraph $graph) { - - $ancestry = $graph->getParentEdges(); - $viewer = $this->getViewer(); - - $revisions = id(new DifferentialRevisionQuery()) - ->setViewer($viewer) - ->withPHIDs(array_keys($ancestry)) - ->execute(); - $revisions = mpull($revisions, null, 'getPHID'); - - $order = id(new PhutilDirectedScalarGraph()) - ->addNodes($ancestry) - ->getTopographicallySortedNodes(); - - $ancestry = array_select_keys($ancestry, $order); - - $traces = id(new PHUIDiffGraphView()) - ->renderGraph($ancestry); - - // Load author handles, and also revision handles for any revisions which - // we failed to load (they might be policy restricted). - $handle_phids = mpull($revisions, 'getAuthorPHID'); - foreach ($order as $phid) { - if (empty($revisions[$phid])) { - $handle_phids[] = $phid; - } - } - $handles = $viewer->loadHandles($handle_phids); - - $rows = array(); - $rowc = array(); - - $ii = 0; - $seen = false; - foreach ($ancestry as $phid => $ignored) { - $revision = idx($revisions, $phid); - if ($revision) { - $status_icon = $revision->getStatusIcon(); - $status_name = $revision->getStatusDisplayName(); - - $status = array( - id(new PHUIIconView())->setIcon($status_icon), - ' ', - $status_name, - ); - - $author = $viewer->renderHandle($revision->getAuthorPHID()); - $title = phutil_tag( - 'a', - array( - 'href' => $revision->getURI(), - ), - array( - $revision->getMonogram(), - ' ', - $revision->getTitle(), - )); - } else { - $status = null; - $author = null; - $title = $viewer->renderHandle($phid); - } - - $rows[] = array( - $traces[$ii++], - $status, - $author, - $title, - ); - - if ($phid == $current->getPHID()) { - $rowc[] = 'highlighted'; - } else { - $rowc[] = null; - } - } - - $stack_table = id(new AphrontTableView($rows)) - ->setHeaders( - array( - null, - pht('Status'), - pht('Author'), - pht('Revision'), - )) - ->setRowClasses($rowc) - ->setColumnClasses( - array( - 'threads', - null, - null, - 'wide', - )); - - // Count how many revisions this one depends on that are not yet closed. - $seen = array(); - $look = array($current->getPHID()); - while ($look) { - $phid = array_pop($look); - - $parents = idx($ancestry, $phid, array()); - foreach ($parents as $parent) { - if (isset($seen[$parent])) { - continue; - } - - $seen[$parent] = $parent; - $look[] = $parent; - } - } - - $blocking_count = 0; - foreach ($seen as $parent) { - if ($parent == $current->getPHID()) { - continue; - } - - $revision = idx($revisions, $parent); - if (!$revision) { - continue; - } - - if ($revision->isClosed()) { - continue; - } - - $blocking_count++; - } - - if (!$blocking_count) { - $stack_name = pht('Stack'); - $stack_color = null; - } else { - $stack_name = pht( - 'Stack (%s Open)', - new PhutilNumber($blocking_count)); - $stack_color = PHUIListItemView::STATUS_FAIL; - } - - return array($stack_name, $stack_color, $stack_table); - } - } diff --git a/src/applications/differential/edge/DifferentialStackGraph.php b/src/applications/differential/edge/DifferentialStackGraph.php deleted file mode 100644 index cc7d735d79..0000000000 --- a/src/applications/differential/edge/DifferentialStackGraph.php +++ /dev/null @@ -1,58 +0,0 @@ -addNodes( - array( - '' => array($revision->getPHID()), - )); - } - - public function isEmpty() { - return (count($this->getNodes()) <= 2); - } - - public function getParentEdges() { - return $this->parentEdges; - } - - protected function loadEdges(array $nodes) { - $query = id(new PhabricatorEdgeQuery()) - ->withSourcePHIDs($nodes) - ->withEdgeTypes( - array( - DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST, - DifferentialRevisionDependedOnByRevisionEdgeType::EDGECONST, - )); - - $query->execute(); - - $map = array(); - foreach ($nodes as $node) { - $parents = $query->getDestinationPHIDs( - array($node), - array( - DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST, - )); - - $children = $query->getDestinationPHIDs( - array($node), - array( - DifferentialRevisionDependedOnByRevisionEdgeType::EDGECONST, - )); - - $this->parentEdges[$node] = $parents; - $this->childEdges[$node] = $children; - - $map[$node] = array_values(array_fuse($parents) + array_fuse($children)); - } - - return $map; - } - -} diff --git a/src/infrastructure/graph/DifferentialRevisionGraph.php b/src/infrastructure/graph/DifferentialRevisionGraph.php new file mode 100644 index 0000000000..718cd9a824 --- /dev/null +++ b/src/infrastructure/graph/DifferentialRevisionGraph.php @@ -0,0 +1,77 @@ +getViewer(); + + if ($object) { + $status_icon = $object->getStatusIcon(); + $status_name = $object->getStatusDisplayName(); + + $status = array( + id(new PHUIIconView())->setIcon($status_icon), + ' ', + $status_name, + ); + + $author = $viewer->renderHandle($object->getAuthorPHID()); + $link = phutil_tag( + 'a', + array( + 'href' => $object->getURI(), + ), + array( + $object->getMonogram(), + ' ', + $object->getTitle(), + )); + } else { + $status = null; + $author = null; + $link = $viewer->renderHandle($phid); + } + + return array( + $trace, + $status, + $author, + $link, + ); + } + + protected function newTable(AphrontTableView $table) { + return $table + ->setHeaders( + array( + null, + pht('Status'), + pht('Author'), + pht('Revision'), + )) + ->setColumnClasses( + array( + 'threads', + null, + null, + 'wide', + )); + } + +} diff --git a/src/infrastructure/graph/PhabricatorObjectGraph.php b/src/infrastructure/graph/PhabricatorObjectGraph.php new file mode 100644 index 0000000000..943ddaad29 --- /dev/null +++ b/src/infrastructure/graph/PhabricatorObjectGraph.php @@ -0,0 +1,157 @@ +viewer = $viewer; + return $this; + } + + public function getViewer() { + if (!$this->viewer) { + throw new PhutilInvalidStateException('setViewer'); + } + + return $this->viewer; + } + + abstract protected function getEdgeTypes(); + abstract protected function getParentEdgeType(); + abstract protected function newQuery(); + abstract protected function newTableRow($phid, $object, $trace); + abstract protected function newTable(AphrontTableView $table); + + final public function setSeedPHID($phid) { + $this->seedPHID = $phid; + + return $this->addNodes( + array( + '' => array($phid), + )); + } + + final public function isEmpty() { + return (count($this->getNodes()) <= 2); + } + + final public function getEdges($type) { + return idx($this->edges, $type, array()); + } + + final protected function loadEdges(array $nodes) { + $edge_types = $this->getEdgeTypes(); + + $query = id(new PhabricatorEdgeQuery()) + ->withSourcePHIDs($nodes) + ->withEdgeTypes($edge_types); + + $query->execute(); + + $map = array(); + foreach ($nodes as $node) { + foreach ($edge_types as $edge_type) { + $dst_phids = $query->getDestinationPHIDs( + array($node), + array($edge_type)); + + $this->edges[$edge_type][$node] = $dst_phids; + foreach ($dst_phids as $dst_phid) { + $map[$node][] = $dst_phid; + } + } + + $map[$node] = array_values(array_fuse($map[$node])); + } + + return $map; + } + + final public function newGraphTable() { + $viewer = $this->getViewer(); + + $ancestry = $this->getEdges($this->getParentEdgeType()); + + $objects = $this->newQuery() + ->setViewer($viewer) + ->withPHIDs(array_keys($ancestry)) + ->execute(); + $objects = mpull($objects, null, 'getPHID'); + + $order = id(new PhutilDirectedScalarGraph()) + ->addNodes($ancestry) + ->getTopographicallySortedNodes(); + + $ancestry = array_select_keys($ancestry, $order); + + $traces = id(new PHUIDiffGraphView()) + ->renderGraph($ancestry); + + $ii = 0; + $rows = array(); + $rowc = array(); + foreach ($ancestry as $phid => $ignored) { + $object = idx($objects, $phid); + $rows[] = $this->newTableRow($phid, $object, $traces[$ii++]); + + if ($phid == $this->seedPHID) { + $rowc[] = 'highlighted'; + } else { + $rowc[] = null; + } + } + + $table = id(new AphrontTableView($rows)) + ->setRowClasses($rowc); + + $this->objects = $objects; + + return $this->newTable($table); + } + + final public function getReachableObjects($edge_type) { + if ($this->objects === null) { + throw new PhutilInvalidStateException('newGraphTable'); + } + + $graph = $this->getEdges($edge_type); + + $seen = array(); + $look = array($this->seedPHID); + while ($look) { + $phid = array_pop($look); + + $parents = idx($graph, $phid, array()); + foreach ($parents as $parent) { + if (isset($seen[$parent])) { + continue; + } + + $seen[$parent] = $parent; + $look[] = $parent; + } + } + + $reachable = array(); + foreach ($seen as $phid) { + if ($phid == $this->seedPHID) { + continue; + } + + $object = idx($this->objects, $phid); + if (!$object) { + continue; + } + + $reachable[] = $object; + } + + return $reachable; + } + +} From 0a132e468fc4569ebe94ab4e52386acf5f0d981a Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 1 Jul 2016 08:50:16 -0700 Subject: [PATCH 29/35] Render parent and child tasks in Maniphest with a graph trace Summary: Ref T4788. This seems reasonable locally, but not sure how it will feel on real data. Might need some tweaks, or might just be a terrible idea. Test Plan: {F1708059} Reviewers: chad Reviewed By: chad Maniphest Tasks: T4788 Differential Revision: https://secure.phabricator.com/D16214 --- src/__phutil_library_map__.php | 2 + .../ManiphestTaskDetailController.php | 22 ++--- .../maniphest/storage/ManiphestTask.php | 4 + .../PhabricatorObjectRelationshipList.php | 6 ++ .../diff/view/PHUIDiffGraphView.php | 27 +++++- .../graph/ManiphestTaskGraph.php | 87 +++++++++++++++++++ .../graph/PhabricatorObjectGraph.php | 2 + 7 files changed, 135 insertions(+), 15 deletions(-) create mode 100644 src/infrastructure/graph/ManiphestTaskGraph.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 80f5cc54eb..7177364d2f 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1430,6 +1430,7 @@ phutil_register_library_map(array( 'ManiphestTaskEditBulkJobType' => 'applications/maniphest/bulk/ManiphestTaskEditBulkJobType.php', 'ManiphestTaskEditController' => 'applications/maniphest/controller/ManiphestTaskEditController.php', 'ManiphestTaskFulltextEngine' => 'applications/maniphest/search/ManiphestTaskFulltextEngine.php', + 'ManiphestTaskGraph' => 'infrastructure/graph/ManiphestTaskGraph.php', 'ManiphestTaskHasCommitEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasCommitEdgeType.php', 'ManiphestTaskHasCommitRelationship' => 'applications/maniphest/relationship/ManiphestTaskHasCommitRelationship.php', 'ManiphestTaskHasDuplicateTaskEdgeType' => 'applications/maniphest/edge/ManiphestTaskHasDuplicateTaskEdgeType.php', @@ -5947,6 +5948,7 @@ phutil_register_library_map(array( 'ManiphestTaskEditBulkJobType' => 'PhabricatorWorkerBulkJobType', 'ManiphestTaskEditController' => 'ManiphestController', 'ManiphestTaskFulltextEngine' => 'PhabricatorFulltextEngine', + 'ManiphestTaskGraph' => 'PhabricatorObjectGraph', 'ManiphestTaskHasCommitEdgeType' => 'PhabricatorEdgeType', 'ManiphestTaskHasCommitRelationship' => 'ManiphestTaskRelationship', 'ManiphestTaskHasDuplicateTaskEdgeType' => 'PhabricatorEdgeType', diff --git a/src/applications/maniphest/controller/ManiphestTaskDetailController.php b/src/applications/maniphest/controller/ManiphestTaskDetailController.php index 1a59bd2fd8..f544fa6b2c 100644 --- a/src/applications/maniphest/controller/ManiphestTaskDetailController.php +++ b/src/applications/maniphest/controller/ManiphestTaskDetailController.php @@ -31,8 +31,6 @@ final class ManiphestTaskDetailController extends ManiphestController { ->setTargetObject($task); $e_commit = ManiphestTaskHasCommitEdgeType::EDGECONST; - $e_dep_on = ManiphestTaskDependsOnTaskEdgeType::EDGECONST; - $e_dep_by = ManiphestTaskDependedOnByTaskEdgeType::EDGECONST; $e_rev = ManiphestTaskHasRevisionEdgeType::EDGECONST; $e_mock = ManiphestTaskHasMockEdgeType::EDGECONST; @@ -43,8 +41,6 @@ final class ManiphestTaskDetailController extends ManiphestController { ->withEdgeTypes( array( $e_commit, - $e_dep_on, - $e_dep_by, $e_rev, $e_mock, )); @@ -91,6 +87,15 @@ final class ManiphestTaskDetailController extends ManiphestController { ->addPropertySection(pht('Description'), $description) ->addPropertySection(pht('Details'), $details); + $task_graph = id(new ManiphestTaskGraph()) + ->setViewer($viewer) + ->setSeedPHID($task->getPHID()) + ->loadGraph(); + if (!$task_graph->isEmpty()) { + $graph_table = $task_graph->newGraphTable(); + $view->addPropertySection(pht('Task Graph'), $graph_table); + } + return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) @@ -186,9 +191,7 @@ final class ManiphestTaskDetailController extends ManiphestController { $edit_uri = $this->getApplicationURI($edit_uri); } - $task_submenu = array(); - - $task_submenu[] = id(new PhabricatorActionView()) + $subtask_item = id(new PhabricatorActionView()) ->setName(pht('Create Subtask')) ->setHref($edit_uri) ->setIcon('fa-level-down') @@ -200,6 +203,7 @@ final class ManiphestTaskDetailController extends ManiphestController { $task); $submenu_actions = array( + $subtask_item, ManiphestTaskHasParentRelationship::RELATIONSHIPKEY, ManiphestTaskHasSubtaskRelationship::RELATIONSHIPKEY, ManiphestTaskMergeInRelationship::RELATIONSHIPKEY, @@ -280,10 +284,6 @@ final class ManiphestTaskDetailController extends ManiphestController { } $edge_types = array( - ManiphestTaskDependedOnByTaskEdgeType::EDGECONST - => pht('Parent Tasks'), - ManiphestTaskDependsOnTaskEdgeType::EDGECONST - => pht('Subtasks'), ManiphestTaskHasRevisionEdgeType::EDGECONST => pht('Differential Revisions'), ManiphestTaskHasMockEdgeType::EDGECONST diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php index 9ac7190782..4fb9fc2861 100644 --- a/src/applications/maniphest/storage/ManiphestTask.php +++ b/src/applications/maniphest/storage/ManiphestTask.php @@ -179,6 +179,10 @@ final class ManiphestTask extends ManiphestDAO return 'T'.$this->getID(); } + public function getURI() { + return '/'.$this->getMonogram(); + } + public function attachGroupByProjectPHID($phid) { $this->groupByProjectPHID = $phid; return $this; diff --git a/src/applications/search/relationship/PhabricatorObjectRelationshipList.php b/src/applications/search/relationship/PhabricatorObjectRelationshipList.php index 31d05fa939..02917bd57d 100644 --- a/src/applications/search/relationship/PhabricatorObjectRelationshipList.php +++ b/src/applications/search/relationship/PhabricatorObjectRelationshipList.php @@ -52,6 +52,12 @@ final class PhabricatorObjectRelationshipList extends Phobject { $actions = array(); foreach ($keys as $key) { + // If we're passed a menu item, just include it verbatim. + if ($key instanceof PhabricatorActionView) { + $actions[] = $key; + continue; + } + $relationship = $this->getRelationship($key); if (!$relationship) { throw new Exception( diff --git a/src/infrastructure/diff/view/PHUIDiffGraphView.php b/src/infrastructure/diff/view/PHUIDiffGraphView.php index 02893cc5d4..11f93efc51 100644 --- a/src/infrastructure/diff/view/PHUIDiffGraphView.php +++ b/src/infrastructure/diff/view/PHUIDiffGraphView.php @@ -137,12 +137,31 @@ final class PHUIDiffGraphView extends Phobject { ); } - // If this is the last page in history, replace the "o" with an "x" so we - // do not draw a connecting line downward, and replace "^" with an "X" for - // repositories with exactly one commit. + // If this is the last page in history, replace any "o" characters at the + // bottom of columns with "x" characters so we do not draw a connecting + // line downward, and replace "^" with an "X" for repositories with + // exactly one commit. if ($this->getIsTail() && $graph) { + $terminated = array(); + foreach (array_reverse(array_keys($graph)) as $key) { + $line = $graph[$key]['line']; + $len = strlen($line); + for ($ii = 0; $ii < $len; $ii++) { + if (isset($terminated[$ii])) { + continue; + } + + $c = $line[$ii]; + if ($c == 'o') { + $terminated[$ii] = true; + $graph[$key]['line'][$ii] = 'x'; + } else if ($c != ' ') { + $terminated[$ii] = true; + } + } + } + $last = array_pop($graph); - $last['line'] = str_replace('o', 'x', $last['line']); $last['line'] = str_replace('^', 'X', $last['line']); $graph[] = $last; } diff --git a/src/infrastructure/graph/ManiphestTaskGraph.php b/src/infrastructure/graph/ManiphestTaskGraph.php new file mode 100644 index 0000000000..32cb45fa49 --- /dev/null +++ b/src/infrastructure/graph/ManiphestTaskGraph.php @@ -0,0 +1,87 @@ +getViewer(); + + if ($object) { + $status = $object->getStatus(); + $priority = $object->getPriority(); + $status_icon = ManiphestTaskStatus::getStatusIcon($status); + $status_name = ManiphestTaskStatus::getTaskStatusName($status); + $priority_color = ManiphestTaskPriority::getTaskPriorityColor($priority); + + + $status = array( + id(new PHUIIconView())->setIcon($status_icon, $priority_color), + ' ', + $status_name, + ); + + $owner_phid = $object->getOwnerPHID(); + if ($owner_phid) { + $assigned = $viewer->renderHandle($owner_phid); + } else { + $assigned = phutil_tag('em', array(), pht('None')); + } + + $link = phutil_tag( + 'a', + array( + 'href' => $object->getURI(), + ), + array( + $object->getMonogram(), + ' ', + $object->getTitle(), + )); + } else { + $status = null; + $assigned = null; + $link = $viewer->renderHandle($phid); + } + + return array( + $trace, + $status, + $assigned, + $link, + ); + } + + protected function newTable(AphrontTableView $table) { + return $table + ->setHeaders( + array( + null, + pht('Status'), + pht('Assigned'), + pht('Task'), + )) + ->setColumnClasses( + array( + 'threads', + null, + null, + 'wide', + )); + } + +} diff --git a/src/infrastructure/graph/PhabricatorObjectGraph.php b/src/infrastructure/graph/PhabricatorObjectGraph.php index 943ddaad29..2580512eb6 100644 --- a/src/infrastructure/graph/PhabricatorObjectGraph.php +++ b/src/infrastructure/graph/PhabricatorObjectGraph.php @@ -55,6 +55,8 @@ abstract class PhabricatorObjectGraph $map = array(); foreach ($nodes as $node) { + $map[$node] = array(); + foreach ($edge_types as $edge_type) { $dst_phids = $query->getDestinationPHIDs( array($node), From 7b5e84282fc6837129d36226368e713ae7db505a Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 1 Jul 2016 09:22:00 -0700 Subject: [PATCH 30/35] Improve "thread" rendering of unusually-shaped graphs Summary: Ref T4788. This fixes all the bugs I was immediately able to catch: - "Directory-Like" graph shapes could draw too many vertical lines. - "Reverse-Directory-Like" graph shapes could draw too few vertical lines. - Terminated, branched graph shapes drew the very last line to the wrong place. This covers the behavior with tests, so we should be able to fix more stuff later without breaking anything. Test Plan: - Added failing tests and made them pass. {F1708158} {F1708159} {F1708160} {F1708161} Reviewers: chad Reviewed By: chad Maniphest Tasks: T4788 Differential Revision: https://secure.phabricator.com/D16216 --- resources/celerity/map.php | 16 ++-- src/__phutil_library_map__.php | 2 + .../diff/view/PHUIDiffGraphView.php | 35 +++++-- .../__tests__/PHUIDiffGraphViewTestCase.php | 94 +++++++++++++++++++ .../diffusion/behavior-commit-graph.js | 2 + 5 files changed, 135 insertions(+), 14 deletions(-) create mode 100644 src/infrastructure/diff/view/__tests__/PHUIDiffGraphViewTestCase.php diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 0c513b7dfa..b41ccde130 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -13,7 +13,7 @@ return array( 'differential.pkg.css' => '3e81ae60', 'differential.pkg.js' => '634399e9', 'diffusion.pkg.css' => '91c5d3a6', - 'diffusion.pkg.js' => '3a9a8bfa', + 'diffusion.pkg.js' => '84c8f8fd', 'maniphest.pkg.css' => '4845691a', 'maniphest.pkg.js' => '949a7498', 'rsrc/css/aphront/aphront-bars.css' => '231ac33c', @@ -394,7 +394,7 @@ return array( 'rsrc/js/application/diffusion/DiffusionLocateFileSource.js' => 'b42eddc7', 'rsrc/js/application/diffusion/behavior-audit-preview.js' => 'd835b03a', 'rsrc/js/application/diffusion/behavior-commit-branches.js' => 'bdaf4d04', - 'rsrc/js/application/diffusion/behavior-commit-graph.js' => '5a0b1a64', + 'rsrc/js/application/diffusion/behavior-commit-graph.js' => '49ae8328', 'rsrc/js/application/diffusion/behavior-diffusion-browse-file.js' => '054a0f0b', 'rsrc/js/application/diffusion/behavior-jump-to.js' => '73d09eef', 'rsrc/js/application/diffusion/behavior-load-blame.js' => '42126667', @@ -619,7 +619,7 @@ return array( 'javelin-behavior-differential-user-select' => 'a8d8459d', 'javelin-behavior-diffusion-browse-file' => '054a0f0b', 'javelin-behavior-diffusion-commit-branches' => 'bdaf4d04', - 'javelin-behavior-diffusion-commit-graph' => '5a0b1a64', + 'javelin-behavior-diffusion-commit-graph' => '49ae8328', 'javelin-behavior-diffusion-jump-to' => '73d09eef', 'javelin-behavior-diffusion-locate-file' => '6d3e1947', 'javelin-behavior-diffusion-pull-lastmodified' => 'f01586dc', @@ -1217,6 +1217,11 @@ return array( 'javelin-uri', 'phabricator-notification', ), + '49ae8328' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-stratcom', + ), '4b700e9e' => array( 'javelin-behavior', 'javelin-dom', @@ -1343,11 +1348,6 @@ return array( 'javelin-vector', 'javelin-dom', ), - '5a0b1a64' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-stratcom', - ), '5a13c79f' => array( 'javelin-install', 'javelin-dom', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 7177364d2f..051ab9c41c 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1602,6 +1602,7 @@ phutil_register_library_map(array( 'PHUICurtainPanelView' => 'view/layout/PHUICurtainPanelView.php', 'PHUICurtainView' => 'view/layout/PHUICurtainView.php', 'PHUIDiffGraphView' => 'infrastructure/diff/view/PHUIDiffGraphView.php', + 'PHUIDiffGraphViewTestCase' => 'infrastructure/diff/view/__tests__/PHUIDiffGraphViewTestCase.php', 'PHUIDiffInlineCommentDetailView' => 'infrastructure/diff/view/PHUIDiffInlineCommentDetailView.php', 'PHUIDiffInlineCommentEditView' => 'infrastructure/diff/view/PHUIDiffInlineCommentEditView.php', 'PHUIDiffInlineCommentRowScaffold' => 'infrastructure/diff/view/PHUIDiffInlineCommentRowScaffold.php', @@ -6139,6 +6140,7 @@ phutil_register_library_map(array( 'PHUICurtainPanelView' => 'AphrontTagView', 'PHUICurtainView' => 'AphrontTagView', 'PHUIDiffGraphView' => 'Phobject', + 'PHUIDiffGraphViewTestCase' => 'PhabricatorTestCase', 'PHUIDiffInlineCommentDetailView' => 'PHUIDiffInlineCommentView', 'PHUIDiffInlineCommentEditView' => 'PHUIDiffInlineCommentView', 'PHUIDiffInlineCommentRowScaffold' => 'AphrontView', diff --git a/src/infrastructure/diff/view/PHUIDiffGraphView.php b/src/infrastructure/diff/view/PHUIDiffGraphView.php index 11f93efc51..ed4b0acf57 100644 --- a/src/infrastructure/diff/view/PHUIDiffGraphView.php +++ b/src/infrastructure/diff/view/PHUIDiffGraphView.php @@ -23,7 +23,7 @@ final class PHUIDiffGraphView extends Phobject { return $this->isTail; } - public function renderGraph(array $parents) { + public function renderRawGraph(array $parents) { // This keeps our accumulated information about each line of the // merge/branch graph. $graph = array(); @@ -47,7 +47,10 @@ final class PHUIDiffGraphView extends Phobject { $line = ''; $found = false; $pos = count($threads); - for ($n = 0; $n < $count; $n++) { + + $thread_count = $pos; + for ($n = 0; $n < $thread_count; $n++) { + if (empty($threads[$n])) { $line .= ' '; continue; @@ -147,16 +150,30 @@ final class PHUIDiffGraphView extends Phobject { $line = $graph[$key]['line']; $len = strlen($line); for ($ii = 0; $ii < $len; $ii++) { - if (isset($terminated[$ii])) { - continue; - } - $c = $line[$ii]; if ($c == 'o') { + // If we've already terminated this thread, we don't need to add + // a terminator. + if (isset($terminated[$ii])) { + continue; + } + $terminated[$ii] = true; + + // If this thread is joinining some other node here, we don't want + // to terminate it. + if (isset($graph[$key + 1])) { + $joins = $graph[$key + 1]['join']; + if (in_array($ii, $joins)) { + continue; + } + } + $graph[$key]['line'][$ii] = 'x'; } else if ($c != ' ') { $terminated[$ii] = true; + } else { + unset($terminated[$ii]); } } } @@ -166,6 +183,12 @@ final class PHUIDiffGraphView extends Phobject { $graph[] = $last; } + return array($graph, $count); + } + + public function renderGraph(array $parents) { + list($graph, $count) = $this->renderRawGraph($parents); + // Render into tags for the behavior. foreach ($graph as $k => $meta) { diff --git a/src/infrastructure/diff/view/__tests__/PHUIDiffGraphViewTestCase.php b/src/infrastructure/diff/view/__tests__/PHUIDiffGraphViewTestCase.php new file mode 100644 index 0000000000..9bcf9645a3 --- /dev/null +++ b/src/infrastructure/diff/view/__tests__/PHUIDiffGraphViewTestCase.php @@ -0,0 +1,94 @@ + array('B'), + 'B' => array('C', 'D', 'E'), + 'E' => array(), + 'D' => array(), + 'C' => array('F', 'G'), + 'G' => array(), + 'F' => array(), + ); + + $graph = $this->newGraph($nodes); + + $picture = array( + '^', + 'o', + '||x', + '|x ', + 'o ', + '|x ', + 'x ', + ); + + $this->assertGraph($picture, $graph, pht('Terminating Tree')); + } + + public function testReverseTree() { + $nodes = array( + 'A' => array('B'), + 'C' => array('B'), + 'B' => array('D'), + 'E' => array('D'), + 'F' => array('D'), + 'D' => array('G'), + 'G' => array(), + ); + + $graph = $this->newGraph($nodes); + + $picture = array( + '^', + '|^', + 'o ', + '|^', + '||^', + 'o ', + 'x', + ); + + $this->assertGraph($picture, $graph, pht('Reverse Tree')); + } + + public function testJoinTerminateTree() { + $nodes = array( + 'A' => array('D'), + 'B' => array('C'), + 'C' => array('D'), + 'D' => array(), + ); + + $graph = $this->newGraph($nodes); + + $picture = array( + '^', + '|^', + '|o', + 'x ', + ); + + $this->assertGraph($picture, $graph, pht('Reverse Tree')); + } + + private function newGraph(array $nodes) { + return id(new PHUIDiffGraphView()) + ->setIsHead(true) + ->setIsTail(true) + ->renderRawGraph($nodes); + } + + private function assertGraph($picture, $graph, $label) { + list($data, $count) = $graph; + $lines = ipull($data, 'line'); + + $picture = implode("\n", $picture); + $lines = implode("\n", $lines); + + $this->assertEqual($picture, $lines, $label); + } + +} diff --git a/webroot/rsrc/js/application/diffusion/behavior-commit-graph.js b/webroot/rsrc/js/application/diffusion/behavior-commit-graph.js index a3a7c7bdf8..317a132240 100644 --- a/webroot/rsrc/js/application/diffusion/behavior-commit-graph.js +++ b/webroot/rsrc/js/application/diffusion/behavior-commit-graph.js @@ -79,6 +79,7 @@ JX.behavior('diffusion-commit-graph', function(config) { c = data.line.charAt(jj); switch (c) { case 'o': + case 'x': case '^': origin = xpos(jj); break; @@ -91,6 +92,7 @@ JX.behavior('diffusion-commit-graph', function(config) { for (jj = 0; jj < data.join.length; jj++) { var join = data.join[jj]; x = xpos(join); + cxt.beginPath(); cxt.moveTo(x, 0); cxt.bezierCurveTo(x, h/4, origin, h/4, origin, h/2); From f26374241ab1bdd5f7b6bf22dbaf057d69c65547 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Fri, 1 Jul 2016 11:21:05 -0700 Subject: [PATCH 31/35] Make Phame Header and Profile Image Transactional Summary: Ref T9360. This makes these transactional. Test Plan: Set new header, delete header. Set new profile image, reset profile image. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Maniphest Tasks: T9360 Differential Revision: https://secure.phabricator.com/D16217 --- .../blog/PhameBlogHeaderPictureController.php | 19 +++- .../PhameBlogProfilePictureController.php | 19 +++- .../phame/editor/PhameBlogEditor.php | 16 +++- .../phame/storage/PhameBlogTransaction.php | 86 +++++++++++++++++-- 4 files changed, 126 insertions(+), 14 deletions(-) diff --git a/src/applications/phame/controller/blog/PhameBlogHeaderPictureController.php b/src/applications/phame/controller/blog/PhameBlogHeaderPictureController.php index fbda0ab1ef..ab026fecbf 100644 --- a/src/applications/phame/controller/blog/PhameBlogHeaderPictureController.php +++ b/src/applications/phame/controller/blog/PhameBlogHeaderPictureController.php @@ -53,12 +53,25 @@ final class PhameBlogHeaderPictureController if (!$errors) { if ($delete_header) { - $blog->setHeaderImagePHID(null); + $new_value = null; } else { - $blog->setHeaderImagePHID($file->getPHID()); $file->attachToObject($blog->getPHID()); + $new_value = $file->getPHID(); } - $blog->save(); + + $xactions = array(); + $xactions[] = id(new PhameBlogTransaction()) + ->setTransactionType(PhameBlogTransaction::TYPE_HEADERIMAGE) + ->setNewValue($new_value); + + $editor = id(new PhameBlogEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnMissingFields(true) + ->setContinueOnNoEffect(true); + + $editor->applyTransactions($blog, $xactions); + return id(new AphrontRedirectResponse())->setURI($blog_uri); } } diff --git a/src/applications/phame/controller/blog/PhameBlogProfilePictureController.php b/src/applications/phame/controller/blog/PhameBlogProfilePictureController.php index 96d59c8ad4..3368046414 100644 --- a/src/applications/phame/controller/blog/PhameBlogProfilePictureController.php +++ b/src/applications/phame/controller/blog/PhameBlogProfilePictureController.php @@ -68,12 +68,25 @@ final class PhameBlogProfilePictureController if (!$errors) { if ($is_default) { - $blog->setProfileImagePHID(null); + $new_value = null; } else { - $blog->setProfileImagePHID($xformed->getPHID()); $xformed->attachToObject($blog->getPHID()); + $new_value = $xformed->getPHID(); } - $blog->save(); + + $xactions = array(); + $xactions[] = id(new PhameBlogTransaction()) + ->setTransactionType(PhameBlogTransaction::TYPE_PROFILEIMAGE) + ->setNewValue($new_value); + + $editor = id(new PhameBlogEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnMissingFields(true) + ->setContinueOnNoEffect(true); + + $editor->applyTransactions($blog, $xactions); + return id(new AphrontRedirectResponse())->setURI($blog_uri); } } diff --git a/src/applications/phame/editor/PhameBlogEditor.php b/src/applications/phame/editor/PhameBlogEditor.php index 197387985b..d075a750ea 100644 --- a/src/applications/phame/editor/PhameBlogEditor.php +++ b/src/applications/phame/editor/PhameBlogEditor.php @@ -21,6 +21,9 @@ final class PhameBlogEditor $types[] = PhameBlogTransaction::TYPE_PARENTSITE; $types[] = PhameBlogTransaction::TYPE_PARENTDOMAIN; $types[] = PhameBlogTransaction::TYPE_STATUS; + $types[] = PhameBlogTransaction::TYPE_HEADERIMAGE; + $types[] = PhameBlogTransaction::TYPE_PROFILEIMAGE; + $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; @@ -44,6 +47,10 @@ final class PhameBlogEditor return $object->getParentSite(); case PhameBlogTransaction::TYPE_PARENTDOMAIN: return $object->getParentDomain(); + case PhameBlogTransaction::TYPE_PROFILEIMAGE: + return $object->getProfileImagePHID(); + case PhameBlogTransaction::TYPE_HEADERIMAGE: + return $object->getHeaderImagePHID(); case PhameBlogTransaction::TYPE_STATUS: return $object->getStatus(); } @@ -59,7 +66,8 @@ final class PhameBlogEditor case PhameBlogTransaction::TYPE_DESCRIPTION: case PhameBlogTransaction::TYPE_STATUS: case PhameBlogTransaction::TYPE_PARENTSITE: - case PhameBlogTransaction::TYPE_PARENTDOMAIN: + case PhameBlogTransaction::TYPE_PROFILEIMAGE: + case PhameBlogTransaction::TYPE_HEADERIMAGE: return $xaction->getNewValue(); case PhameBlogTransaction::TYPE_FULLDOMAIN: $domain = $xaction->getNewValue(); @@ -92,6 +100,10 @@ final class PhameBlogEditor } $object->setDomainFullURI($new_value); return; + case PhameBlogTransaction::TYPE_PROFILEIMAGE: + return $object->setProfileImagePHID($xaction->getNewValue()); + case PhameBlogTransaction::TYPE_HEADERIMAGE: + return $object->setHeaderImagePHID($xaction->getNewValue()); case PhameBlogTransaction::TYPE_STATUS: return $object->setStatus($xaction->getNewValue()); case PhameBlogTransaction::TYPE_PARENTSITE: @@ -114,6 +126,8 @@ final class PhameBlogEditor case PhameBlogTransaction::TYPE_FULLDOMAIN: case PhameBlogTransaction::TYPE_PARENTSITE: case PhameBlogTransaction::TYPE_PARENTDOMAIN: + case PhameBlogTransaction::TYPE_HEADERIMAGE: + case PhameBlogTransaction::TYPE_PROFILEIMAGE: case PhameBlogTransaction::TYPE_STATUS: return; } diff --git a/src/applications/phame/storage/PhameBlogTransaction.php b/src/applications/phame/storage/PhameBlogTransaction.php index 2d74ca5cc6..f08c7be688 100644 --- a/src/applications/phame/storage/PhameBlogTransaction.php +++ b/src/applications/phame/storage/PhameBlogTransaction.php @@ -3,13 +3,15 @@ final class PhameBlogTransaction extends PhabricatorApplicationTransaction { - const TYPE_NAME = 'phame.blog.name'; - const TYPE_SUBTITLE = 'phame.blog.subtitle'; - const TYPE_DESCRIPTION = 'phame.blog.description'; - const TYPE_FULLDOMAIN = 'phame.blog.full.domain'; - const TYPE_STATUS = 'phame.blog.status'; - const TYPE_PARENTSITE = 'phame.blog.parent.site'; - const TYPE_PARENTDOMAIN = 'phame.blog.parent.domain'; + const TYPE_NAME = 'phame.blog.name'; + const TYPE_SUBTITLE = 'phame.blog.subtitle'; + const TYPE_DESCRIPTION = 'phame.blog.description'; + const TYPE_FULLDOMAIN = 'phame.blog.full.domain'; + const TYPE_STATUS = 'phame.blog.status'; + const TYPE_PARENTSITE = 'phame.blog.parent.site'; + const TYPE_PARENTDOMAIN = 'phame.blog.parent.domain'; + const TYPE_PROFILEIMAGE = 'phame.blog.header.image'; + const TYPE_HEADERIMAGE = 'phame.blog.profile.image'; const MAILTAG_DETAILS = 'phame-blog-details'; const MAILTAG_SUBSCRIBERS = 'phame-blog-subscribers'; @@ -34,6 +36,22 @@ final class PhameBlogTransaction return parent::shouldHide(); } + public function getRequiredHandlePHIDs() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + $req_phids = array(); + switch ($this->getTransactionType()) { + case self::TYPE_PROFILEIMAGE: + case self::TYPE_HEADERIMAGE: + $req_phids[] = $old; + $req_phids[] = $new; + break; + } + + return array_merge($req_phids, parent::getRequiredHandlePHIDs()); + } + public function getIcon() { $old = $this->getOldValue(); $new = $this->getNewValue(); @@ -48,6 +66,10 @@ final class PhameBlogTransaction case self::TYPE_DESCRIPTION: case self::TYPE_FULLDOMAIN: return 'fa-pencil'; + case self::TYPE_HEADERIMAGE: + return 'fa-image'; + case self::TYPE_PROFILEIMAGE: + return 'fa-star'; case self::TYPE_STATUS: if ($new == PhameBlog::STATUS_ARCHIVED) { return 'fa-ban'; @@ -88,6 +110,8 @@ final class PhameBlogTransaction case self::TYPE_FULLDOMAIN: case self::TYPE_PARENTSITE: case self::TYPE_PARENTDOMAIN: + case self::TYPE_PROFILEIMAGE: + case self::TYPE_HEADERIMAGE: $tags[] = self::MAILTAG_DETAILS; break; default: @@ -172,6 +196,42 @@ final class PhameBlogTransaction $new); } break; + case self::TYPE_HEADERIMAGE: + if (!$old) { + return pht( + "%s set this blog's header image to %s.", + $this->renderHandleLink($author_phid), + $this->renderHandleLink($new)); + } else if (!$new) { + return pht( + "%s removed this blog's header image.", + $this->renderHandleLink($author_phid)); + } else { + return pht( + "%s updated this blog's header image from %s to %s.", + $this->renderHandleLink($author_phid), + $this->renderHandleLink($old), + $this->renderHandleLink($new)); + } + break; + case self::TYPE_PROFILEIMAGE: + if (!$old) { + return pht( + "%s set this blog's profile image to %s.", + $this->renderHandleLink($author_phid), + $this->renderHandleLink($new)); + } else if (!$new) { + return pht( + "%s removed this blog's profile image.", + $this->renderHandleLink($author_phid)); + } else { + return pht( + "%s updated this blog's profile image from %s to %s.", + $this->renderHandleLink($author_phid), + $this->renderHandleLink($old), + $this->renderHandleLink($new)); + } + break; case self::TYPE_STATUS: switch ($new) { case PhameBlog::STATUS_ACTIVE: @@ -248,6 +308,18 @@ final class PhameBlogTransaction $this->renderHandleLink($author_phid), $this->renderHandleLink($object_phid)); break; + case self::TYPE_HEADERIMAGE: + return pht( + '%s updated the header image for %s.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($object_phid)); + break; + case self::TYPE_PROFILEIMAGE: + return pht( + '%s updated the profile image for %s.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($object_phid)); + break; case self::TYPE_STATUS: switch ($new) { case PhameBlog::STATUS_ACTIVE: From bc3ac3158448dfe966d05787fa9272f08ddc7faa Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 1 Jul 2016 11:37:01 -0700 Subject: [PATCH 32/35] Don't load the entire graph for tasks Summary: Ref T4788. As it turns out, our tasks are very tightly connected. Instead of loading every parent/child task, then every parent/child of those tasks, etc., etc., only load tasks in the "same direction" that we're already heading. For example, we load children of children, but not parents of children. And we load parents of parents, but not children of parents. Basically we only go "up" and "down" now, but not "out" as much. This should reduce the gigantic multiple-thousand-node graphs currently shown in the UI. I still discover the whole graph for revisiosn, because I think it's probably more useful and always much smaller. That might need adjustment too, though. Test Plan: Seems fine locally?? Reviewers: chad Reviewed By: chad Maniphest Tasks: T4788 Differential Revision: https://secure.phabricator.com/D16218 --- .../DifferentialRevisionViewController.php | 1 + .../graph/PhabricatorObjectGraph.php | 35 +++++++++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/applications/differential/controller/DifferentialRevisionViewController.php b/src/applications/differential/controller/DifferentialRevisionViewController.php index adf6a42bfd..e7fcdba377 100644 --- a/src/applications/differential/controller/DifferentialRevisionViewController.php +++ b/src/applications/differential/controller/DifferentialRevisionViewController.php @@ -344,6 +344,7 @@ final class DifferentialRevisionViewController extends DifferentialController { $stack_graph = id(new DifferentialRevisionGraph()) ->setViewer($viewer) ->setSeedPHID($revision->getPHID()) + ->setLoadEntireGraph(true) ->loadGraph(); if (!$stack_graph->isEmpty()) { $stack_table = $stack_graph->newGraphTable(); diff --git a/src/infrastructure/graph/PhabricatorObjectGraph.php b/src/infrastructure/graph/PhabricatorObjectGraph.php index 2580512eb6..0be6d6db76 100644 --- a/src/infrastructure/graph/PhabricatorObjectGraph.php +++ b/src/infrastructure/graph/PhabricatorObjectGraph.php @@ -5,8 +5,10 @@ abstract class PhabricatorObjectGraph private $viewer; private $edges = array(); + private $edgeReach = array(); private $seedPHID; private $objects; + private $loadEntireGraph = false; public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; @@ -29,6 +31,7 @@ abstract class PhabricatorObjectGraph final public function setSeedPHID($phid) { $this->seedPHID = $phid; + $this->edgeReach[$phid] = array_fill_keys($this->getEdgeTypes(), true); return $this->addNodes( array( @@ -41,7 +44,30 @@ abstract class PhabricatorObjectGraph } final public function getEdges($type) { - return idx($this->edges, $type, array()); + $edges = idx($this->edges, $type, array()); + + // Remove any nodes which we never reached. We can get these when loading + // only part of the graph: for example, they point at other subtasks of + // parents or other parents of subtasks. + $nodes = $this->getNodes(); + foreach ($edges as $src => $dsts) { + foreach ($dsts as $key => $dst) { + if (!isset($nodes[$dst])) { + unset($edges[$src][$key]); + } + } + } + + return $edges; + } + + final public function setLoadEntireGraph($load_entire_graph) { + $this->loadEntireGraph = $load_entire_graph; + return $this; + } + + final public function getLoadEntireGraph() { + return $this->loadEntireGraph; } final protected function loadEdges(array $nodes) { @@ -53,6 +79,8 @@ abstract class PhabricatorObjectGraph $query->execute(); + $whole_graph = $this->getLoadEntireGraph(); + $map = array(); foreach ($nodes as $node) { $map[$node] = array(); @@ -64,7 +92,10 @@ abstract class PhabricatorObjectGraph $this->edges[$edge_type][$node] = $dst_phids; foreach ($dst_phids as $dst_phid) { - $map[$node][] = $dst_phid; + if ($whole_graph || isset($this->edgeReach[$node][$edge_type])) { + $map[$node][] = $dst_phid; + } + $this->edgeReach[$dst_phid][$edge_type] = true; } } From 962cae22b76764829757c26552c2b78e37dc2918 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 1 Jul 2016 12:53:41 -0700 Subject: [PATCH 33/35] Make closed vs open objects in object graphs more obvious Summary: Ref T4788. It's not easy to tell at a glance which objects are open vs closed. Try to make that a bit more clear. This could probably use some more tweaking. Test Plan: {F1708330} Reviewers: chad Reviewed By: chad Maniphest Tasks: T4788 Differential Revision: https://secure.phabricator.com/D16219 --- resources/celerity/map.php | 6 +++--- .../graph/DifferentialRevisionGraph.php | 8 +++++-- .../graph/ManiphestTaskGraph.php | 8 +++++-- .../graph/PhabricatorObjectGraph.php | 21 ++++++++++++++++--- webroot/rsrc/css/aphront/table-view.css | 15 +++++++++++++ 5 files changed, 48 insertions(+), 10 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index b41ccde130..52f29b658b 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,7 +7,7 @@ */ return array( 'names' => array( - 'core.pkg.css' => '2cc8508b', + 'core.pkg.css' => '93eb0544', 'core.pkg.js' => 'f2139810', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '3e81ae60', @@ -25,7 +25,7 @@ return array( 'rsrc/css/aphront/notification.css' => '3f6c89c9', 'rsrc/css/aphront/panel-view.css' => '8427b78d', 'rsrc/css/aphront/phabricator-nav-view.css' => 'ac79a758', - 'rsrc/css/aphront/table-view.css' => '9258e19f', + 'rsrc/css/aphront/table-view.css' => '2596314c', 'rsrc/css/aphront/tokenizer.css' => '056da01b', 'rsrc/css/aphront/tooltip.css' => '1a07aea8', 'rsrc/css/aphront/typeahead-browse.css' => '8904346a', @@ -536,7 +536,7 @@ return array( 'aphront-list-filter-view-css' => '5d6f0526', 'aphront-multi-column-view-css' => 'fd18389d', 'aphront-panel-view-css' => '8427b78d', - 'aphront-table-view-css' => '9258e19f', + 'aphront-table-view-css' => '2596314c', 'aphront-tokenizer-control-css' => '056da01b', 'aphront-tooltip-css' => '1a07aea8', 'aphront-typeahead-control-css' => 'd4f16145', diff --git a/src/infrastructure/graph/DifferentialRevisionGraph.php b/src/infrastructure/graph/DifferentialRevisionGraph.php index 718cd9a824..0fba201ea1 100644 --- a/src/infrastructure/graph/DifferentialRevisionGraph.php +++ b/src/infrastructure/graph/DifferentialRevisionGraph.php @@ -18,6 +18,10 @@ final class DifferentialRevisionGraph return new DifferentialRevisionQuery(); } + protected function isClosed($object) { + return $object->isClosed(); + } + protected function newTableRow($phid, $object, $trace) { $viewer = $this->getViewer(); @@ -68,9 +72,9 @@ final class DifferentialRevisionGraph ->setColumnClasses( array( 'threads', + 'graph-status', null, - null, - 'wide', + 'wide object-link', )); } diff --git a/src/infrastructure/graph/ManiphestTaskGraph.php b/src/infrastructure/graph/ManiphestTaskGraph.php index 32cb45fa49..92edf092ba 100644 --- a/src/infrastructure/graph/ManiphestTaskGraph.php +++ b/src/infrastructure/graph/ManiphestTaskGraph.php @@ -18,6 +18,10 @@ final class ManiphestTaskGraph return new ManiphestTaskQuery(); } + protected function isClosed($object) { + return $object->isClosed(); + } + protected function newTableRow($phid, $object, $trace) { $viewer = $this->getViewer(); @@ -78,9 +82,9 @@ final class ManiphestTaskGraph ->setColumnClasses( array( 'threads', + 'graph-status', null, - null, - 'wide', + 'wide object-link', )); } diff --git a/src/infrastructure/graph/PhabricatorObjectGraph.php b/src/infrastructure/graph/PhabricatorObjectGraph.php index 0be6d6db76..7a3790dea4 100644 --- a/src/infrastructure/graph/PhabricatorObjectGraph.php +++ b/src/infrastructure/graph/PhabricatorObjectGraph.php @@ -28,6 +28,7 @@ abstract class PhabricatorObjectGraph abstract protected function newQuery(); abstract protected function newTableRow($phid, $object, $trace); abstract protected function newTable(AphrontTableView $table); + abstract protected function isClosed($object); final public function setSeedPHID($phid) { $this->seedPHID = $phid; @@ -132,14 +133,28 @@ abstract class PhabricatorObjectGraph $object = idx($objects, $phid); $rows[] = $this->newTableRow($phid, $object, $traces[$ii++]); + $classes = array(); if ($phid == $this->seedPHID) { - $rowc[] = 'highlighted'; - } else { - $rowc[] = null; + $classes[] = 'highlighted'; } + + if ($object) { + if ($this->isClosed($object)) { + $classes[] = 'closed'; + } + } + + if ($classes) { + $classes = implode(' ', $classes); + } else { + $classes = null; + } + + $rowc[] = $classes; } $table = id(new AphrontTableView($rows)) + ->setClassName('object-graph-table') ->setRowClasses($rowc); $this->objects = $objects; diff --git a/webroot/rsrc/css/aphront/table-view.css b/webroot/rsrc/css/aphront/table-view.css index af304c930a..a535602270 100644 --- a/webroot/rsrc/css/aphront/table-view.css +++ b/webroot/rsrc/css/aphront/table-view.css @@ -228,6 +228,21 @@ span.single-display-line-content { position: static; } +.aphront-table-view tr.closed td.object-link a, +.aphront-table-view tr.alt-closed td.object-link a { + text-decoration: line-through; + color: rgba({$alphablack}, 0.5); +} + +.aphront-table-view tr.closed td.graph-status, +.aphront-table-view tr.alt-closed td.graph-status { + opacity: 0.5; +} + +.object-graph-table em { + color: {$lightgreytext}; +} + .aphront-table-view tr.highlighted { background: #fdf9e4; } From d3c327ec935599f74a0267d42c3876dd09528b96 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 1 Jul 2016 12:59:15 -0700 Subject: [PATCH 34/35] Set Maniphest status icons to grey for closed tasks in object graph view Summary: See D16219. Test Plan: {F1708338} Reviewers: chad Reviewed By: chad Differential Revision: https://secure.phabricator.com/D16220 --- resources/celerity/map.php | 6 +++--- src/infrastructure/graph/ManiphestTaskGraph.php | 5 ++++- webroot/rsrc/css/aphront/table-view.css | 5 +---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 52f29b658b..999433e6d7 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,7 +7,7 @@ */ return array( 'names' => array( - 'core.pkg.css' => '93eb0544', + 'core.pkg.css' => 'd04e6f67', 'core.pkg.js' => 'f2139810', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '3e81ae60', @@ -25,7 +25,7 @@ return array( 'rsrc/css/aphront/notification.css' => '3f6c89c9', 'rsrc/css/aphront/panel-view.css' => '8427b78d', 'rsrc/css/aphront/phabricator-nav-view.css' => 'ac79a758', - 'rsrc/css/aphront/table-view.css' => '2596314c', + 'rsrc/css/aphront/table-view.css' => 'aeb66852', 'rsrc/css/aphront/tokenizer.css' => '056da01b', 'rsrc/css/aphront/tooltip.css' => '1a07aea8', 'rsrc/css/aphront/typeahead-browse.css' => '8904346a', @@ -536,7 +536,7 @@ return array( 'aphront-list-filter-view-css' => '5d6f0526', 'aphront-multi-column-view-css' => 'fd18389d', 'aphront-panel-view-css' => '8427b78d', - 'aphront-table-view-css' => '2596314c', + 'aphront-table-view-css' => 'aeb66852', 'aphront-tokenizer-control-css' => '056da01b', 'aphront-tooltip-css' => '1a07aea8', 'aphront-typeahead-control-css' => 'd4f16145', diff --git a/src/infrastructure/graph/ManiphestTaskGraph.php b/src/infrastructure/graph/ManiphestTaskGraph.php index 92edf092ba..769de28073 100644 --- a/src/infrastructure/graph/ManiphestTaskGraph.php +++ b/src/infrastructure/graph/ManiphestTaskGraph.php @@ -30,8 +30,11 @@ final class ManiphestTaskGraph $priority = $object->getPriority(); $status_icon = ManiphestTaskStatus::getStatusIcon($status); $status_name = ManiphestTaskStatus::getTaskStatusName($status); - $priority_color = ManiphestTaskPriority::getTaskPriorityColor($priority); + $priority_color = ManiphestTaskPriority::getTaskPriorityColor($priority); + if ($object->isClosed()) { + $priority_color = 'grey'; + } $status = array( id(new PHUIIconView())->setIcon($status_icon, $priority_color), diff --git a/webroot/rsrc/css/aphront/table-view.css b/webroot/rsrc/css/aphront/table-view.css index a535602270..8ec4f2a4c6 100644 --- a/webroot/rsrc/css/aphront/table-view.css +++ b/webroot/rsrc/css/aphront/table-view.css @@ -235,10 +235,7 @@ span.single-display-line-content { } .aphront-table-view tr.closed td.graph-status, -.aphront-table-view tr.alt-closed td.graph-status { - opacity: 0.5; -} - +.aphront-table-view tr.alt-closed td.graph-status, .object-graph-table em { color: {$lightgreytext}; } From ceb395ea9bc30f88096ac2839a595a7967c5a919 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 1 Jul 2016 13:30:42 -0700 Subject: [PATCH 35/35] Don't link object monograms in object graphs Summary: Ref T4788. Test Plan: {F1708372} Reviewers: chad Reviewed By: chad Maniphest Tasks: T4788 Differential Revision: https://secure.phabricator.com/D16221 --- resources/celerity/map.php | 6 +++--- .../graph/DifferentialRevisionGraph.php | 14 ++++++++------ src/infrastructure/graph/ManiphestTaskGraph.php | 14 ++++++++------ webroot/rsrc/css/aphront/table-view.css | 7 ++++++- 4 files changed, 25 insertions(+), 16 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 999433e6d7..be152c24b9 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,7 +7,7 @@ */ return array( 'names' => array( - 'core.pkg.css' => 'd04e6f67', + 'core.pkg.css' => '55d9bb83', 'core.pkg.js' => 'f2139810', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '3e81ae60', @@ -25,7 +25,7 @@ return array( 'rsrc/css/aphront/notification.css' => '3f6c89c9', 'rsrc/css/aphront/panel-view.css' => '8427b78d', 'rsrc/css/aphront/phabricator-nav-view.css' => 'ac79a758', - 'rsrc/css/aphront/table-view.css' => 'aeb66852', + 'rsrc/css/aphront/table-view.css' => '8df59783', 'rsrc/css/aphront/tokenizer.css' => '056da01b', 'rsrc/css/aphront/tooltip.css' => '1a07aea8', 'rsrc/css/aphront/typeahead-browse.css' => '8904346a', @@ -536,7 +536,7 @@ return array( 'aphront-list-filter-view-css' => '5d6f0526', 'aphront-multi-column-view-css' => 'fd18389d', 'aphront-panel-view-css' => '8427b78d', - 'aphront-table-view-css' => 'aeb66852', + 'aphront-table-view-css' => '8df59783', 'aphront-tokenizer-control-css' => '056da01b', 'aphront-tooltip-css' => '1a07aea8', 'aphront-typeahead-control-css' => 'd4f16145', diff --git a/src/infrastructure/graph/DifferentialRevisionGraph.php b/src/infrastructure/graph/DifferentialRevisionGraph.php index 0fba201ea1..3b5d638ec7 100644 --- a/src/infrastructure/graph/DifferentialRevisionGraph.php +++ b/src/infrastructure/graph/DifferentialRevisionGraph.php @@ -41,11 +41,13 @@ final class DifferentialRevisionGraph array( 'href' => $object->getURI(), ), - array( - $object->getMonogram(), - ' ', - $object->getTitle(), - )); + $object->getTitle()); + + $link = array( + $object->getMonogram(), + ' ', + $link, + ); } else { $status = null; $author = null; @@ -74,7 +76,7 @@ final class DifferentialRevisionGraph 'threads', 'graph-status', null, - 'wide object-link', + 'wide pri object-link', )); } diff --git a/src/infrastructure/graph/ManiphestTaskGraph.php b/src/infrastructure/graph/ManiphestTaskGraph.php index 769de28073..b793493cd3 100644 --- a/src/infrastructure/graph/ManiphestTaskGraph.php +++ b/src/infrastructure/graph/ManiphestTaskGraph.php @@ -54,11 +54,13 @@ final class ManiphestTaskGraph array( 'href' => $object->getURI(), ), - array( - $object->getMonogram(), - ' ', - $object->getTitle(), - )); + $object->getTitle()); + + $link = array( + $object->getMonogram(), + ' ', + $link, + ); } else { $status = null; $assigned = null; @@ -87,7 +89,7 @@ final class ManiphestTaskGraph 'threads', 'graph-status', null, - 'wide object-link', + 'wide pri object-link', )); } diff --git a/webroot/rsrc/css/aphront/table-view.css b/webroot/rsrc/css/aphront/table-view.css index 8ec4f2a4c6..dddd78f103 100644 --- a/webroot/rsrc/css/aphront/table-view.css +++ b/webroot/rsrc/css/aphront/table-view.css @@ -228,9 +228,14 @@ span.single-display-line-content { position: static; } +.aphront-table-view tr.closed td.object-link, +.aphront-table-view tr.alt-closed td.object-link { + text-decoration: line-through; + color: rgba({$alphablack}, 0.5); +} + .aphront-table-view tr.closed td.object-link a, .aphront-table-view tr.alt-closed td.object-link a { - text-decoration: line-through; color: rgba({$alphablack}, 0.5); }