diff --git a/resources/builtin/image-526x526.png b/resources/builtin/image-526x526.png new file mode 100644 index 0000000000..853539d21a Binary files /dev/null and b/resources/builtin/image-526x526.png differ diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 9003476d2d..644ec2fb2f 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,18 +7,18 @@ */ return array( 'names' => array( - 'core.pkg.css' => 'e33b14a4', - 'core.pkg.js' => 'ef5e33db', + 'core.pkg.css' => 'b59766ad', + 'core.pkg.js' => 'd7daa6d8', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '2de124c9', - 'differential.pkg.js' => '5c2ba922', + 'differential.pkg.js' => 'd0cd0df6', 'diffusion.pkg.css' => 'f45955ed', 'diffusion.pkg.js' => '3a9a8bfa', 'maniphest.pkg.css' => '4845691a', 'maniphest.pkg.js' => '949a7498', 'rsrc/css/aphront/aphront-bars.css' => '231ac33c', 'rsrc/css/aphront/dark-console.css' => '6378ef3d', - 'rsrc/css/aphront/dialog-view.css' => 'be0e3a46', + 'rsrc/css/aphront/dialog-view.css' => 'b4334e08', 'rsrc/css/aphront/lightbox-attachment.css' => '7acac05d', 'rsrc/css/aphront/list-filter-view.css' => '5d6f0526', 'rsrc/css/aphront/multi-column.css' => 'fd18389d', @@ -36,7 +36,7 @@ return array( 'rsrc/css/application/base/notification-menu.css' => 'f31c0bde', 'rsrc/css/application/base/phabricator-application-launch-view.css' => '95351601', 'rsrc/css/application/base/phui-theme.css' => 'ab7b848c', - 'rsrc/css/application/base/standard-page-view.css' => 'c4467133', + 'rsrc/css/application/base/standard-page-view.css' => 'e709f6d0', 'rsrc/css/application/chatlog/chatlog.css' => 'd295b020', 'rsrc/css/application/conduit/conduit-api.css' => '7bc725c4', 'rsrc/css/application/config/config-options.css' => '0ede4c9b', @@ -93,7 +93,7 @@ return array( 'rsrc/css/application/policy/policy-transaction-detail.css' => '82100a43', 'rsrc/css/application/policy/policy.css' => '957ea14c', 'rsrc/css/application/ponder/ponder-view.css' => '7b0df4da', - 'rsrc/css/application/project/project-card-view.css' => '9c3631e5', + 'rsrc/css/application/project/project-card-view.css' => '9418c97d', 'rsrc/css/application/project/project-view.css' => '4693497c', 'rsrc/css/application/releeph/releeph-core.css' => '9b3c5733', 'rsrc/css/application/releeph/releeph-preview-branch.css' => 'b7a6f4a5', @@ -106,7 +106,7 @@ return array( 'rsrc/css/core/core.css' => '5b3563c8', 'rsrc/css/core/remarkup.css' => 'e1c8b32f', 'rsrc/css/core/syntax.css' => '9fd11da8', - 'rsrc/css/core/z-index.css' => '5c7025bf', + 'rsrc/css/core/z-index.css' => '5b6fcf3f', 'rsrc/css/diviner/diviner-shared.css' => 'aa3656aa', 'rsrc/css/font/font-aleo.css' => '8bdb2835', 'rsrc/css/font/font-awesome.css' => 'c43323c5', @@ -126,7 +126,7 @@ return array( 'rsrc/css/phui/phui-box.css' => '6e8ac7fd', 'rsrc/css/phui/phui-button.css' => 'd6ac72db', 'rsrc/css/phui/phui-chart.css' => '6bf6f78e', - 'rsrc/css/phui/phui-crumbs-view.css' => '414406b5', + 'rsrc/css/phui/phui-crumbs-view.css' => '79d536e5', 'rsrc/css/phui/phui-document-pro.css' => '8799acf7', 'rsrc/css/phui/phui-document-summary.css' => '9ca48bdf', 'rsrc/css/phui/phui-document.css' => '9c71d2bf', @@ -143,20 +143,21 @@ return array( 'rsrc/css/phui/phui-info-view.css' => '6d7c3509', 'rsrc/css/phui/phui-list.css' => '9da2aa00', 'rsrc/css/phui/phui-object-box.css' => '407eaf5a', - 'rsrc/css/phui/phui-object-item-list-view.css' => 'fe594a65', + 'rsrc/css/phui/phui-object-item-list-view.css' => 'be31c3a7', 'rsrc/css/phui/phui-pager.css' => 'bea33d23', 'rsrc/css/phui/phui-pinboard-view.css' => '2495140e', - 'rsrc/css/phui/phui-profile-menu.css' => 'ab4fcf5f', + 'rsrc/css/phui/phui-profile-menu.css' => 'f709256c', 'rsrc/css/phui/phui-property-list-view.css' => '27b2849e', 'rsrc/css/phui/phui-remarkup-preview.css' => '1a8f2591', + 'rsrc/css/phui/phui-segment-bar-view.css' => '46342871', 'rsrc/css/phui/phui-spacing.css' => '042804d6', 'rsrc/css/phui/phui-status.css' => '888cedb8', 'rsrc/css/phui/phui-tag-view.css' => '9d5d4400', 'rsrc/css/phui/phui-timeline-view.css' => '2efceff8', 'rsrc/css/phui/phui-two-column-view.css' => 'c75bfc5b', - 'rsrc/css/phui/workboards/phui-workboard.css' => 'b07a5524', - 'rsrc/css/phui/workboards/phui-workcard.css' => 'b4322ca7', - 'rsrc/css/phui/workboards/phui-workpanel.css' => 'e1bd8d04', + 'rsrc/css/phui/workboards/phui-workboard.css' => 'e9e56029', + 'rsrc/css/phui/workboards/phui-workcard.css' => '3646fb96', + 'rsrc/css/phui/workboards/phui-workpanel.css' => 'a78c0661', 'rsrc/css/sprite-login.css' => '60e8560e', 'rsrc/css/sprite-menu.css' => '9dd65b92', 'rsrc/css/sprite-tokens.css' => '4f399012', @@ -414,7 +415,11 @@ return array( 'rsrc/js/application/phortune/phortune-credit-card-form.js' => '2290aeef', 'rsrc/js/application/policy/behavior-policy-control.js' => 'd0c516d5', 'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '5e9f347c', - 'rsrc/js/application/projects/behavior-project-boards.js' => '48470f95', + 'rsrc/js/application/projects/WorkboardBoard.js' => '52291776', + 'rsrc/js/application/projects/WorkboardCard.js' => 'c587b80f', + 'rsrc/js/application/projects/WorkboardColumn.js' => 'f05d6e5d', + 'rsrc/js/application/projects/WorkboardController.js' => '55baf5ed', + 'rsrc/js/application/projects/behavior-project-boards.js' => '14a1faae', 'rsrc/js/application/projects/behavior-project-create.js' => '065227cc', 'rsrc/js/application/projects/behavior-reorder-columns.js' => 'e1d25dfb', 'rsrc/js/application/releeph/releeph-preview-branch.js' => 'b2b4fbaf', @@ -446,9 +451,9 @@ return array( 'rsrc/js/application/uiexample/gesture-example.js' => '558829c2', 'rsrc/js/application/uiexample/notification-example.js' => '8ce821c5', 'rsrc/js/core/Busy.js' => '59a7976a', - 'rsrc/js/core/DragAndDropFileUpload.js' => 'ad10aeac', - 'rsrc/js/core/DraggableList.js' => '8905523d', - 'rsrc/js/core/FileUpload.js' => '477359c8', + 'rsrc/js/core/DragAndDropFileUpload.js' => '81f182b5', + 'rsrc/js/core/DraggableList.js' => '5a13c79f', + 'rsrc/js/core/FileUpload.js' => '680ea2c8', 'rsrc/js/core/Hovercard.js' => '1bd28176', 'rsrc/js/core/KeyboardShortcut.js' => '1ae869f2', 'rsrc/js/core/KeyboardShortcutManager.js' => 'c1700f6f', @@ -506,14 +511,14 @@ return array( 'rsrc/js/phuix/PHUIXActionView.js' => '8cf6d262', 'rsrc/js/phuix/PHUIXAutocomplete.js' => '9196fb06', 'rsrc/js/phuix/PHUIXDropdownMenu.js' => 'bd4c8dca', - 'rsrc/js/phuix/PHUIXFormControl.js' => '8fba1997', + 'rsrc/js/phuix/PHUIXFormControl.js' => 'a7763e11', 'rsrc/js/phuix/PHUIXIconView.js' => 'bff6884b', ), 'symbols' => array( 'almanac-css' => 'dbb9b3af', 'aphront-bars' => '231ac33c', 'aphront-dark-console-css' => '6378ef3d', - 'aphront-dialog-view-css' => 'be0e3a46', + 'aphront-dialog-view-css' => 'b4334e08', 'aphront-list-filter-view-css' => '5d6f0526', 'aphront-multi-column-view-css' => 'fd18389d', 'aphront-panel-view-css' => '8427b78d', @@ -654,7 +659,7 @@ return array( 'javelin-behavior-phui-profile-menu' => '12884df9', 'javelin-behavior-policy-control' => 'd0c516d5', 'javelin-behavior-policy-rule-editor' => '5e9f347c', - 'javelin-behavior-project-boards' => '48470f95', + 'javelin-behavior-project-boards' => '14a1faae', 'javelin-behavior-project-create' => '065227cc', 'javelin-behavior-quicksand-blacklist' => '7927a7d3', 'javelin-behavior-recurring-edit' => '5f1c4d5f', @@ -721,6 +726,10 @@ return array( 'javelin-view-renderer' => '6c2b09a2', 'javelin-view-visitor' => 'efe49472', 'javelin-websocket' => 'e292eaf4', + 'javelin-workboard-board' => '52291776', + 'javelin-workboard-card' => 'c587b80f', + 'javelin-workboard-column' => 'f05d6e5d', + 'javelin-workboard-controller' => '55baf5ed', 'javelin-workflow' => '5b2e3e2b', 'lightbox-attachment-css' => '7acac05d', 'maniphest-batch-editor' => 'b0f0b6d5', @@ -741,11 +750,11 @@ return array( 'phabricator-core-css' => '5b3563c8', 'phabricator-countdown-css' => 'e7544472', 'phabricator-dashboard-css' => 'eb458607', - 'phabricator-drag-and-drop-file-upload' => 'ad10aeac', - 'phabricator-draggable-list' => '8905523d', + 'phabricator-drag-and-drop-file-upload' => '81f182b5', + 'phabricator-draggable-list' => '5a13c79f', 'phabricator-fatal-config-template-css' => '8e6c6fcd', 'phabricator-feed-css' => 'ecd4ec57', - 'phabricator-file-upload' => '477359c8', + 'phabricator-file-upload' => '680ea2c8', 'phabricator-filetree-view-css' => 'fccf9f82', 'phabricator-flag-css' => '5337623f', 'phabricator-keyboard-shortcut' => '1ae869f2', @@ -764,7 +773,7 @@ return array( 'phabricator-side-menu-view-css' => '3a3d9f41', 'phabricator-slowvote-css' => 'da0afb1b', 'phabricator-source-code-view-css' => 'cbeef983', - 'phabricator-standard-page-view' => 'c4467133', + 'phabricator-standard-page-view' => 'e709f6d0', 'phabricator-textareautils' => '9e54692d', 'phabricator-title' => 'df5e11d2', 'phabricator-tooltip' => '6323f942', @@ -779,7 +788,7 @@ return array( 'phabricator-uiexample-reactor-select' => 'a155550f', 'phabricator-uiexample-reactor-sendclass' => '1def2711', 'phabricator-uiexample-reactor-sendproperties' => 'b1f0ccee', - 'phabricator-zindex-css' => '5c7025bf', + 'phabricator-zindex-css' => '5b6fcf3f', 'phame-css' => '1dbbacf9', 'pholio-css' => '95174bdd', 'pholio-edit-css' => '3ad9d1ee', @@ -799,7 +808,7 @@ return array( 'phui-calendar-list-css' => 'c1c7f338', 'phui-calendar-month-css' => '476be7e0', 'phui-chart-css' => '6bf6f78e', - 'phui-crumbs-view-css' => '414406b5', + 'phui-crumbs-view-css' => '79d536e5', 'phui-document-summary-view-css' => '9ca48bdf', 'phui-document-view-css' => '9c71d2bf', 'phui-document-view-pro-css' => '8799acf7', @@ -819,32 +828,33 @@ return array( 'phui-inline-comment-view-css' => '0fdb3667', 'phui-list-view-css' => '9da2aa00', 'phui-object-box-css' => '407eaf5a', - 'phui-object-item-list-view-css' => 'fe594a65', + 'phui-object-item-list-view-css' => 'be31c3a7', 'phui-pager-css' => 'bea33d23', 'phui-pinboard-view-css' => '2495140e', - 'phui-profile-menu-css' => 'ab4fcf5f', + 'phui-profile-menu-css' => 'f709256c', 'phui-property-list-view-css' => '27b2849e', 'phui-remarkup-preview-css' => '1a8f2591', + 'phui-segment-bar-view-css' => '46342871', 'phui-spacing-css' => '042804d6', 'phui-status-list-view-css' => '888cedb8', 'phui-tag-view-css' => '9d5d4400', 'phui-theme-css' => 'ab7b848c', 'phui-timeline-view-css' => '2efceff8', 'phui-two-column-view-css' => 'c75bfc5b', - 'phui-workboard-view-css' => 'b07a5524', - 'phui-workcard-view-css' => 'b4322ca7', - 'phui-workpanel-view-css' => 'e1bd8d04', + 'phui-workboard-view-css' => 'e9e56029', + 'phui-workcard-view-css' => '3646fb96', + 'phui-workpanel-view-css' => 'a78c0661', 'phuix-action-list-view' => 'b5c256b8', 'phuix-action-view' => '8cf6d262', 'phuix-autocomplete' => '9196fb06', 'phuix-dropdown-menu' => 'bd4c8dca', - 'phuix-form-control-view' => '8fba1997', + 'phuix-form-control-view' => 'a7763e11', 'phuix-icon-view' => 'bff6884b', 'policy-css' => '957ea14c', 'policy-edit-css' => '815c66f7', 'policy-transaction-detail-css' => '82100a43', 'ponder-view-css' => '7b0df4da', - 'project-card-view-css' => '9c3631e5', + 'project-card-view-css' => '9418c97d', 'project-view-css' => '4693497c', 'releeph-core' => '9b3c5733', 'releeph-preview-branch' => 'b7a6f4a5', @@ -956,6 +966,15 @@ return array( 'javelin-dom', 'javelin-history', ), + '14a1faae' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-util', + 'javelin-vector', + 'javelin-stratcom', + 'javelin-workflow', + 'javelin-workboard-controller', + ), '1ad0a787' => array( 'javelin-install', 'javelin-reactor', @@ -1133,11 +1152,6 @@ return array( 'javelin-dom', 'javelin-workflow', ), - '477359c8' => array( - 'javelin-install', - 'javelin-dom', - 'phabricator-notification', - ), 47830651 => array( 'javelin-behavior', 'javelin-dom', @@ -1154,15 +1168,6 @@ return array( 'javelin-dom', 'javelin-workflow', ), - '48470f95' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-util', - 'javelin-vector', - 'javelin-stratcom', - 'javelin-workflow', - 'phabricator-draggable-list', - ), '49b73b36' => array( 'javelin-behavior', 'javelin-dom', @@ -1209,6 +1214,15 @@ return array( 'javelin-dom', 'javelin-reactor-dom', ), + 52291776 => array( + 'javelin-install', + 'javelin-dom', + 'javelin-util', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-draggable-list', + 'javelin-workboard-column', + ), '5359e785' => array( 'javelin-install', 'javelin-util', @@ -1239,6 +1253,16 @@ return array( 'javelin-vector', 'javelin-dom', ), + '55baf5ed' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-util', + 'javelin-vector', + 'javelin-stratcom', + 'javelin-workflow', + 'phabricator-drag-and-drop-file-upload', + 'javelin-workboard-board', + ), '56a1ca03' => array( 'javelin-behavior', 'javelin-behavior-device', @@ -1265,6 +1289,14 @@ return array( 'javelin-dom', 'javelin-stratcom', ), + '5a13c79f' => array( + 'javelin-install', + 'javelin-dom', + 'javelin-stratcom', + 'javelin-util', + 'javelin-vector', + 'javelin-magical-init', + ), '5b2e3e2b' => array( 'javelin-stratcom', 'javelin-request', @@ -1331,6 +1363,11 @@ return array( 'javelin-request', 'javelin-workflow', ), + '680ea2c8' => array( + 'javelin-install', + 'javelin-dom', + 'phabricator-notification', + ), '6882e80a' => array( 'javelin-dom', ), @@ -1450,6 +1487,14 @@ return array( 'javelin-vector', 'javelin-stratcom', ), + '81f182b5' => array( + 'javelin-install', + 'javelin-util', + 'javelin-request', + 'javelin-dom', + 'javelin-uri', + 'phabricator-file-upload', + ), '834a1173' => array( 'javelin-behavior', 'javelin-scrollbar', @@ -1487,14 +1532,6 @@ return array( 'javelin-stratcom', 'javelin-dom', ), - '8905523d' => array( - 'javelin-install', - 'javelin-dom', - 'javelin-stratcom', - 'javelin-util', - 'javelin-vector', - 'javelin-magical-init', - ), '8a41885b' => array( 'javelin-install', 'javelin-dom', @@ -1525,10 +1562,6 @@ return array( 'javelin-stratcom', 'javelin-install', ), - '8fba1997' => array( - 'javelin-install', - 'javelin-dom', - ), '901935ef' => array( 'javelin-behavior', 'javelin-dom', @@ -1622,6 +1655,13 @@ return array( 'javelin-uri', 'phabricator-notification', ), + 'a7763e11' => array( + 'javelin-install', + 'javelin-dom', + ), + 'a78c0661' => array( + 'phui-workcard-view-css', + ), 'a80d0378' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1674,14 +1714,6 @@ return array( 'javelin-util', 'phabricator-busy', ), - 'ad10aeac' => array( - 'javelin-install', - 'javelin-util', - 'javelin-request', - 'javelin-dom', - 'javelin-uri', - 'phabricator-file-upload', - ), 'b064af76' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1786,6 +1818,9 @@ return array( 'javelin-dom', 'javelin-vector', ), + 'c587b80f' => array( + 'javelin-install', + ), 'c72aa091' => array( 'javelin-behavior', 'javelin-dom', @@ -1918,9 +1953,6 @@ return array( 'javelin-dom', 'phabricator-prefab', ), - 'e1bd8d04' => array( - 'phui-workcard-view-css', - ), 'e1d25dfb' => array( 'javelin-behavior', 'javelin-stratcom', @@ -2023,6 +2055,10 @@ return array( 'javelin-workflow', 'javelin-json', ), + 'f05d6e5d' => array( + 'javelin-install', + 'javelin-workboard-card', + ), 'f411b6ae' => array( 'javelin-behavior', 'javelin-stratcom', diff --git a/resources/sql/autopatches/20160206.cover.1.sql b/resources/sql/autopatches/20160206.cover.1.sql new file mode 100644 index 0000000000..1e8b473a5e --- /dev/null +++ b/resources/sql/autopatches/20160206.cover.1.sql @@ -0,0 +1,5 @@ +ALTER TABLE {$NAMESPACE}_maniphest.maniphest_task + ADD properties LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}; + +UPDATE {$NAMESPACE}_maniphest.maniphest_task + SET properties = '{}' WHERE properties = ''; diff --git a/resources/sql/autopatches/20160208.task.1.sql b/resources/sql/autopatches/20160208.task.1.sql new file mode 100644 index 0000000000..786107666c --- /dev/null +++ b/resources/sql/autopatches/20160208.task.1.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_maniphest.maniphest_task + DROP projectPHIDs; diff --git a/resources/sql/autopatches/20160208.task.2.sql b/resources/sql/autopatches/20160208.task.2.sql new file mode 100644 index 0000000000..d0889ca92d --- /dev/null +++ b/resources/sql/autopatches/20160208.task.2.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_maniphest.maniphest_task + DROP attached; diff --git a/resources/sql/autopatches/20160208.task.3.sql b/resources/sql/autopatches/20160208.task.3.sql new file mode 100644 index 0000000000..9fae66d8df --- /dev/null +++ b/resources/sql/autopatches/20160208.task.3.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_maniphest.maniphest_task + ADD points DOUBLE; diff --git a/resources/sql/autopatches/20160212.proj.1.sql b/resources/sql/autopatches/20160212.proj.1.sql new file mode 100644 index 0000000000..7d8c19b0b1 --- /dev/null +++ b/resources/sql/autopatches/20160212.proj.1.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_project.project + ADD properties LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20160212.proj.2.sql b/resources/sql/autopatches/20160212.proj.2.sql new file mode 100644 index 0000000000..f6f793aec4 --- /dev/null +++ b/resources/sql/autopatches/20160212.proj.2.sql @@ -0,0 +1,2 @@ +UPDATE {$NAMESPACE}_project.project + SET properties = '{}' WHERE properties = ''; diff --git a/resources/sql/patches/migrate-maniphest-dependencies.php b/resources/sql/patches/migrate-maniphest-dependencies.php index 074018264c..1c607edd1d 100644 --- a/resources/sql/patches/migrate-maniphest-dependencies.php +++ b/resources/sql/patches/migrate-maniphest-dependencies.php @@ -1,29 +1,3 @@ openTransaction(); - -foreach (new LiskMigrationIterator($table) as $task) { - $id = $task->getID(); - echo pht('Task %d: ', $id); - - $deps = $task->getAttachedPHIDs(ManiphestTaskPHIDType::TYPECONST); - if (!$deps) { - echo "-\n"; - continue; - } - - $editor = new PhabricatorEdgeEditor(); - foreach ($deps as $dep) { - $editor->addEdge( - $task->getPHID(), - ManiphestTaskDependsOnTaskEdgeType::EDGECONST, - $dep); - } - $editor->save(); - echo pht('OKAY')."\n"; -} - -$table->saveTransaction(); -echo pht('Done.')."\n"; +// From 2013-2016, this migration moved dependent tasks to edges. diff --git a/resources/sql/patches/migrate-maniphest-revisions.php b/resources/sql/patches/migrate-maniphest-revisions.php index 2a8f8061b4..26d44299f6 100644 --- a/resources/sql/patches/migrate-maniphest-revisions.php +++ b/resources/sql/patches/migrate-maniphest-revisions.php @@ -1,28 +1,3 @@ establishConnection('w'); - -foreach (new LiskMigrationIterator($table) as $task) { - $id = $task->getID(); - echo pht('Task %d: ', $id); - - $revs = $task->getAttachedPHIDs(DifferentialRevisionPHIDType::TYPECONST); - if (!$revs) { - echo "-\n"; - continue; - } - - $editor = new PhabricatorEdgeEditor(); - foreach ($revs as $rev) { - $editor->addEdge( - $task->getPHID(), - ManiphestTaskHasRevisionEdgeType::EDGECONST, - $rev); - } - $editor->save(); - echo pht('OKAY')."\n"; -} - -echo pht('Done.')."\n"; +// From 2013-2016, this migration moved revisions attached to tasks to edges. diff --git a/scripts/ssh/ssh-exec.php b/scripts/ssh/ssh-exec.php index 45dacddebd..2238bab478 100755 --- a/scripts/ssh/ssh-exec.php +++ b/scripts/ssh/ssh-exec.php @@ -129,7 +129,7 @@ try { throw new Exception( pht( 'Invalid device name ("%s"). There is no device with this name.', - $device->getName())); + $device_name)); } // We're authenticated as a device, but we're going to read the user out of diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 763b41e99b..aacf85ac45 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -241,6 +241,7 @@ phutil_register_library_map(array( 'ConduitPHIDParameterType' => 'applications/conduit/parametertype/ConduitPHIDParameterType.php', 'ConduitParameterType' => 'applications/conduit/parametertype/ConduitParameterType.php', 'ConduitPingConduitAPIMethod' => 'applications/conduit/method/ConduitPingConduitAPIMethod.php', + 'ConduitPointsParameterType' => 'applications/conduit/parametertype/ConduitPointsParameterType.php', 'ConduitProjectListParameterType' => 'applications/conduit/parametertype/ConduitProjectListParameterType.php', 'ConduitQueryConduitAPIMethod' => 'applications/conduit/method/ConduitQueryConduitAPIMethod.php', 'ConduitResultSearchEngineExtension' => 'applications/conduit/query/ConduitResultSearchEngineExtension.php', @@ -1299,6 +1300,7 @@ phutil_register_library_map(array( 'ManiphestHovercardEngineExtension' => 'applications/maniphest/engineextension/ManiphestHovercardEngineExtension.php', 'ManiphestInfoConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestInfoConduitAPIMethod.php', 'ManiphestNameIndex' => 'applications/maniphest/storage/ManiphestNameIndex.php', + 'ManiphestPointsConfigOptionType' => 'applications/maniphest/config/ManiphestPointsConfigOptionType.php', 'ManiphestPriorityConfigOptionType' => 'applications/maniphest/config/ManiphestPriorityConfigOptionType.php', 'ManiphestPriorityEmailCommand' => 'applications/maniphest/command/ManiphestPriorityEmailCommand.php', 'ManiphestProjectNameFulltextEngineExtension' => 'applications/maniphest/engineextension/ManiphestProjectNameFulltextEngineExtension.php', @@ -1339,6 +1341,7 @@ phutil_register_library_map(array( 'ManiphestTaskOpenStatusDatasource' => 'applications/maniphest/typeahead/ManiphestTaskOpenStatusDatasource.php', 'ManiphestTaskPHIDResolver' => 'applications/maniphest/httpparametertype/ManiphestTaskPHIDResolver.php', 'ManiphestTaskPHIDType' => 'applications/maniphest/phid/ManiphestTaskPHIDType.php', + 'ManiphestTaskPoints' => 'applications/maniphest/constants/ManiphestTaskPoints.php', 'ManiphestTaskPriority' => 'applications/maniphest/constants/ManiphestTaskPriority.php', 'ManiphestTaskPriorityDatasource' => 'applications/maniphest/typeahead/ManiphestTaskPriorityDatasource.php', 'ManiphestTaskPriorityHeraldAction' => 'applications/maniphest/herald/ManiphestTaskPriorityHeraldAction.php', @@ -1522,6 +1525,8 @@ phutil_register_library_map(array( 'PHUIPropertyListView' => 'view/phui/PHUIPropertyListView.php', 'PHUIRemarkupPreviewPanel' => 'view/phui/PHUIRemarkupPreviewPanel.php', 'PHUIRemarkupView' => 'infrastructure/markup/view/PHUIRemarkupView.php', + 'PHUISegmentBarSegmentView' => 'view/phui/PHUISegmentBarSegmentView.php', + 'PHUISegmentBarView' => 'view/phui/PHUISegmentBarView.php', 'PHUISpacesNamespaceContextView' => 'applications/spaces/view/PHUISpacesNamespaceContextView.php', 'PHUIStatusItemView' => 'view/phui/PHUIStatusItemView.php', 'PHUIStatusListView' => 'view/phui/PHUIStatusListView.php', @@ -1815,6 +1820,8 @@ phutil_register_library_map(array( 'PhabricatorBinariesSetupCheck' => 'applications/config/check/PhabricatorBinariesSetupCheck.php', 'PhabricatorBitbucketAuthProvider' => 'applications/auth/provider/PhabricatorBitbucketAuthProvider.php', 'PhabricatorBoardLayoutEngine' => 'applications/project/engine/PhabricatorBoardLayoutEngine.php', + 'PhabricatorBoardRenderingEngine' => 'applications/project/engine/PhabricatorBoardRenderingEngine.php', + 'PhabricatorBoardResponseEngine' => 'applications/project/engine/PhabricatorBoardResponseEngine.php', 'PhabricatorBot' => 'infrastructure/daemon/bot/PhabricatorBot.php', 'PhabricatorBotChannel' => 'infrastructure/daemon/bot/target/PhabricatorBotChannel.php', 'PhabricatorBotDebugLogHandler' => 'infrastructure/daemon/bot/handler/PhabricatorBotDebugLogHandler.php', @@ -2203,6 +2210,7 @@ phutil_register_library_map(array( 'PhabricatorEditEngineExtension' => 'applications/transactions/engineextension/PhabricatorEditEngineExtension.php', 'PhabricatorEditEngineExtensionModule' => 'applications/transactions/engineextension/PhabricatorEditEngineExtensionModule.php', 'PhabricatorEditEngineListController' => 'applications/transactions/controller/PhabricatorEditEngineListController.php', + 'PhabricatorEditEnginePointsCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEnginePointsCommentAction.php', 'PhabricatorEditEngineQuery' => 'applications/transactions/query/PhabricatorEditEngineQuery.php', 'PhabricatorEditEngineSearchEngine' => 'applications/transactions/query/PhabricatorEditEngineSearchEngine.php', 'PhabricatorEditEngineSelectCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEngineSelectCommentAction.php', @@ -2810,6 +2818,7 @@ phutil_register_library_map(array( 'PhabricatorPhurlURLViewController' => 'applications/phurl/controller/PhabricatorPhurlURLViewController.php', 'PhabricatorPirateEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorPirateEnglishTranslation.php', 'PhabricatorPlatformSite' => 'aphront/site/PhabricatorPlatformSite.php', + 'PhabricatorPointsEditField' => 'applications/transactions/editfield/PhabricatorPointsEditField.php', 'PhabricatorPolicies' => 'applications/policy/constants/PhabricatorPolicies.php', 'PhabricatorPolicy' => 'applications/policy/storage/PhabricatorPolicy.php', 'PhabricatorPolicyApplication' => 'applications/policy/application/PhabricatorPolicyApplication.php', @@ -2859,6 +2868,7 @@ phutil_register_library_map(array( 'PhabricatorProjectApplication' => 'applications/project/application/PhabricatorProjectApplication.php', 'PhabricatorProjectArchiveController' => 'applications/project/controller/PhabricatorProjectArchiveController.php', 'PhabricatorProjectBoardController' => 'applications/project/controller/PhabricatorProjectBoardController.php', + 'PhabricatorProjectBoardDisableController' => 'applications/project/controller/PhabricatorProjectBoardDisableController.php', 'PhabricatorProjectBoardImportController' => 'applications/project/controller/PhabricatorProjectBoardImportController.php', 'PhabricatorProjectBoardReorderController' => 'applications/project/controller/PhabricatorProjectBoardReorderController.php', 'PhabricatorProjectBoardViewController' => 'applications/project/controller/PhabricatorProjectBoardViewController.php', @@ -2879,12 +2889,14 @@ phutil_register_library_map(array( 'PhabricatorProjectConfiguredCustomField' => 'applications/project/customfield/PhabricatorProjectConfiguredCustomField.php', 'PhabricatorProjectController' => 'applications/project/controller/PhabricatorProjectController.php', 'PhabricatorProjectCoreTestCase' => 'applications/project/__tests__/PhabricatorProjectCoreTestCase.php', + 'PhabricatorProjectCoverController' => 'applications/project/controller/PhabricatorProjectCoverController.php', 'PhabricatorProjectCustomField' => 'applications/project/customfield/PhabricatorProjectCustomField.php', 'PhabricatorProjectCustomFieldNumericIndex' => 'applications/project/storage/PhabricatorProjectCustomFieldNumericIndex.php', 'PhabricatorProjectCustomFieldStorage' => 'applications/project/storage/PhabricatorProjectCustomFieldStorage.php', 'PhabricatorProjectCustomFieldStringIndex' => 'applications/project/storage/PhabricatorProjectCustomFieldStringIndex.php', 'PhabricatorProjectDAO' => 'applications/project/storage/PhabricatorProjectDAO.php', 'PhabricatorProjectDatasource' => 'applications/project/typeahead/PhabricatorProjectDatasource.php', + 'PhabricatorProjectDefaultController' => 'applications/project/controller/PhabricatorProjectDefaultController.php', 'PhabricatorProjectDescriptionField' => 'applications/project/customfield/PhabricatorProjectDescriptionField.php', 'PhabricatorProjectDetailsProfilePanel' => 'applications/project/profilepanel/PhabricatorProjectDetailsProfilePanel.php', 'PhabricatorProjectEditController' => 'applications/project/controller/PhabricatorProjectEditController.php', @@ -2894,6 +2906,7 @@ phutil_register_library_map(array( 'PhabricatorProjectHeraldAction' => 'applications/project/herald/PhabricatorProjectHeraldAction.php', 'PhabricatorProjectHeraldAdapter' => 'applications/project/herald/PhabricatorProjectHeraldAdapter.php', 'PhabricatorProjectHeraldFieldGroup' => 'applications/project/herald/PhabricatorProjectHeraldFieldGroup.php', + 'PhabricatorProjectHovercardEngineExtension' => 'applications/project/engineextension/PhabricatorProjectHovercardEngineExtension.php', 'PhabricatorProjectIconSet' => 'applications/project/icon/PhabricatorProjectIconSet.php', 'PhabricatorProjectIconsConfigOptionType' => 'applications/project/config/PhabricatorProjectIconsConfigOptionType.php', 'PhabricatorProjectInterface' => 'applications/project/interface/PhabricatorProjectInterface.php', @@ -2924,6 +2937,7 @@ phutil_register_library_map(array( 'PhabricatorProjectOrUserFunctionDatasource' => 'applications/project/typeahead/PhabricatorProjectOrUserFunctionDatasource.php', 'PhabricatorProjectPHIDResolver' => 'applications/phid/resolver/PhabricatorProjectPHIDResolver.php', 'PhabricatorProjectPanelController' => 'applications/project/controller/PhabricatorProjectPanelController.php', + 'PhabricatorProjectPointsProfilePanel' => 'applications/project/profilepanel/PhabricatorProjectPointsProfilePanel.php', 'PhabricatorProjectProfileController' => 'applications/project/controller/PhabricatorProjectProfileController.php', 'PhabricatorProjectProfilePanelEngine' => 'applications/project/engine/PhabricatorProjectProfilePanelEngine.php', 'PhabricatorProjectProjectHasMemberEdgeType' => 'applications/project/edge/PhabricatorProjectProjectHasMemberEdgeType.php', @@ -3365,6 +3379,7 @@ phutil_register_library_map(array( 'PhabricatorUnsubscribedFromObjectEdgeType' => 'applications/transactions/edges/PhabricatorUnsubscribedFromObjectEdgeType.php', 'PhabricatorUser' => 'applications/people/storage/PhabricatorUser.php', 'PhabricatorUserBlurbField' => 'applications/people/customfield/PhabricatorUserBlurbField.php', + 'PhabricatorUserCardView' => 'applications/people/view/PhabricatorUserCardView.php', 'PhabricatorUserConfigOptions' => 'applications/people/config/PhabricatorUserConfigOptions.php', 'PhabricatorUserConfiguredCustomField' => 'applications/people/customfield/PhabricatorUserConfiguredCustomField.php', 'PhabricatorUserConfiguredCustomFieldStorage' => 'applications/people/storage/PhabricatorUserConfiguredCustomFieldStorage.php', @@ -3817,7 +3832,6 @@ phutil_register_library_map(array( 'ProjectDefaultJoinCapability' => 'applications/project/capability/ProjectDefaultJoinCapability.php', 'ProjectDefaultViewCapability' => 'applications/project/capability/ProjectDefaultViewCapability.php', 'ProjectEditConduitAPIMethod' => 'applications/project/conduit/ProjectEditConduitAPIMethod.php', - 'ProjectHovercardEngineExtension' => 'applications/project/events/ProjectHovercardEngineExtension.php', 'ProjectQueryConduitAPIMethod' => 'applications/project/conduit/ProjectQueryConduitAPIMethod.php', 'ProjectRemarkupRule' => 'applications/project/remarkup/ProjectRemarkupRule.php', 'ProjectRemarkupRuleTestCase' => 'applications/project/remarkup/__tests__/ProjectRemarkupRuleTestCase.php', @@ -4221,7 +4235,7 @@ phutil_register_library_map(array( 'ConduitGetCapabilitiesConduitAPIMethod' => 'ConduitAPIMethod', 'ConduitGetCertificateConduitAPIMethod' => 'ConduitAPIMethod', 'ConduitIntListParameterType' => 'ConduitListParameterType', - 'ConduitIntParameterType' => 'ConduitListParameterType', + 'ConduitIntParameterType' => 'ConduitParameterType', 'ConduitListParameterType' => 'ConduitParameterType', 'ConduitLogGarbageCollector' => 'PhabricatorGarbageCollector', 'ConduitMethodDoesNotExistException' => 'ConduitMethodNotFoundException', @@ -4230,6 +4244,7 @@ phutil_register_library_map(array( 'ConduitPHIDParameterType' => 'ConduitParameterType', 'ConduitParameterType' => 'Phobject', 'ConduitPingConduitAPIMethod' => 'ConduitAPIMethod', + 'ConduitPointsParameterType' => 'ConduitParameterType', 'ConduitProjectListParameterType' => 'ConduitListParameterType', 'ConduitQueryConduitAPIMethod' => 'ConduitAPIMethod', 'ConduitResultSearchEngineExtension' => 'PhabricatorSearchEngineExtension', @@ -5447,6 +5462,7 @@ phutil_register_library_map(array( 'ManiphestHovercardEngineExtension' => 'PhabricatorHovercardEngineExtension', 'ManiphestInfoConduitAPIMethod' => 'ManiphestConduitAPIMethod', 'ManiphestNameIndex' => 'ManiphestDAO', + 'ManiphestPointsConfigOptionType' => 'PhabricatorConfigJSONOptionType', 'ManiphestPriorityConfigOptionType' => 'PhabricatorConfigJSONOptionType', 'ManiphestPriorityEmailCommand' => 'ManiphestEmailCommand', 'ManiphestProjectNameFulltextEngineExtension' => 'PhabricatorFulltextEngineExtension', @@ -5503,6 +5519,7 @@ phutil_register_library_map(array( 'ManiphestTaskOpenStatusDatasource' => 'PhabricatorTypeaheadDatasource', 'ManiphestTaskPHIDResolver' => 'PhabricatorPHIDResolver', 'ManiphestTaskPHIDType' => 'PhabricatorPHIDType', + 'ManiphestTaskPoints' => 'Phobject', 'ManiphestTaskPriority' => 'ManiphestConstants', 'ManiphestTaskPriorityDatasource' => 'PhabricatorTypeaheadDatasource', 'ManiphestTaskPriorityHeraldAction' => 'HeraldAction', @@ -5702,6 +5719,8 @@ phutil_register_library_map(array( 'PHUIPropertyListView' => 'AphrontView', 'PHUIRemarkupPreviewPanel' => 'AphrontTagView', 'PHUIRemarkupView' => 'AphrontView', + 'PHUISegmentBarSegmentView' => 'AphrontTagView', + 'PHUISegmentBarView' => 'AphrontTagView', 'PHUISpacesNamespaceContextView' => 'AphrontView', 'PHUIStatusItemView' => 'AphrontTagView', 'PHUIStatusListView' => 'AphrontTagView', @@ -6043,6 +6062,8 @@ phutil_register_library_map(array( 'PhabricatorBinariesSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorBitbucketAuthProvider' => 'PhabricatorOAuth1AuthProvider', 'PhabricatorBoardLayoutEngine' => 'Phobject', + 'PhabricatorBoardRenderingEngine' => 'Phobject', + 'PhabricatorBoardResponseEngine' => 'Phobject', 'PhabricatorBot' => 'PhabricatorDaemon', 'PhabricatorBotChannel' => 'PhabricatorBotTarget', 'PhabricatorBotDebugLogHandler' => 'PhabricatorBotHandler', @@ -6500,6 +6521,7 @@ phutil_register_library_map(array( 'PhabricatorEditEngineExtension' => 'Phobject', 'PhabricatorEditEngineExtensionModule' => 'PhabricatorConfigModule', 'PhabricatorEditEngineListController' => 'PhabricatorEditEngineController', + 'PhabricatorEditEnginePointsCommentAction' => 'PhabricatorEditEngineCommentAction', 'PhabricatorEditEngineQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorEditEngineSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorEditEngineSelectCommentAction' => 'PhabricatorEditEngineCommentAction', @@ -7198,6 +7220,7 @@ phutil_register_library_map(array( 'PhabricatorPhurlURLViewController' => 'PhabricatorPhurlController', 'PhabricatorPirateEnglishTranslation' => 'PhutilTranslation', 'PhabricatorPlatformSite' => 'PhabricatorSite', + 'PhabricatorPointsEditField' => 'PhabricatorEditField', 'PhabricatorPolicies' => 'PhabricatorPolicyConstants', 'PhabricatorPolicy' => array( 'PhabricatorPolicyDAO', @@ -7271,6 +7294,7 @@ phutil_register_library_map(array( 'PhabricatorProjectApplication' => 'PhabricatorApplication', 'PhabricatorProjectArchiveController' => 'PhabricatorProjectController', 'PhabricatorProjectBoardController' => 'PhabricatorProjectController', + 'PhabricatorProjectBoardDisableController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBoardImportController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBoardReorderController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectBoardViewController' => 'PhabricatorProjectBoardController', @@ -7302,12 +7326,14 @@ phutil_register_library_map(array( ), 'PhabricatorProjectController' => 'PhabricatorController', 'PhabricatorProjectCoreTestCase' => 'PhabricatorTestCase', + 'PhabricatorProjectCoverController' => 'PhabricatorProjectController', 'PhabricatorProjectCustomField' => 'PhabricatorCustomField', 'PhabricatorProjectCustomFieldNumericIndex' => 'PhabricatorCustomFieldNumericIndexStorage', 'PhabricatorProjectCustomFieldStorage' => 'PhabricatorCustomFieldStorage', 'PhabricatorProjectCustomFieldStringIndex' => 'PhabricatorCustomFieldStringIndexStorage', 'PhabricatorProjectDAO' => 'PhabricatorLiskDAO', 'PhabricatorProjectDatasource' => 'PhabricatorTypeaheadDatasource', + 'PhabricatorProjectDefaultController' => 'PhabricatorProjectBoardController', 'PhabricatorProjectDescriptionField' => 'PhabricatorProjectStandardCustomField', 'PhabricatorProjectDetailsProfilePanel' => 'PhabricatorProfilePanel', 'PhabricatorProjectEditController' => 'PhabricatorProjectController', @@ -7317,6 +7343,7 @@ phutil_register_library_map(array( 'PhabricatorProjectHeraldAction' => 'HeraldAction', 'PhabricatorProjectHeraldAdapter' => 'HeraldAdapter', 'PhabricatorProjectHeraldFieldGroup' => 'HeraldFieldGroup', + 'PhabricatorProjectHovercardEngineExtension' => 'PhabricatorHovercardEngineExtension', 'PhabricatorProjectIconSet' => 'PhabricatorIconSet', 'PhabricatorProjectIconsConfigOptionType' => 'PhabricatorConfigJSONOptionType', 'PhabricatorProjectListController' => 'PhabricatorProjectController', @@ -7346,6 +7373,7 @@ phutil_register_library_map(array( 'PhabricatorProjectOrUserFunctionDatasource' => 'PhabricatorTypeaheadCompositeDatasource', 'PhabricatorProjectPHIDResolver' => 'PhabricatorPHIDResolver', 'PhabricatorProjectPanelController' => 'PhabricatorProjectController', + 'PhabricatorProjectPointsProfilePanel' => 'PhabricatorProfilePanel', 'PhabricatorProjectProfileController' => 'PhabricatorProjectController', 'PhabricatorProjectProfilePanelEngine' => 'PhabricatorProfilePanelEngine', 'PhabricatorProjectProjectHasMemberEdgeType' => 'PhabricatorEdgeType', @@ -7865,6 +7893,7 @@ phutil_register_library_map(array( 'PhabricatorFulltextInterface', ), 'PhabricatorUserBlurbField' => 'PhabricatorUserCustomField', + 'PhabricatorUserCardView' => 'AphrontTagView', 'PhabricatorUserConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorUserConfiguredCustomField' => array( 'PhabricatorUserCustomField', @@ -8452,7 +8481,6 @@ phutil_register_library_map(array( 'ProjectDefaultJoinCapability' => 'PhabricatorPolicyCapability', 'ProjectDefaultViewCapability' => 'PhabricatorPolicyCapability', 'ProjectEditConduitAPIMethod' => 'PhabricatorEditEngineAPIMethod', - 'ProjectHovercardEngineExtension' => 'PhabricatorHovercardEngineExtension', 'ProjectQueryConduitAPIMethod' => 'ProjectConduitAPIMethod', 'ProjectRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'ProjectRemarkupRuleTestCase' => 'PhabricatorTestCase', diff --git a/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php b/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php index 8614e68f56..c23996470d 100644 --- a/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php +++ b/src/applications/calendar/query/PhabricatorCalendarEventSearchEngine.php @@ -266,7 +266,6 @@ final class PhabricatorCalendarEventSearchEngine $list = new PHUIObjectItemListView(); foreach ($events as $event) { - $duration = ''; $event_date_info = $this->getEventDateLabel($event); $creator_handle = $handles[$event->getUserPHID()]; $attendees = array(); @@ -275,18 +274,6 @@ final class PhabricatorCalendarEventSearchEngine $attendees[] = $invitee->getInviteePHID(); } - $attendees = pht( - 'Attending: %s', - $viewer->renderHandleList($attendees) - ->setAsInline(1) - ->render()); - - if (strlen($event->getDuration()) > 0) { - $duration = pht( - 'Duration: %s', - $event->getDuration()); - } - if ($event->getIsGhostEvent()) { $title_text = $event->getMonogram() .' (' @@ -302,9 +289,25 @@ final class PhabricatorCalendarEventSearchEngine ->setObject($event) ->setHeader($title_text) ->setHref($event->getURI()) - ->addAttribute($event_date_info) - ->addAttribute($attendees) - ->addIcon('none', $duration); + ->addAttribute($event_date_info); + + if ($attendees) { + $attending = pht( + 'Attending: %s', + $viewer->renderHandleList($attendees) + ->setAsInline(1) + ->render()); + + $item->addAttribute($attending); + } + + if (strlen($event->getDuration()) > 0) { + $duration = pht( + 'Duration: %s', + $event->getDuration()); + + $item->addIcon('none', $duration); + } $list->addItem($item); } diff --git a/src/applications/conduit/parametertype/ConduitIntParameterType.php b/src/applications/conduit/parametertype/ConduitIntParameterType.php index e9943e53a7..54f66fdf6c 100644 --- a/src/applications/conduit/parametertype/ConduitIntParameterType.php +++ b/src/applications/conduit/parametertype/ConduitIntParameterType.php @@ -1,7 +1,7 @@ raiseValidationException( + $request, + $key, + pht('Expected numeric points value, got something else.')); + } + + if ($value !== null) { + $value = (double)$value; + if ($value < 0) { + $this->raiseValidationException( + $request, + $key, + pht('Point values must be nonnegative.')); + } + } + + return $value; + } + + protected function getParameterTypeName() { + return 'points'; + } + + protected function getParameterFormatDescriptions() { + return array( + pht('A nonnegative number, or null.'), + ); + } + + protected function getParameterExamples() { + return array( + 'null', + '0', + '1', + '15', + '0.5', + ); + } + +} diff --git a/src/applications/diffusion/controller/DiffusionCommitController.php b/src/applications/diffusion/controller/DiffusionCommitController.php index efa3104a92..9d6b845c91 100644 --- a/src/applications/diffusion/controller/DiffusionCommitController.php +++ b/src/applications/diffusion/controller/DiffusionCommitController.php @@ -92,10 +92,9 @@ final class DiffusionCommitController extends DiffusionController { $engine = PhabricatorMarkupEngine::newDifferentialMarkupEngine(); $engine->setConfig('viewer', $user); - require_celerity_resource('phabricator-remarkup-css'); - $headsup_view = id(new PHUIHeaderView()) - ->setHeader(nonempty($commit->getSummary(), pht('Commit Detail'))); + ->setHeader(nonempty($commit->getSummary(), pht('Commit Detail'))) + ->setSubheader(pht('Commit: %s', $commit->getCommitIdentifier())); $headsup_actions = $this->renderHeadsupActionList($commit, $repository); diff --git a/src/applications/diffusion/controller/DiffusionController.php b/src/applications/diffusion/controller/DiffusionController.php index a3c661a26d..00bb0ce4d4 100644 --- a/src/applications/diffusion/controller/DiffusionController.php +++ b/src/applications/diffusion/controller/DiffusionController.php @@ -147,7 +147,7 @@ abstract class DiffusionController extends PhabricatorController { $crumb_list[] = $crumb; $stable_commit = $drequest->getStableCommit(); - $commit_name = $repository->formatCommitName($stable_commit); + $commit_name = $repository->formatCommitName($stable_commit, $local = true); $commit_uri = $repository->getCommitURI($stable_commit); if ($spec['tags']) { @@ -171,8 +171,7 @@ abstract class DiffusionController extends PhabricatorController { if ($spec['commit']) { $crumb = id(new PHUICrumbView()) - ->setName($commit_name) - ->setHref($commit_uri); + ->setName($commit_name); $crumb_list[] = $crumb; return $crumb_list; } diff --git a/src/applications/diffusion/controller/DiffusionServeController.php b/src/applications/diffusion/controller/DiffusionServeController.php index 8d131eca6d..9de692d620 100644 --- a/src/applications/diffusion/controller/DiffusionServeController.php +++ b/src/applications/diffusion/controller/DiffusionServeController.php @@ -427,11 +427,14 @@ final class DiffusionServeController extends DiffusionController { '$PATH')); } + // NOTE: We do not set HTTP_CONTENT_ENCODING here, because we already + // decompressed the request when we read the request body, so the body is + // just plain data with no encoding. + $env = array( 'REQUEST_METHOD' => $_SERVER['REQUEST_METHOD'], 'QUERY_STRING' => $query_string, 'CONTENT_TYPE' => $request->getHTTPHeader('Content-Type'), - 'HTTP_CONTENT_ENCODING' => $request->getHTTPHeader('Content-Encoding'), 'REMOTE_ADDR' => $_SERVER['REMOTE_ADDR'], 'GIT_PROJECT_ROOT' => $repository_root, 'GIT_HTTP_EXPORT_ALL' => '1', diff --git a/src/applications/files/transform/PhabricatorFileThumbnailTransform.php b/src/applications/files/transform/PhabricatorFileThumbnailTransform.php index 8d55109eda..546021fc95 100644 --- a/src/applications/files/transform/PhabricatorFileThumbnailTransform.php +++ b/src/applications/files/transform/PhabricatorFileThumbnailTransform.php @@ -7,6 +7,7 @@ final class PhabricatorFileThumbnailTransform const TRANSFORM_PINBOARD = 'pinboard'; const TRANSFORM_THUMBGRID = 'thumbgrid'; const TRANSFORM_PREVIEW = 'preview'; + const TRANSFORM_WORKCARD = 'workcard'; private $name; private $key; @@ -73,6 +74,11 @@ final class PhabricatorFileThumbnailTransform ->setName(pht('Preview (220px)')) ->setKey(self::TRANSFORM_PREVIEW) ->setDimensions(220, null), + id(new self()) + ->setName(pht('Workcard (526px)')) + ->setKey(self::TRANSFORM_WORKCARD) + ->setScaleUp(true) + ->setDimensions(526, null), ); } diff --git a/src/applications/herald/adapter/HeraldAdapter.php b/src/applications/herald/adapter/HeraldAdapter.php index bd6d4953da..e08118a369 100644 --- a/src/applications/herald/adapter/HeraldAdapter.php +++ b/src/applications/herald/adapter/HeraldAdapter.php @@ -14,6 +14,7 @@ abstract class HeraldAdapter extends Phobject { const CONDITION_IS_ME = 'me'; const CONDITION_IS_NOT_ME = '!me'; const CONDITION_REGEXP = 'regexp'; + const CONDITION_NOT_REGEXP = '!regexp'; const CONDITION_RULE = 'conditions'; const CONDITION_NOT_RULE = '!conditions'; const CONDITION_EXISTS = 'exists'; @@ -322,6 +323,7 @@ abstract class HeraldAdapter extends Phobject { self::CONDITION_IS_ME => pht('is myself'), self::CONDITION_IS_NOT_ME => pht('is not myself'), self::CONDITION_REGEXP => pht('matches regexp'), + self::CONDITION_NOT_REGEXP => pht('does not match regexp'), self::CONDITION_RULE => pht('matches:'), self::CONDITION_NOT_RULE => pht('does not match:'), self::CONDITION_EXISTS => pht('exists'), @@ -364,16 +366,18 @@ abstract class HeraldAdapter extends Phobject { switch ($condition_type) { case self::CONDITION_CONTAINS: - // "Contains" can take an array of strings, as in "Any changed - // filename" for diffs. + case self::CONDITION_NOT_CONTAINS: + // "Contains and "does not contain" can take an array of strings, as in + // "Any changed filename" for diffs. + + $result_if_match = ($condition_type == self::CONDITION_CONTAINS); + foreach ((array)$field_value as $value) { if (stripos($value, $condition_value) !== false) { - return true; + return $result_if_match; } } - return false; - case self::CONDITION_NOT_CONTAINS: - return (stripos($field_value, $condition_value) === false); + return !$result_if_match; case self::CONDITION_IS: return ($field_value == $condition_value); case self::CONDITION_IS_NOT: @@ -427,6 +431,9 @@ abstract class HeraldAdapter extends Phobject { case self::CONDITION_NEVER: return false; case self::CONDITION_REGEXP: + case self::CONDITION_NOT_REGEXP: + $result_if_match = ($condition_type == self::CONDITION_REGEXP); + foreach ((array)$field_value as $value) { // We add the 'S' flag because we use the regexp multiple times. // It shouldn't cause any troubles if the flag is already there @@ -437,10 +444,10 @@ abstract class HeraldAdapter extends Phobject { pht('Regular expression is not valid!')); } if ($result) { - return true; + return $result_if_match; } } - return false; + return !$result_if_match; case self::CONDITION_REGEXP_PAIR: // Match a JSON-encoded pair of regular expressions against a // dictionary. The first regexp must match the dictionary key, and the @@ -509,6 +516,7 @@ abstract class HeraldAdapter extends Phobject { switch ($condition_type) { case self::CONDITION_REGEXP: + case self::CONDITION_NOT_REGEXP: $ok = @preg_match($condition_value, ''); if ($ok === false) { throw new HeraldInvalidConditionException( diff --git a/src/applications/herald/field/HeraldField.php b/src/applications/herald/field/HeraldField.php index 2aba443077..98d6d8ffc4 100644 --- a/src/applications/herald/field/HeraldField.php +++ b/src/applications/herald/field/HeraldField.php @@ -47,6 +47,7 @@ abstract class HeraldField extends Phobject { HeraldAdapter::CONDITION_IS, HeraldAdapter::CONDITION_IS_NOT, HeraldAdapter::CONDITION_REGEXP, + HeraldAdapter::CONDITION_NOT_REGEXP, ); case self::STANDARD_PHID: return array( @@ -76,12 +77,16 @@ abstract class HeraldField extends Phobject { case self::STANDARD_TEXT_LIST: return array( HeraldAdapter::CONDITION_CONTAINS, + HeraldAdapter::CONDITION_NOT_CONTAINS, HeraldAdapter::CONDITION_REGEXP, + HeraldAdapter::CONDITION_NOT_REGEXP, ); case self::STANDARD_TEXT_MAP: return array( HeraldAdapter::CONDITION_CONTAINS, + HeraldAdapter::CONDITION_NOT_CONTAINS, HeraldAdapter::CONDITION_REGEXP, + HeraldAdapter::CONDITION_NOT_REGEXP, HeraldAdapter::CONDITION_REGEXP_PAIR, ); } diff --git a/src/applications/maniphest/conduit/ManiphestGetTaskTransactionsConduitAPIMethod.php b/src/applications/maniphest/conduit/ManiphestGetTaskTransactionsConduitAPIMethod.php index 4c64a56b33..8b0d0496cf 100644 --- a/src/applications/maniphest/conduit/ManiphestGetTaskTransactionsConduitAPIMethod.php +++ b/src/applications/maniphest/conduit/ManiphestGetTaskTransactionsConduitAPIMethod.php @@ -59,6 +59,7 @@ final class ManiphestGetTaskTransactionsConduitAPIMethod $results[$task_id][] = array( 'taskID' => $task_id, + 'transactionID' => $transaction->getID(), 'transactionPHID' => $transaction->getPHID(), 'transactionType' => $transaction->getTransactionType(), 'oldValue' => $transaction->getOldValue(), diff --git a/src/applications/maniphest/config/ManiphestPointsConfigOptionType.php b/src/applications/maniphest/config/ManiphestPointsConfigOptionType.php new file mode 100644 index 0000000000..f79050b4a1 --- /dev/null +++ b/src/applications/maniphest/config/ManiphestPointsConfigOptionType.php @@ -0,0 +1,10 @@ + pht('Invalid'), 'name.full' => pht('Closed, Invalid'), 'closed' => true, + 'claim' => false, 'prefixes' => array( 'invalidate', 'invalidates', @@ -126,6 +127,7 @@ final class PhabricatorManiphestConfigOptions 'transaction.icon' => 'fa-files-o', 'special' => ManiphestTaskStatus::SPECIAL_DUPLICATE, 'closed' => true, + 'claim' => false, ), 'spite' => array( 'name' => pht('Spite'), @@ -202,6 +204,9 @@ The keys you can provide in a specification are: tasks can not be created or edited to have this status. Existing tasks with this status will not be affected, but you can batch edit them or let them die out on their own. + - `claim` //Optional bool.// By default, closing an unassigned task claims + it. You can set this to `false` to disable this behavior for a particular + status. Statuses will appear in the UI in the order specified. Note the status marked `special` as `duplicate` is not settable directly and will not appear in UI @@ -255,6 +260,40 @@ EOTEXT ); $fields_json = id(new PhutilJSON())->encodeFormatted($fields_example); + $points_type = 'custom:ManiphestPointsConfigOptionType'; + + $points_example_1 = array( + 'enabled' => true, + 'label' => pht('Story Points'), + 'action' => pht('Change Story Points'), + ); + $points_json_1 = id(new PhutilJSON())->encodeFormatted($points_example_1); + + $points_example_2 = array( + 'enabled' => true, + 'label' => pht('Estimated Hours'), + 'action' => pht('Change Estimate'), + ); + $points_json_2 = id(new PhutilJSON())->encodeFormatted($points_example_2); + + $points_description = $this->deformat(pht(<<newOption('maniphest.custom-field-definitions', 'wild', array()) ->setSummary(pht('Custom Maniphest fields.')) @@ -336,7 +375,11 @@ EOTEXT '"Needs Triage" panel on the home page. You should adjust this if '. 'you adjust priorities using `%s`.', 'maniphest.priorities')), - + $this->newOption('maniphest.points', $points_type, array()) + ->setSummary(pht('Configure point values for tasks.')) + ->setDescription($points_description) + ->addExample($points_json_1, pht('Points Config')) + ->addExample($points_json_2, pht('Hours Config')), ); } diff --git a/src/applications/maniphest/constants/ManiphestTaskPoints.php b/src/applications/maniphest/constants/ManiphestTaskPoints.php new file mode 100644 index 0000000000..55af72956b --- /dev/null +++ b/src/applications/maniphest/constants/ManiphestTaskPoints.php @@ -0,0 +1,41 @@ + 'optional bool', + 'label' => 'optional string', + 'action' => 'optional string', + )); + } + +} diff --git a/src/applications/maniphest/constants/ManiphestTaskStatus.php b/src/applications/maniphest/constants/ManiphestTaskStatus.php index ab99f212e5..3a839d8fac 100644 --- a/src/applications/maniphest/constants/ManiphestTaskStatus.php +++ b/src/applications/maniphest/constants/ManiphestTaskStatus.php @@ -155,6 +155,10 @@ final class ManiphestTaskStatus extends ManiphestConstants { return false; } + public static function isClaimStatus($status) { + return self::getStatusAttribute($status, 'claim', true); + } + public static function isClosedStatus($status) { return !self::isOpenStatus($status); } @@ -279,6 +283,7 @@ final class ManiphestTaskStatus extends ManiphestConstants { 'suffixes' => 'optional list', 'keywords' => 'optional list', 'disabled' => 'optional bool', + 'claim' => 'optional bool', )); } diff --git a/src/applications/maniphest/controller/ManiphestTaskDetailController.php b/src/applications/maniphest/controller/ManiphestTaskDetailController.php index 6ca5ab2946..a15b8f4594 100644 --- a/src/applications/maniphest/controller/ManiphestTaskDetailController.php +++ b/src/applications/maniphest/controller/ManiphestTaskDetailController.php @@ -229,6 +229,15 @@ final class ManiphestTaskDetailController extends ManiphestController { $view->addProperty(pht('Author'), $author); + if (ManiphestTaskPoints::getIsEnabled()) { + $points = $task->getPoints(); + if ($points !== null) { + $view->addProperty( + ManiphestTaskPoints::getPointsLabel(), + $task->getPoints()); + } + } + $source = $task->getOriginalEmailSource(); if ($source) { $subject = '[T'.$task->getID().'] '.$task->getTitle(); diff --git a/src/applications/maniphest/controller/ManiphestTaskEditController.php b/src/applications/maniphest/controller/ManiphestTaskEditController.php index 5cf07566a4..9483529138 100644 --- a/src/applications/maniphest/controller/ManiphestTaskEditController.php +++ b/src/applications/maniphest/controller/ManiphestTaskEditController.php @@ -9,6 +9,7 @@ final class ManiphestTaskEditController extends ManiphestController { ->addContextParameter('responseType') ->addContextParameter('columnPHID') ->addContextParameter('order') + ->addContextParameter('visiblePHIDs') ->buildResponse(); } diff --git a/src/applications/maniphest/editor/ManiphestEditEngine.php b/src/applications/maniphest/editor/ManiphestEditEngine.php index 3f88a9abe3..fe9b96f25b 100644 --- a/src/applications/maniphest/editor/ManiphestEditEngine.php +++ b/src/applications/maniphest/editor/ManiphestEditEngine.php @@ -77,7 +77,7 @@ final class ManiphestEditEngine $owner_value = array($this->getViewer()->getPHID()); } - return array( + $fields = array( id(new PhabricatorHandlesEditField()) ->setKey('parent') ->setLabel(pht('Parent Task')) @@ -149,18 +149,37 @@ final class ManiphestEditEngine ->setValue($object->getPriority()) ->setOptions($priority_map) ->setCommentActionLabel(pht('Change Priority')), - id(new PhabricatorRemarkupEditField()) - ->setKey('description') - ->setLabel(pht('Description')) - ->setDescription(pht('Task description.')) - ->setConduitDescription(pht('Update the task description.')) - ->setConduitTypeDescription(pht('New task description.')) - ->setTransactionType(ManiphestTransaction::TYPE_DESCRIPTION) - ->setValue($object->getDescription()) - ->setPreviewPanel( - id(new PHUIRemarkupPreviewPanel()) - ->setHeader(pht('Description Preview'))), ); + + if (ManiphestTaskPoints::getIsEnabled()) { + $points_label = ManiphestTaskPoints::getPointsLabel(); + $action_label = ManiphestTaskPoints::getPointsActionLabel(); + + $fields[] = id(new PhabricatorPointsEditField()) + ->setKey('points') + ->setLabel($points_label) + ->setDescription(pht('Point value of the task.')) + ->setConduitDescription(pht('Change the task point value.')) + ->setConduitTypeDescription(pht('New task point value.')) + ->setTransactionType(ManiphestTransaction::TYPE_POINTS) + ->setIsCopyable(true) + ->setValue($object->getPoints()) + ->setCommentActionLabel($action_label); + } + + $fields[] = id(new PhabricatorRemarkupEditField()) + ->setKey('description') + ->setLabel(pht('Description')) + ->setDescription(pht('Task description.')) + ->setConduitDescription(pht('Update the task description.')) + ->setConduitTypeDescription(pht('New task description.')) + ->setTransactionType(ManiphestTransaction::TYPE_DESCRIPTION) + ->setValue($object->getDescription()) + ->setPreviewPanel( + id(new PHUIRemarkupPreviewPanel()) + ->setHeader(pht('Description Preview'))); + + return $fields; } private function getTaskStatusMap(ManiphestTask $task) { @@ -270,7 +289,11 @@ final class ManiphestEditEngine $viewer = $request->getViewer(); $column_phid = $request->getStr('columnPHID'); - $order = $request->getStr('order'); + + $visible_phids = $request->getStrList('visiblePHIDs'); + if (!$visible_phids) { + $visible_phids = array(); + } $column = id(new PhabricatorProjectColumnQuery()) ->setViewer($viewer) @@ -280,93 +303,15 @@ final class ManiphestEditEngine return new Aphront404Response(); } - // If the workboard's project and all descendant projects have been removed - // from the card's project list, we are going to remove it from the board - // completely. + $board_phid = $column->getProjectPHID(); + $object_phid = $task->getPHID(); - // TODO: If the user did something sneaky and changed a subproject, we'll - // currently leave the card where it was but should really move it to the - // proper new column. - - $descendant_projects = id(new PhabricatorProjectQuery()) + return id(new PhabricatorBoardResponseEngine()) ->setViewer($viewer) - ->withAncestorProjectPHIDs(array($column->getProjectPHID())) - ->execute(); - $board_phids = mpull($descendant_projects, 'getPHID', 'getPHID'); - $board_phids[$column->getProjectPHID()] = $column->getProjectPHID(); - - $project_map = array_fuse($task->getProjectPHIDs()); - $remove_card = !array_intersect_key($board_phids, $project_map); - - $positions = id(new PhabricatorProjectColumnPositionQuery()) - ->setViewer($viewer) - ->withBoardPHIDs(array($column->getProjectPHID())) - ->withColumnPHIDs(array($column->getPHID())) - ->execute(); - $task_phids = mpull($positions, 'getObjectPHID'); - - $column_tasks = id(new ManiphestTaskQuery()) - ->setViewer($viewer) - ->withPHIDs($task_phids) - ->needProjectPHIDs(true) - ->execute(); - - if ($order == PhabricatorProjectColumn::ORDER_NATURAL) { - // TODO: This is a little bit awkward, because PHP and JS use - // slightly different sort order parameters to achieve the same - // effect. It would be good to unify this a bit at some point. - $sort_map = array(); - foreach ($positions as $position) { - $sort_map[$position->getObjectPHID()] = array( - -$position->getSequence(), - $position->getID(), - ); - } - } else { - $sort_map = mpull( - $column_tasks, - 'getPrioritySortVector', - 'getPHID'); - } - - $data = array( - 'removeFromBoard' => $remove_card, - 'sortMap' => $sort_map, - ); - - // TODO: This should just use HandlePool once we get through the EditEngine - // transition. - $owner = null; - if ($task->getOwnerPHID()) { - $owner = id(new PhabricatorHandleQuery()) - ->setViewer($viewer) - ->withPHIDs(array($task->getOwnerPHID())) - ->executeOne(); - } - - $handle_phids = $task->getProjectPHIDs(); - $handle_phids = array_fuse($handle_phids); - $handle_phids = array_diff_key($handle_phids, $board_phids); - - $project_handles = $viewer->loadHandles($handle_phids); - $project_handles = iterator_to_array($project_handles); - - $tasks = id(new ProjectBoardTaskCard()) - ->setViewer($viewer) - ->setTask($task) - ->setOwner($owner) - ->setProjectHandles($project_handles) - ->setCanEdit(true) - ->getItem(); - - $tasks->addClass('phui-workcard'); - - $payload = array( - 'tasks' => $tasks, - 'data' => $data, - ); - - return id(new AphrontAjaxResponse())->setContent($payload); + ->setBoardPHID($board_phid) + ->setObjectPHID($object_phid) + ->setVisiblePHIDs($visible_phids) + ->buildResponse(); } diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php index 46e2a617e8..08cfc66632 100644 --- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php +++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php @@ -28,6 +28,8 @@ final class ManiphestTransactionEditor $types[] = ManiphestTransaction::TYPE_UNBLOCK; $types[] = ManiphestTransaction::TYPE_PARENT; $types[] = ManiphestTransaction::TYPE_COLUMN; + $types[] = ManiphestTransaction::TYPE_COVER_IMAGE; + $types[] = ManiphestTransaction::TYPE_POINTS; $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY; @@ -66,6 +68,14 @@ final class ManiphestTransactionEditor return $xaction->getOldValue(); case ManiphestTransaction::TYPE_SUBPRIORITY: return $object->getSubpriority(); + case ManiphestTransaction::TYPE_COVER_IMAGE: + return $object->getCoverImageFilePHID(); + case ManiphestTransaction::TYPE_POINTS: + $points = $object->getPoints(); + if ($points !== null) { + $points = (double)$points; + } + return $points; case ManiphestTransaction::TYPE_MERGED_INTO: case ManiphestTransaction::TYPE_MERGED_FROM: return null; @@ -92,10 +102,20 @@ final class ManiphestTransactionEditor case ManiphestTransaction::TYPE_MERGED_INTO: case ManiphestTransaction::TYPE_MERGED_FROM: case ManiphestTransaction::TYPE_UNBLOCK: + case ManiphestTransaction::TYPE_COVER_IMAGE: return $xaction->getNewValue(); case ManiphestTransaction::TYPE_PARENT: case ManiphestTransaction::TYPE_COLUMN: return $xaction->getNewValue(); + case ManiphestTransaction::TYPE_POINTS: + $value = $xaction->getNewValue(); + if (!strlen($value)) { + $value = null; + } + if ($value !== null) { + $value = (double)$value; + } + return $value; } } @@ -161,6 +181,35 @@ final class ManiphestTransactionEditor case ManiphestTransaction::TYPE_MERGED_INTO: $object->setStatus(ManiphestTaskStatus::getDuplicateStatus()); return; + case ManiphestTransaction::TYPE_COVER_IMAGE: + $file_phid = $xaction->getNewValue(); + + if ($file_phid) { + $file = id(new PhabricatorFileQuery()) + ->setViewer($this->getActor()) + ->withPHIDs(array($file_phid)) + ->executeOne(); + } else { + $file = null; + } + + if (!$file || !$file->isTransformableImage()) { + $object->setProperty('cover.filePHID', null); + $object->setProperty('cover.thumbnailPHID', null); + return; + } + + $xform_key = PhabricatorFileThumbnailTransform::TRANSFORM_WORKCARD; + + $xform = PhabricatorFileTransform::getTransformByKey($xform_key) + ->executeTransform($file); + + $object->setProperty('cover.filePHID', $file->getPHID()); + $object->setProperty('cover.thumbnailPHID', $xform->getPHID()); + return; + case ManiphestTransaction::TYPE_POINTS: + $object->setPoints($xaction->getNewValue()); + return; case ManiphestTransaction::TYPE_MERGED_FROM: case ManiphestTransaction::TYPE_PARENT: case ManiphestTransaction::TYPE_COLUMN: @@ -819,6 +868,65 @@ final class ManiphestTransactionEditor } } break; + case ManiphestTransaction::TYPE_COVER_IMAGE: + foreach ($xactions as $xaction) { + $old = $xaction->getOldValue(); + $new = $xaction->getNewValue(); + if (!$new) { + continue; + } + + if ($new === $old) { + continue; + } + + $file = id(new PhabricatorFileQuery()) + ->setViewer($this->getActor()) + ->withPHIDs(array($new)) + ->executeOne(); + if (!$file) { + $errors[] = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Invalid'), + pht('File "%s" is not valid.', $new), + $xaction); + continue; + } + + if (!$file->isTransformableImage()) { + $errors[] = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Invalid'), + pht('File "%s" is not a valid image file.', $new), + $xaction); + continue; + } + } + break; + + case ManiphestTransaction::TYPE_POINTS: + foreach ($xactions as $xaction) { + $new = $xaction->getNewValue(); + if (strlen($new) && !is_numeric($new)) { + $errors[] = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Invalid'), + pht('Points value must be numeric or empty.'), + $xaction); + continue; + } + + if ((double)$new < 0) { + $errors[] = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Invalid'), + pht('Points value must be nonnegative.'), + $xaction); + continue; + } + } + break; + } return $errors; @@ -863,8 +971,11 @@ final class ManiphestTransactionEditor // If the task is not assigned, not being assigned, currently open, and // being closed, try to assign the actor as the owner. if ($is_unassigned && !$any_assign && $is_open && $is_closing) { + $is_claim = ManiphestTaskStatus::isClaimStatus($new_status); + // Don't assign the actor if they aren't a real user. - if ($actor_phid) { + // Don't claim the task if the status is configured to not claim. + if ($actor_phid && $is_claim) { $results[] = id(new ManiphestTransaction()) ->setTransactionType(ManiphestTransaction::TYPE_OWNER) ->setNewValue($actor_phid); @@ -941,5 +1052,19 @@ final class ManiphestTransactionEditor ->executeOne(); } + protected function extractFilePHIDsFromCustomTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + $phids = parent::extractFilePHIDsFromCustomTransaction($object, $xaction); + + switch ($xaction->getTransactionType()) { + case ManiphestTransaction::TYPE_COVER_IMAGE: + $phids[] = $xaction->getNewValue(); + break; + } + + return $phids; + } + } diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php index cc9a5bd80a..0c1a1bc787 100644 --- a/src/applications/maniphest/storage/ManiphestTask.php +++ b/src/applications/maniphest/storage/ManiphestTask.php @@ -34,19 +34,16 @@ final class ManiphestTask extends ManiphestDAO protected $viewPolicy = PhabricatorPolicies::POLICY_USER; protected $editPolicy = PhabricatorPolicies::POLICY_USER; - protected $projectPHIDs = array(); - protected $ownerOrdering; protected $spacePHID; + protected $properties = array(); + protected $points; private $subscriberPHIDs = self::ATTACHABLE; private $groupByProjectPHID = self::ATTACHABLE; private $customFields = self::ATTACHABLE; private $edgeProjectPHIDs = self::ATTACHABLE; - // TODO: This field is unused and should eventually be removed. - protected $attached = array(); - public static function initializeNewTask(PhabricatorUser $actor) { $app = id(new PhabricatorApplicationQuery()) ->setViewer($actor) @@ -71,9 +68,7 @@ final class ManiphestTask extends ManiphestDAO return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( - 'ccPHIDs' => self::SERIALIZATION_JSON, - 'attached' => self::SERIALIZATION_JSON, - 'projectPHIDs' => self::SERIALIZATION_JSON, + 'properties' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'ownerPHID' => 'phid?', @@ -86,11 +81,7 @@ final class ManiphestTask extends ManiphestDAO 'ownerOrdering' => 'text64?', 'originalEmailSource' => 'text255?', 'subpriority' => 'double', - - // T6203/NULLABILITY - // This should not be nullable. It's going away soon anyway. - 'ccPHIDs' => 'text?', - + 'points' => 'double?', ), self::CONFIG_KEY_SCHEMA => array( 'key_phid' => null, @@ -141,10 +132,6 @@ final class ManiphestTask extends ManiphestDAO ManiphestTaskDependedOnByTaskEdgeType::EDGECONST); } - public function getAttachedPHIDs($type) { - return array_keys(idx($this->attached, $type, array())); - } - public function generatePHID() { return PhabricatorPHID::generateNewPHID(ManiphestTaskPHIDType::TYPECONST); } @@ -207,11 +194,70 @@ final class ManiphestTask extends ManiphestDAO return ManiphestTaskStatus::isClosedStatus($this->getStatus()); } - public function getPrioritySortVector() { + public function setProperty($key, $value) { + $this->properties[$key] = $value; + return $this; + } + + public function getProperty($key, $default = null) { + return idx($this->properties, $key, $default); + } + + public function getCoverImageFilePHID() { + return idx($this->properties, 'cover.filePHID'); + } + + public function getCoverImageThumbnailPHID() { + return idx($this->properties, 'cover.thumbnailPHID'); + } + + public function getWorkboardOrderVectors() { return array( - $this->getPriority(), - -$this->getSubpriority(), - $this->getID(), + PhabricatorProjectColumn::ORDER_PRIORITY => array( + (int)-$this->getPriority(), + (double)-$this->getSubpriority(), + (int)-$this->getID(), + ), + ); + } + + private function comparePriorityTo(ManiphestTask $other) { + $upri = $this->getPriority(); + $vpri = $other->getPriority(); + + if ($upri != $vpri) { + return ($upri - $vpri); + } + + $usub = $this->getSubpriority(); + $vsub = $other->getSubpriority(); + + if ($usub != $vsub) { + return ($usub - $vsub); + } + + $uid = $this->getID(); + $vid = $other->getID(); + + if ($uid != $vid) { + return ($uid - $vid); + } + + return 0; + } + + public function isLowerPriorityThan(ManiphestTask $other) { + return ($this->comparePriorityTo($other) < 0); + } + + public function isHigherPriorityThan(ManiphestTask $other) { + return ($this->comparePriorityTo($other) > 0); + } + + public function getWorkboardProperties() { + return array( + 'status' => $this->getStatus(), + 'points' => (double)$this->getPoints(), ); } @@ -416,6 +462,10 @@ final class ManiphestTask extends ManiphestDAO ->setKey('priority') ->setType('map') ->setDescription(pht('Information about task priority.')), + id(new PhabricatorConduitSearchFieldSpecification()) + ->setKey('points') + ->setType('points') + ->setDescription(pht('Point value of the task.')), ); } @@ -442,6 +492,7 @@ final class ManiphestTask extends ManiphestDAO 'ownerPHID' => $this->getOwnerPHID(), 'status' => $status_info, 'priority' => $priority_info, + 'points' => $this->getPoints(), ); } diff --git a/src/applications/maniphest/storage/ManiphestTransaction.php b/src/applications/maniphest/storage/ManiphestTransaction.php index 6c2c8c306f..55d14083e6 100644 --- a/src/applications/maniphest/storage/ManiphestTransaction.php +++ b/src/applications/maniphest/storage/ManiphestTransaction.php @@ -16,6 +16,8 @@ final class ManiphestTransaction const TYPE_UNBLOCK = 'unblock'; const TYPE_PARENT = 'parent'; const TYPE_COLUMN = 'column'; + const TYPE_COVER_IMAGE = 'cover-image'; + const TYPE_POINTS = 'points'; // NOTE: this type is deprecated. Keep it around for legacy installs // so any transactions render correctly. @@ -162,11 +164,27 @@ final class ManiphestTransaction sort($new_cols); return ($old_cols === $new_cols); + case self::TYPE_COVER_IMAGE: + // At least for now, don't show these. + return true; + case self::TYPE_POINTS: + if (!ManiphestTaskPoints::getIsEnabled()) { + return true; + } } return parent::shouldHide(); } + public function shouldHideForMail(array $xactions) { + switch ($this->getTransactionType()) { + case self::TYPE_POINTS: + return true; + } + + return parent::shouldHideForMail($xactions); + } + public function shouldHideForFeed() { switch ($this->getTransactionType()) { case self::TYPE_UNBLOCK: @@ -177,6 +195,8 @@ final class ManiphestTransaction return true; } break; + case self::TYPE_POINTS: + return true; } return parent::shouldHideForFeed(); @@ -620,6 +640,23 @@ final class ManiphestTransaction $this->renderHandleList($new)); break; + case self::TYPE_POINTS: + if ($old === null) { + return pht( + '%s set the point value for this task to %s.', + $this->renderHandleLink($author_phid), + $new); + } else if ($new === null) { + return pht( + '%s removed the point value for this task.', + $this->renderHandleLink($author_phid)); + } else { + return pht( + '%s changed the point value for this task from %s to %s.', + $this->renderHandleLink($author_phid), + $old, + $new); + } } diff --git a/src/applications/people/controller/PhabricatorPeopleProfileViewController.php b/src/applications/people/controller/PhabricatorPeopleProfileViewController.php index 45125c28b0..ed2a2189a6 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfileViewController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfileViewController.php @@ -130,7 +130,10 @@ final class PhabricatorPeopleProfileViewController ->setViewer($viewer) ->withMemberPHIDs(array($user->getPHID())) ->needImages(true) - ->withStatus(PhabricatorProjectQuery::STATUS_OPEN) + ->withStatuses( + array( + PhabricatorProjectStatus::STATUS_ACTIVE, + )) ->execute(); $header = id(new PHUIHeaderView()) diff --git a/src/applications/people/engineextension/PeopleHovercardEngineExtension.php b/src/applications/people/engineextension/PeopleHovercardEngineExtension.php index e986ec87f7..22d63eda8a 100644 --- a/src/applications/people/engineextension/PeopleHovercardEngineExtension.php +++ b/src/applications/people/engineextension/PeopleHovercardEngineExtension.php @@ -25,6 +25,7 @@ final class PeopleHovercardEngineExtension ->setViewer($viewer) ->withPHIDs($phids) ->needAvailability(true) + ->needProfileImage(true) ->needProfile(true) ->needBadges(true) ->execute(); @@ -47,69 +48,12 @@ final class PeopleHovercardEngineExtension return; } - $hovercard->setTitle($user->getUsername()); + $user_card = id(new PhabricatorUserCardView()) + ->setProfile($user) + ->setViewer($viewer); - $profile = $user->getUserProfile(); - $detail = $user->getRealName(); - if ($profile->getTitle()) { - $detail .= ' - '.$profile->getTitle(); - } - $hovercard->setDetail($detail); + $hovercard->appendChild($user_card); - if ($user->getIsDisabled()) { - $hovercard->addField(pht('Account'), pht('Disabled')); - } else if (!$user->isUserActivated()) { - $hovercard->addField(pht('Account'), pht('Not Activated')); - } else if (PhabricatorApplication::isClassInstalledForViewer( - 'PhabricatorCalendarApplication', - $viewer)) { - $hovercard->addField( - pht('Status'), - $user->getAvailabilityDescription($viewer)); - } - - $hovercard->addField( - pht('User Since'), - phabricator_date($user->getDateCreated(), $viewer)); - - if ($profile->getBlurb()) { - $hovercard->addField(pht('Blurb'), - id(new PhutilUTF8StringTruncator()) - ->setMaximumGlyphs(120) - ->truncateString($profile->getBlurb())); - } - - $badges = $this->buildBadges($user, $viewer); - foreach ($badges as $badge) { - $hovercard->addBadge($badge); - } - } - - private function buildBadges( - PhabricatorUser $user, - $viewer) { - - $class = 'PhabricatorBadgesApplication'; - $items = array(); - - if (PhabricatorApplication::isClassInstalledForViewer($class, $viewer)) { - $badge_phids = $user->getBadgePHIDs(); - if ($badge_phids) { - $badges = id(new PhabricatorBadgesQuery()) - ->setViewer($viewer) - ->withPHIDs($badge_phids) - ->withStatuses(array(PhabricatorBadgesBadge::STATUS_ACTIVE)) - ->execute(); - - foreach ($badges as $badge) { - $items[] = id(new PHUIBadgeMiniView()) - ->setIcon($badge->getIcon()) - ->setHeader($badge->getName()) - ->setQuality($badge->getQuality()); - } - } - } - return $items; } } diff --git a/src/applications/people/view/PhabricatorUserCardView.php b/src/applications/people/view/PhabricatorUserCardView.php new file mode 100644 index 0000000000..60ba08c93b --- /dev/null +++ b/src/applications/people/view/PhabricatorUserCardView.php @@ -0,0 +1,151 @@ +profile = $profile; + return $this; + } + + public function setViewer(PhabricatorUser $viewer) { + $this->viewer = $viewer; + return $this; + } + + public function setTag($tag) { + $this->tag = $tag; + return $this; + } + + protected function getTagName() { + if ($this->tag) { + return $this->tag; + } + return 'div'; + } + + protected function getTagAttributes() { + $classes = array(); + $classes[] = 'project-card-view'; + + if ($this->profile->getIsDisabled()) { + $classes[] = 'project-card-grey'; + } else { + $classes[] = 'project-card-blue'; + } + + return array( + 'class' => implode($classes, ' '), + ); + } + + protected function getTagContent() { + + $user = $this->profile; + $profile = $user->loadUserProfile(); + $picture = $user->getProfileImageURI(); + $viewer = $this->viewer; + + require_celerity_resource('project-card-view-css'); + + $profile_icon = PhabricatorPeopleIconSet::getIconIcon($profile->getIcon()); + $profile_title = $profile->getDisplayTitle(); + + $tag = id(new PHUITagView()) + ->setIcon($profile_icon) + ->setName($profile_title) + ->addClass('project-view-header-tag') + ->setType(PHUITagView::TYPE_SHADE); + + $header = id(new PHUIHeaderView()) + ->setHeader(array($user->getFullName(), $tag)) + ->setUser($viewer) + ->setImage($picture); + + $body = array(); + + $body[] = $this->addItem( + pht('User Since'), + phabricator_date($profile->getDateCreated(), $viewer)); + + if (PhabricatorApplication::isClassInstalledForViewer( + 'PhabricatorCalendarApplication', + $viewer)) { + $availability = $user->getAvailabilityDescription($viewer); + $body[] = $this->addItem(pht('Status'), $availability); + } + + $badges = $this->buildBadges($user, $viewer); + if ($badges) { + $badges = id(new PHUIBadgeBoxView()) + ->addItems($badges) + ->setCollapsed(true); + $body[] = phutil_tag( + 'div', + array( + 'class' => 'phui-hovercard-body-item hovercard-badges', + ), + $badges); + } + + $body = phutil_tag( + 'div', + array( + 'class' => 'project-card-body', + ), + $body); + + $card = phutil_tag( + 'div', + array( + 'class' => 'project-card-inner', + ), + array( + $header, + $body, + )); + + return $card; + } + + private function addItem($label, $value) { + $item = array( + phutil_tag('strong', array(), $label), + ': ', + phutil_tag('span', array(), $value), + ); + return phutil_tag_div('project-card-item', $item); + } + + private function buildBadges( + PhabricatorUser $user, + $viewer) { + + $class = 'PhabricatorBadgesApplication'; + $items = array(); + + if (PhabricatorApplication::isClassInstalledForViewer($class, $viewer)) { + $badge_phids = $user->getBadgePHIDs(); + if ($badge_phids) { + $badges = id(new PhabricatorBadgesQuery()) + ->setViewer($viewer) + ->withPHIDs($badge_phids) + ->withStatuses(array(PhabricatorBadgesBadge::STATUS_ACTIVE)) + ->execute(); + + foreach ($badges as $badge) { + $items[] = id(new PHUIBadgeMiniView()) + ->setIcon($badge->getIcon()) + ->setHeader($badge->getName()) + ->setQuality($badge->getQuality()); + } + } + } + return $items; + } + +} diff --git a/src/applications/policy/query/PhabricatorPolicyQuery.php b/src/applications/policy/query/PhabricatorPolicyQuery.php index df1d6fb0b8..e6e7008827 100644 --- a/src/applications/policy/query/PhabricatorPolicyQuery.php +++ b/src/applications/policy/query/PhabricatorPolicyQuery.php @@ -230,6 +230,10 @@ final class PhabricatorPolicyQuery ->setViewer($viewer) ->withMemberPHIDs(array($viewer->getPHID())) ->withIsMilestone(false) + ->withStatuses( + array( + PhabricatorProjectStatus::STATUS_ACTIVE, + )) ->setLimit($default_limit) ->execute(); $default_projects = mpull($default_projects, null, 'getPHID'); diff --git a/src/applications/project/application/PhabricatorProjectApplication.php b/src/applications/project/application/PhabricatorProjectApplication.php index 5f3cb9e090..3a39aefa22 100644 --- a/src/applications/project/application/PhabricatorProjectApplication.php +++ b/src/applications/project/application/PhabricatorProjectApplication.php @@ -65,13 +65,12 @@ final class PhabricatorProjectApplication extends PhabricatorApplication { => $this->getPanelRouting('PhabricatorProjectPanelController'), 'subprojects/(?P[1-9]\d*)/' => 'PhabricatorProjectSubprojectsController', - 'milestones/(?P[1-9]\d*)/' - => 'PhabricatorProjectMilestonesController', 'board/(?P[1-9]\d*)/'. '(?Pfilter/)?'. '(?:query/(?P[^/]+)/)?' => 'PhabricatorProjectBoardViewController', 'move/(?P[1-9]\d*)/' => 'PhabricatorProjectMoveController', + 'cover/' => 'PhabricatorProjectCoverController', 'board/(?P[1-9]\d*)/' => array( 'edit/(?:(?P\d+)/)?' => 'PhabricatorProjectColumnEditController', @@ -83,6 +82,8 @@ final class PhabricatorProjectApplication extends PhabricatorApplication { => 'PhabricatorProjectBoardImportController', 'reorder/' => 'PhabricatorProjectBoardReorderController', + 'disable/' + => 'PhabricatorProjectBoardDisableController', ), 'update/(?P[1-9]\d*)/(?P[^/]+)/' => 'PhabricatorProjectUpdateController', @@ -93,6 +94,8 @@ final class PhabricatorProjectApplication extends PhabricatorApplication { => 'PhabricatorProjectSilenceController', 'warning/(?P[1-9]\d*)/' => 'PhabricatorProjectSubprojectWarningController', + 'default/(?P[1-9]\d*)/(?P[^/]+)/' + => 'PhabricatorProjectDefaultController', ), '/tag/' => array( '(?P[^/]+)/' => 'PhabricatorProjectViewController', diff --git a/src/applications/project/controller/PhabricatorProjectBoardDisableController.php b/src/applications/project/controller/PhabricatorProjectBoardDisableController.php new file mode 100644 index 0000000000..0440ff9eff --- /dev/null +++ b/src/applications/project/controller/PhabricatorProjectBoardDisableController.php @@ -0,0 +1,61 @@ +getUser(); + $project_id = $request->getURIData('projectID'); + + $project = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->withIDs(array($project_id)) + ->executeOne(); + if (!$project) { + return new Aphront404Response(); + } + + if (!$project->getHasWorkboard()) { + return new Aphront404Response(); + } + + $this->setProject($project); + $id = $project->getID(); + + $board_uri = $this->getApplicationURI("board/{$id}/"); + + if ($request->isFormPost()) { + $xactions = array(); + + $xactions[] = id(new PhabricatorProjectTransaction()) + ->setTransactionType(PhabricatorProjectTransaction::TYPE_HASWORKBOARD) + ->setNewValue(0); + + id(new PhabricatorProjectTransactionEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->applyTransactions($project, $xactions); + + return id(new AphrontRedirectResponse()) + ->setURI($board_uri); + } + + return $this->newDialog() + ->setTitle(pht('Disable Workboard')) + ->appendParagraph( + pht( + 'Disabling a workboard hides the board. Objects on the board '. + 'will no longer be annotated with column names in other '. + 'applications. You can restore the workboard later.')) + ->addCancelButton($board_uri) + ->addSubmitButton(pht('Disable Workboard')); + } + +} diff --git a/src/applications/project/controller/PhabricatorProjectBoardImportController.php b/src/applications/project/controller/PhabricatorProjectBoardImportController.php index 8df4e8b810..988084d3ee 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardImportController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardImportController.php @@ -50,6 +50,10 @@ final class PhabricatorProjectBoardImportController if ($import_column->isHidden()) { continue; } + if ($import_column->getProxy()) { + continue; + } + $new_column = PhabricatorProjectColumn::initializeNewColumn($viewer) ->setSequence($import_column->getSequence()) ->setProjectPHID($project->getPHID()) diff --git a/src/applications/project/controller/PhabricatorProjectBoardViewController.php b/src/applications/project/controller/PhabricatorProjectBoardViewController.php index d0580a95ad..e9a5159295 100644 --- a/src/applications/project/controller/PhabricatorProjectBoardViewController.php +++ b/src/applications/project/controller/PhabricatorProjectBoardViewController.php @@ -7,9 +7,7 @@ final class PhabricatorProjectBoardViewController private $id; private $slug; - private $handles; private $queryKey; - private $filter; private $sortKey; private $showHidden; @@ -57,10 +55,18 @@ final class PhabricatorProjectBoardViewController $search_engine->getQueryResultsPageURI($saved->getQueryKey()))); } - $query_key = $request->getURIData('queryKey'); - if (!$query_key) { - $query_key = 'open'; + $query_key = $this->getDefaultFilter($project); + + $request_query = $request->getStr('filter'); + if (strlen($request_query)) { + $query_key = $request_query; } + + $uri_query = $request->getURIData('queryKey'); + if (strlen($uri_query)) { + $query_key = $uri_query; + } + $this->queryKey = $query_key; $custom_query = null; @@ -122,18 +128,37 @@ final class PhabricatorProjectBoardViewController ->setViewer($viewer) ->setBoardPHIDs(array($board_phid)) ->setObjectPHIDs(array_keys($tasks)) + ->setFetchAllBoards(true) ->executeLayout(); $columns = $layout_engine->getColumns($board_phid); - if (!$columns) { + if (!$columns || !$project->getHasWorkboard()) { + $has_normal_columns = false; + + foreach ($columns as $column) { + if (!$column->getProxyPHID()) { + $has_normal_columns = true; + break; + } + } + $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $project, PhabricatorPolicyCapability::CAN_EDIT); - if (!$can_edit) { - $content = $this->buildNoAccessContent($project); + + if (!$has_normal_columns) { + if (!$can_edit) { + $content = $this->buildNoAccessContent($project); + } else { + $content = $this->buildInitializeContent($project); + } } else { - $content = $this->buildInitializeContent($project); + if (!$can_edit) { + $content = $this->buildDisabledContent($project); + } else { + $content = $this->buildEnableContent($project); + } } if ($content instanceof AphrontResponse) { @@ -213,35 +238,16 @@ final class PhabricatorProjectBoardViewController $board = id(new PHUIWorkboardView()) ->setUser($viewer) - ->setID($board_id); - - $behavior_config = array( - 'boardID' => $board_id, - 'projectPHID' => $project->getPHID(), - 'moveURI' => $this->getApplicationURI('move/'.$project->getID().'/'), - 'createURI' => $this->getCreateURI(), - 'order' => $this->sortKey, - ); - $this->initBehavior( - 'project-boards', - $behavior_config); - - $this->handles = ManiphestTaskListView::loadTaskHandles($viewer, $tasks); - - $all_project_phids = array(); - foreach ($tasks as $task) { - foreach ($task->getProjectPHIDs() as $project_phid) { - $all_project_phids[$project_phid] = $project_phid; - } - } - - foreach ($select_phids as $phid) { - unset($all_project_phids[$phid]); - } - - $all_handles = $viewer->loadHandles($all_project_phids); - $all_handles = iterator_to_array($all_handles); + ->setID($board_id) + ->addSigil('jx-workboard') + ->setMetadata( + array( + 'boardPHID' => $project->getPHID(), + )); + $visible_columns = array(); + $column_phids = array(); + $visible_phids = array(); foreach ($columns as $column) { if (!$this->showHidden) { if ($column->isHidden()) { @@ -268,11 +274,40 @@ final class PhabricatorProjectBoardViewController $column_tasks = array_select_keys($column_tasks, array_keys($tasks)); } + $column_phid = $column->getPHID(); + + $visible_columns[$column_phid] = $column; + $column_phids[$column_phid] = $column_tasks; + + foreach ($column_tasks as $phid => $task) { + $visible_phids[$phid] = $phid; + } + } + + $rendering_engine = id(new PhabricatorBoardRenderingEngine()) + ->setViewer($viewer) + ->setObjects(array_select_keys($tasks, $visible_phids)) + ->setEditMap($task_can_edit_map) + ->setExcludedProjectPHIDs($select_phids); + + $templates = array(); + $column_maps = array(); + $all_tasks = array(); + foreach ($visible_columns as $column_phid => $column) { + $column_tasks = $column_phids[$column_phid]; + $panel = id(new PHUIWorkpanelView()) ->setHeader($column->getDisplayName()) ->setSubHeader($column->getDisplayType()) ->addSigil('workpanel'); + $proxy = $column->getProxy(); + if ($proxy) { + $proxy_id = $proxy->getID(); + $href = $this->getApplicationURI("view/{$proxy_id}/"); + $panel->setHref($href); + } + $header_icon = $column->getHeaderIcon(); if ($header_icon) { $panel->setHeaderIcon($header_icon); @@ -290,14 +325,17 @@ final class PhabricatorProjectBoardViewController $column_menu = $this->buildColumnMenu($project, $column); $panel->addHeaderAction($column_menu); - $tag_id = celerity_generate_unique_node_id(); - $tag_content_id = celerity_generate_unique_node_id(); - $count_tag = id(new PHUITagView()) ->setType(PHUITagView::TYPE_SHADE) ->setShade(PHUITagView::COLOR_BLUE) - ->setID($tag_id) - ->setName(phutil_tag('span', array('id' => $tag_content_id), '-')) + ->addSigil('column-points') + ->setName( + javelin_tag( + 'span', + array( + 'sigil' => 'column-points-content', + ), + pht('-'))) ->setStyle('display: none'); $panel->setHeaderTag($count_tag); @@ -311,38 +349,52 @@ final class PhabricatorProjectBoardViewController ->setMetadata( array( 'columnPHID' => $column->getPHID(), - 'countTagID' => $tag_id, - 'countTagContentID' => $tag_content_id, 'pointLimit' => $column->getPointLimit(), )); foreach ($column_tasks as $task) { - $owner = null; - if ($task->getOwnerPHID()) { - $owner = $this->handles[$task->getOwnerPHID()]; - } - $can_edit = idx($task_can_edit_map, $task->getPHID(), false); + $object_phid = $task->getPHID(); - $handles = array_select_keys($all_handles, $task->getProjectPHIDs()); + $card = $rendering_engine->renderCard($object_phid); + $templates[$object_phid] = hsprintf('%s', $card->getItem()); + $column_maps[$column_phid][] = $object_phid; - $cards->addItem(id(new ProjectBoardTaskCard()) - ->setViewer($viewer) - ->setProjectHandles($handles) - ->setTask($task) - ->setOwner($owner) - ->setCanEdit($can_edit) - ->getItem()); + $all_tasks[$object_phid] = $task; } + $panel->setCards($cards); $board->addPanel($panel); } + $behavior_config = array( + 'moveURI' => $this->getApplicationURI('move/'.$project->getID().'/'), + 'createURI' => $this->getCreateURI(), + 'uploadURI' => '/file/dropupload/', + 'coverURI' => $this->getApplicationURI('cover/'), + 'chunkThreshold' => PhabricatorFileStorageEngine::getChunkThreshold(), + 'pointsEnabled' => ManiphestTaskPoints::getIsEnabled(), + + 'boardPHID' => $project->getPHID(), + 'order' => $this->sortKey, + 'templateMap' => $templates, + 'columnMaps' => $column_maps, + 'orderMaps' => mpull($all_tasks, 'getWorkboardOrderVectors'), + 'propertyMaps' => mpull($all_tasks, 'getWorkboardProperties'), + + 'boardID' => $board_id, + 'projectPHID' => $project->getPHID(), + ); + $this->initBehavior('project-boards', $behavior_config); + + $sort_menu = $this->buildSortMenu( $viewer, + $project, $this->sortKey); $filter_menu = $this->buildFilterMenu( $viewer, + $project, $custom_query, $search_engine, $query_key); @@ -361,6 +413,9 @@ final class PhabricatorProjectBoardViewController ->addClass('project-board-wrapper'); $nav = $this->getProfileMenu(); + $divider = id(new PHUIListItemView()) + ->setType(PHUIListItemView::TYPE_DIVIDER); + $fullscreen = $this->buildFullscreenMenu(); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Workboard')); @@ -368,7 +423,9 @@ final class PhabricatorProjectBoardViewController $crumbs->addAction($sort_menu); $crumbs->addAction($filter_menu); + $crumbs->addAction($divider); $crumbs->addAction($manage_menu); + $crumbs->addAction($fullscreen); return $this->newPage() ->setTitle( @@ -397,20 +454,49 @@ final class PhabricatorProjectBoardViewController $this->showHidden = $request->getBool('hidden'); $this->id = $project->getID(); - $sort_key = $request->getStr('order'); - switch ($sort_key) { + $sort_key = $this->getDefaultSort($project); + + $request_sort = $request->getStr('order'); + if ($this->isValidSort($request_sort)) { + $sort_key = $request_sort; + } + + $this->sortKey = $sort_key; + } + + private function getDefaultSort(PhabricatorProject $project) { + $default_sort = $project->getDefaultWorkboardSort(); + + if ($this->isValidSort($default_sort)) { + return $default_sort; + } + + return PhabricatorProjectColumn::DEFAULT_ORDER; + } + + private function getDefaultFilter(PhabricatorProject $project) { + $default_filter = $project->getDefaultWorkboardFilter(); + + if (strlen($default_filter)) { + return $default_filter; + } + + return 'open'; + } + + private function isValidSort($sort) { + switch ($sort) { case PhabricatorProjectColumn::ORDER_NATURAL: case PhabricatorProjectColumn::ORDER_PRIORITY: - break; - default: - $sort_key = PhabricatorProjectColumn::DEFAULT_ORDER; - break; + return true; } - $this->sortKey = $sort_key; + + return false; } private function buildSortMenu( PhabricatorUser $viewer, + PhabricatorProject $project, $sort_key) { $sort_icon = id(new PHUIIconView()) @@ -441,6 +527,24 @@ final class PhabricatorProjectBoardViewController $items[] = $item; } + $id = $project->getID(); + + $save_uri = "default/{$id}/sort/"; + $save_uri = $this->getApplicationURI($save_uri); + $save_uri = $this->getURIWithState($save_uri, $force = true); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $project, + PhabricatorPolicyCapability::CAN_EDIT); + + $items[] = id(new PhabricatorActionView()) + ->setIcon('fa-floppy-o') + ->setName(pht('Save as Default')) + ->setHref($save_uri) + ->setWorkflow(true) + ->setDisabled(!$can_edit); + $sort_menu = id(new PhabricatorActionListView()) ->setUser($viewer); foreach ($items as $item) { @@ -448,7 +552,7 @@ final class PhabricatorProjectBoardViewController } $sort_button = id(new PHUIListItemView()) - ->setName(pht('Sort: %s', $active_order)) + ->setName($active_order) ->setIcon('fa-sort-amount-asc') ->setHref('#') ->addSigil('boards-dropdown-menu') @@ -459,8 +563,10 @@ final class PhabricatorProjectBoardViewController return $sort_button; } + private function buildFilterMenu( PhabricatorUser $viewer, + PhabricatorProject $project, $custom_query, PhabricatorApplicationSearchEngine $engine, $query_key) { @@ -503,18 +609,40 @@ final class PhabricatorProjectBoardViewController $uri = $engine->getQueryResultsPageURI($key); } - $uri = $this->getURIWithState($uri); + $uri = $this->getURIWithState($uri) + ->setQueryParam('filter', null); $item->setHref($uri); $items[] = $item; } + $id = $project->getID(); + + $filter_uri = $this->getApplicationURI("board/{$id}/filter/"); + $filter_uri = $this->getURIWithState($filter_uri, $force = true); + $items[] = id(new PhabricatorActionView()) ->setIcon('fa-cog') - ->setHref($this->getApplicationURI('board/'.$this->id.'/filter/')) + ->setHref($filter_uri) ->setWorkflow(true) ->setName(pht('Advanced Filter...')); + $save_uri = "default/{$id}/filter/"; + $save_uri = $this->getApplicationURI($save_uri); + $save_uri = $this->getURIWithState($save_uri, $force = true); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $project, + PhabricatorPolicyCapability::CAN_EDIT); + + $items[] = id(new PhabricatorActionView()) + ->setIcon('fa-floppy-o') + ->setName(pht('Save as Default')) + ->setHref($save_uri) + ->setWorkflow(true) + ->setDisabled(!$can_edit); + $filter_menu = id(new PhabricatorActionListView()) ->setUser($viewer); foreach ($items as $item) { @@ -522,7 +650,7 @@ final class PhabricatorProjectBoardViewController } $filter_button = id(new PHUIListItemView()) - ->setName(pht('Filter: %s', $active_filter)) + ->setName($active_filter) ->setIcon('fa-search') ->setHref('#') ->addSigil('boards-dropdown-menu') @@ -541,6 +669,12 @@ final class PhabricatorProjectBoardViewController $request = $this->getRequest(); $viewer = $request->getUser(); + $id = $project->getID(); + + $disable_uri = $this->getApplicationURI("board/{$id}/disable/"); + $add_uri = $this->getApplicationURI("board/{$id}/edit/"); + $reorder_uri = $this->getApplicationURI("board/{$id}/reorder/"); + $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $project, @@ -551,14 +685,14 @@ final class PhabricatorProjectBoardViewController $manage_items[] = id(new PhabricatorActionView()) ->setIcon('fa-plus') ->setName(pht('Add Column')) - ->setHref($this->getApplicationURI('board/'.$this->id.'/edit/')) + ->setHref($add_uri) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit); $manage_items[] = id(new PhabricatorActionView()) ->setIcon('fa-exchange') ->setName(pht('Reorder Columns')) - ->setHref($this->getApplicationURI('board/'.$this->id.'/reorder/')) + ->setHref($reorder_uri) ->setDisabled(!$can_edit) ->setWorkflow(true); @@ -592,6 +726,13 @@ final class PhabricatorProjectBoardViewController ->setHref($batch_edit_uri) ->setDisabled(!$can_batch_edit); + $manage_items[] = id(new PhabricatorActionView()) + ->setIcon('fa-ban') + ->setName(pht('Disable Workboard')) + ->setHref($disable_uri) + ->setWorkflow(true) + ->setDisabled(!$can_edit); + $manage_menu = id(new PhabricatorActionListView()) ->setUser($viewer); foreach ($manage_items as $item) { @@ -599,18 +740,38 @@ final class PhabricatorProjectBoardViewController } $manage_button = id(new PHUIListItemView()) - ->setName(pht('Manage Board')) ->setIcon('fa-cog') ->setHref('#') ->addSigil('boards-dropdown-menu') + ->addSigil('has-tooltip') ->setMetadata( array( + 'tip' => pht('Manage'), + 'align' => 'S', 'items' => hsprintf('%s', $manage_menu), )); return $manage_button; } + private function buildFullscreenMenu() { + + $up = id(new PHUIListItemView()) + ->setIcon('fa-arrows-alt') + ->setHref('#') + ->addClass('phui-workboard-expand-icon') + ->addSigil('jx-toggle-class') + ->addSigil('has-tooltip') + ->setMetaData(array( + 'tip' => pht('Fullscreen'), + 'map' => array( + 'phabricator-standard-page' => 'phui-workboard-fullscreen', + ), + )); + + return $up; + } + private function buildColumnMenu( PhabricatorProject $project, PhabricatorProjectColumn $column) { @@ -639,6 +800,7 @@ final class PhabricatorProjectBoardViewController ->setMetadata( array( 'columnPHID' => $column->getPHID(), + 'boardPHID' => $project->getPHID(), 'projectPHID' => $default_phid, )); @@ -711,22 +873,31 @@ final class PhabricatorProjectBoardViewController * the rest of the board state persistent. If no URI is provided, this method * starts with the request URI. * - * @param string|null URI to add state parameters to. - * @return PhutilURI URI with state parameters. + * @param string|null URI to add state parameters to. + * @param bool True to explicitly include all state. + * @return PhutilURI URI with state parameters. */ - private function getURIWithState($base = null) { + private function getURIWithState($base = null, $force = false) { + $project = $this->getProject(); + if ($base === null) { $base = $this->getRequest()->getRequestURI(); } $base = new PhutilURI($base); - if ($this->sortKey != PhabricatorProjectColumn::DEFAULT_ORDER) { + if ($force || ($this->sortKey != $this->getDefaultSort($project))) { $base->setQueryParam('order', $this->sortKey); } else { $base->setQueryParam('order', null); } + if ($force || ($this->queryKey != $this->getDefaultFilter($project))) { + $base->setQueryParam('filter', $this->queryKey); + } else { + $base->setQueryParam('filter', null); + } + $base->setQueryParam('hidden', $this->showHidden ? 'true' : null); return $base; @@ -849,4 +1020,59 @@ final class PhabricatorProjectBoardViewController ->addCancelButton($profile_uri); } + + private function buildEnableContent(PhabricatorProject $project) { + $request = $this->getRequest(); + $viewer = $this->getViewer(); + + $id = $project->getID(); + $profile_uri = $this->getApplicationURI("profile/{$id}/"); + $board_uri = $this->getApplicationURI("board/{$id}/"); + + if ($request->isFormPost()) { + $xactions = array(); + + $xactions[] = id(new PhabricatorProjectTransaction()) + ->setTransactionType(PhabricatorProjectTransaction::TYPE_HASWORKBOARD) + ->setNewValue(1); + + id(new PhabricatorProjectTransactionEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->applyTransactions($project, $xactions); + + return id(new AphrontRedirectResponse()) + ->setURI($board_uri); + } + + return $this->newDialog() + ->setTitle(pht('Workboard Disabled')) + ->addHiddenInput('initialize', 1) + ->appendParagraph( + pht( + 'This workboard has been disabled, but can be restored to its '. + 'former glory.')) + ->addCancelButton($profile_uri) + ->addSubmitButton(pht('Enable Workboard')); + } + + private function buildDisabledContent(PhabricatorProject $project) { + $viewer = $this->getViewer(); + + $id = $project->getID(); + + $profile_uri = $this->getApplicationURI("profile/{$id}/"); + + return $this->newDialog() + ->setTitle(pht('Workboard Disabled')) + ->appendParagraph( + pht( + 'This workboard has been disabled, and you do not have permission '. + 'to enable it. Only users who can edit this project can restore '. + 'the workboard.')) + ->addCancelButton($profile_uri); + } + } diff --git a/src/applications/project/controller/PhabricatorProjectColumnDetailController.php b/src/applications/project/controller/PhabricatorProjectColumnDetailController.php index e008c832d9..b0cd4ac0b4 100644 --- a/src/applications/project/controller/PhabricatorProjectColumnDetailController.php +++ b/src/applications/project/controller/PhabricatorProjectColumnDetailController.php @@ -120,9 +120,12 @@ final class PhabricatorProjectColumnDetailController ->setActionList($actions); $limit = $column->getPointLimit(); - $properties->addProperty( - pht('Point Limit'), - $limit ? $limit : pht('No Limit')); + if ($limit === null) { + $limit_text = pht('No Limit'); + } else { + $limit_text = $limit; + } + $properties->addProperty(pht('Point Limit'), $limit_text); return $properties; } diff --git a/src/applications/project/controller/PhabricatorProjectColumnHideController.php b/src/applications/project/controller/PhabricatorProjectColumnHideController.php index ae39e783bb..27dbb17c47 100644 --- a/src/applications/project/controller/PhabricatorProjectColumnHideController.php +++ b/src/applications/project/controller/PhabricatorProjectColumnHideController.php @@ -52,42 +52,81 @@ final class PhabricatorProjectColumnHideController ->addCancelButton($view_uri, pht('Okay')); } + $proxy = $column->getProxy(); + if ($request->isFormPost()) { - if ($column->isHidden()) { - $new_status = PhabricatorProjectColumn::STATUS_ACTIVE; + if ($proxy) { + if ($proxy->isArchived()) { + $new_status = PhabricatorProjectStatus::STATUS_ACTIVE; + } else { + $new_status = PhabricatorProjectStatus::STATUS_ARCHIVED; + } + + $xactions = array(); + + $xactions[] = id(new PhabricatorProjectTransaction()) + ->setTransactionType(PhabricatorProjectTransaction::TYPE_STATUS) + ->setNewValue($new_status); + + id(new PhabricatorProjectTransactionEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->applyTransactions($proxy, $xactions); } else { - $new_status = PhabricatorProjectColumn::STATUS_HIDDEN; + if ($column->isHidden()) { + $new_status = PhabricatorProjectColumn::STATUS_ACTIVE; + } else { + $new_status = PhabricatorProjectColumn::STATUS_HIDDEN; + } + + $type_status = PhabricatorProjectColumnTransaction::TYPE_STATUS; + $xactions = array( + id(new PhabricatorProjectColumnTransaction()) + ->setTransactionType($type_status) + ->setNewValue($new_status), + ); + + $editor = id(new PhabricatorProjectColumnTransactionEditor()) + ->setActor($viewer) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->setContentSourceFromRequest($request) + ->applyTransactions($column, $xactions); } - $type_status = PhabricatorProjectColumnTransaction::TYPE_STATUS; - $xactions = array( - id(new PhabricatorProjectColumnTransaction()) - ->setTransactionType($type_status) - ->setNewValue($new_status), - ); - - $editor = id(new PhabricatorProjectColumnTransactionEditor()) - ->setActor($viewer) - ->setContinueOnNoEffect(true) - ->setContentSourceFromRequest($request) - ->applyTransactions($column, $xactions); - return id(new AphrontRedirectResponse())->setURI($view_uri); } - if ($column->isHidden()) { - $title = pht('Show Column'); + if ($proxy) { + if ($column->isHidden()) { + $title = pht('Activate and Show Column'); + $body = pht( + 'This column is hidden because it represents an archived '. + 'subproject. Do you want to activate the subproject so the '. + 'column is visible again?'); + $button = pht('Activate Subproject'); + } else { + $title = pht('Archive and Hide Column'); + $body = pht( + 'This column is visible because it represents an active '. + 'subproject. Do you want to hide the column by archiving the '. + 'subproject?'); + $button = pht('Archive Subproject'); + } } else { - $title = pht('Hide Column'); - } - - if ($column->isHidden()) { - $body = pht( - 'Are you sure you want to show this column?'); - } else { - $body = pht( - 'Are you sure you want to hide this column? It will no longer '. - 'appear on the workboard.'); + if ($column->isHidden()) { + $title = pht('Show Column'); + $body = pht('Are you sure you want to show this column?'); + $button = pht('Show Column'); + } else { + $title = pht('Hide Column'); + $body = pht( + 'Are you sure you want to hide this column? It will no longer '. + 'appear on the workboard.'); + $button = pht('Hide Column'); + } } $dialog = $this->newDialog() @@ -96,7 +135,7 @@ final class PhabricatorProjectColumnHideController ->appendChild($body) ->setDisableWorkflowOnCancel(true) ->addCancelButton($view_uri) - ->addSubmitButton($title); + ->addSubmitButton($button); foreach ($request->getPassthroughRequestData() as $key => $value) { $dialog->addHiddenInput($key, $value); diff --git a/src/applications/project/controller/PhabricatorProjectController.php b/src/applications/project/controller/PhabricatorProjectController.php index 8687e0a3a7..57d531c951 100644 --- a/src/applications/project/controller/PhabricatorProjectController.php +++ b/src/applications/project/controller/PhabricatorProjectController.php @@ -147,4 +147,21 @@ abstract class PhabricatorProjectController extends PhabricatorController { return $this; } + protected function newCardResponse($board_phid, $object_phid) { + $viewer = $this->getViewer(); + + $request = $this->getRequest(); + $visible_phids = $request->getStrList('visiblePHIDs'); + if (!$visible_phids) { + $visible_phids = array(); + } + + return id(new PhabricatorBoardResponseEngine()) + ->setViewer($viewer) + ->setBoardPHID($board_phid) + ->setObjectPHID($object_phid) + ->setVisiblePHIDs($visible_phids) + ->buildResponse(); + } + } diff --git a/src/applications/project/controller/PhabricatorProjectCoverController.php b/src/applications/project/controller/PhabricatorProjectCoverController.php new file mode 100644 index 0000000000..22f787e56b --- /dev/null +++ b/src/applications/project/controller/PhabricatorProjectCoverController.php @@ -0,0 +1,53 @@ +getViewer(); + + $request->validateCSRF(); + + $board_phid = $request->getStr('boardPHID'); + $object_phid = $request->getStr('objectPHID'); + $file_phid = $request->getStr('filePHID'); + + $object = id(new ManiphestTaskQuery()) + ->setViewer($viewer) + ->withPHIDs(array($object_phid)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$object) { + return new Aphront404Response(); + } + + $file = id(new PhabricatorFileQuery()) + ->setViewer($viewer) + ->withPHIDs(array($file_phid)) + ->executeOne(); + if (!$file) { + return new Aphront404Response(); + } + + $xactions = array(); + + $xactions[] = id(new ManiphestTransaction()) + ->setTransactionType(ManiphestTransaction::TYPE_COVER_IMAGE) + ->setNewValue($file->getPHID()); + + $editor = id(new ManiphestTransactionEditor()) + ->setActor($viewer) + ->setContinueOnMissingFields(true) + ->setContinueOnNoEffect(true) + ->setContentSourceFromRequest($request); + + $editor->applyTransactions($object, $xactions); + + return $this->newCardResponse($board_phid, $object_phid); + } + +} diff --git a/src/applications/project/controller/PhabricatorProjectDefaultController.php b/src/applications/project/controller/PhabricatorProjectDefaultController.php new file mode 100644 index 0000000000..0246f33f43 --- /dev/null +++ b/src/applications/project/controller/PhabricatorProjectDefaultController.php @@ -0,0 +1,90 @@ +getViewer(); + $project_id = $request->getURIData('projectID'); + + $project = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->withIDs(array($project_id)) + ->executeOne(); + if (!$project) { + return new Aphront404Response(); + } + $this->setProject($project); + + $target = $request->getURIData('target'); + switch ($target) { + case 'filter': + $title = pht('Set Board Default Filter'); + $body = pht( + 'Make the current filter the new default filter for this board? '. + 'All users will see the new filter as the default when they view '. + 'the board.'); + $button = pht('Save Default Filter'); + + $xaction_value = $request->getStr('filter'); + $xaction_type = PhabricatorProjectTransaction::TYPE_DEFAULT_FILTER; + break; + case 'sort': + $title = pht('Set Board Default Order'); + $body = pht( + 'Make the current sort order the new default order for this board? '. + 'All users will see the new order as the default when they view '. + 'the board.'); + $button = pht('Save Default Order'); + + $xaction_value = $request->getStr('order'); + $xaction_type = PhabricatorProjectTransaction::TYPE_DEFAULT_SORT; + break; + default: + return new Aphront404Response(); + } + + $id = $project->getID(); + + $view_uri = $this->getApplicationURI("board/{$id}/"); + $view_uri = new PhutilURI($view_uri); + foreach ($request->getPassthroughRequestData() as $key => $value) { + $view_uri->setQueryParam($key, $value); + } + + if ($request->isFormPost()) { + $xactions = array(); + + $xactions[] = id(new PhabricatorProjectTransaction()) + ->setTransactionType($xaction_type) + ->setNewValue($xaction_value); + + id(new PhabricatorProjectTransactionEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->applyTransactions($project, $xactions); + + return id(new AphrontRedirectResponse())->setURI($view_uri); + } + + $dialog = $this->newDialog() + ->setTitle($title) + ->appendChild($body) + ->setDisableWorkflowOnCancel(true) + ->addCancelButton($view_uri) + ->addSubmitButton($title); + + foreach ($request->getPassthroughRequestData() as $key => $value) { + $dialog->addHiddenInput($key, $value); + } + + return $dialog; + } +} diff --git a/src/applications/project/controller/PhabricatorProjectMoveController.php b/src/applications/project/controller/PhabricatorProjectMoveController.php index 7cbbf3d0ae..d3540a1781 100644 --- a/src/applications/project/controller/PhabricatorProjectMoveController.php +++ b/src/applications/project/controller/PhabricatorProjectMoveController.php @@ -7,13 +7,14 @@ final class PhabricatorProjectMoveController $viewer = $request->getViewer(); $id = $request->getURIData('id'); + $request->validateCSRF(); + $column_phid = $request->getStr('columnPHID'); $object_phid = $request->getStr('objectPHID'); $after_phid = $request->getStr('afterPHID'); $before_phid = $request->getStr('beforePHID'); $order = $request->getStr('order', PhabricatorProjectColumn::DEFAULT_ORDER); - $project = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->requireCapabilities( @@ -89,55 +90,13 @@ final class PhabricatorProjectMoveController 'projectPHID' => $column->getProjectPHID(), )); - $task_phids = array(); - if ($after_phid) { - $task_phids[] = $after_phid; - } - if ($before_phid) { - $task_phids[] = $before_phid; - } - - if ($task_phids && ($order == PhabricatorProjectColumn::ORDER_PRIORITY)) { - $tasks = id(new ManiphestTaskQuery()) - ->setViewer($viewer) - ->withPHIDs($task_phids) - ->needProjectPHIDs(true) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->execute(); - if (count($tasks) != count($task_phids)) { - return new Aphront404Response(); - } - $tasks = mpull($tasks, null, 'getPHID'); - - $try = array( - array($after_phid, true), - array($before_phid, false), - ); - - $pri = null; - $sub = null; - foreach ($try as $spec) { - list($task_phid, $is_after) = $spec; - $task = idx($tasks, $task_phid); - if ($task) { - list($pri, $sub) = ManiphestTransactionEditor::getAdjacentSubpriority( - $task, - $is_after); - break; - } - } - - if ($pri !== null) { - $xactions[] = id(new ManiphestTransaction()) - ->setTransactionType(ManiphestTransaction::TYPE_PRIORITY) - ->setNewValue($pri); - $xactions[] = id(new ManiphestTransaction()) - ->setTransactionType(ManiphestTransaction::TYPE_SUBPRIORITY) - ->setNewValue($sub); + if ($order == PhabricatorProjectColumn::ORDER_PRIORITY) { + $priority_xactions = $this->getPriorityTransactions( + $object, + $after_phid, + $before_phid); + foreach ($priority_xactions as $xaction) { + $xactions[] = $xaction; } } @@ -175,56 +134,100 @@ final class PhabricatorProjectMoveController $editor->applyTransactions($object, $xactions); - $owner = null; - if ($object->getOwnerPHID()) { - $owner = id(new PhabricatorHandleQuery()) - ->setViewer($viewer) - ->withPHIDs(array($object->getOwnerPHID())) - ->executeOne(); + return $this->newCardResponse($board_phid, $object_phid); + } + + private function getPriorityTransactions( + ManiphestTask $task, + $after_phid, + $before_phid) { + + list($after_task, $before_task) = $this->loadPriorityTasks( + $after_phid, + $before_phid); + + $must_move = false; + if ($after_task && !$task->isLowerPriorityThan($after_task)) { + $must_move = true; } - // Reload the object so it reflects edits which have been applied. - $object = id(new ManiphestTaskQuery()) - ->setViewer($viewer) - ->withPHIDs(array($object_phid)) - ->needProjectPHIDs(true) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); + if ($before_task && !$task->isHigherPriorityThan($before_task)) { + $must_move = true; + } - $except_phids = array($board_phid); - if ($project->getHasSubprojects() || $project->getHasMilestones()) { - $descendants = id(new PhabricatorProjectQuery()) - ->setViewer($viewer) - ->withAncestorProjectPHIDs($except_phids) - ->execute(); - foreach ($descendants as $descendant) { - $except_phids[] = $descendant->getPHID(); + // The move doesn't require a priority change to be valid, so don't + // change the priority since we are not being forced to. + if (!$must_move) { + return array(); + } + + $try = array( + array($after_task, true), + array($before_task, false), + ); + + $pri = null; + $sub = null; + foreach ($try as $spec) { + list($task, $is_after) = $spec; + + if (!$task) { + continue; } + + list($pri, $sub) = ManiphestTransactionEditor::getAdjacentSubpriority( + $task, + $is_after); } - $except_phids = array_fuse($except_phids); - $handle_phids = array_fuse($object->getProjectPHIDs()); - $handle_phids = array_diff_key($handle_phids, $except_phids); + $xactions = array(); + if ($pri !== null) { + $xactions[] = id(new ManiphestTransaction()) + ->setTransactionType(ManiphestTransaction::TYPE_PRIORITY) + ->setNewValue($pri); + $xactions[] = id(new ManiphestTransaction()) + ->setTransactionType(ManiphestTransaction::TYPE_SUBPRIORITY) + ->setNewValue($sub); + } - $project_handles = $viewer->loadHandles($handle_phids); - $project_handles = iterator_to_array($project_handles); + return $xactions; + } - $card = id(new ProjectBoardTaskCard()) + private function loadPriorityTasks($after_phid, $before_phid) { + $viewer = $this->getViewer(); + + $task_phids = array(); + + if ($after_phid) { + $task_phids[] = $after_phid; + } + if ($before_phid) { + $task_phids[] = $before_phid; + } + + if (!$task_phids) { + return array(null, null); + } + + $tasks = id(new ManiphestTaskQuery()) ->setViewer($viewer) - ->setTask($object) - ->setOwner($owner) - ->setCanEdit(true) - ->setProjectHandles($project_handles) - ->getItem(); + ->withPHIDs($task_phids) + ->execute(); + $tasks = mpull($tasks, null, 'getPHID'); - $card->addClass('phui-workcard'); + if ($after_phid) { + $after_task = idx($tasks, $after_phid); + } else { + $after_task = null; + } - return id(new AphrontAjaxResponse())->setContent( - array('task' => $card)); + if ($before_phid) { + $before_task = idx($tasks, $before_phid); + } else { + $before_task = null; + } + + return array($after_task, $before_task); } } diff --git a/src/applications/project/controller/PhabricatorProjectProfileController.php b/src/applications/project/controller/PhabricatorProjectProfileController.php index 4e670475ce..27cf1bf344 100644 --- a/src/applications/project/controller/PhabricatorProjectProfileController.php +++ b/src/applications/project/controller/PhabricatorProjectProfileController.php @@ -201,6 +201,10 @@ final class PhabricatorProjectProfileController ->withParentProjectPHIDs(array($project->getPHID())) ->needImages(true) ->withIsMilestone(true) + ->withStatuses( + array( + PhabricatorProjectStatus::STATUS_ACTIVE, + )) ->setOrder('newest') ->execute(); if (!$milestones) { @@ -244,6 +248,10 @@ final class PhabricatorProjectProfileController ->setViewer($viewer) ->withParentProjectPHIDs(array($project->getPHID())) ->needImages(true) + ->withStatuses( + array( + PhabricatorProjectStatus::STATUS_ACTIVE, + )) ->withIsMilestone(false) ->setLimit($limit) ->execute(); diff --git a/src/applications/project/editor/PhabricatorProjectColumnTransactionEditor.php b/src/applications/project/editor/PhabricatorProjectColumnTransactionEditor.php index 08b0355ce5..19f059b8b5 100644 --- a/src/applications/project/editor/PhabricatorProjectColumnTransactionEditor.php +++ b/src/applications/project/editor/PhabricatorProjectColumnTransactionEditor.php @@ -47,10 +47,12 @@ final class PhabricatorProjectColumnTransactionEditor case PhabricatorProjectColumnTransaction::TYPE_STATUS: return $xaction->getNewValue(); case PhabricatorProjectColumnTransaction::TYPE_LIMIT: - if ($xaction->getNewValue()) { + $value = $xaction->getNewValue(); + if (strlen($value)) { return (int)$xaction->getNewValue(); + } else { + return null; } - return null; } return parent::getCustomTransactionNewValue($object, $xaction); @@ -104,7 +106,9 @@ final class PhabricatorProjectColumnTransactionEditor $errors[] = new PhabricatorApplicationTransactionValidationError( $type, pht('Invalid'), - pht('Column point limit must be empty, or a positive integer.'), + pht( + 'Column point limit must either be empty or a nonnegative '. + 'integer.'), $xaction); } } diff --git a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php index 867bf7e9fe..b1aab1e417 100644 --- a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php +++ b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php @@ -39,6 +39,9 @@ final class PhabricatorProjectTransactionEditor $types[] = PhabricatorProjectTransaction::TYPE_LOCKED; $types[] = PhabricatorProjectTransaction::TYPE_PARENT; $types[] = PhabricatorProjectTransaction::TYPE_MILESTONE; + $types[] = PhabricatorProjectTransaction::TYPE_HASWORKBOARD; + $types[] = PhabricatorProjectTransaction::TYPE_DEFAULT_SORT; + $types[] = PhabricatorProjectTransaction::TYPE_DEFAULT_FILTER; return $types; } @@ -65,9 +68,15 @@ final class PhabricatorProjectTransactionEditor return $object->getColor(); case PhabricatorProjectTransaction::TYPE_LOCKED: return (int)$object->getIsMembershipLocked(); + case PhabricatorProjectTransaction::TYPE_HASWORKBOARD: + return (int)$object->getHasWorkboard(); case PhabricatorProjectTransaction::TYPE_PARENT: case PhabricatorProjectTransaction::TYPE_MILESTONE: return null; + case PhabricatorProjectTransaction::TYPE_DEFAULT_SORT: + return $object->getDefaultWorkboardSort(); + case PhabricatorProjectTransaction::TYPE_DEFAULT_FILTER: + return $object->getDefaultWorkboardFilter(); } return parent::getCustomTransactionOldValue($object, $xaction); @@ -86,7 +95,11 @@ final class PhabricatorProjectTransactionEditor case PhabricatorProjectTransaction::TYPE_LOCKED: case PhabricatorProjectTransaction::TYPE_PARENT: case PhabricatorProjectTransaction::TYPE_MILESTONE: + case PhabricatorProjectTransaction::TYPE_DEFAULT_SORT: + case PhabricatorProjectTransaction::TYPE_DEFAULT_FILTER: return $xaction->getNewValue(); + case PhabricatorProjectTransaction::TYPE_HASWORKBOARD: + return (int)$xaction->getNewValue(); case PhabricatorProjectTransaction::TYPE_SLUGS: return $this->normalizeSlugs($xaction->getNewValue()); } @@ -131,6 +144,15 @@ final class PhabricatorProjectTransactionEditor $object->setMilestoneNumber($number); $object->setParentProjectPHID($xaction->getNewValue()); return; + case PhabricatorProjectTransaction::TYPE_HASWORKBOARD: + $object->setHasWorkboard($xaction->getNewValue()); + return; + case PhabricatorProjectTransaction::TYPE_DEFAULT_SORT: + $object->setDefaultWorkboardSort($xaction->getNewValue()); + return; + case PhabricatorProjectTransaction::TYPE_DEFAULT_FILTER: + $object->setDefaultWorkboardFilter($xaction->getNewValue()); + return; } return parent::applyCustomInternalTransaction($object, $xaction); @@ -147,11 +169,12 @@ final class PhabricatorProjectTransactionEditor case PhabricatorProjectTransaction::TYPE_NAME: // First, add the old name as a secondary slug; this is helpful // for renames and generally a good thing to do. - if ($old !== null) { - $this->addSlug($object, $old, false); + if (!$this->getIsMilestone()) { + if ($old !== null) { + $this->addSlug($object, $old, false); + } + $this->addSlug($object, $new, false); } - $this->addSlug($object, $new, false); - return; case PhabricatorProjectTransaction::TYPE_SLUGS: $old = $xaction->getOldValue(); @@ -172,6 +195,9 @@ final class PhabricatorProjectTransactionEditor case PhabricatorProjectTransaction::TYPE_LOCKED: case PhabricatorProjectTransaction::TYPE_PARENT: case PhabricatorProjectTransaction::TYPE_MILESTONE: + case PhabricatorProjectTransaction::TYPE_HASWORKBOARD: + case PhabricatorProjectTransaction::TYPE_DEFAULT_SORT: + case PhabricatorProjectTransaction::TYPE_DEFAULT_FILTER: return; } @@ -631,6 +657,7 @@ final class PhabricatorProjectTransactionEditor } break; case PhabricatorProjectTransaction::TYPE_PARENT: + case PhabricatorProjectTransaction::TYPE_MILESTONE: $materialize = true; $new_parent = $object->getParentProject(); break; @@ -669,6 +696,11 @@ final class PhabricatorProjectTransactionEditor ->rematerialize($object); } + if ($new_parent) { + id(new PhabricatorProjectsMembershipIndexEngineExtension()) + ->rematerialize($new_parent); + } + return parent::applyFinalEffects($object, $xactions); } @@ -851,8 +883,16 @@ final class PhabricatorProjectTransactionEditor PhabricatorLiskDAO $object, array $xactions) { + // Herald rules may run on behalf of other users and need to execute + // membership checks against ancestors. + $project = id(new PhabricatorProjectQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withPHIDs(array($object->getPHID())) + ->needAncestorMembers(true) + ->executeOne(); + return id(new PhabricatorProjectHeraldAdapter()) - ->setProject($object); + ->setProject($project); } } diff --git a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php index 4415e2fdf9..3525af5ff0 100644 --- a/src/applications/project/engine/PhabricatorBoardLayoutEngine.php +++ b/src/applications/project/engine/PhabricatorBoardLayoutEngine.php @@ -9,6 +9,7 @@ final class PhabricatorBoardLayoutEngine extends Phobject { private $columnMap = array(); private $objectColumnMap = array(); private $boardLayout = array(); + private $fetchAllBoards; private $remQueue = array(); private $addQueue = array(); @@ -40,6 +41,18 @@ final class PhabricatorBoardLayoutEngine extends Phobject { return $this->objectPHIDs; } + /** + * Fetch all boards, even if the board is disabled. + */ + public function setFetchAllBoards($fetch_all) { + $this->fetchAllBoards = $fetch_all; + return $this; + } + + public function getFetchAllBoards() { + return $this->fetchAllBoards; + } + public function executeLayout() { $viewer = $this->getViewer(); @@ -73,9 +86,14 @@ final class PhabricatorBoardLayoutEngine extends Phobject { return array_select_keys($this->columnMap, array_keys($columns)); } - public function getColumnObjectPHIDs($board_phid, $column_phid) { + public function getColumnObjectPositions($board_phid, $column_phid) { $columns = idx($this->boardLayout, $board_phid, array()); - $positions = idx($columns, $column_phid, array()); + return idx($columns, $column_phid, array()); + } + + + public function getColumnObjectPHIDs($board_phid, $column_phid) { + $positions = $this->getColumnObjectPositions($board_phid, $column_phid); return mpull($positions, 'getObjectPHID'); } @@ -301,9 +319,11 @@ final class PhabricatorBoardLayoutEngine extends Phobject { ->execute(); $boards = mpull($boards, null, 'getPHID'); - foreach ($boards as $key => $board) { - if (!$board->getHasWorkboard()) { - unset($boards[$key]); + if (!$this->fetchAllBoards) { + foreach ($boards as $key => $board) { + if (!$board->getHasWorkboard()) { + unset($boards[$key]); + } } } @@ -346,7 +366,12 @@ final class PhabricatorBoardLayoutEngine extends Phobject { if ($board->getHasMilestones() || $board->getHasSubprojects()) { $child_projects = idx($children, $board_phid, array()); - $next_sequence = last($board_columns)->getSequence() + 1; + if ($board_columns) { + $next_sequence = last($board_columns)->getSequence() + 1; + } else { + $next_sequence = 1; + } + $proxy_columns = mpull($board_columns, null, 'getProxyPHID'); foreach ($child_projects as $child_phid => $child) { if (isset($proxy_columns[$child_phid])) { @@ -413,6 +438,7 @@ final class PhabricatorBoardLayoutEngine extends Phobject { $position_groups = mgroup($positions, 'getObjectPHID'); $layout = array(); + $default_phid = null; foreach ($columns as $column) { $column_phid = $column->getPHID(); $layout[$column_phid] = array(); @@ -435,7 +461,7 @@ final class PhabricatorBoardLayoutEngine extends Phobject { // If we have proxies, we need to force cards into the correct proxy // columns. - if ($proxy_map) { + if ($proxy_map && $object_phids) { $edge_query = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs($object_phids) ->withEdgeTypes( @@ -545,8 +571,9 @@ final class PhabricatorBoardLayoutEngine extends Phobject { } } - // If the object has no position, put it on the default column. - if (!$positions) { + // If the object has no position, put it on the default column if + // one exists. + if (!$positions && $default_phid) { $new_position = id(new PhabricatorProjectColumnPosition()) ->setBoardPHID($board_phid) ->setColumnPHID($default_phid) diff --git a/src/applications/project/engine/PhabricatorBoardRenderingEngine.php b/src/applications/project/engine/PhabricatorBoardRenderingEngine.php new file mode 100644 index 0000000000..ca8b0633da --- /dev/null +++ b/src/applications/project/engine/PhabricatorBoardRenderingEngine.php @@ -0,0 +1,144 @@ +viewer = $viewer; + return $this; + } + + public function getViewer() { + return $this->viewer; + } + + public function setObjects(array $objects) { + $this->objects = mpull($objects, null, 'getPHID'); + return $this; + } + + public function getObjects() { + return $this->objects; + } + + public function setExcludedProjectPHIDs(array $phids) { + $this->excludedProjectPHIDs = $phids; + return $this; + } + + public function getExcludedProjectPHIDs() { + return $this->excludedProjectPHIDs; + } + + public function setEditMap(array $edit_map) { + $this->editMap = $edit_map; + return $this; + } + + public function getEditMap() { + return $this->editMap; + } + + public function renderCard($phid) { + $this->willRender(); + + $viewer = $this->getViewer(); + $object = idx($this->getObjects(), $phid); + + $card = id(new ProjectBoardTaskCard()) + ->setViewer($viewer) + ->setTask($object) + ->setCanEdit($this->getCanEdit($phid)); + + $owner_phid = $object->getOwnerPHID(); + if ($owner_phid) { + $owner_handle = $this->handles[$owner_phid]; + $card->setOwner($owner_handle); + } + + $project_phids = $object->getProjectPHIDs(); + $project_handles = array_select_keys($this->handles, $project_phids); + if ($project_handles) { + $card->setProjectHandles($project_handles); + } + + $cover_phid = $object->getCoverImageThumbnailPHID(); + if ($cover_phid) { + $cover_file = idx($this->coverFiles, $cover_phid); + if ($cover_file) { + $card->setCoverImageFile($cover_file); + } + } + + return $card; + } + + private function willRender() { + if ($this->loaded) { + return; + } + + $phids = array(); + foreach ($this->objects as $object) { + $owner_phid = $object->getOwnerPHID(); + if ($owner_phid) { + $phids[$owner_phid] = $owner_phid; + } + + foreach ($object->getProjectPHIDs() as $phid) { + $phids[$phid] = $phid; + } + } + + if ($this->excludedProjectPHIDs) { + foreach ($this->excludedProjectPHIDs as $excluded_phid) { + unset($phids[$excluded_phid]); + } + } + + $viewer = $this->getViewer(); + + $handles = $viewer->loadHandles($phids); + $handles = iterator_to_array($handles); + $this->handles = $handles; + + $cover_phids = array(); + foreach ($this->objects as $object) { + $cover_phid = $object->getCoverImageThumbnailPHID(); + if ($cover_phid) { + $cover_phids[$cover_phid] = $cover_phid; + } + } + + if ($cover_phids) { + $cover_files = id(new PhabricatorFileQuery()) + ->setViewer($viewer) + ->withPHIDs($cover_phids) + ->execute(); + $cover_files = mpull($cover_files, null, 'getPHID'); + } else { + $cover_files = array(); + } + + $this->coverFiles = $cover_files; + + $this->loaded = true; + } + + private function getCanEdit($phid) { + if ($this->editMap === null) { + return true; + } + + return idx($this->editMap, $phid); + } + +} diff --git a/src/applications/project/engine/PhabricatorBoardResponseEngine.php b/src/applications/project/engine/PhabricatorBoardResponseEngine.php new file mode 100644 index 0000000000..969dfa3bc8 --- /dev/null +++ b/src/applications/project/engine/PhabricatorBoardResponseEngine.php @@ -0,0 +1,149 @@ +viewer = $viewer; + return $this; + } + + public function getViewer() { + return $this->viewer; + } + + public function setBoardPHID($board_phid) { + $this->boardPHID = $board_phid; + return $this; + } + + public function getBoardPHID() { + return $this->boardPHID; + } + + public function setObjectPHID($object_phid) { + $this->objectPHID = $object_phid; + return $this; + } + + public function getObjectPHID() { + return $this->objectPHID; + } + + public function setVisiblePHIDs(array $visible_phids) { + $this->visiblePHIDs = $visible_phids; + return $this; + } + + public function getVisiblePHIDs() { + return $this->visiblePHIDs; + } + + public function buildResponse() { + $viewer = $this->getViewer(); + $object_phid = $this->getObjectPHID(); + $board_phid = $this->getBoardPHID(); + + // Load all the other tasks that are visible in the affected columns and + // perform layout for them. + $visible_phids = $this->getAllVisiblePHIDs(); + + $layout_engine = id(new PhabricatorBoardLayoutEngine()) + ->setViewer($viewer) + ->setBoardPHIDs(array($board_phid)) + ->setObjectPHIDs($visible_phids) + ->executeLayout(); + + $object_columns = $layout_engine->getObjectColumns( + $board_phid, + $object_phid); + + $natural = array(); + foreach ($object_columns as $column_phid => $column) { + $column_object_phids = $layout_engine->getColumnObjectPHIDs( + $board_phid, + $column_phid); + $natural[$column_phid] = array_values($column_object_phids); + } + + $all_visible = id(new ManiphestTaskQuery()) + ->setViewer($viewer) + ->withPHIDs($visible_phids) + ->execute(); + + $order_maps = array(); + foreach ($all_visible as $visible) { + $order_maps[$visible->getPHID()] = $visible->getWorkboardOrderVectors(); + } + + $object = id(new ManiphestTaskQuery()) + ->setViewer($viewer) + ->withPHIDs(array($object_phid)) + ->needProjectPHIDs(true) + ->executeOne(); + if (!$object) { + return new Aphront404Response(); + } + + $template = $this->buildTemplate($object); + + $payload = array( + 'objectPHID' => $object_phid, + 'cardHTML' => $template, + 'columnMaps' => $natural, + 'orderMaps' => $order_maps, + 'propertyMaps' => array( + $object_phid => $object->getWorkboardProperties(), + ), + ); + + return id(new AphrontAjaxResponse()) + ->setContent($payload); + } + + private function buildTemplate($object) { + $viewer = $this->getViewer(); + $object_phid = $this->getObjectPHID(); + + $excluded_phids = $this->loadExcludedProjectPHIDs(); + + $rendering_engine = id(new PhabricatorBoardRenderingEngine()) + ->setViewer($viewer) + ->setObjects(array($object)) + ->setExcludedProjectPHIDs($excluded_phids); + + $card = $rendering_engine->renderCard($object_phid); + + return hsprintf('%s', $card->getItem()); + } + + private function loadExcludedProjectPHIDs() { + $viewer = $this->getViewer(); + $board_phid = $this->getBoardPHID(); + + $exclude_phids = array($board_phid); + + $descendants = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->withAncestorProjectPHIDs($exclude_phids) + ->execute(); + + foreach ($descendants as $descendant) { + $exclude_phids[] = $descendant->getPHID(); + } + + return array_fuse($exclude_phids); + } + + private function getAllVisiblePHIDs() { + $visible_phids = $this->getVisiblePHIDs(); + $visible_phids[] = $this->getObjectPHID(); + $visible_phids = array_fuse($visible_phids); + return $visible_phids; + } + +} diff --git a/src/applications/project/engine/PhabricatorProjectEditEngine.php b/src/applications/project/engine/PhabricatorProjectEditEngine.php index 54144cbbb5..ee101cb2e4 100644 --- a/src/applications/project/engine/PhabricatorProjectEditEngine.php +++ b/src/applications/project/engine/PhabricatorProjectEditEngine.php @@ -43,17 +43,7 @@ final class PhabricatorProjectEditEngine } protected function newEditableObject() { - $project = PhabricatorProject::initializeNewProject($this->getViewer()); - - $milestone = $this->getMilestoneProject(); - if ($milestone) { - $default_name = pht( - 'Milestone %s', - new PhutilNumber($milestone->loadNextMilestoneNumber())); - $project->setName($default_name); - } - - return $project; + return PhabricatorProject::initializeNewProject($this->getViewer()); } protected function newObjectQuery() { @@ -88,15 +78,11 @@ final class PhabricatorProjectEditEngine protected function getObjectCreateCancelURI($object) { $parent = $this->getParentProject(); - if ($parent) { - $id = $parent->getID(); - return "/project/subprojects/{$id}/"; - } - $milestone = $this->getMilestoneProject(); - if ($milestone) { - $id = $milestone->getID(); - return "/project/milestones/{$id}/"; + + if ($parent || $milestone) { + $id = nonempty($parent, $milestone)->getID(); + return "/project/subprojects/{$id}/"; } return parent::getObjectCreateCancelURI($object); @@ -143,6 +129,7 @@ final class PhabricatorProjectEditEngine array( 'parent', 'milestone', + 'milestone.previous', 'name', 'std:project:internal:description', 'icon', @@ -170,8 +157,26 @@ final class PhabricatorProjectEditEngine $parent_phid = null; } + $previous_milestone_phid = null; if ($milestone) { $milestone_phid = $milestone->getPHID(); + + // Load the current milestone so we can show the user a hint about what + // it was called, so they don't have to remember if the next one should + // be "Sprint 287" or "Sprint 278". + + $number = ($milestone->loadNextMilestoneNumber() - 1); + if ($number > 0) { + $previous_milestone = id(new PhabricatorProjectQuery()) + ->setViewer($this->getViewer()) + ->withParentProjectPHIDs(array($milestone->getPHID())) + ->withIsMilestone(true) + ->withMilestoneNumberBetween($number, $number) + ->executeOne(); + if ($previous_milestone) { + $previous_milestone_phid = $previous_milestone->getPHID(); + } + } } else { $milestone_phid = null; } @@ -207,6 +212,14 @@ final class PhabricatorProjectEditEngine ->setIsDefaultable(false) ->setIsLockable(false) ->setIsLocked(true), + id(new PhabricatorHandlesEditField()) + ->setKey('milestone.previous') + ->setLabel(pht('Previous Milestone')) + ->setSingleValue($previous_milestone_phid) + ->setIsReorderable(false) + ->setIsDefaultable(false) + ->setIsLockable(false) + ->setIsLocked(true), id(new PhabricatorTextEditField()) ->setKey('name') ->setLabel(pht('Name')) diff --git a/src/applications/project/engine/PhabricatorProjectProfilePanelEngine.php b/src/applications/project/engine/PhabricatorProjectProfilePanelEngine.php index 58b9b0b1ab..3a34b6c80c 100644 --- a/src/applications/project/engine/PhabricatorProjectProfilePanelEngine.php +++ b/src/applications/project/engine/PhabricatorProjectProfilePanelEngine.php @@ -20,6 +20,10 @@ final class PhabricatorProjectProfilePanelEngine ->setBuiltinKey(PhabricatorProject::PANEL_PROFILE) ->setPanelKey(PhabricatorProjectDetailsProfilePanel::PANELKEY); + $panels[] = $this->newPanel() + ->setBuiltinKey(PhabricatorProject::PANEL_POINTS) + ->setPanelKey(PhabricatorProjectPointsProfilePanel::PANELKEY); + $panels[] = $this->newPanel() ->setBuiltinKey(PhabricatorProject::PANEL_WORKBOARD) ->setPanelKey(PhabricatorProjectWorkboardProfilePanel::PANELKEY); diff --git a/src/applications/project/events/ProjectHovercardEngineExtension.php b/src/applications/project/engineextension/PhabricatorProjectHovercardEngineExtension.php similarity index 95% rename from src/applications/project/events/ProjectHovercardEngineExtension.php rename to src/applications/project/engineextension/PhabricatorProjectHovercardEngineExtension.php index deef9a30f8..d2d1f9ab82 100644 --- a/src/applications/project/events/ProjectHovercardEngineExtension.php +++ b/src/applications/project/engineextension/PhabricatorProjectHovercardEngineExtension.php @@ -1,6 +1,6 @@ getViewer(); + + // Only render this element for milestones. + if (!$object->isMilestone()) { + return false; + } + + // Don't show if points aren't configured. + if (!ManiphestTaskPoints::getIsEnabled()) { + return false; + } + + // Points are only available if Maniphest is installed. + $class = 'PhabricatorManiphestApplication'; + if (!PhabricatorApplication::isClassInstalledForViewer($class, $viewer)) { + return false; + } + + return true; + } + + public function getDisplayName( + PhabricatorProfilePanelConfiguration $config) { + return $this->getDefaultName(); + } + + public function buildEditEngineFields( + PhabricatorProfilePanelConfiguration $config) { + return array( + id(new PhabricatorInstructionsEditField()) + ->setValue( + pht( + 'This is a progress bar which shows how many points of work '. + 'are complete within the milestone. It has no configurable '. + 'settings.')), + ); + } + + protected function newNavigationMenuItems( + PhabricatorProfilePanelConfiguration $config) { + $viewer = $this->getViewer(); + $project = $config->getProfileObject(); + + $limit = 250; + + $tasks = id(new ManiphestTaskQuery()) + ->setViewer($viewer) + ->withEdgeLogicPHIDs( + PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, + PhabricatorQueryConstraint::OPERATOR_AND, + array($project->getPHID())) + ->setLimit($limit + 1) + ->execute(); + + if (count($tasks) > $limit) { + return $this->renderError( + pht( + 'Too many tasks to compute statistics for (more than %s).', + new PhutilNumber($limit))); + } + + if (!$tasks) { + return $this->renderError( + pht( + 'This milestone has no tasks yet.')); + } + + $statuses = array(); + $points_done = 0; + $points_total = 0; + $no_points = 0; + foreach ($tasks as $task) { + $points = $task->getPoints(); + + if ($points === null) { + $no_points++; + continue; + } + + if (!$points) { + continue; + } + + $status = $task->getStatus(); + if (empty($statuses[$status])) { + $statuses[$status] = 0; + } + $statuses[$status] += $points; + + if (ManiphestTaskStatus::isClosedStatus($status)) { + $points_done += $points; + } + + $points_total += $points; + } + + if ($no_points == count($tasks)) { + return $this->renderError( + pht('No tasks have assigned point values.')); + } + + + if (!$points_total) { + return $this->renderError( + pht('All tasks with assigned point values are worth zero points.')); + } + + $label = pht( + '%s of %s %s', + new PhutilNumber($points_done), + new PhutilNumber($points_total), + ManiphestTaskPoints::getPointsLabel()); + + $bar = id(new PHUISegmentBarView()) + ->setLabel($label); + + $map = ManiphestTaskStatus::getTaskStatusMap(); + $statuses = array_select_keys($statuses, array_keys($map)); + + foreach ($statuses as $status => $points) { + if (!$points) { + continue; + } + + if (!ManiphestTaskStatus::isClosedStatus($status)) { + continue; + } + + $color = ManiphestTaskStatus::getStatusColor($status); + if (!$color) { + $color = 'sky'; + } + + $tooltip = pht( + '%s %s', + new PhutilNumber($points), + ManiphestTaskStatus::getTaskStatusName($status)); + + $bar->newSegment() + ->setWidth($points / $points_total) + ->setColor($color) + ->setTooltip($tooltip); + } + + $bar = phutil_tag( + 'div', + array( + 'class' => 'phui-profile-segment-bar', + ), + $bar); + + $item = $this->newItem() + ->appendChild($bar); + + return array( + $item, + ); + } + + private function renderError($message) { + $message = phutil_tag( + 'div', + array( + 'class' => 'phui-profile-menu-error', + ), + $message); + + $item = $this->newItem() + ->appendChild($message); + + return array( + $item, + ); + } + +} diff --git a/src/applications/project/profilepanel/PhabricatorProjectSubprojectsProfilePanel.php b/src/applications/project/profilepanel/PhabricatorProjectSubprojectsProfilePanel.php index fad6378d69..342c8025aa 100644 --- a/src/applications/project/profilepanel/PhabricatorProjectSubprojectsProfilePanel.php +++ b/src/applications/project/profilepanel/PhabricatorProjectSubprojectsProfilePanel.php @@ -13,6 +13,14 @@ final class PhabricatorProjectSubprojectsProfilePanel return pht('Subprojects'); } + public function shouldEnableForObject($object) { + if ($object->isMilestone()) { + return false; + } + + return true; + } + public function getDisplayName( PhabricatorProfilePanelConfiguration $config) { $name = $config->getPanelProperty('name'); diff --git a/src/applications/project/profilepanel/PhabricatorProjectWorkboardProfilePanel.php b/src/applications/project/profilepanel/PhabricatorProjectWorkboardProfilePanel.php index 9bf28cdb31..8bfeeb4cfe 100644 --- a/src/applications/project/profilepanel/PhabricatorProjectWorkboardProfilePanel.php +++ b/src/applications/project/profilepanel/PhabricatorProjectWorkboardProfilePanel.php @@ -18,6 +18,18 @@ final class PhabricatorProjectWorkboardProfilePanel return true; } + public function shouldEnableForObject($object) { + $viewer = $this->getViewer(); + + // Workboards are only available if Maniphest is installed. + $class = 'PhabricatorManiphestApplication'; + if (!PhabricatorApplication::isClassInstalledForViewer($class, $viewer)) { + return false; + } + + return true; + } + public function getDisplayName( PhabricatorProfilePanelConfiguration $config) { $name = $config->getPanelProperty('name'); @@ -42,14 +54,6 @@ final class PhabricatorProjectWorkboardProfilePanel protected function newNavigationMenuItems( PhabricatorProfilePanelConfiguration $config) { - $viewer = $this->getViewer(); - - // Workboards are only available if Maniphest is installed. - $class = 'PhabricatorManiphestApplication'; - if (!PhabricatorApplication::isClassInstalledForViewer($class, $viewer)) { - return array(); - } - $project = $config->getProfileObject(); $has_workboard = $project->getHasWorkboard(); diff --git a/src/applications/project/query/PhabricatorProjectQuery.php b/src/applications/project/query/PhabricatorProjectQuery.php index 304dd956e1..d12e66e392 100644 --- a/src/applications/project/query/PhabricatorProjectQuery.php +++ b/src/applications/project/query/PhabricatorProjectQuery.php @@ -20,6 +20,8 @@ final class PhabricatorProjectQuery private $hasSubprojects; private $minDepth; private $maxDepth; + private $minMilestoneNumber; + private $maxMilestoneNumber; private $status = 'status-any'; const STATUS_ANY = 'status-any'; @@ -27,6 +29,7 @@ final class PhabricatorProjectQuery const STATUS_CLOSED = 'status-closed'; const STATUS_ACTIVE = 'status-active'; const STATUS_ARCHIVED = 'status-archived'; + private $statuses; private $needSlugs; private $needMembers; @@ -49,6 +52,11 @@ final class PhabricatorProjectQuery return $this; } + public function withStatuses(array $statuses) { + $this->statuses = $statuses; + return $this; + } + public function withMemberPHIDs(array $member_phids) { $this->memberPHIDs = $member_phids; return $this; @@ -105,6 +113,12 @@ final class PhabricatorProjectQuery return $this; } + public function withMilestoneNumberBetween($min, $max) { + $this->minMilestoneNumber = $min; + $this->maxMilestoneNumber = $max; + return $this; + } + public function needMembers($need_members) { $this->needMembers = $need_members; return $this; @@ -387,6 +401,13 @@ final class PhabricatorProjectQuery $filter); } + if ($this->statuses !== null) { + $where[] = qsprintf( + $conn, + 'status IN (%Ls)', + $this->statuses); + } + if ($this->ids !== null) { $where[] = qsprintf( $conn, @@ -481,6 +502,7 @@ final class PhabricatorProjectQuery } } + if ($this->hasSubprojects !== null) { $where[] = qsprintf( $conn, @@ -502,6 +524,20 @@ final class PhabricatorProjectQuery $this->maxDepth); } + if ($this->minMilestoneNumber !== null) { + $where[] = qsprintf( + $conn, + 'milestoneNumber >= %d', + $this->minMilestoneNumber); + } + + if ($this->maxMilestoneNumber !== null) { + $where[] = qsprintf( + $conn, + 'milestoneNumber <= %d', + $this->maxMilestoneNumber); + } + return $where; } diff --git a/src/applications/project/searchfield/PhabricatorProjectSearchField.php b/src/applications/project/searchfield/PhabricatorProjectSearchField.php index f187444e5d..8b81cb9d15 100644 --- a/src/applications/project/searchfield/PhabricatorProjectSearchField.php +++ b/src/applications/project/searchfield/PhabricatorProjectSearchField.php @@ -34,7 +34,7 @@ final class PhabricatorProjectSearchField if ($slugs) { $projects = id(new PhabricatorProjectQuery()) - ->setViewer($this->requireViewer()) + ->setViewer($this->getViewer()) ->withSlugs($slugs) ->execute(); foreach ($projects as $project) { diff --git a/src/applications/project/storage/PhabricatorProject.php b/src/applications/project/storage/PhabricatorProject.php index 093b368ef7..861fac646b 100644 --- a/src/applications/project/storage/PhabricatorProject.php +++ b/src/applications/project/storage/PhabricatorProject.php @@ -36,6 +36,8 @@ final class PhabricatorProject extends PhabricatorProjectDAO protected $projectDepth; protected $projectPathKey; + protected $properties = array(); + private $memberPHIDs = self::ATTACHABLE; private $watcherPHIDs = self::ATTACHABLE; private $sparseWatchers = self::ATTACHABLE; @@ -48,6 +50,7 @@ final class PhabricatorProject extends PhabricatorProjectDAO const TABLE_DATASOURCE_TOKEN = 'project_datasourcetoken'; const PANEL_PROFILE = 'project.profile'; + const PANEL_POINTS = 'project.points'; const PANEL_WORKBOARD = 'project.workboard'; const PANEL_MEMBERS = 'project.members'; const PANEL_MANAGE = 'project.manage'; @@ -197,6 +200,9 @@ final class PhabricatorProject extends PhabricatorProjectDAO protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, + self::CONFIG_SERIALIZATION => array( + 'properties' => self::SERIALIZATION_JSON, + ), self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'sort128', 'status' => 'text32', @@ -548,6 +554,31 @@ final class PhabricatorProject extends PhabricatorProjectDAO return idx($map, $color, $color); } + public function getProperty($key, $default = null) { + return idx($this->properties, $key, $default); + } + + public function setProperty($key, $value) { + $this->properties[$key] = $value; + return $this; + } + + public function getDefaultWorkboardSort() { + return $this->getProperty('workboard.sort.default'); + } + + public function setDefaultWorkboardSort($sort) { + return $this->setProperty('workboard.sort.default', $sort); + } + + public function getDefaultWorkboardFilter() { + return $this->getProperty('workboard.filter.default'); + } + + public function setDefaultWorkboardFilter($filter) { + return $this->setProperty('workboard.filter.default', $filter); + } + /* -( PhabricatorCustomFieldInterface )------------------------------------ */ diff --git a/src/applications/project/storage/PhabricatorProjectColumn.php b/src/applications/project/storage/PhabricatorProjectColumn.php index 0ab6ec89c0..4092ca7fa8 100644 --- a/src/applications/project/storage/PhabricatorProjectColumn.php +++ b/src/applications/project/storage/PhabricatorProjectColumn.php @@ -86,6 +86,11 @@ final class PhabricatorProjectColumn } public function isHidden() { + $proxy = $this->getProxy(); + if ($proxy) { + return $proxy->isArchived(); + } + return ($this->getStatus() == self::STATUS_HIDDEN); } diff --git a/src/applications/project/storage/PhabricatorProjectTransaction.php b/src/applications/project/storage/PhabricatorProjectTransaction.php index 1186e9c4f7..e812952630 100644 --- a/src/applications/project/storage/PhabricatorProjectTransaction.php +++ b/src/applications/project/storage/PhabricatorProjectTransaction.php @@ -12,6 +12,9 @@ final class PhabricatorProjectTransaction const TYPE_LOCKED = 'project:locked'; const TYPE_PARENT = 'project:parent'; const TYPE_MILESTONE = 'project:milestone'; + const TYPE_HASWORKBOARD = 'project:hasworkboard'; + const TYPE_DEFAULT_SORT = 'project:sort'; + const TYPE_DEFAULT_FILTER = 'project:filter'; // NOTE: This is deprecated, members are just a normal edge now. const TYPE_MEMBERS = 'project:members'; @@ -65,8 +68,29 @@ final class PhabricatorProjectTransaction return parent::getColor(); } - public function getIcon() { + public function shouldHideForFeed() { + switch ($this->getTransactionType()) { + case self::TYPE_HASWORKBOARD: + case self::TYPE_DEFAULT_SORT: + case self::TYPE_DEFAULT_FILTER: + return true; + } + return parent::shouldHideForFeed(); + } + + public function shouldHideForMail(array $xactions) { + switch ($this->getTransactionType()) { + case self::TYPE_HASWORKBOARD: + case self::TYPE_DEFAULT_SORT: + case self::TYPE_DEFAULT_FILTER: + return true; + } + + return parent::shouldHideForMail($xactions); + } + + public function getIcon() { $old = $this->getOldValue(); $new = $this->getNewValue(); @@ -246,6 +270,27 @@ final class PhabricatorProjectTransaction } } break; + + case self::TYPE_HASWORKBOARD: + if ($new) { + return pht( + '%s enabled the workboard for this project.', + $author_handle); + } else { + return pht( + '%s disabled the workboard for this project.', + $author_handle); + } + + case self::TYPE_DEFAULT_SORT: + return pht( + '%s changed the default sort order for the project workboard.', + $author_handle); + + case self::TYPE_DEFAULT_FILTER: + return pht( + '%s changed the default filter for the project workboard.', + $author_handle); } return parent::getTitle(); @@ -366,6 +411,7 @@ final class PhabricatorProjectTransaction $object_handle, $this->renderSlugList($rem)); } + } return parent::getTitleForFeed(); diff --git a/src/applications/project/typeahead/PhabricatorProjectDatasource.php b/src/applications/project/typeahead/PhabricatorProjectDatasource.php index 33fb8df0e9..40d8dbb0e6 100644 --- a/src/applications/project/typeahead/PhabricatorProjectDatasource.php +++ b/src/applications/project/typeahead/PhabricatorProjectDatasource.php @@ -38,6 +38,8 @@ final class PhabricatorProjectDatasource $query->withIsMilestone(false); } + $for_autocomplete = $this->getParameter('autocomplete'); + $projs = $this->executeQuery($query); $projs = mpull($projs, null, 'getPHID'); @@ -58,6 +60,23 @@ final class PhabricatorProjectDatasource if (!isset($has_cols[$proj->getPHID()])) { continue; } + + $slug = $proj->getPrimarySlug(); + if (!strlen($slug)) { + foreach ($proj->getSlugs() as $slug_object) { + $slug = $slug_object->getSlug(); + if (strlen($slug)) { + break; + } + } + } + + // If we're building results for the autocompleter and this project + // doesn't have any usable slugs, don't return it as a result. + if ($for_autocomplete && !strlen($slug)) { + continue; + } + $closed = null; if ($proj->isArchived()) { $closed = pht('Archived'); @@ -78,7 +97,6 @@ final class PhabricatorProjectDatasource ->setPriorityType('proj') ->setClosed($closed); - $slug = $proj->getPrimarySlug(); if (strlen($slug)) { $proj_result->setAutocomplete('#'.$slug); } diff --git a/src/applications/project/view/ProjectBoardTaskCard.php b/src/applications/project/view/ProjectBoardTaskCard.php index 2c69e0f37b..087e9c1789 100644 --- a/src/applications/project/view/ProjectBoardTaskCard.php +++ b/src/applications/project/view/ProjectBoardTaskCard.php @@ -7,6 +7,7 @@ final class ProjectBoardTaskCard extends Phobject { private $task; private $owner; private $canEdit; + private $coverImageFile; public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; @@ -25,6 +26,15 @@ final class ProjectBoardTaskCard extends Phobject { return $this->projectHandles; } + public function setCoverImageFile(PhabricatorFile $cover_image_file) { + $this->coverImageFile = $cover_image_file; + return $this; + } + + public function getCoverImageFile() { + return $this->coverImageFile; + } + public function setTask(ManiphestTask $task) { $this->task = $task; return $this; @@ -68,10 +78,6 @@ final class ProjectBoardTaskCard extends Phobject { ->setHref('/T'.$task->getID()) ->addSigil('project-card') ->setDisabled($task->isClosed()) - ->setMetadata( - array( - 'objectPHID' => $task->getPHID(), - )) ->addAction( id(new PHUIListItemView()) ->setName(pht('Edit')) @@ -84,6 +90,23 @@ final class ProjectBoardTaskCard extends Phobject { $card->addHandleIcon($owner, $owner->getName()); } + $cover_file = $this->getCoverImageFile(); + if ($cover_file) { + $card->setCoverImage($cover_file->getBestURI()); + } + + if (ManiphestTaskPoints::getIsEnabled()) { + $points = $task->getPoints(); + if ($points !== null) { + $points_tag = id(new PHUITagView()) + ->setType(PHUITagView::TYPE_SHADE) + ->setShade(PHUITagView::COLOR_BLUE) + ->setSlimShady(true) + ->setName($points); + $card->addAttribute($points_tag); + } + } + if ($task->isClosed()) { $icon = ManiphestTaskStatus::getStatusIcon($task->getStatus()); $icon = id(new PHUIIconView()) @@ -100,6 +123,8 @@ final class ProjectBoardTaskCard extends Phobject { $card->addAttribute($tag_list); } + $card->addClass('phui-workcard'); + return $card; } diff --git a/src/applications/search/engine/PhabricatorProfilePanelEngine.php b/src/applications/search/engine/PhabricatorProfilePanelEngine.php index a3907feac8..b92ba451d0 100644 --- a/src/applications/search/engine/PhabricatorProfilePanelEngine.php +++ b/src/applications/search/engine/PhabricatorProfilePanelEngine.php @@ -236,9 +236,18 @@ abstract class PhabricatorProfilePanelEngine extends Phobject { ->withProfilePHIDs(array($object->getPHID())) ->execute(); + foreach ($stored_panels as $stored_panel) { + $impl = $stored_panel->getPanel(); + $impl->setViewer($viewer); + } + // Merge the stored panels into the builtin panels. If a builtin panel has // a stored version, replace the defaults with the stored changes. foreach ($stored_panels as $stored_panel) { + if (!$stored_panel->shouldEnableForObject($object)) { + continue; + } + $builtin_key = $stored_panel->getBuiltinKey(); if ($builtin_key !== null) { // If this builtin actually exists, replace the builtin with the @@ -255,12 +264,6 @@ abstract class PhabricatorProfilePanelEngine extends Phobject { } } - foreach ($panels as $panel) { - $impl = $panel->getPanel(); - - $impl->setViewer($viewer); - } - $panels = msort($panels, 'getSortKey'); // Normalize keys since callers shouldn't rely on this array being @@ -302,6 +305,7 @@ abstract class PhabricatorProfilePanelEngine extends Phobject { $builtins = $this->getBuiltinProfilePanels($object); $panels = PhabricatorProfilePanel::getAllPanels(); + $viewer = $this->getViewer(); $order = 1; $map = array(); @@ -335,12 +339,19 @@ abstract class PhabricatorProfilePanelEngine extends Phobject { $panel_key)); } + $panel = clone $panel; + $panel->setViewer($viewer); + $builtin ->setProfilePHID($object->getPHID()) ->attachPanel($panel) ->attachProfileObject($object) ->setPanelOrder($order); + if (!$builtin->shouldEnableForObject($object)) { + continue; + } + $map[$builtin_key] = $builtin; $order++; diff --git a/src/applications/search/profilepanel/PhabricatorProfilePanel.php b/src/applications/search/profilepanel/PhabricatorProfilePanel.php index 8316d13467..8fcbd3f96a 100644 --- a/src/applications/search/profilepanel/PhabricatorProfilePanel.php +++ b/src/applications/search/profilepanel/PhabricatorProfilePanel.php @@ -30,6 +30,10 @@ abstract class PhabricatorProfilePanel extends Phobject { return false; } + public function shouldEnableForObject($object) { + return true; + } + public function canHidePanel( PhabricatorProfilePanelConfiguration $config) { return true; diff --git a/src/applications/search/storage/PhabricatorProfilePanelConfiguration.php b/src/applications/search/storage/PhabricatorProfilePanelConfiguration.php index faeaeb5207..d6f70e802e 100644 --- a/src/applications/search/storage/PhabricatorProfilePanelConfiguration.php +++ b/src/applications/search/storage/PhabricatorProfilePanelConfiguration.php @@ -109,6 +109,10 @@ final class PhabricatorProfilePanelConfiguration return $this->getPanel()->canHidePanel($this); } + public function shouldEnableForObject($object) { + return $this->getPanel()->shouldEnableForObject($object); + } + public function getSortKey() { $order = $this->getPanelOrder(); if ($order === null) { diff --git a/src/applications/transactions/commentaction/PhabricatorEditEnginePointsCommentAction.php b/src/applications/transactions/commentaction/PhabricatorEditEnginePointsCommentAction.php new file mode 100644 index 0000000000..c64262af96 --- /dev/null +++ b/src/applications/transactions/commentaction/PhabricatorEditEnginePointsCommentAction.php @@ -0,0 +1,16 @@ + $this->getValue(), + ); + } + +} diff --git a/src/applications/transactions/editfield/PhabricatorPointsEditField.php b/src/applications/transactions/editfield/PhabricatorPointsEditField.php new file mode 100644 index 0000000000..48c1b3b35b --- /dev/null +++ b/src/applications/transactions/editfield/PhabricatorPointsEditField.php @@ -0,0 +1,17 @@ + $field) { + if (!$field->getIsReorderable()) { + $fixed[$key] = $field; + } + } + $keys = $this->getFieldOrder(); - $fields = array_select_keys($fields, $keys) + $fields; + + $fields = $fixed + array_select_keys($fields, $keys) + $fields; + return $fields; } diff --git a/src/applications/uiexample/examples/PhabricatorAphrontBarUIExample.php b/src/applications/uiexample/examples/PhabricatorAphrontBarUIExample.php index f66c5c1618..741056dd4a 100644 --- a/src/applications/uiexample/examples/PhabricatorAphrontBarUIExample.php +++ b/src/applications/uiexample/examples/PhabricatorAphrontBarUIExample.php @@ -12,10 +12,7 @@ final class PhabricatorAphrontBarUIExample extends PhabricatorUIExample { public function renderExample() { $out = array(); - $out[] = $this->renderTestThings('AphrontProgressBarView', 13, 10); - $out[] = $this->renderTestThings('AphrontGlyphBarView', 13, 10); - $out[] = $this->renderWeirdOrderGlyphBars(); - $out[] = $this->renderAsciiStarBar(); + $out[] = $this->renderRainbow(); return $out; } @@ -26,48 +23,46 @@ final class PhabricatorAphrontBarUIExample extends PhabricatorUIExample { ->appendChild($thing); } - private function renderTestThings($class, $max, $incr) { + private function renderRainbow() { + $colors = array( + 'red', + 'orange', + 'yellow', + 'green', + 'blue', + 'indigo', + 'violet', + ); + + $labels = array( + pht('Empty'), + pht('Red'), + pht('Orange'), + pht('Yellow'), + pht('Green'), + pht('Blue'), + pht('Indigo'), + pht('Violet'), + ); + $bars = array(); - for ($ii = 0; $ii <= $max; $ii++) { - $bars[] = newv($class, array()) - ->setValue($ii * $incr) - ->setMax($max * $incr) - ->setCaption("{$ii} outta {$max} ain't bad!"); - } - return $this->wrap("Test {$class}", $bars); - } - private function renderWeirdOrderGlyphBars() { - $views = array(); - $indices = array(1, 3, 7, 4, 2, 8, 9, 5, 10, 6); - $max = count($indices); - foreach ($indices as $index) { - $views[] = id(new AphrontGlyphBarView()) - ->setValue($index) - ->setMax($max) - ->setNumGlyphs(5) - ->setCaption("Lol score is {$index}/{$max}") - ->setGlyph(hsprintf('%s', 'LOL!')) - ->setBackgroundGlyph(hsprintf('%s', '____')); - $views[] = hsprintf('
'); + for ($jj = -1; $jj < count($colors); $jj++) { + $bar = id(new PHUISegmentBarView()) + ->setLabel($labels[$jj + 1]); + for ($ii = 0; $ii <= $jj; $ii++) { + $bar->newSegment() + ->setWidth(1 / 7) + ->setColor($colors[$ii]); + } + $bars[] = $bar; } - return $this->wrap( - pht('Glyph bars in weird order'), - $views); - } + $bars = phutil_implode_html( + phutil_tag('br'), + $bars); - private function renderAsciiStarBar() { - $bar = id(new AphrontGlyphBarView()) - ->setValue(50) - ->setMax(100) - ->setCaption(pht('Glyphs!')) - ->setNumGlyphs(10) - ->setGlyph(hsprintf('%s', '*')); - - return $this->wrap( - pht('ASCII star glyph bar'), - $bar); + return $this->wrap(pht('Rainbow Bars'), $bars); } } diff --git a/src/docs/contributor/database.diviner b/src/docs/contributor/database.diviner index 8ac6bf7798..59a7dc2b2c 100644 --- a/src/docs/contributor/database.diviner +++ b/src/docs/contributor/database.diviner @@ -28,11 +28,10 @@ Databases ========= Each Phabricator application has its own database. The names are prefixed by -`phabricator_` (this is configurable). This design has two advantages: +`phabricator_` (this is configurable). - - Each database is easier to comprehend and to maintain. - - We don't do cross-database joins so each database can live on its own - machine. This gives us flexibility in sharding data later. +Phabricator uses a separate database for each application. To understand why, +see @{article:Why does Phabricator need so many databases?}. Connections =========== diff --git a/src/docs/flavor/so_many_databases.diviner b/src/docs/flavor/so_many_databases.diviner new file mode 100644 index 0000000000..8fa41e6c97 --- /dev/null +++ b/src/docs/flavor/so_many_databases.diviner @@ -0,0 +1,130 @@ +@title Why does Phabricator need so many databases? +@group lore + +Phabricator uses about 60 databases (and we may have added more by the time you +read this document). This sometimes comes as a surprise, since you might assume +it would only use one database. + +The approach we use is designed to work at scale for huge installs with many +thousands of users. We care a lot about working well for large installs, and +about scaling up gracefully to meet the needs of growing organizations. We want +small startups to be able to install Phabricator and have it grow with them as +they expand to many thousands of employees. + +A cost of this approach is that it makes Phabricator more difficult to install +on shared hosts which require a lot of work to create or authorize access to +each database. However, Phabricator does a lot of advanced or complex things +which are difficult to configure or manage on shared hosts, and we don't +recommend installing it on a shared host. The install documentation explicitly +discouarges installing on shared hosts. + +Broadly, in cases where we must choose between operating well at scale for +growing organizations and installing easily on shared hosts, we prioritize +operating at scale. + + +Listing Databases +================= + +You can get a full list of the databases Phabricator needs with `bin/storage +databases`. It will look something like this: + +``` +$ /core/lib/phabricator/bin/storage databases +secure_audit +secure_calendar +secure_chatlog +secure_conduit +secure_countdown +secure_daemon +secure_differential +secure_draft +secure_drydock +secure_feed +...... +``` + +Roughly, each application has its own database, and then there are some +databases which support internal systems or shared infrastructure. + + +Operating at Scale +================== + +This storage design is aimed at large installs that may need more than one +physical database server to handle the load the install generates. + +The primary reason we use a separate database for each application is to allow +large installs to scale up by spreading database load across more hardware. A +large organization with many thousands of active users may find themselves +limited by the capacity of a single database backend. + +If so, they can launch a second backend, move some applications over to it, and +continue piling on more users. + +This can't continue forever, but provides a substantial amount of headroom for +large installs to spread the workload across more hardware and continue scaling +up. + +To make this possible, we put each application in its own database and use +database boundaries to enforce the logical constraints that the application +must have in order for this to work. For example, we can not perform joins +between separable tables, because they may not be on the same hardware. + +Establishing boundaries with application databases is a simple, straightforward +way to partition storage and make administrative operations like spreading load +realistic. + + +Ease of Development +=================== + +This design is also easier for us to work with, and easier for users who +want to work with the raw data in the database. + +We have a large number of tables (more than 400) and we can not reasonably +reduce the number of tables very much (each table generally represents some +meaningful type of object in some application). It's easier to develop with +tables which are organized into separate application databases, just like it's +easier to work with a large project if you organize source files into +directories. + +If you aren't developing Phabricator and never look at the data in the +database, you probably won't benefit from this organization. However, if you +are a developer or want to extend Phabricator or look under the hood, it's +easier to find what you're looking for and work with the tables when they're +organized by application. + + +More Databases Cost Nothing +=========================== + +In almost all cases, creating more databases has zero cost, just like +organizing source code into directories has zero cost. Even if we didn't derive +enormous benefits from this approach at scale, there is little reason //not// +to organize storage like this. + +There are a handful of administrative tasks which are very slightly more +complex to perform on multiple databases, but these are all either automated +with `bin/storage` or easy to build on top of the list of databases emitted by +`bin/storage databases`. + +For example, you can dump all the databases with `bin/storage dump`, and you +can destroy all the databases with `bin/storage destroy`. + +As mentioned above, an exception to this is that if you're installing on a +shared host and need to jump through hoops to individually authorize access to +each database, databases do cost something. + +However, this cost is an artificial cost imposed by the selected environment, +and this is only the first of many issues you'll run into trying to install and +run Phabricator on a shared host. These issues are why we strongly discourage +using shared hosts, and recommend against them in the install guide. + + +Next Steps +========== + +Continue by: + + - learning more about databases in @{article:Database Schema}. diff --git a/src/docs/user/field/performance.diviner b/src/docs/user/field/performance.diviner index 4aa959b228..2272af582c 100644 --- a/src/docs/user/field/performance.diviner +++ b/src/docs/user/field/performance.diviner @@ -52,7 +52,7 @@ inherent complexity, like these: - {icon times, color=red} A 100MB wiki page takes a long time to render. - {icon times, color=red} A turing-complete simulation of Conway's Game of - Life implented in 958,000 Herald rules executes slowly. + Life implemented in 958,000 Herald rules executes slowly. - {icon times, color=red} Uploading an 8GB file takes several minutes. Generally, the path forward will be: diff --git a/src/infrastructure/customfield/field/PhabricatorCustomField.php b/src/infrastructure/customfield/field/PhabricatorCustomField.php index a89d8005f0..cb561fca58 100644 --- a/src/infrastructure/customfield/field/PhabricatorCustomField.php +++ b/src/infrastructure/customfield/field/PhabricatorCustomField.php @@ -1446,5 +1446,19 @@ abstract class PhabricatorCustomField extends Phobject { return null; } + public function getHeraldFieldStandardType() { + if ($this->proxy) { + return $this->proxy->getHeraldFieldStandardType(); + } + return null; + } + + public function getHeraldDatasource() { + if ($this->proxy) { + return $this->proxy->getHeraldDatasource(); + } + return null; + } + } diff --git a/src/infrastructure/customfield/herald/PhabricatorCustomFieldHeraldField.php b/src/infrastructure/customfield/herald/PhabricatorCustomFieldHeraldField.php index 30e9fc4c12..b9e5724ef6 100644 --- a/src/infrastructure/customfield/herald/PhabricatorCustomFieldHeraldField.php +++ b/src/infrastructure/customfield/herald/PhabricatorCustomFieldHeraldField.php @@ -65,8 +65,20 @@ final class PhabricatorCustomFieldHeraldField extends HeraldField { return $this->getCustomField()->getHeraldFieldConditions(); } + protected function getHeraldFieldStandardType() { + return $this->getCustomField()->getHeraldFieldStandardType(); + } + public function getHeraldFieldValueType($condition) { + if ($this->getHeraldFieldStandardType()) { + return parent::getHeraldFieldValueType($condition); + } + return $this->getCustomField()->getHeraldFieldValueType($condition); } + protected function getDatasource() { + return $this->getCustomField()->getHeraldDatasource(); + } + } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldBool.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldBool.php index b1fc7f7a9b..f1d1371a7d 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldBool.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldBool.php @@ -129,6 +129,10 @@ final class PhabricatorStandardCustomFieldBool ); } + public function getHeraldFieldStandardType() { + return HeraldField::STANDARD_BOOL; + } + protected function getHTTPParameterType() { return new AphrontBoolHTTPParameterType(); } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldLink.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldLink.php index 904a457616..66f3605b9c 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldLink.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldLink.php @@ -77,9 +77,14 @@ final class PhabricatorStandardCustomFieldLink HeraldAdapter::CONDITION_IS, HeraldAdapter::CONDITION_IS_NOT, HeraldAdapter::CONDITION_REGEXP, + HeraldAdapter::CONDITION_NOT_REGEXP, ); } + public function getHeraldFieldStandardType() { + return HeraldField::STANDARD_TEXT; + } + protected function getHTTPParameterType() { return new AphrontStringHTTPParameterType(); } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php index b9b1bf6505..78c8caa5a9 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php @@ -241,6 +241,10 @@ abstract class PhabricatorStandardCustomFieldPHIDs ); } + public function getHeraldFieldStandardType() { + return HeraldField::STANDARD_PHID_NULLABLE; + } + public function getHeraldFieldValue() { // If the field has a `null` value, make sure we hand an `array()` to // Herald. diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldRemarkup.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldRemarkup.php index 14e4eede5a..be7db42004 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldRemarkup.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldRemarkup.php @@ -92,9 +92,14 @@ final class PhabricatorStandardCustomFieldRemarkup HeraldAdapter::CONDITION_IS, HeraldAdapter::CONDITION_IS_NOT, HeraldAdapter::CONDITION_REGEXP, + HeraldAdapter::CONDITION_NOT_REGEXP, ); } + public function getHeraldFieldStandardType() { + return HeraldField::STANDARD_TEXT; + } + protected function getHTTPParameterType() { return new AphrontStringHTTPParameterType(); } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldText.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldText.php index 716e1dfc99..8a4ae97bcf 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldText.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldText.php @@ -60,9 +60,14 @@ final class PhabricatorStandardCustomFieldText HeraldAdapter::CONDITION_IS, HeraldAdapter::CONDITION_IS_NOT, HeraldAdapter::CONDITION_REGEXP, + HeraldAdapter::CONDITION_NOT_REGEXP, ); } + public function getHeraldFieldStandardType() { + return HeraldField::STANDARD_TEXT; + } + protected function getHTTPParameterType() { return new AphrontStringHTTPParameterType(); } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldTokenizer.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldTokenizer.php index f6a542ec7f..f9a69c2838 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldTokenizer.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldTokenizer.php @@ -44,4 +44,12 @@ abstract class PhabricatorStandardCustomFieldTokenizer ->setDatasource($this->getDatasource()); } + public function getHeraldFieldStandardType() { + return HeraldField::STANDARD_PHID_LIST; + } + + public function getHeraldDatasource() { + return $this->getDatasource(); + } + } diff --git a/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php b/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php index ff4174522c..93027ce5d6 100644 --- a/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php +++ b/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php @@ -1514,6 +1514,20 @@ final class PhabricatorUSEnglishTranslation 'Permanently destroyed %s object.', 'Permanently destroyed %s objects.', ), + + '%s added %s watcher(s) for %s: %s.' => array( + array( + '%s added a watcher for %3$s: %4$s.', + '%s added watchers for %3$s: %4$s.', + ), + ), + + '%s removed %s watcher(s) for %s: %s.' => array( + array( + '%s removed a watcher for %3$s: %4$s.', + '%s removed watchers for %3$s: %4$s.', + ), + ), ); } diff --git a/src/view/form/control/PhabricatorRemarkupControl.php b/src/view/form/control/PhabricatorRemarkupControl.php index 11388d257e..eeec4971da 100644 --- a/src/view/form/control/PhabricatorRemarkupControl.php +++ b/src/view/form/control/PhabricatorRemarkupControl.php @@ -45,7 +45,11 @@ final class PhabricatorRemarkupControl extends AphrontFormTextAreaControl { $root_id = celerity_generate_unique_node_id(); $user_datasource = new PhabricatorPeopleDatasource(); - $proj_datasource = new PhabricatorProjectDatasource(); + $proj_datasource = id(new PhabricatorProjectDatasource()) + ->setParameters( + array( + 'autocomplete' => 1, + )); Javelin::initBehavior( 'phabricator-remarkup-assist', diff --git a/src/view/phui/PHUICrumbView.php b/src/view/phui/PHUICrumbView.php index 5039e17606..392a354123 100644 --- a/src/view/phui/PHUICrumbView.php +++ b/src/view/phui/PHUICrumbView.php @@ -73,12 +73,16 @@ final class PHUICrumbView extends AphrontView { ->setIcon($this->icon); } + // Surround the crumb name with spaces so that double clicking it only + // selects the crumb itself. + $name = array(' ', $this->name, ' '); + $name = phutil_tag( 'span', array( 'class' => 'phui-crumb-name', ), - $this->name); + $name); $divider = null; if (!$this->isLastCrumb) { diff --git a/src/view/phui/PHUICrumbsView.php b/src/view/phui/PHUICrumbsView.php index c85c0055d1..00a1f1911e 100644 --- a/src/view/phui/PHUICrumbsView.php +++ b/src/view/phui/PHUICrumbsView.php @@ -52,6 +52,15 @@ final class PHUICrumbsView extends AphrontView { if ($this->actions) { $actions = array(); foreach ($this->actions as $action) { + if ($action->getType() == PHUIListItemView::TYPE_DIVIDER) { + $actions[] = phutil_tag( + 'span', + array( + 'class' => 'phui-crumb-action-divider', + )); + continue; + } + $icon = null; if ($action->getIcon()) { $icon_name = $action->getIcon(); @@ -63,19 +72,26 @@ final class PHUICrumbsView extends AphrontView { ->setIcon($icon_name); } - $name = phutil_tag( - 'span', - array( - 'class' => 'phui-crumbs-action-name', - ), - $action->getName()); + + $action_classes = $action->getClasses(); + $action_classes[] = 'phui-crumbs-action'; + + $name = null; + if ($action->getName()) { + $name = phutil_tag( + 'span', + array( + 'class' => 'phui-crumbs-action-name', + ), + $action->getName()); + } else { + $action_classes[] = 'phui-crumbs-action-icon'; + } $action_sigils = $action->getSigils(); if ($action->getWorkflow()) { $action_sigils[] = 'workflow'; } - $action_classes = $action->getClasses(); - $action_classes[] = 'phui-crumbs-action'; if ($action->getDisabled()) { $action_classes[] = 'phui-crumbs-action-disabled'; diff --git a/src/view/phui/PHUIHeaderView.php b/src/view/phui/PHUIHeaderView.php index 48fedd11ae..50f0e36317 100644 --- a/src/view/phui/PHUIHeaderView.php +++ b/src/view/phui/PHUIHeaderView.php @@ -22,6 +22,7 @@ final class PHUIHeaderView extends AphrontTagView { private $epoch; private $actionIcons = array(); private $badges = array(); + private $href; public function setHeader($header) { $this->header = $header; @@ -147,6 +148,15 @@ final class PHUIHeaderView extends AphrontTagView { return $this; } + public function setHref($href) { + $this->href = $href; + return $this; + } + + public function getHref() { + return $this->href; + } + protected function getTagName() { return 'div'; } @@ -290,12 +300,25 @@ final class PHUIHeaderView extends AphrontTagView { ->setIcon($this->headerIcon); $left[] = $icon; } + + $header_content = $this->header; + + $href = $this->getHref(); + if ($href !== null) { + $header_content = phutil_tag( + 'a', + array( + 'href' => $href, + ), + $header_content); + } + $left[] = phutil_tag( 'span', array( 'class' => 'phui-header-header', ), - $this->header); + $header_content); if ($this->subheader || $this->badges) { $badges = null; diff --git a/src/view/phui/PHUIObjectItemView.php b/src/view/phui/PHUIObjectItemView.php index c7a3d074b5..4b56d746a7 100644 --- a/src/view/phui/PHUIObjectItemView.php +++ b/src/view/phui/PHUIObjectItemView.php @@ -27,6 +27,7 @@ final class PHUIObjectItemView extends AphrontTagView { private $countdownNum; private $countdownNoun; private $launchButton; + private $coverImage; const AGE_FRESH = 'fresh'; const AGE_STALE = 'stale'; @@ -150,6 +151,11 @@ final class PHUIObjectItemView extends AphrontTagView { return $this->imageIcon; } + public function setCoverImage($image) { + $this->coverImage = $image; + return $this; + } + public function setState($state) { $this->state = $state; switch ($state) { @@ -720,16 +726,45 @@ final class PHUIObjectItemView extends AphrontTagView { $actions); } - return phutil_tag( + $frame_content = phutil_tag( 'div', array( - 'class' => 'phui-object-item-frame', + 'class' => 'phui-object-item-frame-content', ), array( $actions, $image, $box, )); + + $frame_cover = null; + if ($this->coverImage) { + $cover_image = phutil_tag( + 'img', + array( + 'src' => $this->coverImage, + 'class' => 'phui-object-item-cover-image', + )); + + $frame_cover = phutil_tag( + 'div', + array( + 'class' => 'phui-object-item-frame-cover', + ), + $cover_image); + } + + $frame = phutil_tag( + 'div', + array( + 'class' => 'phui-object-item-frame', + ), + array( + $frame_cover, + $frame_content, + )); + + return $frame; } private function renderStatusIcon($icon, $label) { diff --git a/src/view/phui/PHUISegmentBarSegmentView.php b/src/view/phui/PHUISegmentBarSegmentView.php new file mode 100644 index 0000000000..6bd3c530ef --- /dev/null +++ b/src/view/phui/PHUISegmentBarSegmentView.php @@ -0,0 +1,79 @@ +width = $width; + return $this; + } + + public function getWidth() { + return $this->width; + } + + public function setColor($color) { + $this->color = $color; + return $this; + } + + public function setPosition($position) { + $this->position = $position; + return $this; + } + + public function setTooltip($tooltip) { + $this->tooltip = $tooltip; + return $this; + } + + protected function canAppendChild() { + return false; + } + + protected function getTagAttributes() { + $classes = array( + 'phui-segment-bar-segment-view', + ); + + if ($this->color) { + $classes[] = $this->color; + } + + // Convert width to a percentage, and round it up slightly so that bars + // are full if they have, e.g., three segments at 1/3 + 1/3 + 1/3. + $width = 100 * $this->width; + $width = ceil(100 * $width) / 100; + $width = sprintf('%.2f%%', $width); + + $left = 100 * $this->position; + $left = floor(100 * $left) / 100; + $left = sprintf('%.2f%%', $left); + + $tooltip = $this->tooltip; + if (strlen($tooltip)) { + Javelin::initBehavior('phabricator-tooltips'); + + $sigil = 'has-tooltip'; + $meta = array( + 'tip' => $tooltip, + 'align' => 'E', + ); + } else { + $sigil = null; + $meta = null; + } + + return array( + 'class' => implode(' ', $classes), + 'style' => "left: {$left}; width: {$width};", + 'sigil' => $sigil, + 'meta' => $meta, + ); + } + +} diff --git a/src/view/phui/PHUISegmentBarView.php b/src/view/phui/PHUISegmentBarView.php new file mode 100644 index 0000000000..632c5327eb --- /dev/null +++ b/src/view/phui/PHUISegmentBarView.php @@ -0,0 +1,65 @@ +label = $label; + return $this; + } + + public function newSegment() { + $segment = new PHUISegmentBarSegmentView(); + $this->segments[] = $segment; + return $segment; + } + + protected function canAppendChild() { + return false; + } + + protected function getTagAttributes() { + return array( + 'class' => 'phui-segment-bar-view', + ); + } + + protected function getTagContent() { + require_celerity_resource('phui-segment-bar-view-css'); + + $label = $this->label; + if (strlen($label)) { + $label = phutil_tag( + 'div', + array( + 'class' => 'phui-segment-bar-label', + ), + $label); + } + + $segments = $this->segments; + + $position = 0; + foreach ($segments as $segment) { + $segment->setPosition($position); + $position += $segment->getWidth(); + } + + $segments = array_reverse($segments); + + $segments = phutil_tag( + 'div', + array( + 'class' => 'phui-segment-bar-segments', + ), + $segments); + + return array( + $label, + $segments, + ); + } + +} diff --git a/src/view/phui/PHUIWorkboardView.php b/src/view/phui/PHUIWorkboardView.php index 45061f25fe..e9ec37452a 100644 --- a/src/view/phui/PHUIWorkboardView.php +++ b/src/view/phui/PHUIWorkboardView.php @@ -25,12 +25,13 @@ final class PHUIWorkboardView extends AphrontTagView { $view->addColumn($panel); } - $board = phutil_tag( + $board = javelin_tag( 'div', - array( - 'class' => 'phui-workboard-view-shadow', - ), - $view); + array( + 'class' => 'phui-workboard-view-shadow', + 'sigil' => 'workboard-shadow lock-scroll-y-while-dragging', + ), + $view); return $board; } diff --git a/src/view/phui/PHUIWorkpanelView.php b/src/view/phui/PHUIWorkpanelView.php index 50b2e12161..b94b423ced 100644 --- a/src/view/phui/PHUIWorkpanelView.php +++ b/src/view/phui/PHUIWorkpanelView.php @@ -9,6 +9,7 @@ final class PHUIWorkpanelView extends AphrontTagView { private $headerActions = array(); private $headerTag; private $headerIcon; + private $href; public function setHeaderIcon($icon) { $this->headerIcon = $icon; @@ -49,6 +50,15 @@ final class PHUIWorkpanelView extends AphrontTagView { return $this; } + public function setHref($href) { + $this->href = $href; + return $this; + } + + public function getHref() { + return $this->href; + } + protected function getTagAttributes() { return array( 'class' => 'phui-workpanel-view', @@ -85,13 +95,20 @@ final class PHUIWorkpanelView extends AphrontTagView { $header->addActionIcon($action); } + $href = $this->getHref(); + if ($href !== null) { + $header->setHref($href); + } + $body = phutil_tag( 'div', array( - 'class' => 'phui-workpanel-body', + 'class' => 'phui-workpanel-body-content', ), $this->cards); + $body = phutil_tag_div('phui-workpanel-body', $body); + $view = id(new PHUIBoxView()) ->setColor(PHUIBoxView::GREY) ->addClass('phui-workpanel-view-inner') diff --git a/webroot/rsrc/css/aphront/dialog-view.css b/webroot/rsrc/css/aphront/dialog-view.css index 3043400357..8909368beb 100644 --- a/webroot/rsrc/css/aphront/dialog-view.css +++ b/webroot/rsrc/css/aphront/dialog-view.css @@ -10,13 +10,17 @@ background-color: #fff; } +.jx-client-dialog .aphront-dialog-view { + box-shadow: {$dropshadow}; +} + .device-phone .aphront-dialog-view { margin: 16px; width: auto; } .aphront-dialog-view-standalone { - margin: auto; + margin: 32px auto; } .aphront-dialog-head { @@ -33,6 +37,7 @@ .aphront-dialog-view-width-full { width: 90%; + max-width: 1040px; } .aphront-dialog-body { diff --git a/webroot/rsrc/css/application/base/standard-page-view.css b/webroot/rsrc/css/application/base/standard-page-view.css index ca69443d07..4b53f61a6a 100644 --- a/webroot/rsrc/css/application/base/standard-page-view.css +++ b/webroot/rsrc/css/application/base/standard-page-view.css @@ -136,18 +136,6 @@ a.handle-availability-disabled { right: 0; } -/* Fixes so pages actually print when magic scrollbar is present */ -!print .main-page-frame { - position: static; - overflow: visible; -} - -!print .jx-scrollbar-viewport { - position: static; - width: auto !important; - height: auto !important; -} - .jx-scrollbar-test { position: absolute; left: -300px; diff --git a/webroot/rsrc/css/application/project/project-card-view.css b/webroot/rsrc/css/application/project/project-card-view.css index 475f7c76f2..5f5155c130 100644 --- a/webroot/rsrc/css/application/project/project-card-view.css +++ b/webroot/rsrc/css/application/project/project-card-view.css @@ -14,7 +14,7 @@ .project-card-view .phui-header-shell { margin: 0; - padding: 12px 12px 16px 12px; + padding: 12px 12px 4px 12px; border: none; border-radius: 3px; } @@ -43,6 +43,7 @@ .project-card-view .phui-header-subheader { font-size: {$normalfontsize}; margin-top: 12px; + padding-bottom: 12px; } .project-card-view .phui-header-header .phui-tag-view { @@ -63,6 +64,10 @@ color: {$bluetext}; } +.project-card-view .project-card-body { + padding: 0 12px 12px 76px; + color: {$darkbluetext}; +} /* Colors */ diff --git a/webroot/rsrc/css/core/z-index.css b/webroot/rsrc/css/core/z-index.css index 2d4c050d0c..23c6c59bf7 100644 --- a/webroot/rsrc/css/core/z-index.css +++ b/webroot/rsrc/css/core/z-index.css @@ -110,10 +110,6 @@ div.phui-calendar-day-event { z-index: 9; } -.drag-frame { - z-index: 10; -} - .jx-mask { z-index: 10; } @@ -142,6 +138,10 @@ div.jx-typeahead-results { z-index: 15; } +.drag-frame { + z-index: 16; +} + .jx-hovercard-container { z-index: 17; } diff --git a/webroot/rsrc/css/phui/phui-crumbs-view.css b/webroot/rsrc/css/phui/phui-crumbs-view.css index 71113dadc9..81c8471c4f 100644 --- a/webroot/rsrc/css/phui/phui-crumbs-view.css +++ b/webroot/rsrc/css/phui/phui-crumbs-view.css @@ -89,12 +89,15 @@ } a.phui-crumbs-action .phui-icon-view { - margin-right: 5px; color: {$darkbluetext}; } +a.phui-crumbs-action .phui-crumbs-action-name { + margin-left: 6px; +} + .device-phone a.phui-crumbs-action .phui-icon-view { - margin-left: 5px; + margin-left: 4px; } .phui-crumb-divider { @@ -112,3 +115,11 @@ body .phui-crumbs-view + .phui-object-box { body .phui-crumbs-view + .phui-object-item-list-view { padding-top: 0; } + +.phui-crumb-action-divider { + border-left: 1px solid {$lightgreyborder}; +} + +.phui-crumbs-action-icon + .phui-crumbs-action-icon { + padding-left: 4px; +} diff --git a/webroot/rsrc/css/phui/phui-object-item-list-view.css b/webroot/rsrc/css/phui/phui-object-item-list-view.css index 551144673b..913975c6eb 100644 --- a/webroot/rsrc/css/phui/phui-object-item-list-view.css +++ b/webroot/rsrc/css/phui/phui-object-item-list-view.css @@ -56,6 +56,10 @@ ul.phui-object-item-list-view { overflow: hidden; } +.phui-object-item-cover-image { + display: none; +} + .phui-object-item-no-bar .phui-object-item-frame { border-width: 1px; } @@ -303,7 +307,7 @@ ul.phui-object-item-list-view { .phui-object-item-attribute { display: inline-block; color: {$greytext}; - vertical-align: middle; + vertical-align: top; } .phui-object-item-attribute-spacer { diff --git a/webroot/rsrc/css/phui/phui-profile-menu.css b/webroot/rsrc/css/phui/phui-profile-menu.css index 889d98a075..76a51d9130 100644 --- a/webroot/rsrc/css/phui/phui-profile-menu.css +++ b/webroot/rsrc/css/phui/phui-profile-menu.css @@ -149,6 +149,25 @@ color: {$menu.profile.text}; } +.phui-profile-menu .phabricator-side-menu .phui-profile-menu-error { + color: {$greytext}; + font-size: {$smallerfontsize}; + padding: 18px 15px; +} + +.phui-profile-menu .phabricator-side-menu .phui-profile-segment-bar { + color: {$menu.profile.text}; + font-size: {$smallerfontsize}; + -webkit-font-smoothing: antialiased; + padding: 8px 12px 16px; +} + +.phui-profile-menu .phui-profile-menu-collapsed .phabricator-side-menu + .phui-profile-segment-bar { + padding: 8px 8px 16px; +} + + .phui-profile-menu .phabricator-side-menu .phui-profile-menu-spacer { box-sizing: border-box; height: {$menu.profile.item.height}; @@ -300,3 +319,7 @@ max-width: {$menu.profile.width}; } } + +!print .phui-profile-menu .phabricator-side-menu { + display: none; +} diff --git a/webroot/rsrc/css/phui/phui-segment-bar-view.css b/webroot/rsrc/css/phui/phui-segment-bar-view.css new file mode 100644 index 0000000000..92665165ba --- /dev/null +++ b/webroot/rsrc/css/phui/phui-segment-bar-view.css @@ -0,0 +1,76 @@ +/** + * @provides phui-segment-bar-view-css + */ + +.phui-segment-bar-label { + font-size: {$smallerfontsize}; + margin-bottom: 4px; +} + +.phui-profile-menu-collapsed .phui-segment-bar-label { + width: 74px; + overflow: hidden; + text-overflow: ellipsis; +} + +.phui-segment-bar-segments { + background: {$lightgreybackground}; + border-radius: 4px; + position: relative; + overflow: hidden; + height: 8px; +} + +.phui-segment-bar-segment-view { + position: absolute; + top: 0; + bottom: 0; + margin-left: -5px; + border-right: 5px solid; + border-radius: 0 4px 4px 0; +} + +.phui-segment-bar-segment-view.red { + background: {$red}; + border-color: {$red}; +} + +.phui-segment-bar-segment-view.orange { + background: {$orange}; + border-color: {$orange}; +} + +.phui-segment-bar-segment-view.yellow { + background: {$yellow}; + border-color: {$yellow} +} + +.phui-segment-bar-segment-view.green { + background: {$green}; + border-color: {$green}; +} + +.phui-segment-bar-segment-view.blue { + background: {$blue}; + border-color: {$blue}; +} + +.phui-segment-bar-segment-view.indigo { + background: {$indigo}; + border-color: {$indigo}; +} + +.phui-segment-bar-segment-view.violet { + background: {$violet}; + border-color: {$violet}; +} + +.phui-segment-bar-segment-view.pink { + background: {$pink}; + border-color: {$pink}; +} + +.phui-segment-bar-segment-view.sky { + background: {$sky}; + border-color: {$sky}; +} diff --git a/webroot/rsrc/css/phui/workboards/phui-workboard.css b/webroot/rsrc/css/phui/workboards/phui-workboard.css index 54b4907861..36913d1663 100644 --- a/webroot/rsrc/css/phui/workboards/phui-workboard.css +++ b/webroot/rsrc/css/phui/workboards/phui-workboard.css @@ -12,6 +12,7 @@ .device-desktop .phui-workboard-view-shadow { overflow-x: auto; + overflow-y: hidden; position: absolute; top: 79px; bottom: 0; @@ -34,8 +35,8 @@ } .phui-workboard-view-shadow::-webkit-scrollbar { - height: 12px; - width: 12px; + height: 8px; + width: 8px; background: rgba(200,200,200,.6); } @@ -52,6 +53,16 @@ left: {$menu.profile.width.collapsed}; } +!print .project-board-wrapper .phui-workboard-view-shadow { + position: static; +} + +!print .project-board-wrapper .aphront-multi-column-column-outer { + display: block; + margin: 0 0 18px; + page-break-inside: avoid; +} + .device-desktop .phui-workboard-view .aphront-multi-column-fixed .aphront-multi-column-inner { margin-left: 0; @@ -60,3 +71,37 @@ .device .project-board-wrapper { margin: 16px; } + +.device-desktop .phui-workboard-view .aphront-multi-column-view { + pointer-events: none; +} + +.device-desktop .phui-workpanel-view { + pointer-events: auto; + cursor: auto; +} + +/* Fullscreen */ + +.device-desktop .phui-workboard-fullscreen .phabricator-main-menu { + display: none; +} + +.device-desktop .phui-workboard-fullscreen .phui-profile-menu + .phui-workboard-view-shadow { + top: 35px; + left: 0; +} + +.device-desktop .phui-workboard-fullscreen .phui-workpanel-body-content { + max-height: calc(100vh - 120px); +} + +.device-desktop .phui-workboard-fullscreen .phui-profile-menu + .phabricator-nav-local { + display: none; +} + +.device .phui-workboard-expand-icon { + display: none; +} diff --git a/webroot/rsrc/css/phui/workboards/phui-workcard.css b/webroot/rsrc/css/phui/workboards/phui-workcard.css index d0c427da57..d35ac11628 100644 --- a/webroot/rsrc/css/phui/workboards/phui-workcard.css +++ b/webroot/rsrc/css/phui/workboards/phui-workcard.css @@ -100,6 +100,20 @@ margin-bottom: 8px; } +.phui-workcard .phui-object-item-cover-image { + display: block; + padding: 8px 8px 0 8px; + width: 263px; +} + +.phui-workcard.phui-object-item.phui-workcard-upload-target { + background-color: {$sh-greenbackground}; +} + +.phui-object-item-list-view .phui-workcard:last-child { + margin-bottom: 0; +} + /* - Draggable Colors --------------------------------------------------------*/ diff --git a/webroot/rsrc/css/phui/workboards/phui-workpanel.css b/webroot/rsrc/css/phui/workboards/phui-workpanel.css index b600577d33..77bddf9f0f 100644 --- a/webroot/rsrc/css/phui/workboards/phui-workpanel.css +++ b/webroot/rsrc/css/phui/workboards/phui-workpanel.css @@ -35,8 +35,12 @@ user-select: none; } +.phui-workpanel-view .phui-box-grey { + background-color: rgba(71,87,120,0.1); +} + .phui-workpanel-view.phui-workboard-column-milestone .phui-box-grey { - background-color: rgba(234, 230, 247, 0.75); + background-color: rgba(234, 230, 247, 0.85); } .phui-workpanel-view .phui-header-col2 .phui-icon-view { @@ -46,11 +50,14 @@ .phui-workpanel-view .phui-workpanel-header-action { float: right; width: 24px; - border-left: 1px solid #b3b5b6; } .phui-workpanel-view .phui-workpanel-body { - padding: 8px 8px 4px 8px; + padding: 8px 4px 8px 0; +} + +.phui-workpanel-view .phui-workpanel-body-content { + padding: 0 4px 0 8px; } .device .phui-workpanel-view .phui-workpanel-body { @@ -89,6 +96,24 @@ opacity: 0.75; } +.device-desktop .phui-workpanel-body-content { + max-height: calc(100vh - 162px); + overflow-y: auto; + overflow-x: hidden; +} + +.device-desktop .phui-workpanel-body-content::-webkit-scrollbar { + height: 8px; + width: 8px; + background: rgba(71,87,120,0.2); + border-radius: 4px; +} + +.device-desktop .phui-workpanel-body-content::-webkit-scrollbar-thumb { + background: rgba(71,87,120,0.4); + border-radius: 4px; +} + .project-panel-empty .phui-object-item-list-view { background: {$sh-indigobackground}; border-radius: 3px; diff --git a/webroot/rsrc/js/application/projects/WorkboardBoard.js b/webroot/rsrc/js/application/projects/WorkboardBoard.js new file mode 100644 index 0000000000..4506041cca --- /dev/null +++ b/webroot/rsrc/js/application/projects/WorkboardBoard.js @@ -0,0 +1,258 @@ +/** + * @provides javelin-workboard-board + * @requires javelin-install + * javelin-dom + * javelin-util + * javelin-stratcom + * javelin-workflow + * phabricator-draggable-list + * javelin-workboard-column + * @javelin + */ + +JX.install('WorkboardBoard', { + + construct: function(controller, phid, root) { + this._controller = controller; + this._phid = phid; + this._root = root; + + this._templates = {}; + this._orderMaps = {}; + this._propertiesMap = {}; + this._buildColumns(); + }, + + properties: { + order: null, + pointsEnabled: false + }, + + members: { + _controller: null, + _phid: null, + _root: null, + _columns: null, + _templates: null, + _orderMaps: null, + _propertiesMap: null, + + getRoot: function() { + return this._root; + }, + + getColumns: function() { + return this._columns; + }, + + getColumn: function(k) { + return this._columns[k]; + }, + + getPHID: function() { + return this._phid; + }, + + setCardTemplate: function(phid, template) { + this._templates[phid] = template; + return this; + }, + + setObjectProperties: function(phid, properties) { + this._propertiesMap[phid] = properties; + return this; + }, + + getObjectProperties: function(phid) { + return this._propertiesMap[phid]; + }, + + getCardTemplate: function(phid) { + return this._templates[phid]; + }, + + getController: function() { + return this._controller; + }, + + setOrderMap: function(phid, map) { + this._orderMaps[phid] = map; + return this; + }, + + getOrderVector: function(phid, key) { + return this._orderMaps[phid][key]; + }, + + start: function() { + this._setupDragHandlers(); + + for (var k in this._columns) { + this._columns[k].redraw(); + } + }, + + _buildColumns: function() { + var nodes = JX.DOM.scry(this.getRoot(), 'ul', 'project-column'); + + this._columns = {}; + for (var ii = 0; ii < nodes.length; ii++) { + var node = nodes[ii]; + var data = JX.Stratcom.getData(node); + var phid = data.columnPHID; + + this._columns[phid] = new JX.WorkboardColumn(this, phid, node); + } + }, + + _setupDragHandlers: function() { + var columns = this.getColumns(); + + var lists = []; + for (var k in columns) { + var column = columns[k]; + + var list = new JX.DraggableList('project-card', column.getRoot()) + .setOuterContainer(this.getRoot()) + .setFindItemsHandler(JX.bind(column, column.getCardNodes)) + .setCanDragX(true) + .setHasInfiniteHeight(true); + + list.listen('didDrop', JX.bind(this, this._onmovecard, list)); + + lists.push(list); + } + + for (var ii = 0; ii < lists.length; ii++) { + lists[ii].setGroup(lists); + } + }, + + _findCardsInColumn: function(column_node) { + return JX.DOM.scry(column_node, 'li', 'project-card'); + }, + + _onmovecard: function(list, item, after_node, src_list) { + list.lock(); + JX.DOM.alterClass(item, 'drag-sending', true); + + var src_phid = JX.Stratcom.getData(src_list.getRootNode()).columnPHID; + var dst_phid = JX.Stratcom.getData(list.getRootNode()).columnPHID; + + var item_phid = JX.Stratcom.getData(item).objectPHID; + var data = { + objectPHID: item_phid, + columnPHID: dst_phid, + order: this.getOrder() + }; + + if (after_node) { + data.afterPHID = JX.Stratcom.getData(after_node).objectPHID; + } + + var before_node = item.nextSibling; + if (before_node) { + var before_phid = JX.Stratcom.getData(before_node).objectPHID; + if (before_phid) { + data.beforePHID = before_phid; + } + } + + var visible_phids = []; + var column = this.getColumn(dst_phid); + for (var object_phid in column.getCards()) { + visible_phids.push(object_phid); + } + + data.visiblePHIDs = visible_phids.join(','); + + var onupdate = JX.bind( + this, + this._oncardupdate, + list, + src_phid, + dst_phid, + data.afterPHID); + + new JX.Workflow(this.getController().getMoveURI(), data) + .setHandler(onupdate) + .start(); + }, + + _oncardupdate: function(list, src_phid, dst_phid, after_phid, response) { + var src_column = this.getColumn(src_phid); + var dst_column = this.getColumn(dst_phid); + + var card = src_column.removeCard(response.objectPHID); + dst_column.addCard(card, after_phid); + + src_column.markForRedraw(); + dst_column.markForRedraw(); + + this.updateCard(response); + + list.unlock(); + }, + + updateCard: function(response, options) { + options = options || {}; + options.dirtyColumns = options.dirtyColumns || {}; + + var columns = this.getColumns(); + + var phid = response.objectPHID; + + if (!this._templates[phid]) { + for (var add_phid in response.columnMaps) { + this.getColumn(add_phid).newCard(phid); + } + } + + this.setCardTemplate(phid, response.cardHTML); + + var order_maps = response.orderMaps; + for (var order_phid in order_maps) { + this.setOrderMap(order_phid, order_maps[order_phid]); + } + + var column_maps = response.columnMaps; + for (var natural_phid in column_maps) { + this.getColumn(natural_phid).setNaturalOrder(column_maps[natural_phid]); + } + + var property_maps = response.propertyMaps; + for (var property_phid in property_maps) { + this.setObjectProperties(property_phid, property_maps[property_phid]); + } + + for (var column_phid in columns) { + var column = columns[column_phid]; + + var cards = column.getCards(); + for (var object_phid in cards) { + if (object_phid !== phid) { + continue; + } + + var card = cards[object_phid]; + card.redraw(); + + column.markForRedraw(); + } + } + + this._redrawColumns(); + }, + + _redrawColumns: function() { + var columns = this.getColumns(); + for (var k in columns) { + if (columns[k].isMarkedForRedraw()) { + columns[k].redraw(); + } + } + } + + } + +}); diff --git a/webroot/rsrc/js/application/projects/WorkboardCard.js b/webroot/rsrc/js/application/projects/WorkboardCard.js new file mode 100644 index 0000000000..b506e655c1 --- /dev/null +++ b/webroot/rsrc/js/application/projects/WorkboardCard.js @@ -0,0 +1,68 @@ +/** + * @provides javelin-workboard-card + * @requires javelin-install + * @javelin + */ + +JX.install('WorkboardCard', { + + construct: function(column, phid) { + this._column = column; + this._phid = phid; + }, + + members: { + _column: null, + _phid: null, + _root: null, + + getPHID: function() { + return this._phid; + }, + + getColumn: function() { + return this._column; + }, + + setColumn: function(column) { + this._column = column; + }, + + getProperties: function() { + return this.getColumn().getBoard().getObjectProperties(this.getPHID()); + }, + + getPoints: function() { + return this.getProperties().points; + }, + + getStatus: function() { + return this.getProperties().status; + }, + + getNode: function() { + if (!this._root) { + var phid = this.getPHID(); + var template = this.getColumn().getBoard().getCardTemplate(phid); + this._root = JX.$H(template).getFragment().firstChild; + + JX.Stratcom.getData(this._root).objectPHID = this.getPHID(); + } + return this._root; + }, + + redraw: function() { + var old_node = this._root; + this._root = null; + var new_node = this.getNode(); + + if (old_node && old_node.parentNode) { + JX.DOM.replace(old_node, new_node); + } + + return this; + } + + } + +}); diff --git a/webroot/rsrc/js/application/projects/WorkboardColumn.js b/webroot/rsrc/js/application/projects/WorkboardColumn.js new file mode 100644 index 0000000000..738cf151c2 --- /dev/null +++ b/webroot/rsrc/js/application/projects/WorkboardColumn.js @@ -0,0 +1,284 @@ +/** + * @provides javelin-workboard-column + * @requires javelin-install + * javelin-workboard-card + * @javelin + */ + +JX.install('WorkboardColumn', { + + construct: function(board, phid, root) { + this._board = board; + this._phid = phid; + this._root = root; + + this._panel = JX.DOM.findAbove(root, 'div', 'workpanel'); + this._pointsNode = JX.DOM.find(this._panel, 'span', 'column-points'); + + this._pointsContentNode = JX.DOM.find( + this._panel, + 'span', + 'column-points-content'); + + this._cards = {}; + this._naturalOrder = []; + }, + + members: { + _phid: null, + _root: null, + _board: null, + _cards: null, + _naturalOrder: null, + _panel: null, + _pointsNode: null, + _pointsContentNode: null, + _dirty: true, + + getPHID: function() { + return this._phid; + }, + + getRoot: function() { + return this._root; + }, + + getCards: function() { + return this._cards; + }, + + getCard: function(phid) { + return this._cards[phid]; + }, + + getBoard: function() { + return this._board; + }, + + setNaturalOrder: function(order) { + this._naturalOrder = order; + return this; + }, + + getPointsNode: function() { + return this._pointsNode; + }, + + getPointsContentNode: function() { + return this._pointsContentNode; + }, + + getWorkpanelNode: function() { + return this._panel; + }, + + newCard: function(phid) { + var card = new JX.WorkboardCard(this, phid); + + this._cards[phid] = card; + this._naturalOrder.push(phid); + + return card; + }, + + removeCard: function(phid) { + var card = this._cards[phid]; + delete this._cards[phid]; + + for (var ii = 0; ii < this._naturalOrder.length; ii++) { + if (this._naturalOrder[ii] == phid) { + this._naturalOrder.splice(ii, 1); + break; + } + } + + return card; + }, + + addCard: function(card, after) { + var phid = card.getPHID(); + + card.setColumn(this); + this._cards[phid] = card; + + var index = 0; + + if (after) { + for (var ii = 0; ii < this._naturalOrder.length; ii++) { + if (this._naturalOrder[ii] == after) { + index = ii + 1; + break; + } + } + } + + if (index > this._naturalOrder.length) { + this._naturalOrder.push(phid); + } else { + this._naturalOrder.splice(index, 0, phid); + } + + return this; + }, + + getCardNodes: function() { + var cards = this.getCards(); + + var nodes = []; + for (var k in cards) { + nodes.push(cards[k].getNode()); + } + + return nodes; + }, + + getCardPHIDs: function() { + return JX.keys(this.getCards()); + }, + + getPointLimit: function() { + return JX.Stratcom.getData(this.getRoot()).pointLimit; + }, + + markForRedraw: function() { + this._dirty = true; + }, + + isMarkedForRedraw: function() { + return this._dirty; + }, + + redraw: function() { + var board = this.getBoard(); + var order = board.getOrder(); + + var list; + if (order == 'natural') { + list = this._getCardsSortedNaturally(); + } else { + list = this._getCardsSortedByKey(order); + } + + var content = []; + for (var ii = 0; ii < list.length; ii++) { + var card = list[ii]; + + var node = card.getNode(); + content.push(node); + + } + + JX.DOM.setContent(this.getRoot(), content); + + this._redrawFrame(); + + this._dirty = false; + }, + + _getCardsSortedNaturally: function() { + var list = []; + + for (var ii = 0; ii < this._naturalOrder.length; ii++) { + var phid = this._naturalOrder[ii]; + list.push(this.getCard(phid)); + } + + return list; + }, + + _getCardsSortedByKey: function(order) { + var cards = this.getCards(); + + var list = []; + for (var k in cards) { + list.push(cards[k]); + } + + list.sort(JX.bind(this, this._sortCards, order)); + + return list; + }, + + _sortCards: function(order, u, v) { + var ud = this.getBoard().getOrderVector(u.getPHID(), order); + var vd = this.getBoard().getOrderVector(v.getPHID(), order); + + for (var ii = 0; ii < ud.length; ii++) { + if (ud[ii] > vd[ii]) { + return 1; + } + + if (ud[ii] < vd[ii]) { + return -1; + } + } + + return 0; + }, + + _redrawFrame: function() { + var cards = this.getCards(); + var board = this.getBoard(); + + var points = {}; + for (var phid in cards) { + var card = cards[phid]; + + var card_points; + if (board.getPointsEnabled()) { + card_points = card.getPoints(); + } else { + card_points = 1; + } + + if (card_points !== null) { + var status = card.getStatus(); + if (!points[status]) { + points[status] = 0; + } + points[status] += card_points; + } + } + + var total_points = 0; + for (var k in points) { + total_points += points[k]; + } + + var limit = this.getPointLimit(); + + var display_value; + if (limit !== null && limit !== 0) { + display_value = total_points + ' / ' + limit; + } else { + display_value = total_points; + } + + var over_limit = ((limit !== null) && (total_points > limit)); + + var content_node = this.getPointsContentNode(); + var points_node = this.getPointsNode(); + + JX.DOM.setContent(content_node, display_value); + + var is_empty = !this.getCardPHIDs().length; + var panel = JX.DOM.findAbove(this.getRoot(), 'div', 'workpanel'); + JX.DOM.alterClass(panel, 'project-panel-empty', is_empty); + JX.DOM.alterClass(panel, 'project-panel-over-limit', over_limit); + + var color_map = { + 'phui-tag-shade-disabled': (total_points === 0), + 'phui-tag-shade-blue': (total_points > 0 && !over_limit), + 'phui-tag-shade-red': (over_limit) + }; + + for (var c in color_map) { + JX.DOM.alterClass(points_node, c, !!color_map[c]); + } + + JX.DOM.show(points_node); + } + + } + +}); diff --git a/webroot/rsrc/js/application/projects/WorkboardController.js b/webroot/rsrc/js/application/projects/WorkboardController.js new file mode 100644 index 0000000000..b284a418dc --- /dev/null +++ b/webroot/rsrc/js/application/projects/WorkboardController.js @@ -0,0 +1,201 @@ +/** + * @provides javelin-workboard-controller + * @requires javelin-install + * javelin-dom + * javelin-util + * javelin-vector + * javelin-stratcom + * javelin-workflow + * phabricator-drag-and-drop-file-upload + * javelin-workboard-board + * @javelin + */ + +JX.install('WorkboardController', { + + construct: function() { + this._boards = {}; + }, + + properties: { + uploadURI: null, + coverURI: null, + moveURI: null, + createURI: null, + chunkThreshold: null + }, + + members: { + _boards: null, + + _panOrigin: null, + _panNode: null, + _panX: null, + + start: function() { + this._setupCoverImageHandlers(); + this._setupPanHandlers(); + this._setupEditHandlers(); + + return this; + }, + + newBoard: function(phid, node) { + var board = new JX.WorkboardBoard(this, phid, node); + this._boards[phid] = board; + return board; + }, + + _getBoard: function(board_phid) { + return this._boards[board_phid]; + }, + + _setupCoverImageHandlers: function() { + if (!JX.PhabricatorDragAndDropFileUpload.isSupported()) { + return; + } + + var drop = new JX.PhabricatorDragAndDropFileUpload('project-card') + .setURI(this.getUploadURI()) + .setChunkThreshold(this.getChunkThreshold()); + + drop.listen('didBeginDrag', function(node) { + JX.DOM.alterClass(node, 'phui-workcard-upload-target', true); + }); + + drop.listen('didEndDrag', function(node) { + JX.DOM.alterClass(node, 'phui-workcard-upload-target', false); + }); + + drop.listen('didUpload', JX.bind(this, this._oncoverupload)); + + drop.start(); + }, + + _oncoverupload: function(file) { + var node = file.getTargetNode(); + + var board = this._getBoardFromNode(node); + + var column_node = JX.DOM.findAbove(node, 'ul', 'project-column'); + var column_phid = JX.Stratcom.getData(column_node).columnPHID; + var column = board.getColumn(column_phid); + + var data = { + boardPHID: board.getPHID(), + objectPHID: JX.Stratcom.getData(node).objectPHID, + filePHID: file.getPHID(), + visiblePHIDs: column.getCardPHIDs() + }; + + new JX.Workflow(this.getCoverURI(), data) + .setHandler(JX.bind(board, board.updateCard)) + .start(); + }, + + _getBoardFromNode: function(node) { + var board_node = JX.DOM.findAbove(node, 'div', 'jx-workboard'); + var board_phid = JX.Stratcom.getData(board_node).boardPHID; + return this._getBoard(board_phid); + }, + + _setupPanHandlers: function() { + var mousedown = JX.bind(this, this._onpanmousedown); + var mousemove = JX.bind(this, this._onpanmousemove); + var mouseup = JX.bind(this, this._onpanmouseup); + + JX.Stratcom.listen('mousedown', 'workboard-shadow', mousedown); + JX.Stratcom.listen('mousemove', null, mousemove); + JX.Stratcom.listen('mouseup', null, mouseup); + }, + + _onpanmousedown: function(e) { + if (!JX.Device.isDesktop()) { + return; + } + + if (e.getNode('workpanel')) { + return; + } + + if (JX.Stratcom.pass()) { + return; + } + + e.kill(); + + this._panOrigin = JX.$V(e); + this._panNode = e.getNode('workboard-shadow'); + this._panX = this._panNode.scrollLeft; + }, + + _onpanmousemove: function(e) { + if (!this._panOrigin) { + return; + } + + var cursor = JX.$V(e); + this._panNode.scrollLeft = this._panX + (this._panOrigin.x - cursor.x); + }, + + _onpanmouseup: function() { + this._panOrigin = null; + }, + + _setupEditHandlers: function() { + var onadd = JX.bind(this, this._onaddcard); + var onedit = JX.bind(this, this._oneditcard); + + JX.Stratcom.listen('click', 'column-add-task', onadd); + JX.Stratcom.listen('click', 'edit-project-card', onedit); + }, + + _onaddcard: function(e) { + // We want the 'boards-dropdown-menu' behavior to see this event and + // close the dropdown, but don't want to follow the link. + e.prevent(); + + var column_data = e.getNodeData('column-add-task'); + var column_phid = column_data.columnPHID; + + var board_phid = column_data.boardPHID; + var board = this._getBoard(board_phid); + var column = board.getColumn(column_phid); + + var request_data = { + responseType: 'card', + columnPHID: column.getPHID(), + projects: column_data.projectPHID, + visiblePHIDs: column.getCardPHIDs(), + order: board.getOrder() + }; + + new JX.Workflow(this.getCreateURI(), request_data) + .setHandler(JX.bind(board, board.updateCard)) + .start(); + }, + + _oneditcard: function(e) { + e.kill(); + + var column_node = e.getNode('project-column'); + var column_phid = JX.Stratcom.getData(column_node).columnPHID; + + var board = this._getBoardFromNode(column_node); + var column = board.getColumn(column_phid); + + var request_data = { + responseType: 'card', + columnPHID: column.getPHID(), + visiblePHIDs: column.getCardPHIDs(), + order: board.getOrder() + }; + + new JX.Workflow(e.getNode('tag:a').href, request_data) + .setHandler(JX.bind(board, board.updateCard)) + .start(); + } + + } + +}); diff --git a/webroot/rsrc/js/application/projects/behavior-project-boards.js b/webroot/rsrc/js/application/projects/behavior-project-boards.js index 8d9b61cc8c..8fccb1dc91 100644 --- a/webroot/rsrc/js/application/projects/behavior-project-boards.js +++ b/webroot/rsrc/js/application/projects/behavior-project-boards.js @@ -6,216 +6,11 @@ * javelin-vector * javelin-stratcom * javelin-workflow - * phabricator-draggable-list + * javelin-workboard-controller */ JX.behavior('project-boards', function(config, statics) { - function finditems(col) { - return JX.DOM.scry(col, 'li', 'project-card'); - } - - function onupdate(col) { - var data = JX.Stratcom.getData(col); - var cards = finditems(col); - - // Update the count of tasks in the column header. - if (!data.countTagNode) { - data.countTagNode = JX.$(data.countTagID); - JX.DOM.show(data.countTagNode); - } - - var sum = 0; - for (var ii = 0; ii < cards.length; ii++) { - // TODO: Allow this to be computed in some more clever way. - sum += 1; - } - - // TODO: This is a little bit hacky, but we don't have a PHUIX version of - // this element yet. - - var over_limit = (data.pointLimit && (sum > data.pointLimit)); - - var display_value = sum; - if (data.pointLimit) { - display_value = sum + ' / ' + data.pointLimit; - } - JX.DOM.setContent(JX.$(data.countTagContentID), display_value); - - - var panel_map = { - 'project-panel-empty': !cards.length, - 'project-panel-over-limit': over_limit - }; - var panel = JX.DOM.findAbove(col, 'div', 'workpanel'); - for (var p in panel_map) { - JX.DOM.alterClass(panel, p, !!panel_map[p]); - } - - var color_map = { - 'phui-tag-shade-disabled': (sum === 0), - 'phui-tag-shade-blue': (sum > 0 && !over_limit), - 'phui-tag-shade-red': (over_limit) - }; - for (var c in color_map) { - JX.DOM.alterClass(data.countTagNode, c, !!color_map[c]); - } - } - - function onresponse(response, item, list) { - list.unlock(); - JX.DOM.alterClass(item, 'drag-sending', false); - JX.DOM.replace(item, JX.$H(response.task)); - } - - function getcolumns() { - return JX.DOM.scry(JX.$(statics.boardID), 'ul', 'project-column'); - } - - function colsort(u, v) { - var ud = JX.Stratcom.getData(u).sort || []; - var vd = JX.Stratcom.getData(v).sort || []; - - for (var ii = 0; ii < ud.length; ii++) { - - if (parseInt(ud[ii]) < parseInt(vd[ii])) { - return 1; - } - if (parseInt(ud[ii]) > parseInt(vd[ii])) { - return -1; - } - } - - return 0; - } - - function getcontainer() { - return JX.DOM.find( - JX.$(statics.boardID), - 'div', - 'aphront-multi-column-view'); - } - - function onbegindrag(item) { - // If the longest column on the board is taller than the window, the board - // will scroll vertically. Dragging an item to the longest column may - // make it longer, by the total height of the board, plus the height of - // the drop target. - - // If this happens, the scrollbar will jump around and the scroll position - // can be adjusted in a disorienting way. To reproduce this, drag a task - // to the bottom of the longest column on a scrolling board and wave the - // task in and out of the column. The scroll bar will jump around and - // it will be hard to lock onto a target. - - // To fix this, set the minimum board height to the current board height - // plus the size of the drop target (which is the size of the item plus - // a bit of margin). This makes sure the scroll bar never needs to - // recalculate. - - var item_size = JX.Vector.getDim(item); - var container = getcontainer(); - var container_size = JX.Vector.getDim(container); - - container.style.minHeight = (item_size.y + container_size.y + 12) + 'px'; - } - - function onenddrag() { - getcontainer().style.minHeight = ''; - } - - function ondrop(list, item, after) { - list.lock(); - JX.DOM.alterClass(item, 'drag-sending', true); - - var item_phid = JX.Stratcom.getData(item).objectPHID; - var data = { - objectPHID: item_phid, - columnPHID: JX.Stratcom.getData(list.getRootNode()).columnPHID - }; - - var after_phid = null; - var items = finditems(list.getRootNode()); - if (after) { - after_phid = JX.Stratcom.getData(after).objectPHID; - data.afterPHID = after_phid; - } - var ii; - var ii_item; - var ii_item_phid; - var ii_prev_item_phid = null; - var before_phid = null; - for (ii = 0; ii < items.length; ii++) { - ii_item = items[ii]; - ii_item_phid = JX.Stratcom.getData(ii_item).objectPHID; - if (ii_item_phid == item_phid) { - // skip the item we just dropped - continue; - } - // note this handles when there is no after phid - we are at the top of - // the list - quite nicely - if (ii_prev_item_phid == after_phid) { - before_phid = ii_item_phid; - break; - } - ii_prev_item_phid = ii_item_phid; - } - if (before_phid) { - data.beforePHID = before_phid; - } - - data.order = statics.order; - - var workflow = new JX.Workflow(statics.moveURI, data) - .setHandler(function(response) { - onresponse(response, item, list); - }); - - workflow.start(); - } - - function onedit(column, r) { - var new_card = JX.$H(r.tasks).getNode(); - var new_data = JX.Stratcom.getData(new_card); - var items = finditems(column); - var edited = false; - var remove_index = null; - - for (var ii = 0; ii < items.length; ii++) { - var item = items[ii]; - - var data = JX.Stratcom.getData(item); - var phid = data.objectPHID; - - if (phid == new_data.objectPHID) { - if (r.data.removeFromBoard) { - remove_index = ii; - } - items[ii] = new_card; - data = new_data; - edited = true; - } - - data.sort = r.data.sortMap[data.objectPHID] || data.sort; - } - - // this is an add then...! - if (!edited) { - items[items.length + 1] = new_card; - new_data.sort = r.data.sortMap[new_data.objectPHID] || new_data.sort; - } - - if (remove_index !== null) { - items.splice(remove_index, 1); - } - - items.sort(colsort); - - JX.DOM.setContent(column, items); - - onupdate(column); - }; - function update_statics(update_config) { statics.boardID = update_config.boardID; statics.projectPHID = update_config.projectPHID; @@ -224,86 +19,7 @@ JX.behavior('project-boards', function(config, statics) { statics.createURI = update_config.createURI; } - function init_board() { - var lists = []; - var ii; - var cols = getcolumns(); - - for (ii = 0; ii < cols.length; ii++) { - var list = new JX.DraggableList('project-card', cols[ii]) - .setFindItemsHandler(JX.bind(null, finditems, cols[ii])) - .setOuterContainer(JX.$(config.boardID)) - .setCanDragX(true); - - list.listen('didSend', JX.bind(list, onupdate, cols[ii])); - list.listen('didReceive', JX.bind(list, onupdate, cols[ii])); - - list.listen('didDrop', JX.bind(null, ondrop, list)); - - list.listen('didBeginDrag', JX.bind(null, onbegindrag)); - list.listen('didEndDrag', JX.bind(null, onenddrag)); - - lists.push(list); - - onupdate(cols[ii]); - } - - for (ii = 0; ii < lists.length; ii++) { - lists[ii].setGroup(lists); - } - } - function setup() { - - JX.Stratcom.listen( - 'click', - ['edit-project-card'], - function(e) { - e.kill(); - var column = e.getNode('project-column'); - var request_data = { - responseType: 'card', - columnPHID: JX.Stratcom.getData(column).columnPHID, - order: statics.order - }; - new JX.Workflow(e.getNode('tag:a').href, request_data) - .setHandler(JX.bind(null, onedit, column)) - .start(); - }); - - JX.Stratcom.listen( - 'click', - ['column-add-task'], - function (e) { - - // We want the 'boards-dropdown-menu' behavior to see this event and - // close the dropdown, but don't want to follow the link. - e.prevent(); - - var column_data = e.getNodeData('column-add-task'); - var column_phid = column_data.columnPHID; - - var request_data = { - responseType: 'card', - columnPHID: column_phid, - projects: column_data.projectPHID, - order: statics.order - }; - - var cols = getcolumns(); - var ii; - var column; - for (ii = 0; ii < cols.length; ii++) { - if (JX.Stratcom.getData(cols[ii]).columnPHID == column_phid) { - column = cols[ii]; - break; - } - } - new JX.Workflow(statics.createURI, request_data) - .setHandler(JX.bind(null, onedit, column)) - .start(); - }); - JX.Stratcom.listen('click', 'boards-dropdown-menu', function(e) { var data = e.getNodeData('boards-dropdown-menu'); if (data.menu) { @@ -344,10 +60,8 @@ JX.behavior('project-boards', function(config, statics) { statics.boardID = new_config.boardID; } update_statics(new_config); - if (data.fromServer) { - init_board(); - } }); + return true; } @@ -356,8 +70,50 @@ JX.behavior('project-boards', function(config, statics) { var current_page_id = JX.Quicksand.getCurrentPageID(); statics.boardConfigCache = {}; statics.boardConfigCache[current_page_id] = config; - init_board(); statics.setup = setup(); } + if (!statics.workboard) { + statics.workboard = new JX.WorkboardController() + .setUploadURI(config.uploadURI) + .setCoverURI(config.coverURI) + .setMoveURI(config.moveURI) + .setCreateURI(config.createURI) + .setChunkThreshold(config.chunkThreshold) + .start(); + } + + var board_phid = config.projectPHID; + var board_node = JX.$(config.boardID); + + var board = statics.workboard.newBoard(board_phid, board_node) + .setOrder(config.order) + .setPointsEnabled(config.pointsEnabled); + + var templates = config.templateMap; + for (var k in templates) { + board.setCardTemplate(k, templates[k]); + } + + var column_maps = config.columnMaps; + for (var column_phid in column_maps) { + var column = board.getColumn(column_phid); + var column_map = column_maps[column_phid]; + for (var ii = 0; ii < column_map.length; ii++) { + column.newCard(column_map[ii]); + } + } + + var order_maps = config.orderMaps; + for (var object_phid in order_maps) { + board.setOrderMap(object_phid, order_maps[object_phid]); + } + + var property_maps = config.propertyMaps; + for (var property_phid in property_maps) { + board.setObjectProperties(property_phid, property_maps[property_phid]); + } + + board.start(); + }); diff --git a/webroot/rsrc/js/core/DragAndDropFileUpload.js b/webroot/rsrc/js/core/DragAndDropFileUpload.js index cee617061c..08cda15798 100644 --- a/webroot/rsrc/js/core/DragAndDropFileUpload.js +++ b/webroot/rsrc/js/core/DragAndDropFileUpload.js @@ -11,8 +11,12 @@ JX.install('PhabricatorDragAndDropFileUpload', { - construct : function(node) { - this._node = node; + construct : function(target) { + if (JX.DOM.isNode(target)) { + this._node = target; + } else { + this._sigil = target; + } }, events : [ @@ -39,6 +43,7 @@ JX.install('PhabricatorDragAndDropFileUpload', { members : { _node : null, + _sigil: null, _depth : 0, _isEnabled: false, @@ -53,18 +58,21 @@ JX.install('PhabricatorDragAndDropFileUpload', { _updateDepth : function(delta) { if (this._depth === 0 && delta > 0) { - this.invoke('didBeginDrag'); + this.invoke('didBeginDrag', this._getTarget()); } this._depth += delta; if (this._depth === 0 && delta < 0) { - this.invoke('didEndDrag'); + this.invoke('didEndDrag', this._getTarget()); } }, - start : function() { + _getTarget: function() { + return this._target || this._node; + }, + start : function() { // TODO: move this to JX.DOM.contains()? function contains(container, child) { @@ -80,87 +88,96 @@ JX.install('PhabricatorDragAndDropFileUpload', { // Firefox has some issues sometimes; implement this click handler so // the user can recover. See T5188. - JX.DOM.listen( - this._node, - 'click', - null, - JX.bind(this, function (e) { - if (!this.getIsEnabled()) { - return; - } - if (this._depth) { - e.kill(); - // Force depth to 0. - this._updateDepth(-this._depth); - } - })); + var on_click = JX.bind(this, function (e) { + if (!this.getIsEnabled()) { + return; + } + + if (this._depth) { + e.kill(); + // Force depth to 0. + this._updateDepth(-this._depth); + } + }); // We track depth so that the _node may have children inside of it and // not become unselected when they are dragged over. - JX.DOM.listen( - this._node, - 'dragenter', - null, - JX.bind(this, function(e) { - if (!this.getIsEnabled()) { - return; + var on_dragenter = JX.bind(this, function(e) { + if (!this.getIsEnabled()) { + return; + } + + if (!this._node) { + var target = e.getNode(this._sigil); + if (target !== this._target) { + this._updateDepth(-this._depth); + this._target = target; } + } - if (contains(this._node, e.getTarget())) { - this._updateDepth(1); - } - })); + if (contains(this._getTarget(), e.getTarget())) { + this._updateDepth(1); + } - JX.DOM.listen( - this._node, - 'dragleave', - null, - JX.bind(this, function(e) { - if (!this.getIsEnabled()) { - return; - } + }); - if (contains(this._node, e.getTarget())) { - this._updateDepth(-1); - } - })); + var on_dragleave = JX.bind(this, function(e) { + if (!this.getIsEnabled()) { + return; + } - JX.DOM.listen( - this._node, - 'dragover', - null, - JX.bind(this, function(e) { - if (!this.getIsEnabled()) { - return; - } + if (!this._getTarget()) { + return; + } - // NOTE: We must set this, or Chrome refuses to drop files from the - // download shelf. - e.getRawEvent().dataTransfer.dropEffect = 'copy'; - e.kill(); - })); + if (contains(this._getTarget(), e.getTarget())) { + this._updateDepth(-1); + } + }); - JX.DOM.listen( - this._node, - 'drop', - null, - JX.bind(this, function(e) { - if (!this.getIsEnabled()) { - return; - } + var on_dragover = JX.bind(this, function(e) { + if (!this.getIsEnabled()) { + return; + } - e.kill(); + // NOTE: We must set this, or Chrome refuses to drop files from the + // download shelf. + e.getRawEvent().dataTransfer.dropEffect = 'copy'; + e.kill(); + }); - var files = e.getRawEvent().dataTransfer.files; - for (var ii = 0; ii < files.length; ii++) { - this._sendRequest(files[ii]); - } + var on_drop = JX.bind(this, function(e) { + if (!this.getIsEnabled()) { + return; + } - // Force depth to 0. - this._updateDepth(-this._depth); - })); + e.kill(); - if (JX.PhabricatorDragAndDropFileUpload.isPasteSupported()) { + var files = e.getRawEvent().dataTransfer.files; + for (var ii = 0; ii < files.length; ii++) { + this._sendRequest(files[ii]); + } + + // Force depth to 0. + this._updateDepth(-this._depth); + }); + + if (this._node) { + JX.DOM.listen(this._node, 'click', null, on_click); + JX.DOM.listen(this._node, 'dragenter', null, on_dragenter); + JX.DOM.listen(this._node, 'dragleave', null, on_dragleave); + JX.DOM.listen(this._node, 'dragover', null, on_dragover); + JX.DOM.listen(this._node, 'drop', null, on_drop); + } else { + JX.Stratcom.listen('click', this._sigil, on_click); + JX.Stratcom.listen('dragenter', this._sigil, on_dragenter); + JX.Stratcom.listen('dragleave', this._sigil, on_dragleave); + JX.Stratcom.listen('dragover', this._sigil, on_dragover); + JX.Stratcom.listen('drop', this._sigil, on_drop); + } + + if (JX.PhabricatorDragAndDropFileUpload.isPasteSupported() && + this._node) { JX.DOM.listen( this._node, 'paste', @@ -399,6 +416,7 @@ JX.install('PhabricatorDragAndDropFileUpload', { .setURI(r.uri) .setMarkup(r.html) .setStatus('done') + .setTargetNode(this._getTarget()) .update(); this.invoke('didUpload', file); diff --git a/webroot/rsrc/js/core/DraggableList.js b/webroot/rsrc/js/core/DraggableList.js index efe137e7ab..99dd6e8fba 100644 --- a/webroot/rsrc/js/core/DraggableList.js +++ b/webroot/rsrc/js/core/DraggableList.js @@ -40,7 +40,8 @@ JX.install('DraggableList', { properties : { findItemsHandler: null, canDragX: false, - outerContainer: null + outerContainer: null, + hasInfiniteHeight: false }, members : { @@ -286,6 +287,7 @@ JX.install('DraggableList', { _getTargetList : function(p) { var target_list; + var infinity; if (this._hasGroup()) { var group = this._group; for (var ii = 0; ii < group.length; ii++) { @@ -293,6 +295,15 @@ JX.install('DraggableList', { var rp = JX.$V(root); var rd = JX.Vector.getDim(root); + if (group[ii].getHasInfiniteHeight()) { + // The math doesn't work out quite right if we actually use + // Math.Infinity, so approximate infinity as the document height. + infinity = infinity || JX.Vector.getDocument().y; + + rp.y = 0; + rd.y = infinity; + } + var is_target = false; if (p.x >= rp.x && p.y >= rp.y) { if (p.x <= (rp.x + rd.x) && p.y <= (rp.y + rd.y)) { @@ -310,6 +321,10 @@ JX.install('DraggableList', { return target_list; }, + _getTarget: function() { + return this._target; + }, + _setTarget : function(cur_target) { var ghost = this.getGhostNode(); var target = this._target; @@ -433,8 +448,6 @@ JX.install('DraggableList', { this._cursorOrigin.y - (this._cursorScroll.y - s.y)); } - this._updateAutoscroll(this._cursorPosition); - var p = JX.$V(this._cursorPosition.x, this._cursorPosition.y); var group = this._group; @@ -459,6 +472,8 @@ JX.install('DraggableList', { } } + this._updateAutoscroll(this._cursorPosition); + var f = JX.$V(this._frame); p.x -= f.x; p.y -= f.y; @@ -475,7 +490,7 @@ JX.install('DraggableList', { }, _updateAutoscroll: function(p) { - var container = this._dragging.parentNode; + var container = this._getScrollAnchor().parentNode; var autoscroll = {}; var outer = this.getOuterContainer(); @@ -595,6 +610,22 @@ JX.install('DraggableList', { this.invoke('didEndDrag', dragging); }, + _getScrollAnchor: function() { + // If you drag an item from column "A" into column "B", then move the + // mouse to the top or bottom of the screen, we need to scroll the target + // column (column "B"), not the original column. + + var group = this._group; + for (var ii = 0; ii < group.length; ii++) { + var target = group[ii]._getTarget(); + if (target) { + return group[ii]._ghostNode; + } + } + + return this._dragging; + }, + _onautoscroll: function() { var u = this._autoscroll.up; var d = this._autoscroll.down; @@ -613,20 +644,22 @@ JX.install('DraggableList', { var amount = 12 * (delta / 10); + var anchor = this._getScrollAnchor(); + if (u && (u != d)) { - this._tryScroll(this._dragging, u, 'scrollTop', amount); + this._tryScroll(anchor, u, 'scrollTop', amount); } if (d && (d != u)) { - this._tryScroll(this._dragging, d, 'scrollTop', -amount); + this._tryScroll(anchor, d, 'scrollTop', -amount); } if (l && (l != r)) { - this._tryScroll(this._dragging, l, 'scrollLeft', amount); + this._tryScroll(anchor, l, 'scrollLeft', amount); } if (r && (r != l)) { - this._tryScroll(this._dragging, r, 'scrollLeft', -amount); + this._tryScroll(anchor, r, 'scrollLeft', -amount); } }, @@ -639,19 +672,41 @@ JX.install('DraggableList', { var container = from.parentNode; while (container) { - // Read the current scroll value. - value = container[property]; - // Try to scroll. - container[property] -= amount; + // In Safari, we'll eventually reach `window.document`, which is not + // sufficently node-like to support sigil tests. + var lock = false; + if (container === window.document) { + lock = false; + } else { + // Some elements may respond to, e.g., `scrollTop` adjustment, even + // though they are not scrollable. This sigil disables adjustment + // for them. + var lock_sigil; + if (property == 'scrollTop') { + lock_sigil = 'lock-scroll-y-while-dragging'; + } - // If we scrolled it, we're all done. - if (container[property] != value) { - break; + if (lock_sigil) { + lock = JX.Stratcom.hasSigil(container, lock_sigil); + } } - if (container == to) { - break; + if (!lock) { + // Read the current scroll value. + value = container[property]; + + // Try to scroll. + container[property] -= amount; + + // If we scrolled it, we're all done. + if (container[property] != value) { + break; + } + + if (container == to) { + break; + } } container = container.parentNode; diff --git a/webroot/rsrc/js/core/FileUpload.js b/webroot/rsrc/js/core/FileUpload.js index eff1121c59..22ba33f97a 100644 --- a/webroot/rsrc/js/core/FileUpload.js +++ b/webroot/rsrc/js/core/FileUpload.js @@ -23,6 +23,7 @@ JX.install('PhabricatorFileUpload', { URI: null, status: null, markup: null, + targetNode: null, error: null }, diff --git a/webroot/rsrc/js/phuix/PHUIXFormControl.js b/webroot/rsrc/js/phuix/PHUIXFormControl.js index a2770cda0a..cd004b0e8f 100644 --- a/webroot/rsrc/js/phuix/PHUIXFormControl.js +++ b/webroot/rsrc/js/phuix/PHUIXFormControl.js @@ -35,6 +35,9 @@ JX.install('PHUIXFormControl', { case 'select': input = this._newSelect(spec); break; + case 'points': + input = this._newPoints(spec); + break; default: // TODO: Default or better error? JX.$E('Bad Input Type'); @@ -149,6 +152,25 @@ JX.install('PHUIXFormControl', { {}, spec.order); + return { + node: node, + get: function() { + return node.value; + }, + set: function(value) { + node.value = value; + } + }; + }, + + _newPoints: function(spec) { + var attrs = { + type: 'text', + value: spec.value + }; + + var node = JX.$N('input', attrs); + return { node: node, get: function() {