From 6d525848bf96cd82cf01b0a9970f2d61f7912374 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Tue, 11 Oct 2016 10:02:22 -0700 Subject: [PATCH 01/55] Remove box-shadow from feed story actors Summary: We've been removing these, remove for consistency. Test Plan: profile image with transparency Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D16687 --- resources/celerity/map.php | 6 +++--- webroot/rsrc/css/phui/phui-feed-story.css | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index badb3d06fb..1e25a4cd03 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,7 +9,7 @@ return array( 'names' => array( 'conpherence.pkg.css' => '4601645d', 'conpherence.pkg.js' => '11f3e07e', - 'core.pkg.css' => 'de918edf', + 'core.pkg.css' => 'b8364d01', 'core.pkg.js' => '30185d95', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => 'e1d704ce', @@ -136,7 +136,7 @@ return array( 'rsrc/css/phui/phui-document-pro.css' => 'ca1fed81', 'rsrc/css/phui/phui-document-summary.css' => '9ca48bdf', 'rsrc/css/phui/phui-document.css' => 'c32e8dec', - 'rsrc/css/phui/phui-feed-story.css' => 'aa49845d', + 'rsrc/css/phui/phui-feed-story.css' => '44a9c8e9', 'rsrc/css/phui/phui-fontkit.css' => '9cda225e', 'rsrc/css/phui/phui-form-view.css' => '9e22b190', 'rsrc/css/phui/phui-form.css' => 'aac1d51d', @@ -909,7 +909,7 @@ return array( 'phui-document-summary-view-css' => '9ca48bdf', 'phui-document-view-css' => 'c32e8dec', 'phui-document-view-pro-css' => 'ca1fed81', - 'phui-feed-story-css' => 'aa49845d', + 'phui-feed-story-css' => '44a9c8e9', 'phui-font-icon-base-css' => '870a7360', 'phui-fontkit-css' => '9cda225e', 'phui-form-css' => 'aac1d51d', diff --git a/webroot/rsrc/css/phui/phui-feed-story.css b/webroot/rsrc/css/phui/phui-feed-story.css index d971202ebe..3fdfeb5a57 100644 --- a/webroot/rsrc/css/phui/phui-feed-story.css +++ b/webroot/rsrc/css/phui/phui-feed-story.css @@ -16,7 +16,6 @@ float: left; margin-right: 8px; border-radius: 3px; - box-shadow: {$borderinset}; } .phui-feed-story-head .phui-feed-story-actor-image { From d79972ecb3a2a1ca0c2a73563d337872a86f3e1c Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 11 Oct 2016 11:53:22 -0700 Subject: [PATCH 02/55] Provide start/end date time via Conduit for Calendar Summary: Fixes T11706. I think this approach (roughly: provide the information in a few different formats) is generally reasonable, and should let clients choose how much date/time magic they want to do. Test Plan: Called `calenadar.event.search`, viewed results. Reviewers: chad Reviewed By: chad Maniphest Tasks: T11706 Differential Revision: https://secure.phabricator.com/D16688 --- .../storage/PhabricatorCalendarEvent.php | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/applications/calendar/storage/PhabricatorCalendarEvent.php b/src/applications/calendar/storage/PhabricatorCalendarEvent.php index 67a04d226e..e6ad12ab3c 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarEvent.php +++ b/src/applications/calendar/storage/PhabricatorCalendarEvent.php @@ -1056,13 +1056,27 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO ->setKey('description') ->setType('string') ->setDescription(pht('The event description.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('startDateTime') + ->setType('datetime') + ->setDescription(pht('Start date and time of the event.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('endDateTime') + ->setType('datetime') + ->setDescription(pht('End date and time of the event.')), ); } public function getFieldValuesForConduit() { + $start_datetime = $this->newStartDateTime(); + $end_datetime = $this->newEndDateTime(); + return array( 'name' => $this->getName(), 'description' => $this->getDescription(), + 'isAllDay' => $this->getIsAllDay(), + 'startDateTime' => $this->getConduitDateTime($start_datetime), + 'endDateTime' => $this->getConduitDateTime($end_datetime), ); } @@ -1070,4 +1084,26 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO return array(); } + private function getConduitDateTime($datetime) { + if (!$datetime) { + return null; + } + + $epoch = $datetime->getEpoch(); + + // TODO: Possibly pass the actual viewer in from the Conduit stuff, or + // retain it when setting the viewer timezone? + $viewer = id(new PhabricatorUser()) + ->overrideTimezoneIdentifier($this->viewerTimezone); + + return array( + 'epoch' => $epoch, + 'display' => array( + 'default' => phabricator_datetime($epoch, $viewer), + ), + 'iso8601' => $datetime->getISO8601(), + 'timezone' => $this->viewerTimezone, + ); + } + } From eec2d953e0704082e1df710c0a9c728c5bce1665 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Tue, 11 Oct 2016 12:19:25 -0700 Subject: [PATCH 03/55] Update durable column to 8 rooms Summary: We have more space here for last 8. Test Plan: Reload, see 8. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D16689 --- .../conpherence/controller/ConpherenceColumnViewController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/applications/conpherence/controller/ConpherenceColumnViewController.php b/src/applications/conpherence/controller/ConpherenceColumnViewController.php index 9705c1f755..de83379aab 100644 --- a/src/applications/conpherence/controller/ConpherenceColumnViewController.php +++ b/src/applications/conpherence/controller/ConpherenceColumnViewController.php @@ -9,7 +9,7 @@ final class ConpherenceColumnViewController extends $latest_conpherences = array(); $latest_participant = id(new ConpherenceParticipantQuery()) ->withParticipantPHIDs(array($user->getPHID())) - ->setLimit(6) + ->setLimit(8) ->execute(); if ($latest_participant) { $conpherence_phids = mpull($latest_participant, 'getConpherencePHID'); From fa90f8bef413f061107d3cc06830ff4da3ec15f4 Mon Sep 17 00:00:00 2001 From: Mike Riley Date: Tue, 11 Oct 2016 19:55:43 +0000 Subject: [PATCH 04/55] Expose Drydock authorizations via Conduit Summary: `DrydockAuthorizationSearchEngine` was being used solely to display authorizations for a specific blueprint from the web UI and consequently expected that callers set a specific blueprint before performing a query. Here we check to see if a blueprint has been set in cases where the engine could be operating from either Conduit or the web. Ref T11694 Test Plan: - called the API method from the console - approved an authorization - followed the "view all" link from a blueprint page Reviewers: #blessed_reviewers, epriestley Reviewed By: #blessed_reviewers, epriestley Subscribers: epriestley Maniphest Tasks: T11694 Differential Revision: https://secure.phabricator.com/D16592 --- src/__phutil_library_map__.php | 3 ++ ...ockAuthorizationSearchConduitAPIMethod.php | 18 +++++++ .../DrydockAuthorizationSearchEngine.php | 37 +++++++++++++- .../drydock/storage/DrydockAuthorization.php | 51 ++++++++++++++++++- 4 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 src/applications/drydock/conduit/DrydockAuthorizationSearchConduitAPIMethod.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 085e86c84b..05142f0e9e 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -919,6 +919,7 @@ phutil_register_library_map(array( 'DrydockAuthorizationListView' => 'applications/drydock/view/DrydockAuthorizationListView.php', 'DrydockAuthorizationPHIDType' => 'applications/drydock/phid/DrydockAuthorizationPHIDType.php', 'DrydockAuthorizationQuery' => 'applications/drydock/query/DrydockAuthorizationQuery.php', + 'DrydockAuthorizationSearchConduitAPIMethod' => 'applications/drydock/conduit/DrydockAuthorizationSearchConduitAPIMethod.php', 'DrydockAuthorizationSearchEngine' => 'applications/drydock/query/DrydockAuthorizationSearchEngine.php', 'DrydockAuthorizationViewController' => 'applications/drydock/controller/DrydockAuthorizationViewController.php', 'DrydockBlueprint' => 'applications/drydock/storage/DrydockBlueprint.php', @@ -5468,12 +5469,14 @@ phutil_register_library_map(array( 'DrydockAuthorization' => array( 'DrydockDAO', 'PhabricatorPolicyInterface', + 'PhabricatorConduitResultInterface', ), 'DrydockAuthorizationAuthorizeController' => 'DrydockController', 'DrydockAuthorizationListController' => 'DrydockController', 'DrydockAuthorizationListView' => 'AphrontView', 'DrydockAuthorizationPHIDType' => 'PhabricatorPHIDType', 'DrydockAuthorizationQuery' => 'DrydockQuery', + 'DrydockAuthorizationSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'DrydockAuthorizationSearchEngine' => 'PhabricatorApplicationSearchEngine', 'DrydockAuthorizationViewController' => 'DrydockController', 'DrydockBlueprint' => array( diff --git a/src/applications/drydock/conduit/DrydockAuthorizationSearchConduitAPIMethod.php b/src/applications/drydock/conduit/DrydockAuthorizationSearchConduitAPIMethod.php new file mode 100644 index 0000000000..7c90639bcc --- /dev/null +++ b/src/applications/drydock/conduit/DrydockAuthorizationSearchConduitAPIMethod.php @@ -0,0 +1,18 @@ +getBlueprint(); - $query->withBlueprintPHIDs(array($blueprint->getPHID())); + if ($blueprint) { + $query->withBlueprintPHIDs(array($blueprint->getPHID())); + } return $query; } @@ -38,15 +40,46 @@ final class DrydockAuthorizationSearchEngine protected function buildQueryFromParameters(array $map) { $query = $this->newQuery(); + if ($map['blueprintPHIDs']) { + $query->withBlueprintPHIDs($map['blueprintPHIDs']); + } + + if ($map['objectPHIDs']) { + $query->withObjectPHIDs($map['objectPHIDs']); + } + return $query; } protected function buildCustomSearchFields() { - return array(); + return array( + id(new PhabricatorSearchDatasourceField()) + ->setLabel(pht('Blueprints')) + ->setKey('blueprintPHIDs') + ->setConduitParameterType(new ConduitPHIDListParameterType()) + ->setDescription(pht('Search authorizations for specific blueprints.')) + ->setAliases(array('blueprint', 'blueprints')) + ->setDatasource(new DrydockBlueprintDatasource()), + id(new PhabricatorPHIDsSearchField()) + ->setLabel(pht('Objects')) + ->setKey('objectPHIDs') + ->setDescription(pht('Search authorizations from specific objects.')) + ->setAliases(array('object', 'objects')), + ); + } + + protected function getHiddenFields() { + return array( + 'blueprintPHIDs', + 'objectPHIDs', + ); } protected function getURI($path) { $blueprint = $this->getBlueprint(); + if (!$blueprint) { + throw new PhutilInvalidStateException('setBlueprint'); + } $id = $blueprint->getID(); return "/drydock/blueprint/{$id}/authorizations/".$path; } diff --git a/src/applications/drydock/storage/DrydockAuthorization.php b/src/applications/drydock/storage/DrydockAuthorization.php index 9503a03767..cfd186c9d3 100644 --- a/src/applications/drydock/storage/DrydockAuthorization.php +++ b/src/applications/drydock/storage/DrydockAuthorization.php @@ -2,7 +2,8 @@ final class DrydockAuthorization extends DrydockDAO implements - PhabricatorPolicyInterface { + PhabricatorPolicyInterface, + PhabricatorConduitResultInterface { const OBJECTAUTH_ACTIVE = 'active'; const OBJECTAUTH_INACTIVE = 'inactive'; @@ -204,4 +205,52 @@ final class DrydockAuthorization extends DrydockDAO } +/* -( PhabricatorConduitResultInterface )---------------------------------- */ + + + public function getFieldSpecificationsForConduit() { + return array( + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('blueprintPHID') + ->setType('phid') + ->setDescription(pht( + 'PHID of the blueprint this request was made for.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('blueprintAuthorizationState') + ->setType('map') + ->setDescription(pht('Authorization state of this request.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('objectPHID') + ->setType('phid') + ->setDescription(pht( + 'PHID of the object which requested authorization.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('objectAuthorizationState') + ->setType('map') + ->setDescription(pht('Authorization state of the requesting object.')), + ); + } + + public function getFieldValuesForConduit() { + $blueprint_state = $this->getBlueprintAuthorizationState(); + $object_state = $this->getObjectAuthorizationState(); + return array( + 'blueprintPHID' => $this->getBlueprintPHID(), + 'blueprintAuthorizationState' => array( + 'value' => $blueprint_state, + 'name' => self::getBlueprintStateName($blueprint_state), + ), + 'objectPHID' => $this->getObjectPHID(), + 'objectAuthorizationState' => array( + 'value' => $object_state, + 'name' => self::getObjectStateName($object_state), + ), + ); + } + + public function getConduitSearchAttachments() { + return array( + ); + } + } From 13b4b37d30d2944968a232a71bbcbfd4a3adef4a Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 11 Oct 2016 12:32:32 -0700 Subject: [PATCH 05/55] Force a couple of Conduit results to the proper types in Calendar Summary: Ref T11706. Add some casts so we don't return `"0"` for `false`. Also I forgot to document one of the things. Test Plan: Called `calendar.event.search`. Reviewers: chad Reviewed By: chad Maniphest Tasks: T11706 Differential Revision: https://secure.phabricator.com/D16690 --- .../calendar/storage/PhabricatorCalendarEvent.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/applications/calendar/storage/PhabricatorCalendarEvent.php b/src/applications/calendar/storage/PhabricatorCalendarEvent.php index e6ad12ab3c..fd70d7780f 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarEvent.php +++ b/src/applications/calendar/storage/PhabricatorCalendarEvent.php @@ -1056,6 +1056,10 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO ->setKey('description') ->setType('string') ->setDescription(pht('The event description.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('isAllDay') + ->setType('bool') + ->setDescription(pht('True if the event is an all day event.')), id(new PhabricatorConduitSearchFieldSpecification()) ->setKey('startDateTime') ->setType('datetime') @@ -1074,7 +1078,7 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO return array( 'name' => $this->getName(), 'description' => $this->getDescription(), - 'isAllDay' => $this->getIsAllDay(), + 'isAllDay' => (bool)$this->getIsAllDay(), 'startDateTime' => $this->getConduitDateTime($start_datetime), 'endDateTime' => $this->getConduitDateTime($end_datetime), ); @@ -1097,7 +1101,7 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO ->overrideTimezoneIdentifier($this->viewerTimezone); return array( - 'epoch' => $epoch, + 'epoch' => (int)$epoch, 'display' => array( 'default' => phabricator_datetime($epoch, $viewer), ), From 754397c4e75d748e9a193e42c0ca2305be6106f0 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Tue, 11 Oct 2016 15:37:46 -0700 Subject: [PATCH 06/55] Fix some minor UI issues with mobile application search Summary: Background is now always white, spacing in header is more consistent Test Plan: test mobile, table, desktop application search apps. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D16691 --- resources/celerity/map.php | 6 +++--- .../PhabricatorApplicationSearchController.php | 2 +- .../css/application/search/application-search-view.css | 10 ++++++++++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 1e25a4cd03..bb082d5a78 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,7 +9,7 @@ return array( 'names' => array( 'conpherence.pkg.css' => '4601645d', 'conpherence.pkg.js' => '11f3e07e', - 'core.pkg.css' => 'b8364d01', + 'core.pkg.css' => '7ca260a3', 'core.pkg.js' => '30185d95', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => 'e1d704ce', @@ -102,7 +102,7 @@ return array( 'rsrc/css/application/releeph/releeph-preview-branch.css' => 'b7a6f4a5', 'rsrc/css/application/releeph/releeph-request-differential-create-dialog.css' => '8d8b92cd', 'rsrc/css/application/releeph/releeph-request-typeahead.css' => '667a48ae', - 'rsrc/css/application/search/application-search-view.css' => 'be6454ec', + 'rsrc/css/application/search/application-search-view.css' => '8452c849', 'rsrc/css/application/search/search-results.css' => '7dea472c', 'rsrc/css/application/slowvote/slowvote.css' => 'a94b7230', 'rsrc/css/application/tokens/tokens.css' => '3d0f239e', @@ -610,7 +610,7 @@ return array( 'aphront-tokenizer-control-css' => '056da01b', 'aphront-tooltip-css' => '1a07aea8', 'aphront-typeahead-control-css' => 'd4f16145', - 'application-search-view-css' => 'be6454ec', + 'application-search-view-css' => '8452c849', 'auth-css' => '0877ed6e', 'bulk-job-css' => 'df9c1d4a', 'changeset-view-manager' => 'a2828756', diff --git a/src/applications/search/controller/PhabricatorApplicationSearchController.php b/src/applications/search/controller/PhabricatorApplicationSearchController.php index 6350df5e02..92a8181b4f 100644 --- a/src/applications/search/controller/PhabricatorApplicationSearchController.php +++ b/src/applications/search/controller/PhabricatorApplicationSearchController.php @@ -329,7 +329,6 @@ final class PhabricatorApplicationSearchController $crumbs->addTextCrumb($title); } - $nav->addClass('application-search-view'); require_celerity_resource('application-search-view-css'); return $this->newPage() @@ -337,6 +336,7 @@ final class PhabricatorApplicationSearchController ->setTitle(pht('Query: %s', $title)) ->setCrumbs($crumbs) ->setNavigation($nav) + ->addClass('application-search-view') ->appendChild($body); } diff --git a/webroot/rsrc/css/application/search/application-search-view.css b/webroot/rsrc/css/application/search/application-search-view.css index 4a573f5111..1320df9c0f 100644 --- a/webroot/rsrc/css/application/search/application-search-view.css +++ b/webroot/rsrc/css/application/search/application-search-view.css @@ -11,6 +11,11 @@ padding: 0 16px 24px; } +.device-phone .application-search-view + .application-search-results.phui-object-box { + padding: 0 12px 24px; +} + .application-search-view .application-search-results .phui-profile-header { padding: 16px 8px; border-bottom: 1px solid {$thinblueborder}; @@ -31,6 +36,11 @@ padding: 12px 0; } +.device-phone .application-search-results + .phui-profile-header.phui-header-shell { + padding: 12px 0 12px 4px; +} + .device-phone .application-search-results .phui-profile-header.phui-header-shell .phui-header-header { font-size: 16px; From 0244ec311529dd67639b5e5aba3fb1cfbf4fd791 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Tue, 11 Oct 2016 20:13:26 -0700 Subject: [PATCH 07/55] Add Room typeahead for Conpherence Search Summary: Ref T3165. Builds an ngram table for Conpherence Room titles, allowing a tokenizer for searching a subset of rooms. Test Plan: Say `Gabbert` in two different rooms, search all, see two rooms returned. Search specific room, see specific result. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Maniphest Tasks: T3165 Differential Revision: https://secure.phabricator.com/D16692 --- .../20161011.conpherence.ngrams.php | 11 +++++ .../20161011.conpherence.ngrams.sql | 7 +++ src/__phutil_library_map__.php | 5 ++ .../query/ConpherenceThreadQuery.php | 6 +++ .../query/ConpherenceThreadSearchEngine.php | 11 ++++- .../conpherence/storage/ConpherenceThread.php | 13 ++++- .../storage/ConpherenceThreadTitleNgrams.php | 17 +++++++ .../typeahead/ConpherenceThreadDatasource.php | 47 +++++++++++++++++++ 8 files changed, 114 insertions(+), 3 deletions(-) create mode 100644 resources/sql/autopatches/20161011.conpherence.ngrams.php create mode 100644 resources/sql/autopatches/20161011.conpherence.ngrams.sql create mode 100644 src/applications/conpherence/storage/ConpherenceThreadTitleNgrams.php create mode 100644 src/applications/conpherence/typeahead/ConpherenceThreadDatasource.php diff --git a/resources/sql/autopatches/20161011.conpherence.ngrams.php b/resources/sql/autopatches/20161011.conpherence.ngrams.php new file mode 100644 index 0000000000..457143f6c7 --- /dev/null +++ b/resources/sql/autopatches/20161011.conpherence.ngrams.php @@ -0,0 +1,11 @@ +getPHID(), + array( + 'force' => true, + )); +} diff --git a/resources/sql/autopatches/20161011.conpherence.ngrams.sql b/resources/sql/autopatches/20161011.conpherence.ngrams.sql new file mode 100644 index 0000000000..e06c1489f7 --- /dev/null +++ b/resources/sql/autopatches/20161011.conpherence.ngrams.sql @@ -0,0 +1,7 @@ +CREATE TABLE {$NAMESPACE}_conpherence.conpherence_threadtitle_ngrams ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + objectID INT UNSIGNED NOT NULL, + ngram CHAR(3) NOT NULL COLLATE {$COLLATE_TEXT}, + KEY `key_object` (objectID), + KEY `key_ngram` (ngram, objectID) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 05142f0e9e..fb6cbb32ae 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -313,6 +313,7 @@ phutil_register_library_map(array( 'ConpherenceSchemaSpec' => 'applications/conpherence/storage/ConpherenceSchemaSpec.php', 'ConpherenceTestCase' => 'applications/conpherence/__tests__/ConpherenceTestCase.php', 'ConpherenceThread' => 'applications/conpherence/storage/ConpherenceThread.php', + 'ConpherenceThreadDatasource' => 'applications/conpherence/typeahead/ConpherenceThreadDatasource.php', 'ConpherenceThreadIndexEngineExtension' => 'applications/conpherence/engineextension/ConpherenceThreadIndexEngineExtension.php', 'ConpherenceThreadListView' => 'applications/conpherence/view/ConpherenceThreadListView.php', 'ConpherenceThreadMailReceiver' => 'applications/conpherence/mail/ConpherenceThreadMailReceiver.php', @@ -320,6 +321,7 @@ phutil_register_library_map(array( 'ConpherenceThreadQuery' => 'applications/conpherence/query/ConpherenceThreadQuery.php', 'ConpherenceThreadRemarkupRule' => 'applications/conpherence/remarkup/ConpherenceThreadRemarkupRule.php', 'ConpherenceThreadSearchEngine' => 'applications/conpherence/query/ConpherenceThreadSearchEngine.php', + 'ConpherenceThreadTitleNgrams' => 'applications/conpherence/storage/ConpherenceThreadTitleNgrams.php', 'ConpherenceTransaction' => 'applications/conpherence/storage/ConpherenceTransaction.php', 'ConpherenceTransactionComment' => 'applications/conpherence/storage/ConpherenceTransactionComment.php', 'ConpherenceTransactionQuery' => 'applications/conpherence/query/ConpherenceTransactionQuery.php', @@ -4814,7 +4816,9 @@ phutil_register_library_map(array( 'PhabricatorApplicationTransactionInterface', 'PhabricatorMentionableInterface', 'PhabricatorDestructibleInterface', + 'PhabricatorNgramsInterface', ), + 'ConpherenceThreadDatasource' => 'PhabricatorTypeaheadDatasource', 'ConpherenceThreadIndexEngineExtension' => 'PhabricatorIndexEngineExtension', 'ConpherenceThreadListView' => 'AphrontView', 'ConpherenceThreadMailReceiver' => 'PhabricatorObjectMailReceiver', @@ -4822,6 +4826,7 @@ phutil_register_library_map(array( 'ConpherenceThreadQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'ConpherenceThreadRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'ConpherenceThreadSearchEngine' => 'PhabricatorApplicationSearchEngine', + 'ConpherenceThreadTitleNgrams' => 'PhabricatorSearchNgrams', 'ConpherenceTransaction' => 'PhabricatorApplicationTransaction', 'ConpherenceTransactionComment' => 'PhabricatorApplicationTransactionComment', 'ConpherenceTransactionQuery' => 'PhabricatorApplicationTransactionQuery', diff --git a/src/applications/conpherence/query/ConpherenceThreadQuery.php b/src/applications/conpherence/query/ConpherenceThreadQuery.php index 640a19a612..7cb7003121 100644 --- a/src/applications/conpherence/query/ConpherenceThreadQuery.php +++ b/src/applications/conpherence/query/ConpherenceThreadQuery.php @@ -76,6 +76,12 @@ final class ConpherenceThreadQuery return $this; } + public function withTitleNgrams($ngrams) { + return $this->withNgramsConstraint( + id(new ConpherenceThreadTitleNgrams()), + $ngrams); + } + protected function loadPage() { $table = new ConpherenceThread(); $conn_r = $table->establishConnection('r'); diff --git a/src/applications/conpherence/query/ConpherenceThreadSearchEngine.php b/src/applications/conpherence/query/ConpherenceThreadSearchEngine.php index f2c172b0f2..f41bf835e7 100644 --- a/src/applications/conpherence/query/ConpherenceThreadSearchEngine.php +++ b/src/applications/conpherence/query/ConpherenceThreadSearchEngine.php @@ -22,8 +22,13 @@ final class ConpherenceThreadSearchEngine ->setLabel(pht('Participants')) ->setKey('participants') ->setAliases(array('participant')), + id(new PhabricatorSearchDatasourceField()) + ->setLabel(pht('Rooms')) + ->setKey('phids') + ->setDescription(pht('Search by room titles.')) + ->setDatasource(id(new ConpherenceThreadDatasource())), id(new PhabricatorSearchTextField()) - ->setLabel(pht('Contains Words')) + ->setLabel(pht('Room Contains Words')) ->setKey('fulltext'), ); } @@ -47,7 +52,9 @@ final class ConpherenceThreadSearchEngine if ($map['fulltext']) { $query->withFulltext($map['fulltext']); } - + if ($map['phids']) { + $query->withPHIDs($map['phids']); + } return $query; } diff --git a/src/applications/conpherence/storage/ConpherenceThread.php b/src/applications/conpherence/storage/ConpherenceThread.php index 36930f6151..6032835c42 100644 --- a/src/applications/conpherence/storage/ConpherenceThread.php +++ b/src/applications/conpherence/storage/ConpherenceThread.php @@ -5,7 +5,8 @@ final class ConpherenceThread extends ConpherenceDAO PhabricatorPolicyInterface, PhabricatorApplicationTransactionInterface, PhabricatorMentionableInterface, - PhabricatorDestructibleInterface { + PhabricatorDestructibleInterface, + PhabricatorNgramsInterface { protected $title; protected $topic; @@ -427,6 +428,16 @@ final class ConpherenceThread extends ConpherenceDAO return $timeline; } +/* -( PhabricatorNgramInterface )------------------------------------------ */ + + + public function newNgrams() { + return array( + id(new ConpherenceThreadTitleNgrams()) + ->setValue($this->getTitle()), + ); + } + /* -( PhabricatorDestructibleInterface )----------------------------------- */ diff --git a/src/applications/conpherence/storage/ConpherenceThreadTitleNgrams.php b/src/applications/conpherence/storage/ConpherenceThreadTitleNgrams.php new file mode 100644 index 0000000000..7f8ec99d02 --- /dev/null +++ b/src/applications/conpherence/storage/ConpherenceThreadTitleNgrams.php @@ -0,0 +1,17 @@ +getViewer(); + $raw_query = $this->getRawQuery(); + + $rooms = id(new ConpherenceThreadQuery()) + ->setViewer($viewer) + ->withTitleNgrams($raw_query) + ->needParticipants(true) + ->execute(); + + $results = array(); + foreach ($rooms as $room) { + if (strlen($room->getTopic())) { + $topic = $room->getTopic(); + } else { + $topic = phutil_tag('em', array(), pht('No topic set')); + } + + $token = id(new PhabricatorTypeaheadResult()) + ->setName($room->getTitle()) + ->setPHID($room->getPHID()) + ->addAttribute($topic); + + $results[] = $token; + } + + return $results; + } + +} From ea6db2ae9bf25a93ff6696d1bff2c99054f9ddd3 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Wed, 12 Oct 2016 09:05:56 -0700 Subject: [PATCH 08/55] Increase Conpherence notification panel transaction fetch Summary: We currently fetch 15 transactions for 5 rooms, which leads to some room subtitles in the notification panel to being blank since nothing was fetched. I don't think this is a great fix, but moves the bar much further. Maybe there is a more accurate fix that isn't 5 SQL queries? Test Plan: Review notification panel in sandbox, ensure all threads have some additional information. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D16695 --- .../controller/ConpherenceNotificationPanelController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/applications/conpherence/controller/ConpherenceNotificationPanelController.php b/src/applications/conpherence/controller/ConpherenceNotificationPanelController.php index c72490f60f..03f9c926ca 100644 --- a/src/applications/conpherence/controller/ConpherenceNotificationPanelController.php +++ b/src/applications/conpherence/controller/ConpherenceNotificationPanelController.php @@ -20,7 +20,7 @@ final class ConpherenceNotificationPanelController ->withPHIDs(array_keys($participant_data)) ->needProfileImage(true) ->needTransactions(true) - ->setTransactionLimit(3 * 5) + ->setTransactionLimit(50) ->needParticipantCache(true) ->execute(); } From 2ab07ed29bd971d1338e883d86c45554bac13e4e Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 11 Oct 2016 11:45:57 -0700 Subject: [PATCH 09/55] Prepare for event imports in Calendar Summary: Ref T10747. Adds a bunch of stuff so we can keep track of which events we've imported from external sources. This doesn't do anything yet: you can't actually import anything. Test Plan: - Ran `bin/storage upgrade`. - Clicked "Imports", saw an empty wasteland. - Created/edited events. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10747 Differential Revision: https://secure.phabricator.com/D16696 --- .../autopatches/20161012.cal.01.import.sql | 15 +++ .../20161012.cal.02.importxaction.sql | 19 +++ .../20161012.cal.03.eventimport.sql | 11 ++ src/__phutil_library_map__.php | 22 +++ .../PhabricatorCalendarApplication.php | 11 +- ...PhabricatorCalendarEventListController.php | 4 + ...habricatorCalendarImportListController.php | 12 ++ .../PhabricatorCalendarImportEditor.php | 18 +++ .../PhabricatorCalendarImportPHIDType.php | 50 +++++++ .../query/PhabricatorCalendarEventQuery.php | 50 ++++++- .../query/PhabricatorCalendarImportQuery.php | 81 +++++++++++ .../PhabricatorCalendarImportSearchEngine.php | 81 +++++++++++ ...bricatorCalendarImportTransactionQuery.php | 10 ++ .../storage/PhabricatorCalendarEvent.php | 70 +++++++++- .../storage/PhabricatorCalendarImport.php | 126 ++++++++++++++++++ .../PhabricatorCalendarImportTransaction.php | 18 +++ .../storage/PhabricatorSlowvotePoll.php | 2 +- 17 files changed, 596 insertions(+), 4 deletions(-) create mode 100644 resources/sql/autopatches/20161012.cal.01.import.sql create mode 100644 resources/sql/autopatches/20161012.cal.02.importxaction.sql create mode 100644 resources/sql/autopatches/20161012.cal.03.eventimport.sql create mode 100644 src/applications/calendar/controller/PhabricatorCalendarImportListController.php create mode 100644 src/applications/calendar/editor/PhabricatorCalendarImportEditor.php create mode 100644 src/applications/calendar/phid/PhabricatorCalendarImportPHIDType.php create mode 100644 src/applications/calendar/query/PhabricatorCalendarImportQuery.php create mode 100644 src/applications/calendar/query/PhabricatorCalendarImportSearchEngine.php create mode 100644 src/applications/calendar/query/PhabricatorCalendarImportTransactionQuery.php create mode 100644 src/applications/calendar/storage/PhabricatorCalendarImport.php create mode 100644 src/applications/calendar/storage/PhabricatorCalendarImportTransaction.php diff --git a/resources/sql/autopatches/20161012.cal.01.import.sql b/resources/sql/autopatches/20161012.cal.01.import.sql new file mode 100644 index 0000000000..aff544f015 --- /dev/null +++ b/resources/sql/autopatches/20161012.cal.01.import.sql @@ -0,0 +1,15 @@ +CREATE TABLE {$NAMESPACE}_calendar.calendar_import ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + name LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}, + authorPHID VARBINARY(64) NOT NULL, + viewPolicy VARBINARY(64) NOT NULL, + editPolicy VARBINARY(64) NOT NULL, + engineType VARCHAR(64) NOT NULL, + parameters LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}, + isDisabled BOOL NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (phid), + KEY `key_author` (authorPHID) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20161012.cal.02.importxaction.sql b/resources/sql/autopatches/20161012.cal.02.importxaction.sql new file mode 100644 index 0000000000..e17474ac0b --- /dev/null +++ b/resources/sql/autopatches/20161012.cal.02.importxaction.sql @@ -0,0 +1,19 @@ +CREATE TABLE {$NAMESPACE}_calendar.calendar_importtransaction ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + authorPHID VARBINARY(64) NOT NULL, + objectPHID VARBINARY(64) NOT NULL, + viewPolicy VARBINARY(64) NOT NULL, + editPolicy VARBINARY(64) NOT NULL, + commentPHID VARBINARY(64) DEFAULT NULL, + commentVersion INT UNSIGNED NOT NULL, + transactionType VARCHAR(32) COLLATE {$COLLATE_TEXT} NOT NULL, + oldValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + newValue LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + contentSource LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + metadata LONGTEXT COLLATE {$COLLATE_TEXT} NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (`phid`), + KEY `key_object` (`objectPHID`) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20161012.cal.03.eventimport.sql b/resources/sql/autopatches/20161012.cal.03.eventimport.sql new file mode 100644 index 0000000000..968b40e00b --- /dev/null +++ b/resources/sql/autopatches/20161012.cal.03.eventimport.sql @@ -0,0 +1,11 @@ +ALTER TABLE {$NAMESPACE}_calendar.calendar_event + ADD importAuthorPHID VARBINARY(64); + +ALTER TABLE {$NAMESPACE}_calendar.calendar_event + ADD importSourcePHID VARBINARY(64); + +ALTER TABLE {$NAMESPACE}_calendar.calendar_event + ADD importUIDIndex BINARY(12); + +ALTER TABLE {$NAMESPACE}_calendar.calendar_event + ADD importUID LONGTEXT COLLATE {$COLLATE_TEXT}; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index fb6cbb32ae..4eb1dc5a6b 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2100,6 +2100,14 @@ phutil_register_library_map(array( 'PhabricatorCalendarHolidayTestCase' => 'applications/calendar/storage/__tests__/PhabricatorCalendarHolidayTestCase.php', 'PhabricatorCalendarICSWriter' => 'applications/calendar/util/PhabricatorCalendarICSWriter.php', 'PhabricatorCalendarIconSet' => 'applications/calendar/icon/PhabricatorCalendarIconSet.php', + 'PhabricatorCalendarImport' => 'applications/calendar/storage/PhabricatorCalendarImport.php', + 'PhabricatorCalendarImportEditor' => 'applications/calendar/editor/PhabricatorCalendarImportEditor.php', + 'PhabricatorCalendarImportListController' => 'applications/calendar/controller/PhabricatorCalendarImportListController.php', + 'PhabricatorCalendarImportPHIDType' => 'applications/calendar/phid/PhabricatorCalendarImportPHIDType.php', + 'PhabricatorCalendarImportQuery' => 'applications/calendar/query/PhabricatorCalendarImportQuery.php', + 'PhabricatorCalendarImportSearchEngine' => 'applications/calendar/query/PhabricatorCalendarImportSearchEngine.php', + 'PhabricatorCalendarImportTransaction' => 'applications/calendar/storage/PhabricatorCalendarImportTransaction.php', + 'PhabricatorCalendarImportTransactionQuery' => 'applications/calendar/query/PhabricatorCalendarImportTransactionQuery.php', 'PhabricatorCalendarRemarkupRule' => 'applications/calendar/remarkup/PhabricatorCalendarRemarkupRule.php', 'PhabricatorCalendarReplyHandler' => 'applications/calendar/mail/PhabricatorCalendarReplyHandler.php', 'PhabricatorCalendarSchemaSpec' => 'applications/calendar/storage/PhabricatorCalendarSchemaSpec.php', @@ -6789,6 +6797,7 @@ phutil_register_library_map(array( 'PhabricatorCalendarEvent' => array( 'PhabricatorCalendarDAO', 'PhabricatorPolicyInterface', + 'PhabricatorExtendedPolicyInterface', 'PhabricatorProjectInterface', 'PhabricatorMarkupInterface', 'PhabricatorApplicationTransactionInterface', @@ -6879,6 +6888,19 @@ phutil_register_library_map(array( 'PhabricatorCalendarHolidayTestCase' => 'PhabricatorTestCase', 'PhabricatorCalendarICSWriter' => 'Phobject', 'PhabricatorCalendarIconSet' => 'PhabricatorIconSet', + 'PhabricatorCalendarImport' => array( + 'PhabricatorCalendarDAO', + 'PhabricatorPolicyInterface', + 'PhabricatorApplicationTransactionInterface', + 'PhabricatorDestructibleInterface', + ), + 'PhabricatorCalendarImportEditor' => 'PhabricatorApplicationTransactionEditor', + 'PhabricatorCalendarImportListController' => 'PhabricatorCalendarController', + 'PhabricatorCalendarImportPHIDType' => 'PhabricatorPHIDType', + 'PhabricatorCalendarImportQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorCalendarImportSearchEngine' => 'PhabricatorApplicationSearchEngine', + 'PhabricatorCalendarImportTransaction' => 'PhabricatorModularTransaction', + 'PhabricatorCalendarImportTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorCalendarRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'PhabricatorCalendarReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'PhabricatorCalendarSchemaSpec' => 'PhabricatorConfigSchemaSpec', diff --git a/src/applications/calendar/application/PhabricatorCalendarApplication.php b/src/applications/calendar/application/PhabricatorCalendarApplication.php index 208fc83ae9..7563bcc096 100644 --- a/src/applications/calendar/application/PhabricatorCalendarApplication.php +++ b/src/applications/calendar/application/PhabricatorCalendarApplication.php @@ -73,7 +73,16 @@ final class PhabricatorCalendarApplication extends PhabricatorApplication { => 'PhabricatorCalendarExportICSController', 'disable/(?P[1-9]\d*)/' => 'PhabricatorCalendarExportDisableController', - + ), + 'import/' => array( + $this->getQueryRoutePattern() + => 'PhabricatorCalendarImportListController', + $this->getEditRoutePattern('edit/') + => 'PhabricatorCalendarImportEditController', + '(?P[1-9]\d*)/' + => 'PhabricatorCalendarImportViewController', + 'disable/(?P[1-9]\d*)/' + => 'PhabricatorCalendarImportDisableController', ), ), ); diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventListController.php b/src/applications/calendar/controller/PhabricatorCalendarEventListController.php index ac9015fbcf..cc9bed332a 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarEventListController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventListController.php @@ -43,6 +43,10 @@ final class PhabricatorCalendarEventListController ->setType(PHUIListItemView::TYPE_LABEL) ->setName(pht('Import/Export')); + $items[] = id(new PHUIListItemView()) + ->setName('Imports') + ->setHref('/calendar/import/'); + $items[] = id(new PHUIListItemView()) ->setName('Exports') ->setHref('/calendar/export/'); diff --git a/src/applications/calendar/controller/PhabricatorCalendarImportListController.php b/src/applications/calendar/controller/PhabricatorCalendarImportListController.php new file mode 100644 index 0000000000..c5ac5cd3e8 --- /dev/null +++ b/src/applications/calendar/controller/PhabricatorCalendarImportListController.php @@ -0,0 +1,12 @@ +setController($this) + ->buildResponse(); + } + +} diff --git a/src/applications/calendar/editor/PhabricatorCalendarImportEditor.php b/src/applications/calendar/editor/PhabricatorCalendarImportEditor.php new file mode 100644 index 0000000000..52bc5c5ff2 --- /dev/null +++ b/src/applications/calendar/editor/PhabricatorCalendarImportEditor.php @@ -0,0 +1,18 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $import = $objects[$phid]; + + $id = $import->getID(); + $name = $import->getName(); + $uri = $import->getURI(); + + $handle + ->setName($name) + ->setFullName(pht('Calendar Import %s: %s', $id, $name)) + ->setURI($uri); + + if ($import->getIsDisabled()) { + $handle->setStatus(PhabricatorObjectHandle::STATUS_CLOSED); + } + } + } + +} diff --git a/src/applications/calendar/query/PhabricatorCalendarEventQuery.php b/src/applications/calendar/query/PhabricatorCalendarEventQuery.php index 9b928c9d1a..e7147d6ef8 100644 --- a/src/applications/calendar/query/PhabricatorCalendarEventQuery.php +++ b/src/applications/calendar/query/PhabricatorCalendarEventQuery.php @@ -14,6 +14,7 @@ final class PhabricatorCalendarEventQuery private $instanceSequencePairs; private $isStub; private $parentEventPHIDs; + private $importSourcePHIDs; private $generateGhosts = false; @@ -77,6 +78,11 @@ final class PhabricatorCalendarEventQuery return $this; } + public function withImportSourcePHIDs(array $import_phids) { + $this->importSourcePHIDs = $import_phids; + return $this; + } + protected function getDefaultOrderVector() { return array('start', 'id'); } @@ -411,6 +417,13 @@ final class PhabricatorCalendarEventQuery $this->parentEventPHIDs); } + if ($this->importSourcePHIDs !== null) { + $where[] = qsprintf( + $conn, + 'event.importSourcePHID IN (%Ls)', + $this->importSourcePHIDs); + } + return $where; } @@ -441,6 +454,42 @@ final class PhabricatorCalendarEventQuery $events = $this->getEventsInRange($events); + $import_phids = array(); + foreach ($events as $event) { + $import_phid = $event->getImportSourcePHID(); + if ($import_phid !== null) { + $import_phids[$import_phid] = $import_phid; + } + } + + if ($import_phids) { + $imports = id(new PhabricatorCalendarImportQuery()) + ->setParentQuery($this) + ->setViewer($viewer) + ->withPHIDs($import_phids) + ->execute(); + $sources = mpull($sources, null, 'getPHID'); + } else { + $sources = array(); + } + + foreach ($events as $key => $event) { + $import_phid = $event->getImportSourcePHID(); + if ($import_phid === null) { + $event->attachImportSource(null); + continue; + } + + $import = idx($imports, $import_phid); + if (!$import) { + unset($events[$key]); + $this->didRejectResult($event); + continue; + } + + $event->attachImportSource($import); + } + $phids = array(); foreach ($events as $event) { @@ -561,5 +610,4 @@ final class PhabricatorCalendarEventQuery return $raw_limit; } - } diff --git a/src/applications/calendar/query/PhabricatorCalendarImportQuery.php b/src/applications/calendar/query/PhabricatorCalendarImportQuery.php new file mode 100644 index 0000000000..9ed6791dfe --- /dev/null +++ b/src/applications/calendar/query/PhabricatorCalendarImportQuery.php @@ -0,0 +1,81 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withAuthorPHIDs(array $phids) { + $this->authorPHIDs = $phids; + return $this; + } + + public function withIsDisabled($is_disabled) { + $this->isDisabled = $is_disabled; + return $this; + } + + public function newResultObject() { + return new PhabricatorCalendarImport(); + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); + + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'import.id IN (%Ld)', + $this->ids); + } + + if ($this->phids !== null) { + $where[] = qsprintf( + $conn, + 'import.phid IN (%Ls)', + $this->phids); + } + + if ($this->authorPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'import.authorPHID IN (%Ls)', + $this->authorPHIDs); + } + + if ($this->isDisabled !== null) { + $where[] = qsprintf( + $conn, + 'import.isDisabled = %d', + (int)$this->isDisabled); + } + + return $where; + } + + protected function getPrimaryTableAlias() { + return 'import'; + } + + public function getQueryApplicationClass() { + return 'PhabricatorCalendarApplication'; + } + +} diff --git a/src/applications/calendar/query/PhabricatorCalendarImportSearchEngine.php b/src/applications/calendar/query/PhabricatorCalendarImportSearchEngine.php new file mode 100644 index 0000000000..deeefbd478 --- /dev/null +++ b/src/applications/calendar/query/PhabricatorCalendarImportSearchEngine.php @@ -0,0 +1,81 @@ +newQuery(); + + return $query; + } + + protected function getURI($path) { + return '/calendar/import/'.$path; + } + + protected function getBuiltinQueryNames() { + $names = array( + 'all' => pht('All Imports'), + ); + + return $names; + } + + public function buildSavedQueryFromBuiltin($query_key) { + $query = $this->newSavedQuery(); + $query->setQueryKey($query_key); + + switch ($query_key) { + case 'all': + return $query; + } + + return parent::buildSavedQueryFromBuiltin($query_key); + } + + protected function renderResultList( + array $imports, + PhabricatorSavedQuery $query, + array $handles) { + + assert_instances_of($imports, 'PhabricatorCalendarImport'); + $viewer = $this->requireViewer(); + + $list = new PHUIObjectItemListView(); + foreach ($imports as $import) { + $item = id(new PHUIObjectItemView()) + ->setViewer($viewer) + ->setObjectName(pht('Import %d', $import->getID())) + ->setHeader($import->getName()) + ->setHref($import->getURI()); + + if ($import->getIsDisabled()) { + $item->setDisabled(true); + } + + $list->addItem($item); + } + + $result = new PhabricatorApplicationSearchResultView(); + $result->setObjectList($list); + $result->setNoDataString(pht('No imports found.')); + + return $result; + } +} diff --git a/src/applications/calendar/query/PhabricatorCalendarImportTransactionQuery.php b/src/applications/calendar/query/PhabricatorCalendarImportTransactionQuery.php new file mode 100644 index 0000000000..123ec9b3c4 --- /dev/null +++ b/src/applications/calendar/query/PhabricatorCalendarImportTransactionQuery.php @@ -0,0 +1,10 @@ +setAllDayDateTo(0) ->setStartDateTime($datetime_start) ->setEndDateTime($datetime_end) + ->attachImportSource(null) ->applyViewerTimezone($actor); } @@ -306,6 +314,14 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO $this->mailKey = Filesystem::readRandomCharacters(20); } + $import_uid = $this->getImportUID(); + if ($import_uid !== null) { + $index = PhabricatorHash::digestForIndex($import_uid); + } else { + $index = null; + } + $this->setImportUIDIndex($index); + $this->updateUTCEpochs(); return parent::save(); @@ -344,6 +360,11 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO 'utcUntilEpoch' => 'epoch?', 'utcInstanceEpoch' => 'epoch?', + 'importAuthorPHID' => 'phid?', + 'importSourcePHID' => 'phid?', + 'importUIDIndex' => 'bytes12?', + 'importUID' => 'text?', + // TODO: DEPRECATED. 'allDayDateFrom' => 'epoch', 'allDayDateTo' => 'epoch', @@ -885,6 +906,17 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO return $set; } + public function getImportSource() { + return $this->assertAttached($this->importSource); + } + + public function attachImportSource( + PhabricatorCalendarImport $import = null) { + $this->importSource = $import; + return $this; + } + + /* -( Markup Interface )--------------------------------------------------- */ @@ -947,11 +979,19 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO case PhabricatorPolicyCapability::CAN_VIEW: return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: - return $this->getEditPolicy(); + if ($this->getImportSource()) { + return PhabricatorPolicy::POLICY_NOONE; + } else { + return $this->getEditPolicy(); + } } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + if ($this->getImportSource()) { + return false; + } + // The host of an event can always view and edit it. $user_phid = $this->getHostPHID(); if ($user_phid) { @@ -974,12 +1014,40 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO } public function describeAutomaticCapability($capability) { + if ($this->getImportSource()) { + return pht( + 'Events imported from external sources can not be edited in '. + 'Phabricator.'); + } + return pht( 'The host of an event can always view and edit it. Users who are '. 'invited to an event can always view it.'); } +/* -( PhabricatorExtendedPolicyInterface )--------------------------------- */ + + + public function getExtendedPolicy($capability, PhabricatorUser $viewer) { + $extended = array(); + + switch ($capability) { + case PhabricatorPolicyCapability::CAN_VIEW: + $import_source = $this->getImportSource(); + if ($import_source) { + $extended[] = array( + $import_source, + PhabricatorPolicyCapability::CAN_VIEW, + ); + } + break; + } + + return $extended; + } + + /* -( PhabricatorApplicationTransactionInterface )------------------------- */ diff --git a/src/applications/calendar/storage/PhabricatorCalendarImport.php b/src/applications/calendar/storage/PhabricatorCalendarImport.php new file mode 100644 index 0000000000..d75d9fa66b --- /dev/null +++ b/src/applications/calendar/storage/PhabricatorCalendarImport.php @@ -0,0 +1,126 @@ +setAuthorPHID($actor->getPHID()) + ->setViewPolicy($actor->getPHID()) + ->setEditPolicy($actor->getPHID()) + ->setIsDisabled(0); + } + + protected function getConfiguration() { + return array( + self::CONFIG_AUX_PHID => true, + self::CONFIG_SERIALIZATION => array( + 'parameters' => self::SERIALIZATION_JSON, + ), + self::CONFIG_COLUMN_SCHEMA => array( + 'name' => 'text', + 'engineType' => 'text64', + 'isDisabled' => 'bool', + ), + self::CONFIG_KEY_SCHEMA => array( + 'key_author' => array( + 'columns' => array('authorPHID'), + ), + ), + ) + parent::getConfiguration(); + } + + public function getPHIDType() { + return PhabricatorCalendarImportPHIDType::TYPECONST; + } + + public function getURI() { + $id = $this->getID(); + return "/calendar/import/{$id}/"; + } + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + ); + } + + public function getPolicy($capability) { + switch ($capability) { + case PhabricatorPolicyCapability::CAN_VIEW: + return $this->getViewPolicy(); + case PhabricatorPolicyCapability::CAN_EDIT: + return $this->getEditPolicy(); + } + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return false; + } + + public function describeAutomaticCapability($capability) { + return null; + } + + +/* -( PhabricatorApplicationTransactionInterface )------------------------- */ + + + public function getApplicationTransactionEditor() { + return new PhabricatorCalendarImportEditor(); + } + + public function getApplicationTransactionObject() { + return $this; + } + + public function getApplicationTransactionTemplate() { + return new PhabricatorCalendarImportTransaction(); + } + + public function willRenderTimeline( + PhabricatorApplicationTransactionView $timeline, + AphrontRequest $request) { + return $timeline; + } + + +/* -( PhabricatorDestructibleInterface )----------------------------------- */ + + + public function destroyObjectPermanently( + PhabricatorDestructionEngine $engine) { + $viewer = $engine->getViewer(); + + $this->openTransaction(); + + $events = id(new PhabricatorCalendarEventQuery()) + ->withImportSourcePHIDs(array($this->getPHID())) + ->execute(); + foreach ($events as $event) { + $engine->destroyObject($event); + } + + $this->delete(); + $this->saveTransaction(); + } + +} diff --git a/src/applications/calendar/storage/PhabricatorCalendarImportTransaction.php b/src/applications/calendar/storage/PhabricatorCalendarImportTransaction.php new file mode 100644 index 0000000000..2102e94576 --- /dev/null +++ b/src/applications/calendar/storage/PhabricatorCalendarImportTransaction.php @@ -0,0 +1,18 @@ +getAuthorPHID()); } -/* -( PhabricatorDestructableInterface )----------------------------------- */ +/* -( PhabricatorDestructibleInterface )----------------------------------- */ public function destroyObjectPermanently( PhabricatorDestructionEngine $engine) { From 86a00ee4abd6a138b9151695f741216afe2fdc0c Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 12 Oct 2016 11:31:21 -0700 Subject: [PATCH 10/55] Make Calendar ICS imports sort of work in a crude, approximate way Summary: Ref T10747. This barely works, but can technically import some event data. Test Plan: Used import flow to import a ".ics" document. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10747 Differential Revision: https://secure.phabricator.com/D16699 --- src/__phutil_library_map__.php | 22 +++ .../AphrontFileHTTPParameterType.php | 60 ++++++++ ...habricatorCalendarImportEditController.php | 92 +++++++++++++ ...habricatorCalendarImportListController.php | 13 ++ ...habricatorCalendarImportViewController.php | 130 ++++++++++++++++++ .../PhabricatorCalendarImportEditEngine.php | 115 ++++++++++++++++ .../PhabricatorCalendarImportEditor.php | 24 ++++ .../PhabricatorCalendarICSImportEngine.php | 74 ++++++++++ .../PhabricatorCalendarImportEngine.php | 68 +++++++++ .../query/PhabricatorCalendarEventQuery.php | 4 +- .../query/PhabricatorCalendarImportQuery.php | 18 +++ .../PhabricatorCalendarImportSearchEngine.php | 2 +- .../storage/PhabricatorCalendarEvent.php | 63 ++++++++- .../storage/PhabricatorCalendarImport.php | 35 ++++- ...icatorCalendarImportDisableTransaction.php | 28 ++++ ...icatorCalendarImportICSFileTransaction.php | 80 +++++++++++ ...abricatorCalendarImportNameTransaction.php | 39 ++++++ ...abricatorCalendarImportTransactionType.php | 4 + .../editfield/PhabricatorFileEditField.php | 23 ++++ 19 files changed, 888 insertions(+), 6 deletions(-) create mode 100644 src/aphront/httpparametertype/AphrontFileHTTPParameterType.php create mode 100644 src/applications/calendar/controller/PhabricatorCalendarImportEditController.php create mode 100644 src/applications/calendar/controller/PhabricatorCalendarImportViewController.php create mode 100644 src/applications/calendar/editor/PhabricatorCalendarImportEditEngine.php create mode 100644 src/applications/calendar/import/PhabricatorCalendarICSImportEngine.php create mode 100644 src/applications/calendar/import/PhabricatorCalendarImportEngine.php create mode 100644 src/applications/calendar/xaction/PhabricatorCalendarImportDisableTransaction.php create mode 100644 src/applications/calendar/xaction/PhabricatorCalendarImportICSFileTransaction.php create mode 100644 src/applications/calendar/xaction/PhabricatorCalendarImportNameTransaction.php create mode 100644 src/applications/calendar/xaction/PhabricatorCalendarImportTransactionType.php create mode 100644 src/applications/transactions/editfield/PhabricatorFileEditField.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 4eb1dc5a6b..e5b770a89f 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -140,6 +140,7 @@ phutil_register_library_map(array( 'AphrontDialogView' => 'view/AphrontDialogView.php', 'AphrontEpochHTTPParameterType' => 'aphront/httpparametertype/AphrontEpochHTTPParameterType.php', 'AphrontException' => 'aphront/exception/AphrontException.php', + 'AphrontFileHTTPParameterType' => 'aphront/httpparametertype/AphrontFileHTTPParameterType.php', 'AphrontFileResponse' => 'aphront/response/AphrontFileResponse.php', 'AphrontFormCheckboxControl' => 'view/form/control/AphrontFormCheckboxControl.php', 'AphrontFormControl' => 'view/form/control/AphrontFormControl.php', @@ -2098,16 +2099,25 @@ phutil_register_library_map(array( 'PhabricatorCalendarExportViewController' => 'applications/calendar/controller/PhabricatorCalendarExportViewController.php', 'PhabricatorCalendarHoliday' => 'applications/calendar/storage/PhabricatorCalendarHoliday.php', 'PhabricatorCalendarHolidayTestCase' => 'applications/calendar/storage/__tests__/PhabricatorCalendarHolidayTestCase.php', + 'PhabricatorCalendarICSImportEngine' => 'applications/calendar/import/PhabricatorCalendarICSImportEngine.php', 'PhabricatorCalendarICSWriter' => 'applications/calendar/util/PhabricatorCalendarICSWriter.php', 'PhabricatorCalendarIconSet' => 'applications/calendar/icon/PhabricatorCalendarIconSet.php', 'PhabricatorCalendarImport' => 'applications/calendar/storage/PhabricatorCalendarImport.php', + 'PhabricatorCalendarImportDisableTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportDisableTransaction.php', + 'PhabricatorCalendarImportEditController' => 'applications/calendar/controller/PhabricatorCalendarImportEditController.php', + 'PhabricatorCalendarImportEditEngine' => 'applications/calendar/editor/PhabricatorCalendarImportEditEngine.php', 'PhabricatorCalendarImportEditor' => 'applications/calendar/editor/PhabricatorCalendarImportEditor.php', + 'PhabricatorCalendarImportEngine' => 'applications/calendar/import/PhabricatorCalendarImportEngine.php', + 'PhabricatorCalendarImportICSFileTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportICSFileTransaction.php', 'PhabricatorCalendarImportListController' => 'applications/calendar/controller/PhabricatorCalendarImportListController.php', + 'PhabricatorCalendarImportNameTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportNameTransaction.php', 'PhabricatorCalendarImportPHIDType' => 'applications/calendar/phid/PhabricatorCalendarImportPHIDType.php', 'PhabricatorCalendarImportQuery' => 'applications/calendar/query/PhabricatorCalendarImportQuery.php', 'PhabricatorCalendarImportSearchEngine' => 'applications/calendar/query/PhabricatorCalendarImportSearchEngine.php', 'PhabricatorCalendarImportTransaction' => 'applications/calendar/storage/PhabricatorCalendarImportTransaction.php', 'PhabricatorCalendarImportTransactionQuery' => 'applications/calendar/query/PhabricatorCalendarImportTransactionQuery.php', + 'PhabricatorCalendarImportTransactionType' => 'applications/calendar/xaction/PhabricatorCalendarImportTransactionType.php', + 'PhabricatorCalendarImportViewController' => 'applications/calendar/controller/PhabricatorCalendarImportViewController.php', 'PhabricatorCalendarRemarkupRule' => 'applications/calendar/remarkup/PhabricatorCalendarRemarkupRule.php', 'PhabricatorCalendarReplyHandler' => 'applications/calendar/mail/PhabricatorCalendarReplyHandler.php', 'PhabricatorCalendarSchemaSpec' => 'applications/calendar/storage/PhabricatorCalendarSchemaSpec.php', @@ -2582,6 +2592,7 @@ phutil_register_library_map(array( 'PhabricatorFileDeleteController' => 'applications/files/controller/PhabricatorFileDeleteController.php', 'PhabricatorFileDropUploadController' => 'applications/files/controller/PhabricatorFileDropUploadController.php', 'PhabricatorFileEditController' => 'applications/files/controller/PhabricatorFileEditController.php', + 'PhabricatorFileEditField' => 'applications/transactions/editfield/PhabricatorFileEditField.php', 'PhabricatorFileEditor' => 'applications/files/editor/PhabricatorFileEditor.php', 'PhabricatorFileExternalRequest' => 'applications/files/storage/PhabricatorFileExternalRequest.php', 'PhabricatorFileExternalRequestGarbageCollector' => 'applications/files/garbagecollector/PhabricatorFileExternalRequestGarbageCollector.php', @@ -4638,6 +4649,7 @@ phutil_register_library_map(array( ), 'AphrontEpochHTTPParameterType' => 'AphrontHTTPParameterType', 'AphrontException' => 'Exception', + 'AphrontFileHTTPParameterType' => 'AphrontHTTPParameterType', 'AphrontFileResponse' => 'AphrontResponse', 'AphrontFormCheckboxControl' => 'AphrontFormControl', 'AphrontFormControl' => 'AphrontView', @@ -6886,6 +6898,7 @@ phutil_register_library_map(array( 'PhabricatorCalendarExportViewController' => 'PhabricatorCalendarController', 'PhabricatorCalendarHoliday' => 'PhabricatorCalendarDAO', 'PhabricatorCalendarHolidayTestCase' => 'PhabricatorTestCase', + 'PhabricatorCalendarICSImportEngine' => 'PhabricatorCalendarImportEngine', 'PhabricatorCalendarICSWriter' => 'Phobject', 'PhabricatorCalendarIconSet' => 'PhabricatorIconSet', 'PhabricatorCalendarImport' => array( @@ -6894,13 +6907,21 @@ phutil_register_library_map(array( 'PhabricatorApplicationTransactionInterface', 'PhabricatorDestructibleInterface', ), + 'PhabricatorCalendarImportDisableTransaction' => 'PhabricatorCalendarImportTransactionType', + 'PhabricatorCalendarImportEditController' => 'PhabricatorCalendarController', + 'PhabricatorCalendarImportEditEngine' => 'PhabricatorEditEngine', 'PhabricatorCalendarImportEditor' => 'PhabricatorApplicationTransactionEditor', + 'PhabricatorCalendarImportEngine' => 'Phobject', + 'PhabricatorCalendarImportICSFileTransaction' => 'PhabricatorCalendarImportTransactionType', 'PhabricatorCalendarImportListController' => 'PhabricatorCalendarController', + 'PhabricatorCalendarImportNameTransaction' => 'PhabricatorCalendarImportTransactionType', 'PhabricatorCalendarImportPHIDType' => 'PhabricatorPHIDType', 'PhabricatorCalendarImportQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorCalendarImportSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorCalendarImportTransaction' => 'PhabricatorModularTransaction', 'PhabricatorCalendarImportTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'PhabricatorCalendarImportTransactionType' => 'PhabricatorModularTransactionType', + 'PhabricatorCalendarImportViewController' => 'PhabricatorCalendarController', 'PhabricatorCalendarRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'PhabricatorCalendarReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', 'PhabricatorCalendarSchemaSpec' => 'PhabricatorConfigSchemaSpec', @@ -7448,6 +7469,7 @@ phutil_register_library_map(array( 'PhabricatorFileDeleteController' => 'PhabricatorFileController', 'PhabricatorFileDropUploadController' => 'PhabricatorFileController', 'PhabricatorFileEditController' => 'PhabricatorFileController', + 'PhabricatorFileEditField' => 'PhabricatorEditField', 'PhabricatorFileEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorFileExternalRequest' => array( 'PhabricatorFileDAO', diff --git a/src/aphront/httpparametertype/AphrontFileHTTPParameterType.php b/src/aphront/httpparametertype/AphrontFileHTTPParameterType.php new file mode 100644 index 0000000000..ceff8c1ddb --- /dev/null +++ b/src/aphront/httpparametertype/AphrontFileHTTPParameterType.php @@ -0,0 +1,60 @@ +getFileKey($key); + return $request->getExists($key) || + $request->getFileExists($file_key); + } + + protected function getParameterValue(AphrontRequest $request, $key) { + $value = $request->getStrList($key); + if ($value) { + return head($value); + } + + // NOTE: At least for now, we'll attempt to read a direct upload if we + // miss on a PHID. Currently, PHUIFormFileControl does a client-side + // upload on workflow forms (which is good) but doesn't have a hook for + // non-workflow forms (which isn't as good). Giving it a hook is desirable, + // but complicated. Even if we do hook it, it may be reasonable to keep + // this code around as a fallback if the client-side JS goes awry. + + $file_key = $this->getFileKey($key); + if (!$request->getFileExists($file_key)) { + return null; + } + + $viewer = $this->getViewer(); + $file = PhabricatorFile::newFromPHPUpload( + idx($_FILES, $file_key), + array( + 'authorPHID' => $viewer->getPHID(), + 'viewPolicy' => PhabricatorPolicies::POLICY_NOONE, + )); + return $file->getPHID(); + } + + protected function getParameterTypeName() { + return 'file'; + } + + protected function getParameterFormatDescriptions() { + return array( + pht('A file PHID.'), + ); + } + + protected function getParameterExamples() { + return array( + 'v=PHID-FILE-wxyz', + ); + } + +} diff --git a/src/applications/calendar/controller/PhabricatorCalendarImportEditController.php b/src/applications/calendar/controller/PhabricatorCalendarImportEditController.php new file mode 100644 index 0000000000..0af9fa1540 --- /dev/null +++ b/src/applications/calendar/controller/PhabricatorCalendarImportEditController.php @@ -0,0 +1,92 @@ +setController($this); + + $id = $request->getURIData('id'); + if (!$id) { + $list_uri = $this->getApplicationURI('import/'); + + $import_type = $request->getStr('importType'); + $import_engines = PhabricatorCalendarImportEngine::getAllImportEngines(); + if (empty($import_engines[$import_type])) { + return $this->buildEngineTypeResponse($list_uri); + } + + $import_engine = $import_engines[$import_type]; + + $engine + ->addContextParameter('importType', $import_type) + ->setImportEngine($import_engine); + } + + return $engine->buildResponse(); + } + + private function buildEngineTypeResponse($cancel_uri) { + $import_engines = PhabricatorCalendarImportEngine::getAllImportEngines(); + + $request = $this->getRequest(); + $viewer = $this->getViewer(); + + $e_import = null; + $errors = array(); + if ($request->isFormPost()) { + $e_import = pht('Required'); + $errors[] = pht( + 'To import events, you must select a source to import from.'); + } + + $type_control = id(new AphrontFormRadioButtonControl()) + ->setLabel(pht('Import Type')) + ->setName('importType') + ->setError($e_import); + + foreach ($import_engines as $import_engine) { + $type_control->addButton( + $import_engine->getImportEngineType(), + $import_engine->getImportEngineName(), + $import_engine->getImportEngineHint()); + } + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb(pht('New Import')); + $crumbs->setBorder(true); + + $title = pht('Choose Import Type'); + $header = id(new PHUIHeaderView()) + ->setHeader(pht('New Import')) + ->setHeaderIcon('fa-upload'); + + $form = id(new AphrontFormView()) + ->setUser($viewer) + ->appendChild($type_control) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue(pht('Continue')) + ->addCancelButton($cancel_uri)); + + $box = id(new PHUIObjectBoxView()) + ->setFormErrors($errors) + ->setHeaderText(pht('Import')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setForm($form); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setFooter( + array( + $box, + )); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->appendChild($view); + } + +} diff --git a/src/applications/calendar/controller/PhabricatorCalendarImportListController.php b/src/applications/calendar/controller/PhabricatorCalendarImportListController.php index c5ac5cd3e8..df78ea2445 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarImportListController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarImportListController.php @@ -9,4 +9,17 @@ final class PhabricatorCalendarImportListController ->buildResponse(); } + protected function buildApplicationCrumbs() { + $crumbs = parent::buildApplicationCrumbs(); + + $crumbs->addAction( + id(new PHUIListItemView()) + ->setName(pht('Import Events')) + ->setHref($this->getApplicationURI('import/edit/')) + ->setIcon('fa-upload')); + + return $crumbs; + } + + } diff --git a/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php b/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php new file mode 100644 index 0000000000..e2be04050d --- /dev/null +++ b/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php @@ -0,0 +1,130 @@ +getViewer(); + + $import = id(new PhabricatorCalendarImportQuery()) + ->setViewer($viewer) + ->withIDs(array($request->getURIData('id'))) + ->executeOne(); + if (!$import) { + return new Aphront404Response(); + } + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb( + pht('Imports'), + '/calendar/import/'); + $crumbs->addTextCrumb(pht('Import %d', $import->getID())); + $crumbs->setBorder(true); + + $timeline = $this->buildTransactionTimeline( + $import, + new PhabricatorCalendarImportTransactionQuery()); + $timeline->setShouldTerminate(true); + + $header = $this->buildHeaderView($import); + $curtain = $this->buildCurtain($import); + $details = $this->buildPropertySection($import); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setMainColumn( + array( + $timeline, + )) + ->setCurtain($curtain) + ->addPropertySection(pht('Details'), $details); + + $page_title = pht( + 'Import %d %s', + $import->getID(), + $import->getDisplayName()); + + return $this->newPage() + ->setTitle($page_title) + ->setCrumbs($crumbs) + ->setPageObjectPHIDs(array($import->getPHID())) + ->appendChild($view); + } + + private function buildHeaderView( + PhabricatorCalendarImport $import) { + $viewer = $this->getViewer(); + $id = $import->getID(); + + if ($import->getIsDisabled()) { + $icon = 'fa-ban'; + $color = 'red'; + $status = pht('Disabled'); + } else { + $icon = 'fa-check'; + $color = 'bluegrey'; + $status = pht('Active'); + } + + $header = id(new PHUIHeaderView()) + ->setViewer($viewer) + ->setHeader($import->getDisplayName()) + ->setStatus($icon, $color, $status) + ->setPolicyObject($import); + + return $header; + } + + private function buildCurtain(PhabricatorCalendarImport $import) { + $viewer = $this->getViewer(); + $id = $import->getID(); + + $curtain = $this->newCurtainView($import); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $import, + PhabricatorPolicyCapability::CAN_EDIT); + + $edit_uri = "import/edit/{$id}/"; + $edit_uri = $this->getApplicationURI($edit_uri); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit Import')) + ->setIcon('fa-pencil') + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit) + ->setHref($edit_uri)); + + $disable_uri = "import/disable/{$id}/"; + $disable_uri = $this->getApplicationURI($disable_uri); + if ($import->getIsDisabled()) { + $disable_name = pht('Enable Import'); + $disable_icon = 'fa-check'; + } else { + $disable_name = pht('Disable Import'); + $disable_icon = 'fa-ban'; + } + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName($disable_name) + ->setIcon($disable_icon) + ->setDisabled(!$can_edit) + ->setWorkflow(true) + ->setHref($disable_uri)); + + return $curtain; + } + + private function buildPropertySection( + PhabricatorCalendarImport $import) { + $viewer = $this->getViewer(); + + $properties = id(new PHUIPropertyListView()) + ->setViewer($viewer); + + return $properties; + } +} diff --git a/src/applications/calendar/editor/PhabricatorCalendarImportEditEngine.php b/src/applications/calendar/editor/PhabricatorCalendarImportEditEngine.php new file mode 100644 index 0000000000..031a890d59 --- /dev/null +++ b/src/applications/calendar/editor/PhabricatorCalendarImportEditEngine.php @@ -0,0 +1,115 @@ +importEngine = $engine; + return $this; + } + + public function getImportEngine() { + return $this->importEngine; + } + + public function getEngineName() { + return pht('Calendar Imports'); + } + + public function isEngineConfigurable() { + return false; + } + + public function getSummaryHeader() { + return pht('Configure Calendar Import Forms'); + } + + public function getSummaryText() { + return pht('Configure how users create and edit imports.'); + } + + public function getEngineApplicationClass() { + return 'PhabricatorCalendarApplication'; + } + + protected function newEditableObject() { + $viewer = $this->getViewer(); + $engine = $this->getImportEngine(); + + return PhabricatorCalendarImport::initializeNewCalendarImport( + $viewer, + $engine); + } + + protected function newObjectQuery() { + return new PhabricatorCalendarImportQuery(); + } + + protected function getObjectCreateTitleText($object) { + return pht('Create New Import'); + } + + protected function getObjectEditTitleText($object) { + return pht('Edit Import: %s', $object->getDisplayName()); + } + + protected function getObjectEditShortText($object) { + return pht('Import %d', $object->getID()); + } + + protected function getObjectCreateShortText() { + return pht('Create Import'); + } + + protected function getObjectName() { + return pht('Import'); + } + + protected function getObjectViewURI($object) { + return $object->getURI(); + } + + protected function getEditorURI() { + return $this->getApplication()->getApplicationURI('import/edit/'); + } + + protected function buildCustomEditFields($object) { + $viewer = $this->getViewer(); + + $fields = array( + id(new PhabricatorTextEditField()) + ->setKey('name') + ->setLabel(pht('Name')) + ->setDescription(pht('Name of the import.')) + ->setTransactionType( + PhabricatorCalendarImportNameTransaction::TRANSACTIONTYPE) + ->setConduitDescription(pht('Rename the import.')) + ->setConduitTypeDescription(pht('New import name.')) + ->setValue($object->getName()), + id(new PhabricatorBoolEditField()) + ->setKey('disabled') + ->setOptions(pht('Active'), pht('Disabled')) + ->setLabel(pht('Disabled')) + ->setDescription(pht('Disable the import.')) + ->setTransactionType( + PhabricatorCalendarImportDisableTransaction::TRANSACTIONTYPE) + ->setIsConduitOnly(true) + ->setConduitDescription(pht('Disable or restore the import.')) + ->setConduitTypeDescription(pht('True to cancel the import.')) + ->setValue($object->getIsDisabled()), + ); + + $import_engine = $object->getEngine(); + foreach ($import_engine->newEditEngineFields($this, $object) as $field) { + $fields[] = $field; + } + + return $fields; + } + + +} diff --git a/src/applications/calendar/editor/PhabricatorCalendarImportEditor.php b/src/applications/calendar/editor/PhabricatorCalendarImportEditor.php index 52bc5c5ff2..fe22a9f8c9 100644 --- a/src/applications/calendar/editor/PhabricatorCalendarImportEditor.php +++ b/src/applications/calendar/editor/PhabricatorCalendarImportEditor.php @@ -15,4 +15,28 @@ final class PhabricatorCalendarImportEditor return pht('%s created this import.', $author); } + public function getTransactionTypes() { + $types = parent::getTransactionTypes(); + + $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; + $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; + + return $types; + } + + protected function applyFinalEffects( + PhabricatorLiskDAO $object, + array $xactions) { + + if ($this->getIsNewObject()) { + $actor = $this->getActor(); + + $import_engine = $object->getEngine(); + $import_engine->didCreateImport($actor, $object); + } + + return $xactions; + } + + } diff --git a/src/applications/calendar/import/PhabricatorCalendarICSImportEngine.php b/src/applications/calendar/import/PhabricatorCalendarICSImportEngine.php new file mode 100644 index 0000000000..1b2f120b4c --- /dev/null +++ b/src/applications/calendar/import/PhabricatorCalendarICSImportEngine.php @@ -0,0 +1,74 @@ +getIsCreate()) { + $fields[] = id(new PhabricatorFileEditField()) + ->setKey('icsFilePHID') + ->setLabel(pht('ICS File')) + ->setDescription(pht('ICS file to import.')) + ->setTransactionType( + PhabricatorCalendarImportICSFileTransaction::TRANSACTIONTYPE) + ->setConduitDescription(pht('File PHID to import.')) + ->setConduitTypeDescription(pht('File PHID.')); + } + + return $fields; + } + + public function getDisplayName(PhabricatorCalendarImport $import) { + $filename_key = PhabricatorCalendarImportICSFileTransaction::PARAMKEY_NAME; + $filename = $import->getParameter($filename_key); + if (strlen($filename)) { + return pht('ICS File "%s"', $filename); + } else { + return pht('ICS File'); + } + } + + public function didCreateImport( + PhabricatorUser $viewer, + PhabricatorCalendarImport $import) { + + $phid_key = PhabricatorCalendarImportICSFileTransaction::PARAMKEY_FILE; + $file_phid = $import->getParameter($phid_key); + + $file = id(new PhabricatorFileQuery()) + ->setViewer($viewer) + ->withPHIDs(array($file_phid)) + ->executeOne(); + if (!$file) { + throw new Exception( + pht( + 'Unable to load file ("%s") for import.', + $file_phid)); + } + + $data = $file->loadFileData(); + + $parser = id(new PhutilICSParser()); + + $document = $parser->parseICSData($data); + + return $this->importEventDocument($viewer, $import, $document); + } + + + +} diff --git a/src/applications/calendar/import/PhabricatorCalendarImportEngine.php b/src/applications/calendar/import/PhabricatorCalendarImportEngine.php new file mode 100644 index 0000000000..da7602e18f --- /dev/null +++ b/src/applications/calendar/import/PhabricatorCalendarImportEngine.php @@ -0,0 +1,68 @@ +getPhobjectClassConstant('ENGINETYPE', 64); + } + + + abstract public function getImportEngineName(); + abstract public function getImportEngineHint(); + + abstract public function newEditEngineFields( + PhabricatorEditEngine $engine, + PhabricatorCalendarImport $import); + + abstract public function getDisplayName(PhabricatorCalendarImport $import); + + abstract public function didCreateImport( + PhabricatorUser $viewer, + PhabricatorCalendarImport $import); + + final public static function getAllImportEngines() { + return id(new PhutilClassMapQuery()) + ->setAncestorClass(__CLASS__) + ->setUniqueMethod('getImportEngineType') + ->setSortMethod('getImportEngineName') + ->execute(); + } + + final protected function importEventDocument( + PhabricatorUser $viewer, + PhabricatorCalendarImport $import, + PhutilCalendarRootNode $root) { + + $event_type = PhutilCalendarEventNode::NODETYPE; + + $events = array(); + foreach ($root->getChildren() as $document) { + foreach ($document->getChildren() as $node) { + if ($node->getNodeType() != $event_type) { + // TODO: Warn that we ignored this. + continue; + } + + $event = PhabricatorCalendarEvent::newFromDocumentNode($viewer, $node); + + $event + ->setImportAuthorPHID($viewer->getPHID()) + ->setImportSourcePHID($import->getPHID()) + ->attachImportSource($import); + + $events[] = $event; + } + } + + // TODO: Use transactions. + // TODO: Update existing events instead of fataling. + foreach ($events as $event) { + $event->save(); + } + + } + + + +} diff --git a/src/applications/calendar/query/PhabricatorCalendarEventQuery.php b/src/applications/calendar/query/PhabricatorCalendarEventQuery.php index e7147d6ef8..1bcdefebfd 100644 --- a/src/applications/calendar/query/PhabricatorCalendarEventQuery.php +++ b/src/applications/calendar/query/PhabricatorCalendarEventQuery.php @@ -468,9 +468,9 @@ final class PhabricatorCalendarEventQuery ->setViewer($viewer) ->withPHIDs($import_phids) ->execute(); - $sources = mpull($sources, null, 'getPHID'); + $imports = mpull($imports, null, 'getPHID'); } else { - $sources = array(); + $imports = array(); } foreach ($events as $key => $event) { diff --git a/src/applications/calendar/query/PhabricatorCalendarImportQuery.php b/src/applications/calendar/query/PhabricatorCalendarImportQuery.php index 9ed6791dfe..5d44657994 100644 --- a/src/applications/calendar/query/PhabricatorCalendarImportQuery.php +++ b/src/applications/calendar/query/PhabricatorCalendarImportQuery.php @@ -70,6 +70,24 @@ final class PhabricatorCalendarImportQuery return $where; } + protected function willFilterPage(array $page) { + $engines = PhabricatorCalendarImportEngine::getAllImportEngines(); + foreach ($page as $key => $import) { + $engine_type = $import->getEngineType(); + $engine = idx($engines, $engine_type); + + if (!$engine) { + unset($page[$key]); + $this->didRejectResult($import); + continue; + } + + $import->attachEngine(clone $engine); + } + + return $page; + } + protected function getPrimaryTableAlias() { return 'import'; } diff --git a/src/applications/calendar/query/PhabricatorCalendarImportSearchEngine.php b/src/applications/calendar/query/PhabricatorCalendarImportSearchEngine.php index deeefbd478..75252b6dac 100644 --- a/src/applications/calendar/query/PhabricatorCalendarImportSearchEngine.php +++ b/src/applications/calendar/query/PhabricatorCalendarImportSearchEngine.php @@ -62,7 +62,7 @@ final class PhabricatorCalendarImportSearchEngine $item = id(new PHUIObjectItemView()) ->setViewer($viewer) ->setObjectName(pht('Import %d', $import->getID())) - ->setHeader($import->getName()) + ->setHeader($import->getDisplayName()) ->setHref($import->getURI()); if ($import->getIsDisabled()) { diff --git a/src/applications/calendar/storage/PhabricatorCalendarEvent.php b/src/applications/calendar/storage/PhabricatorCalendarEvent.php index 1fda3a19b8..61d0b5c25d 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarEvent.php +++ b/src/applications/calendar/storage/PhabricatorCalendarEvent.php @@ -81,6 +81,7 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO $datetime_end = $datetime_start->newRelativeDateTime('PT1H'); return id(new PhabricatorCalendarEvent()) + ->setDescription('') ->setHostPHID($actor->getPHID()) ->setIsCancelled(0) ->setIsAllDay(0) @@ -101,6 +102,66 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO ->applyViewerTimezone($actor); } + public static function newFromDocumentNode( + PhabricatorUser $actor, + PhutilCalendarEventNode $node) { + $timezone = $actor->getTimezoneIdentifier(); + + $uid = $node->getUID(); + + $name = $node->getName(); + if (!strlen($name)) { + if (strlen($uid)) { + $name = pht('Unnamed Event "%s"', $node->getUID()); + } else { + $name = pht('Unnamed Imported Event'); + } + } + + $description = $node->getDescription(); + + $instance_iso = $node->getRecurrenceID(); + if (strlen($instance_iso)) { + $instance_datetime = PhutilCalendarAbsoluteDateTime::newFromISO8601( + $instance_iso); + $instance_epoch = $instance_datetime->getEpoch(); + } else { + $instance_epoch = null; + } + $full_uid = $uid.'/'.$instance_epoch; + + $start_datetime = $node->getStartDateTime() + ->setViewerTimezone($timezone); + $end_datetime = $node->getEndDateTime() + ->setViewerTimezone($timezone); + + $rrule = $node->getRecurrenceRule(); + + $event = self::initializeNewCalendarEvent($actor) + ->setName($name) + ->setStartDateTime($start_datetime) + ->setEndDateTime($end_datetime) + ->setImportUID($full_uid) + ->setUTCInstanceEpoch($instance_epoch); + + if (strlen($description)) { + $event->setDescription($description); + } + + if ($rrule) { + $event->setRecurrenceRule($rrule); + $event->setIsRecurring(1); + + $until_datetime = $rrule->getUntil() + ->setViewerTimezone($timezone); + if ($until_datetime) { + $event->setUntilDateTime($until_datetime); + } + } + + return $event; + } + private function newChild( PhabricatorUser $actor, $sequence, @@ -980,7 +1041,7 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO return $this->getViewPolicy(); case PhabricatorPolicyCapability::CAN_EDIT: if ($this->getImportSource()) { - return PhabricatorPolicy::POLICY_NOONE; + return PhabricatorPolicies::POLICY_NOONE; } else { return $this->getEditPolicy(); } diff --git a/src/applications/calendar/storage/PhabricatorCalendarImport.php b/src/applications/calendar/storage/PhabricatorCalendarImport.php index d75d9fa66b..18c113a5c6 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarImport.php +++ b/src/applications/calendar/storage/PhabricatorCalendarImport.php @@ -17,12 +17,16 @@ final class PhabricatorCalendarImport private $engine = self::ATTACHABLE; - public static function initializeNewCalendarImport(PhabricatorUser $actor) { + public static function initializeNewCalendarImport( + PhabricatorUser $actor, + PhabricatorCalendarImportEngine $engine) { return id(new self()) ->setAuthorPHID($actor->getPHID()) ->setViewPolicy($actor->getPHID()) ->setEditPolicy($actor->getPHID()) - ->setIsDisabled(0); + ->setIsDisabled(0) + ->setEngineType($engine->getImportEngineType()) + ->attachEngine($engine); } protected function getConfiguration() { @@ -53,6 +57,33 @@ final class PhabricatorCalendarImport return "/calendar/import/{$id}/"; } + public function attachEngine(PhabricatorCalendarImportEngine $engine) { + $this->engine = $engine; + return $this; + } + + public function getEngine() { + return $this->assertAttached($this->engine); + } + + public function getParameter($key, $default = null) { + return idx($this->parameters, $key, $default); + } + + public function setParameter($key, $value) { + $this->parameters[$key] = $value; + return $this; + } + + public function getDisplayName() { + $name = $this->getName(); + if (strlen($name)) { + return $name; + } + + return $this->getEngine()->getDisplayName($this); + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/calendar/xaction/PhabricatorCalendarImportDisableTransaction.php b/src/applications/calendar/xaction/PhabricatorCalendarImportDisableTransaction.php new file mode 100644 index 0000000000..9cc23ab33f --- /dev/null +++ b/src/applications/calendar/xaction/PhabricatorCalendarImportDisableTransaction.php @@ -0,0 +1,28 @@ +getIsDisabled(); + } + + public function applyInternalEffects($object, $value) { + $object->setIsDisabled((int)$value); + } + + public function getTitle() { + if ($this->getNewValue()) { + return pht( + '%s disabled this import.', + $this->renderAuthor()); + } else { + return pht( + '%s enabled this import.', + $this->renderAuthor()); + } + } + +} diff --git a/src/applications/calendar/xaction/PhabricatorCalendarImportICSFileTransaction.php b/src/applications/calendar/xaction/PhabricatorCalendarImportICSFileTransaction.php new file mode 100644 index 0000000000..59cd91053f --- /dev/null +++ b/src/applications/calendar/xaction/PhabricatorCalendarImportICSFileTransaction.php @@ -0,0 +1,80 @@ +getParameter(self::PARAMKEY_FILE); + } + + public function applyInternalEffects($object, $value) { + $object->setParameter(self::PARAMKEY_FILE, $value); + + $viewer = $this->getActor(); + $file = id(new PhabricatorFileQuery()) + ->setViewer($viewer) + ->withPHIDs(array($value)) + ->executeOne(); + if ($file) { + $object->setParameter(self::PARAMKEY_NAME, $file->getName()); + } + } + + public function getTitle() { + return pht( + '%s imported an ICS file.', + $this->renderAuthor()); + } + + public function validateTransactions($object, array $xactions) { + $viewer = $this->getActor(); + $errors = array(); + + $ics_type = PhabricatorCalendarICSImportEngine::ENGINETYPE; + $import_type = $object->getEngine()->getImportEngineType(); + if ($import_type != $ics_type) { + if (!$xactions) { + return $errors; + } + + $errors[] = $this->newInvalidError( + pht( + 'You can not attach an ICS file to an import type other than '. + 'an ICS import (type is "%s").', + $import_type)); + + return $errors; + } + + $new_value = $object->getParameter(self::PARAMKEY_FILE); + foreach ($xactions as $xaction) { + $new_value = $xaction->getNewValue(); + if (!strlen($new_value)) { + continue; + } + + $file = id(new PhabricatorFileQuery()) + ->setViewer($viewer) + ->withPHIDs(array($new_value)) + ->executeOne(); + if (!$file) { + $errors[] = $this->newInvalidError( + pht( + 'File PHID "%s" is not valid or not visible.', + $new_value), + $xaction); + } + } + + if (!$new_value) { + $errors[] = $this->newRequiredError( + pht('You must select an ".ics" file to import.')); + } + + return $errors; + } +} diff --git a/src/applications/calendar/xaction/PhabricatorCalendarImportNameTransaction.php b/src/applications/calendar/xaction/PhabricatorCalendarImportNameTransaction.php new file mode 100644 index 0000000000..49705af009 --- /dev/null +++ b/src/applications/calendar/xaction/PhabricatorCalendarImportNameTransaction.php @@ -0,0 +1,39 @@ +getName(); + } + + public function applyInternalEffects($object, $value) { + $object->setName($value); + } + + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + if (!strlen($old)) { + return pht( + '%s named this import %s.', + $this->renderAuthor(), + $this->renderNewValue()); + } else if (!strlen($new)) { + return pht( + '%s removed the name of this import (was: %s).', + $this->renderAuthor(), + $this->renderOldValue()); + } else { + return pht( + '%s renamed this import from %s to %s.', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } + } + +} diff --git a/src/applications/calendar/xaction/PhabricatorCalendarImportTransactionType.php b/src/applications/calendar/xaction/PhabricatorCalendarImportTransactionType.php new file mode 100644 index 0000000000..9659ce7759 --- /dev/null +++ b/src/applications/calendar/xaction/PhabricatorCalendarImportTransactionType.php @@ -0,0 +1,4 @@ +setEncType('multipart/form-data'); + return parent::appendToForm($form); + } + +} From d3487b6371cff190a46623b15497d493d186e2c5 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Wed, 12 Oct 2016 18:41:09 -0700 Subject: [PATCH 11/55] Remove unused drag and drop Conpherence code Summary: Ref T11730. Removes unused code since this is now it's own page. Test Plan: rebuild maps, grep for javelin code, classnames Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Maniphest Tasks: T11730 Differential Revision: https://secure.phabricator.com/D16700 --- resources/celerity/map.php | 11 +---- resources/celerity/packages.php | 1 - src/__phutil_library_map__.php | 2 - ...onpherenceFormDragAndDropUploadControl.php | 40 ------------------- .../behavior-drag-and-drop-photo.js | 38 ------------------ 5 files changed, 1 insertion(+), 91 deletions(-) delete mode 100644 src/applications/conpherence/view/ConpherenceFormDragAndDropUploadControl.php delete mode 100644 webroot/rsrc/js/application/conpherence/behavior-drag-and-drop-photo.js diff --git a/resources/celerity/map.php b/resources/celerity/map.php index bb082d5a78..46db1cb4f4 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -8,7 +8,7 @@ return array( 'names' => array( 'conpherence.pkg.css' => '4601645d', - 'conpherence.pkg.js' => '11f3e07e', + 'conpherence.pkg.js' => '44dd69f5', 'core.pkg.css' => '7ca260a3', 'core.pkg.js' => '30185d95', 'darkconsole.pkg.js' => 'e7393ebb', @@ -437,7 +437,6 @@ return array( 'rsrc/js/application/calendar/behavior-recurring-edit.js' => '5f1c4d5f', 'rsrc/js/application/config/behavior-reorder-fields.js' => 'b6993408', 'rsrc/js/application/conpherence/ConpherenceThreadManager.js' => '01774ab2', - 'rsrc/js/application/conpherence/behavior-drag-and-drop-photo.js' => 'cf86d16a', 'rsrc/js/application/conpherence/behavior-durable-column.js' => 'c5238acb', 'rsrc/js/application/conpherence/behavior-menu.js' => '9eb55204', 'rsrc/js/application/conpherence/behavior-participant-pane.js' => '8604caa8', @@ -665,7 +664,6 @@ return array( 'javelin-behavior-choose-control' => '327a00d1', 'javelin-behavior-comment-actions' => '0300eae6', 'javelin-behavior-config-reorder-fields' => 'b6993408', - 'javelin-behavior-conpherence-drag-and-drop-photo' => 'cf86d16a', 'javelin-behavior-conpherence-menu' => '9eb55204', 'javelin-behavior-conpherence-participant-pane' => '8604caa8', 'javelin-behavior-conpherence-pontificate' => 'f2e58483', @@ -2010,12 +2008,6 @@ return array( 'javelin-util', 'phabricator-notification-css', ), - 'cf86d16a' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-workflow', - 'phabricator-drag-and-drop-file-upload', - ), 'cfd23f37' => array( 'javelin-install', 'javelin-util', @@ -2305,7 +2297,6 @@ return array( 'conpherence-header-pane-css', ), 'conpherence.pkg.js' => array( - 'javelin-behavior-conpherence-drag-and-drop-photo', 'javelin-behavior-conpherence-menu', 'javelin-behavior-conpherence-participant-pane', 'javelin-behavior-conpherence-pontificate', diff --git a/resources/celerity/packages.php b/resources/celerity/packages.php index a3a2a6132a..e10ba48d28 100644 --- a/resources/celerity/packages.php +++ b/resources/celerity/packages.php @@ -162,7 +162,6 @@ return array( 'conpherence-header-pane-css', ), 'conpherence.pkg.js' => array( - 'javelin-behavior-conpherence-drag-and-drop-photo', 'javelin-behavior-conpherence-menu', 'javelin-behavior-conpherence-participant-pane', 'javelin-behavior-conpherence-pontificate', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index e5b770a89f..49a3e000cf 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -291,7 +291,6 @@ phutil_register_library_map(array( 'ConpherenceDAO' => 'applications/conpherence/storage/ConpherenceDAO.php', 'ConpherenceDurableColumnView' => 'applications/conpherence/view/ConpherenceDurableColumnView.php', 'ConpherenceEditor' => 'applications/conpherence/editor/ConpherenceEditor.php', - 'ConpherenceFormDragAndDropUploadControl' => 'applications/conpherence/view/ConpherenceFormDragAndDropUploadControl.php', 'ConpherenceFulltextQuery' => 'applications/conpherence/query/ConpherenceFulltextQuery.php', 'ConpherenceIndex' => 'applications/conpherence/storage/ConpherenceIndex.php', 'ConpherenceLayoutView' => 'applications/conpherence/view/ConpherenceLayoutView.php', @@ -4808,7 +4807,6 @@ phutil_register_library_map(array( 'ConpherenceDAO' => 'PhabricatorLiskDAO', 'ConpherenceDurableColumnView' => 'AphrontTagView', 'ConpherenceEditor' => 'PhabricatorApplicationTransactionEditor', - 'ConpherenceFormDragAndDropUploadControl' => 'AphrontFormControl', 'ConpherenceFulltextQuery' => 'PhabricatorOffsetPagedQuery', 'ConpherenceIndex' => 'ConpherenceDAO', 'ConpherenceLayoutView' => 'AphrontTagView', diff --git a/src/applications/conpherence/view/ConpherenceFormDragAndDropUploadControl.php b/src/applications/conpherence/view/ConpherenceFormDragAndDropUploadControl.php deleted file mode 100644 index 3d9ea57cf9..0000000000 --- a/src/applications/conpherence/view/ConpherenceFormDragAndDropUploadControl.php +++ /dev/null @@ -1,40 +0,0 @@ -dropID = $drop_id; - return $this; - } - public function getDropID() { - return $this->dropID; - } - - protected function getCustomControlClass() { - return null; - } - - protected function renderInput() { - - $drop_id = celerity_generate_unique_node_id(); - Javelin::initBehavior('conpherence-drag-and-drop-photo', - array( - 'target' => $drop_id, - 'form_pane' => 'conpherence-form', - 'upload_uri' => '/file/dropupload/', - 'activated_class' => 'conpherence-dialogue-upload-photo', - )); - require_celerity_resource('conpherence-update-css'); - - return phutil_tag( - 'div', - array( - 'id' => $drop_id, - 'class' => 'conpherence-dialogue-drag-photo', - ), - pht('Drag and drop an image here to upload it.')); - } - -} diff --git a/webroot/rsrc/js/application/conpherence/behavior-drag-and-drop-photo.js b/webroot/rsrc/js/application/conpherence/behavior-drag-and-drop-photo.js deleted file mode 100644 index 7980bf3871..0000000000 --- a/webroot/rsrc/js/application/conpherence/behavior-drag-and-drop-photo.js +++ /dev/null @@ -1,38 +0,0 @@ -/** - * @provides javelin-behavior-conpherence-drag-and-drop-photo - * @requires javelin-behavior - * javelin-dom - * javelin-workflow - * phabricator-drag-and-drop-file-upload - */ - -JX.behavior('conpherence-drag-and-drop-photo', function(config) { - - var target = JX.$(config.target); - var form_pane = JX.$(config.form_pane); - - function onupload(f) { - var data = { - 'file_id' : f.getID(), - 'action' : 'metadata' - }; - - var form = JX.DOM.find(form_pane, 'form'); - var workflow = JX.Workflow.newFromForm(form, data); - workflow.start(); - } - - if (JX.PhabricatorDragAndDropFileUpload.isSupported()) { - var drop = new JX.PhabricatorDragAndDropFileUpload(target) - .setURI(config.upload_uri); - drop.listen('didBeginDrag', function() { - JX.DOM.alterClass(target, config.activated_class, true); - }); - drop.listen('didEndDrag', function() { - JX.DOM.alterClass(target, config.activated_class, false); - }); - drop.listen('didUpload', onupload); - drop.start(); - } - -}); From ced151e6f2e8f95f393ec3d954286fde4b0e563c Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 13 Oct 2016 08:01:08 -0700 Subject: [PATCH 12/55] Use transactions when importing events in Calendar, and update existing events Summary: Ref T10747. - Apply what changes we can with transactions, so you can see how an event has changed and import actions are more explicit. - I'll hide these from email/feed soon: I want them to appear on the event, but not generate notifications, since that could be especially annoying for automated events. - When importing, try to update existing events if we can. Test Plan: Imported a ".ics" file several times with minor changes, saw them reflected in the UI with transactions. {F1870027} Reviewers: chad Reviewed By: chad Maniphest Tasks: T10747 Differential Revision: https://secure.phabricator.com/D16701 --- .../PhabricatorCalendarImportEngine.php | 213 ++++++++++++++++-- .../query/PhabricatorCalendarEventQuery.php | 26 +++ .../storage/PhabricatorCalendarEvent.php | 60 ----- 3 files changed, 226 insertions(+), 73 deletions(-) diff --git a/src/applications/calendar/import/PhabricatorCalendarImportEngine.php b/src/applications/calendar/import/PhabricatorCalendarImportEngine.php index da7602e18f..d7b5dd1e5f 100644 --- a/src/applications/calendar/import/PhabricatorCalendarImportEngine.php +++ b/src/applications/calendar/import/PhabricatorCalendarImportEngine.php @@ -36,7 +36,7 @@ abstract class PhabricatorCalendarImportEngine $event_type = PhutilCalendarEventNode::NODETYPE; - $events = array(); + $nodes = array(); foreach ($root->getChildren() as $document) { foreach ($document->getChildren() as $node) { if ($node->getNodeType() != $event_type) { @@ -44,25 +44,212 @@ abstract class PhabricatorCalendarImportEngine continue; } - $event = PhabricatorCalendarEvent::newFromDocumentNode($viewer, $node); - - $event - ->setImportAuthorPHID($viewer->getPHID()) - ->setImportSourcePHID($import->getPHID()) - ->attachImportSource($import); - - $events[] = $event; + $nodes[] = $node; } } - // TODO: Use transactions. - // TODO: Update existing events instead of fataling. - foreach ($events as $event) { - $event->save(); + $node_map = array(); + $parent_uids = array(); + foreach ($nodes as $node) { + $full_uid = $this->getFullNodeUID($node); + if (isset($node_map[$full_uid])) { + // TODO: Warn that we got a duplicate. + continue; + } + $node_map[$full_uid] = $node; } + if ($node_map) { + $events = id(new PhabricatorCalendarEventQuery()) + ->setViewer($viewer) + ->withImportAuthorPHIDs(array($viewer->getPHID())) + ->withImportUIDs(array_keys($node_map)) + ->execute(); + $events = mpull($events, null, 'getImportUID'); + } else { + $events = null; + } + + $xactions = array(); + $update_map = array(); + foreach ($node_map as $full_uid => $node) { + $event = idx($events, $full_uid); + if (!$event) { + $event = PhabricatorCalendarEvent::initializeNewCalendarEvent($viewer); + } + + $event + ->setImportAuthorPHID($viewer->getPHID()) + ->setImportSourcePHID($import->getPHID()) + ->setImportUID($full_uid) + ->attachImportSource($import); + + $this->updateEventFromNode($viewer, $event, $node); + $xactions[$full_uid] = $this->newUpdateTransactions($event, $node); + $update_map[$full_uid] = $event; + } + + // Reorder events so we create parents first. This allows us to populate + // "instanceOfEventPHID" correctly. + $insert_order = array(); + foreach ($update_map as $full_uid => $event) { + $parent_uid = $this->getParentNodeUID($node_map[$full_uid]); + if ($parent_uid === null) { + $insert_order[$full_uid] = $full_uid; + continue; + } + + if (empty($update_map[$parent_uid])) { + // The parent was not present in this import, which means it either + // does not exist or we're going to delete it anyway. We just drop + // this node. + + // TODO: Warn that we got rid of an event with no parent. + + continue; + } + + // Otherwise, we're going to insert the parent first, then insert + // the child. + $insert_order[$parent_uid] = $parent_uid; + $insert_order[$full_uid] = $full_uid; + } + + // TODO: Define per-engine content sources so this can say "via Upload" or + // whatever. + $content_source = PhabricatorContentSource::newForSource( + PhabricatorWebContentSource::SOURCECONST); + + $update_map = array_select_keys($update_map, $insert_order); + foreach ($update_map as $full_uid => $event) { + $parent_uid = $this->getParentNodeUID($node_map[$full_uid]); + if ($parent_uid) { + $parent_phid = $update_map[$full_uid]->getPHID(); + } else { + $parent_phid = null; + } + + $event->setInstanceOfEventPHID($parent_phid); + + $event_xactions = $xactions[$full_uid]; + + $editor = id(new PhabricatorCalendarEventEditor()) + ->setActor($viewer) + ->setActingAsPHID($import->getPHID()) + ->setContentSource($content_source) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true); + + $editor->applyTransactions($event, $event_xactions); + } + + // TODO: When the source is a subscription-based ICS file or some other + // similar source, we should load all events from the source here and + // destroy the ones we didn't update. These are events that have been + // deleted. } + private function getFullNodeUID(PhutilCalendarEventNode $node) { + $uid = $node->getUID(); + $instance_epoch = $this->getNodeInstanceEpoch($node); + $full_uid = $uid.'/'.$instance_epoch; + return $full_uid; + } + + private function getParentNodeUID(PhutilCalendarEventNode $node) { + $recurrence_id = $node->getRecurrenceID(); + + if (!strlen($recurrence_id)) { + return null; + } + + return $node->getUID().'/'; + } + + private function getNodeInstanceEpoch(PhutilCalendarEventNode $node) { + $instance_iso = $node->getRecurrenceID(); + if (strlen($instance_iso)) { + $instance_datetime = PhutilCalendarAbsoluteDateTime::newFromISO8601( + $instance_iso); + $instance_epoch = $instance_datetime->getEpoch(); + } else { + $instance_epoch = null; + } + + return $instance_epoch; + } + + private function newUpdateTransactions( + PhabricatorCalendarEvent $event, + PhutilCalendarEventNode $node) { + + $xactions = array(); + $uid = $node->getUID(); + + $name = $node->getName(); + if (!strlen($name)) { + if (strlen($uid)) { + $name = pht('Unnamed Event "%s"', $uid); + } else { + $name = pht('Unnamed Imported Event'); + } + } + $xactions[] = id(new PhabricatorCalendarEventTransaction()) + ->setTransactionType( + PhabricatorCalendarEventNameTransaction::TRANSACTIONTYPE) + ->setNewValue($name); + + $description = $node->getDescription(); + $xactions[] = id(new PhabricatorCalendarEventTransaction()) + ->setTransactionType( + PhabricatorCalendarEventDescriptionTransaction::TRANSACTIONTYPE) + ->setNewValue((string)$description); + + $is_recurring = (bool)$node->getRecurrenceRule(); + $xactions[] = id(new PhabricatorCalendarEventTransaction()) + ->setTransactionType( + PhabricatorCalendarEventRecurringTransaction::TRANSACTIONTYPE) + ->setNewValue($is_recurring); + + return $xactions; + } + + private function updateEventFromNode( + PhabricatorUser $actor, + PhabricatorCalendarEvent $event, + PhutilCalendarEventNode $node) { + + $instance_epoch = $this->getNodeInstanceEpoch($node); + $event->setUTCInstanceEpoch($instance_epoch); + + $timezone = $actor->getTimezoneIdentifier(); + + // TODO: These should be transactional, but the transaction only accepts + // epoch timestamps right now. + $start_datetime = $node->getStartDateTime() + ->setViewerTimezone($timezone); + $end_datetime = $node->getEndDateTime() + ->setViewerTimezone($timezone); + + $event + ->setStartDateTime($start_datetime) + ->setEndDateTime($end_datetime); + + // TODO: This should be transactional, but the transaction only accepts + // simple frequency rules right now. + $rrule = $node->getRecurrenceRule(); + if ($rrule) { + $event->setRecurrenceRule($rrule); + + $until_datetime = $rrule->getUntil() + ->setViewerTimezone($timezone); + if ($until_datetime) { + $event->setUntilDateTime($until_datetime); + } + } + + return $event; + } } diff --git a/src/applications/calendar/query/PhabricatorCalendarEventQuery.php b/src/applications/calendar/query/PhabricatorCalendarEventQuery.php index 1bcdefebfd..9949ee2f5a 100644 --- a/src/applications/calendar/query/PhabricatorCalendarEventQuery.php +++ b/src/applications/calendar/query/PhabricatorCalendarEventQuery.php @@ -15,6 +15,8 @@ final class PhabricatorCalendarEventQuery private $isStub; private $parentEventPHIDs; private $importSourcePHIDs; + private $importAuthorPHIDs; + private $importUIDs; private $generateGhosts = false; @@ -83,6 +85,16 @@ final class PhabricatorCalendarEventQuery return $this; } + public function withImportAuthorPHIDs(array $author_phids) { + $this->importAuthorPHIDs = $author_phids; + return $this; + } + + public function withImportUIDs(array $uids) { + $this->importUIDs = $uids; + return $this; + } + protected function getDefaultOrderVector() { return array('start', 'id'); } @@ -424,6 +436,20 @@ final class PhabricatorCalendarEventQuery $this->importSourcePHIDs); } + if ($this->importAuthorPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'event.importAuthorPHID IN (%Ls)', + $this->importAuthorPHIDs); + } + + if ($this->importUIDs !== null) { + $where[] = qsprintf( + $conn, + 'event.importUID IN (%Ls)', + $this->importUIDs); + } + return $where; } diff --git a/src/applications/calendar/storage/PhabricatorCalendarEvent.php b/src/applications/calendar/storage/PhabricatorCalendarEvent.php index 61d0b5c25d..2cf9e4117c 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarEvent.php +++ b/src/applications/calendar/storage/PhabricatorCalendarEvent.php @@ -102,66 +102,6 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO ->applyViewerTimezone($actor); } - public static function newFromDocumentNode( - PhabricatorUser $actor, - PhutilCalendarEventNode $node) { - $timezone = $actor->getTimezoneIdentifier(); - - $uid = $node->getUID(); - - $name = $node->getName(); - if (!strlen($name)) { - if (strlen($uid)) { - $name = pht('Unnamed Event "%s"', $node->getUID()); - } else { - $name = pht('Unnamed Imported Event'); - } - } - - $description = $node->getDescription(); - - $instance_iso = $node->getRecurrenceID(); - if (strlen($instance_iso)) { - $instance_datetime = PhutilCalendarAbsoluteDateTime::newFromISO8601( - $instance_iso); - $instance_epoch = $instance_datetime->getEpoch(); - } else { - $instance_epoch = null; - } - $full_uid = $uid.'/'.$instance_epoch; - - $start_datetime = $node->getStartDateTime() - ->setViewerTimezone($timezone); - $end_datetime = $node->getEndDateTime() - ->setViewerTimezone($timezone); - - $rrule = $node->getRecurrenceRule(); - - $event = self::initializeNewCalendarEvent($actor) - ->setName($name) - ->setStartDateTime($start_datetime) - ->setEndDateTime($end_datetime) - ->setImportUID($full_uid) - ->setUTCInstanceEpoch($instance_epoch); - - if (strlen($description)) { - $event->setDescription($description); - } - - if ($rrule) { - $event->setRecurrenceRule($rrule); - $event->setIsRecurring(1); - - $until_datetime = $rrule->getUntil() - ->setViewerTimezone($timezone); - if ($until_datetime) { - $event->setUntilDateTime($until_datetime); - } - } - - return $event; - } - private function newChild( PhabricatorUser $actor, $sequence, From 6e2a86470b4797e835bd896c4117694ca6108a01 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 13 Oct 2016 08:31:34 -0700 Subject: [PATCH 13/55] Support disabling calendar imports Summary: Ref T10747. This doesn't do much for ICS file imports (you can't disable them since it doesn't do anything meaningful) but will matter more for ICS-subscription imports later. Test Plan: Clicked "Disable" on an ICS file import, got explanatory dialog. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10747 Differential Revision: https://secure.phabricator.com/D16702 --- src/__phutil_library_map__.php | 2 + ...ricatorCalendarImportDisableController.php | 71 +++++++++++++++++++ ...habricatorCalendarImportViewController.php | 5 +- .../PhabricatorCalendarICSImportEngine.php | 15 ++++ .../PhabricatorCalendarImportEngine.php | 10 +++ 5 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 src/applications/calendar/controller/PhabricatorCalendarImportDisableController.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 49a3e000cf..4e430f22ed 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2102,6 +2102,7 @@ phutil_register_library_map(array( 'PhabricatorCalendarICSWriter' => 'applications/calendar/util/PhabricatorCalendarICSWriter.php', 'PhabricatorCalendarIconSet' => 'applications/calendar/icon/PhabricatorCalendarIconSet.php', 'PhabricatorCalendarImport' => 'applications/calendar/storage/PhabricatorCalendarImport.php', + 'PhabricatorCalendarImportDisableController' => 'applications/calendar/controller/PhabricatorCalendarImportDisableController.php', 'PhabricatorCalendarImportDisableTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportDisableTransaction.php', 'PhabricatorCalendarImportEditController' => 'applications/calendar/controller/PhabricatorCalendarImportEditController.php', 'PhabricatorCalendarImportEditEngine' => 'applications/calendar/editor/PhabricatorCalendarImportEditEngine.php', @@ -6905,6 +6906,7 @@ phutil_register_library_map(array( 'PhabricatorApplicationTransactionInterface', 'PhabricatorDestructibleInterface', ), + 'PhabricatorCalendarImportDisableController' => 'PhabricatorCalendarController', 'PhabricatorCalendarImportDisableTransaction' => 'PhabricatorCalendarImportTransactionType', 'PhabricatorCalendarImportEditController' => 'PhabricatorCalendarController', 'PhabricatorCalendarImportEditEngine' => 'PhabricatorEditEngine', diff --git a/src/applications/calendar/controller/PhabricatorCalendarImportDisableController.php b/src/applications/calendar/controller/PhabricatorCalendarImportDisableController.php new file mode 100644 index 0000000000..5a9b720be4 --- /dev/null +++ b/src/applications/calendar/controller/PhabricatorCalendarImportDisableController.php @@ -0,0 +1,71 @@ +getViewer(); + + $import = id(new PhabricatorCalendarImportQuery()) + ->setViewer($viewer) + ->withIDs(array($request->getURIData('id'))) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$import) { + return new Aphront404Response(); + } + + $import_uri = $import->getURI(); + $is_disable = !$import->getIsDisabled(); + + if (!$import->getEngine()->canDisable($viewer, $import)) { + $reason = $import->getEngine()->explainCanDisable($viewer, $import); + return $this->newDialog() + ->setTitle(pht('Unable to Disable')) + ->appendParagraph($reason) + ->addCancelButton($import_uri); + } + + if ($request->isFormPost()) { + $xactions = array(); + $xactions[] = id(new PhabricatorCalendarImportTransaction()) + ->setTransactionType( + PhabricatorCalendarImportDisableTransaction::TRANSACTIONTYPE) + ->setNewValue($is_disable ? 1 : 0); + + $editor = id(new PhabricatorCalendarImportEditor()) + ->setActor($viewer) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->setContentSourceFromRequest($request); + + $editor->applyTransactions($import, $xactions); + + return id(new AphrontRedirectResponse())->setURI($import_uri); + } + + if ($is_disable) { + $title = pht('Disable Import'); + $body = pht( + 'Disable this import? Events from this source will no longer be '. + 'updated.'); + $button = pht('Disable Import'); + } else { + $title = pht('Enable Import'); + $body = pht( + 'Enable this import? Events from this source will be updated again.'); + $button = pht('Enable Import'); + } + + return $this->newDialog() + ->setTitle($title) + ->appendParagraph($body) + ->addCancelButton($import_uri) + ->addSubmitButton($button); + } + +} diff --git a/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php b/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php index e2be04050d..12744cf61e 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php @@ -80,6 +80,7 @@ final class PhabricatorCalendarImportViewController $id = $import->getID(); $curtain = $this->newCurtainView($import); + $engine = $import->getEngine(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, @@ -89,6 +90,8 @@ final class PhabricatorCalendarImportViewController $edit_uri = "import/edit/{$id}/"; $edit_uri = $this->getApplicationURI($edit_uri); + $can_disable = ($can_edit && $engine->canDisable($viewer, $import)); + $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Import')) @@ -111,7 +114,7 @@ final class PhabricatorCalendarImportViewController id(new PhabricatorActionView()) ->setName($disable_name) ->setIcon($disable_icon) - ->setDisabled(!$can_edit) + ->setDisabled(!$can_disable) ->setWorkflow(true) ->setHref($disable_uri)); diff --git a/src/applications/calendar/import/PhabricatorCalendarICSImportEngine.php b/src/applications/calendar/import/PhabricatorCalendarICSImportEngine.php index 1b2f120b4c..2874e723a2 100644 --- a/src/applications/calendar/import/PhabricatorCalendarICSImportEngine.php +++ b/src/applications/calendar/import/PhabricatorCalendarICSImportEngine.php @@ -70,5 +70,20 @@ final class PhabricatorCalendarICSImportEngine } + public function canDisable( + PhabricatorUser $viewer, + PhabricatorCalendarImport $import) { + return false; + } + + public function explainCanDisable( + PhabricatorUser $viewer, + PhabricatorCalendarImport $import) { + return pht( + 'You can not disable import of an ICS file because the entire import '. + 'occurs immediately when you upload the file. There is no further '. + 'activity to disable.'); + } + } diff --git a/src/applications/calendar/import/PhabricatorCalendarImportEngine.php b/src/applications/calendar/import/PhabricatorCalendarImportEngine.php index d7b5dd1e5f..e6242762ff 100644 --- a/src/applications/calendar/import/PhabricatorCalendarImportEngine.php +++ b/src/applications/calendar/import/PhabricatorCalendarImportEngine.php @@ -21,6 +21,16 @@ abstract class PhabricatorCalendarImportEngine PhabricatorUser $viewer, PhabricatorCalendarImport $import); + abstract public function canDisable( + PhabricatorUser $viewer, + PhabricatorCalendarImport $import); + + public function explainCanDisable( + PhabricatorUser $viewer, + PhabricatorCalendarImport $import) { + throw new PhutilMethodNotImplementedException(); + } + final public static function getAllImportEngines() { return id(new PhutilClassMapQuery()) ->setAncestorClass(__CLASS__) From c2411e3dcc9c8187a2920caa483ce84d7c84ed9c Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 13 Oct 2016 08:51:27 -0700 Subject: [PATCH 14/55] When viewing a Calendar import, show all the events it imported Summary: Ref T10747. Show which events a source imported, and link to the full list as a query result. Test Plan: {F1870049} Reviewers: chad Reviewed By: chad Maniphest Tasks: T10747 Differential Revision: https://secure.phabricator.com/D16703 --- ...habricatorCalendarImportViewController.php | 45 +++++++++++++++++++ .../PhabricatorCalendarEventSearchEngine.php | 8 ++++ 2 files changed, 53 insertions(+) diff --git a/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php b/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php index 12744cf61e..92da19d61b 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php @@ -30,10 +30,13 @@ final class PhabricatorCalendarImportViewController $curtain = $this->buildCurtain($import); $details = $this->buildPropertySection($import); + $imported_events = $this->buildImportedEvents($import); + $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setMainColumn( array( + $imported_events, $timeline, )) ->setCurtain($curtain) @@ -130,4 +133,46 @@ final class PhabricatorCalendarImportViewController return $properties; } + + private function buildImportedEvents( + PhabricatorCalendarImport $import) { + $viewer = $this->getViewer(); + + $engine = id(new PhabricatorCalendarEventSearchEngine()) + ->setViewer($viewer); + + $saved = $engine->newSavedQuery() + ->setParameter('importSourcePHIDs', array($import->getPHID())); + + $pager = $engine->newPagerForSavedQuery($saved); + $pager->setPageSize(25); + + $query = $engine->buildQueryFromSavedQuery($saved); + + $results = $engine->executeQuery($query, $pager); + $view = $engine->renderResults($results, $saved); + $list = $view->getObjectList(); + $list->setNoDataString(pht('No imported events.')); + + $all_uri = $this->getApplicationURI(); + $all_uri = (string)id(new PhutilURI($all_uri)) + ->setQueryParam('importSourcePHID', $import->getPHID()) + ->setQueryParam('display', 'list'); + + $all_button = id(new PHUIButtonView()) + ->setTag('a') + ->setText(pht('View All')) + ->setIcon('fa-search') + ->setHref($all_uri); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Imported Events')) + ->addActionLink($all_button); + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setObjectList($list); + } + } diff --git a/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php b/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php index a29b7ea37a..8d55da994a 100644 --- a/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php +++ b/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php @@ -51,6 +51,10 @@ final class PhabricatorCalendarEventSearchEngine ->setKey('isCancelled') ->setOptions($this->getCancelledOptions()) ->setDefault('active'), + id(new PhabricatorPHIDsSearchField()) + ->setLabel(pht('Import Sources')) + ->setKey('importSourcePHIDs') + ->setAliases(array('importSourcePHID')), id(new PhabricatorSearchSelectField()) ->setLabel(pht('Display Options')) ->setKey('display') @@ -114,6 +118,10 @@ final class PhabricatorCalendarEventSearchEngine break; } + if ($map['importSourcePHIDs']) { + $query->withImportSourcePHIDs($map['importSourcePHIDs']); + } + // Generate ghosts (and ignore stub events) if we aren't querying for // specific events or exporting. if (!empty($map['export'])) { From 508a2a14985785d66e7266b49d6b2e840b5f7dcb Mon Sep 17 00:00:00 2001 From: Chad Little Date: Thu, 13 Oct 2016 11:09:54 -0700 Subject: [PATCH 15/55] Basic Conpherence Search in Thread Summary: Adds a search bar toggle and results for searching inside a Conpherence Room. The UI of the results itself are not styled yet, and will follow up with another diff. Test Plan: Go to Conpherence, search for "asdf", get lots of results. Search for nothing, get no change, search for something fictitious, get no threads found (will follow up with search result UI). Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D16697 --- resources/celerity/map.php | 66 +++++++++-------- resources/celerity/packages.php | 1 - src/__phutil_library_map__.php | 2 + .../PhabricatorConpherenceApplication.php | 2 + .../controller/ConpherenceController.php | 70 +++++++++++++++++++ .../ConpherenceThreadSearchController.php | 41 +++++++++++ .../ConpherenceUpdateController.php | 4 -- .../controller/ConpherenceViewController.php | 3 + .../view/ConpherenceLayoutView.php | 20 ++++++ .../application/conpherence/header-pane.css | 14 +++- .../application/conpherence/message-pane.css | 65 +++++++++++++++++ .../css/application/conpherence/update.css | 13 ---- webroot/rsrc/css/core/z-index.css | 14 +++- webroot/rsrc/css/phui/phui-icon.css | 10 +++ .../behavior-conpherence-search.js | 63 +++++++++++++++++ .../application/conpherence/behavior-menu.js | 45 ++---------- 16 files changed, 340 insertions(+), 93 deletions(-) create mode 100644 src/applications/conpherence/controller/ConpherenceThreadSearchController.php delete mode 100644 webroot/rsrc/css/application/conpherence/update.css create mode 100644 webroot/rsrc/js/application/conpherence/behavior-conpherence-search.js diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 46db1cb4f4..b012c7904e 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,9 +7,9 @@ */ return array( 'names' => array( - 'conpherence.pkg.css' => '4601645d', - 'conpherence.pkg.js' => '44dd69f5', - 'core.pkg.css' => '7ca260a3', + 'conpherence.pkg.css' => 'c839a862', + 'conpherence.pkg.js' => 'b18c9dc5', + 'core.pkg.css' => 'b99bbf5e', 'core.pkg.js' => '30185d95', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => 'e1d704ce', @@ -47,13 +47,12 @@ return array( 'rsrc/css/application/config/setup-issue.css' => 'f794cfc3', 'rsrc/css/application/config/unhandled-exception.css' => '4c96257a', 'rsrc/css/application/conpherence/durable-column.css' => '44bcaa19', - 'rsrc/css/application/conpherence/header-pane.css' => '20a7028c', + 'rsrc/css/application/conpherence/header-pane.css' => 'e8acbd37', 'rsrc/css/application/conpherence/menu.css' => '4f51db5a', - 'rsrc/css/application/conpherence/message-pane.css' => '0d7dff02', + 'rsrc/css/application/conpherence/message-pane.css' => 'eff20ae7', 'rsrc/css/application/conpherence/notification.css' => '965db05b', 'rsrc/css/application/conpherence/participant-pane.css' => '7bba0b56', 'rsrc/css/application/conpherence/transaction.css' => '46253e19', - 'rsrc/css/application/conpherence/update.css' => '53bc527a', 'rsrc/css/application/contentsource/content-source-view.css' => '4b8b05d4', 'rsrc/css/application/countdown/timer.css' => '16c52f5c', 'rsrc/css/application/daemon/bulk-job.css' => 'df9c1d4a', @@ -110,7 +109,7 @@ return array( 'rsrc/css/core/core.css' => 'd0801452', 'rsrc/css/core/remarkup.css' => 'cd912f2c', 'rsrc/css/core/syntax.css' => '769d3498', - 'rsrc/css/core/z-index.css' => '0d4e5558', + 'rsrc/css/core/z-index.css' => 'd1270942', 'rsrc/css/diviner/diviner-shared.css' => 'aa3656aa', 'rsrc/css/font/font-aleo.css' => '8bdb2835', 'rsrc/css/font/font-awesome.css' => '2b7ebbcc', @@ -144,7 +143,7 @@ return array( 'rsrc/css/phui/phui-header-view.css' => '06385974', 'rsrc/css/phui/phui-hovercard.css' => 'de1a2119', 'rsrc/css/phui/phui-icon-set-selector.css' => '1ab67aad', - 'rsrc/css/phui/phui-icon.css' => '9bab6f02', + 'rsrc/css/phui/phui-icon.css' => '417f80fb', 'rsrc/css/phui/phui-image-mask.css' => 'a8498f9c', 'rsrc/css/phui/phui-info-panel.css' => '27ea50a1', 'rsrc/css/phui/phui-info-view.css' => '28efab79', @@ -437,8 +436,9 @@ return array( 'rsrc/js/application/calendar/behavior-recurring-edit.js' => '5f1c4d5f', 'rsrc/js/application/config/behavior-reorder-fields.js' => 'b6993408', 'rsrc/js/application/conpherence/ConpherenceThreadManager.js' => '01774ab2', + 'rsrc/js/application/conpherence/behavior-conpherence-search.js' => '3bc9d2b1', 'rsrc/js/application/conpherence/behavior-durable-column.js' => 'c5238acb', - 'rsrc/js/application/conpherence/behavior-menu.js' => '9eb55204', + 'rsrc/js/application/conpherence/behavior-menu.js' => '0f82ba76', 'rsrc/js/application/conpherence/behavior-participant-pane.js' => '8604caa8', 'rsrc/js/application/conpherence/behavior-pontificate.js' => 'f2e58483', 'rsrc/js/application/conpherence/behavior-quicksand-blacklist.js' => '7927a7d3', @@ -617,14 +617,13 @@ return array( 'config-options-css' => '0ede4c9b', 'config-page-css' => '8798e14f', 'conpherence-durable-column-view' => '44bcaa19', - 'conpherence-header-pane-css' => '20a7028c', + 'conpherence-header-pane-css' => 'e8acbd37', 'conpherence-menu-css' => '4f51db5a', - 'conpherence-message-pane-css' => '0d7dff02', + 'conpherence-message-pane-css' => 'eff20ae7', 'conpherence-notification-css' => '965db05b', 'conpherence-participant-pane-css' => '7bba0b56', 'conpherence-thread-manager' => '01774ab2', 'conpherence-transaction-css' => '46253e19', - 'conpherence-update-css' => '53bc527a', 'd3' => 'a11a5ff2', 'differential-changeset-view-css' => '9ef7d354', 'differential-core-view-css' => '5b7b8ff4', @@ -664,9 +663,10 @@ return array( 'javelin-behavior-choose-control' => '327a00d1', 'javelin-behavior-comment-actions' => '0300eae6', 'javelin-behavior-config-reorder-fields' => 'b6993408', - 'javelin-behavior-conpherence-menu' => '9eb55204', + 'javelin-behavior-conpherence-menu' => '0f82ba76', 'javelin-behavior-conpherence-participant-pane' => '8604caa8', 'javelin-behavior-conpherence-pontificate' => 'f2e58483', + 'javelin-behavior-conpherence-search' => '3bc9d2b1', 'javelin-behavior-countdown-timer' => 'e4cc26b3', 'javelin-behavior-dark-console' => 'f411b6ae', 'javelin-behavior-dashboard-async-panel' => '469c0d9e', @@ -880,7 +880,7 @@ return array( 'phabricator-uiexample-reactor-select' => 'a155550f', 'phabricator-uiexample-reactor-sendclass' => '1def2711', 'phabricator-uiexample-reactor-sendproperties' => 'b1f0ccee', - 'phabricator-zindex-css' => '0d4e5558', + 'phabricator-zindex-css' => 'd1270942', 'phame-css' => '8efb0729', 'pholio-css' => 'ca89d380', 'pholio-edit-css' => '07676f51', @@ -917,7 +917,7 @@ return array( 'phui-hovercard' => '1bd28176', 'phui-hovercard-view-css' => 'de1a2119', 'phui-icon-set-selector-css' => '1ab67aad', - 'phui-icon-view-css' => '9bab6f02', + 'phui-icon-view-css' => '417f80fb', 'phui-image-mask-css' => 'a8498f9c', 'phui-info-panel-css' => '27ea50a1', 'phui-info-view-css' => '28efab79', @@ -1072,6 +1072,20 @@ return array( 'javelin-install', 'javelin-util', ), + '0f82ba76' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-util', + 'javelin-stratcom', + 'javelin-workflow', + 'javelin-behavior-device', + 'javelin-history', + 'javelin-vector', + 'javelin-scrollbar', + 'phabricator-title', + 'phabricator-shaped-request', + 'conpherence-thread-manager', + ), '116cf19b' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1205,6 +1219,13 @@ return array( 'javelin-dom', 'javelin-magical-init', ), + '3bc9d2b1' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-util', + 'javelin-workflow', + 'javelin-stratcom', + ), '3cb0b2fc' => array( 'javelin-behavior', 'javelin-dom', @@ -1745,20 +1766,6 @@ return array( 'javelin-workflow', 'javelin-stratcom', ), - '9eb55204' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-util', - 'javelin-stratcom', - 'javelin-workflow', - 'javelin-behavior-device', - 'javelin-history', - 'javelin-vector', - 'javelin-scrollbar', - 'phabricator-title', - 'phabricator-shaped-request', - 'conpherence-thread-manager', - ), '9ef7d354' => array( 'phui-inline-comment-view-css', ), @@ -2292,7 +2299,6 @@ return array( 'conpherence-message-pane-css', 'conpherence-notification-css', 'conpherence-transaction-css', - 'conpherence-update-css', 'conpherence-participant-pane-css', 'conpherence-header-pane-css', ), diff --git a/resources/celerity/packages.php b/resources/celerity/packages.php index e10ba48d28..8e7d339902 100644 --- a/resources/celerity/packages.php +++ b/resources/celerity/packages.php @@ -157,7 +157,6 @@ return array( 'conpherence-message-pane-css', 'conpherence-notification-css', 'conpherence-transaction-css', - 'conpherence-update-css', 'conpherence-participant-pane-css', 'conpherence-header-pane-css', ), diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 4e430f22ed..fbae5beb97 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -320,6 +320,7 @@ phutil_register_library_map(array( 'ConpherenceThreadMembersPolicyRule' => 'applications/conpherence/policyrule/ConpherenceThreadMembersPolicyRule.php', 'ConpherenceThreadQuery' => 'applications/conpherence/query/ConpherenceThreadQuery.php', 'ConpherenceThreadRemarkupRule' => 'applications/conpherence/remarkup/ConpherenceThreadRemarkupRule.php', + 'ConpherenceThreadSearchController' => 'applications/conpherence/controller/ConpherenceThreadSearchController.php', 'ConpherenceThreadSearchEngine' => 'applications/conpherence/query/ConpherenceThreadSearchEngine.php', 'ConpherenceThreadTitleNgrams' => 'applications/conpherence/storage/ConpherenceThreadTitleNgrams.php', 'ConpherenceTransaction' => 'applications/conpherence/storage/ConpherenceTransaction.php', @@ -4844,6 +4845,7 @@ phutil_register_library_map(array( 'ConpherenceThreadMembersPolicyRule' => 'PhabricatorPolicyRule', 'ConpherenceThreadQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'ConpherenceThreadRemarkupRule' => 'PhabricatorObjectRemarkupRule', + 'ConpherenceThreadSearchController' => 'ConpherenceController', 'ConpherenceThreadSearchEngine' => 'PhabricatorApplicationSearchEngine', 'ConpherenceThreadTitleNgrams' => 'PhabricatorSearchNgrams', 'ConpherenceTransaction' => 'PhabricatorApplicationTransaction', diff --git a/src/applications/conpherence/application/PhabricatorConpherenceApplication.php b/src/applications/conpherence/application/PhabricatorConpherenceApplication.php index 3081919503..5faaf40b9c 100644 --- a/src/applications/conpherence/application/PhabricatorConpherenceApplication.php +++ b/src/applications/conpherence/application/PhabricatorConpherenceApplication.php @@ -37,6 +37,8 @@ final class PhabricatorConpherenceApplication extends PhabricatorApplication { => 'ConpherenceListController', 'thread/(?P[1-9]\d*)/' => 'ConpherenceListController', + 'threadsearch/(?P[1-9]\d*)/' + => 'ConpherenceThreadSearchController', '(?P[1-9]\d*)/' => 'ConpherenceViewController', '(?P[1-9]\d*)/(?P[1-9]\d*)/' diff --git a/src/applications/conpherence/controller/ConpherenceController.php b/src/applications/conpherence/controller/ConpherenceController.php index 389a8ce9b7..097877f475 100644 --- a/src/applications/conpherence/controller/ConpherenceController.php +++ b/src/applications/conpherence/controller/ConpherenceController.php @@ -113,6 +113,20 @@ abstract class ConpherenceController extends PhabricatorController { ->setHref('#') ->addClass('conpherence-participant-toggle')); + Javelin::initBehavior( + 'conpherence-search', + array( + 'searchURI' => '/conpherence/threadsearch/'.$conpherence->getID().'/', + )); + + $header->addActionItem( + id(new PHUIIconCircleView()) + ->addSigil('conpherence-search-toggle') + ->setIcon('fa-search') + ->setHref('#') + ->setColor('green') + ->addClass('conpherence-search-toggle')); + if ($can_join && !$participating) { $action = ConpherenceUpdateActions::JOIN_ROOM; $uri = $this->getApplicationURI('update/'.$conpherence->getID().'/'); @@ -149,4 +163,60 @@ abstract class ConpherenceController extends PhabricatorController { return $header; } + public function buildSearchForm() { + $viewer = $this->getViewer(); + $conpherence = $this->conpherence; + $name = $conpherence->getTitle(); + + $bar = javelin_tag( + 'input', + array( + 'type' => 'text', + 'id' => 'conpherence-search-input', + 'name' => 'fulltext', + 'class' => 'conpherence-search-input', + 'sigil' => 'conpherence-search-input', + 'placeholder' => pht('Search %s...', $name), + )); + + $id = $conpherence->getID(); + $form = phabricator_form( + $viewer, + array( + 'method' => 'POST', + 'action' => '/conpherence/threadsearch/'.$id.'/', + 'sigil' => 'conpherence-search-form', + 'class' => 'conpherence-search-form', + ), + array( + $bar, + )); + + $form_view = phutil_tag( + 'div', + array( + 'class' => 'conpherence-search-form-view', + ), + $form); + + $results = phutil_tag( + 'div', + array( + 'id' => 'conpherence-search-results', + 'class' => 'conpherence-search-results', + )); + + $view = phutil_tag( + 'div', + array( + 'class' => 'conpherence-search-window', + ), + array( + $form_view, + $results, + )); + + return $view; + } + } diff --git a/src/applications/conpherence/controller/ConpherenceThreadSearchController.php b/src/applications/conpherence/controller/ConpherenceThreadSearchController.php new file mode 100644 index 0000000000..e426e99128 --- /dev/null +++ b/src/applications/conpherence/controller/ConpherenceThreadSearchController.php @@ -0,0 +1,41 @@ +getViewer(); + $conpherence_id = $request->getURIData('id'); + $fulltext = $request->getStr('fulltext'); + + $conpherence = id(new ConpherenceThreadQuery()) + ->setViewer($viewer) + ->withIDs(array($conpherence_id)) + ->executeOne(); + + if (!$conpherence) { + return new Aphront404Response(); + } + + $engine = new ConpherenceThreadSearchEngine(); + $engine->setViewer($viewer); + $saved = $engine->buildSavedQueryFromBuiltin('all') + ->setParameter('phids', array($conpherence->getPHID())) + ->setParameter('fulltext', $fulltext); + + $pager = $engine->newPagerForSavedQuery($saved); + $pager->setPageSize(15); + + $query = $engine->buildQueryFromSavedQuery($saved); + + $results = $engine->executeQuery($query, $pager); + $view = $engine->renderResults($results, $saved); + + return id(new AphrontAjaxResponse()) + ->setContent($view->getObjectList()); + } +} diff --git a/src/applications/conpherence/controller/ConpherenceUpdateController.php b/src/applications/conpherence/controller/ConpherenceUpdateController.php index db21b3d02f..534470e998 100644 --- a/src/applications/conpherence/controller/ConpherenceUpdateController.php +++ b/src/applications/conpherence/controller/ConpherenceUpdateController.php @@ -327,7 +327,6 @@ final class ConpherenceUpdateController ->setUser($user) ->setDatasource(new PhabricatorPeopleDatasource())); - require_celerity_resource('conpherence-update-css'); $view = id(new AphrontDialogView()) ->setTitle(pht('Add Participants')) ->addHiddenInput('action', 'add_person') @@ -407,8 +406,6 @@ final class ConpherenceUpdateController } } - require_celerity_resource('conpherence-update-css'); - $dialog = id(new AphrontDialogView()) ->setTitle($title) ->addHiddenInput('action', 'remove_person') @@ -471,7 +468,6 @@ final class ConpherenceUpdateController ->setCapability(PhabricatorPolicyCapability::CAN_JOIN) ->setPolicies($policies)); - require_celerity_resource('conpherence-update-css'); $view = id(new AphrontDialogView()) ->setTitle($title) ->addHiddenInput('action', 'metadata') diff --git a/src/applications/conpherence/controller/ConpherenceViewController.php b/src/applications/conpherence/controller/ConpherenceViewController.php index 029a6115a2..64a3a67bcf 100644 --- a/src/applications/conpherence/controller/ConpherenceViewController.php +++ b/src/applications/conpherence/controller/ConpherenceViewController.php @@ -89,9 +89,11 @@ final class ConpherenceViewController extends ->setObject($conpherence) ->execute(); $header = $this->buildHeaderPaneContent($conpherence, $policy_objects); + $search = $this->buildSearchForm(); $form = $this->renderFormContent(); $content = array( 'header' => $header, + 'search' => $search, 'transactions' => $messages, 'form' => $form, ); @@ -128,6 +130,7 @@ final class ConpherenceViewController extends ->setBaseURI($this->getApplicationURI()) ->setThread($conpherence) ->setHeader($header) + ->setSearch($search) ->setMessages($messages) ->setReplyForm($form) ->setLatestTransactionID($data['latest_transaction_id']) diff --git a/src/applications/conpherence/view/ConpherenceLayoutView.php b/src/applications/conpherence/view/ConpherenceLayoutView.php index f0d94d9a06..b1777e4f97 100644 --- a/src/applications/conpherence/view/ConpherenceLayoutView.php +++ b/src/applications/conpherence/view/ConpherenceLayoutView.php @@ -7,6 +7,7 @@ final class ConpherenceLayoutView extends AphrontTagView { private $threadView; private $role; private $header; + private $search; private $messages; private $replyForm; private $latestTransactionID; @@ -26,6 +27,11 @@ final class ConpherenceLayoutView extends AphrontTagView { return $this; } + public function setSearch($search) { + $this->search = $search; + return $this; + } + public function setRole($role) { $this->role = $role; return $this; @@ -131,6 +137,12 @@ final class ConpherenceLayoutView extends AphrontTagView { 'class' => 'conpherence-content-pane', ), array( + phutil_tag( + 'div', + array( + 'class' => 'conpherence-loading-mask', + ), + ''), javelin_tag( 'div', array( @@ -184,6 +196,14 @@ final class ConpherenceLayoutView extends AphrontTagView { 'sigil' => 'conpherence-messages', ), nonempty($this->messages, '')), + javelin_tag( + 'div', + array( + 'class' => 'conpherence-search-main', + 'id' => 'conpherence-search-main', + 'sigil' => 'conpherence-search-main', + ), + nonempty($this->search, '')), phutil_tag( 'div', array( diff --git a/webroot/rsrc/css/application/conpherence/header-pane.css b/webroot/rsrc/css/application/conpherence/header-pane.css index 522771cc15..a2f876ae5f 100644 --- a/webroot/rsrc/css/application/conpherence/header-pane.css +++ b/webroot/rsrc/css/application/conpherence/header-pane.css @@ -66,7 +66,17 @@ } .conpherence-participant-toggle.phui-icon-circle .phui-icon-view { - color: {$sky}; + color: {$sky}; +} + +.show-searchbar .conpherence-search-toggle.phui-icon-circle { + text-decoration: none; + border-color: {$green}; + cursor: pointer; +} + +.show-searchbar .conpherence-search-toggle.phui-icon-circle .phui-icon-view { + color: {$green}; } .hide-widgets .conpherence-participant-toggle.phui-icon-circle { @@ -76,5 +86,5 @@ } .hide-widgets .conpherence-participant-toggle.phui-icon-circle .phui-icon-view { - color: {$lightblueborder}; + color: {$lightblueborder}; } diff --git a/webroot/rsrc/css/application/conpherence/message-pane.css b/webroot/rsrc/css/application/conpherence/message-pane.css index 6df4cb8828..a7ff8000db 100644 --- a/webroot/rsrc/css/application/conpherence/message-pane.css +++ b/webroot/rsrc/css/application/conpherence/message-pane.css @@ -400,3 +400,68 @@ margin-top: 0; margin-bottom: 0; } + +/***** Thread Loading *********************************************************/ + +.conpherence-layout .conpherence-loading-mask { + height: 0; + opacity: 0; + top: 0; + right: 0; + left: 240px; + bottom: 0; + transition: all 0.3s; + position: fixed; + background-color: #fff; +} + +.conpherence-layout.loading .conpherence-loading-mask { + opacity: 1; + height: auto; +} + + +/***** Thread Search **********************************************************/ + +.conpherence-search-main { + opacity: 0; + transition: all 0.2s; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + height: 0; +} + +.show-searchbar .conpherence-search-main { + opacity: 1; + height: auto; +} + +.show-searchbar .conpherence-search-form-view { + display: block; + height: 54px; + background: {$lightbluebackground}; + position: absolute; + top: 1px; + left: 0; + right: 0; +} + +input.conpherence-search-input { + padding-left: 8px; + width: calc(100% - 24px); + border-radius: 20px; + margin: 12px; +} + +.conpherence-search-results { + position: absolute; + background: #fff; + top: 54px; + left: 0; + right: 0; + bottom: 0; + overflow-y: auto; +} diff --git a/webroot/rsrc/css/application/conpherence/update.css b/webroot/rsrc/css/application/conpherence/update.css deleted file mode 100644 index 8b29c085ed..0000000000 --- a/webroot/rsrc/css/application/conpherence/update.css +++ /dev/null @@ -1,13 +0,0 @@ -/** - * @provides conpherence-update-css - */ - -.aphront-dialog-view .conpherence-dialogue-drag-photo { - border: 1px dashed #bfbfbf; - padding: 10px 0px 10px 10px; -} - -.aphront-dialog-view .conpherence-dialogue-upload-photo { - background: {$lightgreen}; - border-color: {$green}; -} diff --git a/webroot/rsrc/css/core/z-index.css b/webroot/rsrc/css/core/z-index.css index 3447a18de4..3bfff11070 100644 --- a/webroot/rsrc/css/core/z-index.css +++ b/webroot/rsrc/css/core/z-index.css @@ -68,9 +68,8 @@ div.phui-calendar-day-event { z-index: 4; } -.loading .messages-loading-mask, -.loading .widgets-loading-mask { - z-index: 5; +.conpherence-message-pane .conpherence-search-main { + z-index: 4; } .dark-console { @@ -89,6 +88,11 @@ div.phui-calendar-day-event { z-index: 6; } +.loading .messages-loading-mask, +.loading .widgets-loading-mask { + z-index: 6; +} + .conpherence-durable-column { z-index: 7; } @@ -154,6 +158,10 @@ div.jx-typeahead-results { z-index: 25; } +.conpherence-layout .conpherence-loading-mask { + z-index: 30; +} + .phuix-dropdown-menu { z-index: 32; } diff --git a/webroot/rsrc/css/phui/phui-icon.css b/webroot/rsrc/css/phui/phui-icon.css index 338ff4e4c3..1ceb4c3855 100644 --- a/webroot/rsrc/css/phui/phui-icon.css +++ b/webroot/rsrc/css/phui/phui-icon.css @@ -101,6 +101,16 @@ a.phui-icon-circle.hover-pink:hover .phui-icon-view { color: {$pink}; } +a.phui-icon-circle.hover-green:hover { + text-decoration: none; + border-color: {$green}; + cursor: pointer; +} + +a.phui-icon-circle.hover-green:hover .phui-icon-view { + color: {$green}; +} + /* - Icon in a Square ------------------------------------------------------- */ .phui-icon-view.phui-icon-square { diff --git a/webroot/rsrc/js/application/conpherence/behavior-conpherence-search.js b/webroot/rsrc/js/application/conpherence/behavior-conpherence-search.js new file mode 100644 index 0000000000..3566d03e23 --- /dev/null +++ b/webroot/rsrc/js/application/conpherence/behavior-conpherence-search.js @@ -0,0 +1,63 @@ +/** + * @provides javelin-behavior-conpherence-search + * @requires javelin-behavior + * javelin-dom + * javelin-util + * javelin-workflow + * javelin-stratcom + */ + +JX.behavior('conpherence-search', function(config) { + + var shown = true; + var request = null; + + function _toggleSearch(e) { + e.kill(); + var node = JX.$('conpherence-main-layout'); + + shown = !shown; + JX.DOM.alterClass(node, 'show-searchbar', !shown); + JX.Stratcom.invoke('resize'); + } + + function _doSearch(e) { + e.kill(); + var search_text = JX.$('conpherence-search-input').value; + var search_node = JX.$('conpherence-search-results'); + + if (request || !search_text) { + return; + } + + request = new JX.Request(config.searchURI, function(response) { + JX.DOM.setContent(search_node, JX.$H(response)); + request = null; + }); + request.setData({fulltext: search_text}); + request.send(); + + } + + JX.Stratcom.listen( + ['submit', 'didSyntheticSubmit'], + 'conpherence-search-input', + _doSearch); + + JX.Stratcom.listen( + 'keydown', + 'conpherence-search-input', + function(e) { + if (e.getSpecialKey() != 'return') { + return; + } + e.kill(); + _doSearch(e); + }); + + JX.Stratcom.listen( + 'click', + 'conpherence-search-toggle', + _toggleSearch); + +}); diff --git a/webroot/rsrc/js/application/conpherence/behavior-menu.js b/webroot/rsrc/js/application/conpherence/behavior-menu.js index 50add23158..97d232329e 100644 --- a/webroot/rsrc/js/application/conpherence/behavior-menu.js +++ b/webroot/rsrc/js/application/conpherence/behavior-menu.js @@ -36,12 +36,15 @@ JX.behavior('conpherence-menu', function(config) { }); threadManager.setDidLoadThreadCallback(function(r) { var header = JX.$H(r.header); + var search = JX.$H(r.search); var messages = JX.$H(r.transactions); var form = JX.$H(r.form); var root = JX.DOM.find(document, 'div', 'conpherence-layout'); var header_root = JX.DOM.find(root, 'div', 'conpherence-header-pane'); + var search_root = JX.DOM.find(root, 'div', 'conpherence-search-main'); var form_root = JX.DOM.find(root, 'div', 'conpherence-form'); JX.DOM.setContent(header_root, header); + JX.DOM.setContent(search_root, search); JX.DOM.setContent(scrollbar.getContentNode(), messages); JX.DOM.setContent(form_root, form); @@ -210,14 +213,8 @@ JX.behavior('conpherence-menu', function(config) { } function markThreadLoading(loading) { - var root = JX.DOM.find(document, 'div', 'conpherence-layout'); - var header_root = JX.DOM.find(root, 'div', 'conpherence-header-pane'); - var messages_root = JX.DOM.find(root, 'div', 'conpherence-message-pane'); - var form_root = JX.DOM.find(root, 'div', 'conpherence-form'); - - JX.DOM.alterClass(header_root, 'loading', loading); - JX.DOM.alterClass(messages_root, 'loading', loading); - JX.DOM.alterClass(form_root, 'loading', loading); + var root = JX.$('conpherence-main-layout'); + JX.DOM.alterClass(root, 'loading', loading); try { var textarea = JX.DOM.find(form, 'textarea'); @@ -378,38 +375,6 @@ JX.behavior('conpherence-menu', function(config) { selectThread(e.getNode('conpherence-menu-click'), true); }); - JX.Stratcom.listen('click', 'conpherence-edit-metadata', function (e) { - e.kill(); - var root = e.getNode('conpherence-layout'); - var form = JX.DOM.find(root, 'form', 'conpherence-pontificate'); - var data = e.getNodeData('conpherence-edit-metadata'); - var header = JX.DOM.find(root, 'div', 'conpherence-header-pane'); - var messages = scrollbar.getContentNode(); - - new JX.Workflow.newFromForm(form, data) - .setHandler(JX.bind(this, function(r) { - JX.DOM.appendContent(messages, JX.$H(r.transactions)); - _scrollMessageWindow(); - - JX.DOM.setContent( - header, - JX.$H(r.header) - ); - - try { - // update the menu entry - JX.DOM.replace( - JX.$(r.conpherence_phid + '-nav-item'), - JX.$H(r.nav_item) - ); - selectThreadByID(r.conpherence_phid + '-nav-item'); - } catch (ex) { - // Ignore; this view may not have a menu. - } - })) - .start(); - }); - /** * On devices, we just show a thread list, so we don't want to automatically * select or load any threads. On desktop, we automatically select the first From 8759f7e6ec62515fbefa53dde772eff7a477904a Mon Sep 17 00:00:00 2001 From: Mike Riley Date: Thu, 13 Oct 2016 20:48:24 +0000 Subject: [PATCH 16/55] Expose Drydock blueprints via Conduit Summary: This search engine ports cleanly to Conduit out of the box. Ref T11694 Test Plan: called the API method from the console, browsed blueprints in the ui Reviewers: epriestley, #blessed_reviewers Reviewed By: epriestley, #blessed_reviewers Subscribers: Korvin, epriestley Maniphest Tasks: T11694 Differential Revision: https://secure.phabricator.com/D16593 --- src/__phutil_library_map__.php | 3 ++ ...DrydockBlueprintSearchConduitAPIMethod.php | 18 +++++++++++ .../drydock/storage/DrydockBlueprint.php | 32 ++++++++++++++++++- 3 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 src/applications/drydock/conduit/DrydockBlueprintSearchConduitAPIMethod.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index fbae5beb97..5fee27238f 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -940,6 +940,7 @@ phutil_register_library_map(array( 'DrydockBlueprintNameNgrams' => 'applications/drydock/storage/DrydockBlueprintNameNgrams.php', 'DrydockBlueprintPHIDType' => 'applications/drydock/phid/DrydockBlueprintPHIDType.php', 'DrydockBlueprintQuery' => 'applications/drydock/query/DrydockBlueprintQuery.php', + 'DrydockBlueprintSearchConduitAPIMethod' => 'applications/drydock/conduit/DrydockBlueprintSearchConduitAPIMethod.php', 'DrydockBlueprintSearchEngine' => 'applications/drydock/query/DrydockBlueprintSearchEngine.php', 'DrydockBlueprintTransaction' => 'applications/drydock/storage/DrydockBlueprintTransaction.php', 'DrydockBlueprintTransactionQuery' => 'applications/drydock/query/DrydockBlueprintTransactionQuery.php', @@ -5512,6 +5513,7 @@ phutil_register_library_map(array( 'PhabricatorCustomFieldInterface', 'PhabricatorNgramsInterface', 'PhabricatorProjectInterface', + 'PhabricatorConduitResultInterface', ), 'DrydockBlueprintController' => 'DrydockController', 'DrydockBlueprintCoreCustomField' => array( @@ -5530,6 +5532,7 @@ phutil_register_library_map(array( 'DrydockBlueprintNameNgrams' => 'PhabricatorSearchNgrams', 'DrydockBlueprintPHIDType' => 'PhabricatorPHIDType', 'DrydockBlueprintQuery' => 'DrydockQuery', + 'DrydockBlueprintSearchConduitAPIMethod' => 'PhabricatorSearchEngineAPIMethod', 'DrydockBlueprintSearchEngine' => 'PhabricatorApplicationSearchEngine', 'DrydockBlueprintTransaction' => 'PhabricatorApplicationTransaction', 'DrydockBlueprintTransactionQuery' => 'PhabricatorApplicationTransactionQuery', diff --git a/src/applications/drydock/conduit/DrydockBlueprintSearchConduitAPIMethod.php b/src/applications/drydock/conduit/DrydockBlueprintSearchConduitAPIMethod.php new file mode 100644 index 0000000000..80f5019414 --- /dev/null +++ b/src/applications/drydock/conduit/DrydockBlueprintSearchConduitAPIMethod.php @@ -0,0 +1,18 @@ +setKey('name') + ->setType('string') + ->setDescription(pht('The name of this blueprint.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('type') + ->setType('string') + ->setDescription(pht('The type of resource this blueprint provides.')), + ); + } + + public function getFieldValuesForConduit() { + return array( + 'name' => $this->getBlueprintName(), + 'type' => $this->getImplementation()->getType(), + ); + } + + public function getConduitSearchAttachments() { + return array( + ); + } + } From 8247edff98e86515dc7da5f1046eb8743bad6b97 Mon Sep 17 00:00:00 2001 From: Mike Riley Date: Thu, 13 Oct 2016 21:07:02 +0000 Subject: [PATCH 17/55] Modularize Owners package transactions Summary: Converts Owners package transactions to modular transactions. Test Plan: - created a new package - edited all simple properties from the web ui - checked that project and user owners were added as reviewers appropriately to new diffs - inspected the change details for various types of path add / remove / update / reorder changes Reviewers: epriestley, #blessed_reviewers Reviewed By: epriestley, #blessed_reviewers Subscribers: Korvin, epriestley Differential Revision: https://secure.phabricator.com/D16651 --- src/__phutil_library_map__.php | 22 +- .../PhabricatorOwnersArchiveController.php | 3 +- .../PhabricatorOwnersPathsController.php | 2 +- .../PhabricatorOwnersPackageEditEngine.php | 21 +- ...bricatorOwnersPackageTransactionEditor.php | 342 ------------------ .../PhabricatorOwnersPackageTransaction.php | 254 +------------ ...icatorOwnersPackageAuditingTransaction.php | 32 ++ ...atorOwnersPackageAutoreviewTransaction.php | 56 +++ ...torOwnersPackageDescriptionTransaction.php | 29 ++ ...icatorOwnersPackageDominionTransaction.php | 56 +++ ...habricatorOwnersPackageNameTransaction.php | 52 +++ ...bricatorOwnersPackageOwnersTransaction.php | 76 ++++ ...abricatorOwnersPackagePathsTransaction.php | 189 ++++++++++ ...ricatorOwnersPackagePrimaryTransaction.php | 15 + ...bricatorOwnersPackageStatusTransaction.php | 29 ++ ...habricatorOwnersPackageTransactionType.php | 4 + 16 files changed, 578 insertions(+), 604 deletions(-) create mode 100644 src/applications/owners/xaction/PhabricatorOwnersPackageAuditingTransaction.php create mode 100644 src/applications/owners/xaction/PhabricatorOwnersPackageAutoreviewTransaction.php create mode 100644 src/applications/owners/xaction/PhabricatorOwnersPackageDescriptionTransaction.php create mode 100644 src/applications/owners/xaction/PhabricatorOwnersPackageDominionTransaction.php create mode 100644 src/applications/owners/xaction/PhabricatorOwnersPackageNameTransaction.php create mode 100644 src/applications/owners/xaction/PhabricatorOwnersPackageOwnersTransaction.php create mode 100644 src/applications/owners/xaction/PhabricatorOwnersPackagePathsTransaction.php create mode 100644 src/applications/owners/xaction/PhabricatorOwnersPackagePrimaryTransaction.php create mode 100644 src/applications/owners/xaction/PhabricatorOwnersPackageStatusTransaction.php create mode 100644 src/applications/owners/xaction/PhabricatorOwnersPackageTransactionType.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 5fee27238f..34d379c376 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -3017,20 +3017,30 @@ phutil_register_library_map(array( 'PhabricatorOwnersListController' => 'applications/owners/controller/PhabricatorOwnersListController.php', 'PhabricatorOwnersOwner' => 'applications/owners/storage/PhabricatorOwnersOwner.php', 'PhabricatorOwnersPackage' => 'applications/owners/storage/PhabricatorOwnersPackage.php', + 'PhabricatorOwnersPackageAuditingTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackageAuditingTransaction.php', + 'PhabricatorOwnersPackageAutoreviewTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackageAutoreviewTransaction.php', 'PhabricatorOwnersPackageDatasource' => 'applications/owners/typeahead/PhabricatorOwnersPackageDatasource.php', + 'PhabricatorOwnersPackageDescriptionTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackageDescriptionTransaction.php', + 'PhabricatorOwnersPackageDominionTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackageDominionTransaction.php', 'PhabricatorOwnersPackageEditEngine' => 'applications/owners/editor/PhabricatorOwnersPackageEditEngine.php', 'PhabricatorOwnersPackageFulltextEngine' => 'applications/owners/query/PhabricatorOwnersPackageFulltextEngine.php', 'PhabricatorOwnersPackageFunctionDatasource' => 'applications/owners/typeahead/PhabricatorOwnersPackageFunctionDatasource.php', 'PhabricatorOwnersPackageNameNgrams' => 'applications/owners/storage/PhabricatorOwnersPackageNameNgrams.php', + 'PhabricatorOwnersPackageNameTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackageNameTransaction.php', 'PhabricatorOwnersPackageOwnerDatasource' => 'applications/owners/typeahead/PhabricatorOwnersPackageOwnerDatasource.php', + 'PhabricatorOwnersPackageOwnersTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackageOwnersTransaction.php', 'PhabricatorOwnersPackagePHIDType' => 'applications/owners/phid/PhabricatorOwnersPackagePHIDType.php', + 'PhabricatorOwnersPackagePathsTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackagePathsTransaction.php', + 'PhabricatorOwnersPackagePrimaryTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackagePrimaryTransaction.php', 'PhabricatorOwnersPackageQuery' => 'applications/owners/query/PhabricatorOwnersPackageQuery.php', 'PhabricatorOwnersPackageRemarkupRule' => 'applications/owners/remarkup/PhabricatorOwnersPackageRemarkupRule.php', 'PhabricatorOwnersPackageSearchEngine' => 'applications/owners/query/PhabricatorOwnersPackageSearchEngine.php', + 'PhabricatorOwnersPackageStatusTransaction' => 'applications/owners/xaction/PhabricatorOwnersPackageStatusTransaction.php', 'PhabricatorOwnersPackageTestCase' => 'applications/owners/storage/__tests__/PhabricatorOwnersPackageTestCase.php', 'PhabricatorOwnersPackageTransaction' => 'applications/owners/storage/PhabricatorOwnersPackageTransaction.php', 'PhabricatorOwnersPackageTransactionEditor' => 'applications/owners/editor/PhabricatorOwnersPackageTransactionEditor.php', 'PhabricatorOwnersPackageTransactionQuery' => 'applications/owners/query/PhabricatorOwnersPackageTransactionQuery.php', + 'PhabricatorOwnersPackageTransactionType' => 'applications/owners/xaction/PhabricatorOwnersPackageTransactionType.php', 'PhabricatorOwnersPath' => 'applications/owners/storage/PhabricatorOwnersPath.php', 'PhabricatorOwnersPathsController' => 'applications/owners/controller/PhabricatorOwnersPathsController.php', 'PhabricatorOwnersPathsSearchEngineAttachment' => 'applications/owners/engineextension/PhabricatorOwnersPathsSearchEngineAttachment.php', @@ -7949,20 +7959,30 @@ phutil_register_library_map(array( 'PhabricatorFulltextInterface', 'PhabricatorNgramsInterface', ), + 'PhabricatorOwnersPackageAuditingTransaction' => 'PhabricatorOwnersPackageTransactionType', + 'PhabricatorOwnersPackageAutoreviewTransaction' => 'PhabricatorOwnersPackageTransactionType', 'PhabricatorOwnersPackageDatasource' => 'PhabricatorTypeaheadDatasource', + 'PhabricatorOwnersPackageDescriptionTransaction' => 'PhabricatorOwnersPackageTransactionType', + 'PhabricatorOwnersPackageDominionTransaction' => 'PhabricatorOwnersPackageTransactionType', 'PhabricatorOwnersPackageEditEngine' => 'PhabricatorEditEngine', 'PhabricatorOwnersPackageFulltextEngine' => 'PhabricatorFulltextEngine', 'PhabricatorOwnersPackageFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorOwnersPackageNameNgrams' => 'PhabricatorSearchNgrams', + 'PhabricatorOwnersPackageNameTransaction' => 'PhabricatorOwnersPackageTransactionType', 'PhabricatorOwnersPackageOwnerDatasource' => 'PhabricatorTypeaheadCompositeDatasource', + 'PhabricatorOwnersPackageOwnersTransaction' => 'PhabricatorOwnersPackageTransactionType', 'PhabricatorOwnersPackagePHIDType' => 'PhabricatorPHIDType', + 'PhabricatorOwnersPackagePathsTransaction' => 'PhabricatorOwnersPackageTransactionType', + 'PhabricatorOwnersPackagePrimaryTransaction' => 'PhabricatorOwnersPackageTransactionType', 'PhabricatorOwnersPackageQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorOwnersPackageRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'PhabricatorOwnersPackageSearchEngine' => 'PhabricatorApplicationSearchEngine', + 'PhabricatorOwnersPackageStatusTransaction' => 'PhabricatorOwnersPackageTransactionType', 'PhabricatorOwnersPackageTestCase' => 'PhabricatorTestCase', - 'PhabricatorOwnersPackageTransaction' => 'PhabricatorApplicationTransaction', + 'PhabricatorOwnersPackageTransaction' => 'PhabricatorModularTransaction', 'PhabricatorOwnersPackageTransactionEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorOwnersPackageTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'PhabricatorOwnersPackageTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorOwnersPath' => 'PhabricatorOwnersDAO', 'PhabricatorOwnersPathsController' => 'PhabricatorOwnersController', 'PhabricatorOwnersPathsSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', diff --git a/src/applications/owners/controller/PhabricatorOwnersArchiveController.php b/src/applications/owners/controller/PhabricatorOwnersArchiveController.php index 2ef618a2d5..47f9d87d23 100644 --- a/src/applications/owners/controller/PhabricatorOwnersArchiveController.php +++ b/src/applications/owners/controller/PhabricatorOwnersArchiveController.php @@ -31,8 +31,9 @@ final class PhabricatorOwnersArchiveController $xactions = array(); + $type = PhabricatorOwnersPackageStatusTransaction::TRANSACTIONTYPE; $xactions[] = id(new PhabricatorOwnersPackageTransaction()) - ->setTransactionType(PhabricatorOwnersPackageTransaction::TYPE_STATUS) + ->setTransactionType($type) ->setNewValue($new_status); id(new PhabricatorOwnersPackageTransactionEditor()) diff --git a/src/applications/owners/controller/PhabricatorOwnersPathsController.php b/src/applications/owners/controller/PhabricatorOwnersPathsController.php index 7f911d29d9..b69faf5648 100644 --- a/src/applications/owners/controller/PhabricatorOwnersPathsController.php +++ b/src/applications/owners/controller/PhabricatorOwnersPathsController.php @@ -48,7 +48,7 @@ final class PhabricatorOwnersPathsController ); } - $type_paths = PhabricatorOwnersPackageTransaction::TYPE_PATHS; + $type_paths = PhabricatorOwnersPackagePathsTransaction::TRANSACTIONTYPE; $xactions = array(); $xactions[] = id(new PhabricatorOwnersPackageTransaction()) diff --git a/src/applications/owners/editor/PhabricatorOwnersPackageEditEngine.php b/src/applications/owners/editor/PhabricatorOwnersPackageEditEngine.php index b20613c92e..4adaebc582 100644 --- a/src/applications/owners/editor/PhabricatorOwnersPackageEditEngine.php +++ b/src/applications/owners/editor/PhabricatorOwnersPackageEditEngine.php @@ -95,14 +95,16 @@ EOTEXT ->setKey('name') ->setLabel(pht('Name')) ->setDescription(pht('Name of the package.')) - ->setTransactionType(PhabricatorOwnersPackageTransaction::TYPE_NAME) + ->setTransactionType( + PhabricatorOwnersPackageNameTransaction::TRANSACTIONTYPE) ->setIsRequired(true) ->setValue($object->getName()), id(new PhabricatorDatasourceEditField()) ->setKey('owners') ->setLabel(pht('Owners')) ->setDescription(pht('Users and projects which own the package.')) - ->setTransactionType(PhabricatorOwnersPackageTransaction::TYPE_OWNERS) + ->setTransactionType( + PhabricatorOwnersPackageOwnersTransaction::TRANSACTIONTYPE) ->setDatasource(new PhabricatorProjectOrUserDatasource()) ->setIsCopyable(true) ->setValue($object->getOwnerPHIDs()), @@ -112,7 +114,7 @@ EOTEXT ->setDescription( pht('Change package dominion rules.')) ->setTransactionType( - PhabricatorOwnersPackageTransaction::TYPE_DOMINION) + PhabricatorOwnersPackageDominionTransaction::TRANSACTIONTYPE) ->setIsCopyable(true) ->setValue($object->getDominion()) ->setOptions($dominion_map), @@ -124,7 +126,7 @@ EOTEXT 'Automatically trigger reviews for commits affecting files in '. 'this package.')) ->setTransactionType( - PhabricatorOwnersPackageTransaction::TYPE_AUTOREVIEW) + PhabricatorOwnersPackageAutoreviewTransaction::TRANSACTIONTYPE) ->setIsCopyable(true) ->setValue($object->getAutoReview()) ->setOptions($autoreview_map), @@ -135,7 +137,8 @@ EOTEXT pht( 'Automatically trigger audits for commits affecting files in '. 'this package.')) - ->setTransactionType(PhabricatorOwnersPackageTransaction::TYPE_AUDITING) + ->setTransactionType( + PhabricatorOwnersPackageAuditingTransaction::TRANSACTIONTYPE) ->setIsCopyable(true) ->setValue($object->getAuditingEnabled()) ->setOptions( @@ -148,13 +151,14 @@ EOTEXT ->setLabel(pht('Description')) ->setDescription(pht('Human-readable description of the package.')) ->setTransactionType( - PhabricatorOwnersPackageTransaction::TYPE_DESCRIPTION) + PhabricatorOwnersPackageDescriptionTransaction::TRANSACTIONTYPE) ->setValue($object->getDescription()), id(new PhabricatorSelectEditField()) ->setKey('status') ->setLabel(pht('Status')) ->setDescription(pht('Archive or enable the package.')) - ->setTransactionType(PhabricatorOwnersPackageTransaction::TYPE_STATUS) + ->setTransactionType( + PhabricatorOwnersPackageStatusTransaction::TRANSACTIONTYPE) ->setIsConduitOnly(true) ->setValue($object->getStatus()) ->setOptions($object->getStatusNameMap()), @@ -162,7 +166,8 @@ EOTEXT ->setKey('paths.set') ->setLabel(pht('Paths')) ->setIsConduitOnly(true) - ->setTransactionType(PhabricatorOwnersPackageTransaction::TYPE_PATHS) + ->setTransactionType( + PhabricatorOwnersPackagePathsTransaction::TRANSACTIONTYPE) ->setConduitDescription( pht('Overwrite existing package paths with new paths.')) ->setConduitTypeDescription( diff --git a/src/applications/owners/editor/PhabricatorOwnersPackageTransactionEditor.php b/src/applications/owners/editor/PhabricatorOwnersPackageTransactionEditor.php index 5fb4a9af8a..40657abd57 100644 --- a/src/applications/owners/editor/PhabricatorOwnersPackageTransactionEditor.php +++ b/src/applications/owners/editor/PhabricatorOwnersPackageTransactionEditor.php @@ -14,354 +14,12 @@ final class PhabricatorOwnersPackageTransactionEditor public function getTransactionTypes() { $types = parent::getTransactionTypes(); - $types[] = PhabricatorOwnersPackageTransaction::TYPE_NAME; - $types[] = PhabricatorOwnersPackageTransaction::TYPE_OWNERS; - $types[] = PhabricatorOwnersPackageTransaction::TYPE_AUDITING; - $types[] = PhabricatorOwnersPackageTransaction::TYPE_DESCRIPTION; - $types[] = PhabricatorOwnersPackageTransaction::TYPE_PATHS; - $types[] = PhabricatorOwnersPackageTransaction::TYPE_STATUS; - $types[] = PhabricatorOwnersPackageTransaction::TYPE_AUTOREVIEW; - $types[] = PhabricatorOwnersPackageTransaction::TYPE_DOMINION; - $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; return $types; } - protected function getCustomTransactionOldValue( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case PhabricatorOwnersPackageTransaction::TYPE_NAME: - return $object->getName(); - case PhabricatorOwnersPackageTransaction::TYPE_OWNERS: - $phids = mpull($object->getOwners(), 'getUserPHID'); - $phids = array_values($phids); - return $phids; - case PhabricatorOwnersPackageTransaction::TYPE_AUDITING: - return (int)$object->getAuditingEnabled(); - case PhabricatorOwnersPackageTransaction::TYPE_DESCRIPTION: - return $object->getDescription(); - case PhabricatorOwnersPackageTransaction::TYPE_PATHS: - $paths = $object->getPaths(); - return mpull($paths, 'getRef'); - case PhabricatorOwnersPackageTransaction::TYPE_STATUS: - return $object->getStatus(); - case PhabricatorOwnersPackageTransaction::TYPE_AUTOREVIEW: - return $object->getAutoReview(); - case PhabricatorOwnersPackageTransaction::TYPE_DOMINION: - return $object->getDominion(); - } - } - - protected function getCustomTransactionNewValue( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case PhabricatorOwnersPackageTransaction::TYPE_NAME: - case PhabricatorOwnersPackageTransaction::TYPE_DESCRIPTION: - case PhabricatorOwnersPackageTransaction::TYPE_STATUS: - case PhabricatorOwnersPackageTransaction::TYPE_AUTOREVIEW: - case PhabricatorOwnersPackageTransaction::TYPE_DOMINION: - return $xaction->getNewValue(); - case PhabricatorOwnersPackageTransaction::TYPE_PATHS: - $new = $xaction->getNewValue(); - foreach ($new as $key => $info) { - $new[$key]['excluded'] = (int)idx($info, 'excluded'); - } - return $new; - case PhabricatorOwnersPackageTransaction::TYPE_AUDITING: - return (int)$xaction->getNewValue(); - case PhabricatorOwnersPackageTransaction::TYPE_OWNERS: - $phids = $xaction->getNewValue(); - $phids = array_unique($phids); - $phids = array_values($phids); - return $phids; - } - } - - protected function transactionHasEffect( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case PhabricatorOwnersPackageTransaction::TYPE_PATHS: - $old = $xaction->getOldValue(); - $new = $xaction->getNewValue(); - - $diffs = PhabricatorOwnersPath::getTransactionValueChanges($old, $new); - list($rem, $add) = $diffs; - - return ($rem || $add); - } - - return parent::transactionHasEffect($object, $xaction); - } - - protected function applyCustomInternalTransaction( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case PhabricatorOwnersPackageTransaction::TYPE_NAME: - $object->setName($xaction->getNewValue()); - return; - case PhabricatorOwnersPackageTransaction::TYPE_DESCRIPTION: - $object->setDescription($xaction->getNewValue()); - return; - case PhabricatorOwnersPackageTransaction::TYPE_AUDITING: - $object->setAuditingEnabled($xaction->getNewValue()); - return; - case PhabricatorOwnersPackageTransaction::TYPE_OWNERS: - case PhabricatorOwnersPackageTransaction::TYPE_PATHS: - return; - case PhabricatorOwnersPackageTransaction::TYPE_STATUS: - $object->setStatus($xaction->getNewValue()); - return; - case PhabricatorOwnersPackageTransaction::TYPE_AUTOREVIEW: - $object->setAutoReview($xaction->getNewValue()); - return; - case PhabricatorOwnersPackageTransaction::TYPE_DOMINION: - $object->setDominion($xaction->getNewValue()); - return; - } - - return parent::applyCustomInternalTransaction($object, $xaction); - } - - protected function applyCustomExternalTransaction( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - switch ($xaction->getTransactionType()) { - case PhabricatorOwnersPackageTransaction::TYPE_NAME: - case PhabricatorOwnersPackageTransaction::TYPE_DESCRIPTION: - case PhabricatorOwnersPackageTransaction::TYPE_AUDITING: - case PhabricatorOwnersPackageTransaction::TYPE_STATUS: - case PhabricatorOwnersPackageTransaction::TYPE_AUTOREVIEW: - case PhabricatorOwnersPackageTransaction::TYPE_DOMINION: - return; - case PhabricatorOwnersPackageTransaction::TYPE_OWNERS: - $old = $xaction->getOldValue(); - $new = $xaction->getNewValue(); - - $owners = $object->getOwners(); - $owners = mpull($owners, null, 'getUserPHID'); - - $rem = array_diff($old, $new); - foreach ($rem as $phid) { - if (isset($owners[$phid])) { - $owners[$phid]->delete(); - unset($owners[$phid]); - } - } - - $add = array_diff($new, $old); - foreach ($add as $phid) { - $owners[$phid] = id(new PhabricatorOwnersOwner()) - ->setPackageID($object->getID()) - ->setUserPHID($phid) - ->save(); - } - - // TODO: Attach owners here - return; - case PhabricatorOwnersPackageTransaction::TYPE_PATHS: - $old = $xaction->getOldValue(); - $new = $xaction->getNewValue(); - - $paths = $object->getPaths(); - - $diffs = PhabricatorOwnersPath::getTransactionValueChanges($old, $new); - list($rem, $add) = $diffs; - - $set = PhabricatorOwnersPath::getSetFromTransactionValue($rem); - foreach ($paths as $path) { - $ref = $path->getRef(); - if (PhabricatorOwnersPath::isRefInSet($ref, $set)) { - $path->delete(); - } - } - - foreach ($add as $ref) { - $path = PhabricatorOwnersPath::newFromRef($ref) - ->setPackageID($object->getID()) - ->save(); - } - - return; - } - - return parent::applyCustomExternalTransaction($object, $xaction); - } - - protected function validateTransaction( - PhabricatorLiskDAO $object, - $type, - array $xactions) { - - $errors = parent::validateTransaction($object, $type, $xactions); - - switch ($type) { - case PhabricatorOwnersPackageTransaction::TYPE_NAME: - $missing = $this->validateIsEmptyTextField( - $object->getName(), - $xactions); - - if ($missing) { - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Required'), - pht('Package name is required.'), - nonempty(last($xactions), null)); - - $error->setIsMissingFieldError(true); - $errors[] = $error; - } - - foreach ($xactions as $xaction) { - $new = $xaction->getNewValue(); - if (preg_match('([,!])', $new)) { - $errors[] = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Invalid'), - pht( - 'Package names may not contain commas (",") or exclamation '. - 'marks ("!"). These characters are ambiguous when package '. - 'names are parsed from the command line.'), - $xaction); - } - } - - break; - case PhabricatorOwnersPackageTransaction::TYPE_AUTOREVIEW: - $map = PhabricatorOwnersPackage::getAutoreviewOptionsMap(); - foreach ($xactions as $xaction) { - $new = $xaction->getNewValue(); - - if (empty($map[$new])) { - $valid = array_keys($map); - - $errors[] = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Invalid'), - pht( - 'Autoreview setting "%s" is not valid. '. - 'Valid settings are: %s.', - $new, - implode(', ', $valid)), - $xaction); - } - } - break; - case PhabricatorOwnersPackageTransaction::TYPE_DOMINION: - $map = PhabricatorOwnersPackage::getDominionOptionsMap(); - foreach ($xactions as $xaction) { - $new = $xaction->getNewValue(); - - if (empty($map[$new])) { - $valid = array_keys($map); - - $errors[] = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Invalid'), - pht( - 'Dominion setting "%s" is not valid. '. - 'Valid settings are: %s.', - $new, - implode(', ', $valid)), - $xaction); - } - } - break; - case PhabricatorOwnersPackageTransaction::TYPE_PATHS: - if (!$xactions) { - continue; - } - - $old = mpull($object->getPaths(), 'getRef'); - foreach ($xactions as $xaction) { - $new = $xaction->getNewValue(); - - // Check that we have a list of paths. - if (!is_array($new)) { - $errors[] = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Invalid'), - pht('Path specification must be a list of paths.'), - $xaction); - continue; - } - - // Check that each item in the list is formatted properly. - $type_exception = null; - foreach ($new as $key => $value) { - try { - PhutilTypeSpec::checkMap( - $value, - array( - 'repositoryPHID' => 'string', - 'path' => 'string', - 'excluded' => 'optional wild', - )); - } catch (PhutilTypeCheckException $ex) { - $errors[] = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Invalid'), - pht( - 'Path specification list contains invalid value '. - 'in key "%s": %s.', - $key, - $ex->getMessage()), - $xaction); - $type_exception = $ex; - } - } - - if ($type_exception) { - continue; - } - - // Check that any new paths reference legitimate repositories which - // the viewer has permission to see. - list($rem, $add) = PhabricatorOwnersPath::getTransactionValueChanges( - $old, - $new); - - if ($add) { - $repository_phids = ipull($add, 'repositoryPHID'); - - $repositories = id(new PhabricatorRepositoryQuery()) - ->setViewer($this->getActor()) - ->withPHIDs($repository_phids) - ->execute(); - $repositories = mpull($repositories, null, 'getPHID'); - - foreach ($add as $ref) { - $repository_phid = $ref['repositoryPHID']; - if (isset($repositories[$repository_phid])) { - continue; - } - - $errors[] = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Invalid'), - pht( - 'Path specification list references repository PHID "%s", '. - 'but that is not a valid, visible repository.', - $repository_phid)); - } - } - } - break; - } - - return $errors; - } - protected function shouldSendMail( PhabricatorLiskDAO $object, array $xactions) { diff --git a/src/applications/owners/storage/PhabricatorOwnersPackageTransaction.php b/src/applications/owners/storage/PhabricatorOwnersPackageTransaction.php index 359a1fcb8a..66e15b634a 100644 --- a/src/applications/owners/storage/PhabricatorOwnersPackageTransaction.php +++ b/src/applications/owners/storage/PhabricatorOwnersPackageTransaction.php @@ -1,17 +1,7 @@ getOldValue(); - $new = $this->getNewValue(); - - switch ($this->getTransactionType()) { - case self::TYPE_OWNERS: - if (!is_array($old)) { - $old = array(); - } - - if (!is_array($new)) { - $new = array(); - } - - $add = array_diff($new, $old); - foreach ($add as $phid) { - $phids[] = $phid; - } - $rem = array_diff($old, $new); - foreach ($rem as $phid) { - $phids[] = $phid; - } - break; - } - - return $phids; - } - - public function shouldHide() { - $old = $this->getOldValue(); - $new = $this->getNewValue(); - - switch ($this->getTransactionType()) { - case self::TYPE_DESCRIPTION: - if ($old === null) { - return true; - } - break; - case self::TYPE_PRIMARY: - // TODO: Eventually, remove these transactions entirely. - return true; - } - - return parent::shouldHide(); - } - - public function getTitle() { - $old = $this->getOldValue(); - $new = $this->getNewValue(); - $author_phid = $this->getAuthorPHID(); - - switch ($this->getTransactionType()) { - case PhabricatorTransactions::TYPE_CREATE: - return pht( - '%s created this package.', - $this->renderHandleLink($author_phid)); - case self::TYPE_NAME: - if ($old === null) { - return pht( - '%s created this package.', - $this->renderHandleLink($author_phid)); - } else { - return pht( - '%s renamed this package from "%s" to "%s".', - $this->renderHandleLink($author_phid), - $old, - $new); - } - case self::TYPE_OWNERS: - $add = array_diff($new, $old); - $rem = array_diff($old, $new); - if ($add && !$rem) { - return pht( - '%s added %s owner(s): %s.', - $this->renderHandleLink($author_phid), - count($add), - $this->renderHandleList($add)); - } else if ($rem && !$add) { - return pht( - '%s removed %s owner(s): %s.', - $this->renderHandleLink($author_phid), - count($rem), - $this->renderHandleList($rem)); - } else { - return pht( - '%s changed %s package owner(s), added %s: %s; removed %s: %s.', - $this->renderHandleLink($author_phid), - count($add) + count($rem), - count($add), - $this->renderHandleList($add), - count($rem), - $this->renderHandleList($rem)); - } - case self::TYPE_AUDITING: - if ($new) { - return pht( - '%s enabled auditing for this package.', - $this->renderHandleLink($author_phid)); - } else { - return pht( - '%s disabled auditing for this package.', - $this->renderHandleLink($author_phid)); - } - case self::TYPE_DESCRIPTION: - return pht( - '%s updated the description for this package.', - $this->renderHandleLink($author_phid)); - case self::TYPE_PATHS: - // TODO: Flesh this out. - return pht( - '%s updated paths for this package.', - $this->renderHandleLink($author_phid)); - case self::TYPE_STATUS: - if ($new == PhabricatorOwnersPackage::STATUS_ACTIVE) { - return pht( - '%s activated this package.', - $this->renderHandleLink($author_phid)); - } else if ($new == PhabricatorOwnersPackage::STATUS_ARCHIVED) { - return pht( - '%s archived this package.', - $this->renderHandleLink($author_phid)); - } - case self::TYPE_AUTOREVIEW: - $map = PhabricatorOwnersPackage::getAutoreviewOptionsMap(); - $map = ipull($map, 'name'); - - $old = idx($map, $old, $old); - $new = idx($map, $new, $new); - - return pht( - '%s adjusted autoreview from "%s" to "%s".', - $this->renderHandleLink($author_phid), - $old, - $new); - case self::TYPE_DOMINION: - $map = PhabricatorOwnersPackage::getDominionOptionsMap(); - $map = ipull($map, 'short'); - - $old = idx($map, $old, $old); - $new = idx($map, $new, $new); - - return pht( - '%s adjusted package dominion rules from "%s" to "%s".', - $this->renderHandleLink($author_phid), - $old, - $new); - } - - return parent::getTitle(); - } - - public function hasChangeDetails() { - switch ($this->getTransactionType()) { - case self::TYPE_DESCRIPTION: - return ($this->getOldValue() !== null); - case self::TYPE_PATHS: - return true; - } - - return parent::hasChangeDetails(); - } - - public function renderChangeDetails(PhabricatorUser $viewer) { - switch ($this->getTransactionType()) { - case self::TYPE_DESCRIPTION: - $old = $this->getOldValue(); - $new = $this->getNewValue(); - - return $this->renderTextCorpusChangeDetails( - $viewer, - $old, - $new); - case self::TYPE_PATHS: - $old = $this->getOldValue(); - $new = $this->getNewValue(); - - $diffs = PhabricatorOwnersPath::getTransactionValueChanges($old, $new); - list($rem, $add) = $diffs; - - $rows = array(); - foreach ($rem as $ref) { - $rows[] = array( - 'class' => 'diff-removed', - 'change' => '-', - ) + $ref; - } - - foreach ($add as $ref) { - $rows[] = array( - 'class' => 'diff-added', - 'change' => '+', - ) + $ref; - } - - $rowc = array(); - foreach ($rows as $key => $row) { - $rowc[] = $row['class']; - $rows[$key] = array( - $row['change'], - $row['excluded'] ? pht('Exclude') : pht('Include'), - $viewer->renderHandle($row['repositoryPHID']), - $row['path'], - ); - } - - $table = id(new AphrontTableView($rows)) - ->setRowClasses($rowc) - ->setHeaders( - array( - null, - pht('Type'), - pht('Repository'), - pht('Path'), - )) - ->setColumnClasses( - array( - null, - null, - null, - 'wide', - )); - - return $table; - } - - return parent::renderChangeDetails($viewer); - } - - public function getRemarkupBlocks() { - $blocks = parent::getRemarkupBlocks(); - - switch ($this->getTransactionType()) { - case self::TYPE_DESCRIPTION: - $blocks[] = $this->getNewValue(); - break; - } - - return $blocks; + public function getBaseTransactionClass() { + return 'PhabricatorOwnersPackageTransactionType'; } } diff --git a/src/applications/owners/xaction/PhabricatorOwnersPackageAuditingTransaction.php b/src/applications/owners/xaction/PhabricatorOwnersPackageAuditingTransaction.php new file mode 100644 index 0000000000..df4f0feb01 --- /dev/null +++ b/src/applications/owners/xaction/PhabricatorOwnersPackageAuditingTransaction.php @@ -0,0 +1,32 @@ +getAuditingEnabled(); + } + + public function generateNewValue($object, $value) { + return (int)$value; + } + + public function applyInternalEffects($object, $value) { + $object->setAuditingEnabled($value); + } + + public function getTitle() { + if ($this->getNewValue()) { + return pht( + '%s enabled auditing for this package.', + $this->renderAuthor()); + } else { + return pht( + '%s disabled auditing for this package.', + $this->renderAuthor()); + } + } + +} diff --git a/src/applications/owners/xaction/PhabricatorOwnersPackageAutoreviewTransaction.php b/src/applications/owners/xaction/PhabricatorOwnersPackageAutoreviewTransaction.php new file mode 100644 index 0000000000..03ec4f7af1 --- /dev/null +++ b/src/applications/owners/xaction/PhabricatorOwnersPackageAutoreviewTransaction.php @@ -0,0 +1,56 @@ +getAutoReview(); + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + $map = PhabricatorOwnersPackage::getAutoreviewOptionsMap(); + foreach ($xactions as $xaction) { + $new = $xaction->getNewValue(); + + if (empty($map[$new])) { + $valid = array_keys($map); + + $errors[] = $this->newInvalidError( + pht( + 'Autoreview setting "%s" is not valid. '. + 'Valid settings are: %s.', + $new, + implode(', ', $valid)), + $xaction); + } + } + + return $errors; + } + + public function applyInternalEffects($object, $value) { + $object->setAutoReview($value); + } + + public function getTitle() { + $map = PhabricatorOwnersPackage::getAutoreviewOptionsMap(); + $map = ipull($map, 'name'); + + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + $old = idx($map, $old, $old); + $new = idx($map, $new, $new); + + return pht( + '%s adjusted autoreview from %s to %s.', + $this->renderAuthor(), + $this->renderValue($old), + $this->renderValue($new)); + } + +} diff --git a/src/applications/owners/xaction/PhabricatorOwnersPackageDescriptionTransaction.php b/src/applications/owners/xaction/PhabricatorOwnersPackageDescriptionTransaction.php new file mode 100644 index 0000000000..8b2effe80c --- /dev/null +++ b/src/applications/owners/xaction/PhabricatorOwnersPackageDescriptionTransaction.php @@ -0,0 +1,29 @@ +getDescription(); + } + + public function applyInternalEffects($object, $value) { + $object->setDescription($value); + } + + public function getTitle() { + return pht( + '%s updated the description for this package.', + $this->renderAuthor()); + } + + public function newChangeDetailView() { + return id(new PhabricatorApplicationTransactionTextDiffDetailView()) + ->setViewer($this->getViewer()) + ->setOldText($this->getOldValue()) + ->setNewText($this->getNewValue()); + } + +} diff --git a/src/applications/owners/xaction/PhabricatorOwnersPackageDominionTransaction.php b/src/applications/owners/xaction/PhabricatorOwnersPackageDominionTransaction.php new file mode 100644 index 0000000000..0c1ff0a916 --- /dev/null +++ b/src/applications/owners/xaction/PhabricatorOwnersPackageDominionTransaction.php @@ -0,0 +1,56 @@ +getDominion(); + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + $map = PhabricatorOwnersPackage::getDominionOptionsMap(); + foreach ($xactions as $xaction) { + $new = $xaction->getNewValue(); + + if (empty($map[$new])) { + $valid = array_keys($map); + + $errors[] = $this->newInvalidError( + pht( + 'Dominion setting "%s" is not valid. '. + 'Valid settings are: %s.', + $new, + implode(', ', $valid)), + $xaction); + } + } + + return $errors; + } + + public function applyInternalEffects($object, $value) { + $object->setDominion($value); + } + + public function getTitle() { + $map = PhabricatorOwnersPackage::getDominionOptionsMap(); + $map = ipull($map, 'short'); + + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + $old = idx($map, $old, $old); + $new = idx($map, $new, $new); + + return pht( + '%s adjusted package dominion rules from %s to %s.', + $this->renderAuthor(), + $this->renderValue($old), + $this->renderValue($new)); + } + +} diff --git a/src/applications/owners/xaction/PhabricatorOwnersPackageNameTransaction.php b/src/applications/owners/xaction/PhabricatorOwnersPackageNameTransaction.php new file mode 100644 index 0000000000..b4e50daec2 --- /dev/null +++ b/src/applications/owners/xaction/PhabricatorOwnersPackageNameTransaction.php @@ -0,0 +1,52 @@ +getName(); + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + $missing = $this->isEmptyTextTransaction( + $object->getName(), + $xactions); + + if ($missing) { + $errors[] = $this->newRequiredError( + pht('Package name is required.'), + nonempty(last($xactions), null)); + } + + foreach ($xactions as $xaction) { + $new = $xaction->getNewValue(); + if (preg_match('([,!])', $new)) { + $errors[] = $this->newInvalidError( + pht( + 'Package names may not contain commas (",") or exclamation '. + 'marks ("!"). These characters are ambiguous when package '. + 'names are parsed from the command line.'), + $xaction); + } + } + + return $errors; + } + + public function applyInternalEffects($object, $value) { + $object->setName($value); + } + + public function getTitle() { + return pht( + '%s renamed this package from %s to %s.', + $this->renderAuthor(), + $this->renderOldValue(), + $this->renderNewValue()); + } + +} diff --git a/src/applications/owners/xaction/PhabricatorOwnersPackageOwnersTransaction.php b/src/applications/owners/xaction/PhabricatorOwnersPackageOwnersTransaction.php new file mode 100644 index 0000000000..7fef0d5e6c --- /dev/null +++ b/src/applications/owners/xaction/PhabricatorOwnersPackageOwnersTransaction.php @@ -0,0 +1,76 @@ +getOwners(), 'getUserPHID'); + $phids = array_values($phids); + return $phids; + } + + public function generateNewValue($object, $value) { + $phids = array_unique($value); + $phids = array_values($phids); + return $phids; + } + + public function applyExternalEffects($object, $value) { + $old = $this->generateOldValue($object); + $new = $value; + + $owners = $object->getOwners(); + $owners = mpull($owners, null, 'getUserPHID'); + + $rem = array_diff($old, $new); + foreach ($rem as $phid) { + if (isset($owners[$phid])) { + $owners[$phid]->delete(); + unset($owners[$phid]); + } + } + + $add = array_diff($new, $old); + foreach ($add as $phid) { + $owners[$phid] = id(new PhabricatorOwnersOwner()) + ->setPackageID($object->getID()) + ->setUserPHID($phid) + ->save(); + } + + // TODO: Attach owners here + } + + public function getTitle() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + $add = array_diff($new, $old); + $rem = array_diff($old, $new); + if ($add && !$rem) { + return pht( + '%s added %s owner(s): %s.', + $this->renderAuthor(), + count($add), + $this->renderHandleList($add)); + } else if ($rem && !$add) { + return pht( + '%s removed %s owner(s): %s.', + $this->renderAuthor(), + count($rem), + $this->renderHandleList($rem)); + } else { + return pht( + '%s changed %s package owner(s), added %s: %s; removed %s: %s.', + $this->renderAuthor(), + count($add) + count($rem), + count($add), + $this->renderHandleList($add), + count($rem), + $this->renderHandleList($rem)); + } + } + +} diff --git a/src/applications/owners/xaction/PhabricatorOwnersPackagePathsTransaction.php b/src/applications/owners/xaction/PhabricatorOwnersPackagePathsTransaction.php new file mode 100644 index 0000000000..5952b78aed --- /dev/null +++ b/src/applications/owners/xaction/PhabricatorOwnersPackagePathsTransaction.php @@ -0,0 +1,189 @@ +getPaths(); + return mpull($paths, 'getRef'); + } + + public function generateNewValue($object, $value) { + $new = $value; + foreach ($new as $key => $info) { + $new[$key]['excluded'] = (int)idx($info, 'excluded'); + } + return $new; + } + + public function validateTransactions($object, array $xactions) { + $errors = array(); + + if (!$xactions) { + return $errors; + } + + $old = mpull($object->getPaths(), 'getRef'); + foreach ($xactions as $xaction) { + $new = $xaction->getNewValue(); + + // Check that we have a list of paths. + if (!is_array($new)) { + $errors[] = $this->newInvalidError( + pht('Path specification must be a list of paths.'), + $xaction); + continue; + } + + // Check that each item in the list is formatted properly. + $type_exception = null; + foreach ($new as $key => $value) { + try { + PhutilTypeSpec::checkMap( + $value, + array( + 'repositoryPHID' => 'string', + 'path' => 'string', + 'excluded' => 'optional wild', + )); + } catch (PhutilTypeCheckException $ex) { + $errors[] = $this->newInvalidError( + pht( + 'Path specification list contains invalid value '. + 'in key "%s": %s.', + $key, + $ex->getMessage()), + $xaction); + $type_exception = $ex; + } + } + + if ($type_exception) { + continue; + } + + // Check that any new paths reference legitimate repositories which + // the viewer has permission to see. + list($rem, $add) = PhabricatorOwnersPath::getTransactionValueChanges( + $old, + $new); + + if ($add) { + $repository_phids = ipull($add, 'repositoryPHID'); + + $repositories = id(new PhabricatorRepositoryQuery()) + ->setViewer($this->getActor()) + ->withPHIDs($repository_phids) + ->execute(); + $repositories = mpull($repositories, null, 'getPHID'); + + foreach ($add as $ref) { + $repository_phid = $ref['repositoryPHID']; + if (isset($repositories[$repository_phid])) { + continue; + } + + $errors[] = $this->newInvalidError( + pht( + 'Path specification list references repository PHID "%s", '. + 'but that is not a valid, visible repository.', + $repository_phid)); + } + } + } + + return $errors; + } + + public function applyExternalEffects($object, $value) { + $old = $this->generateOldValue($object); + $new = $value; + + $paths = $object->getPaths(); + + $diffs = PhabricatorOwnersPath::getTransactionValueChanges($old, $new); + list($rem, $add) = $diffs; + + $set = PhabricatorOwnersPath::getSetFromTransactionValue($rem); + foreach ($paths as $path) { + $ref = $path->getRef(); + if (PhabricatorOwnersPath::isRefInSet($ref, $set)) { + $path->delete(); + } + } + + foreach ($add as $ref) { + $path = PhabricatorOwnersPath::newFromRef($ref) + ->setPackageID($object->getID()) + ->save(); + } + } + + public function getTitle() { + // TODO: Flesh this out. + return pht( + '%s updated paths for this package.', + $this->renderAuthor()); + } + + public function hasChangeDetailView() { + return true; + } + + public function newChangeDetailView() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + $diffs = PhabricatorOwnersPath::getTransactionValueChanges($old, $new); + list($rem, $add) = $diffs; + + $rows = array(); + foreach ($rem as $ref) { + $rows[] = array( + 'class' => 'diff-removed', + 'change' => '-', + ) + $ref; + } + + foreach ($add as $ref) { + $rows[] = array( + 'class' => 'diff-added', + 'change' => '+', + ) + $ref; + } + + $rowc = array(); + foreach ($rows as $key => $row) { + $rowc[] = $row['class']; + $rows[$key] = array( + $row['change'], + $row['excluded'] ? pht('Exclude') : pht('Include'), + $this->renderHandle($row['repositoryPHID']), + $row['path'], + ); + } + + $table = id(new AphrontTableView($rows)) + ->setViewer($this->getViewer()) + ->setRowClasses($rowc) + ->setHeaders( + array( + null, + pht('Type'), + pht('Repository'), + pht('Path'), + )) + ->setColumnClasses( + array( + null, + null, + null, + 'wide', + )); + + return $table; + } + +} diff --git a/src/applications/owners/xaction/PhabricatorOwnersPackagePrimaryTransaction.php b/src/applications/owners/xaction/PhabricatorOwnersPackagePrimaryTransaction.php new file mode 100644 index 0000000000..76d3c2f10f --- /dev/null +++ b/src/applications/owners/xaction/PhabricatorOwnersPackagePrimaryTransaction.php @@ -0,0 +1,15 @@ +getStatus(); + } + + public function applyInternalEffects($object, $value) { + $object->setStatus($value); + } + + public function getTitle() { + $new = $this->getNewValue(); + if ($new == PhabricatorOwnersPackage::STATUS_ACTIVE) { + return pht( + '%s activated this package.', + $this->renderAuthor()); + } else if ($new == PhabricatorOwnersPackage::STATUS_ARCHIVED) { + return pht( + '%s archived this package.', + $this->renderAuthor()); + } + } + +} diff --git a/src/applications/owners/xaction/PhabricatorOwnersPackageTransactionType.php b/src/applications/owners/xaction/PhabricatorOwnersPackageTransactionType.php new file mode 100644 index 0000000000..5faaa79a17 --- /dev/null +++ b/src/applications/owners/xaction/PhabricatorOwnersPackageTransactionType.php @@ -0,0 +1,4 @@ + Date: Thu, 13 Oct 2016 19:48:41 -0700 Subject: [PATCH 18/55] Only show loading animation on thread change in Conpherence Summary: Fixes the send on enter flash, only uses the `Threads` loading animation on changing threads, not sending a message. Test Plan: Change threads, post a message. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D16705 --- resources/celerity/map.php | 34 +++++++++---------- .../application/conpherence/behavior-menu.js | 12 +++---- 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index b012c7904e..5094fb88e9 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -8,7 +8,7 @@ return array( 'names' => array( 'conpherence.pkg.css' => 'c839a862', - 'conpherence.pkg.js' => 'b18c9dc5', + 'conpherence.pkg.js' => 'cbe4d9be', 'core.pkg.css' => 'b99bbf5e', 'core.pkg.js' => '30185d95', 'darkconsole.pkg.js' => 'e7393ebb', @@ -438,7 +438,7 @@ return array( 'rsrc/js/application/conpherence/ConpherenceThreadManager.js' => '01774ab2', 'rsrc/js/application/conpherence/behavior-conpherence-search.js' => '3bc9d2b1', 'rsrc/js/application/conpherence/behavior-durable-column.js' => 'c5238acb', - 'rsrc/js/application/conpherence/behavior-menu.js' => '0f82ba76', + 'rsrc/js/application/conpherence/behavior-menu.js' => '07928ca3', 'rsrc/js/application/conpherence/behavior-participant-pane.js' => '8604caa8', 'rsrc/js/application/conpherence/behavior-pontificate.js' => 'f2e58483', 'rsrc/js/application/conpherence/behavior-quicksand-blacklist.js' => '7927a7d3', @@ -663,7 +663,7 @@ return array( 'javelin-behavior-choose-control' => '327a00d1', 'javelin-behavior-comment-actions' => '0300eae6', 'javelin-behavior-config-reorder-fields' => 'b6993408', - 'javelin-behavior-conpherence-menu' => '0f82ba76', + 'javelin-behavior-conpherence-menu' => '07928ca3', 'javelin-behavior-conpherence-participant-pane' => '8604caa8', 'javelin-behavior-conpherence-pontificate' => 'f2e58483', 'javelin-behavior-conpherence-search' => '3bc9d2b1', @@ -1040,6 +1040,20 @@ return array( 'phabricator-prefab', 'phuix-icon-view', ), + '07928ca3' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-util', + 'javelin-stratcom', + 'javelin-workflow', + 'javelin-behavior-device', + 'javelin-history', + 'javelin-vector', + 'javelin-scrollbar', + 'phabricator-title', + 'phabricator-shaped-request', + 'conpherence-thread-manager', + ), '08675c6d' => array( 'javelin-behavior', 'javelin-behavior-device', @@ -1072,20 +1086,6 @@ return array( 'javelin-install', 'javelin-util', ), - '0f82ba76' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-util', - 'javelin-stratcom', - 'javelin-workflow', - 'javelin-behavior-device', - 'javelin-history', - 'javelin-vector', - 'javelin-scrollbar', - 'phabricator-title', - 'phabricator-shaped-request', - 'conpherence-thread-manager', - ), '116cf19b' => array( 'javelin-behavior', 'javelin-stratcom', diff --git a/webroot/rsrc/js/application/conpherence/behavior-menu.js b/webroot/rsrc/js/application/conpherence/behavior-menu.js index 97d232329e..844a6891f2 100644 --- a/webroot/rsrc/js/application/conpherence/behavior-menu.js +++ b/webroot/rsrc/js/application/conpherence/behavior-menu.js @@ -32,7 +32,7 @@ JX.behavior('conpherence-menu', function(config) { return scrollbar.getContentNode(); }); threadManager.setWillLoadThreadCallback(function() { - markThreadLoading(true); + markThreadsLoading(true); }); threadManager.setDidLoadThreadCallback(function(r) { var header = JX.$H(r.header); @@ -48,7 +48,7 @@ JX.behavior('conpherence-menu', function(config) { JX.DOM.setContent(scrollbar.getContentNode(), messages); JX.DOM.setContent(form_root, form); - markThreadLoading(false); + markThreadsLoading(false); didRedrawThread(true); }); @@ -207,15 +207,11 @@ JX.behavior('conpherence-menu', function(config) { } function markThreadsLoading(loading) { - var root = JX.DOM.find(document, 'div', 'conpherence-layout'); - var menu = JX.DOM.find(root, 'div', 'conpherence-menu-pane'); - JX.DOM.alterClass(menu, 'loading', loading); + var root = JX.$('conpherence-main-layout'); + JX.DOM.alterClass(root, 'loading', loading); } function markThreadLoading(loading) { - var root = JX.$('conpherence-main-layout'); - JX.DOM.alterClass(root, 'loading', loading); - try { var textarea = JX.DOM.find(form, 'textarea'); textarea.disabled = loading; From c71bb0550c5d42a0fc3c801b9de70fbd1aa7fe96 Mon Sep 17 00:00:00 2001 From: Giedrius Dubinskas Date: Fri, 14 Oct 2016 14:45:57 +0000 Subject: [PATCH 19/55] Conduit accept int/bool parameters as strings Summary: Accept Conduit parameter values as strings (e.g. from `curl`) and convert to required type. Test Plan: Call conduit method with int/bool parameter iusing `curl` and make sure it does not result in validation error, e.g. ``` $ curl http://$PHABRICATOR_HOST/api/maniphest.search -d api.token=$CONDUIT_TOKEN -d constraints[modifiedEnd]=$(date +%s) -d constraints[hasParents]=true -d limit=1 ``` Fixes T10456. Reviewers: epriestley, #blessed_reviewers Reviewed By: epriestley, #blessed_reviewers Subscribers: Korvin Maniphest Tasks: T10456 Differential Revision: https://secure.phabricator.com/D16694 --- src/applications/conduit/call/ConduitCall.php | 4 +- .../PhabricatorConduitAPIController.php | 12 +++-- .../ConduitBoolParameterType.php | 14 ++--- .../ConduitColumnsParameterType.php | 4 +- .../ConduitEpochParameterType.php | 12 ++--- .../ConduitIntListParameterType.php | 14 ++--- .../parametertype/ConduitIntParameterType.php | 14 ++--- .../ConduitListParameterType.php | 25 ++++----- .../ConduitPHIDListParameterType.php | 6 +-- .../ConduitPHIDParameterType.php | 4 +- .../parametertype/ConduitParameterType.php | 53 +++++++++++++++++-- .../ConduitPointsParameterType.php | 4 +- .../ConduitProjectListParameterType.php | 6 +-- .../ConduitStringListParameterType.php | 6 +-- .../ConduitStringParameterType.php | 14 ++--- .../ConduitUserListParameterType.php | 6 +-- .../ConduitUserParameterType.php | 4 +- .../conduit/protocol/ConduitAPIRequest.php | 8 ++- .../PhabricatorApplicationSearchEngine.php | 4 +- .../search/field/PhabricatorSearchField.php | 8 ++- .../editengine/PhabricatorEditEngine.php | 5 +- 21 files changed, 127 insertions(+), 100 deletions(-) diff --git a/src/applications/conduit/call/ConduitCall.php b/src/applications/conduit/call/ConduitCall.php index 017d96ae8b..6be49daef0 100644 --- a/src/applications/conduit/call/ConduitCall.php +++ b/src/applications/conduit/call/ConduitCall.php @@ -15,7 +15,7 @@ final class ConduitCall extends Phobject { private $request; private $user; - public function __construct($method, array $params) { + public function __construct($method, array $params, $strictly_typed = true) { $this->method = $method; $this->handler = $this->buildMethodHandler($method); @@ -41,7 +41,7 @@ final class ConduitCall extends Phobject { "'".implode("', '", array_keys($invalid_params))."'")); } - $this->request = new ConduitAPIRequest($params); + $this->request = new ConduitAPIRequest($params, $strictly_typed); } public function getAPIRequest() { diff --git a/src/applications/conduit/controller/PhabricatorConduitAPIController.php b/src/applications/conduit/controller/PhabricatorConduitAPIController.php index b9e8b1b15e..991865b564 100644 --- a/src/applications/conduit/controller/PhabricatorConduitAPIController.php +++ b/src/applications/conduit/controller/PhabricatorConduitAPIController.php @@ -25,9 +25,11 @@ final class PhabricatorConduitAPIController try { - list($metadata, $params) = $this->decodeConduitParams($request, $method); + list($metadata, $params, $strictly_typed) = $this->decodeConduitParams( + $request, + $method); - $call = new ConduitCall($method, $params); + $call = new ConduitCall($method, $params, $strictly_typed); $method_implementation = $call->getMethodImplementation(); $result = null; @@ -638,7 +640,7 @@ final class PhabricatorConduitAPIController $metadata = idx($params, '__conduit__', array()); unset($params['__conduit__']); - return array($metadata, $params); + return array($metadata, $params, true); } // Otherwise, look for a single parameter called 'params' which has the @@ -659,7 +661,7 @@ final class PhabricatorConduitAPIController $metadata = idx($params, '__conduit__', array()); unset($params['__conduit__']); - return array($metadata, $params); + return array($metadata, $params, true); } // If we do not have `params`, assume this is a simple HTTP request with @@ -675,7 +677,7 @@ final class PhabricatorConduitAPIController } } - return array($metadata, $params); + return array($metadata, $params, false); } private function authorizeOAuthMethodAccess( diff --git a/src/applications/conduit/parametertype/ConduitBoolParameterType.php b/src/applications/conduit/parametertype/ConduitBoolParameterType.php index fe1564350f..7ad9dd13e5 100644 --- a/src/applications/conduit/parametertype/ConduitBoolParameterType.php +++ b/src/applications/conduit/parametertype/ConduitBoolParameterType.php @@ -3,17 +3,9 @@ final class ConduitBoolParameterType extends ConduitParameterType { - protected function getParameterValue(array $request, $key) { - $value = parent::getParameterValue($request, $key); - - if (!is_bool($value)) { - $this->raiseValidationException( - $request, - $key, - pht('Expected boolean (true or false), got something else.')); - } - - return $value; + protected function getParameterValue(array $request, $key, $strict) { + $value = parent::getParameterValue($request, $key, $strict); + return $this->parseBoolValue($request, $key, $value, $strict); } protected function getParameterTypeName() { diff --git a/src/applications/conduit/parametertype/ConduitColumnsParameterType.php b/src/applications/conduit/parametertype/ConduitColumnsParameterType.php index c6669fae06..1892747892 100644 --- a/src/applications/conduit/parametertype/ConduitColumnsParameterType.php +++ b/src/applications/conduit/parametertype/ConduitColumnsParameterType.php @@ -3,10 +3,10 @@ final class ConduitColumnsParameterType extends ConduitParameterType { - protected function getParameterValue(array $request, $key) { + protected function getParameterValue(array $request, $key, $strict) { // We don't do any meaningful validation here because the transaction // itself validates everything and the input format is flexible. - return parent::getParameterValue($request, $key); + return parent::getParameterValue($request, $key, $strict); } protected function getParameterTypeName() { diff --git a/src/applications/conduit/parametertype/ConduitEpochParameterType.php b/src/applications/conduit/parametertype/ConduitEpochParameterType.php index 1594186e5c..e8fe095c50 100644 --- a/src/applications/conduit/parametertype/ConduitEpochParameterType.php +++ b/src/applications/conduit/parametertype/ConduitEpochParameterType.php @@ -3,15 +3,9 @@ final class ConduitEpochParameterType extends ConduitParameterType { - protected function getParameterValue(array $request, $key) { - $value = parent::getParameterValue($request, $key); - - if (!is_int($value)) { - $this->raiseValidationException( - $request, - $key, - pht('Expected epoch timestamp as integer, got something else.')); - } + protected function getParameterValue(array $request, $key, $strict) { + $value = parent::getParameterValue($request, $key, $strict); + $value = $this->parseIntValue($request, $key, $value, $strict); if ($value <= 0) { $this->raiseValidationException( diff --git a/src/applications/conduit/parametertype/ConduitIntListParameterType.php b/src/applications/conduit/parametertype/ConduitIntListParameterType.php index 07c87dcd8a..7733977d0e 100644 --- a/src/applications/conduit/parametertype/ConduitIntListParameterType.php +++ b/src/applications/conduit/parametertype/ConduitIntListParameterType.php @@ -3,19 +3,11 @@ final class ConduitIntListParameterType extends ConduitListParameterType { - protected function getParameterValue(array $request, $key) { - $list = parent::getParameterValue($request, $key); + protected function getParameterValue(array $request, $key, $strict) { + $list = parent::getParameterValue($request, $key, $strict); foreach ($list as $idx => $item) { - if (!is_int($item)) { - $this->raiseValidationException( - $request, - $key, - pht( - 'Expected a list of integers, but item with index "%s" is '. - 'not an integer.', - $idx)); - } + $list[$idx] = $this->parseIntValue($request, $key.'['.$idx.']', $item); } return $list; diff --git a/src/applications/conduit/parametertype/ConduitIntParameterType.php b/src/applications/conduit/parametertype/ConduitIntParameterType.php index 54f66fdf6c..e0d91e5d93 100644 --- a/src/applications/conduit/parametertype/ConduitIntParameterType.php +++ b/src/applications/conduit/parametertype/ConduitIntParameterType.php @@ -3,17 +3,9 @@ final class ConduitIntParameterType extends ConduitParameterType { - protected function getParameterValue(array $request, $key) { - $value = parent::getParameterValue($request, $key); - - if (!is_int($value)) { - $this->raiseValidationException( - $request, - $key, - pht('Expected integer, got something else.')); - } - - return $value; + protected function getParameterValue(array $request, $key, $strict) { + $value = parent::getParameterValue($request, $key, $strict); + return $this->parseIntValue($request, $key, $value, $strict); } protected function getParameterTypeName() { diff --git a/src/applications/conduit/parametertype/ConduitListParameterType.php b/src/applications/conduit/parametertype/ConduitListParameterType.php index 6ec3898ac2..aebadeb175 100644 --- a/src/applications/conduit/parametertype/ConduitListParameterType.php +++ b/src/applications/conduit/parametertype/ConduitListParameterType.php @@ -14,8 +14,8 @@ abstract class ConduitListParameterType return $this->allowEmptyList; } - protected function getParameterValue(array $request, $key) { - $value = parent::getParameterValue($request, $key); + protected function getParameterValue(array $request, $key, $strict) { + $value = parent::getParameterValue($request, $key, $strict); if (!is_array($value)) { $this->raiseValidationException( @@ -48,17 +48,18 @@ abstract class ConduitListParameterType return $value; } - protected function validateStringList(array $request, $key, array $list) { + protected function parseStringList( + array $request, + $key, + array $list, + $strict) { + foreach ($list as $idx => $item) { - if (!is_string($item)) { - $this->raiseValidationException( - $request, - $key, - pht( - 'Expected a list of strings, but item with index "%s" is '. - 'not a string.', - $idx)); - } + $list[$idx] = $this->parseStringValue( + $request, + $key.'['.$idx.']', + $item, + $strict); } return $list; diff --git a/src/applications/conduit/parametertype/ConduitPHIDListParameterType.php b/src/applications/conduit/parametertype/ConduitPHIDListParameterType.php index 60199dbe45..bbe89b6d43 100644 --- a/src/applications/conduit/parametertype/ConduitPHIDListParameterType.php +++ b/src/applications/conduit/parametertype/ConduitPHIDListParameterType.php @@ -3,9 +3,9 @@ final class ConduitPHIDListParameterType extends ConduitListParameterType { - protected function getParameterValue(array $request, $key) { - $list = parent::getParameterValue($request, $key); - return $this->validateStringList($request, $key, $list); + protected function getParameterValue(array $request, $key, $strict) { + $list = parent::getParameterValue($request, $key, $strict); + return $this->parseStringList($request, $key, $list, $strict); } protected function getParameterTypeName() { diff --git a/src/applications/conduit/parametertype/ConduitPHIDParameterType.php b/src/applications/conduit/parametertype/ConduitPHIDParameterType.php index f182758071..3bb45697dc 100644 --- a/src/applications/conduit/parametertype/ConduitPHIDParameterType.php +++ b/src/applications/conduit/parametertype/ConduitPHIDParameterType.php @@ -3,8 +3,8 @@ final class ConduitPHIDParameterType extends ConduitParameterType { - protected function getParameterValue(array $request, $key) { - $value = parent::getParameterValue($request, $key); + protected function getParameterValue(array $request, $key, $strict) { + $value = parent::getParameterValue($request, $key, $strict); if (!is_string($value)) { $this->raiseValidationException( diff --git a/src/applications/conduit/parametertype/ConduitParameterType.php b/src/applications/conduit/parametertype/ConduitParameterType.php index 011401433e..4eca31d96c 100644 --- a/src/applications/conduit/parametertype/ConduitParameterType.php +++ b/src/applications/conduit/parametertype/ConduitParameterType.php @@ -30,12 +30,12 @@ abstract class ConduitParameterType extends Phobject { } - final public function getValue(array $request, $key) { + final public function getValue(array $request, $key, $strict = true) { if (!$this->getExists($request, $key)) { return $this->getParameterDefault(); } - return $this->getParameterValue($request, $key); + return $this->getParameterValue($request, $key, $strict); } final public function getKeys($key) { @@ -85,7 +85,7 @@ abstract class ConduitParameterType extends Phobject { return array_key_exists($key, $request); } - protected function getParameterValue(array $request, $key) { + protected function getParameterValue(array $request, $key, $strict) { return $request[$key]; } @@ -93,6 +93,53 @@ abstract class ConduitParameterType extends Phobject { return array($key); } + protected function parseStringValue(array $request, $key, $value, $strict) { + if (!is_string($value)) { + $this->raiseValidationException( + $request, + $key, + pht('Expected string, got something else.')); + } + return $value; + } + + protected function parseIntValue(array $request, $key, $value, $strict) { + if (!$strict && is_string($value) && ctype_digit($value)) { + $value = $value + 0; + if (!is_int($value)) { + $this->raiseValidationException( + $request, + $key, + pht('Integer overflow.')); + } + } else if (!is_int($value)) { + $this->raiseValidationException( + $request, + $key, + pht('Expected integer, got something else.')); + } + return $value; + } + + protected function parseBoolValue(array $request, $key, $value, $strict) { + $bool_strings = array( + '0' => false, + '1' => true, + 'false' => false, + 'true' => true, + ); + + if (!$strict && is_string($value) && isset($bool_strings[$value])) { + $value = $bool_strings[$value]; + } else if (!is_bool($value)) { + $this->raiseValidationException( + $request, + $key, + pht('Expected boolean (true or false), got something else.')); + } + return $value; + } + abstract protected function getParameterTypeName(); diff --git a/src/applications/conduit/parametertype/ConduitPointsParameterType.php b/src/applications/conduit/parametertype/ConduitPointsParameterType.php index 5b330aabd1..9e5be819f2 100644 --- a/src/applications/conduit/parametertype/ConduitPointsParameterType.php +++ b/src/applications/conduit/parametertype/ConduitPointsParameterType.php @@ -3,8 +3,8 @@ final class ConduitPointsParameterType extends ConduitParameterType { - protected function getParameterValue(array $request, $key) { - $value = parent::getParameterValue($request, $key); + protected function getParameterValue(array $request, $key, $strict) { + $value = parent::getParameterValue($request, $key, $strict); if (($value !== null) && !is_numeric($value)) { $this->raiseValidationException( diff --git a/src/applications/conduit/parametertype/ConduitProjectListParameterType.php b/src/applications/conduit/parametertype/ConduitProjectListParameterType.php index c26db7febf..bd504c7eb4 100644 --- a/src/applications/conduit/parametertype/ConduitProjectListParameterType.php +++ b/src/applications/conduit/parametertype/ConduitProjectListParameterType.php @@ -3,9 +3,9 @@ final class ConduitProjectListParameterType extends ConduitListParameterType { - protected function getParameterValue(array $request, $key) { - $list = parent::getParameterValue($request, $key); - $list = $this->validateStringList($request, $key, $list); + protected function getParameterValue(array $request, $key, $strict) { + $list = parent::getParameterValue($request, $key, $strict); + $list = $this->parseStringList($request, $key, $list, $strict); return id(new PhabricatorProjectPHIDResolver()) ->setViewer($this->getViewer()) ->resolvePHIDs($list); diff --git a/src/applications/conduit/parametertype/ConduitStringListParameterType.php b/src/applications/conduit/parametertype/ConduitStringListParameterType.php index 664a1ded99..20c9389f81 100644 --- a/src/applications/conduit/parametertype/ConduitStringListParameterType.php +++ b/src/applications/conduit/parametertype/ConduitStringListParameterType.php @@ -3,9 +3,9 @@ final class ConduitStringListParameterType extends ConduitListParameterType { - protected function getParameterValue(array $request, $key) { - $list = parent::getParameterValue($request, $key); - return $this->validateStringList($request, $key, $list); + protected function getParameterValue(array $request, $key, $strict) { + $list = parent::getParameterValue($request, $key, $strict); + return $this->parseStringList($request, $key, $list, $strict); } protected function getParameterTypeName() { diff --git a/src/applications/conduit/parametertype/ConduitStringParameterType.php b/src/applications/conduit/parametertype/ConduitStringParameterType.php index b93490fc73..f10f3731e2 100644 --- a/src/applications/conduit/parametertype/ConduitStringParameterType.php +++ b/src/applications/conduit/parametertype/ConduitStringParameterType.php @@ -3,17 +3,9 @@ final class ConduitStringParameterType extends ConduitParameterType { - protected function getParameterValue(array $request, $key) { - $value = parent::getParameterValue($request, $key); - - if (!is_string($value)) { - $this->raiseValidationException( - $request, - $key, - pht('Expected string, got something else.')); - } - - return $value; + protected function getParameterValue(array $request, $key, $strict) { + $value = parent::getParameterValue($request, $key, $strict); + return $this->parseStringValue($request, $key, $value, $strict); } protected function getParameterTypeName() { diff --git a/src/applications/conduit/parametertype/ConduitUserListParameterType.php b/src/applications/conduit/parametertype/ConduitUserListParameterType.php index ad6555146d..85a9095ac5 100644 --- a/src/applications/conduit/parametertype/ConduitUserListParameterType.php +++ b/src/applications/conduit/parametertype/ConduitUserListParameterType.php @@ -3,9 +3,9 @@ final class ConduitUserListParameterType extends ConduitListParameterType { - protected function getParameterValue(array $request, $key) { - $list = parent::getParameterValue($request, $key); - $list = $this->validateStringList($request, $key, $list); + protected function getParameterValue(array $request, $key, $strict) { + $list = parent::getParameterValue($request, $key, $strict); + $list = $this->parseStringList($request, $key, $list, $strict); return id(new PhabricatorUserPHIDResolver()) ->setViewer($this->getViewer()) ->resolvePHIDs($list); diff --git a/src/applications/conduit/parametertype/ConduitUserParameterType.php b/src/applications/conduit/parametertype/ConduitUserParameterType.php index 3590d1a405..ede7f1f466 100644 --- a/src/applications/conduit/parametertype/ConduitUserParameterType.php +++ b/src/applications/conduit/parametertype/ConduitUserParameterType.php @@ -3,8 +3,8 @@ final class ConduitUserParameterType extends ConduitParameterType { - protected function getParameterValue(array $request, $key) { - $value = parent::getParameterValue($request, $key); + protected function getParameterValue(array $request, $key, $strict) { + $value = parent::getParameterValue($request, $key, $strict); if ($value === null) { return null; diff --git a/src/applications/conduit/protocol/ConduitAPIRequest.php b/src/applications/conduit/protocol/ConduitAPIRequest.php index 47cc31fba0..3a2818a47a 100644 --- a/src/applications/conduit/protocol/ConduitAPIRequest.php +++ b/src/applications/conduit/protocol/ConduitAPIRequest.php @@ -6,9 +6,11 @@ final class ConduitAPIRequest extends Phobject { private $user; private $isClusterRequest = false; private $oauthToken; + private $isStrictlyTyped = true; - public function __construct(array $params) { + public function __construct(array $params, $strictly_typed) { $this->params = $params; + $this->isStrictlyTyped = $strictly_typed; } public function getValue($key, $default = null) { @@ -68,6 +70,10 @@ final class ConduitAPIRequest extends Phobject { return $this->isClusterRequest; } + public function getIsStrictlyTyped() { + return $this->isStrictlyTyped; + } + public function newContentSource() { return PhabricatorContentSource::newForSource( PhabricatorConduitContentSource::SOURCECONST); diff --git a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php index a1279ad8ae..221703b7fd 100644 --- a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php +++ b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php @@ -1115,7 +1115,9 @@ abstract class PhabricatorApplicationSearchEngine extends Phobject { continue; } - $value = $field->readValueFromConduitRequest($constraints); + $value = $field->readValueFromConduitRequest( + $constraints, + $request->getIsStrictlyTyped()); $saved_query->setParameter($field->getKey(), $value); } diff --git a/src/applications/search/field/PhabricatorSearchField.php b/src/applications/search/field/PhabricatorSearchField.php index d31aeb1950..f45befead6 100644 --- a/src/applications/search/field/PhabricatorSearchField.php +++ b/src/applications/search/field/PhabricatorSearchField.php @@ -323,10 +323,14 @@ abstract class PhabricatorSearchField extends Phobject { $this->getConduitKey()); } - public function readValueFromConduitRequest(array $constraints) { + public function readValueFromConduitRequest( + array $constraints, + $strict = true) { + return $this->getConduitParameterType()->getValue( $constraints, - $this->getConduitKey()); + $this->getConduitKey(), + $strict); } public function getValidConstraintKeys() { diff --git a/src/applications/transactions/editengine/PhabricatorEditEngine.php b/src/applications/transactions/editengine/PhabricatorEditEngine.php index 2805162336..8a71509c99 100644 --- a/src/applications/transactions/editengine/PhabricatorEditEngine.php +++ b/src/applications/transactions/editengine/PhabricatorEditEngine.php @@ -1903,7 +1903,10 @@ abstract class PhabricatorEditEngine $parameter_type->setViewer($viewer); try { - $xaction['value'] = $parameter_type->getValue($xaction, 'value'); + $xaction['value'] = $parameter_type->getValue( + $xaction, + 'value', + $request->getIsStrictlyTyped()); } catch (Exception $ex) { throw new PhutilProxyException( pht( From 3d985585937c9ed24f11e03532f1a8ae16aa465a Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 13 Oct 2016 10:45:15 -0700 Subject: [PATCH 20/55] Add import log messages to Calendar imports Summary: Ref T10747. When stuff goes wrong (or right) let the user know what happened. Test Plan: {F1870139} Reviewers: chad Reviewed By: chad Maniphest Tasks: T10747 Differential Revision: https://secure.phabricator.com/D16704 --- .../autopatches/20161013.cal.01.importlog.sql | 8 ++ src/__phutil_library_map__.php | 24 ++++ ...habricatorCalendarImportViewController.php | 68 ++++++++++- .../PhabricatorCalendarImportEngine.php | 87 +++++++++++++- ...habricatorCalendarImportDefaultLogType.php | 20 ++++ ...bricatorCalendarImportDuplicateLogType.php | 23 ++++ .../PhabricatorCalendarImportEmptyLogType.php | 32 +++++ ...icatorCalendarImportIgnoredNodeLogType.php | 23 ++++ .../PhabricatorCalendarImportLogType.php | 39 ++++++ ...abricatorCalendarImportOriginalLogType.php | 26 ++++ ...PhabricatorCalendarImportOrphanLogType.php | 25 ++++ ...PhabricatorCalendarImportUpdateLogType.php | 38 ++++++ .../PhabricatorCalendarImportLogQuery.php | 111 ++++++++++++++++++ .../storage/PhabricatorCalendarEvent.php | 4 + .../storage/PhabricatorCalendarImport.php | 20 ++++ .../storage/PhabricatorCalendarImportLog.php | 103 ++++++++++++++++ 16 files changed, 643 insertions(+), 8 deletions(-) create mode 100644 resources/sql/autopatches/20161013.cal.01.importlog.sql create mode 100644 src/applications/calendar/importlog/PhabricatorCalendarImportDefaultLogType.php create mode 100644 src/applications/calendar/importlog/PhabricatorCalendarImportDuplicateLogType.php create mode 100644 src/applications/calendar/importlog/PhabricatorCalendarImportEmptyLogType.php create mode 100644 src/applications/calendar/importlog/PhabricatorCalendarImportIgnoredNodeLogType.php create mode 100644 src/applications/calendar/importlog/PhabricatorCalendarImportLogType.php create mode 100644 src/applications/calendar/importlog/PhabricatorCalendarImportOriginalLogType.php create mode 100644 src/applications/calendar/importlog/PhabricatorCalendarImportOrphanLogType.php create mode 100644 src/applications/calendar/importlog/PhabricatorCalendarImportUpdateLogType.php create mode 100644 src/applications/calendar/query/PhabricatorCalendarImportLogQuery.php create mode 100644 src/applications/calendar/storage/PhabricatorCalendarImportLog.php diff --git a/resources/sql/autopatches/20161013.cal.01.importlog.sql b/resources/sql/autopatches/20161013.cal.01.importlog.sql new file mode 100644 index 0000000000..938f0c8d72 --- /dev/null +++ b/resources/sql/autopatches/20161013.cal.01.importlog.sql @@ -0,0 +1,8 @@ +CREATE TABLE {$NAMESPACE}_calendar.calendar_importlog ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + importPHID VARBINARY(64) NOT NULL, + parameters LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + KEY `key_import` (`importPHID`) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 34d379c376..02a69fb3aa 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2104,21 +2104,31 @@ phutil_register_library_map(array( 'PhabricatorCalendarICSWriter' => 'applications/calendar/util/PhabricatorCalendarICSWriter.php', 'PhabricatorCalendarIconSet' => 'applications/calendar/icon/PhabricatorCalendarIconSet.php', 'PhabricatorCalendarImport' => 'applications/calendar/storage/PhabricatorCalendarImport.php', + 'PhabricatorCalendarImportDefaultLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportDefaultLogType.php', 'PhabricatorCalendarImportDisableController' => 'applications/calendar/controller/PhabricatorCalendarImportDisableController.php', 'PhabricatorCalendarImportDisableTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportDisableTransaction.php', + 'PhabricatorCalendarImportDuplicateLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportDuplicateLogType.php', 'PhabricatorCalendarImportEditController' => 'applications/calendar/controller/PhabricatorCalendarImportEditController.php', 'PhabricatorCalendarImportEditEngine' => 'applications/calendar/editor/PhabricatorCalendarImportEditEngine.php', 'PhabricatorCalendarImportEditor' => 'applications/calendar/editor/PhabricatorCalendarImportEditor.php', + 'PhabricatorCalendarImportEmptyLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportEmptyLogType.php', 'PhabricatorCalendarImportEngine' => 'applications/calendar/import/PhabricatorCalendarImportEngine.php', 'PhabricatorCalendarImportICSFileTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportICSFileTransaction.php', + 'PhabricatorCalendarImportIgnoredNodeLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportIgnoredNodeLogType.php', 'PhabricatorCalendarImportListController' => 'applications/calendar/controller/PhabricatorCalendarImportListController.php', + 'PhabricatorCalendarImportLog' => 'applications/calendar/storage/PhabricatorCalendarImportLog.php', + 'PhabricatorCalendarImportLogQuery' => 'applications/calendar/query/PhabricatorCalendarImportLogQuery.php', + 'PhabricatorCalendarImportLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportLogType.php', 'PhabricatorCalendarImportNameTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportNameTransaction.php', + 'PhabricatorCalendarImportOriginalLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportOriginalLogType.php', + 'PhabricatorCalendarImportOrphanLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportOrphanLogType.php', 'PhabricatorCalendarImportPHIDType' => 'applications/calendar/phid/PhabricatorCalendarImportPHIDType.php', 'PhabricatorCalendarImportQuery' => 'applications/calendar/query/PhabricatorCalendarImportQuery.php', 'PhabricatorCalendarImportSearchEngine' => 'applications/calendar/query/PhabricatorCalendarImportSearchEngine.php', 'PhabricatorCalendarImportTransaction' => 'applications/calendar/storage/PhabricatorCalendarImportTransaction.php', 'PhabricatorCalendarImportTransactionQuery' => 'applications/calendar/query/PhabricatorCalendarImportTransactionQuery.php', 'PhabricatorCalendarImportTransactionType' => 'applications/calendar/xaction/PhabricatorCalendarImportTransactionType.php', + 'PhabricatorCalendarImportUpdateLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportUpdateLogType.php', 'PhabricatorCalendarImportViewController' => 'applications/calendar/controller/PhabricatorCalendarImportViewController.php', 'PhabricatorCalendarRemarkupRule' => 'applications/calendar/remarkup/PhabricatorCalendarRemarkupRule.php', 'PhabricatorCalendarReplyHandler' => 'applications/calendar/mail/PhabricatorCalendarReplyHandler.php', @@ -6921,21 +6931,35 @@ phutil_register_library_map(array( 'PhabricatorApplicationTransactionInterface', 'PhabricatorDestructibleInterface', ), + 'PhabricatorCalendarImportDefaultLogType' => 'PhabricatorCalendarImportLogType', 'PhabricatorCalendarImportDisableController' => 'PhabricatorCalendarController', 'PhabricatorCalendarImportDisableTransaction' => 'PhabricatorCalendarImportTransactionType', + 'PhabricatorCalendarImportDuplicateLogType' => 'PhabricatorCalendarImportLogType', 'PhabricatorCalendarImportEditController' => 'PhabricatorCalendarController', 'PhabricatorCalendarImportEditEngine' => 'PhabricatorEditEngine', 'PhabricatorCalendarImportEditor' => 'PhabricatorApplicationTransactionEditor', + 'PhabricatorCalendarImportEmptyLogType' => 'PhabricatorCalendarImportLogType', 'PhabricatorCalendarImportEngine' => 'Phobject', 'PhabricatorCalendarImportICSFileTransaction' => 'PhabricatorCalendarImportTransactionType', + 'PhabricatorCalendarImportIgnoredNodeLogType' => 'PhabricatorCalendarImportLogType', 'PhabricatorCalendarImportListController' => 'PhabricatorCalendarController', + 'PhabricatorCalendarImportLog' => array( + 'PhabricatorCalendarDAO', + 'PhabricatorPolicyInterface', + 'PhabricatorDestructibleInterface', + ), + 'PhabricatorCalendarImportLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorCalendarImportLogType' => 'Phobject', 'PhabricatorCalendarImportNameTransaction' => 'PhabricatorCalendarImportTransactionType', + 'PhabricatorCalendarImportOriginalLogType' => 'PhabricatorCalendarImportLogType', + 'PhabricatorCalendarImportOrphanLogType' => 'PhabricatorCalendarImportLogType', 'PhabricatorCalendarImportPHIDType' => 'PhabricatorPHIDType', 'PhabricatorCalendarImportQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorCalendarImportSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorCalendarImportTransaction' => 'PhabricatorModularTransaction', 'PhabricatorCalendarImportTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'PhabricatorCalendarImportTransactionType' => 'PhabricatorModularTransactionType', + 'PhabricatorCalendarImportUpdateLogType' => 'PhabricatorCalendarImportLogType', 'PhabricatorCalendarImportViewController' => 'PhabricatorCalendarController', 'PhabricatorCalendarRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'PhabricatorCalendarReplyHandler' => 'PhabricatorApplicationTransactionReplyHandler', diff --git a/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php b/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php index 92da19d61b..5da2be2044 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php @@ -30,12 +30,14 @@ final class PhabricatorCalendarImportViewController $curtain = $this->buildCurtain($import); $details = $this->buildPropertySection($import); + $log_messages = $this->buildLogMessages($import); $imported_events = $this->buildImportedEvents($import); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setMainColumn( array( + $log_messages, $imported_events, $timeline, )) @@ -134,8 +136,70 @@ final class PhabricatorCalendarImportViewController return $properties; } - private function buildImportedEvents( - PhabricatorCalendarImport $import) { + private function buildLogMessages(PhabricatorCalendarImport $import) { + $viewer = $this->getViewer(); + + $logs = id(new PhabricatorCalendarImportLogQuery()) + ->setViewer($viewer) + ->withImportPHIDs(array($import->getPHID())) + ->setLimit(25) + ->execute(); + + $rows = array(); + foreach ($logs as $log) { + $icon = $log->getDisplayIcon($viewer); + $color = $log->getDisplayColor($viewer); + $name = $log->getDisplayType($viewer); + $description = $log->getDisplayDescription($viewer); + + $rows[] = array( + $log->getID(), + id(new PHUIIconView())->setIcon($icon, $color), + $name, + $description, + phabricator_datetime($log->getDateCreated(), $viewer), + ); + } + + $table = id(new AphrontTableView($rows)) + ->setHeaders( + array( + pht('ID'), + null, + pht('Type'), + pht('Mesage'), + pht('Date'), + )) + ->setColumnClasses( + array( + null, + null, + 'pri', + 'wide', + null, + )); + + $all_uri = $this->getApplicationURI('import/log/'); + $all_uri = (string)id(new PhutilURI($all_uri)) + ->setQueryParam('importSourcePHID', $import->getPHID()); + + $all_button = id(new PHUIButtonView()) + ->setTag('a') + ->setText(pht('View All')) + ->setIcon('fa-search') + ->setHref($all_uri); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Log Messages')) + ->addActionLink($all_button); + + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setTable($table); + } + + private function buildImportedEvents(PhabricatorCalendarImport $import) { $viewer = $this->getViewer(); $engine = id(new PhabricatorCalendarEventSearchEngine()) diff --git a/src/applications/calendar/import/PhabricatorCalendarImportEngine.php b/src/applications/calendar/import/PhabricatorCalendarImportEngine.php index e6242762ff..14c96a76ae 100644 --- a/src/applications/calendar/import/PhabricatorCalendarImportEngine.php +++ b/src/applications/calendar/import/PhabricatorCalendarImportEngine.php @@ -49,8 +49,13 @@ abstract class PhabricatorCalendarImportEngine $nodes = array(); foreach ($root->getChildren() as $document) { foreach ($document->getChildren() as $node) { - if ($node->getNodeType() != $event_type) { - // TODO: Warn that we ignored this. + $node_type = $node->getNodeType(); + if ($node_type != $event_type) { + $import->newLogMessage( + PhabricatorCalendarImportIgnoredNodeLogType::LOGTYPE, + array( + 'node.type' => $node_type, + )); continue; } @@ -59,16 +64,62 @@ abstract class PhabricatorCalendarImportEngine } $node_map = array(); - $parent_uids = array(); foreach ($nodes as $node) { $full_uid = $this->getFullNodeUID($node); if (isset($node_map[$full_uid])) { - // TODO: Warn that we got a duplicate. + $import->newLogMessage( + PhabricatorCalendarImportDuplicateLogType::LOGTYPE, + array( + 'uid.full' => $full_uid, + )); continue; } $node_map[$full_uid] = $node; } + // If we already know about some of these events and they were created + // here, we're not going to import it again. This can happen if a user + // exports an event and then tries to import it again. This is probably + // not what they meant to do and this pathway generally leads to madness. + $likely_phids = array(); + foreach ($node_map as $full_uid => $node) { + $uid = $node->getUID(); + $matches = null; + if (preg_match('/^(PHID-.*)@(.*)\z/', $uid, $matches)) { + $likely_phids[$full_uid] = $matches[1]; + } + } + + if ($likely_phids) { + // NOTE: We're using the omnipotent viewer here because we don't want + // to collide with events that already exist, even if you can't see + // them. + $events = id(new PhabricatorCalendarEventQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withPHIDs($likely_phids) + ->execute(); + $events = mpull($events, null, 'getPHID'); + foreach ($node_map as $full_uid => $node) { + $phid = idx($likely_phids, $full_uid); + if (!$phid) { + continue; + } + + $event = idx($events, $phid); + if (!$event) { + continue; + } + + $import->newLogMessage( + PhabricatorCalendarImportOriginalLogType::LOGTYPE, + array( + 'phid' => $event->getPHID(), + )); + + unset($node_map[$full_uid]); + } + } + if ($node_map) { $events = id(new PhabricatorCalendarEventQuery()) ->setViewer($viewer) @@ -114,7 +165,12 @@ abstract class PhabricatorCalendarImportEngine // does not exist or we're going to delete it anyway. We just drop // this node. - // TODO: Warn that we got rid of an event with no parent. + $import->newLogMessage( + PhabricatorCalendarImportOrphanLogType::LOGTYPE, + array( + 'uid.full' => $full_uid, + 'uid.parent' => $parent_uid, + )); continue; } @@ -130,6 +186,10 @@ abstract class PhabricatorCalendarImportEngine $content_source = PhabricatorContentSource::newForSource( PhabricatorWebContentSource::SOURCECONST); + // NOTE: We're using the omnipotent user here because imported events are + // otherwise immutable. + $edit_actor = PhabricatorUser::getOmnipotentUser(); + $update_map = array_select_keys($update_map, $insert_order); foreach ($update_map as $full_uid => $event) { $parent_uid = $this->getParentNodeUID($node_map[$full_uid]); @@ -144,13 +204,28 @@ abstract class PhabricatorCalendarImportEngine $event_xactions = $xactions[$full_uid]; $editor = id(new PhabricatorCalendarEventEditor()) - ->setActor($viewer) + ->setActor($edit_actor) ->setActingAsPHID($import->getPHID()) ->setContentSource($content_source) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true); + $is_new = !$event->getID(); + $editor->applyTransactions($event, $event_xactions); + + $import->newLogMessage( + PhabricatorCalendarImportUpdateLogType::LOGTYPE, + array( + 'new' => $is_new, + 'phid' => $event->getPHID(), + )); + } + + if (!$update_map) { + $import->newLogMessage( + PhabricatorCalendarImportEmptyLogType::LOGTYPE, + array()); } // TODO: When the source is a subscription-based ICS file or some other diff --git a/src/applications/calendar/importlog/PhabricatorCalendarImportDefaultLogType.php b/src/applications/calendar/importlog/PhabricatorCalendarImportDefaultLogType.php new file mode 100644 index 0000000000..4e61e3f94f --- /dev/null +++ b/src/applications/calendar/importlog/PhabricatorCalendarImportDefaultLogType.php @@ -0,0 +1,20 @@ +getParameter('type'); + if (strlen($type)) { + return pht('Unknown Message "%s"', $type); + } else { + return pht('Unknown Message'); + } + } + +} diff --git a/src/applications/calendar/importlog/PhabricatorCalendarImportDuplicateLogType.php b/src/applications/calendar/importlog/PhabricatorCalendarImportDuplicateLogType.php new file mode 100644 index 0000000000..0f72932404 --- /dev/null +++ b/src/applications/calendar/importlog/PhabricatorCalendarImportDuplicateLogType.php @@ -0,0 +1,23 @@ +getParameter('uid.full'); + return pht( + 'Ignored duplicate event "%s" present in source.', + $duplicate_uid); + } + +} diff --git a/src/applications/calendar/importlog/PhabricatorCalendarImportEmptyLogType.php b/src/applications/calendar/importlog/PhabricatorCalendarImportEmptyLogType.php new file mode 100644 index 0000000000..5a540645ba --- /dev/null +++ b/src/applications/calendar/importlog/PhabricatorCalendarImportEmptyLogType.php @@ -0,0 +1,32 @@ +getParameter('node.type'); + return pht( + 'Ignored unsupported "%s" node present in source.', + $node_type); + } + +} diff --git a/src/applications/calendar/importlog/PhabricatorCalendarImportLogType.php b/src/applications/calendar/importlog/PhabricatorCalendarImportLogType.php new file mode 100644 index 0000000000..a6c4ca4ec3 --- /dev/null +++ b/src/applications/calendar/importlog/PhabricatorCalendarImportLogType.php @@ -0,0 +1,39 @@ +getPhobjectClassConstant('LOGTYPE', 64); + } + + final public static function getAllLogTypes() { + return id(new PhutilClassMapQuery()) + ->setAncestorClass(__CLASS__) + ->setUniqueMethod('getLogTypeConstant') + ->execute(); + } + + abstract public function getDisplayType( + PhabricatorUser $viewer, + PhabricatorCalendarImportLog $log); + + public function getDisplayIcon( + PhabricatorUser $viewer, + PhabricatorCalendarImportLog $log) { + return 'fa-warning'; + } + + public function getDisplayColor( + PhabricatorUser $viewer, + PhabricatorCalendarImportLog $log) { + return 'yellow'; + } + + public function getDisplayDescription( + PhabricatorUser $viewer, + PhabricatorCalendarImportLog $log) { + return null; + } + +} diff --git a/src/applications/calendar/importlog/PhabricatorCalendarImportOriginalLogType.php b/src/applications/calendar/importlog/PhabricatorCalendarImportOriginalLogType.php new file mode 100644 index 0000000000..567cc5f68e --- /dev/null +++ b/src/applications/calendar/importlog/PhabricatorCalendarImportOriginalLogType.php @@ -0,0 +1,26 @@ +getParameter('phid'); + + return pht( + 'Ignored an event (%s) because the original version of this event '. + 'was created here.', + $viewer->renderHandle($phid)); + } + +} diff --git a/src/applications/calendar/importlog/PhabricatorCalendarImportOrphanLogType.php b/src/applications/calendar/importlog/PhabricatorCalendarImportOrphanLogType.php new file mode 100644 index 0000000000..236b0e7f65 --- /dev/null +++ b/src/applications/calendar/importlog/PhabricatorCalendarImportOrphanLogType.php @@ -0,0 +1,25 @@ +getParameter('uid.full'); + $parent_uid = $log->getParameter('uid.parent'); + return pht( + 'Found orphaned child event ("%s") without a parent event ("%s").', + $child_uid, + $parent_uid); + } + +} diff --git a/src/applications/calendar/importlog/PhabricatorCalendarImportUpdateLogType.php b/src/applications/calendar/importlog/PhabricatorCalendarImportUpdateLogType.php new file mode 100644 index 0000000000..9b20c54615 --- /dev/null +++ b/src/applications/calendar/importlog/PhabricatorCalendarImportUpdateLogType.php @@ -0,0 +1,38 @@ +getParameter('new'); + if ($is_new) { + return pht('Imported Event'); + } else { + return pht('Updated Event'); + } + } + + public function getDisplayDescription( + PhabricatorUser $viewer, + PhabricatorCalendarImportLog $log) { + $event_phid = $log->getParameter('phid'); + return $viewer->renderHandle($event_phid); + } + + public function getDisplayIcon( + PhabricatorUser $viewer, + PhabricatorCalendarImportLog $log) { + return 'fa-upload'; + } + + public function getDisplayColor( + PhabricatorUser $viewer, + PhabricatorCalendarImportLog $log) { + return 'green'; + } + +} diff --git a/src/applications/calendar/query/PhabricatorCalendarImportLogQuery.php b/src/applications/calendar/query/PhabricatorCalendarImportLogQuery.php new file mode 100644 index 0000000000..81f309bdca --- /dev/null +++ b/src/applications/calendar/query/PhabricatorCalendarImportLogQuery.php @@ -0,0 +1,111 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withImportPHIDs(array $phids) { + $this->importPHIDs = $phids; + return $this; + } + + public function newResultObject() { + return new PhabricatorCalendarImportLog(); + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); + + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'log.id IN (%Ld)', + $this->ids); + } + + if ($this->phids !== null) { + $where[] = qsprintf( + $conn, + 'log.phid IN (%Ls)', + $this->phids); + } + + if ($this->importPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'log.importPHID IN (%Ls)', + $this->importPHIDs); + } + + + return $where; + } + + protected function willFilterPage(array $page) { + $viewer = $this->getViewer(); + + $type_map = PhabricatorCalendarImportLogType::getAllLogTypes(); + foreach ($page as $log) { + $type_constant = $log->getParameter('type'); + + $type_object = idx($type_map, $type_constant); + if (!$type_object) { + $type_object = new PhabricatorCalendarImportDefaultLogType(); + } + + $type_object = clone $type_object; + $log->attachLogType($type_object); + } + + $import_phids = mpull($page, 'getImportPHID'); + + if ($import_phids) { + $imports = id(new PhabricatorCalendarImportQuery()) + ->setViewer($viewer) + ->withPHIDs($import_phids) + ->execute(); + $imports = mpull($imports, null, 'getPHID'); + } else { + $imports = array(); + } + + foreach ($page as $key => $log) { + $import = idx($imports, $log->getImportPHID()); + if (!$import) { + $this->didRejectResult($import); + unset($page[$key]); + continue; + } + + $log->attachImport($import); + } + + return $page; + } + + protected function getPrimaryTableAlias() { + return 'log'; + } + + public function getQueryApplicationClass() { + return 'PhabricatorCalendarApplication'; + } + +} diff --git a/src/applications/calendar/storage/PhabricatorCalendarEvent.php b/src/applications/calendar/storage/PhabricatorCalendarEvent.php index 2cf9e4117c..7ad6d85881 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarEvent.php +++ b/src/applications/calendar/storage/PhabricatorCalendarEvent.php @@ -907,6 +907,10 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO return $set; } + public function isImportedEvent() { + return (bool)$this->getImportSourcePHID(); + } + public function getImportSource() { return $this->assertAttached($this->importSource); } diff --git a/src/applications/calendar/storage/PhabricatorCalendarImport.php b/src/applications/calendar/storage/PhabricatorCalendarImport.php index 18c113a5c6..b1d0ddf58b 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarImport.php +++ b/src/applications/calendar/storage/PhabricatorCalendarImport.php @@ -133,6 +133,17 @@ final class PhabricatorCalendarImport return $timeline; } + public function newLogMessage($type, array $parameters) { + $parameters = array( + 'type' => $type, + ) + $parameters; + + return id(new PhabricatorCalendarImportLog()) + ->setImportPHID($this->getPHID()) + ->setParameters($parameters) + ->save(); + } + /* -( PhabricatorDestructibleInterface )----------------------------------- */ @@ -144,12 +155,21 @@ final class PhabricatorCalendarImport $this->openTransaction(); $events = id(new PhabricatorCalendarEventQuery()) + ->setViewer($viewer) ->withImportSourcePHIDs(array($this->getPHID())) ->execute(); foreach ($events as $event) { $engine->destroyObject($event); } + $logs = id(new PhabricatorCalendarImportLogQuery()) + ->setViewer($viewer) + ->withImportPHIDs(array($this->getPHID())) + ->execute(); + foreach ($logs as $log) { + $engine->destroyObject($log); + } + $this->delete(); $this->saveTransaction(); } diff --git a/src/applications/calendar/storage/PhabricatorCalendarImportLog.php b/src/applications/calendar/storage/PhabricatorCalendarImportLog.php new file mode 100644 index 0000000000..e25a3090e0 --- /dev/null +++ b/src/applications/calendar/storage/PhabricatorCalendarImportLog.php @@ -0,0 +1,103 @@ + array( + 'parameters' => self::SERIALIZATION_JSON, + ), + self::CONFIG_KEY_SCHEMA => array( + 'key_import' => array( + 'columns' => array('importPHID'), + ), + ), + ) + parent::getConfiguration(); + } + + public function getParameter($key, $default = null) { + return idx($this->parameters, $key, $default); + } + + public function setParameter($key, $value) { + $this->parameters[$key] = $value; + return $this; + } + + public function getImport() { + return $this->assertAttached($this->import); + } + + public function attachImport(PhabricatorCalendarImport $import) { + $this->import = $import; + return $this; + } + + public function getDisplayIcon(PhabricatorUser $viewer) { + return $this->getLogType()->getDisplayIcon($viewer, $this); + } + + public function getDisplayColor(PhabricatorUser $viewer) { + return $this->getLogType()->getDisplayColor($viewer, $this); + } + + public function getDisplayType(PhabricatorUser $viewer) { + return $this->getLogType()->getDisplayType($viewer, $this); + } + + public function getDisplayDescription(PhabricatorUser $viewer) { + return $this->getLogType()->getDisplayDescription($viewer, $this); + } + + public function getLogType() { + return $this->assertAttached($this->logType); + } + + public function attachLogType(PhabricatorCalendarImportLogType $type) { + $this->logType = $type; + return $this; + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + ); + } + + public function getPolicy($capability) { + return PhabricatorPolicies::getMostOpenPolicy(); + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return false; + } + + public function describeAutomaticCapability($capability) { + return null; + } + + +/* -( PhabricatorDestructibleInterface )----------------------------------- */ + + + public function destroyObjectPermanently( + PhabricatorDestructionEngine $engine) { + $viewer = $engine->getViewer(); + $this->delete(); + } + +} From 3593be4f7b664a03a74bfed384bb22e88066ff57 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Fri, 14 Oct 2016 09:06:24 -0700 Subject: [PATCH 21/55] Fix "Show More Messages" bug in Conpherence Summary: Hides the search form itself as well when not in use. Test Plan: Write lots of posts, scroll up to "see more messages", see I can click and load messages now. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D16706 --- resources/celerity/map.php | 6 +++--- webroot/rsrc/css/application/conpherence/message-pane.css | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 5094fb88e9..0be558d045 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,7 +7,7 @@ */ return array( 'names' => array( - 'conpherence.pkg.css' => 'c839a862', + 'conpherence.pkg.css' => '823b1104', 'conpherence.pkg.js' => 'cbe4d9be', 'core.pkg.css' => 'b99bbf5e', 'core.pkg.js' => '30185d95', @@ -49,7 +49,7 @@ return array( 'rsrc/css/application/conpherence/durable-column.css' => '44bcaa19', 'rsrc/css/application/conpherence/header-pane.css' => 'e8acbd37', 'rsrc/css/application/conpherence/menu.css' => '4f51db5a', - 'rsrc/css/application/conpherence/message-pane.css' => 'eff20ae7', + 'rsrc/css/application/conpherence/message-pane.css' => '4db388a6', 'rsrc/css/application/conpherence/notification.css' => '965db05b', 'rsrc/css/application/conpherence/participant-pane.css' => '7bba0b56', 'rsrc/css/application/conpherence/transaction.css' => '46253e19', @@ -619,7 +619,7 @@ return array( 'conpherence-durable-column-view' => '44bcaa19', 'conpherence-header-pane-css' => 'e8acbd37', 'conpherence-menu-css' => '4f51db5a', - 'conpherence-message-pane-css' => 'eff20ae7', + 'conpherence-message-pane-css' => '4db388a6', 'conpherence-notification-css' => '965db05b', 'conpherence-participant-pane-css' => '7bba0b56', 'conpherence-thread-manager' => '01774ab2', diff --git a/webroot/rsrc/css/application/conpherence/message-pane.css b/webroot/rsrc/css/application/conpherence/message-pane.css index a7ff8000db..c26686c555 100644 --- a/webroot/rsrc/css/application/conpherence/message-pane.css +++ b/webroot/rsrc/css/application/conpherence/message-pane.css @@ -439,6 +439,10 @@ height: auto; } +.conpherence-search-form-view { + display: none; +} + .show-searchbar .conpherence-search-form-view { display: block; height: 54px; From 52dd354dad1e65d8c94c58c02d59664e139df2b1 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Fri, 14 Oct 2016 09:25:15 -0700 Subject: [PATCH 22/55] Fix Conphernce sometimes searching wrong room Summary: I passed this in as a config, but need to parse it live when threads change, otherwise the wrong room could be searched. Test Plan: Search in one room, click a second, search again, see correct results. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D16707 --- resources/celerity/map.php | 18 +++++++++--------- .../controller/ConpherenceController.php | 7 ++----- .../conpherence/behavior-conpherence-search.js | 5 +++-- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 0be558d045..860f429952 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -436,7 +436,7 @@ return array( 'rsrc/js/application/calendar/behavior-recurring-edit.js' => '5f1c4d5f', 'rsrc/js/application/config/behavior-reorder-fields.js' => 'b6993408', 'rsrc/js/application/conpherence/ConpherenceThreadManager.js' => '01774ab2', - 'rsrc/js/application/conpherence/behavior-conpherence-search.js' => '3bc9d2b1', + 'rsrc/js/application/conpherence/behavior-conpherence-search.js' => 'dfa4e1ac', 'rsrc/js/application/conpherence/behavior-durable-column.js' => 'c5238acb', 'rsrc/js/application/conpherence/behavior-menu.js' => '07928ca3', 'rsrc/js/application/conpherence/behavior-participant-pane.js' => '8604caa8', @@ -666,7 +666,7 @@ return array( 'javelin-behavior-conpherence-menu' => '07928ca3', 'javelin-behavior-conpherence-participant-pane' => '8604caa8', 'javelin-behavior-conpherence-pontificate' => 'f2e58483', - 'javelin-behavior-conpherence-search' => '3bc9d2b1', + 'javelin-behavior-conpherence-search' => 'dfa4e1ac', 'javelin-behavior-countdown-timer' => 'e4cc26b3', 'javelin-behavior-dark-console' => 'f411b6ae', 'javelin-behavior-dashboard-async-panel' => '469c0d9e', @@ -1219,13 +1219,6 @@ return array( 'javelin-dom', 'javelin-magical-init', ), - '3bc9d2b1' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-util', - 'javelin-workflow', - 'javelin-stratcom', - ), '3cb0b2fc' => array( 'javelin-behavior', 'javelin-dom', @@ -2095,6 +2088,13 @@ return array( 'df5e11d2' => array( 'javelin-install', ), + 'dfa4e1ac' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-util', + 'javelin-workflow', + 'javelin-stratcom', + ), 'e0ec7f2f' => array( 'javelin-behavior', 'javelin-dom', diff --git a/src/applications/conpherence/controller/ConpherenceController.php b/src/applications/conpherence/controller/ConpherenceController.php index 097877f475..3645cfe87e 100644 --- a/src/applications/conpherence/controller/ConpherenceController.php +++ b/src/applications/conpherence/controller/ConpherenceController.php @@ -113,11 +113,7 @@ abstract class ConpherenceController extends PhabricatorController { ->setHref('#') ->addClass('conpherence-participant-toggle')); - Javelin::initBehavior( - 'conpherence-search', - array( - 'searchURI' => '/conpherence/threadsearch/'.$conpherence->getID().'/', - )); + Javelin::initBehavior('conpherence-search'); $header->addActionItem( id(new PHUIIconCircleView()) @@ -187,6 +183,7 @@ abstract class ConpherenceController extends PhabricatorController { 'action' => '/conpherence/threadsearch/'.$id.'/', 'sigil' => 'conpherence-search-form', 'class' => 'conpherence-search-form', + 'id' => 'conpherence-search-form', ), array( $bar, diff --git a/webroot/rsrc/js/application/conpherence/behavior-conpherence-search.js b/webroot/rsrc/js/application/conpherence/behavior-conpherence-search.js index 3566d03e23..aa1c2469b7 100644 --- a/webroot/rsrc/js/application/conpherence/behavior-conpherence-search.js +++ b/webroot/rsrc/js/application/conpherence/behavior-conpherence-search.js @@ -7,7 +7,7 @@ * javelin-stratcom */ -JX.behavior('conpherence-search', function(config) { +JX.behavior('conpherence-search', function() { var shown = true; var request = null; @@ -24,13 +24,14 @@ JX.behavior('conpherence-search', function(config) { function _doSearch(e) { e.kill(); var search_text = JX.$('conpherence-search-input').value; + var search_uri = JX.$('conpherence-search-form').action; var search_node = JX.$('conpherence-search-results'); if (request || !search_text) { return; } - request = new JX.Request(config.searchURI, function(response) { + request = new JX.Request(search_uri, function(response) { JX.DOM.setContent(search_node, JX.$H(response)); request = null; }); From 49165bc6d784ca0b84ba57df8f9c79fd3f35cb08 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Sat, 15 Oct 2016 13:58:46 +0000 Subject: [PATCH 23/55] New UI for fulltext message search Summary: Basically all here, but still probably needs some polish (links to jump? full dates?). Looks much better, still duplicates messages though sometimes. Needs to debug that more. Test Plan: Revisit search UI inside Conpherence, outside Conpherence, and normal room searches in Conpherence. {F1870748} {F1870749} {F1870750} Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D16708 --- resources/celerity/map.php | 10 +- .../ConpherenceThreadSearchController.php | 2 +- .../query/ConpherenceThreadSearchEngine.php | 121 ++++++++++-------- .../view/ConpherenceTransactionView.php | 17 +-- .../application/conpherence/message-pane.css | 86 ++++++------- .../application/conpherence/transaction.css | 27 +++- 6 files changed, 150 insertions(+), 113 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 860f429952..f6ec637a91 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,7 +7,7 @@ */ return array( 'names' => array( - 'conpherence.pkg.css' => '823b1104', + 'conpherence.pkg.css' => 'f934296b', 'conpherence.pkg.js' => 'cbe4d9be', 'core.pkg.css' => 'b99bbf5e', 'core.pkg.js' => '30185d95', @@ -49,10 +49,10 @@ return array( 'rsrc/css/application/conpherence/durable-column.css' => '44bcaa19', 'rsrc/css/application/conpherence/header-pane.css' => 'e8acbd37', 'rsrc/css/application/conpherence/menu.css' => '4f51db5a', - 'rsrc/css/application/conpherence/message-pane.css' => '4db388a6', + 'rsrc/css/application/conpherence/message-pane.css' => '7a94bf5e', 'rsrc/css/application/conpherence/notification.css' => '965db05b', 'rsrc/css/application/conpherence/participant-pane.css' => '7bba0b56', - 'rsrc/css/application/conpherence/transaction.css' => '46253e19', + 'rsrc/css/application/conpherence/transaction.css' => '85129c68', 'rsrc/css/application/contentsource/content-source-view.css' => '4b8b05d4', 'rsrc/css/application/countdown/timer.css' => '16c52f5c', 'rsrc/css/application/daemon/bulk-job.css' => 'df9c1d4a', @@ -619,11 +619,11 @@ return array( 'conpherence-durable-column-view' => '44bcaa19', 'conpherence-header-pane-css' => 'e8acbd37', 'conpherence-menu-css' => '4f51db5a', - 'conpherence-message-pane-css' => '4db388a6', + 'conpherence-message-pane-css' => '7a94bf5e', 'conpherence-notification-css' => '965db05b', 'conpherence-participant-pane-css' => '7bba0b56', 'conpherence-thread-manager' => '01774ab2', - 'conpherence-transaction-css' => '46253e19', + 'conpherence-transaction-css' => '85129c68', 'd3' => 'a11a5ff2', 'differential-changeset-view-css' => '9ef7d354', 'differential-core-view-css' => '5b7b8ff4', diff --git a/src/applications/conpherence/controller/ConpherenceThreadSearchController.php b/src/applications/conpherence/controller/ConpherenceThreadSearchController.php index e426e99128..155a61c234 100644 --- a/src/applications/conpherence/controller/ConpherenceThreadSearchController.php +++ b/src/applications/conpherence/controller/ConpherenceThreadSearchController.php @@ -36,6 +36,6 @@ final class ConpherenceThreadSearchController $view = $engine->renderResults($results, $saved); return id(new AphrontAjaxResponse()) - ->setContent($view->getObjectList()); + ->setContent($view->getContent()); } } diff --git a/src/applications/conpherence/query/ConpherenceThreadSearchEngine.php b/src/applications/conpherence/query/ConpherenceThreadSearchEngine.php index f41bf835e7..d9d06f8333 100644 --- a/src/applications/conpherence/query/ConpherenceThreadSearchEngine.php +++ b/src/applications/conpherence/query/ConpherenceThreadSearchEngine.php @@ -13,7 +13,8 @@ final class ConpherenceThreadSearchEngine public function newQuery() { return id(new ConpherenceThreadQuery()) - ->needParticipantCache(true); + ->needParticipantCache(true) + ->needProfileImage(true); } protected function buildCustomSearchFields() { @@ -147,6 +148,7 @@ final class ConpherenceThreadSearchEngine $context = array(); } + $content = array(); $list = new PHUIObjectItemListView(); $list->setUser($viewer); foreach ($conpherences as $conpherence_phid => $conpherence) { @@ -157,63 +159,82 @@ final class ConpherenceThreadSearchEngine $icon_name = $conpherence->getPolicyIconName($policy_objects); $icon = id(new PHUIIconView()) ->setIcon($icon_name); - $item = id(new PHUIObjectItemView()) - ->setObjectName($conpherence->getMonogram()) - ->setHeader($title) - ->setHref('/'.$conpherence->getMonogram()) - ->setObject($conpherence) - ->addIcon('none', $created) - ->addIcon( - 'none', - pht('Messages: %d', $conpherence->getMessageCount())) - ->addAttribute( - array( - $icon, - ' ', - pht( - 'Last updated %s', - phabricator_datetime($conpherence->getDateModified(), $viewer)), - )); - $messages = idx($context, $conpherence_phid); - if ($messages) { - foreach ($messages as $group) { - $rows = array(); - foreach ($group as $message) { - $xaction = $message['xaction']; - if (!$xaction) { - continue; + if (!strlen($fulltext)) { + $item = id(new PHUIObjectItemView()) + ->setObjectName($conpherence->getMonogram()) + ->setHeader($title) + ->setHref('/'.$conpherence->getMonogram()) + ->setObject($conpherence) + ->setImageURI($conpherence->getProfileImageURI()) + ->addIcon('none', $created) + ->addIcon( + 'none', + pht('Messages: %d', $conpherence->getMessageCount())) + ->addAttribute( + array( + $icon, + ' ', + pht( + 'Last updated %s', + phabricator_datetime($conpherence->getDateModified(), $viewer)), + )); + $list->addItem($item); + } else { + $messages = idx($context, $conpherence_phid); + $box = array(); + $list = null; + if ($messages) { + foreach ($messages as $group) { + $rows = array(); + foreach ($group as $message) { + $xaction = $message['xaction']; + if (!$xaction) { + continue; + } + + $view = id(new ConpherenceTransactionView()) + ->setUser($viewer) + ->setHandles($handles) + ->setMarkupEngine($engines[$conpherence_phid]) + ->setConpherenceThread($conpherence) + ->setConpherenceTransaction($xaction) + ->setFullDisplay(true) + ->addClass('conpherence-fulltext-result'); + + if ($message['match']) { + $view->addClass('conpherence-fulltext-match'); + } + + $rows[] = $view; } - - $view = id(new ConpherenceTransactionView()) - ->setUser($viewer) - ->setHandles($handles) - ->setMarkupEngine($engines[$conpherence_phid]) - ->setConpherenceThread($conpherence) - ->setConpherenceTransaction($xaction) - ->setFullDisplay(false) - ->addClass('conpherence-fulltext-result'); - - if ($message['match']) { - $view->addClass('conpherence-fulltext-match'); - } - - $rows[] = $view; + $box[] = id(new PHUIBoxView()) + ->appendChild($rows) + ->addClass('conpherence-fulltext-results'); } - - $box = id(new PHUIBoxView()) - ->appendChild($rows) - ->addClass('conpherence-fulltext-results'); - $item->appendChild($box); } - } + $header = id(new PHUIHeaderView()) + ->setHeader($title) + ->setHeaderIcon($icon_name) + ->setHref('/'.$monogram); - $list->addItem($item); + $content[] = id(new PHUIObjectBoxView()) + ->setHeader($header) + ->appendChild($box); + } + } + + if ($list) { + $content = $list; + } else { + $content = id(new PHUIBoxView()) + ->addClass('conpherence-search-room-results') + ->appendChild($content); } $result = new PhabricatorApplicationSearchResultView(); - $result->setObjectList($list); - $result->setNoDataString(pht('No threads found.')); + $result->setContent($content); + $result->setNoDataString(pht('No results found.')); return $result; } diff --git a/src/applications/conpherence/view/ConpherenceTransactionView.php b/src/applications/conpherence/view/ConpherenceTransactionView.php index 12123aafcd..e3991ab979 100644 --- a/src/applications/conpherence/view/ConpherenceTransactionView.php +++ b/src/applications/conpherence/view/ConpherenceTransactionView.php @@ -247,17 +247,14 @@ final class ConpherenceTransactionView extends AphrontView { break; } - $this->appendChild( - phutil_tag( - 'div', - array( - 'class' => $content_class, - ), - $content)); + $view = phutil_tag( + 'div', + array( + 'class' => $content_class, + ), + $content); - return phutil_tag_div( - 'conpherence-transaction-content', - $this->renderChildren()); + return phutil_tag_div('conpherence-transaction-content', $view); } } diff --git a/webroot/rsrc/css/application/conpherence/message-pane.css b/webroot/rsrc/css/application/conpherence/message-pane.css index c26686c555..37ee9581fe 100644 --- a/webroot/rsrc/css/application/conpherence/message-pane.css +++ b/webroot/rsrc/css/application/conpherence/message-pane.css @@ -198,18 +198,18 @@ width: 100%; } -.conpherence-message-pane .conpherence-transaction-view { +.conpherence-transaction-view { padding: 2px 0px; margin: 4px 20px; background-size: 100%; min-height: auto; } -.device-phone .conpherence-message-pane .conpherence-transaction-view { +.device-phone .conpherence-transaction-view { margin: 0 8px; } -.conpherence-message-pane .conpherence-transaction-image { +.conpherence-transaction-image { float: left; border-radius: 3px; height: 35px; @@ -219,88 +219,85 @@ top: 5px; } -.device-phone .conpherence-message-pane .conpherence-transaction-image { +.device-phone .conpherence-transaction-image { height: 25px; width: 25px; background-size: 25px; } -.conpherence-message-pane .conpherence-comment.anchor-target, -.conpherence-message-pane .conpherence-edited.anchor-target { +.conpherence-transaction-view.conpherence-comment.anchor-target, +.conpherence-transaction-view.conpherence-edited.anchor-target { background: {$lightyellow}; } -.conpherence-message-pane .conpherence-comment.anchor-target { +.cconpherence-transaction-view.conpherence-comment.anchor-target { margin: 4px 8px 4px 8px; padding: 2px 4px 2px 4px; } -.conpherence-message-pane .conpherence-edited.anchor-target { +.conpherence-transaction-view.conpherence-edited.anchor-target { margin: 0px 8px 0px 8px; padding: 0px 4px 0px 4px; } -.conpherence-message-pane .conpherence-transaction-detail { +.conpherence-transaction-view .conpherence-transaction-detail { border-width: 0; margin-left: 45px; } -.device-phone .conpherence-message-pane .conpherence-transaction-detail { +.device-phone .conpherence-transaction-view .conpherence-transaction-detail { margin-left: 32px; } -.conpherence-message-pane .conpherence-transaction-view.date-marker { +.conpherence-transaction-view.date-marker { padding: 0; margin: 20px 20px 4px; min-height: auto; } -.device-phone .conpherence-message-pane -.conpherence-transaction-view.date-marker { +.device-phone .conpherence-transaction-view.date-marker { margin: 12px 0 4px; } -.device-tablet .conpherence-message-pane - .conpherence-transaction-view.date-marker { - padding-left: 37px; +.device-tablet .conpherence-transaction-view.date-marker { + padding-left: 37px; } -.conpherence-message-pane .conpherence-transaction-view.date-marker - .date { - left: 40px; - font-size: {$normalfontsize}; - padding: 0px 4px; +.conpherence-transaction-view.date-marker .date { + left: 40px; + font-size: {$normalfontsize}; + padding: 0px 4px; } -.device .conpherence-message-pane .conpherence-transaction-view.date-marker - .date { - left: 4px; +.device .conpherence-transaction-view.date-marker .date { + left: 4px; } -.device-phone .conpherence-message-pane .conpherence-edited { +.device-phone .conpherence-transaction-view.conpherence-edited { min-height: none; color: {$lightgreytext}; margin: 0 8px; } -.conpherence-message-pane .conpherence-edited .conpherence-transaction-content { - color: {$lightgreytext}; - font-size: {$biggerfontsize}; - font-style: italic; - margin: 0; - padding: 0; - float: left; - line-height: 20px; +.conpherence-transaction-view.conpherence-edited + .conpherence-transaction-content { + color: {$lightgreytext}; + font-size: {$biggerfontsize}; + font-style: italic; + margin: 0; + padding: 0; + float: left; + line-height: 20px; } -.conpherence-message-pane .conpherence-edited { +.conpherence-transaction-view.conpherence-edited { padding: 0; margin-top: 0; margin-bottom: 0; min-height: inherit; } -.conpherence-message-pane .conpherence-edited + .conpherence-comment { +.conpherence-transaction-view.conpherence-edited + .conpherence-comment { margin-top: 16px; } @@ -309,28 +306,29 @@ margin-top: 24px; } -.conpherence-message-pane .conpherence-edited .conpherence-transaction-header { - float: right; +.conpherence-transaction-view.conpherence-edited + .conpherence-transaction-header { + float: right; } -.conpherence-message-pane .conpherence-edited +.conpherence-transaction-view.conpherence-edited .conpherence-transaction-content a { color: {$darkbluetext}; } -.conpherence-message-pane .conpherence-transaction-info { +.conpherence-transaction-view .conpherence-transaction-info { margin: 0 8px; } -.conpherence-message-pane .conpherence-transaction-info, -.conpherence-message-pane .anchor-link, -.conpherence-message-pane .phabricator-content-source-view { +.conpherence-transaction-view .conpherence-transaction-info, +.conpherence-transaction-view .anchor-link, +.conpherence-transaction-view .phabricator-content-source-view { color: {$lightgreytext}; line-height: 16px; font-size: {$smallerfontsize}; } -.conpherence-message-pane .conpherence-transaction-content { +.conpherence-transaction-view .conpherence-transaction-content { padding: 2px 0 8px 0; } @@ -453,7 +451,7 @@ right: 0; } -input.conpherence-search-input { +.conpherence-search-form-view input.conpherence-search-input { padding-left: 8px; width: calc(100% - 24px); border-radius: 20px; diff --git a/webroot/rsrc/css/application/conpherence/transaction.css b/webroot/rsrc/css/application/conpherence/transaction.css index 62454c448e..6e52a8c30c 100644 --- a/webroot/rsrc/css/application/conpherence/transaction.css +++ b/webroot/rsrc/css/application/conpherence/transaction.css @@ -28,10 +28,15 @@ font-weight: bold; } +/***** Thread Search **********************************************************/ + .conpherence-fulltext-results { - margin: 0 8px 8px; - background: {$lightgreybackground}; - border: 1px solid {$lightgreyborder}; + padding: 8px 0; +} + +.conpherence-fulltext-results + .conpherence-fulltext-results { + border-top: 2px solid {$thinblueborder}; + margin-top: -8px; } .conpherence-fulltext-result { @@ -46,3 +51,19 @@ .conpherence-fulltext-results .epoch-link { float: right; } + +.conpherence-message-pane .conpherence-fulltext-results + .conpherence-transaction-view.conpherence-fulltext-result { + margin-left: 0; + margin-right: 0; +} + +.conpherence-message-pane .conpherence-search-room-results .phui-object-box { + border: none; + margin: 0; +} + +.conpherence-message-pane .conpherence-search-room-results + .phui-object-box .phui-header-shell { + display: none; +} From 10e464142d10a83f64454203963397382d1ec15e Mon Sep 17 00:00:00 2001 From: Chad Little Date: Sat, 15 Oct 2016 13:59:30 +0000 Subject: [PATCH 24/55] Focus search or pontificate in Conpherence when toggled Summary: This focuses the search field when the user opens search, and then the textarea for pontificate if the search box is closed. Test Plan: Open/Close/Open/Close Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D16709 --- resources/celerity/map.php | 18 +++++++++--------- .../conpherence/behavior-conpherence-search.js | 7 +++++++ 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index f6ec637a91..c455ddd156 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -436,7 +436,7 @@ return array( 'rsrc/js/application/calendar/behavior-recurring-edit.js' => '5f1c4d5f', 'rsrc/js/application/config/behavior-reorder-fields.js' => 'b6993408', 'rsrc/js/application/conpherence/ConpherenceThreadManager.js' => '01774ab2', - 'rsrc/js/application/conpherence/behavior-conpherence-search.js' => 'dfa4e1ac', + 'rsrc/js/application/conpherence/behavior-conpherence-search.js' => '3e137827', 'rsrc/js/application/conpherence/behavior-durable-column.js' => 'c5238acb', 'rsrc/js/application/conpherence/behavior-menu.js' => '07928ca3', 'rsrc/js/application/conpherence/behavior-participant-pane.js' => '8604caa8', @@ -666,7 +666,7 @@ return array( 'javelin-behavior-conpherence-menu' => '07928ca3', 'javelin-behavior-conpherence-participant-pane' => '8604caa8', 'javelin-behavior-conpherence-pontificate' => 'f2e58483', - 'javelin-behavior-conpherence-search' => 'dfa4e1ac', + 'javelin-behavior-conpherence-search' => '3e137827', 'javelin-behavior-countdown-timer' => 'e4cc26b3', 'javelin-behavior-dark-console' => 'f411b6ae', 'javelin-behavior-dashboard-async-panel' => '469c0d9e', @@ -1227,6 +1227,13 @@ return array( 'javelin-util', 'javelin-uri', ), + '3e137827' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-util', + 'javelin-workflow', + 'javelin-stratcom', + ), '3f5d6dbf' => array( 'javelin-behavior', 'javelin-dom', @@ -2088,13 +2095,6 @@ return array( 'df5e11d2' => array( 'javelin-install', ), - 'dfa4e1ac' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-util', - 'javelin-workflow', - 'javelin-stratcom', - ), 'e0ec7f2f' => array( 'javelin-behavior', 'javelin-dom', diff --git a/webroot/rsrc/js/application/conpherence/behavior-conpherence-search.js b/webroot/rsrc/js/application/conpherence/behavior-conpherence-search.js index aa1c2469b7..07c0cda342 100644 --- a/webroot/rsrc/js/application/conpherence/behavior-conpherence-search.js +++ b/webroot/rsrc/js/application/conpherence/behavior-conpherence-search.js @@ -18,6 +18,13 @@ JX.behavior('conpherence-search', function() { shown = !shown; JX.DOM.alterClass(node, 'show-searchbar', !shown); + if (!shown) { + JX.$('conpherence-search-input').focus(); + } else { + var form_root = JX.DOM.find(document, 'div', 'conpherence-form'); + var textarea = JX.DOM.find(form_root, 'textarea'); + textarea.focus(); + } JX.Stratcom.invoke('resize'); } From b1449fab63ff073b771721a6da5d8c09351015f7 Mon Sep 17 00:00:00 2001 From: Giedrius Dubinskas Date: Mon, 17 Oct 2016 12:38:15 +0000 Subject: [PATCH 25/55] Fixed undefined variable error in call from ConduitIntListParameterType Summary: `$strict` parameter was missing in `$this->parseIntValue(...)` call. Test Plan: ``` $ curl http://$PHABRICATOR_HOST/api/maniphest.search -d api.token=$CONDUIT_TOKEN -d constraints[priorities][0]=90 -d limit=1 # OK ``` Reviewers: epriestley, #blessed_reviewers Reviewed By: epriestley, #blessed_reviewers Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D16712 --- .../conduit/parametertype/ConduitIntListParameterType.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/applications/conduit/parametertype/ConduitIntListParameterType.php b/src/applications/conduit/parametertype/ConduitIntListParameterType.php index 7733977d0e..27e0ff9843 100644 --- a/src/applications/conduit/parametertype/ConduitIntListParameterType.php +++ b/src/applications/conduit/parametertype/ConduitIntListParameterType.php @@ -7,7 +7,11 @@ final class ConduitIntListParameterType $list = parent::getParameterValue($request, $key, $strict); foreach ($list as $idx => $item) { - $list[$idx] = $this->parseIntValue($request, $key.'['.$idx.']', $item); + $list[$idx] = $this->parseIntValue( + $request, + $key.'['.$idx.']', + $item, + $strict); } return $list; From dd25b2b48bae75db4b9b0c1461a13f06c27263fc Mon Sep 17 00:00:00 2001 From: Chad Little Date: Sun, 16 Oct 2016 08:27:14 -0700 Subject: [PATCH 26/55] Remove imagePHIDs column from ConpherenceThread Summary: Ref T11730. Removes the unused column, seen no issues during past week migrations. Test Plan: Run migration, check database no longer contains column. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Maniphest Tasks: T11730 Differential Revision: https://secure.phabricator.com/D16711 --- resources/sql/autopatches/20161016.conpherence.imagephids.sql | 2 ++ src/applications/conpherence/storage/ConpherenceThread.php | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 resources/sql/autopatches/20161016.conpherence.imagephids.sql diff --git a/resources/sql/autopatches/20161016.conpherence.imagephids.sql b/resources/sql/autopatches/20161016.conpherence.imagephids.sql new file mode 100644 index 0000000000..98ba1ef5ce --- /dev/null +++ b/resources/sql/autopatches/20161016.conpherence.imagephids.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_conpherence.conpherence_thread + DROP COLUMN imagePHIDs; diff --git a/src/applications/conpherence/storage/ConpherenceThread.php b/src/applications/conpherence/storage/ConpherenceThread.php index 6032835c42..e912f52591 100644 --- a/src/applications/conpherence/storage/ConpherenceThread.php +++ b/src/applications/conpherence/storage/ConpherenceThread.php @@ -10,7 +10,6 @@ final class ConpherenceThread extends ConpherenceDAO protected $title; protected $topic; - protected $imagePHIDs = array(); // TODO; nuke after migrations protected $profileImagePHID; protected $messageCount; protected $recentParticipantPHIDs = array(); @@ -42,7 +41,6 @@ final class ConpherenceThread extends ConpherenceDAO self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'recentParticipantPHIDs' => self::SERIALIZATION_JSON, - 'imagePHIDs' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'title' => 'text255?', From ac8e11359d9ca70f68b3a4b5a09fac0f890a1f4b Mon Sep 17 00:00:00 2001 From: Chad Little Date: Sat, 15 Oct 2016 20:26:15 -0700 Subject: [PATCH 27/55] Remove 'full-display' setting from Conpherence, spruce up search results Summary: This removes 'full-display', 'minimal-display' from Conpherence, which I recall was because we had 2 UIs for column and regular chat. I'm also tossing in slightly nicer search results, with a link to the actual message and the full date shown for context. Test Plan: Post a message in mobile, tablet, full conpherence, and in durable column. Clean up UI in durable column. Do a search in Full UI, click on result date, get taken to the message... usually. My test data is a little wonky, but I think this works most of the time. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D16710 --- resources/celerity/map.php | 80 +++++++++---------- .../ConpherenceTransactionRenderer.php | 4 +- .../ConpherenceUpdateController.php | 66 +++++++-------- .../controller/ConpherenceViewController.php | 1 - .../query/ConpherenceThreadSearchEngine.php | 2 +- .../view/ConpherenceDurableColumnView.php | 3 +- .../view/ConpherenceTransactionView.php | 59 ++++++-------- .../conpherence/durable-column.css | 63 ++++----------- .../application/conpherence/message-pane.css | 2 +- .../conpherence/ConpherenceThreadManager.js | 9 --- .../behavior-conpherence-search.js | 11 +++ .../conpherence/behavior-durable-column.js | 3 +- 12 files changed, 124 insertions(+), 179 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index c455ddd156..1cbecdaf99 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,10 +7,10 @@ */ return array( 'names' => array( - 'conpherence.pkg.css' => 'f934296b', + 'conpherence.pkg.css' => '49b8aaac', 'conpherence.pkg.js' => 'cbe4d9be', 'core.pkg.css' => 'b99bbf5e', - 'core.pkg.js' => '30185d95', + 'core.pkg.js' => '3eb7abf7', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => 'e1d704ce', 'differential.pkg.js' => '634399e9', @@ -46,10 +46,10 @@ return array( 'rsrc/css/application/config/config-template.css' => '8f18fa41', 'rsrc/css/application/config/setup-issue.css' => 'f794cfc3', 'rsrc/css/application/config/unhandled-exception.css' => '4c96257a', - 'rsrc/css/application/conpherence/durable-column.css' => '44bcaa19', + 'rsrc/css/application/conpherence/durable-column.css' => 'd82e130c', 'rsrc/css/application/conpherence/header-pane.css' => 'e8acbd37', 'rsrc/css/application/conpherence/menu.css' => '4f51db5a', - 'rsrc/css/application/conpherence/message-pane.css' => '7a94bf5e', + 'rsrc/css/application/conpherence/message-pane.css' => 'b80f1675', 'rsrc/css/application/conpherence/notification.css' => '965db05b', 'rsrc/css/application/conpherence/participant-pane.css' => '7bba0b56', 'rsrc/css/application/conpherence/transaction.css' => '85129c68', @@ -435,9 +435,9 @@ return array( 'rsrc/js/application/calendar/behavior-month-view.js' => 'fe33e256', 'rsrc/js/application/calendar/behavior-recurring-edit.js' => '5f1c4d5f', 'rsrc/js/application/config/behavior-reorder-fields.js' => 'b6993408', - 'rsrc/js/application/conpherence/ConpherenceThreadManager.js' => '01774ab2', - 'rsrc/js/application/conpherence/behavior-conpherence-search.js' => '3e137827', - 'rsrc/js/application/conpherence/behavior-durable-column.js' => 'c5238acb', + 'rsrc/js/application/conpherence/ConpherenceThreadManager.js' => '358c717b', + 'rsrc/js/application/conpherence/behavior-conpherence-search.js' => '9bbf3762', + 'rsrc/js/application/conpherence/behavior-durable-column.js' => 'aa3bd034', 'rsrc/js/application/conpherence/behavior-menu.js' => '07928ca3', 'rsrc/js/application/conpherence/behavior-participant-pane.js' => '8604caa8', 'rsrc/js/application/conpherence/behavior-pontificate.js' => 'f2e58483', @@ -616,13 +616,13 @@ return array( 'conduit-api-css' => '7bc725c4', 'config-options-css' => '0ede4c9b', 'config-page-css' => '8798e14f', - 'conpherence-durable-column-view' => '44bcaa19', + 'conpherence-durable-column-view' => 'd82e130c', 'conpherence-header-pane-css' => 'e8acbd37', 'conpherence-menu-css' => '4f51db5a', - 'conpherence-message-pane-css' => '7a94bf5e', + 'conpherence-message-pane-css' => 'b80f1675', 'conpherence-notification-css' => '965db05b', 'conpherence-participant-pane-css' => '7bba0b56', - 'conpherence-thread-manager' => '01774ab2', + 'conpherence-thread-manager' => '358c717b', 'conpherence-transaction-css' => '85129c68', 'd3' => 'a11a5ff2', 'differential-changeset-view-css' => '9ef7d354', @@ -666,7 +666,7 @@ return array( 'javelin-behavior-conpherence-menu' => '07928ca3', 'javelin-behavior-conpherence-participant-pane' => '8604caa8', 'javelin-behavior-conpherence-pontificate' => 'f2e58483', - 'javelin-behavior-conpherence-search' => '3e137827', + 'javelin-behavior-conpherence-search' => '9bbf3762', 'javelin-behavior-countdown-timer' => 'e4cc26b3', 'javelin-behavior-dark-console' => 'f411b6ae', 'javelin-behavior-dashboard-async-panel' => '469c0d9e', @@ -695,7 +695,7 @@ return array( 'javelin-behavior-diffusion-pull-lastmodified' => 'f01586dc', 'javelin-behavior-doorkeeper-tag' => 'e5822781', 'javelin-behavior-drydock-live-operation-status' => '901935ef', - 'javelin-behavior-durable-column' => 'c5238acb', + 'javelin-behavior-durable-column' => 'aa3bd034', 'javelin-behavior-editengine-reorder-configs' => 'd7a74243', 'javelin-behavior-editengine-reorder-fields' => 'b59e1e96', 'javelin-behavior-error-log' => '6882e80a', @@ -974,17 +974,6 @@ return array( 'javelin-request', 'javelin-typeahead-source', ), - '01774ab2' => array( - 'javelin-dom', - 'javelin-util', - 'javelin-stratcom', - 'javelin-install', - 'javelin-aphlict', - 'javelin-workflow', - 'javelin-router', - 'javelin-behavior-device', - 'javelin-vector', - ), '019f36c4' => array( 'javelin-behavior', 'javelin-dom', @@ -1211,6 +1200,17 @@ return array( 'javelin-dom', 'javelin-workflow', ), + '358c717b' => array( + 'javelin-dom', + 'javelin-util', + 'javelin-stratcom', + 'javelin-install', + 'javelin-aphlict', + 'javelin-workflow', + 'javelin-router', + 'javelin-behavior-device', + 'javelin-vector', + ), '3ab51e2c' => array( 'javelin-behavior', 'javelin-behavior-device', @@ -1227,13 +1227,6 @@ return array( 'javelin-util', 'javelin-uri', ), - '3e137827' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-util', - 'javelin-workflow', - 'javelin-stratcom', - ), '3f5d6dbf' => array( 'javelin-behavior', 'javelin-dom', @@ -1759,6 +1752,13 @@ return array( 'phabricator-phtize', 'changeset-view-manager', ), + '9bbf3762' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-util', + 'javelin-workflow', + 'javelin-stratcom', + ), '9bdbbab0' => array( 'javelin-behavior', 'javelin-dom', @@ -1839,6 +1839,16 @@ return array( 'javelin-util', 'phabricator-prefab', ), + 'aa3bd034' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-stratcom', + 'javelin-behavior-device', + 'javelin-scrollbar', + 'javelin-quicksand', + 'phabricator-keyboard-shortcut', + 'conpherence-thread-manager', + ), 'ab2f381b' => array( 'javelin-request', 'javelin-behavior', @@ -1966,16 +1976,6 @@ return array( 'javelin-install', 'javelin-dom', ), - 'c5238acb' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-stratcom', - 'javelin-behavior-device', - 'javelin-scrollbar', - 'javelin-quicksand', - 'phabricator-keyboard-shortcut', - 'conpherence-thread-manager', - ), 'c587b80f' => array( 'javelin-install', ), diff --git a/src/applications/conpherence/ConpherenceTransactionRenderer.php b/src/applications/conpherence/ConpherenceTransactionRenderer.php index 341d287e8b..187247e063 100644 --- a/src/applications/conpherence/ConpherenceTransactionRenderer.php +++ b/src/applications/conpherence/ConpherenceTransactionRenderer.php @@ -5,7 +5,6 @@ final class ConpherenceTransactionRenderer extends Phobject { public static function renderTransactions( PhabricatorUser $user, ConpherenceThread $conpherence, - $full_display = true, $marker_type = 'older') { $transactions = $conpherence->getTransactions(); @@ -74,8 +73,7 @@ final class ConpherenceTransactionRenderer extends Phobject { ->setUser($user) ->setConpherenceThread($conpherence) ->setHandles($handles) - ->setMarkupEngine($engine) - ->setFullDisplay($full_display); + ->setMarkupEngine($engine); foreach ($transactions as $transaction) { $collapsed = false; diff --git a/src/applications/conpherence/controller/ConpherenceUpdateController.php b/src/applications/conpherence/controller/ConpherenceUpdateController.php index 534470e998..724b802566 100644 --- a/src/applications/conpherence/controller/ConpherenceUpdateController.php +++ b/src/applications/conpherence/controller/ConpherenceUpdateController.php @@ -335,9 +335,6 @@ final class ConpherenceUpdateController $request->getInt('latest_transaction_id')) ->appendForm($form); - if ($request->getExists('minimal_display')) { - $view->addHiddenInput('minimal_display', true); - } return $view; } @@ -477,9 +474,6 @@ final class ConpherenceUpdateController ->addHiddenInput('__continue__', true) ->appendChild($form); - if ($request->getExists('minimal_display')) { - $view->addHiddenInput('minimal_display', true); - } if ($request->getExists('force_ajax')) { $view->addHiddenInput('force_ajax', true); } @@ -492,7 +486,6 @@ final class ConpherenceUpdateController $conpherence_id, $latest_transaction_id) { - $minimal_display = $this->getRequest()->getExists('minimal_display'); $need_transactions = false; $need_participant_cache = true; switch ($action) { @@ -525,8 +518,7 @@ final class ConpherenceUpdateController if ($need_transactions && $conpherence->getTransactions()) { $data = ConpherenceTransactionRenderer::renderTransactions( $user, - $conpherence, - !$minimal_display); + $conpherence); $key = PhabricatorConpherenceColumnMinimizeSetting::SETTINGKEY; $minimized = $user->getUserSetting($key); if (!$minimized) { @@ -547,35 +539,33 @@ final class ConpherenceUpdateController $nav_item = null; $header = null; $people_widget = null; - if (!$minimal_display) { - switch ($action) { - case ConpherenceUpdateActions::METADATA: - $policy_objects = id(new PhabricatorPolicyQuery()) - ->setViewer($user) - ->setObject($conpherence) - ->execute(); - $header = $this->buildHeaderPaneContent( - $conpherence, - $policy_objects); - $header = hsprintf('%s', $header); - $nav_item = id(new ConpherenceThreadListView()) - ->setUser($user) - ->setBaseURI($this->getApplicationURI()) - ->renderSingleThread($conpherence, $policy_objects); - $nav_item = hsprintf('%s', $nav_item); - break; - case ConpherenceUpdateActions::ADD_PERSON: - $people_widget = id(new ConpherenceParticipantView()) - ->setUser($user) - ->setConpherence($conpherence) - ->setUpdateURI($update_uri); - $people_widget = hsprintf('%s', $people_widget->render()); - break; - case ConpherenceUpdateActions::REMOVE_PERSON: - case ConpherenceUpdateActions::NOTIFICATIONS: - default: - break; - } + switch ($action) { + case ConpherenceUpdateActions::METADATA: + $policy_objects = id(new PhabricatorPolicyQuery()) + ->setViewer($user) + ->setObject($conpherence) + ->execute(); + $header = $this->buildHeaderPaneContent( + $conpherence, + $policy_objects); + $header = hsprintf('%s', $header); + $nav_item = id(new ConpherenceThreadListView()) + ->setUser($user) + ->setBaseURI($this->getApplicationURI()) + ->renderSingleThread($conpherence, $policy_objects); + $nav_item = hsprintf('%s', $nav_item); + break; + case ConpherenceUpdateActions::ADD_PERSON: + $people_widget = id(new ConpherenceParticipantView()) + ->setUser($user) + ->setConpherence($conpherence) + ->setUpdateURI($update_uri); + $people_widget = hsprintf('%s', $people_widget->render()); + break; + case ConpherenceUpdateActions::REMOVE_PERSON: + case ConpherenceUpdateActions::NOTIFICATIONS: + default: + break; } $data = $conpherence->getDisplayData($user); $dropdown_query = id(new AphlictDropdownDataQuery()) diff --git a/src/applications/conpherence/controller/ConpherenceViewController.php b/src/applications/conpherence/controller/ConpherenceViewController.php index 64a3a67bcf..8d93f03b7d 100644 --- a/src/applications/conpherence/controller/ConpherenceViewController.php +++ b/src/applications/conpherence/controller/ConpherenceViewController.php @@ -73,7 +73,6 @@ final class ConpherenceViewController extends $data = ConpherenceTransactionRenderer::renderTransactions( $user, $conpherence, - $full_display = true, $marker_type); $messages = ConpherenceTransactionRenderer::renderMessagePaneContent( $data['transactions'], diff --git a/src/applications/conpherence/query/ConpherenceThreadSearchEngine.php b/src/applications/conpherence/query/ConpherenceThreadSearchEngine.php index d9d06f8333..3129028c2e 100644 --- a/src/applications/conpherence/query/ConpherenceThreadSearchEngine.php +++ b/src/applications/conpherence/query/ConpherenceThreadSearchEngine.php @@ -199,7 +199,7 @@ final class ConpherenceThreadSearchEngine ->setMarkupEngine($engines[$conpherence_phid]) ->setConpherenceThread($conpherence) ->setConpherenceTransaction($xaction) - ->setFullDisplay(true) + ->setSearchResult(true) ->addClass('conpherence-fulltext-result'); if ($message['match']) { diff --git a/src/applications/conpherence/view/ConpherenceDurableColumnView.php b/src/applications/conpherence/view/ConpherenceDurableColumnView.php index 786cbea174..e3240bc2e4 100644 --- a/src/applications/conpherence/view/ConpherenceDurableColumnView.php +++ b/src/applications/conpherence/view/ConpherenceDurableColumnView.php @@ -413,8 +413,7 @@ final class ConpherenceDurableColumnView extends AphrontTagView { $data = ConpherenceTransactionRenderer::renderTransactions( $this->getUser(), - $conpherence, - $full_display = false); + $conpherence); $messages = ConpherenceTransactionRenderer::renderMessagePaneContent( $data['transactions'], $data['oldest_transaction_id'], diff --git a/src/applications/conpherence/view/ConpherenceTransactionView.php b/src/applications/conpherence/view/ConpherenceTransactionView.php index e3991ab979..c18a672ca2 100644 --- a/src/applications/conpherence/view/ConpherenceTransactionView.php +++ b/src/applications/conpherence/view/ConpherenceTransactionView.php @@ -6,8 +6,8 @@ final class ConpherenceTransactionView extends AphrontView { private $conpherenceTransaction; private $handles; private $markupEngine; - private $fullDisplay; private $classes = array(); + private $searchResult; private $timeOnly; public function setConpherenceThread(ConpherenceThread $t) { @@ -47,17 +47,13 @@ final class ConpherenceTransactionView extends AphrontView { return $this->markupEngine; } - public function setFullDisplay($bool) { - $this->fullDisplay = $bool; + public function addClass($class) { + $this->classes[] = $class; return $this; } - private function getFullDisplay() { - return $this->fullDisplay; - } - - public function addClass($class) { - $this->classes[] = $class; + public function setSearchResult($result) { + $this->searchResult = $result; return $this; } @@ -100,11 +96,7 @@ final class ConpherenceTransactionView extends AphrontView { $image = $this->renderTransactionImage(); $content = $this->renderTransactionContent(); $classes = implode(' ', $this->classes); - - $transaction_dom_id = null; - if ($this->getFullDisplay()) { - $transaction_dom_id = 'anchor-'.$transaction->getID(); - } + $transaction_dom_id = 'anchor-'.$transaction->getID(); $header = phutil_tag_div( 'conpherence-transaction-header grouped', @@ -137,12 +129,25 @@ final class ConpherenceTransactionView extends AphrontView { $tip = phabricator_datetime($transaction->getDateCreated(), $viewer); $label = phabricator_time($transaction->getDateCreated(), $viewer); $width = 360; - if ($this->getFullDisplay()) { - Javelin::initBehavior('phabricator-watch-anchor'); - $anchor = id(new PhabricatorAnchorView()) - ->setAnchorName($transaction->getID()) - ->render(); + Javelin::initBehavior('phabricator-watch-anchor'); + $anchor = id(new PhabricatorAnchorView()) + ->setAnchorName($transaction->getID()) + ->render(); + + if ($this->searchResult) { + $uri = $thread->getMonogram(); + $info[] = hsprintf( + '%s', + javelin_tag( + 'a', + array( + 'href' => '/'.$uri.'#'.$transaction->getID(), + 'class' => 'transaction-date', + 'sigil' => 'conpherence-search-result-jump', + ), + $tip)); + } else { $info[] = hsprintf( '%s%s', $anchor, @@ -150,7 +155,7 @@ final class ConpherenceTransactionView extends AphrontView { 'a', array( 'href' => '#'.$transaction->getID(), - 'class' => 'anchor-link', + 'class' => 'transaction-date anchor-link', 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => $tip, @@ -158,20 +163,6 @@ final class ConpherenceTransactionView extends AphrontView { ), ), $label)); - } else { - $href = '/'.$thread->getMonogram().'#'.$transaction->getID(); - $info[] = javelin_tag( - 'a', - array( - 'href' => $href, - 'class' => 'epoch-link', - 'sigil' => 'has-tooltip', - 'meta' => array( - 'tip' => $tip, - 'size' => $width, - ), - ), - $label); } return phutil_tag( diff --git a/webroot/rsrc/css/application/conpherence/durable-column.css b/webroot/rsrc/css/application/conpherence/durable-column.css index f94fa1f6b8..d141115b73 100644 --- a/webroot/rsrc/css/application/conpherence/durable-column.css +++ b/webroot/rsrc/css/application/conpherence/durable-column.css @@ -180,19 +180,6 @@ padding: 8px 12px 0; } -.conpherence-durable-column-transactions - .conpherence-transaction-view.conpherence-edited { - color: {$lightgreytext}; - margin: 0; - padding: 0; - font-style: italic; -} - -.conpherence-durable-column-transactions .conpherence-edited - .conpherence-transaction-header { - display: none; -} - .conpherence-durable-column-transactions .conpherence-transaction-view { background: none; margin: 0; @@ -205,48 +192,29 @@ word-wrap: break-word; } -.conpherence-durable-column-transactions .conpherence-transaction-detail { - border: 0; - margin: 0 0 0 32px; +.conpherence-durable-column-transactions .conpherence-transaction-view + .conpherence-transaction-detail { + border: 0; + margin: 0 0 0 32px; } -.conpherence-durable-column-transactions .conpherence-transaction-detail - .conpherence-transaction-header { +.conpherence-durable-column-transactions .conpherence-transaction-view + .conpherence-transaction-detail .conpherence-transaction-header { background: none; padding: 0 0 2px 0; } .conpherence-durable-column-transactions -.conpherence-transaction-view.date-marker { - margin: 12px 0 0; + .conpherence-transaction-view.date-marker { + margin: 12px 0 0; } .conpherence-durable-column-transactions -.conpherence-transaction-view.date-marker .date { - left: 0; - font-size: {$normalfontsize}; - top: -14px; - padding: 0 6px 0 0; -} - -.conpherence-durable-column-transactions .conpherence-transaction-detail -.conpherence-transaction-header .conpherence-transaction-info { - color: {$lightbluetext}; - font-size: {$smallerfontsize}; -} - -.conpherence-transaction-header .epoch-link { - color: {$lightgreytext}; -} - -.conpherence-durable-column-transactions .conpherence-transaction-detail -.conpherence-transaction-header .phui-link-person { - margin: 0 8px 0 0; -} - -.conpherence-durable-column-transactions .conpherence-transaction-detail -.conpherence-transaction-content .phui-link-person { - color: #000; + .conpherence-transaction-view.date-marker .date { + left: 0; + font-size: {$normalfontsize}; + top: -10px; + padding: 0 6px 0 0; } .conpherence-durable-column-transactions @@ -267,9 +235,8 @@ img { } .conpherence-durable-column-transactions .conpherence-transaction-detail -.conpherence-transaction-content { - background: #fff; - padding: 0 0 8px 0; + .conpherence-transaction-content { + padding: 0 0 8px 0; } .conpherence-durable-column-textarea { diff --git a/webroot/rsrc/css/application/conpherence/message-pane.css b/webroot/rsrc/css/application/conpherence/message-pane.css index 37ee9581fe..dd1eccfc80 100644 --- a/webroot/rsrc/css/application/conpherence/message-pane.css +++ b/webroot/rsrc/css/application/conpherence/message-pane.css @@ -321,7 +321,7 @@ } .conpherence-transaction-view .conpherence-transaction-info, -.conpherence-transaction-view .anchor-link, +.conpherence-transaction-view .transaction-date, .conpherence-transaction-view .phabricator-content-source-view { color: {$lightgreytext}; line-height: 16px; diff --git a/webroot/rsrc/js/application/conpherence/ConpherenceThreadManager.js b/webroot/rsrc/js/application/conpherence/ConpherenceThreadManager.js index 0a39863c74..831cceab59 100644 --- a/webroot/rsrc/js/application/conpherence/ConpherenceThreadManager.js +++ b/webroot/rsrc/js/application/conpherence/ConpherenceThreadManager.js @@ -31,7 +31,6 @@ JX.install('ConpherenceThreadManager', { _transactionCache: null, _canEditLoadedThread: null, _updating: null, - _minimalDisplay: false, _messagesRootCallback: JX.bag, _willLoadThreadCallback: JX.bag, _didLoadThreadCallback: JX.bag, @@ -150,11 +149,6 @@ JX.install('ConpherenceThreadManager', { return this._canEditLoadedThread; }, - setMinimalDisplay: function(bool) { - this._minimalDisplay = bool; - return this; - }, - setMessagesRootCallback: function(callback) { this._messagesRootCallback = callback; return this; @@ -196,9 +190,6 @@ JX.install('ConpherenceThreadManager', { }, _getParams: function(base_params) { - if (this._minimalDisplay) { - base_params.minimal_display = true; - } if (this._latestTransactionID) { base_params.latest_transaction_id = this._latestTransactionID; } diff --git a/webroot/rsrc/js/application/conpherence/behavior-conpherence-search.js b/webroot/rsrc/js/application/conpherence/behavior-conpherence-search.js index 07c0cda342..39ddd29f65 100644 --- a/webroot/rsrc/js/application/conpherence/behavior-conpherence-search.js +++ b/webroot/rsrc/js/application/conpherence/behavior-conpherence-search.js @@ -44,7 +44,13 @@ JX.behavior('conpherence-search', function() { }); request.setData({fulltext: search_text}); request.send(); + } + function _viewResult(e) { + e.kill(); + var uri = e.getNode('tag:a'); + _toggleSearch(e); + JX.$U(uri).go(); } JX.Stratcom.listen( @@ -63,6 +69,11 @@ JX.behavior('conpherence-search', function() { _doSearch(e); }); + JX.Stratcom.listen( + 'click', + 'conpherence-search-result-jump', + _viewResult); + JX.Stratcom.listen( 'click', 'conpherence-search-toggle', diff --git a/webroot/rsrc/js/application/conpherence/behavior-durable-column.js b/webroot/rsrc/js/application/conpherence/behavior-durable-column.js index a7fe3fbef7..c6c82bc3be 100644 --- a/webroot/rsrc/js/application/conpherence/behavior-durable-column.js +++ b/webroot/rsrc/js/application/conpherence/behavior-durable-column.js @@ -114,7 +114,6 @@ JX.behavior('durable-column', function(config, statics) { */ var threadManager = new JX.ConpherenceThreadManager(); - threadManager.setMinimalDisplay(true); threadManager.setMessagesRootCallback(function() { return _getColumnMessagesNode(); }); @@ -282,7 +281,7 @@ JX.behavior('durable-column', function(config, statics) { function _sendMessage(e) { e.kill(); var form = _getColumnFormNode(); - threadManager.sendMessage(form, { minimal_display: true }); + threadManager.sendMessage(form, {}); } JX.Stratcom.listen( From dad17fb98a6b19be21f904b2f09b7cecd4d78abc Mon Sep 17 00:00:00 2001 From: epriestley Date: Mon, 17 Oct 2016 15:18:09 -0700 Subject: [PATCH 28/55] Make "metamta.differential.inline-patches" imply a reasonable byte limit, not just a line limit Summary: Fixes T11748. This option currently implies a line limit (e.g., inline patches that are less than 100 lines long). This breaks down if a diff has a 10MB line, like a huge blob of JSON all on one line. For now, imply a reasonable byte limit (256 bytes per line). See T11767 for future work to make this and related options more cohesive. Test Plan: - With option at `1000`: sent Differential email, saw patches inlined. - With option at `10`: sent Differential email, saw patches dropped because of the byte limit. - `var_dump()`'d the actual limits and used `bin/worker execute --id ...` to sanity check that things were working properly. Reviewers: chad Reviewed By: chad Maniphest Tasks: T11748 Differential Revision: https://secure.phabricator.com/D16714 --- .../PhabricatorDifferentialConfigOptions.php | 21 ++++++++++----- .../editor/DifferentialTransactionEditor.php | 27 ++++++++++++++++--- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/src/applications/differential/config/PhabricatorDifferentialConfigOptions.php b/src/applications/differential/config/PhabricatorDifferentialConfigOptions.php index 1d7b646270..3eb1568a76 100644 --- a/src/applications/differential/config/PhabricatorDifferentialConfigOptions.php +++ b/src/applications/differential/config/PhabricatorDifferentialConfigOptions.php @@ -70,6 +70,19 @@ final class PhabricatorDifferentialConfigOptions ); } + $inline_description = $this->deformat( + pht(<<newOption( 'differential.fields', @@ -254,13 +267,7 @@ final class PhabricatorDifferentialConfigOptions 'int', 0) ->setSummary(pht('Inline patches in email, as body text.')) - ->setDescription( - pht( - "To include patches inline in email bodies, set this to a ". - "positive integer. Patches will be inlined if they are at most ". - "that many lines. For instance, a value of 100 means 'inline ". - "patches if they are no longer than 100 lines'. By default, ". - "patches are not inlined.")), + ->setDescription($inline_description), $this->newOption( 'metamta.differential.patch-format', 'enum', diff --git a/src/applications/differential/editor/DifferentialTransactionEditor.php b/src/applications/differential/editor/DifferentialTransactionEditor.php index 3e9fe2b99f..b4828494c0 100644 --- a/src/applications/differential/editor/DifferentialTransactionEditor.php +++ b/src/applications/differential/editor/DifferentialTransactionEditor.php @@ -1264,11 +1264,30 @@ final class DifferentialTransactionEditor $config_attach = PhabricatorEnv::getEnvConfig($config_key_attach); if ($config_inline || $config_attach) { - $patch = $this->buildPatchForMail($diff); - $lines = substr_count($patch, "\n"); + $body_limit = PhabricatorEnv::getEnvConfig('metamta.email-body-limit'); - if ($config_inline && ($lines <= $config_inline)) { - $this->appendChangeDetailsForMail($object, $diff, $patch, $body); + $patch = $this->buildPatchForMail($diff); + if ($config_inline) { + $lines = substr_count($patch, "\n"); + $bytes = strlen($patch); + + // Limit the patch size to the smaller of 256 bytes per line or + // the mail body limit. This prevents degenerate behavior for patches + // with one line that is 10MB long. See T11748. + $byte_limits = array(); + $byte_limits[] = (256 * $config_inline); + $byte_limits[] = $body_limit; + $byte_limit = min($byte_limits); + + $lines_ok = ($lines <= $config_inline); + $bytes_ok = ($bytes <= $byte_limit); + + if ($lines_ok && $bytes_ok) { + $this->appendChangeDetailsForMail($object, $diff, $patch, $body); + } else { + // TODO: Provide a helpful message about the patch being too + // large or lengthy here. + } } if ($config_attach) { From 4e831e786e7af465bd17f92f477a9983762895ea Mon Sep 17 00:00:00 2001 From: Giedrius Dubinskas Date: Tue, 18 Oct 2016 11:58:24 +0000 Subject: [PATCH 29/55] Fix Phriction document move on to existing document placeholder Summary: Looks like the logic was there already but some minor parts were missing. Fixes T8082. Test Plan: - Create document `/w/foo` - Delete document `/w/foo` - Create document `/w/bar` - Move document `/w/bar` for `/w/foo` No error was displayed and document `/w/bar` was moved to `/w/foo`. Reviewers: epriestley, #blessed_reviewers Reviewed By: epriestley, #blessed_reviewers Subscribers: Korvin Maniphest Tasks: T8082 Differential Revision: https://secure.phabricator.com/D16713 --- .../controller/PhrictionMoveController.php | 20 +++++++++++++++---- .../editor/PhrictionTransactionEditor.php | 16 ++++++++------- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/applications/phriction/controller/PhrictionMoveController.php b/src/applications/phriction/controller/PhrictionMoveController.php index a5febbbaaa..53e5ae16ec 100644 --- a/src/applications/phriction/controller/PhrictionMoveController.php +++ b/src/applications/phriction/controller/PhrictionMoveController.php @@ -31,11 +31,11 @@ final class PhrictionMoveController extends PhrictionController { if ($request->isFormPost()) { $v_note = $request->getStr('description'); $v_slug = $request->getStr('slug'); + $normal_slug = PhabricatorSlug::normalize($v_slug); // If what the user typed isn't what we're actually using, warn them // about it. if (strlen($v_slug)) { - $normal_slug = PhabricatorSlug::normalize($v_slug); $no_slash_slug = rtrim($normal_slug, '/'); if ($normal_slug !== $v_slug && $no_slash_slug !== $v_slug) { return $this->newDialog() @@ -66,9 +66,21 @@ final class PhrictionMoveController extends PhrictionController { $xactions[] = id(new PhrictionTransaction()) ->setTransactionType(PhrictionTransaction::TYPE_MOVE_TO) ->setNewValue($document); - $target_document = PhrictionDocument::initializeNewDocument( - $viewer, - $v_slug); + $target_document = id(new PhrictionDocumentQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withSlugs(array($normal_slug)) + ->needContent(true) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$target_document) { + $target_document = PhrictionDocument::initializeNewDocument( + $viewer, + $v_slug); + } try { $editor->applyTransactions($target_document, $xactions); $redir_uri = PhrictionDocument::getSlugURI( diff --git a/src/applications/phriction/editor/PhrictionTransactionEditor.php b/src/applications/phriction/editor/PhrictionTransactionEditor.php index f0606c65a4..af238dfcf9 100644 --- a/src/applications/phriction/editor/PhrictionTransactionEditor.php +++ b/src/applications/phriction/editor/PhrictionTransactionEditor.php @@ -588,6 +588,7 @@ final class PhrictionTransactionEditor // Prevent overwrites and no-op moves. $exists = PhrictionDocumentStatus::STATUS_EXISTS; if ($target_document) { + $message = null; if ($target_document->getSlug() == $source_document->getSlug()) { $message = pht( 'You can not move a document to its existing location. '. @@ -598,13 +599,14 @@ final class PhrictionTransactionEditor 'overwrite an existing document which is already at that '. 'location. Move or delete the existing document first.'); } - - $error = new PhabricatorApplicationTransactionValidationError( - $type, - pht('Invalid'), - $message, - $xaction); - $errors[] = $error; + if ($message !== null) { + $error = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Invalid'), + $message, + $xaction); + $errors[] = $error; + } } break; From 919eac3f904c3a4eff9fb9d5a5d2c7fb8b7a9b58 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Tue, 18 Oct 2016 10:23:39 -0700 Subject: [PATCH 30/55] Provide link to all rooms when on mobile Conpherence Summary: There isn't any link back to all your joined rooms when on mobile, add it here. Test Plan: Pull up mobile, click on menu, see list of threads, click on other room. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D16717 --- .../conpherence/controller/ConpherenceController.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/applications/conpherence/controller/ConpherenceController.php b/src/applications/conpherence/controller/ConpherenceController.php index 3645cfe87e..e6690a55d1 100644 --- a/src/applications/conpherence/controller/ConpherenceController.php +++ b/src/applications/conpherence/controller/ConpherenceController.php @@ -18,6 +18,12 @@ abstract class ConpherenceController extends PhabricatorController { // Local Links if ($conpherence) { + $nav->addMenuItem( + id(new PHUIListItemView()) + ->setName(pht('Joined Rooms')) + ->setType(PHUIListItemView::TYPE_LINK) + ->setHref($this->getApplicationURI())); + $nav->addMenuItem( id(new PHUIListItemView()) ->setName(pht('Edit Room')) From 860809ae79b1b3b9a72aca0ed38fa163bb833d35 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 18 Oct 2016 10:07:44 -0700 Subject: [PATCH 31/55] Reject high-frequency and out-of-range events during import Summary: Ref T10747. Don't let users import SECONDLY events, or events outside of the range of a signed 32-bit integer (these are likely not too hard to support, but they're more headaches than we need right now). Test Plan: Tried to import these no-good problem events, got helpful import errors. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10747 Differential Revision: https://secure.phabricator.com/D16716 --- src/__phutil_library_map__.php | 4 ++ .../PhabricatorCalendarImportEngine.php | 61 +++++++++++++++++++ .../PhabricatorCalendarImportEpochLogType.php | 34 +++++++++++ ...bricatorCalendarImportFrequencyLogType.php | 38 ++++++++++++ 4 files changed, 137 insertions(+) create mode 100644 src/applications/calendar/importlog/PhabricatorCalendarImportEpochLogType.php create mode 100644 src/applications/calendar/importlog/PhabricatorCalendarImportFrequencyLogType.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 02a69fb3aa..344fcbec7c 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2113,6 +2113,8 @@ phutil_register_library_map(array( 'PhabricatorCalendarImportEditor' => 'applications/calendar/editor/PhabricatorCalendarImportEditor.php', 'PhabricatorCalendarImportEmptyLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportEmptyLogType.php', 'PhabricatorCalendarImportEngine' => 'applications/calendar/import/PhabricatorCalendarImportEngine.php', + 'PhabricatorCalendarImportEpochLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportEpochLogType.php', + 'PhabricatorCalendarImportFrequencyLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportFrequencyLogType.php', 'PhabricatorCalendarImportICSFileTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportICSFileTransaction.php', 'PhabricatorCalendarImportIgnoredNodeLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportIgnoredNodeLogType.php', 'PhabricatorCalendarImportListController' => 'applications/calendar/controller/PhabricatorCalendarImportListController.php', @@ -6940,6 +6942,8 @@ phutil_register_library_map(array( 'PhabricatorCalendarImportEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorCalendarImportEmptyLogType' => 'PhabricatorCalendarImportLogType', 'PhabricatorCalendarImportEngine' => 'Phobject', + 'PhabricatorCalendarImportEpochLogType' => 'PhabricatorCalendarImportLogType', + 'PhabricatorCalendarImportFrequencyLogType' => 'PhabricatorCalendarImportLogType', 'PhabricatorCalendarImportICSFileTransaction' => 'PhabricatorCalendarImportTransactionType', 'PhabricatorCalendarImportIgnoredNodeLogType' => 'PhabricatorCalendarImportLogType', 'PhabricatorCalendarImportListController' => 'PhabricatorCalendarController', diff --git a/src/applications/calendar/import/PhabricatorCalendarImportEngine.php b/src/applications/calendar/import/PhabricatorCalendarImportEngine.php index 14c96a76ae..6555b693e7 100644 --- a/src/applications/calendar/import/PhabricatorCalendarImportEngine.php +++ b/src/applications/calendar/import/PhabricatorCalendarImportEngine.php @@ -63,6 +63,67 @@ abstract class PhabricatorCalendarImportEngine } } + // Reject events which have dates outside of the range of a signed + // 32-bit integer. We'll need to accommodate a wider range of events + // eventually, but have about 20 years until it's an issue and we'll + // all be dead by then. + foreach ($nodes as $key => $node) { + $dates = array(); + $dates[] = $node->getStartDateTime(); + $dates[] = $node->getEndDateTime(); + $dates[] = $node->getCreatedDateTime(); + $dates[] = $node->getModifiedDateTime(); + $rrule = $node->getRecurrenceRule(); + if ($rrule) { + $dates[] = $rrule->getUntil(); + } + + $bad_date = false; + foreach ($dates as $date) { + if ($date === null) { + continue; + } + + $year = $date->getYear(); + if ($year < 1970 || $year > 2037) { + $bad_date = true; + break; + } + } + + if ($bad_date) { + $import->newLogMessage( + PhabricatorCalendarImportEpochLogType::LOGTYPE, + array()); + unset($nodes[$key]); + } + } + + // Reject events which occur too frequently. Users do not normally define + // these events and the UI and application make many assumptions which are + // incompatible with events recurring once per second. + foreach ($nodes as $key => $node) { + $rrule = $node->getRecurrenceRule(); + if (!$rrule) { + // This is not a recurring event, so we don't need to check the + // frequency. + continue; + } + $scale = $rrule->getFrequencyScale(); + if ($scale >= PhutilCalendarRecurrenceRule::SCALE_DAILY) { + // This is a daily, weekly, monthly, or yearly event. These are + // supported. + } else { + // This is an hourly, minutely, or secondly event. + $import->newLogMessage( + PhabricatorCalendarImportFrequencyLogType::LOGTYPE, + array( + 'frequency' => $rrule->getFrequency(), + )); + unset($nodes[$key]); + } + } + $node_map = array(); foreach ($nodes as $node) { $full_uid = $this->getFullNodeUID($node); diff --git a/src/applications/calendar/importlog/PhabricatorCalendarImportEpochLogType.php b/src/applications/calendar/importlog/PhabricatorCalendarImportEpochLogType.php new file mode 100644 index 0000000000..22570e4859 --- /dev/null +++ b/src/applications/calendar/importlog/PhabricatorCalendarImportEpochLogType.php @@ -0,0 +1,34 @@ +getParameter('frequency'); + + return pht( + 'Ignored an event with an unsupported frequency rule ("%s"). Events '. + 'which repeat more frequently than daily are not supported.', + $frequency); + } + + public function getDisplayIcon( + PhabricatorUser $viewer, + PhabricatorCalendarImportLog $log) { + return 'fa-clock-o'; + } + + public function getDisplayColor( + PhabricatorUser $viewer, + PhabricatorCalendarImportLog $log) { + return 'red'; + } + +} From d8318089c85e5c58425e48ddbe7244031f7b6908 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Tue, 18 Oct 2016 11:22:54 -0700 Subject: [PATCH 32/55] Fix super long topics in Conpherence header Summary: Add some ellipsis if the topic is absurdly long. Test Plan: desktop, mobile, tablet. MMMMMMMmmmMMMMMMmMMMmMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D16718 --- resources/celerity/map.php | 6 +++--- webroot/rsrc/css/application/conpherence/header-pane.css | 8 ++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 1cbecdaf99..bf8f0eed15 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,7 +7,7 @@ */ return array( 'names' => array( - 'conpherence.pkg.css' => '49b8aaac', + 'conpherence.pkg.css' => '6412a825', 'conpherence.pkg.js' => 'cbe4d9be', 'core.pkg.css' => 'b99bbf5e', 'core.pkg.js' => '3eb7abf7', @@ -47,7 +47,7 @@ return array( 'rsrc/css/application/config/setup-issue.css' => 'f794cfc3', 'rsrc/css/application/config/unhandled-exception.css' => '4c96257a', 'rsrc/css/application/conpherence/durable-column.css' => 'd82e130c', - 'rsrc/css/application/conpherence/header-pane.css' => 'e8acbd37', + 'rsrc/css/application/conpherence/header-pane.css' => '1c81cda6', 'rsrc/css/application/conpherence/menu.css' => '4f51db5a', 'rsrc/css/application/conpherence/message-pane.css' => 'b80f1675', 'rsrc/css/application/conpherence/notification.css' => '965db05b', @@ -617,7 +617,7 @@ return array( 'config-options-css' => '0ede4c9b', 'config-page-css' => '8798e14f', 'conpherence-durable-column-view' => 'd82e130c', - 'conpherence-header-pane-css' => 'e8acbd37', + 'conpherence-header-pane-css' => '1c81cda6', 'conpherence-menu-css' => '4f51db5a', 'conpherence-message-pane-css' => 'b80f1675', 'conpherence-notification-css' => '965db05b', diff --git a/webroot/rsrc/css/application/conpherence/header-pane.css b/webroot/rsrc/css/application/conpherence/header-pane.css index a2f876ae5f..f181a1adb1 100644 --- a/webroot/rsrc/css/application/conpherence/header-pane.css +++ b/webroot/rsrc/css/application/conpherence/header-pane.css @@ -18,6 +18,9 @@ padding: 0; font-size: 12px; margin: 0; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; } .conpherence-header-pane .phui-header-col1 { @@ -40,6 +43,11 @@ .conpherence-header-pane .phui-header-col2 { height: 40px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + width: 60%; + max-width: 0; } .conpherence-header-pane .phui-header-action-list .phui-header-action-item From 94a5a09d75441decb40771b8d918d4ef664b6d5a Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 18 Oct 2016 13:21:16 -0700 Subject: [PATCH 33/55] Add a SearchEngine for Calendar import logs Summary: Ref T10747. - Look at more than 25 logs! - Review your favorite logs. Heartwarming! :) Test Plan: Looked at logs. Wow! Logs! Reviewers: chad Reviewed By: chad Maniphest Tasks: T10747 Differential Revision: https://secure.phabricator.com/D16719 --- src/__phutil_library_map__.php | 6 ++ .../PhabricatorCalendarApplication.php | 4 + ...ricatorCalendarImportLogListController.php | 12 +++ ...habricatorCalendarImportViewController.php | 38 +-------- ...abricatorCalendarImportLogSearchEngine.php | 77 +++++++++++++++++ .../view/PhabricatorCalendarImportLogView.php | 84 +++++++++++++++++++ 6 files changed, 187 insertions(+), 34 deletions(-) create mode 100644 src/applications/calendar/controller/PhabricatorCalendarImportLogListController.php create mode 100644 src/applications/calendar/query/PhabricatorCalendarImportLogSearchEngine.php create mode 100644 src/applications/calendar/view/PhabricatorCalendarImportLogView.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 344fcbec7c..1733b447eb 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2119,8 +2119,11 @@ phutil_register_library_map(array( 'PhabricatorCalendarImportIgnoredNodeLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportIgnoredNodeLogType.php', 'PhabricatorCalendarImportListController' => 'applications/calendar/controller/PhabricatorCalendarImportListController.php', 'PhabricatorCalendarImportLog' => 'applications/calendar/storage/PhabricatorCalendarImportLog.php', + 'PhabricatorCalendarImportLogListController' => 'applications/calendar/controller/PhabricatorCalendarImportLogListController.php', 'PhabricatorCalendarImportLogQuery' => 'applications/calendar/query/PhabricatorCalendarImportLogQuery.php', + 'PhabricatorCalendarImportLogSearchEngine' => 'applications/calendar/query/PhabricatorCalendarImportLogSearchEngine.php', 'PhabricatorCalendarImportLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportLogType.php', + 'PhabricatorCalendarImportLogView' => 'applications/calendar/view/PhabricatorCalendarImportLogView.php', 'PhabricatorCalendarImportNameTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportNameTransaction.php', 'PhabricatorCalendarImportOriginalLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportOriginalLogType.php', 'PhabricatorCalendarImportOrphanLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportOrphanLogType.php', @@ -6952,8 +6955,11 @@ phutil_register_library_map(array( 'PhabricatorPolicyInterface', 'PhabricatorDestructibleInterface', ), + 'PhabricatorCalendarImportLogListController' => 'PhabricatorCalendarController', 'PhabricatorCalendarImportLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorCalendarImportLogSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorCalendarImportLogType' => 'Phobject', + 'PhabricatorCalendarImportLogView' => 'AphrontView', 'PhabricatorCalendarImportNameTransaction' => 'PhabricatorCalendarImportTransactionType', 'PhabricatorCalendarImportOriginalLogType' => 'PhabricatorCalendarImportLogType', 'PhabricatorCalendarImportOrphanLogType' => 'PhabricatorCalendarImportLogType', diff --git a/src/applications/calendar/application/PhabricatorCalendarApplication.php b/src/applications/calendar/application/PhabricatorCalendarApplication.php index 7563bcc096..ebd0dde088 100644 --- a/src/applications/calendar/application/PhabricatorCalendarApplication.php +++ b/src/applications/calendar/application/PhabricatorCalendarApplication.php @@ -83,6 +83,10 @@ final class PhabricatorCalendarApplication extends PhabricatorApplication { => 'PhabricatorCalendarImportViewController', 'disable/(?P[1-9]\d*)/' => 'PhabricatorCalendarImportDisableController', + 'log/' => array( + $this->getQueryRoutePattern() + => 'PhabricatorCalendarImportLogListController', + ), ), ), ); diff --git a/src/applications/calendar/controller/PhabricatorCalendarImportLogListController.php b/src/applications/calendar/controller/PhabricatorCalendarImportLogListController.php new file mode 100644 index 0000000000..91bdef4fdf --- /dev/null +++ b/src/applications/calendar/controller/PhabricatorCalendarImportLogListController.php @@ -0,0 +1,12 @@ +setController($this) + ->buildResponse(); + } + +} diff --git a/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php b/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php index 5da2be2044..2333dab45c 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php @@ -145,39 +145,9 @@ final class PhabricatorCalendarImportViewController ->setLimit(25) ->execute(); - $rows = array(); - foreach ($logs as $log) { - $icon = $log->getDisplayIcon($viewer); - $color = $log->getDisplayColor($viewer); - $name = $log->getDisplayType($viewer); - $description = $log->getDisplayDescription($viewer); - - $rows[] = array( - $log->getID(), - id(new PHUIIconView())->setIcon($icon, $color), - $name, - $description, - phabricator_datetime($log->getDateCreated(), $viewer), - ); - } - - $table = id(new AphrontTableView($rows)) - ->setHeaders( - array( - pht('ID'), - null, - pht('Type'), - pht('Mesage'), - pht('Date'), - )) - ->setColumnClasses( - array( - null, - null, - 'pri', - 'wide', - null, - )); + $logs_view = id(new PhabricatorCalendarImportLogView()) + ->setViewer($viewer) + ->setLogs($logs); $all_uri = $this->getApplicationURI('import/log/'); $all_uri = (string)id(new PhutilURI($all_uri)) @@ -196,7 +166,7 @@ final class PhabricatorCalendarImportViewController return id(new PHUIObjectBoxView()) ->setHeader($header) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) - ->setTable($table); + ->setTable($logs_view); } private function buildImportedEvents(PhabricatorCalendarImport $import) { diff --git a/src/applications/calendar/query/PhabricatorCalendarImportLogSearchEngine.php b/src/applications/calendar/query/PhabricatorCalendarImportLogSearchEngine.php new file mode 100644 index 0000000000..81a1256fca --- /dev/null +++ b/src/applications/calendar/query/PhabricatorCalendarImportLogSearchEngine.php @@ -0,0 +1,77 @@ +setLabel(pht('Import Sources')) + ->setKey('importSourcePHIDs') + ->setAliases(array('importSourcePHID')), + ); + } + + protected function buildQueryFromParameters(array $map) { + $query = $this->newQuery(); + + if ($map['importSourcePHIDs']) { + $query->withImportPHIDs($map['importSourcePHIDs']); + } + + return $query; + } + + protected function getURI($path) { + return '/calendar/import/log/'.$path; + } + + protected function getBuiltinQueryNames() { + $names = array( + 'all' => pht('All Logs'), + ); + + return $names; + } + + public function buildSavedQueryFromBuiltin($query_key) { + $query = $this->newSavedQuery(); + $query->setQueryKey($query_key); + + switch ($query_key) { + case 'all': + return $query; + } + + return parent::buildSavedQueryFromBuiltin($query_key); + } + + protected function renderResultList( + array $logs, + PhabricatorSavedQuery $query, + array $handles) { + + assert_instances_of($logs, 'PhabricatorCalendarImportLog'); + $viewer = $this->requireViewer(); + + $view = id(new PhabricatorCalendarImportLogView()) + ->setShowImportSources(true) + ->setViewer($viewer) + ->setLogs($logs); + + return id(new PhabricatorApplicationSearchResultView()) + ->setTable($view->newTable()); + } +} diff --git a/src/applications/calendar/view/PhabricatorCalendarImportLogView.php b/src/applications/calendar/view/PhabricatorCalendarImportLogView.php new file mode 100644 index 0000000000..abf40b3cd7 --- /dev/null +++ b/src/applications/calendar/view/PhabricatorCalendarImportLogView.php @@ -0,0 +1,84 @@ +logs = $logs; + return $this; + } + + public function getLogs() { + return $this->logs; + } + + public function setShowImportSources($show_import_sources) { + $this->showImportSources = $show_import_sources; + return $this; + } + + public function getShowImportSources() { + return $this->showImportSources; + } + + public function render() { + return $this->newTable(); + } + + public function newTable() { + $viewer = $this->getViewer(); + $logs = $this->getLogs(); + + $show_sources = $this->getShowImportSources(); + + $rows = array(); + foreach ($logs as $log) { + $icon = $log->getDisplayIcon($viewer); + $color = $log->getDisplayColor($viewer); + $name = $log->getDisplayType($viewer); + $description = $log->getDisplayDescription($viewer); + + $rows[] = array( + $log->getID(), + ($show_sources + ? $viewer->renderHandle($log->getImport()->getPHID()) + : null), + id(new PHUIIconView())->setIcon($icon, $color), + $name, + $description, + phabricator_datetime($log->getDateCreated(), $viewer), + ); + } + + $table = id(new AphrontTableView($rows)) + ->setHeaders( + array( + pht('ID'), + pht('Source'), + null, + pht('Type'), + pht('Mesage'), + pht('Date'), + )) + ->setColumnVisibility( + array( + true, + $show_sources, + )) + ->setColumnClasses( + array( + null, + null, + null, + 'pri', + 'wide', + null, + )); + + return $table; + } + +} From b47a42bf55831a1d95ad067b7c1b1079673b12ca Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 18 Oct 2016 13:34:28 -0700 Subject: [PATCH 34/55] Allow events from a particular import source to be bulk-deleted Summary: Ref T10747. If you accidentally import the wrong thing, you can clean up the big mess you made. These imported events are read-only so it's OK to destroy them completely (vs disable/hide/archive). Test Plan: Destroyed some imported events. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10747 Differential Revision: https://secure.phabricator.com/D16720 --- src/__phutil_library_map__.php | 4 ++ .../PhabricatorCalendarApplication.php | 2 + ...bricatorCalendarImportDeleteController.php | 64 +++++++++++++++++++ ...habricatorCalendarImportViewController.php | 18 ++++++ .../PhabricatorCalendarImportEngine.php | 13 ++++ .../view/PhabricatorCalendarImportLogView.php | 2 +- ...ricatorCalendarImportDeleteTransaction.php | 30 +++++++++ 7 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 src/applications/calendar/controller/PhabricatorCalendarImportDeleteController.php create mode 100644 src/applications/calendar/xaction/PhabricatorCalendarImportDeleteTransaction.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 1733b447eb..bf92e734b1 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2105,6 +2105,8 @@ phutil_register_library_map(array( 'PhabricatorCalendarIconSet' => 'applications/calendar/icon/PhabricatorCalendarIconSet.php', 'PhabricatorCalendarImport' => 'applications/calendar/storage/PhabricatorCalendarImport.php', 'PhabricatorCalendarImportDefaultLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportDefaultLogType.php', + 'PhabricatorCalendarImportDeleteController' => 'applications/calendar/controller/PhabricatorCalendarImportDeleteController.php', + 'PhabricatorCalendarImportDeleteTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportDeleteTransaction.php', 'PhabricatorCalendarImportDisableController' => 'applications/calendar/controller/PhabricatorCalendarImportDisableController.php', 'PhabricatorCalendarImportDisableTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportDisableTransaction.php', 'PhabricatorCalendarImportDuplicateLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportDuplicateLogType.php', @@ -6937,6 +6939,8 @@ phutil_register_library_map(array( 'PhabricatorDestructibleInterface', ), 'PhabricatorCalendarImportDefaultLogType' => 'PhabricatorCalendarImportLogType', + 'PhabricatorCalendarImportDeleteController' => 'PhabricatorCalendarController', + 'PhabricatorCalendarImportDeleteTransaction' => 'PhabricatorCalendarImportTransactionType', 'PhabricatorCalendarImportDisableController' => 'PhabricatorCalendarController', 'PhabricatorCalendarImportDisableTransaction' => 'PhabricatorCalendarImportTransactionType', 'PhabricatorCalendarImportDuplicateLogType' => 'PhabricatorCalendarImportLogType', diff --git a/src/applications/calendar/application/PhabricatorCalendarApplication.php b/src/applications/calendar/application/PhabricatorCalendarApplication.php index ebd0dde088..807e2b9956 100644 --- a/src/applications/calendar/application/PhabricatorCalendarApplication.php +++ b/src/applications/calendar/application/PhabricatorCalendarApplication.php @@ -83,6 +83,8 @@ final class PhabricatorCalendarApplication extends PhabricatorApplication { => 'PhabricatorCalendarImportViewController', 'disable/(?P[1-9]\d*)/' => 'PhabricatorCalendarImportDisableController', + 'delete/(?P[1-9]\d*)/' + => 'PhabricatorCalendarImportDeleteController', 'log/' => array( $this->getQueryRoutePattern() => 'PhabricatorCalendarImportLogListController', diff --git a/src/applications/calendar/controller/PhabricatorCalendarImportDeleteController.php b/src/applications/calendar/controller/PhabricatorCalendarImportDeleteController.php new file mode 100644 index 0000000000..fd65777515 --- /dev/null +++ b/src/applications/calendar/controller/PhabricatorCalendarImportDeleteController.php @@ -0,0 +1,64 @@ +getViewer(); + + $import = id(new PhabricatorCalendarImportQuery()) + ->setViewer($viewer) + ->withIDs(array($request->getURIData('id'))) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$import) { + return new Aphront404Response(); + } + + $import_uri = $import->getURI(); + + $engine = $import->getEngine(); + if (!$engine->canDeleteAnyEvents($viewer, $import)) { + return $this->newDialog() + ->setTitle(pht('No Imported Events')) + ->appendParagraph( + pht( + 'No events from this source currently exist. They may have '. + 'failed to import, have been updated by another source, or '. + 'already have been deleted.')) + ->addCancelButton($import_uri, pht('Done')); + } + + if ($request->isFormPost()) { + $xactions = array(); + $xactions[] = id(new PhabricatorCalendarImportTransaction()) + ->setTransactionType( + PhabricatorCalendarImportDeleteTransaction::TRANSACTIONTYPE) + ->setNewValue(true); + + $editor = id(new PhabricatorCalendarImportEditor()) + ->setActor($viewer) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->setContentSourceFromRequest($request); + + $editor->applyTransactions($import, $xactions); + + return id(new AphrontRedirectResponse())->setURI($import_uri); + } + + return $this->newDialog() + ->setTitle(pht('Delete Imported Events')) + ->appendParagraph( + pht( + 'Delete all the events that were imported from this source? '. + 'This action can not be undone.')) + ->addCancelButton($import_uri) + ->addSubmitButton(pht('Delete Events')); + } + +} diff --git a/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php b/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php index 2333dab45c..0ecd5787cb 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php @@ -123,6 +123,24 @@ final class PhabricatorCalendarImportViewController ->setWorkflow(true) ->setHref($disable_uri)); + + if ($can_edit) { + $can_delete = $engine->canDeleteAnyEvents($viewer, $import); + } else { + $can_delete = false; + } + + $delete_uri = "import/delete/{$id}/"; + $delete_uri = $this->getApplicationURI($delete_uri); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Delete Imported Events')) + ->setIcon('fa-times') + ->setDisabled(!$can_delete) + ->setWorkflow(true) + ->setHref($delete_uri)); + return $curtain; } diff --git a/src/applications/calendar/import/PhabricatorCalendarImportEngine.php b/src/applications/calendar/import/PhabricatorCalendarImportEngine.php index 6555b693e7..6e1b466cc0 100644 --- a/src/applications/calendar/import/PhabricatorCalendarImportEngine.php +++ b/src/applications/calendar/import/PhabricatorCalendarImportEngine.php @@ -398,4 +398,17 @@ abstract class PhabricatorCalendarImportEngine return $event; } + public function canDeleteAnyEvents( + PhabricatorUser $viewer, + PhabricatorCalendarImport $import) { + + $any_event = id(new PhabricatorCalendarEventQuery()) + ->setViewer($viewer) + ->withImportSourcePHIDs(array($import->getPHID())) + ->setLimit(1) + ->execute(); + + return (bool)$any_event; + } + } diff --git a/src/applications/calendar/view/PhabricatorCalendarImportLogView.php b/src/applications/calendar/view/PhabricatorCalendarImportLogView.php index abf40b3cd7..d33a8fdd38 100644 --- a/src/applications/calendar/view/PhabricatorCalendarImportLogView.php +++ b/src/applications/calendar/view/PhabricatorCalendarImportLogView.php @@ -60,7 +60,7 @@ final class PhabricatorCalendarImportLogView extends AphrontView { pht('Source'), null, pht('Type'), - pht('Mesage'), + pht('Message'), pht('Date'), )) ->setColumnVisibility( diff --git a/src/applications/calendar/xaction/PhabricatorCalendarImportDeleteTransaction.php b/src/applications/calendar/xaction/PhabricatorCalendarImportDeleteTransaction.php new file mode 100644 index 0000000000..83bf78169f --- /dev/null +++ b/src/applications/calendar/xaction/PhabricatorCalendarImportDeleteTransaction.php @@ -0,0 +1,30 @@ +setViewer($this->getActor()) + ->withImportSourcePHIDs(array($object->getPHID())) + ->execute(); + + $engine = new PhabricatorDestructionEngine(); + foreach ($events as $event) { + $engine->destroyObject($event); + } + } + + public function getTitle() { + return pht( + '%s deleted imported events from this source.', + $this->renderAuthor()); + } + +} From 67cb277bed6c60e130a4e3e17088b68d5ae4773d Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 18 Oct 2016 13:47:41 -0700 Subject: [PATCH 35/55] When import fails because we can't parse an ICS file, show it nicely Summary: Ref T10747. When we hit an ICS parser error, render it into a log instead of fataling. (This will be more important in the future with subscription-based URL ICS import.) Test Plan: {F1875292} Reviewers: chad Reviewed By: chad Maniphest Tasks: T10747 Differential Revision: https://secure.phabricator.com/D16721 --- src/__phutil_library_map__.php | 2 ++ .../PhabricatorCalendarICSImportEngine.php | 20 +++++++++-- .../PhabricatorCalendarImportEngine.php | 28 ++++++++------- .../PhabricatorCalendarImportICSLogType.php | 36 +++++++++++++++++++ .../view/PhabricatorCalendarImportLogView.php | 14 ++++---- 5 files changed, 78 insertions(+), 22 deletions(-) create mode 100644 src/applications/calendar/importlog/PhabricatorCalendarImportICSLogType.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index bf92e734b1..9ce3cab82a 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2118,6 +2118,7 @@ phutil_register_library_map(array( 'PhabricatorCalendarImportEpochLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportEpochLogType.php', 'PhabricatorCalendarImportFrequencyLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportFrequencyLogType.php', 'PhabricatorCalendarImportICSFileTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportICSFileTransaction.php', + 'PhabricatorCalendarImportICSLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportICSLogType.php', 'PhabricatorCalendarImportIgnoredNodeLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportIgnoredNodeLogType.php', 'PhabricatorCalendarImportListController' => 'applications/calendar/controller/PhabricatorCalendarImportListController.php', 'PhabricatorCalendarImportLog' => 'applications/calendar/storage/PhabricatorCalendarImportLog.php', @@ -6952,6 +6953,7 @@ phutil_register_library_map(array( 'PhabricatorCalendarImportEpochLogType' => 'PhabricatorCalendarImportLogType', 'PhabricatorCalendarImportFrequencyLogType' => 'PhabricatorCalendarImportLogType', 'PhabricatorCalendarImportICSFileTransaction' => 'PhabricatorCalendarImportTransactionType', + 'PhabricatorCalendarImportICSLogType' => 'PhabricatorCalendarImportLogType', 'PhabricatorCalendarImportIgnoredNodeLogType' => 'PhabricatorCalendarImportLogType', 'PhabricatorCalendarImportListController' => 'PhabricatorCalendarController', 'PhabricatorCalendarImportLog' => array( diff --git a/src/applications/calendar/import/PhabricatorCalendarICSImportEngine.php b/src/applications/calendar/import/PhabricatorCalendarICSImportEngine.php index 2874e723a2..ae20635b6b 100644 --- a/src/applications/calendar/import/PhabricatorCalendarICSImportEngine.php +++ b/src/applications/calendar/import/PhabricatorCalendarICSImportEngine.php @@ -62,9 +62,25 @@ final class PhabricatorCalendarICSImportEngine $data = $file->loadFileData(); - $parser = id(new PhutilICSParser()); + $parser = new PhutilICSParser(); - $document = $parser->parseICSData($data); + try { + $document = $parser->parseICSData($data); + } catch (PhutilICSParserException $ex) { + // TODO: In theory, it would be nice to store these in a fully abstract + // form so they can be translated at display time. As-is, we'll store the + // error messages in whatever language we were using when the parser + // failure occurred. + + $import->newLogMessage( + PhabricatorCalendarImportICSLogType::LOGTYPE, + array( + 'ics.code' => $ex->getParserFailureCode(), + 'ics.message' => $ex->getMessage(), + )); + + $document = null; + } return $this->importEventDocument($viewer, $import, $document); } diff --git a/src/applications/calendar/import/PhabricatorCalendarImportEngine.php b/src/applications/calendar/import/PhabricatorCalendarImportEngine.php index 6e1b466cc0..25eb65a720 100644 --- a/src/applications/calendar/import/PhabricatorCalendarImportEngine.php +++ b/src/applications/calendar/import/PhabricatorCalendarImportEngine.php @@ -42,24 +42,26 @@ abstract class PhabricatorCalendarImportEngine final protected function importEventDocument( PhabricatorUser $viewer, PhabricatorCalendarImport $import, - PhutilCalendarRootNode $root) { + PhutilCalendarRootNode $root = null) { $event_type = PhutilCalendarEventNode::NODETYPE; $nodes = array(); - foreach ($root->getChildren() as $document) { - foreach ($document->getChildren() as $node) { - $node_type = $node->getNodeType(); - if ($node_type != $event_type) { - $import->newLogMessage( - PhabricatorCalendarImportIgnoredNodeLogType::LOGTYPE, - array( - 'node.type' => $node_type, - )); - continue; - } + if ($root) { + foreach ($root->getChildren() as $document) { + foreach ($document->getChildren() as $node) { + $node_type = $node->getNodeType(); + if ($node_type != $event_type) { + $import->newLogMessage( + PhabricatorCalendarImportIgnoredNodeLogType::LOGTYPE, + array( + 'node.type' => $node_type, + )); + continue; + } - $nodes[] = $node; + $nodes[] = $node; + } } } diff --git a/src/applications/calendar/importlog/PhabricatorCalendarImportICSLogType.php b/src/applications/calendar/importlog/PhabricatorCalendarImportICSLogType.php new file mode 100644 index 0000000000..30994b08ad --- /dev/null +++ b/src/applications/calendar/importlog/PhabricatorCalendarImportICSLogType.php @@ -0,0 +1,36 @@ +getParameter('ics.code'), + $log->getParameter('ics.message')); + } + + + public function getDisplayIcon( + PhabricatorUser $viewer, + PhabricatorCalendarImportLog $log) { + return 'fa-file'; + } + + public function getDisplayColor( + PhabricatorUser $viewer, + PhabricatorCalendarImportLog $log) { + return 'red'; + } + +} diff --git a/src/applications/calendar/view/PhabricatorCalendarImportLogView.php b/src/applications/calendar/view/PhabricatorCalendarImportLogView.php index d33a8fdd38..8c2c2af68e 100644 --- a/src/applications/calendar/view/PhabricatorCalendarImportLogView.php +++ b/src/applications/calendar/view/PhabricatorCalendarImportLogView.php @@ -48,7 +48,7 @@ final class PhabricatorCalendarImportLogView extends AphrontView { : null), id(new PHUIIconView())->setIcon($icon, $color), $name, - $description, + phutil_escape_html_newlines($description), phabricator_datetime($log->getDateCreated(), $viewer), ); } @@ -70,12 +70,12 @@ final class PhabricatorCalendarImportLogView extends AphrontView { )) ->setColumnClasses( array( - null, - null, - null, - 'pri', - 'wide', - null, + 'top', + 'top', + 'top', + 'top pri', + 'top wide', + 'top', )); return $table; From f9f25c1e4d5b3636102bca5a51297239bfedc3a5 Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 18 Oct 2016 14:24:17 -0700 Subject: [PATCH 36/55] Allow users to drop .ics files on calendar views to import them Summary: Ref T10747. When a user drops a ".ics" file or a bunch of ".ics" files into a calendar view, import the events. (Possibly we should just do this if you drop ".ics" files into any application, but we can look at that later.) Test Plan: Dropped some .ics files into calendar views, got imports. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10747 Differential Revision: https://secure.phabricator.com/D16722 --- resources/celerity/map.php | 20 ++--- src/__phutil_library_map__.php | 2 + .../PhabricatorCalendarApplication.php | 2 + ...PhabricatorCalendarEventListController.php | 4 + ...habricatorCalendarImportDropController.php | 86 +++++++++++++++++++ .../PhabricatorCalendarImportEngine.php | 4 +- .../PhabricatorCalendarEventSearchEngine.php | 40 ++++++--- .../storage/PhabricatorCalendarImport.php | 1 + .../PhabricatorGlobalUploadTargetView.php | 52 ++++++++++- ...PhabricatorApplicationSearchResultView.php | 2 - .../js/core/behavior-global-drag-and-drop.js | 16 +++- 11 files changed, 198 insertions(+), 31 deletions(-) create mode 100644 src/applications/calendar/controller/PhabricatorCalendarImportDropController.php diff --git a/resources/celerity/map.php b/resources/celerity/map.php index bf8f0eed15..9f13ec0351 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -10,7 +10,7 @@ return array( 'conpherence.pkg.css' => '6412a825', 'conpherence.pkg.js' => 'cbe4d9be', 'core.pkg.css' => 'b99bbf5e', - 'core.pkg.js' => '3eb7abf7', + 'core.pkg.js' => '2d9fc958', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => 'e1d704ce', 'differential.pkg.js' => '634399e9', @@ -555,7 +555,7 @@ return array( 'rsrc/js/core/behavior-file-tree.js' => '88236f00', 'rsrc/js/core/behavior-form.js' => '5c54cbf3', 'rsrc/js/core/behavior-gesture.js' => '3ab51e2c', - 'rsrc/js/core/behavior-global-drag-and-drop.js' => 'c8e57404', + 'rsrc/js/core/behavior-global-drag-and-drop.js' => '960f6a39', 'rsrc/js/core/behavior-high-security-warning.js' => 'a464fe03', 'rsrc/js/core/behavior-history-install.js' => '7ee2b591', 'rsrc/js/core/behavior-hovercard.js' => 'bcaccd64', @@ -701,7 +701,7 @@ return array( 'javelin-behavior-error-log' => '6882e80a', 'javelin-behavior-event-all-day' => '937bb700', 'javelin-behavior-fancy-datepicker' => '568931f3', - 'javelin-behavior-global-drag-and-drop' => 'c8e57404', + 'javelin-behavior-global-drag-and-drop' => '960f6a39', 'javelin-behavior-herald-rule-editor' => '7ebaeed3', 'javelin-behavior-high-security-warning' => 'a464fe03', 'javelin-behavior-history-install' => '7ee2b591', @@ -1735,6 +1735,13 @@ return array( 'javelin-dom', 'phabricator-busy', ), + '960f6a39' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-uri', + 'javelin-mask', + 'phabricator-drag-and-drop-file-upload', + ), '988040b4' => array( 'javelin-install', 'javelin-dom', @@ -1982,13 +1989,6 @@ return array( 'c7ccd872' => array( 'phui-fontkit-css', ), - 'c8e57404' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-uri', - 'javelin-mask', - 'phabricator-drag-and-drop-file-upload', - ), 'c90a04fc' => array( 'javelin-dom', 'javelin-dynval', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 9ce3cab82a..d225434c9c 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2109,6 +2109,7 @@ phutil_register_library_map(array( 'PhabricatorCalendarImportDeleteTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportDeleteTransaction.php', 'PhabricatorCalendarImportDisableController' => 'applications/calendar/controller/PhabricatorCalendarImportDisableController.php', 'PhabricatorCalendarImportDisableTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportDisableTransaction.php', + 'PhabricatorCalendarImportDropController' => 'applications/calendar/controller/PhabricatorCalendarImportDropController.php', 'PhabricatorCalendarImportDuplicateLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportDuplicateLogType.php', 'PhabricatorCalendarImportEditController' => 'applications/calendar/controller/PhabricatorCalendarImportEditController.php', 'PhabricatorCalendarImportEditEngine' => 'applications/calendar/editor/PhabricatorCalendarImportEditEngine.php', @@ -6944,6 +6945,7 @@ phutil_register_library_map(array( 'PhabricatorCalendarImportDeleteTransaction' => 'PhabricatorCalendarImportTransactionType', 'PhabricatorCalendarImportDisableController' => 'PhabricatorCalendarController', 'PhabricatorCalendarImportDisableTransaction' => 'PhabricatorCalendarImportTransactionType', + 'PhabricatorCalendarImportDropController' => 'PhabricatorCalendarController', 'PhabricatorCalendarImportDuplicateLogType' => 'PhabricatorCalendarImportLogType', 'PhabricatorCalendarImportEditController' => 'PhabricatorCalendarController', 'PhabricatorCalendarImportEditEngine' => 'PhabricatorEditEngine', diff --git a/src/applications/calendar/application/PhabricatorCalendarApplication.php b/src/applications/calendar/application/PhabricatorCalendarApplication.php index 807e2b9956..fa95f6951b 100644 --- a/src/applications/calendar/application/PhabricatorCalendarApplication.php +++ b/src/applications/calendar/application/PhabricatorCalendarApplication.php @@ -85,6 +85,8 @@ final class PhabricatorCalendarApplication extends PhabricatorApplication { => 'PhabricatorCalendarImportDisableController', 'delete/(?P[1-9]\d*)/' => 'PhabricatorCalendarImportDeleteController', + 'drop/' + => 'PhabricatorCalendarImportDropController', 'log/' => array( $this->getQueryRoutePattern() => 'PhabricatorCalendarImportLogListController', diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventListController.php b/src/applications/calendar/controller/PhabricatorCalendarEventListController.php index cc9bed332a..711b2eeb3e 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarEventListController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventListController.php @@ -7,6 +7,10 @@ final class PhabricatorCalendarEventListController return true; } + public function isGlobalDragAndDropUploadEnabled() { + return true; + } + public function handleRequest(AphrontRequest $request) { $year = $request->getURIData('year'); $month = $request->getURIData('month'); diff --git a/src/applications/calendar/controller/PhabricatorCalendarImportDropController.php b/src/applications/calendar/controller/PhabricatorCalendarImportDropController.php new file mode 100644 index 0000000000..03f515087b --- /dev/null +++ b/src/applications/calendar/controller/PhabricatorCalendarImportDropController.php @@ -0,0 +1,86 @@ +getViewer(); + + if (!$request->validateCSRF()) { + return new Aphront400Response(); + } + + $cancel_uri = $this->getApplicationURI(); + + $ids = $request->getStrList('h'); + if ($ids) { + $files = id(new PhabricatorFileQuery()) + ->setViewer($viewer) + ->withIDs($ids) + ->setRaisePolicyExceptions(true) + ->execute(); + } else { + $files = array(); + } + + if (!$files) { + return $this->newDialog() + ->setTitle(pht('Nothing Uploaded')) + ->appendParagraph( + pht( + 'Drag and drop .ics files to upload them and import them into '. + 'Calendar.')) + ->addCancelButton($cancel_uri, pht('Done')); + } + + $engine = new PhabricatorCalendarICSImportEngine(); + $imports = array(); + foreach ($files as $file) { + $import = PhabricatorCalendarImport::initializeNewCalendarImport( + $viewer, + clone $engine); + + $xactions = array(); + $xactions[] = id(new PhabricatorCalendarImportTransaction()) + ->setTransactionType( + PhabricatorCalendarImportICSFileTransaction::TRANSACTIONTYPE) + ->setNewValue($file->getPHID()); + + $editor = id(new PhabricatorCalendarImportEditor()) + ->setActor($viewer) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->setContentSourceFromRequest($request); + + $editor->applyTransactions($import, $xactions); + + $imports[] = $import; + } + + $import_phids = mpull($imports, 'getPHID'); + $events = id(new PhabricatorCalendarEventQuery()) + ->setViewer($viewer) + ->withImportSourcePHIDs($import_phids) + ->execute(); + + if (count($events) == 1) { + // The user imported exactly one event. This is consistent with dropping + // a .ics file from an email; just take them to the event. + $event = head($events); + $next_uri = $event->getURI(); + } else if (count($imports) > 1) { + // The user imported multiple different files. Take them to a summary + // list of generated import activity. + $source_phids = implode(',', $import_phids); + $next_uri = '/calendar/import/log/?importSourcePHIDs='.$source_phids; + } else { + // The user imported one file, which had zero or more than one event. + // Take them to the import detail page. + $import = head($imports); + $next_uri = $import->getURI(); + } + + return id(new AphrontRedirectResponse())->setURI($next_uri); + } + +} diff --git a/src/applications/calendar/import/PhabricatorCalendarImportEngine.php b/src/applications/calendar/import/PhabricatorCalendarImportEngine.php index 25eb65a720..e9270b3d41 100644 --- a/src/applications/calendar/import/PhabricatorCalendarImportEngine.php +++ b/src/applications/calendar/import/PhabricatorCalendarImportEngine.php @@ -390,9 +390,9 @@ abstract class PhabricatorCalendarImportEngine if ($rrule) { $event->setRecurrenceRule($rrule); - $until_datetime = $rrule->getUntil() - ->setViewerTimezone($timezone); + $until_datetime = $rrule->getUntil(); if ($until_datetime) { + $until_datetime->setViewerTimezone($timezone); $event->setUntilDateTime($until_datetime); } } diff --git a/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php b/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php index 8d55da994a..26c3dc938b 100644 --- a/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php +++ b/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php @@ -321,11 +321,9 @@ final class PhabricatorCalendarEventSearchEngine $list->addItem($item); } - $result = new PhabricatorApplicationSearchResultView(); - $result->setObjectList($list); - $result->setNoDataString(pht('No events found.')); - - return $result; + return $this->newResultView() + ->setObjectList($list) + ->setNoDataString(pht('No events found.')); } private function buildCalendarMonthView( @@ -393,10 +391,9 @@ final class PhabricatorCalendarEventSearchEngine ->setProfileHeader(true) ->setHeader($from->format('F Y')); - return id(new PhabricatorApplicationSearchResultView()) + return $this->newResultView($month_view) ->setCrumbs($crumbs) - ->setHeader($header) - ->setContent($month_view); + ->setHeader($header); } private function buildCalendarDayView( @@ -467,10 +464,9 @@ final class PhabricatorCalendarEventSearchEngine ->setProfileHeader(true) ->setHeader($from->format('D, F jS')); - return id(new PhabricatorApplicationSearchResultView()) + return $this->newResultView($day_view) ->setCrumbs($crumbs) - ->setHeader($header) - ->setContent($day_view); + ->setHeader($header); } private function getDisplayYearAndMonthAndDay( @@ -596,4 +592,26 @@ final class PhabricatorCalendarEventSearchEngine ); } + + private function newResultView($content = null) { + // If we aren't rendering a dashboard panel, activate global drag-and-drop + // so you can import ".ics" files by dropping them directly onto the + // calendar. + if (!$this->isPanelContext()) { + $drop_upload = id(new PhabricatorGlobalUploadTargetView()) + ->setViewer($this->requireViewer()) + ->setHintText("\xE2\x87\xAA ".pht('Drop .ics Files to Import')) + ->setSubmitURI('/calendar/import/drop/') + ->setViewPolicy(PhabricatorPolicies::POLICY_NOONE); + + $content = array( + $drop_upload, + $content, + ); + } + + return id(new PhabricatorApplicationSearchResultView()) + ->setContent($content); + } + } diff --git a/src/applications/calendar/storage/PhabricatorCalendarImport.php b/src/applications/calendar/storage/PhabricatorCalendarImport.php index b1d0ddf58b..93b0897ee5 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarImport.php +++ b/src/applications/calendar/storage/PhabricatorCalendarImport.php @@ -21,6 +21,7 @@ final class PhabricatorCalendarImport PhabricatorUser $actor, PhabricatorCalendarImportEngine $engine) { return id(new self()) + ->setName('') ->setAuthorPHID($actor->getPHID()) ->setViewPolicy($actor->getPHID()) ->setEditPolicy($actor->getPHID()) diff --git a/src/applications/files/view/PhabricatorGlobalUploadTargetView.php b/src/applications/files/view/PhabricatorGlobalUploadTargetView.php index 3e0690815c..76aacbe36f 100644 --- a/src/applications/files/view/PhabricatorGlobalUploadTargetView.php +++ b/src/applications/files/view/PhabricatorGlobalUploadTargetView.php @@ -14,6 +14,9 @@ final class PhabricatorGlobalUploadTargetView extends AphrontView { private $showIfSupportedID; + private $hintText; + private $viewPolicy; + private $submitURI; public function setShowIfSupportedID($show_if_supported_id) { $this->showIfSupportedID = $show_if_supported_id; @@ -24,8 +27,37 @@ final class PhabricatorGlobalUploadTargetView extends AphrontView { return $this->showIfSupportedID; } + public function setHintText($hint_text) { + $this->hintText = $hint_text; + return $this; + } + + public function getHintText() { + return $this->hintText; + } + + public function setViewPolicy($view_policy) { + $this->viewPolicy = $view_policy; + return $this; + } + + public function getViewPolicy() { + return $this->viewPolicy; + } + + public function setSubmitURI($submit_uri) { + $this->submitURI = $submit_uri; + return $this; + } + + public function getSubmitURI() { + return $this->submitURI; + } + + + public function render() { - $viewer = $this->getUser(); + $viewer = $this->getViewer(); if (!$viewer->isLoggedIn()) { return null; } @@ -34,18 +66,30 @@ final class PhabricatorGlobalUploadTargetView extends AphrontView { require_celerity_resource('global-drag-and-drop-css'); + $hint_text = $this->getHintText(); + if (!strlen($hint_text)) { + $hint_text = "\xE2\x87\xAA ".pht('Drop Files to Upload'); + } + // Use the configured default view policy. Drag and drop uploads use // a more restrictive view policy if we don't specify a policy explicitly, // as the more restrictive policy is correct for most drop targets (like // Pholio uploads and Remarkup text areas). - $view_policy = PhabricatorFile::initializeNewFile()->getViewPolicy(); + $view_policy = $this->getViewPolicy(); + if ($view_policy === null) { + $view_policy = PhabricatorFile::initializeNewFile()->getViewPolicy(); + } + + $submit_uri = $this->getSubmitURI(); + $done_uri = '/file/query/authored/'; Javelin::initBehavior('global-drag-and-drop', array( 'ifSupported' => $this->showIfSupportedID, 'instructions' => $instructions_id, 'uploadURI' => '/file/dropupload/', - 'browseURI' => '/file/query/authored/', + 'submitURI' => $submit_uri, + 'browseURI' => $done_uri, 'viewPolicy' => $view_policy, 'chunkThreshold' => PhabricatorFileStorageEngine::getChunkThreshold(), )); @@ -57,6 +101,6 @@ final class PhabricatorGlobalUploadTargetView extends AphrontView { 'class' => 'phabricator-global-upload-instructions', 'style' => 'display: none;', ), - "\xE2\x87\xAA ".pht('Drop Files to Upload')); + $hint_text); } } diff --git a/src/applications/search/view/PhabricatorApplicationSearchResultView.php b/src/applications/search/view/PhabricatorApplicationSearchResultView.php index 1c3f4aad65..3576d5238e 100644 --- a/src/applications/search/view/PhabricatorApplicationSearchResultView.php +++ b/src/applications/search/view/PhabricatorApplicationSearchResultView.php @@ -94,6 +94,4 @@ final class PhabricatorApplicationSearchResultView extends Phobject { return $this->header; } - - } diff --git a/webroot/rsrc/js/core/behavior-global-drag-and-drop.js b/webroot/rsrc/js/core/behavior-global-drag-and-drop.js index a291772b6e..08b0fd1226 100644 --- a/webroot/rsrc/js/core/behavior-global-drag-and-drop.js +++ b/webroot/rsrc/js/core/behavior-global-drag-and-drop.js @@ -70,7 +70,14 @@ JX.behavior('global-drag-and-drop', function(config, statics) { // If whatever the user dropped in has finished uploading, send them to // their uploads. var uri; - uri = JX.$U(config.browseURI); + var is_submit = !!config.submitURI; + + if (is_submit) { + uri = JX.$U(config.submitURI); + } else { + uri = JX.$U(config.browseURI); + } + var ids = []; for (var ii = 0; ii < statics.files.length; ii++) { ids.push(statics.files[ii].getID()); @@ -79,7 +86,12 @@ JX.behavior('global-drag-and-drop', function(config, statics) { statics.files = []; - uri.go(); + if (is_submit) { + new JX.Workflow(uri) + .start(); + } else { + uri.go(); + } } }); From cc0f0b38652509395d3f8b33b2b4e62234f58b7d Mon Sep 17 00:00:00 2001 From: epriestley Date: Tue, 18 Oct 2016 14:33:23 -0700 Subject: [PATCH 37/55] Don't publish feed stories or send mail about imported events Summary: Ref T10747. Although I could possibly imagine some very selective cases where we do this eventually, these are read-only for now and not interesting to publish/mail about. The presumption is that the original/authoritative system has already notified relevant parties or they're subscribing passively. Test Plan: Imported some name changes for events, saw no more mail/feed stuff. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10747 Differential Revision: https://secure.phabricator.com/D16723 --- .../calendar/editor/PhabricatorCalendarEventEditor.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php b/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php index 33442b6702..655037c140 100644 --- a/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php +++ b/src/applications/calendar/editor/PhabricatorCalendarEventEditor.php @@ -200,6 +200,11 @@ final class PhabricatorCalendarEventEditor protected function shouldPublishFeedStory( PhabricatorLiskDAO $object, array $xactions) { + + if ($object->isImportedEvent()) { + return false; + } + return true; } @@ -210,6 +215,11 @@ final class PhabricatorCalendarEventEditor protected function shouldSendMail( PhabricatorLiskDAO $object, array $xactions) { + + if ($object->isImportedEvent()) { + return false; + } + return true; } From b750c0a218aae63318c9bbf0501637b208ee6bfa Mon Sep 17 00:00:00 2001 From: Chad Little Date: Tue, 18 Oct 2016 21:13:53 -0700 Subject: [PATCH 38/55] Allow tablet breakpoint in Conpherence to have persistent participant pane Summary: tablet viewpoints here are wide enough to support the participant column, go ahead and give it dedicated space. Test Plan: review tablet, desktop, and mobile breakpoints in conpherence with column toggled on and off Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D16724 --- resources/celerity/map.php | 10 +++++----- .../css/application/conpherence/message-pane.css | 12 ++++++++++++ .../css/application/conpherence/participant-pane.css | 2 +- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 9f13ec0351..51e462010e 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,7 +7,7 @@ */ return array( 'names' => array( - 'conpherence.pkg.css' => '6412a825', + 'conpherence.pkg.css' => 'fabab894', 'conpherence.pkg.js' => 'cbe4d9be', 'core.pkg.css' => 'b99bbf5e', 'core.pkg.js' => '2d9fc958', @@ -49,9 +49,9 @@ return array( 'rsrc/css/application/conpherence/durable-column.css' => 'd82e130c', 'rsrc/css/application/conpherence/header-pane.css' => '1c81cda6', 'rsrc/css/application/conpherence/menu.css' => '4f51db5a', - 'rsrc/css/application/conpherence/message-pane.css' => 'b80f1675', + 'rsrc/css/application/conpherence/message-pane.css' => 'b499f371', 'rsrc/css/application/conpherence/notification.css' => '965db05b', - 'rsrc/css/application/conpherence/participant-pane.css' => '7bba0b56', + 'rsrc/css/application/conpherence/participant-pane.css' => 'ac1baaa8', 'rsrc/css/application/conpherence/transaction.css' => '85129c68', 'rsrc/css/application/contentsource/content-source-view.css' => '4b8b05d4', 'rsrc/css/application/countdown/timer.css' => '16c52f5c', @@ -619,9 +619,9 @@ return array( 'conpherence-durable-column-view' => 'd82e130c', 'conpherence-header-pane-css' => '1c81cda6', 'conpherence-menu-css' => '4f51db5a', - 'conpherence-message-pane-css' => 'b80f1675', + 'conpherence-message-pane-css' => 'b499f371', 'conpherence-notification-css' => '965db05b', - 'conpherence-participant-pane-css' => '7bba0b56', + 'conpherence-participant-pane-css' => 'ac1baaa8', 'conpherence-thread-manager' => '358c717b', 'conpherence-transaction-css' => '85129c68', 'd3' => 'a11a5ff2', diff --git a/webroot/rsrc/css/application/conpherence/message-pane.css b/webroot/rsrc/css/application/conpherence/message-pane.css index dd1eccfc80..db82a4a336 100644 --- a/webroot/rsrc/css/application/conpherence/message-pane.css +++ b/webroot/rsrc/css/application/conpherence/message-pane.css @@ -82,6 +82,12 @@ } .device .conpherence-message-pane .conpherence-messages { + left: 0; + bottom: 44px; + box-shadow: none; +} + +.device-phone .conpherence-message-pane .conpherence-messages { left: 0; right: 0; bottom: 44px; @@ -186,6 +192,12 @@ } .device .conpherence-message-pane .phui-form-view { + left: 0; + height: 34px; + width: auto; +} + +.device-phone .conpherence-message-pane .phui-form-view { left: 0; right: 0; height: 34px; diff --git a/webroot/rsrc/css/application/conpherence/participant-pane.css b/webroot/rsrc/css/application/conpherence/participant-pane.css index 94bedce84d..c72d098226 100644 --- a/webroot/rsrc/css/application/conpherence/participant-pane.css +++ b/webroot/rsrc/css/application/conpherence/participant-pane.css @@ -15,7 +15,7 @@ -webkit-overflow-scrolling: touch; } -.device .conpherence-participant-pane { +.device-phone .conpherence-participant-pane { background-color: {$page.background}; } From 89f0015ae6ec910b49a5a75d1a761ec7461a5c27 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Tue, 18 Oct 2016 21:49:02 -0700 Subject: [PATCH 39/55] Don't show participants in Conpherence left open on mobile Summary: Fixes T11764. Moves rendering of the column to client-side, which can skip if it detects we're on mobile. Test Plan: Open column on desktop, switch to mobile, don't see column. Toggle column on mobile on and off. Switch back to desktop. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Maniphest Tasks: T11764 Differential Revision: https://secure.phabricator.com/D16725 --- resources/celerity/map.php | 52 +++++++++---------- .../view/ConpherenceLayoutView.php | 23 +++----- .../application/conpherence/behavior-menu.js | 34 +----------- .../conpherence/behavior-toggle-widget.js | 22 ++++++-- 4 files changed, 53 insertions(+), 78 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 51e462010e..7d0d2996b7 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -8,7 +8,7 @@ return array( 'names' => array( 'conpherence.pkg.css' => 'fabab894', - 'conpherence.pkg.js' => 'cbe4d9be', + 'conpherence.pkg.js' => '6249a1cf', 'core.pkg.css' => 'b99bbf5e', 'core.pkg.js' => '2d9fc958', 'darkconsole.pkg.js' => 'e7393ebb', @@ -438,11 +438,11 @@ return array( 'rsrc/js/application/conpherence/ConpherenceThreadManager.js' => '358c717b', 'rsrc/js/application/conpherence/behavior-conpherence-search.js' => '9bbf3762', 'rsrc/js/application/conpherence/behavior-durable-column.js' => 'aa3bd034', - 'rsrc/js/application/conpherence/behavior-menu.js' => '07928ca3', + 'rsrc/js/application/conpherence/behavior-menu.js' => '7524fcfa', 'rsrc/js/application/conpherence/behavior-participant-pane.js' => '8604caa8', 'rsrc/js/application/conpherence/behavior-pontificate.js' => 'f2e58483', 'rsrc/js/application/conpherence/behavior-quicksand-blacklist.js' => '7927a7d3', - 'rsrc/js/application/conpherence/behavior-toggle-widget.js' => '9bdbbab0', + 'rsrc/js/application/conpherence/behavior-toggle-widget.js' => '3dbf94d5', 'rsrc/js/application/countdown/timer.js' => 'e4cc26b3', 'rsrc/js/application/daemon/behavior-bulk-job-reload.js' => 'edf8a145', 'rsrc/js/application/dashboard/behavior-dashboard-async-panel.js' => '469c0d9e', @@ -663,7 +663,7 @@ return array( 'javelin-behavior-choose-control' => '327a00d1', 'javelin-behavior-comment-actions' => '0300eae6', 'javelin-behavior-config-reorder-fields' => 'b6993408', - 'javelin-behavior-conpherence-menu' => '07928ca3', + 'javelin-behavior-conpherence-menu' => '7524fcfa', 'javelin-behavior-conpherence-participant-pane' => '8604caa8', 'javelin-behavior-conpherence-pontificate' => 'f2e58483', 'javelin-behavior-conpherence-search' => '9bbf3762', @@ -772,7 +772,7 @@ return array( 'javelin-behavior-test-payment-form' => 'fc91ab6c', 'javelin-behavior-time-typeahead' => '522431f7', 'javelin-behavior-toggle-class' => '92b9ec77', - 'javelin-behavior-toggle-widget' => '9bdbbab0', + 'javelin-behavior-toggle-widget' => '3dbf94d5', 'javelin-behavior-typeahead-browse' => '635de1ec', 'javelin-behavior-typeahead-search' => '93d0c9e3', 'javelin-behavior-view-placeholder' => '47830651', @@ -1029,20 +1029,6 @@ return array( 'phabricator-prefab', 'phuix-icon-view', ), - '07928ca3' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-util', - 'javelin-stratcom', - 'javelin-workflow', - 'javelin-behavior-device', - 'javelin-history', - 'javelin-vector', - 'javelin-scrollbar', - 'phabricator-title', - 'phabricator-shaped-request', - 'conpherence-thread-manager', - ), '08675c6d' => array( 'javelin-behavior', 'javelin-behavior-device', @@ -1227,6 +1213,13 @@ return array( 'javelin-util', 'javelin-uri', ), + '3dbf94d5' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-util', + 'javelin-workflow', + 'javelin-stratcom', + ), '3f5d6dbf' => array( 'javelin-behavior', 'javelin-dom', @@ -1548,6 +1541,20 @@ return array( 'javelin-vector', 'javelin-dom', ), + '7524fcfa' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-util', + 'javelin-stratcom', + 'javelin-workflow', + 'javelin-behavior-device', + 'javelin-history', + 'javelin-vector', + 'javelin-scrollbar', + 'phabricator-title', + 'phabricator-shaped-request', + 'conpherence-thread-manager', + ), '769d3498' => array( 'syntax-default-css', ), @@ -1766,13 +1773,6 @@ return array( 'javelin-workflow', 'javelin-stratcom', ), - '9bdbbab0' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-util', - 'javelin-workflow', - 'javelin-stratcom', - ), '9ef7d354' => array( 'phui-inline-comment-view-css', ), diff --git a/src/applications/conpherence/view/ConpherenceLayoutView.php b/src/applications/conpherence/view/ConpherenceLayoutView.php index b1777e4f97..e8d94ed81d 100644 --- a/src/applications/conpherence/view/ConpherenceLayoutView.php +++ b/src/applications/conpherence/view/ConpherenceLayoutView.php @@ -61,26 +61,17 @@ final class ConpherenceLayoutView extends AphrontTagView { return $this; } - public function getWidgetColumnVisible() { - $widget_key = PhabricatorConpherenceWidgetVisibleSetting::SETTINGKEY; - $user = $this->getUser(); - return (bool)$user->getUserSetting($widget_key, false); - } - protected function getTagAttributes() { $classes = array(); - if (!$this->getWidgetColumnVisible()) { - $classes[] = 'hide-widgets'; - } + $classes[] = 'conpherence-layout'; + $classes[] = 'hide-widgets'; + $classes[] = 'conpherence-role-'.$this->role; return array( - 'id' => 'conpherence-main-layout', - 'sigil' => 'conpherence-layout', - 'class' => 'conpherence-layout '. - implode(' ', $classes). - ' conpherence-role-'.$this->role, - ); - + 'id' => 'conpherence-main-layout', + 'sigil' => 'conpherence-layout', + 'class' => implode(' ', $classes), + ); } protected function getTagContent() { diff --git a/webroot/rsrc/js/application/conpherence/behavior-menu.js b/webroot/rsrc/js/application/conpherence/behavior-menu.js index 844a6891f2..1621126a5e 100644 --- a/webroot/rsrc/js/application/conpherence/behavior-menu.js +++ b/webroot/rsrc/js/application/conpherence/behavior-menu.js @@ -191,16 +191,6 @@ JX.behavior('conpherence-menu', function(config) { if (_thread.visible !== null || !config.hasWidgets) { reloadWidget(data); - } else { - JX.Stratcom.invoke( - 'conpherence-update-widgets', - null, - { - widget : getDefaultWidget(), - buildSelectors : false, - toggleWidget : true, - threadID : _thread.selected - }); } _thread.visible = _thread.selected; @@ -259,32 +249,10 @@ JX.behavior('conpherence-menu', function(config) { var root = JX.DOM.find(document, 'div', 'conpherence-layout'); var widgets_root = JX.DOM.find(root, 'div', 'conpherence-widgets-holder'); JX.DOM.setContent(widgets_root, JX.$H(response.widgets)); - - JX.Stratcom.invoke( - 'conpherence-update-widgets', - null, - { - widget : widget, - buildSelectors : true, - toggleWidget : true, - threadID : _thread.selected - }); - - markWidgetLoading(false); } function getDefaultWidget() { - var device = JX.Device.getDevice(); - var widget = 'conpherence-message-pane'; - if (device == 'desktop') { - widget = 'widgets-people'; - var uri = JX.$U(location.href); - var params = uri.getQueryParams(); - if ('settings' in params) { - widget = 'widgets-settings'; - } - } - return widget; + return 'widgets-people'; } /** diff --git a/webroot/rsrc/js/application/conpherence/behavior-toggle-widget.js b/webroot/rsrc/js/application/conpherence/behavior-toggle-widget.js index 1b36f8ff79..0022273c0e 100644 --- a/webroot/rsrc/js/application/conpherence/behavior-toggle-widget.js +++ b/webroot/rsrc/js/application/conpherence/behavior-toggle-widget.js @@ -9,6 +9,20 @@ JX.behavior('toggle-widget', function(config) { + var device; + + function init() { + device = JX.Device.getDevice(); + if (device != 'phone') { + var node = JX.$('conpherence-main-layout'); + JX.DOM.alterClass(node, 'hide-widgets', !config.show); + JX.Stratcom.invoke('resize'); + } else { + config.show = 0; + } + } + init(); + function _toggleColumn(e) { e.kill(); var node = JX.$('conpherence-main-layout'); @@ -16,9 +30,11 @@ JX.behavior('toggle-widget', function(config) { JX.DOM.alterClass(node, 'hide-widgets', !config.show); JX.Stratcom.invoke('resize'); - new JX.Request(config.settingsURI) - .setData({value: (config.show ? 1 : 0)}) - .send(); + if (device != 'phone') { + new JX.Request(config.settingsURI) + .setData({value: (config.show ? 1 : 0)}) + .send(); + } } JX.Stratcom.listen( From 5039b9ca28e4993e0c0c7cde600026f0001e585c Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 19 Oct 2016 08:34:09 -0700 Subject: [PATCH 40/55] Add some descriptive properties when viewing a Calendar import Summary: Ref T10747. When viewing an import detail page, show a little more information about what you're looking at. Test Plan: {F1876957} Reviewers: chad Reviewed By: chad Maniphest Tasks: T10747 Differential Revision: https://secure.phabricator.com/D16726 --- ...PhabricatorCalendarImportViewController.php | 11 +++++++++++ .../PhabricatorCalendarICSImportEngine.php | 18 ++++++++++++++++++ .../import/PhabricatorCalendarImportEngine.php | 8 ++++++++ 3 files changed, 37 insertions(+) diff --git a/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php b/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php index 0ecd5787cb..59b0f86915 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarImportViewController.php @@ -151,6 +151,17 @@ final class PhabricatorCalendarImportViewController $properties = id(new PHUIPropertyListView()) ->setViewer($viewer); + $engine = $import->getEngine(); + + $properties->addProperty( + pht('Source Type'), + $engine->getImportEngineTypeName()); + + $engine->appendImportProperties( + $viewer, + $import, + $properties); + return $properties; } diff --git a/src/applications/calendar/import/PhabricatorCalendarICSImportEngine.php b/src/applications/calendar/import/PhabricatorCalendarICSImportEngine.php index ae20635b6b..ff98666cbe 100644 --- a/src/applications/calendar/import/PhabricatorCalendarICSImportEngine.php +++ b/src/applications/calendar/import/PhabricatorCalendarICSImportEngine.php @@ -9,10 +9,28 @@ final class PhabricatorCalendarICSImportEngine return pht('Import .ics File'); } + public function getImportEngineTypeName() { + return pht('.ics File'); + } + public function getImportEngineHint() { return pht('Import an event in ".ics" (iCalendar) format.'); } + + public function appendImportProperties( + PhabricatorUser $viewer, + PhabricatorCalendarImport $import, + PHUIPropertyListView $properties) { + + $phid_key = PhabricatorCalendarImportICSFileTransaction::PARAMKEY_FILE; + $file_phid = $import->getParameter($phid_key); + + $properties->addProperty( + pht('Source File'), + $viewer->renderHandle($file_phid)); + } + public function newEditEngineFields( PhabricatorEditEngine $engine, PhabricatorCalendarImport $import) { diff --git a/src/applications/calendar/import/PhabricatorCalendarImportEngine.php b/src/applications/calendar/import/PhabricatorCalendarImportEngine.php index e9270b3d41..2fc9399fe3 100644 --- a/src/applications/calendar/import/PhabricatorCalendarImportEngine.php +++ b/src/applications/calendar/import/PhabricatorCalendarImportEngine.php @@ -9,8 +9,16 @@ abstract class PhabricatorCalendarImportEngine abstract public function getImportEngineName(); + abstract public function getImportEngineTypeName(); abstract public function getImportEngineHint(); + public function appendImportProperties( + PhabricatorUser $viewer, + PhabricatorCalendarImport $import, + PHUIPropertyListView $properties) { + return; + } + abstract public function newEditEngineFields( PhabricatorEditEngine $engine, PhabricatorCalendarImport $import); From d860008b6a45168981f1cc6c498bc8550384625b Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 19 Oct 2016 09:01:19 -0700 Subject: [PATCH 41/55] Make event detail view more user-friendly for imported events Summary: Ref T10747. When viewing an imported event: - Make it more clear that it is imported and where it is from. - Add some explicit "this is imported" help. Test Plan: Viewed imported and normal events. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10747 Differential Revision: https://secure.phabricator.com/D16727 --- .../PhabricatorCalendarController.php | 19 ++++++++++ ...abricatorCalendarEventCancelController.php | 18 +++++++--- ...PhabricatorCalendarEventEditController.php | 14 ++++++++ ...PhabricatorCalendarEventJoinController.php | 5 +++ ...PhabricatorCalendarEventViewController.php | 36 +++++++++++++++++++ .../PhabricatorCalendarImportPHIDType.php | 2 +- .../storage/PhabricatorCalendarEvent.php | 8 +++++ 7 files changed, 96 insertions(+), 6 deletions(-) diff --git a/src/applications/calendar/controller/PhabricatorCalendarController.php b/src/applications/calendar/controller/PhabricatorCalendarController.php index 36a9bfbbe9..88ffba9380 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarController.php @@ -18,4 +18,23 @@ abstract class PhabricatorCalendarController extends PhabricatorController { ->setContent($ics_data); } + protected function newImportedEventResponse(PhabricatorCalendarEvent $event) { + if (!$event->isImportedEvent()) { + return null; + } + + // Give the user a specific, detailed message if they try to edit an + // imported event via common web paths. Other edits (including those via + // the API) are blocked by the normal policy system, but this makes it more + // clear exactly why the event can't be edited. + + return $this->newDialog() + ->setTitle(pht('Can Not Edit Imported Event')) + ->appendParagraph( + pht( + 'This event has been imported from an external source and '. + 'can not be edited.')) + ->addCancelButton($event->getURI(), pht('Done')); + } + } diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventCancelController.php b/src/applications/calendar/controller/PhabricatorCalendarEventCancelController.php index 63ce289970..fbf7f9d45e 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarEventCancelController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventCancelController.php @@ -7,19 +7,27 @@ final class PhabricatorCalendarEventCancelController $viewer = $request->getViewer(); $id = $request->getURIData('id'); + // Just check CAN_VIEW first. Then we'll check if this is an import so + // we can raise a better error. $event = id(new PhabricatorCalendarEventQuery()) ->setViewer($viewer) ->withIDs(array($id)) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) ->executeOne(); if (!$event) { return new Aphront404Response(); } + $response = $this->newImportedEventResponse($event); + if ($response) { + return $response; + } + + // Now that we've done the import check, check for CAN_EDIT. + PhabricatorPolicyFilter::requireCapability( + $viewer, + $event, + PhabricatorPolicyCapability::CAN_EDIT); + $cancel_uri = $event->getURI(); $is_parent = $event->isParentEvent(); diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php b/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php index b5beea9019..7c01e795b7 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventEditController.php @@ -4,6 +4,20 @@ final class PhabricatorCalendarEventEditController extends PhabricatorCalendarController { public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); + + $id = $request->getURIData('id'); + if ($id) { + $event = id(new PhabricatorCalendarEventQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->executeOne(); + $response = $this->newImportedEventResponse($event); + if ($response) { + return $response; + } + } + return id(new PhabricatorCalendarEventEditEngine()) ->setController($this) ->buildResponse(); diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventJoinController.php b/src/applications/calendar/controller/PhabricatorCalendarEventJoinController.php index 9b58d39dec..52ad8cb133 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarEventJoinController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventJoinController.php @@ -15,6 +15,11 @@ final class PhabricatorCalendarEventJoinController return new Aphront404Response(); } + $response = $this->newImportedEventResponse($event); + if ($response) { + return $response; + } + $cancel_uri = $event->getURI(); $action = $request->getURIData('action'); diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php b/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php index 89e8109a4c..c8bc9b5593 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php @@ -63,6 +63,19 @@ final class PhabricatorCalendarEventViewController ->setHeader(pht('Details')); $recurring_header = $this->buildRecurringHeader($event); + // NOTE: This is a bit hacky: for imported events, we're just hiding the + // comment form without actually preventing comments. Users could still + // submit a request to add comments to these events. This isn't really a + // major problem since they can't do anything truly bad and there isn't an + // easy way to selectively disable this or some other similar behaviors + // today, but it would probably be nice to fully disable these + // "pseudo-edits" (like commenting and probably subscribing and awarding + // tokens) at some point. + if ($event->isImportedEvent()) { + $comment_view = null; + $timeline->setShouldTerminate(true); + } + $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setSubheader($subheader) @@ -105,6 +118,16 @@ final class PhabricatorCalendarEventViewController ->setPolicyObject($event) ->setHeaderIcon($event->getIcon()); + if ($event->isImportedEvent()) { + $header->addTag( + id(new PHUITagView()) + ->setType(PHUITagView::TYPE_SHADE) + ->setName(pht('Imported')) + ->setIcon('fa-download') + ->setHref($event->getImportSource()->getURI()) + ->setShade('orange')); + } + foreach ($this->buildRSVPActions($event) as $action) { $header->addActionLink($action); } @@ -141,12 +164,15 @@ final class PhabricatorCalendarEventViewController ->setWorkflow(!$can_edit)); } + $can_attend = !$event->isImportedEvent(); + if ($is_attending) { $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Decline Event')) ->setIcon('fa-user-times') ->setHref($this->getApplicationURI("event/join/{$id}/")) + ->setDisabled(!$can_attend) ->setWorkflow(true)); } else { $curtain->addAction( @@ -154,6 +180,7 @@ final class PhabricatorCalendarEventViewController ->setName(pht('Join Event')) ->setIcon('fa-user-plus') ->setHref($this->getApplicationURI("event/join/{$id}/")) + ->setDisabled(!$can_attend) ->setWorkflow(true)); } @@ -261,6 +288,15 @@ final class PhabricatorCalendarEventViewController pht('None')); } + if ($event->isImportedEvent()) { + $properties->addProperty( + pht('Imported By'), + pht( + '%s from %s', + $viewer->renderHandle($event->getImportAuthorPHID()), + $viewer->renderHandle($event->getImportSourcePHID()))); + } + $properties->addProperty( pht('Invitees'), $invitee_list); diff --git a/src/applications/calendar/phid/PhabricatorCalendarImportPHIDType.php b/src/applications/calendar/phid/PhabricatorCalendarImportPHIDType.php index 05d1cc0bb9..876ec7acf7 100644 --- a/src/applications/calendar/phid/PhabricatorCalendarImportPHIDType.php +++ b/src/applications/calendar/phid/PhabricatorCalendarImportPHIDType.php @@ -33,7 +33,7 @@ final class PhabricatorCalendarImportPHIDType extends PhabricatorPHIDType { $import = $objects[$phid]; $id = $import->getID(); - $name = $import->getName(); + $name = $import->getDisplayName(); $uri = $import->getURI(); $handle diff --git a/src/applications/calendar/storage/PhabricatorCalendarEvent.php b/src/applications/calendar/storage/PhabricatorCalendarEvent.php index 7ad6d85881..6baa54e008 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarEvent.php +++ b/src/applications/calendar/storage/PhabricatorCalendarEvent.php @@ -576,6 +576,10 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO } } + if ($this->isImportedEvent()) { + return 'fa-download'; + } + return $this->getIcon(); } @@ -584,6 +588,10 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO return 'red'; } + if ($this->isImportedEvent()) { + return 'orange'; + } + if ($viewer->isLoggedIn()) { $status = $this->getUserInviteStatus($viewer->getPHID()); switch ($status) { From c3de8f8305a996aeb5aee618e648b386f67744ce Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 19 Oct 2016 09:18:16 -0700 Subject: [PATCH 42/55] When generating Calendar event stubs, inherit import properties Summary: Ref T10747. Previously, importing a recurring event failed to mark the instnaces of the event as imported. Now, we copy the source/UID/importer over. Test Plan: Imported a recurring event, viewed event series, saw all of them marked imported. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10747 Differential Revision: https://secure.phabricator.com/D16728 --- .../storage/PhabricatorCalendarEvent.php | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/applications/calendar/storage/PhabricatorCalendarEvent.php b/src/applications/calendar/storage/PhabricatorCalendarEvent.php index 6baa54e008..64bdf07d46 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarEvent.php +++ b/src/applications/calendar/storage/PhabricatorCalendarEvent.php @@ -179,11 +179,10 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO ->setName($parent->getName()) ->setDescription($parent->getDescription()); - $sequence = $this->getSequenceIndex(); - if ($start) { $start_datetime = $start; } else { + $sequence = $this->getSequenceIndex(); $start_datetime = $parent->newSequenceIndexDateTime($sequence); if (!$start_datetime) { @@ -201,6 +200,19 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO ->setStartDateTime($start_datetime) ->setEndDateTime($end_datetime); + if ($parent->isImportedEvent()) { + $full_uid = $parent->getImportUID().'/'.$start_datetime->getEpoch(); + + // NOTE: We don't attach the import source because this gets called + // from CalendarEventQuery while building ghosts, before we've loaded + // and attached sources. Possibly this sequence should be flipped. + + $this + ->setImportAuthorPHID($parent->getImportAuthorPHID()) + ->setImportSourcePHID($parent->getImportSourcePHID()) + ->setImportUID($full_uid); + } + return $this; } From 314dc30017d4177744dd9a6828ae089b955c3147 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 19 Oct 2016 09:29:10 -0700 Subject: [PATCH 43/55] Add a URI-based ICS import source engine Summary: Ref T10747. This doesn't have a "keep up to date" option yet, but can, e.g., fetch a Google Calendar URI Test Plan: Fetched a Google Calendar URI, got some events imported. Reviewers: chad Reviewed By: chad Maniphest Tasks: T10747 Differential Revision: https://secure.phabricator.com/D16730 --- src/__phutil_library_map__.php | 8 ++ ...PhabricatorCalendarICSFileImportEngine.php | 102 ++++++++++++++++ .../PhabricatorCalendarICSImportEngine.php | 95 +-------------- .../PhabricatorCalendarICSURIImportEngine.php | 111 ++++++++++++++++++ .../PhabricatorCalendarImportFetchLogType.php | 33 ++++++ .../PhabricatorCalendarImportICSLogType.php | 2 +- ...icatorCalendarImportICSFileTransaction.php | 2 +- ...ricatorCalendarImportICSURITransaction.php | 73 ++++++++++++ 8 files changed, 332 insertions(+), 94 deletions(-) create mode 100644 src/applications/calendar/import/PhabricatorCalendarICSFileImportEngine.php create mode 100644 src/applications/calendar/import/PhabricatorCalendarICSURIImportEngine.php create mode 100644 src/applications/calendar/importlog/PhabricatorCalendarImportFetchLogType.php create mode 100644 src/applications/calendar/xaction/PhabricatorCalendarImportICSURITransaction.php diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index d225434c9c..e859dcdce9 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2100,7 +2100,9 @@ phutil_register_library_map(array( 'PhabricatorCalendarExportViewController' => 'applications/calendar/controller/PhabricatorCalendarExportViewController.php', 'PhabricatorCalendarHoliday' => 'applications/calendar/storage/PhabricatorCalendarHoliday.php', 'PhabricatorCalendarHolidayTestCase' => 'applications/calendar/storage/__tests__/PhabricatorCalendarHolidayTestCase.php', + 'PhabricatorCalendarICSFileImportEngine' => 'applications/calendar/import/PhabricatorCalendarICSFileImportEngine.php', 'PhabricatorCalendarICSImportEngine' => 'applications/calendar/import/PhabricatorCalendarICSImportEngine.php', + 'PhabricatorCalendarICSURIImportEngine' => 'applications/calendar/import/PhabricatorCalendarICSURIImportEngine.php', 'PhabricatorCalendarICSWriter' => 'applications/calendar/util/PhabricatorCalendarICSWriter.php', 'PhabricatorCalendarIconSet' => 'applications/calendar/icon/PhabricatorCalendarIconSet.php', 'PhabricatorCalendarImport' => 'applications/calendar/storage/PhabricatorCalendarImport.php', @@ -2117,9 +2119,11 @@ phutil_register_library_map(array( 'PhabricatorCalendarImportEmptyLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportEmptyLogType.php', 'PhabricatorCalendarImportEngine' => 'applications/calendar/import/PhabricatorCalendarImportEngine.php', 'PhabricatorCalendarImportEpochLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportEpochLogType.php', + 'PhabricatorCalendarImportFetchLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportFetchLogType.php', 'PhabricatorCalendarImportFrequencyLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportFrequencyLogType.php', 'PhabricatorCalendarImportICSFileTransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportICSFileTransaction.php', 'PhabricatorCalendarImportICSLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportICSLogType.php', + 'PhabricatorCalendarImportICSURITransaction' => 'applications/calendar/xaction/PhabricatorCalendarImportICSURITransaction.php', 'PhabricatorCalendarImportIgnoredNodeLogType' => 'applications/calendar/importlog/PhabricatorCalendarImportIgnoredNodeLogType.php', 'PhabricatorCalendarImportListController' => 'applications/calendar/controller/PhabricatorCalendarImportListController.php', 'PhabricatorCalendarImportLog' => 'applications/calendar/storage/PhabricatorCalendarImportLog.php', @@ -6931,7 +6935,9 @@ phutil_register_library_map(array( 'PhabricatorCalendarExportViewController' => 'PhabricatorCalendarController', 'PhabricatorCalendarHoliday' => 'PhabricatorCalendarDAO', 'PhabricatorCalendarHolidayTestCase' => 'PhabricatorTestCase', + 'PhabricatorCalendarICSFileImportEngine' => 'PhabricatorCalendarICSImportEngine', 'PhabricatorCalendarICSImportEngine' => 'PhabricatorCalendarImportEngine', + 'PhabricatorCalendarICSURIImportEngine' => 'PhabricatorCalendarICSImportEngine', 'PhabricatorCalendarICSWriter' => 'Phobject', 'PhabricatorCalendarIconSet' => 'PhabricatorIconSet', 'PhabricatorCalendarImport' => array( @@ -6953,9 +6959,11 @@ phutil_register_library_map(array( 'PhabricatorCalendarImportEmptyLogType' => 'PhabricatorCalendarImportLogType', 'PhabricatorCalendarImportEngine' => 'Phobject', 'PhabricatorCalendarImportEpochLogType' => 'PhabricatorCalendarImportLogType', + 'PhabricatorCalendarImportFetchLogType' => 'PhabricatorCalendarImportLogType', 'PhabricatorCalendarImportFrequencyLogType' => 'PhabricatorCalendarImportLogType', 'PhabricatorCalendarImportICSFileTransaction' => 'PhabricatorCalendarImportTransactionType', 'PhabricatorCalendarImportICSLogType' => 'PhabricatorCalendarImportLogType', + 'PhabricatorCalendarImportICSURITransaction' => 'PhabricatorCalendarImportTransactionType', 'PhabricatorCalendarImportIgnoredNodeLogType' => 'PhabricatorCalendarImportLogType', 'PhabricatorCalendarImportListController' => 'PhabricatorCalendarController', 'PhabricatorCalendarImportLog' => array( diff --git a/src/applications/calendar/import/PhabricatorCalendarICSFileImportEngine.php b/src/applications/calendar/import/PhabricatorCalendarICSFileImportEngine.php new file mode 100644 index 0000000000..7284776da6 --- /dev/null +++ b/src/applications/calendar/import/PhabricatorCalendarICSFileImportEngine.php @@ -0,0 +1,102 @@ +getParameter($phid_key); + + $properties->addProperty( + pht('Source File'), + $viewer->renderHandle($file_phid)); + } + + public function newEditEngineFields( + PhabricatorEditEngine $engine, + PhabricatorCalendarImport $import) { + $fields = array(); + + if ($engine->getIsCreate()) { + $fields[] = id(new PhabricatorFileEditField()) + ->setKey('icsFilePHID') + ->setLabel(pht('ICS File')) + ->setDescription(pht('ICS file to import.')) + ->setTransactionType( + PhabricatorCalendarImportICSFileTransaction::TRANSACTIONTYPE) + ->setConduitDescription(pht('File PHID to import.')) + ->setConduitTypeDescription(pht('File PHID.')); + } + + return $fields; + } + + public function getDisplayName(PhabricatorCalendarImport $import) { + $filename_key = PhabricatorCalendarImportICSFileTransaction::PARAMKEY_NAME; + $filename = $import->getParameter($filename_key); + if (strlen($filename)) { + return pht('ICS File "%s"', $filename); + } else { + return pht('ICS File'); + } + } + + public function didCreateImport( + PhabricatorUser $viewer, + PhabricatorCalendarImport $import) { + + $phid_key = PhabricatorCalendarImportICSFileTransaction::PARAMKEY_FILE; + $file_phid = $import->getParameter($phid_key); + + $file = id(new PhabricatorFileQuery()) + ->setViewer($viewer) + ->withPHIDs(array($file_phid)) + ->executeOne(); + if (!$file) { + throw new Exception( + pht( + 'Unable to load file ("%s") for import.', + $file_phid)); + } + + $data = $file->loadFileData(); + + return $this->importICSData($viewer, $import, $data); + } + + + public function canDisable( + PhabricatorUser $viewer, + PhabricatorCalendarImport $import) { + return false; + } + + public function explainCanDisable( + PhabricatorUser $viewer, + PhabricatorCalendarImport $import) { + return pht( + 'You can not disable import of an ICS file because the entire import '. + 'occurs immediately when you upload the file. There is no further '. + 'activity to disable.'); + } + + +} diff --git a/src/applications/calendar/import/PhabricatorCalendarICSImportEngine.php b/src/applications/calendar/import/PhabricatorCalendarICSImportEngine.php index ff98666cbe..3d94085469 100644 --- a/src/applications/calendar/import/PhabricatorCalendarICSImportEngine.php +++ b/src/applications/calendar/import/PhabricatorCalendarICSImportEngine.php @@ -1,84 +1,12 @@ getParameter($phid_key); - - $properties->addProperty( - pht('Source File'), - $viewer->renderHandle($file_phid)); - } - - public function newEditEngineFields( - PhabricatorEditEngine $engine, - PhabricatorCalendarImport $import) { - $fields = array(); - - if ($engine->getIsCreate()) { - $fields[] = id(new PhabricatorFileEditField()) - ->setKey('icsFilePHID') - ->setLabel(pht('ICS File')) - ->setDescription(pht('ICS file to import.')) - ->setTransactionType( - PhabricatorCalendarImportICSFileTransaction::TRANSACTIONTYPE) - ->setConduitDescription(pht('File PHID to import.')) - ->setConduitTypeDescription(pht('File PHID.')); - } - - return $fields; - } - - public function getDisplayName(PhabricatorCalendarImport $import) { - $filename_key = PhabricatorCalendarImportICSFileTransaction::PARAMKEY_NAME; - $filename = $import->getParameter($filename_key); - if (strlen($filename)) { - return pht('ICS File "%s"', $filename); - } else { - return pht('ICS File'); - } - } - - public function didCreateImport( - PhabricatorUser $viewer, - PhabricatorCalendarImport $import) { - - $phid_key = PhabricatorCalendarImportICSFileTransaction::PARAMKEY_FILE; - $file_phid = $import->getParameter($phid_key); - - $file = id(new PhabricatorFileQuery()) - ->setViewer($viewer) - ->withPHIDs(array($file_phid)) - ->executeOne(); - if (!$file) { - throw new Exception( - pht( - 'Unable to load file ("%s") for import.', - $file_phid)); - } - - $data = $file->loadFileData(); + $data) { $parser = new PhutilICSParser(); @@ -103,21 +31,4 @@ final class PhabricatorCalendarICSImportEngine return $this->importEventDocument($viewer, $import, $document); } - - public function canDisable( - PhabricatorUser $viewer, - PhabricatorCalendarImport $import) { - return false; - } - - public function explainCanDisable( - PhabricatorUser $viewer, - PhabricatorCalendarImport $import) { - return pht( - 'You can not disable import of an ICS file because the entire import '. - 'occurs immediately when you upload the file. There is no further '. - 'activity to disable.'); - } - - } diff --git a/src/applications/calendar/import/PhabricatorCalendarICSURIImportEngine.php b/src/applications/calendar/import/PhabricatorCalendarICSURIImportEngine.php new file mode 100644 index 0000000000..7b1754a911 --- /dev/null +++ b/src/applications/calendar/import/PhabricatorCalendarICSURIImportEngine.php @@ -0,0 +1,111 @@ +getParameter($uri_key); + + // Since the URI may contain a secret hash, don't show it to users who + // can not edit the import. + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $import, + PhabricatorPolicyCapability::CAN_EDIT); + if (!$can_edit) { + $uri_display = phutil_tag('em', array(), pht('Restricted')); + } else if (!PhabricatorEnv::isValidRemoteURIForLink($uri)) { + $uri_display = $uri; + } else { + $uri_display = phutil_tag( + 'a', + array( + 'href' => $uri, + 'target' => '_blank', + ), + $uri); + } + + $properties->addProperty(pht('Source URI'), $uri_display); + } + + public function newEditEngineFields( + PhabricatorEditEngine $engine, + PhabricatorCalendarImport $import) { + $fields = array(); + + if ($engine->getIsCreate()) { + $fields[] = id(new PhabricatorTextEditField()) + ->setKey('uri') + ->setLabel(pht('URI')) + ->setDescription(pht('URI to import.')) + ->setTransactionType( + PhabricatorCalendarImportICSURITransaction::TRANSACTIONTYPE) + ->setConduitDescription(pht('URI to import.')) + ->setConduitTypeDescription(pht('New URI.')); + } + + return $fields; + } + + public function getDisplayName(PhabricatorCalendarImport $import) { + return pht('ICS URI'); + } + + public function didCreateImport( + PhabricatorUser $viewer, + PhabricatorCalendarImport $import) { + + $uri_key = PhabricatorCalendarImportICSURITransaction::PARAMKEY_URI; + $uri = $import->getParameter($uri_key); + + PhabricatorSystemActionEngine::willTakeAction( + array($viewer->getPHID()), + new PhabricatorFilesOutboundRequestAction(), + 1); + + $file = PhabricatorFile::newFromFileDownload( + $uri, + array( + 'viewPolicy' => PhabricatorPolicies::POLICY_NOONE, + 'authorPHID' => $import->getAuthorPHID(), + 'canCDN' => true, + )); + + $import->newLogMessage( + PhabricatorCalendarImportFetchLogType::LOGTYPE, + array( + 'file.phid' => $file->getPHID(), + )); + + $data = $file->loadFileData(); + + return $this->importICSData($viewer, $import, $data); + } + + public function canDisable( + PhabricatorUser $viewer, + PhabricatorCalendarImport $import) { + return true; + } + +} diff --git a/src/applications/calendar/importlog/PhabricatorCalendarImportFetchLogType.php b/src/applications/calendar/importlog/PhabricatorCalendarImportFetchLogType.php new file mode 100644 index 0000000000..2a674239d6 --- /dev/null +++ b/src/applications/calendar/importlog/PhabricatorCalendarImportFetchLogType.php @@ -0,0 +1,33 @@ +renderHandle($log->getParameter('file.phid')); + } + + public function getDisplayIcon( + PhabricatorUser $viewer, + PhabricatorCalendarImportLog $log) { + return 'fa-download'; + } + + public function getDisplayColor( + PhabricatorUser $viewer, + PhabricatorCalendarImportLog $log) { + return 'green'; + } + +} diff --git a/src/applications/calendar/importlog/PhabricatorCalendarImportICSLogType.php b/src/applications/calendar/importlog/PhabricatorCalendarImportICSLogType.php index 30994b08ad..b3032f073e 100644 --- a/src/applications/calendar/importlog/PhabricatorCalendarImportICSLogType.php +++ b/src/applications/calendar/importlog/PhabricatorCalendarImportICSLogType.php @@ -15,7 +15,7 @@ final class PhabricatorCalendarImportICSLogType PhabricatorUser $viewer, PhabricatorCalendarImportLog $log) { return pht( - 'Failed to parse ICS file ("%s"): %s', + 'Failed to parse ICS data ("%s"): %s', $log->getParameter('ics.code'), $log->getParameter('ics.message')); } diff --git a/src/applications/calendar/xaction/PhabricatorCalendarImportICSFileTransaction.php b/src/applications/calendar/xaction/PhabricatorCalendarImportICSFileTransaction.php index 59cd91053f..26a968d13d 100644 --- a/src/applications/calendar/xaction/PhabricatorCalendarImportICSFileTransaction.php +++ b/src/applications/calendar/xaction/PhabricatorCalendarImportICSFileTransaction.php @@ -34,7 +34,7 @@ final class PhabricatorCalendarImportICSFileTransaction $viewer = $this->getActor(); $errors = array(); - $ics_type = PhabricatorCalendarICSImportEngine::ENGINETYPE; + $ics_type = PhabricatorCalendarICSFileImportEngine::ENGINETYPE; $import_type = $object->getEngine()->getImportEngineType(); if ($import_type != $ics_type) { if (!$xactions) { diff --git a/src/applications/calendar/xaction/PhabricatorCalendarImportICSURITransaction.php b/src/applications/calendar/xaction/PhabricatorCalendarImportICSURITransaction.php new file mode 100644 index 0000000000..13fdbd232b --- /dev/null +++ b/src/applications/calendar/xaction/PhabricatorCalendarImportICSURITransaction.php @@ -0,0 +1,73 @@ +getParameter(self::PARAMKEY_URI); + } + + public function applyInternalEffects($object, $value) { + $object->setParameter(self::PARAMKEY_URI, $value); + } + + public function getTitle() { + // NOTE: This transaction intentionally does not disclose the actual + // URI. + return pht( + '%s updated the import URI.', + $this->renderAuthor()); + } + + public function validateTransactions($object, array $xactions) { + $viewer = $this->getActor(); + $errors = array(); + + $ics_type = PhabricatorCalendarICSURIImportEngine::ENGINETYPE; + $import_type = $object->getEngine()->getImportEngineType(); + if ($import_type != $ics_type) { + if (!$xactions) { + return $errors; + } + + $errors[] = $this->newInvalidError( + pht( + 'You can not attach an ICS URI to an import type other than '. + 'an ICS URI import (type is "%s").', + $import_type)); + + return $errors; + } + + $new_value = $object->getParameter(self::PARAMKEY_URI); + foreach ($xactions as $xaction) { + $new_value = $xaction->getNewValue(); + if (!strlen($new_value)) { + continue; + } + + try { + PhabricatorEnv::requireValidRemoteURIForFetch( + $new_value, + array( + 'http', + 'https', + )); + } catch (Exception $ex) { + $errors[] = $this->newInvalidError( + $ex->getMessage(), + $xaction); + } + } + + if (!strlen($new_value)) { + $errors[] = $this->newRequiredError( + pht('You must select an ".ics" URI to import.')); + } + + return $errors; + } +} From 67073f0d8af2095fe2b16b1f6c86b883bc3a91ed Mon Sep 17 00:00:00 2001 From: Chad Little Date: Wed, 19 Oct 2016 15:01:41 -0700 Subject: [PATCH 44/55] Clean up mobile css on config page Summary: I seemed to have missed this, lots of funky desktop padding on mobile. Test Plan: Review Config / Maniphest / etc on a mobile browser. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D16731 --- resources/celerity/map.php | 10 +++++----- .../css/application/config/config-page.css | 18 ++++++++++++++++++ webroot/rsrc/css/phui/phui-two-column-view.css | 2 +- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 7d0d2996b7..4c79d8893e 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,7 +9,7 @@ return array( 'names' => array( 'conpherence.pkg.css' => 'fabab894', 'conpherence.pkg.js' => '6249a1cf', - 'core.pkg.css' => 'b99bbf5e', + 'core.pkg.css' => '46d588e4', 'core.pkg.js' => '2d9fc958', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => 'e1d704ce', @@ -42,7 +42,7 @@ return array( 'rsrc/css/application/chatlog/chatlog.css' => 'd295b020', 'rsrc/css/application/conduit/conduit-api.css' => '7bc725c4', 'rsrc/css/application/config/config-options.css' => '0ede4c9b', - 'rsrc/css/application/config/config-page.css' => '8798e14f', + 'rsrc/css/application/config/config-page.css' => 'b80124ae', 'rsrc/css/application/config/config-template.css' => '8f18fa41', 'rsrc/css/application/config/setup-issue.css' => 'f794cfc3', 'rsrc/css/application/config/unhandled-exception.css' => '4c96257a', @@ -161,7 +161,7 @@ return array( 'rsrc/css/phui/phui-status.css' => 'd5263e49', 'rsrc/css/phui/phui-tag-view.css' => '6bbd83e2', 'rsrc/css/phui/phui-timeline-view.css' => 'bc523970', - 'rsrc/css/phui/phui-two-column-view.css' => 'fcfbe347', + 'rsrc/css/phui/phui-two-column-view.css' => 'bbe32c23', 'rsrc/css/phui/workboards/phui-workboard-color.css' => 'ac6fe6a7', 'rsrc/css/phui/workboards/phui-workboard.css' => 'e09eb53a', 'rsrc/css/phui/workboards/phui-workcard.css' => '0c62d7c5', @@ -615,7 +615,7 @@ return array( 'changeset-view-manager' => 'a2828756', 'conduit-api-css' => '7bc725c4', 'config-options-css' => '0ede4c9b', - 'config-page-css' => '8798e14f', + 'config-page-css' => 'b80124ae', 'conpherence-durable-column-view' => 'd82e130c', 'conpherence-header-pane-css' => '1c81cda6', 'conpherence-menu-css' => '4f51db5a', @@ -937,7 +937,7 @@ return array( 'phui-tag-view-css' => '6bbd83e2', 'phui-theme-css' => '798c69b8', 'phui-timeline-view-css' => 'bc523970', - 'phui-two-column-view-css' => 'fcfbe347', + 'phui-two-column-view-css' => 'bbe32c23', 'phui-workboard-color-css' => 'ac6fe6a7', 'phui-workboard-view-css' => 'e09eb53a', 'phui-workcard-view-css' => '0c62d7c5', diff --git a/webroot/rsrc/css/application/config/config-page.css b/webroot/rsrc/css/application/config/config-page.css index a46ead7a2c..5fe15aadb8 100644 --- a/webroot/rsrc/css/application/config/config-page.css +++ b/webroot/rsrc/css/application/config/config-page.css @@ -8,19 +8,37 @@ border-bottom: 1px solid {$thinblueborder}; } +.device-phone .config-page-header { + margin: 4px 12px 0; + padding-bottom: 4px; +} + .config-page-header .phui-profile-header { padding: 0; } +.device-phone .config-page-header .phui-profile-header { + padding-left: 4px; + padding-right: 4px; +} + .config-page-header .phui-profile-header.phui-header-shell .phui-header-header { font-size: 20px; } +.device-phone .config-page-header .phui-profile-header.phui-header-shell + .phui-header-header { + font-size: 16px; +} .config-page-content { margin: 0 24px; } +.device-phone .config-page-content { + margin: 0 4px; +} + .device-desktop .config-page-content .phui-object-item-list-view { padding-left: 0; padding-right: 0; diff --git a/webroot/rsrc/css/phui/phui-two-column-view.css b/webroot/rsrc/css/phui/phui-two-column-view.css index 57f8d81934..e8f1ce0de5 100644 --- a/webroot/rsrc/css/phui/phui-two-column-view.css +++ b/webroot/rsrc/css/phui/phui-two-column-view.css @@ -23,7 +23,7 @@ } .device-phone .phui-two-column-header .phui-header-header { - font-size: 18px; + font-size: 16px; } .phui-two-column-view .phui-two-column-header .phui-header-shell { From a00e867de0941ed4b50d4bda294ccfb884a8bba5 Mon Sep 17 00:00:00 2001 From: Chad Little Date: Wed, 19 Oct 2016 15:15:06 -0700 Subject: [PATCH 45/55] Hide footer on mobile Conpherence Summary: This has been causing scrolling issues for me for a while, turns out the footer behind the Conpherence layout can bubble up and cause scrolling issues if you don't hit the scrollarea in the right place with your finger. Hiding it via CSS or removing the footer in the configuration resolves the issue on my phone on secure. Test Plan: Test with and without footer on secure, hide footer in CSS since it's not visible anyways. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D16732 --- resources/celerity/map.php | 6 +++--- webroot/rsrc/css/application/conpherence/message-pane.css | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 4c79d8893e..350c5c53b0 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,7 +7,7 @@ */ return array( 'names' => array( - 'conpherence.pkg.css' => 'fabab894', + 'conpherence.pkg.css' => 'cea72e09', 'conpherence.pkg.js' => '6249a1cf', 'core.pkg.css' => '46d588e4', 'core.pkg.js' => '2d9fc958', @@ -49,7 +49,7 @@ return array( 'rsrc/css/application/conpherence/durable-column.css' => 'd82e130c', 'rsrc/css/application/conpherence/header-pane.css' => '1c81cda6', 'rsrc/css/application/conpherence/menu.css' => '4f51db5a', - 'rsrc/css/application/conpherence/message-pane.css' => 'b499f371', + 'rsrc/css/application/conpherence/message-pane.css' => '394ae8fa', 'rsrc/css/application/conpherence/notification.css' => '965db05b', 'rsrc/css/application/conpherence/participant-pane.css' => 'ac1baaa8', 'rsrc/css/application/conpherence/transaction.css' => '85129c68', @@ -619,7 +619,7 @@ return array( 'conpherence-durable-column-view' => 'd82e130c', 'conpherence-header-pane-css' => '1c81cda6', 'conpherence-menu-css' => '4f51db5a', - 'conpherence-message-pane-css' => 'b499f371', + 'conpherence-message-pane-css' => '394ae8fa', 'conpherence-notification-css' => '965db05b', 'conpherence-participant-pane-css' => 'ac1baaa8', 'conpherence-thread-manager' => '358c717b', diff --git a/webroot/rsrc/css/application/conpherence/message-pane.css b/webroot/rsrc/css/application/conpherence/message-pane.css index db82a4a336..6336ac0cc5 100644 --- a/webroot/rsrc/css/application/conpherence/message-pane.css +++ b/webroot/rsrc/css/application/conpherence/message-pane.css @@ -411,6 +411,11 @@ margin-bottom: 0; } +/* this causes scrolling issues on iDevices */ +.device .conpherence-layout + .phabricator-standard-page-footer { + display: none; +} + /***** Thread Loading *********************************************************/ .conpherence-layout .conpherence-loading-mask { From a3253f78ce1477a5132250e68f10ee80283fff30 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 20 Oct 2016 08:33:27 -0700 Subject: [PATCH 46/55] Make query engines "overheat" instead of stalling when filtering too many results Summary: Ref T11773. This is an initial first step toward a more complete solution, but should make the worst case much less bad: prior to this change, the worst case was "30 second exeuction timeout". After this patch, the worst case is "no results + explanatory message", which is strictly better. Test Plan: Made all feed stories fail policy checks, loaded home page. - Before adding overheating: 9,600 queries / 20 seconds - After adding overheating: 376 queries / 800ms Reviewers: chad Reviewed By: chad Maniphest Tasks: T11773 Differential Revision: https://secure.phabricator.com/D16735 --- ...PhabricatorApplicationSearchController.php | 29 +++++++++++++++++++ .../policy/PhabricatorPolicyAwareQuery.php | 25 ++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/src/applications/search/controller/PhabricatorApplicationSearchController.php b/src/applications/search/controller/PhabricatorApplicationSearchController.php index 92a8181b4f..386be2ca8a 100644 --- a/src/applications/search/controller/PhabricatorApplicationSearchController.php +++ b/src/applications/search/controller/PhabricatorApplicationSearchController.php @@ -238,6 +238,8 @@ final class PhabricatorApplicationSearchController $nux_view = null; } + $is_overheated = $query->getIsOverheated(); + if ($nux_view) { $box->appendChild($nux_view); } else { @@ -265,6 +267,10 @@ final class PhabricatorApplicationSearchController $box->appendChild($list->getContent()); } + if ($is_overheated) { + $box->appendChild($this->newOverheatedView($objects)); + } + $result_header = $list->getHeader(); if ($result_header) { $box->setHeader($result_header); @@ -525,4 +531,27 @@ final class PhabricatorApplicationSearchController ->setDropdownMenu($action_list); } + private function newOverheatedView(array $results) { + if ($results) { + $message = pht( + 'Most objects matching your query are not visible to you, so '. + 'filtering results is taking a long time. Only some results are '. + 'shown. Refine your query to find results more quickly.'); + } else { + $message = pht( + 'Most objects matching your query are not visible to you, so '. + 'filtering results is taking a long time. Refine your query to '. + 'find results more quickly.'); + } + + return id(new PHUIInfoView()) + ->setSeverity(PHUIInfoView::SEVERITY_WARNING) + ->setFlush(true) + ->setTitle(pht('Query Overheated')) + ->setErrors( + array( + $message, + )); + } + } diff --git a/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php b/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php index 42d8001ee2..7aa0f28dfe 100644 --- a/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php +++ b/src/infrastructure/query/policy/PhabricatorPolicyAwareQuery.php @@ -44,6 +44,7 @@ abstract class PhabricatorPolicyAwareQuery extends PhabricatorOffsetPagedQuery { * and `null` (inherit from parent query, with no exceptions by default). */ private $raisePolicyExceptions; + private $isOverheated; /* -( Query Configuration )------------------------------------------------ */ @@ -215,6 +216,14 @@ abstract class PhabricatorPolicyAwareQuery extends PhabricatorOffsetPagedQuery { $this->willExecute(); + // If we examine and filter significantly more objects than the query + // limit, we stop early. This prevents us from looping through a huge + // number of records when the viewer can see few or none of them. See + // T11773 for some discussion. + $this->isOverheated = false; + $overheat_limit = $limit * 10; + $total_seen = 0; + do { if ($need) { $this->rawResultLimit = min($need - $count, 1024); @@ -232,6 +241,8 @@ abstract class PhabricatorPolicyAwareQuery extends PhabricatorOffsetPagedQuery { $page = array(); } + $total_seen += count($page); + if ($page) { $maybe_visible = $this->willFilterPage($page); if ($maybe_visible) { @@ -302,6 +313,11 @@ abstract class PhabricatorPolicyAwareQuery extends PhabricatorOffsetPagedQuery { } $this->nextPage($page); + + if ($overheat_limit && ($total_seen >= $overheat_limit)) { + $this->isOverheated = true; + break; + } } while (true); $results = $this->didLoadResults($results); @@ -369,6 +385,15 @@ abstract class PhabricatorPolicyAwareQuery extends PhabricatorOffsetPagedQuery { return $this; } + + public function getIsOverheated() { + if ($this->isOverheated === null) { + throw new PhutilInvalidStateException('execute'); + } + return $this->isOverheated; + } + + /** * Return a map of all object PHIDs which were loaded in the query but * filtered out by policy constraints. This allows a caller to distinguish From 1f6ad5e7ddd1d631d4cdccdc874c234b21908ee3 Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 20 Oct 2016 11:14:33 -0700 Subject: [PATCH 47/55] Support ".ico" in Celerity and simplify rewite rule configuration Summary: See D16734. - Add ".ico" files to the Celerity map. - Add a formal route for "/favicon.ico". - Remove instructions to configure `/rsrc/` and `/favicon.ico` rewrite rules. Long ago, we served resources directly via `/rsrc/` in at least some cases. As we added more features, this stopped working more and more often (for example, Apache can never serve CSS this way, because it doesn't know how to post-process `{$variables}`). In modern code (until this change), only `/favicon.ico` is still expected to be served this way. Instead, serve it with an explicit route via controller (this allows different Sites to have different favicons, for example). Remove the instructions suggesting the old rewrite rules be configured. It's OK if they're still in place -- they won't break anything, so we don't need to rush to get users to delete them. We should keep "webroot/favicon.ico" in place for now, since it needs to be there for users with the old rewrite rule. Test Plan: - Ran celerity map. - Loaded `/favicon.ico`, got resource via route. - Used `celerity_generate_resource_uri()` to get paths to other icons, loaded them, got icons. Reviewers: chad Reviewed By: chad Differential Revision: https://secure.phabricator.com/D16737 --- resources/celerity/map.php | 5 +++++ src/__phutil_library_map__.php | 2 ++ .../controller/CelerityResourceController.php | 1 + .../resources/CelerityResourcesOnDisk.php | 1 + .../PhabricatorSystemApplication.php | 1 + .../PhabricatorSystemFaviconController.php | 19 +++++++++++++++++++ .../configuration/configuration_guide.diviner | 8 -------- 7 files changed, 29 insertions(+), 8 deletions(-) create mode 100644 src/applications/system/controller/PhabricatorSystemFaviconController.php diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 350c5c53b0..96181688f8 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -16,6 +16,7 @@ return array( 'differential.pkg.js' => '634399e9', 'diffusion.pkg.css' => '91c5d3a6', 'diffusion.pkg.js' => '84c8f8fd', + 'favicon.ico' => '30672e08', 'maniphest.pkg.css' => '4845691a', 'maniphest.pkg.js' => '949a7498', 'rsrc/css/aphront/aphront-bars.css' => '231ac33c', @@ -285,6 +286,7 @@ return array( 'rsrc/favicons/dark/favicon-196x196.png' => '5e06ee72', 'rsrc/favicons/dark/favicon-32x32.png' => 'bdd7e16b', 'rsrc/favicons/dark/favicon-96x96.png' => '0cf55978', + 'rsrc/favicons/dark/favicon.ico' => '4343aaa6', 'rsrc/favicons/dark/mstile-144x144.png' => '4dc9d42d', 'rsrc/favicons/dark/mstile-150x150.png' => '2dc61c90', 'rsrc/favicons/dark/mstile-310x150.png' => '4fe58ab2', @@ -295,6 +297,7 @@ return array( 'rsrc/favicons/favicon-196x196.png' => '95db275e', 'rsrc/favicons/favicon-32x32.png' => '5bd18b6c', 'rsrc/favicons/favicon-96x96.png' => '7242c8e9', + 'rsrc/favicons/favicon.ico' => 'cdb11121', 'rsrc/favicons/mask-icon.svg' => 'e132a80f', 'rsrc/favicons/mstile-144x144.png' => '310c2ee5', 'rsrc/favicons/mstile-150x150.png' => '74bf5133', @@ -314,6 +317,7 @@ return array( 'rsrc/favicons/red/favicon-196x196.png' => '94c089a5', 'rsrc/favicons/red/favicon-32x32.png' => '5848673e', 'rsrc/favicons/red/favicon-96x96.png' => '895d54e8', + 'rsrc/favicons/red/favicon.ico' => '25172b6b', 'rsrc/favicons/red/mstile-144x144.png' => '448639f5', 'rsrc/favicons/red/mstile-150x150.png' => 'c2ba45d0', 'rsrc/favicons/red/mstile-310x150.png' => 'b0e50799', @@ -332,6 +336,7 @@ return array( 'rsrc/favicons/yellow/favicon-196x196.png' => '932c7c65', 'rsrc/favicons/yellow/favicon-32x32.png' => '005c9f92', 'rsrc/favicons/yellow/favicon-96x96.png' => '3ad9bfa9', + 'rsrc/favicons/yellow/favicon.ico' => '2f5b2991', 'rsrc/favicons/yellow/mstile-144x144.png' => 'fc52335c', 'rsrc/favicons/yellow/mstile-150x150.png' => '9e556f80', 'rsrc/favicons/yellow/mstile-310x150.png' => '2c915073', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index e859dcdce9..eafb0acdd5 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -3808,6 +3808,7 @@ phutil_register_library_map(array( 'PhabricatorSystemDAO' => 'applications/system/storage/PhabricatorSystemDAO.php', 'PhabricatorSystemDestructionGarbageCollector' => 'applications/system/garbagecollector/PhabricatorSystemDestructionGarbageCollector.php', 'PhabricatorSystemDestructionLog' => 'applications/system/storage/PhabricatorSystemDestructionLog.php', + 'PhabricatorSystemFaviconController' => 'applications/system/controller/PhabricatorSystemFaviconController.php', 'PhabricatorSystemReadOnlyController' => 'applications/system/controller/PhabricatorSystemReadOnlyController.php', 'PhabricatorSystemRemoveDestroyWorkflow' => 'applications/system/management/PhabricatorSystemRemoveDestroyWorkflow.php', 'PhabricatorSystemRemoveLogWorkflow' => 'applications/system/management/PhabricatorSystemRemoveLogWorkflow.php', @@ -8942,6 +8943,7 @@ phutil_register_library_map(array( 'PhabricatorSystemDAO' => 'PhabricatorLiskDAO', 'PhabricatorSystemDestructionGarbageCollector' => 'PhabricatorGarbageCollector', 'PhabricatorSystemDestructionLog' => 'PhabricatorSystemDAO', + 'PhabricatorSystemFaviconController' => 'PhabricatorController', 'PhabricatorSystemReadOnlyController' => 'PhabricatorController', 'PhabricatorSystemRemoveDestroyWorkflow' => 'PhabricatorSystemRemoveWorkflow', 'PhabricatorSystemRemoveLogWorkflow' => 'PhabricatorSystemRemoveWorkflow', diff --git a/src/applications/celerity/controller/CelerityResourceController.php b/src/applications/celerity/controller/CelerityResourceController.php index c1ea652084..95d674c641 100644 --- a/src/applications/celerity/controller/CelerityResourceController.php +++ b/src/applications/celerity/controller/CelerityResourceController.php @@ -145,6 +145,7 @@ abstract class CelerityResourceController extends PhabricatorController { 'eot' => 'font/eot', 'ttf' => 'font/ttf', 'mp3' => 'audio/mpeg', + 'ico' => 'image/x-icon', ); } diff --git a/src/applications/celerity/resources/CelerityResourcesOnDisk.php b/src/applications/celerity/resources/CelerityResourcesOnDisk.php index 3187c5b9ad..1f9e614da9 100644 --- a/src/applications/celerity/resources/CelerityResourcesOnDisk.php +++ b/src/applications/celerity/resources/CelerityResourcesOnDisk.php @@ -39,6 +39,7 @@ abstract class CelerityResourcesOnDisk extends CelerityPhysicalResources { 'ttf', 'eot', 'mp3', + 'ico', ); } diff --git a/src/applications/system/application/PhabricatorSystemApplication.php b/src/applications/system/application/PhabricatorSystemApplication.php index 0ec8f6a7a4..7ce2b4dbe0 100644 --- a/src/applications/system/application/PhabricatorSystemApplication.php +++ b/src/applications/system/application/PhabricatorSystemApplication.php @@ -26,6 +26,7 @@ final class PhabricatorSystemApplication extends PhabricatorApplication { '/readonly/' => array( '(?P[^/]+)/' => 'PhabricatorSystemReadOnlyController', ), + '/favicon.ico' => 'PhabricatorSystemFaviconController', ); } diff --git a/src/applications/system/controller/PhabricatorSystemFaviconController.php b/src/applications/system/controller/PhabricatorSystemFaviconController.php new file mode 100644 index 0000000000..d9a31cbcb6 --- /dev/null +++ b/src/applications/system/controller/PhabricatorSystemFaviconController.php @@ -0,0 +1,19 @@ +setContent($content) + ->setMimeType('image/x-icon') + ->setCacheDurationInSeconds(phutil_units('24 hours in seconds')) + ->setCanCDN(true); + } +} diff --git a/src/docs/user/configuration/configuration_guide.diviner b/src/docs/user/configuration/configuration_guide.diviner index b420996848..b4c494449b 100644 --- a/src/docs/user/configuration/configuration_guide.diviner +++ b/src/docs/user/configuration/configuration_guide.diviner @@ -48,8 +48,6 @@ this: DocumentRoot /path/to/phabricator/webroot RewriteEngine on - RewriteRule ^/rsrc/(.*) - [L,QSA] - RewriteRule ^/favicon.ico - [L,QSA] RewriteRule ^(.*)$ /index.php?__path__=$1 [B,L,QSA] @@ -91,10 +89,6 @@ For nginx, use a configuration like this: rewrite ^/(.*)$ /index.php?__path__=/$1 last; } - location = /favicon.ico { - try_files $uri =204; - } - location /index.php { fastcgi_pass localhost:9000; fastcgi_index index.php; @@ -130,8 +124,6 @@ For lighttpd, add a section like this to your lighttpd.conf: $HTTP["host"] =~ "phabricator(\.example\.com)?" { server.document-root = "/path/to/phabricator/webroot" url.rewrite-once = ( - "^(/rsrc/.*)$" => "$1", - "^(/favicon.ico)$" => "$1", # This simulates QSA ("query string append") mode in apache "^(/[^?]*)\?(.*)" => "/index.php?__path__=$1&$2", "^(/.*)$" => "/index.php?__path__=$1", From 1fdb8ba1123b557577cb19faa85611751aec76eb Mon Sep 17 00:00:00 2001 From: Chad Little Date: Thu, 20 Oct 2016 11:41:57 -0700 Subject: [PATCH 48/55] JX.Favicon for Conpherence Summary: I think maybe these should be more separate from JX.Title, but seems to work ok. May build new favicons just for messages though. Proof of concept UI. Test Plan: Send message on one browser, see red icon in other browser. Click on menu, count and favicon switch back to normal. Reviewers: epriestley Reviewed By: epriestley Subscribers: Korvin Differential Revision: https://secure.phabricator.com/D16734 --- resources/celerity/map.php | 102 +++++------------- src/view/page/PhabricatorBarePageView.php | 12 ++- .../page/menu/PhabricatorMainMenuView.php | 20 ++++ .../dark/apple-touch-icon-114x114.png | Bin 9079 -> 0 bytes .../dark/apple-touch-icon-120x120.png | Bin 9798 -> 0 bytes .../dark/apple-touch-icon-144x144.png | Bin 12871 -> 0 bytes .../dark/apple-touch-icon-152x152.png | Bin 15159 -> 0 bytes .../favicons/dark/apple-touch-icon-57x57.png | Bin 3177 -> 0 bytes .../favicons/dark/apple-touch-icon-60x60.png | Bin 3213 -> 0 bytes .../favicons/dark/apple-touch-icon-72x72.png | Bin 4423 -> 0 bytes .../favicons/dark/apple-touch-icon-76x76.png | Bin 4830 -> 0 bytes webroot/rsrc/favicons/dark/favicon-128.png | Bin 5799 -> 0 bytes webroot/rsrc/favicons/dark/favicon-16x16.png | Bin 603 -> 0 bytes .../rsrc/favicons/dark/favicon-196x196.png | Bin 25430 -> 0 bytes webroot/rsrc/favicons/dark/favicon-32x32.png | Bin 1263 -> 0 bytes webroot/rsrc/favicons/dark/favicon-96x96.png | Bin 5921 -> 0 bytes webroot/rsrc/favicons/dark/favicon.ico | Bin 34494 -> 0 bytes webroot/rsrc/favicons/dark/mstile-144x144.png | Bin 12871 -> 0 bytes webroot/rsrc/favicons/dark/mstile-150x150.png | Bin 45458 -> 0 bytes webroot/rsrc/favicons/dark/mstile-310x150.png | Bin 79468 -> 0 bytes webroot/rsrc/favicons/dark/mstile-310x310.png | Bin 168197 -> 0 bytes webroot/rsrc/favicons/dark/mstile-70x70.png | Bin 5799 -> 0 bytes webroot/rsrc/favicons/favicon-mention.ico | Bin 0 -> 34494 bytes webroot/rsrc/favicons/favicon-message.ico | Bin 0 -> 34494 bytes .../favicons/red/apple-touch-icon-114x114.png | Bin 10576 -> 0 bytes .../favicons/red/apple-touch-icon-120x120.png | Bin 10974 -> 0 bytes .../favicons/red/apple-touch-icon-144x144.png | Bin 14245 -> 0 bytes .../favicons/red/apple-touch-icon-152x152.png | Bin 16672 -> 0 bytes .../favicons/red/apple-touch-icon-57x57.png | Bin 3652 -> 0 bytes .../favicons/red/apple-touch-icon-60x60.png | Bin 3706 -> 0 bytes .../favicons/red/apple-touch-icon-72x72.png | Bin 4780 -> 0 bytes .../favicons/red/apple-touch-icon-76x76.png | Bin 5416 -> 0 bytes webroot/rsrc/favicons/red/favicon-128.png | Bin 5725 -> 0 bytes webroot/rsrc/favicons/red/favicon-16x16.png | Bin 608 -> 0 bytes webroot/rsrc/favicons/red/favicon-196x196.png | Bin 28189 -> 0 bytes webroot/rsrc/favicons/red/favicon-32x32.png | Bin 1272 -> 0 bytes webroot/rsrc/favicons/red/favicon-96x96.png | Bin 6494 -> 0 bytes webroot/rsrc/favicons/red/favicon.ico | Bin 34494 -> 0 bytes webroot/rsrc/favicons/red/mstile-144x144.png | Bin 14245 -> 0 bytes webroot/rsrc/favicons/red/mstile-150x150.png | Bin 54563 -> 0 bytes webroot/rsrc/favicons/red/mstile-310x150.png | Bin 90452 -> 0 bytes webroot/rsrc/favicons/red/mstile-310x310.png | Bin 206330 -> 0 bytes webroot/rsrc/favicons/red/mstile-70x70.png | Bin 5725 -> 0 bytes .../yellow/apple-touch-icon-114x114.png | Bin 9696 -> 0 bytes .../yellow/apple-touch-icon-120x120.png | Bin 10252 -> 0 bytes .../yellow/apple-touch-icon-144x144.png | Bin 13659 -> 0 bytes .../yellow/apple-touch-icon-152x152.png | Bin 16032 -> 0 bytes .../yellow/apple-touch-icon-57x57.png | Bin 3232 -> 0 bytes .../yellow/apple-touch-icon-60x60.png | Bin 3421 -> 0 bytes .../yellow/apple-touch-icon-72x72.png | Bin 4510 -> 0 bytes .../yellow/apple-touch-icon-76x76.png | Bin 5329 -> 0 bytes webroot/rsrc/favicons/yellow/favicon-128.png | Bin 5575 -> 0 bytes .../rsrc/favicons/yellow/favicon-16x16.png | Bin 555 -> 0 bytes .../rsrc/favicons/yellow/favicon-196x196.png | Bin 28291 -> 0 bytes .../rsrc/favicons/yellow/favicon-32x32.png | Bin 1158 -> 0 bytes .../rsrc/favicons/yellow/favicon-96x96.png | Bin 5847 -> 0 bytes webroot/rsrc/favicons/yellow/favicon.ico | Bin 34494 -> 0 bytes .../rsrc/favicons/yellow/mstile-144x144.png | Bin 13659 -> 0 bytes .../rsrc/favicons/yellow/mstile-150x150.png | Bin 53189 -> 0 bytes .../rsrc/favicons/yellow/mstile-310x150.png | Bin 90624 -> 0 bytes .../rsrc/favicons/yellow/mstile-310x310.png | Bin 210372 -> 0 bytes webroot/rsrc/favicons/yellow/mstile-70x70.png | Bin 5575 -> 0 bytes .../aphlict/behavior-aphlict-dropdown.js | 13 +++ webroot/rsrc/js/core/Favicon.js | 35 ++++++ webroot/rsrc/js/core/Title.js | 4 +- 65 files changed, 104 insertions(+), 82 deletions(-) delete mode 100644 webroot/rsrc/favicons/dark/apple-touch-icon-114x114.png delete mode 100644 webroot/rsrc/favicons/dark/apple-touch-icon-120x120.png delete mode 100644 webroot/rsrc/favicons/dark/apple-touch-icon-144x144.png delete mode 100644 webroot/rsrc/favicons/dark/apple-touch-icon-152x152.png delete mode 100644 webroot/rsrc/favicons/dark/apple-touch-icon-57x57.png delete mode 100644 webroot/rsrc/favicons/dark/apple-touch-icon-60x60.png delete mode 100644 webroot/rsrc/favicons/dark/apple-touch-icon-72x72.png delete mode 100644 webroot/rsrc/favicons/dark/apple-touch-icon-76x76.png delete mode 100644 webroot/rsrc/favicons/dark/favicon-128.png delete mode 100644 webroot/rsrc/favicons/dark/favicon-16x16.png delete mode 100644 webroot/rsrc/favicons/dark/favicon-196x196.png delete mode 100644 webroot/rsrc/favicons/dark/favicon-32x32.png delete mode 100644 webroot/rsrc/favicons/dark/favicon-96x96.png delete mode 100644 webroot/rsrc/favicons/dark/favicon.ico delete mode 100644 webroot/rsrc/favicons/dark/mstile-144x144.png delete mode 100644 webroot/rsrc/favicons/dark/mstile-150x150.png delete mode 100644 webroot/rsrc/favicons/dark/mstile-310x150.png delete mode 100644 webroot/rsrc/favicons/dark/mstile-310x310.png delete mode 100644 webroot/rsrc/favicons/dark/mstile-70x70.png create mode 100644 webroot/rsrc/favicons/favicon-mention.ico create mode 100644 webroot/rsrc/favicons/favicon-message.ico delete mode 100644 webroot/rsrc/favicons/red/apple-touch-icon-114x114.png delete mode 100644 webroot/rsrc/favicons/red/apple-touch-icon-120x120.png delete mode 100644 webroot/rsrc/favicons/red/apple-touch-icon-144x144.png delete mode 100644 webroot/rsrc/favicons/red/apple-touch-icon-152x152.png delete mode 100644 webroot/rsrc/favicons/red/apple-touch-icon-57x57.png delete mode 100644 webroot/rsrc/favicons/red/apple-touch-icon-60x60.png delete mode 100644 webroot/rsrc/favicons/red/apple-touch-icon-72x72.png delete mode 100644 webroot/rsrc/favicons/red/apple-touch-icon-76x76.png delete mode 100644 webroot/rsrc/favicons/red/favicon-128.png delete mode 100644 webroot/rsrc/favicons/red/favicon-16x16.png delete mode 100644 webroot/rsrc/favicons/red/favicon-196x196.png delete mode 100644 webroot/rsrc/favicons/red/favicon-32x32.png delete mode 100644 webroot/rsrc/favicons/red/favicon-96x96.png delete mode 100644 webroot/rsrc/favicons/red/favicon.ico delete mode 100644 webroot/rsrc/favicons/red/mstile-144x144.png delete mode 100644 webroot/rsrc/favicons/red/mstile-150x150.png delete mode 100644 webroot/rsrc/favicons/red/mstile-310x150.png delete mode 100644 webroot/rsrc/favicons/red/mstile-310x310.png delete mode 100644 webroot/rsrc/favicons/red/mstile-70x70.png delete mode 100644 webroot/rsrc/favicons/yellow/apple-touch-icon-114x114.png delete mode 100644 webroot/rsrc/favicons/yellow/apple-touch-icon-120x120.png delete mode 100644 webroot/rsrc/favicons/yellow/apple-touch-icon-144x144.png delete mode 100644 webroot/rsrc/favicons/yellow/apple-touch-icon-152x152.png delete mode 100644 webroot/rsrc/favicons/yellow/apple-touch-icon-57x57.png delete mode 100644 webroot/rsrc/favicons/yellow/apple-touch-icon-60x60.png delete mode 100644 webroot/rsrc/favicons/yellow/apple-touch-icon-72x72.png delete mode 100644 webroot/rsrc/favicons/yellow/apple-touch-icon-76x76.png delete mode 100644 webroot/rsrc/favicons/yellow/favicon-128.png delete mode 100644 webroot/rsrc/favicons/yellow/favicon-16x16.png delete mode 100644 webroot/rsrc/favicons/yellow/favicon-196x196.png delete mode 100644 webroot/rsrc/favicons/yellow/favicon-32x32.png delete mode 100644 webroot/rsrc/favicons/yellow/favicon-96x96.png delete mode 100644 webroot/rsrc/favicons/yellow/favicon.ico delete mode 100644 webroot/rsrc/favicons/yellow/mstile-144x144.png delete mode 100644 webroot/rsrc/favicons/yellow/mstile-150x150.png delete mode 100644 webroot/rsrc/favicons/yellow/mstile-310x150.png delete mode 100644 webroot/rsrc/favicons/yellow/mstile-310x310.png delete mode 100644 webroot/rsrc/favicons/yellow/mstile-70x70.png create mode 100644 webroot/rsrc/js/core/Favicon.js diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 96181688f8..8c4ba2ef05 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -10,7 +10,7 @@ return array( 'conpherence.pkg.css' => 'cea72e09', 'conpherence.pkg.js' => '6249a1cf', 'core.pkg.css' => '46d588e4', - 'core.pkg.js' => '2d9fc958', + 'core.pkg.js' => '035325a7', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => 'e1d704ce', 'differential.pkg.js' => '634399e9', @@ -273,30 +273,13 @@ return array( 'rsrc/favicons/apple-touch-icon-60x60.png' => '8ff52925', 'rsrc/favicons/apple-touch-icon-72x72.png' => 'a2bb65d6', 'rsrc/favicons/apple-touch-icon-76x76.png' => '2d061a11', - 'rsrc/favicons/dark/apple-touch-icon-114x114.png' => 'd0c8978c', - 'rsrc/favicons/dark/apple-touch-icon-120x120.png' => '3a618bc0', - 'rsrc/favicons/dark/apple-touch-icon-144x144.png' => '92c1e188', - 'rsrc/favicons/dark/apple-touch-icon-152x152.png' => '7ce7e469', - 'rsrc/favicons/dark/apple-touch-icon-57x57.png' => 'e3f3f38b', - 'rsrc/favicons/dark/apple-touch-icon-60x60.png' => '1e0dcc72', - 'rsrc/favicons/dark/apple-touch-icon-72x72.png' => '7fb599b6', - 'rsrc/favicons/dark/apple-touch-icon-76x76.png' => '91146961', - 'rsrc/favicons/dark/favicon-128.png' => 'd6ac4346', - 'rsrc/favicons/dark/favicon-16x16.png' => '17434bb0', - 'rsrc/favicons/dark/favicon-196x196.png' => '5e06ee72', - 'rsrc/favicons/dark/favicon-32x32.png' => 'bdd7e16b', - 'rsrc/favicons/dark/favicon-96x96.png' => '0cf55978', - 'rsrc/favicons/dark/favicon.ico' => '4343aaa6', - 'rsrc/favicons/dark/mstile-144x144.png' => '4dc9d42d', - 'rsrc/favicons/dark/mstile-150x150.png' => '2dc61c90', - 'rsrc/favicons/dark/mstile-310x150.png' => '4fe58ab2', - 'rsrc/favicons/dark/mstile-310x310.png' => 'e62c1677', - 'rsrc/favicons/dark/mstile-70x70.png' => '6d1f41b7', 'rsrc/favicons/favicon-128.png' => '72f7e812', 'rsrc/favicons/favicon-16x16.png' => 'fc6275ba', 'rsrc/favicons/favicon-196x196.png' => '95db275e', 'rsrc/favicons/favicon-32x32.png' => '5bd18b6c', 'rsrc/favicons/favicon-96x96.png' => '7242c8e9', + 'rsrc/favicons/favicon-mention.ico' => '1fdd0fa4', + 'rsrc/favicons/favicon-message.ico' => '115bc010', 'rsrc/favicons/favicon.ico' => 'cdb11121', 'rsrc/favicons/mask-icon.svg' => 'e132a80f', 'rsrc/favicons/mstile-144x144.png' => '310c2ee5', @@ -304,44 +287,6 @@ return array( 'rsrc/favicons/mstile-310x150.png' => '4a49d3ee', 'rsrc/favicons/mstile-310x310.png' => 'a52ab264', 'rsrc/favicons/mstile-70x70.png' => '5edce7b8', - 'rsrc/favicons/red/apple-touch-icon-114x114.png' => '91e37d1d', - 'rsrc/favicons/red/apple-touch-icon-120x120.png' => '66687533', - 'rsrc/favicons/red/apple-touch-icon-144x144.png' => 'bc06002c', - 'rsrc/favicons/red/apple-touch-icon-152x152.png' => 'a713de42', - 'rsrc/favicons/red/apple-touch-icon-57x57.png' => '4729688b', - 'rsrc/favicons/red/apple-touch-icon-60x60.png' => '07b9b609', - 'rsrc/favicons/red/apple-touch-icon-72x72.png' => 'b20c3698', - 'rsrc/favicons/red/apple-touch-icon-76x76.png' => 'c6e7dd5c', - 'rsrc/favicons/red/favicon-128.png' => 'e2b2f8fe', - 'rsrc/favicons/red/favicon-16x16.png' => '929fbceb', - 'rsrc/favicons/red/favicon-196x196.png' => '94c089a5', - 'rsrc/favicons/red/favicon-32x32.png' => '5848673e', - 'rsrc/favicons/red/favicon-96x96.png' => '895d54e8', - 'rsrc/favicons/red/favicon.ico' => '25172b6b', - 'rsrc/favicons/red/mstile-144x144.png' => '448639f5', - 'rsrc/favicons/red/mstile-150x150.png' => 'c2ba45d0', - 'rsrc/favicons/red/mstile-310x150.png' => 'b0e50799', - 'rsrc/favicons/red/mstile-310x310.png' => '2475c5a5', - 'rsrc/favicons/red/mstile-70x70.png' => '49b197e8', - 'rsrc/favicons/yellow/apple-touch-icon-114x114.png' => '5271fb3f', - 'rsrc/favicons/yellow/apple-touch-icon-120x120.png' => '6c3d9bf9', - 'rsrc/favicons/yellow/apple-touch-icon-144x144.png' => '6484472c', - 'rsrc/favicons/yellow/apple-touch-icon-152x152.png' => 'e305dda8', - 'rsrc/favicons/yellow/apple-touch-icon-57x57.png' => 'fa6c43d4', - 'rsrc/favicons/yellow/apple-touch-icon-60x60.png' => '2673f162', - 'rsrc/favicons/yellow/apple-touch-icon-72x72.png' => '3ad8020c', - 'rsrc/favicons/yellow/apple-touch-icon-76x76.png' => '58cffd81', - 'rsrc/favicons/yellow/favicon-128.png' => '3b2f8233', - 'rsrc/favicons/yellow/favicon-16x16.png' => 'f3a90518', - 'rsrc/favicons/yellow/favicon-196x196.png' => '932c7c65', - 'rsrc/favicons/yellow/favicon-32x32.png' => '005c9f92', - 'rsrc/favicons/yellow/favicon-96x96.png' => '3ad9bfa9', - 'rsrc/favicons/yellow/favicon.ico' => '2f5b2991', - 'rsrc/favicons/yellow/mstile-144x144.png' => 'fc52335c', - 'rsrc/favicons/yellow/mstile-150x150.png' => '9e556f80', - 'rsrc/favicons/yellow/mstile-310x150.png' => '2c915073', - 'rsrc/favicons/yellow/mstile-310x310.png' => 'ee49978d', - 'rsrc/favicons/yellow/mstile-70x70.png' => '85c7c939', 'rsrc/image/BFCFDA.png' => 'd5ec91f4', 'rsrc/image/actions/edit.png' => '2fc41442', 'rsrc/image/avatar.png' => 'e132bb6a', @@ -430,7 +375,7 @@ return array( 'rsrc/image/texture/table_header_hover.png' => '038ec3b9', 'rsrc/image/texture/table_header_tall.png' => 'd56b434f', 'rsrc/js/application/aphlict/Aphlict.js' => '5359e785', - 'rsrc/js/application/aphlict/behavior-aphlict-dropdown.js' => '49e20786', + 'rsrc/js/application/aphlict/behavior-aphlict-dropdown.js' => '2a171a9d', 'rsrc/js/application/aphlict/behavior-aphlict-listen.js' => 'fb20ac8d', 'rsrc/js/application/aphlict/behavior-aphlict-status.js' => '5e2634b9', 'rsrc/js/application/aphlict/behavior-desktop-notifications-control.js' => 'edd1ba66', @@ -535,6 +480,7 @@ return array( 'rsrc/js/core/Busy.js' => '59a7976a', 'rsrc/js/core/DragAndDropFileUpload.js' => '58dea2fa', 'rsrc/js/core/DraggableList.js' => '5a13c79f', + 'rsrc/js/core/Favicon.js' => '1fe2510c', 'rsrc/js/core/FileUpload.js' => '680ea2c8', 'rsrc/js/core/Hovercard.js' => '1bd28176', 'rsrc/js/core/KeyboardShortcut.js' => '1ae869f2', @@ -544,7 +490,7 @@ return array( 'rsrc/js/core/Prefab.js' => 'cfd23f37', 'rsrc/js/core/ShapedRequest.js' => '7cbe244b', 'rsrc/js/core/TextAreaUtils.js' => '320810c8', - 'rsrc/js/core/Title.js' => 'df5e11d2', + 'rsrc/js/core/Title.js' => '485aaa6c', 'rsrc/js/core/ToolTip.js' => '6323f942', 'rsrc/js/core/behavior-active-nav.js' => 'e379b58e', 'rsrc/js/core/behavior-audio-source.js' => '59b251eb', @@ -653,7 +599,7 @@ return array( 'inline-comment-summary-css' => '51efda3a', 'javelin-aphlict' => '5359e785', 'javelin-behavior' => '61cbc29a', - 'javelin-behavior-aphlict-dropdown' => '49e20786', + 'javelin-behavior-aphlict-dropdown' => '2a171a9d', 'javelin-behavior-aphlict-listen' => 'fb20ac8d', 'javelin-behavior-aphlict-status' => '5e2634b9', 'javelin-behavior-aphront-basic-tokenizer' => 'b3a4b884', @@ -851,6 +797,7 @@ return array( 'phabricator-drag-and-drop-file-upload' => '58dea2fa', 'phabricator-draggable-list' => '5a13c79f', 'phabricator-fatal-config-template-css' => '8f18fa41', + 'phabricator-favicon' => '1fe2510c', 'phabricator-feed-css' => 'ecd4ec57', 'phabricator-file-upload' => '680ea2c8', 'phabricator-filetree-view-css' => 'fccf9f82', @@ -872,7 +819,7 @@ return array( 'phabricator-source-code-view-css' => 'cbeef983', 'phabricator-standard-page-view' => '79176f5a', 'phabricator-textareautils' => '320810c8', - 'phabricator-title' => 'df5e11d2', + 'phabricator-title' => '485aaa6c', 'phabricator-tooltip' => '6323f942', 'phabricator-ui-example-css' => '528b19de', 'phabricator-uiexample-javelin-view' => 'd4a14807', @@ -1144,6 +1091,10 @@ return array( 'javelin-uri', 'javelin-routable', ), + '1fe2510c' => array( + 'javelin-install', + 'javelin-dom', + ), '21df4ff5' => array( 'javelin-install', 'javelin-workboard-card', @@ -1163,6 +1114,17 @@ return array( 'javelin-install', 'javelin-util', ), + '2a171a9d' => array( + 'javelin-behavior', + 'javelin-request', + 'javelin-stratcom', + 'javelin-vector', + 'javelin-dom', + 'javelin-uri', + 'javelin-behavior-device', + 'phabricator-title', + 'phabricator-favicon', + ), '2b8de964' => array( 'javelin-install', 'javelin-util', @@ -1282,6 +1244,9 @@ return array( 'phabricator-drag-and-drop-file-upload', 'phabricator-textareautils', ), + '485aaa6c' => array( + 'javelin-install', + ), '491416b3' => array( 'javelin-behavior', 'javelin-uri', @@ -1292,16 +1257,6 @@ return array( 'javelin-dom', 'javelin-stratcom', ), - '49e20786' => array( - 'javelin-behavior', - 'javelin-request', - 'javelin-stratcom', - 'javelin-vector', - 'javelin-dom', - 'javelin-uri', - 'javelin-behavior-device', - 'phabricator-title', - ), '4a021c10' => array( 'javelin-install', 'javelin-util', @@ -2097,9 +2052,6 @@ return array( 'javelin-typeahead-ondemand-source', 'javelin-dom', ), - 'df5e11d2' => array( - 'javelin-install', - ), 'e0ec7f2f' => array( 'javelin-behavior', 'javelin-dom', diff --git a/src/view/page/PhabricatorBarePageView.php b/src/view/page/PhabricatorBarePageView.php index 82b4d43faf..ca9d432286 100644 --- a/src/view/page/PhabricatorBarePageView.php +++ b/src/view/page/PhabricatorBarePageView.php @@ -111,11 +111,13 @@ class PhabricatorBarePageView extends AphrontPageView { '/rsrc/favicons/apple-touch-icon-152x152.png'), )); - $apple_tag = phutil_tag( - 'meta', + $favicon_tag = phutil_tag( + 'link', array( - 'name' => 'apple-mobile-web-app-status-bar-style', - 'content' => 'black-translucent', + 'id' => 'favicon', + 'rel' => 'shortcut icon', + 'href' => celerity_get_resource_uri( + '/rsrc/favicons/favicon.ico'), )); $referrer_tag = phutil_tag( @@ -146,7 +148,7 @@ class PhabricatorBarePageView extends AphrontPageView { $icon_tag_76, $icon_tag_120, $icon_tag_152, - $apple_tag, + $favicon_tag, $referrer_tag, CelerityStaticResourceResponse::renderInlineScript( $framebust.jsprintf('window.__DEV__=%d;', ($developer ? 1 : 0))), diff --git a/src/view/page/menu/PhabricatorMainMenuView.php b/src/view/page/menu/PhabricatorMainMenuView.php index 7d9fccde5b..4bd09992e4 100644 --- a/src/view/page/menu/PhabricatorMainMenuView.php +++ b/src/view/page/menu/PhabricatorMainMenuView.php @@ -23,6 +23,17 @@ final class PhabricatorMainMenuView extends AphrontView { return $this->controller; } + private function getFaviconURI($type = null) { + switch ($type) { + case 'message': + return celerity_get_resource_uri('/rsrc/favicons/favicon-message.ico'); + case 'mention': + return celerity_get_resource_uri('/rsrc/favicons/favicon-mention.ico'); + default: + return celerity_get_resource_uri('/rsrc/favicons/favicon.ico'); + } + } + public function render() { $viewer = $this->getViewer(); @@ -440,6 +451,9 @@ final class PhabricatorMainMenuView extends AphrontView { 'countType' => $conpherence_data['countType'], 'countNumber' => $message_count_number, 'unreadClass' => 'message-unread', + 'favicon' => $this->getFaviconURI('default'), + 'message_favicon' => $this->getFaviconURI('message'), + 'mention_favicon' => $this->getFaviconURI('mention'), )); $message_notification_dropdown = javelin_tag( @@ -518,6 +532,9 @@ final class PhabricatorMainMenuView extends AphrontView { 'countType' => $notification_data['countType'], 'countNumber' => $count_number, 'unreadClass' => 'alert-unread', + 'favicon' => $this->getFaviconURI('default'), + 'message_favicon' => $this->getFaviconURI('message'), + 'mention_favicon' => $this->getFaviconURI('mention'), )); $notification_dropdown = javelin_tag( @@ -600,6 +617,9 @@ final class PhabricatorMainMenuView extends AphrontView { 'countType' => null, 'countNumber' => null, 'unreadClass' => 'setup-unread', + 'favicon' => $this->getFaviconURI('default'), + 'message_favicon' => $this->getFaviconURI('message'), + 'mention_favicon' => $this->getFaviconURI('mention'), )); $setup_notification_dropdown = javelin_tag( diff --git a/webroot/rsrc/favicons/dark/apple-touch-icon-114x114.png b/webroot/rsrc/favicons/dark/apple-touch-icon-114x114.png deleted file mode 100644 index ddb29bcde226fb14234cd106dc9456880a224e58..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9079 zcmV--BZ%CIP)Z zd3apKmGAFVw>K@8Fh(s~5^l-CFvJ+g1P3r?o8)Ce*f)cX7rX=v_(2|-_wqhIz7P^m zCJAKlIKX5QHe+mT7AGWuVKqw#yKTTYju%-*k}V10RWg#SuJiurTes`ht-Ewf#>u?% zeYbDbIj2sYI<=juu6r*b1X9&$(c07?!*~uCq^fI&64CJhjsy4@0BHtB5>Uy&Fanh@ za}mH!3IY)wU~mTkyBKI8UoCDA# z22TX@7yzQ5vyvd%M4)#7egnYE%v}Gs+L(2qit4m*8!eEnTF%{x0M_C@9vw7#TT_En zvh}2TKEw{*VQ6b^kg2M)U;+q85{WYaTnyk`W|`)oJSoQAqN7!g7AFFTL<4|l0IXt` zuQAZ8SoAXLg(R|+SLe4ukX}(b1DH2w7DQW9gKTTgwKzBKFuASKF|^`w3(HR2sW8&$ zZCXp#^YGD}JX6(aK|)LrBo>3HSxWhWl=6Bhok~>7>lb3)b^b6pTAUe>Ql=dpUI0ah=(aG@DY>Mj(e1bJj@zYM$Es!N2Ct|}3l*=ZPK!2U zh_T@cfmC&E5fM!Va09a(A16&%=j;!9RGvg)9e{h7c^QMdlpT~!bpOI;C)5A1#R07& zSwK&7Sm(%8b!|Bj-N?*0fqA%n3`W4%To^=EmX9|oZ=!Z0TExu1Y;D@Mr_Tx&2#2L2 zQ`NO4M07JV-vV&?;n1P~f%Xv5UCexUYty!lFFbc3JA|4@)2|5=(AwM}E2`2$AGX>Y zoZ@rlz$B5F!Qh1~xw0QS+7}98dXu0Q0^Q0im$WuD^a45iDx2uGvDwM2k0^en69E61 zY>S)YwT=rK(-lu;P2B_l_c6j6w-mh1aZY7XYko`u|IfH~+});+Ci zX%b^iN0gY$EZ=4+$9i~8`6kVj8FcjrZ@%~fFP3sFvwWKQv<{mi8klfMHUv^NwPOH03(0i_FW~=P*CU01pn3q`YHi%MsXxR~Drgfo zFDWz*z;j@(=(|e)9|2JtfNz5N!@h`_J9`#HYg2<{2Gr%o;g74Zjw7N8U|!A46|NlQ z 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 diff --git a/webroot/rsrc/favicons/dark/apple-touch-icon-144x144.png b/webroot/rsrc/favicons/dark/apple-touch-icon-144x144.png deleted file mode 100644 index d2cf49d29357035f82de0ccd691ef5074ddcfb8b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12871 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 S4*G%!;LB$PaFv8n;Qs(Ea0zMv diff --git a/webroot/rsrc/favicons/dark/apple-touch-icon-152x152.png b/webroot/rsrc/favicons/dark/apple-touch-icon-152x152.png deleted file mode 100644 index 41a146e9f38905e2432c28ec0fabd5dd3e3fff35..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15159 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 HB=mm(go?&t diff --git a/webroot/rsrc/favicons/dark/apple-touch-icon-57x57.png b/webroot/rsrc/favicons/dark/apple-touch-icon-57x57.png deleted file mode 100644 index a5141404ceae3d58f8f81b9b461a7c8bae89c757..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3177 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 P00000NkvXXu0mjff2BdN diff --git a/webroot/rsrc/favicons/dark/apple-touch-icon-60x60.png b/webroot/rsrc/favicons/dark/apple-touch-icon-60x60.png deleted file mode 100644 index c8dc8def049d97669f9017617be4be7a1c8d3b85..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3213 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 diff --git a/webroot/rsrc/favicons/dark/favicon-16x16.png b/webroot/rsrc/favicons/dark/favicon-16x16.png deleted file mode 100644 index e98657cc57dcb02930a3e46e6d597a83175051d5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 603 zcmV-h0;K(kP)>H8mYU6fy;Mvnjd%FfeYGs8p=c8P_e3e46h$KlI@cRe`dg<=!GLs0mv^kZ# zfUN6j5Q0E&4*=()ao)X8@LAVcUtj0g(E$L49u711XdHlQwdRzqH8~s(%S~9$jc!va zm2EoUQZ?PBY9ewWE)2tH)V!QbiqmH!B0hLUd@B@eT14ZQObzr+{GKrP;)jpS&b_6z zbu)gSm%SbjkH^PspJg&x%9RS~bjEZ||ItQ(7ZXqL`@A;tm#?ez96W?!7`8Ub_1PY^ zf`4voAcWxF{UNU3yiKiUP^(pKWMEWJeOi+0I+&vz1W*(OpSPP=)2{&t1p}< pO!6EsP|WAb|Gi97bM#-=`4_T+``Gz7h%NvC002ovPDHLkV1k*h6(9fr diff --git a/webroot/rsrc/favicons/dark/favicon-196x196.png b/webroot/rsrc/favicons/dark/favicon-196x196.png deleted file mode 100644 index 78cbe52e78d00895ce3788f041a9e517368ba525..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25430 zcmXt918}5G*PU=;+u3YvZ0u~fv2E_ewy_%<8*Oacwv)}owr%}+zpB6LnP;k|rnQ`%{R^aGU3u&PHP#~>J>rV%$`K(lH-W1bWCH{@H9Np`0kslLD;Wyh!I zGiAwl1a1{`zk1ahKt=k2XP-Y+cbD$nJ(u?EUqwO|d=rrpXoamf9C#i^Sx~aBAPByb4{(I7w zVu5Te5%lFjaLT?_^^hVj{jZIl9#2cF%tDf=RM-h2AW^;$k{hnu8StW+aK6b`ELG_z zkCjxF2uz5|tCH`Mp`LR)D7wYT z-vXX$a=L2nv`pFt;IN>v?T<`sdn|uc{DgB#ZHrib=DhvHR{0WQJlo~Lh8=g^ES+0| z$IJ!iTgyy-q)iK)=A}QkN1bMOLk&*GIE9Jb%0Bogx*3(FS5aFP+pL;m&6;a2o3W7B z+`*}6NPk2*gV-Ty0vzaY zmtD|9o7tQ|Q`nv3KNAb!6AB~MWQdWAc?LO9-E}zZijU4x$q}YoYtX7SG(mQ{Ew>@_ z7>kOg2EeQsb~3{DoD1pTBwRD5(PE6{ zp=V07OiVB5&wpgGc)XOX$gXec0FwW3bj;>2nf`sHaZDO5c(rf*-IKJ28bX2qf-w05 z#7oeJH$37@aC-y?kE|%19iPtxM5_JhtY4*NKpX8W;|n@Lx%)-e*tfS*>K*x#{6Mgf zP0vhd=XZv374FQFq4fkY_rQf*nm?)5%9c(W;S|=%-`1)})oQYN#S*dK*v?Dcc&t~v zzubye7}G3r;i=RQm9(JJN?U zW&*;!+IwiYO|5|gXl6;n zpD_<3=O}Pb%Djnun$qOm-|Ij{XwQMeiw_A~4;r(BcPDoV!=q!kjHu+x&pCp8mej~V zrV>33HwCei!5M&{jjWTKaP*Cp2gB8(fi}=yvm^?d6vV#hYjQjE97S2eTtOHE@mR__ zLHLq<$NA$Au*6yy`0@(k-YoFk%c7m8_NpJxZE(M0lJzfud-+n`9=S2J%*Xt7EBtGP z3tOMIuCYxkdquJG>qqunth+^2l1!F5LJC0w6Tq-^kgL!@m;_-)wSbH1;vC9{tp3_p z+&$24D|bL!`bzURL*M{%47~|e5kNuq#i*_PpdB?cJF;2Lq~@) zG#m}V)G`FC{C~F1lv`C|V#KdzcP_mHc;$)wx}YdmA%8>-29I+iVPBH(W23jhClaSi z7z#udOHK&O3v?1XedwFPqI}MXl)zIQ(+u~n>6!u0uqJw)OPX5lqPs*`ULGP@QWG2i zmFyoj5!yF1lRtIk;%`Nf3_LK!ZH1Q9NjBH2J#$z(2C6gTW2IOf2!r!8QBa>J6ykB> zbD_=4+D>t-=Gc0}yaEm~rD)Lt^T4p=K4oqVg`4vLR-FV~`&Qmohn+*q6X`nxmE+@c ztHXSU&~N)}pz&rr-K8%L6{?aE?|us3%>LH-#p*q!YPQo&eRsY%OSaJi!klW8=qFQO|eb z6KsQto4RkdDbl8C@mIn8h21~Dt&=%hFjLa%>2Yf&u7wV&x^75zYFRP%)L&O8$YoCYGp?)X*!sHWDNCe3deQfp?;CqSLaF$um4uqf|}@Q zntt}a51D}ST79ggyk;TK&!(jYso>If$`dTqY9tb^Vrq7aOfhyXzeUTS#tokmEcSC_ zX)P~FIo)dQF-sa~ZcjmtU#6LBW7RosKX@r6(aJIOWub;5~GDk?Y1yWiKJ9 zXmp)@@1pGbp~~c;8S!u#%RjMNlDf;34tC353(`7yvR zoGwkb?Myhav^=HUE< z=&DrbO$>yf9UGsXR=-sNzn2VB~~z4AdnNku(Dg^Od7bh@Ot z5YuKQP6#oHkZrKY2P?#SC~{}L_(5E}pdK{@2`uw*OgE~C!MIK!eeSM^5H^R}HJZvI zThr*8`Wh!>hO)mr0T*FHmUWaKVz8YJhnJO{>LYtHK- z1m)S9-2FhC_iXse{Hz|VBT|x0SX)3BY2~YCB

ls}aW;&NT|8N)7~iCMQWdOWBy* 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 Z{{Y4upe_)5&uIVv002ovPDHLkV1i$XRrUY? diff --git a/webroot/rsrc/favicons/dark/favicon-96x96.png b/webroot/rsrc/favicons/dark/favicon-96x96.png deleted file mode 100644 index 9cc09dd8334c85649b18d788e4c448084233abd6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5921 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 Dsn8!2 diff --git a/webroot/rsrc/favicons/dark/favicon.ico b/webroot/rsrc/favicons/dark/favicon.ico deleted file mode 100644 index ce3d048bd41840d90fa9eca729a83f01ec4aa3df..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34494 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 F|9_d!XK(-j diff --git a/webroot/rsrc/favicons/dark/mstile-144x144.png b/webroot/rsrc/favicons/dark/mstile-144x144.png deleted file mode 100644 index d2cf49d29357035f82de0ccd691ef5074ddcfb8b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12871 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 S4*G%!;LB$PaFv8n;Qs(Ea0zMv diff --git a/webroot/rsrc/favicons/dark/mstile-150x150.png b/webroot/rsrc/favicons/dark/mstile-150x150.png deleted file mode 100644 index b56a239e029c0418440161909540b007058e12c9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45458 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