diff --git a/.arclint b/.arclint index 1b5b63976a..6047e2ccb6 100644 --- a/.arclint +++ b/.arclint @@ -61,7 +61,19 @@ "type": "spelling" }, "text": { - "type": "text" + "type": "text", + "exclude": [ + "(^src/(.*/)?__tests__/[^/]+/.*\\.(txt|json))" + ] + }, + "text-without-length": { + "type": "text", + "include": [ + "(^src/(.*/)?__tests__/[^/]+/.*\\.(txt|json))" + ], + "severity": { + "3": "disabled" + } }, "xhpast": { "type": "xhpast", diff --git a/bin/nuance b/bin/nuance new file mode 120000 index 0000000000..c2cf50a211 --- /dev/null +++ b/bin/nuance @@ -0,0 +1 @@ +../scripts/setup/manage_nuance.php \ No newline at end of file diff --git a/resources/celerity/map.php b/resources/celerity/map.php index be8d5b9d1c..fdc6357f60 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,10 +7,10 @@ */ return array( 'names' => array( - 'core.pkg.css' => 'dd1447be', + 'core.pkg.css' => '9c8e888d', 'core.pkg.js' => '7d8faf57', 'darkconsole.pkg.js' => 'e7393ebb', - 'differential.pkg.css' => '2de124c9', + 'differential.pkg.css' => '7d0a63a7', 'differential.pkg.js' => 'd0cd0df6', 'diffusion.pkg.css' => 'f45955ed', 'diffusion.pkg.js' => '3a9a8bfa', @@ -59,7 +59,7 @@ return array( 'rsrc/css/application/differential/add-comment.css' => 'c47f8c40', 'rsrc/css/application/differential/changeset-view.css' => 'b6b0d1bb', 'rsrc/css/application/differential/core.css' => '7ac3cabc', - 'rsrc/css/application/differential/phui-inline-comment.css' => '0fdb3667', + 'rsrc/css/application/differential/phui-inline-comment.css' => '5953c28e', 'rsrc/css/application/differential/revision-comment.css' => '14b8565a', 'rsrc/css/application/differential/revision-history.css' => '0e8eb855', 'rsrc/css/application/differential/revision-list.css' => 'f3c47d33', @@ -72,7 +72,7 @@ return array( 'rsrc/css/application/flag/flag.css' => '5337623f', 'rsrc/css/application/harbormaster/harbormaster.css' => '834879db', 'rsrc/css/application/herald/herald-test.css' => 'a52e323e', - 'rsrc/css/application/herald/herald.css' => '46596280', + 'rsrc/css/application/herald/herald.css' => 'dc31f6e9', 'rsrc/css/application/maniphest/batch-editor.css' => 'b0f0b6d5', 'rsrc/css/application/maniphest/report.css' => '9b9580b7', 'rsrc/css/application/maniphest/task-edit.css' => 'fda62a9b', @@ -111,7 +111,7 @@ return array( 'rsrc/css/font/font-aleo.css' => '8bdb2835', 'rsrc/css/font/font-awesome.css' => 'c43323c5', 'rsrc/css/font/font-lato.css' => 'c7ccd872', - 'rsrc/css/font/phui-font-icon-base.css' => 'ecbbb4c2', + 'rsrc/css/font/phui-font-icon-base.css' => '6449bce8', 'rsrc/css/layout/phabricator-filetree-view.css' => 'fccf9f82', 'rsrc/css/layout/phabricator-side-menu-view.css' => '3a3d9f41', 'rsrc/css/layout/phabricator-source-code-view.css' => 'cbeef983', @@ -123,10 +123,11 @@ return array( 'rsrc/css/phui/phui-action-panel.css' => '91c7b835', 'rsrc/css/phui/phui-badge.css' => 'f25c3476', 'rsrc/css/phui/phui-big-info-view.css' => 'bd903741', - 'rsrc/css/phui/phui-box.css' => 'c9e01148', + 'rsrc/css/phui/phui-box.css' => '3830ab21', 'rsrc/css/phui/phui-button.css' => 'a64a8de6', 'rsrc/css/phui/phui-chart.css' => '6bf6f78e', 'rsrc/css/phui/phui-crumbs-view.css' => '79d536e5', + 'rsrc/css/phui/phui-curtain-view.css' => '7148ae25', 'rsrc/css/phui/phui-document-pro.css' => '92d5b648', 'rsrc/css/phui/phui-document-summary.css' => '9ca48bdf', 'rsrc/css/phui/phui-document.css' => '9c71d2bf', @@ -134,8 +135,8 @@ return array( 'rsrc/css/phui/phui-fontkit.css' => '9cda225e', 'rsrc/css/phui/phui-form-view.css' => '4a1a0f5e', 'rsrc/css/phui/phui-form.css' => 'aac1d51d', - 'rsrc/css/phui/phui-head-thing.css' => '11731da0', - 'rsrc/css/phui/phui-header-view.css' => 'fc4acf14', + 'rsrc/css/phui/phui-head-thing.css' => '31638812', + 'rsrc/css/phui/phui-header-view.css' => '26cffd3d', 'rsrc/css/phui/phui-hovercard.css' => 'de1a2119', 'rsrc/css/phui/phui-icon-set-selector.css' => '1ab67aad', 'rsrc/css/phui/phui-icon.css' => '3f33ab57', @@ -148,14 +149,14 @@ return array( 'rsrc/css/phui/phui-pager.css' => 'bea33d23', 'rsrc/css/phui/phui-pinboard-view.css' => '2495140e', 'rsrc/css/phui/phui-profile-menu.css' => '7e92a89a', - 'rsrc/css/phui/phui-property-list-view.css' => '27b2849e', + 'rsrc/css/phui/phui-property-list-view.css' => 'b12e801c', 'rsrc/css/phui/phui-remarkup-preview.css' => '1a8f2591', 'rsrc/css/phui/phui-segment-bar-view.css' => '46342871', 'rsrc/css/phui/phui-spacing.css' => '042804d6', 'rsrc/css/phui/phui-status.css' => '37309046', - 'rsrc/css/phui/phui-tag-view.css' => '9d5d4400', - 'rsrc/css/phui/phui-timeline-view.css' => '2efceff8', - 'rsrc/css/phui/phui-two-column-view.css' => 'd0ad8c10', + 'rsrc/css/phui/phui-tag-view.css' => '6bbd83e2', + 'rsrc/css/phui/phui-timeline-view.css' => 'a0173eba', + 'rsrc/css/phui/phui-two-column-view.css' => 'e6bf86b6', 'rsrc/css/phui/workboards/phui-workboard-color.css' => 'ac6fe6a7', 'rsrc/css/phui/workboards/phui-workboard.css' => 'e6d89647', 'rsrc/css/phui/workboards/phui-workcard.css' => '3646fb96', @@ -272,6 +273,7 @@ return array( 'rsrc/image/checker_dark.png' => 'd8e65881', 'rsrc/image/checker_light.png' => 'a0155918', 'rsrc/image/checker_lighter.png' => 'd5da91b6', + 'rsrc/image/d5d8e1.png' => '0c2a1497', 'rsrc/image/darkload.gif' => '1ffd3ec6', 'rsrc/image/divot.png' => '94dded62', 'rsrc/image/examples/hero.png' => '979a86ae', @@ -419,7 +421,7 @@ return array( 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '5e9f347c', 'rsrc/js/application/projects/WorkboardBoard.js' => '52291776', 'rsrc/js/application/projects/WorkboardCard.js' => 'c587b80f', - 'rsrc/js/application/projects/WorkboardColumn.js' => 'f05d6e5d', + 'rsrc/js/application/projects/WorkboardColumn.js' => 'bae58312', 'rsrc/js/application/projects/WorkboardController.js' => '55baf5ed', 'rsrc/js/application/projects/behavior-project-boards.js' => '14a1faae', 'rsrc/js/application/projects/behavior-project-create.js' => '065227cc', @@ -560,7 +562,7 @@ return array( 'font-lato' => 'c7ccd872', 'global-drag-and-drop-css' => '5c1b47c2', 'harbormaster-css' => '834879db', - 'herald-css' => '46596280', + 'herald-css' => 'dc31f6e9', 'herald-rule-editor' => '746ca158', 'herald-test-css' => 'a52e323e', 'inline-comment-summary-css' => '51efda3a', @@ -730,7 +732,7 @@ return array( 'javelin-websocket' => 'e292eaf4', 'javelin-workboard-board' => '52291776', 'javelin-workboard-card' => 'c587b80f', - 'javelin-workboard-column' => 'f05d6e5d', + 'javelin-workboard-column' => 'bae58312', 'javelin-workboard-controller' => '55baf5ed', 'javelin-workflow' => '5b2e3e2b', 'lightbox-attachment-css' => '7acac05d', @@ -803,7 +805,7 @@ return array( 'phui-action-panel-css' => '91c7b835', 'phui-badge-view-css' => 'f25c3476', 'phui-big-info-view-css' => 'bd903741', - 'phui-box-css' => 'c9e01148', + 'phui-box-css' => '3830ab21', 'phui-button-css' => 'a64a8de6', 'phui-calendar-css' => 'ccabe893', 'phui-calendar-day-css' => 'd1cf6f93', @@ -811,16 +813,17 @@ return array( 'phui-calendar-month-css' => '476be7e0', 'phui-chart-css' => '6bf6f78e', 'phui-crumbs-view-css' => '79d536e5', + 'phui-curtain-view-css' => '7148ae25', 'phui-document-summary-view-css' => '9ca48bdf', 'phui-document-view-css' => '9c71d2bf', 'phui-document-view-pro-css' => '92d5b648', 'phui-feed-story-css' => '04aec08f', - 'phui-font-icon-base-css' => 'ecbbb4c2', + 'phui-font-icon-base-css' => '6449bce8', 'phui-fontkit-css' => '9cda225e', 'phui-form-css' => 'aac1d51d', 'phui-form-view-css' => '4a1a0f5e', - 'phui-head-thing-view-css' => '11731da0', - 'phui-header-view-css' => 'fc4acf14', + 'phui-head-thing-view-css' => '31638812', + 'phui-header-view-css' => '26cffd3d', 'phui-hovercard' => '1bd28176', 'phui-hovercard-view-css' => 'de1a2119', 'phui-icon-set-selector-css' => '1ab67aad', @@ -828,22 +831,22 @@ return array( 'phui-image-mask-css' => 'a8498f9c', 'phui-info-panel-css' => '27ea50a1', 'phui-info-view-css' => '6d7c3509', - 'phui-inline-comment-view-css' => '0fdb3667', + 'phui-inline-comment-view-css' => '5953c28e', 'phui-list-view-css' => '9da2aa00', 'phui-object-box-css' => '91628842', 'phui-object-item-list-view-css' => '18b2ce8e', 'phui-pager-css' => 'bea33d23', 'phui-pinboard-view-css' => '2495140e', 'phui-profile-menu-css' => '7e92a89a', - 'phui-property-list-view-css' => '27b2849e', + 'phui-property-list-view-css' => 'b12e801c', 'phui-remarkup-preview-css' => '1a8f2591', 'phui-segment-bar-view-css' => '46342871', 'phui-spacing-css' => '042804d6', 'phui-status-list-view-css' => '37309046', - 'phui-tag-view-css' => '9d5d4400', + 'phui-tag-view-css' => '6bbd83e2', 'phui-theme-css' => '027ba77e', - 'phui-timeline-view-css' => '2efceff8', - 'phui-two-column-view-css' => 'd0ad8c10', + 'phui-timeline-view-css' => 'a0173eba', + 'phui-two-column-view-css' => 'e6bf86b6', 'phui-workboard-color-css' => 'ac6fe6a7', 'phui-workboard-view-css' => 'e6d89647', 'phui-workcard-view-css' => '3646fb96', @@ -1791,6 +1794,10 @@ return array( 'b6b0d1bb' => array( 'phui-inline-comment-view-css', ), + 'bae58312' => array( + 'javelin-install', + 'javelin-workboard-card', + ), 'bcaccd64' => array( 'javelin-behavior', 'javelin-behavior-device', @@ -2059,10 +2066,6 @@ return array( 'javelin-workflow', 'javelin-json', ), - 'f05d6e5d' => array( - 'javelin-install', - 'javelin-workboard-card', - ), 'f411b6ae' => array( 'javelin-behavior', 'javelin-stratcom', diff --git a/resources/sql/autopatches/20160308.nuance.01.disabled.sql b/resources/sql/autopatches/20160308.nuance.01.disabled.sql new file mode 100644 index 0000000000..f9a6d11320 --- /dev/null +++ b/resources/sql/autopatches/20160308.nuance.01.disabled.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_nuance.nuance_source + ADD isDisabled BOOL NOT NULL; diff --git a/resources/sql/autopatches/20160308.nuance.02.cursordata.sql b/resources/sql/autopatches/20160308.nuance.02.cursordata.sql new file mode 100644 index 0000000000..a3ac917f0c --- /dev/null +++ b/resources/sql/autopatches/20160308.nuance.02.cursordata.sql @@ -0,0 +1,12 @@ +CREATE TABLE {$NAMESPACE}_nuance.nuance_importcursordata ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + sourcePHID VARBINARY(64) NOT NULL, + cursorKey VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT}, + cursorType VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT}, + properties LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (phid), + UNIQUE KEY `key_source` (sourcePHID, cursorKey) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20160308.nuance.03.sourcen.sql b/resources/sql/autopatches/20160308.nuance.03.sourcen.sql new file mode 100644 index 0000000000..42ec4b87eb --- /dev/null +++ b/resources/sql/autopatches/20160308.nuance.03.sourcen.sql @@ -0,0 +1,7 @@ +CREATE TABLE {$NAMESPACE}_nuance.nuance_sourcename_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/resources/sql/autopatches/20160308.nuance.04.sourcei.php b/resources/sql/autopatches/20160308.nuance.04.sourcei.php new file mode 100644 index 0000000000..eb0d1da113 --- /dev/null +++ b/resources/sql/autopatches/20160308.nuance.04.sourcei.php @@ -0,0 +1,11 @@ +getPHID(), + array( + 'force' => true, + )); +} diff --git a/resources/sql/autopatches/20160308.nuance.05.sourcename.sql b/resources/sql/autopatches/20160308.nuance.05.sourcename.sql new file mode 100644 index 0000000000..a2b70c683f --- /dev/null +++ b/resources/sql/autopatches/20160308.nuance.05.sourcename.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_nuance.nuance_source + CHANGE name name VARCHAR(255) NOT NULL COLLATE {$COLLATE_SORT}; diff --git a/resources/sql/autopatches/20160308.nuance.06.label.sql b/resources/sql/autopatches/20160308.nuance.06.label.sql new file mode 100644 index 0000000000..4ba67ed3e4 --- /dev/null +++ b/resources/sql/autopatches/20160308.nuance.06.label.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_nuance.nuance_item + DROP sourceLabel; diff --git a/resources/sql/autopatches/20160308.nuance.07.itemtype.sql b/resources/sql/autopatches/20160308.nuance.07.itemtype.sql new file mode 100644 index 0000000000..d34b5c77e9 --- /dev/null +++ b/resources/sql/autopatches/20160308.nuance.07.itemtype.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_nuance.nuance_item + ADD itemType VARCHAR(64) NOT NULL COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20160308.nuance.08.itemkey.sql b/resources/sql/autopatches/20160308.nuance.08.itemkey.sql new file mode 100644 index 0000000000..12b6a88673 --- /dev/null +++ b/resources/sql/autopatches/20160308.nuance.08.itemkey.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_nuance.nuance_item + ADD itemKey VARCHAR(64) NOT NULL COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20160308.nuance.09.itemcontainer.sql b/resources/sql/autopatches/20160308.nuance.09.itemcontainer.sql new file mode 100644 index 0000000000..0b76c76827 --- /dev/null +++ b/resources/sql/autopatches/20160308.nuance.09.itemcontainer.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_nuance.nuance_item + ADD itemContainerKey VARCHAR(64) COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20160308.nuance.10.itemkeyu.sql b/resources/sql/autopatches/20160308.nuance.10.itemkeyu.sql new file mode 100644 index 0000000000..b455ada156 --- /dev/null +++ b/resources/sql/autopatches/20160308.nuance.10.itemkeyu.sql @@ -0,0 +1,2 @@ +UPDATE {$NAMESPACE}_nuance.nuance_item + SET itemKey = id WHERE itemKey = ''; diff --git a/resources/sql/autopatches/20160308.nuance.11.requestor.sql b/resources/sql/autopatches/20160308.nuance.11.requestor.sql new file mode 100644 index 0000000000..590b44197a --- /dev/null +++ b/resources/sql/autopatches/20160308.nuance.11.requestor.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_nuance.nuance_item + CHANGE requestorPHID requestorPHID VARBINARY(64); diff --git a/resources/sql/autopatches/20160308.nuance.12.queue.sql b/resources/sql/autopatches/20160308.nuance.12.queue.sql new file mode 100644 index 0000000000..bb0554b57d --- /dev/null +++ b/resources/sql/autopatches/20160308.nuance.12.queue.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_nuance.nuance_item + CHANGE queuePHID queuePHID VARBINARY(64); diff --git a/scripts/setup/manage_nuance.php b/scripts/setup/manage_nuance.php new file mode 100755 index 0000000000..ebf312305a --- /dev/null +++ b/scripts/setup/manage_nuance.php @@ -0,0 +1,21 @@ +#!/usr/bin/env php +setTagline(pht('manage Nuance')); +$args->setSynopsis(<<parseStandardArguments(); + +$workflows = id(new PhutilClassMapQuery()) + ->setAncestorClass('NuanceManagementWorkflow') + ->execute(); +$workflows[] = new PhutilHelpArgumentWorkflow(); +$args->parseWorkflows($workflows); diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 0a5b5e7cd9..bfad24494a 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -840,6 +840,8 @@ phutil_register_library_map(array( 'DoorkeeperAsanaRemarkupRule' => 'applications/doorkeeper/remarkup/DoorkeeperAsanaRemarkupRule.php', 'DoorkeeperBridge' => 'applications/doorkeeper/bridge/DoorkeeperBridge.php', 'DoorkeeperBridgeAsana' => 'applications/doorkeeper/bridge/DoorkeeperBridgeAsana.php', + 'DoorkeeperBridgeGitHub' => 'applications/doorkeeper/bridge/DoorkeeperBridgeGitHub.php', + 'DoorkeeperBridgeGitHubIssue' => 'applications/doorkeeper/bridge/DoorkeeperBridgeGitHubIssue.php', 'DoorkeeperBridgeJIRA' => 'applications/doorkeeper/bridge/DoorkeeperBridgeJIRA.php', 'DoorkeeperBridgeJIRATestCase' => 'applications/doorkeeper/bridge/__tests__/DoorkeeperBridgeJIRATestCase.php', 'DoorkeeperDAO' => 'applications/doorkeeper/storage/DoorkeeperDAO.php', @@ -886,7 +888,7 @@ phutil_register_library_map(array( 'DrydockBlueprintTransactionQuery' => 'applications/drydock/query/DrydockBlueprintTransactionQuery.php', 'DrydockBlueprintViewController' => 'applications/drydock/controller/DrydockBlueprintViewController.php', 'DrydockCommand' => 'applications/drydock/storage/DrydockCommand.php', - 'DrydockCommandError' => 'applications/drydock/DrydockCommandError/DrydockCommandError.php', + 'DrydockCommandError' => 'applications/drydock/exception/DrydockCommandError.php', 'DrydockCommandInterface' => 'applications/drydock/interface/command/DrydockCommandInterface.php', 'DrydockCommandQuery' => 'applications/drydock/query/DrydockCommandQuery.php', 'DrydockConsoleController' => 'applications/drydock/controller/DrydockConsoleController.php', @@ -1419,22 +1421,42 @@ phutil_register_library_map(array( 'NuanceConduitAPIMethod' => 'applications/nuance/conduit/NuanceConduitAPIMethod.php', 'NuanceConsoleController' => 'applications/nuance/controller/NuanceConsoleController.php', 'NuanceController' => 'applications/nuance/controller/NuanceController.php', - 'NuanceCreateItemConduitAPIMethod' => 'applications/nuance/conduit/NuanceCreateItemConduitAPIMethod.php', 'NuanceDAO' => 'applications/nuance/storage/NuanceDAO.php', + 'NuanceGitHubEventItemType' => 'applications/nuance/item/NuanceGitHubEventItemType.php', + 'NuanceGitHubImportCursor' => 'applications/nuance/cursor/NuanceGitHubImportCursor.php', + 'NuanceGitHubIssuesImportCursor' => 'applications/nuance/cursor/NuanceGitHubIssuesImportCursor.php', + 'NuanceGitHubRawEvent' => 'applications/nuance/github/NuanceGitHubRawEvent.php', + 'NuanceGitHubRawEventTestCase' => 'applications/nuance/github/__tests__/NuanceGitHubRawEventTestCase.php', + 'NuanceGitHubRepositoryImportCursor' => 'applications/nuance/cursor/NuanceGitHubRepositoryImportCursor.php', + 'NuanceGitHubRepositorySourceDefinition' => 'applications/nuance/source/NuanceGitHubRepositorySourceDefinition.php', + 'NuanceImportCursor' => 'applications/nuance/cursor/NuanceImportCursor.php', + 'NuanceImportCursorData' => 'applications/nuance/storage/NuanceImportCursorData.php', + 'NuanceImportCursorDataQuery' => 'applications/nuance/query/NuanceImportCursorDataQuery.php', + 'NuanceImportCursorPHIDType' => 'applications/nuance/phid/NuanceImportCursorPHIDType.php', 'NuanceItem' => 'applications/nuance/storage/NuanceItem.php', - 'NuanceItemEditController' => 'applications/nuance/controller/NuanceItemEditController.php', + 'NuanceItemController' => 'applications/nuance/controller/NuanceItemController.php', 'NuanceItemEditor' => 'applications/nuance/editor/NuanceItemEditor.php', + 'NuanceItemListController' => 'applications/nuance/controller/NuanceItemListController.php', + 'NuanceItemManageController' => 'applications/nuance/controller/NuanceItemManageController.php', 'NuanceItemPHIDType' => 'applications/nuance/phid/NuanceItemPHIDType.php', 'NuanceItemQuery' => 'applications/nuance/query/NuanceItemQuery.php', + 'NuanceItemSearchEngine' => 'applications/nuance/query/NuanceItemSearchEngine.php', 'NuanceItemTransaction' => 'applications/nuance/storage/NuanceItemTransaction.php', 'NuanceItemTransactionComment' => 'applications/nuance/storage/NuanceItemTransactionComment.php', 'NuanceItemTransactionQuery' => 'applications/nuance/query/NuanceItemTransactionQuery.php', + 'NuanceItemType' => 'applications/nuance/item/NuanceItemType.php', + 'NuanceItemUpdateWorker' => 'applications/nuance/worker/NuanceItemUpdateWorker.php', 'NuanceItemViewController' => 'applications/nuance/controller/NuanceItemViewController.php', + 'NuanceManagementImportWorkflow' => 'applications/nuance/management/NuanceManagementImportWorkflow.php', + 'NuanceManagementUpdateWorkflow' => 'applications/nuance/management/NuanceManagementUpdateWorkflow.php', + 'NuanceManagementWorkflow' => 'applications/nuance/management/NuanceManagementWorkflow.php', 'NuancePhabricatorFormSourceDefinition' => 'applications/nuance/source/NuancePhabricatorFormSourceDefinition.php', 'NuanceQuery' => 'applications/nuance/query/NuanceQuery.php', 'NuanceQueue' => 'applications/nuance/storage/NuanceQueue.php', + 'NuanceQueueController' => 'applications/nuance/controller/NuanceQueueController.php', 'NuanceQueueDatasource' => 'applications/nuance/typeahead/NuanceQueueDatasource.php', 'NuanceQueueEditController' => 'applications/nuance/controller/NuanceQueueEditController.php', + 'NuanceQueueEditEngine' => 'applications/nuance/editor/NuanceQueueEditEngine.php', 'NuanceQueueEditor' => 'applications/nuance/editor/NuanceQueueEditor.php', 'NuanceQueueListController' => 'applications/nuance/controller/NuanceQueueListController.php', 'NuanceQueuePHIDType' => 'applications/nuance/phid/NuanceQueuePHIDType.php', @@ -1457,15 +1479,17 @@ phutil_register_library_map(array( 'NuanceSchemaSpec' => 'applications/nuance/storage/NuanceSchemaSpec.php', 'NuanceSource' => 'applications/nuance/storage/NuanceSource.php', 'NuanceSourceActionController' => 'applications/nuance/controller/NuanceSourceActionController.php', - 'NuanceSourceCreateController' => 'applications/nuance/controller/NuanceSourceCreateController.php', + 'NuanceSourceController' => 'applications/nuance/controller/NuanceSourceController.php', 'NuanceSourceDefaultEditCapability' => 'applications/nuance/capability/NuanceSourceDefaultEditCapability.php', 'NuanceSourceDefaultViewCapability' => 'applications/nuance/capability/NuanceSourceDefaultViewCapability.php', 'NuanceSourceDefinition' => 'applications/nuance/source/NuanceSourceDefinition.php', 'NuanceSourceDefinitionTestCase' => 'applications/nuance/source/__tests__/NuanceSourceDefinitionTestCase.php', 'NuanceSourceEditController' => 'applications/nuance/controller/NuanceSourceEditController.php', + 'NuanceSourceEditEngine' => 'applications/nuance/editor/NuanceSourceEditEngine.php', 'NuanceSourceEditor' => 'applications/nuance/editor/NuanceSourceEditor.php', 'NuanceSourceListController' => 'applications/nuance/controller/NuanceSourceListController.php', 'NuanceSourceManageCapability' => 'applications/nuance/capability/NuanceSourceManageCapability.php', + 'NuanceSourceNameNgrams' => 'applications/nuance/storage/NuanceSourceNameNgrams.php', 'NuanceSourcePHIDType' => 'applications/nuance/phid/NuanceSourcePHIDType.php', 'NuanceSourceQuery' => 'applications/nuance/query/NuanceSourceQuery.php', 'NuanceSourceSearchEngine' => 'applications/nuance/query/NuanceSourceSearchEngine.php', @@ -1474,6 +1498,7 @@ phutil_register_library_map(array( 'NuanceSourceTransactionQuery' => 'applications/nuance/query/NuanceSourceTransactionQuery.php', 'NuanceSourceViewController' => 'applications/nuance/controller/NuanceSourceViewController.php', 'NuanceTransaction' => 'applications/nuance/storage/NuanceTransaction.php', + 'NuanceWorker' => 'applications/nuance/worker/NuanceWorker.php', 'OwnersConduitAPIMethod' => 'applications/owners/conduit/OwnersConduitAPIMethod.php', 'OwnersEditConduitAPIMethod' => 'applications/owners/conduit/OwnersEditConduitAPIMethod.php', 'OwnersPackageReplyHandler' => 'applications/owners/mail/OwnersPackageReplyHandler.php', @@ -1505,6 +1530,9 @@ phutil_register_library_map(array( 'PHUIColorPalletteExample' => 'applications/uiexample/examples/PHUIColorPalletteExample.php', 'PHUICrumbView' => 'view/phui/PHUICrumbView.php', 'PHUICrumbsView' => 'view/phui/PHUICrumbsView.php', + 'PHUICurtainExtension' => 'view/extension/PHUICurtainExtension.php', + 'PHUICurtainPanelView' => 'view/layout/PHUICurtainPanelView.php', + 'PHUICurtainView' => 'view/layout/PHUICurtainView.php', 'PHUIDiffInlineCommentDetailView' => 'infrastructure/diff/view/PHUIDiffInlineCommentDetailView.php', 'PHUIDiffInlineCommentEditView' => 'infrastructure/diff/view/PHUIDiffInlineCommentEditView.php', 'PHUIDiffInlineCommentRowScaffold' => 'infrastructure/diff/view/PHUIDiffInlineCommentRowScaffold.php', @@ -3011,6 +3039,7 @@ phutil_register_library_map(array( 'PhabricatorProjectWatcherListView' => 'applications/project/view/PhabricatorProjectWatcherListView.php', 'PhabricatorProjectWorkboardBackgroundColor' => 'applications/project/constants/PhabricatorProjectWorkboardBackgroundColor.php', 'PhabricatorProjectWorkboardProfilePanel' => 'applications/project/profilepanel/PhabricatorProjectWorkboardProfilePanel.php', + 'PhabricatorProjectsCurtainExtension' => 'applications/project/engineextension/PhabricatorProjectsCurtainExtension.php', 'PhabricatorProjectsEditEngineExtension' => 'applications/project/engineextension/PhabricatorProjectsEditEngineExtension.php', 'PhabricatorProjectsEditField' => 'applications/transactions/editfield/PhabricatorProjectsEditField.php', 'PhabricatorProjectsFulltextEngineExtension' => 'applications/project/engineextension/PhabricatorProjectsFulltextEngineExtension.php', @@ -3320,6 +3349,7 @@ phutil_register_library_map(array( 'PhabricatorSubscriptionsAddSelfHeraldAction' => 'applications/subscriptions/herald/PhabricatorSubscriptionsAddSelfHeraldAction.php', 'PhabricatorSubscriptionsAddSubscribersHeraldAction' => 'applications/subscriptions/herald/PhabricatorSubscriptionsAddSubscribersHeraldAction.php', 'PhabricatorSubscriptionsApplication' => 'applications/subscriptions/application/PhabricatorSubscriptionsApplication.php', + 'PhabricatorSubscriptionsCurtainExtension' => 'applications/subscriptions/engineextension/PhabricatorSubscriptionsCurtainExtension.php', 'PhabricatorSubscriptionsEditController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php', 'PhabricatorSubscriptionsEditEngineExtension' => 'applications/subscriptions/engineextension/PhabricatorSubscriptionsEditEngineExtension.php', 'PhabricatorSubscriptionsEditor' => 'applications/subscriptions/editor/PhabricatorSubscriptionsEditor.php', @@ -3388,6 +3418,7 @@ phutil_register_library_map(array( 'PhabricatorTokenUIEventListener' => 'applications/tokens/event/PhabricatorTokenUIEventListener.php', 'PhabricatorTokenizerEditField' => 'applications/transactions/editfield/PhabricatorTokenizerEditField.php', 'PhabricatorTokensApplication' => 'applications/tokens/application/PhabricatorTokensApplication.php', + 'PhabricatorTokensCurtainExtension' => 'applications/tokens/engineextension/PhabricatorTokensCurtainExtension.php', 'PhabricatorTokensSettingsPanel' => 'applications/settings/panel/PhabricatorTokensSettingsPanel.php', 'PhabricatorTooltipUIExample' => 'applications/uiexample/examples/PhabricatorTooltipUIExample.php', 'PhabricatorTransactions' => 'applications/transactions/constants/PhabricatorTransactions.php', @@ -3763,6 +3794,7 @@ phutil_register_library_map(array( 'PhragmentZIPController' => 'applications/phragment/controller/PhragmentZIPController.php', 'PhrequentConduitAPIMethod' => 'applications/phrequent/conduit/PhrequentConduitAPIMethod.php', 'PhrequentController' => 'applications/phrequent/controller/PhrequentController.php', + 'PhrequentCurtainExtension' => 'applications/phrequent/engineextension/PhrequentCurtainExtension.php', 'PhrequentDAO' => 'applications/phrequent/storage/PhrequentDAO.php', 'PhrequentListController' => 'applications/phrequent/controller/PhrequentListController.php', 'PhrequentPopConduitAPIMethod' => 'applications/phrequent/conduit/PhrequentPopConduitAPIMethod.php', @@ -4943,6 +4975,8 @@ phutil_register_library_map(array( 'DoorkeeperAsanaRemarkupRule' => 'DoorkeeperRemarkupRule', 'DoorkeeperBridge' => 'Phobject', 'DoorkeeperBridgeAsana' => 'DoorkeeperBridge', + 'DoorkeeperBridgeGitHub' => 'DoorkeeperBridge', + 'DoorkeeperBridgeGitHubIssue' => 'DoorkeeperBridgeGitHub', 'DoorkeeperBridgeJIRA' => 'DoorkeeperBridge', 'DoorkeeperBridgeJIRATestCase' => 'PhabricatorTestCase', 'DoorkeeperDAO' => 'PhabricatorLiskDAO', @@ -5008,6 +5042,7 @@ phutil_register_library_map(array( 'DrydockDAO', 'PhabricatorPolicyInterface', ), + 'DrydockCommandError' => 'Phobject', 'DrydockCommandInterface' => 'DrydockInterface', 'DrydockCommandQuery' => 'DrydockQuery', 'DrydockConsoleController' => 'DrydockController', @@ -5649,21 +5684,42 @@ phutil_register_library_map(array( 'NuanceConduitAPIMethod' => 'ConduitAPIMethod', 'NuanceConsoleController' => 'NuanceController', 'NuanceController' => 'PhabricatorController', - 'NuanceCreateItemConduitAPIMethod' => 'NuanceConduitAPIMethod', 'NuanceDAO' => 'PhabricatorLiskDAO', + 'NuanceGitHubEventItemType' => 'NuanceItemType', + 'NuanceGitHubImportCursor' => 'NuanceImportCursor', + 'NuanceGitHubIssuesImportCursor' => 'NuanceGitHubImportCursor', + 'NuanceGitHubRawEvent' => 'Phobject', + 'NuanceGitHubRawEventTestCase' => 'PhabricatorTestCase', + 'NuanceGitHubRepositoryImportCursor' => 'NuanceGitHubImportCursor', + 'NuanceGitHubRepositorySourceDefinition' => 'NuanceSourceDefinition', + 'NuanceImportCursor' => 'Phobject', + 'NuanceImportCursorData' => array( + 'NuanceDAO', + 'PhabricatorPolicyInterface', + ), + 'NuanceImportCursorDataQuery' => 'NuanceQuery', + 'NuanceImportCursorPHIDType' => 'PhabricatorPHIDType', 'NuanceItem' => array( 'NuanceDAO', 'PhabricatorPolicyInterface', 'PhabricatorApplicationTransactionInterface', ), - 'NuanceItemEditController' => 'NuanceController', + 'NuanceItemController' => 'NuanceController', 'NuanceItemEditor' => 'PhabricatorApplicationTransactionEditor', + 'NuanceItemListController' => 'NuanceItemController', + 'NuanceItemManageController' => 'NuanceController', 'NuanceItemPHIDType' => 'PhabricatorPHIDType', 'NuanceItemQuery' => 'NuanceQuery', + 'NuanceItemSearchEngine' => 'PhabricatorApplicationSearchEngine', 'NuanceItemTransaction' => 'NuanceTransaction', 'NuanceItemTransactionComment' => 'PhabricatorApplicationTransactionComment', 'NuanceItemTransactionQuery' => 'PhabricatorApplicationTransactionQuery', + 'NuanceItemType' => 'Phobject', + 'NuanceItemUpdateWorker' => 'NuanceWorker', 'NuanceItemViewController' => 'NuanceController', + 'NuanceManagementImportWorkflow' => 'NuanceManagementWorkflow', + 'NuanceManagementUpdateWorkflow' => 'NuanceManagementWorkflow', + 'NuanceManagementWorkflow' => 'PhabricatorManagementWorkflow', 'NuancePhabricatorFormSourceDefinition' => 'NuanceSourceDefinition', 'NuanceQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'NuanceQueue' => array( @@ -5671,17 +5727,19 @@ phutil_register_library_map(array( 'PhabricatorPolicyInterface', 'PhabricatorApplicationTransactionInterface', ), + 'NuanceQueueController' => 'NuanceController', 'NuanceQueueDatasource' => 'PhabricatorTypeaheadDatasource', - 'NuanceQueueEditController' => 'NuanceController', + 'NuanceQueueEditController' => 'NuanceQueueController', + 'NuanceQueueEditEngine' => 'PhabricatorEditEngine', 'NuanceQueueEditor' => 'PhabricatorApplicationTransactionEditor', - 'NuanceQueueListController' => 'NuanceController', + 'NuanceQueueListController' => 'NuanceQueueController', 'NuanceQueuePHIDType' => 'PhabricatorPHIDType', 'NuanceQueueQuery' => 'NuanceQuery', 'NuanceQueueSearchEngine' => 'PhabricatorApplicationSearchEngine', 'NuanceQueueTransaction' => 'NuanceTransaction', 'NuanceQueueTransactionComment' => 'PhabricatorApplicationTransactionComment', 'NuanceQueueTransactionQuery' => 'PhabricatorApplicationTransactionQuery', - 'NuanceQueueViewController' => 'NuanceController', + 'NuanceQueueViewController' => 'NuanceQueueController', 'NuanceRequestor' => array( 'NuanceDAO', 'PhabricatorPolicyInterface', @@ -5701,25 +5759,29 @@ phutil_register_library_map(array( 'NuanceDAO', 'PhabricatorApplicationTransactionInterface', 'PhabricatorPolicyInterface', + 'PhabricatorNgramsInterface', ), 'NuanceSourceActionController' => 'NuanceController', - 'NuanceSourceCreateController' => 'NuanceController', + 'NuanceSourceController' => 'NuanceController', 'NuanceSourceDefaultEditCapability' => 'PhabricatorPolicyCapability', 'NuanceSourceDefaultViewCapability' => 'PhabricatorPolicyCapability', 'NuanceSourceDefinition' => 'Phobject', 'NuanceSourceDefinitionTestCase' => 'PhabricatorTestCase', - 'NuanceSourceEditController' => 'NuanceController', + 'NuanceSourceEditController' => 'NuanceSourceController', + 'NuanceSourceEditEngine' => 'PhabricatorEditEngine', 'NuanceSourceEditor' => 'PhabricatorApplicationTransactionEditor', - 'NuanceSourceListController' => 'NuanceController', + 'NuanceSourceListController' => 'NuanceSourceController', 'NuanceSourceManageCapability' => 'PhabricatorPolicyCapability', + 'NuanceSourceNameNgrams' => 'PhabricatorSearchNgrams', 'NuanceSourcePHIDType' => 'PhabricatorPHIDType', 'NuanceSourceQuery' => 'NuanceQuery', 'NuanceSourceSearchEngine' => 'PhabricatorApplicationSearchEngine', 'NuanceSourceTransaction' => 'NuanceTransaction', 'NuanceSourceTransactionComment' => 'PhabricatorApplicationTransactionComment', 'NuanceSourceTransactionQuery' => 'PhabricatorApplicationTransactionQuery', - 'NuanceSourceViewController' => 'NuanceController', + 'NuanceSourceViewController' => 'NuanceSourceController', 'NuanceTransaction' => 'PhabricatorApplicationTransaction', + 'NuanceWorker' => 'PhabricatorWorker', 'OwnersConduitAPIMethod' => 'ConduitAPIMethod', 'OwnersEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', 'OwnersPackageReplyHandler' => 'PhabricatorMailReplyHandler', @@ -5751,6 +5813,9 @@ phutil_register_library_map(array( 'PHUIColorPalletteExample' => 'PhabricatorUIExample', 'PHUICrumbView' => 'AphrontView', 'PHUICrumbsView' => 'AphrontView', + 'PHUICurtainExtension' => 'Phobject', + 'PHUICurtainPanelView' => 'AphrontTagView', + 'PHUICurtainView' => 'AphrontTagView', 'PHUIDiffInlineCommentDetailView' => 'PHUIDiffInlineCommentView', 'PHUIDiffInlineCommentEditView' => 'PHUIDiffInlineCommentView', 'PHUIDiffInlineCommentRowScaffold' => 'AphrontView', @@ -7503,6 +7568,7 @@ phutil_register_library_map(array( 'PhabricatorProjectWatcherListView' => 'PhabricatorProjectUserListView', 'PhabricatorProjectWorkboardBackgroundColor' => 'Phobject', 'PhabricatorProjectWorkboardProfilePanel' => 'PhabricatorProfilePanel', + 'PhabricatorProjectsCurtainExtension' => 'PHUICurtainExtension', 'PhabricatorProjectsEditEngineExtension' => 'PhabricatorEditEngineExtension', 'PhabricatorProjectsEditField' => 'PhabricatorTokenizerEditField', 'PhabricatorProjectsFulltextEngineExtension' => 'PhabricatorFulltextEngineExtension', @@ -7872,6 +7938,7 @@ phutil_register_library_map(array( 'PhabricatorSubscriptionsAddSelfHeraldAction' => 'PhabricatorSubscriptionsHeraldAction', 'PhabricatorSubscriptionsAddSubscribersHeraldAction' => 'PhabricatorSubscriptionsHeraldAction', 'PhabricatorSubscriptionsApplication' => 'PhabricatorApplication', + 'PhabricatorSubscriptionsCurtainExtension' => 'PHUICurtainExtension', 'PhabricatorSubscriptionsEditController' => 'PhabricatorController', 'PhabricatorSubscriptionsEditEngineExtension' => 'PhabricatorEditEngineExtension', 'PhabricatorSubscriptionsEditor' => 'PhabricatorEditor', @@ -7945,6 +8012,7 @@ phutil_register_library_map(array( 'PhabricatorTokenUIEventListener' => 'PhabricatorEventListener', 'PhabricatorTokenizerEditField' => 'PhabricatorPHIDListEditField', 'PhabricatorTokensApplication' => 'PhabricatorApplication', + 'PhabricatorTokensCurtainExtension' => 'PHUICurtainExtension', 'PhabricatorTokensSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorTooltipUIExample' => 'PhabricatorUIExample', 'PhabricatorTransactions' => 'Phobject', @@ -8431,6 +8499,7 @@ phutil_register_library_map(array( 'PhragmentZIPController' => 'PhragmentController', 'PhrequentConduitAPIMethod' => 'ConduitAPIMethod', 'PhrequentController' => 'PhabricatorController', + 'PhrequentCurtainExtension' => 'PHUICurtainExtension', 'PhrequentDAO' => 'PhabricatorLiskDAO', 'PhrequentListController' => 'PhrequentController', 'PhrequentPopConduitAPIMethod' => 'PhrequentConduitAPIMethod', diff --git a/src/applications/almanac/controller/AlmanacBindingViewController.php b/src/applications/almanac/controller/AlmanacBindingViewController.php index b75565525c..ead3e1b4c1 100644 --- a/src/applications/almanac/controller/AlmanacBindingViewController.php +++ b/src/applications/almanac/controller/AlmanacBindingViewController.php @@ -26,26 +26,23 @@ final class AlmanacBindingViewController $title = pht('Binding %s', $binding->getID()); - $property_list = $this->buildPropertyList($binding); - $action_list = $this->buildActionList($binding); - $property_list->setActionList($action_list); + $properties = $this->buildPropertyList($binding); + $details = $this->buildPropertySection($binding); + $curtain = $this->buildCurtain($binding); $header = id(new PHUIHeaderView()) ->setUser($viewer) ->setHeader($title) - ->setPolicyObject($binding); + ->setPolicyObject($binding) + ->setHeaderIcon('fa-object-group'); if ($binding->getIsDisabled()) { $header->setStatus('fa-ban', 'red', pht('Disabled')); } - $box = id(new PHUIObjectBoxView()) - ->setHeader($header) - ->addPropertyList($property_list); - + $issue = null; if ($binding->getService()->isClusterService()) { - $this->addClusterMessage( - $box, + $issue = $this->addClusterMessage( pht('The service for this binding is a cluster service.'), pht( 'The service for this binding is a cluster service. You do not '. @@ -56,24 +53,33 @@ final class AlmanacBindingViewController $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($service->getName(), $service_uri); $crumbs->addTextCrumb($title); + $crumbs->setBorder(true); $timeline = $this->buildTransactionTimeline( $binding, new AlmanacBindingTransactionQuery()); $timeline->setShouldTerminate(true); + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setCurtain($curtain) + ->setMainColumn(array( + $issue, + $this->buildAlmanacPropertiesTable($binding), + $timeline, + )) + ->addPropertySection(pht('DETAILS'), $details); + return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->appendChild( array( - $box, - $this->buildAlmanacPropertiesTable($binding), - $timeline, + $view, )); } - private function buildPropertyList(AlmanacBinding $binding) { + private function buildPropertySection(AlmanacBinding $binding) { $viewer = $this->getViewer(); $properties = id(new PHUIPropertyListView()) @@ -98,23 +104,36 @@ final class AlmanacBindingViewController return $properties; } - private function buildActionList(AlmanacBinding $binding) { + private function buildPropertyList(AlmanacBinding $binding) { + $viewer = $this->getViewer(); + + $properties = id(new PHUIPropertyListView()) + ->setUser($viewer) + ->setObject($binding); + $properties->invokeWillRenderEvent(); + + return $properties; + } + + private function buildCurtain(AlmanacBinding $binding) { $viewer = $this->getViewer(); - $id = $binding->getID(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $binding, PhabricatorPolicyCapability::CAN_EDIT); - $actions = id(new PhabricatorActionListView()) - ->setUser($viewer); + $id = $binding->getID(); + $edit_uri = $this->getApplicationURI("binding/edit/{$id}/"); + $disable_uri = $this->getApplicationURI("binding/disable/{$id}/"); - $actions->addAction( + $curtain = $this->newCurtainView($binding); + + $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-pencil') ->setName(pht('Edit Binding')) - ->setHref($this->getApplicationURI("binding/edit/{$id}/")) + ->setHref($edit_uri) ->setWorkflow(!$can_edit) ->setDisabled(!$can_edit)); @@ -126,17 +145,15 @@ final class AlmanacBindingViewController $disable_text = pht('Disable Binding'); } - $disable_href = $this->getApplicationURI("binding/disable/{$id}/"); - - $actions->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setIcon($disable_icon) ->setName($disable_text) - ->setHref($disable_href) + ->setHref($disable_uri) ->setWorkflow(true) ->setDisabled(!$can_edit)); - return $actions; + return $curtain; } } diff --git a/src/applications/almanac/controller/AlmanacDeviceViewController.php b/src/applications/almanac/controller/AlmanacDeviceViewController.php index f6bf697e31..efc4334132 100644 --- a/src/applications/almanac/controller/AlmanacDeviceViewController.php +++ b/src/applications/almanac/controller/AlmanacDeviceViewController.php @@ -23,8 +23,7 @@ final class AlmanacDeviceViewController $title = pht('Device %s', $device->getName()); - $properties = $this->buildPropertyList($device); - $actions = $this->buildActionList($device); + $curtain = $this->buildCurtain($device); $header = id(new PHUIHeaderView()) ->setUser($viewer) @@ -55,6 +54,7 @@ final class AlmanacDeviceViewController $view = id(new PHUITwoColumnView()) ->setHeader($header) + ->setCurtain($curtain) ->setMainColumn(array( $issue, $interfaces, @@ -62,9 +62,7 @@ final class AlmanacDeviceViewController $this->buildSSHKeysTable($device), $this->buildServicesTable($device), $timeline, - )) - ->setPropertyList($properties) - ->setActionList($actions); + )); return $this->newPage() ->setTitle($title) @@ -75,37 +73,28 @@ final class AlmanacDeviceViewController )); } - private function buildPropertyList(AlmanacDevice $device) { + private function buildCurtain(AlmanacDevice $device) { $viewer = $this->getViewer(); - $properties = id(new PHUIPropertyListView()) - ->setUser($viewer) - ->setObject($device); - - return $properties; - } - - private function buildActionList(AlmanacDevice $device) { - $viewer = $this->getViewer(); - $id = $device->getID(); - $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $device, PhabricatorPolicyCapability::CAN_EDIT); - $actions = id(new PhabricatorActionListView()) - ->setUser($viewer); + $id = $device->getID(); + $edit_uri = $this->getApplicationURI("device/edit/{$id}/"); - $actions->addAction( + $curtain = $this->newCurtainView($device); + + $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-pencil') ->setName(pht('Edit Device')) - ->setHref($this->getApplicationURI("device/edit/{$id}/")) + ->setHref($edit_uri) ->setWorkflow(!$can_edit) ->setDisabled(!$can_edit)); - return $actions; + return $curtain; } private function buildInterfaceList(AlmanacDevice $device) { diff --git a/src/applications/almanac/controller/AlmanacNamespaceViewController.php b/src/applications/almanac/controller/AlmanacNamespaceViewController.php index b999f219fc..a673bdf648 100644 --- a/src/applications/almanac/controller/AlmanacNamespaceViewController.php +++ b/src/applications/almanac/controller/AlmanacNamespaceViewController.php @@ -21,8 +21,7 @@ final class AlmanacNamespaceViewController $title = pht('Namespace %s', $namespace->getName()); - $properties = $this->buildPropertyList($namespace); - $actions = $this->buildActionList($namespace); + $curtain = $this->buildCurtain($namespace); $header = id(new PHUIHeaderView()) ->setUser($viewer) @@ -41,11 +40,10 @@ final class AlmanacNamespaceViewController $view = id(new PHUITwoColumnView()) ->setHeader($header) + ->setCurtain($curtain) ->setMainColumn(array( $timeline, - )) - ->setPropertyList($properties) - ->setActionList($actions); + )); return $this->newPage() ->setTitle($title) @@ -56,39 +54,28 @@ final class AlmanacNamespaceViewController )); } - private function buildPropertyList(AlmanacNamespace $namespace) { + private function buildCurtain(AlmanacNamespace $namespace) { $viewer = $this->getViewer(); - $properties = id(new PHUIPropertyListView()) - ->setUser($viewer) - ->setObject($namespace); - - $properties->invokeWillRenderEvent(); - - return $properties; - } - - private function buildActionList(AlmanacNamespace $namespace) { - $viewer = $this->getViewer(); - $id = $namespace->getID(); - $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $namespace, PhabricatorPolicyCapability::CAN_EDIT); - $actions = id(new PhabricatorActionListView()) - ->setUser($viewer); + $id = $namespace->getID(); + $edit_uri = $this->getApplicationURI("namespace/edit/{$id}/"); - $actions->addAction( + $curtain = $this->newCurtainView($namespace); + + $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-pencil') ->setName(pht('Edit Namespace')) - ->setHref($this->getApplicationURI("namespace/edit/{$id}/")) + ->setHref($edit_uri) ->setWorkflow(!$can_edit) ->setDisabled(!$can_edit)); - return $actions; + return $curtain; } } diff --git a/src/applications/almanac/controller/AlmanacNetworkViewController.php b/src/applications/almanac/controller/AlmanacNetworkViewController.php index 11e6eaf799..271ae1a0fb 100644 --- a/src/applications/almanac/controller/AlmanacNetworkViewController.php +++ b/src/applications/almanac/controller/AlmanacNetworkViewController.php @@ -21,8 +21,7 @@ final class AlmanacNetworkViewController $title = pht('Network %s', $network->getName()); - $properties = $this->buildPropertyList($network); - $actions = $this->buildActionList($network); + $curtain = $this->buildCurtain($network); $header = id(new PHUIHeaderView()) ->setUser($viewer) @@ -41,11 +40,10 @@ final class AlmanacNetworkViewController $view = id(new PHUITwoColumnView()) ->setHeader($header) + ->setCurtain($curtain) ->setMainColumn(array( $timeline, - )) - ->setPropertyList($properties) - ->setActionList($actions); + )); return $this->newPage() ->setTitle($title) @@ -56,39 +54,29 @@ final class AlmanacNetworkViewController )); } - private function buildPropertyList(AlmanacNetwork $network) { + + private function buildCurtain(AlmanacNetwork $network) { $viewer = $this->getViewer(); - $properties = id(new PHUIPropertyListView()) - ->setUser($viewer) - ->setObject($network); - - $properties->invokeWillRenderEvent(); - - return $properties; - } - - private function buildActionList(AlmanacNetwork $network) { - $viewer = $this->getViewer(); - $id = $network->getID(); - $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $network, PhabricatorPolicyCapability::CAN_EDIT); - $actions = id(new PhabricatorActionListView()) - ->setUser($viewer); + $id = $network->getID(); + $edit_uri = $this->getApplicationURI("network/edit/{$id}/"); - $actions->addAction( + $curtain = $this->newCurtainView($network); + + $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-pencil') ->setName(pht('Edit Network')) - ->setHref($this->getApplicationURI("network/edit/{$id}/")) + ->setHref($edit_uri) ->setWorkflow(!$can_edit) ->setDisabled(!$can_edit)); - return $actions; + return $curtain; } } diff --git a/src/applications/almanac/controller/AlmanacServiceViewController.php b/src/applications/almanac/controller/AlmanacServiceViewController.php index 0b59a47442..1036dc9e78 100644 --- a/src/applications/almanac/controller/AlmanacServiceViewController.php +++ b/src/applications/almanac/controller/AlmanacServiceViewController.php @@ -23,8 +23,7 @@ final class AlmanacServiceViewController $title = pht('Service %s', $service->getName()); - $properties = $this->buildPropertyList($service); - $actions = $this->buildActionList($service); + $curtain = $this->buildCurtain($service); $details = $this->buildPropertySection($service); $header = id(new PHUIHeaderView()) @@ -55,36 +54,19 @@ final class AlmanacServiceViewController $view = id(new PHUITwoColumnView()) ->setHeader($header) + ->setCurtain($curtain) ->setMainColumn(array( $issue, $details, $bindings, $this->buildAlmanacPropertiesTable($service), $timeline, - )) - ->setPropertyList($properties) - ->setActionList($actions); + )); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) - ->appendChild( - array( - $view, - )); - } - - private function buildPropertyList( - AlmanacService $service) { - $viewer = $this->getViewer(); - - $view = id(new PHUIPropertyListView()) - ->setUser($viewer) - ->setObject($service); - - $view->invokeWillRenderEvent(); - - return $view; + ->appendChild($view); } private function buildPropertySection( @@ -104,27 +86,28 @@ final class AlmanacServiceViewController ->appendChild($properties); } - private function buildActionList(AlmanacService $service) { + private function buildCurtain(AlmanacService $service) { $viewer = $this->getViewer(); - $id = $service->getID(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $service, PhabricatorPolicyCapability::CAN_EDIT); - $actions = id(new PhabricatorActionListView()) - ->setUser($viewer); + $id = $service->getID(); + $edit_uri = $this->getApplicationURI("service/edit/{$id}/"); - $actions->addAction( + $curtain = $this->newCurtainView($service); + + $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-pencil') ->setName(pht('Edit Service')) - ->setHref($this->getApplicationURI("service/edit/{$id}/")) + ->setHref($edit_uri) ->setWorkflow(!$can_edit) ->setDisabled(!$can_edit)); - return $actions; + return $curtain; } private function buildBindingList(AlmanacService $service) { diff --git a/src/applications/almanac/query/AlmanacServiceSearchEngine.php b/src/applications/almanac/query/AlmanacServiceSearchEngine.php index 1a9509a2c2..a2fd9c23b7 100644 --- a/src/applications/almanac/query/AlmanacServiceSearchEngine.php +++ b/src/applications/almanac/query/AlmanacServiceSearchEngine.php @@ -15,10 +15,6 @@ final class AlmanacServiceSearchEngine return new AlmanacServiceQuery(); } - public function newResultObject() { - return new AlmanacService(); - } - protected function buildQueryFromParameters(array $map) { $query = $this->newQuery(); diff --git a/src/applications/almanac/storage/AlmanacDevice.php b/src/applications/almanac/storage/AlmanacDevice.php index f46b03600f..6c7f3cb57f 100644 --- a/src/applications/almanac/storage/AlmanacDevice.php +++ b/src/applications/almanac/storage/AlmanacDevice.php @@ -246,7 +246,7 @@ final class AlmanacDevice } -/* -( PhabricatorNgramInterface )------------------------------------------ */ +/* -( PhabricatorNgramsInterface )----------------------------------------- */ public function newNgrams() { diff --git a/src/applications/almanac/storage/AlmanacNamespace.php b/src/applications/almanac/storage/AlmanacNamespace.php index 6a3baca637..4bbb4d4090 100644 --- a/src/applications/almanac/storage/AlmanacNamespace.php +++ b/src/applications/almanac/storage/AlmanacNamespace.php @@ -210,7 +210,7 @@ final class AlmanacNamespace } -/* -( PhabricatorNgramInterface )------------------------------------------ */ +/* -( PhabricatorNgramsInterface )----------------------------------------- */ public function newNgrams() { diff --git a/src/applications/almanac/storage/AlmanacNetwork.php b/src/applications/almanac/storage/AlmanacNetwork.php index 064b612999..2f37ab4778 100644 --- a/src/applications/almanac/storage/AlmanacNetwork.php +++ b/src/applications/almanac/storage/AlmanacNetwork.php @@ -116,7 +116,7 @@ final class AlmanacNetwork } -/* -( PhabricatorNgramInterface )------------------------------------------ */ +/* -( PhabricatorNgramsInterface )----------------------------------------- */ public function newNgrams() { diff --git a/src/applications/almanac/storage/AlmanacService.php b/src/applications/almanac/storage/AlmanacService.php index b280de9ba8..37c3a44ba6 100644 --- a/src/applications/almanac/storage/AlmanacService.php +++ b/src/applications/almanac/storage/AlmanacService.php @@ -251,7 +251,7 @@ final class AlmanacService } -/* -( PhabricatorNgramInterface )------------------------------------------ */ +/* -( PhabricatorNgramsInterface )----------------------------------------- */ public function newNgrams() { diff --git a/src/applications/audit/editor/PhabricatorAuditEditor.php b/src/applications/audit/editor/PhabricatorAuditEditor.php index 51efdf175b..3df9013301 100644 --- a/src/applications/audit/editor/PhabricatorAuditEditor.php +++ b/src/applications/audit/editor/PhabricatorAuditEditor.php @@ -636,6 +636,8 @@ final class PhabricatorAuditEditor } } + $phids[] = $this->getActingAsPHID(); + return $phids; } diff --git a/src/applications/badges/controller/PhabricatorBadgesViewController.php b/src/applications/badges/controller/PhabricatorBadgesViewController.php index 3858965d41..fac82ccc8d 100644 --- a/src/applications/badges/controller/PhabricatorBadgesViewController.php +++ b/src/applications/badges/controller/PhabricatorBadgesViewController.php @@ -43,8 +43,7 @@ final class PhabricatorBadgesViewController ->setStatus($status_icon, $status_color, $status_name) ->setHeaderIcon('fa-trophy'); - $properties = $this->buildPropertyListView($badge); - $actions = $this->buildActionListView($badge); + $curtain = $this->buildCurtain($badge); $details = $this->buildDetailsView($badge); $timeline = $this->buildTransactionTimeline( @@ -64,36 +63,19 @@ final class PhabricatorBadgesViewController $view = id(new PHUITwoColumnView()) ->setHeader($header) + ->setCurtain($curtain) ->setMainColumn(array( $recipient_list, $timeline, $add_comment, )) - ->setPropertyList($properties) - ->setActionList($actions) ->addPropertySection(pht('BADGE DETAILS'), $details); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->setPageObjectPHIDs(array($badge->getPHID())) - ->appendChild( - array( - $view, - )); - } - - private function buildPropertyListView( - PhabricatorBadgesBadge $badge) { - $viewer = $this->getViewer(); - - $view = id(new PHUIPropertyListView()) - ->setUser($viewer) - ->setObject($badge); - - $view->invokeWillRenderEvent(); - - return $view; + ->appendChild($view); } private function buildDetailsView( @@ -137,53 +119,55 @@ final class PhabricatorBadgesViewController return $view; } - private function buildActionListView(PhabricatorBadgesBadge $badge) { + private function buildCurtain(PhabricatorBadgesBadge $badge) { $viewer = $this->getViewer(); - $id = $badge->getID(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $badge, PhabricatorPolicyCapability::CAN_EDIT); - $view = id(new PhabricatorActionListView()) - ->setUser($viewer) - ->setObject($badge); + $id = $badge->getID(); + $edit_uri = $this->getApplicationURI("/edit/{$id}/"); + $archive_uri = $this->getApplicationURI("/archive/{$id}/"); + $award_uri = $this->getApplicationURI("/recipients/{$id}/"); - $view->addAction( + $curtain = $this->newCurtainView($badge); + + $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Badge')) ->setIcon('fa-pencil') ->setDisabled(!$can_edit) - ->setHref($this->getApplicationURI("/edit/{$id}/"))); + ->setHref($edit_uri)); if ($badge->isArchived()) { - $view->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Activate Badge')) ->setIcon('fa-check') ->setDisabled(!$can_edit) ->setWorkflow($can_edit) - ->setHref($this->getApplicationURI("/archive/{$id}/"))); + ->setHref($archive_uri)); } else { - $view->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Archive Badge')) ->setIcon('fa-ban') ->setDisabled(!$can_edit) ->setWorkflow($can_edit) - ->setHref($this->getApplicationURI("/archive/{$id}/"))); + ->setHref($archive_uri)); } - $view->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setName('Add Recipients') ->setIcon('fa-users') ->setDisabled(!$can_edit) ->setWorkflow(true) - ->setHref($this->getApplicationURI("/recipients/{$id}/"))); + ->setHref($award_uri)); - return $view; + return $curtain; } private function buildCommentForm(PhabricatorBadgesBadge $badge) { diff --git a/src/applications/badges/storage/PhabricatorBadgesBadge.php b/src/applications/badges/storage/PhabricatorBadgesBadge.php index 254b02a6eb..d32a46a803 100644 --- a/src/applications/badges/storage/PhabricatorBadgesBadge.php +++ b/src/applications/badges/storage/PhabricatorBadgesBadge.php @@ -181,10 +181,6 @@ final class PhabricatorBadgesBadge extends PhabricatorBadgesDAO return ($this->creatorPHID == $phid); } - public function shouldShowSubscribersProperty() { - return true; - } - /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ diff --git a/src/applications/badges/view/PhabricatorBadgesRecipientsListView.php b/src/applications/badges/view/PhabricatorBadgesRecipientsListView.php index 6b633bed5b..68633a6a29 100644 --- a/src/applications/badges/view/PhabricatorBadgesRecipientsListView.php +++ b/src/applications/badges/view/PhabricatorBadgesRecipientsListView.php @@ -16,8 +16,7 @@ final class PhabricatorBadgesRecipientsListView extends AphrontView { } public function render() { - - $viewer = $this->user; + $viewer = $this->getViewer(); $badge = $this->badge; $handles = $this->handles; diff --git a/src/applications/base/controller/PhabricatorController.php b/src/applications/base/controller/PhabricatorController.php index e6997d97ab..5d8ff7ba60 100644 --- a/src/applications/base/controller/PhabricatorController.php +++ b/src/applications/base/controller/PhabricatorController.php @@ -468,7 +468,32 @@ abstract class PhabricatorController extends AphrontController { public function newApplicationMenu() { return id(new PHUIApplicationMenuView()) - ->setViewer($this->getRequest()->getUser()); + ->setViewer($this->getViewer()); + } + + public function newCurtainView($object) { + $viewer = $this->getViewer(); + + $action_list = id(new PhabricatorActionListView()) + ->setViewer($viewer); + + // NOTE: Applications (objects of class PhabricatorApplication) can't + // currently be set here, although they don't need any of the extensions + // anyway. This should probably work differently than it does, though. + if ($object instanceof PhabricatorLiskDAO) { + $action_list->setObject($object); + } + + $curtain = id(new PHUICurtainView()) + ->setViewer($viewer) + ->setActionList($action_list); + + $panels = PHUICurtainExtension::buildExtensionPanels($viewer, $object); + foreach ($panels as $panel) { + $curtain->addPanel($panel); + } + + return $curtain; } protected function buildTransactionTimeline( diff --git a/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php b/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php index 8d13f85e61..5880db1210 100644 --- a/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php +++ b/src/applications/calendar/controller/PhabricatorCalendarEventViewController.php @@ -63,8 +63,7 @@ final class PhabricatorCalendarEventViewController } $header = $this->buildHeaderView($event); - $actions = $this->buildActionView($event); - $properties = $this->buildPropertyListView($event); + $curtain = $this->buildCurtain($event); $details = $this->buildPropertySection($event); $description = $this->buildDescriptionView($event); @@ -90,11 +89,13 @@ final class PhabricatorCalendarEventViewController $view = id(new PHUITwoColumnView()) ->setHeader($header) - ->setMainColumn($timeline) - ->setPropertyList($properties) + ->setMainColumn(array( + $timeline, + $add_comment_form, + )) + ->setCurtain($curtain) ->addPropertySection(pht('DETAILS'), $details) - ->addPropertySection(pht('DESCRIPTION'), $description) - ->setActionList($actions); + ->addPropertySection(pht('DESCRIPTION'), $description); return $this->newPage() ->setTitle($page_title) @@ -148,16 +149,12 @@ final class PhabricatorCalendarEventViewController return $header; } - private function buildActionView(PhabricatorCalendarEvent $event) { + private function buildCurtain(PhabricatorCalendarEvent $event) { $viewer = $this->getRequest()->getUser(); $id = $event->getID(); $is_cancelled = $event->getIsCancelled(); $is_attending = $event->getIsUserAttending($viewer->getPHID()); - $actions = id(new PhabricatorActionListView()) - ->setUser($viewer) - ->setObject($event); - $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $event, @@ -178,8 +175,10 @@ final class PhabricatorCalendarEventViewController $edit_uri = "event/edit/{$id}/"; } + $curtain = $this->newCurtainView($event); + if ($edit_label && $edit_uri) { - $actions->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setName($edit_label) ->setIcon('fa-pencil') @@ -189,14 +188,14 @@ final class PhabricatorCalendarEventViewController } if ($is_attending) { - $actions->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Decline Event')) ->setIcon('fa-user-times') ->setHref($this->getApplicationURI("event/join/{$id}/")) ->setWorkflow(true)); } else { - $actions->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Join Event')) ->setIcon('fa-user-plus') @@ -230,7 +229,7 @@ final class PhabricatorCalendarEventViewController } if ($is_cancelled) { - $actions->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setName($reinstate_label) ->setIcon('fa-plus') @@ -238,7 +237,7 @@ final class PhabricatorCalendarEventViewController ->setDisabled($cancel_disabled) ->setWorkflow(true)); } else { - $actions->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setName($cancel_label) ->setIcon('fa-times') @@ -247,20 +246,7 @@ final class PhabricatorCalendarEventViewController ->setWorkflow(true)); } - return $actions; - } - - private function buildPropertyListView( - PhabricatorCalendarEvent $event) { - $viewer = $this->getViewer(); - - $properties = id(new PHUIPropertyListView()) - ->setUser($viewer) - ->setObject($event); - - $properties->invokeWillRenderEvent(); - - return $properties; + return $curtain; } private function buildPropertySection( diff --git a/src/applications/calendar/storage/PhabricatorCalendarEvent.php b/src/applications/calendar/storage/PhabricatorCalendarEvent.php index 36aa905b3e..8432a157e2 100644 --- a/src/applications/calendar/storage/PhabricatorCalendarEvent.php +++ b/src/applications/calendar/storage/PhabricatorCalendarEvent.php @@ -531,10 +531,6 @@ final class PhabricatorCalendarEvent extends PhabricatorCalendarDAO return ($phid == $this->getUserPHID()); } - public function shouldShowSubscribersProperty() { - return true; - } - /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ diff --git a/src/applications/celerity/postprocessor/CelerityDefaultPostprocessor.php b/src/applications/celerity/postprocessor/CelerityDefaultPostprocessor.php index 13cef002cf..7d87d28ff2 100644 --- a/src/applications/celerity/postprocessor/CelerityDefaultPostprocessor.php +++ b/src/applications/celerity/postprocessor/CelerityDefaultPostprocessor.php @@ -71,6 +71,7 @@ final class CelerityDefaultPostprocessor 'hoverselectedgrey' => '#bbc4ca', 'hoverselectedblue' => '#e6e9ee', 'borderinset' => 'inset 0 0 0 1px rgba(55,55,55,.15)', + 'timeline' => '#d5d8e1', // Alphas 'alphawhite' => '255,255,255', diff --git a/src/applications/conpherence/controller/ConpherenceWidgetController.php b/src/applications/conpherence/controller/ConpherenceWidgetController.php index e6707577e9..af9d799519 100644 --- a/src/applications/conpherence/controller/ConpherenceWidgetController.php +++ b/src/applications/conpherence/controller/ConpherenceWidgetController.php @@ -76,7 +76,7 @@ final class ConpherenceWidgetController extends ConpherenceController { ), id(new PHUIHeaderView()) ->setHeader($header) - ->addActionIcon($new_icon)); + ->addActionItem($new_icon)); $user = $this->getRequest()->getUser(); // now the widget bodies $widgets[] = javelin_tag( diff --git a/src/applications/countdown/controller/PhabricatorCountdownViewController.php b/src/applications/countdown/controller/PhabricatorCountdownViewController.php index 9a983f0ed9..6e259df555 100644 --- a/src/applications/countdown/controller/PhabricatorCountdownViewController.php +++ b/src/applications/countdown/controller/PhabricatorCountdownViewController.php @@ -49,8 +49,7 @@ final class PhabricatorCountdownViewController ->setStatus($icon, $color, $status) ->setHeaderIcon('fa-rocket'); - $actions = $this->buildActionListView($countdown); - $properties = $this->buildPropertyListView($countdown); + $curtain = $this->buildCurtain($countdown); $subheader = $this->buildSubheaderView($countdown); $timeline = $this->buildTransactionTimeline( @@ -67,9 +66,8 @@ final class PhabricatorCountdownViewController $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setSubheader($subheader) - ->setMainColumn($content) - ->setPropertyList($properties) - ->setActionList($actions); + ->setCurtain($curtain) + ->setMainColumn($content); return $this->newPage() ->setTitle($title) @@ -78,28 +76,22 @@ final class PhabricatorCountdownViewController array( $countdown->getPHID(), )) - ->appendChild( - array( - $view, - )); + ->appendChild($view); } - private function buildActionListView(PhabricatorCountdown $countdown) { - $request = $this->getRequest(); - $viewer = $request->getUser(); + private function buildCurtain(PhabricatorCountdown $countdown) { + $viewer = $this->getViewer(); $id = $countdown->getID(); - $view = id(new PhabricatorActionListView()) - ->setObject($countdown) - ->setUser($viewer); - $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $countdown, PhabricatorPolicyCapability::CAN_EDIT); - $view->addAction( + $curtain = $this->newCurtainView($countdown); + + $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-pencil') ->setName(pht('Edit Countdown')) @@ -107,7 +99,7 @@ final class PhabricatorCountdownViewController ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); - $view->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-times') ->setName(pht('Delete Countdown')) @@ -115,17 +107,7 @@ final class PhabricatorCountdownViewController ->setDisabled(!$can_edit) ->setWorkflow(true)); - return $view; - } - - private function buildPropertyListView( - PhabricatorCountdown $countdown) { - $viewer = $this->getViewer(); - $view = id(new PHUIPropertyListView()) - ->setUser($viewer) - ->setObject($countdown); - $view->invokeWillRenderEvent(); - return $view; + return $curtain; } private function buildSubheaderView( diff --git a/src/applications/countdown/storage/PhabricatorCountdown.php b/src/applications/countdown/storage/PhabricatorCountdown.php index 1a864907eb..52a395f0c2 100644 --- a/src/applications/countdown/storage/PhabricatorCountdown.php +++ b/src/applications/countdown/storage/PhabricatorCountdown.php @@ -70,9 +70,6 @@ final class PhabricatorCountdown extends PhabricatorCountdownDAO return ($phid == $this->getAuthorPHID()); } - public function shouldShowSubscribersProperty() { - return true; - } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ diff --git a/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php b/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php index a2beba3cd2..447a614600 100644 --- a/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php +++ b/src/applications/daemon/management/PhabricatorDaemonManagementWorkflow.php @@ -158,8 +158,10 @@ abstract class PhabricatorDaemonManagementWorkflow $this->printLaunchingDaemons($daemons, $debug); + $trace = PhutilArgumentParser::isTraceModeEnabled(); + $flags = array(); - if ($debug || PhabricatorEnv::getEnvConfig('phd.trace')) { + if ($trace || PhabricatorEnv::getEnvConfig('phd.trace')) { $flags[] = '--trace'; } diff --git a/src/applications/daemon/view/PhabricatorDaemonLogEventsView.php b/src/applications/daemon/view/PhabricatorDaemonLogEventsView.php index 4c0caa6fd7..039906e718 100644 --- a/src/applications/daemon/view/PhabricatorDaemonLogEventsView.php +++ b/src/applications/daemon/view/PhabricatorDaemonLogEventsView.php @@ -24,12 +24,9 @@ final class PhabricatorDaemonLogEventsView extends AphrontView { } public function render() { + $viewer = $this->getViewer(); $rows = array(); - if (!$this->user) { - throw new PhutilInvalidStateException('setUser'); - } - foreach ($this->events as $event) { // Limit display log size. If a daemon gets stuck in an output loop this @@ -83,8 +80,8 @@ final class PhabricatorDaemonLogEventsView extends AphrontView { $row = array( $event->getLogType(), - phabricator_date($event->getEpoch(), $this->user), - phabricator_time($event->getEpoch(), $this->user), + phabricator_date($event->getEpoch(), $viewer), + phabricator_time($event->getEpoch(), $viewer), array( $message, $more, diff --git a/src/applications/daemon/view/PhabricatorDaemonLogListView.php b/src/applications/daemon/view/PhabricatorDaemonLogListView.php index 6c96509505..046d1a29f5 100644 --- a/src/applications/daemon/view/PhabricatorDaemonLogListView.php +++ b/src/applications/daemon/view/PhabricatorDaemonLogListView.php @@ -11,11 +11,9 @@ final class PhabricatorDaemonLogListView extends AphrontView { } public function render() { - $rows = array(); + $viewer = $this->getViewer(); - if (!$this->user) { - throw new PhutilInvalidStateException('setUser'); - } + $rows = array(); $list = new PHUIObjectItemListView(); $list->setFlush(true); @@ -27,7 +25,7 @@ final class PhabricatorDaemonLogListView extends AphrontView { ->setObjectName(pht('Daemon %s', $id)) ->setHeader($log->getDaemon()) ->setHref("/daemon/log/{$id}/") - ->addIcon('none', phabricator_datetime($epoch, $this->user)); + ->addIcon('none', phabricator_datetime($epoch, $viewer)); $status = $log->getStatus(); switch ($status) { diff --git a/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php b/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php index 57877783bb..22ab5b20bf 100644 --- a/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php +++ b/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php @@ -253,7 +253,7 @@ final class PhabricatorDashboardPanelRenderingEngine extends Phobject { ->setIcon('fa-pencil') ->setWorkflow(true) ->setHref((string)$edit_uri); - $header->addActionIcon($action_edit); + $header->addActionItem($action_edit); if ($dashboard_id) { $uri = id(new PhutilURI( @@ -263,7 +263,7 @@ final class PhabricatorDashboardPanelRenderingEngine extends Phobject { ->setIcon('fa-trash-o') ->setHref((string)$uri) ->setWorkflow(true); - $header->addActionIcon($action_remove); + $header->addActionItem($action_remove); } return $header; } diff --git a/src/applications/dashboard/paneltype/PhabricatorDashboardQueryPanelType.php b/src/applications/dashboard/paneltype/PhabricatorDashboardQueryPanelType.php index 92195aa8f5..76e6e8432b 100644 --- a/src/applications/dashboard/paneltype/PhabricatorDashboardQueryPanelType.php +++ b/src/applications/dashboard/paneltype/PhabricatorDashboardQueryPanelType.php @@ -119,7 +119,7 @@ final class PhabricatorDashboardQueryPanelType $icon = id(new PHUIIconView()) ->setIcon('fa-search') ->setHref($href); - $header->addActionIcon($icon); + $header->addActionItem($icon); return $header; } diff --git a/src/applications/differential/constants/DifferentialRevisionStatus.php b/src/applications/differential/constants/DifferentialRevisionStatus.php index 50bc903c40..4f332838ac 100644 --- a/src/applications/differential/constants/DifferentialRevisionStatus.php +++ b/src/applications/differential/constants/DifferentialRevisionStatus.php @@ -8,10 +8,11 @@ final class DifferentialRevisionStatus extends Phobject { - const COLOR_STATUS_DEFAULT = 'status'; - const COLOR_STATUS_DARK = 'status-dark'; - const COLOR_STATUS_GREEN = 'status-green'; - const COLOR_STATUS_RED = 'status-red'; + const COLOR_STATUS_DEFAULT = 'bluegrey'; + const COLOR_STATUS_DARK = 'indigo'; + const COLOR_STATUS_BLUE = 'blue'; + const COLOR_STATUS_GREEN = 'green'; + const COLOR_STATUS_RED = 'red'; public static function getRevisionStatusColor($status) { $default = self::COLOR_STATUS_DEFAULT; @@ -30,7 +31,7 @@ final class DifferentialRevisionStatus extends Phobject { ArcanistDifferentialRevisionStatus::ABANDONED => self::COLOR_STATUS_DARK, ArcanistDifferentialRevisionStatus::IN_PREPARATION => - self::COLOR_STATUS_DARK, + self::COLOR_STATUS_BLUE, ); return idx($map, $status, $default); } @@ -42,38 +43,30 @@ final class DifferentialRevisionStatus extends Phobject { ArcanistDifferentialRevisionStatus::NEEDS_REVIEW => 'fa-square-o bluegrey', ArcanistDifferentialRevisionStatus::NEEDS_REVISION => - 'fa-refresh red', + 'fa-refresh', ArcanistDifferentialRevisionStatus::CHANGES_PLANNED => - 'fa-headphones red', + 'fa-headphones', ArcanistDifferentialRevisionStatus::ACCEPTED => - 'fa-check green', + 'fa-check', ArcanistDifferentialRevisionStatus::CLOSED => 'fa-check-square-o', ArcanistDifferentialRevisionStatus::ABANDONED => - 'fa-check-square-o', + 'fa-plane', ArcanistDifferentialRevisionStatus::IN_PREPARATION => - 'fa-question-circle blue', + 'fa-question-circle', ); return idx($map, $status, $default); } public static function renderFullDescription($status) { - $color = self::getRevisionStatusColor($status); $status_name = ArcanistDifferentialRevisionStatus::getNameForRevisionStatus($status); - $img = id(new PHUIIconView()) - ->setIcon(self::getRevisionStatusIcon($status)); - - $tag = phutil_tag( - 'span', - array( - 'class' => 'phui-header-status phui-header-'.$color, - ), - array( - $img, - $status_name, - )); + $tag = id(new PHUITagView()) + ->setName($status_name) + ->setIcon(self::getRevisionStatusIcon($status)) + ->setShade(self::getRevisionStatusColor($status)) + ->setType(PHUITagView::TYPE_SHADE); return $tag; } diff --git a/src/applications/differential/controller/DifferentialController.php b/src/applications/differential/controller/DifferentialController.php index 46ac290c09..1aba876c68 100644 --- a/src/applications/differential/controller/DifferentialController.php +++ b/src/applications/differential/controller/DifferentialController.php @@ -190,6 +190,12 @@ abstract class DifferentialController extends PhabricatorController { } } + // Cast duration to a float since it used to be a string in some + // cases. + if (isset($map['duration'])) { + $map['duration'] = (double)$map['duration']; + } + return $map; } diff --git a/src/applications/differential/controller/DifferentialRevisionViewController.php b/src/applications/differential/controller/DifferentialRevisionViewController.php index ccd24220b5..cad2932a0c 100644 --- a/src/applications/differential/controller/DifferentialRevisionViewController.php +++ b/src/applications/differential/controller/DifferentialRevisionViewController.php @@ -481,19 +481,21 @@ final class DifferentialRevisionViewController extends DifferentialController { ->setBaseURI(new PhutilURI('/D'.$revision->getID())) ->setCollapsed((bool)$collapsed) ->build($changesets); - $nav->appendChild($content); - $nav->setCrumbs($crumbs); - $content = $nav; } else { - array_unshift($content, $crumbs); + $nav = null; } - return $this->buildApplicationPage( - $content, - array( - 'title' => $object_id.' '.$revision->getTitle(), - 'pageObjects' => array($revision->getPHID()), - )); + $page = $this->newPage() + ->setTitle($object_id.' '.$revision->getTitle()) + ->setCrumbs($crumbs) + ->setPageObjectPHIDs(array($revision->getPHID())) + ->appendChild($content); + + if ($nav) { + $page->setNavigation($nav); + } + + return $page; } private function getRevisionActions(DifferentialRevision $revision) { @@ -1076,6 +1078,10 @@ final class DifferentialRevisionViewController extends DifferentialController { return null; } + if (!$diff->getBuildable()) { + return null; + } + $interesting_messages = array(); foreach ($diff->getUnitMessages() as $message) { switch ($message->getResult()) { diff --git a/src/applications/differential/parser/DifferentialChangesetParser.php b/src/applications/differential/parser/DifferentialChangesetParser.php index b5e4545d85..f6c7309ec7 100644 --- a/src/applications/differential/parser/DifferentialChangesetParser.php +++ b/src/applications/differential/parser/DifferentialChangesetParser.php @@ -1000,7 +1000,8 @@ final class DifferentialChangesetParser extends Phobject { } } - $this->comments = msort($this->comments, 'getID'); + $this->comments = $this->reorderAndThreadComments($this->comments); + foreach ($this->comments as $comment) { $final = $comment->getLineNumber() + $comment->getLineLength(); @@ -1569,4 +1570,67 @@ final class DifferentialChangesetParser extends Phobject { return array($old_back, $new_back); } + private function reorderAndThreadComments(array $comments) { + $comments = msort($comments, 'getID'); + + // Build an empty map of all the comments we actually have. If a comment + // is a reply but the parent has gone missing, we don't want it to vanish + // completely. + $comment_phids = mpull($comments, 'getPHID'); + $replies = array_fill_keys($comment_phids, array()); + + // Now, remove all comments which are replies, leaving only the top-level + // comments. + foreach ($comments as $key => $comment) { + $reply_phid = $comment->getReplyToCommentPHID(); + if (isset($replies[$reply_phid])) { + $replies[$reply_phid][] = $comment; + unset($comments[$key]); + } + } + + // For each top level comment, add the comment, then add any replies + // to it. Do this recursively so threads are shown in threaded order. + $results = array(); + foreach ($comments as $comment) { + $results[] = $comment; + $phid = $comment->getPHID(); + $descendants = $this->getInlineReplies($replies, $phid, 1); + foreach ($descendants as $descendant) { + $results[] = $descendant; + } + } + + // If we have anything left, they were cyclic references. Just dump + // them in a the end. This should be impossible, but users are very + // creative. + foreach ($replies as $phid => $comments) { + foreach ($comments as $comment) { + $results[] = $comment; + } + } + + return $results; + } + + private function getInlineReplies(array &$replies, $phid, $depth) { + $comments = idx($replies, $phid, array()); + unset($replies[$phid]); + + $results = array(); + foreach ($comments as $comment) { + $results[] = $comment; + $descendants = $this->getInlineReplies( + $replies, + $comment->getPHID(), + $depth + 1); + foreach ($descendants as $descendant) { + $results[] = $descendant; + } + } + + return $results; + } + + } diff --git a/src/applications/differential/query/DifferentialDiffQuery.php b/src/applications/differential/query/DifferentialDiffQuery.php index 1616d58ac8..23c016446c 100644 --- a/src/applications/differential/query/DifferentialDiffQuery.php +++ b/src/applications/differential/query/DifferentialDiffQuery.php @@ -6,7 +6,9 @@ final class DifferentialDiffQuery private $ids; private $phids; private $revisionIDs; + private $needChangesets = false; + private $needProperties; public function withIDs(array $ids) { $this->ids = $ids; @@ -28,19 +30,17 @@ final class DifferentialDiffQuery return $this; } + public function needProperties($need_properties) { + $this->needProperties = $need_properties; + return $this; + } + + public function newResultObject() { + return new DifferentialDiff(); + } + protected function loadPage() { - $table = new DifferentialDiff(); - $conn_r = $table->establishConnection('r'); - - $data = queryfx_all( - $conn_r, - 'SELECT * FROM %T %Q %Q %Q', - $table->getTableName(), - $this->buildWhereClause($conn_r), - $this->buildOrderClause($conn_r), - $this->buildLimitClause($conn_r)); - - return $table->loadAllFromArray($data); + return $this->loadStandardPage($this->newResultObject()); } protected function willFilterPage(array $diffs) { @@ -76,6 +76,23 @@ final class DifferentialDiffQuery return $diffs; } + protected function didFilterPage(array $diffs) { + if ($this->needProperties) { + $properties = id(new DifferentialDiffProperty())->loadAllWhere( + 'diffID IN (%Ld)', + mpull($diffs, 'getID')); + + $properties = mgroup($properties, 'getDiffID'); + foreach ($diffs as $diff) { + $map = idx($properties, $diff->getID(), array()); + $map = mpull($map, 'getData', 'getName'); + $diff->attachDiffProperties($map); + } + } + + return $diffs; + } + private function loadChangesets(array $diffs) { id(new DifferentialChangesetQuery()) ->setViewer($this->getViewer()) @@ -88,32 +105,31 @@ final class DifferentialDiffQuery return $diffs; } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { - $where = array(); + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); if ($this->ids) { $where[] = qsprintf( - $conn_r, + $conn, 'id IN (%Ld)', $this->ids); } if ($this->phids) { $where[] = qsprintf( - $conn_r, + $conn, 'phid IN (%Ls)', $this->phids); } if ($this->revisionIDs) { $where[] = qsprintf( - $conn_r, + $conn, 'revisionID IN (%Ld)', $this->revisionIDs); } - $where[] = $this->buildPagingClause($conn_r); - return $this->formatWhereClause($where); + return $where; } public function getQueryApplicationClass() { diff --git a/src/applications/differential/storage/DifferentialRevision.php b/src/applications/differential/storage/DifferentialRevision.php index e67208c53a..6ca586c747 100644 --- a/src/applications/differential/storage/DifferentialRevision.php +++ b/src/applications/differential/storage/DifferentialRevision.php @@ -485,10 +485,6 @@ final class DifferentialRevision extends DifferentialDAO return false; } - public function shouldShowSubscribersProperty() { - return true; - } - /* -( PhabricatorCustomFieldInterface )------------------------------------ */ diff --git a/src/applications/differential/view/DifferentialAddCommentView.php b/src/applications/differential/view/DifferentialAddCommentView.php index 5524869a77..2d586e7be4 100644 --- a/src/applications/differential/view/DifferentialAddCommentView.php +++ b/src/applications/differential/view/DifferentialAddCommentView.php @@ -50,6 +50,7 @@ final class DifferentialAddCommentView extends AphrontView { } public function render() { + $viewer = $this->getViewer(); $this->requireResource('differential-revision-add-comment-css'); $revision = $this->revision; @@ -73,7 +74,7 @@ final class DifferentialAddCommentView extends AphrontView { $form = new AphrontFormView(); $form ->setWorkflow(true) - ->setUser($this->user) + ->setViewer($viewer) ->setAction($this->actionURI) ->addHiddenInput('revision_id', $revision->getID()) ->appendChild( @@ -108,7 +109,7 @@ final class DifferentialAddCommentView extends AphrontView { ->setID('comment-content') ->setLabel(pht('Comment')) ->setValue($this->draft ? $this->draft->getDraft() : null) - ->setUser($this->user)) + ->setViewer($viewer)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Submit'))); diff --git a/src/applications/differential/view/DifferentialChangesetListView.php b/src/applications/differential/view/DifferentialChangesetListView.php index ec509d62dd..0cd2923018 100644 --- a/src/applications/differential/view/DifferentialChangesetListView.php +++ b/src/applications/differential/view/DifferentialChangesetListView.php @@ -113,6 +113,8 @@ final class DifferentialChangesetListView extends AphrontView { } public function render() { + $viewer = $this->getViewer(); + $this->requireResource('differential-changeset-view-css'); $changesets = $this->changesets; @@ -148,7 +150,7 @@ final class DifferentialChangesetListView extends AphrontView { )); $renderer = DifferentialChangesetParser::getDefaultRendererForViewer( - $this->getUser()); + $viewer); $output = array(); $ids = array(); @@ -163,7 +165,7 @@ final class DifferentialChangesetListView extends AphrontView { $ref = $this->references[$key]; $detail = id(new DifferentialChangesetDetailView()) - ->setUser($this->getUser()); + ->setUser($viewer); $uniq_id = 'diff-'.$changeset->getAnchorName(); $detail->setID($uniq_id); @@ -261,6 +263,7 @@ final class DifferentialChangesetListView extends AphrontView { DifferentialChangesetDetailView $detail, $ref, DifferentialChangeset $changeset) { + $viewer = $this->getViewer(); $meta = array(); @@ -280,7 +283,7 @@ final class DifferentialChangesetListView extends AphrontView { try { $meta['diffusionURI'] = (string)$repository->getDiffusionBrowseURIForPath( - $this->user, + $viewer, $changeset->getAbsoluteRepositoryPath($repository, $this->diff), idx($changeset->getMetadata(), 'line:first'), $this->getBranch()); @@ -308,13 +311,12 @@ final class DifferentialChangesetListView extends AphrontView { } } - $user = $this->user; - if ($user && $repository) { + if ($viewer && $repository) { $path = ltrim( $changeset->getAbsoluteRepositoryPath($repository, $this->diff), '/'); $line = idx($changeset->getMetadata(), 'line:first', 1); - $editor_link = $user->loadEditorLink($path, $line, $repository); + $editor_link = $viewer->loadEditorLink($path, $line, $repository); if ($editor_link) { $meta['editor'] = $editor_link; } else { diff --git a/src/applications/differential/view/DifferentialLocalCommitsView.php b/src/applications/differential/view/DifferentialLocalCommitsView.php index 4da850539d..ebacabc3f3 100644 --- a/src/applications/differential/view/DifferentialLocalCommitsView.php +++ b/src/applications/differential/view/DifferentialLocalCommitsView.php @@ -17,10 +17,7 @@ final class DifferentialLocalCommitsView extends AphrontView { } public function render() { - $user = $this->user; - if (!$user) { - throw new PhutilInvalidStateException('setUser'); - } + $viewer = $this->getViewer(); $local = $this->localCommits; if (!$local) { @@ -94,7 +91,7 @@ final class DifferentialLocalCommitsView extends AphrontView { idx($commit, 'date'), idx($commit, 'time')); if ($date) { - $date = phabricator_datetime($date, $user); + $date = phabricator_datetime($date, $viewer); } $row[] = $date; diff --git a/src/applications/differential/view/DifferentialRevisionListView.php b/src/applications/differential/view/DifferentialRevisionListView.php index 2c14bbc4a5..92394fcb3e 100644 --- a/src/applications/differential/view/DifferentialRevisionListView.php +++ b/src/applications/differential/view/DifferentialRevisionListView.php @@ -57,10 +57,7 @@ final class DifferentialRevisionListView extends AphrontView { } public function render() { - $user = $this->user; - if (!$user) { - throw new PhutilInvalidStateException('setUser'); - } + $viewer = $this->getViewer(); $fresh = PhabricatorEnv::getEnvConfig('differential.days-fresh'); if ($fresh) { @@ -83,12 +80,12 @@ final class DifferentialRevisionListView extends AphrontView { foreach ($this->revisions as $revision) { $item = id(new PHUIObjectItemView()) - ->setUser($user); + ->setUser($viewer); $icons = array(); $phid = $revision->getPHID(); - $flag = $revision->getFlag($user); + $flag = $revision->getFlag($viewer); if ($flag) { $flag_class = PhabricatorFlagColor::getCSSClass($flag->getColor()); $icons['flag'] = phutil_tag( @@ -99,7 +96,7 @@ final class DifferentialRevisionListView extends AphrontView { ''); } - if ($revision->getDrafts($user)) { + if ($revision->getDrafts($viewer)) { $icons['draft'] = true; } diff --git a/src/applications/diffusion/view/DiffusionTagListView.php b/src/applications/diffusion/view/DiffusionTagListView.php index 3b27284f5e..923fa30fc5 100644 --- a/src/applications/diffusion/view/DiffusionTagListView.php +++ b/src/applications/diffusion/view/DiffusionTagListView.php @@ -100,7 +100,7 @@ final class DiffusionTagListView extends DiffusionView { $build, $author, $description, - phabricator_datetime($tag->getEpoch(), $this->user), + phabricator_datetime($tag->getEpoch(), $this->getViewer()), ); } diff --git a/src/applications/doorkeeper/bridge/DoorkeeperBridge.php b/src/applications/doorkeeper/bridge/DoorkeeperBridge.php index 2061150aba..b09a1933c1 100644 --- a/src/applications/doorkeeper/bridge/DoorkeeperBridge.php +++ b/src/applications/doorkeeper/bridge/DoorkeeperBridge.php @@ -3,6 +3,7 @@ abstract class DoorkeeperBridge extends Phobject { private $viewer; + private $context = array(); private $throwOnMissingLink; public function setThrowOnMissingLink($throw_on_missing_link) { @@ -19,6 +20,15 @@ abstract class DoorkeeperBridge extends Phobject { return $this->viewer; } + final public function setContext($context) { + $this->context = $context; + return $this; + } + + final public function getContextProperty($key, $default = null) { + return idx($this->context, $key, $default); + } + public function isEnabled() { return true; } diff --git a/src/applications/doorkeeper/bridge/DoorkeeperBridgeGitHub.php b/src/applications/doorkeeper/bridge/DoorkeeperBridgeGitHub.php new file mode 100644 index 0000000000..19ad54d977 --- /dev/null +++ b/src/applications/doorkeeper/bridge/DoorkeeperBridgeGitHub.php @@ -0,0 +1,50 @@ +getApplicationType() != self::APPTYPE_GITHUB) { + return false; + } + + if ($ref->getApplicationDomain() != self::APPDOMAIN_GITHUB) { + return false; + } + + return true; + } + + protected function getGitHubAccessToken() { + $context_token = $this->getContextProperty('github.token'); + if ($context_token) { + return $context_token->openEnvelope(); + } + + // TODO: Do a bunch of work to fetch the viewer's linked account if + // they have one. + + return $this->didFailOnMissingLink(); + } + + protected function parseGitHubIssueID($id) { + $matches = null; + if (!preg_match('(^([^/]+)/([^/]+)#([1-9]\d*)\z)', $id, $matches)) { + throw new Exception( + pht( + 'GitHub Issue ID "%s" is not properly formatted. Expected an ID '. + 'in the form "owner/repository#123".', + $id)); + } + + return array( + $matches[1], + $matches[2], + (int)$matches[3], + ); + } + + +} diff --git a/src/applications/doorkeeper/bridge/DoorkeeperBridgeGitHubIssue.php b/src/applications/doorkeeper/bridge/DoorkeeperBridgeGitHubIssue.php new file mode 100644 index 0000000000..50dc28bf9d --- /dev/null +++ b/src/applications/doorkeeper/bridge/DoorkeeperBridgeGitHubIssue.php @@ -0,0 +1,97 @@ +getObjectType() !== self::OBJTYPE_GITHUB_ISSUE) { + return false; + } + + return true; + } + + public function pullRefs(array $refs) { + $token = $this->getGitHubAccessToken(); + if (!strlen($token)) { + return null; + } + + $template = id(new PhutilGitHubFuture()) + ->setAccessToken($token); + + $futures = array(); + $id_map = mpull($refs, 'getObjectID', 'getObjectKey'); + foreach ($id_map as $key => $id) { + list($user, $repository, $number) = $this->parseGitHubIssueID($id); + $uri = "/repos/{$user}/{$repository}/issues/{$number}"; + $data = array(); + $futures[$key] = id(clone $template) + ->setRawGitHubQuery($uri, $data); + } + + $results = array(); + $failed = array(); + foreach (new FutureIterator($futures) as $key => $future) { + try { + $results[$key] = $future->resolve(); + } catch (Exception $ex) { + if (($ex instanceof HTTPFutureResponseStatus) && + ($ex->getStatusCode() == 404)) { + // TODO: Do we end up here for deleted objects and invisible + // objects? + } else { + phlog($ex); + $failed[$key] = $ex; + } + } + } + + $viewer = $this->getViewer(); + + foreach ($refs as $ref) { + $ref->setAttribute('name', pht('GitHub Issue %s', $ref->getObjectID())); + + $did_fail = idx($failed, $ref->getObjectKey()); + if ($did_fail) { + $ref->setSyncFailed(true); + continue; + } + + $result = idx($results, $ref->getObjectKey()); + if (!$result) { + continue; + } + + $body = $result->getBody(); + + $ref->setIsVisible(true); + $ref->setAttribute('api.raw', $body); + $ref->setAttribute('name', $body['title']); + + $obj = $ref->getExternalObject(); + if ($obj->getID()) { + continue; + } + + $this->fillObjectFromData($obj, $result); + + $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); + $obj->save(); + unset($unguarded); + } + } + + public function fillObjectFromData(DoorkeeperExternalObject $obj, $result) { + $body = $result->getBody(); + $uri = $body['html_url']; + $obj->setObjectURI($uri); + } + +} diff --git a/src/applications/doorkeeper/engine/DoorkeeperImportEngine.php b/src/applications/doorkeeper/engine/DoorkeeperImportEngine.php index e4e55b2528..8c58f5bd88 100644 --- a/src/applications/doorkeeper/engine/DoorkeeperImportEngine.php +++ b/src/applications/doorkeeper/engine/DoorkeeperImportEngine.php @@ -7,6 +7,7 @@ final class DoorkeeperImportEngine extends Phobject { private $phids = array(); private $localOnly; private $throwOnMissingLink; + private $context = array(); public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; @@ -37,6 +38,10 @@ final class DoorkeeperImportEngine extends Phobject { return $this; } + public function setContextProperty($key, $value) { + $this->context[$key] = $value; + return $this; + } /** * Configure behavior if remote refs can not be retrieved because an @@ -96,6 +101,7 @@ final class DoorkeeperImportEngine extends Phobject { foreach ($bridges as $key => $bridge) { $bridge->setViewer($viewer); $bridge->setThrowOnMissingLink($this->throwOnMissingLink); + $bridge->setContext($this->context); } $working_set = $refs; diff --git a/src/applications/drydock/DrydockCommandError/DrydockCommandError.php b/src/applications/drydock/DrydockCommandError/DrydockCommandError.php deleted file mode 100644 index d9acbe7456..0000000000 --- a/src/applications/drydock/DrydockCommandError/DrydockCommandError.php +++ /dev/null @@ -1,18 +0,0 @@ - $phase, - 'command' => (string)$command, - 'raw' => (string)$ex->getCommand(), - 'err' => $ex->getError(), - 'stdout' => $ex->getStdout(), - 'stderr' => $ex->getStderr(), - ); - return $error; - } -} diff --git a/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php index 8b138405eb..a5d61ec067 100644 --- a/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php +++ b/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php @@ -4,6 +4,8 @@ final class DrydockWorkingCopyBlueprintImplementation extends DrydockBlueprintImplementation { const PHASE_SQUASHMERGE = 'squashmerge'; + const PHASE_REMOTEFETCH = 'blueprint.workingcopy.fetch.remote'; + const PHASE_MERGEFETCH = 'blueprint.workingcopy.fetch.staging'; public function isEnabled() { return true; @@ -240,11 +242,11 @@ final class DrydockWorkingCopyBlueprintImplementation $default = null; foreach ($map as $directory => $spec) { + $interface->pushWorkingDirectory("{$root}/repo/{$directory}/"); + $cmd = array(); $arg = array(); - $interface->pushWorkingDirectory("{$root}/repo/{$directory}/"); - $cmd[] = 'git clean -d --force'; $cmd[] = 'git fetch'; @@ -266,7 +268,20 @@ final class DrydockWorkingCopyBlueprintImplementation $cmd[] = 'git reset --hard origin/%s'; $arg[] = $branch; - } else if ($ref) { + } + + $this->execxv($interface, $cmd, $arg); + + if (idx($spec, 'default')) { + $default = $directory; + } + + // If we're fetching a ref from a remote, do that separately so we can + // raise a more tailored error. + if ($ref) { + $cmd = array(); + $arg = array(); + $ref_uri = $ref['uri']; $ref_ref = $ref['ref']; @@ -277,17 +292,25 @@ final class DrydockWorkingCopyBlueprintImplementation $cmd[] = 'git checkout %s --'; $arg[] = $ref_ref; - } - $cmd = implode(' && ', $cmd); - $argv = array_merge(array($cmd), $arg); + try { + $this->execxv($interface, $cmd, $arg); + } catch (CommandException $ex) { + $display_command = csprintf( + 'git fetch %R %R', + $ref_uri, + $ref_ref); - $result = call_user_func_array( - array($interface, 'execx'), - $argv); + $error = DrydockCommandError::newFromCommandException($ex) + ->setPhase(self::PHASE_REMOTEFETCH) + ->setDisplayCommand($display_command); - if (idx($spec, 'default')) { - $default = $directory; + $lease->setAttribute( + 'workingcopy.vcs.error', + $error->toDictionary()); + + throw $ex; + } } $merges = idx($spec, 'merges'); @@ -428,11 +451,29 @@ final class DrydockWorkingCopyBlueprintImplementation $src_uri = $merge['src.uri']; $src_ref = $merge['src.ref']; - $interface->execx( - 'git fetch --no-tags -- %s +%s:%s', - $src_uri, - $src_ref, - $src_ref); + + try { + $interface->execx( + 'git fetch --no-tags -- %s +%s:%s', + $src_uri, + $src_ref, + $src_ref); + } catch (CommandException $ex) { + $display_command = csprintf( + 'git fetch %R +%R:%R', + $src_uri, + $src_ref, + $src_ref); + + $error = DrydockCommandError::newFromCommandException($ex) + ->setPhase(self::PHASE_MERGEFETCH) + ->setDisplayCommand($display_command); + + $lease->setAttribute('workingcopy.vcs.error', $error->toDictionary()); + + throw $ex; + } + // NOTE: This can never actually generate a commit because we pass // "--squash", but git sometimes runs code to check that a username and @@ -443,32 +484,36 @@ final class DrydockWorkingCopyBlueprintImplementation 'drydock@phabricator', $src_ref); - // Show the user a simplified command if the operation fails and we need to - // report an error. - $show_command = csprintf( - 'git merge --squash -- %R', - $src_ref); - try { $interface->execx('%C', $real_command); } catch (CommandException $ex) { - $error = DrydockCommandError::newFromCommandException( - self::PHASE_SQUASHMERGE, - $show_command, - $ex); + $display_command = csprintf( + 'git merge --squash %R', + $src_ref); - $lease->setAttribute('workingcopy.vcs.error', $error); + $error = DrydockCommandError::newFromCommandException($ex) + ->setPhase(self::PHASE_SQUASHMERGE) + ->setDisplayCommand($display_command); + + $lease->setAttribute('workingcopy.vcs.error', $error->toDictionary()); throw $ex; } } public function getCommandError(DrydockLease $lease) { - $error = $lease->getAttribute('workingcopy.vcs.error'); - if (!$error) { - return null; - } else { - return $error; - } + return $lease->getAttribute('workingcopy.vcs.error'); } + private function execxv( + DrydockCommandInterface $interface, + array $commands, + array $arguments) { + + $commands = implode(' && ', $commands); + $argv = array_merge(array($commands), $arguments); + + return call_user_func_array(array($interface, 'execx'), $argv); + } + + } diff --git a/src/applications/drydock/exception/DrydockCommandError.php b/src/applications/drydock/exception/DrydockCommandError.php new file mode 100644 index 0000000000..26314d1f89 --- /dev/null +++ b/src/applications/drydock/exception/DrydockCommandError.php @@ -0,0 +1,58 @@ +command = (string)$ex->getCommand(); + + $error->error = $ex->getError(); + $error->stdout = $ex->getStdout(); + $error->stderr = $ex->getStderr(); + + return $error; + } + + public function setPhase($phase) { + $this->phase = $phase; + return $this; + } + + public function getPhase() { + return $this->phase; + } + + public function setDisplayCommand($display_command) { + $this->displayCommand = (string)$display_command; + return $this; + } + + public function getDisplayCommand() { + return $this->displayCommand; + } + + public function toDictionary() { + $display_command = $this->getDisplayCommand(); + if ($display_command === null) { + $display_command = $this->command; + } + + return array( + 'phase' => $this->getPhase(), + 'command' => $display_command, + 'raw' => $this->command, + 'err' => $this->error, + 'stdout' => $this->stdout, + 'stderr' => $this->stderr, + ); + } + +} diff --git a/src/applications/drydock/operation/DrydockLandRepositoryOperation.php b/src/applications/drydock/operation/DrydockLandRepositoryOperation.php index 5beb18d07b..4e306772a3 100644 --- a/src/applications/drydock/operation/DrydockLandRepositoryOperation.php +++ b/src/applications/drydock/operation/DrydockLandRepositoryOperation.php @@ -4,7 +4,9 @@ final class DrydockLandRepositoryOperation extends DrydockRepositoryOperationType { const OPCONST = 'land'; - const PHASE_PUSH = 'push'; + + const PHASE_PUSH = 'op.land.push'; + const PHASE_COMMIT = 'op.land.commit'; public function getOperationDescription( DrydockRepositoryOperation $operation, @@ -119,25 +121,42 @@ final class DrydockLandRepositoryOperation $committer_info['email'], "{$author_name} <{$author_email}>"); - $future - ->write($commit_message) - ->resolvex(); + $future->write($commit_message); try { - $interface->execx( - 'git push origin -- %s:%s', - 'HEAD', - $push_dst); + $future->resolvex(); } catch (CommandException $ex) { - $show_command = csprintf( - 'git push origin -- %s:%s', - 'HEAD', - $push_dst); - $error = DrydockCommandError::newFromCommandException( - self::PHASE_PUSH, - $show_command, - $ex); - $operation->setCommandError($error); + $display_command = csprintf('git commit'); + + // TODO: One reason this can fail is if the changes have already been + // merged. We could try to detect that. + + $error = DrydockCommandError::newFromCommandException($ex) + ->setPhase(self::PHASE_COMMIT) + ->setDisplayCommand($display_command); + + $operation->setCommandError($error->toDictionary()); + + throw $ex; + } + + try { + $interface->execx( + 'git push origin -- %s:%s', + 'HEAD', + $push_dst); + } catch (CommandException $ex) { + $display_command = csprintf( + 'git push origin %R:%R', + 'HEAD', + $push_dst); + + $error = DrydockCommandError::newFromCommandException($ex) + ->setPhase(self::PHASE_PUSH) + ->setDisplayCommand($display_command); + + $operation->setCommandError($error->toDictionary()); + throw $ex; } } @@ -229,6 +248,29 @@ final class DrydockLandRepositoryOperation ); } + // Check if this diff was pushed to a staging area. + $diff = id(new DifferentialDiffQuery()) + ->setViewer($viewer) + ->withIDs(array($revision->getActiveDiff()->getID())) + ->needProperties(true) + ->executeOne(); + + // Older diffs won't have this property. They may still have been pushed. + // At least for now, assume staging changes are present if the property + // is missing. This should smooth the transition to the more formal + // approach. + $has_staging = $diff->hasDiffProperty('arc.staging'); + if ($has_staging) { + $staging = $diff->getProperty('arc.staging'); + if (!is_array($staging)) { + $staging = array(); + } + $status = idx($staging, 'status'); + if ($status != ArcanistDiffWorkflow::STAGING_PUSHED) { + return $this->getBarrierToLandingFromStagingStatus($status); + } + } + // TODO: At some point we should allow installs to give "land reviewed // code" permission to more users than "push any commit", because it is // a much less powerful operation. For now, just require push so this @@ -317,4 +359,85 @@ final class DrydockLandRepositoryOperation return null; } + private function getBarrierToLandingFromStagingStatus($status) { + switch ($status) { + case ArcanistDiffWorkflow::STAGING_USER_SKIP: + return array( + 'title' => pht('Staging Area Skipped'), + 'body' => pht( + 'The diff author used the %s flag to skip pushing this change to '. + 'staging. Changes must be pushed to staging before they can be '. + 'landed from the web.', + phutil_tag('tt', array(), '--skip-staging')), + ); + case ArcanistDiffWorkflow::STAGING_DIFF_RAW: + return array( + 'title' => pht('Raw Diff Source'), + 'body' => pht( + 'The diff was generated from a raw input source, so the change '. + 'could not be pushed to staging. Changes must be pushed to '. + 'staging before they can be landed from the web.'), + ); + case ArcanistDiffWorkflow::STAGING_REPOSITORY_UNKNOWN: + return array( + 'title' => pht('Unknown Repository'), + 'body' => pht( + 'When the diff was generated, the client was not able to '. + 'determine which repository it belonged to, so the change '. + 'was not pushed to staging. Changes must be pushed to staging '. + 'before they can be landed from the web.'), + ); + case ArcanistDiffWorkflow::STAGING_REPOSITORY_UNAVAILABLE: + return array( + 'title' => pht('Staging Unavailable'), + 'body' => pht( + 'When this diff was generated, the server was running an older '. + 'version of Phabricator which did not support staging areas, so '. + 'the change was not pushed to staging. Changes must be pushed '. + 'to staging before they can be landed from the web.'), + ); + case ArcanistDiffWorkflow::STAGING_REPOSITORY_UNSUPPORTED: + return array( + 'title' => pht('Repository Unsupported'), + 'body' => pht( + 'When this diff was generated, the server was running an older '. + 'version of Phabricator which did not support staging areas for '. + 'this version control system, so the chagne was not pushed to '. + 'staging. Changes must be pushed to staging before they can be '. + 'landed from the web.'), + ); + + case ArcanistDiffWorkflow::STAGING_REPOSITORY_UNCONFIGURED: + return array( + 'title' => pht('Repository Unconfigured'), + 'body' => pht( + 'When this diff was generated, the repository was not configured '. + 'with a staging area, so the change was not pushed to staging. '. + 'Changes must be pushed to staging before they can be landed '. + 'from the web.'), + ); + case ArcanistDiffWorkflow::STAGING_CLIENT_UNSUPPORTED: + return array( + 'title' => pht('Client Support Unavailable'), + 'body' => pht( + 'When this diff was generated, the client did not support '. + 'staging areas for this version control system, so the change '. + 'was not pushed to staging. Changes must be pushed to staging '. + 'before they can be landed from the web. Updating the client '. + 'may resolve this issue.'), + ); + default: + return array( + 'title' => pht('Unknown Error'), + 'body' => pht( + 'When this diff was generated, it was not pushed to staging for '. + 'an unknown reason (the status code was "%s"). Changes must be '. + 'pushed to staging before they can be landed from the web. '. + 'The server may be running an out-of-date version of Phabricator, '. + 'and updating may provide more information about this error.', + $status), + ); + } + } + } diff --git a/src/applications/drydock/storage/DrydockBlueprint.php b/src/applications/drydock/storage/DrydockBlueprint.php index a7deb73cc3..87ea777f72 100644 --- a/src/applications/drydock/storage/DrydockBlueprint.php +++ b/src/applications/drydock/storage/DrydockBlueprint.php @@ -350,7 +350,7 @@ final class DrydockBlueprint extends DrydockDAO } -/* -( PhabricatorNgramInterface )------------------------------------------ */ +/* -( PhabricatorNgramsInterface )----------------------------------------- */ public function newNgrams() { diff --git a/src/applications/drydock/view/DrydockRepositoryOperationStatusView.php b/src/applications/drydock/view/DrydockRepositoryOperationStatusView.php index 23ad6b81fe..d84b4cc0e7 100644 --- a/src/applications/drydock/view/DrydockRepositoryOperationStatusView.php +++ b/src/applications/drydock/view/DrydockRepositoryOperationStatusView.php @@ -82,6 +82,20 @@ final class DrydockRepositoryOperationStatusView 'This change did not merge cleanly. This usually indicates '. 'that the change is out of date and needs to be updated.'); break; + case DrydockWorkingCopyBlueprintImplementation::PHASE_REMOTEFETCH: + $message = pht( + 'This change could not be fetched from the remote.'); + break; + case DrydockWorkingCopyBlueprintImplementation::PHASE_MERGEFETCH: + $message = pht( + 'This change could not be fetched from the remote staging '. + 'area. It may not have been pushed, or may have been removed.'); + break; + case DrydockLandRepositoryOperation::PHASE_COMMIT: + $message = pht( + 'Committing this change failed. It may already have been '. + 'merged.'); + break; case DrydockLandRepositoryOperation::PHASE_PUSH: $message = pht( 'The push failed. This usually indicates '. @@ -123,10 +137,23 @@ final class DrydockRepositoryOperationStatusView private function renderVCSErrorTable(array $vcs_error) { $rows = array(); - $rows[] = array(pht('Command'), $vcs_error['command']); + + $rows[] = array( + pht('Command'), + phutil_censor_credentials($vcs_error['command']), + ); + $rows[] = array(pht('Error'), $vcs_error['err']); - $rows[] = array(pht('Stdout'), $vcs_error['stdout']); - $rows[] = array(pht('Stderr'), $vcs_error['stderr']); + + $rows[] = array( + pht('Stdout'), + phutil_censor_credentials($vcs_error['stdout']), + ); + + $rows[] = array( + pht('Stderr'), + phutil_censor_credentials($vcs_error['stderr']), + ); $table = id(new AphrontTableView($rows)) ->setColumnClasses( diff --git a/src/applications/files/storage/PhabricatorFile.php b/src/applications/files/storage/PhabricatorFile.php index 7d2780778f..28328796ce 100644 --- a/src/applications/files/storage/PhabricatorFile.php +++ b/src/applications/files/storage/PhabricatorFile.php @@ -1348,10 +1348,6 @@ final class PhabricatorFile extends PhabricatorFileDAO return ($this->authorPHID == $phid); } - public function shouldShowSubscribersProperty() { - return true; - } - /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ diff --git a/src/applications/fund/controller/FundInitiativeViewController.php b/src/applications/fund/controller/FundInitiativeViewController.php index f4535d574e..a416054ae3 100644 --- a/src/applications/fund/controller/FundInitiativeViewController.php +++ b/src/applications/fund/controller/FundInitiativeViewController.php @@ -46,8 +46,7 @@ final class FundInitiativeViewController ->setStatus($status_icon, $status_color, $status_name) ->setHeaderIcon('fa-heart'); - $properties = $this->buildPropertyListView($initiative); - $actions = $this->buildActionListView($initiative); + $curtain = $this->buildCurtain($initiative); $details = $this->buildPropertySectionView($initiative); $timeline = $this->buildTransactionTimeline( @@ -57,31 +56,15 @@ final class FundInitiativeViewController $view = id(new PHUITwoColumnView()) ->setHeader($header) + ->setCurtain($curtain) ->setMainColumn($timeline) - ->setPropertyList($properties) - ->addPropertySection(pht('DETAILS'), $details) - ->setActionList($actions); + ->addPropertySection(pht('DETAILS'), $details); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) ->setPageObjectPHIDs(array($initiative->getPHID())) - ->appendChild( - array( - $view, - )); - } - - private function buildPropertyListView(FundInitiative $initiative) { - $viewer = $this->getRequest()->getUser(); - - $view = id(new PHUIPropertyListView()) - ->setUser($viewer) - ->setObject($initiative); - - $view->invokeWillRenderEvent(); - - return $view; + ->appendChild($view); } private function buildPropertySectionView(FundInitiative $initiative) { @@ -124,8 +107,9 @@ final class FundInitiativeViewController return $view; } - private function buildActionListView(FundInitiative $initiative) { - $viewer = $this->getRequest()->getUser(); + private function buildCurtain(FundInitiative $initiative) { + $viewer = $this->getViewer(); + $id = $initiative->getID(); $can_edit = PhabricatorPolicyFilter::hasCapability( @@ -133,11 +117,9 @@ final class FundInitiativeViewController $initiative, PhabricatorPolicyCapability::CAN_EDIT); - $view = id(new PhabricatorActionListView()) - ->setUser($viewer) - ->setObject($initiative); + $curtain = $this->newCurtainView($initiative); - $view->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Initiative')) ->setIcon('fa-pencil') @@ -153,7 +135,7 @@ final class FundInitiativeViewController $close_icon = 'fa-times'; } - $view->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setName($close_name) ->setIcon($close_icon) @@ -161,7 +143,7 @@ final class FundInitiativeViewController ->setWorkflow(true) ->setHref($this->getApplicationURI("/close/{$id}/"))); - $view->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Back Initiative')) ->setIcon('fa-money') @@ -169,13 +151,13 @@ final class FundInitiativeViewController ->setWorkflow(true) ->setHref($this->getApplicationURI("/back/{$id}/"))); - $view->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('View Backers')) ->setIcon('fa-bank') ->setHref($this->getApplicationURI("/backers/{$id}/"))); - return $view; + return $curtain; } } diff --git a/src/applications/fund/storage/FundInitiative.php b/src/applications/fund/storage/FundInitiative.php index 3375551443..517c377c44 100644 --- a/src/applications/fund/storage/FundInitiative.php +++ b/src/applications/fund/storage/FundInitiative.php @@ -178,10 +178,6 @@ final class FundInitiative extends FundDAO return ($phid == $this->getOwnerPHID()); } - public function shouldShowSubscribersProperty() { - return true; - } - /* -( PhabricatorTokenRecevierInterface )---------------------------------- */ diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuildLog.php b/src/applications/harbormaster/storage/build/HarbormasterBuildLog.php index 40090d0ac3..290b288c6c 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuildLog.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuildLog.php @@ -114,6 +114,8 @@ final class HarbormasterBuildLog $this->rope->append($content); $this->flush(); + + return $this; } private function flush() { diff --git a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php index c5b2142ac5..2a101ca355 100644 --- a/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php +++ b/src/applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php @@ -127,10 +127,6 @@ final class HarbormasterBuildPlan extends HarbormasterDAO return false; } - public function shouldShowSubscribersProperty() { - return true; - } - /* -( PhabricatorApplicationTransactionInterface )------------------------- */ @@ -201,7 +197,7 @@ final class HarbormasterBuildPlan extends HarbormasterDAO } -/* -( PhabricatorNgramInterface )------------------------------------------ */ +/* -( PhabricatorNgramsInterface )----------------------------------------- */ public function newNgrams() { diff --git a/src/applications/herald/controller/HeraldRuleViewController.php b/src/applications/herald/controller/HeraldRuleViewController.php index 4036be250b..f5e058d3f3 100644 --- a/src/applications/herald/controller/HeraldRuleViewController.php +++ b/src/applications/herald/controller/HeraldRuleViewController.php @@ -33,8 +33,7 @@ final class HeraldRuleViewController extends HeraldController { pht('Active')); } - $actions = $this->buildActionView($rule); - $properties = $this->buildPropertyView($rule); + $curtain = $this->buildCurtain($rule); $details = $this->buildPropertySectionView($rule); $description = $this->buildDescriptionView($rule); @@ -44,10 +43,6 @@ final class HeraldRuleViewController extends HeraldController { $crumbs->addTextCrumb("H{$id}"); $crumbs->setBorder(true); - $object_box = id(new PHUIObjectBoxView()) - ->setHeader($header) - ->addPropertyList($properties); - $timeline = $this->buildTransactionTimeline( $rule, new HeraldTransactionQuery()); @@ -57,35 +52,30 @@ final class HeraldRuleViewController extends HeraldController { $view = id(new PHUITwoColumnView()) ->setHeader($header) + ->setCurtain($curtain) ->setMainColumn($timeline) ->addPropertySection(pht('DETAILS'), $details) - ->addPropertySection(pht('DESCRIPTION'), $description) - ->setPropertyList($properties) - ->setActionList($actions); + ->addPropertySection(pht('DESCRIPTION'), $description); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) - ->appendChild( - array( - $view, - )); + ->appendChild($view); } - private function buildActionView(HeraldRule $rule) { - $viewer = $this->getRequest()->getUser(); - $id = $rule->getID(); + private function buildCurtain(HeraldRule $rule) { + $viewer = $this->getViewer(); - $view = id(new PhabricatorActionListView()) - ->setUser($viewer) - ->setObject($rule); + $id = $rule->getID(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $rule, PhabricatorPolicyCapability::CAN_EDIT); - $view->addAction( + $curtain = $this->newCurtainView($rule); + + $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Rule')) ->setHref($this->getApplicationURI("edit/{$id}/")) @@ -103,7 +93,7 @@ final class HeraldRuleViewController extends HeraldController { $disable_name = pht('Archive Rule'); } - $view->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Disable Rule')) ->setHref($this->getApplicationURI($disable_uri)) @@ -112,23 +102,10 @@ final class HeraldRuleViewController extends HeraldController { ->setDisabled(!$can_edit) ->setWorkflow(true)); - return $view; + return $curtain; } - private function buildPropertyView( - HeraldRule $rule) { - - $viewer = $this->getRequest()->getUser(); - $view = id(new PHUIPropertyListView()) - ->setUser($viewer) - ->setObject($rule); - - $view->invokeWillRenderEvent(); - - return $view; - } - - private function buildPropertySectionView( + private function buildPropertySectionView( HeraldRule $rule) { $viewer = $this->getRequest()->getUser(); diff --git a/src/applications/herald/storage/HeraldRule.php b/src/applications/herald/storage/HeraldRule.php index 8d585263c0..9902e952ef 100644 --- a/src/applications/herald/storage/HeraldRule.php +++ b/src/applications/herald/storage/HeraldRule.php @@ -328,10 +328,6 @@ final class HeraldRule extends HeraldDAO return $this->isPersonalRule() && $phid == $this->getAuthorPHID(); } - public function shouldShowSubscribersProperty() { - return true; - } - /* -( PhabricatorDestructibleInterface )----------------------------------- */ diff --git a/src/applications/home/controller/PhabricatorHomeMainController.php b/src/applications/home/controller/PhabricatorHomeMainController.php index 1b8557fc7f..e53c4fac8d 100644 --- a/src/applications/home/controller/PhabricatorHomeMainController.php +++ b/src/applications/home/controller/PhabricatorHomeMainController.php @@ -322,7 +322,7 @@ final class PhabricatorHomeMainController extends PhabricatorHomeController { ->setHref($href); $header = id(new PHUIHeaderView()) ->setHeader($title) - ->addActionIcon($icon); + ->addActionItem($icon); return $header; } diff --git a/src/applications/legalpad/storage/LegalpadDocument.php b/src/applications/legalpad/storage/LegalpadDocument.php index 51f158a978..fcecac991f 100644 --- a/src/applications/legalpad/storage/LegalpadDocument.php +++ b/src/applications/legalpad/storage/LegalpadDocument.php @@ -163,10 +163,6 @@ final class LegalpadDocument extends LegalpadDAO return ($this->creatorPHID == $phid); } - public function shouldShowSubscribersProperty() { - return true; - } - /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/applications/macro/controller/PhabricatorMacroViewController.php b/src/applications/macro/controller/PhabricatorMacroViewController.php index 4e43c796cf..8c12e5528a 100644 --- a/src/applications/macro/controller/PhabricatorMacroViewController.php +++ b/src/applications/macro/controller/PhabricatorMacroViewController.php @@ -23,9 +23,8 @@ final class PhabricatorMacroViewController $title_short = pht('Macro "%s"', $macro->getName()); $title_long = pht('Image Macro "%s"', $macro->getName()); - $actions = $this->buildActionView($macro); + $curtain = $this->buildCurtain($macro); $subheader = $this->buildSubheaderView($macro); - $properties = $this->buildPropertyView($macro); $file = $this->buildFileView($macro); $details = $this->buildPropertySectionView($macro); @@ -40,7 +39,8 @@ final class PhabricatorMacroViewController $header = id(new PHUIHeaderView()) ->setUser($viewer) ->setPolicyObject($macro) - ->setHeader($title_long); + ->setHeader($macro->getName()) + ->setHeaderIcon('fa-file-image-o'); if (!$macro->getIsDisabled()) { $header->setStatus('fa-check', 'bluegrey', pht('Active')); @@ -67,35 +67,29 @@ final class PhabricatorMacroViewController $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setSubheader($subheader) + ->setCurtain($curtain) ->setMainColumn(array( $timeline, $add_comment_form, )) ->addPropertySection(pht('MACRO'), $file) - ->addPropertySection(pht('DETAILS'), $details) - ->setPropertyList($properties) - ->setActionList($actions); + ->addPropertySection(pht('DETAILS'), $details); return $this->newPage() ->setTitle($title_short) ->setCrumbs($crumbs) ->setPageObjectPHIDs(array($macro->getPHID())) - ->appendChild( - array( - $view, - )); + ->appendChild($view); } - private function buildActionView( + private function buildCurtain( PhabricatorFileImageMacro $macro) { $can_manage = $this->hasApplicationCapability( PhabricatorMacroManageCapability::CAPABILITY); - $request = $this->getRequest(); - $view = id(new PhabricatorActionListView()) - ->setUser($request->getUser()) - ->setObject($macro) - ->addAction( + $curtain = $this->newCurtainView($macro); + + $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Macro')) ->setHref($this->getApplicationURI('/edit/'.$macro->getID().'/')) @@ -103,7 +97,7 @@ final class PhabricatorMacroViewController ->setWorkflow(!$can_manage) ->setIcon('fa-pencil')); - $view->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Audio')) ->setHref($this->getApplicationURI('/audio/'.$macro->getID().'/')) @@ -112,7 +106,7 @@ final class PhabricatorMacroViewController ->setIcon('fa-music')); if ($macro->getIsDisabled()) { - $view->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Activate Macro')) ->setHref($this->getApplicationURI('/disable/'.$macro->getID().'/')) @@ -120,7 +114,7 @@ final class PhabricatorMacroViewController ->setDisabled(!$can_manage) ->setIcon('fa-check')); } else { - $view->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Archive Macro')) ->setHref($this->getApplicationURI('/disable/'.$macro->getID().'/')) @@ -129,7 +123,7 @@ final class PhabricatorMacroViewController ->setIcon('fa-ban')); } - return $view; + return $curtain; } private function buildSubheaderView( @@ -177,7 +171,11 @@ final class PhabricatorMacroViewController $viewer->renderHandle($audio_phid)); } - return $view; + if ($view->hasAnyProperties()) { + return $view; + } + + return null; } private function buildFileView( @@ -201,17 +199,4 @@ final class PhabricatorMacroViewController return null; } - private function buildPropertyView( - PhabricatorFileImageMacro $macro) { - $viewer = $this->getViewer(); - - $view = id(new PHUIPropertyListView()) - ->setUser($this->getRequest()->getUser()) - ->setObject($macro); - - $view->invokeWillRenderEvent(); - - return $view; - } - } diff --git a/src/applications/macro/storage/PhabricatorFileImageMacro.php b/src/applications/macro/storage/PhabricatorFileImageMacro.php index 5cf23133a3..37f0d5c06b 100644 --- a/src/applications/macro/storage/PhabricatorFileImageMacro.php +++ b/src/applications/macro/storage/PhabricatorFileImageMacro.php @@ -111,10 +111,6 @@ final class PhabricatorFileImageMacro extends PhabricatorFileDAO return false; } - public function shouldShowSubscribersProperty() { - return true; - } - /* -( PhabricatorTokenRecevierInterface )---------------------------------- */ diff --git a/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php b/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php index 724309175d..dc140d380a 100644 --- a/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php +++ b/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php @@ -77,6 +77,7 @@ final class PhabricatorManiphestConfigOptions 'name.full' => pht('Closed, Resolved'), 'closed' => true, 'special' => ManiphestTaskStatus::SPECIAL_CLOSED, + 'transaction.icon' => 'fa-check-circle', 'prefixes' => array( 'closed', 'closes', @@ -97,6 +98,7 @@ final class PhabricatorManiphestConfigOptions 'wontfix' => array( 'name' => pht('Wontfix'), 'name.full' => pht('Closed, Wontfix'), + 'transaction.icon' => 'fa-ban', 'closed' => true, 'prefixes' => array( 'wontfix', @@ -110,6 +112,7 @@ final class PhabricatorManiphestConfigOptions 'invalid' => array( 'name' => pht('Invalid'), 'name.full' => pht('Closed, Invalid'), + 'transaction.icon' => 'fa-minus-circle', 'closed' => true, 'claim' => false, 'prefixes' => array( diff --git a/src/applications/maniphest/constants/ManiphestTaskStatus.php b/src/applications/maniphest/constants/ManiphestTaskStatus.php index 3a839d8fac..eeeb4cf165 100644 --- a/src/applications/maniphest/constants/ManiphestTaskStatus.php +++ b/src/applications/maniphest/constants/ManiphestTaskStatus.php @@ -82,29 +82,22 @@ final class ManiphestTaskStatus extends ManiphestConstants { return self::getStatusAttribute($status, 'name', pht('Unknown Status')); } - public static function renderFullDescription($status) { + public static function renderFullDescription($status, $priority) { if (self::isOpenStatus($status)) { - $color = 'status'; - $icon_color = 'bluegrey'; + $name = pht('%s, %s', self::getTaskStatusFullName($status), $priority); + $color = 'grey'; + $icon = 'fa-square-o'; } else { - $color = 'status-dark'; - $icon_color = ''; + $name = self::getTaskStatusFullName($status); + $color = 'indigo'; + $icon = 'fa-check-square-o'; } - $icon = self::getStatusIcon($status); - - $img = id(new PHUIIconView()) - ->setIcon($icon.' '.$icon_color); - - $tag = phutil_tag( - 'span', - array( - 'class' => 'phui-header-status phui-header-'.$color, - ), - array( - $img, - self::getTaskStatusFullName($status), - )); + $tag = id(new PHUITagView()) + ->setName($name) + ->setIcon($icon) + ->setType(PHUITagView::TYPE_SHADE) + ->setShade($color); return $tag; } diff --git a/src/applications/maniphest/controller/ManiphestTaskDetailController.php b/src/applications/maniphest/controller/ManiphestTaskDetailController.php index a15b8f4594..ceda169489 100644 --- a/src/applications/maniphest/controller/ManiphestTaskDetailController.php +++ b/src/applications/maniphest/controller/ManiphestTaskDetailController.php @@ -26,6 +26,10 @@ final class ManiphestTaskDetailController extends ManiphestController { ->setViewer($viewer) ->readFieldsFromStorage($task); + $edit_engine = id(new ManiphestEditEngine()) + ->setViewer($viewer) + ->setTargetObject($task); + $e_commit = ManiphestTaskHasCommitEdgeType::EDGECONST; $e_dep_on = ManiphestTaskDependsOnTaskEdgeType::EDGECONST; $e_dep_by = ManiphestTaskDependedOnByTaskEdgeType::EDGECONST; @@ -65,34 +69,34 @@ final class ManiphestTaskDetailController extends ManiphestController { new ManiphestTransactionQuery(), $engine); - $actions = $this->buildActionView($task); - $monogram = $task->getMonogram(); $crumbs = $this->buildApplicationCrumbs() - ->addTextCrumb($monogram, '/'.$monogram); + ->addTextCrumb($monogram) + ->setBorder(true); $header = $this->buildHeaderView($task); - $properties = $this->buildPropertyView( - $task, $field_list, $edges, $actions, $handles); + $details = $this->buildPropertyView($task, $field_list, $edges, $handles); $description = $this->buildDescriptionView($task, $engine); - - $object_box = id(new PHUIObjectBoxView()) - ->setHeader($header) - ->addPropertyList($properties); - - if ($description) { - $object_box->addPropertyList($description); - } + $curtain = $this->buildCurtain($task, $edit_engine); $title = pht('%s %s', $monogram, $task->getTitle()); - $comment_view = id(new ManiphestEditEngine()) - ->setViewer($viewer) + $comment_view = $edit_engine ->buildEditEngineCommentView($task); $timeline->setQuoteRef($monogram); $comment_view->setTransactionTimeline($timeline); + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setCurtain($curtain) + ->setMainColumn(array( + $timeline, + $comment_view, + )) + ->addPropertySection(pht('DESCRIPTION'), $description) + ->addPropertySection(pht('DETAILS'), $details); + return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) @@ -102,10 +106,9 @@ final class ManiphestTaskDetailController extends ManiphestController { )) ->appendChild( array( - $object_box, - $timeline, - $comment_view, - )); + $view, + )); + } private function buildHeaderView(ManiphestTask $task) { @@ -114,17 +117,42 @@ final class ManiphestTaskDetailController extends ManiphestController { ->setUser($this->getRequest()->getUser()) ->setPolicyObject($task); - $status = $task->getStatus(); - $status_name = ManiphestTaskStatus::renderFullDescription($status); + $priority_name = ManiphestTaskPriority::getTaskPriorityName( + $task->getPriority()); + $priority_color = ManiphestTaskPriority::getTaskPriorityColor( + $task->getPriority()); + $status = $task->getStatus(); + $status_name = ManiphestTaskStatus::renderFullDescription( + $status, $priority_name, $priority_color); $view->addProperty(PHUIHeaderView::PROPERTY_STATUS, $status_name); + $view->setHeaderIcon(ManiphestTaskStatus::getStatusIcon( + $task->getStatus()).' '.$priority_color); + + if (ManiphestTaskPoints::getIsEnabled()) { + $points = $task->getPoints(); + if ($points !== null) { + $points_name = pht('%s %s', + $task->getPoints(), + ManiphestTaskPoints::getPointsLabel()); + $tag = id(new PHUITagView()) + ->setName($points_name) + ->setShade('blue') + ->setType(PHUITagView::TYPE_SHADE); + + $view->addTag($tag); + } + } + return $view; } - private function buildActionView(ManiphestTask $task) { - $viewer = $this->getRequest()->getUser(); + private function buildCurtain( + ManiphestTask $task, + PhabricatorEditEngine $edit_engine) { + $viewer = $this->getViewer(); $id = $task->getID(); $phid = $task->getPHID(); @@ -134,11 +162,9 @@ final class ManiphestTaskDetailController extends ManiphestController { $task, PhabricatorPolicyCapability::CAN_EDIT); - $view = id(new PhabricatorActionListView()) - ->setUser($viewer) - ->setObject($task); + $curtain = $this->newCurtainView($task); - $view->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Task')) ->setIcon('fa-pencil') @@ -146,7 +172,7 @@ final class ManiphestTaskDetailController extends ManiphestController { ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); - $view->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Merge Duplicates In')) ->setHref("/search/attach/{$phid}/TASK/merge/") @@ -155,11 +181,12 @@ final class ManiphestTaskDetailController extends ManiphestController { ->setDisabled(!$can_edit) ->setWorkflow(true)); - $edit_config = id(new ManiphestEditEngine()) - ->setViewer($viewer) - ->loadDefaultEditConfiguration(); - + $edit_config = $edit_engine->loadDefaultEditConfiguration(); $can_create = (bool)$edit_config; + + $can_reassign = $edit_engine->hasEditAccessToTransaction( + ManiphestTransaction::TYPE_OWNER); + if ($can_create) { $form_key = $edit_config->getIdentifier(); $edit_uri = id(new PhutilURI("/task/edit/form/{$form_key}/")) @@ -174,7 +201,7 @@ final class ManiphestTaskDetailController extends ManiphestController { $edit_uri = $this->getApplicationURI($edit_uri); } - $view->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Create Subtask')) ->setHref($edit_uri) @@ -182,7 +209,7 @@ final class ManiphestTaskDetailController extends ManiphestController { ->setDisabled(!$can_create) ->setWorkflow(!$can_create)); - $view->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Blocking Tasks')) ->setHref("/search/attach/{$phid}/TASK/blocks/") @@ -191,52 +218,55 @@ final class ManiphestTaskDetailController extends ManiphestController { ->setDisabled(!$can_edit) ->setWorkflow(true)); - return $view; + + $owner_phid = $task->getOwnerPHID(); + $author_phid = $task->getAuthorPHID(); + $handles = $viewer->loadHandles(array($owner_phid, $author_phid)); + + if ($owner_phid) { + $image_uri = $handles[$owner_phid]->getImageURI(); + $image_href = $handles[$owner_phid]->getURI(); + $owner = $viewer->renderHandle($owner_phid)->render(); + $content = phutil_tag('strong', array(), $owner); + $assigned_to = id(new PHUIHeadThingView()) + ->setImage($image_uri) + ->setImageHref($image_href) + ->setContent($content); + } else { + $assigned_to = phutil_tag('em', array(), pht('None')); + } + + $curtain->newPanel() + ->setHeaderText(pht('Assigned To')) + ->appendChild($assigned_to); + + $author_uri = $handles[$author_phid]->getImageURI(); + $author_href = $handles[$author_phid]->getURI(); + $author = $viewer->renderHandle($author_phid)->render(); + $content = phutil_tag('strong', array(), $author); + $date = phabricator_date($task->getDateCreated(), $viewer); + $content = pht('%s, %s', $content, $date); + $authored_by = id(new PHUIHeadThingView()) + ->setImage($author_uri) + ->setImageHref($author_href) + ->setContent($content); + + $curtain->newPanel() + ->setHeaderText(pht('Authored By')) + ->appendChild($authored_by); + + return $curtain; } private function buildPropertyView( ManiphestTask $task, PhabricatorCustomFieldList $field_list, array $edges, - PhabricatorActionListView $actions, $handles) { $viewer = $this->getRequest()->getUser(); - $view = id(new PHUIPropertyListView()) - ->setUser($viewer) - ->setObject($task) - ->setActionList($actions); - - $owner_phid = $task->getOwnerPHID(); - if ($owner_phid) { - $assigned_to = $handles - ->renderHandle($owner_phid) - ->setShowHovercard(true); - } else { - $assigned_to = phutil_tag('em', array(), pht('None')); - } - - $view->addProperty(pht('Assigned To'), $assigned_to); - - $view->addProperty( - pht('Priority'), - ManiphestTaskPriority::getTaskPriorityName($task->getPriority())); - - $author = $handles - ->renderHandle($task->getAuthorPHID()) - ->setShowHovercard(true); - - $view->addProperty(pht('Author'), $author); - - if (ManiphestTaskPoints::getIsEnabled()) { - $points = $task->getPoints(); - if ($points !== null) { - $view->addProperty( - ManiphestTaskPoints::getPointsLabel(), - $task->getPoints()); - } - } + ->setUser($viewer); $source = $task->getOriginalEmailSource(); if ($source) { @@ -304,14 +334,16 @@ final class ManiphestTaskDetailController extends ManiphestController { phutil_implode_html(phutil_tag('br'), $revisions_commits)); } - $view->invokeWillRenderEvent(); - $field_list->appendFieldsToPropertyList( $task, $viewer, $view); - return $view; + if ($view->hasAnyProperties()) { + return $view; + } + + return null; } private function buildDescriptionView( @@ -321,9 +353,6 @@ final class ManiphestTaskDetailController extends ManiphestController { $section = null; if (strlen($task->getDescription())) { $section = new PHUIPropertyListView(); - $section->addSectionHeader( - pht('Description'), - PHUIPropertyListView::ICON_SUMMARY); $section->addTextContent( phutil_tag( 'div', diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php index 0c1a1bc787..7718bdfcf8 100644 --- a/src/applications/maniphest/storage/ManiphestTask.php +++ b/src/applications/maniphest/storage/ManiphestTask.php @@ -269,10 +269,6 @@ final class ManiphestTask extends ManiphestDAO return ($phid == $this->getOwnerPHID()); } - public function shouldShowSubscribersProperty() { - return true; - } - /* -( Markup Interface )--------------------------------------------------- */ diff --git a/src/applications/maniphest/storage/ManiphestTransaction.php b/src/applications/maniphest/storage/ManiphestTransaction.php index 55d14083e6..caf57a3f71 100644 --- a/src/applications/maniphest/storage/ManiphestTransaction.php +++ b/src/applications/maniphest/storage/ManiphestTransaction.php @@ -301,7 +301,7 @@ final class ManiphestTransaction if ($this->getAuthorPHID() == $new) { return pht('Claimed'); } else if (!$new) { - return pht('Up For Grabs'); + return pht('Unassigned'); } else if (!$old) { return pht('Assigned'); } else { @@ -547,8 +547,9 @@ final class ManiphestTransaction $this->renderHandleLink($author_phid)); } else if (!$new) { return pht( - '%s placed this task up for grabs.', - $this->renderHandleLink($author_phid)); + '%s removed %s as the assignee of this task.', + $this->renderHandleLink($author_phid), + $this->renderHandleLink($old)); } else if (!$old) { return pht( '%s assigned this task to %s.', diff --git a/src/applications/meta/controller/PhabricatorApplicationDetailViewController.php b/src/applications/meta/controller/PhabricatorApplicationDetailViewController.php index 19aad0e6a5..443a7e37d3 100644 --- a/src/applications/meta/controller/PhabricatorApplicationDetailViewController.php +++ b/src/applications/meta/controller/PhabricatorApplicationDetailViewController.php @@ -24,11 +24,13 @@ final class PhabricatorApplicationDetailViewController $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($selected->getName()); + $crumbs->setBorder(true); $header = id(new PHUIHeaderView()) ->setHeader($title) ->setUser($viewer) - ->setPolicyObject($selected); + ->setPolicyObject($selected) + ->setHeaderIcon($selected->getIcon()); if ($selected->isInstalled()) { $header->setStatus('fa-check', 'bluegrey', pht('Installed')); @@ -36,12 +38,9 @@ final class PhabricatorApplicationDetailViewController $header->setStatus('fa-ban', 'dark', pht('Uninstalled')); } - $actions = $this->buildActionView($viewer, $selected); - $properties = $this->buildPropertyView($selected, $actions); - - $object_box = id(new PHUIObjectBoxView()) - ->setHeader($header) - ->addPropertyList($properties); + $curtain = $this->buildCurtain($selected); + $details = $this->buildPropertySectionView($selected); + $policies = $this->buildPolicyView($selected); $configs = PhabricatorApplicationConfigurationPanel::loadAllPanelsForApplication( @@ -51,29 +50,35 @@ final class PhabricatorApplicationDetailViewController foreach ($configs as $config) { $config->setViewer($viewer); $config->setApplication($selected); + $panel = $config->buildConfigurationPagePanel(); + $panel->setBackground(PHUIObjectBoxView::BLUE_PROPERTY); + $panels[] = $panel; - $panels[] = $config->buildConfigurationPagePanel(); } - return $this->buildApplicationPage( - array( - $crumbs, - $object_box, - $panels, - ), - array( - 'title' => $title, + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setCurtain($curtain) + ->setMainColumn(array( + $policies, + $panels, + )) + ->addPropertySection(pht('DETAILS'), $details); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->appendChild( + array( + $view, )); } - private function buildPropertyView( - PhabricatorApplication $application, - PhabricatorActionListView $actions) { - - $viewer = $this->getRequest()->getUser(); + private function buildPropertySectionView( + PhabricatorApplication $application) { + $viewer = $this->getViewer(); $properties = id(new PHUIPropertyListView()); - $properties->setActionList($actions); $properties->addProperty( pht('Description'), @@ -111,37 +116,53 @@ final class PhabricatorApplicationDetailViewController $properties->addTextContent($overview); } + return $properties; + } + + private function buildPolicyView( + PhabricatorApplication $application) { + + $viewer = $this->getViewer(); + $properties = id(new PHUIPropertyListView()) + ->setStacked(true); + + $header = id(new PHUIHeaderView()) + ->setHeader(pht('POLICIES')) + ->setHeaderIcon('fa-lock'); + $descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions( $viewer, $application); - $properties->addSectionHeader( - pht('Policies'), 'fa-lock'); - foreach ($application->getCapabilities() as $capability) { $properties->addProperty( $application->getCapabilityLabel($capability), idx($descriptions, $capability)); } - return $properties; + return id(new PHUIObjectBoxView()) + ->setHeader($header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->appendChild($properties); + } - private function buildActionView( - PhabricatorUser $user, - PhabricatorApplication $selected) { - - $view = id(new PhabricatorActionListView()) - ->setUser($user); + private function buildCurtain(PhabricatorApplication $application) { + $viewer = $this->getViewer(); $can_edit = PhabricatorPolicyFilter::hasCapability( - $user, - $selected, + $viewer, + $application, PhabricatorPolicyCapability::CAN_EDIT); - $edit_uri = $this->getApplicationURI('edit/'.get_class($selected).'/'); + $key = get_class($application); + $edit_uri = $this->getApplicationURI("edit/{$key}/"); + $install_uri = $this->getApplicationURI("{$key}/install/"); + $uninstall_uri = $this->getApplicationURI("{$key}/uninstall/"); - $view->addAction( + $curtain = $this->newCurtainView($application); + + $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Policies')) ->setIcon('fa-pencil') @@ -149,45 +170,42 @@ final class PhabricatorApplicationDetailViewController ->setWorkflow(!$can_edit) ->setHref($edit_uri)); - if ($selected->canUninstall()) { - if ($selected->isInstalled()) { - $view->addAction( + if ($application->canUninstall()) { + if ($application->isInstalled()) { + $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Uninstall')) ->setIcon('fa-times') ->setDisabled(!$can_edit) ->setWorkflow(true) - ->setHref( - $this->getApplicationURI(get_class($selected).'/uninstall/'))); + ->setHref($uninstall_uri)); } else { $action = id(new PhabricatorActionView()) ->setName(pht('Install')) ->setIcon('fa-plus') ->setDisabled(!$can_edit) ->setWorkflow(true) - ->setHref( - $this->getApplicationURI(get_class($selected).'/install/')); + ->setHref($install_uri); $prototypes_enabled = PhabricatorEnv::getEnvConfig( 'phabricator.show-prototypes'); - if ($selected->isPrototype() && !$prototypes_enabled) { + if ($application->isPrototype() && !$prototypes_enabled) { $action->setDisabled(true); } - $view->addAction($action); + $curtain->addAction($action); } } else { - $view->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Uninstall')) ->setIcon('fa-times') ->setWorkflow(true) ->setDisabled(true) - ->setHref( - $this->getApplicationURI(get_class($selected).'/uninstall/'))); + ->setHref($uninstall_uri)); } - return $view; + return $curtain; } } diff --git a/src/applications/metamta/contentsource/PhabricatorContentSourceView.php b/src/applications/metamta/contentsource/PhabricatorContentSourceView.php index f159b2059d..5b19f41cb2 100644 --- a/src/applications/metamta/contentsource/PhabricatorContentSourceView.php +++ b/src/applications/metamta/contentsource/PhabricatorContentSourceView.php @@ -9,6 +9,13 @@ final class PhabricatorContentSourceView extends AphrontView { return $this; } + public function getSourceName() { + $map = PhabricatorContentSource::getSourceNameMap(); + $source = $this->contentSource->getSource(); + return idx($map, $source, null); + + } + public function render() { require_celerity_resource('phabricator-content-source-view-css'); diff --git a/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php b/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php index efae153a0f..e4da2a04d6 100644 --- a/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php +++ b/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php @@ -254,10 +254,19 @@ abstract class PhabricatorMailReplyHandler extends Phobject { $map = $to + $cc; foreach ($map as $phid => $user) { + // Preserve the original To/Cc information on the target. + if (isset($to[$phid])) { + $target_to = array($phid => $user); + $target_cc = array(); + } else { + $target_to = array(); + $target_cc = array($phid => $user); + } + $target = id(clone $template) ->setViewer($user) - ->setToMap(array($phid => $user)) - ->setCCMap(array()); + ->setToMap($target_to) + ->setCCMap($target_cc); if ($supports_private_replies) { $reply_to = $this->getPrivateReplyHandlerEmailAddress($user); diff --git a/src/applications/nuance/application/PhabricatorNuanceApplication.php b/src/applications/nuance/application/PhabricatorNuanceApplication.php index e3d5825d4e..92e89f8255 100644 --- a/src/applications/nuance/application/PhabricatorNuanceApplication.php +++ b/src/applications/nuance/application/PhabricatorNuanceApplication.php @@ -40,22 +40,19 @@ final class PhabricatorNuanceApplication extends PhabricatorApplication { '/nuance/' => array( '' => 'NuanceConsoleController', 'item/' => array( + $this->getQueryRoutePattern() => 'NuanceItemListController', 'view/(?P[1-9]\d*)/' => 'NuanceItemViewController', - 'edit/(?P[1-9]\d*)/' => 'NuanceItemEditController', - 'new/' => 'NuanceItemEditController', + 'manage/(?P[1-9]\d*)/' => 'NuanceItemManageController', ), 'source/' => array( - '(?:query/(?P[^/]+)/)?' => 'NuanceSourceListController', + $this->getQueryRoutePattern() => 'NuanceSourceListController', + $this->getEditRoutePattern('edit/') => 'NuanceSourceEditController', 'view/(?P[1-9]\d*)/' => 'NuanceSourceViewController', - 'edit/(?P[1-9]\d*)/' => 'NuanceSourceEditController', - 'new/(?P[^/]+)/' => 'NuanceSourceEditController', - 'create/' => 'NuanceSourceCreateController', ), 'queue/' => array( - '(?:query/(?P[^/]+)/)?' => 'NuanceQueueListController', + $this->getQueryRoutePattern() => 'NuanceQueueListController', + $this->getEditRoutePattern('edit/') => 'NuanceQueueEditController', 'view/(?P[1-9]\d*)/' => 'NuanceQueueViewController', - 'edit/(?P[1-9]\d*)/' => 'NuanceQueueEditController', - 'new/' => 'NuanceQueueEditController', ), 'requestor/' => array( 'view/(?P[1-9]\d*)/' => 'NuanceRequestorViewController', diff --git a/src/applications/nuance/conduit/NuanceCreateItemConduitAPIMethod.php b/src/applications/nuance/conduit/NuanceCreateItemConduitAPIMethod.php deleted file mode 100644 index b05c09fec4..0000000000 --- a/src/applications/nuance/conduit/NuanceCreateItemConduitAPIMethod.php +++ /dev/null @@ -1,73 +0,0 @@ - 'required string', - 'sourcePHID' => 'required string', - 'ownerPHID' => 'optional string', - ); - } - - protected function defineReturnType() { - return 'nonempty dict'; - } - - protected function defineErrorTypes() { - return array( - 'ERR-NO-REQUESTOR-PHID' => pht('Items must have a requestor.'), - 'ERR-NO-SOURCE-PHID' => pht('Items must have a source.'), - ); - } - - protected function execute(ConduitAPIRequest $request) { - $source_phid = $request->getValue('sourcePHID'); - $owner_phid = $request->getValue('ownerPHID'); - $requestor_phid = $request->getValue('requestorPHID'); - - $user = $request->getUser(); - - $item = NuanceItem::initializeNewItem(); - $xactions = array(); - - if ($source_phid) { - $xactions[] = id(new NuanceItemTransaction()) - ->setTransactionType(NuanceItemTransaction::TYPE_SOURCE) - ->setNewValue($source_phid); - } else { - throw new ConduitException('ERR-NO-SOURCE-PHID'); - } - - if ($owner_phid) { - $xactions[] = id(new NuanceItemTransaction()) - ->setTransactionType(NuanceItemTransaction::TYPE_OWNER) - ->setNewValue($owner_phid); - } - - if ($requestor_phid) { - $xactions[] = id(new NuanceItemTransaction()) - ->setTransactionType(NuanceItemTransaction::TYPE_REQUESTOR) - ->setNewValue($requestor_phid); - } else { - throw new ConduitException('ERR-NO-REQUESTOR-PHID'); - } - - $source = PhabricatorContentSource::newFromConduitRequest($request); - $editor = id(new NuanceItemEditor()) - ->setActor($user) - ->setContentSource($source) - ->applyTransactions($item, $xactions); - - return $item->toDictionary(); - } - -} diff --git a/src/applications/nuance/controller/NuanceConsoleController.php b/src/applications/nuance/controller/NuanceConsoleController.php index ffcde21ba3..6416c19aaf 100644 --- a/src/applications/nuance/controller/NuanceConsoleController.php +++ b/src/applications/nuance/controller/NuanceConsoleController.php @@ -26,6 +26,13 @@ final class NuanceConsoleController extends NuanceController { ->setIcon('fa-filter') ->addAttribute(pht('Manage Nuance sources.'))); + $menu->addItem( + id(new PHUIObjectItemView()) + ->setHeader(pht('Items')) + ->setHref($this->getApplicationURI('item/')) + ->setIcon('fa-clone') + ->addAttribute(pht('Manage Nuance items.'))); + $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Console')); diff --git a/src/applications/nuance/controller/NuanceItemController.php b/src/applications/nuance/controller/NuanceItemController.php new file mode 100644 index 0000000000..fa5b01ddb9 --- /dev/null +++ b/src/applications/nuance/controller/NuanceItemController.php @@ -0,0 +1,11 @@ +newApplicationMenu() + ->setSearchEngine(new NuanceItemSearchEngine()); + } + +} diff --git a/src/applications/nuance/controller/NuanceItemEditController.php b/src/applications/nuance/controller/NuanceItemEditController.php deleted file mode 100644 index afb84e6471..0000000000 --- a/src/applications/nuance/controller/NuanceItemEditController.php +++ /dev/null @@ -1,104 +0,0 @@ -getViewer(); - $id = $request->getURIData('id'); - - $item = id(new NuanceItemQuery()) - ->setViewer($viewer) - ->withIDs(array($id)) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - if (!$item) { - return new Aphront404Response(); - } - - $title = pht('Item %d', $item->getID()); - - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb($title); - $crumbs->addTextCrumb(pht('Edit')); - - $properties = $this->buildPropertyView($item); - $actions = $this->buildActionView($item); - $properties->setActionList($actions); - - $box = id(new PHUIObjectBoxView()) - ->setHeaderText($title) - ->addPropertyList($properties); - - $timeline = $this->buildTransactionTimeline( - $item, - new NuanceItemTransactionQuery()); - - $timeline->setShouldTerminate(true); - - return $this->buildApplicationPage( - array( - $crumbs, - $box, - $timeline, - ), - array( - 'title' => $title, - )); - } - - private function buildPropertyView(NuanceItem $item) { - $viewer = $this->getViewer(); - - $properties = id(new PHUIPropertyListView()) - ->setUser($viewer) - ->setObject($item); - - $properties->addProperty( - pht('Date Created'), - phabricator_datetime($item->getDateCreated(), $viewer)); - - $properties->addProperty( - pht('Requestor'), - $viewer->renderHandle($item->getRequestorPHID())); - - $properties->addProperty( - pht('Source'), - $viewer->renderHandle($item->getSourcePHID())); - - $properties->addProperty( - pht('Queue'), - $viewer->renderHandle($item->getQueuePHID())); - - $source = $item->getSource(); - $definition = $source->requireDefinition(); - - $definition->renderItemEditProperties( - $viewer, - $item, - $properties); - - return $properties; - } - - private function buildActionView(NuanceItem $item) { - $viewer = $this->getViewer(); - $id = $item->getID(); - - $actions = id(new PhabricatorActionListView()) - ->setUser($viewer); - - $actions->addAction( - id(new PhabricatorActionView()) - ->setName(pht('View Item')) - ->setIcon('fa-eye') - ->setHref($this->getApplicationURI("item/view/{$id}/"))); - - return $actions; - } - - -} diff --git a/src/applications/nuance/controller/NuanceItemListController.php b/src/applications/nuance/controller/NuanceItemListController.php new file mode 100644 index 0000000000..2135076fee --- /dev/null +++ b/src/applications/nuance/controller/NuanceItemListController.php @@ -0,0 +1,12 @@ +setController($this) + ->buildResponse(); + } + +} diff --git a/src/applications/nuance/controller/NuanceItemManageController.php b/src/applications/nuance/controller/NuanceItemManageController.php new file mode 100644 index 0000000000..c86d2cd985 --- /dev/null +++ b/src/applications/nuance/controller/NuanceItemManageController.php @@ -0,0 +1,109 @@ +getViewer(); + $id = $request->getURIData('id'); + + $item = id(new NuanceItemQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->executeOne(); + if (!$item) { + return new Aphront404Response(); + } + + $title = pht('Item %d', $item->getID()); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb( + pht('Items'), + $this->getApplicationURI('item/')); + $crumbs->addTextCrumb( + $title, + $item->getURI()); + $crumbs->addTextCrumb(pht('Manage')); + $crumbs->setBorder(true); + + $properties = $this->buildPropertyView($item); + $curtain = $this->buildCurtain($item); + + $header = id(new PHUIHeaderView()) + ->setHeader($title); + + $timeline = $this->buildTransactionTimeline( + $item, + new NuanceItemTransactionQuery()); + $timeline->setShouldTerminate(true); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setCurtain($curtain) + ->addPropertySection(pht('DETAILS'), $properties) + ->setMainColumn($timeline); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->appendChild($view); + } + + private function buildPropertyView(NuanceItem $item) { + $viewer = $this->getViewer(); + + $properties = id(new PHUIPropertyListView()) + ->setUser($viewer); + + $properties->addProperty( + pht('Date Created'), + phabricator_datetime($item->getDateCreated(), $viewer)); + + $requestor_phid = $item->getRequestorPHID(); + if ($requestor_phid) { + $requestor_view = $viewer->renderHandle($requestor_phid); + } else { + $requestor_view = phutil_tag('em', array(), pht('None')); + } + $properties->addProperty(pht('Requestor'), $requestor_view); + + $properties->addProperty( + pht('Source'), + $viewer->renderHandle($item->getSourcePHID())); + + $queue_phid = $item->getQueuePHID(); + if ($queue_phid) { + $queue_view = $viewer->renderHandle($queue_phid); + } else { + $queue_view = phutil_tag('em', array(), pht('None')); + } + $properties->addProperty(pht('Queue'), $queue_view); + + $source = $item->getSource(); + $definition = $source->getDefinition(); + + $definition->renderItemEditProperties( + $viewer, + $item, + $properties); + + return $properties; + } + + private function buildCurtain(NuanceItem $item) { + $viewer = $this->getViewer(); + $id = $item->getID(); + + $curtain = $this->newCurtainView($item); + + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('View Item')) + ->setIcon('fa-eye') + ->setHref($item->getURI())); + + return $curtain; + } + + +} diff --git a/src/applications/nuance/controller/NuanceItemViewController.php b/src/applications/nuance/controller/NuanceItemViewController.php index d325afbd29..fd8fbc4563 100644 --- a/src/applications/nuance/controller/NuanceItemViewController.php +++ b/src/applications/nuance/controller/NuanceItemViewController.php @@ -15,72 +15,58 @@ final class NuanceItemViewController extends NuanceController { } $title = pht('Item %d', $item->getID()); + $name = $item->getDisplayName(); $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb( + pht('Items'), + $this->getApplicationURI('item/')); $crumbs->addTextCrumb($title); + $crumbs->setBorder(true); - $properties = $this->buildPropertyView($item); - $actions = $this->buildActionView($item); - $properties->setActionList($actions); + $curtain = $this->buildCurtain($item); + $content = $this->buildContent($item); - $box = id(new PHUIObjectBoxView()) - ->setHeaderText($title) - ->addPropertyList($properties); + $header = id(new PHUIHeaderView()) + ->setHeader($name); - return $this->buildApplicationPage( - array( - $crumbs, - $box, - ), - array( - 'title' => $title, - )); + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setCurtain($curtain) + ->setMainColumn($content); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->appendChild($view); } - private function buildPropertyView(NuanceItem $item) { - $viewer = $this->getViewer(); - - $properties = id(new PHUIPropertyListView()) - ->setUser($viewer) - ->setObject($item); - - $properties->addProperty( - pht('Date Created'), - phabricator_datetime($item->getDateCreated(), $viewer)); - - $source = $item->getSource(); - $definition = $source->requireDefinition(); - - $definition->renderItemViewProperties( - $viewer, - $item, - $properties); - - return $properties; - } - - private function buildActionView(NuanceItem $item) { + private function buildCurtain(NuanceItem $item) { $viewer = $this->getViewer(); $id = $item->getID(); - $actions = id(new PhabricatorActionListView()) - ->setUser($viewer); - $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $item, PhabricatorPolicyCapability::CAN_EDIT); - $actions->addAction( - id(new PhabricatorActionView()) - ->setName(pht('Edit Item')) - ->setIcon('fa-pencil') - ->setHref($this->getApplicationURI("item/edit/{$id}/")) - ->setDisabled(!$can_edit) - ->setWorkflow(!$can_edit)); + $curtain = $this->newCurtainView($item); - return $actions; + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Manage Item')) + ->setIcon('fa-cogs') + ->setHref($this->getApplicationURI("item/manage/{$id}/"))); + + return $curtain; } + private function buildContent(NuanceItem $item) { + $viewer = $this->getViewer(); + $impl = $item->getImplementation(); + + $impl->setViewer($viewer); + return $impl->buildItemView($item); + } } diff --git a/src/applications/nuance/controller/NuanceQueueController.php b/src/applications/nuance/controller/NuanceQueueController.php new file mode 100644 index 0000000000..5736c9a234 --- /dev/null +++ b/src/applications/nuance/controller/NuanceQueueController.php @@ -0,0 +1,11 @@ +newApplicationMenu() + ->setSearchEngine(new NuanceQueueSearchEngine()); + } + +} diff --git a/src/applications/nuance/controller/NuanceQueueEditController.php b/src/applications/nuance/controller/NuanceQueueEditController.php index cfb657615d..5f220e1ee4 100644 --- a/src/applications/nuance/controller/NuanceQueueEditController.php +++ b/src/applications/nuance/controller/NuanceQueueEditController.php @@ -1,135 +1,12 @@ getViewer(); - $queues_uri = $this->getApplicationURI('queue/'); - - $queue_id = $request->getURIData('id'); - $is_new = !$queue_id; - if ($is_new) { - $queue = NuanceQueue::initializeNewQueue(); - $cancel_uri = $queues_uri; - } else { - $queue = id(new NuanceQueueQuery()) - ->setViewer($viewer) - ->withIDs(array($queue_id)) - ->executeOne(); - if (!$queue) { - return new Aphront404Response(); - } - $cancel_uri = $queue->getURI(); - } - - $v_name = $queue->getName(); - $e_name = true; - $v_edit = $queue->getEditPolicy(); - $v_view = $queue->getViewPolicy(); - - $validation_exception = null; - if ($request->isFormPost()) { - $e_name = null; - - $v_name = $request->getStr('name'); - $v_edit = $request->getStr('editPolicy'); - $v_view = $request->getStr('viewPolicy'); - - $type_name = NuanceQueueTransaction::TYPE_NAME; - $type_view = PhabricatorTransactions::TYPE_VIEW_POLICY; - $type_edit = PhabricatorTransactions::TYPE_EDIT_POLICY; - - $xactions = array(); - - $xactions[] = id(new NuanceQueueTransaction()) - ->setTransactionType($type_name) - ->setNewValue($v_name); - - $xactions[] = id(new NuanceQueueTransaction()) - ->setTransactionType($type_view) - ->setNewValue($v_view); - - $xactions[] = id(new NuanceQueueTransaction()) - ->setTransactionType($type_edit) - ->setNewValue($v_edit); - - $editor = id(new NuanceQueueEditor()) - ->setActor($viewer) - ->setContentSourceFromRequest($request) - ->setContinueOnNoEffect(true); - - try { - - $editor->applyTransactions($queue, $xactions); - - $uri = $queue->getURI(); - return id(new AphrontRedirectResponse())->setURI($uri); - } catch (PhabricatorApplicationTransactionValidationException $ex) { - $validation_exception = $ex; - - $e_name = $ex->getShortMessage($type_name); - } - } - - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb(pht('Queues'), $queues_uri); - - if ($is_new) { - $title = pht('Create Queue'); - $crumbs->addTextCrumb(pht('Create')); - } else { - $title = pht('Edit %s', $queue->getName()); - $crumbs->addTextCrumb($queue->getName(), $queue->getURI()); - $crumbs->addTextCrumb(pht('Edit')); - } - - $policies = id(new PhabricatorPolicyQuery()) - ->setViewer($viewer) - ->setObject($queue) - ->execute(); - - $form = id(new AphrontFormView()) - ->setUser($viewer) - ->appendChild( - id(new AphrontFormTextControl()) - ->setLabel(pht('Name')) - ->setName('name') - ->setError($e_name) - ->setValue($v_name)) - ->appendChild( - id(new AphrontFormPolicyControl()) - ->setUser($viewer) - ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) - ->setPolicyObject($queue) - ->setPolicies($policies) - ->setValue($v_view) - ->setName('viewPolicy')) - ->appendChild( - id(new AphrontFormPolicyControl()) - ->setUser($viewer) - ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) - ->setPolicyObject($queue) - ->setPolicies($policies) - ->setValue($v_edit) - ->setName('editPolicy')) - ->appendChild( - id(new AphrontFormSubmitControl()) - ->addCancelButton($cancel_uri) - ->setValue(pht('Save'))); - - $box = id(new PHUIObjectBoxView()) - ->setHeaderText($title) - ->setValidationException($validation_exception) - ->appendChild($form); - - return $this->buildApplicationPage( - array( - $crumbs, - $box, - ), - array( - 'title' => $title, - )); + return id(new NuanceQueueEditEngine()) + ->setController($this) + ->buildResponse(); } } diff --git a/src/applications/nuance/controller/NuanceQueueListController.php b/src/applications/nuance/controller/NuanceQueueListController.php index e139386bdf..bf104da2a8 100644 --- a/src/applications/nuance/controller/NuanceQueueListController.php +++ b/src/applications/nuance/controller/NuanceQueueListController.php @@ -1,46 +1,20 @@ getRequest(); - $controller = id(new PhabricatorApplicationSearchController($request)) - ->setQueryKey($request->getURIData('queryKey')) - ->setSearchEngine(new NuanceQueueSearchEngine()) - ->setNavigation($this->buildSideNavView()); - - return $this->delegateToController($controller); - } - - public function buildSideNavView($for_app = false) { - $user = $this->getRequest()->getUser(); - - $nav = new AphrontSideNavFilterView(); - $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); - - id(new NuanceQueueSearchEngine()) - ->setViewer($user) - ->addNavigationItems($nav->getMenu()); - - $nav->selectFilter(null); - - return $nav; + return id(new NuanceQueueSearchEngine()) + ->setController($this) + ->buildResponse(); } protected function buildApplicationCrumbs() { $crumbs = parent::buildApplicationCrumbs(); - // TODO: Maybe use SourceManage capability? - $can_create = true; - - $crumbs->addAction( - id(new PHUIListItemView()) - ->setName(pht('Create Queue')) - ->setHref($this->getApplicationURI('queue/new/')) - ->setIcon('fa-plus-square') - ->setDisabled(!$can_create) - ->setWorkflow(!$can_create)); + id(new NuanceQueueEditEngine()) + ->setViewer($this->getViewer()) + ->addActionToCrumbs($crumbs); return $crumbs; } diff --git a/src/applications/nuance/controller/NuanceQueueViewController.php b/src/applications/nuance/controller/NuanceQueueViewController.php index 71e5e04ce7..8f4e85565a 100644 --- a/src/applications/nuance/controller/NuanceQueueViewController.php +++ b/src/applications/nuance/controller/NuanceQueueViewController.php @@ -1,6 +1,7 @@ getViewer(); @@ -18,29 +19,25 @@ final class NuanceQueueViewController extends NuanceController { $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Queues'), $this->getApplicationURI('queue/')); $crumbs->addTextCrumb($queue->getName()); + $crumbs->setBorder(true); $header = $this->buildHeaderView($queue); - $actions = $this->buildActionView($queue); - $properties = $this->buildPropertyView($queue, $actions); - - $box = id(new PHUIObjectBoxView()) - ->setHeader($header) - ->addPropertyList($properties); + $curtain = $this->buildCurtain($queue); $timeline = $this->buildTransactionTimeline( $queue, new NuanceQueueTransactionQuery()); $timeline->setShouldTerminate(true); - return $this->buildApplicationPage( - array( - $crumbs, - $box, - $timeline, - ), - array( - 'title' => $title, - )); + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setCurtain($curtain) + ->setMainColumn($timeline); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->appendChild($view); } private function buildHeaderView(NuanceQueue $queue) { @@ -54,19 +51,18 @@ final class NuanceQueueViewController extends NuanceController { return $header; } - private function buildActionView(NuanceQueue $queue) { + private function buildCurtain(NuanceQueue $queue) { $viewer = $this->getViewer(); $id = $queue->getID(); - $actions = id(new PhabricatorActionListView()) - ->setUser($viewer); - $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $queue, PhabricatorPolicyCapability::CAN_EDIT); - $actions->addAction( + $curtain = $this->newCurtainView($queue); + + $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Queue')) ->setIcon('fa-pencil') @@ -74,19 +70,7 @@ final class NuanceQueueViewController extends NuanceController { ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); - return $actions; + return $curtain; } - private function buildPropertyView( - NuanceQueue $queue, - PhabricatorActionListView $actions) { - $viewer = $this->getViewer(); - - $properties = id(new PHUIPropertyListView()) - ->setUser($viewer) - ->setObject($queue) - ->setActionList($actions); - - return $properties; - } } diff --git a/src/applications/nuance/controller/NuanceSourceActionController.php b/src/applications/nuance/controller/NuanceSourceActionController.php index 06739cea1e..a9fb41bccd 100644 --- a/src/applications/nuance/controller/NuanceSourceActionController.php +++ b/src/applications/nuance/controller/NuanceSourceActionController.php @@ -13,8 +13,11 @@ final class NuanceSourceActionController extends NuanceController { return new Aphront404Response(); } - $def = $source->requireDefinition(); - $def->setActor($viewer); + $def = $source->getDefinition(); + + $def + ->setViewer($viewer) + ->setSource($source); $response = $def->handleActionRequest($request); if ($response instanceof AphrontResponse) { @@ -25,14 +28,10 @@ final class NuanceSourceActionController extends NuanceController { $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($title); - return $this->buildApplicationPage( - array( - $crumbs, - $response, - ), - array( - 'title' => $title, - )); + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->appendChild($response); } } diff --git a/src/applications/nuance/controller/NuanceSourceController.php b/src/applications/nuance/controller/NuanceSourceController.php new file mode 100644 index 0000000000..c3b73376b5 --- /dev/null +++ b/src/applications/nuance/controller/NuanceSourceController.php @@ -0,0 +1,11 @@ +newApplicationMenu() + ->setSearchEngine(new NuanceSourceSearchEngine()); + } + +} diff --git a/src/applications/nuance/controller/NuanceSourceCreateController.php b/src/applications/nuance/controller/NuanceSourceCreateController.php deleted file mode 100644 index 22dd41024c..0000000000 --- a/src/applications/nuance/controller/NuanceSourceCreateController.php +++ /dev/null @@ -1,57 +0,0 @@ -requireApplicationCapability( - NuanceSourceManageCapability::CAPABILITY); - - $viewer = $this->getViewer(); - $map = NuanceSourceDefinition::getAllDefinitions(); - $cancel_uri = $this->getApplicationURI('source/'); - - if ($request->isFormPost()) { - $type = $request->getStr('type'); - if (isset($map[$type])) { - $uri = $this->getApplicationURI('source/new/'.$type.'/'); - return id(new AphrontRedirectResponse())->setURI($uri); - } - } - - $source_types = id(new AphrontFormRadioButtonControl()) - ->setName('type') - ->setLabel(pht('Source Type')); - - foreach ($map as $type => $definition) { - $source_types->addButton( - $type, - $definition->getName(), - $definition->getSourceDescription()); - } - - $form = id(new AphrontFormView()) - ->setUser($viewer) - ->appendChild($source_types) - ->appendChild( - id(new AphrontFormSubmitControl()) - ->setValue(pht('Continue')) - ->addCancelButton($cancel_uri)); - - $box = id(new PHUIObjectBoxView()) - ->setHeaderText(pht('Choose Source Type')) - ->appendChild($form); - - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb(pht('Sources'), $cancel_uri); - $crumbs->addTextCrumb(pht('New')); - - return $this->buildApplicationPage( - array( - $crumbs, - $box, - ), - array( - 'title' => pht('Choose Source Type'), - )); - } -} diff --git a/src/applications/nuance/controller/NuanceSourceEditController.php b/src/applications/nuance/controller/NuanceSourceEditController.php index 18234bc778..76025b2eb9 100644 --- a/src/applications/nuance/controller/NuanceSourceEditController.php +++ b/src/applications/nuance/controller/NuanceSourceEditController.php @@ -1,72 +1,76 @@ requireApplicationCapability( - NuanceSourceManageCapability::CAPABILITY); + $engine = id(new NuanceSourceEditEngine()) + ->setController($this); - $viewer = $this->getViewer(); + $id = $request->getURIData('id'); + if (!$id) { + $this->requireApplicationCapability( + NuanceSourceManageCapability::CAPABILITY); - $sources_uri = $this->getApplicationURI('source/'); - - $source_id = $request->getURIData('id'); - $is_new = !$source_id; - - if ($is_new) { - $source = NuanceSource::initializeNewSource($viewer); - - $type = $request->getURIData('type'); + $cancel_uri = $this->getApplicationURI('source/'); $map = NuanceSourceDefinition::getAllDefinitions(); - - if (empty($map[$type])) { - return new Aphront404Response(); + $source_type = $request->getStr('sourceType'); + if (!isset($map[$source_type])) { + return $this->buildSourceTypeResponse($cancel_uri); } - $source->setType($type); - $cancel_uri = $sources_uri; - } else { - $source = id(new NuanceSourceQuery()) - ->setViewer($viewer) - ->withIDs(array($source_id)) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - if (!$source) { - return new Aphront404Response(); - } - $cancel_uri = $source->getURI(); + $engine + ->setSourceDefinition($map[$source_type]) + ->addContextParameter('sourceType', $source_type); } - $definition = $source->requireDefinition(); - $definition->setActor($viewer); + return $engine->buildResponse(); + } - $response = $definition->buildEditLayout($request); - if ($response instanceof AphrontResponse) { - return $response; + private function buildSourceTypeResponse($cancel_uri) { + $viewer = $this->getViewer(); + $request = $this->getRequest(); + $map = NuanceSourceDefinition::getAllDefinitions(); + + $errors = array(); + $e_source = null; + if ($request->isFormPost()) { + $errors[] = pht('You must choose a source type.'); + $e_source = pht('Required'); } - $layout = $response; + + $source_types = id(new AphrontFormRadioButtonControl()) + ->setName('sourceType') + ->setLabel(pht('Source Type')); + + foreach ($map as $type => $definition) { + $source_types->addButton( + $type, + $definition->getName(), + $definition->getSourceDescription()); + } + + $form = id(new AphrontFormView()) + ->setUser($viewer) + ->appendChild($source_types) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue(pht('Continue')) + ->addCancelButton($cancel_uri)); + + $box = id(new PHUIObjectBoxView()) + ->setFormErrors($errors) + ->setHeaderText(pht('Choose Source Type')) + ->appendChild($form); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb(pht('Sources'), $sources_uri); + $crumbs->addTextCrumb(pht('Sources'), $cancel_uri); + $crumbs->addTextCrumb(pht('New')); - if ($is_new) { - $crumbs->addTextCrumb(pht('New')); - } else { - $crumbs->addTextCrumb($source->getName(), $cancel_uri); - $crumbs->addTextCrumb(pht('Edit')); - } - - return $this->buildApplicationPage( - array( - $crumbs, - $layout, - ), - array( - 'title' => $definition->getEditTitle(), - )); + return $this->newPage() + ->setTitle(pht('Choose Source Type')) + ->setCrumbs($crumbs) + ->appendChild($box); } + } diff --git a/src/applications/nuance/controller/NuanceSourceListController.php b/src/applications/nuance/controller/NuanceSourceListController.php index 1d906dc3d3..a38c992d9a 100644 --- a/src/applications/nuance/controller/NuanceSourceListController.php +++ b/src/applications/nuance/controller/NuanceSourceListController.php @@ -1,46 +1,20 @@ getRequest(); - $controller = id(new PhabricatorApplicationSearchController($request)) - ->setQueryKey($request->getURIData('queryKey')) - ->setSearchEngine(new NuanceSourceSearchEngine()) - ->setNavigation($this->buildSideNavView()); - - return $this->delegateToController($controller); - } - - public function buildSideNavView($for_app = false) { - $user = $this->getRequest()->getUser(); - - $nav = new AphrontSideNavFilterView(); - $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); - - id(new NuanceSourceSearchEngine()) - ->setViewer($user) - ->addNavigationItems($nav->getMenu()); - - $nav->selectFilter(null); - - return $nav; + return id(new NuanceSourceSearchEngine()) + ->setController($this) + ->buildResponse(); } protected function buildApplicationCrumbs() { $crumbs = parent::buildApplicationCrumbs(); - $can_create = $this->hasApplicationCapability( - NuanceSourceManageCapability::CAPABILITY); - - $crumbs->addAction( - id(new PHUIListItemView()) - ->setName(pht('Create Source')) - ->setHref($this->getApplicationURI('source/create/')) - ->setIcon('fa-plus-square') - ->setDisabled(!$can_create) - ->setWorkflow(!$can_create)); + id(new NuanceSourceEditEngine()) + ->setViewer($this->getViewer()) + ->addActionToCrumbs($crumbs); return $crumbs; } diff --git a/src/applications/nuance/controller/NuanceSourceViewController.php b/src/applications/nuance/controller/NuanceSourceViewController.php index 78d6949455..af602bfd7e 100644 --- a/src/applications/nuance/controller/NuanceSourceViewController.php +++ b/src/applications/nuance/controller/NuanceSourceViewController.php @@ -1,6 +1,7 @@ getViewer(); @@ -15,53 +16,38 @@ final class NuanceSourceViewController extends NuanceController { $source_id = $source->getID(); - $timeline = $this->buildTransactionTimeline( - $source, - new NuanceSourceTransactionQuery()); - $timeline->setShouldTerminate(true); - $header = $this->buildHeaderView($source); - $actions = $this->buildActionView($source); - $properties = $this->buildPropertyView($source, $actions); - - $box = id(new PHUIObjectBoxView()) - ->setHeader($header) - ->addPropertyList($properties); + $curtain = $this->buildCurtain($source); + $properties = $this->buildPropertyView($source); $title = $source->getName(); - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb(pht('Sources'), $this->getApplicationURI('source/')); - - $crumbs->addTextCrumb($title); - - - $can_edit = PhabricatorPolicyFilter::hasCapability( - $viewer, - $source, - PhabricatorPolicyCapability::CAN_EDIT); $routing_list = id(new PHUIPropertyListView()) ->addProperty( pht('Default Queue'), $viewer->renderHandle($source->getDefaultQueuePHID())); - $routing_header = id(new PHUIHeaderView()) - ->setHeader(pht('Routing Rules')); + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb(pht('Sources'), $this->getApplicationURI('source/')); + $crumbs->addTextCrumb($title); + $crumbs->setBorder(true); - $routing = id(new PHUIObjectBoxView()) - ->setHeader($routing_header) - ->addPropertyList($routing_list); + $timeline = $this->buildTransactionTimeline( + $source, + new NuanceSourceTransactionQuery()); + $timeline->setShouldTerminate(true); - return $this->buildApplicationPage( - array( - $crumbs, - $box, - $routing, - $timeline, - ), - array( - 'title' => $title, - )); + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setCurtain($curtain) + ->addPropertySection(pht('DETAILS'), $properties) + ->addPropertySection(pht('ROUTING'), $routing_list) + ->setMainColumn($timeline); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->appendChild($view); } private function buildHeaderView(NuanceSource $source) { @@ -75,7 +61,7 @@ final class NuanceSourceViewController extends NuanceController { return $header; } - private function buildActionView(NuanceSource $source) { + private function buildCurtain(NuanceSource $source) { $viewer = $this->getViewer(); $id = $source->getID(); @@ -87,7 +73,9 @@ final class NuanceSourceViewController extends NuanceController { $source, PhabricatorPolicyCapability::CAN_EDIT); - $actions->addAction( + $curtain = $this->newCurtainView($source); + + $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Source')) ->setIcon('fa-pencil') @@ -96,38 +84,33 @@ final class NuanceSourceViewController extends NuanceController { ->setWorkflow(!$can_edit)); $request = $this->getRequest(); - $definition = $source->requireDefinition(); + $definition = $source->getDefinition(); + + $definition + ->setViewer($viewer) + ->setSource($source); + $source_actions = $definition->getSourceViewActions($request); foreach ($source_actions as $source_action) { - $actions->addAction($source_action); + $curtain->addAction($source_action); } - return $actions; + return $curtain; } private function buildPropertyView( - NuanceSource $source, - PhabricatorActionListView $actions) { - $viewer = $this->getRequest()->getUser(); + NuanceSource $source) { + $viewer = $this->getViewer(); $properties = id(new PHUIPropertyListView()) - ->setUser($viewer) - ->setObject($source) - ->setActionList($actions); + ->setViewer($viewer); + + $definition = $source->getDefinition(); - $definition = $source->requireDefinition(); $properties->addProperty( pht('Source Type'), $definition->getName()); - $descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions( - $viewer, - $source); - - $properties->addProperty( - pht('Editable By'), - $descriptions[PhabricatorPolicyCapability::CAN_EDIT]); - return $properties; } } diff --git a/src/applications/nuance/cursor/NuanceGitHubImportCursor.php b/src/applications/nuance/cursor/NuanceGitHubImportCursor.php new file mode 100644 index 0000000000..fc21e2f1d6 --- /dev/null +++ b/src/applications/nuance/cursor/NuanceGitHubImportCursor.php @@ -0,0 +1,258 @@ +getCursorProperty('github.poll.ttl'); + if ($ttl && ($ttl >= $now)) { + $this->logInfo( + pht( + 'Respecting "%s" or minimum poll delay: waiting for %s second(s) '. + 'to poll GitHub.', + 'X-Poll-Interval', + new PhutilNumber(1 + ($ttl - $now)))); + + return false; + } + + // Respect GitHub's API rate limiting. If we've exceeded the rate limit, + // wait until it resets to try again. + $limit = $this->getCursorProperty('github.limit.ttl'); + if ($limit && ($limit >= $now)) { + $this->logInfo( + pht( + 'Respecting "%s": waiting for %s second(s) to poll GitHub.', + 'X-RateLimit-Reset', + new PhutilNumber(1 + ($limit - $now)))); + return false; + } + + return true; + } + + final protected function pullDataFromSource() { + $viewer = $this->getViewer(); + $now = PhabricatorTime::getNow(); + + $source = $this->getSource(); + + $user = $source->getSourceProperty('github.user'); + $repository = $source->getSourceProperty('github.repository'); + $api_token = $source->getSourceProperty('github.token'); + + // This API only supports fetching 10 pages of 30 events each, for a total + // of 300 events. + $etag = null; + $new_items = array(); + $hit_known_items = false; + + $max_page = $this->getMaximumPage(); + $page_size = $this->getPageSize(); + + for ($page = 1; $page <= $max_page; $page++) { + $uri = $this->getGitHubAPIEndpointURI($user, $repository); + + $data = array( + 'page' => $page, + 'per_page' => $page_size, + ); + + $future = id(new PhutilGitHubFuture()) + ->setAccessToken($api_token) + ->setRawGitHubQuery($uri, $data); + + if ($page == 1) { + $cursor_etag = $this->getCursorProperty('github.poll.etag'); + if ($cursor_etag) { + $future->addHeader('If-None-Match', $cursor_etag); + } + } + + $this->logInfo( + pht( + 'Polling GitHub Repository API endpoint "%s".', + $uri)); + $response = $future->resolve(); + + // Do this first: if we hit the rate limit, we get a response but the + // body isn't valid. + $this->updateRateLimits($response); + + if ($response->getStatus()->getStatusCode() == 304) { + $this->logInfo( + pht( + 'Received a 304 Not Modified from GitHub, no new events.')); + } + + // This means we hit a rate limit or a "Not Modified" because of the + // "ETag" header. In either case, we should bail out. + if ($response->getStatus()->isError()) { + $this->updatePolling($response, $now, false); + $this->getCursorData()->save(); + return false; + } + + if ($page == 1) { + $etag = $response->getHeaderValue('ETag'); + } + + $records = $response->getBody(); + foreach ($records as $record) { + $item = $this->newNuanceItemFromGitHubRecord($record); + $item_key = $item->getItemKey(); + + $this->logInfo( + pht( + 'Fetched event "%s".', + $item_key)); + + $new_items[$item->getItemKey()] = $item; + } + + if ($new_items) { + $existing = id(new NuanceItemQuery()) + ->setViewer($viewer) + ->withSourcePHIDs(array($source->getPHID())) + ->withItemKeys(array_keys($new_items)) + ->execute(); + $existing = mpull($existing, null, 'getItemKey'); + foreach ($new_items as $key => $new_item) { + if (isset($existing[$key])) { + unset($new_items[$key]); + $hit_known_items = true; + + $this->logInfo( + pht( + 'Event "%s" is previously known.', + $key)); + } + } + } + + if ($hit_known_items) { + break; + } + + if (count($records) < $page_size) { + break; + } + } + + // TODO: When we go through the whole queue without hitting anything we + // have seen before, we should record some sort of global event so we + // can tell the user when the bridging started or was interrupted? + if (!$hit_known_items) { + $already_polled = $this->getCursorProperty('github.polled'); + if ($already_polled) { + // TODO: This is bad: we missed some items, maybe because too much + // stuff happened too fast or the daemons were broken for a long + // time. + } else { + // TODO: This is OK, we're doing the initial import. + } + } + + if ($etag !== null) { + $this->updateETag($etag); + } + + $this->updatePolling($response, $now, true); + + // Reverse the new items so we insert them in chronological order. + $new_items = array_reverse($new_items); + + $source->openTransaction(); + foreach ($new_items as $new_item) { + $new_item->save(); + } + $this->getCursorData()->save(); + $source->saveTransaction(); + + foreach ($new_items as $new_item) { + $new_item->scheduleUpdate(); + } + + return false; + } + + private function updateRateLimits(PhutilGitHubResponse $response) { + $remaining = $response->getHeaderValue('X-RateLimit-Remaining'); + $limit_reset = $response->getHeaderValue('X-RateLimit-Reset'); + $now = PhabricatorTime::getNow(); + + $limit_ttl = null; + if (strlen($remaining)) { + $remaining = (int)$remaining; + if (!$remaining) { + $limit_ttl = (int)$limit_reset; + } + } + + $this->setCursorProperty('github.limit.ttl', $limit_ttl); + + $this->logInfo( + pht( + 'This key has %s remaining API request(s), '. + 'limit resets in %s second(s).', + new PhutilNumber($remaining), + new PhutilNumber($limit_reset - $now))); + } + + private function updateETag($etag) { + + $this->setCursorProperty('github.poll.etag', $etag); + + $this->logInfo( + pht( + 'ETag for this request was "%s".', + $etag)); + } + + private function updatePolling( + PhutilGitHubResponse $response, + $start, + $success) { + + if ($success) { + $this->setCursorProperty('github.polled', true); + } + + $poll_interval = (int)$response->getHeaderValue('X-Poll-Interval'); + $poll_interval = max($this->getMinimumDelayBetweenPolls(), $poll_interval); + + $poll_ttl = $start + $poll_interval; + $this->setCursorProperty('github.poll.ttl', $poll_ttl); + + $now = PhabricatorTime::getNow(); + + $this->logInfo( + pht( + 'Set API poll TTL to +%s second(s) (%s second(s) from now).', + new PhutilNumber($poll_interval), + new PhutilNumber($poll_ttl - $now))); + } + +} diff --git a/src/applications/nuance/cursor/NuanceGitHubIssuesImportCursor.php b/src/applications/nuance/cursor/NuanceGitHubIssuesImportCursor.php new file mode 100644 index 0000000000..d250b1d02d --- /dev/null +++ b/src/applications/nuance/cursor/NuanceGitHubIssuesImportCursor.php @@ -0,0 +1,30 @@ +getSource(); + + $id = $record['id']; + $item_key = "github.issueevent.{$id}"; + + $container_key = null; + + return NuanceItem::initializeNewItem() + ->setStatus(NuanceItem::STATUS_IMPORTING) + ->setSourcePHID($source->getPHID()) + ->setItemType(NuanceGitHubEventItemType::ITEMTYPE) + ->setItemKey($item_key) + ->setItemContainerKey($container_key) + ->setItemProperty('api.type', 'issue') + ->setItemProperty('api.raw', $record); + } + +} diff --git a/src/applications/nuance/cursor/NuanceGitHubRepositoryImportCursor.php b/src/applications/nuance/cursor/NuanceGitHubRepositoryImportCursor.php new file mode 100644 index 0000000000..72aca276d2 --- /dev/null +++ b/src/applications/nuance/cursor/NuanceGitHubRepositoryImportCursor.php @@ -0,0 +1,49 @@ +getSource(); + + $id = $record['id']; + $item_key = "github.event.{$id}"; + + $container_key = null; + + $issue_id = idxv( + $record, + array( + 'payload', + 'issue', + 'id', + )); + if ($issue_id) { + $container_key = "github.issue.{$issue_id}"; + } + + return NuanceItem::initializeNewItem() + ->setStatus(NuanceItem::STATUS_IMPORTING) + ->setSourcePHID($source->getPHID()) + ->setItemType(NuanceGitHubEventItemType::ITEMTYPE) + ->setItemKey($item_key) + ->setItemContainerKey($container_key) + ->setItemProperty('api.type', 'repository') + ->setItemProperty('api.raw', $record); + } + +} diff --git a/src/applications/nuance/cursor/NuanceImportCursor.php b/src/applications/nuance/cursor/NuanceImportCursor.php new file mode 100644 index 0000000000..03dad4d7e4 --- /dev/null +++ b/src/applications/nuance/cursor/NuanceImportCursor.php @@ -0,0 +1,108 @@ +getPhobjectClassConstant('CURSORTYPE', 32); + } + + public function setCursorData(NuanceImportCursorData $cursor_data) { + $this->cursorData = $cursor_data; + return $this; + } + + public function getCursorData() { + return $this->cursorData; + } + + public function setSource($source) { + $this->source = $source; + return $this; + } + + public function getSource() { + return $this->source; + } + + public function setCursorKey($cursor_key) { + $this->cursorKey = $cursor_key; + return $this; + } + + public function getCursorKey() { + return $this->cursorKey; + } + + public function setViewer($viewer) { + $this->viewer = $viewer; + return $this; + } + + public function getViewer() { + return $this->viewer; + } + + final public function importFromSource() { + if (!$this->shouldPullDataFromSource()) { + return false; + } + + $source = $this->getSource(); + $key = $this->getCursorKey(); + + $parts = array( + 'nsc', + $source->getID(), + PhabricatorHash::digestToLength($key, 20), + ); + $lock_name = implode('.', $parts); + + $lock = PhabricatorGlobalLock::newLock($lock_name); + $lock->lock(1); + + try { + $more_data = $this->pullDataFromSource(); + } catch (Exception $ex) { + $lock->unlock(); + throw $ex; + } + + $lock->unlock(); + + return $more_data; + } + + final public function newEmptyCursorData(NuanceSource $source) { + return id(new NuanceImportCursorData()) + ->setCursorKey($this->getCursorKey()) + ->setCursorType($this->getCursorType()) + ->setSourcePHID($source->getPHID()); + } + + final protected function logInfo($message) { + echo tsprintf( + " %s\n", + $this->getCursorKey(), + $message); + + return $this; + } + + final protected function getCursorProperty($key, $default = null) { + return $this->getCursorData()->getCursorProperty($key, $default); + } + + final protected function setCursorProperty($key, $value) { + $this->getCursorData()->setCursorProperty($key, $value); + return $this; + } + +} diff --git a/src/applications/nuance/editor/NuanceQueueEditEngine.php b/src/applications/nuance/editor/NuanceQueueEditEngine.php new file mode 100644 index 0000000000..8e4de2611f --- /dev/null +++ b/src/applications/nuance/editor/NuanceQueueEditEngine.php @@ -0,0 +1,80 @@ +getName()); + } + + protected function getObjectEditShortText($object) { + return pht('Edit Queue'); + } + + protected function getObjectCreateShortText() { + return pht('Create Queue'); + } + + protected function getEditorURI() { + return '/nuance/queue/edit/'; + } + + protected function getObjectCreateCancelURI($object) { + return '/nuance/queue/'; + } + + protected function getObjectViewURI($object) { + return $object->getURI(); + } + + protected function buildCustomEditFields($object) { + return array( + id(new PhabricatorTextEditField()) + ->setKey('name') + ->setLabel(pht('Name')) + ->setDescription(pht('Name of the queue.')) + ->setTransactionType(NuanceQueueTransaction::TYPE_NAME) + ->setIsRequired(true) + ->setValue($object->getName()), + ); + } + +} diff --git a/src/applications/nuance/editor/NuanceQueueEditor.php b/src/applications/nuance/editor/NuanceQueueEditor.php index 589ab5da5c..cb3ead2417 100644 --- a/src/applications/nuance/editor/NuanceQueueEditor.php +++ b/src/applications/nuance/editor/NuanceQueueEditor.php @@ -68,6 +68,7 @@ final class NuanceQueueEditor return parent::applyCustomExternalTransaction($object, $xaction); } + protected function validateTransaction( PhabricatorLiskDAO $object, $type, diff --git a/src/applications/nuance/editor/NuanceSourceEditEngine.php b/src/applications/nuance/editor/NuanceSourceEditEngine.php new file mode 100644 index 0000000000..a01a21c20b --- /dev/null +++ b/src/applications/nuance/editor/NuanceSourceEditEngine.php @@ -0,0 +1,108 @@ +sourceDefinition = $source_definition; + return $this; + } + + public function getSourceDefinition() { + return $this->sourceDefinition; + } + + public function isEngineConfigurable() { + return false; + } + + public function getEngineName() { + return pht('Nuance Sources'); + } + + public function getSummaryHeader() { + return pht('Edit Nuance Source Configurations'); + } + + public function getSummaryText() { + return pht('This engine is used to edit Nuance sources.'); + } + + public function getEngineApplicationClass() { + return 'PhabricatorNuanceApplication'; + } + + protected function newEditableObject() { + $viewer = $this->getViewer(); + + $definition = $this->getSourceDefinition(); + if (!$definition) { + throw new PhutilInvalidStateException('setSourceDefinition'); + } + + return NuanceSource::initializeNewSource( + $viewer, + $definition); + } + + protected function newObjectQuery() { + return new NuanceSourceQuery(); + } + + protected function getObjectCreateTitleText($object) { + return pht('Create Source'); + } + + protected function getObjectCreateButtonText($object) { + return pht('Create Source'); + } + + protected function getObjectEditTitleText($object) { + return pht('Edit Source: %s', $object->getName()); + } + + protected function getObjectEditShortText($object) { + return pht('Edit Source'); + } + + protected function getObjectCreateShortText() { + return pht('Create Source'); + } + + protected function getEditorURI() { + return '/nuance/source/edit/'; + } + + protected function getObjectCreateCancelURI($object) { + return '/nuance/source/'; + } + + protected function getObjectViewURI($object) { + return $object->getURI(); + } + + protected function buildCustomEditFields($object) { + return array( + id(new PhabricatorTextEditField()) + ->setKey('name') + ->setLabel(pht('Name')) + ->setDescription(pht('Name of the source.')) + ->setTransactionType(NuanceSourceTransaction::TYPE_NAME) + ->setIsRequired(true) + ->setValue($object->getName()), + id(new PhabricatorDatasourceEditField()) + ->setKey('defaultQueue') + ->setLabel(pht('Default Queue')) + ->setDescription(pht('Default queue.')) + ->setTransactionType(NuanceSourceTransaction::TYPE_DEFAULT_QUEUE) + ->setDatasource(new NuanceQueueDatasource()) + ->setSingleValue($object->getDefaultQueuePHID()), + ); + } + +} diff --git a/src/applications/nuance/editor/NuanceSourceEditor.php b/src/applications/nuance/editor/NuanceSourceEditor.php index 233b2ae163..5fbc02b962 100644 --- a/src/applications/nuance/editor/NuanceSourceEditor.php +++ b/src/applications/nuance/editor/NuanceSourceEditor.php @@ -11,6 +11,10 @@ final class NuanceSourceEditor return pht('Nuance Sources'); } + protected function supportsSearch() { + return true; + } + public function getTransactionTypes() { $types = parent::getTransactionTypes(); diff --git a/src/applications/nuance/github/NuanceGitHubRawEvent.php b/src/applications/nuance/github/NuanceGitHubRawEvent.php new file mode 100644 index 0000000000..50ab9020ef --- /dev/null +++ b/src/applications/nuance/github/NuanceGitHubRawEvent.php @@ -0,0 +1,137 @@ +type = $type; + $event->raw = $raw; + return $event; + } + + public function getRepositoryFullName() { + return $this->getRepositoryFullRawName(); + } + + public function isIssueEvent() { + if ($this->isPullRequestEvent()) { + return false; + } + + if ($this->type == self::TYPE_ISSUE) { + return true; + } + + switch ($this->getIssueRawKind()) { + case 'IssuesEvent': + return true; + case 'IssueCommentEvent': + if (!$this->getRawPullRequestData()) { + return true; + } + break; + } + + return false; + } + + public function isPullRequestEvent() { + if ($this->type == self::TYPE_ISSUE) { + // TODO: This is wrong, some of these are pull events. + return false; + } + + $raw = $this->raw; + + switch ($this->getIssueRawKind()) { + case 'PullRequestEvent': + return true; + case 'IssueCommentEvent': + if ($this->getRawPullRequestData()) { + return true; + } + break; + } + + return false; + } + + public function getIssueNumber() { + if (!$this->isIssueEvent()) { + return null; + } + + return $this->getRawIssueNumber(); + } + + public function getPullRequestNumber() { + if (!$this->isPullRequestEvent()) { + return null; + } + + return $this->getRawIssueNumber(); + } + + private function getRepositoryFullRawName() { + $raw = $this->raw; + + $full = idxv($raw, array('repo', 'name')); + if (strlen($full)) { + return $full; + } + + // For issue events, the repository is not identified explicitly in the + // response body. Parse it out of the URI. + + $matches = null; + $ok = preg_match( + '(/repos/((?:[^/]+)/(?:[^/]+))/issues/events/)', + idx($raw, 'url'), + $matches); + + if ($ok) { + return $matches[1]; + } + + return null; + } + + private function getIssueRawKind() { + $raw = $this->raw; + return idxv($raw, array('type')); + } + + private function getRawIssueNumber() { + $raw = $this->raw; + + if ($this->type == self::TYPE_ISSUE) { + return idxv($raw, array('issue', 'number')); + } + + if ($this->type == self::TYPE_REPOSITORY) { + $issue_number = idxv($raw, array('payload', 'issue', 'number')); + if ($issue_number) { + return $issue_number; + } + + $pull_number = idxv($raw, array('payload', 'number')); + if ($pull_number) { + return $pull_number; + } + } + + return null; + } + + private function getRawPullRequestData() { + $raw = $this->raw; + return idxv($raw, array('payload', 'issue', 'pull_request')); + } + +} diff --git a/src/applications/nuance/github/__tests__/NuanceGitHubRawEventTestCase.php b/src/applications/nuance/github/__tests__/NuanceGitHubRawEventTestCase.php new file mode 100644 index 0000000000..f956079b6a --- /dev/null +++ b/src/applications/nuance/github/__tests__/NuanceGitHubRawEventTestCase.php @@ -0,0 +1,108 @@ +readTestCases($path); + + foreach ($cases as $name => $info) { + $input = $info['input']; + $expect = $info['expect']; + + $event = NuanceGitHubRawEvent::newEvent( + NuanceGitHubRawEvent::TYPE_ISSUE, + $input); + + $this->assertGitHubRawEventParse($expect, $event, $name); + } + } + + public function testRepositoryEvents() { + $path = dirname(__FILE__).'/repositoryevents/'; + + $cases = $this->readTestCases($path); + + foreach ($cases as $name => $info) { + $input = $info['input']; + $expect = $info['expect']; + + $event = NuanceGitHubRawEvent::newEvent( + NuanceGitHubRawEvent::TYPE_REPOSITORY, + $input); + + $this->assertGitHubRawEventParse($expect, $event, $name); + } + } + + private function assertGitHubRawEventParse( + array $expect, + NuanceGitHubRawEvent $event, + $name) { + + $actual = array( + 'repository.name.full' => $event->getRepositoryFullName(), + 'is.issue' => $event->isIssueEvent(), + 'is.pull' => $event->isPullRequestEvent(), + 'issue.number' => $event->getIssueNumber(), + 'pull.number' => $event->getPullRequestNumber(), + ); + + // Only verify the keys which are actually present in the test. This + // allows tests to specify only relevant keys. + $actual = array_select_keys($actual, array_keys($expect)); + + ksort($expect); + ksort($actual); + + $this->assertEqual($expect, $actual, $name); + } + + private function readTestCases($path) { + $files = Filesystem::listDirectory($path, $include_hidden = false); + + $tests = array(); + foreach ($files as $file) { + $data = Filesystem::readFile($path.$file); + + $parts = preg_split('/^~{5,}$/m', $data); + if (count($parts) < 2) { + throw new Exception( + pht( + 'Expected test file "%s" to contain an input section in JSON, '. + 'then an expected result section in JSON, with the two sections '. + 'separated by a line of "~~~~~", but the divider is not present '. + 'in the file.', + $file)); + } else if (count($parts) > 2) { + throw new Exception( + pht( + 'Expected test file "%s" to contain exactly two sections, '. + 'but it has more than two sections.')); + } + + list($input, $expect) = $parts; + + try { + $input = phutil_json_decode($input); + $expect = phutil_json_decode($expect); + } catch (Exception $ex) { + throw new PhutilProxyException( + pht( + 'Exception while decoding test data for test "%s".', + $file), + $ex); + } + + $tests[$file] = array( + 'input' => $input, + 'expect' => $expect, + ); + } + + return $tests; + } + +} diff --git a/src/applications/nuance/github/__tests__/issueevents/assigned.txt b/src/applications/nuance/github/__tests__/issueevents/assigned.txt new file mode 100644 index 0000000000..bdf5046e00 --- /dev/null +++ b/src/applications/nuance/github/__tests__/issueevents/assigned.txt @@ -0,0 +1,114 @@ +{ + "id": 583217900, + "url": "https://api.github.com/repos/epriestley/poems/issues/events/583217900", + "actor": { + "login": "epriestley", + "id": 102631, + "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "html_url": "https://github.com/epriestley", + "followers_url": "https://api.github.com/users/epriestley/followers", + "following_url": "https://api.github.com/users/epriestley/following{/other_user}", + "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", + "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", + "organizations_url": "https://api.github.com/users/epriestley/orgs", + "repos_url": "https://api.github.com/users/epriestley/repos", + "events_url": "https://api.github.com/users/epriestley/events{/privacy}", + "received_events_url": "https://api.github.com/users/epriestley/received_events", + "type": "User", + "site_admin": false + }, + "event": "assigned", + "commit_id": null, + "commit_url": null, + "created_at": "2016-03-09T12:42:53Z", + "assignee": { + "login": "epriestley", + "id": 102631, + "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "html_url": "https://github.com/epriestley", + "followers_url": "https://api.github.com/users/epriestley/followers", + "following_url": "https://api.github.com/users/epriestley/following{/other_user}", + "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", + "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", + "organizations_url": "https://api.github.com/users/epriestley/orgs", + "repos_url": "https://api.github.com/users/epriestley/repos", + "events_url": "https://api.github.com/users/epriestley/events{/privacy}", + "received_events_url": "https://api.github.com/users/epriestley/received_events", + "type": "User", + "site_admin": false + }, + "assigner": { + "login": "epriestley", + "id": 102631, + "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "html_url": "https://github.com/epriestley", + "followers_url": "https://api.github.com/users/epriestley/followers", + "following_url": "https://api.github.com/users/epriestley/following{/other_user}", + "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", + "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", + "organizations_url": "https://api.github.com/users/epriestley/orgs", + "repos_url": "https://api.github.com/users/epriestley/repos", + "events_url": "https://api.github.com/users/epriestley/events{/privacy}", + "received_events_url": "https://api.github.com/users/epriestley/received_events", + "type": "User", + "site_admin": false + }, + "issue": { + "url": "https://api.github.com/repos/epriestley/poems/issues/1", + "repository_url": "https://api.github.com/repos/epriestley/poems", + "labels_url": "https://api.github.com/repos/epriestley/poems/issues/1/labels{/name}", + "comments_url": "https://api.github.com/repos/epriestley/poems/issues/1/comments", + "events_url": "https://api.github.com/repos/epriestley/poems/issues/1/events", + "html_url": "https://github.com/epriestley/poems/issues/1", + "id": 139138813, + "number": 1, + "title": "Enforce haiku in commit messages edit", + "user": { + "login": "epriestley", + "id": 102631, + "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "html_url": "https://github.com/epriestley", + "followers_url": "https://api.github.com/users/epriestley/followers", + "following_url": "https://api.github.com/users/epriestley/following{/other_user}", + "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", + "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", + "organizations_url": "https://api.github.com/users/epriestley/orgs", + "repos_url": "https://api.github.com/users/epriestley/repos", + "events_url": "https://api.github.com/users/epriestley/events{/privacy}", + "received_events_url": "https://api.github.com/users/epriestley/received_events", + "type": "User", + "site_admin": false + }, + "labels": [ + + ], + "state": "open", + "locked": false, + "assignee": null, + "milestone": null, + "comments": 5, + "created_at": "2016-03-08T00:41:08Z", + "updated_at": "2016-03-09T14:34:46Z", + "closed_at": null, + "body": "OK" + } +} +~~~~~ +{ + "repository.name.full": "epriestley/poems", + "is.issue": true, + "is.pull": false, + "issue.number": 1 +} diff --git a/src/applications/nuance/github/__tests__/issueevents/closed.txt b/src/applications/nuance/github/__tests__/issueevents/closed.txt new file mode 100644 index 0000000000..7651d07172 --- /dev/null +++ b/src/applications/nuance/github/__tests__/issueevents/closed.txt @@ -0,0 +1,76 @@ +{ + "id": 583218864, + "url": "https://api.github.com/repos/epriestley/poems/issues/events/583218864", + "actor": { + "login": "epriestley", + "id": 102631, + "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "html_url": "https://github.com/epriestley", + "followers_url": "https://api.github.com/users/epriestley/followers", + "following_url": "https://api.github.com/users/epriestley/following{/other_user}", + "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", + "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", + "organizations_url": "https://api.github.com/users/epriestley/orgs", + "repos_url": "https://api.github.com/users/epriestley/repos", + "events_url": "https://api.github.com/users/epriestley/events{/privacy}", + "received_events_url": "https://api.github.com/users/epriestley/received_events", + "type": "User", + "site_admin": false + }, + "event": "closed", + "commit_id": null, + "commit_url": null, + "created_at": "2016-03-09T12:43:53Z", + "issue": { + "url": "https://api.github.com/repos/epriestley/poems/issues/1", + "repository_url": "https://api.github.com/repos/epriestley/poems", + "labels_url": "https://api.github.com/repos/epriestley/poems/issues/1/labels{/name}", + "comments_url": "https://api.github.com/repos/epriestley/poems/issues/1/comments", + "events_url": "https://api.github.com/repos/epriestley/poems/issues/1/events", + "html_url": "https://github.com/epriestley/poems/issues/1", + "id": 139138813, + "number": 1, + "title": "Enforce haiku in commit messages edit", + "user": { + "login": "epriestley", + "id": 102631, + "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "html_url": "https://github.com/epriestley", + "followers_url": "https://api.github.com/users/epriestley/followers", + "following_url": "https://api.github.com/users/epriestley/following{/other_user}", + "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", + "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", + "organizations_url": "https://api.github.com/users/epriestley/orgs", + "repos_url": "https://api.github.com/users/epriestley/repos", + "events_url": "https://api.github.com/users/epriestley/events{/privacy}", + "received_events_url": "https://api.github.com/users/epriestley/received_events", + "type": "User", + "site_admin": false + }, + "labels": [ + + ], + "state": "open", + "locked": false, + "assignee": null, + "milestone": null, + "comments": 5, + "created_at": "2016-03-08T00:41:08Z", + "updated_at": "2016-03-09T14:34:46Z", + "closed_at": null, + "body": "OK" + } +} +~~~~~ +{ + "repository.name.full": "epriestley/poems", + "is.issue": true, + "is.pull": false, + "issue.number": 1 +} diff --git a/src/applications/nuance/github/__tests__/issueevents/demilestoned.txt b/src/applications/nuance/github/__tests__/issueevents/demilestoned.txt new file mode 100644 index 0000000000..bbde0e7d24 --- /dev/null +++ b/src/applications/nuance/github/__tests__/issueevents/demilestoned.txt @@ -0,0 +1,79 @@ +{ + "id": 583218613, + "url": "https://api.github.com/repos/epriestley/poems/issues/events/583218613", + "actor": { + "login": "epriestley", + "id": 102631, + "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "html_url": "https://github.com/epriestley", + "followers_url": "https://api.github.com/users/epriestley/followers", + "following_url": "https://api.github.com/users/epriestley/following{/other_user}", + "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", + "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", + "organizations_url": "https://api.github.com/users/epriestley/orgs", + "repos_url": "https://api.github.com/users/epriestley/repos", + "events_url": "https://api.github.com/users/epriestley/events{/privacy}", + "received_events_url": "https://api.github.com/users/epriestley/received_events", + "type": "User", + "site_admin": false + }, + "event": "demilestoned", + "commit_id": null, + "commit_url": null, + "created_at": "2016-03-09T12:43:36Z", + "milestone": { + "title": "b" + }, + "issue": { + "url": "https://api.github.com/repos/epriestley/poems/issues/1", + "repository_url": "https://api.github.com/repos/epriestley/poems", + "labels_url": "https://api.github.com/repos/epriestley/poems/issues/1/labels{/name}", + "comments_url": "https://api.github.com/repos/epriestley/poems/issues/1/comments", + "events_url": "https://api.github.com/repos/epriestley/poems/issues/1/events", + "html_url": "https://github.com/epriestley/poems/issues/1", + "id": 139138813, + "number": 1, + "title": "Enforce haiku in commit messages edit", + "user": { + "login": "epriestley", + "id": 102631, + "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "html_url": "https://github.com/epriestley", + "followers_url": "https://api.github.com/users/epriestley/followers", + "following_url": "https://api.github.com/users/epriestley/following{/other_user}", + "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", + "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", + "organizations_url": "https://api.github.com/users/epriestley/orgs", + "repos_url": "https://api.github.com/users/epriestley/repos", + "events_url": "https://api.github.com/users/epriestley/events{/privacy}", + "received_events_url": "https://api.github.com/users/epriestley/received_events", + "type": "User", + "site_admin": false + }, + "labels": [ + + ], + "state": "open", + "locked": false, + "assignee": null, + "milestone": null, + "comments": 5, + "created_at": "2016-03-08T00:41:08Z", + "updated_at": "2016-03-09T14:34:46Z", + "closed_at": null, + "body": "OK" + } +} +~~~~~ +{ + "repository.name.full": "epriestley/poems", + "is.issue": true, + "is.pull": false, + "issue.number": 1 +} diff --git a/src/applications/nuance/github/__tests__/issueevents/labeled.txt b/src/applications/nuance/github/__tests__/issueevents/labeled.txt new file mode 100644 index 0000000000..bf41262ac4 --- /dev/null +++ b/src/applications/nuance/github/__tests__/issueevents/labeled.txt @@ -0,0 +1,80 @@ +{ + "id": 583217784, + "url": "https://api.github.com/repos/epriestley/poems/issues/events/583217784", + "actor": { + "login": "epriestley", + "id": 102631, + "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "html_url": "https://github.com/epriestley", + "followers_url": "https://api.github.com/users/epriestley/followers", + "following_url": "https://api.github.com/users/epriestley/following{/other_user}", + "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", + "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", + "organizations_url": "https://api.github.com/users/epriestley/orgs", + "repos_url": "https://api.github.com/users/epriestley/repos", + "events_url": "https://api.github.com/users/epriestley/events{/privacy}", + "received_events_url": "https://api.github.com/users/epriestley/received_events", + "type": "User", + "site_admin": false + }, + "event": "labeled", + "commit_id": null, + "commit_url": null, + "created_at": "2016-03-09T12:42:44Z", + "label": { + "name": "bug", + "color": "fc2929" + }, + "issue": { + "url": "https://api.github.com/repos/epriestley/poems/issues/1", + "repository_url": "https://api.github.com/repos/epriestley/poems", + "labels_url": "https://api.github.com/repos/epriestley/poems/issues/1/labels{/name}", + "comments_url": "https://api.github.com/repos/epriestley/poems/issues/1/comments", + "events_url": "https://api.github.com/repos/epriestley/poems/issues/1/events", + "html_url": "https://github.com/epriestley/poems/issues/1", + "id": 139138813, + "number": 1, + "title": "Enforce haiku in commit messages edit", + "user": { + "login": "epriestley", + "id": 102631, + "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "html_url": "https://github.com/epriestley", + "followers_url": "https://api.github.com/users/epriestley/followers", + "following_url": "https://api.github.com/users/epriestley/following{/other_user}", + "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", + "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", + "organizations_url": "https://api.github.com/users/epriestley/orgs", + "repos_url": "https://api.github.com/users/epriestley/repos", + "events_url": "https://api.github.com/users/epriestley/events{/privacy}", + "received_events_url": "https://api.github.com/users/epriestley/received_events", + "type": "User", + "site_admin": false + }, + "labels": [ + + ], + "state": "open", + "locked": false, + "assignee": null, + "milestone": null, + "comments": 5, + "created_at": "2016-03-08T00:41:08Z", + "updated_at": "2016-03-09T14:34:46Z", + "closed_at": null, + "body": "OK" + } +} +~~~~~ +{ + "repository.name.full": "epriestley/poems", + "is.issue": true, + "is.pull": false, + "issue.number": 1 +} diff --git a/src/applications/nuance/github/__tests__/issueevents/locked.txt b/src/applications/nuance/github/__tests__/issueevents/locked.txt new file mode 100644 index 0000000000..440eec0d95 --- /dev/null +++ b/src/applications/nuance/github/__tests__/issueevents/locked.txt @@ -0,0 +1,76 @@ +{ + "id": 583218006, + "url": "https://api.github.com/repos/epriestley/poems/issues/events/583218006", + "actor": { + "login": "epriestley", + "id": 102631, + "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "html_url": "https://github.com/epriestley", + "followers_url": "https://api.github.com/users/epriestley/followers", + "following_url": "https://api.github.com/users/epriestley/following{/other_user}", + "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", + "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", + "organizations_url": "https://api.github.com/users/epriestley/orgs", + "repos_url": "https://api.github.com/users/epriestley/repos", + "events_url": "https://api.github.com/users/epriestley/events{/privacy}", + "received_events_url": "https://api.github.com/users/epriestley/received_events", + "type": "User", + "site_admin": false + }, + "event": "locked", + "commit_id": null, + "commit_url": null, + "created_at": "2016-03-09T12:42:58Z", + "issue": { + "url": "https://api.github.com/repos/epriestley/poems/issues/1", + "repository_url": "https://api.github.com/repos/epriestley/poems", + "labels_url": "https://api.github.com/repos/epriestley/poems/issues/1/labels{/name}", + "comments_url": "https://api.github.com/repos/epriestley/poems/issues/1/comments", + "events_url": "https://api.github.com/repos/epriestley/poems/issues/1/events", + "html_url": "https://github.com/epriestley/poems/issues/1", + "id": 139138813, + "number": 1, + "title": "Enforce haiku in commit messages edit", + "user": { + "login": "epriestley", + "id": 102631, + "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "html_url": "https://github.com/epriestley", + "followers_url": "https://api.github.com/users/epriestley/followers", + "following_url": "https://api.github.com/users/epriestley/following{/other_user}", + "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", + "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", + "organizations_url": "https://api.github.com/users/epriestley/orgs", + "repos_url": "https://api.github.com/users/epriestley/repos", + "events_url": "https://api.github.com/users/epriestley/events{/privacy}", + "received_events_url": "https://api.github.com/users/epriestley/received_events", + "type": "User", + "site_admin": false + }, + "labels": [ + + ], + "state": "open", + "locked": false, + "assignee": null, + "milestone": null, + "comments": 5, + "created_at": "2016-03-08T00:41:08Z", + "updated_at": "2016-03-09T14:34:46Z", + "closed_at": null, + "body": "OK" + } +} +~~~~~ +{ + "repository.name.full": "epriestley/poems", + "is.issue": true, + "is.pull": false, + "issue.number": 1 +} diff --git a/src/applications/nuance/github/__tests__/issueevents/milestoned.txt b/src/applications/nuance/github/__tests__/issueevents/milestoned.txt new file mode 100644 index 0000000000..e8b32b1113 --- /dev/null +++ b/src/applications/nuance/github/__tests__/issueevents/milestoned.txt @@ -0,0 +1,79 @@ +{ + "id": 583217866, + "url": "https://api.github.com/repos/epriestley/poems/issues/events/583217866", + "actor": { + "login": "epriestley", + "id": 102631, + "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "html_url": "https://github.com/epriestley", + "followers_url": "https://api.github.com/users/epriestley/followers", + "following_url": "https://api.github.com/users/epriestley/following{/other_user}", + "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", + "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", + "organizations_url": "https://api.github.com/users/epriestley/orgs", + "repos_url": "https://api.github.com/users/epriestley/repos", + "events_url": "https://api.github.com/users/epriestley/events{/privacy}", + "received_events_url": "https://api.github.com/users/epriestley/received_events", + "type": "User", + "site_admin": false + }, + "event": "milestoned", + "commit_id": null, + "commit_url": null, + "created_at": "2016-03-09T12:42:50Z", + "milestone": { + "title": "b" + }, + "issue": { + "url": "https://api.github.com/repos/epriestley/poems/issues/1", + "repository_url": "https://api.github.com/repos/epriestley/poems", + "labels_url": "https://api.github.com/repos/epriestley/poems/issues/1/labels{/name}", + "comments_url": "https://api.github.com/repos/epriestley/poems/issues/1/comments", + "events_url": "https://api.github.com/repos/epriestley/poems/issues/1/events", + "html_url": "https://github.com/epriestley/poems/issues/1", + "id": 139138813, + "number": 1, + "title": "Enforce haiku in commit messages edit", + "user": { + "login": "epriestley", + "id": 102631, + "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "html_url": "https://github.com/epriestley", + "followers_url": "https://api.github.com/users/epriestley/followers", + "following_url": "https://api.github.com/users/epriestley/following{/other_user}", + "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", + "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", + "organizations_url": "https://api.github.com/users/epriestley/orgs", + "repos_url": "https://api.github.com/users/epriestley/repos", + "events_url": "https://api.github.com/users/epriestley/events{/privacy}", + "received_events_url": "https://api.github.com/users/epriestley/received_events", + "type": "User", + "site_admin": false + }, + "labels": [ + + ], + "state": "open", + "locked": false, + "assignee": null, + "milestone": null, + "comments": 5, + "created_at": "2016-03-08T00:41:08Z", + "updated_at": "2016-03-09T14:34:46Z", + "closed_at": null, + "body": "OK" + } +} +~~~~~ +{ + "repository.name.full": "epriestley/poems", + "is.issue": true, + "is.pull": false, + "issue.number": 1 +} diff --git a/src/applications/nuance/github/__tests__/issueevents/renamed.txt b/src/applications/nuance/github/__tests__/issueevents/renamed.txt new file mode 100644 index 0000000000..0cbbd1ebb8 --- /dev/null +++ b/src/applications/nuance/github/__tests__/issueevents/renamed.txt @@ -0,0 +1,80 @@ +{ + "id": 583218162, + "url": "https://api.github.com/repos/epriestley/poems/issues/events/583218162", + "actor": { + "login": "epriestley", + "id": 102631, + "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "html_url": "https://github.com/epriestley", + "followers_url": "https://api.github.com/users/epriestley/followers", + "following_url": "https://api.github.com/users/epriestley/following{/other_user}", + "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", + "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", + "organizations_url": "https://api.github.com/users/epriestley/orgs", + "repos_url": "https://api.github.com/users/epriestley/repos", + "events_url": "https://api.github.com/users/epriestley/events{/privacy}", + "received_events_url": "https://api.github.com/users/epriestley/received_events", + "type": "User", + "site_admin": false + }, + "event": "renamed", + "commit_id": null, + "commit_url": null, + "created_at": "2016-03-09T12:43:07Z", + "rename": { + "from": "Enforce haiku in commit messages", + "to": "Enforce haiku in commit messages edit" + }, + "issue": { + "url": "https://api.github.com/repos/epriestley/poems/issues/1", + "repository_url": "https://api.github.com/repos/epriestley/poems", + "labels_url": "https://api.github.com/repos/epriestley/poems/issues/1/labels{/name}", + "comments_url": "https://api.github.com/repos/epriestley/poems/issues/1/comments", + "events_url": "https://api.github.com/repos/epriestley/poems/issues/1/events", + "html_url": "https://github.com/epriestley/poems/issues/1", + "id": 139138813, + "number": 1, + "title": "Enforce haiku in commit messages edit", + "user": { + "login": "epriestley", + "id": 102631, + "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "html_url": "https://github.com/epriestley", + "followers_url": "https://api.github.com/users/epriestley/followers", + "following_url": "https://api.github.com/users/epriestley/following{/other_user}", + "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", + "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", + "organizations_url": "https://api.github.com/users/epriestley/orgs", + "repos_url": "https://api.github.com/users/epriestley/repos", + "events_url": "https://api.github.com/users/epriestley/events{/privacy}", + "received_events_url": "https://api.github.com/users/epriestley/received_events", + "type": "User", + "site_admin": false + }, + "labels": [ + + ], + "state": "open", + "locked": false, + "assignee": null, + "milestone": null, + "comments": 5, + "created_at": "2016-03-08T00:41:08Z", + "updated_at": "2016-03-09T14:34:46Z", + "closed_at": null, + "body": "OK" + } +} +~~~~~ +{ + "repository.name.full": "epriestley/poems", + "is.issue": true, + "is.pull": false, + "issue.number": 1 +} diff --git a/src/applications/nuance/github/__tests__/issueevents/reopened.txt b/src/applications/nuance/github/__tests__/issueevents/reopened.txt new file mode 100644 index 0000000000..bc778728d8 --- /dev/null +++ b/src/applications/nuance/github/__tests__/issueevents/reopened.txt @@ -0,0 +1,76 @@ +{ + "id": 583218814, + "url": "https://api.github.com/repos/epriestley/poems/issues/events/583218814", + "actor": { + "login": "epriestley", + "id": 102631, + "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "html_url": "https://github.com/epriestley", + "followers_url": "https://api.github.com/users/epriestley/followers", + "following_url": "https://api.github.com/users/epriestley/following{/other_user}", + "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", + "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", + "organizations_url": "https://api.github.com/users/epriestley/orgs", + "repos_url": "https://api.github.com/users/epriestley/repos", + "events_url": "https://api.github.com/users/epriestley/events{/privacy}", + "received_events_url": "https://api.github.com/users/epriestley/received_events", + "type": "User", + "site_admin": false + }, + "event": "reopened", + "commit_id": null, + "commit_url": null, + "created_at": "2016-03-09T12:43:50Z", + "issue": { + "url": "https://api.github.com/repos/epriestley/poems/issues/1", + "repository_url": "https://api.github.com/repos/epriestley/poems", + "labels_url": "https://api.github.com/repos/epriestley/poems/issues/1/labels{/name}", + "comments_url": "https://api.github.com/repos/epriestley/poems/issues/1/comments", + "events_url": "https://api.github.com/repos/epriestley/poems/issues/1/events", + "html_url": "https://github.com/epriestley/poems/issues/1", + "id": 139138813, + "number": 1, + "title": "Enforce haiku in commit messages edit", + "user": { + "login": "epriestley", + "id": 102631, + "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "html_url": "https://github.com/epriestley", + "followers_url": "https://api.github.com/users/epriestley/followers", + "following_url": "https://api.github.com/users/epriestley/following{/other_user}", + "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", + "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", + "organizations_url": "https://api.github.com/users/epriestley/orgs", + "repos_url": "https://api.github.com/users/epriestley/repos", + "events_url": "https://api.github.com/users/epriestley/events{/privacy}", + "received_events_url": "https://api.github.com/users/epriestley/received_events", + "type": "User", + "site_admin": false + }, + "labels": [ + + ], + "state": "open", + "locked": false, + "assignee": null, + "milestone": null, + "comments": 5, + "created_at": "2016-03-08T00:41:08Z", + "updated_at": "2016-03-09T14:34:46Z", + "closed_at": null, + "body": "OK" + } +} +~~~~~ +{ + "repository.name.full": "epriestley/poems", + "is.issue": true, + "is.pull": false, + "issue.number": 1 +} diff --git a/src/applications/nuance/github/__tests__/issueevents/unassigned.txt b/src/applications/nuance/github/__tests__/issueevents/unassigned.txt new file mode 100644 index 0000000000..bc8b9e1df9 --- /dev/null +++ b/src/applications/nuance/github/__tests__/issueevents/unassigned.txt @@ -0,0 +1,114 @@ +{ + "id": 583218511, + "url": "https://api.github.com/repos/epriestley/poems/issues/events/583218511", + "actor": { + "login": "epriestley", + "id": 102631, + "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "html_url": "https://github.com/epriestley", + "followers_url": "https://api.github.com/users/epriestley/followers", + "following_url": "https://api.github.com/users/epriestley/following{/other_user}", + "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", + "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", + "organizations_url": "https://api.github.com/users/epriestley/orgs", + "repos_url": "https://api.github.com/users/epriestley/repos", + "events_url": "https://api.github.com/users/epriestley/events{/privacy}", + "received_events_url": "https://api.github.com/users/epriestley/received_events", + "type": "User", + "site_admin": false + }, + "event": "unassigned", + "commit_id": null, + "commit_url": null, + "created_at": "2016-03-09T12:43:29Z", + "assignee": { + "login": "epriestley", + "id": 102631, + "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "html_url": "https://github.com/epriestley", + "followers_url": "https://api.github.com/users/epriestley/followers", + "following_url": "https://api.github.com/users/epriestley/following{/other_user}", + "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", + "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", + "organizations_url": "https://api.github.com/users/epriestley/orgs", + "repos_url": "https://api.github.com/users/epriestley/repos", + "events_url": "https://api.github.com/users/epriestley/events{/privacy}", + "received_events_url": "https://api.github.com/users/epriestley/received_events", + "type": "User", + "site_admin": false + }, + "assigner": { + "login": "epriestley", + "id": 102631, + "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "html_url": "https://github.com/epriestley", + "followers_url": "https://api.github.com/users/epriestley/followers", + "following_url": "https://api.github.com/users/epriestley/following{/other_user}", + "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", + "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", + "organizations_url": "https://api.github.com/users/epriestley/orgs", + "repos_url": "https://api.github.com/users/epriestley/repos", + "events_url": "https://api.github.com/users/epriestley/events{/privacy}", + "received_events_url": "https://api.github.com/users/epriestley/received_events", + "type": "User", + "site_admin": false + }, + "issue": { + "url": "https://api.github.com/repos/epriestley/poems/issues/1", + "repository_url": "https://api.github.com/repos/epriestley/poems", + "labels_url": "https://api.github.com/repos/epriestley/poems/issues/1/labels{/name}", + "comments_url": "https://api.github.com/repos/epriestley/poems/issues/1/comments", + "events_url": "https://api.github.com/repos/epriestley/poems/issues/1/events", + "html_url": "https://github.com/epriestley/poems/issues/1", + "id": 139138813, + "number": 1, + "title": "Enforce haiku in commit messages edit", + "user": { + "login": "epriestley", + "id": 102631, + "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "html_url": "https://github.com/epriestley", + "followers_url": "https://api.github.com/users/epriestley/followers", + "following_url": "https://api.github.com/users/epriestley/following{/other_user}", + "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", + "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", + "organizations_url": "https://api.github.com/users/epriestley/orgs", + "repos_url": "https://api.github.com/users/epriestley/repos", + "events_url": "https://api.github.com/users/epriestley/events{/privacy}", + "received_events_url": "https://api.github.com/users/epriestley/received_events", + "type": "User", + "site_admin": false + }, + "labels": [ + + ], + "state": "open", + "locked": false, + "assignee": null, + "milestone": null, + "comments": 5, + "created_at": "2016-03-08T00:41:08Z", + "updated_at": "2016-03-09T14:34:46Z", + "closed_at": null, + "body": "OK" + } +} +~~~~~ +{ + "repository.name.full": "epriestley/poems", + "is.issue": true, + "is.pull": false, + "issue.number": 1 +} diff --git a/src/applications/nuance/github/__tests__/issueevents/unlabeled.txt b/src/applications/nuance/github/__tests__/issueevents/unlabeled.txt new file mode 100644 index 0000000000..e3435605e0 --- /dev/null +++ b/src/applications/nuance/github/__tests__/issueevents/unlabeled.txt @@ -0,0 +1,80 @@ +{ + "id": 583218703, + "url": "https://api.github.com/repos/epriestley/poems/issues/events/583218703", + "actor": { + "login": "epriestley", + "id": 102631, + "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "html_url": "https://github.com/epriestley", + "followers_url": "https://api.github.com/users/epriestley/followers", + "following_url": "https://api.github.com/users/epriestley/following{/other_user}", + "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", + "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", + "organizations_url": "https://api.github.com/users/epriestley/orgs", + "repos_url": "https://api.github.com/users/epriestley/repos", + "events_url": "https://api.github.com/users/epriestley/events{/privacy}", + "received_events_url": "https://api.github.com/users/epriestley/received_events", + "type": "User", + "site_admin": false + }, + "event": "unlabeled", + "commit_id": null, + "commit_url": null, + "created_at": "2016-03-09T12:43:42Z", + "label": { + "name": "bug", + "color": "fc2929" + }, + "issue": { + "url": "https://api.github.com/repos/epriestley/poems/issues/1", + "repository_url": "https://api.github.com/repos/epriestley/poems", + "labels_url": "https://api.github.com/repos/epriestley/poems/issues/1/labels{/name}", + "comments_url": "https://api.github.com/repos/epriestley/poems/issues/1/comments", + "events_url": "https://api.github.com/repos/epriestley/poems/issues/1/events", + "html_url": "https://github.com/epriestley/poems/issues/1", + "id": 139138813, + "number": 1, + "title": "Enforce haiku in commit messages edit", + "user": { + "login": "epriestley", + "id": 102631, + "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "html_url": "https://github.com/epriestley", + "followers_url": "https://api.github.com/users/epriestley/followers", + "following_url": "https://api.github.com/users/epriestley/following{/other_user}", + "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", + "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", + "organizations_url": "https://api.github.com/users/epriestley/orgs", + "repos_url": "https://api.github.com/users/epriestley/repos", + "events_url": "https://api.github.com/users/epriestley/events{/privacy}", + "received_events_url": "https://api.github.com/users/epriestley/received_events", + "type": "User", + "site_admin": false + }, + "labels": [ + + ], + "state": "open", + "locked": false, + "assignee": null, + "milestone": null, + "comments": 5, + "created_at": "2016-03-08T00:41:08Z", + "updated_at": "2016-03-09T14:34:46Z", + "closed_at": null, + "body": "OK" + } +} +~~~~~ +{ + "repository.name.full": "epriestley/poems", + "is.issue": true, + "is.pull": false, + "issue.number": 1 +} diff --git a/src/applications/nuance/github/__tests__/issueevents/unlocked.txt b/src/applications/nuance/github/__tests__/issueevents/unlocked.txt new file mode 100644 index 0000000000..e59ba6e93f --- /dev/null +++ b/src/applications/nuance/github/__tests__/issueevents/unlocked.txt @@ -0,0 +1,76 @@ +{ + "id": 583218062, + "url": "https://api.github.com/repos/epriestley/poems/issues/events/583218062", + "actor": { + "login": "epriestley", + "id": 102631, + "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "html_url": "https://github.com/epriestley", + "followers_url": "https://api.github.com/users/epriestley/followers", + "following_url": "https://api.github.com/users/epriestley/following{/other_user}", + "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", + "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", + "organizations_url": "https://api.github.com/users/epriestley/orgs", + "repos_url": "https://api.github.com/users/epriestley/repos", + "events_url": "https://api.github.com/users/epriestley/events{/privacy}", + "received_events_url": "https://api.github.com/users/epriestley/received_events", + "type": "User", + "site_admin": false + }, + "event": "unlocked", + "commit_id": null, + "commit_url": null, + "created_at": "2016-03-09T12:43:01Z", + "issue": { + "url": "https://api.github.com/repos/epriestley/poems/issues/1", + "repository_url": "https://api.github.com/repos/epriestley/poems", + "labels_url": "https://api.github.com/repos/epriestley/poems/issues/1/labels{/name}", + "comments_url": "https://api.github.com/repos/epriestley/poems/issues/1/comments", + "events_url": "https://api.github.com/repos/epriestley/poems/issues/1/events", + "html_url": "https://github.com/epriestley/poems/issues/1", + "id": 139138813, + "number": 1, + "title": "Enforce haiku in commit messages edit", + "user": { + "login": "epriestley", + "id": 102631, + "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "html_url": "https://github.com/epriestley", + "followers_url": "https://api.github.com/users/epriestley/followers", + "following_url": "https://api.github.com/users/epriestley/following{/other_user}", + "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", + "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", + "organizations_url": "https://api.github.com/users/epriestley/orgs", + "repos_url": "https://api.github.com/users/epriestley/repos", + "events_url": "https://api.github.com/users/epriestley/events{/privacy}", + "received_events_url": "https://api.github.com/users/epriestley/received_events", + "type": "User", + "site_admin": false + }, + "labels": [ + + ], + "state": "open", + "locked": false, + "assignee": null, + "milestone": null, + "comments": 5, + "created_at": "2016-03-08T00:41:08Z", + "updated_at": "2016-03-09T14:34:46Z", + "closed_at": null, + "body": "OK" + } +} +~~~~~ +{ + "repository.name.full": "epriestley/poems", + "is.issue": true, + "is.pull": false, + "issue.number": 1 +} diff --git a/src/applications/nuance/github/__tests__/repositoryevents/IssueCommentEvent.created.pull.txt b/src/applications/nuance/github/__tests__/repositoryevents/IssueCommentEvent.created.pull.txt new file mode 100644 index 0000000000..0068f7c092 --- /dev/null +++ b/src/applications/nuance/github/__tests__/repositoryevents/IssueCommentEvent.created.pull.txt @@ -0,0 +1,161 @@ +{ + "id": "3740938746", + "type": "IssueCommentEvent", + "actor": { + "id": 102631, + "login": "epriestley", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "avatar_url": "https://avatars.githubusercontent.com/u/102631?" + }, + "repo": { + "id": 14627834, + "name": "epriestley/poems", + "url": "https://api.github.com/repos/epriestley/poems" + }, + "payload": { + "action": "created", + "issue": { + "url": "https://api.github.com/repos/epriestley/poems/issues/2", + "repository_url": "https://api.github.com/repos/epriestley/poems", + "labels_url": "https://api.github.com/repos/epriestley/poems/issues/2/labels{/name}", + "comments_url": "https://api.github.com/repos/epriestley/poems/issues/2/comments", + "events_url": "https://api.github.com/repos/epriestley/poems/issues/2/events", + "html_url": "https://github.com/epriestley/poems/pull/2", + "id": 139568860, + "number": 2, + "title": "Please Merge Quack2 into Feature", + "user": { + "login": "epriestley", + "id": 102631, + "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "html_url": "https://github.com/epriestley", + "followers_url": "https://api.github.com/users/epriestley/followers", + "following_url": "https://api.github.com/users/epriestley/following{/other_user}", + "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", + "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", + "organizations_url": "https://api.github.com/users/epriestley/orgs", + "repos_url": "https://api.github.com/users/epriestley/repos", + "events_url": "https://api.github.com/users/epriestley/events{/privacy}", + "received_events_url": "https://api.github.com/users/epriestley/received_events", + "type": "User", + "site_admin": false + }, + "labels": [ + { + "url": "https://api.github.com/repos/epriestley/poems/labels/bug", + "name": "bug", + "color": "fc2929" + } + ], + "state": "open", + "locked": false, + "assignee": { + "login": "epriestley", + "id": 102631, + "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "html_url": "https://github.com/epriestley", + "followers_url": "https://api.github.com/users/epriestley/followers", + "following_url": "https://api.github.com/users/epriestley/following{/other_user}", + "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", + "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", + "organizations_url": "https://api.github.com/users/epriestley/orgs", + "repos_url": "https://api.github.com/users/epriestley/repos", + "events_url": "https://api.github.com/users/epriestley/events{/privacy}", + "received_events_url": "https://api.github.com/users/epriestley/received_events", + "type": "User", + "site_admin": false + }, + "milestone": { + "url": "https://api.github.com/repos/epriestley/poems/milestones/1", + "html_url": "https://github.com/epriestley/poems/milestones/b", + "labels_url": "https://api.github.com/repos/epriestley/poems/milestones/1/labels", + "id": 1633589, + "number": 1, + "title": "b", + "description": null, + "creator": { + "login": "epriestley", + "id": 102631, + "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "html_url": "https://github.com/epriestley", + "followers_url": "https://api.github.com/users/epriestley/followers", + "following_url": "https://api.github.com/users/epriestley/following{/other_user}", + "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", + "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", + "organizations_url": "https://api.github.com/users/epriestley/orgs", + "repos_url": "https://api.github.com/users/epriestley/repos", + "events_url": "https://api.github.com/users/epriestley/events{/privacy}", + "received_events_url": "https://api.github.com/users/epriestley/received_events", + "type": "User", + "site_admin": false + }, + "open_issues": 1, + "closed_issues": 0, + "state": "open", + "created_at": "2016-03-09T12:42:50Z", + "updated_at": "2016-03-09T12:52:41Z", + "due_on": null, + "closed_at": null + }, + "comments": 1, + "created_at": "2016-03-09T12:52:31Z", + "updated_at": "2016-03-09T12:53:06Z", + "closed_at": null, + "pull_request": { + "url": "https://api.github.com/repos/epriestley/poems/pulls/2", + "html_url": "https://github.com/epriestley/poems/pull/2", + "diff_url": "https://github.com/epriestley/poems/pull/2.diff", + "patch_url": "https://github.com/epriestley/poems/pull/2.patch" + }, + "body": "" + }, + "comment": { + "url": "https://api.github.com/repos/epriestley/poems/issues/comments/194282800", + "html_url": "https://github.com/epriestley/poems/pull/2#issuecomment-194282800", + "issue_url": "https://api.github.com/repos/epriestley/poems/issues/2", + "id": 194282800, + "user": { + "login": "epriestley", + "id": 102631, + "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "html_url": "https://github.com/epriestley", + "followers_url": "https://api.github.com/users/epriestley/followers", + "following_url": "https://api.github.com/users/epriestley/following{/other_user}", + "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", + "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", + "organizations_url": "https://api.github.com/users/epriestley/orgs", + "repos_url": "https://api.github.com/users/epriestley/repos", + "events_url": "https://api.github.com/users/epriestley/events{/privacy}", + "received_events_url": "https://api.github.com/users/epriestley/received_events", + "type": "User", + "site_admin": false + }, + "created_at": "2016-03-09T12:53:06Z", + "updated_at": "2016-03-09T12:53:06Z", + "body": "wub wub" + } + }, + "public": true, + "created_at": "2016-03-09T12:53:06Z" +} +~~~~~ +{ + "repository.name.full": "epriestley/poems", + "is.issue": false, + "is.pull": true, + "issue.number": null, + "pull.number": 2 +} diff --git a/src/applications/nuance/github/__tests__/repositoryevents/IssueCommentEvent.created.txt b/src/applications/nuance/github/__tests__/repositoryevents/IssueCommentEvent.created.txt new file mode 100644 index 0000000000..c6fa5ccb54 --- /dev/null +++ b/src/applications/nuance/github/__tests__/repositoryevents/IssueCommentEvent.created.txt @@ -0,0 +1,98 @@ +{ + "id": "3733510485", + "type": "IssueCommentEvent", + "actor": { + "id": 102631, + "login": "epriestley", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "avatar_url": "https://avatars.githubusercontent.com/u/102631?" + }, + "repo": { + "id": 14627834, + "name": "epriestley/poems", + "url": "https://api.github.com/repos/epriestley/poems" + }, + "payload": { + "action": "created", + "issue": { + "url": "https://api.github.com/repos/epriestley/poems/issues/1", + "repository_url": "https://api.github.com/repos/epriestley/poems", + "labels_url": "https://api.github.com/repos/epriestley/poems/issues/1/labels{/name}", + "comments_url": "https://api.github.com/repos/epriestley/poems/issues/1/comments", + "events_url": "https://api.github.com/repos/epriestley/poems/issues/1/events", + "html_url": "https://github.com/epriestley/poems/issues/1", + "id": 139138813, + "number": 1, + "title": "Enforce haiku in commit messages", + "user": { + "login": "epriestley", + "id": 102631, + "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "html_url": "https://github.com/epriestley", + "followers_url": "https://api.github.com/users/epriestley/followers", + "following_url": "https://api.github.com/users/epriestley/following{/other_user}", + "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", + "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", + "organizations_url": "https://api.github.com/users/epriestley/orgs", + "repos_url": "https://api.github.com/users/epriestley/repos", + "events_url": "https://api.github.com/users/epriestley/events{/privacy}", + "received_events_url": "https://api.github.com/users/epriestley/received_events", + "type": "User", + "site_admin": false + }, + "labels": [ + + ], + "state": "open", + "locked": false, + "assignee": null, + "milestone": null, + "comments": 1, + "created_at": "2016-03-08T00:41:08Z", + "updated_at": "2016-03-08T00:41:22Z", + "closed_at": null, + "body": "OK" + }, + "comment": { + "url": "https://api.github.com/repos/epriestley/poems/issues/comments/193528669", + "html_url": "https://github.com/epriestley/poems/issues/1#issuecomment-193528669", + "issue_url": "https://api.github.com/repos/epriestley/poems/issues/1", + "id": 193528669, + "user": { + "login": "epriestley", + "id": 102631, + "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "html_url": "https://github.com/epriestley", + "followers_url": "https://api.github.com/users/epriestley/followers", + "following_url": "https://api.github.com/users/epriestley/following{/other_user}", + "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", + "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", + "organizations_url": "https://api.github.com/users/epriestley/orgs", + "repos_url": "https://api.github.com/users/epriestley/repos", + "events_url": "https://api.github.com/users/epriestley/events{/privacy}", + "received_events_url": "https://api.github.com/users/epriestley/received_events", + "type": "User", + "site_admin": false + }, + "created_at": "2016-03-08T00:41:22Z", + "updated_at": "2016-03-08T00:41:22Z", + "body": "comment on issue" + } + }, + "public": true, + "created_at": "2016-03-08T00:41:22Z" +} +~~~~~ +{ + "repository.name.full": "epriestley/poems", + "is.issue": true, + "is.pull": false, + "issue.number": 1 +} diff --git a/src/applications/nuance/github/__tests__/repositoryevents/IssuesEvent.closed.txt b/src/applications/nuance/github/__tests__/repositoryevents/IssuesEvent.closed.txt new file mode 100644 index 0000000000..8373e2ee52 --- /dev/null +++ b/src/applications/nuance/github/__tests__/repositoryevents/IssuesEvent.closed.txt @@ -0,0 +1,70 @@ +{ + "id": "3740905151", + "type": "IssuesEvent", + "actor": { + "id": 102631, + "login": "epriestley", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "avatar_url": "https://avatars.githubusercontent.com/u/102631?" + }, + "repo": { + "id": 14627834, + "name": "epriestley/poems", + "url": "https://api.github.com/repos/epriestley/poems" + }, + "payload": { + "action": "closed", + "issue": { + "url": "https://api.github.com/repos/epriestley/poems/issues/1", + "repository_url": "https://api.github.com/repos/epriestley/poems", + "labels_url": "https://api.github.com/repos/epriestley/poems/issues/1/labels{/name}", + "comments_url": "https://api.github.com/repos/epriestley/poems/issues/1/comments", + "events_url": "https://api.github.com/repos/epriestley/poems/issues/1/events", + "html_url": "https://github.com/epriestley/poems/issues/1", + "id": 139138813, + "number": 1, + "title": "Enforce haiku in commit messages edit", + "user": { + "login": "epriestley", + "id": 102631, + "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "html_url": "https://github.com/epriestley", + "followers_url": "https://api.github.com/users/epriestley/followers", + "following_url": "https://api.github.com/users/epriestley/following{/other_user}", + "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", + "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", + "organizations_url": "https://api.github.com/users/epriestley/orgs", + "repos_url": "https://api.github.com/users/epriestley/repos", + "events_url": "https://api.github.com/users/epriestley/events{/privacy}", + "received_events_url": "https://api.github.com/users/epriestley/received_events", + "type": "User", + "site_admin": false + }, + "labels": [ + + ], + "state": "closed", + "locked": false, + "assignee": null, + "milestone": null, + "comments": 2, + "created_at": "2016-03-08T00:41:08Z", + "updated_at": "2016-03-09T12:43:48Z", + "closed_at": "2016-03-09T12:43:48Z", + "body": "OK" + } + }, + "public": true, + "created_at": "2016-03-09T12:43:48Z" +} +~~~~~ +{ + "repository.name.full": "epriestley/poems", + "is.issue": true, + "is.pull": false, + "issue.number": 1 +} diff --git a/src/applications/nuance/github/__tests__/repositoryevents/IssuesEvent.opened.txt b/src/applications/nuance/github/__tests__/repositoryevents/IssuesEvent.opened.txt new file mode 100644 index 0000000000..91068727e7 --- /dev/null +++ b/src/applications/nuance/github/__tests__/repositoryevents/IssuesEvent.opened.txt @@ -0,0 +1,70 @@ +{ + "id": "3733509737", + "type": "IssuesEvent", + "actor": { + "id": 102631, + "login": "epriestley", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "avatar_url": "https://avatars.githubusercontent.com/u/102631?" + }, + "repo": { + "id": 14627834, + "name": "epriestley/poems", + "url": "https://api.github.com/repos/epriestley/poems" + }, + "payload": { + "action": "opened", + "issue": { + "url": "https://api.github.com/repos/epriestley/poems/issues/1", + "repository_url": "https://api.github.com/repos/epriestley/poems", + "labels_url": "https://api.github.com/repos/epriestley/poems/issues/1/labels{/name}", + "comments_url": "https://api.github.com/repos/epriestley/poems/issues/1/comments", + "events_url": "https://api.github.com/repos/epriestley/poems/issues/1/events", + "html_url": "https://github.com/epriestley/poems/issues/1", + "id": 139138813, + "number": 1, + "title": "Enforce haiku in commit messages", + "user": { + "login": "epriestley", + "id": 102631, + "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "html_url": "https://github.com/epriestley", + "followers_url": "https://api.github.com/users/epriestley/followers", + "following_url": "https://api.github.com/users/epriestley/following{/other_user}", + "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", + "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", + "organizations_url": "https://api.github.com/users/epriestley/orgs", + "repos_url": "https://api.github.com/users/epriestley/repos", + "events_url": "https://api.github.com/users/epriestley/events{/privacy}", + "received_events_url": "https://api.github.com/users/epriestley/received_events", + "type": "User", + "site_admin": false + }, + "labels": [ + + ], + "state": "open", + "locked": false, + "assignee": null, + "milestone": null, + "comments": 0, + "created_at": "2016-03-08T00:41:08Z", + "updated_at": "2016-03-08T00:41:08Z", + "closed_at": null, + "body": "OK" + } + }, + "public": true, + "created_at": "2016-03-08T00:41:08Z" +} +~~~~~ +{ + "repository.name.full": "epriestley/poems", + "is.issue": true, + "is.pull": false, + "issue.number": 1 +} diff --git a/src/applications/nuance/github/__tests__/repositoryevents/IssuesEvent.reopened.txt b/src/applications/nuance/github/__tests__/repositoryevents/IssuesEvent.reopened.txt new file mode 100644 index 0000000000..6ab81e1028 --- /dev/null +++ b/src/applications/nuance/github/__tests__/repositoryevents/IssuesEvent.reopened.txt @@ -0,0 +1,70 @@ +{ + "id": "3740908680", + "type": "IssuesEvent", + "actor": { + "id": 102631, + "login": "epriestley", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "avatar_url": "https://avatars.githubusercontent.com/u/102631?" + }, + "repo": { + "id": 14627834, + "name": "epriestley/poems", + "url": "https://api.github.com/repos/epriestley/poems" + }, + "payload": { + "action": "reopened", + "issue": { + "url": "https://api.github.com/repos/epriestley/poems/issues/1", + "repository_url": "https://api.github.com/repos/epriestley/poems", + "labels_url": "https://api.github.com/repos/epriestley/poems/issues/1/labels{/name}", + "comments_url": "https://api.github.com/repos/epriestley/poems/issues/1/comments", + "events_url": "https://api.github.com/repos/epriestley/poems/issues/1/events", + "html_url": "https://github.com/epriestley/poems/issues/1", + "id": 139138813, + "number": 1, + "title": "Enforce haiku in commit messages edit", + "user": { + "login": "epriestley", + "id": 102631, + "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "html_url": "https://github.com/epriestley", + "followers_url": "https://api.github.com/users/epriestley/followers", + "following_url": "https://api.github.com/users/epriestley/following{/other_user}", + "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", + "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", + "organizations_url": "https://api.github.com/users/epriestley/orgs", + "repos_url": "https://api.github.com/users/epriestley/repos", + "events_url": "https://api.github.com/users/epriestley/events{/privacy}", + "received_events_url": "https://api.github.com/users/epriestley/received_events", + "type": "User", + "site_admin": false + }, + "labels": [ + + ], + "state": "open", + "locked": false, + "assignee": null, + "milestone": null, + "comments": 3, + "created_at": "2016-03-08T00:41:08Z", + "updated_at": "2016-03-09T12:44:49Z", + "closed_at": null, + "body": "OK" + } + }, + "public": true, + "created_at": "2016-03-09T12:44:49Z" +} +~~~~~ +{ + "repository.name.full": "epriestley/poems", + "is.issue": true, + "is.pull": false, + "issue.number": 1 +} diff --git a/src/applications/nuance/github/__tests__/repositoryevents/PullRequestEvent.opened.txt b/src/applications/nuance/github/__tests__/repositoryevents/PullRequestEvent.opened.txt new file mode 100644 index 0000000000..848ed63afe --- /dev/null +++ b/src/applications/nuance/github/__tests__/repositoryevents/PullRequestEvent.opened.txt @@ -0,0 +1,334 @@ +{ + "id": "3740936638", + "type": "PullRequestEvent", + "actor": { + "id": 102631, + "login": "epriestley", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "avatar_url": "https://avatars.githubusercontent.com/u/102631?" + }, + "repo": { + "id": 14627834, + "name": "epriestley/poems", + "url": "https://api.github.com/repos/epriestley/poems" + }, + "payload": { + "action": "opened", + "number": 2, + "pull_request": { + "url": "https://api.github.com/repos/epriestley/poems/pulls/2", + "id": 62223852, + "html_url": "https://github.com/epriestley/poems/pull/2", + "diff_url": "https://github.com/epriestley/poems/pull/2.diff", + "patch_url": "https://github.com/epriestley/poems/pull/2.patch", + "issue_url": "https://api.github.com/repos/epriestley/poems/issues/2", + "number": 2, + "state": "open", + "locked": false, + "title": "Please Merge Quack2 into Feature", + "user": { + "login": "epriestley", + "id": 102631, + "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "html_url": "https://github.com/epriestley", + "followers_url": "https://api.github.com/users/epriestley/followers", + "following_url": "https://api.github.com/users/epriestley/following{/other_user}", + "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", + "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", + "organizations_url": "https://api.github.com/users/epriestley/orgs", + "repos_url": "https://api.github.com/users/epriestley/repos", + "events_url": "https://api.github.com/users/epriestley/events{/privacy}", + "received_events_url": "https://api.github.com/users/epriestley/received_events", + "type": "User", + "site_admin": false + }, + "body": "", + "created_at": "2016-03-09T12:52:31Z", + "updated_at": "2016-03-09T12:52:31Z", + "closed_at": null, + "merged_at": null, + "merge_commit_sha": null, + "assignee": null, + "milestone": null, + "commits_url": "https://api.github.com/repos/epriestley/poems/pulls/2/commits", + "review_comments_url": "https://api.github.com/repos/epriestley/poems/pulls/2/comments", + "review_comment_url": "https://api.github.com/repos/epriestley/poems/pulls/comments{/number}", + "comments_url": "https://api.github.com/repos/epriestley/poems/issues/2/comments", + "statuses_url": "https://api.github.com/repos/epriestley/poems/statuses/6cf5f6d0c8c06c4c73b8783666d9b3ecce138244", + "head": { + "label": "epriestley:feature", + "ref": "feature", + "sha": "6cf5f6d0c8c06c4c73b8783666d9b3ecce138244", + "user": { + "login": "epriestley", + "id": 102631, + "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "html_url": "https://github.com/epriestley", + "followers_url": "https://api.github.com/users/epriestley/followers", + "following_url": "https://api.github.com/users/epriestley/following{/other_user}", + "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", + "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", + "organizations_url": "https://api.github.com/users/epriestley/orgs", + "repos_url": "https://api.github.com/users/epriestley/repos", + "events_url": "https://api.github.com/users/epriestley/events{/privacy}", + "received_events_url": "https://api.github.com/users/epriestley/received_events", + "type": "User", + "site_admin": false + }, + "repo": { + "id": 14627834, + "name": "poems", + "full_name": "epriestley/poems", + "owner": { + "login": "epriestley", + "id": 102631, + "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "html_url": "https://github.com/epriestley", + "followers_url": "https://api.github.com/users/epriestley/followers", + "following_url": "https://api.github.com/users/epriestley/following{/other_user}", + "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", + "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", + "organizations_url": "https://api.github.com/users/epriestley/orgs", + "repos_url": "https://api.github.com/users/epriestley/repos", + "events_url": "https://api.github.com/users/epriestley/events{/privacy}", + "received_events_url": "https://api.github.com/users/epriestley/received_events", + "type": "User", + "site_admin": false + }, + "private": false, + "html_url": "https://github.com/epriestley/poems", + "description": "Poems (Mirror)", + "fork": false, + "url": "https://api.github.com/repos/epriestley/poems", + "forks_url": "https://api.github.com/repos/epriestley/poems/forks", + "keys_url": "https://api.github.com/repos/epriestley/poems/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/epriestley/poems/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/epriestley/poems/teams", + "hooks_url": "https://api.github.com/repos/epriestley/poems/hooks", + "issue_events_url": "https://api.github.com/repos/epriestley/poems/issues/events{/number}", + "events_url": "https://api.github.com/repos/epriestley/poems/events", + "assignees_url": "https://api.github.com/repos/epriestley/poems/assignees{/user}", + "branches_url": "https://api.github.com/repos/epriestley/poems/branches{/branch}", + "tags_url": "https://api.github.com/repos/epriestley/poems/tags", + "blobs_url": "https://api.github.com/repos/epriestley/poems/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/epriestley/poems/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/epriestley/poems/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/epriestley/poems/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/epriestley/poems/statuses/{sha}", + "languages_url": "https://api.github.com/repos/epriestley/poems/languages", + "stargazers_url": "https://api.github.com/repos/epriestley/poems/stargazers", + "contributors_url": "https://api.github.com/repos/epriestley/poems/contributors", + "subscribers_url": "https://api.github.com/repos/epriestley/poems/subscribers", + "subscription_url": "https://api.github.com/repos/epriestley/poems/subscription", + "commits_url": "https://api.github.com/repos/epriestley/poems/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/epriestley/poems/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/epriestley/poems/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/epriestley/poems/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/epriestley/poems/contents/{+path}", + "compare_url": "https://api.github.com/repos/epriestley/poems/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/epriestley/poems/merges", + "archive_url": "https://api.github.com/repos/epriestley/poems/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/epriestley/poems/downloads", + "issues_url": "https://api.github.com/repos/epriestley/poems/issues{/number}", + "pulls_url": "https://api.github.com/repos/epriestley/poems/pulls{/number}", + "milestones_url": "https://api.github.com/repos/epriestley/poems/milestones{/number}", + "notifications_url": "https://api.github.com/repos/epriestley/poems/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/epriestley/poems/labels{/name}", + "releases_url": "https://api.github.com/repos/epriestley/poems/releases{/id}", + "deployments_url": "https://api.github.com/repos/epriestley/poems/deployments", + "created_at": "2013-11-22T19:47:42Z", + "updated_at": "2016-01-21T17:10:27Z", + "pushed_at": "2016-01-21T17:10:21Z", + "git_url": "git://github.com/epriestley/poems.git", + "ssh_url": "git@github.com:epriestley/poems.git", + "clone_url": "https://github.com/epriestley/poems.git", + "svn_url": "https://github.com/epriestley/poems", + "homepage": null, + "size": 715, + "stargazers_count": 9, + "watchers_count": 9, + "language": "PHP", + "has_issues": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "forks_count": 0, + "mirror_url": null, + "open_issues_count": 2, + "forks": 0, + "open_issues": 2, + "watchers": 9, + "default_branch": "master" + } + }, + "base": { + "label": "epriestley:quack2", + "ref": "quack2", + "sha": "5a9c51e86615f6e1097b2a4a73ef0fe75981c1dd", + "user": { + "login": "epriestley", + "id": 102631, + "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "html_url": "https://github.com/epriestley", + "followers_url": "https://api.github.com/users/epriestley/followers", + "following_url": "https://api.github.com/users/epriestley/following{/other_user}", + "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", + "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", + "organizations_url": "https://api.github.com/users/epriestley/orgs", + "repos_url": "https://api.github.com/users/epriestley/repos", + "events_url": "https://api.github.com/users/epriestley/events{/privacy}", + "received_events_url": "https://api.github.com/users/epriestley/received_events", + "type": "User", + "site_admin": false + }, + "repo": { + "id": 14627834, + "name": "poems", + "full_name": "epriestley/poems", + "owner": { + "login": "epriestley", + "id": 102631, + "avatar_url": "https://avatars.githubusercontent.com/u/102631?v=3", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "html_url": "https://github.com/epriestley", + "followers_url": "https://api.github.com/users/epriestley/followers", + "following_url": "https://api.github.com/users/epriestley/following{/other_user}", + "gists_url": "https://api.github.com/users/epriestley/gists{/gist_id}", + "starred_url": "https://api.github.com/users/epriestley/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/epriestley/subscriptions", + "organizations_url": "https://api.github.com/users/epriestley/orgs", + "repos_url": "https://api.github.com/users/epriestley/repos", + "events_url": "https://api.github.com/users/epriestley/events{/privacy}", + "received_events_url": "https://api.github.com/users/epriestley/received_events", + "type": "User", + "site_admin": false + }, + "private": false, + "html_url": "https://github.com/epriestley/poems", + "description": "Poems (Mirror)", + "fork": false, + "url": "https://api.github.com/repos/epriestley/poems", + "forks_url": "https://api.github.com/repos/epriestley/poems/forks", + "keys_url": "https://api.github.com/repos/epriestley/poems/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/epriestley/poems/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/epriestley/poems/teams", + "hooks_url": "https://api.github.com/repos/epriestley/poems/hooks", + "issue_events_url": "https://api.github.com/repos/epriestley/poems/issues/events{/number}", + "events_url": "https://api.github.com/repos/epriestley/poems/events", + "assignees_url": "https://api.github.com/repos/epriestley/poems/assignees{/user}", + "branches_url": "https://api.github.com/repos/epriestley/poems/branches{/branch}", + "tags_url": "https://api.github.com/repos/epriestley/poems/tags", + "blobs_url": "https://api.github.com/repos/epriestley/poems/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/epriestley/poems/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/epriestley/poems/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/epriestley/poems/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/epriestley/poems/statuses/{sha}", + "languages_url": "https://api.github.com/repos/epriestley/poems/languages", + "stargazers_url": "https://api.github.com/repos/epriestley/poems/stargazers", + "contributors_url": "https://api.github.com/repos/epriestley/poems/contributors", + "subscribers_url": "https://api.github.com/repos/epriestley/poems/subscribers", + "subscription_url": "https://api.github.com/repos/epriestley/poems/subscription", + "commits_url": "https://api.github.com/repos/epriestley/poems/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/epriestley/poems/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/epriestley/poems/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/epriestley/poems/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/epriestley/poems/contents/{+path}", + "compare_url": "https://api.github.com/repos/epriestley/poems/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/epriestley/poems/merges", + "archive_url": "https://api.github.com/repos/epriestley/poems/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/epriestley/poems/downloads", + "issues_url": "https://api.github.com/repos/epriestley/poems/issues{/number}", + "pulls_url": "https://api.github.com/repos/epriestley/poems/pulls{/number}", + "milestones_url": "https://api.github.com/repos/epriestley/poems/milestones{/number}", + "notifications_url": "https://api.github.com/repos/epriestley/poems/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/epriestley/poems/labels{/name}", + "releases_url": "https://api.github.com/repos/epriestley/poems/releases{/id}", + "deployments_url": "https://api.github.com/repos/epriestley/poems/deployments", + "created_at": "2013-11-22T19:47:42Z", + "updated_at": "2016-01-21T17:10:27Z", + "pushed_at": "2016-01-21T17:10:21Z", + "git_url": "git://github.com/epriestley/poems.git", + "ssh_url": "git@github.com:epriestley/poems.git", + "clone_url": "https://github.com/epriestley/poems.git", + "svn_url": "https://github.com/epriestley/poems", + "homepage": null, + "size": 715, + "stargazers_count": 9, + "watchers_count": 9, + "language": "PHP", + "has_issues": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "forks_count": 0, + "mirror_url": null, + "open_issues_count": 2, + "forks": 0, + "open_issues": 2, + "watchers": 9, + "default_branch": "master" + } + }, + "_links": { + "self": { + "href": "https://api.github.com/repos/epriestley/poems/pulls/2" + }, + "html": { + "href": "https://github.com/epriestley/poems/pull/2" + }, + "issue": { + "href": "https://api.github.com/repos/epriestley/poems/issues/2" + }, + "comments": { + "href": "https://api.github.com/repos/epriestley/poems/issues/2/comments" + }, + "review_comments": { + "href": "https://api.github.com/repos/epriestley/poems/pulls/2/comments" + }, + "review_comment": { + "href": "https://api.github.com/repos/epriestley/poems/pulls/comments{/number}" + }, + "commits": { + "href": "https://api.github.com/repos/epriestley/poems/pulls/2/commits" + }, + "statuses": { + "href": "https://api.github.com/repos/epriestley/poems/statuses/6cf5f6d0c8c06c4c73b8783666d9b3ecce138244" + } + }, + "merged": false, + "mergeable": null, + "mergeable_state": "unknown", + "merged_by": null, + "comments": 0, + "review_comments": 0, + "commits": 26, + "additions": 26, + "deletions": 0, + "changed_files": 1 + } + }, + "public": true, + "created_at": "2016-03-09T12:52:31Z" +} +~~~~~ +{ + "repository.name.full": "epriestley/poems", + "is.issue": false, + "is.pull": true, + "issue.number": null, + "pull.number": 2 +} diff --git a/src/applications/nuance/github/__tests__/repositoryevents/PushEvent.txt b/src/applications/nuance/github/__tests__/repositoryevents/PushEvent.txt new file mode 100644 index 0000000000..f36fed2f52 --- /dev/null +++ b/src/applications/nuance/github/__tests__/repositoryevents/PushEvent.txt @@ -0,0 +1,45 @@ +{ + "id": "3498724127", + "type": "PushEvent", + "actor": { + "id": 102631, + "login": "epriestley", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "avatar_url": "https://avatars.githubusercontent.com/u/102631?" + }, + "repo": { + "id": 14627834, + "name": "epriestley/poems", + "url": "https://api.github.com/repos/epriestley/poems" + }, + "payload": { + "push_id": 924333172, + "size": 1, + "distinct_size": 1, + "ref": "refs/heads/master", + "head": "c829132d37c4c1da80d319942a5a1e500632b52f", + "before": "d8262dc45f0bd79c06571c6851d47efaeb6b599b", + "commits": [ + { + "sha": "c829132d37c4c1da80d319942a5a1e500632b52f", + "author": { + "email": "git@epriestley.com", + "name": "epriestley" + }, + "message": "Put 16K files in a single directory", + "distinct": true, + "url": "https://api.github.com/repos/epriestley/poems/commits/c829132d37c4c1da80d319942a5a1e500632b52f" + } + ] + }, + "public": true, + "created_at": "2016-01-06T11:21:59Z" +} +~~~~~ +{ + "repository.name.full": "epriestley/poems", + "is.issue": false, + "is.pull": false, + "issue.number": null +} diff --git a/src/applications/nuance/github/__tests__/repositoryevents/WatchEvent.started.txt b/src/applications/nuance/github/__tests__/repositoryevents/WatchEvent.started.txt new file mode 100644 index 0000000000..7cc6ed8164 --- /dev/null +++ b/src/applications/nuance/github/__tests__/repositoryevents/WatchEvent.started.txt @@ -0,0 +1,29 @@ +{ + "id": "3740950917", + "type": "WatchEvent", + "actor": { + "id": 102631, + "login": "epriestley", + "gravatar_id": "", + "url": "https://api.github.com/users/epriestley", + "avatar_url": "https://avatars.githubusercontent.com/u/102631?" + }, + "repo": { + "id": 14627834, + "name": "epriestley/poems", + "url": "https://api.github.com/repos/epriestley/poems" + }, + "payload": { + "action": "started" + }, + "public": true, + "created_at": "2016-03-09T12:56:28Z" +} +~~~~~ +{ + "repository.name.full": "epriestley/poems", + "is.issue": false, + "is.pull": false, + "issue.number": null, + "pull.number": null +} diff --git a/src/applications/nuance/item/NuanceGitHubEventItemType.php b/src/applications/nuance/item/NuanceGitHubEventItemType.php new file mode 100644 index 0000000000..1e6a72b3b3 --- /dev/null +++ b/src/applications/nuance/item/NuanceGitHubEventItemType.php @@ -0,0 +1,147 @@ +getItemProperty('api.type'); + switch ($api_type) { + case 'issue': + return $this->getGitHubIssueAPIEventDisplayName($item); + case 'repository': + return $this->getGitHubRepositoryAPIEventDisplayName($item); + default: + return pht('GitHub Event (Unknown API Type "%s")', $api_type); + } + } + + private function getGitHubIssueAPIEventDisplayName(NuanceItem $item) { + $raw = $item->getItemProperty('api.raw', array()); + + $action = idxv($raw, array('event')); + $number = idxv($raw, array('issue', 'number')); + + return pht('GitHub Issue #%d (%s)', $number, $action); + } + + private function getGitHubRepositoryAPIEventDisplayName(NuanceItem $item) { + $raw = $item->getItemProperty('api.raw', array()); + + $repo = idxv($raw, array('repo', 'name'), pht('')); + + $type = idx($raw, 'type'); + switch ($type) { + case 'PushEvent': + $head = idxv($raw, array('payload', 'head')); + $head = substr($head, 0, 8); + $name = pht('Push %s', $head); + break; + case 'IssuesEvent': + $action = idxv($raw, array('payload', 'action')); + $number = idxv($raw, array('payload', 'issue', 'number')); + $name = pht('Issue #%d (%s)', $number, $action); + break; + case 'IssueCommentEvent': + $action = idxv($raw, array('payload', 'action')); + $number = idxv($raw, array('payload', 'issue', 'number')); + $name = pht('Issue #%d (Comment, %s)', $number, $action); + break; + case 'PullRequestEvent': + $action = idxv($raw, array('payload', 'action')); + $number = idxv($raw, array('payload', 'pull_request', 'number')); + $name = pht('Pull Request #%d (%s)', $number, $action); + break; + default: + $name = pht('Unknown Event ("%s")', $type); + break; + } + + return pht('GitHub %s %s', $repo, $name); + } + + public function canUpdateItems() { + return true; + } + + protected function updateItemFromSource(NuanceItem $item) { + $viewer = $this->getViewer(); + $is_dirty = false; + + // TODO: Link up the requestor, etc. + + $source = $item->getSource(); + $token = $source->getSourceProperty('github.token'); + $token = new PhutilOpaqueEnvelope($token); + + $ref = $this->getDoorkeeperRef($item); + if ($ref) { + $ref = id(new DoorkeeperImportEngine()) + ->setViewer($viewer) + ->setRefs(array($ref)) + ->setThrowOnMissingLink(true) + ->setContextProperty('github.token', $token) + ->executeOne(); + + if ($ref->getSyncFailed()) { + $xobj = null; + } else { + $xobj = $ref->getExternalObject(); + } + + if ($xobj) { + $item->setItemProperty('doorkeeper.xobj.phid', $xobj->getPHID()); + $is_dirty = true; + } + } + + if ($item->getStatus() == NuanceItem::STATUS_IMPORTING) { + $item->setStatus(NuanceItem::STATUS_ROUTING); + $is_dirty = true; + } + + if ($is_dirty) { + $item->save(); + } + } + + private function getDoorkeeperRef(NuanceItem $item) { + $raw = $this->newRawEvent($item); + + $full_repository = $raw->getRepositoryFullName(); + if (!strlen($full_repository)) { + return null; + } + + if ($raw->isIssueEvent()) { + $ref_type = DoorkeeperBridgeGitHubIssue::OBJTYPE_GITHUB_ISSUE; + $issue_number = $raw->getIssueNumber(); + $full_ref = "{$full_repository}#{$issue_number}"; + } else { + return null; + } + + return id(new DoorkeeperObjectRef()) + ->setApplicationType(DoorkeeperBridgeGitHub::APPTYPE_GITHUB) + ->setApplicationDomain(DoorkeeperBridgeGitHub::APPDOMAIN_GITHUB) + ->setObjectType($ref_type) + ->setObjectID($full_ref); + } + + private function newRawEvent(NuanceItem $item) { + $type = $item->getItemProperty('api.type'); + $raw = $item->getItemProperty('api.raw', array()); + + return NuanceGitHubRawEvent::newEvent($type, $raw); + } + +} diff --git a/src/applications/nuance/item/NuanceItemType.php b/src/applications/nuance/item/NuanceItemType.php new file mode 100644 index 0000000000..1e0cc7a95c --- /dev/null +++ b/src/applications/nuance/item/NuanceItemType.php @@ -0,0 +1,63 @@ +viewer = $viewer; + return $this; + } + + public function getViewer() { + return $this->viewer; + } + + public function canUpdateItems() { + return false; + } + + final public function buildItemView(NuanceItem $item) { + return $this->newItemView($item); + } + + protected function newItemView() { + return null; + } + + public function getItemTypeDisplayIcon() { + return null; + } + + abstract public function getItemTypeDisplayName(); + abstract public function getItemDisplayName(NuanceItem $item); + + final public function updateItem(NuanceItem $item) { + if (!$this->canUpdateItems()) { + throw new Exception( + pht( + 'This item type ("%s", of class "%s") can not update items.', + $this->getItemTypeConstant(), + get_class($this))); + } + + $this->updateItemFromSource($item); + } + + protected function updateItemFromSource(NuanceItem $item) { + throw new PhutilMethodNotImplementedException(); + } + + final public function getItemTypeConstant() { + return $this->getPhobjectClassConstant('ITEMTYPE', 64); + } + + final public static function getAllItemTypes() { + return id(new PhutilClassMapQuery()) + ->setAncestorClass(__CLASS__) + ->setUniqueMethod('getItemTypeConstant') + ->execute(); + } + +} diff --git a/src/applications/nuance/management/NuanceManagementImportWorkflow.php b/src/applications/nuance/management/NuanceManagementImportWorkflow.php new file mode 100644 index 0000000000..b14537f885 --- /dev/null +++ b/src/applications/nuance/management/NuanceManagementImportWorkflow.php @@ -0,0 +1,85 @@ +setName('import') + ->setExamples('**import** --source __source__ [__options__]') + ->setSynopsis(pht('Import data from a source.')) + ->setArguments( + array( + array( + 'name' => 'source', + 'param' => 'source', + 'help' => pht('Choose which source to import.'), + ), + array( + 'name' => 'cursor', + 'param' => 'cursor', + 'help' => pht('Import only a particular cursor.'), + ), + )); + } + + public function execute(PhutilArgumentParser $args) { + $source = $this->loadSource($args, 'source'); + + $definition = $source->getDefinition() + ->setViewer($this->getViewer()) + ->setSource($source); + + if (!$definition->hasImportCursors()) { + throw new PhutilArgumentUsageException( + pht( + 'This source ("%s") does not expose import cursors.', + $source->getName())); + } + + $cursors = $definition->getImportCursors(); + if (!$cursors) { + throw new PhutilArgumentUsageException( + pht( + 'This source ("%s") does not have any import cursors.', + $source->getName())); + } + + $select = $args->getArg('cursor'); + if (strlen($select)) { + if (empty($cursors[$select])) { + throw new PhutilArgumentUsageException( + pht( + 'This source ("%s") does not have a "%s" cursor. Available '. + 'cursors: %s.', + $source->getName(), + $select, + implode(', ', array_keys($cursors)))); + } else { + echo tsprintf( + "%s\n", + pht( + 'Importing cursor "%s" only.', + $select)); + $cursors = array_select_keys($cursors, array($select)); + } + } else { + echo tsprintf( + "%s\n", + pht( + 'Importing all cursors: %s.', + implode(', ', array_keys($cursors)))); + + echo tsprintf( + "%s\n", + pht('(Use --cursor to import only a particular cursor.)')); + } + + foreach ($cursors as $cursor) { + $cursor->importFromSource(); + } + + return 0; + } + +} diff --git a/src/applications/nuance/management/NuanceManagementUpdateWorkflow.php b/src/applications/nuance/management/NuanceManagementUpdateWorkflow.php new file mode 100644 index 0000000000..10878770f6 --- /dev/null +++ b/src/applications/nuance/management/NuanceManagementUpdateWorkflow.php @@ -0,0 +1,30 @@ +setName('update') + ->setExamples('**update** --item __item__ [__options__]') + ->setSynopsis(pht('Update or route an item.')) + ->setArguments( + array( + array( + 'name' => 'item', + 'param' => 'item', + 'help' => pht('Choose which item to route.'), + ), + )); + } + + public function execute(PhutilArgumentParser $args) { + $item = $this->loadItem($args, 'item'); + + PhabricatorWorker::setRunAllTasksInProcess(true); + $item->scheduleUpdate(); + + return 0; + } + +} diff --git a/src/applications/nuance/management/NuanceManagementWorkflow.php b/src/applications/nuance/management/NuanceManagementWorkflow.php new file mode 100644 index 0000000000..2c7eb6b662 --- /dev/null +++ b/src/applications/nuance/management/NuanceManagementWorkflow.php @@ -0,0 +1,117 @@ +getArg($key); + if (!strlen($source)) { + throw new PhutilArgumentUsageException( + pht( + 'Specify a source with %s.', + '--'.$key)); + } + + $query = id(new NuanceSourceQuery()) + ->setViewer($this->getViewer()) + ->setRaisePolicyExceptions(true); + + $type_unknown = PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN; + + if (ctype_digit($source)) { + $kind = 'id'; + $query->withIDs(array($source)); + } else if (phid_get_type($source) !== $type_unknown) { + $kind = 'phid'; + $query->withPHIDs($source); + } else { + $kind = 'name'; + $query->withNameNgrams($source); + } + + $sources = $query->execute(); + + if (!$sources) { + switch ($kind) { + case 'id': + $message = pht( + 'No source exists with ID "%s".', + $source); + break; + case 'phid': + $message = pht( + 'No source exists with PHID "%s".', + $source); + break; + default: + $message = pht( + 'No source exists with a name matching "%s".', + $source); + break; + } + + throw new PhutilArgumentUsageException($message); + } else if (count($sources) > 1) { + $message = pht( + 'More than one source matches "%s". Choose a narrower query, or '. + 'use an ID or PHID to select a source. Matching sources: %s.', + $source, + implode(', ', mpull($sources, 'getName'))); + + throw new PhutilArgumentUsageException($message); + } + + return head($sources); + } + + protected function loadITem(PhutilArgumentParser $argv, $key) { + $item = $argv->getArg($key); + if (!strlen($item)) { + throw new PhutilArgumentUsageException( + pht( + 'Specify a item with %s.', + '--'.$key)); + } + + $query = id(new NuanceItemQuery()) + ->setViewer($this->getViewer()) + ->setRaisePolicyExceptions(true); + + $type_unknown = PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN; + + if (ctype_digit($item)) { + $kind = 'id'; + $query->withIDs(array($item)); + } else if (phid_get_type($item) !== $type_unknown) { + $kind = 'phid'; + $query->withPHIDs($item); + } else { + throw new PhutilArgumentUsageException( + pht( + 'Specify the ID or PHID of an item to update. Parameter "%s" '. + 'is not an ID or PHID.', + $item)); + } + + $items = $query->execute(); + + if (!$items) { + switch ($kind) { + case 'id': + $message = pht( + 'No item exists with ID "%s".', + $item); + break; + case 'phid': + $message = pht( + 'No item exists with PHID "%s".', + $item); + break; + } + + throw new PhutilArgumentUsageException($message); + } + + return head($items); + } +} diff --git a/src/applications/nuance/phid/NuanceImportCursorPHIDType.php b/src/applications/nuance/phid/NuanceImportCursorPHIDType.php new file mode 100644 index 0000000000..9d1f816a71 --- /dev/null +++ b/src/applications/nuance/phid/NuanceImportCursorPHIDType.php @@ -0,0 +1,38 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + $viewer = $query->getViewer(); + foreach ($handles as $phid => $handle) { + $item = $objects[$phid]; + } + } + +} diff --git a/src/applications/nuance/phid/NuanceItemPHIDType.php b/src/applications/nuance/phid/NuanceItemPHIDType.php index f401c63594..ee51633ea9 100644 --- a/src/applications/nuance/phid/NuanceItemPHIDType.php +++ b/src/applications/nuance/phid/NuanceItemPHIDType.php @@ -33,7 +33,7 @@ final class NuanceItemPHIDType extends PhabricatorPHIDType { foreach ($handles as $phid => $handle) { $item = $objects[$phid]; - $handle->setName($item->getLabel($viewer)); + $handle->setName($item->getItemDisplayName()); $handle->setURI($item->getURI()); } } diff --git a/src/applications/nuance/query/NuanceImportCursorDataQuery.php b/src/applications/nuance/query/NuanceImportCursorDataQuery.php new file mode 100644 index 0000000000..ae451abfb9 --- /dev/null +++ b/src/applications/nuance/query/NuanceImportCursorDataQuery.php @@ -0,0 +1,60 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withSourcePHIDs(array $source_phids) { + $this->sourcePHIDs = $source_phids; + return $this; + } + + public function newResultObject() { + return new NuanceImportCursorData(); + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); + + if ($this->sourcePHIDs !== null) { + $where[] = qsprintf( + $conn, + 'sourcePHID IN (%Ls)', + $this->sourcePHIDs); + } + + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->phids !== null) { + $where[] = qsprintf( + $conn, + 'phid IN (%Ls)', + $this->phids); + } + + return $where; + } + +} diff --git a/src/applications/nuance/query/NuanceItemQuery.php b/src/applications/nuance/query/NuanceItemQuery.php index fbcac6e5b7..5bb0ec70ec 100644 --- a/src/applications/nuance/query/NuanceItemQuery.php +++ b/src/applications/nuance/query/NuanceItemQuery.php @@ -6,6 +6,9 @@ final class NuanceItemQuery private $ids; private $phids; private $sourcePHIDs; + private $itemTypes; + private $itemKeys; + private $containerKeys; public function withIDs(array $ids) { $this->ids = $ids; @@ -22,6 +25,21 @@ final class NuanceItemQuery return $this; } + public function withItemTypes(array $item_types) { + $this->itemTypes = $item_types; + return $this; + } + + public function withItemKeys(array $item_keys) { + $this->itemKeys = $item_keys; + return $this; + } + + public function withItemContainerKeys(array $container_keys) { + $this->containerKeys = $container_keys; + return $this; + } + public function newResultObject() { return new NuanceItem(); } @@ -52,6 +70,17 @@ final class NuanceItemQuery $item->attachSource($source); } + $type_map = NuanceItemType::getAllItemTypes(); + foreach ($items as $key => $item) { + $type = idx($type_map, $item->getItemType()); + if (!$type) { + $this->didRejectResult($items[$key]); + unset($items[$key]); + continue; + } + $item->attachImplementation($type); + } + return $items; } @@ -79,6 +108,27 @@ final class NuanceItemQuery $this->phids); } + if ($this->itemTypes !== null) { + $where[] = qsprintf( + $conn, + 'itemType IN (%Ls)', + $this->itemTypes); + } + + if ($this->itemKeys !== null) { + $where[] = qsprintf( + $conn, + 'itemKey IN (%Ls)', + $this->itemKeys); + } + + if ($this->containerKeys !== null) { + $where[] = qsprintf( + $conn, + 'itemContainerKey IN (%Ls)', + $this->containerKeys); + } + return $where; } diff --git a/src/applications/nuance/query/NuanceItemSearchEngine.php b/src/applications/nuance/query/NuanceItemSearchEngine.php new file mode 100644 index 0000000000..2f0951b4e0 --- /dev/null +++ b/src/applications/nuance/query/NuanceItemSearchEngine.php @@ -0,0 +1,85 @@ +newQuery(); + + return $query; + } + + protected function buildCustomSearchFields() { + return array( + ); + } + + protected function getURI($path) { + return '/nuance/item/'.$path; + } + + protected function getBuiltinQueryNames() { + $names = array( + 'all' => pht('All Items'), + ); + + 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 $items, + PhabricatorSavedQuery $query, + array $handles) { + assert_instances_of($items, 'NuanceItem'); + + $viewer = $this->requireViewer(); + + $list = new PHUIObjectItemListView(); + $list->setUser($viewer); + foreach ($items as $item) { + $impl = $item->getImplementation(); + + $view = id(new PHUIObjectItemView()) + ->setObjectName(pht('Item %d', $item->getID())) + ->setHeader($item->getDisplayName()) + ->setHref($item->getURI()); + + $view->addIcon( + $impl->getItemTypeDisplayIcon(), + $impl->getItemTypeDisplayName()); + + $list->addItem($view); + } + + $result = new PhabricatorApplicationSearchResultView(); + $result->setObjectList($list); + $result->setNoDataString(pht('No items found.')); + + return $result; + } + +} diff --git a/src/applications/nuance/query/NuanceQueueQuery.php b/src/applications/nuance/query/NuanceQueueQuery.php index d50e393667..10f761d189 100644 --- a/src/applications/nuance/query/NuanceQueueQuery.php +++ b/src/applications/nuance/query/NuanceQueueQuery.php @@ -16,20 +16,12 @@ final class NuanceQueueQuery return $this; } + public function newResultObject() { + return new NuanceQueue(); + } + protected function loadPage() { - $table = new NuanceQueue(); - $conn = $table->establishConnection('r'); - - $data = queryfx_all( - $conn, - '%Q FROM %T %Q %Q %Q', - $this->buildSelectClause($conn), - $table->getTableName(), - $this->buildWhereClause($conn), - $this->buildOrderClause($conn), - $this->buildLimitClause($conn)); - - return $table->loadAllFromArray($data); + return $this->loadStandardPage($this->newResultObject()); } protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { diff --git a/src/applications/nuance/query/NuanceQueueSearchEngine.php b/src/applications/nuance/query/NuanceQueueSearchEngine.php index 12259982f1..2f794c2a9c 100644 --- a/src/applications/nuance/query/NuanceQueueSearchEngine.php +++ b/src/applications/nuance/query/NuanceQueueSearchEngine.php @@ -11,21 +11,19 @@ final class NuanceQueueSearchEngine return pht('Nuance Queues'); } - public function buildSavedQueryFromRequest(AphrontRequest $request) { - $saved = new PhabricatorSavedQuery(); - - return $saved; + public function newQuery() { + return new NuanceQueueQuery(); } - public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { - $query = id(new NuanceQueueQuery()); + protected function buildQueryFromParameters(array $map) { + $query = $this->newQuery(); return $query; } - public function buildSearchForm( - AphrontFormView $form, - PhabricatorSavedQuery $saved_query) {} + protected function buildCustomSearchFields() { + return array(); + } protected function getURI($path) { return '/nuance/queue/'.$path; diff --git a/src/applications/nuance/query/NuanceSourceQuery.php b/src/applications/nuance/query/NuanceSourceQuery.php index ee4b964ee3..907d9c314f 100644 --- a/src/applications/nuance/query/NuanceSourceQuery.php +++ b/src/applications/nuance/query/NuanceSourceQuery.php @@ -6,6 +6,8 @@ final class NuanceSourceQuery private $ids; private $phids; private $types; + private $isDisabled; + private $hasCursors; public function withIDs(array $ids) { $this->ids = $ids; @@ -22,46 +24,113 @@ final class NuanceSourceQuery return $this; } - protected function loadPage() { - $table = new NuanceSource(); - $conn = $table->establishConnection('r'); - - $data = queryfx_all( - $conn, - '%Q FROM %T %Q %Q %Q', - $this->buildSelectClause($conn), - $table->getTableName(), - $this->buildWhereClause($conn), - $this->buildOrderClause($conn), - $this->buildLimitClause($conn)); - - return $table->loadAllFromArray($data); + public function withIsDisabled($disabled) { + $this->isDisabled = $disabled; + return $this; } + public function withHasImportCursors($has_cursors) { + $this->hasCursors = $has_cursors; + return $this; + } + + public function withNameNgrams($ngrams) { + return $this->withNgramsConstraint( + new NuanceSourceNameNgrams(), + $ngrams); + } + + public function newResultObject() { + return new NuanceSource(); + } + + protected function getPrimaryTableAlias() { + return 'source'; + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function willFilterPage(array $sources) { + $all_types = NuanceSourceDefinition::getAllDefinitions(); + + foreach ($sources as $key => $source) { + $definition = idx($all_types, $source->getType()); + if (!$definition) { + $this->didRejectResult($source); + unset($sources[$key]); + continue; + } + $source->attachDefinition($definition); + } + + return $sources; + } + + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); if ($this->types !== null) { $where[] = qsprintf( $conn, - 'type IN (%Ls)', + 'source.type IN (%Ls)', $this->types); } if ($this->ids !== null) { $where[] = qsprintf( $conn, - 'id IN (%Ld)', + 'source.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, - 'phid IN (%Ls)', + 'source.phid IN (%Ls)', $this->phids); } + if ($this->isDisabled !== null) { + $where[] = qsprintf( + $conn, + 'source.isDisabled = %d', + (int)$this->isDisabled); + } + + if ($this->hasCursors !== null) { + $cursor_types = array(); + + $definitions = NuanceSourceDefinition::getAllDefinitions(); + foreach ($definitions as $key => $definition) { + if ($definition->hasImportCursors()) { + $cursor_types[] = $key; + } + } + + if ($this->hasCursors) { + if (!$cursor_types) { + throw new PhabricatorEmptyQueryException(); + } else { + $where[] = qsprintf( + $conn, + 'source.type IN (%Ls)', + $cursor_types); + } + } else { + if (!$cursor_types) { + // Apply no constraint. + } else { + $where[] = qsprintf( + $conn, + 'source.type NOT IN (%Ls)', + $cursor_types); + } + } + } + return $where; } diff --git a/src/applications/nuance/query/NuanceSourceSearchEngine.php b/src/applications/nuance/query/NuanceSourceSearchEngine.php index 02a8b502fb..44f131aa1b 100644 --- a/src/applications/nuance/query/NuanceSourceSearchEngine.php +++ b/src/applications/nuance/query/NuanceSourceSearchEngine.php @@ -11,21 +11,28 @@ final class NuanceSourceSearchEngine return pht('Nuance Sources'); } - public function buildSavedQueryFromRequest(AphrontRequest $request) { - $saved = new PhabricatorSavedQuery(); - - return $saved; + public function newQuery() { + return new NuanceSourceQuery(); } - public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { - $query = id(new NuanceSourceQuery()); + protected function buildQueryFromParameters(array $map) { + $query = $this->newQuery(); + + if ($map['match'] !== null) { + $query->withNameNgrams($map['match']); + } return $query; } - public function buildSearchForm( - AphrontFormView $form, - PhabricatorSavedQuery $saved_query) {} + protected function buildCustomSearchFields() { + return array( + id(new PhabricatorSearchTextField()) + ->setLabel(pht('Name Contains')) + ->setKey('match') + ->setDescription(pht('Search for sources by name substring.')), + ); + } protected function getURI($path) { return '/nuance/source/'.$path; diff --git a/src/applications/nuance/source/NuanceGitHubRepositorySourceDefinition.php b/src/applications/nuance/source/NuanceGitHubRepositorySourceDefinition.php new file mode 100644 index 0000000000..8768e35864 --- /dev/null +++ b/src/applications/nuance/source/NuanceGitHubRepositorySourceDefinition.php @@ -0,0 +1,31 @@ +setCursorKey('events.repository'), + id(new NuanceGitHubIssuesImportCursor()) + ->setCursorKey('events.issues'), + ); + } + +} diff --git a/src/applications/nuance/source/NuancePhabricatorFormSourceDefinition.php b/src/applications/nuance/source/NuancePhabricatorFormSourceDefinition.php index cca5a19cd9..a54211ee95 100644 --- a/src/applications/nuance/source/NuancePhabricatorFormSourceDefinition.php +++ b/src/applications/nuance/source/NuancePhabricatorFormSourceDefinition.php @@ -26,34 +26,6 @@ final class NuancePhabricatorFormSourceDefinition return $actions; } - public function updateItems() { - return null; - } - - protected function augmentEditForm( - AphrontFormView $form, - PhabricatorApplicationTransactionValidationException $ex = null) { - - /* TODO - add a box to allow for custom fields to be defined here, so that - * these NuanceSource objects made from this definition can be used to - * capture arbitrary data */ - - return $form; - } - - protected function buildTransactions(AphrontRequest $request) { - $transactions = parent::buildTransactions($request); - - // TODO -- as above - - return $transactions; - } - - public function renderView() {} - - public function renderListView() {} - - public function handleActionRequest(AphrontRequest $request) { $viewer = $request->getViewer(); @@ -100,13 +72,6 @@ final class NuancePhabricatorFormSourceDefinition return $box; } - public function renderItemViewProperties( - PhabricatorUser $viewer, - NuanceItem $item, - PHUIPropertyListView $view) { - $this->renderItemCommonProperties($viewer, $item, $view); - } - public function renderItemEditProperties( PhabricatorUser $viewer, NuanceItem $item, diff --git a/src/applications/nuance/source/NuanceSourceDefinition.php b/src/applications/nuance/source/NuanceSourceDefinition.php index 4b37e39845..742837ef82 100644 --- a/src/applications/nuance/source/NuanceSourceDefinition.php +++ b/src/applications/nuance/source/NuanceSourceDefinition.php @@ -5,42 +5,31 @@ */ abstract class NuanceSourceDefinition extends Phobject { - private $actor; - private $sourceObject; + private $viewer; + private $source; - public function setActor(PhabricatorUser $actor) { - $this->actor = $actor; + public function setViewer(PhabricatorUser $viewer) { + $this->viewer = $viewer; return $this; } - public function getActor() { - return $this->actor; - } - - public function requireActor() { - $actor = $this->getActor(); - if (!$actor) { - throw new PhutilInvalidStateException('setActor'); + public function getViewer() { + if (!$this->viewer) { + throw new PhutilInvalidStateException('setViewer'); } - return $actor; + return $this->viewer; } - public function setSourceObject(NuanceSource $source) { - $source->setType($this->getSourceTypeConstant()); - $this->sourceObject = $source; + public function setSource(NuanceSource $source) { + $this->source = $source; return $this; } - public function getSourceObject() { - return $this->sourceObject; - } - - public function requireSourceObject() { - $source = $this->getSourceObject(); - if (!$source) { - throw new PhutilInvalidStateException('setSourceObject'); + public function getSource() { + if (!$this->source) { + throw new PhutilInvalidStateException('setSource'); } - return $source; + return $this->source; } public function getSourceViewActions(AphrontRequest $request) { @@ -54,6 +43,84 @@ abstract class NuanceSourceDefinition extends Phobject { ->execute(); } + public function hasImportCursors() { + return false; + } + + final public function getImportCursors() { + if (!$this->hasImportCursors()) { + throw new Exception( + pht('This source has no input cursors.')); + } + + $viewer = PhabricatorUser::getOmnipotentUser(); + $source = $this->getSource(); + $cursors = $this->newImportCursors(); + + $data = id(new NuanceImportCursorDataQuery()) + ->setViewer($viewer) + ->withSourcePHIDs(array($source->getPHID())) + ->execute(); + $data = mpull($data, null, 'getCursorKey'); + + $map = array(); + foreach ($cursors as $cursor) { + if (!($cursor instanceof NuanceImportCursor)) { + throw new Exception( + pht( + 'Source "%s" (of class "%s") returned an invalid value from '. + 'method "%s": all values must be objects of class "%s".', + $this->getName(), + get_class($this), + 'newImportCursors()', + 'NuanceImportCursor')); + } + + $key = $cursor->getCursorKey(); + if (!strlen($key)) { + throw new Exception( + pht( + 'Source "%s" (of class "%s") returned an import cursor with '. + 'a missing key from "%s". Each cursor must have a unique, '. + 'nonempty key.', + $this->getName(), + get_class($this), + 'newImportCursors()')); + } + + $other = idx($map, $key); + if ($other) { + throw new Exception( + pht( + 'Source "%s" (of class "%s") returned two cursors from method '. + '"%s" with the same key ("%s"). Each cursor must have a unique '. + 'key.', + $this->getName(), + get_class($this), + 'newImportCursors()', + $key)); + } + + $map[$key] = $cursor; + + $cursor_data = idx($data, $key); + if (!$cursor_data) { + $cursor_data = $cursor->newEmptyCursorData($source); + } + + $cursor + ->setViewer($viewer) + ->setSource($source) + ->setCursorData($cursor_data); + } + + return $map; + } + + protected function newImportCursors() { + throw new PhutilMethodNotImplementedException(); + } + /** * A human readable string like "Twitter" or "Phabricator Form". */ @@ -73,181 +140,14 @@ abstract class NuanceSourceDefinition extends Phobject { */ abstract public function getSourceTypeConstant(); - /** - * Code to create and update @{class:NuanceItem}s and - * @{class:NuanceRequestor}s via daemons goes here. - * - * If that does not make sense for the @{class:NuanceSource} you are - * defining, simply return null. For example, - * @{class:NuancePhabricatorFormSourceDefinition} since these are one-way - * contact forms. - */ - abstract public function updateItems(); - - private function loadSourceObjectPolicies( - PhabricatorUser $user, - NuanceSource $source) { - - $user = $this->requireActor(); - $source = $this->requireSourceObject(); - return id(new PhabricatorPolicyQuery()) - ->setViewer($user) - ->setObject($source) - ->execute(); + public function renderView() { + return null; } - final public function getEditTitle() { - $source = $this->requireSourceObject(); - if ($source->getPHID()) { - $title = pht('Edit "%s" source.', $source->getName()); - } else { - $title = pht('Create a new "%s" source.', $this->getName()); - } - - return $title; + public function renderListView() { + return null; } - final public function buildEditLayout(AphrontRequest $request) { - $actor = $this->requireActor(); - $source = $this->requireSourceObject(); - - $form_errors = array(); - $error_messages = array(); - $transactions = array(); - $validation_exception = null; - if ($request->isFormPost()) { - $transactions = $this->buildTransactions($request); - try { - $editor = id(new NuanceSourceEditor()) - ->setActor($actor) - ->setContentSourceFromRequest($request) - ->setContinueOnNoEffect(true) - ->applyTransactions($source, $transactions); - - return id(new AphrontRedirectResponse()) - ->setURI($source->getURI()); - - } catch (PhabricatorApplicationTransactionValidationException $ex) { - $validation_exception = $ex; - } - - } - - $form = $this->renderEditForm($validation_exception); - $layout = id(new PHUIObjectBoxView()) - ->setHeaderText($this->getEditTitle()) - ->setValidationException($validation_exception) - ->setFormErrors($error_messages) - ->setForm($form); - - return $layout; - } - - /** - * Code to create a form to edit the @{class:NuanceItem} you are defining. - * - * return @{class:AphrontFormView} - */ - private function renderEditForm( - PhabricatorApplicationTransactionValidationException $ex = null) { - $user = $this->requireActor(); - $source = $this->requireSourceObject(); - $policies = $this->loadSourceObjectPolicies($user, $source); - $e_name = null; - if ($ex) { - $e_name = $ex->getShortMessage(NuanceSourceTransaction::TYPE_NAME); - } - - $form = id(new AphrontFormView()) - ->setUser($user) - ->appendChild( - id(new AphrontFormTextControl()) - ->setLabel(pht('Name')) - ->setName('name') - ->setError($e_name) - ->setValue($source->getName())); - - $form = $this->augmentEditForm($form, $ex); - - $default_phid = $source->getDefaultQueuePHID(); - if ($default_phid) { - $default_queues = array($default_phid); - } else { - $default_queues = array(); - } - - $form - ->appendControl( - id(new AphrontFormTokenizerControl()) - ->setLabel(pht('Default Queue')) - ->setName('defaultQueuePHIDs') - ->setLimit(1) - ->setDatasource(new NuanceQueueDatasource()) - ->setValue($default_queues)) - ->appendChild( - id(new AphrontFormPolicyControl()) - ->setUser($user) - ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) - ->setPolicyObject($source) - ->setPolicies($policies) - ->setName('viewPolicy')) - ->appendChild( - id(new AphrontFormPolicyControl()) - ->setUser($user) - ->setCapability(PhabricatorPolicyCapability::CAN_EDIT) - ->setPolicyObject($source) - ->setPolicies($policies) - ->setName('editPolicy')) - ->appendChild( - id(new AphrontFormSubmitControl()) - ->addCancelButton($source->getURI()) - ->setValue(pht('Save'))); - - return $form; - } - - /** - * return @{class:AphrontFormView} - */ - protected function augmentEditForm( - AphrontFormView $form, - PhabricatorApplicationTransactionValidationException $ex = null) { - - return $form; - } - - /** - * Hook to build up @{class:PhabricatorTransactions}. - * - * return array $transactions - */ - protected function buildTransactions(AphrontRequest $request) { - $transactions = array(); - - $transactions[] = id(new NuanceSourceTransaction()) - ->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY) - ->setNewValue($request->getStr('editPolicy')); - - $transactions[] = id(new NuanceSourceTransaction()) - ->setTransactionType(PhabricatorTransactions::TYPE_VIEW_POLICY) - ->setNewValue($request->getStr('viewPolicy')); - - $transactions[] = id(new NuanceSourceTransaction()) - ->setTransactionType(NuanceSourceTransaction::TYPE_NAME) - ->setNewvalue($request->getStr('name')); - - $transactions[] = id(new NuanceSourceTransaction()) - ->setTransactionType(NuanceSourceTransaction::TYPE_DEFAULT_QUEUE) - ->setNewvalue(head($request->getArr('defaultQueuePHIDs'))); - - return $transactions; - } - - abstract public function renderView(); - - abstract public function renderListView(); - - protected function newItemFromProperties( NuanceRequestor $requestor, array $properties, @@ -256,8 +156,7 @@ abstract class NuanceSourceDefinition extends Phobject { // TODO: Should we have a tighter actor/viewer model? Requestors will // often have no real user associated with them... $actor = PhabricatorUser::getOmnipotentUser(); - - $source = $this->requireSourceObject(); + $source = $this->getSource(); $item = NuanceItem::initializeNewItem(); @@ -294,13 +193,6 @@ abstract class NuanceSourceDefinition extends Phobject { return $item; } - public function renderItemViewProperties( - PhabricatorUser $viewer, - NuanceItem $item, - PHUIPropertyListView $view) { - return; - } - public function renderItemEditProperties( PhabricatorUser $viewer, NuanceItem $item, @@ -317,7 +209,7 @@ abstract class NuanceSourceDefinition extends Phobject { } public function getActionURI($path = null) { - $source_id = $this->getSourceObject()->getID(); + $source_id = $this->getSource()->getID(); return '/action/'.$source_id.'/'.ltrim($path, '/'); } diff --git a/src/applications/nuance/storage/NuanceImportCursorData.php b/src/applications/nuance/storage/NuanceImportCursorData.php new file mode 100644 index 0000000000..01f3d56c6d --- /dev/null +++ b/src/applications/nuance/storage/NuanceImportCursorData.php @@ -0,0 +1,70 @@ + true, + self::CONFIG_SERIALIZATION => array( + 'properties' => self::SERIALIZATION_JSON, + ), + self::CONFIG_COLUMN_SCHEMA => array( + 'cursorType' => 'text32', + 'cursorKey' => 'text32', + ), + self::CONFIG_KEY_SCHEMA => array( + 'key_source' => array( + 'columns' => array('sourcePHID', 'cursorKey'), + 'unique' => true, + ), + ), + ) + parent::getConfiguration(); + } + + public function generatePHID() { + return PhabricatorPHID::generateNewPHID( + NuanceImportCursorPHIDType::TYPECONST); + } + + public function getCursorProperty($key, $default = null) { + return idx($this->properties, $key, $default); + } + + public function setCursorProperty($key, $value) { + $this->properties[$key] = $value; + return $this; + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + ); + } + + public function getPolicy($capability) { + switch ($capability) { + case PhabricatorPolicyCapability::CAN_VIEW: + return PhabricatorPolicies::POLICY_USER; + } + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return false; + } + + public function describeAutomaticCapability($capability) { + return null; + } + +} diff --git a/src/applications/nuance/storage/NuanceItem.php b/src/applications/nuance/storage/NuanceItem.php index 7ce517333f..a83db3ec70 100644 --- a/src/applications/nuance/storage/NuanceItem.php +++ b/src/applications/nuance/storage/NuanceItem.php @@ -6,20 +6,25 @@ final class NuanceItem PhabricatorPolicyInterface, PhabricatorApplicationTransactionInterface { - const STATUS_OPEN = 0; - const STATUS_ASSIGNED = 10; - const STATUS_CLOSED = 20; + const STATUS_IMPORTING = 'importing'; + const STATUS_ROUTING = 'routing'; + const STATUS_OPEN = 'open'; + const STATUS_ASSIGNED = 'assigned'; + const STATUS_CLOSED = 'closed'; protected $status; protected $ownerPHID; protected $requestorPHID; protected $sourcePHID; - protected $sourceLabel; + protected $queuePHID; + protected $itemType; + protected $itemKey; + protected $itemContainerKey; protected $data = array(); protected $mailKey; - protected $queuePHID; private $source = self::ATTACHABLE; + private $implementation = self::ATTACHABLE; public static function initializeNewItem() { return id(new NuanceItem()) @@ -34,8 +39,12 @@ final class NuanceItem ), self::CONFIG_COLUMN_SCHEMA => array( 'ownerPHID' => 'phid?', - 'sourceLabel' => 'text255?', - 'status' => 'uint32', + 'requestorPHID' => 'phid?', + 'queuePHID' => 'phid?', + 'itemType' => 'text64', + 'itemKey' => 'text64', + 'itemContainerKey' => 'text64?', + 'status' => 'text32', 'mailKey' => 'bytes20', ), self::CONFIG_KEY_SCHEMA => array( @@ -51,6 +60,13 @@ final class NuanceItem 'key_queue' => array( 'columns' => array('queuePHID', 'status'), ), + 'key_container' => array( + 'columns' => array('sourcePHID', 'itemContainerKey'), + ), + 'key_item' => array( + 'columns' => array('sourcePHID', 'itemKey'), + 'unique' => true, + ), ), ) + parent::getConfiguration(); } @@ -71,18 +87,6 @@ final class NuanceItem return '/nuance/item/view/'.$this->getID().'/'; } - public function getLabel(PhabricatorUser $viewer) { - // this is generated at the time the item is created based on - // the configuration from the item source. It is typically - // something like 'Twitter'. - $source_label = $this->getSourceLabel(); - - return pht( - 'Item via %s @ %s.', - $source_label, - phabricator_datetime($this->getDateCreated(), $viewer)); - } - public function getRequestor() { return $this->assertAttached($this->requestor); } @@ -99,11 +103,11 @@ final class NuanceItem $this->source = $source; } - public function getNuanceProperty($key, $default = null) { + public function getItemProperty($key, $default = null) { return idx($this->data, $key, $default); } - public function setNuanceProperty($key, $value) { + public function setItemProperty($key, $value) { $this->data[$key] = $value; return $this; } @@ -135,17 +139,28 @@ final class NuanceItem return null; } - public function toDictionary() { - return array( - 'id' => $this->getID(), - 'phid' => $this->getPHID(), - 'ownerPHID' => $this->getOwnerPHID(), - 'requestorPHID' => $this->getRequestorPHID(), - 'sourcePHID' => $this->getSourcePHID(), - 'sourceLabel' => $this->getSourceLabel(), - 'dateCreated' => $this->getDateCreated(), - 'dateModified' => $this->getDateModified(), - ); + public function getDisplayName() { + return $this->getImplementation()->getItemDisplayName($this); + } + + public function scheduleUpdate() { + PhabricatorWorker::scheduleTask( + 'NuanceItemUpdateWorker', + array( + 'itemPHID' => $this->getPHID(), + ), + array( + 'objectPHID' => $this->getPHID(), + )); + } + + public function getImplementation() { + return $this->assertAttached($this->implementation); + } + + public function attachImplementation(NuanceItemType $type) { + $this->implementation = $type; + return $this; } diff --git a/src/applications/nuance/storage/NuanceQueue.php b/src/applications/nuance/storage/NuanceQueue.php index 093b437e24..15197d9bd8 100644 --- a/src/applications/nuance/storage/NuanceQueue.php +++ b/src/applications/nuance/storage/NuanceQueue.php @@ -27,7 +27,9 @@ final class NuanceQueue } public static function initializeNewQueue() { - return new NuanceQueue(); + return id(new self()) + ->setViewPolicy(PhabricatorPolicies::POLICY_USER) + ->setEditPolicy(PhabricatorPolicies::POLICY_USER); } public function save() { diff --git a/src/applications/nuance/storage/NuanceQueueTransaction.php b/src/applications/nuance/storage/NuanceQueueTransaction.php index c1630cdbd8..44309c2ae6 100644 --- a/src/applications/nuance/storage/NuanceQueueTransaction.php +++ b/src/applications/nuance/storage/NuanceQueueTransaction.php @@ -12,18 +12,6 @@ final class NuanceQueueTransaction extends NuanceTransaction { return new NuanceQueueTransactionComment(); } - public function shouldHide() { - $old = $this->getOldValue(); - $type = $this->getTransactionType(); - - switch ($type) { - case self::TYPE_NAME: - return ($old === null); - } - - return parent::shouldHide(); - } - public function getTitle() { $old = $this->getOldValue(); $new = $this->getNewValue(); @@ -32,6 +20,10 @@ final class NuanceQueueTransaction extends NuanceTransaction { $author_phid = $this->getAuthorPHID(); switch ($type) { + case PhabricatorTransactions::TYPE_CREATE: + return pht( + '%s created this queue.', + $this->renderHandleLink($author_phid)); case self::TYPE_NAME: return pht( '%s renamed this queue from "%s" to "%s".', diff --git a/src/applications/nuance/storage/NuanceSource.php b/src/applications/nuance/storage/NuanceSource.php index 4ce61289f9..8ab934e247 100644 --- a/src/applications/nuance/storage/NuanceSource.php +++ b/src/applications/nuance/storage/NuanceSource.php @@ -3,17 +3,19 @@ final class NuanceSource extends NuanceDAO implements PhabricatorApplicationTransactionInterface, - PhabricatorPolicyInterface { + PhabricatorPolicyInterface, + PhabricatorNgramsInterface { protected $name; protected $type; - protected $data; + protected $data = array(); protected $mailKey; protected $viewPolicy; protected $editPolicy; protected $defaultQueuePHID; + protected $isDisabled; - private $definition; + private $definition = self::ATTACHABLE; protected function getConfiguration() { return array( @@ -22,9 +24,10 @@ final class NuanceSource extends NuanceDAO 'data' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( - 'name' => 'text255?', + 'name' => 'sort255', 'type' => 'text32', 'mailKey' => 'bytes20', + 'isDisabled' => 'bool', ), self::CONFIG_KEY_SCHEMA => array( 'key_type' => array( @@ -49,7 +52,9 @@ final class NuanceSource extends NuanceDAO return '/nuance/source/view/'.$this->getID().'/'; } - public static function initializeNewSource(PhabricatorUser $actor) { + public static function initializeNewSource( + PhabricatorUser $actor, + NuanceSourceDefinition $definition) { $app = id(new PhabricatorApplicationQuery()) ->setViewer($actor) ->withClasses(array('PhabricatorNuanceApplication')) @@ -62,32 +67,28 @@ final class NuanceSource extends NuanceDAO return id(new NuanceSource()) ->setViewPolicy($view_policy) - ->setEditPolicy($edit_policy); + ->setEditPolicy($edit_policy) + ->setType($definition->getSourceTypeConstant()) + ->attachDefinition($definition) + ->setIsDisabled(0); } public function getDefinition() { - if ($this->definition === null) { - $definitions = NuanceSourceDefinition::getAllDefinitions(); - if (isset($definitions[$this->getType()])) { - $definition = clone $definitions[$this->getType()]; - $definition->setSourceObject($this); - $this->definition = $definition; - } - } - - return $this->definition; + return $this->assertAttached($this->definition); } - public function requireDefinition() { - $definition = $this->getDefinition(); - if (!$definition) { - throw new Exception( - pht( - 'Unable to load source definition implementation for source '. - 'type "%s".', - $this->getType())); - } - return $definition; + public function attachDefinition(NuanceSourceDefinition $definition) { + $this->definition = $definition; + return $this; + } + + public function getSourceProperty($key, $default = null) { + return idx($this->data, $key, $default); + } + + public function setSourceProperty($key, $value) { + $this->data[$key] = $value; + return $this; } @@ -141,4 +142,15 @@ final class NuanceSource extends NuanceDAO return null; } + +/* -( PhabricatorNgramsInterface )----------------------------------------- */ + + + public function newNgrams() { + return array( + id(new NuanceSourceNameNgrams()) + ->setValue($this->getName()), + ); + } + } diff --git a/src/applications/nuance/storage/NuanceSourceNameNgrams.php b/src/applications/nuance/storage/NuanceSourceNameNgrams.php new file mode 100644 index 0000000000..8bff4b8a2a --- /dev/null +++ b/src/applications/nuance/storage/NuanceSourceNameNgrams.php @@ -0,0 +1,18 @@ +getTaskDataValue('itemPHID'); + + $hash = PhabricatorHash::digestForIndex($item_phid); + $lock_key = "nuance.item.{$hash}"; + $lock = PhabricatorGlobalLock::newLock($lock_key); + + $lock->lock(1); + try { + $item = $this->loadItem($item_phid); + $this->updateItem($item); + $this->routeItem($item); + } catch (Exception $ex) { + $lock->unlock(); + throw $ex; + } + + $lock->unlock(); + } + + private function updateItem(NuanceItem $item) { + $impl = $item->getImplementation(); + if (!$impl->canUpdateItems()) { + return null; + } + + $viewer = $this->getViewer(); + + $impl->setViewer($viewer); + $impl->updateItem($item); + } + + private function routeItem(NuanceItem $item) { + $status = $item->getStatus(); + if ($status != NuanceItem::STATUS_ROUTING) { + return; + } + + $source = $item->getSource(); + + // For now, always route items into the source's default queue. + + $item + ->setQueuePHID($source->getDefaultQueuePHID()) + ->setStatus(NuanceItem::STATUS_OPEN) + ->save(); + } + +} diff --git a/src/applications/nuance/worker/NuanceWorker.php b/src/applications/nuance/worker/NuanceWorker.php new file mode 100644 index 0000000000..5d09de9e69 --- /dev/null +++ b/src/applications/nuance/worker/NuanceWorker.php @@ -0,0 +1,25 @@ +setViewer($this->getViewer()) + ->withPHIDs(array($item_phid)) + ->executeOne(); + + if (!$item) { + throw new PhabricatorWorkerPermanentFailureException( + pht( + 'There is no Nuance item with PHID "%s".', + $item_phid)); + } + + return $item; + } + +} diff --git a/src/applications/owners/controller/PhabricatorOwnersDetailController.php b/src/applications/owners/controller/PhabricatorOwnersDetailController.php index fc09508bfe..5da36ad473 100644 --- a/src/applications/owners/controller/PhabricatorOwnersDetailController.php +++ b/src/applications/owners/controller/PhabricatorOwnersDetailController.php @@ -43,9 +43,8 @@ final class PhabricatorOwnersDetailController ->setViewer($viewer) ->readFieldsFromStorage($package); - $actions = $this->buildPackageActionView($package); - $properties = $this->buildPackagePropertyView($package, $field_list); - $properties->setActionList($actions); + $curtain = $this->buildCurtain($package); + $details = $this->buildPackageDetailView($package, $field_list); if ($package->isArchived()) { $header_icon = 'fa-ban'; @@ -61,11 +60,8 @@ final class PhabricatorOwnersDetailController ->setUser($viewer) ->setHeader($package->getName()) ->setStatus($header_icon, $header_color, $header_name) - ->setPolicyObject($package); - - $panel = id(new PHUIObjectBoxView()) - ->setHeader($header) - ->addPropertyList($properties); + ->setPolicyObject($package) + ->setHeaderIcon('fa-gift'); $commit_views = array(); @@ -91,11 +87,13 @@ final class PhabricatorOwnersDetailController $commit_views[] = array( 'view' => $view, - 'header' => pht('Commits in this Package that Need Attention'), + 'header' => pht('Needs Attention'), + 'icon' => 'fa-warning', 'button' => id(new PHUIButtonView()) ->setTag('a') ->setHref($commit_uri->alter('status', $status_concern)) - ->setText(pht('View All Problem Commits')), + ->setIcon('fa-list-ul') + ->setText(pht('View All')), ); $all_commits = id(new DiffusionCommitQuery()) @@ -112,11 +110,13 @@ final class PhabricatorOwnersDetailController $commit_views[] = array( 'view' => $view, - 'header' => pht('Recent Commits in Package'), + 'header' => pht('Recent Commits'), + 'icon' => 'fa-code', 'button' => id(new PHUIButtonView()) ->setTag('a') ->setHref($commit_uri) - ->setText(pht('View All Package Commits')), + ->setIcon('fa-list-ul') + ->setText(pht('View All')), ); $phids = array(); @@ -128,14 +128,16 @@ final class PhabricatorOwnersDetailController $commit_panels = array(); foreach ($commit_views as $commit_view) { - $commit_panel = new PHUIObjectBoxView(); - $header = new PHUIHeaderView(); - $header->setHeader($commit_view['header']); + $commit_panel = id(new PHUIObjectBoxView()) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY); + $commit_header = id(new PHUIHeaderView()) + ->setHeader($commit_view['header']) + ->setHeaderIcon($commit_view['icon']); if (isset($commit_view['button'])) { - $header->addActionLink($commit_view['button']); + $commit_header->addActionLink($commit_view['button']); } $commit_view['view']->setHandles($handles); - $commit_panel->setHeader($header); + $commit_panel->setHeader($commit_header); $commit_panel->appendChild($commit_view['view']); $commit_panels[] = $commit_panel; @@ -143,32 +145,34 @@ final class PhabricatorOwnersDetailController $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($package->getName()); + $crumbs->setBorder(true); $timeline = $this->buildTransactionTimeline( $package, new PhabricatorOwnersPackageTransactionQuery()); $timeline->setShouldTerminate(true); - return $this->buildApplicationPage( - array( - $crumbs, - $panel, + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setCurtain($curtain) + ->setMainColumn(array( $this->renderPathsTable($paths, $repositories), $commit_panels, $timeline, - ), - array( - 'title' => $package->getName(), - )); + )) + ->addPropertySection(pht('Details'), $details); + + return $this->newPage() + ->setTitle($package->getName()) + ->setCrumbs($crumbs) + ->appendChild($view); } - - private function buildPackagePropertyView( + private function buildPackageDetailView( PhabricatorOwnersPackage $package, PhabricatorCustomFieldList $field_list) { $viewer = $this->getViewer(); - $view = id(new PHUIPropertyListView()) ->setUser($viewer); @@ -190,13 +194,10 @@ final class PhabricatorOwnersDetailController $description = $package->getDescription(); if (strlen($description)) { $description = new PHUIRemarkupView($viewer, $description); - $view->addSectionHeader( - pht('Description'), PHUIPropertyListView::ICON_SUMMARY); + $view->addSectionHeader(pht('Description')); $view->addTextContent($description); } - $view->invokeWillRenderEvent(); - $field_list->appendFieldsToPropertyList( $package, $viewer, @@ -205,7 +206,7 @@ final class PhabricatorOwnersDetailController return $view; } - private function buildPackageActionView(PhabricatorOwnersPackage $package) { + private function buildCurtain(PhabricatorOwnersPackage $package) { $viewer = $this->getViewer(); $can_edit = PhabricatorPolicyFilter::hasCapability( @@ -217,45 +218,43 @@ final class PhabricatorOwnersDetailController $edit_uri = $this->getApplicationURI("/edit/{$id}/"); $paths_uri = $this->getApplicationURI("/paths/{$id}/"); - $action_list = id(new PhabricatorActionListView()) - ->setUser($viewer) - ->setObject($package); + $curtain = $this->newCurtainView($package); - $action_list->addAction( - id(new PhabricatorActionView()) - ->setName(pht('Edit Package')) - ->setIcon('fa-pencil') - ->setDisabled(!$can_edit) - ->setWorkflow(!$can_edit) - ->setHref($edit_uri)); + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit Package')) + ->setIcon('fa-pencil') + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit) + ->setHref($edit_uri)); if ($package->isArchived()) { - $action_list->addAction( - id(new PhabricatorActionView()) - ->setName(pht('Activate Package')) - ->setIcon('fa-check') - ->setDisabled(!$can_edit) - ->setWorkflow($can_edit) - ->setHref($this->getApplicationURI("/archive/{$id}/"))); + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Activate Package')) + ->setIcon('fa-check') + ->setDisabled(!$can_edit) + ->setWorkflow($can_edit) + ->setHref($this->getApplicationURI("/archive/{$id}/"))); } else { - $action_list->addAction( - id(new PhabricatorActionView()) - ->setName(pht('Archive Package')) - ->setIcon('fa-ban') - ->setDisabled(!$can_edit) - ->setWorkflow($can_edit) - ->setHref($this->getApplicationURI("/archive/{$id}/"))); + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Archive Package')) + ->setIcon('fa-ban') + ->setDisabled(!$can_edit) + ->setWorkflow($can_edit) + ->setHref($this->getApplicationURI("/archive/{$id}/"))); } - $action_list->addAction( - id(new PhabricatorActionView()) - ->setName(pht('Edit Paths')) - ->setIcon('fa-folder-open') - ->setDisabled(!$can_edit) - ->setWorkflow(!$can_edit) - ->setHref($paths_uri)); + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit Paths')) + ->setIcon('fa-folder-open') + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit) + ->setHref($paths_uri)); - return $action_list; + return $curtain; } private function renderPathsTable(array $paths, array $repositories) { @@ -314,8 +313,13 @@ final class PhabricatorOwnersDetailController 'wide', )); + $header = id(new PHUIHeaderView()) + ->setHeader(pht('Paths')) + ->setHeaderIcon('fa-folder-open'); + $box = id(new PHUIObjectBoxView()) - ->setHeaderText(pht('Paths')) + ->setHeader($header) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setTable($table); if ($info) { diff --git a/src/applications/owners/storage/PhabricatorOwnersPackage.php b/src/applications/owners/storage/PhabricatorOwnersPackage.php index e007b18c00..adfc4d94d8 100644 --- a/src/applications/owners/storage/PhabricatorOwnersPackage.php +++ b/src/applications/owners/storage/PhabricatorOwnersPackage.php @@ -451,7 +451,7 @@ final class PhabricatorOwnersPackage } -/* -( PhabricatorNgramInterface )------------------------------------------ */ +/* -( PhabricatorNgramsInterface )----------------------------------------- */ public function newNgrams() { diff --git a/src/applications/passphrase/controller/PassphraseCredentialViewController.php b/src/applications/passphrase/controller/PassphraseCredentialViewController.php index 5ec2fe7c07..46bb13a688 100644 --- a/src/applications/passphrase/controller/PassphraseCredentialViewController.php +++ b/src/applications/passphrase/controller/PassphraseCredentialViewController.php @@ -31,26 +31,21 @@ final class PassphraseCredentialViewController extends PassphraseController { $crumbs->setBorder(true); $header = $this->buildHeaderView($credential); - $actions = $this->buildActionView($credential, $type); - $properties = $this->buildPropertyView($credential, $type); + $curtain = $this->buildCurtain($credential, $type); $subheader = $this->buildSubheaderView($credential); $content = $this->buildPropertySectionView($credential, $type); $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setSubheader($subheader) + ->setCurtain($curtain) ->setMainColumn($timeline) - ->addPropertySection(pht('PROPERTIES'), $content) - ->setPropertyList($properties) - ->setActionList($actions); + ->addPropertySection(pht('PROPERTIES'), $content); return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) - ->appendChild( - array( - $view, - )); + ->appendChild($view); } private function buildHeaderView(PassphraseCredential $credential) { @@ -59,7 +54,8 @@ final class PassphraseCredentialViewController extends PassphraseController { $header = id(new PHUIHeaderView()) ->setUser($viewer) ->setHeader($credential->getName()) - ->setPolicyObject($credential); + ->setPolicyObject($credential) + ->setHeaderIcon('fa-user-secret'); if ($credential->getIsDestroyed()) { $header->setStatus('fa-ban', 'red', pht('Destroyed')); @@ -97,10 +93,10 @@ final class PassphraseCredentialViewController extends PassphraseController { ->setContent($content); } - private function buildActionView( + private function buildCurtain( PassphraseCredential $credential, PassphraseCredentialType $type) { - $viewer = $this->getRequest()->getUser(); + $viewer = $this->getViewer(); $id = $credential->getID(); @@ -122,16 +118,14 @@ final class PassphraseCredentialViewController extends PassphraseController { $credential_conduit_icon = 'fa-wrench'; } - $actions = id(new PhabricatorActionListView()) - ->setObject($credential) - ->setUser($viewer); - $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $credential, PhabricatorPolicyCapability::CAN_EDIT); - $actions->addAction( + $curtain = $this->newCurtainView($credential); + + $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Credential')) ->setIcon('fa-pencil') @@ -140,7 +134,7 @@ final class PassphraseCredentialViewController extends PassphraseController { ->setWorkflow(!$can_edit)); if (!$credential->getIsDestroyed()) { - $actions->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Destroy Credential')) ->setIcon('fa-times') @@ -148,7 +142,7 @@ final class PassphraseCredentialViewController extends PassphraseController { ->setDisabled(!$can_edit) ->setWorkflow(true)); - $actions->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Show Secret')) ->setIcon('fa-eye') @@ -157,7 +151,7 @@ final class PassphraseCredentialViewController extends PassphraseController { ->setWorkflow(true)); if ($type->hasPublicKey()) { - $actions->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Show Public Key')) ->setIcon('fa-download') @@ -166,7 +160,7 @@ final class PassphraseCredentialViewController extends PassphraseController { ->setWorkflow(true)); } - $actions->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setName($credential_conduit_text) ->setIcon($credential_conduit_icon) @@ -174,7 +168,7 @@ final class PassphraseCredentialViewController extends PassphraseController { ->setDisabled(!$can_edit) ->setWorkflow(true)); - $actions->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setName($credential_lock_text) ->setIcon($credential_lock_icon) @@ -183,8 +177,7 @@ final class PassphraseCredentialViewController extends PassphraseController { ->setWorkflow(true)); } - - return $actions; + return $curtain; } private function buildPropertySectionView( @@ -235,17 +228,4 @@ final class PassphraseCredentialViewController extends PassphraseController { return $properties; } - private function buildPropertyView( - PassphraseCredential $credential, - PassphraseCredentialType $type) { - $viewer = $this->getRequest()->getUser(); - - $properties = id(new PHUIPropertyListView()) - ->setUser($viewer) - ->setObject($credential); - - $properties->invokeWillRenderEvent(); - return $properties; - } - } diff --git a/src/applications/passphrase/storage/PassphraseCredential.php b/src/applications/passphrase/storage/PassphraseCredential.php index f263523b49..8384f5bd52 100644 --- a/src/applications/passphrase/storage/PassphraseCredential.php +++ b/src/applications/passphrase/storage/PassphraseCredential.php @@ -157,10 +157,6 @@ final class PassphraseCredential extends PassphraseDAO return false; } - public function shouldShowSubscribersProperty() { - return true; - } - /* -( PhabricatorDestructibleInterface )----------------------------------- */ diff --git a/src/applications/paste/controller/PhabricatorPasteViewController.php b/src/applications/paste/controller/PhabricatorPasteViewController.php index f259cdc6af..f29c93a162 100644 --- a/src/applications/paste/controller/PhabricatorPasteViewController.php +++ b/src/applications/paste/controller/PhabricatorPasteViewController.php @@ -40,15 +40,9 @@ final class PhabricatorPasteViewController extends PhabricatorPasteController { return new Aphront404Response(); } - $forks = id(new PhabricatorPasteQuery()) - ->setViewer($viewer) - ->withParentPHIDs(array($paste->getPHID())) - ->execute(); - $fork_phids = mpull($forks, 'getPHID'); - $header = $this->buildHeaderView($paste); - $actions = $this->buildActionView($viewer, $paste); - $properties = $this->buildPropertyView($paste, $fork_phids); + $curtain = $this->buildCurtain($paste); + $subheader = $this->buildSubheaderView($paste); $source_code = $this->buildSourceCodeView($paste, $this->highlightMap); @@ -78,8 +72,7 @@ final class PhabricatorPasteViewController extends PhabricatorPasteController { $timeline, $comment_view, )) - ->setPropertyList($properties) - ->setActionList($actions) + ->setCurtain($curtain) ->addClass('ponder-question-view'); return $this->newPage() @@ -110,14 +103,15 @@ final class PhabricatorPasteViewController extends PhabricatorPasteController { ->setHeader($title) ->setUser($this->getRequest()->getUser()) ->setStatus($header_icon, $header_color, $header_name) - ->setPolicyObject($paste); + ->setPolicyObject($paste) + ->setHeaderIcon('fa-clipboard'); return $header; } - private function buildActionView( - PhabricatorUser $viewer, - PhabricatorPaste $paste) { + private function buildCurtain(PhabricatorPaste $paste) { + $viewer = $this->getViewer(); + $curtain = $this->newCurtainView($paste); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, @@ -125,43 +119,42 @@ final class PhabricatorPasteViewController extends PhabricatorPasteController { PhabricatorPolicyCapability::CAN_EDIT); $id = $paste->getID(); + $edit_uri = $this->getApplicationURI("edit/{$id}/"); + $archive_uri = $this->getApplicationURI("archive/{$id}/"); + $raw_uri = $this->getApplicationURI("raw/{$id}/"); - $action_list = id(new PhabricatorActionListView()) - ->setUser($viewer) - ->setObject($paste); - - $action_list->addAction( - id(new PhabricatorActionView()) - ->setName(pht('Edit Paste')) - ->setIcon('fa-pencil') - ->setDisabled(!$can_edit) - ->setHref($this->getApplicationURI("edit/{$id}/"))); + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit Paste')) + ->setIcon('fa-pencil') + ->setDisabled(!$can_edit) + ->setHref($edit_uri)); if ($paste->isArchived()) { - $action_list->addAction( + $curtain->addAction( id(new PhabricatorActionView()) - ->setName(pht('Activate Paste')) - ->setIcon('fa-check') - ->setDisabled(!$can_edit) - ->setWorkflow($can_edit) - ->setHref($this->getApplicationURI("archive/{$id}/"))); + ->setName(pht('Activate Paste')) + ->setIcon('fa-check') + ->setDisabled(!$can_edit) + ->setWorkflow($can_edit) + ->setHref($archive_uri)); } else { - $action_list->addAction( + $curtain->addAction( id(new PhabricatorActionView()) - ->setName(pht('Archive Paste')) - ->setIcon('fa-ban') - ->setDisabled(!$can_edit) - ->setWorkflow($can_edit) - ->setHref($this->getApplicationURI("archive/{$id}/"))); + ->setName(pht('Archive Paste')) + ->setIcon('fa-ban') + ->setDisabled(!$can_edit) + ->setWorkflow($can_edit) + ->setHref($archive_uri)); } - $action_list->addAction( - id(new PhabricatorActionView()) - ->setName(pht('View Raw File')) - ->setIcon('fa-file-text-o') - ->setHref($this->getApplicationURI("raw/{$id}/"))); + $curtain->addAction( + id(new PhabricatorActionView()) + ->setName(pht('View Raw File')) + ->setIcon('fa-file-text-o') + ->setHref($raw_uri)); - return $action_list; + return $curtain; } @@ -190,32 +183,4 @@ final class PhabricatorPasteViewController extends PhabricatorPasteController { ->setContent($content); } - private function buildPropertyView( - PhabricatorPaste $paste, - array $child_phids) { - $viewer = $this->getViewer(); - - $properties = id(new PHUIPropertyListView()) - ->setUser($viewer) - ->setObject($paste); - - if ($paste->getParentPHID()) { - $properties->addProperty( - pht('Forked From'), - $viewer->renderHandle($paste->getParentPHID())); - } - - if ($child_phids) { - $properties->addProperty( - pht('Forks'), - $viewer->renderHandleList($child_phids)); - } - - $descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions( - $viewer, - $paste); - - return $properties; - } - } diff --git a/src/applications/paste/storage/PhabricatorPaste.php b/src/applications/paste/storage/PhabricatorPaste.php index 624c70c410..bc0909cd45 100644 --- a/src/applications/paste/storage/PhabricatorPaste.php +++ b/src/applications/paste/storage/PhabricatorPaste.php @@ -155,10 +155,6 @@ final class PhabricatorPaste extends PhabricatorPasteDAO return ($this->authorPHID == $phid); } - public function shouldShowSubscribersProperty() { - return true; - } - /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ diff --git a/src/applications/people/controller/PhabricatorPeopleProfileManageController.php b/src/applications/people/controller/PhabricatorPeopleProfileManageController.php index 204fc7abda..51cf79ecff 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfileManageController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfileManageController.php @@ -35,17 +35,13 @@ final class PhabricatorPeopleProfileManageController $header = id(new PHUIHeaderView()) ->setHeader($user->getFullName()) ->setSubheader(array($profile_icon, $profile_title)) - ->setImage($picture); + ->setImage($picture) + ->setProfileHeader(true); - $actions = $this->buildActionList($user); + $curtain = $this->buildCurtain($user); $properties = $this->buildPropertyView($user); - $properties->setActionList($actions); $name = $user->getUsername(); - $object_box = id(new PHUIObjectBoxView()) - ->setHeader($header) - ->addPropertyList($properties); - $nav = $this->getProfileMenu(); $nav->selectFilter(PhabricatorPeopleProfilePanelEngine::PANEL_MANAGE); @@ -56,6 +52,16 @@ final class PhabricatorPeopleProfileManageController $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Manage')); + $crumbs->setBorder(true); + + $manage = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setCurtain($curtain) + ->addPropertySection(pht('DETAILS'), $properties) + ->setMainColumn( + array( + $timeline, + )); return $this->newPage() ->setTitle( @@ -67,8 +73,7 @@ final class PhabricatorPeopleProfileManageController ->setCrumbs($crumbs) ->appendChild( array( - $object_box, - $timeline, + $manage, )); } @@ -87,18 +92,17 @@ final class PhabricatorPeopleProfileManageController return $view; } - private function buildActionList(PhabricatorUser $user) { + private function buildCurtain(PhabricatorUser $user) { $viewer = $this->getViewer(); - $actions = id(new PhabricatorActionListView()) - ->setUser($viewer); - $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $user, PhabricatorPolicyCapability::CAN_EDIT); - $actions->addAction( + $curtain = $this->newCurtainView($user); + + $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-pencil') ->setName(pht('Edit Profile')) @@ -106,7 +110,7 @@ final class PhabricatorPeopleProfileManageController ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); - $actions->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-picture-o') ->setName(pht('Edit Profile Picture')) @@ -114,7 +118,7 @@ final class PhabricatorPeopleProfileManageController ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); - $actions->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-wrench') ->setName(pht('Edit Settings')) @@ -134,7 +138,7 @@ final class PhabricatorPeopleProfileManageController $is_self = ($user->getPHID() === $viewer->getPHID()); $can_admin = ($is_admin && !$is_self); - $actions->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setIcon($empower_icon) ->setName($empower_name) @@ -142,7 +146,7 @@ final class PhabricatorPeopleProfileManageController ->setWorkflow(true) ->setHref($this->getApplicationURI('empower/'.$user->getID().'/'))); - $actions->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-tag') ->setName(pht('Change Username')) @@ -158,7 +162,7 @@ final class PhabricatorPeopleProfileManageController $disable_name = pht('Disable User'); } - $actions->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setIcon($disable_icon) ->setName($disable_name) @@ -166,7 +170,7 @@ final class PhabricatorPeopleProfileManageController ->setWorkflow(true) ->setHref($this->getApplicationURI('disable/'.$user->getID().'/'))); - $actions->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-times') ->setName(pht('Delete User')) @@ -176,7 +180,7 @@ final class PhabricatorPeopleProfileManageController $can_welcome = ($is_admin && $user->canEstablishWebSessions()); - $actions->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-envelope') ->setName(pht('Send Welcome Email')) @@ -184,7 +188,7 @@ final class PhabricatorPeopleProfileManageController ->setDisabled(!$can_welcome) ->setHref($this->getApplicationURI('welcome/'.$user->getID().'/'))); - return $actions; + return $curtain; } diff --git a/src/applications/people/controller/PhabricatorPeopleProfileViewController.php b/src/applications/people/controller/PhabricatorPeopleProfileViewController.php index 60b02b01f4..d8bb1d4f4d 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfileViewController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfileViewController.php @@ -63,7 +63,6 @@ final class PhabricatorPeopleProfileViewController $home = id(new PHUITwoColumnView()) ->setHeader($header) - ->setFluid(true) ->addClass('project-view-home') ->setMainColumn( array( diff --git a/src/applications/phame/storage/PhameBlog.php b/src/applications/phame/storage/PhameBlog.php index d9b9bdb41d..861def127b 100644 --- a/src/applications/phame/storage/PhameBlog.php +++ b/src/applications/phame/storage/PhameBlog.php @@ -336,10 +336,6 @@ final class PhameBlog extends PhameDAO return ($this->creatorPHID == $phid); } - public function shouldShowSubscribersProperty() { - return true; - } - /* -( PhabricatorConduitResultInterface )---------------------------------- */ diff --git a/src/applications/phame/storage/PhamePost.php b/src/applications/phame/storage/PhamePost.php index 01dd71e88d..fb2e8058dc 100644 --- a/src/applications/phame/storage/PhamePost.php +++ b/src/applications/phame/storage/PhamePost.php @@ -282,10 +282,6 @@ final class PhamePost extends PhameDAO return ($this->bloggerPHID == $phid); } - public function shouldShowSubscribersProperty() { - return true; - } - /* -( PhabricatorConduitResultInterface )---------------------------------- */ diff --git a/src/applications/phame/view/PhameBlogListView.php b/src/applications/phame/view/PhameBlogListView.php index 0d897b730a..5aa58ee8f9 100644 --- a/src/applications/phame/view/PhameBlogListView.php +++ b/src/applications/phame/view/PhameBlogListView.php @@ -3,7 +3,6 @@ final class PhameBlogListView extends AphrontTagView { private $blogs; - private $viewer; public function setBlogs($blogs) { assert_instances_of($blogs, 'PhameBlog'); @@ -11,11 +10,6 @@ final class PhameBlogListView extends AphrontTagView { return $this; } - public function setViewer($viewer) { - $this->viewer = $viewer; - return $this; - } - protected function getTagAttributes() { $classes = array(); $classes[] = 'phame-blog-list'; diff --git a/src/applications/phame/view/PhameDraftListView.php b/src/applications/phame/view/PhameDraftListView.php index 87c5a6d7b5..294eeb11bc 100644 --- a/src/applications/phame/view/PhameDraftListView.php +++ b/src/applications/phame/view/PhameDraftListView.php @@ -4,7 +4,6 @@ final class PhameDraftListView extends AphrontTagView { private $posts; private $blogs; - private $viewer; public function setPosts($posts) { assert_instances_of($posts, 'PhamePost'); @@ -18,11 +17,6 @@ final class PhameDraftListView extends AphrontTagView { return $this; } - public function setViewer($viewer) { - $this->viewer = $viewer; - return $this; - } - protected function getTagAttributes() { $classes = array(); $classes[] = 'phame-blog-list'; diff --git a/src/applications/phame/view/PhamePostListView.php b/src/applications/phame/view/PhamePostListView.php index dbe77d4aa6..fe6da36541 100644 --- a/src/applications/phame/view/PhamePostListView.php +++ b/src/applications/phame/view/PhamePostListView.php @@ -4,7 +4,6 @@ final class PhamePostListView extends AphrontTagView { private $posts; private $nodata; - private $viewer; private $showBlog = false; private $isExternal; private $isLive; @@ -25,11 +24,6 @@ final class PhamePostListView extends AphrontTagView { return $this; } - public function setViewer($viewer) { - $this->viewer = $viewer; - return $this; - } - public function setIsExternal($is_external) { $this->isExternal = $is_external; return $this; @@ -53,7 +47,7 @@ final class PhamePostListView extends AphrontTagView { } protected function getTagContent() { - $viewer = $this->viewer; + $viewer = $this->getViewer(); $posts = $this->posts; $nodata = $this->nodata; diff --git a/src/applications/pholio/storage/PholioMock.php b/src/applications/pholio/storage/PholioMock.php index c817b7f32b..49cf2ace5d 100644 --- a/src/applications/pholio/storage/PholioMock.php +++ b/src/applications/pholio/storage/PholioMock.php @@ -184,10 +184,6 @@ final class PholioMock extends PholioDAO return ($this->authorPHID == $phid); } - public function shouldShowSubscribersProperty() { - return true; - } - /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ diff --git a/src/applications/phrequent/engineextension/PhrequentCurtainExtension.php b/src/applications/phrequent/engineextension/PhrequentCurtainExtension.php new file mode 100644 index 0000000000..25d0e424a6 --- /dev/null +++ b/src/applications/phrequent/engineextension/PhrequentCurtainExtension.php @@ -0,0 +1,87 @@ +getViewer(); + + $events = id(new PhrequentUserTimeQuery()) + ->setViewer($viewer) + ->withObjectPHIDs(array($object->getPHID())) + ->needPreemptingEvents(true) + ->execute(); + $event_groups = mgroup($events, 'getUserPHID'); + + if (!$events) { + return; + } + + $handles = $viewer->loadHandles(array_keys($event_groups)); + $status_view = new PHUIStatusListView(); + + foreach ($event_groups as $user_phid => $event_group) { + $item = new PHUIStatusItemView(); + $item->setTarget($handles[$user_phid]->renderLink()); + + $state = 'stopped'; + foreach ($event_group as $event) { + if ($event->getDateEnded() === null) { + if ($event->isPreempted()) { + $state = 'suspended'; + } else { + $state = 'active'; + break; + } + } + } + + switch ($state) { + case 'active': + $item->setIcon( + PHUIStatusItemView::ICON_CLOCK, + 'green', + pht('Working Now')); + break; + case 'suspended': + $item->setIcon( + PHUIStatusItemView::ICON_CLOCK, + 'yellow', + pht('Interrupted')); + break; + case 'stopped': + $item->setIcon( + PHUIStatusItemView::ICON_CLOCK, + 'bluegrey', + pht('Not Working Now')); + break; + } + + $block = new PhrequentTimeBlock($event_group); + $item->setNote( + phutil_format_relative_time( + $block->getTimeSpentOnObject( + $object->getPHID(), + time()))); + + $status_view->addItem($item); + } + + + return $this->newPanel() + ->setHeaderText(pht('Time Spent')) + ->setOrder(40000) + ->appendChild($status_view); + } + +} diff --git a/src/applications/phriction/controller/PhrictionDocumentController.php b/src/applications/phriction/controller/PhrictionDocumentController.php index 360c0fbf3f..85bfa1a660 100644 --- a/src/applications/phriction/controller/PhrictionDocumentController.php +++ b/src/applications/phriction/controller/PhrictionDocumentController.php @@ -151,7 +151,7 @@ final class PhrictionDocumentController array(), pht( 'This document has been moved. You can edit it to put new '. - 'contne here, or use history to revert to an earlier '. + 'content here, or use history to revert to an earlier '. 'version.'))); } diff --git a/src/applications/phriction/storage/PhrictionDocument.php b/src/applications/phriction/storage/PhrictionDocument.php index 915cfed1eb..f931ac8dbe 100644 --- a/src/applications/phriction/storage/PhrictionDocument.php +++ b/src/applications/phriction/storage/PhrictionDocument.php @@ -194,9 +194,6 @@ final class PhrictionDocument extends PhrictionDAO return false; } - public function shouldShowSubscribersProperty() { - return true; - } /* -( PhabricatorApplicationTransactionInterface )------------------------- */ diff --git a/src/applications/phurl/controller/PhabricatorPhurlURLViewController.php b/src/applications/phurl/controller/PhabricatorPhurlURLViewController.php index dbc7fbcc79..946e35c854 100644 --- a/src/applications/phurl/controller/PhabricatorPhurlURLViewController.php +++ b/src/applications/phurl/controller/PhabricatorPhurlURLViewController.php @@ -24,24 +24,20 @@ final class PhabricatorPhurlURLViewController $title = $url->getMonogram(); $page_title = $title.' '.$url->getName(); $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb($title, $url->getURI()); + $crumbs->addTextCrumb($title); + $crumbs->setBorder(true); $timeline = $this->buildTransactionTimeline( $url, new PhabricatorPhurlURLTransactionQuery()); $header = $this->buildHeaderView($url); - $actions = $this->buildActionView($url); - $properties = $this->buildPropertyView($url); + $curtain = $this->buildCurtain($url); + $details = $this->buildPropertySectionView($url); - $properties->setActionList($actions); $url_error = id(new PHUIInfoView()) ->setErrors(array(pht('This URL is invalid due to a bad protocol.'))) ->setIsHidden($url->isValid()); - $box = id(new PHUIObjectBoxView()) - ->setHeader($header) - ->addPropertyList($properties) - ->setInfoView($url_error); $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business'); $add_comment_header = $is_serious @@ -58,71 +54,80 @@ final class PhabricatorPhurlURLViewController ->setAction($comment_uri) ->setSubmitButtonName(pht('Add Comment')); - return $this->buildApplicationPage( - array( - $crumbs, - $box, + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setCurtain($curtain) + ->setMainColumn(array( + $url_error, + $details, $timeline, $add_comment_form, - ), - array( - 'title' => $page_title, - 'pageObjects' => array($url->getPHID()), )); + + return $this->newPage() + ->setTitle($page_title) + ->setCrumbs($crumbs) + ->setPageObjectPHIDs(array($url->getPHID())) + ->appendChild( + array( + $view, + )); + } private function buildHeaderView(PhabricatorPhurlURL $url) { $viewer = $this->getViewer(); - $icon = 'fa-compress'; - $color = 'green'; + $icon = 'fa-check'; + $color = 'bluegrey'; $status = pht('Active'); + $id = $url->getID(); + + $visit = id(new PHUIButtonView()) + ->setTag('a') + ->setText(pht('Visit URL')) + ->setIcon('fa-external-link') + ->setHref("u/{$id}") + ->setDisabled(!$url->isValid()); $header = id(new PHUIHeaderView()) ->setUser($viewer) ->setHeader($url->getDisplayName()) ->setStatus($icon, $color, $status) - ->setPolicyObject($url); + ->setPolicyObject($url) + ->setHeaderIcon('fa-compress') + ->addActionLink($visit); return $header; } - private function buildActionView(PhabricatorPhurlURL $url) { + private function buildCurtain(PhabricatorPhurlURL $url) { $viewer = $this->getViewer(); $id = $url->getID(); - $actions = id(new PhabricatorActionListView()) - ->setUser($viewer) - ->setObject($url); + $curtain = $this->newCurtainView($url); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $url, PhabricatorPolicyCapability::CAN_EDIT); - $actions + $curtain ->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit')) ->setIcon('fa-pencil') ->setHref($this->getApplicationURI("url/edit/{$id}/")) ->setDisabled(!$can_edit) - ->setWorkflow(!$can_edit)) - ->addAction( - id(new PhabricatorActionView()) - ->setName(pht('Visit URL')) - ->setIcon('fa-external-link') - ->setHref("u/{$id}") - ->setDisabled(!$url->isValid())); + ->setWorkflow(!$can_edit)); - return $actions; + return $curtain; } - private function buildPropertyView(PhabricatorPhurlURL $url) { + private function buildPropertySectionView(PhabricatorPhurlURL $url) { $viewer = $this->getViewer(); $properties = id(new PHUIPropertyListView()) - ->setUser($viewer) - ->setObject($url); + ->setUser($viewer); $properties->addProperty( pht('Original URL'), @@ -132,18 +137,17 @@ final class PhabricatorPhurlURLViewController pht('Alias'), $url->getAlias()); - $properties->invokeWillRenderEvent(); - $description = $url->getDescription(); if (strlen($description)) { $description = new PHUIRemarkupView($viewer, $description); - $properties->addSectionHeader( - pht('Description'), - PHUIPropertyListView::ICON_SUMMARY); + $properties->addSectionHeader(pht('Description')); $properties->addTextContent($description); } - return $properties; + return id(new PHUIObjectBoxView()) + ->setHeaderText(pht('DETAILS')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->appendChild($properties); } } diff --git a/src/applications/phurl/storage/PhabricatorPhurlURL.php b/src/applications/phurl/storage/PhabricatorPhurlURL.php index 30a3b0b93b..19d5968b0c 100644 --- a/src/applications/phurl/storage/PhabricatorPhurlURL.php +++ b/src/applications/phurl/storage/PhabricatorPhurlURL.php @@ -169,9 +169,6 @@ final class PhabricatorPhurlURL extends PhabricatorPhurlDAO return ($phid == $this->getAuthorPHID()); } - public function shouldShowSubscribersProperty() { - return true; - } /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ diff --git a/src/applications/ponder/controller/PonderQuestionViewController.php b/src/applications/ponder/controller/PonderQuestionViewController.php index 41819a34f0..a1105c6c8e 100644 --- a/src/applications/ponder/controller/PonderQuestionViewController.php +++ b/src/applications/ponder/controller/PonderQuestionViewController.php @@ -43,8 +43,7 @@ final class PonderQuestionViewController extends PonderController { $header->setStatus($icon, 'dark', $text); } - $properties = $this->buildPropertyListView($question); - $actions = $this->buildActionListView($question); + $curtain = $this->buildCurtain($question); $details = $this->buildPropertySectionView($question); $can_edit = PhabricatorPolicyFilter::hasCapability( @@ -118,29 +117,24 @@ final class PonderQuestionViewController extends PonderController { $ponder_view = id(new PHUITwoColumnView()) ->setHeader($header) ->setSubheader($subheader) + ->setCurtain($curtain) ->setMainColumn($ponder_content) - ->setPropertyList($properties) ->addPropertySection(pht('DETAILS'), $details) - ->setActionList($actions) ->addClass('ponder-question-view'); $page_objects = array_merge( - array($question->getPHID()), - mpull($question->getAnswers(), 'getPHID')); + array($question->getPHID()), + mpull($question->getAnswers(), 'getPHID')); return $this->newPage() ->setTitle('Q'.$question->getID().' '.$question->getTitle()) ->setCrumbs($crumbs) ->setPageObjectPHIDs($page_objects) - ->appendChild( - array( - $ponder_view, - )); + ->appendChild($ponder_view); } - private function buildActionListView(PonderQuestion $question) { + private function buildCurtain(PonderQuestion $question) { $viewer = $this->getViewer(); - $request = $this->getRequest(); $id = $question->getID(); $can_edit = PhabricatorPolicyFilter::hasCapability( @@ -148,9 +142,7 @@ final class PonderQuestionViewController extends PonderController { $question, PhabricatorPolicyCapability::CAN_EDIT); - $view = id(new PhabricatorActionListView()) - ->setUser($viewer) - ->setObject($question); + $curtain = $this->newCurtainView($question); if ($question->getStatus() == PonderQuestionStatus::STATUS_OPEN) { $name = pht('Close Question'); @@ -160,7 +152,7 @@ final class PonderQuestionViewController extends PonderController { $icon = 'fa-square-o'; } - $view->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-pencil') ->setName(pht('Edit Question')) @@ -168,7 +160,7 @@ final class PonderQuestionViewController extends PonderController { ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); - $view->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setName($name) ->setIcon($icon) @@ -176,26 +168,13 @@ final class PonderQuestionViewController extends PonderController { ->setDisabled(!$can_edit) ->setHref($this->getApplicationURI("/question/status/{$id}/"))); - $view->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setIcon('fa-list') ->setName(pht('View History')) ->setHref($this->getApplicationURI("/question/history/{$id}/"))); - return $view; - } - - private function buildPropertyListView( - PonderQuestion $question) { - - $viewer = $this->getViewer(); - $view = id(new PHUIPropertyListView()) - ->setUser($viewer) - ->setObject($question); - - $view->invokeWillRenderEvent(); - - return $view; + return $curtain; } private function buildSubheaderView( diff --git a/src/applications/ponder/storage/PonderAnswer.php b/src/applications/ponder/storage/PonderAnswer.php index 025c020637..76c9057497 100644 --- a/src/applications/ponder/storage/PonderAnswer.php +++ b/src/applications/ponder/storage/PonderAnswer.php @@ -219,10 +219,6 @@ final class PonderAnswer extends PonderDAO return ($phid == $this->getAuthorPHID()); } - public function shouldShowSubscribersProperty() { - return true; - } - /* -( PhabricatorDestructibleInterface )----------------------------------- */ diff --git a/src/applications/ponder/storage/PonderQuestion.php b/src/applications/ponder/storage/PonderQuestion.php index 5ab719e3ac..99ce64c9f7 100644 --- a/src/applications/ponder/storage/PonderQuestion.php +++ b/src/applications/ponder/storage/PonderQuestion.php @@ -248,10 +248,6 @@ final class PonderQuestion extends PonderDAO return ($phid == $this->getAuthorPHID()); } - public function shouldShowSubscribersProperty() { - return true; - } - /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ diff --git a/src/applications/ponder/view/PonderAddAnswerView.php b/src/applications/ponder/view/PonderAddAnswerView.php index 989837aa94..d958e9843e 100644 --- a/src/applications/ponder/view/PonderAddAnswerView.php +++ b/src/applications/ponder/view/PonderAddAnswerView.php @@ -18,7 +18,7 @@ final class PonderAddAnswerView extends AphrontView { public function render() { $question = $this->question; - $viewer = $this->user; + $viewer = $this->getViewer(); $authors = mpull($question->getAnswers(), null, 'getAuthorPHID'); if (isset($authors[$viewer->getPHID()])) { @@ -49,7 +49,7 @@ final class PonderAddAnswerView extends AphrontView { $form = new AphrontFormView(); $form - ->setUser($this->user) + ->setViewer($viewer) ->setAction($this->actionURI) ->setWorkflow(true) ->addHiddenInput('question_id', $question->getID()) @@ -59,7 +59,7 @@ final class PonderAddAnswerView extends AphrontView { ->setLabel(pht('Answer')) ->setError(true) ->setID('answer-content') - ->setUser($this->user)) + ->setViewer($viewer)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue(pht('Add Answer'))); diff --git a/src/applications/project/controller/PhabricatorProjectManageController.php b/src/applications/project/controller/PhabricatorProjectManageController.php index 87420d1aa3..d84df87e93 100644 --- a/src/applications/project/controller/PhabricatorProjectManageController.php +++ b/src/applications/project/controller/PhabricatorProjectManageController.php @@ -30,12 +30,8 @@ final class PhabricatorProjectManageController $header->setStatus('fa-ban', 'red', pht('Archived')); } - $actions = $this->buildActionListView($project); - $properties = $this->buildPropertyListView($project, $actions); - - $object_box = id(new PHUIObjectBoxView()) - ->setHeader($header) - ->addPropertyList($properties); + $curtain = $this->buildCurtain($project); + $properties = $this->buildPropertyListView($project); $timeline = $this->buildTransactionTimeline( $project, @@ -47,6 +43,16 @@ final class PhabricatorProjectManageController $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Manage')); + $crumbs->setBorder(true); + + $manage = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setCurtain($curtain) + ->addPropertySection(pht('DETAILS'), $properties) + ->setMainColumn( + array( + $timeline, + )); return $this->newPage() ->setNavigation($nav) @@ -58,26 +64,22 @@ final class PhabricatorProjectManageController )) ->appendChild( array( - $object_box, - $timeline, + $manage, )); } - private function buildActionListView(PhabricatorProject $project) { - $request = $this->getRequest(); - $viewer = $request->getUser(); + private function buildCurtain(PhabricatorProject $project) { + $viewer = $this->getViewer(); $id = $project->getID(); - - $view = id(new PhabricatorActionListView()) - ->setUser($viewer); - $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $project, PhabricatorPolicyCapability::CAN_EDIT); - $view->addAction( + $curtain = $this->newCurtainView($project); + + $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Details')) ->setIcon('fa-pencil') @@ -85,7 +87,7 @@ final class PhabricatorProjectManageController ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); - $view->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Menu')) ->setIcon('fa-th-list') @@ -93,7 +95,7 @@ final class PhabricatorProjectManageController ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); - $view->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Picture')) ->setIcon('fa-picture-o') @@ -102,7 +104,7 @@ final class PhabricatorProjectManageController ->setWorkflow(!$can_edit)); if ($project->isArchived()) { - $view->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Activate Project')) ->setIcon('fa-check') @@ -110,7 +112,7 @@ final class PhabricatorProjectManageController ->setDisabled(!$can_edit) ->setWorkflow(true)); } else { - $view->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Archive Project')) ->setIcon('fa-ban') @@ -119,18 +121,15 @@ final class PhabricatorProjectManageController ->setWorkflow(true)); } - return $view; + return $curtain; } private function buildPropertyListView( - PhabricatorProject $project, - PhabricatorActionListView $actions) { - $request = $this->getRequest(); - $viewer = $request->getUser(); + PhabricatorProject $project) { + $viewer = $this->getViewer(); $view = id(new PHUIPropertyListView()) - ->setUser($viewer) - ->setActionList($actions); + ->setUser($viewer); $view->addProperty( pht('Looks Like'), diff --git a/src/applications/project/controller/PhabricatorProjectProfileController.php b/src/applications/project/controller/PhabricatorProjectProfileController.php index f61ebd28d2..c9f3c2b9d3 100644 --- a/src/applications/project/controller/PhabricatorProjectProfileController.php +++ b/src/applications/project/controller/PhabricatorProjectProfileController.php @@ -91,7 +91,6 @@ final class PhabricatorProjectProfileController $home = id(new PHUITwoColumnView()) ->setHeader($header) - ->setFluid(true) ->addClass('project-view-home') ->setMainColumn( array( diff --git a/src/applications/project/engineextension/PhabricatorProjectsCurtainExtension.php b/src/applications/project/engineextension/PhabricatorProjectsCurtainExtension.php new file mode 100644 index 0000000000..cc394c2283 --- /dev/null +++ b/src/applications/project/engineextension/PhabricatorProjectsCurtainExtension.php @@ -0,0 +1,91 @@ +getViewer(); + + $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( + $object->getPHID(), + PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); + + $has_projects = (bool)$project_phids; + $project_phids = array_reverse($project_phids); + $handles = $viewer->loadHandles($project_phids); + + // If this object can appear on boards, build the workboard annotations. + // Some day, this might be a generic interface. For now, only tasks can + // appear on boards. + $can_appear_on_boards = ($object instanceof ManiphestTask); + + $annotations = array(); + if ($has_projects && $can_appear_on_boards) { + $engine = id(new PhabricatorBoardLayoutEngine()) + ->setViewer($viewer) + ->setBoardPHIDs($project_phids) + ->setObjectPHIDs(array($object->getPHID())) + ->executeLayout(); + + // TDOO: Generalize this UI and move it out of Maniphest. + require_celerity_resource('maniphest-task-summary-css'); + + foreach ($project_phids as $project_phid) { + $handle = $handles[$project_phid]; + + $columns = $engine->getObjectColumns( + $project_phid, + $object->getPHID()); + + $annotation = array(); + foreach ($columns as $column) { + $project_id = $column->getProject()->getID(); + + $column_name = pht('(%s)', $column->getDisplayName()); + $column_link = phutil_tag( + 'a', + array( + 'href' => "/project/board/{$project_id}/", + 'class' => 'maniphest-board-link', + ), + $column_name); + + $annotation[] = $column_link; + } + + if ($annotation) { + $annotations[$project_phid] = array( + ' ', + phutil_implode_html(', ', $annotation), + ); + } + } + + } + + if ($has_projects) { + $list = id(new PHUIHandleTagListView()) + ->setHandles($handles) + ->setAnnotations($annotations) + ->setShowHovercards(true); + } else { + $list = phutil_tag('em', array(), pht('None')); + } + + return $this->newPanel() + ->setHeaderText(pht('Projects')) + ->setOrder(10000) + ->appendChild($list); + } + +} diff --git a/src/applications/project/view/PhabricatorProjectUserListView.php b/src/applications/project/view/PhabricatorProjectUserListView.php index e7f6631bfb..d590cbb559 100644 --- a/src/applications/project/view/PhabricatorProjectUserListView.php +++ b/src/applications/project/view/PhabricatorProjectUserListView.php @@ -45,7 +45,7 @@ abstract class PhabricatorProjectUserListView extends AphrontView { abstract protected function getHeaderText(); public function render() { - $viewer = $this->getUser(); + $viewer = $this->getViewer(); $project = $this->getProject(); $user_phids = $this->getUserPHIDs(); diff --git a/src/applications/repository/storage/PhabricatorRepositoryCommit.php b/src/applications/repository/storage/PhabricatorRepositoryCommit.php index 9fd218db9a..18732e5e4c 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryCommit.php +++ b/src/applications/repository/storage/PhabricatorRepositoryCommit.php @@ -443,10 +443,6 @@ final class PhabricatorRepositoryCommit return ($phid == $this->getAuthorPHID()); } - public function shouldShowSubscribersProperty() { - return true; - } - /* -( PhabricatorApplicationTransactionInterface )------------------------- */ diff --git a/src/applications/repository/storage/PhabricatorRepositorySchemaSpec.php b/src/applications/repository/storage/PhabricatorRepositorySchemaSpec.php index 8ff2ed0c9b..4471151d4e 100644 --- a/src/applications/repository/storage/PhabricatorRepositorySchemaSpec.php +++ b/src/applications/repository/storage/PhabricatorRepositorySchemaSpec.php @@ -155,6 +155,9 @@ final class PhabricatorRepositorySchemaSpec 'repositoryID' => array( 'columns' => array('repositoryID', 'pathID', 'commitSequence'), ), + 'key_history' => array( + 'columns' => array('commitID', 'isDirect', 'changeType'), + ), )); $this->buildRawSchema( diff --git a/src/applications/slowvote/controller/PhabricatorSlowvotePollController.php b/src/applications/slowvote/controller/PhabricatorSlowvotePollController.php index 2eb2c08c75..f788f5e94c 100644 --- a/src/applications/slowvote/controller/PhabricatorSlowvotePollController.php +++ b/src/applications/slowvote/controller/PhabricatorSlowvotePollController.php @@ -46,8 +46,7 @@ final class PhabricatorSlowvotePollController ->setPolicyObject($poll) ->setHeaderIcon('fa-bar-chart'); - $actions = $this->buildActionView($poll); - $properties = $this->buildPropertyView($poll); + $curtain = $this->buildCurtain($poll); $subheader = $this->buildSubheaderView($poll); $crumbs = $this->buildApplicationCrumbs(); @@ -68,37 +67,31 @@ final class PhabricatorSlowvotePollController $view = id(new PHUITwoColumnView()) ->setHeader($header) ->setSubheader($subheader) - ->setMainColumn($poll_content) - ->setPropertyList($properties) - ->setActionList($actions); + ->setCurtain($curtain) + ->setMainColumn($poll_content); return $this->newPage() ->setTitle('V'.$poll->getID().' '.$poll->getQuestion()) ->setCrumbs($crumbs) ->setPageObjectPHIDs(array($poll->getPHID())) - ->appendChild( - array( - $view, - )); + ->appendChild($view); } - private function buildActionView(PhabricatorSlowvotePoll $poll) { - $viewer = $this->getRequest()->getUser(); - - $view = id(new PhabricatorActionListView()) - ->setUser($viewer) - ->setObject($poll); + private function buildCurtain(PhabricatorSlowvotePoll $poll) { + $viewer = $this->getViewer(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $poll, PhabricatorPolicyCapability::CAN_EDIT); + $curtain = $this->newCurtainView($poll); + $is_closed = $poll->getIsClosed(); $close_poll_text = $is_closed ? pht('Reopen Poll') : pht('Close Poll'); $close_poll_icon = $is_closed ? 'fa-play-circle-o' : 'fa-ban'; - $view->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Poll')) ->setIcon('fa-pencil') @@ -106,7 +99,7 @@ final class PhabricatorSlowvotePollController ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); - $view->addAction( + $curtain->addAction( id(new PhabricatorActionView()) ->setName($close_poll_text) ->setIcon($close_poll_icon) @@ -114,19 +107,7 @@ final class PhabricatorSlowvotePollController ->setDisabled(!$can_edit) ->setWorkflow(true)); - return $view; - } - - private function buildPropertyView( - PhabricatorSlowvotePoll $poll) { - - $viewer = $this->getRequest()->getUser(); - $view = id(new PHUIPropertyListView()) - ->setUser($viewer) - ->setObject($poll); - $view->invokeWillRenderEvent(); - - return $view; + return $curtain; } private function buildSubheaderView( diff --git a/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php b/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php index 2240c510aa..6896f7ba2f 100644 --- a/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php +++ b/src/applications/slowvote/storage/PhabricatorSlowvotePoll.php @@ -179,10 +179,6 @@ final class PhabricatorSlowvotePoll extends PhabricatorSlowvoteDAO return ($phid == $this->getAuthorPHID()); } - public function shouldShowSubscribersProperty() { - return true; - } - /* -( PhabricatorTokenReceiverInterface )---------------------------------- */ diff --git a/src/applications/subscriptions/engineextension/PhabricatorSubscriptionsCurtainExtension.php b/src/applications/subscriptions/engineextension/PhabricatorSubscriptionsCurtainExtension.php new file mode 100644 index 0000000000..6467141de2 --- /dev/null +++ b/src/applications/subscriptions/engineextension/PhabricatorSubscriptionsCurtainExtension.php @@ -0,0 +1,39 @@ +getViewer(); + $object_phid = $object->getPHID(); + + $subscriber_phids = PhabricatorSubscribersQuery::loadSubscribersForPHID( + $object_phid); + + $handles = $viewer->loadHandles($subscriber_phids); + + // TODO: This class can't accept a HandleList yet. + $handles = iterator_to_array($handles); + + $susbscribers_view = id(new SubscriptionListStringBuilder()) + ->setObjectPHID($object_phid) + ->setHandles($handles) + ->buildPropertyString(); + + return $this->newPanel() + ->setHeaderText(pht('Subscribers')) + ->setOrder(20000) + ->appendChild($susbscribers_view); + } + +} diff --git a/src/applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php b/src/applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php index b69ce4243f..acbb978238 100644 --- a/src/applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php +++ b/src/applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php @@ -96,11 +96,6 @@ final class PhabricatorSubscriptionsUIEventListener return; } - if (!$object->shouldShowSubscribersProperty()) { - // This object doesn't render subscribers in its property list. - return; - } - $subscribers = PhabricatorSubscribersQuery::loadSubscribersForPHID( $object->getPHID()); if ($subscribers) { diff --git a/src/applications/subscriptions/interface/PhabricatorSubscribableInterface.php b/src/applications/subscriptions/interface/PhabricatorSubscribableInterface.php index e54e2cd42e..1af9e7107f 100644 --- a/src/applications/subscriptions/interface/PhabricatorSubscribableInterface.php +++ b/src/applications/subscriptions/interface/PhabricatorSubscribableInterface.php @@ -13,15 +13,6 @@ interface PhabricatorSubscribableInterface { */ public function isAutomaticallySubscribed($phid); - - /** - * Return `true` to indicate that "Subscribers:" should be shown when - * rendering property lists for this object, or `false` to omit the property. - * - * @return bool True to show the "Subscribers:" property. - */ - public function shouldShowSubscribersProperty(); - } // TEMPLATE IMPLEMENTATION ///////////////////////////////////////////////////// @@ -33,8 +24,4 @@ interface PhabricatorSubscribableInterface { return false; } - public function shouldShowSubscribersProperty() { - return true; - } - */ diff --git a/src/applications/tokens/engineextension/PhabricatorTokensCurtainExtension.php b/src/applications/tokens/engineextension/PhabricatorTokensCurtainExtension.php new file mode 100644 index 0000000000..1d1ca2551c --- /dev/null +++ b/src/applications/tokens/engineextension/PhabricatorTokensCurtainExtension.php @@ -0,0 +1,67 @@ +getViewer(); + + $tokens_given = id(new PhabricatorTokenGivenQuery()) + ->setViewer($viewer) + ->withObjectPHIDs(array($object->getPHID())) + ->execute(); + if (!$tokens_given) { + return null; + } + + $author_phids = mpull($tokens_given, 'getAuthorPHID'); + $handles = $viewer->loadHandles($author_phids); + + Javelin::initBehavior('phabricator-tooltips'); + + $list = array(); + foreach ($tokens_given as $token_given) { + $token = $token_given->getToken(); + + $aural = javelin_tag( + 'span', + array( + 'aural' => true, + ), + pht( + '"%s" token, awarded by %s.', + $token->getName(), + $handles[$token_given->getAuthorPHID()]->getName())); + + $list[] = javelin_tag( + 'span', + array( + 'sigil' => 'has-tooltip', + 'class' => 'token-icon', + 'meta' => array( + 'tip' => $handles[$token_given->getAuthorPHID()]->getName(), + ), + ), + array( + $aural, + $token->renderIcon(), + )); + } + + return $this->newPanel() + ->setHeaderText(pht('Tokens')) + ->setOrder(30000) + ->appendChild($list); + } + +} diff --git a/src/applications/tokens/query/PhabricatorTokenGivenQuery.php b/src/applications/tokens/query/PhabricatorTokenGivenQuery.php index 224efff198..b4a86428c1 100644 --- a/src/applications/tokens/query/PhabricatorTokenGivenQuery.php +++ b/src/applications/tokens/query/PhabricatorTokenGivenQuery.php @@ -58,10 +58,12 @@ final class PhabricatorTokenGivenQuery } protected function willFilterPage(array $results) { + $viewer = $this->getViewer(); + $object_phids = mpull($results, 'getObjectPHID'); $objects = id(new PhabricatorObjectQuery()) - ->setViewer($this->getViewer()) + ->setViewer($viewer) ->withPHIDs($object_phids) ->execute(); $objects = mpull($objects, null, 'getPHID'); @@ -80,6 +82,31 @@ final class PhabricatorTokenGivenQuery unset($results[$key]); } + if (!$results) { + return $results; + } + + $token_phids = mpull($results, 'getTokenPHID'); + + $tokens = id(new PhabricatorTokenQuery()) + ->setViewer($viewer) + ->withPHIDs($token_phids) + ->execute(); + $tokens = mpull($tokens, null, 'getPHID'); + + foreach ($results as $key => $result) { + $token_phid = $result->getTokenPHID(); + + $token = idx($tokens, $token_phid); + if (!$token) { + $this->didRejectResult($result); + unset($results[$key]); + continue; + } + + $result->attachToken($token); + } + return $results; } diff --git a/src/applications/tokens/storage/PhabricatorTokenGiven.php b/src/applications/tokens/storage/PhabricatorTokenGiven.php index 59ffc819d1..44db1baaf5 100644 --- a/src/applications/tokens/storage/PhabricatorTokenGiven.php +++ b/src/applications/tokens/storage/PhabricatorTokenGiven.php @@ -8,6 +8,7 @@ final class PhabricatorTokenGiven extends PhabricatorTokenDAO protected $tokenPHID; private $object = self::ATTACHABLE; + private $token = self::ATTACHABLE; protected function getConfiguration() { return array( @@ -35,6 +36,15 @@ final class PhabricatorTokenGiven extends PhabricatorTokenDAO return $this->assertAttached($this->object); } + public function attachToken(PhabricatorToken $token) { + $this->token = $token; + return $this; + } + + public function getToken() { + return $this->assertAttached($this->token); + } + public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, diff --git a/src/applications/transactions/editengine/PhabricatorEditEngine.php b/src/applications/transactions/editengine/PhabricatorEditEngine.php index ce7ec1c437..8be7c9d6f2 100644 --- a/src/applications/transactions/editengine/PhabricatorEditEngine.php +++ b/src/applications/transactions/editengine/PhabricatorEditEngine.php @@ -821,7 +821,7 @@ abstract class PhabricatorEditEngine } private function buildCrumbs($object, $final = false) { - $controller = $this->getcontroller(); + $controller = $this->getController(); $crumbs = $controller->buildApplicationCrumbsForEditEngine(); if ($this->getIsCreate()) { @@ -1179,6 +1179,60 @@ abstract class PhabricatorEditEngine return $actions; } + + /** + * Test if the viewer could apply a certain type of change by using the + * normal "Edit" form. + * + * This method returns `true` if the user has access to an edit form and + * that edit form has a field which applied the specified transaction type, + * and that field is visible and editable for the user. + * + * For example, you can use it to test if a user is able to reassign tasks + * or not, prior to rendering dedicated UI for task reassingment. + * + * Note that this method does NOT test if the user can actually edit the + * current object, just if they have access to the related field. + * + * @param const Transaction type to test for. + * @return bool True if the user could "Edit" to apply the transaction type. + */ + final public function hasEditAccessToTransaction($xaction_type) { + $viewer = $this->getViewer(); + + $config = $this->loadDefaultEditConfiguration(); + if (!$config) { + return false; + } + + $object = $this->getTargetObject(); + if (!$object) { + $object = $this->newEditableObject(); + } + + $fields = $this->buildEditFields($object); + + $field = null; + foreach ($fields as $form_field) { + $field_xaction_type = $form_field->getTransactionType(); + if ($field_xaction_type === $xaction_type) { + $field = $form_field; + break; + } + } + + if (!$field) { + return false; + } + + if (!$field->shouldReadValueFromSubmit()) { + return false; + } + + return true; + } + + final public function addActionToCrumbs(PHUICrumbsView $crumbs) { $viewer = $this->getViewer(); diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php index 8084ea302f..ee8a7a47a3 100644 --- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php +++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php @@ -605,24 +605,27 @@ abstract class PhabricatorApplicationTransaction break; } - // If a transaction publishes an inline comment: - // - // - Don't show it if there are other kinds of transactions. The - // rationale here is that application mail will make the presence - // of inline comments obvious enough by including them prominently - // in the body. We could change this in the future if the obviousness - // needs to be increased. - // - If there are only inline transactions, only show the first - // transaction. The rationale is that seeing multiple "added an inline - // comment" transactions is not useful. - if ($this->isInlineCommentTransaction()) { + $inlines = array(); + + // If there's a normal comment, we don't need to publish the inline + // transaction, since the normal comment covers things. foreach ($xactions as $xaction) { - if (!$xaction->isInlineCommentTransaction()) { + if ($xaction->isInlineCommentTransaction()) { + $inlines[] = $xaction; + continue; + } + + // We found a normal comment, so hide this inline transaction. + if ($xaction->hasComment()) { return true; } } - return ($this !== head($xactions)); + + // If there are several inline comments, only publish the first one. + if ($this !== head($inlines)) { + return true; + } } return $this->shouldHide(); diff --git a/src/docs/user/userguide/almanac.diviner b/src/docs/user/userguide/almanac.diviner index b7c30cf77a..a6b04e1fd1 100644 --- a/src/docs/user/userguide/almanac.diviner +++ b/src/docs/user/userguide/almanac.diviner @@ -96,7 +96,7 @@ them from the service. Concepts ======== -The major concepts in Almanac are **devices*, **interfaces**, **services**, +The major concepts in Almanac are **devices**, **interfaces**, **services**, **bindings**, **networks**, and **namespaces**. **Devices**: Almanac devices represent physical or virtual devices. diff --git a/src/infrastructure/daemon/workers/PhabricatorTriggerDaemon.php b/src/infrastructure/daemon/workers/PhabricatorTriggerDaemon.php index 87955fe595..1620c8f99a 100644 --- a/src/infrastructure/daemon/workers/PhabricatorTriggerDaemon.php +++ b/src/infrastructure/daemon/workers/PhabricatorTriggerDaemon.php @@ -16,6 +16,10 @@ final class PhabricatorTriggerDaemon private $garbageCollectors; private $nextCollection; + private $anyNuanceData; + private $nuanceSources; + private $nuanceCursors; + protected function run() { // The trigger daemon is a low-level infrastructure daemon which schedules @@ -99,6 +103,7 @@ final class PhabricatorTriggerDaemon $lock->unlock(); $sleep_duration = $this->getSleepDuration(); + $sleep_duration = $this->runNuanceImportCursors($sleep_duration); $sleep_duration = $this->runGarbageCollection($sleep_duration); $this->sleep($sleep_duration); } while (!$this->shouldExit()); @@ -379,4 +384,75 @@ final class PhabricatorTriggerDaemon return false; } + +/* -( Nuance Importers )--------------------------------------------------- */ + + + private function runNuanceImportCursors($duration) { + $run_until = (PhabricatorTime::getNow() + $duration); + + do { + $more_data = $this->updateNuanceImportCursors(); + if (!$more_data) { + break; + } + } while (PhabricatorTime::getNow() <= $run_until); + + $remaining = max(0, $run_until - PhabricatorTime::getNow()); + + return $remaining; + } + + + private function updateNuanceImportCursors() { + $nuance_app = 'PhabricatorNuanceApplication'; + if (!PhabricatorApplication::isClassInstalled($nuance_app)) { + return false; + } + + // If we haven't loaded sources yet, load them first. + if (!$this->nuanceSources && !$this->nuanceCursors) { + $this->anyNuanceData = false; + + $sources = id(new NuanceSourceQuery()) + ->setViewer($this->getViewer()) + ->withIsDisabled(false) + ->withHasImportCursors(true) + ->execute(); + if (!$sources) { + return false; + } + + $this->nuanceSources = array_reverse($sources); + } + + // If we don't have any cursors, move to the next source and generate its + // cursors. + if (!$this->nuanceCursors) { + $source = array_pop($this->nuanceSources); + + $definition = $source->getDefinition() + ->setViewer($this->getViewer()) + ->setSource($source); + + $cursors = $definition->getImportCursors(); + $this->nuanceCursors = array_reverse($cursors); + } + + // Update the next cursor. + $cursor = array_pop($this->nuanceCursors); + if ($cursor) { + $more_data = $cursor->importFromSource(); + if ($more_data) { + $this->anyNuanceData = true; + } + } + + if (!$this->nuanceSources && !$this->nuanceCursors) { + return $this->anyNuanceData; + } + + return true; + } + } diff --git a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJob.php b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJob.php index 0b70268cb4..ca7508d2b2 100644 --- a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJob.php +++ b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJob.php @@ -218,10 +218,6 @@ final class PhabricatorWorkerBulkJob return false; } - public function shouldShowSubscribersProperty() { - return true; - } - /* -( PhabricatorApplicationTransactionInterface )------------------------- */ diff --git a/src/infrastructure/diff/PhabricatorInlineCommentController.php b/src/infrastructure/diff/PhabricatorInlineCommentController.php index aa37d29c72..1c92b167b0 100644 --- a/src/infrastructure/diff/PhabricatorInlineCommentController.php +++ b/src/infrastructure/diff/PhabricatorInlineCommentController.php @@ -305,13 +305,18 @@ abstract class PhabricatorInlineCommentController pht('Failed to load comment "%s".', $reply_phid)); } - // NOTE: It's fine to reply to a comment from a different changeset, so - // the reply comment may not appear on the same changeset that the new - // comment appears on. This is expected in the case of ghost comments. - // We currently put the new comment on the visible changeset, not the - // original comment's changeset. + // When replying, force the new comment into the same location as the + // old comment. If we don't do this, replying to a ghost comment from + // diff A while viewing diff B can end up placing the two comments in + // different places while viewing diff C, because the porting algorithm + // makes a different decision. Forcing the comments to bind to the same + // place makes sure they stick together no matter which diff is being + // viewed. See T10562 for discussion. + $this->changesetID = $reply_comment->getChangesetID(); $this->isNewFile = $reply_comment->getIsNewFile(); + $this->lineNumber = $reply_comment->getLineNumber(); + $this->lineLength = $reply_comment->getLineLength(); } } diff --git a/src/infrastructure/diff/view/PHUIDiffInlineCommentDetailView.php b/src/infrastructure/diff/view/PHUIDiffInlineCommentDetailView.php index b58b1764a8..808fb5d067 100644 --- a/src/infrastructure/diff/view/PHUIDiffInlineCommentDetailView.php +++ b/src/infrastructure/diff/view/PHUIDiffInlineCommentDetailView.php @@ -176,7 +176,7 @@ final class PHUIDiffInlineCommentDetailView if ($inline->getHasReplies()) { $classes[] = 'inline-comment-has-reply'; } - // I think this is unused + if ($inline->getReplyToCommentPHID()) { $classes[] = 'inline-comment-is-reply'; } diff --git a/src/infrastructure/diff/view/PHUIDiffInlineCommentEditView.php b/src/infrastructure/diff/view/PHUIDiffInlineCommentEditView.php index 3a9bc616d9..a7c10b5b48 100644 --- a/src/infrastructure/diff/view/PHUIDiffInlineCommentEditView.php +++ b/src/infrastructure/diff/view/PHUIDiffInlineCommentEditView.php @@ -78,12 +78,11 @@ final class PHUIDiffInlineCommentEditView if (!$this->uri) { throw new PhutilInvalidStateException('setSubmitURI'); } - if (!$this->user) { - throw new PhutilInvalidStateException('setUser'); - } + + $viewer = $this->getViewer(); $content = phabricator_form( - $this->user, + $viewer, array( 'action' => $this->uri, 'method' => 'POST', diff --git a/src/view/AphrontDialogView.php b/src/view/AphrontDialogView.php index f8bc3fa060..26dee0c398 100644 --- a/src/view/AphrontDialogView.php +++ b/src/view/AphrontDialogView.php @@ -236,11 +236,11 @@ final class AphrontDialogView $this->cancelText); } - if (!$this->user) { + if (!$this->hasViewer()) { throw new Exception( pht( 'You must call %s when rendering an %s.', - 'setUser()', + 'setViewer()', __CLASS__)); } @@ -308,7 +308,7 @@ final class AphrontDialogView if (!$this->renderAsForm) { $buttons = array( phabricator_form( - $this->user, + $this->getViewer(), $form_attributes, array_merge($hidden_inputs, $buttons)), ); @@ -376,7 +376,7 @@ final class AphrontDialogView if ($this->renderAsForm) { return phabricator_form( - $this->user, + $this->getViewer(), $form_attributes + $attributes, array($hidden_inputs, $content)); } else { diff --git a/src/view/AphrontView.php b/src/view/AphrontView.php index 91565ca6bd..c014d7d50c 100644 --- a/src/view/AphrontView.php +++ b/src/view/AphrontView.php @@ -6,7 +6,7 @@ abstract class AphrontView extends Phobject implements PhutilSafeHTMLProducerInterface { - protected $user; + private $viewer; protected $children = array(); @@ -14,19 +14,65 @@ abstract class AphrontView extends Phobject /** - * @task config + * Set the user viewing this element. + * + * @param PhabricatorUser Viewing user. + * @return this */ - public function setUser(PhabricatorUser $user) { - $this->user = $user; + public function setViewer(PhabricatorUser $viewer) { + $this->viewer = $viewer; return $this; } /** + * Get the user viewing this element. + * + * Throws an exception if no viewer has been set. + * + * @return PhabricatorUser Viewing user. + */ + public function getViewer() { + if (!$this->viewer) { + throw new PhutilInvalidStateException('setViewer'); + } + + return $this->viewer; + } + + + /** + * Test if a viewer has been set on this elmeent. + * + * @return bool True if a viewer is available. + */ + public function hasViewer() { + return (bool)$this->viewer; + } + + + /** + * Deprecated, use @{method:setViewer}. + * * @task config + * @deprecated + */ + public function setUser(PhabricatorUser $user) { + return $this->setViewer($user); + } + + + /** + * Deprecated, use @{method:getViewer}. + * + * @task config + * @deprecated */ protected function getUser() { - return $this->user; + if (!$this->hasViewer()) { + return null; + } + return $this->getViewer(); } diff --git a/src/view/extension/PHUICurtainExtension.php b/src/view/extension/PHUICurtainExtension.php new file mode 100644 index 0000000000..7f7693cbac --- /dev/null +++ b/src/view/extension/PHUICurtainExtension.php @@ -0,0 +1,124 @@ +viewer = $viewer; + return $this; + } + + public function getViewer() { + return $this->viewer; + } + + abstract public function shouldEnableForObject($object); + abstract public function getExtensionApplication(); + + public function buildCurtainPanels($object) { + $panel = $this->buildCurtainPanel($object); + + if ($panel !== null) { + return array($panel); + } + + return array(); + } + + public function buildCurtainPanel($object) { + throw new PhutilMethodNotImplementedException(); + } + + final public function getExtensionKey() { + return $this->getPhobjectClassConstant('EXTENSIONKEY'); + } + + final public static function getAllExtensions() { + return id(new PhutilClassMapQuery()) + ->setAncestorClass(__CLASS__) + ->setUniqueMethod('getExtensionKey') + ->execute(); + } + + protected function newPanel() { + return new PHUICurtainPanelView(); + } + + final public static function buildExtensionPanels( + PhabricatorUser $viewer, + $object) { + + $extensions = self::getAllExtensions(); + foreach ($extensions as $extension) { + $extension->setViewer($viewer); + } + + foreach ($extensions as $key => $extension) { + $application = $extension->getExtensionApplication(); + if (!($application instanceof PhabricatorApplication)) { + throw new Exception( + pht( + 'Curtain extension ("%s", of class "%s") did not return an '. + 'application from method "%s". This method must return an '. + 'object of class "%s".', + $key, + get_class($extension), + 'getExtensionApplication()', + 'PhabricatorApplication')); + } + + $has_application = PhabricatorApplication::isClassInstalledForViewer( + get_class($application), + $viewer); + + if (!$has_application) { + unset($extensions[$key]); + } + } + + foreach ($extensions as $key => $extension) { + if (!$extension->shouldEnableForObject($object)) { + unset($extensions[$key]); + } + } + + $result = array(); + + foreach ($extensions as $key => $extension) { + $panels = $extension->buildCurtainPanels($object); + if (!is_array($panels)) { + throw new Exception( + pht( + 'Curtain extension ("%s", of class "%s") did not return a list of '. + 'curtain panels from method "%s". This method must return an '. + 'array, and each value in the array must be a "%s" object.', + $key, + get_class($extension), + 'buildCurtainPanels()', + 'PHUICurtainPanelView')); + } + + foreach ($panels as $panel_key => $panel) { + if (!($panel instanceof PHUICurtainPanelView)) { + throw new Exception( + pht( + 'Curtain extension ("%s", of class "%s") returned a list of '. + 'curtain panels from "%s" that contains an invalid value: '. + 'a value (with key "%s") is not an object of class "%s". '. + 'Each item in the returned array must be a panel.', + $key, + get_class($extension), + 'buildCurtainPanels()', + $panel_key, + 'PHUICurtainPanelView')); + } + + $result[] = $panel; + } + } + + return $result; + } + +} diff --git a/src/view/form/AphrontFormView.php b/src/view/form/AphrontFormView.php index 810208ffae..ecd4c1206e 100644 --- a/src/view/form/AphrontFormView.php +++ b/src/view/form/AphrontFormView.php @@ -85,13 +85,13 @@ final class AphrontFormView extends AphrontView { public function appendRemarkupInstructions($remarkup) { return $this->appendInstructions( - new PHUIRemarkupView($this->getUser(), $remarkup)); + new PHUIRemarkupView($this->getViewer(), $remarkup)); } public function buildLayoutView() { foreach ($this->controls as $control) { - $control->setUser($this->getUser()); + $control->setViewer($this->getViewer()); $control->willRender(); } @@ -123,7 +123,7 @@ final class AphrontFormView extends AphrontView { $layout = $this->buildLayoutView(); - if (!$this->user) { + if (!$this->hasViewer()) { throw new Exception( pht( 'You must pass the user to %s.', @@ -136,7 +136,7 @@ final class AphrontFormView extends AphrontView { } return phabricator_form( - $this->user, + $this->getViewer(), array( 'class' => $this->shaded ? 'phui-form-shaded' : null, 'action' => $this->action, diff --git a/src/view/form/control/AphrontFormDateControl.php b/src/view/form/control/AphrontFormDateControl.php index 75398d688a..25dec8eef0 100644 --- a/src/view/form/control/AphrontFormDateControl.php +++ b/src/view/form/control/AphrontFormDateControl.php @@ -137,12 +137,12 @@ final class AphrontFormDateControl extends AphrontFormControl { } private function getTimeFormat() { - return $this->getUser() + return $this->getViewer() ->getPreference(PhabricatorUserPreferences::PREFERENCE_TIME_FORMAT); } private function getDateFormat() { - return $this->getUser() + return $this->getViewer() ->getPreference(PhabricatorUserPreferences::PREFERENCE_DATE_FORMAT); } @@ -153,7 +153,7 @@ final class AphrontFormDateControl extends AphrontFormControl { private function formatTime($epoch, $fmt) { return phabricator_format_local_time( $epoch, - $this->user, + $this->getViewer(), $fmt); } @@ -259,7 +259,7 @@ final class AphrontFormDateControl extends AphrontFormControl { ), $time_sel); - $preferences = $this->user->loadPreferences(); + $preferences = $this->getViewer()->loadPreferences(); $pref_week_start = PhabricatorUserPreferences::PREFERENCE_WEEK_START_DAY; $week_start = $preferences->getPreference($pref_week_start, 0); @@ -300,12 +300,9 @@ final class AphrontFormDateControl extends AphrontFormControl { return $this->zone; } - $user = $this->getUser(); - if (!$this->getUser()) { - throw new PhutilInvalidStateException('setUser'); - } + $viewer = $this->getViewer(); - $user_zone = $user->getTimezoneIdentifier(); + $user_zone = $viewer->getTimezoneIdentifier(); $this->zone = new DateTimeZone($user_zone); return $this->zone; } diff --git a/src/view/form/control/AphrontFormTokenizerControl.php b/src/view/form/control/AphrontFormTokenizerControl.php index 3f24dd1348..3d65c4e525 100644 --- a/src/view/form/control/AphrontFormTokenizerControl.php +++ b/src/view/form/control/AphrontFormTokenizerControl.php @@ -90,8 +90,8 @@ final class AphrontFormTokenizerControl extends AphrontFormControl { } $username = null; - if ($this->user) { - $username = $this->user->getUsername(); + if ($this->hasViewer()) { + $username = $this->getViewer()->getUsername(); } $datasource_uri = $datasource->getDatasourceURI(); diff --git a/src/view/layout/AphrontSideNavFilterView.php b/src/view/layout/AphrontSideNavFilterView.php index 3a267510f0..d59bf5ccac 100644 --- a/src/view/layout/AphrontSideNavFilterView.php +++ b/src/view/layout/AphrontSideNavFilterView.php @@ -199,9 +199,6 @@ final class AphrontSideNavFilterView extends AphrontView { } private function renderFlexNav() { - - $user = $this->user; - require_celerity_resource('phabricator-nav-view-css'); $nav_classes = array(); diff --git a/src/view/layout/PHUICurtainPanelView.php b/src/view/layout/PHUICurtainPanelView.php new file mode 100644 index 0000000000..238e56a374 --- /dev/null +++ b/src/view/layout/PHUICurtainPanelView.php @@ -0,0 +1,63 @@ +headerText = $header_text; + return $this; + } + + public function getHeaderText() { + return $this->headerText; + } + + public function setOrder($order) { + $this->order = $order; + return $this; + } + + public function getOrder() { + return $this->order; + } + + public function getOrderVector() { + return id(new PhutilSortVector()) + ->addInt($this->getOrder()); + } + + protected function getTagAttributes() { + return array( + 'class' => 'phui-curtain-panel', + ); + } + + protected function getTagContent() { + $header = null; + + $header_text = $this->getHeaderText(); + if (strlen($header_text)) { + $header = phutil_tag( + 'div', + array( + 'class' => 'phui-curtain-panel-header', + ), + $header_text); + } + + $body = phutil_tag( + 'div', + array( + 'class' => 'phui-curtain-panel-body', + ), + $this->renderChildren()); + + return array( + $header, + $body, + ); + } + +} diff --git a/src/view/layout/PHUICurtainView.php b/src/view/layout/PHUICurtainView.php new file mode 100644 index 0000000000..af02ceb932 --- /dev/null +++ b/src/view/layout/PHUICurtainView.php @@ -0,0 +1,63 @@ +getActionList()->addAction($action); + return $this; + } + + public function addPanel(PHUICurtainPanelView $curtain_panel) { + $this->panels[] = $curtain_panel; + return $this; + } + + public function newPanel() { + $panel = new PHUICurtainPanelView(); + $this->addPanel($panel); + + // By default, application panels go at the bottom of the curtain, below + // extension panels. + $panel->setOrder(100000); + + return $panel; + } + + public function setActionList(PhabricatorActionListView $action_list) { + $this->actionList = $action_list; + return $this; + } + + public function getActionList() { + return $this->actionList; + } + + protected function canAppendChild() { + return false; + } + + protected function getTagContent() { + $action_list = $this->actionList; + + require_celerity_resource('phui-curtain-view-css'); + + $panels = $this->renderPanels(); + + return id(new PHUIObjectBoxView()) + ->appendChild($action_list) + ->appendChild($panels) + ->addClass('phui-two-column-properties'); + } + + private function renderPanels() { + $panels = $this->panels; + $panels = msortv($panels, 'getOrderVector'); + + return $panels; + } + + +} diff --git a/src/view/layout/PhabricatorActionListView.php b/src/view/layout/PhabricatorActionListView.php index 6c343af4fe..4965f02793 100644 --- a/src/view/layout/PhabricatorActionListView.php +++ b/src/view/layout/PhabricatorActionListView.php @@ -22,9 +22,7 @@ final class PhabricatorActionListView extends AphrontView { } public function render() { - if (!$this->user) { - throw new PhutilInvalidStateException('setUser'); - } + $viewer = $this->getViewer(); $event = new PhabricatorEvent( PhabricatorEventType::TYPE_UI_DIDRENDERACTIONS, @@ -32,7 +30,7 @@ final class PhabricatorActionListView extends AphrontView { 'object' => $this->object, 'actions' => $this->actions, )); - $event->setUser($this->user); + $event->setUser($viewer); PhutilEventEngine::dispatchEvent($event); $actions = $event->getValue('actions'); @@ -41,7 +39,7 @@ final class PhabricatorActionListView extends AphrontView { } foreach ($actions as $action) { - $action->setUser($this->user); + $action->setViewer($viewer); } require_celerity_resource('phabricator-action-list-view-css'); diff --git a/src/view/layout/PhabricatorActionView.php b/src/view/layout/PhabricatorActionView.php index b89c00daf5..3efa32071d 100644 --- a/src/view/layout/PhabricatorActionView.php +++ b/src/view/layout/PhabricatorActionView.php @@ -125,11 +125,11 @@ final class PhabricatorActionView extends AphrontView { $sigils = $sigils ? implode(' ', $sigils) : null; if ($this->renderAsForm) { - if (!$this->user) { + if (!$this->hasViewer()) { throw new Exception( pht( 'Call %s when rendering an action as a form.', - 'setUser()')); + 'setViewer()')); } $item = javelin_tag( @@ -140,7 +140,7 @@ final class PhabricatorActionView extends AphrontView { array($icon, $this->name)); $item = phabricator_form( - $this->user, + $this->getViewer(), array( 'action' => $this->getHref(), 'method' => 'POST', diff --git a/src/view/page/PhabricatorStandardPageView.php b/src/view/page/PhabricatorStandardPageView.php index e85abb2140..f38bf3c18b 100644 --- a/src/view/page/PhabricatorStandardPageView.php +++ b/src/view/page/PhabricatorStandardPageView.php @@ -490,7 +490,7 @@ final class PhabricatorStandardPageView extends PhabricatorBarePageView $nav->appendFooter($footer); $content = phutil_implode_html('', array($nav->render())); } else { - $contnet = array(); + $content = array(); $crumbs = $this->getCrumbs(); if ($crumbs) { diff --git a/src/view/page/menu/PhabricatorMainMenuSearchView.php b/src/view/page/menu/PhabricatorMainMenuSearchView.php index a5c5653cc3..d3c7319f7d 100644 --- a/src/view/page/menu/PhabricatorMainMenuSearchView.php +++ b/src/view/page/menu/PhabricatorMainMenuSearchView.php @@ -24,7 +24,7 @@ final class PhabricatorMainMenuSearchView extends AphrontView { } public function render() { - $user = $this->user; + $viewer = $this->getViewer(); $target_id = celerity_generate_unique_node_id(); $search_id = $this->getID(); @@ -86,7 +86,7 @@ final class PhabricatorMainMenuSearchView extends AphrontView { $selector = $this->buildModeSelector($selector_id, $application_id); $form = phabricator_form( - $user, + $viewer, array( 'action' => '/search/', 'method' => 'POST', @@ -109,7 +109,7 @@ final class PhabricatorMainMenuSearchView extends AphrontView { } private function buildModeSelector($selector_id, $application_id) { - $viewer = $this->getUser(); + $viewer = $this->getViewer(); $items = array(); $items[] = array( diff --git a/src/view/page/menu/PhabricatorMainMenuView.php b/src/view/page/menu/PhabricatorMainMenuView.php index 5ce25fb3da..e804573ff0 100644 --- a/src/view/page/menu/PhabricatorMainMenuView.php +++ b/src/view/page/menu/PhabricatorMainMenuView.php @@ -24,7 +24,7 @@ final class PhabricatorMainMenuView extends AphrontView { } public function render() { - $user = $this->user; + $viewer = $this->getViewer(); require_celerity_resource('phabricator-main-menu-view'); @@ -35,7 +35,7 @@ final class PhabricatorMainMenuView extends AphrontView { $app_button = ''; $aural = null; - if ($user->isLoggedIn() && $user->isUserActivated()) { + if ($viewer->isLoggedIn() && $viewer->isUserActivated()) { list($menu, $dropdowns, $aural) = $this->renderNotificationMenu(); if (array_filter($menu)) { $alerts[] = $menu; @@ -77,10 +77,10 @@ final class PhabricatorMainMenuView extends AphrontView { $controller = $this->getController(); foreach ($applications as $application) { $app_actions = $application->buildMainMenuItems( - $user, + $viewer, $controller); $app_extra = $application->buildMainMenuExtraNodes( - $user, + $viewer, $controller); foreach ($app_actions as $action) { @@ -97,7 +97,7 @@ final class PhabricatorMainMenuView extends AphrontView { $extensions = PhabricatorMainMenuBarExtension::getAllEnabledExtensions(); foreach ($extensions as $extension) { - $extension->setViewer($user); + $extension->setViewer($viewer); $controller = $this->getController(); if ($controller) { @@ -158,7 +158,7 @@ final class PhabricatorMainMenuView extends AphrontView { } private function renderSearch() { - $user = $this->user; + $viewer = $this->getViewer(); $result = null; @@ -166,15 +166,15 @@ final class PhabricatorMainMenuView extends AphrontView { 'helpURI' => '/help/keyboardshortcut/', ); - if ($user->isLoggedIn()) { - $show_search = $user->isUserActivated(); + if ($viewer->isLoggedIn()) { + $show_search = $viewer->isUserActivated(); } else { $show_search = PhabricatorEnv::getEnvConfig('policy.allow-public'); } if ($show_search) { $search = new PhabricatorMainMenuSearchView(); - $search->setUser($user); + $search->setViewer($viewer); $application = null; $controller = $this->getController(); @@ -188,7 +188,7 @@ final class PhabricatorMainMenuView extends AphrontView { $result = $search; $pref_shortcut = PhabricatorUserPreferences::PREFERENCE_SEARCH_SHORTCUT; - if ($user->loadPreferences()->getPreference($pref_shortcut, true)) { + if ($viewer->loadPreferences()->getPreference($pref_shortcut, true)) { $keyboard_config['searchID'] = $search->getID(); } } @@ -230,7 +230,7 @@ final class PhabricatorMainMenuView extends AphrontView { } private function renderApplicationMenu(array $bar_items) { - $user = $this->getUser(); + $viewer = $this->getViewer(); $view = $this->getApplicationMenu(); @@ -302,7 +302,7 @@ final class PhabricatorMainMenuView extends AphrontView { $logo_uri = $cache->getKey($cache_key_logo); if (!$logo_uri) { $file = id(new PhabricatorFileQuery()) - ->setViewer($this->getUser()) + ->setViewer($this->getViewer()) ->withPHIDs(array($custom_header)) ->executeOne(); if ($file) { @@ -355,7 +355,7 @@ final class PhabricatorMainMenuView extends AphrontView { } private function renderNotificationMenu() { - $user = $this->user; + $viewer = $this->getViewer(); require_celerity_resource('phabricator-notification-css'); require_celerity_resource('phabricator-notification-menu-css'); @@ -364,7 +364,7 @@ final class PhabricatorMainMenuView extends AphrontView { $aural = array(); $dropdown_query = id(new AphlictDropdownDataQuery()) - ->setViewer($user); + ->setViewer($viewer); $dropdown_data = $dropdown_query->execute(); $message_tag = ''; diff --git a/src/view/phui/PHUIFeedStoryView.php b/src/view/phui/PHUIFeedStoryView.php index bacd266089..978e25062f 100644 --- a/src/view/phui/PHUIFeedStoryView.php +++ b/src/view/phui/PHUIFeedStoryView.php @@ -172,8 +172,8 @@ final class PHUIFeedStoryView extends AphrontView { if ($this->epoch) { // TODO: This is really bad; when rendering through Conduit and via // renderText() we don't have a user. - if ($this->user) { - $foot = phabricator_datetime($this->epoch, $this->user); + if ($this->hasViewer()) { + $foot = phabricator_datetime($this->epoch, $this->getViewer()); } else { $foot = null; } diff --git a/src/view/phui/PHUIHeaderView.php b/src/view/phui/PHUIHeaderView.php index 0e12bc9abe..8111944c4d 100644 --- a/src/view/phui/PHUIHeaderView.php +++ b/src/view/phui/PHUIHeaderView.php @@ -20,7 +20,7 @@ final class PHUIHeaderView extends AphrontTagView { private $buttonBar = null; private $policyObject; private $epoch; - private $actionIcons = array(); + private $actionItems = array(); private $badges = array(); private $href; private $actionList; @@ -105,8 +105,8 @@ final class PHUIHeaderView extends AphrontTagView { return $this; } - public function addActionIcon(PHUIIconView $action) { - $this->actionIcons[] = $action; + public function addActionItem($action) { + $this->actionItems[] = $action; return $this; } @@ -116,25 +116,17 @@ final class PHUIHeaderView extends AphrontTagView { } public function setStatus($icon, $color, $name) { - $header_class = 'phui-header-status'; - if ($color) { - $icon = $icon.' '.$color; - $header_class = $header_class.'-'.$color; + // TODO: Normalize "closed/archived" to constants. + if ($color == 'dark') { + $color = PHUITagView::COLOR_INDIGO; } - $img = id(new PHUIIconView()) - ->setIcon($icon); - - $tag = phutil_tag( - 'span', - array( - 'class' => "phui-header-status {$header_class}", - ), - array( - $img, - $name, - )); + $tag = id(new PHUITagView()) + ->setName($name) + ->setIcon($icon) + ->setShade($color) + ->setType(PHUITagView::TYPE_SHADE); return $this->addProperty(self::PROPERTY_STATUS, $tag); } @@ -285,26 +277,18 @@ final class PHUIHeaderView extends AphrontTagView { $this->buttonBar); } - if ($this->actionIcons || $this->tags) { + if ($this->actionItems) { $action_list = array(); - if ($this->actionIcons) { - foreach ($this->actionIcons as $icon) { + if ($this->actionItems) { + foreach ($this->actionItems as $item) { $action_list[] = phutil_tag( 'li', array( - 'class' => 'phui-header-action-icon', + 'class' => 'phui-header-action-item', ), - $icon); + $item); } } - if ($this->tags) { - $action_list[] = phutil_tag( - 'li', - array( - 'class' => 'phui-header-action-tag', - ), - array_interleave(' ', $this->tags)); - } $right[] = phutil_tag( 'ul', array( @@ -362,7 +346,7 @@ final class PHUIHeaderView extends AphrontTagView { )); } - if ($this->properties || $this->policyObject) { + if ($this->properties || $this->policyObject || $this->tags) { $property_list = array(); foreach ($this->properties as $type => $property) { switch ($type) { @@ -379,6 +363,10 @@ final class PHUIHeaderView extends AphrontTagView { $property_list[] = $this->renderPolicyProperty($this->policyObject); } + if ($this->tags) { + $property_list[] = $this->tags; + } + $left[] = phutil_tag( 'div', array( diff --git a/src/view/phui/PHUIHovercardView.php b/src/view/phui/PHUIHovercardView.php index e5c89bb8fd..115964f191 100644 --- a/src/view/phui/PHUIHovercardView.php +++ b/src/view/phui/PHUIHovercardView.php @@ -106,7 +106,7 @@ final class PHUIHovercardView extends AphrontTagView { $header->setHeader($title); if ($this->tags) { foreach ($this->tags as $tag) { - $header->addTag($tag); + $header->addActionItem($tag); } } diff --git a/src/view/phui/PHUITimelineEventView.php b/src/view/phui/PHUITimelineEventView.php index ecd9329860..0d9b75782f 100644 --- a/src/view/phui/PHUITimelineEventView.php +++ b/src/view/phui/PHUITimelineEventView.php @@ -232,11 +232,12 @@ final class PHUITimelineEventView extends AphrontView { $fill_classes = array(); $fill_classes[] = 'phui-timeline-icon-fill'; if ($this->color) { + $fill_classes[] = 'fill-has-color'; $fill_classes[] = 'phui-timeline-icon-fill-'.$this->color; } $icon = id(new PHUIIconView()) - ->setIcon($this->icon.' white') + ->setIcon($this->icon) ->addClass('phui-timeline-icon'); $icon = phutil_tag( @@ -504,11 +505,12 @@ final class PHUITimelineEventView extends AphrontView { } $source = $this->getContentSource(); + $content_source = null; if ($source) { - $extra[] = id(new PhabricatorContentSourceView()) + $content_source = id(new PhabricatorContentSourceView()) ->setContentSource($source) - ->setUser($this->getUser()) - ->render(); + ->setUser($this->getUser()); + $content_source = pht('Via %s', $content_source->getSourceName()); } $date_created = null; @@ -528,6 +530,7 @@ final class PHUITimelineEventView extends AphrontView { $this->getUser()); if ($this->anchor) { Javelin::initBehavior('phabricator-watch-anchor'); + Javelin::initBehavior('phabricator-tooltips'); $anchor = id(new PhabricatorAnchorView()) ->setAnchorName($this->anchor) @@ -535,10 +538,14 @@ final class PHUITimelineEventView extends AphrontView { $date = array( $anchor, - phutil_tag( + javelin_tag( 'a', array( 'href' => '#'.$this->anchor, + 'sigil' => 'has-tooltip', + 'meta' => array( + 'tip' => $content_source, + ), ), $date), ); diff --git a/src/view/phui/PHUITwoColumnView.php b/src/view/phui/PHUITwoColumnView.php index fb27a1a50d..d174547559 100644 --- a/src/view/phui/PHUITwoColumnView.php +++ b/src/view/phui/PHUITwoColumnView.php @@ -8,9 +8,9 @@ final class PHUITwoColumnView extends AphrontTagView { private $fluid; private $header; private $subheader; + private $footer; private $propertySection = array(); - private $actionList; - private $propertyList; + private $curtain; const DISPLAY_LEFT = 'phui-side-column-left'; const DISPLAY_RIGHT = 'phui-side-column-right'; @@ -35,19 +35,23 @@ final class PHUITwoColumnView extends AphrontTagView { return $this; } + public function setFooter($footer) { + $this->footer = $footer; + return $this; + } + public function addPropertySection($title, $section) { $this->propertySection[] = array($title, $section); return $this; } - public function setActionList(PhabricatorActionListView $list) { - $this->actionList = $list; + public function setCurtain(PHUICurtainView $curtain) { + $this->curtain = $curtain; return $this; } - public function setPropertyList(PHUIPropertyListView $list) { - $this->propertyList = $list; - return $this; + public function getCurtain() { + return $this->curtain; } public function setFluid($fluid) { @@ -91,6 +95,8 @@ final class PHUITwoColumnView extends AphrontTagView { $main = $this->buildMainColumn(); $side = $this->buildSideColumn(); + $footer = $this->buildFooter(); + $order = array($side, $main); $inner = phutil_tag_div('phui-two-column-row grouped', $order); @@ -98,9 +104,12 @@ final class PHUITwoColumnView extends AphrontTagView { $header = null; if ($this->header) { - if ($this->actionList) { - $this->header->setActionList($this->actionList); + $curtain = $this->getCurtain(); + if ($curtain) { + $action_list = $curtain->getActionList(); + $this->header->setActionList($action_list); } + $header = phutil_tag_div( 'phui-two-column-header', $this->header); } @@ -120,6 +129,7 @@ final class PHUITwoColumnView extends AphrontTagView { $header, $subheader, $table, + $footer, )); } @@ -151,20 +161,8 @@ final class PHUITwoColumnView extends AphrontTagView { } private function buildSideColumn() { - $property_list = $this->propertyList; - $action_list = $this->actionList; - $properties = null; - if ($property_list || $action_list) { - if ($property_list) { - $property_list->setStacked(true); - } - - $properties = id(new PHUIObjectBoxView()) - ->appendChild($action_list) - ->appendChild($property_list) - ->addClass('phui-two-column-properties'); - } + $curtain = $this->getCurtain(); return phutil_tag( 'div', @@ -172,8 +170,23 @@ final class PHUITwoColumnView extends AphrontTagView { 'class' => 'phui-side-column', ), array( - $properties, + $curtain, $this->sideColumn, )); } + + private function buildFooter() { + + $footer = $this->footer; + + return phutil_tag( + 'div', + array( + 'class' => 'phui-two-column-content phui-two-column-footer', + ), + array( + $footer, + )); + + } } diff --git a/src/view/phui/PHUIWorkpanelView.php b/src/view/phui/PHUIWorkpanelView.php index b94b423ced..911d38c2e3 100644 --- a/src/view/phui/PHUIWorkpanelView.php +++ b/src/view/phui/PHUIWorkpanelView.php @@ -83,16 +83,16 @@ final class PHUIWorkpanelView extends AphrontTagView { ->setHeader($this->header) ->setSubheader($this->subheader); - if ($this->headerIcon) { - $header->setHeaderIcon($this->headerIcon); + foreach ($this->headerActions as $action) { + $header->addActionItem($action); } if ($this->headerTag) { - $header->addTag($this->headerTag); + $header->addActionItem($this->headerTag); } - foreach ($this->headerActions as $action) { - $header->addActionIcon($action); + if ($this->headerIcon) { + $header->setHeaderIcon($this->headerIcon); } $href = $this->getHref(); diff --git a/src/view/phui/calendar/PHUICalendarDayView.php b/src/view/phui/calendar/PHUICalendarDayView.php index 34299e39c3..d71554abe5 100644 --- a/src/view/phui/calendar/PHUICalendarDayView.php +++ b/src/view/phui/calendar/PHUICalendarDayView.php @@ -278,7 +278,7 @@ final class PHUICalendarDayView extends AphrontView { ->addClass('calendar-day-view-sidebar'); $list = id(new PHUICalendarListView()) - ->setUser($this->user) + ->setUser($this->getViewer()) ->setView('day'); if (count($events) == 0) { @@ -304,7 +304,7 @@ final class PHUICalendarDayView extends AphrontView { $box_start_time = clone $display_start_day; - $today_time = PhabricatorTime::getTodayMidnightDateTime($this->user); + $today_time = PhabricatorTime::getTodayMidnightDateTime($this->getViewer()); $tomorrow_time = clone $today_time; $tomorrow_time->modify('+1 day'); @@ -437,7 +437,7 @@ final class PHUICalendarDayView extends AphrontView { } private function getDateTime() { - $user = $this->user; + $user = $this->getViewer(); $timezone = new DateTimeZone($user->getTimezoneIdentifier()); $day = $this->day; diff --git a/src/view/phui/calendar/PHUICalendarMonthView.php b/src/view/phui/calendar/PHUICalendarMonthView.php index 5efa4c1059..d40736494e 100644 --- a/src/view/phui/calendar/PHUICalendarMonthView.php +++ b/src/view/phui/calendar/PHUICalendarMonthView.php @@ -51,9 +51,7 @@ final class PHUICalendarMonthView extends AphrontView { } public function render() { - if (empty($this->user)) { - throw new PhutilInvalidStateException('setUser'); - } + $viewer = $this->getViewer(); $events = msort($this->events, 'getEpochStart'); $days = $this->getDatesInMonth(); @@ -93,7 +91,7 @@ final class PHUICalendarMonthView extends AphrontView { $counter = 0; $list = new PHUICalendarListView(); - $list->setUser($this->user); + $list->setViewer($viewer); foreach ($all_day_events as $item) { if ($counter <= $max_daily) { $list->addEvent($item); @@ -495,9 +493,9 @@ final class PHUICalendarMonthView extends AphrontView { * @return list List of DateTimes, one for each day. */ private function getDatesInMonth() { - $user = $this->user; + $viewer = $this->getViewer(); - $timezone = new DateTimeZone($user->getTimezoneIdentifier()); + $timezone = new DateTimeZone($viewer->getTimezoneIdentifier()); $month = $this->month; $year = $this->year; @@ -575,7 +573,7 @@ final class PHUICalendarMonthView extends AphrontView { } private function getWeekStartAndEnd() { - $preferences = $this->user->loadPreferences(); + $preferences = $this->getViewer()->loadPreferences(); $pref_week_start = PhabricatorUserPreferences::PREFERENCE_WEEK_START_DAY; $week_start = $preferences->getPreference($pref_week_start, 0); @@ -585,7 +583,7 @@ final class PHUICalendarMonthView extends AphrontView { } private function getDateTime() { - $user = $this->user; + $user = $this->getViewer(); $timezone = new DateTimeZone($user->getTimezoneIdentifier()); $month = $this->month; diff --git a/webroot/rsrc/css/application/differential/phui-inline-comment.css b/webroot/rsrc/css/application/differential/phui-inline-comment.css index 0055f25699..f93b2c8d6c 100644 --- a/webroot/rsrc/css/application/differential/phui-inline-comment.css +++ b/webroot/rsrc/css/application/differential/phui-inline-comment.css @@ -61,7 +61,7 @@ /* Tighten up spacing on replies */ .differential-inline-comment.inline-comment-is-reply { - margin-top: -4px; + margin-top: -12px; } .differential-inline-comment .inline-head-right { diff --git a/webroot/rsrc/css/application/herald/herald.css b/webroot/rsrc/css/application/herald/herald.css index 26cb9cfd71..17b09a1a86 100644 --- a/webroot/rsrc/css/application/herald/herald.css +++ b/webroot/rsrc/css/application/herald/herald.css @@ -44,7 +44,7 @@ .herald-list-description { color: {$bluetext}; font-weight: bold; - padding: 8px 0; + padding: 12px 0; } .herald-list-icon { @@ -52,6 +52,6 @@ } .herald-list-item { - padding-bottom: 20px; + padding-bottom: 4px; color: {$darkbluetext}; } diff --git a/webroot/rsrc/css/font/phui-font-icon-base.css b/webroot/rsrc/css/font/phui-font-icon-base.css index 2ddd423d3c..23194b3bda 100644 --- a/webroot/rsrc/css/font/phui-font-icon-base.css +++ b/webroot/rsrc/css/font/phui-font-icon-base.css @@ -148,7 +148,7 @@ } .phui-icon-view.lightgreytext, .phui-icon-view.grey { - color: {$lightgreytext}; + color: rgba({$alphagrey},0.3); } /* Hovers */ diff --git a/webroot/rsrc/css/phui/phui-box.css b/webroot/rsrc/css/phui/phui-box.css index 7eaca8b741..1d42cde8fa 100644 --- a/webroot/rsrc/css/phui/phui-box.css +++ b/webroot/rsrc/css/phui/phui-box.css @@ -66,6 +66,14 @@ padding: 0; } +.phui-box.phui-box-blue-property .phui-header-header { + text-transform: uppercase; +} + +.phui-box.phui-box-blue-property .phui-header-header .phui-header-icon { + margin-right: 6px; +} + .phui-box.phui-box-blue-property .phui-header-action-link { margin-top: 0; margin-bottom: 0; diff --git a/webroot/rsrc/css/phui/phui-curtain-view.css b/webroot/rsrc/css/phui/phui-curtain-view.css new file mode 100644 index 0000000000..b33369abe0 --- /dev/null +++ b/webroot/rsrc/css/phui/phui-curtain-view.css @@ -0,0 +1,41 @@ +/** + * @provides phui-curtain-view-css + */ + +.phui-curtain-panel { + padding: 16px 0; + margin: 0 4px; +} + +.device .phui-curtain-panel { + padding: 8px 0; + margin: 0; +} + +.device-desktop .phui-curtain-panel { + border-top: 1px solid rgba({$alphablue}, .1); +} + +.phui-curtain-panel-header { + padding: 0 0 4px; + color: {$bluetext}; + font-weight: bold; +} + +.phui-curtain-panel-body { + padding: 4px 0 0; +} + +.device .phui-curtain-panel-body { + padding: 0; +} + +/* Project tags */ + +.phui-curtain-panel-body .phabricator-handle-tag-list-item { + line-height: 21px; +} + +.phui-side-column .phui-curtain-panel-body .phui-tag-view { + white-space: pre-wrap; +} diff --git a/webroot/rsrc/css/phui/phui-head-thing.css b/webroot/rsrc/css/phui/phui-head-thing.css index bce67ef387..b82c9321a3 100644 --- a/webroot/rsrc/css/phui/phui-head-thing.css +++ b/webroot/rsrc/css/phui/phui-head-thing.css @@ -10,7 +10,7 @@ padding-left: 32px; } -.device-phone .phui-head-thing-view { +.device-phone .phui-two-column-subheader .phui-head-thing-view { min-height: 24px; height: auto; line-height: inherit; diff --git a/webroot/rsrc/css/phui/phui-header-view.css b/webroot/rsrc/css/phui/phui-header-view.css index cd4b7b1238..a61a73fc88 100644 --- a/webroot/rsrc/css/phui/phui-header-view.css +++ b/webroot/rsrc/css/phui/phui-header-view.css @@ -176,6 +176,11 @@ body .phui-header-shell.phui-bleed-header } .phui-header-subheader .phui-icon-view { + margin-right: 4px; +} + +.phui-header-subheader .phui-tag-view .phui-icon-view, +.phui-header-subheader .policy-header-callout .phui-icon-view { display: inline-block; margin: -2px 4px -2px 0; font-size: 15px; @@ -187,11 +192,21 @@ body .phui-header-shell.phui-bleed-header } .policy-header-callout, -.phui-header-subheader .phui-header-status { +.phui-header-subheader .phui-tag-core { padding: 3px 9px; border-radius: 3px; - background: rgba({$alphablue}, 0.08); + background: rgba({$alphablue}, 0.1); margin-right: 8px; + -webkit-font-smoothing: auto; + border-color: transparent; +} + + +.phui-header-subheader .phui-tag-view, +.phui-header-subheader .phui-tag-type-shade .phui-tag-core { + border: none; + font-weight: normal; + -webkit-font-smoothing: auto; } .policy-header-callout.policy-adjusted-weaker { @@ -221,26 +236,6 @@ body .phui-header-shell.phui-bleed-header color: {$sh-orangetext}; } -.phui-header-subheader .phui-header-status-dark { - color: {$sh-indigotext}; - background: {$sh-indigobackground}; - margin-right: 8px; -} - -.phui-header-subheader .phui-header-status-dark .phui-icon-view { - color: {$sh-indigotext}; -} - -.phui-header-subheader .phui-header-status-red { - color: {$sh-redtext}; - background: {$sh-redbackground}; -} - -.phui-header-subheader .phui-header-status-green { - color: {$sh-greentext}; - background: {$sh-greenbackground}; -} - .phui-header-action-links .phui-mobile-menu { display: none; } @@ -258,12 +253,9 @@ body .phui-header-shell.phui-bleed-header float: right; } -.phui-header-action-list li.phui-header-action-icon { +.phui-header-action-list .phui-header-action-item .phui-icon-view { height: 18px; width: 16px; -} - -.phui-header-action-list .phui-header-action-icon .phui-icon-view { font-size: 16px; line-height: 20px; display: block; diff --git a/webroot/rsrc/css/phui/phui-property-list-view.css b/webroot/rsrc/css/phui/phui-property-list-view.css index 3558bf7a9e..52e3ee0944 100644 --- a/webroot/rsrc/css/phui/phui-property-list-view.css +++ b/webroot/rsrc/css/phui/phui-property-list-view.css @@ -125,7 +125,7 @@ } .phui-property-list-text-content { - padding: 12px 4px; + padding: 16px 4px; overflow: hidden; } diff --git a/webroot/rsrc/css/phui/phui-tag-view.css b/webroot/rsrc/css/phui/phui-tag-view.css index a68ed19484..729c4719e5 100644 --- a/webroot/rsrc/css/phui/phui-tag-view.css +++ b/webroot/rsrc/css/phui/phui-tag-view.css @@ -257,6 +257,26 @@ a.phui-tag-view:hover.phui-tag-shade-blue .phui-tag-core, border-color: {$sh-blueborder}; } +/* - Sky ------------------------------------------------------------------- */ + +.phui-tag-shade-sky .phui-tag-core, +.jx-tokenizer-token.sky { + background: #E0F0FA; + border-color: {$sh-lightblueborder}; + color: {$sh-bluetext}; +} + +.phui-tag-shade-sky .phui-icon-view, +.jx-tokenizer-token.sky .phui-icon-view, +.jx-tokenizer-token.sky .jx-tokenizer-x { + color: {$sh-blueicon}; +} + +a.phui-tag-view:hover.phui-tag-shade-sky .phui-tag-core, +.jx-tokenizer-token.sky:hover { + border-color: {$sh-blueborder}; +} + /* - Indigo ----------------------------------------------------------------- */ .phui-tag-shade-indigo .phui-tag-core, diff --git a/webroot/rsrc/css/phui/phui-timeline-view.css b/webroot/rsrc/css/phui/phui-timeline-view.css index d69df93953..ed4f77aa61 100644 --- a/webroot/rsrc/css/phui/phui-timeline-view.css +++ b/webroot/rsrc/css/phui/phui-timeline-view.css @@ -4,9 +4,9 @@ .phui-timeline-view { padding: 0 16px; - background-image: url('/rsrc/image/BFCFDA.png'); + background-image: url('/rsrc/image/d5d8e1.png'); background-repeat: repeat-y; - background-position: 94px; + background-position: 96px; } .device .phui-timeline-view { @@ -23,8 +23,8 @@ } .phui-timeline-major-event .phui-timeline-group { - border-left: 1px solid {$lightblueborder}; - border-right: 1px solid {$lightblueborder}; + border-left: 1px solid {$timeline}; + border-right: 1px solid {$timeline}; border-radius: 3px; } @@ -34,7 +34,7 @@ } .device-desktop .phui-timeline-event-view.phui-timeline-minor-event { - margin-left: 65px; + margin-left: 67px; } .device-desktop .phui-timeline-spacer { @@ -50,7 +50,7 @@ } .device-desktop .phui-timeline-wedge { - border-bottom: 1px solid {$lightblueborder}; + border-bottom: 1px solid {$timeline}; position: absolute; width: 12px; } @@ -61,25 +61,25 @@ } .phui-timeline-major-event .phui-timeline-content { - border-top: 1px solid {$lightblueborder}; - border-bottom: 1px solid {$lightblueborder}; + border-top: 1px solid {$timeline}; + border-bottom: 1px solid {$timeline}; border-radius: 3px; } .phui-timeline-title { - line-height: 22px; + line-height: 24px; min-height: 19px; position: relative; - color: {$bluetext}; + color: {$greytext}; } .phui-timeline-minor-event .phui-timeline-title { - padding: 4px 8px 4px 33px; + padding: 1px 8px 4px 33px; } .phui-timeline-title a { font-weight: bold; - color: {$darkbluetext}; + color: {$darkgreytext}; } .device-desktop .phui-timeline-wedge { @@ -91,7 +91,7 @@ } .device-desktop .phui-timeline-minor-event .phui-timeline-wedge { - top: 13px; + top: 12px; left: -18px; width: 20px; } @@ -100,7 +100,6 @@ background-repeat: no-repeat; position: absolute; border-radius: 3px; - box-shadow: {$borderinset}; background-size: 100%; display: block; } @@ -113,22 +112,29 @@ } .device-desktop .phui-timeline-minor-event .phui-timeline-image { - width: 28px; - height: 28px; - background-size: 28px auto; + width: 26px; + height: 26px; + background-size: 26px auto; left: -41px; } .phui-timeline-major-event .phui-timeline-title { - background: {$lightbluebackground}; + background: {$lightgreybackground}; min-height: 22px; border-top-right-radius: 3px; + border-top-left-radius: 3px; } -.phui-timeline-title + .phui-timeline-title { +.phui-timeline-major-event .phui-timeline-title + .phui-timeline-title { border-radius: 0; + padding-top: 0; } +.phui-timeline-major-event .phui-timeline-title + .phui-timeline-title + .phui-timeline-icon-fill { + margin-top: 0; + } + .phui-timeline-title { padding: 5px 8px; overflow-x: auto; @@ -136,7 +142,7 @@ } .phui-timeline-title-with-icon { - padding-left: 38px; + padding-left: 36px; } .phui-timeline-title-with-menu { @@ -170,9 +176,10 @@ .phui-timeline-major-event .phui-timeline-content .phui-timeline-core-content { - padding: 16px 12px; + padding: 16px; line-height: 18px; background: #fff; + border-top: 1px solid rgba({$alphablue},.1); border-bottom-left-radius: 3px; border-bottom-right-radius: 3px; } @@ -203,52 +210,49 @@ border-width: 0; } -.phui-timeline-spacer.phui-timeline-spacer-bold { - border-bottom: 4px solid {$lightblueborder}; - margin: 0; -} - -.phui-timeline-spacer-bold + .phui-timeline-spacer { - background-color: #ebecee; -} - .phui-timeline-icon-fill { position: absolute; - width: 32px; - height: 32px; - background-color: {$lightblueborder}; + width: 34px; + height: 34px; top: 0; left: 0; text-align: center; } -.phui-icon-view.phui-timeline-icon:before { - font-size: 15px; +.phui-timeline-icon { + color: {$sh-blueicon}; } -.phui-timeline-minor-event .phui-timeline-icon-fill { - height: 28px; - width: 28px; +.phui-icon-view.phui-timeline-icon { + font-size: 14px; +} + +.phui-timeline-icon-fill { + height: 26px; + width: 26px; border-radius: 3px; + background-color: #E6E9F1; +} + +.phui-timeline-major-event .phui-timeline-icon-fill { + margin: 4px; } .phui-timeline-icon-fill .phui-timeline-icon { - margin-top: 8px; -} - -.phui-timeline-minor-event .phui-timeline-icon-fill .phui-timeline-icon { - margin-top: 7px; + margin-top: 6px; } .phui-timeline-extra, .phui-timeline-extra .phabricator-content-source-view { - font-size: {$smallestfontsize}; + font-size: {$smallerfontsize}; font-weight: normal; - color: {$lightbluetext}; + color: {$lightgreytext}; + margin-left: 8px; } .phui-timeline-title .phui-timeline-extra a { font-weight: normal; + color: {$lightgreytext}; } .device-desktop .phui-timeline-extra { @@ -267,6 +271,10 @@ margin: 0; } +.phui-timeline-icon-fill.fill-has-color .phui-icon-view { + color: #fff; +} + .phui-timeline-icon-fill-red { background-color: {$red}; } @@ -304,7 +312,7 @@ } .phui-timeline-icon-fill-black { - background-color: #333; + background-color: #000; } .phui-timeline-shell.anchor-target { @@ -343,7 +351,7 @@ .phui-timeline-title .phui-timeline-extra-information a { font-weight: normal; - color: {$bluetext}; + color: {$greytext}; } .phui-timeline-comment-actions .phui-icon-view { @@ -359,11 +367,11 @@ right: 3px; top: 6px; width: 28px; - height: 22px; + height: 24px; text-align: center; line-height: 22px; - font-size: 15px; - border-left: 1px solid {$lightblueborder}; + font-size: 16px; + border-left: 1px solid {$thinblueborder}; } .phui-timeline-menu:focus { @@ -379,11 +387,13 @@ a.phui-timeline-menu .phui-icon-view { } .device-desktop a.phui-timeline-menu:hover .phui-icon-view { - color: {$darkgreytext}; + color: {$sky}; } .phui-timeline-menu.phuix-dropdown-open { - background: {$hovergrey}; + background: rgba({$alphablue},0.1); + border: none; + border-radius: 3px; } .phui-timeline-view + .phui-object-box { diff --git a/webroot/rsrc/css/phui/phui-two-column-view.css b/webroot/rsrc/css/phui/phui-two-column-view.css index 6c36aa8bd1..090fafb63a 100644 --- a/webroot/rsrc/css/phui/phui-two-column-view.css +++ b/webroot/rsrc/css/phui/phui-two-column-view.css @@ -8,6 +8,10 @@ margin-bottom: 24px; } +.device .phui-two-column-view .phui-two-column-header { + margin-bottom: 12px; +} + .phui-two-column-view.with-subheader .phui-two-column-header { margin-bottom: 0; } @@ -83,7 +87,7 @@ .phui-two-column-view .phui-timeline-view { padding: 0; - background-position: 78px; + background-position: 80px; } .phui-two-column-view .phui-main-column .phui-object-box + .phui-timeline-view { @@ -99,6 +103,14 @@ margin: 0; } +.phui-main-column > .phui-timeline-view:first-child { + border-top: 1px solid {$thinblueborder}; +} + +.device-phone .phui-main-column .phui-timeline-older-transactions-are-hidden { + margin: 0; +} + /* Main Column Properties */ .device-desktop .phui-main-column .phui-property-list-key { @@ -131,34 +143,6 @@ border: 1px solid rgba({$alphablue}, .2); } -.phui-two-column-properties .phui-property-list-stacked - .phui-property-list-properties .phui-property-list-key { - margin: 4px 0 8px 0; - padding: 20px 4px 0; - border-top: 1px solid rgba({$alphablue}, .2); -} - -.phui-two-column-properties .phui-property-list-stacked - .phui-property-list-properties .phui-property-list-value { - margin: 0 0 20px 0; - padding: 0 4px; -} - -.device-desktop .phui-two-column-properties .phui-property-list-container { - padding: 0; -} - -.device .phui-two-column-properties .phui-property-list-stacked - .phui-property-list-properties .phui-property-list-key { - margin: 12px 0 4px 0; - padding: 0; - border: none; -} - -.device .phui-two-column-properties .phui-property-list-container { - padding: 0 0 12px 0; -} - .device .phui-two-column-content .phui-two-column-properties.phui-object-box { padding: 0 12px; } diff --git a/webroot/rsrc/image/d5d8e1.png b/webroot/rsrc/image/d5d8e1.png new file mode 100644 index 0000000000..352aeb61dc Binary files /dev/null and b/webroot/rsrc/image/d5d8e1.png differ diff --git a/webroot/rsrc/js/application/projects/WorkboardColumn.js b/webroot/rsrc/js/application/projects/WorkboardColumn.js index 738cf151c2..ced39dcd73 100644 --- a/webroot/rsrc/js/application/projects/WorkboardColumn.js +++ b/webroot/rsrc/js/application/projects/WorkboardColumn.js @@ -221,6 +221,7 @@ JX.install('WorkboardColumn', { var board = this.getBoard(); var points = {}; + var count = 0; for (var phid in cards) { var card = cards[phid]; @@ -238,6 +239,8 @@ JX.install('WorkboardColumn', { } points[status] += card_points; } + + count++; } var total_points = 0; @@ -254,6 +257,10 @@ JX.install('WorkboardColumn', { display_value = total_points; } + if (board.getPointsEnabled()) { + display_value = count + ' | ' + display_value; + } + var over_limit = ((limit !== null) && (total_points > limit)); var content_node = this.getPointsContentNode();