diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php index e3689134b5..ffade93324 100644 --- a/src/__celerity_resource_map__.php +++ b/src/__celerity_resource_map__.php @@ -1,541 +1,541 @@ array( 'uri' => '/res/056b0c12/rsrc/css/aphront/dark-console.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/aphront/dark-console.css', ), 'aphront-dialog-view-css' => array( - 'uri' => '/res/a05107ae/rsrc/css/aphront/dialog-view.css', + 'uri' => '/res/c8324e86/rsrc/css/aphront/dialog-view.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/aphront/dialog-view.css', ), 'aphront-error-view-css' => array( 'uri' => '/res/19b27527/rsrc/css/aphront/error-view.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/aphront/error-view.css', ), 'aphront-form-view-css' => array( 'uri' => '/res/8aaef437/rsrc/css/aphront/form-view.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/aphront/form-view.css', ), 'aphront-panel-view-css' => array( 'uri' => '/res/63672373/rsrc/css/aphront/panel-view.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/aphront/panel-view.css', ), 'aphront-request-failure-view-css' => array( 'uri' => '/res/97b8337a/rsrc/css/aphront/request-failure-view.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/aphront/request-failure-view.css', ), 'aphront-side-nav-view-css' => array( 'uri' => '/res/09b7eb85/rsrc/css/aphront/side-nav-view.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/aphront/side-nav-view.css', ), 'aphront-table-view-css' => array( 'uri' => '/res/6a70f0f0/rsrc/css/aphront/table-view.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/aphront/table-view.css', ), 'aphront-tokenizer-control-css' => array( 'uri' => '/res/a3d23074/rsrc/css/aphront/tokenizer.css', 'type' => 'css', 'requires' => array( 0 => 'aphront-typeahead-control-css', ), 'disk' => '/rsrc/css/aphront/tokenizer.css', ), 'aphront-typeahead-control-css' => array( 'uri' => '/res/928df9f0/rsrc/css/aphront/typeahead.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/aphront/typeahead.css', ), 'phabricator-standard-page-view' => array( 'uri' => '/res/4ebe4b10/rsrc/css/application/base/standard-page-view.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/application/base/standard-page-view.css', ), 'differential-revision-add-comment-css' => array( 'uri' => '/res/aaae14d3/rsrc/css/application/differential/add-comment.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/application/differential/add-comment.css', ), 'differential-changeset-view-css' => array( 'uri' => '/res/f26ca6f9/rsrc/css/application/differential/changeset-view.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/application/differential/changeset-view.css', ), 'differential-core-view-css' => array( 'uri' => '/res/525d1a12/rsrc/css/application/differential/core.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/application/differential/core.css', ), 'differential-revision-comment-list-css' => array( 'uri' => '/res/10b9a829/rsrc/css/application/differential/revision-comment-list.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/application/differential/revision-comment-list.css', ), 'differential-revision-comment-css' => array( 'uri' => '/res/b271baaf/rsrc/css/application/differential/revision-comment.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/application/differential/revision-comment.css', ), 'differential-revision-detail-css' => array( 'uri' => '/res/230a67c6/rsrc/css/application/differential/revision-detail.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/application/differential/revision-detail.css', ), 'differential-revision-history-css' => array( 'uri' => '/res/755f3da3/rsrc/css/application/differential/revision-history.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/application/differential/revision-history.css', ), 'differential-table-of-contents-css' => array( 'uri' => '/res/e68f6f05/rsrc/css/application/differential/table-of-contents.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/application/differential/table-of-contents.css', ), 'phabricator-directory-css' => array( 'uri' => '/res/6a000601/rsrc/css/application/directory/phabricator-directory.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/application/directory/phabricator-directory.css', ), 'mainphest-task-detail-css' => array( 'uri' => '/res/e5f3beca/rsrc/css/application/maniphest/task-detail.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/application/maniphest/task-detail.css', ), 'maniphest-task-summary-css' => array( 'uri' => '/res/10d5ec2e/rsrc/css/application/maniphest/task-summary.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/application/maniphest/task-summary.css', ), 'maniphest-transaction-detail-css' => array( 'uri' => '/res/658912c5/rsrc/css/application/maniphest/transaction-detail.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/application/maniphest/transaction-detail.css', ), 'phabricator-object-selector-css' => array( - 'uri' => '/res/270ce107/rsrc/css/application/objectselector/object-selector.css', + 'uri' => '/res/52a7e289/rsrc/css/application/objectselector/object-selector.css', 'type' => 'css', 'requires' => array( 0 => 'aphront-dialog-view-css', ), 'disk' => '/rsrc/css/application/objectselector/object-selector.css', ), 'phabricator-core-buttons-css' => array( 'uri' => '/res/ee35ffe1/rsrc/css/core/buttons.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/core/buttons.css', ), 'phabricator-core-css' => array( 'uri' => '/res/6eebb99b/rsrc/css/core/core.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/core/core.css', ), 'phabricator-remarkup-css' => array( 'uri' => '/res/786989c3/rsrc/css/core/remarkup.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/core/remarkup.css', ), 'syntax-highlighting-css' => array( 'uri' => '/res/fb673ece/rsrc/css/core/syntax.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/core/syntax.css', ), 'javelin-behavior-dark-console' => array( 'uri' => '/res/020b0265/rsrc/js/application/core/behavior-dark-console.js', 'type' => 'js', 'requires' => array( ), 'disk' => '/rsrc/js/application/core/behavior-dark-console.js', ), 'javelin-behavior-phabricator-object-selector' => array( - 'uri' => '/res/7f7eda6a/rsrc/js/application/core/behavior-object-selector.js', + 'uri' => '/res/e849ced6/rsrc/js/application/core/behavior-object-selector.js', 'type' => 'js', 'requires' => array( 0 => 'javelin-lib-dev', ), 'disk' => '/rsrc/js/application/core/behavior-object-selector.js', ), 'javelin-behavior-aphront-basic-tokenizer' => array( 'uri' => '/res/8317d761/rsrc/js/application/core/behavior-tokenizer.js', 'type' => 'js', 'requires' => array( 0 => 'javelin-lib-dev', ), 'disk' => '/rsrc/js/application/core/behavior-tokenizer.js', ), 'javelin-behavior-workflow' => array( 'uri' => '/res/15446e7e/rsrc/js/application/core/behavior-workflow.js', 'type' => 'js', 'requires' => array( 0 => 'javelin-lib-dev', ), 'disk' => '/rsrc/js/application/core/behavior-workflow.js', ), 'javelin-behavior-differential-add-reviewers' => array( 'uri' => '/res/330154e4/rsrc/js/application/differential/behavior-add-reviewers.js', 'type' => 'js', 'requires' => array( 0 => 'javelin-lib-dev', ), 'disk' => '/rsrc/js/application/differential/behavior-add-reviewers.js', ), 'javelin-behavior-differential-feedback-preview' => array( 'uri' => '/res/8695d8b8/rsrc/js/application/differential/behavior-comment-preview.js', 'type' => 'js', 'requires' => array( 0 => 'javelin-lib-dev', ), 'disk' => '/rsrc/js/application/differential/behavior-comment-preview.js', ), 'javelin-behavior-differential-diff-radios' => array( 'uri' => '/res/fdeb3823/rsrc/js/application/differential/behavior-diff-radios.js', 'type' => 'js', 'requires' => array( 0 => 'javelin-lib-dev', ), 'disk' => '/rsrc/js/application/differential/behavior-diff-radios.js', ), 'javelin-behavior-differential-edit-inline-comments' => array( 'uri' => '/res/74747b2e/rsrc/js/application/differential/behavior-edit-inline-comments.js', 'type' => 'js', 'requires' => array( 0 => 'javelin-lib-dev', ), 'disk' => '/rsrc/js/application/differential/behavior-edit-inline-comments.js', ), 'javelin-behavior-differential-populate' => array( 'uri' => '/res/a13dcd7e/rsrc/js/application/differential/behavior-populate.js', 'type' => 'js', 'requires' => array( 0 => 'javelin-lib-dev', ), 'disk' => '/rsrc/js/application/differential/behavior-populate.js', ), 'javelin-behavior-differential-show-all-comments' => array( 'uri' => '/res/2a3592b8/rsrc/js/application/differential/behavior-show-all-comments.js', 'type' => 'js', 'requires' => array( 0 => 'javelin-lib-dev', ), 'disk' => '/rsrc/js/application/differential/behavior-show-all-comments.js', ), 'javelin-behavior-differential-show-more' => array( 'uri' => '/res/ea998002/rsrc/js/application/differential/behavior-show-more.js', 'type' => 'js', 'requires' => array( 0 => 'javelin-lib-dev', ), 'disk' => '/rsrc/js/application/differential/behavior-show-more.js', ), 'javelin-behavior-maniphest-transaction-controls' => array( 'uri' => '/res/fc6a8722/rsrc/js/application/maniphest/behavior-transaction-controls.js', 'type' => 'js', 'requires' => array( 0 => 'javelin-lib-dev', ), 'disk' => '/rsrc/js/application/maniphest/behavior-transaction-controls.js', ), 'javelin-magical-init' => array( 'uri' => '/res/76614f84/rsrc/js/javelin/init.dev.js', 'type' => 'js', 'requires' => array( ), 'disk' => '/rsrc/js/javelin/init.dev.js', ), 'javelin-init-prod' => array( 'uri' => '/res/1267c868/rsrc/js/javelin/init.min.js', 'type' => 'js', 'requires' => array( ), 'disk' => '/rsrc/js/javelin/init.min.js', ), 'javelin-lib-dev' => array( 'uri' => '/res/a0e7a5e9/rsrc/js/javelin/javelin.dev.js', 'type' => 'js', 'requires' => array( ), 'disk' => '/rsrc/js/javelin/javelin.dev.js', ), 'javelin-lib-prod' => array( 'uri' => '/res/2f2b3b2e/rsrc/js/javelin/javelin.min.js', 'type' => 'js', 'requires' => array( ), 'disk' => '/rsrc/js/javelin/javelin.min.js', ), 'javelin-typeahead-dev' => array( 'uri' => '/res/6de6ae59/rsrc/js/javelin/typeahead.dev.js', 'type' => 'js', 'requires' => array( ), 'disk' => '/rsrc/js/javelin/typeahead.dev.js', ), 'javelin-typeahead-prod' => array( 'uri' => '/res/69d5fad1/rsrc/js/javelin/typeahead.min.js', 'type' => 'js', 'requires' => array( ), 'disk' => '/rsrc/js/javelin/typeahead.min.js', ), 'javelin-workflow-dev' => array( 'uri' => '/res/c6b17f93/rsrc/js/javelin/workflow.dev.js', 'type' => 'js', 'requires' => array( ), 'disk' => '/rsrc/js/javelin/workflow.dev.js', ), 'javelin-workflow-prod' => array( 'uri' => '/res/b758e0a0/rsrc/js/javelin/workflow.min.js', 'type' => 'js', 'requires' => array( ), 'disk' => '/rsrc/js/javelin/workflow.min.js', ), ), array ( 'packages' => array ( - '4f907a28' => + 'aa43d409' => array ( 'name' => 'core.pkg.css', 'symbols' => array ( 0 => 'phabricator-core-css', 1 => 'phabricator-core-buttons-css', 2 => 'phabricator-standard-page-view', 3 => 'aphront-dialog-view-css', 4 => 'aphront-form-view-css', 5 => 'aphront-panel-view-css', 6 => 'aphront-side-nav-view-css', 7 => 'aphront-table-view-css', 8 => 'aphront-tokenizer-control-css', 9 => 'aphront-typeahead-control-css', 10 => 'phabricator-directory-css', 11 => 'phabricator-remarkup-css', 12 => 'syntax-highlighting-css', ), - 'uri' => '/res/pkg/4f907a28/core.pkg.css', + 'uri' => '/res/pkg/aa43d409/core.pkg.css', 'type' => 'css', ), '2525bbc7' => array ( 'name' => 'differential.pkg.css', 'symbols' => array ( 0 => 'differential-core-view-css', 1 => 'differential-changeset-view-css', 2 => 'differential-revision-detail-css', 3 => 'differential-revision-history-css', 4 => 'differential-table-of-contents-css', 5 => 'differential-revision-comment-css', 6 => 'differential-revision-add-comment-css', 7 => 'differential-revision-comment-list-css', ), 'uri' => '/res/pkg/2525bbc7/differential.pkg.css', 'type' => 'css', ), '30d594cf' => array ( 'name' => 'differential.pkg.js', 'symbols' => array ( 0 => 'javelin-behavior-differential-feedback-preview', 1 => 'javelin-behavior-differential-edit-inline-comments', 2 => 'javelin-behavior-differential-populate', 3 => 'javelin-behavior-differential-show-more', 4 => 'javelin-behavior-differential-diff-radios', ), 'uri' => '/res/pkg/30d594cf/differential.pkg.js', 'type' => 'js', ), ), 'reverse' => array ( - 'phabricator-core-css' => '4f907a28', - 'phabricator-core-buttons-css' => '4f907a28', - 'phabricator-standard-page-view' => '4f907a28', - 'aphront-dialog-view-css' => '4f907a28', - 'aphront-form-view-css' => '4f907a28', - 'aphront-panel-view-css' => '4f907a28', - 'aphront-side-nav-view-css' => '4f907a28', - 'aphront-table-view-css' => '4f907a28', - 'aphront-tokenizer-control-css' => '4f907a28', - 'aphront-typeahead-control-css' => '4f907a28', - 'phabricator-directory-css' => '4f907a28', - 'phabricator-remarkup-css' => '4f907a28', - 'syntax-highlighting-css' => '4f907a28', + 'phabricator-core-css' => 'aa43d409', + 'phabricator-core-buttons-css' => 'aa43d409', + 'phabricator-standard-page-view' => 'aa43d409', + 'aphront-dialog-view-css' => 'aa43d409', + 'aphront-form-view-css' => 'aa43d409', + 'aphront-panel-view-css' => 'aa43d409', + 'aphront-side-nav-view-css' => 'aa43d409', + 'aphront-table-view-css' => 'aa43d409', + 'aphront-tokenizer-control-css' => 'aa43d409', + 'aphront-typeahead-control-css' => 'aa43d409', + 'phabricator-directory-css' => 'aa43d409', + 'phabricator-remarkup-css' => 'aa43d409', + 'syntax-highlighting-css' => 'aa43d409', 'differential-core-view-css' => '2525bbc7', 'differential-changeset-view-css' => '2525bbc7', 'differential-revision-detail-css' => '2525bbc7', 'differential-revision-history-css' => '2525bbc7', 'differential-table-of-contents-css' => '2525bbc7', 'differential-revision-comment-css' => '2525bbc7', 'differential-revision-add-comment-css' => '2525bbc7', 'differential-revision-comment-list-css' => '2525bbc7', 'javelin-behavior-differential-feedback-preview' => '30d594cf', 'javelin-behavior-differential-edit-inline-comments' => '30d594cf', 'javelin-behavior-differential-populate' => '30d594cf', 'javelin-behavior-differential-show-more' => '30d594cf', 'javelin-behavior-differential-diff-radios' => '30d594cf', ), )); diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index cbc4666dcf..31cc63253e 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1,486 +1,488 @@ array( 'Aphront400Response' => 'aphront/response/400', 'Aphront404Response' => 'aphront/response/404', 'AphrontAjaxResponse' => 'aphront/response/ajax', 'AphrontApplicationConfiguration' => 'aphront/applicationconfiguration', 'AphrontController' => 'aphront/controller', 'AphrontDatabaseConnection' => 'storage/connection/base', 'AphrontDefaultApplicationConfiguration' => 'aphront/default/configuration', 'AphrontDefaultApplicationController' => 'aphront/default/controller', 'AphrontDialogResponse' => 'aphront/response/dialog', 'AphrontDialogView' => 'view/dialog', 'AphrontErrorView' => 'view/form/error', 'AphrontException' => 'aphront/exception/base', 'AphrontFileResponse' => 'aphront/response/file', 'AphrontFormCheckboxControl' => 'view/form/control/checkbox', 'AphrontFormControl' => 'view/form/control/base', 'AphrontFormDividerControl' => 'view/form/control/divider', 'AphrontFormFileControl' => 'view/form/control/file', 'AphrontFormMarkupControl' => 'view/form/control/markup', 'AphrontFormPasswordControl' => 'view/form/control/password', 'AphrontFormRecaptchaControl' => 'view/form/control/recaptcha', 'AphrontFormSelectControl' => 'view/form/control/select', 'AphrontFormStaticControl' => 'view/form/control/static', 'AphrontFormSubmitControl' => 'view/form/control/submit', 'AphrontFormTextAreaControl' => 'view/form/control/textarea', 'AphrontFormTextControl' => 'view/form/control/text', 'AphrontFormTokenizerControl' => 'view/form/control/tokenizer', 'AphrontFormView' => 'view/form/base', 'AphrontMySQLDatabaseConnection' => 'storage/connection/mysql', 'AphrontNullView' => 'view/null', 'AphrontPageView' => 'view/page/base', 'AphrontPanelView' => 'view/layout/panel', 'AphrontQueryConnectionException' => 'storage/exception/connection', 'AphrontQueryConnectionLostException' => 'storage/exception/connectionlost', 'AphrontQueryCountException' => 'storage/exception/count', 'AphrontQueryDuplicateKeyException' => 'storage/exception/duplicatekey', 'AphrontQueryException' => 'storage/exception/base', 'AphrontQueryObjectMissingException' => 'storage/exception/objectmissing', 'AphrontQueryParameterException' => 'storage/exception/parameter', 'AphrontQueryRecoverableException' => 'storage/exception/recoverable', 'AphrontRedirectException' => 'aphront/exception/redirect', 'AphrontRedirectResponse' => 'aphront/response/redirect', 'AphrontRequest' => 'aphront/request', 'AphrontRequestFailureView' => 'view/page/failure', 'AphrontResponse' => 'aphront/response/base', 'AphrontSideNavView' => 'view/layout/sidenav', 'AphrontTableView' => 'view/control/table', 'AphrontURIMapper' => 'aphront/mapper', 'AphrontView' => 'view/base', 'AphrontWebpageResponse' => 'aphront/response/webpage', 'CelerityAPI' => 'infrastructure/celerity/api', 'CelerityResourceController' => 'infrastructure/celerity/controller', 'CelerityResourceMap' => 'infrastructure/celerity/map', 'CelerityStaticResourceResponse' => 'infrastructure/celerity/response', 'ConduitAPIMethod' => 'applications/conduit/method/base', 'ConduitAPIRequest' => 'applications/conduit/protocol/request', 'ConduitAPI_conduit_connect_Method' => 'applications/conduit/method/conduit/connect', 'ConduitAPI_differential_creatediff_Method' => 'applications/conduit/method/differential/creatediff', 'ConduitAPI_differential_createrevision_Method' => 'applications/conduit/method/differential/createrevision', 'ConduitAPI_differential_find_Method' => 'applications/conduit/method/differential/find', 'ConduitAPI_differential_getcommitmessage_Method' => 'applications/conduit/method/differential/getcommitmessage', 'ConduitAPI_differential_markcommitted_Method' => 'applications/conduit/method/differential/markcommitted', 'ConduitAPI_differential_parsecommitmessage_Method' => 'applications/conduit/method/differential/parsecommitmessage', 'ConduitAPI_differential_setdiffproperty_Method' => 'applications/conduit/method/differential/setdiffproperty', 'ConduitAPI_differential_updaterevision_Method' => 'applications/conduit/method/differential/updaterevision', 'ConduitAPI_file_upload_Method' => 'applications/conduit/method/file/upload', 'ConduitAPI_user_find_Method' => 'applications/conduit/method/user/find', 'ConduitException' => 'applications/conduit/protocol/exception', 'DarkConsole' => 'aphront/console/api', 'DarkConsoleConfigPlugin' => 'aphront/console/plugin/config', 'DarkConsoleController' => 'aphront/console/controller', 'DarkConsoleCore' => 'aphront/console/core', 'DarkConsoleErrorLogPlugin' => 'aphront/console/plugin/errorlog', 'DarkConsoleErrorLogPluginAPI' => 'aphront/console/plugin/errorlog/api', 'DarkConsolePlugin' => 'aphront/console/plugin/base', 'DarkConsoleRequestPlugin' => 'aphront/console/plugin/request', 'DarkConsoleServicesPlugin' => 'aphront/console/plugin/services', 'DarkConsoleServicesPluginAPI' => 'aphront/console/plugin/services/api', 'DarkConsoleXHProfPlugin' => 'aphront/console/plugin/xhprof', 'DarkConsoleXHProfPluginAPI' => 'aphront/console/plugin/xhprof/api', 'DifferentialAction' => 'applications/differential/constants/action', 'DifferentialAddCommentView' => 'applications/differential/view/addcomment', + 'DifferentialAttachController' => 'applications/differential/controller/attach', 'DifferentialCCWelcomeMail' => 'applications/differential/mail/ccwelcome', 'DifferentialChangeType' => 'applications/differential/constants/changetype', 'DifferentialChangeset' => 'applications/differential/storage/changeset', 'DifferentialChangesetDetailView' => 'applications/differential/view/changesetdetailview', 'DifferentialChangesetListView' => 'applications/differential/view/changesetlistview', 'DifferentialChangesetParser' => 'applications/differential/parser/changeset', 'DifferentialChangesetViewController' => 'applications/differential/controller/changesetview', 'DifferentialComment' => 'applications/differential/storage/comment', 'DifferentialCommentEditor' => 'applications/differential/editor/comment', 'DifferentialCommentMail' => 'applications/differential/mail/comment', 'DifferentialCommentPreviewController' => 'applications/differential/controller/commentpreview', 'DifferentialCommentSaveController' => 'applications/differential/controller/commentsave', 'DifferentialCommitMessage' => 'applications/differential/parser/commitmessage', 'DifferentialCommitMessageData' => 'applications/differential/data/commitmessage', 'DifferentialCommitMessageParserException' => 'applications/differential/parser/commitmessage/exception', 'DifferentialController' => 'applications/differential/controller/base', 'DifferentialDAO' => 'applications/differential/storage/base', 'DifferentialDiff' => 'applications/differential/storage/diff', 'DifferentialDiffContentMail' => 'applications/differential/mail/diffcontent', 'DifferentialDiffCreateController' => 'applications/differential/controller/diffcreate', 'DifferentialDiffProperty' => 'applications/differential/storage/diffproperty', 'DifferentialDiffTableOfContentsView' => 'applications/differential/view/difftableofcontents', 'DifferentialDiffViewController' => 'applications/differential/controller/diffview', 'DifferentialHunk' => 'applications/differential/storage/hunk', 'DifferentialInlineComment' => 'applications/differential/storage/inlinecomment', 'DifferentialInlineCommentEditController' => 'applications/differential/controller/inlinecommentedit', 'DifferentialInlineCommentPreviewController' => 'applications/differential/controller/inlinecommentpreview', 'DifferentialInlineCommentView' => 'applications/differential/view/inlinecomment', 'DifferentialLintStatus' => 'applications/differential/constants/lintstatus', 'DifferentialMail' => 'applications/differential/mail/base', 'DifferentialMarkupEngineFactory' => 'applications/differential/parser/markup', 'DifferentialNewDiffMail' => 'applications/differential/mail/newdiff', 'DifferentialReviewRequestMail' => 'applications/differential/mail/reviewrequest', 'DifferentialRevision' => 'applications/differential/storage/revision', 'DifferentialRevisionCommentListView' => 'applications/differential/view/revisioncommentlist', 'DifferentialRevisionCommentView' => 'applications/differential/view/revisioncomment', 'DifferentialRevisionControlSystem' => 'applications/differential/constants/revisioncontrolsystem', 'DifferentialRevisionDetailView' => 'applications/differential/view/revisiondetail', 'DifferentialRevisionEditController' => 'applications/differential/controller/revisionedit', 'DifferentialRevisionEditor' => 'applications/differential/editor/revision', 'DifferentialRevisionListController' => 'applications/differential/controller/revisionlist', 'DifferentialRevisionListData' => 'applications/differential/data/revisionlist', 'DifferentialRevisionStatus' => 'applications/differential/constants/revisionstatus', 'DifferentialRevisionUpdateHistoryView' => 'applications/differential/view/revisionupdatehistory', 'DifferentialRevisionViewController' => 'applications/differential/controller/revisionview', 'DifferentialUnitStatus' => 'applications/differential/constants/unitstatus', 'Javelin' => 'infrastructure/javelin/api', 'LiskDAO' => 'storage/lisk/dao', 'ManiphestController' => 'applications/maniphest/controller/base', 'ManiphestDAO' => 'applications/maniphest/storage/base', 'ManiphestTask' => 'applications/maniphest/storage/task', 'ManiphestTaskCreateController' => 'applications/maniphest/controller/createtask', 'ManiphestTaskDetailController' => 'applications/maniphest/controller/taskdetail', 'ManiphestTaskListController' => 'applications/maniphest/controller/tasklist', 'ManiphestTaskListView' => 'applications/maniphest/view/tasklist', 'ManiphestTaskPriority' => 'applications/maniphest/constants/priority', 'ManiphestTaskSelectorController' => 'applications/maniphest/controller/taskselector', 'ManiphestTaskSelectorSearchController' => 'applications/maniphest/controller/taskselectorsearch', 'ManiphestTaskStatus' => 'applications/maniphest/constants/status', 'ManiphestTaskSummaryView' => 'applications/maniphest/view/tasksummary', 'ManiphestTransaction' => 'applications/maniphest/storage/transaction', 'ManiphestTransactionDetailView' => 'applications/maniphest/view/transactiondetail', 'ManiphestTransactionEditor' => 'applications/maniphest/editor/transaction', 'ManiphestTransactionListView' => 'applications/maniphest/view/transactionlist', 'ManiphestTransactionSaveController' => 'applications/maniphest/controller/transactionsave', 'ManiphestTransactionType' => 'applications/maniphest/constants/transactiontype', 'Phabricator404Controller' => 'applications/base/controller/404', 'PhabricatorAuthController' => 'applications/auth/controller/base', 'PhabricatorConduitAPIController' => 'applications/conduit/controller/api', 'PhabricatorConduitConnectionLog' => 'applications/conduit/storage/connectionlog', 'PhabricatorConduitConsoleController' => 'applications/conduit/controller/console', 'PhabricatorConduitController' => 'applications/conduit/controller/base', 'PhabricatorConduitDAO' => 'applications/conduit/storage/base', 'PhabricatorConduitLogController' => 'applications/conduit/controller/log', 'PhabricatorConduitMethodCallLog' => 'applications/conduit/storage/methodcalllog', 'PhabricatorController' => 'applications/base/controller/base', 'PhabricatorDirectoryCategory' => 'applications/directory/storage/category', 'PhabricatorDirectoryCategoryDeleteController' => 'applications/directory/controller/categorydelete', 'PhabricatorDirectoryCategoryEditController' => 'applications/directory/controller/categoryedit', 'PhabricatorDirectoryCategoryListController' => 'applications/directory/controller/categorylist', 'PhabricatorDirectoryController' => 'applications/directory/controller/base', 'PhabricatorDirectoryDAO' => 'applications/directory/storage/base', 'PhabricatorDirectoryItem' => 'applications/directory/storage/item', 'PhabricatorDirectoryItemDeleteController' => 'applications/directory/controller/itemdelete', 'PhabricatorDirectoryItemEditController' => 'applications/directory/controller/itemedit', 'PhabricatorDirectoryItemListController' => 'applications/directory/controller/itemlist', 'PhabricatorDirectoryMainController' => 'applications/directory/controller/main', 'PhabricatorDraft' => 'applications/draft/storage/draft', 'PhabricatorDraftDAO' => 'applications/draft/storage/base', 'PhabricatorEmailLoginController' => 'applications/auth/controller/email', 'PhabricatorEmailTokenController' => 'applications/auth/controller/emailtoken', 'PhabricatorEnv' => 'infrastructure/env', 'PhabricatorFacebookAuthController' => 'applications/auth/controller/facebookauth', 'PhabricatorFacebookAuthDiagnosticsController' => 'applications/auth/controller/facebookauth/diagnostics', 'PhabricatorFile' => 'applications/files/storage/file', 'PhabricatorFileController' => 'applications/files/controller/base', 'PhabricatorFileDAO' => 'applications/files/storage/base', 'PhabricatorFileListController' => 'applications/files/controller/list', 'PhabricatorFileStorageBlob' => 'applications/files/storage/storageblob', 'PhabricatorFileURI' => 'applications/files/uri', 'PhabricatorFileUploadController' => 'applications/files/controller/upload', 'PhabricatorFileViewController' => 'applications/files/controller/view', + 'PhabricatorHandleObjectSelectorDataView' => 'applications/phid/handle/view/selector', 'PhabricatorLiskDAO' => 'applications/base/storage/lisk', 'PhabricatorLoginController' => 'applications/auth/controller/login', 'PhabricatorLogoutController' => 'applications/auth/controller/logout', 'PhabricatorMailImplementationAdapter' => 'applications/metamta/adapter/base', 'PhabricatorMailImplementationAmazonSESAdapter' => 'applications/metamta/adapter/amazonses', 'PhabricatorMailImplementationPHPMailerLiteAdapter' => 'applications/metamta/adapter/phpmailerlite', 'PhabricatorMetaMTAController' => 'applications/metamta/controller/base', 'PhabricatorMetaMTADAO' => 'applications/metamta/storage/base', 'PhabricatorMetaMTADaemon' => 'applications/metamta/daemon/mta', 'PhabricatorMetaMTAListController' => 'applications/metamta/controller/list', 'PhabricatorMetaMTAMail' => 'applications/metamta/storage/mail', 'PhabricatorMetaMTAMailingList' => 'applications/metamta/storage/mailinglist', 'PhabricatorMetaMTAMailingListEditController' => 'applications/metamta/controller/mailinglistedit', 'PhabricatorMetaMTAMailingListsController' => 'applications/metamta/controller/mailinglists', 'PhabricatorMetaMTASendController' => 'applications/metamta/controller/send', 'PhabricatorMetaMTAViewController' => 'applications/metamta/controller/view', 'PhabricatorObjectHandle' => 'applications/phid/handle', 'PhabricatorObjectHandleData' => 'applications/phid/handle/data', 'PhabricatorObjectSelectorDialog' => 'view/control/objectselector', 'PhabricatorPHID' => 'applications/phid/storage/phid', 'PhabricatorPHIDAllocateController' => 'applications/phid/controller/allocate', 'PhabricatorPHIDController' => 'applications/phid/controller/base', 'PhabricatorPHIDDAO' => 'applications/phid/storage/base', 'PhabricatorPHIDListController' => 'applications/phid/controller/list', 'PhabricatorPHIDLookupController' => 'applications/phid/controller/lookup', 'PhabricatorPHIDType' => 'applications/phid/storage/type', 'PhabricatorPHIDTypeEditController' => 'applications/phid/controller/typeedit', 'PhabricatorPHIDTypeListController' => 'applications/phid/controller/typelist', 'PhabricatorPeopleController' => 'applications/people/controller/base', 'PhabricatorPeopleEditController' => 'applications/people/controller/edit', 'PhabricatorPeopleListController' => 'applications/people/controller/list', 'PhabricatorPeopleProfileController' => 'applications/people/controller/profile', 'PhabricatorRemarkupRuleDifferential' => 'infrastructure/markup/remarkup/markuprule/differential', 'PhabricatorRemarkupRuleManiphest' => 'infrastructure/markup/remarkup/markuprule/maniphest', 'PhabricatorRepository' => 'applications/repository/storage/repository', 'PhabricatorRepositoryController' => 'applications/repository/controller/base', 'PhabricatorRepositoryCreateController' => 'applications/repository/controller/create', 'PhabricatorRepositoryDAO' => 'applications/repository/storage/base', 'PhabricatorRepositoryEditController' => 'applications/repository/controller/edit', 'PhabricatorRepositoryGitHubNotification' => 'applications/repository/storage/githubnotification', 'PhabricatorRepositoryGitHubPostReceiveController' => 'applications/repository/controller/github-post-receive', 'PhabricatorRepositoryListController' => 'applications/repository/controller/list', 'PhabricatorSearchAbstractDocument' => 'applications/search/index/abstractdocument', 'PhabricatorSearchBaseController' => 'applications/search/controller/base', 'PhabricatorSearchController' => 'applications/search/controller/search', 'PhabricatorSearchDAO' => 'applications/search/storage/base', 'PhabricatorSearchDifferentialIndexer' => 'applications/search/index/indexer/differential', 'PhabricatorSearchDocument' => 'applications/search/storage/document/document', 'PhabricatorSearchDocumentField' => 'applications/search/storage/document/field', 'PhabricatorSearchDocumentIndexer' => 'applications/search/index/indexer/base', 'PhabricatorSearchDocumentRelationship' => 'applications/search/storage/document/relationship', 'PhabricatorSearchExecutor' => 'applications/search/execute/base', 'PhabricatorSearchField' => 'applications/search/constants/field', 'PhabricatorSearchManiphestIndexer' => 'applications/search/index/indexer/maniphest', 'PhabricatorSearchMySQLExecutor' => 'applications/search/execute/mysql', 'PhabricatorSearchQuery' => 'applications/search/storage/query', 'PhabricatorSearchRelationship' => 'applications/search/constants/relationship', 'PhabricatorStandardPageView' => 'view/page/standard', 'PhabricatorTypeaheadCommonDatasourceController' => 'applications/typeahead/controller/common', 'PhabricatorTypeaheadDatasourceController' => 'applications/typeahead/controller/base', 'PhabricatorUser' => 'applications/people/storage/user', 'PhabricatorUserDAO' => 'applications/people/storage/base', 'PhabricatorUserSettingsController' => 'applications/people/controller/settings', 'PhabricatorXHProfController' => 'applications/xhprof/controller/base', 'PhabricatorXHProfProfileController' => 'applications/xhprof/controller/profile', 'PhabricatorXHProfProfileSymbolView' => 'applications/xhprof/view/symbol', 'PhabricatorXHProfProfileTopLevelView' => 'applications/xhprof/view/toplevel', ), 'function' => array( '_qsprintf_check_scalar_type' => 'storage/qsprintf', '_qsprintf_check_type' => 'storage/qsprintf', 'celerity_generate_unique_node_id' => 'infrastructure/celerity/api', 'celerity_register_resource_map' => 'infrastructure/celerity/map', 'javelin_render_tag' => 'infrastructure/javelin/markup', 'phabricator_format_relative_time' => 'view/utils', 'phabricator_format_timestamp' => 'view/utils', 'phabricator_format_units_generic' => 'view/utils', 'phabricator_render_form' => 'infrastructure/javelin/markup', 'qsprintf' => 'storage/qsprintf', 'queryfx' => 'storage/queryfx', 'queryfx_all' => 'storage/queryfx', 'queryfx_one' => 'storage/queryfx', 'require_celerity_resource' => 'infrastructure/celerity/api', 'vqsprintf' => 'storage/qsprintf', 'vqueryfx' => 'storage/queryfx', 'vqueryfx_all' => 'storage/queryfx', 'xsprintf_query' => 'storage/qsprintf', ), 'requires_class' => array( 'Aphront400Response' => 'AphrontResponse', 'Aphront404Response' => 'AphrontResponse', 'AphrontAjaxResponse' => 'AphrontResponse', 'AphrontDefaultApplicationConfiguration' => 'AphrontApplicationConfiguration', 'AphrontDefaultApplicationController' => 'AphrontController', 'AphrontDialogResponse' => 'AphrontResponse', 'AphrontDialogView' => 'AphrontView', 'AphrontErrorView' => 'AphrontView', 'AphrontFileResponse' => 'AphrontResponse', 'AphrontFormCheckboxControl' => 'AphrontFormControl', 'AphrontFormControl' => 'AphrontView', 'AphrontFormDividerControl' => 'AphrontFormControl', 'AphrontFormFileControl' => 'AphrontFormControl', 'AphrontFormMarkupControl' => 'AphrontFormControl', 'AphrontFormPasswordControl' => 'AphrontFormControl', 'AphrontFormRecaptchaControl' => 'AphrontFormControl', 'AphrontFormSelectControl' => 'AphrontFormControl', 'AphrontFormStaticControl' => 'AphrontFormControl', 'AphrontFormSubmitControl' => 'AphrontFormControl', 'AphrontFormTextAreaControl' => 'AphrontFormControl', 'AphrontFormTextControl' => 'AphrontFormControl', 'AphrontFormTokenizerControl' => 'AphrontFormControl', 'AphrontFormView' => 'AphrontView', 'AphrontMySQLDatabaseConnection' => 'AphrontDatabaseConnection', 'AphrontNullView' => 'AphrontView', 'AphrontPageView' => 'AphrontView', 'AphrontPanelView' => 'AphrontView', 'AphrontQueryConnectionException' => 'AphrontQueryException', 'AphrontQueryConnectionLostException' => 'AphrontQueryRecoverableException', 'AphrontQueryCountException' => 'AphrontQueryException', 'AphrontQueryDuplicateKeyException' => 'AphrontQueryException', 'AphrontQueryObjectMissingException' => 'AphrontQueryException', 'AphrontQueryParameterException' => 'AphrontQueryException', 'AphrontQueryRecoverableException' => 'AphrontQueryException', 'AphrontRedirectException' => 'AphrontException', 'AphrontRedirectResponse' => 'AphrontResponse', 'AphrontRequestFailureView' => 'AphrontView', 'AphrontSideNavView' => 'AphrontView', 'AphrontTableView' => 'AphrontView', 'AphrontWebpageResponse' => 'AphrontResponse', 'CelerityResourceController' => 'AphrontController', 'ConduitAPI_conduit_connect_Method' => 'ConduitAPIMethod', 'ConduitAPI_differential_creatediff_Method' => 'ConduitAPIMethod', 'ConduitAPI_differential_createrevision_Method' => 'ConduitAPIMethod', 'ConduitAPI_differential_find_Method' => 'ConduitAPIMethod', 'ConduitAPI_differential_getcommitmessage_Method' => 'ConduitAPIMethod', 'ConduitAPI_differential_markcommitted_Method' => 'ConduitAPIMethod', 'ConduitAPI_differential_parsecommitmessage_Method' => 'ConduitAPIMethod', 'ConduitAPI_differential_setdiffproperty_Method' => 'ConduitAPIMethod', 'ConduitAPI_differential_updaterevision_Method' => 'ConduitAPIMethod', 'ConduitAPI_file_upload_Method' => 'ConduitAPIMethod', 'ConduitAPI_user_find_Method' => 'ConduitAPIMethod', 'DarkConsoleConfigPlugin' => 'DarkConsolePlugin', 'DarkConsoleController' => 'PhabricatorController', 'DarkConsoleErrorLogPlugin' => 'DarkConsolePlugin', 'DarkConsoleRequestPlugin' => 'DarkConsolePlugin', 'DarkConsoleServicesPlugin' => 'DarkConsolePlugin', 'DarkConsoleXHProfPlugin' => 'DarkConsolePlugin', 'DifferentialAddCommentView' => 'AphrontView', + 'DifferentialAttachController' => 'DifferentialController', 'DifferentialCCWelcomeMail' => 'DifferentialReviewRequestMail', 'DifferentialChangeset' => 'DifferentialDAO', 'DifferentialChangesetDetailView' => 'AphrontView', 'DifferentialChangesetListView' => 'AphrontView', 'DifferentialChangesetViewController' => 'DifferentialController', 'DifferentialComment' => 'DifferentialDAO', 'DifferentialCommentMail' => 'DifferentialMail', 'DifferentialCommentPreviewController' => 'DifferentialController', 'DifferentialCommentSaveController' => 'DifferentialController', 'DifferentialController' => 'PhabricatorController', 'DifferentialDAO' => 'PhabricatorLiskDAO', 'DifferentialDiff' => 'DifferentialDAO', 'DifferentialDiffContentMail' => 'DifferentialMail', 'DifferentialDiffCreateController' => 'DifferentialController', 'DifferentialDiffProperty' => 'DifferentialDAO', 'DifferentialDiffTableOfContentsView' => 'AphrontView', 'DifferentialDiffViewController' => 'DifferentialController', 'DifferentialHunk' => 'DifferentialDAO', 'DifferentialInlineComment' => 'DifferentialDAO', 'DifferentialInlineCommentEditController' => 'DifferentialController', 'DifferentialInlineCommentPreviewController' => 'DifferentialController', 'DifferentialInlineCommentView' => 'AphrontView', 'DifferentialNewDiffMail' => 'DifferentialReviewRequestMail', 'DifferentialReviewRequestMail' => 'DifferentialMail', 'DifferentialRevision' => 'DifferentialDAO', 'DifferentialRevisionCommentListView' => 'AphrontView', 'DifferentialRevisionCommentView' => 'AphrontView', 'DifferentialRevisionDetailView' => 'AphrontView', 'DifferentialRevisionEditController' => 'DifferentialController', 'DifferentialRevisionListController' => 'DifferentialController', 'DifferentialRevisionUpdateHistoryView' => 'AphrontView', 'DifferentialRevisionViewController' => 'DifferentialController', 'ManiphestController' => 'PhabricatorController', 'ManiphestDAO' => 'PhabricatorLiskDAO', 'ManiphestTask' => 'ManiphestDAO', 'ManiphestTaskCreateController' => 'ManiphestController', 'ManiphestTaskDetailController' => 'ManiphestController', 'ManiphestTaskListController' => 'ManiphestController', 'ManiphestTaskListView' => 'AphrontView', 'ManiphestTaskSelectorController' => 'ManiphestController', 'ManiphestTaskSelectorSearchController' => 'ManiphestController', 'ManiphestTaskSummaryView' => 'AphrontView', 'ManiphestTransaction' => 'ManiphestDAO', 'ManiphestTransactionDetailView' => 'AphrontView', 'ManiphestTransactionListView' => 'AphrontView', 'ManiphestTransactionSaveController' => 'ManiphestController', 'Phabricator404Controller' => 'PhabricatorController', 'PhabricatorAuthController' => 'PhabricatorController', 'PhabricatorConduitAPIController' => 'PhabricatorConduitController', 'PhabricatorConduitConnectionLog' => 'PhabricatorConduitDAO', 'PhabricatorConduitConsoleController' => 'PhabricatorConduitController', 'PhabricatorConduitController' => 'PhabricatorController', 'PhabricatorConduitDAO' => 'PhabricatorLiskDAO', 'PhabricatorConduitLogController' => 'PhabricatorConduitController', 'PhabricatorConduitMethodCallLog' => 'PhabricatorConduitDAO', 'PhabricatorController' => 'AphrontController', 'PhabricatorDirectoryCategory' => 'PhabricatorDirectoryDAO', 'PhabricatorDirectoryCategoryDeleteController' => 'PhabricatorDirectoryController', 'PhabricatorDirectoryCategoryEditController' => 'PhabricatorDirectoryController', 'PhabricatorDirectoryCategoryListController' => 'PhabricatorDirectoryController', 'PhabricatorDirectoryController' => 'PhabricatorController', 'PhabricatorDirectoryDAO' => 'PhabricatorLiskDAO', 'PhabricatorDirectoryItem' => 'PhabricatorDirectoryDAO', 'PhabricatorDirectoryItemDeleteController' => 'PhabricatorDirectoryController', 'PhabricatorDirectoryItemEditController' => 'PhabricatorDirectoryController', 'PhabricatorDirectoryItemListController' => 'PhabricatorDirectoryController', 'PhabricatorDirectoryMainController' => 'PhabricatorDirectoryController', 'PhabricatorDraft' => 'PhabricatorDraftDAO', 'PhabricatorDraftDAO' => 'PhabricatorLiskDAO', 'PhabricatorEmailLoginController' => 'PhabricatorAuthController', 'PhabricatorEmailTokenController' => 'PhabricatorAuthController', 'PhabricatorFacebookAuthController' => 'PhabricatorAuthController', 'PhabricatorFacebookAuthDiagnosticsController' => 'PhabricatorAuthController', 'PhabricatorFile' => 'PhabricatorFileDAO', 'PhabricatorFileController' => 'PhabricatorController', 'PhabricatorFileDAO' => 'PhabricatorLiskDAO', 'PhabricatorFileListController' => 'PhabricatorFileController', 'PhabricatorFileStorageBlob' => 'PhabricatorFileDAO', 'PhabricatorFileUploadController' => 'PhabricatorFileController', 'PhabricatorFileViewController' => 'PhabricatorFileController', 'PhabricatorLiskDAO' => 'LiskDAO', 'PhabricatorLoginController' => 'PhabricatorAuthController', 'PhabricatorLogoutController' => 'PhabricatorAuthController', 'PhabricatorMailImplementationAmazonSESAdapter' => 'PhabricatorMailImplementationPHPMailerLiteAdapter', 'PhabricatorMailImplementationPHPMailerLiteAdapter' => 'PhabricatorMailImplementationAdapter', 'PhabricatorMetaMTAController' => 'PhabricatorController', 'PhabricatorMetaMTADAO' => 'PhabricatorLiskDAO', 'PhabricatorMetaMTAListController' => 'PhabricatorMetaMTAController', 'PhabricatorMetaMTAMail' => 'PhabricatorMetaMTADAO', 'PhabricatorMetaMTAMailingList' => 'PhabricatorMetaMTADAO', 'PhabricatorMetaMTAMailingListEditController' => 'PhabricatorMetaMTAController', 'PhabricatorMetaMTAMailingListsController' => 'PhabricatorMetaMTAController', 'PhabricatorMetaMTASendController' => 'PhabricatorMetaMTAController', 'PhabricatorMetaMTAViewController' => 'PhabricatorMetaMTAController', - 'PhabricatorObjectSelectorDialog' => 'AphrontDialogView', 'PhabricatorPHID' => 'PhabricatorPHIDDAO', 'PhabricatorPHIDAllocateController' => 'PhabricatorPHIDController', 'PhabricatorPHIDController' => 'PhabricatorController', 'PhabricatorPHIDDAO' => 'PhabricatorLiskDAO', 'PhabricatorPHIDListController' => 'PhabricatorPHIDController', 'PhabricatorPHIDLookupController' => 'PhabricatorPHIDController', 'PhabricatorPHIDType' => 'PhabricatorPHIDDAO', 'PhabricatorPHIDTypeEditController' => 'PhabricatorPHIDController', 'PhabricatorPHIDTypeListController' => 'PhabricatorPHIDController', 'PhabricatorPeopleController' => 'PhabricatorController', 'PhabricatorPeopleEditController' => 'PhabricatorPeopleController', 'PhabricatorPeopleListController' => 'PhabricatorPeopleController', 'PhabricatorPeopleProfileController' => 'PhabricatorPeopleController', 'PhabricatorRemarkupRuleDifferential' => 'PhutilRemarkupRule', 'PhabricatorRemarkupRuleManiphest' => 'PhutilRemarkupRule', 'PhabricatorRepository' => 'PhabricatorRepositoryDAO', 'PhabricatorRepositoryController' => 'PhabricatorController', 'PhabricatorRepositoryCreateController' => 'PhabricatorController', 'PhabricatorRepositoryDAO' => 'PhabricatorLiskDAO', 'PhabricatorRepositoryEditController' => 'PhabricatorController', 'PhabricatorRepositoryGitHubNotification' => 'PhabricatorRepositoryDAO', 'PhabricatorRepositoryGitHubPostReceiveController' => 'PhabricatorRepositoryController', 'PhabricatorRepositoryListController' => 'PhabricatorController', 'PhabricatorSearchBaseController' => 'PhabricatorController', 'PhabricatorSearchController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchDAO' => 'PhabricatorLiskDAO', 'PhabricatorSearchDifferentialIndexer' => 'PhabricatorSearchDocumentIndexer', 'PhabricatorSearchDocument' => 'PhabricatorSearchDAO', 'PhabricatorSearchDocumentField' => 'PhabricatorSearchDAO', 'PhabricatorSearchDocumentRelationship' => 'PhabricatorSearchDAO', 'PhabricatorSearchManiphestIndexer' => 'PhabricatorSearchDocumentIndexer', 'PhabricatorSearchMySQLExecutor' => 'PhabricatorSearchExecutor', 'PhabricatorSearchQuery' => 'PhabricatorSearchDAO', 'PhabricatorStandardPageView' => 'AphrontPageView', 'PhabricatorTypeaheadCommonDatasourceController' => 'PhabricatorTypeaheadDatasourceController', 'PhabricatorTypeaheadDatasourceController' => 'PhabricatorController', 'PhabricatorUser' => 'PhabricatorUserDAO', 'PhabricatorUserDAO' => 'PhabricatorLiskDAO', 'PhabricatorUserSettingsController' => 'PhabricatorPeopleController', 'PhabricatorXHProfController' => 'PhabricatorController', 'PhabricatorXHProfProfileController' => 'PhabricatorXHProfController', 'PhabricatorXHProfProfileSymbolView' => 'AphrontView', 'PhabricatorXHProfProfileTopLevelView' => 'AphrontView', ), 'requires_interface' => array( ), )); diff --git a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php index 42cca14afc..d2bb937c58 100644 --- a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php +++ b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php @@ -1,249 +1,265 @@ array( '$' => 'PhabricatorDirectoryMainController', ), '/directory/' => array( 'item/$' => 'PhabricatorDirectoryItemListController', 'item/edit/(?:(?P\d+)/)?$' => 'PhabricatorDirectoryItemEditController', 'item/delete/(?P\d+)/' => 'PhabricatorDirectoryItemDeleteController', 'category/$' => 'PhabricatorDirectoryCategoryListController', 'category/edit/(?:(?P\d+)/)?$' => 'PhabricatorDirectoryCategoryEditController', 'category/delete/(?P\d+)/' => 'PhabricatorDirectoryCategoryDeleteController', ), '/file/' => array( '$' => 'PhabricatorFileListController', 'upload/$' => 'PhabricatorFileUploadController', '(?Pinfo)/(?P[^/]+)/' => 'PhabricatorFileViewController', '(?Pview)/(?P[^/]+)/' => 'PhabricatorFileViewController', '(?Pdownload)/(?P[^/]+)/' => 'PhabricatorFileViewController', ), '/phid/' => array( '$' => 'PhabricatorPHIDLookupController', 'list/$' => 'PhabricatorPHIDListController', 'type/$' => 'PhabricatorPHIDTypeListController', 'type/edit/(?:(?P\d+)/)?$' => 'PhabricatorPHIDTypeEditController', 'new/$' => 'PhabricatorPHIDAllocateController', ), '/people/' => array( '$' => 'PhabricatorPeopleListController', 'edit/(?:(?P\w+)/)?$' => 'PhabricatorPeopleEditController', ), '/p/(?P\w+)/$' => 'PhabricatorPeopleProfileController', '/conduit/' => array( '$' => 'PhabricatorConduitConsoleController', 'method/(?P[^/]+)$' => 'PhabricatorConduitConsoleController', 'log/$' => 'PhabricatorConduitLogController', ), '/api/(?P[^/]+)$' => 'PhabricatorConduitAPIController', '/D(?P\d+)' => 'DifferentialRevisionViewController', '/differential/' => array( '$' => 'DifferentialRevisionListController', 'filter/(?P\w+)/$' => 'DifferentialRevisionListController', 'diff/' => array( '(?P\d+)/$' => 'DifferentialDiffViewController', 'create/$' => 'DifferentialDiffCreateController', ), 'changeset/$' => 'DifferentialChangesetViewController', 'revision/edit/(?:(?P\d+)/)?$' => 'DifferentialRevisionEditController', 'comment/' => array( 'preview/(?P\d+)/$' => 'DifferentialCommentPreviewController', 'save/$' => 'DifferentialCommentSaveController', 'inline/' => array( 'preview/(?P\d+)/$' => 'DifferentialInlineCommentPreviewController', 'edit/(?P\d+)/$' => 'DifferentialInlineCommentEditController', ), ), + 'attach/(?P\d+)/(?P\w+)/$' => 'DifferentialAttachController', ), '/res/' => array( '(?Ppkg/)?(?P[a-f0-9]{8})/(?P.+\.(?:css|js))$' => 'CelerityResourceController', ), '/typeahead/' => array( 'common/(?P\w+)/$' => 'PhabricatorTypeaheadCommonDatasourceController', ), '/mail/' => array( '$' => 'PhabricatorMetaMTAListController', 'send/$' => 'PhabricatorMetaMTASendController', 'view/(?P\d+)/$' => 'PhabricatorMetaMTAViewController', 'lists/$' => 'PhabricatorMetaMTAMailingListsController', 'lists/edit/(?:(?P\d+)/)?$' => 'PhabricatorMetaMTAMailingListEditController', ), '/login/' => array( '$' => 'PhabricatorLoginController', 'email/$' => 'PhabricatorEmailLoginController', 'etoken/(?P\w+)/$' => 'PhabricatorEmailTokenController', ), '/logout/$' => 'PhabricatorLogoutController', '/facebook-auth/' => array( '$' => 'PhabricatorFacebookAuthController', 'diagnose/$' => 'PhabricatorFacebookAuthDiagnosticsController', ), '/xhprof/' => array( 'profile/(?P[^/]+)/$' => 'PhabricatorXHProfProfileController', ), '/~/' => 'DarkConsoleController', '/settings/' => array( '(?:page/(?P[^/]+)/)?$' => 'PhabricatorUserSettingsController', ), '/maniphest/' => array( '$' => 'ManiphestTaskListController', 'view/(?P\w+)/$' => 'ManiphestTaskListController', 'task/' => array( 'create/' => 'ManiphestTaskCreateController', ), 'transaction/' => array( 'save/' => 'ManiphestTransactionSaveController', ), 'select/$' => 'ManiphestTaskSelectorController', 'select/search/$' => 'ManiphestTaskSelectorSearchController', ), '/T(?P\d+)$' => 'ManiphestTaskDetailController', '/github-post-receive/(?P\d+)/(?P[^/]+)/$' => 'PhabricatorRepositoryGitHubPostReceiveController', '/repository/' => array( '$' => 'PhabricatorRepositoryListController', 'create/$' => 'PhabricatorRepositoryCreateController', 'edit/(?P\d+)/$' => 'PhabricatorRepositoryEditController', 'delete/(?P\d+)/$' => 'PhabricatorRepositoryDeleteController', ), '/search/' => array( '$' => 'PhabricatorSearchController', '(?P\d+)/$' => 'PhabricatorSearchController', ), ); } public function buildRequest() { $request = new AphrontRequest($this->getHost(), $this->getPath()); $request->setRequestData($_GET + $_POST); $request->setApplicationConfiguration($this); return $request; } public function handleException(Exception $ex) { $class = phutil_escape_html(get_class($ex)); $message = phutil_escape_html($ex->getMessage()); $content = '
'. '

Unhandled Exception "'.$class.'": '.$message.'

'. ''.phutil_escape_html((string)$ex).''. '
'; + if ($this->getRequest()->isAjax()) { + $dialog = new AphrontDialogView(); + $dialog + ->setTitle('Exception!') + ->setClass('aphront-exception-dialog') + ->setUser($this->getRequest()->getUser()) + ->appendChild($content) + ->addCancelButton('/'); + + $response = new AphrontDialogResponse(); + $response->setDialog($dialog); + + return $response; + } + $view = new PhabricatorStandardPageView(); $view->setRequest($this->getRequest()); $view->appendChild($content); $response = new AphrontWebpageResponse(); $response->setContent($view->render()); return $response; } public function willSendResponse(AphrontResponse $response) { $request = $this->getRequest(); if ($response instanceof AphrontDialogResponse) { if (!$request->isAjax()) { $view = new PhabricatorStandardPageView(); $view->setRequest($request); $view->appendChild( '
'. $response->buildResponseString(). '
'); $response = new AphrontWebpageResponse(); $response->setContent($view->render()); return $response; } else { return id(new AphrontAjaxResponse()) ->setContent(array( 'dialog' => $response->buildResponseString(), )); } } else if ($response instanceof AphrontRedirectResponse) { if ($request->isAjax()) { return id(new AphrontAjaxResponse()) ->setContent( array( 'redirect' => $response->getURI(), )); } } else if ($response instanceof Aphront404Response) { $failure = new AphrontRequestFailureView(); $failure->setHeader('404 Not Found'); $failure->appendChild( '

The page you requested was not found.

'); $view = new PhabricatorStandardPageView(); $view->setTitle('404 Not Found'); $view->setRequest($this->getRequest()); $view->appendChild($failure); $response = new AphrontWebpageResponse(); $response->setContent($view->render()); $response->setHTTPResponseCode(404); return $response; } return $response; } public function build404Controller() { return array(new Phabricator404Controller($this->getRequest()), array()); } } diff --git a/src/aphront/default/configuration/__init__.php b/src/aphront/default/configuration/__init__.php index 0da64e03d9..62bdc33fb1 100644 --- a/src/aphront/default/configuration/__init__.php +++ b/src/aphront/default/configuration/__init__.php @@ -1,21 +1,23 @@ id = $data['id']; + $this->type = $data['type']; + } + + public function processRequest() { + + $request = $this->getRequest(); + $user = $request->getUser(); + + $revision = id(new DifferentialRevision())->load($this->id); + if (!$revision) { + return new Aphront404Response(); + } + + if ($request->isFormPost()) { + $phids = explode(';', $request->getStr('phids')); + $old_phids = $revision->getAttachedPHIDs('TASK'); + + if (($phids || $old_phids) && ($phids != $old_phids)) { + $tasks = id(new ManiphestTask())->loadAllWhere( + 'phid in (%Ls)', + array_merge($phids, $old_phids)); + $tasks = mpull($tasks, null, 'getPHID'); + + // Remove PHIDs which don't actually exist. + $phids = array_keys(array_select_keys($tasks, $phids)); + + $revision->setAttachedPHIDs($this->type, $phids); + $revision->save(); + + $editor = new ManiphestTransactionEditor(); + $type = ManiphestTransactionType::TYPE_ATTACH; + foreach ($tasks as $task) { + $transaction = new ManiphestTransaction(); + $transaction->setAuthorPHID($user->getPHID()); + $transaction->setTransactionType($type); + $new = $task->getAttached(); + if (empty($new['DREV'])) { + $new['DREV'] = array(); + } + $rev_phid = $revision->getPHID(); + if (in_array($task->getPHID(), $phids)) { + if (in_array($rev_phid, $task->getAttachedPHIDs('DREV'))) { + // TODO: maybe the transaction editor should be responsible for + // this? + continue; + } + $new['DREV'][$rev_phid] = array(); + } else { + if (!in_array($rev_phid, $task->getAttachedPHIDs('DREV'))) { + continue; + } + unset($new['DREV'][$rev_phid]); + } + $transaction->setNewValue($new); + $editor->applyTransactions($task, array($transaction)); + } + } + + if ($request->isAjax()) { + return id(new AphrontRedirectResponse()); + } else { + return id(new AphrontRedirectResponse()) + ->setURI('/D'.$revision->getID()); + } + } else { + $phids = $revision->getAttachedPHIDs($this->type); + } + + + $handles = id(new PhabricatorObjectHandleData($phids)) + ->loadHandles(); + + $obj_dialog = new PhabricatorObjectSelectorDialog(); + $obj_dialog + ->setUser($user) + ->setHandles($handles) + ->setFilters(array( + 'assigned' => 'Assigned to Me', + 'created' => 'Created By Me', + 'open' => 'All Open Tasks', + 'all' => 'All Tasks', + )) + ->setCancelURI('#') + ->setSearchURI('/maniphest/select/search/') + ->setNoun('Tasks'); + + $dialog = $obj_dialog->buildDialog(); + + return id(new AphrontDialogResponse())->setDialog($dialog); + } + +} diff --git a/src/applications/differential/controller/attach/__init__.php b/src/applications/differential/controller/attach/__init__.php new file mode 100644 index 0000000000..ec798503d2 --- /dev/null +++ b/src/applications/differential/controller/attach/__init__.php @@ -0,0 +1,24 @@ +revisionID = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $revision = id(new DifferentialRevision())->load($this->revisionID); if (!$revision) { return new Aphront404Response(); } $revision->loadRelationships(); $diffs = $revision->loadDiffs(); $diff_vs = $request->getInt('vs'); $target = end($diffs); $diffs = mpull($diffs, null, 'getID'); if (empty($diffs[$diff_vs])) { $diff_vs = null; } list($changesets, $vs_map) = $this->loadChangesetsAndVsMap($diffs, $diff_vs, $target); $comments = $revision->loadComments(); $comments = array_merge( $this->getImplicitComments($revision), $comments); $inlines = $this->loadInlineComments($comments, $changesets); $object_phids = array_merge( $revision->getReviewers(), $revision->getCCPHIDs(), array( $revision->getAuthorPHID(), $user->getPHID(), ), mpull($comments, 'getAuthorPHID')); + foreach ($revision->getAttached() as $type => $phids) { + foreach ($phids as $phid => $info) { + $object_phids[] = $phid; + } + } $object_phids = array_unique($object_phids); $handles = id(new PhabricatorObjectHandleData($object_phids)) ->loadHandles(); $request_uri = $request->getRequestURI(); $limit = 100; $large = $request->getStr('large'); if (count($changesets) > $limit && !$large) { $count = number_format(count($changesets)); $warning = new AphrontErrorView(); $warning->setTitle('Very Large Diff'); $warning->setSeverity(AphrontErrorView::SEVERITY_WARNING); $warning->setWidth(AphrontErrorView::WIDTH_WIDE); $warning->appendChild( "

This diff is very large and affects {$count} files. Only ". "the first {$limit} files are shown. ". "". phutil_render_tag( 'a', array( 'href' => $request_uri->alter('large', 'true'), ), 'Show All Files'). ""); $warning = $warning->render(); $visible_changesets = array_slice($changesets, 0, $limit, true); } else { $warning = null; $visible_changesets = $changesets; } $revision_detail = new DifferentialRevisionDetailView(); $revision_detail->setRevision($revision); $properties = $this->getRevisionProperties($revision, $target, $handles); $revision_detail->setProperties($properties); $actions = $this->getRevisionActions($revision); $revision_detail->setActions($actions); $comment_view = new DifferentialRevisionCommentListView(); $comment_view->setComments($comments); $comment_view->setHandles($handles); $comment_view->setInlineComments($inlines); $comment_view->setChangesets($changesets); $comment_view->setUser($user); $diff_history = new DifferentialRevisionUpdateHistoryView(); $diff_history->setDiffs($diffs); $diff_history->setSelectedVersusDiffID($diff_vs); $diff_history->setSelectedDiffID($target->getID()); $toc_view = new DifferentialDiffTableOfContentsView(); $toc_view->setChangesets($changesets); $changeset_view = new DifferentialChangesetListView(); $changeset_view->setChangesets($visible_changesets); $changeset_view->setEditable(true); $changeset_view->setRevision($revision); $changeset_view->setVsMap($vs_map); $draft = id(new PhabricatorDraft())->loadOneWhere( 'authorPHID = %s AND draftKey = %s', $user->getPHID(), 'differential-comment-'.$revision->getID()); if ($draft) { $draft = $draft->getDraft(); } else { $draft = null; } $comment_form = new DifferentialAddCommentView(); $comment_form->setRevision($revision); $comment_form->setActions($this->getRevisionCommentActions($revision)); $comment_form->setActionURI('/differential/comment/save/'); $comment_form->setUser($user); $comment_form->setDraft($draft); return $this->buildStandardPageResponse( '

'. $revision_detail->render(). $comment_view->render(). $diff_history->render(). $toc_view->render(). $warning. $changeset_view->render(). $comment_form->render(). '
', array( 'title' => $revision->getTitle(), )); } private function getImplicitComments(DifferentialRevision $revision) { $template = new DifferentialComment(); $template->setAuthorPHID($revision->getAuthorPHID()); $template->setRevisionID($revision->getID()); $template->setDateCreated($revision->getDateCreated()); $comments = array(); if (strlen($revision->getSummary())) { $summary_comment = clone $template; $summary_comment->setContent($revision->getSummary()); $summary_comment->setAction(DifferentialAction::ACTION_SUMMARIZE); $comments[] = $summary_comment; } if (strlen($revision->getTestPlan())) { $testplan_comment = clone $template; $testplan_comment->setContent($revision->getTestPlan()); $testplan_comment->setAction(DifferentialAction::ACTION_TESTPLAN); $comments[] = $testplan_comment; } return $comments; } private function getRevisionProperties( DifferentialRevision $revision, DifferentialDiff $diff, array $handles) { $properties = array(); $status = $revision->getStatus(); $status = DifferentialRevisionStatus::getNameForRevisionStatus($status); $properties['Revision Status'] = ''.$status.''; $author = $handles[$revision->getAuthorPHID()]; $properties['Author'] = $author->renderLink(); $properties['Reviewers'] = $this->renderHandleLinkList( array_select_keys( $handles, $revision->getReviewers())); $properties['CCs'] = $this->renderHandleLinkList( array_select_keys( $handles, $revision->getCCPHIDs())); $host = $diff->getSourceMachine(); if ($host) { $properties['Host'] = phutil_escape_html($host); } $path = $diff->getSourcePath(); if ($path) { $branch = $diff->getBranch() ? ' ('.$diff->getBranch().')' : ''; $properties['Path'] = phutil_escape_html("{$path} {$branch}"); } $lstar = DifferentialRevisionUpdateHistoryView::renderDiffLintStar($diff); $lmsg = DifferentialRevisionUpdateHistoryView::getDiffLintMessage($diff); $properties['Lint'] = $lstar.' '.$lmsg; $ustar = DifferentialRevisionUpdateHistoryView::renderDiffUnitStar($diff); $umsg = DifferentialRevisionUpdateHistoryView::getDiffUnitMessage($diff); $properties['Unit'] = $ustar.' '.$umsg; + $tasks = $revision->getAttachedPHIDs('TASK'); + if ($tasks) { + $links = array(); + foreach ($tasks as $task_phid) { + $links[] = $handles[$task_phid]->renderLink(); + } + $properties['Maniphest Tasks'] = implode('
', $links); + } + return $properties; } private function getRevisionActions(DifferentialRevision $revision) { $viewer_phid = $this->getRequest()->getUser()->getPHID(); $viewer_is_owner = ($revision->getAuthorPHID() == $viewer_phid); $viewer_is_reviewer = in_array($viewer_phid, $revision->getReviewers()); $viewer_is_cc = in_array($viewer_phid, $revision->getCCPHIDs()); $status = $revision->getStatus(); $revision_id = $revision->getID(); $revision_phid = $revision->getPHID(); $links = array(); if ($viewer_is_owner) { $links[] = array( 'class' => 'revision-edit', 'href' => "/differential/revision/edit/{$revision_id}/", 'name' => 'Edit Revision', ); } if (!$viewer_is_owner && !$viewer_is_reviewer) { $action = $viewer_is_cc ? 'rem' : 'add'; $links[] = array( 'class' => $viewer_is_cc ? 'subscribe-rem' : 'subscribe-add', 'href' => "/differential/subscribe/{$action}/{$revision_id}/", 'name' => $viewer_is_cc ? 'Unsubscribe' : 'Subscribe', ); } else { $links[] = array( 'class' => 'subscribe-rem unavailable', 'name' => 'Automatically Subscribed', ); } + require_celerity_resource('phabricator-object-selector-css'); + require_celerity_resource('javelin-behavior-phabricator-object-selector'); + + $links[] = array( + 'class' => 'attach-maniphest', + 'name' => 'Edit Maniphest Tasks', + 'href' => "/differential/attach/{$revision_id}/TASK/", + 'sigil' => 'workflow', + ); + $links[] = array( 'class' => 'transcripts-metamta', 'name' => 'MetaMTA Transcripts', 'href' => "/mail/?phid={$revision_phid}", ); return $links; } private function renderHandleLinkList(array $list) { if (empty($list)) { return 'None'; } return implode(', ', mpull($list, 'renderLink')); } private function getRevisionCommentActions(DifferentialRevision $revision) { $actions = array( DifferentialAction::ACTION_COMMENT => true, ); $viewer_phid = $this->getRequest()->getUser()->getPHID(); $viewer_is_owner = ($viewer_phid == $revision->getAuthorPHID()); if ($viewer_is_owner) { switch ($revision->getStatus()) { case DifferentialRevisionStatus::NEEDS_REVIEW: $actions[DifferentialAction::ACTION_ABANDON] = true; break; case DifferentialRevisionStatus::NEEDS_REVISION: case DifferentialRevisionStatus::ACCEPTED: $actions[DifferentialAction::ACTION_ABANDON] = true; $actions[DifferentialAction::ACTION_REQUEST] = true; break; case DifferentialRevisionStatus::COMMITTED: break; case DifferentialRevisionStatus::ABANDONED: $actions[DifferentialAction::ACTION_RECLAIM] = true; break; } } else { switch ($revision->getStatus()) { case DifferentialRevisionStatus::NEEDS_REVIEW: $actions[DifferentialAction::ACTION_ACCEPT] = true; $actions[DifferentialAction::ACTION_REJECT] = true; break; case DifferentialRevisionStatus::NEEDS_REVISION: $actions[DifferentialAction::ACTION_ACCEPT] = true; break; case DifferentialRevisionStatus::ACCEPTED: $actions[DifferentialAction::ACTION_REJECT] = true; break; case DifferentialRevisionStatus::COMMITTED: case DifferentialRevisionStatus::ABANDONED: break; } } $actions[DifferentialAction::ACTION_ADDREVIEWERS] = true; return array_keys($actions); } private function loadInlineComments(array $comments, array &$changesets) { $inline_comments = array(); $comment_ids = array_filter(mpull($comments, 'getID')); if (!$comment_ids) { return $inline_comments; } $inline_comments = id(new DifferentialInlineComment()) ->loadAllWhere( 'commentID in (%Ld)', $comment_ids); $load_changesets = array(); foreach ($inline_comments as $inline) { $changeset_id = $inline->getChangesetID(); if (isset($changesets[$changeset_id])) { continue; } $load_changesets[$changeset_id] = true; } $more_changesets = array(); if ($load_changesets) { $changeset_ids = array_keys($load_changesets); $more_changesets += id(new DifferentialChangeset()) ->loadAllWhere( 'id IN (%Ld)', $changeset_ids); } if ($more_changesets) { $changesets += $more_changesets; $changesets = msort($changesets, 'getSortKey'); } return $inline_comments; } private function loadChangesetsAndVsMap(array $diffs, $diff_vs, $target) { $load_ids = array(); if ($diff_vs) { $load_ids[] = $diff_vs; } $load_ids[] = $target->getID(); $raw_changesets = id(new DifferentialChangeset()) ->loadAllWhere( 'diffID IN (%Ld)', $load_ids); $changeset_groups = mgroup($raw_changesets, 'getDiffID'); $changesets = idx($changeset_groups, $target->getID(), array()); $changesets = mpull($changesets, null, 'getID'); $vs_map = array(); if ($diff_vs) { $vs_changesets = idx($changeset_groups, $diff_vs, array()); $vs_changesets = mpull($vs_changesets, null, 'getFilename'); foreach ($changesets as $key => $changeset) { $file = $changeset->getFilename(); if (isset($vs_changesets[$file])) { $vs_map[$changeset->getID()] = $vs_changesets[$file]->getID(); unset($vs_changesets[$file]); } } foreach ($vs_changesets as $changeset) { $changesets[$changeset->getID()] = $changeset; $vs_map[$changeset->getID()] = -1; } } $changesets = msort($changesets, 'getSortKey'); return array($changesets, $vs_map); } } /* protected function getRevisionActions(DifferentialRevision $revision) { $viewer_id = $this->getRequest()->getViewerContext()->getUserID(); $viewer_is_owner = ($viewer_id == $revision->getOwnerID()); $viewer_is_reviewer = ((array_search($viewer_id, $revision->getReviewers())) !== false); $viewer_is_cc = ((array_search($viewer_id, $revision->getCCFBIDs())) !== false); $status = $revision->getStatus(); $links = array(); if (!$viewer_is_owner && !$viewer_is_reviewer) { $action = $viewer_is_cc ? 'rem' : 'add'; $revision_id = $revision->getID(); $href = "/differential/subscribe/{$action}/{$revision_id}"; $links[] = array( $viewer_is_cc ? 'subscribe-disabled' : 'subscribe-enabled', {$viewer_is_cc ? 'Unsubscribe' : 'Subscribe'}, ); } else { $links[] = array( 'subscribe-disabled unavailable', Automatically Subscribed, ); } $blast_uri = RedirectURI( '/intern/differential/?action=tasks&fbid='.$revision->getFBID()) ->setTier('intern'); $links[] = array( 'tasks', Edit Tasks, ); $engineering_repository_id = RepositoryRef::getByCallsign('E')->getID(); $svn_revision = $revision->getSVNRevision(); if ($status == DifferentialConstants::COMMITTED && $svn_revision && $revision->getRepositoryID() == $engineering_repository_id) { $href = '/intern/push/request.php?rev='.$svn_revision; $href = RedirectURI($href)->setTier('intern'); $links[] = array( 'merge', Ask for Merge, ); } $links[] = array( 'herald-transcript', getFBID()} >Herald Transcripts, ); $links[] = array( 'metamta-transcript', getFBID()} >MetaMTA Transcripts, ); $list =
    ; foreach ($links as $link) { list($class, $tag) = $link; $list->appendChild(
  • {$tag}
  • ); } return $list; protected function getSandcastleURI(Diff $diff) { $uri = $this->getDiffProperty($diff, 'facebook:sandcastle_uri'); if (!$uri) { $uri = $diff->getSandboxURL(); } return $uri; } protected function getDiffProperty(Diff $diff, $property, $default = null) { $diff_id = $diff->getID(); if (empty($this->diffProperties[$diff_id])) { $props = id(new DifferentialDiffProperty()) ->loadAllWhere('diffID = %s', $diff_id); $dict = array_pull($props, 'getData', 'getName'); $this->diffProperties[$diff_id] = $dict; } return idx($this->diffProperties[$diff_id], $property, $default); } $diff_table->appendChild( {id( - - - - - - - - - - - Search - - - '; - $result_box = - '
    '. + $handles = id(new PhabricatorObjectHandleData($phids)) + ->loadHandles(); - '
    '; - $attached_box = - '
    '. - '
    '. - '
    '. - 'Currently Attached Tasks'. - '
    '. - '
    '. - '
    '. - '
    '. - '
    '; - - require_celerity_resource('phabricator-object-selector-css'); + $obj_dialog = new PhabricatorObjectSelectorDialog(); + $obj_dialog + ->setUser($user) + ->setHandles($handles) + ->setFilters(array( + 'assigned' => 'Assigned to Me', + 'created' => 'Created By Me', + 'open' => 'All Open Tasks', + 'all' => 'All Tasks', + )) + ->setCancelURI('#') + ->setSearchURI('/maniphest/select/search/') + ->setNoun('Tasks'); - Javelin::initBehavior( - 'phabricator-object-selector', - array( - 'filter' => $filter_id, - 'query' => $query_id, - 'search' => $search_id, - 'results' => $results_id, - 'current' => $current_id, - 'uri' => '/maniphest/select/search/', - )); + $dialog = $obj_dialog->buildDialog(); - $dialog = new PhabricatorObjectSelectorDialog(); - $dialog - ->setUser($user) - ->setTitle('Manage Attached Tasks') - ->setClass('phabricator-object-selector-dialog') - ->appendChild($search_box) - ->appendChild($result_box) - ->appendChild($attached_box) - ->addCancelButton('#') - ->addSubmitButton('Save Tasks'); return id(new AphrontDialogResponse())->setDialog($dialog); } } - -/* - - ' - - - - -
    - - - T20: Internet Attack Internets -
    '. - ' - - - - -
    - - - T21: Internet Attack Internets -
    '. - ' - - - - -
    - - - T22: Internet Attack Internets -
    '. - 'more results
    '. - 'more results
    '. - 'more results
    '. - 'more results
    '. - 'more results
    '. - 'more results
    '. - 'more results
    '. - 'more results
    '. - 'more results
    '. - 'more results
    '. - -*/ - - -/* - - ' - - - - -
    - - - T22: Internet Attack Internets -
    '. - ' - - - - -
    - - - T22: Internet Attack Internets -
    '. - -*/ diff --git a/src/applications/maniphest/controller/taskselector/__init__.php b/src/applications/maniphest/controller/taskselector/__init__.php index 5affed985e..cf69aa48ca 100644 --- a/src/applications/maniphest/controller/taskselector/__init__.php +++ b/src/applications/maniphest/controller/taskselector/__init__.php @@ -1,18 +1,17 @@ getRequest(); $user = $request->getUser(); $query = new PhabricatorSearchQuery(); $query->setQuery($request->getStr('query')); $query->setParameter('type', 'TASK'); $exec = new PhabricatorSearchMySQLExecutor(); $results = $exec->executeSearch($query); + $results = ipull($results, 'phid'); + + $handles = id(new PhabricatorObjectHandleData($results)) + ->loadHandles(); $data = array(); - foreach ($results as $result) { - $data[] = array( - 'phid' => $result['phid'], - 'name' => $result['documentTitle'], - 'href' => '#', - ); + foreach ($handles as $handle) { + $view = new PhabricatorHandleObjectSelectorDataView($handle); + $data[] = $view->renderData(); } return id(new AphrontAjaxResponse())->setContent($data); } } diff --git a/src/applications/maniphest/controller/taskselectorsearch/__init__.php b/src/applications/maniphest/controller/taskselectorsearch/__init__.php index ada22948be..63b05f488d 100644 --- a/src/applications/maniphest/controller/taskselectorsearch/__init__.php +++ b/src/applications/maniphest/controller/taskselectorsearch/__init__.php @@ -1,17 +1,19 @@ getCCPHIDs(); $email_to = array(); $email_to[] = $task->getOwnerPHID(); foreach ($transactions as $key => $transaction) { $type = $transaction->getTransactionType(); $new = $transaction->getNewValue(); $email_to[] = $transaction->getAuthorPHID(); switch ($type) { case ManiphestTransactionType::TYPE_NONE: $old = null; break; case ManiphestTransactionType::TYPE_STATUS: $old = $task->getStatus(); break; case ManiphestTransactionType::TYPE_OWNER: $old = $task->getOwnerPHID(); break; case ManiphestTransactionType::TYPE_CCS: $old = $task->getCCPHIDs(); break; case ManiphestTransactionType::TYPE_PRIORITY: $old = $task->getPriority(); break; + case ManiphestTransactionType::TYPE_ATTACH: + $old = $task->getAttached(); + break; default: throw new Exception('Unknown action type.'); } if (($old !== null) && ($old == $new)) { if (count($transactions) > 1 && !$transaction->hasComments()) { // If we have at least one other transaction and this one isn't // doing anything and doesn't have any comments, just throw it // away. unset($transactions[$key]); continue; } else { $transaction->setOldValue(null); $transaction->setNewValue(null); $transaction->setTransactionType(ManiphestTransactionType::TYPE_NONE); } } else { switch ($type) { case ManiphestTransactionType::TYPE_NONE: break; case ManiphestTransactionType::TYPE_STATUS: $task->setStatus($new); break; case ManiphestTransactionType::TYPE_OWNER: $task->setOwnerPHID($new); break; case ManiphestTransactionType::TYPE_CCS: $task->setCCPHIDs($new); break; case ManiphestTransactionType::TYPE_PRIORITY: $task->setPriority($new); break; + case ManiphestTransactionType::TYPE_ATTACH: + $task->setAttached($new); + break; default: throw new Exception('Unknown action type.'); } $transaction->setOldValue($old); $transaction->setNewValue($new); } } $task->save(); foreach ($transactions as $transaction) { $transaction->setTaskID($task->getID()); $transaction->save(); } $email_to[] = $task->getOwnerPHID(); $email_cc = array_merge( $email_cc, $task->getCCPHIDs()); // TODO: Do this offline via timeline PhabricatorSearchManiphestIndexer::indexTask($task); $this->sendEmail($task, $transactions, $email_to, $email_cc); } private function sendEmail($task, $transactions, $email_to, $email_cc) { $email_to = array_filter(array_unique($email_to)); $email_cc = array_filter(array_unique($email_cc)); $phids = array(); foreach ($transactions as $transaction) { foreach ($transaction->extractPHIDs() as $phid) { $phids[$phid] = true; } } $phids = array_keys($phids); $handles = id(new PhabricatorObjectHandleData($phids)) ->loadHandles(); - $view = new ManiphestTransactionDetailView(); $view->setTransactionGroup($transactions); $view->setHandles($handles); list($action, $body) = $view->renderForEmail($with_date = false); $is_create = false; foreach ($transactions as $transaction) { $type = $transaction->getTransactionType(); if (($type == ManiphestTransactionType::TYPE_STATUS) && ($transaction->getOldValue() === null) && ($transaction->getNewValue() == ManiphestTaskStatus::STATUS_OPEN)) { $is_create = true; } } $task_uri = PhabricatorEnv::getURI('/T'.$task->getID()); if ($is_create) { $body .= "\n\n". "TASK DESCRIPTION\n". " ".$task->getDescription(); } $body .= "\n\n". "TASK DETAIL\n". " ".$task_uri."\n"; $base = substr(md5($task->getPHID()), 0, 27).' '.pack("N", time()); $thread_index = base64_encode($base); $message_id = 'getPHID().'>'; id(new PhabricatorMetaMTAMail()) ->setSubject( '[Maniphest] T'.$task->getID().' '.$action.': '.$task->getTitle()) ->setFrom($transaction->getAuthorPHID()) ->addTos($email_to) ->addCCs($email_cc) ->addHeader('Thread-Index', $thread_index) ->addHeader('Thread-Topic', 'Maniphest Task '.$task->getID()) ->addHeader('In-Reply-To', $message_id) ->addHeader('References', $message_id) ->setRelatedPHID($task->getPHID()) ->setBody($body) ->save(); } } diff --git a/src/applications/maniphest/storage/task/ManiphestTask.php b/src/applications/maniphest/storage/task/ManiphestTask.php index e690c978ee..6184d70554 100644 --- a/src/applications/maniphest/storage/task/ManiphestTask.php +++ b/src/applications/maniphest/storage/task/ManiphestTask.php @@ -1,52 +1,56 @@ true, self::CONFIG_SERIALIZATION => array( 'ccPHIDs' => self::SERIALIZATION_JSON, - 'relatedPHIDs' => self::SERIALIZATION_JSON, + 'attached' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } + public function getAttachedPHIDs($type) { + return array_keys(idx($this->attached, $type, array())); + } + public function generatePHID() { return PhabricatorPHID::generateNewPHID('TASK'); } public function getCCPHIDs() { return nonempty($this->ccPHIDs, array()); } } diff --git a/src/applications/maniphest/storage/transaction/ManiphestTransaction.php b/src/applications/maniphest/storage/transaction/ManiphestTransaction.php index 07d9e8d681..132fa10d70 100644 --- a/src/applications/maniphest/storage/transaction/ManiphestTransaction.php +++ b/src/applications/maniphest/storage/transaction/ManiphestTransaction.php @@ -1,86 +1,102 @@ array( 'oldValue' => self::SERIALIZATION_JSON, 'newValue' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function extractPHIDs() { $phids = array(); switch ($this->getTransactionType()) { case ManiphestTransactionType::TYPE_CCS: foreach ($this->getOldValue() as $phid) { $phids[] = $phid; } foreach ($this->getNewValue() as $phid) { $phids[] = $phid; } break; case ManiphestTransactionType::TYPE_OWNER: $phids[] = $this->getOldValue(); $phids[] = $this->getNewValue(); break; + case ManiphestTransactionType::TYPE_ATTACH: + $old = $this->getOldValue(); + $new = $this->getNewValue(); + if (!is_array($old)) { + $old = array(); + } + if (!is_array($new)) { + $new = array(); + } + $val = array_merge(array_values($old), array_values($new)); + foreach ($val as $stuff) { + foreach ($stuff as $phid => $ignored) { + $phids[] = $phid; + } + } + break; } $phids[] = $this->getAuthorPHID(); return $phids; } public function canGroupWith($target) { if ($target->getAuthorPHID() != $this->getAuthorPHID()) { return false; } if ($target->hasComments() && $this->hasComments()) { return false; } $ttime = $target->getDateCreated(); $stime = $this->getDateCreated(); if (abs($stime - $ttime) > 60) { return false; } if ($target->getTransactionType() == $this->getTransactionType()) { return false; } return true; } public function hasComments() { return (bool)strlen(trim($this->getComments())); } } diff --git a/src/applications/maniphest/view/transactiondetail/ManiphestTransactionDetailView.php b/src/applications/maniphest/view/transactiondetail/ManiphestTransactionDetailView.php index 8af452ebdf..04a40b050a 100644 --- a/src/applications/maniphest/view/transactiondetail/ManiphestTransactionDetailView.php +++ b/src/applications/maniphest/view/transactiondetail/ManiphestTransactionDetailView.php @@ -1,247 +1,282 @@ transactions = $transactions; return $this; } public function setHandles(array $handles) { $this->handles = $handles; return $this; } public function setMarkupEngine(PhutilMarkupEngine $engine) { $this->markupEngine = $engine; return $this; } public function renderForEmail($with_date) { $this->forEmail = true; $transaction = reset($this->transactions); $author = $this->renderHandles(array($transaction->getAuthorPHID())); $action = null; $descs = array(); $comments = null; foreach ($this->transactions as $transaction) { list($verb, $desc, $classes) = $this->describeAction($transaction); if ($action === null) { $action = $verb; } $desc = $author.' '.$desc.'.'; if ($with_date) { $desc = 'On '.date('M jS \a\t g:i A', $transaction->getDateCreated()). ', '.$desc; } $descs[] = $desc; if ($transaction->hasComments()) { $comments = $transaction->getComments(); } } $descs = implode("\n", $descs); if ($comments) { $descs .= "\n".$comments; } $this->forEmail = false; return array($action, $descs); } public function render() { $handles = $this->handles; $transactions = $this->transactions; require_celerity_resource('maniphest-transaction-detail-css'); $comment_transaction = null; foreach ($this->transactions as $transaction) { if ($transaction->hasComments()) { $comment_transaction = $transaction; break; } } $any_transaction = reset($transactions); $author = $this->handles[$any_transaction->getAuthorPHID()]; $more_classes = array(); $descs = array(); foreach ($transactions as $transaction) { list($verb, $desc, $classes) = $this->describeAction($transaction); $more_classes = array_merge($more_classes, $classes); $descs[] = $author->renderLink().' '.$desc.'.'; } $descs = implode('
    ', $descs); $more_classes = implode(' ', $classes); if ($comment_transaction && $comment_transaction->hasComments()) { $comments = $comment_transaction->getCache(); if (!strlen($comments)) { $comments = $comment_transaction->getComments(); - $comments = $this->markupEngine->markupText($comments); - $transaction->setCache($comments); - $transaction->save(); + if (strlen($comments)) { + $comments = $this->markupEngine->markupText($comments); + $comment_transaction->setCache($comments); + if ($comment_transaction->getID()) { + $comment_transaction->save(); + } + } } $comment_block = '
    '. $comments. '
    '; } else { $comment_block = null; } return phutil_render_tag( 'div', array( 'class' => "maniphest-transaction-detail-container", 'style' => "background-image: url('".$author->getImageURI()."')", ), '
    '. '
    '. '
    '. phabricator_format_timestamp($transaction->getDateCreated()). '
    '. $descs. '
    '. $comment_block. '
    '); } private function describeAction($transaction) { $verb = null; $desc = null; $classes = array(); $handles = $this->handles; $type = $transaction->getTransactionType(); $author_phid = $transaction->getAuthorPHID(); $new = $transaction->getNewValue(); $old = $transaction->getOldValue(); switch ($type) { case ManiphestTransactionType::TYPE_NONE: $verb = 'Commented On'; $desc = 'added a comment'; break; case ManiphestTransactionType::TYPE_OWNER: if ($transaction->getAuthorPHID() == $new) { $verb = 'Claimed'; $desc = 'claimed this task'; } else if (!$new) { $verb = 'Up For Grabs'; $desc = 'placed this task up for grabs'; } else if (!$old) { $verb = 'Assigned'; $desc = 'assigned this task to '.$this->renderHandles(array($new)); } else { $verb = 'Reassigned'; $desc = 'reassigned this task from '. $this->renderHandles(array($old)). ' to '. $this->renderHandles(array($new)); } break; case ManiphestTransactionType::TYPE_CCS: $added = array_diff($new, $old); $removed = array_diff($old, $new); if ($added && !$removed) { $verb = 'Added CC'; if (count($added) == 1) { $desc = 'added '.$this->renderHandles($added).' to CC'; } else { $desc = 'added CCs: '.$this->renderHandles($added); } } else if ($removed && !$added) { $verb = 'Removed CC'; if (count($removed) == 1) { $desc = 'removed '.$this->renderHandles($removed).' from CC'; } else { $desc = 'removed CCs: '.$this->renderHandles($removed); } } else { $verb = 'Changed CC'; $desc = 'changed CCs, added: '.$this->renderHandles($added).'; '. 'removed: '.$this->renderHandles($removed); } break; case ManiphestTransactionType::TYPE_STATUS: if ($new == ManiphestTaskStatus::STATUS_OPEN) { if ($old) { $verb = 'Reopened'; $desc = 'reopened this task'; } else { $verb = 'Created'; $desc = 'created this task'; } } else if ($new == ManiphestTaskStatus::STATUS_CLOSED_SPITE) { $verb = 'Spited'; $desc = 'closed this task out of spite'; } else { $verb = 'Closed'; $full = idx(ManiphestTaskStatus::getTaskStatusMap(), $new, '???'); $desc = 'closed this task as "'.$full.'"'; } break; case ManiphestTransactionType::TYPE_PRIORITY: $old_name = ManiphestTaskPriority::getTaskPriorityName($old); $new_name = ManiphestTaskPriority::getTaskPriorityName($new); if ($old == ManiphestTaskPriority::PRIORITY_TRIAGE) { $verb = 'Triaged'; $desc = 'triaged this task as "'.$new_name.'" priority'; } else if ($old > $new) { $verb = 'Lowered Priority'; $desc = 'lowered the priority of this task from "'.$old_name.'" to '. '"'.$new_name.'"'; } else { $verb = 'Raised Priority'; $desc = 'raised the priority of this task from "'.$old_name.'" to '. '"'.$new_name.'"'; } break; + case ManiphestTransactionType::TYPE_ATTACH: + $old = nonempty($old, array()); + $new = nonempty($new, array()); + + $old = array_keys(idx($old, 'DREV', array())); + $new = array_keys(idx($new, 'DREV', array())); + $added = array_diff($new, $old); + $removed = array_diff($old, $new); + + $add_desc = $this->renderHandles($added); + $rem_desc = $this->renderHandles($removed); + + if ($added && !$removed) { + $verb = 'Attached'; + if (count($added) == 1) { + $desc = 'attached Differential Revision: '.$add_desc; + } else { + $desc = 'attached Differential Revisions: '.$add_desc; + } + } else if ($removed && !$added) { + $verb = 'Detached'; + if (count($removed) == 1) { + $desc = 'detached Differential Revision: '.$rem_desc; + } else { + $desc = 'detached Differential Revisions: '.$rem_desc; + } + } else { + $desc = 'changed attached Differential Revisions, added: '.$add_desc. + 'removed: '.$rem_desc; + } + break; default: - return ' brazenly '.$type."'d"; + return array($type, ' brazenly '.$type."'d", $classes); } return array($verb, $desc, $classes); } private function renderHandles($phids) { $links = array(); foreach ($phids as $phid) { if ($this->forEmail) { $links[] = $this->handles[$phid]->getName(); } else { $links[] = $this->handles[$phid]->renderLink(); } } return implode(', ', $links); } } diff --git a/src/applications/phid/handle/PhabricatorObjectHandle.php b/src/applications/phid/handle/PhabricatorObjectHandle.php index aee61e6ef9..27b4926ae4 100644 --- a/src/applications/phid/handle/PhabricatorObjectHandle.php +++ b/src/applications/phid/handle/PhabricatorObjectHandle.php @@ -1,101 +1,110 @@ uri = $uri; return $this; } public function getURI() { return $this->uri; } public function setPHID($phid) { $this->phid = $phid; return $this; } public function getPHID() { return $this->phid; } public function setName($name) { $this->name = $name; return $this; } public function getName() { return $this->name; } public function setFullName($full_name) { $this->fullName = $full_name; return $this; } public function getFullName() { return $this->fullName; } public function setType($type) { $this->type = $type; return $this; } public function getType() { return $this->type; } public function setEmail($email) { $this->email = $email; return $this; } public function getEmail() { return $this->email; } public function setImageURI($uri) { $this->imageURI = $uri; return $this; } public function getImageURI() { return $this->imageURI; } public function renderLink() { + + switch ($this->getType()) { + case 'USER': + $name = $this->getName(); + break; + default: + $name = $this->getFullName(); + } + return phutil_render_tag( 'a', array( 'href' => $this->getURI(), ), - phutil_escape_html($this->getName())); + phutil_escape_html($name)); } } diff --git a/src/applications/phid/handle/data/PhabricatorObjectHandleData.php b/src/applications/phid/handle/data/PhabricatorObjectHandleData.php index 3b69492b46..d6f0d3b06f 100644 --- a/src/applications/phid/handle/data/PhabricatorObjectHandleData.php +++ b/src/applications/phid/handle/data/PhabricatorObjectHandleData.php @@ -1,191 +1,193 @@ phids = $phids; } public function loadHandles() { $types = array(); foreach ($this->phids as $phid) { $type = $this->lookupType($phid); $types[$type][] = $phid; } $handles = array(); foreach ($types as $type => $phids) { switch ($type) { case 'USER': $class = 'PhabricatorUser'; PhutilSymbolLoader::loadClass($class); $object = newv($class, array()); $users = $object->loadAllWhere('phid IN (%Ls)', $phids); $users = mpull($users, null, 'getPHID'); foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); if (empty($users[$phid])) { $handle->setType(self::TYPE_UNKNOWN); $handle->setName('Unknown User'); } else { $user = $users[$phid]; $handle->setType($type); $handle->setName($user->getUsername()); $handle->setURI('/p/'.$user->getUsername().'/'); $handle->setEmail($user->getEmail()); $handle->setFullName( $user->getUsername().' ('.$user->getRealName().')'); $img_phid = $user->getProfileImagePHID(); if ($img_phid) { $handle->setImageURI( PhabricatorFileURI::getViewURIForPHID($img_phid)); } } $handles[$phid] = $handle; } break; case 'MLST': $class = 'PhabricatorMetaMTAMailingList'; PhutilSymbolLoader::loadClass($class); $object = newv($class, array()); $lists = $object->loadAllWhere('phid IN (%Ls)', $phids); $lists = mpull($lists, null, 'getPHID'); foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); if (empty($lists[$phid])) { $handle->setType(self::TYPE_UNKNOWN); $handle->setName('Unknown Mailing List'); } else { $list = $lists[$phid]; $handle->setType($type); $handle->setEmail($list->getEmail()); $handle->setName($list->getName()); $handle->setURI($list->getURI()); $handle->setFullName($list->getName()); } $handles[$phid] = $handle; } break; case 'DREV': $class = 'DifferentialRevision'; PhutilSymbolLoader::loadClass($class); $object = newv($class, array()); $revs = $object->loadAllWhere('phid in (%Ls)', $phids); $revs = mpull($revs, null, 'getPHID'); foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); if (empty($revs[$phid])) { $handle->setType(self::TYPE_UNKNOWN); $handle->setName('Unknown Revision'); } else { $rev = $revs[$phid]; $handle->setType($type); $handle->setName($rev->getTitle()); $handle->setURI('/D'.$rev->getID()); + $handle->setFullName('D'.$rev->getID().': '.$rev->getTitle()); } $handles[$phid] = $handle; } break; case 'TASK': $class = 'ManiphestTask'; PhutilSymbolLoader::loadClass($class); $object = newv($class, array()); $tasks = $object->loadAllWhere('phid in (%Ls)', $phids); $tasks = mpull($tasks, null, 'getPHID'); foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); if (empty($tasks[$phid])) { $handle->setType(self::TYPE_UNKNOWN); $handle->setName('Unknown Revision'); } else { $task = $tasks[$phid]; $handle->setType($type); $handle->setName($task->getTitle()); $handle->setURI('/T'.$task->getID()); + $handle->setFullName('T'.$task->getID().': '.$task->getTitle()); } $handles[$phid] = $handle; } break; case 'FILE': $class = 'PhabricatorFile'; PhutilSymbolLoader::loadClass($class); $object = newv($class, array()); $files = $object->loadAllWhere('phid IN (%Ls)', $phids); $files = mpull($files, null, 'getPHID'); foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setPHID($phid); if (empty($files[$phid])) { $handle->setType(self::TYPE_UNKNOWN); $handle->setName('Unknown File'); } else { $file = $files[$phid]; $handle->setType($type); $handle->setName($file->getName()); $handle->setURI($file->getViewURI()); } $handles[$phid] = $handle; } break; default: foreach ($phids as $phid) { $handle = new PhabricatorObjectHandle(); $handle->setType($type); $handle->setPHID($phid); $handle->setName('Unknown Object'); $handles[$phid] = $handle; } break; } } return $handles; } private function lookupType($phid) { $matches = null; if (preg_match('/^PHID-([^-]{4})-/', $phid, $matches)) { return $matches[1]; } return self::TYPE_UNKNOWN; } } diff --git a/src/view/control/objectselector/PhabricatorObjectSelectorDialog.php b/src/applications/phid/handle/view/selector/PhabricatorHandleObjectSelectorDataView.php similarity index 63% copy from src/view/control/objectselector/PhabricatorObjectSelectorDialog.php copy to src/applications/phid/handle/view/selector/PhabricatorHandleObjectSelectorDataView.php index 8a4018a256..954346464f 100644 --- a/src/view/control/objectselector/PhabricatorObjectSelectorDialog.php +++ b/src/applications/phid/handle/view/selector/PhabricatorHandleObjectSelectorDataView.php @@ -1,22 +1,35 @@ handle = $handle; + } + + public function renderData() { + $handle = $this->handle; + return array( + 'phid' => $handle->getPHID(), + 'name' => $handle->getFullName(), + 'href' => $handle->getURI(), + ); + } } diff --git a/src/applications/phid/handle/view/selector/__init__.php b/src/applications/phid/handle/view/selector/__init__.php new file mode 100644 index 0000000000..c49f68eb85 --- /dev/null +++ b/src/applications/phid/handle/view/selector/__init__.php @@ -0,0 +1,10 @@ +setName('Sawyer') * ->setBreed('Pug') * ->save(); * * Note that **Lisk automatically builds getters and setters for all of your * object's properties** via __call(). You can override these by defining * versions yourself. * * Calling save() will persist the object to the database. After calling * save(), you can call getID() to retrieve the object's ID. * * To load objects by ID, use the load() method: * * $dog = id(new Dog())->load($id); * * This will load the Dog record with ID $id into $dog, or ##null## if no such * record exists (load() is an instance method rather than a static method * because PHP does not support late static binding, at least until PHP 5.3). * * To update an object, change its properties and save it: * * $dog->setBreed('Lab')->save(); * * To delete an object, call delete(): * * $dog->delete(); * * That's Lisk CRUD in a nutshell. * * = Queries = * * Often, you want to load a bunch of objects, or execute a more specialized * query. Use loadAllWhere() or loadOneWhere() to do this: * * $pugs = $dog->loadAllWhere('breed = %s', 'Pug'); * $sawyer = $dog->loadOneWhere('name = %s', 'Sawyer'); * * These methods work like @{function:queryfx}, but only take half of a query * (the part after the WHERE keyword). Lisk will handle the connection, columns, * and object construction; you are responsible for the rest of it. * loadAllWhere() returns a list of objects, while loadOneWhere() returns a * single object (or null). * * @task config Configuring Lisk * @task load Loading Objects * @task info Examining Objects * @task save Writing Objects * @task hook Hooks and Callbacks * @task util Utilities * * @group storage */ abstract class LiskDAO { const CONFIG_OPTIMISTIC_LOCKS = 'enable-locks'; const CONFIG_IDS = 'id-mechanism'; const CONFIG_TIMESTAMPS = 'timestamps'; const CONFIG_AUX_PHID = 'auxiliary-phid'; const CONFIG_SERIALIZATION = 'col-serialization'; const SERIALIZATION_NONE = 'id'; const SERIALIZATION_JSON = 'json'; const SERIALIZATION_PHP = 'php'; const IDS_AUTOINCREMENT = 'ids-auto'; const IDS_PHID = 'ids-phid'; const IDS_MANUAL = 'ids-manual'; /** * Build an empty object. * * @return obj Empty object. */ public function __construct() { $id_key = $this->getIDKey(); if ($id_key) { $this->$id_key = null; } } abstract protected function establishConnection($mode); /* -( Configuring Lisk )--------------------------------------------------- */ /** * Change Lisk behaviors, like optimistic locks and timestamps. If you want * to change these behaviors, you should override this method in your child * class and change the options you're interested in. For example: * * public function getConfiguration() { * return array( * Lisk_DataAccessObject::CONFIG_EXAMPLE => true, * ) + parent::getConfiguration(); * } * * The available options are: * * CONFIG_OPTIMISTIC_LOCKS * Lisk automatically performs optimistic locking on objects, which protects * you from read-modify-write concurrency problems. Lock failures are * detected at write time and arise when two users read an object, then both * save it. In theory, you should detect these failures and accommodate them * in some sensible way (for instance, by showing the user differences * between the original record and the copy they are trying to update, and * prompting them to merge them). In practice, most Lisk tools are quick * and dirty and don't get to that level of sophistication, but optimistic * locks can still protect you from yourself sometimes. If you don't want * to use optimistic locks, you can disable them. The performance cost of * doing this locking is very very small (optimistic locks were chosen * because they're simple and cheap, and highly optimized for the case where * collisions are rare). By default, this option is OFF. * * CONFIG_IDS * Lisk objects need to have a unique identifying ID. The three mechanisms * available for generating this ID are IDS_AUTOINCREMENT (default, assumes * the ID column is an autoincrement primary key), IDS_PHID (to generate a * unique PHID for each object) or IDS_MANUAL (you are taking full * responsibility for ID management). * * CONFIG_TIMESTAMPS * Lisk can automatically handle keeping track of a `dateCreated' and * `dateModified' column, which it will update when it creates or modifies * an object. If you don't want to do this, you may disable this option. * By default, this option is ON. * * CONFIG_AUX_PHID * This option can be enabled by being set to some truthy value. The meaning * of this value is defined by your PHID generation mechanism. If this option * is enabled, a `phid' property will be populated with a unique PHID when an * object is created (or if it is saved and does not currently have one). You * need to override generatePHID() and hook it into your PHID generation * mechanism for this to work. By default, this option is OFF. * * CONFIG_SERIALIZATION * You can optionally provide a column serialization map that will be applied * to values when they are written to the database. For example: * * self::CONFIG_SERIALIZATION => array( * 'complex' => self::SERIALIZATION_JSON, * ) * * This will cause Lisk to JSON-serialize the 'complex' field before it is * written, and unserialize it when it is read. * * * @return dictionary Map of configuration options to values. * * @task config */ protected function getConfiguration() { return array( self::CONFIG_OPTIMISTIC_LOCKS => false, self::CONFIG_IDS => self::IDS_AUTOINCREMENT, self::CONFIG_TIMESTAMPS => true, ); } /** * Determine the setting of a configuration option for this class of objects. * * @param const Option name, one of the CONFIG_* constants. * @return mixed Option value, if configured (null if unavailable). * * @task config */ public function getConfigOption($option_name) { static $options = null; if (!isset($options)) { $options = $this->getConfiguration(); } return idx($options, $option_name); } /* -( Loading Objects )---------------------------------------------------- */ /** * Load an object by ID. You need to invoke this as an instance method, not * a class method, because PHP doesn't have late static binding (until * PHP 5.3.0). For example: * * $dog = id(new Dog())->load($dog_id); * * @param int Numeric ID identifying the object to load. * @return obj|null Identified object, or null if it does not exist. * * @task load */ public function load($id) { if (!($id = (int)$id)) { throw new Exception("Bogus ID provided to load()."); } return $this->loadOneWhere( '%C = %d', $this->getIDKeyForUse(), $id); } /** * Loads all of the objects, unconditionally. * * @return dict Dictionary of all persisted objects of this type, keyed * on object ID. * * @task load */ public function loadAll() { return $this->loadAllWhere('1 = 1'); } /** * Load all objects which match a WHERE clause. You provide everything after * the 'WHERE'; Lisk handles everything up to it. For example: * * $old_dogs = id(new Dog())->loadAllWhere('age > %d', 7); * * The pattern and arguments are as per queryfx(). * * @param string queryfx()-style SQL WHERE clause. * @param ... Zero or more conversions. * @return dict Dictionary of matching objects, keyed on ID. * * @task load */ public function loadAllWhere($pattern/*, $arg, $arg, $arg ... */) { $args = func_get_args(); $data = call_user_func_array( array($this, 'loadRawDataWhere'), $args); return $this->loadAllFromArray($data); } /** * Load a single object identified by a 'WHERE' clause. You provide * everything after the 'WHERE', and Lisk builds the first half of the * query. See loadAllWhere(). This method is similar, but returns a single * result instead of a list. * * @param string queryfx()-style SQL WHERE clause. * @param ... Zero or more conversions. * @return obj|null Matching object, or null if no object matches. * * @task load */ public function loadOneWhere($pattern/*, $arg, $arg, $arg ... */) { $args = func_get_args(); $data = call_user_func_array( array($this, 'loadRawDataWhere'), $args); if (count($data) > 1) { throw new AphrontQueryCountException( "More than 1 result from loadOneWhere()!"); } $data = reset($data); if (!$data) { return null; } return $this->loadFromArray($data); } protected function loadRawDataWhere($pattern/*, $arg, $arg, $arg ... */) { $connection = $this->getConnection('r'); $lock_clause = ''; if ($connection->isReadLocking()) { $lock_clause = 'FOR UPDATE'; } else if ($connection->isWriteLocking()) { $lock_clause = 'LOCK IN SHARE MODE'; } $args = func_get_args(); $args = array_slice($args, 1); $pattern = 'SELECT * FROM %T WHERE '.$pattern.' %Q'; array_unshift($args, $this->getTableName()); array_push($args, $lock_clause); array_unshift($args, $pattern); return call_user_func_array( array($connection, 'queryData'), $args); } /** * Reload an object from the database, discarding any changes to persistent * properties. If the object uses optimistic locks and you are in a locking * mode while transactional, this will effectively synchronize the locks. * This is pretty heady. It is unlikely you need to use this method. * * @return this * * @task load */ public function reload() { if (!$this->getID()) { throw new Exception("Unable to reload object that hasn't been loaded!"); } $use_locks = $this->getConfigOption(self::CONFIG_OPTIMISTIC_LOCKS); if (!$use_locks) { $result = $this->loadOneWhere( '%C = %d', $this->getIDKeyForUse(), $this->getID()); } else { $result = $this->loadOneWhere( '%C = %d AND %C = %d', $this->getIDKeyForUse(), $this->getID(), 'version', $this->getVersion()); } if (!$result) { throw new AphrontQueryObjectMissingException($use_locks); } return $this; } /** * Initialize this object's properties from a dictionary. Generally, you * load single objects with loadOneWhere(), but sometimes it may be more * convenient to pull data from elsewhere directly (e.g., a complicated * join via queryData()) and then load from an array representation. * * @param dict Dictionary of properties, which should be equivalent to * selecting a row from the table or calling getProperties(). * @return this * * @task load */ public function loadFromArray(array $row) { $map = array(); foreach ($row as $k => $v) { $map[$k] = $v; } $this->willReadData($map); foreach ($map as $prop => $value) { $this->$prop = $value; } $this->didReadData(); return $this; } /** * Initialize a list of objects from a list of dictionaries. Usually you * load lists of objects with loadAllWhere(), but sometimes that isn't * flexible enough. One case is if you need to do joins to select the right * objects: * * function loadAllWithOwner($owner) { * $data = $this->queryData( * 'SELECT d.* * FROM owner o * JOIN owner_has_dog od ON o.id = od.ownerID * JOIN dog d ON od.dogID = d.id * WHERE o.id = %d', * $owner); * return $this->loadAllFromArray($data); * } * * This is a lot messier than loadAllWhere(), but more flexible. * * @param list List of property dictionaries. * @return dict List of constructed objects, keyed on ID. * * @task load */ public function loadAllFromArray(array $rows) { $result = array(); $id_key = $this->getIDKey(); foreach ($rows as $row) { $obj = clone $this; if ($id_key) { $result[$row[$id_key]] = $obj->loadFromArray($row); } else { $result[] = $obj->loadFromArray($row); } } return $result; } /* -( Examining Objects )-------------------------------------------------- */ /** * Retrieve the unique, numerical ID identifying this object. This value * will be null if the object hasn't been persisted. * * @return int Unique numerical ID. * * @task info */ public function getID() { $id_key = $this->getIDKeyForUse(); return $this->$id_key; } /** * Retrieve a list of all object properties. Note that some may be * "transient", which means they should not be persisted to the database. * Transient properties can be identified by calling * getTransientProperties(). * * @return dict Dictionary of normalized (lowercase) to canonical (original * case) property names. * * @task info */ protected function getProperties() { static $properties = null; if (!isset($properties)) { $class = new ReflectionClass(get_class($this)); $properties = array(); foreach ($class->getProperties(ReflectionProperty::IS_PROTECTED) as $p) { $properties[strtolower($p->getName())] = $p->getName(); } $id_key = $this->getIDKey(); if ($id_key) { if (!isset($properties[strtolower($id_key)])) { $properties[strtolower($id_key)] = $id_key; } } if ($this->getConfigOption(self::CONFIG_OPTIMISTIC_LOCKS)) { $properties['version'] = 'version'; } if ($this->getConfigOption(self::CONFIG_TIMESTAMPS)) { $properties['datecreated'] = 'dateCreated'; $properties['datemodified'] = 'dateModified'; } if (!$this->isPHIDPrimaryID() && $this->getConfigOption(self::CONFIG_AUX_PHID)) { $properties['phid'] = 'phid'; } } return $properties; } /** * Check if a property exists on this object. * * @return string|null Canonical property name, or null if the property * does not exist. * * @task info */ protected function checkProperty($property) { static $properties = null; if ($properties === null) { $properties = $this->getProperties(); } $property = strtolower($property); if (empty($properties[$property])) { return null; } return $properties[$property]; } /** * Get or build the database connection for this object. * * @return LiskDatabaseConnection Lisk connection object. * * @task info */ protected function getConnection($mode) { if ($mode != 'r' && $mode != 'w') { throw new Exception("Unknown mode '{$mode}', should be 'r' or 'w'."); } // TODO: We don't do anything with the read/write mode right now, but // should. if (!isset($this->__connection)) { $this->__connection = $this->establishConnection($mode); } return $this->__connection; } /** * Convert this object into a property dictionary. This dictionary can be * restored into an object by using loadFromArray() (unless you're using * legacy features with CONFIG_CONVERT_CAMELCASE, but in that case you should * just go ahead and die in a fire). * * @return dict Dictionary of object properties. * * @task info */ protected function getPropertyValues() { $map = array(); foreach ($this->getProperties() as $p) { // We may receive a warning here for properties we've implicitly added // through configuration; squelch it. $map[$p] = @$this->$p; } return $map; } /** * Convert this object into a property dictionary containing only properties * which will be persisted to the database. * * @return dict Dictionary of persistent object properties. * * @task info */ protected function getPersistentPropertyValues() { $map = $this->getPropertyValues(); foreach ($this->getTransientProperties() as $p) { unset($map[$p]); } return $map; } /* -( Writing Objects )---------------------------------------------------- */ /** * Persist this object to the database. In most cases, this is the only * method you need to call to do writes. If the object has not yet been * inserted this will do an insert; if it has, it will do an update. * * @return this * * @task save */ public function save() { if ($this->shouldInsertWhenSaved()) { return $this->insert(); } else { return $this->update(); } } /** * Save this object, forcing the query to use REPLACE regardless of object * state. * * @return this * * @task save */ public function replace() { return $this->insertRecordIntoDatabase('REPLACE'); } /** * Save this object, forcing the query to use INSERT regardless of object * state. * * @return this * * @task save */ public function insert() { return $this->insertRecordIntoDatabase('INSERT'); } /** * Save this object, forcing the query to use UPDATE regardless of object * state. * * @return this * * @task save */ public function update() { $use_locks = $this->getConfigOption(self::CONFIG_OPTIMISTIC_LOCKS); $this->willSaveObject(); $data = $this->getPersistentPropertyValues(); $this->willWriteData($data); $map = array(); foreach ($data as $k => $v) { if ($use_locks && $k == 'version') { continue; } $map[$k] = $v; } $conn = $this->getConnection('w'); foreach ($map as $key => $value) { $map[$key] = qsprintf($conn, '%C = %ns', $key, $value); } $map = implode(', ', $map); if ($use_locks) { $conn->query( 'UPDATE %T SET %Q, version = version + 1 WHERE %C = %d AND %C = %d', $this->getTableName(), $map, $this->getIDKeyForUse(), $this->getID(), 'version', $this->getVersion()); + if ($conn->getAffectedRows() !== 1) { + throw new AphrontQueryObjectMissingException($use_locks); + } + $this->setVersion($this->getVersion() + 1); } else { $conn->query( 'UPDATE %T SET %Q WHERE %C = %d', $this->getTableName(), $map, $this->getIDKeyForUse(), $this->getID()); - } - - if ($conn->getAffectedRows() !== 1) { - throw new AphrontQueryObjectMissingException($use_locks); - } - - if ($use_locks) { - $this->setVersion($this->getVersion() + 1); + // We can't detect a missing object because updating an object without + // changing any values doesn't affect rows. We could jiggle timestamps + // to catch this for objects which track them if we wanted. } $this->didWriteData(); return $this; } /** * Delete this object, permanently. * * @return this * * @task save */ public function delete() { $this->willDelete(); $conn = $this->getConnection('w'); $conn->query( 'DELETE FROM %T WHERE %C = %d', $this->getTableName(), $this->getIDKeyForUse(), $this->getID()); $this->didDelete(); return $this; } /** * Internal implementation of INSERT and REPLACE. * * @param const Either "INSERT" or "REPLACE", to force the desired mode. * * @task save */ protected function insertRecordIntoDatabase($mode) { $this->willSaveObject(); $data = $this->getPersistentPropertyValues(); $id_mechanism = $this->getConfigOption(self::CONFIG_IDS); switch ($id_mechanism) { // If we are using autoincrement IDs, let MySQL assign the value for the // ID column. case self::IDS_AUTOINCREMENT: unset($data[$this->getIDKeyForUse()]); break; case self::IDS_PHID: if (empty($data[$this->getIDKeyForUse()])) { $phid = $this->generatePHID(); $this->setID($phid); $data[$this->getIDKeyForUse()] = $phid; } break; case self::IDS_MANUAL: break; default: throw new Exception('Unknown CONFIG_IDs mechanism!'); } if ($this->getConfigOption(self::CONFIG_OPTIMISTIC_LOCKS)) { $data['version'] = 0; } $this->willWriteData($data); $conn = $this->getConnection('w'); $columns = array_keys($data); foreach ($data as $key => $value) { $data[$key] = qsprintf($conn, '%ns', $value); } $data = implode(', ', $data); $conn->query( '%Q INTO %T (%LC) VALUES (%Q)', $mode, $this->getTableName(), $columns, $data); // Update the object with the initial Version value if ($this->getConfigOption(self::CONFIG_OPTIMISTIC_LOCKS)) { $this->setVersion(0); } // Only use the insert id if this table is using auto-increment ids if ($id_mechanism === self::IDS_AUTOINCREMENT) { $this->setID($conn->getInsertID()); } $this->didWriteData(); return $this; } /** * Method used to determine whether to insert or update when saving. * * @return bool true if the record should be inserted */ protected function shouldInsertWhenSaved() { $key_type = $this->getConfigOption(self::CONFIG_IDS); $use_locks = $this->getConfigOption(self::CONFIG_OPTIMISTIC_LOCKS); if ($key_type == self::IDS_MANUAL) { if ($use_locks) { // If we are manually keyed and the object has a version (which means // that it has been saved to the DB before), do an update, otherwise // perform an insert. if ($this->getID() && $this->getVersion() !== null) { return false; } else { return true; } } else { throw new Exception( 'You are not using optimistic locks, but are using manual IDs. You '. 'must override the shouldInsertWhenSaved() method to properly '. 'detect when to insert a new record.'); } } else { return !$this->getID(); } } /* -( Hooks and Callbacks )------------------------------------------------ */ /** * Retrieve the database table name. By default, this is the class name. * * @return string Table name for object storage. * * @task hook */ public function getTableName() { return get_class($this); } /** * Helper: Whether this class is configured to use PHIDs as the primary ID. * @task internal */ private function isPHIDPrimaryID() { return ($this->getConfigOption(self::CONFIG_IDS) === self::IDS_PHID); } /** * Retrieve the primary key column, "id" by default. If you can not * reasonably name your ID column "id", override this method. * * @return string Name of the ID column. * * @task hook */ public function getIDKey() { return $this->isPHIDPrimaryID() ? 'phid' : 'id'; } protected function getIDKeyForUse() { $id_key = $this->getIDKey(); if (!$id_key) { throw new Exception( "This DAO does not have a single-part primary key. The method you ". "called requires a single-part primary key."); } return $id_key; } /** * Generate a new PHID, used by CONFIG_AUX_PHID and IDS_PHID. * * @return phid Unique, newly allocated PHID. * * @task hook */ protected function generatePHID() { throw new Exception( "To use CONFIG_AUX_PHID or IDS_PHID, you need to overload ". "generatePHID() to perform PHID generation."); } /** * If your object has properties which you don't want to be persisted to the * database, you can override this method and specify them. * * @return list List of properties which should NOT be persisted. * Property names should be in normalized (lowercase) form. * By default, all properties are persistent. * * @task hook */ protected function getTransientProperties() { return array(); } /** * Hook to apply serialization or validation to data before it is written to * the database. See also willReadData(). * * @task hook */ protected function willWriteData(array &$data) { $this->applyLiskDataSerialization($data, false); } /** * Hook to perform actions after data has been written to the database. * * @task hook */ protected function didWriteData() {} /** * Hook to make internal object state changes prior to INSERT, REPLACE or * UPDATE. * * @task hook */ protected function willSaveObject() { $use_timestamps = $this->getConfigOption(self::CONFIG_TIMESTAMPS); if ($use_timestamps) { if (!$this->getDateCreated()) { $this->setDateCreated(time()); } $this->setDateModified(time()); } if (($this->isPHIDPrimaryID() && !$this->getID())) { // If PHIDs are the primary ID, the subclass could have overridden the // name of the ID column. $this->setID($this->generatePHID()); } else if ($this->getConfigOption(self::CONFIG_AUX_PHID) && !$this->getPHID()) { // The subclass could still want PHIDs. $this->setPHID($this->generatePHID()); } } /** * Hook to apply serialization or validation to data as it is read from the * database. See also willWriteData(). * * @task hook */ protected function willReadData(array &$data) { $this->applyLiskDataSerialization($data, $deserialize = true); } /** * Hook to perform an action on data after it is read from the database. * * @task hook */ protected function didReadData() {} /** * Hook to perform an action before the deletion of an object. * * @task hook */ protected function willDelete() {} /** * Hook to perform an action after the deletion of an object. * * @task hook */ protected function didDelete() {} /* -( Utilities )---------------------------------------------------------- */ /** * Applies configured serialization to a dictionary of values. * * @task util */ protected function applyLiskDataSerialization(array &$data, $deserialize) { $serialization = $this->getConfigOption(self::CONFIG_SERIALIZATION); if ($serialization) { foreach (array_intersect_key($serialization, $data) as $col => $format) { switch ($format) { case self::SERIALIZATION_NONE: break; case self::SERIALIZATION_PHP: if ($deserialize) { $data[$col] = unserialize($data[$col]); } else { $data[$col] = serialize($data[$col]); } break; case self::SERIALIZATION_JSON: if ($deserialize) { $data[$col] = json_decode($data[$col], true); } else { $data[$col] = json_encode($data[$col]); } break; default: throw new Exception("Unknown serialization format '{$format}'."); } } } } /** * Black magic. Builds implied get*() and set*() for all properties. * * @param string Method name. * @param list Argument vector. * @return mixed get*() methods return the property value. set*() methods * return $this. * @task util */ public function __call($method, $args) { if (!strncmp($method, 'get', 3)) { $property = substr($method, 3); if (!($property = $this->checkProperty($property))) { throw new Exception("Bad getter call: {$method}"); } if (count($args) !== 0) { throw new Exception("Getter call should have zero args: {$method}"); } if (isset($this->$property)) { return $this->$property; } return null; } if (!strncmp($method, 'set', 3)) { $property = substr($method, 3); $property = $this->checkProperty($property); if (!$property) { throw new Exception("Bad setter call: {$method}"); } if (count($args) !== 1) { throw new Exception("Setter should have exactly one arg: {$method}"); } if ($property == 'ID') { $property = $this->getIDKeyForUse(); } $this->$property = $args[0]; return $this; } throw new Exception("Unable to resolve method: {$method}."); } } diff --git a/src/view/control/objectselector/PhabricatorObjectSelectorDialog.php b/src/view/control/objectselector/PhabricatorObjectSelectorDialog.php index 8a4018a256..a35983caa9 100644 --- a/src/view/control/objectselector/PhabricatorObjectSelectorDialog.php +++ b/src/view/control/objectselector/PhabricatorObjectSelectorDialog.php @@ -1,22 +1,164 @@ user = $user; + return $this; + } + + public function setFilters(array $filters) { + $this->filters = $filters; + return $this; + } + + public function setHandles(array $handles) { + $this->handles = $handles; + return $this; + } + + public function setCancelURI($cancel_uri) { + $this->cancelURI = $cancel_uri; + return $this; + } + + public function setSubmitURI($submit_uri) { + $this->submitURI = $submit_uri; + return $this; + } + + public function setSearchURI($search_uri) { + $this->searchURI = $search_uri; + return $this; + } + + public function setNoun($noun) { + $this->noun = $noun; + return $this; + } + + public function buildDialog() { + $user = $this->user; + + $filter_id = celerity_generate_unique_node_id(); + $query_id = celerity_generate_unique_node_id(); + $results_id = celerity_generate_unique_node_id(); + $current_id = celerity_generate_unique_node_id(); + $search_id = celerity_generate_unique_node_id(); + $form_id = celerity_generate_unique_node_id(); + + require_celerity_resource('phabricator-object-selector-css'); + + $options = array(); + foreach ($this->filters as $key => $label) { + $options[] = phutil_render_tag( + 'option', + array( + 'value' => $key + ), + $label); + } + $options = implode("\n", $options); + + $search_box = phabricator_render_form( + $user, + array( + 'method' => 'POST', + 'action' => $this->submitURI, + 'id' => $search_id, + ), + ' + + + + + + '); + $result_box = + '
    '. + '
    '; + $attached_box = + '
    '. + '
    '. + '
    '. + 'Currently Attached '.$this->noun. + '
    '. + '
    '. + '
    '. + '
    '. + '
    '; + + + $dialog = new AphrontDialogView(); + $dialog + ->setUser($this->user) + ->setTitle('Manage Attached '.$this->noun) + ->setClass('phabricator-object-selector-dialog') + ->appendChild($search_box) + ->appendChild($result_box) + ->appendChild($attached_box) + ->setRenderDialogAsDiv() + ->setFormID($form_id) + ->addSubmitButton('Save '.$this->noun); + + if ($this->cancelURI) { + $dialog->addCancelButton($this->cancelURI); + } + + $handle_views = array(); + foreach ($this->handles as $phid => $handle) { + $view = new PhabricatorHandleObjectSelectorDataView($handle); + $handle_views[$phid] = $view->renderData(); + } + $dialog->addHiddenInput('phids', implode(';', array_keys($this->handles))); + + + Javelin::initBehavior( + 'phabricator-object-selector', + array( + 'filter' => $filter_id, + 'query' => $query_id, + 'search' => $search_id, + 'results' => $results_id, + 'current' => $current_id, + 'form' => $form_id, + 'uri' => $this->searchURI, + 'handles' => $handle_views, + )); + + return $dialog; + } } diff --git a/src/view/control/objectselector/__init__.php b/src/view/control/objectselector/__init__.php index 071915f630..319c6a84a3 100644 --- a/src/view/control/objectselector/__init__.php +++ b/src/view/control/objectselector/__init__.php @@ -1,12 +1,18 @@ user = $user; return $this; } public function setSubmitURI($uri) { $this->submitURI = $uri; return $this; } public function setTitle($title) { $this->title = $title; return $this; } public function getTitle() { return $this->title; } public function addSubmitButton($text = 'Okay') { $this->submitButton = $text; return $this; } public function addCancelButton($uri) { $this->cancelURI = $uri; return $this; } public function addHiddenInput($key, $value) { - $this->hidden[$key] = $value; + $this->hidden[] = array($key, $value); return $this; } public function setClass($class) { $this->class = $class; return $this; } + public function setRenderDialogAsDiv() { + // TODO: This API is awkward. + $this->renderAsForm = false; + return $this; + } + + public function setFormID($id) { + $this->formID = $id; + return $this; + } + final public function render() { require_celerity_resource('aphront-dialog-view-css'); $buttons = array(); if ($this->submitButton) { $buttons[] = phutil_render_tag( 'button', array( 'name' => '__submit__', 'sigil' => '__default__', ), phutil_escape_html($this->submitButton)); } if ($this->cancelURI) { $buttons[] = javelin_render_tag( 'a', array( 'href' => $this->cancelURI, 'class' => 'button grey', 'name' => '__cancel__', 'sigil' => 'jx-workflow-button', ), 'Cancel'); } + $buttons = implode('', $buttons); if (!$this->user) { throw new Exception( "You must call setUser() when rendering an AphrontDialogView."); } - $csrf = $this->user->getCSRFToken(); + + $more = $this->class; + + $attributes = array( + 'class' => 'aphront-dialog-view '.$more, + 'sigil' => 'jx-dialog', + ); + + $form_attributes = array( + 'action' => $this->submitURI, + 'method' => 'post', + 'id' => $this->formID, + ); $hidden_inputs = array(); - foreach ($this->hidden as $key => $value) { - $hidden_inputs[] = phutil_render_tag( + foreach ($this->hidden as $desc) { + list($key, $value) = $desc; + $hidden_inputs[] = javelin_render_tag( 'input', array( 'type' => 'hidden', 'name' => $key, 'value' => $value, + 'sigil' => 'aphront-dialog-application-input' )); } $hidden_inputs = implode("\n", $hidden_inputs); + $hidden_inputs = + ''. + $hidden_inputs; - $more = $this->class; - return javelin_render_tag( - 'form', - array( - 'class' => 'aphront-dialog-view '.$more, - 'action' => $this->submitURI, - 'method' => 'post', - 'sigil' => 'jx-dialog', - ), - ''. - ''. - ''. - $hidden_inputs. + if (!$this->renderAsForm) { + $buttons = phabricator_render_form( + $this->user, + $form_attributes, + $hidden_inputs.$buttons); + } + + $content = '
    '. phutil_escape_html($this->title). '
    '. '
    '. $this->renderChildren(). '
    '. '
    '. - implode('', $buttons). + $buttons. '
    '. - '
    '); + ''; + + if ($this->renderAsForm) { + return phabricator_render_form( + $this->user, + $form_attributes + $attributes, + $hidden_inputs. + $content); + } else { + return javelin_render_tag( + 'div', + $attributes, + $content); + } } } diff --git a/webroot/rsrc/css/aphront/dialog-view.css b/webroot/rsrc/css/aphront/dialog-view.css index 6c47fc9c69..00c27559cb 100644 --- a/webroot/rsrc/css/aphront/dialog-view.css +++ b/webroot/rsrc/css/aphront/dialog-view.css @@ -1,60 +1,68 @@ /** * @provides aphront-dialog-view-css */ .aphront-dialog-view { width: 480px; padding: 8px; background: #666; margin: auto; } .aphront-dialog-head { background: #003366; border: none; font-size: 15px; padding: 5px 12px 6px; color: #ffffff; } .aphront-dialog-body { background: #ffffff; padding: 16px 12px; border: none; overflow: hidden; } .aphront-dialog-tail { border: none; background: #ededed; padding: 0.5em; text-align: right; } .aphront-dialog-tail button, .aphront-dialog-tail a.button { float: right; margin-left: .5em; } .jx-client-dialog { position: absolute; z-index: 6; } .jx-mask { opacity: .5; -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=75)"; filter: alpha(opacity=75); background: #999; position: absolute; z-index: 5; top: 0; left: 0; right: 0; bottom: 0; width: 100%; height: 100%; min-height: 100%; } + +.aphront-exception-dialog { + width: 95%; +} + +.aphront-exception-dialog .aphront-dialog-head { + background: #aa0000; +} diff --git a/webroot/rsrc/css/application/objectselector/object-selector.css b/webroot/rsrc/css/application/objectselector/object-selector.css index 50421f4ba1..0b2e23eff2 100644 --- a/webroot/rsrc/css/application/objectselector/object-selector.css +++ b/webroot/rsrc/css/application/objectselector/object-selector.css @@ -1,81 +1,88 @@ /** * @provides phabricator-object-selector-css * @requires aphront-dialog-view-css */ -form.phabricator-object-selector-dialog { - width: 800px; +.phabricator-object-selector-dialog { + width: 960px; } -form.phabricator-object-selector-dialog .aphront-dialog-body { +.phabricator-object-selector-dialog .aphront-dialog-body { padding: 0; } .phabricator-object-selector-search { width: 100%; background: #ededed; } .phabricator-object-selector-search td { padding: 4px 8px; } td.phabricator-object-selector-search-text { width: 100%; } .phabricator-object-selector-search-text input { width: 100%; } .phabricator-object-selector-results { position: relative; - height: 16em; + height: 24em; border: solid #bbbbbb; border-width: 1px 0px; overflow-y: scroll; overflow-x: hidden; } .phabricator-object-selector-handle { width: 100%; background: #e9e9e9; margin-bottom: 1px; } .phabricator-object-selector-handle td { padding: 4px 1em; } .phabricator-object-selector-handle th { padding: 4px 1em; font-weight: bold; vertical-align: middle; width: 100%; overflow: hidden; } .phabricator-object-selector-header { padding: 2px; border-bottom: 1px solid #d0d0d0; margin-bottom: 16px; color: #444444; } .phabricator-object-selector-attach-explicit { padding: 4px; background: #f3f3f3; border: solid #bbbbbb; border-width: 1px 0px; } .phabricator-object-selector-currently-attached { background: #fff; padding: 16px; border: 1px solid #dddddd; } .phabricator-object-selector-current { background: #ededed; padding: 8px 8px; } + + +.object-selector-nothing { + padding: 1em; + color: #888888; + text-align: center; +} diff --git a/webroot/rsrc/js/application/core/behavior-object-selector.js b/webroot/rsrc/js/application/core/behavior-object-selector.js index 00e0fc547a..58d43002bc 100644 --- a/webroot/rsrc/js/application/core/behavior-object-selector.js +++ b/webroot/rsrc/js/application/core/behavior-object-selector.js @@ -1,139 +1,159 @@ /** * @provides javelin-behavior-phabricator-object-selector * @requires javelin-lib-dev */ JX.behavior('phabricator-object-selector', function(config) { var n = 0; var phids = {}; - var handles = {}; + var handles = config.handles; + for (var k in handles) { + phids[k] = true; + } var attach_list = {}; + var phid_input = JX.DOM.find( + JX.$(config.form), + 'input', + 'aphront-dialog-application-input'); + function onreceive(seq, r) { if (seq != n) { return; } var display = []; attach_list = {}; for (var k in r) { handles[r[k].phid] = r[k]; display.push(renderHandle(r[k], true)); } if (!display.length) { display = renderNote('No results.'); } JX.DOM.setContent(JX.$(config.results), display); } function redrawAttached() { var display = []; for (var k in phids) { display.push(renderHandle(handles[k], false)); } if (!display.length) { display = renderNote('Nothing attached.'); } JX.DOM.setContent(JX.$(config.current), display); + phid_input.value = JX.keys(phids).join(';'); } function renderHandle(h, attach) { + var link = JX.$N( + 'a', + {href : h.uri, target : '_blank'}, + h.name); + var td = JX.$N('td'); + var table = JX.$N( 'table', {className: 'phabricator-object-selector-handle'}, JX.$N( 'tbody', {}, - [JX.$N('th', {}, h.name), td])); + [JX.$N('th', {}, link), td])); + var btn = JX.$N( 'a', {className: 'button small grey'}, attach ? 'Attach' : 'Remove'); JX.Stratcom.addSigil(btn, 'object-attach-button'); JX.Stratcom.addData(btn, {handle : h, table : table}); if (attach) { attach_list[h.phid] = btn; + if (h.phid in phids) { + JX.DOM.alterClass(btn, 'disabled', true); + btn.disabled = true; + } } JX.DOM.setContent(td, btn); return table; } function renderNote(note) { - return JX.$N('div', {}, note); + return JX.$N('div', {className : 'object-selector-nothing'}, note); } function sendQuery() { JX.DOM.setContent(JX.$(config.results), renderNote('Loading...')) new JX.Request(config.uri, JX.bind(null, onreceive, ++n)) .setData({ filter: JX.$(config.filter).value, query: JX.$(config.query).value }) .send(); } JX.DOM.listen( JX.$(config.search), - 'click', + 'submit', null, function(e) { e.kill(); sendQuery(); }); JX.DOM.listen( JX.$(config.results), 'click', 'object-attach-button', function(e) { e.kill(); var button = e.getNode('object-attach-button'); if (button.disabled) { return; } var data = e.getNodeData('object-attach-button'); phids[data.handle.phid] = true; JX.DOM.alterClass(button, 'disabled', true); button.disabled = true; redrawAttached(); }); JX.DOM.listen( JX.$(config.current), 'click', 'object-attach-button', function(e) { e.kill(); var button = e.getNode('object-attach-button'); if (button.disabled) { return; } var data = e.getNodeData('object-attach-button'); delete phids[data.handle.phid]; if (attach_list[data.handle.phid]) { JX.DOM.alterClass(attach_list[data.handle.phid], 'disabled', false); attach_list[data.handle.phid].disabled = false; } redrawAttached(); }); sendQuery(); redrawAttached(); });