mirror of
https://we.phorge.it/source/phorge.git
synced 2025-01-07 05:11:05 +01:00
(stable) Promote 2019 Week 13
This commit is contained in:
commit
bc1a879953
120 changed files with 5147 additions and 805 deletions
|
@ -9,12 +9,12 @@ return array(
|
|||
'names' => array(
|
||||
'conpherence.pkg.css' => '3c8a0668',
|
||||
'conpherence.pkg.js' => '020aebcf',
|
||||
'core.pkg.css' => '34ce1741',
|
||||
'core.pkg.js' => 'f9c2509b',
|
||||
'core.pkg.css' => '7e6e954b',
|
||||
'core.pkg.js' => 'a747b035',
|
||||
'differential.pkg.css' => '8d8360fb',
|
||||
'differential.pkg.js' => '67e02996',
|
||||
'diffusion.pkg.css' => '42c75c37',
|
||||
'diffusion.pkg.js' => '91192d85',
|
||||
'diffusion.pkg.js' => 'a98c0bf7',
|
||||
'maniphest.pkg.css' => '35995d6d',
|
||||
'maniphest.pkg.js' => 'c9308721',
|
||||
'rsrc/audio/basic/alert.mp3' => '17889334',
|
||||
|
@ -30,7 +30,7 @@ return array(
|
|||
'rsrc/css/aphront/notification.css' => '30240bd2',
|
||||
'rsrc/css/aphront/panel-view.css' => '46923d46',
|
||||
'rsrc/css/aphront/phabricator-nav-view.css' => 'f8a0c1bf',
|
||||
'rsrc/css/aphront/table-view.css' => '205053cd',
|
||||
'rsrc/css/aphront/table-view.css' => '7dc3a9c2',
|
||||
'rsrc/css/aphront/tokenizer.css' => 'b52d0668',
|
||||
'rsrc/css/aphront/tooltip.css' => 'e3f2412f',
|
||||
'rsrc/css/aphront/typeahead-browse.css' => 'b7ed02d2',
|
||||
|
@ -38,7 +38,7 @@ return array(
|
|||
'rsrc/css/application/almanac/almanac.css' => '2e050f4f',
|
||||
'rsrc/css/application/auth/auth.css' => 'add92fd8',
|
||||
'rsrc/css/application/base/main-menu-view.css' => '8e2d9a28',
|
||||
'rsrc/css/application/base/notification-menu.css' => 'e6962e89',
|
||||
'rsrc/css/application/base/notification-menu.css' => '4df1ee30',
|
||||
'rsrc/css/application/base/phui-theme.css' => '35883b37',
|
||||
'rsrc/css/application/base/standard-page-view.css' => '8a295cb9',
|
||||
'rsrc/css/application/chatlog/chatlog.css' => 'abdc76ee',
|
||||
|
@ -99,7 +99,8 @@ return array(
|
|||
'rsrc/css/application/policy/policy-transaction-detail.css' => 'c02b8384',
|
||||
'rsrc/css/application/policy/policy.css' => 'ceb56a08',
|
||||
'rsrc/css/application/ponder/ponder-view.css' => '05a09d0a',
|
||||
'rsrc/css/application/project/project-card-view.css' => '3b1f7b20',
|
||||
'rsrc/css/application/project/project-card-view.css' => '4e7371cd',
|
||||
'rsrc/css/application/project/project-triggers.css' => 'cb866c2d',
|
||||
'rsrc/css/application/project/project-view.css' => '567858b3',
|
||||
'rsrc/css/application/releeph/releeph-core.css' => 'f81ff2db',
|
||||
'rsrc/css/application/releeph/releeph-preview-branch.css' => '22db5c07',
|
||||
|
@ -131,7 +132,7 @@ return array(
|
|||
'rsrc/css/phui/object-item/phui-oi-color.css' => 'b517bfa0',
|
||||
'rsrc/css/phui/object-item/phui-oi-drag-ui.css' => 'da15d3dc',
|
||||
'rsrc/css/phui/object-item/phui-oi-flush-ui.css' => '490e2e2e',
|
||||
'rsrc/css/phui/object-item/phui-oi-list-view.css' => '909f3844',
|
||||
'rsrc/css/phui/object-item/phui-oi-list-view.css' => 'a65865a7',
|
||||
'rsrc/css/phui/object-item/phui-oi-simple-ui.css' => '6a30fa46',
|
||||
'rsrc/css/phui/phui-action-list.css' => 'c4972757',
|
||||
'rsrc/css/phui/phui-action-panel.css' => '6c386cbf',
|
||||
|
@ -178,7 +179,7 @@ return array(
|
|||
'rsrc/css/phui/workboards/phui-workboard-color.css' => 'e86de308',
|
||||
'rsrc/css/phui/workboards/phui-workboard.css' => '74fc9d98',
|
||||
'rsrc/css/phui/workboards/phui-workcard.css' => '9e9eb0df',
|
||||
'rsrc/css/phui/workboards/phui-workpanel.css' => 'c5b408ad',
|
||||
'rsrc/css/phui/workboards/phui-workpanel.css' => '3ae89b20',
|
||||
'rsrc/css/sprite-login.css' => '18b368a6',
|
||||
'rsrc/css/sprite-tokens.css' => 'f1896dc5',
|
||||
'rsrc/css/syntax/syntax-default.css' => '055fc231',
|
||||
|
@ -248,7 +249,7 @@ return array(
|
|||
'rsrc/externals/javelin/lib/Routable.js' => '6a18c42e',
|
||||
'rsrc/externals/javelin/lib/Router.js' => '32755edb',
|
||||
'rsrc/externals/javelin/lib/Scrollbar.js' => 'a43ae2ae',
|
||||
'rsrc/externals/javelin/lib/Sound.js' => 'e562708c',
|
||||
'rsrc/externals/javelin/lib/Sound.js' => 'd4cc2d2a',
|
||||
'rsrc/externals/javelin/lib/URI.js' => '2e255291',
|
||||
'rsrc/externals/javelin/lib/Vector.js' => 'e9c80beb',
|
||||
'rsrc/externals/javelin/lib/WebSocket.js' => 'fdc13e4e',
|
||||
|
@ -383,7 +384,7 @@ return array(
|
|||
'rsrc/js/application/diffusion/DiffusionLocateFileSource.js' => '94243d89',
|
||||
'rsrc/js/application/diffusion/behavior-audit-preview.js' => 'b7b73831',
|
||||
'rsrc/js/application/diffusion/behavior-commit-branches.js' => '4b671572',
|
||||
'rsrc/js/application/diffusion/behavior-commit-graph.js' => '1c88f154',
|
||||
'rsrc/js/application/diffusion/behavior-commit-graph.js' => 'ef836bf2',
|
||||
'rsrc/js/application/diffusion/behavior-locate-file.js' => '87428eb2',
|
||||
'rsrc/js/application/diffusion/behavior-pull-lastmodified.js' => 'c715c123',
|
||||
'rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js' => '6a85bc5a',
|
||||
|
@ -408,15 +409,16 @@ return array(
|
|||
'rsrc/js/application/phortune/phortune-credit-card-form.js' => 'd12d214f',
|
||||
'rsrc/js/application/policy/behavior-policy-control.js' => '0eaa33a9',
|
||||
'rsrc/js/application/policy/behavior-policy-rule-editor.js' => '9347f172',
|
||||
'rsrc/js/application/projects/WorkboardBoard.js' => '9d59f098',
|
||||
'rsrc/js/application/projects/WorkboardBoard.js' => 'c02a5497',
|
||||
'rsrc/js/application/projects/WorkboardCard.js' => '0392a5d8',
|
||||
'rsrc/js/application/projects/WorkboardCardTemplate.js' => '2a61f8d4',
|
||||
'rsrc/js/application/projects/WorkboardColumn.js' => 'ec5c5ce0',
|
||||
'rsrc/js/application/projects/WorkboardColumn.js' => 'c3d24e63',
|
||||
'rsrc/js/application/projects/WorkboardController.js' => '42c7a5a7',
|
||||
'rsrc/js/application/projects/WorkboardDropEffect.js' => '8e0aa661',
|
||||
'rsrc/js/application/projects/WorkboardHeader.js' => '111bfd2d',
|
||||
'rsrc/js/application/projects/WorkboardHeaderTemplate.js' => 'b65351bd',
|
||||
'rsrc/js/application/projects/WorkboardHeaderTemplate.js' => 'ebe83a6b',
|
||||
'rsrc/js/application/projects/WorkboardOrderTemplate.js' => '03e8891f',
|
||||
'rsrc/js/application/projects/behavior-project-boards.js' => '412af9d4',
|
||||
'rsrc/js/application/projects/behavior-project-boards.js' => 'aad45445',
|
||||
'rsrc/js/application/projects/behavior-project-create.js' => '34c53422',
|
||||
'rsrc/js/application/projects/behavior-reorder-columns.js' => '8ac32fd9',
|
||||
'rsrc/js/application/releeph/releeph-preview-branch.js' => '75184d68',
|
||||
|
@ -431,13 +433,18 @@ return array(
|
|||
'rsrc/js/application/transactions/behavior-show-older-transactions.js' => '600f440c',
|
||||
'rsrc/js/application/transactions/behavior-transaction-comment-form.js' => '2bdadf1a',
|
||||
'rsrc/js/application/transactions/behavior-transaction-list.js' => '9cec214e',
|
||||
'rsrc/js/application/trigger/TriggerRule.js' => '1c60c3fc',
|
||||
'rsrc/js/application/trigger/TriggerRuleControl.js' => '5faf27b9',
|
||||
'rsrc/js/application/trigger/TriggerRuleEditor.js' => 'b49fd60c',
|
||||
'rsrc/js/application/trigger/TriggerRuleType.js' => '4feea7d3',
|
||||
'rsrc/js/application/trigger/trigger-rule-editor.js' => '398fdf13',
|
||||
'rsrc/js/application/typeahead/behavior-typeahead-browse.js' => '70245195',
|
||||
'rsrc/js/application/typeahead/behavior-typeahead-search.js' => '7b139193',
|
||||
'rsrc/js/application/uiexample/gesture-example.js' => '242dedd0',
|
||||
'rsrc/js/application/uiexample/notification-example.js' => '29819b75',
|
||||
'rsrc/js/core/Busy.js' => '5202e831',
|
||||
'rsrc/js/core/DragAndDropFileUpload.js' => '4370900d',
|
||||
'rsrc/js/core/DraggableList.js' => '8bc7d797',
|
||||
'rsrc/js/core/DraggableList.js' => 'c9ad6f70',
|
||||
'rsrc/js/core/Favicon.js' => '7930776a',
|
||||
'rsrc/js/core/FileUpload.js' => 'ab85e184',
|
||||
'rsrc/js/core/Hovercard.js' => '074f0783',
|
||||
|
@ -491,7 +498,7 @@ return array(
|
|||
'rsrc/js/core/behavior-select-on-click.js' => '66365ee2',
|
||||
'rsrc/js/core/behavior-setup-check-https.js' => '01384686',
|
||||
'rsrc/js/core/behavior-time-typeahead.js' => '5803b9e7',
|
||||
'rsrc/js/core/behavior-toggle-class.js' => 'f5c78ae3',
|
||||
'rsrc/js/core/behavior-toggle-class.js' => '32db8374',
|
||||
'rsrc/js/core/behavior-tokenizer.js' => '3b4899b0',
|
||||
'rsrc/js/core/behavior-tooltip.js' => '73ecc1f8',
|
||||
'rsrc/js/core/behavior-user-menu.js' => '60cd9241',
|
||||
|
@ -524,7 +531,7 @@ return array(
|
|||
'aphront-list-filter-view-css' => 'feb64255',
|
||||
'aphront-multi-column-view-css' => 'fbc00ba3',
|
||||
'aphront-panel-view-css' => '46923d46',
|
||||
'aphront-table-view-css' => '205053cd',
|
||||
'aphront-table-view-css' => '7dc3a9c2',
|
||||
'aphront-tokenizer-control-css' => 'b52d0668',
|
||||
'aphront-tooltip-css' => 'e3f2412f',
|
||||
'aphront-typeahead-control-css' => '8779483d',
|
||||
|
@ -599,7 +606,7 @@ return array(
|
|||
'javelin-behavior-differential-diff-radios' => '925fe8cd',
|
||||
'javelin-behavior-differential-populate' => 'dfa1d313',
|
||||
'javelin-behavior-diffusion-commit-branches' => '4b671572',
|
||||
'javelin-behavior-diffusion-commit-graph' => '1c88f154',
|
||||
'javelin-behavior-diffusion-commit-graph' => 'ef836bf2',
|
||||
'javelin-behavior-diffusion-locate-file' => '87428eb2',
|
||||
'javelin-behavior-diffusion-pull-lastmodified' => 'c715c123',
|
||||
'javelin-behavior-document-engine' => '243d6c22',
|
||||
|
@ -657,7 +664,7 @@ return array(
|
|||
'javelin-behavior-phuix-example' => 'c2c500a7',
|
||||
'javelin-behavior-policy-control' => '0eaa33a9',
|
||||
'javelin-behavior-policy-rule-editor' => '9347f172',
|
||||
'javelin-behavior-project-boards' => '412af9d4',
|
||||
'javelin-behavior-project-boards' => 'aad45445',
|
||||
'javelin-behavior-project-create' => '34c53422',
|
||||
'javelin-behavior-quicksand-blacklist' => '5a6f6a06',
|
||||
'javelin-behavior-read-only-warning' => 'b9109f8f',
|
||||
|
@ -680,8 +687,9 @@ return array(
|
|||
'javelin-behavior-stripe-payment-form' => '02cb4398',
|
||||
'javelin-behavior-test-payment-form' => '4a7fb02b',
|
||||
'javelin-behavior-time-typeahead' => '5803b9e7',
|
||||
'javelin-behavior-toggle-class' => 'f5c78ae3',
|
||||
'javelin-behavior-toggle-class' => '32db8374',
|
||||
'javelin-behavior-toggle-widget' => '8f959ad0',
|
||||
'javelin-behavior-trigger-rule-editor' => '398fdf13',
|
||||
'javelin-behavior-typeahead-browse' => '70245195',
|
||||
'javelin-behavior-typeahead-search' => '7b139193',
|
||||
'javelin-behavior-user-menu' => '60cd9241',
|
||||
|
@ -710,7 +718,7 @@ return array(
|
|||
'javelin-routable' => '6a18c42e',
|
||||
'javelin-router' => '32755edb',
|
||||
'javelin-scrollbar' => 'a43ae2ae',
|
||||
'javelin-sound' => 'e562708c',
|
||||
'javelin-sound' => 'd4cc2d2a',
|
||||
'javelin-stratcom' => '0889b835',
|
||||
'javelin-tokenizer' => '89a1ae3a',
|
||||
'javelin-typeahead' => 'a4356cde',
|
||||
|
@ -729,13 +737,14 @@ return array(
|
|||
'javelin-view-renderer' => '9aae2b66',
|
||||
'javelin-view-visitor' => '308f9fe4',
|
||||
'javelin-websocket' => 'fdc13e4e',
|
||||
'javelin-workboard-board' => '9d59f098',
|
||||
'javelin-workboard-board' => 'c02a5497',
|
||||
'javelin-workboard-card' => '0392a5d8',
|
||||
'javelin-workboard-card-template' => '2a61f8d4',
|
||||
'javelin-workboard-column' => 'ec5c5ce0',
|
||||
'javelin-workboard-column' => 'c3d24e63',
|
||||
'javelin-workboard-controller' => '42c7a5a7',
|
||||
'javelin-workboard-drop-effect' => '8e0aa661',
|
||||
'javelin-workboard-header' => '111bfd2d',
|
||||
'javelin-workboard-header-template' => 'b65351bd',
|
||||
'javelin-workboard-header-template' => 'ebe83a6b',
|
||||
'javelin-workboard-order-template' => '03e8891f',
|
||||
'javelin-workflow' => '958e9045',
|
||||
'maniphest-report-css' => '3d53188b',
|
||||
|
@ -761,7 +770,7 @@ return array(
|
|||
'phabricator-diff-changeset-list' => '04023d82',
|
||||
'phabricator-diff-inline' => 'a4a14a94',
|
||||
'phabricator-drag-and-drop-file-upload' => '4370900d',
|
||||
'phabricator-draggable-list' => '8bc7d797',
|
||||
'phabricator-draggable-list' => 'c9ad6f70',
|
||||
'phabricator-fatal-config-template-css' => '20babf50',
|
||||
'phabricator-favicon' => '7930776a',
|
||||
'phabricator-feed-css' => 'd8b6e3f8',
|
||||
|
@ -774,7 +783,7 @@ return array(
|
|||
'phabricator-nav-view-css' => 'f8a0c1bf',
|
||||
'phabricator-notification' => 'a9b91e3f',
|
||||
'phabricator-notification-css' => '30240bd2',
|
||||
'phabricator-notification-menu-css' => 'e6962e89',
|
||||
'phabricator-notification-menu-css' => '4df1ee30',
|
||||
'phabricator-object-selector-css' => 'ee77366f',
|
||||
'phabricator-phtize' => '2f1db1ed',
|
||||
'phabricator-prefab' => '5793d835',
|
||||
|
@ -844,7 +853,7 @@ return array(
|
|||
'phui-oi-color-css' => 'b517bfa0',
|
||||
'phui-oi-drag-ui-css' => 'da15d3dc',
|
||||
'phui-oi-flush-ui-css' => '490e2e2e',
|
||||
'phui-oi-list-view-css' => '909f3844',
|
||||
'phui-oi-list-view-css' => 'a65865a7',
|
||||
'phui-oi-simple-ui-css' => '6a30fa46',
|
||||
'phui-pager-css' => 'd022c7ad',
|
||||
'phui-pinboard-view-css' => '1f08f5d8',
|
||||
|
@ -860,7 +869,7 @@ return array(
|
|||
'phui-workboard-color-css' => 'e86de308',
|
||||
'phui-workboard-view-css' => '74fc9d98',
|
||||
'phui-workcard-view-css' => '9e9eb0df',
|
||||
'phui-workpanel-view-css' => 'c5b408ad',
|
||||
'phui-workpanel-view-css' => '3ae89b20',
|
||||
'phuix-action-list-view' => 'c68f183f',
|
||||
'phuix-action-view' => 'aaa08f3b',
|
||||
'phuix-autocomplete' => '8f139ef0',
|
||||
|
@ -872,7 +881,8 @@ return array(
|
|||
'policy-edit-css' => '8794e2ed',
|
||||
'policy-transaction-detail-css' => 'c02b8384',
|
||||
'ponder-view-css' => '05a09d0a',
|
||||
'project-card-view-css' => '3b1f7b20',
|
||||
'project-card-view-css' => '4e7371cd',
|
||||
'project-triggers-css' => 'cb866c2d',
|
||||
'project-view-css' => '567858b3',
|
||||
'releeph-core' => 'f81ff2db',
|
||||
'releeph-preview-branch' => '22db5c07',
|
||||
|
@ -884,6 +894,10 @@ return array(
|
|||
'syntax-default-css' => '055fc231',
|
||||
'syntax-highlighting-css' => '4234f572',
|
||||
'tokens-css' => 'ce5a50bd',
|
||||
'trigger-rule' => '1c60c3fc',
|
||||
'trigger-rule-control' => '5faf27b9',
|
||||
'trigger-rule-editor' => 'b49fd60c',
|
||||
'trigger-rule-type' => '4feea7d3',
|
||||
'typeahead-browse-css' => 'b7ed02d2',
|
||||
'unhandled-exception-css' => '9ecfc00d',
|
||||
),
|
||||
|
@ -1019,11 +1033,6 @@ return array(
|
|||
'javelin-install',
|
||||
'javelin-util',
|
||||
),
|
||||
'1c88f154' => array(
|
||||
'javelin-behavior',
|
||||
'javelin-dom',
|
||||
'javelin-stratcom',
|
||||
),
|
||||
'1cab0e9a' => array(
|
||||
'javelin-behavior',
|
||||
'javelin-dom',
|
||||
|
@ -1172,6 +1181,11 @@ return array(
|
|||
'javelin-install',
|
||||
'javelin-util',
|
||||
),
|
||||
'32db8374' => array(
|
||||
'javelin-behavior',
|
||||
'javelin-stratcom',
|
||||
'javelin-dom',
|
||||
),
|
||||
34450586 => array(
|
||||
'javelin-color',
|
||||
'javelin-install',
|
||||
|
@ -1203,6 +1217,15 @@ return array(
|
|||
'javelin-install',
|
||||
'javelin-dom',
|
||||
),
|
||||
'398fdf13' => array(
|
||||
'javelin-behavior',
|
||||
'trigger-rule-editor',
|
||||
'trigger-rule',
|
||||
'trigger-rule-type',
|
||||
),
|
||||
'3ae89b20' => array(
|
||||
'phui-workcard-view-css',
|
||||
),
|
||||
'3b4899b0' => array(
|
||||
'javelin-behavior',
|
||||
'phabricator-prefab',
|
||||
|
@ -1227,15 +1250,6 @@ return array(
|
|||
'javelin-behavior',
|
||||
'javelin-uri',
|
||||
),
|
||||
'412af9d4' => array(
|
||||
'javelin-behavior',
|
||||
'javelin-dom',
|
||||
'javelin-util',
|
||||
'javelin-vector',
|
||||
'javelin-stratcom',
|
||||
'javelin-workflow',
|
||||
'javelin-workboard-controller',
|
||||
),
|
||||
'4234f572' => array(
|
||||
'syntax-default-css',
|
||||
),
|
||||
|
@ -1342,6 +1356,9 @@ return array(
|
|||
'javelin-sound',
|
||||
'phabricator-notification',
|
||||
),
|
||||
'4feea7d3' => array(
|
||||
'trigger-rule-control',
|
||||
),
|
||||
'506aa3f4' => array(
|
||||
'javelin-behavior',
|
||||
'javelin-stratcom',
|
||||
|
@ -1427,6 +1444,9 @@ return array(
|
|||
'javelin-dom',
|
||||
'phuix-dropdown-menu',
|
||||
),
|
||||
'5faf27b9' => array(
|
||||
'phuix-form-control-view',
|
||||
),
|
||||
'600f440c' => array(
|
||||
'javelin-behavior',
|
||||
'javelin-stratcom',
|
||||
|
@ -1593,14 +1613,6 @@ return array(
|
|||
'javelin-dom',
|
||||
'javelin-typeahead-normalizer',
|
||||
),
|
||||
'8bc7d797' => array(
|
||||
'javelin-install',
|
||||
'javelin-dom',
|
||||
'javelin-stratcom',
|
||||
'javelin-util',
|
||||
'javelin-vector',
|
||||
'javelin-magical-init',
|
||||
),
|
||||
'8c2ed2bf' => array(
|
||||
'javelin-behavior',
|
||||
'javelin-dom',
|
||||
|
@ -1615,6 +1627,10 @@ return array(
|
|||
'phabricator-shaped-request',
|
||||
'conpherence-thread-manager',
|
||||
),
|
||||
'8e0aa661' => array(
|
||||
'javelin-install',
|
||||
'javelin-dom',
|
||||
),
|
||||
'8e2d9a28' => array(
|
||||
'phui-theme-css',
|
||||
),
|
||||
|
@ -1725,18 +1741,6 @@ return array(
|
|||
'javelin-uri',
|
||||
'phabricator-textareautils',
|
||||
),
|
||||
'9d59f098' => array(
|
||||
'javelin-install',
|
||||
'javelin-dom',
|
||||
'javelin-util',
|
||||
'javelin-stratcom',
|
||||
'javelin-workflow',
|
||||
'phabricator-draggable-list',
|
||||
'javelin-workboard-column',
|
||||
'javelin-workboard-header-template',
|
||||
'javelin-workboard-card-template',
|
||||
'javelin-workboard-order-template',
|
||||
),
|
||||
'9f081f05' => array(
|
||||
'javelin-behavior',
|
||||
'javelin-dom',
|
||||
|
@ -1822,6 +1826,16 @@ return array(
|
|||
'javelin-dom',
|
||||
'javelin-util',
|
||||
),
|
||||
'aad45445' => array(
|
||||
'javelin-behavior',
|
||||
'javelin-dom',
|
||||
'javelin-util',
|
||||
'javelin-vector',
|
||||
'javelin-stratcom',
|
||||
'javelin-workflow',
|
||||
'javelin-workboard-controller',
|
||||
'javelin-workboard-drop-effect',
|
||||
),
|
||||
'ab85e184' => array(
|
||||
'javelin-install',
|
||||
'javelin-dom',
|
||||
|
@ -1865,6 +1879,10 @@ return array(
|
|||
'b347a301' => array(
|
||||
'javelin-behavior',
|
||||
),
|
||||
'b49fd60c' => array(
|
||||
'multirow-row-manager',
|
||||
'trigger-rule',
|
||||
),
|
||||
'b517bfa0' => array(
|
||||
'phui-oi-list-view-css',
|
||||
),
|
||||
|
@ -1885,9 +1903,6 @@ return array(
|
|||
'javelin-stratcom',
|
||||
'javelin-dom',
|
||||
),
|
||||
'b65351bd' => array(
|
||||
'javelin-install',
|
||||
),
|
||||
'b7b73831' => array(
|
||||
'javelin-behavior',
|
||||
'javelin-dom',
|
||||
|
@ -1916,6 +1931,18 @@ return array(
|
|||
'bde53589' => array(
|
||||
'phui-inline-comment-view-css',
|
||||
),
|
||||
'c02a5497' => array(
|
||||
'javelin-install',
|
||||
'javelin-dom',
|
||||
'javelin-util',
|
||||
'javelin-stratcom',
|
||||
'javelin-workflow',
|
||||
'phabricator-draggable-list',
|
||||
'javelin-workboard-column',
|
||||
'javelin-workboard-header-template',
|
||||
'javelin-workboard-card-template',
|
||||
'javelin-workboard-order-template',
|
||||
),
|
||||
'c03f2fb4' => array(
|
||||
'javelin-install',
|
||||
),
|
||||
|
@ -1936,8 +1963,10 @@ return array(
|
|||
'phabricator-phtize',
|
||||
'javelin-dom',
|
||||
),
|
||||
'c5b408ad' => array(
|
||||
'phui-workcard-view-css',
|
||||
'c3d24e63' => array(
|
||||
'javelin-install',
|
||||
'javelin-workboard-card',
|
||||
'javelin-workboard-header',
|
||||
),
|
||||
'c687e867' => array(
|
||||
'javelin-behavior',
|
||||
|
@ -1978,6 +2007,14 @@ return array(
|
|||
'javelin-util',
|
||||
'phabricator-keyboard-shortcut-manager',
|
||||
),
|
||||
'c9ad6f70' => array(
|
||||
'javelin-install',
|
||||
'javelin-dom',
|
||||
'javelin-stratcom',
|
||||
'javelin-util',
|
||||
'javelin-vector',
|
||||
'javelin-magical-init',
|
||||
),
|
||||
'cf32921f' => array(
|
||||
'javelin-behavior',
|
||||
'javelin-dom',
|
||||
|
@ -2004,6 +2041,9 @@ return array(
|
|||
'd3799cb4' => array(
|
||||
'javelin-install',
|
||||
),
|
||||
'd4cc2d2a' => array(
|
||||
'javelin-install',
|
||||
),
|
||||
'd8a86cfb' => array(
|
||||
'javelin-behavior',
|
||||
'javelin-dom',
|
||||
|
@ -2038,9 +2078,6 @@ return array(
|
|||
'javelin-dom',
|
||||
'javelin-history',
|
||||
),
|
||||
'e562708c' => array(
|
||||
'javelin-install',
|
||||
),
|
||||
'e5bdb730' => array(
|
||||
'javelin-behavior',
|
||||
'javelin-stratcom',
|
||||
|
@ -2068,14 +2105,12 @@ return array(
|
|||
'javelin-install',
|
||||
'javelin-event',
|
||||
),
|
||||
'ebe83a6b' => array(
|
||||
'javelin-install',
|
||||
),
|
||||
'ec4e31c0' => array(
|
||||
'phui-timeline-view-css',
|
||||
),
|
||||
'ec5c5ce0' => array(
|
||||
'javelin-install',
|
||||
'javelin-workboard-card',
|
||||
'javelin-workboard-header',
|
||||
),
|
||||
'ee77366f' => array(
|
||||
'aphront-dialog-view-css',
|
||||
),
|
||||
|
@ -2084,6 +2119,11 @@ return array(
|
|||
'phabricator-keyboard-shortcut',
|
||||
'javelin-stratcom',
|
||||
),
|
||||
'ef836bf2' => array(
|
||||
'javelin-behavior',
|
||||
'javelin-dom',
|
||||
'javelin-stratcom',
|
||||
),
|
||||
'f166c949' => array(
|
||||
'javelin-behavior',
|
||||
'javelin-behavior-device',
|
||||
|
@ -2114,11 +2154,6 @@ return array(
|
|||
'javelin-stratcom',
|
||||
'javelin-dom',
|
||||
),
|
||||
'f5c78ae3' => array(
|
||||
'javelin-behavior',
|
||||
'javelin-stratcom',
|
||||
'javelin-dom',
|
||||
),
|
||||
'f84bcbf4' => array(
|
||||
'javelin-behavior',
|
||||
'javelin-stratcom',
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
CREATE TABLE {$NAMESPACE}_project.project_trigger (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
phid VARBINARY(64) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL COLLATE {$COLLATE_TEXT},
|
||||
editPolicy VARBINARY(64) NOT NULL,
|
||||
ruleset LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT},
|
||||
dateCreated INT UNSIGNED NOT NULL,
|
||||
dateModified INT UNSIGNED NOT NULL
|
||||
) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT};
|
19
resources/sql/autopatches/20190312.triggers.02.xaction.sql
Normal file
19
resources/sql/autopatches/20190312.triggers.02.xaction.sql
Normal file
|
@ -0,0 +1,19 @@
|
|||
CREATE TABLE {$NAMESPACE}_project.project_triggertransaction (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
phid VARBINARY(64) NOT NULL,
|
||||
authorPHID VARBINARY(64) NOT NULL,
|
||||
objectPHID VARBINARY(64) NOT NULL,
|
||||
viewPolicy VARBINARY(64) NOT NULL,
|
||||
editPolicy VARBINARY(64) NOT NULL,
|
||||
commentPHID VARBINARY(64) DEFAULT NULL,
|
||||
commentVersion INT UNSIGNED NOT NULL,
|
||||
transactionType VARCHAR(32) NOT NULL,
|
||||
oldValue LONGTEXT NOT NULL,
|
||||
newValue LONGTEXT NOT NULL,
|
||||
contentSource LONGTEXT NOT NULL,
|
||||
metadata LONGTEXT NOT NULL,
|
||||
dateCreated INT UNSIGNED NOT NULL,
|
||||
dateModified INT UNSIGNED NOT NULL,
|
||||
UNIQUE KEY `key_phid` (`phid`),
|
||||
KEY `key_object` (`objectPHID`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE {$COLLATE_TEXT};
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE {$NAMESPACE}_project.project_column
|
||||
ADD triggerPHID VARBINARY(64);
|
8
resources/sql/autopatches/20190322.triggers.01.usage.sql
Normal file
8
resources/sql/autopatches/20190322.triggers.01.usage.sql
Normal file
|
@ -0,0 +1,8 @@
|
|||
CREATE TABLE {$NAMESPACE}_project.project_triggerusage (
|
||||
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
triggerPHID VARBINARY(64) NOT NULL,
|
||||
examplePHID VARBINARY(64),
|
||||
columnCount INT UNSIGNED NOT NULL,
|
||||
activeColumnCount INT UNSIGNED NOT NULL,
|
||||
UNIQUE KEY `key_trigger` (triggerPHID)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET={$CHARSET} COLLATE {$COLLATE_TEXT};
|
|
@ -3692,6 +3692,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorOlderInlinesSetting' => 'applications/settings/setting/PhabricatorOlderInlinesSetting.php',
|
||||
'PhabricatorOneTimeTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorOneTimeTriggerClock.php',
|
||||
'PhabricatorOpcodeCacheSpec' => 'applications/cache/spec/PhabricatorOpcodeCacheSpec.php',
|
||||
'PhabricatorOptionExportField' => 'infrastructure/export/field/PhabricatorOptionExportField.php',
|
||||
'PhabricatorOptionGroupSetting' => 'applications/settings/setting/PhabricatorOptionGroupSetting.php',
|
||||
'PhabricatorOwnerPathQuery' => 'applications/owners/query/PhabricatorOwnerPathQuery.php',
|
||||
'PhabricatorOwnersApplication' => 'applications/owners/application/PhabricatorOwnersApplication.php',
|
||||
|
@ -4058,6 +4059,8 @@ phutil_register_library_map(array(
|
|||
'PhabricatorProjectColumnEditController' => 'applications/project/controller/PhabricatorProjectColumnEditController.php',
|
||||
'PhabricatorProjectColumnHeader' => 'applications/project/order/PhabricatorProjectColumnHeader.php',
|
||||
'PhabricatorProjectColumnHideController' => 'applications/project/controller/PhabricatorProjectColumnHideController.php',
|
||||
'PhabricatorProjectColumnLimitTransaction' => 'applications/project/xaction/column/PhabricatorProjectColumnLimitTransaction.php',
|
||||
'PhabricatorProjectColumnNameTransaction' => 'applications/project/xaction/column/PhabricatorProjectColumnNameTransaction.php',
|
||||
'PhabricatorProjectColumnNaturalOrder' => 'applications/project/order/PhabricatorProjectColumnNaturalOrder.php',
|
||||
'PhabricatorProjectColumnOrder' => 'applications/project/order/PhabricatorProjectColumnOrder.php',
|
||||
'PhabricatorProjectColumnOwnerOrder' => 'applications/project/order/PhabricatorProjectColumnOwnerOrder.php',
|
||||
|
@ -4067,12 +4070,16 @@ phutil_register_library_map(array(
|
|||
'PhabricatorProjectColumnPositionQuery' => 'applications/project/query/PhabricatorProjectColumnPositionQuery.php',
|
||||
'PhabricatorProjectColumnPriorityOrder' => 'applications/project/order/PhabricatorProjectColumnPriorityOrder.php',
|
||||
'PhabricatorProjectColumnQuery' => 'applications/project/query/PhabricatorProjectColumnQuery.php',
|
||||
'PhabricatorProjectColumnRemoveTriggerController' => 'applications/project/controller/PhabricatorProjectColumnRemoveTriggerController.php',
|
||||
'PhabricatorProjectColumnSearchEngine' => 'applications/project/query/PhabricatorProjectColumnSearchEngine.php',
|
||||
'PhabricatorProjectColumnStatusOrder' => 'applications/project/order/PhabricatorProjectColumnStatusOrder.php',
|
||||
'PhabricatorProjectColumnStatusTransaction' => 'applications/project/xaction/column/PhabricatorProjectColumnStatusTransaction.php',
|
||||
'PhabricatorProjectColumnTitleOrder' => 'applications/project/order/PhabricatorProjectColumnTitleOrder.php',
|
||||
'PhabricatorProjectColumnTransaction' => 'applications/project/storage/PhabricatorProjectColumnTransaction.php',
|
||||
'PhabricatorProjectColumnTransactionEditor' => 'applications/project/editor/PhabricatorProjectColumnTransactionEditor.php',
|
||||
'PhabricatorProjectColumnTransactionQuery' => 'applications/project/query/PhabricatorProjectColumnTransactionQuery.php',
|
||||
'PhabricatorProjectColumnTransactionType' => 'applications/project/xaction/column/PhabricatorProjectColumnTransactionType.php',
|
||||
'PhabricatorProjectColumnTriggerTransaction' => 'applications/project/xaction/column/PhabricatorProjectColumnTriggerTransaction.php',
|
||||
'PhabricatorProjectConfigOptions' => 'applications/project/config/PhabricatorProjectConfigOptions.php',
|
||||
'PhabricatorProjectConfiguredCustomField' => 'applications/project/customfield/PhabricatorProjectConfiguredCustomField.php',
|
||||
'PhabricatorProjectController' => 'applications/project/controller/PhabricatorProjectController.php',
|
||||
|
@ -4087,6 +4094,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorProjectDefaultController' => 'applications/project/controller/PhabricatorProjectDefaultController.php',
|
||||
'PhabricatorProjectDescriptionField' => 'applications/project/customfield/PhabricatorProjectDescriptionField.php',
|
||||
'PhabricatorProjectDetailsProfileMenuItem' => 'applications/project/menuitem/PhabricatorProjectDetailsProfileMenuItem.php',
|
||||
'PhabricatorProjectDropEffect' => 'applications/project/icon/PhabricatorProjectDropEffect.php',
|
||||
'PhabricatorProjectEditController' => 'applications/project/controller/PhabricatorProjectEditController.php',
|
||||
'PhabricatorProjectEditEngine' => 'applications/project/engine/PhabricatorProjectEditEngine.php',
|
||||
'PhabricatorProjectEditPictureController' => 'applications/project/controller/PhabricatorProjectEditPictureController.php',
|
||||
|
@ -4165,6 +4173,30 @@ phutil_register_library_map(array(
|
|||
'PhabricatorProjectTransactionEditor' => 'applications/project/editor/PhabricatorProjectTransactionEditor.php',
|
||||
'PhabricatorProjectTransactionQuery' => 'applications/project/query/PhabricatorProjectTransactionQuery.php',
|
||||
'PhabricatorProjectTransactionType' => 'applications/project/xaction/PhabricatorProjectTransactionType.php',
|
||||
'PhabricatorProjectTrigger' => 'applications/project/storage/PhabricatorProjectTrigger.php',
|
||||
'PhabricatorProjectTriggerController' => 'applications/project/controller/trigger/PhabricatorProjectTriggerController.php',
|
||||
'PhabricatorProjectTriggerCorruptionException' => 'applications/project/exception/PhabricatorProjectTriggerCorruptionException.php',
|
||||
'PhabricatorProjectTriggerEditController' => 'applications/project/controller/trigger/PhabricatorProjectTriggerEditController.php',
|
||||
'PhabricatorProjectTriggerEditor' => 'applications/project/editor/PhabricatorProjectTriggerEditor.php',
|
||||
'PhabricatorProjectTriggerInvalidRule' => 'applications/project/trigger/PhabricatorProjectTriggerInvalidRule.php',
|
||||
'PhabricatorProjectTriggerListController' => 'applications/project/controller/trigger/PhabricatorProjectTriggerListController.php',
|
||||
'PhabricatorProjectTriggerManiphestPriorityRule' => 'applications/project/trigger/PhabricatorProjectTriggerManiphestPriorityRule.php',
|
||||
'PhabricatorProjectTriggerManiphestStatusRule' => 'applications/project/trigger/PhabricatorProjectTriggerManiphestStatusRule.php',
|
||||
'PhabricatorProjectTriggerNameTransaction' => 'applications/project/xaction/trigger/PhabricatorProjectTriggerNameTransaction.php',
|
||||
'PhabricatorProjectTriggerPHIDType' => 'applications/project/phid/PhabricatorProjectTriggerPHIDType.php',
|
||||
'PhabricatorProjectTriggerPlaySoundRule' => 'applications/project/trigger/PhabricatorProjectTriggerPlaySoundRule.php',
|
||||
'PhabricatorProjectTriggerQuery' => 'applications/project/query/PhabricatorProjectTriggerQuery.php',
|
||||
'PhabricatorProjectTriggerRule' => 'applications/project/trigger/PhabricatorProjectTriggerRule.php',
|
||||
'PhabricatorProjectTriggerRuleRecord' => 'applications/project/trigger/PhabricatorProjectTriggerRuleRecord.php',
|
||||
'PhabricatorProjectTriggerRulesetTransaction' => 'applications/project/xaction/trigger/PhabricatorProjectTriggerRulesetTransaction.php',
|
||||
'PhabricatorProjectTriggerSearchEngine' => 'applications/project/query/PhabricatorProjectTriggerSearchEngine.php',
|
||||
'PhabricatorProjectTriggerTransaction' => 'applications/project/storage/PhabricatorProjectTriggerTransaction.php',
|
||||
'PhabricatorProjectTriggerTransactionQuery' => 'applications/project/query/PhabricatorProjectTriggerTransactionQuery.php',
|
||||
'PhabricatorProjectTriggerTransactionType' => 'applications/project/xaction/trigger/PhabricatorProjectTriggerTransactionType.php',
|
||||
'PhabricatorProjectTriggerUnknownRule' => 'applications/project/trigger/PhabricatorProjectTriggerUnknownRule.php',
|
||||
'PhabricatorProjectTriggerUsage' => 'applications/project/storage/PhabricatorProjectTriggerUsage.php',
|
||||
'PhabricatorProjectTriggerUsageIndexEngineExtension' => 'applications/project/engineextension/PhabricatorProjectTriggerUsageIndexEngineExtension.php',
|
||||
'PhabricatorProjectTriggerViewController' => 'applications/project/controller/trigger/PhabricatorProjectTriggerViewController.php',
|
||||
'PhabricatorProjectTypeTransaction' => 'applications/project/xaction/PhabricatorProjectTypeTransaction.php',
|
||||
'PhabricatorProjectUIEventListener' => 'applications/project/events/PhabricatorProjectUIEventListener.php',
|
||||
'PhabricatorProjectUpdateController' => 'applications/project/controller/PhabricatorProjectUpdateController.php',
|
||||
|
@ -9687,6 +9719,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorOlderInlinesSetting' => 'PhabricatorSelectSetting',
|
||||
'PhabricatorOneTimeTriggerClock' => 'PhabricatorTriggerClock',
|
||||
'PhabricatorOpcodeCacheSpec' => 'PhabricatorCacheSpec',
|
||||
'PhabricatorOptionExportField' => 'PhabricatorExportField',
|
||||
'PhabricatorOptionGroupSetting' => 'PhabricatorSetting',
|
||||
'PhabricatorOwnerPathQuery' => 'Phobject',
|
||||
'PhabricatorOwnersApplication' => 'PhabricatorApplication',
|
||||
|
@ -10151,6 +10184,8 @@ phutil_register_library_map(array(
|
|||
'PhabricatorProjectColumnEditController' => 'PhabricatorProjectBoardController',
|
||||
'PhabricatorProjectColumnHeader' => 'Phobject',
|
||||
'PhabricatorProjectColumnHideController' => 'PhabricatorProjectBoardController',
|
||||
'PhabricatorProjectColumnLimitTransaction' => 'PhabricatorProjectColumnTransactionType',
|
||||
'PhabricatorProjectColumnNameTransaction' => 'PhabricatorProjectColumnTransactionType',
|
||||
'PhabricatorProjectColumnNaturalOrder' => 'PhabricatorProjectColumnOrder',
|
||||
'PhabricatorProjectColumnOrder' => 'Phobject',
|
||||
'PhabricatorProjectColumnOwnerOrder' => 'PhabricatorProjectColumnOrder',
|
||||
|
@ -10163,12 +10198,16 @@ phutil_register_library_map(array(
|
|||
'PhabricatorProjectColumnPositionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
|
||||
'PhabricatorProjectColumnPriorityOrder' => 'PhabricatorProjectColumnOrder',
|
||||
'PhabricatorProjectColumnQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
|
||||
'PhabricatorProjectColumnRemoveTriggerController' => 'PhabricatorProjectBoardController',
|
||||
'PhabricatorProjectColumnSearchEngine' => 'PhabricatorApplicationSearchEngine',
|
||||
'PhabricatorProjectColumnStatusOrder' => 'PhabricatorProjectColumnOrder',
|
||||
'PhabricatorProjectColumnStatusTransaction' => 'PhabricatorProjectColumnTransactionType',
|
||||
'PhabricatorProjectColumnTitleOrder' => 'PhabricatorProjectColumnOrder',
|
||||
'PhabricatorProjectColumnTransaction' => 'PhabricatorApplicationTransaction',
|
||||
'PhabricatorProjectColumnTransaction' => 'PhabricatorModularTransaction',
|
||||
'PhabricatorProjectColumnTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
|
||||
'PhabricatorProjectColumnTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
|
||||
'PhabricatorProjectColumnTransactionType' => 'PhabricatorModularTransactionType',
|
||||
'PhabricatorProjectColumnTriggerTransaction' => 'PhabricatorProjectColumnTransactionType',
|
||||
'PhabricatorProjectConfigOptions' => 'PhabricatorApplicationConfigOptions',
|
||||
'PhabricatorProjectConfiguredCustomField' => array(
|
||||
'PhabricatorProjectStandardCustomField',
|
||||
|
@ -10186,6 +10225,7 @@ phutil_register_library_map(array(
|
|||
'PhabricatorProjectDefaultController' => 'PhabricatorProjectBoardController',
|
||||
'PhabricatorProjectDescriptionField' => 'PhabricatorProjectStandardCustomField',
|
||||
'PhabricatorProjectDetailsProfileMenuItem' => 'PhabricatorProfileMenuItem',
|
||||
'PhabricatorProjectDropEffect' => 'Phobject',
|
||||
'PhabricatorProjectEditController' => 'PhabricatorProjectController',
|
||||
'PhabricatorProjectEditEngine' => 'PhabricatorEditEngine',
|
||||
'PhabricatorProjectEditPictureController' => 'PhabricatorProjectController',
|
||||
|
@ -10266,6 +10306,36 @@ phutil_register_library_map(array(
|
|||
'PhabricatorProjectTransactionEditor' => 'PhabricatorApplicationTransactionEditor',
|
||||
'PhabricatorProjectTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
|
||||
'PhabricatorProjectTransactionType' => 'PhabricatorModularTransactionType',
|
||||
'PhabricatorProjectTrigger' => array(
|
||||
'PhabricatorProjectDAO',
|
||||
'PhabricatorApplicationTransactionInterface',
|
||||
'PhabricatorPolicyInterface',
|
||||
'PhabricatorIndexableInterface',
|
||||
'PhabricatorDestructibleInterface',
|
||||
),
|
||||
'PhabricatorProjectTriggerController' => 'PhabricatorProjectController',
|
||||
'PhabricatorProjectTriggerCorruptionException' => 'Exception',
|
||||
'PhabricatorProjectTriggerEditController' => 'PhabricatorProjectTriggerController',
|
||||
'PhabricatorProjectTriggerEditor' => 'PhabricatorApplicationTransactionEditor',
|
||||
'PhabricatorProjectTriggerInvalidRule' => 'PhabricatorProjectTriggerRule',
|
||||
'PhabricatorProjectTriggerListController' => 'PhabricatorProjectTriggerController',
|
||||
'PhabricatorProjectTriggerManiphestPriorityRule' => 'PhabricatorProjectTriggerRule',
|
||||
'PhabricatorProjectTriggerManiphestStatusRule' => 'PhabricatorProjectTriggerRule',
|
||||
'PhabricatorProjectTriggerNameTransaction' => 'PhabricatorProjectTriggerTransactionType',
|
||||
'PhabricatorProjectTriggerPHIDType' => 'PhabricatorPHIDType',
|
||||
'PhabricatorProjectTriggerPlaySoundRule' => 'PhabricatorProjectTriggerRule',
|
||||
'PhabricatorProjectTriggerQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
|
||||
'PhabricatorProjectTriggerRule' => 'Phobject',
|
||||
'PhabricatorProjectTriggerRuleRecord' => 'Phobject',
|
||||
'PhabricatorProjectTriggerRulesetTransaction' => 'PhabricatorProjectTriggerTransactionType',
|
||||
'PhabricatorProjectTriggerSearchEngine' => 'PhabricatorApplicationSearchEngine',
|
||||
'PhabricatorProjectTriggerTransaction' => 'PhabricatorModularTransaction',
|
||||
'PhabricatorProjectTriggerTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
|
||||
'PhabricatorProjectTriggerTransactionType' => 'PhabricatorModularTransactionType',
|
||||
'PhabricatorProjectTriggerUnknownRule' => 'PhabricatorProjectTriggerRule',
|
||||
'PhabricatorProjectTriggerUsage' => 'PhabricatorProjectDAO',
|
||||
'PhabricatorProjectTriggerUsageIndexEngineExtension' => 'PhabricatorIndexEngineExtension',
|
||||
'PhabricatorProjectTriggerViewController' => 'PhabricatorProjectTriggerController',
|
||||
'PhabricatorProjectTypeTransaction' => 'PhabricatorProjectTransactionType',
|
||||
'PhabricatorProjectUIEventListener' => 'PhabricatorEventListener',
|
||||
'PhabricatorProjectUpdateController' => 'PhabricatorProjectController',
|
||||
|
|
|
@ -3,6 +3,12 @@
|
|||
final class PhabricatorAuthChallengeStatusController
|
||||
extends PhabricatorAuthController {
|
||||
|
||||
public function shouldAllowPartialSessions() {
|
||||
// We expect that users may request the status of an MFA challenge when
|
||||
// they hit the session upgrade gate on login.
|
||||
return true;
|
||||
}
|
||||
|
||||
public function handleRequest(AphrontRequest $request) {
|
||||
$viewer = $this->getViewer();
|
||||
$id = $request->getURIData('id');
|
||||
|
|
|
@ -608,6 +608,7 @@ abstract class PhabricatorController extends AphrontController {
|
|||
$this->setCurrentApplication($application);
|
||||
|
||||
$controller = new LegalpadDocumentSignController();
|
||||
$controller->setIsSessionGate(true);
|
||||
return $this->delegateToController($controller);
|
||||
}
|
||||
|
||||
|
|
|
@ -51,13 +51,16 @@ final class ConpherenceThreadIndexEngineExtension
|
|||
ConpherenceThread $thread,
|
||||
ConpherenceTransaction $xaction) {
|
||||
|
||||
$previous = id(new ConpherenceTransactionQuery())
|
||||
$pager = id(new AphrontCursorPagerView())
|
||||
->setPageSize(1)
|
||||
->setAfterID($xaction->getID());
|
||||
|
||||
$previous_xactions = id(new ConpherenceTransactionQuery())
|
||||
->setViewer($this->getViewer())
|
||||
->withObjectPHIDs(array($thread->getPHID()))
|
||||
->withTransactionTypes(array(PhabricatorTransactions::TYPE_COMMENT))
|
||||
->setAfterID($xaction->getID())
|
||||
->setLimit(1)
|
||||
->executeOne();
|
||||
->executeWithCursorPager($pager);
|
||||
$previous = head($previous_xactions);
|
||||
|
||||
$index = id(new ConpherenceIndex())
|
||||
->setThreadPHID($thread->getPHID())
|
||||
|
|
|
@ -106,9 +106,64 @@ final class PhabricatorDashboardQueryPanelType
|
|||
}
|
||||
}
|
||||
|
||||
$results = $engine->executeQuery($query, $pager);
|
||||
$query->setReturnPartialResultsOnOverheat(true);
|
||||
|
||||
return $engine->renderResults($results, $saved);
|
||||
$results = $engine->executeQuery($query, $pager);
|
||||
$results_view = $engine->renderResults($results, $saved);
|
||||
|
||||
$is_overheated = $query->getIsOverheated();
|
||||
$overheated_view = null;
|
||||
if ($is_overheated) {
|
||||
$content = $results_view->getContent();
|
||||
|
||||
$overheated_message =
|
||||
PhabricatorApplicationSearchController::newOverheatedError(
|
||||
(bool)$results);
|
||||
|
||||
$overheated_warning = id(new PHUIInfoView())
|
||||
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
|
||||
->setTitle(pht('Query Overheated'))
|
||||
->setErrors(
|
||||
array(
|
||||
$overheated_message,
|
||||
));
|
||||
|
||||
$overheated_box = id(new PHUIBoxView())
|
||||
->addClass('mmt mmb')
|
||||
->appendChild($overheated_warning);
|
||||
|
||||
$content = array($content, $overheated_box);
|
||||
$results_view->setContent($content);
|
||||
}
|
||||
|
||||
if ($pager->getHasMoreResults()) {
|
||||
$item_list = $results_view->getObjectList();
|
||||
|
||||
$more_href = $engine->getQueryResultsPageURI($key);
|
||||
if ($item_list) {
|
||||
$item_list->newTailButton()
|
||||
->setHref($more_href);
|
||||
} else {
|
||||
// For search engines that do not return an object list, add a fake
|
||||
// one to the end so we can render a "View All Results" button that
|
||||
// looks like it does in normal applications. At time of writing,
|
||||
// several major applications like Maniphest (which has group headers)
|
||||
// and Feed (which uses custom rendering) don't return simple lists.
|
||||
|
||||
$content = $results_view->getContent();
|
||||
|
||||
$more_list = id(new PHUIObjectItemListView())
|
||||
->setAllowEmptyList(true);
|
||||
|
||||
$more_list->newTailButton()
|
||||
->setHref($more_href);
|
||||
|
||||
$content = array($content, $more_list);
|
||||
$results_view->setContent($content);
|
||||
}
|
||||
}
|
||||
|
||||
return $results_view;
|
||||
}
|
||||
|
||||
public function adjustPanelHeader(
|
||||
|
@ -120,10 +175,18 @@ final class PhabricatorDashboardQueryPanelType
|
|||
$search_engine = $this->getSearchEngine($panel);
|
||||
$key = $panel->getProperty('key');
|
||||
$href = $search_engine->getQueryResultsPageURI($key);
|
||||
|
||||
$icon = id(new PHUIIconView())
|
||||
->setIcon('fa-search')
|
||||
->setHref($href);
|
||||
$header->addActionItem($icon);
|
||||
->setIcon('fa-search');
|
||||
|
||||
$button = id(new PHUIButtonView())
|
||||
->setTag('a')
|
||||
->setText(pht('View All'))
|
||||
->setIcon($icon)
|
||||
->setHref($href)
|
||||
->setColor(PHUIButtonView::GREY);
|
||||
|
||||
$header->addActionLink($button);
|
||||
|
||||
return $header;
|
||||
}
|
||||
|
|
|
@ -512,8 +512,7 @@ abstract class DiffusionController extends PhabricatorController {
|
|||
->setIcon('fa-code')
|
||||
->setHref($drequest->generateURI(
|
||||
array(
|
||||
'action' => 'branch',
|
||||
'path' => '/',
|
||||
'action' => 'browse',
|
||||
)))
|
||||
->setSelected($key == 'code'));
|
||||
|
||||
|
|
|
@ -455,12 +455,16 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController {
|
|||
private function newBuildsView(HarbormasterBuildPlan $plan) {
|
||||
$viewer = $this->getViewer();
|
||||
|
||||
$limit = 10;
|
||||
$builds = id(new HarbormasterBuildQuery())
|
||||
->setViewer($viewer)
|
||||
->withBuildPlanPHIDs(array($plan->getPHID()))
|
||||
->setLimit(10)
|
||||
->setLimit($limit + 1)
|
||||
->execute();
|
||||
|
||||
$more_results = (count($builds) > $limit);
|
||||
$builds = array_slice($builds, 0, $limit);
|
||||
|
||||
$list = id(new HarbormasterBuildView())
|
||||
->setViewer($viewer)
|
||||
->setBuilds($builds)
|
||||
|
@ -472,6 +476,11 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController {
|
|||
$this->getApplicationURI('/build/'),
|
||||
array('plan' => $plan->getPHID()));
|
||||
|
||||
if ($more_results) {
|
||||
$list->newTailButton()
|
||||
->setHref($more_href);
|
||||
}
|
||||
|
||||
$more_link = id(new PHUIButtonView())
|
||||
->setTag('a')
|
||||
->setIcon('fa-list-ul')
|
||||
|
@ -491,14 +500,18 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController {
|
|||
private function newRulesView(HarbormasterBuildPlan $plan) {
|
||||
$viewer = $this->getViewer();
|
||||
|
||||
$limit = 10;
|
||||
$rules = id(new HeraldRuleQuery())
|
||||
->setViewer($viewer)
|
||||
->withDisabled(false)
|
||||
->withAffectedObjectPHIDs(array($plan->getPHID()))
|
||||
->needValidateAuthors(true)
|
||||
->setLimit(10)
|
||||
->setLimit($limit + 1)
|
||||
->execute();
|
||||
|
||||
$more_results = (count($rules) > $limit);
|
||||
$rules = array_slice($rules, 0, $limit);
|
||||
|
||||
$list = id(new HeraldRuleListView())
|
||||
->setViewer($viewer)
|
||||
->setRules($rules)
|
||||
|
@ -510,6 +523,11 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController {
|
|||
'/herald/',
|
||||
array('affectedPHID' => $plan->getPHID()));
|
||||
|
||||
if ($more_results) {
|
||||
$list->newTailButton()
|
||||
->setHref($more_href);
|
||||
}
|
||||
|
||||
$more_link = id(new PHUIButtonView())
|
||||
->setTag('a')
|
||||
->setIcon('fa-list-ul')
|
||||
|
|
|
@ -350,15 +350,19 @@ final class HarbormasterBuildPlanBehavior
|
|||
->setKey(self::BEHAVIOR_RESTARTABLE)
|
||||
->setEditInstructions(
|
||||
pht(
|
||||
'Usually, builds may be restarted. This may be useful if you '.
|
||||
'suspect a build has failed for environmental or circumstantial '.
|
||||
'reasons unrelated to the actual code, and want to give it '.
|
||||
'another chance at glory.'.
|
||||
'Usually, builds may be restarted by users who have permission '.
|
||||
'to edit the related build plan. (You can change who is allowed '.
|
||||
'to restart a build by adjusting the "Runnable" behavior.)'.
|
||||
"\n\n".
|
||||
'Restarting a build may be useful if you suspect it has failed '.
|
||||
'for environmental or circumstantial reasons unrelated to the '.
|
||||
'actual code, and want to give it another chance at glory.'.
|
||||
"\n\n".
|
||||
'If you want to prevent a build from being restarted, you can '.
|
||||
'change the behavior here. This may be useful to prevent '.
|
||||
'accidents where a build with a dangerous side effect (like '.
|
||||
'deployment) is restarted improperly.'))
|
||||
'change when it may be restarted by adjusting this behavior. '.
|
||||
'This may be useful to prevent accidents where a build with a '.
|
||||
'dangerous side effect (like deployment) is restarted '.
|
||||
'improperly.'))
|
||||
->setName(pht('Restartable'))
|
||||
->setOptions($restart_options),
|
||||
id(new self())
|
||||
|
|
|
@ -77,149 +77,76 @@ final class PHUIHomeView
|
|||
return $view;
|
||||
}
|
||||
|
||||
private function buildHomepagePanel($title, $href, $view) {
|
||||
$title = phutil_tag(
|
||||
'a',
|
||||
array(
|
||||
'href' => $href,
|
||||
),
|
||||
$title);
|
||||
|
||||
$icon = id(new PHUIIconView())
|
||||
->setIcon('fa-search')
|
||||
->setHref($href);
|
||||
|
||||
$header = id(new PHUIHeaderView())
|
||||
->setHeader($title)
|
||||
->addActionItem($icon);
|
||||
|
||||
$box = id(new PHUIObjectBoxView())
|
||||
->setHeader($header);
|
||||
|
||||
if ($view->getObjectList()) {
|
||||
$box->setObjectList($view->getObjectList());
|
||||
}
|
||||
if ($view->getContent()) {
|
||||
$box->appendChild($view->getContent());
|
||||
}
|
||||
|
||||
return $box;
|
||||
}
|
||||
|
||||
private function buildRevisionPanel() {
|
||||
$viewer = $this->getViewer();
|
||||
if (!$viewer->isLoggedIn()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$engine = new DifferentialRevisionSearchEngine();
|
||||
$engine->setViewer($viewer);
|
||||
$saved = $engine->buildSavedQueryFromBuiltin('active');
|
||||
$query = $engine->buildQueryFromSavedQuery($saved);
|
||||
$pager = $engine->newPagerForSavedQuery($saved);
|
||||
$pager->setPageSize(15);
|
||||
$results = $engine->executeQuery($query, $pager);
|
||||
$view = $engine->renderResults($results, $saved);
|
||||
$panel = $this->newQueryPanel()
|
||||
->setName(pht('Active Revisions'))
|
||||
->setProperty('class', 'DifferentialRevisionSearchEngine')
|
||||
->setProperty('key', 'active');
|
||||
|
||||
$title = pht('Active Revisions');
|
||||
$href = '/differential/query/active/';
|
||||
|
||||
return $this->buildHomepagePanel($title, $href, $view);
|
||||
return $this->renderPanel($panel);
|
||||
}
|
||||
|
||||
private function buildTasksPanel() {
|
||||
$viewer = $this->getViewer();
|
||||
|
||||
$query = 'assigned';
|
||||
$title = pht('Assigned Tasks');
|
||||
$href = '/maniphest/query/assigned/';
|
||||
if (!$viewer->isLoggedIn()) {
|
||||
if ($viewer->isLoggedIn()) {
|
||||
$name = pht('Assigned Tasks');
|
||||
$query = 'assigned';
|
||||
} else {
|
||||
$name = pht('Open Tasks');
|
||||
$query = 'open';
|
||||
$title = pht('Open Tasks');
|
||||
$href = '/maniphest/query/open/';
|
||||
}
|
||||
|
||||
$engine = new ManiphestTaskSearchEngine();
|
||||
$engine->setViewer($viewer);
|
||||
$saved = $engine->buildSavedQueryFromBuiltin($query);
|
||||
$query = $engine->buildQueryFromSavedQuery($saved);
|
||||
$pager = $engine->newPagerForSavedQuery($saved);
|
||||
$pager->setPageSize(15);
|
||||
$results = $engine->executeQuery($query, $pager);
|
||||
$view = $engine->renderResults($results, $saved);
|
||||
$panel = $this->newQueryPanel()
|
||||
->setName($name)
|
||||
->setProperty('class', 'ManiphestTaskSearchEngine')
|
||||
->setProperty('key', $query)
|
||||
->setProperty('limit', 15);
|
||||
|
||||
return $this->buildHomepagePanel($title, $href, $view);
|
||||
return $this->renderPanel($panel);
|
||||
}
|
||||
|
||||
public function buildFeedPanel() {
|
||||
$viewer = $this->getViewer();
|
||||
$panel = $this->newQueryPanel()
|
||||
->setName(pht('Recent Activity'))
|
||||
->setProperty('class', 'PhabricatorFeedSearchEngine')
|
||||
->setProperty('key', 'all')
|
||||
->setProperty('limit', 40);
|
||||
|
||||
$engine = new PhabricatorFeedSearchEngine();
|
||||
$engine->setViewer($viewer);
|
||||
$saved = $engine->buildSavedQueryFromBuiltin('all');
|
||||
$query = $engine->buildQueryFromSavedQuery($saved);
|
||||
$pager = $engine->newPagerForSavedQuery($saved);
|
||||
$pager->setPageSize(40);
|
||||
$results = $engine->executeQuery($query, $pager);
|
||||
$view = $engine->renderResults($results, $saved);
|
||||
// Low tech NUX.
|
||||
if (!$results && ($viewer->getIsAdmin() == 1)) {
|
||||
$instance = PhabricatorEnv::getEnvConfig('cluster.instance');
|
||||
if (!$instance) {
|
||||
$content = pht(<<<EOT
|
||||
Welcome to Phabricator, here are some links to get you started:
|
||||
- [[ /config/ | Configure Phabricator ]]
|
||||
- [[ /guides/ | Quick Start Guide ]]
|
||||
- [[ /diffusion/ | Create a Repository ]]
|
||||
- [[ /people/invite/send/ | Invite People ]]
|
||||
- [[ https://twitter.com/phabricator/ | Follow us on Twitter ]]
|
||||
EOT
|
||||
);
|
||||
} else {
|
||||
$content = pht(<<<EOT
|
||||
Welcome to Phabricator, here are some links to get you started:
|
||||
- [[ /guides/ | Quick Start Guide ]]
|
||||
- [[ /diffusion/ | Create a Repository ]]
|
||||
- [[ https://twitter.com/phabricator/ | Follow us on Twitter ]]
|
||||
EOT
|
||||
);
|
||||
}
|
||||
|
||||
if ($results) {
|
||||
$list = new PHUIObjectItemListView();
|
||||
$view = new PhabricatorApplicationSearchResultView();
|
||||
$view->setObjectList($list);
|
||||
} else {
|
||||
$content = id(new PHUIBoxView())
|
||||
->appendChild(new PHUIRemarkupView($viewer, $content))
|
||||
->addClass('mlt mlb msr msl');
|
||||
$view = new PhabricatorApplicationSearchResultView();
|
||||
$view->setContent($content);
|
||||
}
|
||||
}
|
||||
|
||||
$title = pht('Recent Activity');
|
||||
$href = '/feed/';
|
||||
|
||||
return $this->buildHomepagePanel($title, $href, $view);
|
||||
return $this->renderPanel($panel);
|
||||
}
|
||||
|
||||
public function buildRepositoryPanel() {
|
||||
$panel = $this->newQueryPanel()
|
||||
->setName(pht('Active Repositories'))
|
||||
->setProperty('class', 'PhabricatorRepositorySearchEngine')
|
||||
->setProperty('key', 'active')
|
||||
->setProperty('limit', 5);
|
||||
|
||||
return $this->renderPanel($panel);
|
||||
}
|
||||
|
||||
private function newQueryPanel() {
|
||||
$panel_type = id(new PhabricatorDashboardQueryPanelType())
|
||||
->getPanelTypeKey();
|
||||
|
||||
return id(new PhabricatorDashboardPanel())
|
||||
->setPanelType($panel_type);
|
||||
}
|
||||
|
||||
private function renderPanel(PhabricatorDashboardPanel $panel) {
|
||||
$viewer = $this->getViewer();
|
||||
|
||||
$engine = new PhabricatorRepositorySearchEngine();
|
||||
$engine->setViewer($viewer);
|
||||
$saved = $engine->buildSavedQueryFromBuiltin('active');
|
||||
$query = $engine->buildQueryFromSavedQuery($saved);
|
||||
$pager = $engine->newPagerForSavedQuery($saved);
|
||||
$pager->setPageSize(5);
|
||||
$results = $engine->executeQuery($query, $pager);
|
||||
$view = $engine->renderResults($results, $saved);
|
||||
|
||||
$title = pht('Active Repositories');
|
||||
$href = '/diffusion/';
|
||||
|
||||
return $this->buildHomepagePanel($title, $href, $view);
|
||||
return id(new PhabricatorDashboardPanelRenderingEngine())
|
||||
->setViewer($viewer)
|
||||
->setPanel($panel)
|
||||
->setParentPanelPHIDs(array())
|
||||
->renderPanel();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
final class LegalpadDocumentSignController extends LegalpadController {
|
||||
|
||||
private $isSessionGate;
|
||||
|
||||
public function shouldAllowPublic() {
|
||||
return true;
|
||||
}
|
||||
|
@ -10,6 +12,15 @@ final class LegalpadDocumentSignController extends LegalpadController {
|
|||
return true;
|
||||
}
|
||||
|
||||
public function setIsSessionGate($is_session_gate) {
|
||||
$this->isSessionGate = $is_session_gate;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getIsSessionGate() {
|
||||
return $this->isSessionGate;
|
||||
}
|
||||
|
||||
public function handleRequest(AphrontRequest $request) {
|
||||
$viewer = $request->getUser();
|
||||
|
||||
|
@ -251,8 +262,14 @@ final class LegalpadDocumentSignController extends LegalpadController {
|
|||
$header = id(new PHUIHeaderView())
|
||||
->setHeader($title)
|
||||
->setUser($viewer)
|
||||
->setEpoch($content_updated)
|
||||
->addActionLink(
|
||||
->setEpoch($content_updated);
|
||||
|
||||
// If we're showing the user this document because it's required to use
|
||||
// Phabricator and they haven't signed it, don't show the "Manage" button,
|
||||
// since it won't work.
|
||||
$is_gate = $this->getIsSessionGate();
|
||||
if (!$is_gate) {
|
||||
$header->addActionLink(
|
||||
id(new PHUIButtonView())
|
||||
->setTag('a')
|
||||
->setIcon('fa-pencil')
|
||||
|
@ -260,6 +277,7 @@ final class LegalpadDocumentSignController extends LegalpadController {
|
|||
->setHref($manage_uri)
|
||||
->setDisabled(!$can_edit)
|
||||
->setWorkflow(!$can_edit));
|
||||
}
|
||||
|
||||
$preamble_box = null;
|
||||
if (strlen($document->getPreamble())) {
|
||||
|
|
|
@ -55,11 +55,22 @@ final class LegalpadDocumentRequireSignatureTransaction
|
|||
public function validateTransactions($object, array $xactions) {
|
||||
$errors = array();
|
||||
|
||||
$is_admin = $this->getActor()->getIsAdmin();
|
||||
$old = (bool)$object->getRequireSignature();
|
||||
foreach ($xactions as $xaction) {
|
||||
$new = (bool)$xaction->getNewValue();
|
||||
|
||||
if (!$is_admin) {
|
||||
$errors[] = $this->newInvalidError(
|
||||
pht('Only admins may require signature.'));
|
||||
if ($old === $new) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$is_admin = $this->getActor()->getIsAdmin();
|
||||
if (!$is_admin) {
|
||||
$errors[] = $this->newInvalidError(
|
||||
pht(
|
||||
'Only administrators may change whether a document '.
|
||||
'requires a signature.'),
|
||||
$xaction);
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
|
|
|
@ -30,6 +30,7 @@ final class ManiphestTaskGraphController
|
|||
->setViewer($viewer)
|
||||
->setSeedPHID($task->getPHID())
|
||||
->setLimit($graph_limit)
|
||||
->setIsStandalone(true)
|
||||
->loadGraph();
|
||||
if (!$task_graph->isEmpty()) {
|
||||
$parent_type = ManiphestTaskDependedOnByTaskEdgeType::EDGECONST;
|
||||
|
|
|
@ -123,22 +123,23 @@ information about the move, including an optional specific position within the
|
|||
column.
|
||||
|
||||
The target column should be identified as `columnPHID`, and you may select a
|
||||
position by passing either `beforePHID` or `afterPHID`, specifying the PHID of
|
||||
a task currently in the column that you want to move this task before or after:
|
||||
position by passing either `beforePHIDs` or `afterPHIDs`, specifying the PHIDs
|
||||
of tasks currently in the column that you want to move this task before or
|
||||
after:
|
||||
|
||||
```lang=json
|
||||
[
|
||||
{
|
||||
"columnPHID": "PHID-PCOL-4444",
|
||||
"beforePHID": "PHID-TASK-5555"
|
||||
"beforePHIDs": ["PHID-TASK-5555"]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Note that this affects only the "natural" position of the task. The task
|
||||
position when the board is sorted by some other attribute (like priority)
|
||||
depends on that attribute value: change a task's priority to move it on
|
||||
priority-sorted boards.
|
||||
When you specify multiple PHIDs, the task will be moved adjacent to the first
|
||||
valid PHID found in either of the lists. This allows positional moves to
|
||||
generally work as users expect even if the client view of the board has fallen
|
||||
out of date and some of the nearby tasks have moved elsewhere.
|
||||
EODOCS
|
||||
);
|
||||
|
||||
|
|
|
@ -243,8 +243,7 @@ final class ManiphestTransactionEditor
|
|||
foreach ($projects as $project) {
|
||||
$body->addLinkSection(
|
||||
pht('WORKBOARD'),
|
||||
PhabricatorEnv::getProductionURI(
|
||||
'/project/board/'.$project->getID().'/'));
|
||||
PhabricatorEnv::getProductionURI($project->getWorkboardURI()));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -428,6 +427,7 @@ final class ManiphestTransactionEditor
|
|||
private function buildMoveTransaction(
|
||||
PhabricatorLiskDAO $object,
|
||||
PhabricatorApplicationTransaction $xaction) {
|
||||
$actor = $this->getActor();
|
||||
|
||||
$new = $xaction->getNewValue();
|
||||
if (!is_array($new)) {
|
||||
|
@ -435,7 +435,7 @@ final class ManiphestTransactionEditor
|
|||
$new = array($new);
|
||||
}
|
||||
|
||||
$nearby_phids = array();
|
||||
$relative_phids = array();
|
||||
foreach ($new as $key => $value) {
|
||||
if (!is_array($value)) {
|
||||
$this->validateColumnPHID($value);
|
||||
|
@ -448,35 +448,83 @@ final class ManiphestTransactionEditor
|
|||
$value,
|
||||
array(
|
||||
'columnPHID' => 'string',
|
||||
'beforePHIDs' => 'optional list<string>',
|
||||
'afterPHIDs' => 'optional list<string>',
|
||||
|
||||
// Deprecated older variations of "beforePHIDs" and "afterPHIDs".
|
||||
'beforePHID' => 'optional string',
|
||||
'afterPHID' => 'optional string',
|
||||
));
|
||||
|
||||
$new[$key] = $value;
|
||||
|
||||
if (!empty($value['beforePHID'])) {
|
||||
$nearby_phids[] = $value['beforePHID'];
|
||||
}
|
||||
$value = $value + array(
|
||||
'beforePHIDs' => array(),
|
||||
'afterPHIDs' => array(),
|
||||
);
|
||||
|
||||
// Normalize the legacy keys "beforePHID" and "afterPHID" keys to the
|
||||
// modern format.
|
||||
if (!empty($value['afterPHID'])) {
|
||||
$nearby_phids[] = $value['afterPHID'];
|
||||
if ($value['afterPHIDs']) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Transaction specifies both "afterPHID" and "afterPHIDs". '.
|
||||
'Specify only "afterPHIDs".'));
|
||||
}
|
||||
$value['afterPHIDs'] = array($value['afterPHID']);
|
||||
unset($value['afterPHID']);
|
||||
}
|
||||
|
||||
if (isset($value['beforePHID'])) {
|
||||
if ($value['beforePHIDs']) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Transaction specifies both "beforePHID" and "beforePHIDs". '.
|
||||
'Specify only "beforePHIDs".'));
|
||||
}
|
||||
$value['beforePHIDs'] = array($value['beforePHID']);
|
||||
unset($value['beforePHID']);
|
||||
}
|
||||
|
||||
foreach ($value['beforePHIDs'] as $phid) {
|
||||
$relative_phids[] = $phid;
|
||||
}
|
||||
|
||||
foreach ($value['afterPHIDs'] as $phid) {
|
||||
$relative_phids[] = $phid;
|
||||
}
|
||||
|
||||
$new[$key] = $value;
|
||||
}
|
||||
|
||||
if ($nearby_phids) {
|
||||
$nearby_objects = id(new PhabricatorObjectQuery())
|
||||
->setViewer($this->getActor())
|
||||
->withPHIDs($nearby_phids)
|
||||
// We require that objects you specify in "beforePHIDs" or "afterPHIDs"
|
||||
// are real objects which exist and which you have permission to view.
|
||||
// If you provide other objects, we remove them from the specification.
|
||||
|
||||
if ($relative_phids) {
|
||||
$objects = id(new PhabricatorObjectQuery())
|
||||
->setViewer($actor)
|
||||
->withPHIDs($relative_phids)
|
||||
->execute();
|
||||
$nearby_objects = mpull($nearby_objects, null, 'getPHID');
|
||||
$objects = mpull($objects, null, 'getPHID');
|
||||
} else {
|
||||
$nearby_objects = array();
|
||||
$objects = array();
|
||||
}
|
||||
|
||||
foreach ($new as $key => $value) {
|
||||
$value['afterPHIDs'] = $this->filterValidPHIDs(
|
||||
$value['afterPHIDs'],
|
||||
$objects);
|
||||
$value['beforePHIDs'] = $this->filterValidPHIDs(
|
||||
$value['beforePHIDs'],
|
||||
$objects);
|
||||
|
||||
$new[$key] = $value;
|
||||
}
|
||||
|
||||
$column_phids = ipull($new, 'columnPHID');
|
||||
if ($column_phids) {
|
||||
$columns = id(new PhabricatorProjectColumnQuery())
|
||||
->setViewer($this->getActor())
|
||||
->setViewer($actor)
|
||||
->withPHIDs($column_phids)
|
||||
->execute();
|
||||
$columns = mpull($columns, null, 'getPHID');
|
||||
|
@ -487,10 +535,9 @@ final class ManiphestTransactionEditor
|
|||
$board_phids = mpull($columns, 'getProjectPHID');
|
||||
$object_phid = $object->getPHID();
|
||||
|
||||
$object_phids = $nearby_phids;
|
||||
|
||||
// Note that we may not have an object PHID if we're creating a new
|
||||
// object.
|
||||
$object_phids = array();
|
||||
if ($object_phid) {
|
||||
$object_phids[] = $object_phid;
|
||||
}
|
||||
|
@ -517,49 +564,6 @@ final class ManiphestTransactionEditor
|
|||
|
||||
$board_phid = $column->getProjectPHID();
|
||||
|
||||
$nearby = array();
|
||||
|
||||
if (!empty($spec['beforePHID'])) {
|
||||
$nearby['beforePHID'] = $spec['beforePHID'];
|
||||
}
|
||||
|
||||
if (!empty($spec['afterPHID'])) {
|
||||
$nearby['afterPHID'] = $spec['afterPHID'];
|
||||
}
|
||||
|
||||
if (count($nearby) > 1) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Column move transaction moves object to multiple positions. '.
|
||||
'Specify only "beforePHID" or "afterPHID", not both.'));
|
||||
}
|
||||
|
||||
foreach ($nearby as $where => $nearby_phid) {
|
||||
if (empty($nearby_objects[$nearby_phid])) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Column move transaction specifies object "%s" as "%s", but '.
|
||||
'there is no corresponding object with this PHID.',
|
||||
$object_phid,
|
||||
$where));
|
||||
}
|
||||
|
||||
$nearby_columns = $layout_engine->getObjectColumns(
|
||||
$board_phid,
|
||||
$nearby_phid);
|
||||
$nearby_columns = mpull($nearby_columns, null, 'getPHID');
|
||||
|
||||
if (empty($nearby_columns[$column_phid])) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Column move transaction specifies object "%s" as "%s" in '.
|
||||
'column "%s", but this object is not in that column!',
|
||||
$nearby_phid,
|
||||
$where,
|
||||
$column_phid));
|
||||
}
|
||||
}
|
||||
|
||||
if ($object_phid) {
|
||||
$old_columns = $layout_engine->getObjectColumns(
|
||||
$board_phid,
|
||||
|
@ -578,8 +582,8 @@ final class ManiphestTransactionEditor
|
|||
// We can just drop this column change if it has no effect.
|
||||
$from_map = array_fuse($spec['fromColumnPHIDs']);
|
||||
$already_here = isset($from_map[$column_phid]);
|
||||
$is_reordering = (bool)$nearby;
|
||||
|
||||
$is_reordering = ($spec['afterPHIDs'] || $spec['beforePHIDs']);
|
||||
if ($already_here && !$is_reordering) {
|
||||
unset($new[$key]);
|
||||
} else {
|
||||
|
@ -677,8 +681,9 @@ final class ManiphestTransactionEditor
|
|||
private function applyBoardMove($object, array $move) {
|
||||
$board_phid = $move['boardPHID'];
|
||||
$column_phid = $move['columnPHID'];
|
||||
$before_phid = idx($move, 'beforePHID');
|
||||
$after_phid = idx($move, 'afterPHID');
|
||||
|
||||
$before_phids = $move['beforePHIDs'];
|
||||
$after_phids = $move['afterPHIDs'];
|
||||
|
||||
$object_phid = $object->getPHID();
|
||||
|
||||
|
@ -730,24 +735,12 @@ final class ManiphestTransactionEditor
|
|||
$object_phid);
|
||||
}
|
||||
|
||||
if ($before_phid) {
|
||||
$engine->queueAddPositionBefore(
|
||||
$board_phid,
|
||||
$column_phid,
|
||||
$object_phid,
|
||||
$before_phid);
|
||||
} else if ($after_phid) {
|
||||
$engine->queueAddPositionAfter(
|
||||
$board_phid,
|
||||
$column_phid,
|
||||
$object_phid,
|
||||
$after_phid);
|
||||
} else {
|
||||
$engine->queueAddPosition(
|
||||
$board_phid,
|
||||
$column_phid,
|
||||
$object_phid);
|
||||
}
|
||||
$engine->queueAddPosition(
|
||||
$board_phid,
|
||||
$column_phid,
|
||||
$object_phid,
|
||||
$after_phids,
|
||||
$before_phids);
|
||||
|
||||
$engine->applyPositionUpdates();
|
||||
}
|
||||
|
@ -849,4 +842,16 @@ final class ManiphestTransactionEditor
|
|||
return $errors;
|
||||
}
|
||||
|
||||
private function filterValidPHIDs($phid_list, array $object_map) {
|
||||
foreach ($phid_list as $key => $phid) {
|
||||
if (isset($object_map[$phid])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
unset($phid_list[$key]);
|
||||
}
|
||||
|
||||
return array_values($phid_list);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -47,8 +47,7 @@ final class ManiphestHovercardEngineExtension
|
|||
|
||||
$card = id(new ProjectBoardTaskCard())
|
||||
->setViewer($viewer)
|
||||
->setTask($task)
|
||||
->setCanEdit(false);
|
||||
->setTask($task);
|
||||
|
||||
$owner_phid = $task->getOwnerPHID();
|
||||
if ($owner_phid) {
|
||||
|
|
|
@ -82,17 +82,35 @@ final class ManiphestTaskCoverImageTransaction
|
|||
|
||||
if (!$file) {
|
||||
$errors[] = $this->newInvalidError(
|
||||
pht('"%s" is not a valid file PHID.',
|
||||
$file_phid));
|
||||
} else {
|
||||
if (!$file->isViewableImage()) {
|
||||
$mime_type = $file->getMimeType();
|
||||
$errors[] = $this->newInvalidError(
|
||||
pht('File mime type of "%s" is not a valid viewable image.',
|
||||
$mime_type));
|
||||
}
|
||||
pht(
|
||||
'File PHID ("%s") is invalid, or you do not have permission '.
|
||||
'to view it.',
|
||||
$file_phid),
|
||||
$xaction);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$file->isViewableImage()) {
|
||||
$errors[] = $this->newInvalidError(
|
||||
pht(
|
||||
'File ("%s", with MIME type "%s") is not a viewable image file.',
|
||||
$file_phid,
|
||||
$file->getMimeType()),
|
||||
$xaction);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$file->isTransformableImage()) {
|
||||
$errors[] = $this->newInvalidError(
|
||||
pht(
|
||||
'File ("%s", with MIME type "%s") can not be transformed into '.
|
||||
'a thumbnail. You may be missing support for this file type in '.
|
||||
'the "GD" extension.',
|
||||
$file_phid,
|
||||
$file->getMimeType()),
|
||||
$xaction);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
|
|
|
@ -64,9 +64,27 @@ final class ManiphestTaskTitleTransaction
|
|||
public function validateTransactions($object, array $xactions) {
|
||||
$errors = array();
|
||||
|
||||
if ($this->isEmptyTextTransaction($object->getTitle(), $xactions)) {
|
||||
$errors[] = $this->newRequiredError(
|
||||
pht('Tasks must have a title.'));
|
||||
// If the user is acting via "Bulk Edit" or another workflow which
|
||||
// continues on missing fields, they may be applying a transaction which
|
||||
// removes the task title. Mark these transactions as invalid first,
|
||||
// then flag the missing field error if we don't find any more specific
|
||||
// problems.
|
||||
|
||||
foreach ($xactions as $xaction) {
|
||||
$new = $xaction->getNewValue();
|
||||
if (!strlen($new)) {
|
||||
$errors[] = $this->newInvalidError(
|
||||
pht('Tasks must have a title.'),
|
||||
$xaction);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$errors) {
|
||||
if ($this->isEmptyTextTransaction($object->getTitle(), $xactions)) {
|
||||
$errors[] = $this->newRequiredError(
|
||||
pht('Tasks must have a title.'));
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
|
|
|
@ -175,6 +175,12 @@ final class PhabricatorPeopleProfileViewController
|
|||
return null;
|
||||
}
|
||||
|
||||
// Don't show calendar information for disabled users, since it's probably
|
||||
// not useful or accurate and may be misleading.
|
||||
if ($user->getIsDisabled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$midnight = PhabricatorTime::getTodayMidnightDateTime($viewer);
|
||||
$week_end = clone $midnight;
|
||||
$week_end = $week_end->modify('+3 days');
|
||||
|
|
|
@ -30,6 +30,12 @@ final class PhabricatorUserStatusField
|
|||
$user = $this->getObject();
|
||||
$viewer = $this->requireViewer();
|
||||
|
||||
// Don't show availability for disabled users, since this is vaguely
|
||||
// misleading to say "Availability: Available" and probably not useful.
|
||||
if ($user->getIsDisabled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return id(new PHUIUserAvailabilityView())
|
||||
->setViewer($viewer)
|
||||
->setAvailableUser($user);
|
||||
|
|
|
@ -95,14 +95,17 @@ final class PhabricatorUserCardView extends AphrontTagView {
|
|||
'fa-user-plus',
|
||||
phabricator_date($user->getDateCreated(), $viewer));
|
||||
|
||||
if (PhabricatorApplication::isClassInstalledForViewer(
|
||||
'PhabricatorCalendarApplication',
|
||||
$viewer)) {
|
||||
$body[] = $this->addItem(
|
||||
'fa-calendar-o',
|
||||
id(new PHUIUserAvailabilityView())
|
||||
->setViewer($viewer)
|
||||
->setAvailableUser($user));
|
||||
$has_calendar = PhabricatorApplication::isClassInstalledForViewer(
|
||||
'PhabricatorCalendarApplication',
|
||||
$viewer);
|
||||
if ($has_calendar) {
|
||||
if (!$user->getIsDisabled()) {
|
||||
$body[] = $this->addItem(
|
||||
'fa-calendar-o',
|
||||
id(new PHUIUserAvailabilityView())
|
||||
->setViewer($viewer)
|
||||
->setAvailableUser($user));
|
||||
}
|
||||
}
|
||||
|
||||
$classes[] = 'project-card-image';
|
||||
|
@ -150,8 +153,8 @@ final class PhabricatorUserCardView extends AphrontTagView {
|
|||
'class' => 'project-card-inner',
|
||||
),
|
||||
array(
|
||||
$image,
|
||||
$header,
|
||||
$image,
|
||||
));
|
||||
|
||||
return $card;
|
||||
|
|
|
@ -1008,29 +1008,32 @@ final class PhabricatorProjectCoreTestCase extends PhabricatorTestCase {
|
|||
$task2->getPHID(),
|
||||
$task1->getPHID(),
|
||||
);
|
||||
$this->assertTasksInColumn($expect, $user, $board, $column);
|
||||
$label = pht('Simple move');
|
||||
$this->assertTasksInColumn($expect, $user, $board, $column, $label);
|
||||
|
||||
// Move the second task after the first task.
|
||||
$options = array(
|
||||
'afterPHID' => $task1->getPHID(),
|
||||
'afterPHIDs' => array($task1->getPHID()),
|
||||
);
|
||||
$this->moveToColumn($user, $board, $task2, $column, $column, $options);
|
||||
$expect = array(
|
||||
$task1->getPHID(),
|
||||
$task2->getPHID(),
|
||||
);
|
||||
$this->assertTasksInColumn($expect, $user, $board, $column);
|
||||
$label = pht('With afterPHIDs');
|
||||
$this->assertTasksInColumn($expect, $user, $board, $column, $label);
|
||||
|
||||
// Move the second task before the first task.
|
||||
$options = array(
|
||||
'beforePHID' => $task1->getPHID(),
|
||||
'beforePHIDs' => array($task1->getPHID()),
|
||||
);
|
||||
$this->moveToColumn($user, $board, $task2, $column, $column, $options);
|
||||
$expect = array(
|
||||
$task2->getPHID(),
|
||||
$task1->getPHID(),
|
||||
);
|
||||
$this->assertTasksInColumn($expect, $user, $board, $column);
|
||||
$label = pht('With beforePHIDs');
|
||||
$this->assertTasksInColumn($expect, $user, $board, $column, $label);
|
||||
}
|
||||
|
||||
public function testMilestoneMoves() {
|
||||
|
@ -1333,7 +1336,8 @@ final class PhabricatorProjectCoreTestCase extends PhabricatorTestCase {
|
|||
array $expect,
|
||||
PhabricatorUser $viewer,
|
||||
PhabricatorProject $board,
|
||||
PhabricatorProjectColumn $column) {
|
||||
PhabricatorProjectColumn $column,
|
||||
$label = null) {
|
||||
|
||||
$engine = id(new PhabricatorBoardLayoutEngine())
|
||||
->setViewer($viewer)
|
||||
|
@ -1346,7 +1350,7 @@ final class PhabricatorProjectCoreTestCase extends PhabricatorTestCase {
|
|||
$column->getPHID());
|
||||
$object_phids = array_values($object_phids);
|
||||
|
||||
$this->assertEqual($expect, $object_phids);
|
||||
$this->assertEqual($expect, $object_phids, $label);
|
||||
}
|
||||
|
||||
private function addColumn(
|
||||
|
|
|
@ -89,6 +89,18 @@ final class PhabricatorProjectApplication extends PhabricatorApplication {
|
|||
'background/'
|
||||
=> 'PhabricatorProjectBoardBackgroundController',
|
||||
),
|
||||
'column/' => array(
|
||||
'remove/(?P<id>\d+)/' =>
|
||||
'PhabricatorProjectColumnRemoveTriggerController',
|
||||
),
|
||||
'trigger/' => array(
|
||||
$this->getQueryRoutePattern() =>
|
||||
'PhabricatorProjectTriggerListController',
|
||||
'(?P<id>[1-9]\d*)/' =>
|
||||
'PhabricatorProjectTriggerViewController',
|
||||
$this->getEditRoutePattern('edit/') =>
|
||||
'PhabricatorProjectTriggerEditController',
|
||||
),
|
||||
'update/(?P<id>[1-9]\d*)/(?P<action>[^/]+)/'
|
||||
=> 'PhabricatorProjectUpdateController',
|
||||
'manage/(?P<id>[1-9]\d*)/' => 'PhabricatorProjectManageController',
|
||||
|
|
|
@ -55,7 +55,7 @@ final class PhabricatorProjectBoardBackgroundController
|
|||
$nav = $this->getProfileMenu();
|
||||
|
||||
$crumbs = id($this->buildApplicationCrumbs())
|
||||
->addTextCrumb(pht('Workboard'), "/project/board/{$board_id}/")
|
||||
->addTextCrumb(pht('Workboard'), $board->getWorkboardURI())
|
||||
->addTextCrumb(pht('Manage'), $manage_uri)
|
||||
->addTextCrumb(pht('Background Color'));
|
||||
|
||||
|
|
|
@ -34,7 +34,7 @@ final class PhabricatorProjectBoardManageController
|
|||
$curtain = $this->buildCurtainView($board);
|
||||
|
||||
$crumbs = $this->buildApplicationCrumbs();
|
||||
$crumbs->addTextCrumb(pht('Workboard'), "/project/board/{$board_id}/");
|
||||
$crumbs->addTextCrumb(pht('Workboard'), $board->getWorkboardURI());
|
||||
$crumbs->addTextCrumb(pht('Manage'));
|
||||
$crumbs->setBorder(true);
|
||||
|
||||
|
|
|
@ -540,8 +540,9 @@ final class PhabricatorProjectBoardViewController
|
|||
->setExcludedProjectPHIDs($select_phids);
|
||||
|
||||
$templates = array();
|
||||
$column_maps = array();
|
||||
$all_tasks = array();
|
||||
$column_templates = array();
|
||||
$sounds = array();
|
||||
foreach ($visible_columns as $column_phid => $column) {
|
||||
$column_tasks = $column_phids[$column_phid];
|
||||
|
||||
|
@ -574,6 +575,11 @@ final class PhabricatorProjectBoardViewController
|
|||
$column_menu = $this->buildColumnMenu($project, $column);
|
||||
$panel->addHeaderAction($column_menu);
|
||||
|
||||
if ($column->canHaveTrigger()) {
|
||||
$trigger_menu = $this->buildTriggerMenu($column);
|
||||
$panel->addHeaderAction($trigger_menu);
|
||||
}
|
||||
|
||||
$count_tag = id(new PHUITagView())
|
||||
->setType(PHUITagView::TYPE_SHADE)
|
||||
->setColor(PHUITagView::COLOR_BLUE)
|
||||
|
@ -601,18 +607,42 @@ final class PhabricatorProjectBoardViewController
|
|||
'pointLimit' => $column->getPointLimit(),
|
||||
));
|
||||
|
||||
$card_phids = array();
|
||||
foreach ($column_tasks as $task) {
|
||||
$object_phid = $task->getPHID();
|
||||
|
||||
$card = $rendering_engine->renderCard($object_phid);
|
||||
$templates[$object_phid] = hsprintf('%s', $card->getItem());
|
||||
$column_maps[$column_phid][] = $object_phid;
|
||||
$card_phids[] = $object_phid;
|
||||
|
||||
$all_tasks[$object_phid] = $task;
|
||||
}
|
||||
|
||||
$panel->setCards($cards);
|
||||
$board->addPanel($panel);
|
||||
|
||||
$drop_effects = $column->getDropEffects();
|
||||
$drop_effects = mpull($drop_effects, 'toDictionary');
|
||||
|
||||
$preview_effect = null;
|
||||
if ($column->canHaveTrigger()) {
|
||||
$trigger = $column->getTrigger();
|
||||
if ($trigger) {
|
||||
$preview_effect = $trigger->getPreviewEffect()
|
||||
->toDictionary();
|
||||
|
||||
foreach ($trigger->getSoundEffects() as $sound) {
|
||||
$sounds[] = $sound;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$column_templates[] = array(
|
||||
'columnPHID' => $column_phid,
|
||||
'effects' => $drop_effects,
|
||||
'cardPHIDs' => $card_phids,
|
||||
'triggerPreviewEffect' => $preview_effect,
|
||||
);
|
||||
}
|
||||
|
||||
$order_key = $this->sortKey;
|
||||
|
@ -637,10 +667,8 @@ final class PhabricatorProjectBoardViewController
|
|||
|
||||
$properties = array();
|
||||
foreach ($all_tasks as $task) {
|
||||
$properties[$task->getPHID()] = array(
|
||||
'points' => (double)$task->getPoints(),
|
||||
'status' => $task->getStatus(),
|
||||
);
|
||||
$properties[$task->getPHID()] =
|
||||
PhabricatorBoardResponseEngine::newTaskProperties($task);
|
||||
}
|
||||
|
||||
$behavior_config = array(
|
||||
|
@ -656,12 +684,13 @@ final class PhabricatorProjectBoardViewController
|
|||
'headers' => $headers,
|
||||
'headerKeys' => $header_keys,
|
||||
'templateMap' => $templates,
|
||||
'columnMaps' => $column_maps,
|
||||
'orderMaps' => $vector_map,
|
||||
'propertyMaps' => $properties,
|
||||
'columnTemplates' => $column_templates,
|
||||
|
||||
'boardID' => $board_id,
|
||||
'projectPHID' => $project->getPHID(),
|
||||
'preloadSounds' => $sounds,
|
||||
);
|
||||
$this->initBehavior('project-boards', $behavior_config);
|
||||
|
||||
|
@ -697,7 +726,7 @@ final class PhabricatorProjectBoardViewController
|
|||
->setType(PHUIListItemView::TYPE_DIVIDER);
|
||||
$fullscreen = $this->buildFullscreenMenu();
|
||||
|
||||
$crumbs = $this->buildApplicationCrumbs();
|
||||
$crumbs = $this->newWorkboardCrumbs();
|
||||
$crumbs->addTextCrumb(pht('Workboard'));
|
||||
$crumbs->setBorder(true);
|
||||
|
||||
|
@ -1111,10 +1140,8 @@ final class PhabricatorProjectBoardViewController
|
|||
));
|
||||
}
|
||||
|
||||
if (count($specs) > 1) {
|
||||
$column_items[] = id(new PhabricatorActionView())
|
||||
->setType(PhabricatorActionView::TYPE_DIVIDER);
|
||||
}
|
||||
$column_items[] = id(new PhabricatorActionView())
|
||||
->setType(PhabricatorActionView::TYPE_DIVIDER);
|
||||
|
||||
$batch_edit_uri = $request->getRequestURI();
|
||||
$batch_edit_uri->replaceQueryParam('batch', $column->getID());
|
||||
|
@ -1181,7 +1208,7 @@ final class PhabricatorProjectBoardViewController
|
|||
}
|
||||
|
||||
$column_button = id(new PHUIIconView())
|
||||
->setIcon('fa-caret-down')
|
||||
->setIcon('fa-pencil')
|
||||
->setHref('#')
|
||||
->addSigil('boards-dropdown-menu')
|
||||
->setMetadata(
|
||||
|
@ -1192,6 +1219,75 @@ final class PhabricatorProjectBoardViewController
|
|||
return $column_button;
|
||||
}
|
||||
|
||||
private function buildTriggerMenu(PhabricatorProjectColumn $column) {
|
||||
$viewer = $this->getViewer();
|
||||
$trigger = $column->getTrigger();
|
||||
|
||||
$can_edit = PhabricatorPolicyFilter::hasCapability(
|
||||
$viewer,
|
||||
$column,
|
||||
PhabricatorPolicyCapability::CAN_EDIT);
|
||||
|
||||
$trigger_items = array();
|
||||
if (!$trigger) {
|
||||
$set_uri = $this->getApplicationURI(
|
||||
new PhutilURI(
|
||||
'trigger/edit/',
|
||||
array(
|
||||
'columnPHID' => $column->getPHID(),
|
||||
)));
|
||||
|
||||
$trigger_items[] = id(new PhabricatorActionView())
|
||||
->setIcon('fa-cogs')
|
||||
->setName(pht('New Trigger...'))
|
||||
->setHref($set_uri)
|
||||
->setDisabled(!$can_edit);
|
||||
} else {
|
||||
$trigger_items[] = id(new PhabricatorActionView())
|
||||
->setIcon('fa-cogs')
|
||||
->setName(pht('View Trigger'))
|
||||
->setHref($trigger->getURI())
|
||||
->setDisabled(!$can_edit);
|
||||
}
|
||||
|
||||
$remove_uri = $this->getApplicationURI(
|
||||
new PhutilURI(
|
||||
urisprintf(
|
||||
'column/remove/%d/',
|
||||
$column->getID())));
|
||||
|
||||
$trigger_items[] = id(new PhabricatorActionView())
|
||||
->setIcon('fa-times')
|
||||
->setName(pht('Remove Trigger'))
|
||||
->setHref($remove_uri)
|
||||
->setWorkflow(true)
|
||||
->setDisabled(!$can_edit || !$trigger);
|
||||
|
||||
$trigger_menu = id(new PhabricatorActionListView())
|
||||
->setUser($viewer);
|
||||
foreach ($trigger_items as $item) {
|
||||
$trigger_menu->addAction($item);
|
||||
}
|
||||
|
||||
if ($trigger) {
|
||||
$trigger_icon = 'fa-cogs';
|
||||
} else {
|
||||
$trigger_icon = 'fa-cogs grey';
|
||||
}
|
||||
|
||||
$trigger_button = id(new PHUIIconView())
|
||||
->setIcon($trigger_icon)
|
||||
->setHref('#')
|
||||
->addSigil('boards-dropdown-menu')
|
||||
->addSigil('trigger-preview')
|
||||
->setMetadata(
|
||||
array(
|
||||
'items' => hsprintf('%s', $trigger_menu),
|
||||
'columnPHID' => $column->getPHID(),
|
||||
));
|
||||
|
||||
return $trigger_button;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add current state parameters (like order and the visibility of hidden
|
||||
|
|
|
@ -47,7 +47,7 @@ final class PhabricatorProjectColumnDetailController
|
|||
$properties = $this->buildPropertyView($column);
|
||||
|
||||
$crumbs = $this->buildApplicationCrumbs();
|
||||
$crumbs->addTextCrumb(pht('Workboard'), "/project/board/{$project_id}/");
|
||||
$crumbs->addTextCrumb(pht('Workboard'), $project->getWorkboardURI());
|
||||
$crumbs->addTextCrumb(pht('Column: %s', $title));
|
||||
$crumbs->setBorder(true);
|
||||
|
||||
|
|
|
@ -50,8 +50,7 @@ final class PhabricatorProjectColumnEditController
|
|||
$v_name = $column->getName();
|
||||
|
||||
$validation_exception = null;
|
||||
$base_uri = '/board/'.$project_id.'/';
|
||||
$view_uri = $this->getApplicationURI($base_uri);
|
||||
$view_uri = $project->getWorkboardURI();
|
||||
|
||||
if ($request->isFormPost()) {
|
||||
$v_name = $request->getStr('name');
|
||||
|
@ -76,8 +75,8 @@ final class PhabricatorProjectColumnEditController
|
|||
|
||||
$xactions = array();
|
||||
|
||||
$type_name = PhabricatorProjectColumnTransaction::TYPE_NAME;
|
||||
$type_limit = PhabricatorProjectColumnTransaction::TYPE_LIMIT;
|
||||
$type_name = PhabricatorProjectColumnNameTransaction::TRANSACTIONTYPE;
|
||||
$type_limit = PhabricatorProjectColumnLimitTransaction::TRANSACTIONTYPE;
|
||||
|
||||
if (!$column->getProxy()) {
|
||||
$xactions[] = id(new PhabricatorProjectColumnTransaction())
|
||||
|
@ -93,7 +92,6 @@ final class PhabricatorProjectColumnEditController
|
|||
$editor = id(new PhabricatorProjectColumnTransactionEditor())
|
||||
->setActor($viewer)
|
||||
->setContinueOnNoEffect(true)
|
||||
->setContinueOnMissingFields(true)
|
||||
->setContentSourceFromRequest($request)
|
||||
->applyTransactions($column, $xactions);
|
||||
return id(new AphrontRedirectResponse())->setURI($view_uri);
|
||||
|
|
|
@ -38,7 +38,7 @@ final class PhabricatorProjectColumnHideController
|
|||
|
||||
$column_phid = $column->getPHID();
|
||||
|
||||
$view_uri = $this->getApplicationURI('/board/'.$project_id.'/');
|
||||
$view_uri = $project->getWorkboardURI();
|
||||
$view_uri = new PhutilURI($view_uri);
|
||||
foreach ($request->getPassthroughRequestData() as $key => $value) {
|
||||
$view_uri->replaceQueryParam($key, $value);
|
||||
|
@ -82,7 +82,9 @@ final class PhabricatorProjectColumnHideController
|
|||
$new_status = PhabricatorProjectColumn::STATUS_HIDDEN;
|
||||
}
|
||||
|
||||
$type_status = PhabricatorProjectColumnTransaction::TYPE_STATUS;
|
||||
$type_status =
|
||||
PhabricatorProjectColumnStatusTransaction::TRANSACTIONTYPE;
|
||||
|
||||
$xactions = array(
|
||||
id(new PhabricatorProjectColumnTransaction())
|
||||
->setTransactionType($type_status)
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorProjectColumnRemoveTriggerController
|
||||
extends PhabricatorProjectBoardController {
|
||||
|
||||
public function handleRequest(AphrontRequest $request) {
|
||||
$viewer = $request->getViewer();
|
||||
$id = $request->getURIData('id');
|
||||
|
||||
$column = id(new PhabricatorProjectColumnQuery())
|
||||
->setViewer($viewer)
|
||||
->withIDs(array($id))
|
||||
->requireCapabilities(
|
||||
array(
|
||||
PhabricatorPolicyCapability::CAN_VIEW,
|
||||
PhabricatorPolicyCapability::CAN_EDIT,
|
||||
))
|
||||
->executeOne();
|
||||
if (!$column) {
|
||||
return new Aphront404Response();
|
||||
}
|
||||
|
||||
$done_uri = $column->getWorkboardURI();
|
||||
|
||||
if (!$column->getTriggerPHID()) {
|
||||
return $this->newDialog()
|
||||
->setTitle(pht('No Trigger'))
|
||||
->appendParagraph(
|
||||
pht('This column does not have a trigger.'))
|
||||
->addCancelButton($done_uri);
|
||||
}
|
||||
|
||||
if ($request->isFormPost()) {
|
||||
$column_xactions = array();
|
||||
|
||||
$column_xactions[] = $column->getApplicationTransactionTemplate()
|
||||
->setTransactionType(
|
||||
PhabricatorProjectColumnTriggerTransaction::TRANSACTIONTYPE)
|
||||
->setNewValue(null);
|
||||
|
||||
$column_editor = $column->getApplicationTransactionEditor()
|
||||
->setActor($viewer)
|
||||
->setContentSourceFromRequest($request)
|
||||
->setContinueOnNoEffect(true)
|
||||
->setContinueOnMissingFields(true);
|
||||
|
||||
$column_editor->applyTransactions($column, $column_xactions);
|
||||
|
||||
return id(new AphrontRedirectResponse())->setURI($done_uri);
|
||||
}
|
||||
|
||||
$body = pht('Really remove the trigger from this column?');
|
||||
|
||||
return $this->newDialog()
|
||||
->setTitle(pht('Remove Trigger'))
|
||||
->appendParagraph($body)
|
||||
->addSubmitButton(pht('Remove Trigger'))
|
||||
->addCancelButton($done_uri);
|
||||
}
|
||||
}
|
|
@ -109,6 +109,14 @@ abstract class PhabricatorProjectController extends PhabricatorController {
|
|||
}
|
||||
|
||||
protected function buildApplicationCrumbs() {
|
||||
return $this->newApplicationCrumbs('profile');
|
||||
}
|
||||
|
||||
protected function newWorkboardCrumbs() {
|
||||
return $this->newApplicationCrumbs('workboard');
|
||||
}
|
||||
|
||||
private function newApplicationCrumbs($mode) {
|
||||
$crumbs = parent::buildApplicationCrumbs();
|
||||
|
||||
$project = $this->getProject();
|
||||
|
@ -117,10 +125,24 @@ abstract class PhabricatorProjectController extends PhabricatorController {
|
|||
$ancestors = array_reverse($ancestors);
|
||||
$ancestors[] = $project;
|
||||
foreach ($ancestors as $ancestor) {
|
||||
$crumbs->addTextCrumb(
|
||||
$ancestor->getName(),
|
||||
$ancestor->getProfileURI()
|
||||
);
|
||||
if ($ancestor->getPHID() === $project->getPHID()) {
|
||||
// Link the current project's crumb to its profile no matter what,
|
||||
// since we're already on the right context page for it and linking
|
||||
// to the current page isn't helpful.
|
||||
$crumb_uri = $ancestor->getProfileURI();
|
||||
} else {
|
||||
switch ($mode) {
|
||||
case 'workboard':
|
||||
$crumb_uri = $ancestor->getWorkboardURI();
|
||||
break;
|
||||
case 'profile':
|
||||
default:
|
||||
$crumb_uri = $ancestor->getProfileURI();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$crumbs->addTextCrumb($ancestor->getName(), $crumb_uri);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -152,7 +174,8 @@ abstract class PhabricatorProjectController extends PhabricatorController {
|
|||
protected function newCardResponse(
|
||||
$board_phid,
|
||||
$object_phid,
|
||||
PhabricatorProjectColumnOrder $ordering = null) {
|
||||
PhabricatorProjectColumnOrder $ordering = null,
|
||||
$sounds = array()) {
|
||||
|
||||
$viewer = $this->getViewer();
|
||||
|
||||
|
@ -166,7 +189,8 @@ abstract class PhabricatorProjectController extends PhabricatorController {
|
|||
->setViewer($viewer)
|
||||
->setBoardPHID($board_phid)
|
||||
->setObjectPHID($object_phid)
|
||||
->setVisiblePHIDs($visible_phids);
|
||||
->setVisiblePHIDs($visible_phids)
|
||||
->setSounds($sounds);
|
||||
|
||||
if ($ordering) {
|
||||
$engine->setOrdering($ordering);
|
||||
|
|
|
@ -11,8 +11,9 @@ final class PhabricatorProjectMoveController
|
|||
|
||||
$column_phid = $request->getStr('columnPHID');
|
||||
$object_phid = $request->getStr('objectPHID');
|
||||
$after_phid = $request->getStr('afterPHID');
|
||||
$before_phid = $request->getStr('beforePHID');
|
||||
|
||||
$after_phids = $request->getStrList('afterPHIDs');
|
||||
$before_phids = $request->getStrList('beforePHIDs');
|
||||
|
||||
$order = $request->getStr('order');
|
||||
if (!strlen($order)) {
|
||||
|
@ -70,6 +71,7 @@ final class PhabricatorProjectMoveController
|
|||
$columns = id(new PhabricatorProjectColumnQuery())
|
||||
->setViewer($viewer)
|
||||
->withProjectPHIDs(array($project->getPHID()))
|
||||
->needTriggers(true)
|
||||
->execute();
|
||||
|
||||
$columns = mpull($columns, null, 'getPHID');
|
||||
|
@ -86,12 +88,10 @@ final class PhabricatorProjectMoveController
|
|||
->setObjectPHIDs(array($object_phid))
|
||||
->executeLayout();
|
||||
|
||||
$order_params = array();
|
||||
if ($after_phid) {
|
||||
$order_params['afterPHID'] = $after_phid;
|
||||
} else if ($before_phid) {
|
||||
$order_params['beforePHID'] = $before_phid;
|
||||
}
|
||||
$order_params = array(
|
||||
'afterPHIDs' => $after_phids,
|
||||
'beforePHIDs' => $before_phids,
|
||||
);
|
||||
|
||||
$xactions = array();
|
||||
$xactions[] = id(new ManiphestTransaction())
|
||||
|
@ -110,6 +110,24 @@ final class PhabricatorProjectMoveController
|
|||
$xactions[] = $header_xaction;
|
||||
}
|
||||
|
||||
$sounds = array();
|
||||
if ($column->canHaveTrigger()) {
|
||||
$trigger = $column->getTrigger();
|
||||
if ($trigger) {
|
||||
$trigger_xactions = $trigger->newDropTransactions(
|
||||
$viewer,
|
||||
$column,
|
||||
$object);
|
||||
foreach ($trigger_xactions as $trigger_xaction) {
|
||||
$xactions[] = $trigger_xaction;
|
||||
}
|
||||
|
||||
foreach ($trigger->getSoundEffects() as $effect) {
|
||||
$sounds[] = $effect;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$editor = id(new ManiphestTransactionEditor())
|
||||
->setActor($viewer)
|
||||
->setContinueOnMissingFields(true)
|
||||
|
@ -119,7 +137,11 @@ final class PhabricatorProjectMoveController
|
|||
|
||||
$editor->applyTransactions($object, $xactions);
|
||||
|
||||
return $this->newCardResponse($board_phid, $object_phid, $ordering);
|
||||
return $this->newCardResponse(
|
||||
$board_phid,
|
||||
$object_phid,
|
||||
$ordering,
|
||||
$sounds);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
abstract class PhabricatorProjectTriggerController
|
||||
extends PhabricatorProjectController {
|
||||
|
||||
protected function buildApplicationCrumbs() {
|
||||
$crumbs = parent::buildApplicationCrumbs();
|
||||
|
||||
$crumbs->addTextCrumb(
|
||||
pht('Triggers'),
|
||||
$this->getApplicationURI('trigger/'));
|
||||
|
||||
return $crumbs;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,293 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorProjectTriggerEditController
|
||||
extends PhabricatorProjectTriggerController {
|
||||
|
||||
public function handleRequest(AphrontRequest $request) {
|
||||
$request = $this->getRequest();
|
||||
$viewer = $request->getViewer();
|
||||
|
||||
$id = $request->getURIData('id');
|
||||
if ($id) {
|
||||
$trigger = id(new PhabricatorProjectTriggerQuery())
|
||||
->setViewer($viewer)
|
||||
->withIDs(array($id))
|
||||
->requireCapabilities(
|
||||
array(
|
||||
PhabricatorPolicyCapability::CAN_VIEW,
|
||||
PhabricatorPolicyCapability::CAN_EDIT,
|
||||
))
|
||||
->executeOne();
|
||||
if (!$trigger) {
|
||||
return new Aphront404Response();
|
||||
}
|
||||
} else {
|
||||
$trigger = PhabricatorProjectTrigger::initializeNewTrigger();
|
||||
}
|
||||
|
||||
$column_phid = $request->getStr('columnPHID');
|
||||
if ($column_phid) {
|
||||
$column = id(new PhabricatorProjectColumnQuery())
|
||||
->setViewer($viewer)
|
||||
->withPHIDs(array($column_phid))
|
||||
->requireCapabilities(
|
||||
array(
|
||||
PhabricatorPolicyCapability::CAN_VIEW,
|
||||
PhabricatorPolicyCapability::CAN_EDIT,
|
||||
))
|
||||
->executeOne();
|
||||
if (!$column) {
|
||||
return new Aphront404Response();
|
||||
}
|
||||
$board_uri = $column->getWorkboardURI();
|
||||
} else {
|
||||
$column = null;
|
||||
$board_uri = null;
|
||||
}
|
||||
|
||||
if ($board_uri) {
|
||||
$cancel_uri = $board_uri;
|
||||
} else if ($trigger->getID()) {
|
||||
$cancel_uri = $trigger->getURI();
|
||||
} else {
|
||||
$cancel_uri = $this->getApplicationURI('trigger/');
|
||||
}
|
||||
|
||||
$v_name = $trigger->getName();
|
||||
$v_edit = $trigger->getEditPolicy();
|
||||
$v_rules = $trigger->getTriggerRules();
|
||||
|
||||
$e_name = null;
|
||||
$e_edit = null;
|
||||
|
||||
$validation_exception = null;
|
||||
if ($request->isFormPost()) {
|
||||
try {
|
||||
$v_name = $request->getStr('name');
|
||||
$v_edit = $request->getStr('editPolicy');
|
||||
|
||||
// Read the JSON rules from the request and convert them back into
|
||||
// "TriggerRule" objects so we can render the correct form state
|
||||
// if the user is modifying the rules
|
||||
$raw_rules = $request->getStr('rules');
|
||||
$raw_rules = phutil_json_decode($raw_rules);
|
||||
|
||||
$copy = clone $trigger;
|
||||
$copy->setRuleset($raw_rules);
|
||||
$v_rules = $copy->getTriggerRules();
|
||||
|
||||
$xactions = array();
|
||||
if (!$trigger->getID()) {
|
||||
$xactions[] = $trigger->getApplicationTransactionTemplate()
|
||||
->setTransactionType(PhabricatorTransactions::TYPE_CREATE)
|
||||
->setNewValue(true);
|
||||
}
|
||||
|
||||
$xactions[] = $trigger->getApplicationTransactionTemplate()
|
||||
->setTransactionType(
|
||||
PhabricatorProjectTriggerNameTransaction::TRANSACTIONTYPE)
|
||||
->setNewValue($v_name);
|
||||
|
||||
$xactions[] = $trigger->getApplicationTransactionTemplate()
|
||||
->setTransactionType(PhabricatorTransactions::TYPE_EDIT_POLICY)
|
||||
->setNewValue($v_edit);
|
||||
|
||||
$xactions[] = $trigger->getApplicationTransactionTemplate()
|
||||
->setTransactionType(
|
||||
PhabricatorProjectTriggerRulesetTransaction::TRANSACTIONTYPE)
|
||||
->setNewValue($raw_rules);
|
||||
|
||||
$editor = $trigger->getApplicationTransactionEditor()
|
||||
->setActor($viewer)
|
||||
->setContentSourceFromRequest($request)
|
||||
->setContinueOnNoEffect(true);
|
||||
|
||||
$editor->applyTransactions($trigger, $xactions);
|
||||
|
||||
$next_uri = $trigger->getURI();
|
||||
|
||||
if ($column) {
|
||||
$column_xactions = array();
|
||||
|
||||
$column_xactions[] = $column->getApplicationTransactionTemplate()
|
||||
->setTransactionType(
|
||||
PhabricatorProjectColumnTriggerTransaction::TRANSACTIONTYPE)
|
||||
->setNewValue($trigger->getPHID());
|
||||
|
||||
$column_editor = $column->getApplicationTransactionEditor()
|
||||
->setActor($viewer)
|
||||
->setContentSourceFromRequest($request)
|
||||
->setContinueOnNoEffect(true)
|
||||
->setContinueOnMissingFields(true);
|
||||
|
||||
$column_editor->applyTransactions($column, $column_xactions);
|
||||
|
||||
$next_uri = $column->getWorkboardURI();
|
||||
}
|
||||
|
||||
return id(new AphrontRedirectResponse())->setURI($next_uri);
|
||||
} catch (PhabricatorApplicationTransactionValidationException $ex) {
|
||||
$validation_exception = $ex;
|
||||
|
||||
$e_name = $ex->getShortMessage(
|
||||
PhabricatorProjectTriggerNameTransaction::TRANSACTIONTYPE);
|
||||
|
||||
$e_edit = $ex->getShortMessage(
|
||||
PhabricatorTransactions::TYPE_EDIT_POLICY);
|
||||
|
||||
$trigger->setEditPolicy($v_edit);
|
||||
}
|
||||
}
|
||||
|
||||
if ($trigger->getID()) {
|
||||
$title = $trigger->getObjectName();
|
||||
$submit = pht('Save Trigger');
|
||||
$header = pht('Edit Trigger: %s', $trigger->getObjectName());
|
||||
} else {
|
||||
$title = pht('New Trigger');
|
||||
$submit = pht('Create Trigger');
|
||||
$header = pht('New Trigger');
|
||||
}
|
||||
|
||||
$form_id = celerity_generate_unique_node_id();
|
||||
$table_id = celerity_generate_unique_node_id();
|
||||
$create_id = celerity_generate_unique_node_id();
|
||||
$input_id = celerity_generate_unique_node_id();
|
||||
|
||||
$form = id(new AphrontFormView())
|
||||
->setViewer($viewer)
|
||||
->setID($form_id);
|
||||
|
||||
if ($column) {
|
||||
$form->addHiddenInput('columnPHID', $column->getPHID());
|
||||
}
|
||||
|
||||
$form->appendControl(
|
||||
id(new AphrontFormTextControl())
|
||||
->setLabel(pht('Name'))
|
||||
->setName('name')
|
||||
->setValue($v_name)
|
||||
->setError($e_name)
|
||||
->setPlaceholder($trigger->getDefaultName()));
|
||||
|
||||
$policies = id(new PhabricatorPolicyQuery())
|
||||
->setViewer($viewer)
|
||||
->setObject($trigger)
|
||||
->execute();
|
||||
|
||||
$form->appendControl(
|
||||
id(new AphrontFormPolicyControl())
|
||||
->setName('editPolicy')
|
||||
->setPolicyObject($trigger)
|
||||
->setCapability(PhabricatorPolicyCapability::CAN_EDIT)
|
||||
->setPolicies($policies)
|
||||
->setError($e_edit));
|
||||
|
||||
$form->appendChild(
|
||||
phutil_tag(
|
||||
'input',
|
||||
array(
|
||||
'type' => 'hidden',
|
||||
'name' => 'rules',
|
||||
'id' => $input_id,
|
||||
)));
|
||||
|
||||
$form->appendChild(
|
||||
id(new PHUIFormInsetView())
|
||||
->setTitle(pht('Rules'))
|
||||
->setDescription(
|
||||
pht(
|
||||
'When a card is dropped into a column which uses this trigger:'))
|
||||
->setRightButton(
|
||||
javelin_tag(
|
||||
'a',
|
||||
array(
|
||||
'href' => '#',
|
||||
'class' => 'button button-green',
|
||||
'id' => $create_id,
|
||||
'mustcapture' => true,
|
||||
),
|
||||
pht('New Rule')))
|
||||
->setContent(
|
||||
javelin_tag(
|
||||
'table',
|
||||
array(
|
||||
'id' => $table_id,
|
||||
'class' => 'trigger-rules-table',
|
||||
))));
|
||||
|
||||
$this->setupEditorBehavior(
|
||||
$trigger,
|
||||
$v_rules,
|
||||
$form_id,
|
||||
$table_id,
|
||||
$create_id,
|
||||
$input_id);
|
||||
|
||||
$form->appendControl(
|
||||
id(new AphrontFormSubmitControl())
|
||||
->setValue($submit)
|
||||
->addCancelButton($cancel_uri));
|
||||
|
||||
$header = id(new PHUIHeaderView())
|
||||
->setHeader($header);
|
||||
|
||||
$box_view = id(new PHUIObjectBoxView())
|
||||
->setHeader($header)
|
||||
->setValidationException($validation_exception)
|
||||
->appendChild($form);
|
||||
|
||||
$column_view = id(new PHUITwoColumnView())
|
||||
->setFooter($box_view);
|
||||
|
||||
$crumbs = $this->buildApplicationCrumbs()
|
||||
->setBorder(true);
|
||||
|
||||
if ($column) {
|
||||
$crumbs->addTextCrumb(
|
||||
pht(
|
||||
'%s: %s',
|
||||
$column->getProject()->getDisplayName(),
|
||||
$column->getName()),
|
||||
$board_uri);
|
||||
}
|
||||
|
||||
$crumbs->addTextCrumb($title);
|
||||
|
||||
return $this->newPage()
|
||||
->setTitle($title)
|
||||
->setCrumbs($crumbs)
|
||||
->appendChild($column_view);
|
||||
}
|
||||
|
||||
private function setupEditorBehavior(
|
||||
PhabricatorProjectTrigger $trigger,
|
||||
array $rule_list,
|
||||
$form_id,
|
||||
$table_id,
|
||||
$create_id,
|
||||
$input_id) {
|
||||
|
||||
$rule_list = mpull($rule_list, 'toDictionary');
|
||||
$rule_list = array_values($rule_list);
|
||||
|
||||
$type_list = PhabricatorProjectTriggerRule::getAllTriggerRules();
|
||||
$type_list = mpull($type_list, 'newTemplate');
|
||||
$type_list = array_values($type_list);
|
||||
|
||||
require_celerity_resource('project-triggers-css');
|
||||
|
||||
Javelin::initBehavior(
|
||||
'trigger-rule-editor',
|
||||
array(
|
||||
'formNodeID' => $form_id,
|
||||
'tableNodeID' => $table_id,
|
||||
'createNodeID' => $create_id,
|
||||
'inputNodeID' => $input_id,
|
||||
|
||||
'rules' => $rule_list,
|
||||
'types' => $type_list,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorProjectTriggerListController
|
||||
extends PhabricatorProjectTriggerController {
|
||||
|
||||
public function shouldAllowPublic() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function handleRequest(AphrontRequest $request) {
|
||||
return id(new PhabricatorProjectTriggerSearchEngine())
|
||||
->setController($this)
|
||||
->buildResponse();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,231 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorProjectTriggerViewController
|
||||
extends PhabricatorProjectTriggerController {
|
||||
|
||||
public function shouldAllowPublic() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function handleRequest(AphrontRequest $request) {
|
||||
$request = $this->getRequest();
|
||||
$viewer = $request->getViewer();
|
||||
|
||||
$id = $request->getURIData('id');
|
||||
|
||||
$trigger = id(new PhabricatorProjectTriggerQuery())
|
||||
->setViewer($viewer)
|
||||
->withIDs(array($id))
|
||||
->executeOne();
|
||||
if (!$trigger) {
|
||||
return new Aphront404Response();
|
||||
}
|
||||
|
||||
$rules_view = $this->newRulesView($trigger);
|
||||
$columns_view = $this->newColumnsView($trigger);
|
||||
|
||||
$title = $trigger->getObjectName();
|
||||
|
||||
$header = id(new PHUIHeaderView())
|
||||
->setHeader($trigger->getDisplayName());
|
||||
|
||||
$timeline = $this->buildTransactionTimeline(
|
||||
$trigger,
|
||||
new PhabricatorProjectTriggerTransactionQuery());
|
||||
$timeline->setShouldTerminate(true);
|
||||
|
||||
$curtain = $this->newCurtain($trigger);
|
||||
|
||||
$column_view = id(new PHUITwoColumnView())
|
||||
->setHeader($header)
|
||||
->setCurtain($curtain)
|
||||
->setMainColumn(
|
||||
array(
|
||||
$rules_view,
|
||||
$columns_view,
|
||||
$timeline,
|
||||
));
|
||||
|
||||
$crumbs = $this->buildApplicationCrumbs()
|
||||
->addTextCrumb($trigger->getObjectName())
|
||||
->setBorder(true);
|
||||
|
||||
return $this->newPage()
|
||||
->setTitle($title)
|
||||
->setCrumbs($crumbs)
|
||||
->appendChild($column_view);
|
||||
}
|
||||
|
||||
private function newColumnsView(PhabricatorProjectTrigger $trigger) {
|
||||
$viewer = $this->getViewer();
|
||||
|
||||
// NOTE: When showing columns which use this trigger, we want to represent
|
||||
// all columns the trigger is used by: even columns the user can't see.
|
||||
|
||||
// If we hide columns the viewer can't see, they might think that the
|
||||
// trigger isn't widely used and is safe to edit, when it may actually
|
||||
// be in use on workboards they don't have access to.
|
||||
|
||||
// Query the columns with the omnipotent viewer first, then pull out their
|
||||
// PHIDs and throw the actual objects away. Re-query with the real viewer
|
||||
// so we load only the columns they can actually see, but have a list of
|
||||
// all the impacted column PHIDs.
|
||||
|
||||
// (We're also exposing the status of columns the user might not be able
|
||||
// to see. This technically violates policy, but the trigger usage table
|
||||
// hints at it anyway and it seems unlikely to ever have any security
|
||||
// impact, but is useful in assessing whether a trigger is really in use
|
||||
// or not.)
|
||||
|
||||
$omnipotent_viewer = PhabricatorUser::getOmnipotentUser();
|
||||
$all_columns = id(new PhabricatorProjectColumnQuery())
|
||||
->setViewer($omnipotent_viewer)
|
||||
->withTriggerPHIDs(array($trigger->getPHID()))
|
||||
->execute();
|
||||
$column_map = mpull($all_columns, 'getStatus', 'getPHID');
|
||||
|
||||
if ($column_map) {
|
||||
$visible_columns = id(new PhabricatorProjectColumnQuery())
|
||||
->setViewer($viewer)
|
||||
->withPHIDs(array_keys($column_map))
|
||||
->execute();
|
||||
$visible_columns = mpull($visible_columns, null, 'getPHID');
|
||||
} else {
|
||||
$visible_columns = array();
|
||||
}
|
||||
|
||||
$rows = array();
|
||||
foreach ($column_map as $column_phid => $column_status) {
|
||||
$column = idx($visible_columns, $column_phid);
|
||||
|
||||
if ($column) {
|
||||
$project = $column->getProject();
|
||||
|
||||
$project_name = phutil_tag(
|
||||
'a',
|
||||
array(
|
||||
'href' => $project->getURI(),
|
||||
),
|
||||
$project->getDisplayName());
|
||||
|
||||
$column_name = phutil_tag(
|
||||
'a',
|
||||
array(
|
||||
'href' => $column->getWorkboardURI(),
|
||||
),
|
||||
$column->getDisplayName());
|
||||
} else {
|
||||
$project_name = null;
|
||||
$column_name = phutil_tag('em', array(), pht('Restricted Column'));
|
||||
}
|
||||
|
||||
if ($column_status == PhabricatorProjectColumn::STATUS_ACTIVE) {
|
||||
$status_icon = id(new PHUIIconView())
|
||||
->setIcon('fa-columns', 'blue')
|
||||
->setTooltip(pht('Active Column'));
|
||||
} else {
|
||||
$status_icon = id(new PHUIIconView())
|
||||
->setIcon('fa-eye-slash', 'grey')
|
||||
->setTooltip(pht('Hidden Column'));
|
||||
}
|
||||
|
||||
$rows[] = array(
|
||||
$status_icon,
|
||||
$project_name,
|
||||
$column_name,
|
||||
);
|
||||
}
|
||||
|
||||
$table_view = id(new AphrontTableView($rows))
|
||||
->setNoDataString(pht('This trigger is not used by any columns.'))
|
||||
->setHeaders(
|
||||
array(
|
||||
null,
|
||||
pht('Project'),
|
||||
pht('Column'),
|
||||
))
|
||||
->setColumnClasses(
|
||||
array(
|
||||
null,
|
||||
null,
|
||||
'wide pri',
|
||||
));
|
||||
|
||||
$header_view = id(new PHUIHeaderView())
|
||||
->setHeader(pht('Used by Columns'));
|
||||
|
||||
return id(new PHUIObjectBoxView())
|
||||
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
|
||||
->setHeader($header_view)
|
||||
->setTable($table_view);
|
||||
}
|
||||
|
||||
private function newRulesView(PhabricatorProjectTrigger $trigger) {
|
||||
$viewer = $this->getViewer();
|
||||
$rules = $trigger->getTriggerRules();
|
||||
|
||||
$rows = array();
|
||||
foreach ($rules as $rule) {
|
||||
$value = $rule->getRecord()->getValue();
|
||||
|
||||
$rows[] = array(
|
||||
$rule->getRuleViewIcon($value),
|
||||
$rule->getRuleViewLabel(),
|
||||
$rule->getRuleViewDescription($value),
|
||||
);
|
||||
}
|
||||
|
||||
$table_view = id(new AphrontTableView($rows))
|
||||
->setNoDataString(pht('This trigger has no rules.'))
|
||||
->setHeaders(
|
||||
array(
|
||||
null,
|
||||
pht('Rule'),
|
||||
pht('Action'),
|
||||
))
|
||||
->setColumnClasses(
|
||||
array(
|
||||
null,
|
||||
'pri',
|
||||
'wide',
|
||||
));
|
||||
|
||||
$header_view = id(new PHUIHeaderView())
|
||||
->setHeader(pht('Trigger Rules'))
|
||||
->setSubheader(
|
||||
pht(
|
||||
'When a card is dropped into a column that uses this trigger, '.
|
||||
'these actions will be taken.'));
|
||||
|
||||
return id(new PHUIObjectBoxView())
|
||||
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
|
||||
->setHeader($header_view)
|
||||
->setTable($table_view);
|
||||
}
|
||||
private function newCurtain(PhabricatorProjectTrigger $trigger) {
|
||||
$viewer = $this->getViewer();
|
||||
|
||||
$can_edit = PhabricatorPolicyFilter::hasCapability(
|
||||
$viewer,
|
||||
$trigger,
|
||||
PhabricatorPolicyCapability::CAN_EDIT);
|
||||
|
||||
$curtain = $this->newCurtainView($trigger);
|
||||
|
||||
$edit_uri = $this->getApplicationURI(
|
||||
urisprintf(
|
||||
'trigger/edit/%d/',
|
||||
$trigger->getID()));
|
||||
|
||||
$curtain->addAction(
|
||||
id(new PhabricatorActionView())
|
||||
->setName(pht('Edit Trigger'))
|
||||
->setIcon('fa-pencil')
|
||||
->setHref($edit_uri)
|
||||
->setDisabled(!$can_edit)
|
||||
->setWorkflow(!$can_edit));
|
||||
|
||||
return $curtain;
|
||||
}
|
||||
|
||||
}
|
|
@ -11,130 +11,12 @@ final class PhabricatorProjectColumnTransactionEditor
|
|||
return pht('Workboard Columns');
|
||||
}
|
||||
|
||||
public function getTransactionTypes() {
|
||||
$types = parent::getTransactionTypes();
|
||||
|
||||
$types[] = PhabricatorProjectColumnTransaction::TYPE_NAME;
|
||||
$types[] = PhabricatorProjectColumnTransaction::TYPE_STATUS;
|
||||
$types[] = PhabricatorProjectColumnTransaction::TYPE_LIMIT;
|
||||
|
||||
return $types;
|
||||
public function getCreateObjectTitle($author, $object) {
|
||||
return pht('%s created this column.', $author);
|
||||
}
|
||||
|
||||
protected function getCustomTransactionOldValue(
|
||||
PhabricatorLiskDAO $object,
|
||||
PhabricatorApplicationTransaction $xaction) {
|
||||
|
||||
switch ($xaction->getTransactionType()) {
|
||||
case PhabricatorProjectColumnTransaction::TYPE_NAME:
|
||||
return $object->getName();
|
||||
case PhabricatorProjectColumnTransaction::TYPE_STATUS:
|
||||
return $object->getStatus();
|
||||
case PhabricatorProjectColumnTransaction::TYPE_LIMIT:
|
||||
return $object->getPointLimit();
|
||||
|
||||
}
|
||||
|
||||
return parent::getCustomTransactionOldValue($object, $xaction);
|
||||
}
|
||||
|
||||
protected function getCustomTransactionNewValue(
|
||||
PhabricatorLiskDAO $object,
|
||||
PhabricatorApplicationTransaction $xaction) {
|
||||
|
||||
switch ($xaction->getTransactionType()) {
|
||||
case PhabricatorProjectColumnTransaction::TYPE_NAME:
|
||||
case PhabricatorProjectColumnTransaction::TYPE_STATUS:
|
||||
return $xaction->getNewValue();
|
||||
case PhabricatorProjectColumnTransaction::TYPE_LIMIT:
|
||||
$value = $xaction->getNewValue();
|
||||
if (strlen($value)) {
|
||||
return (int)$xaction->getNewValue();
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return parent::getCustomTransactionNewValue($object, $xaction);
|
||||
}
|
||||
|
||||
protected function applyCustomInternalTransaction(
|
||||
PhabricatorLiskDAO $object,
|
||||
PhabricatorApplicationTransaction $xaction) {
|
||||
|
||||
switch ($xaction->getTransactionType()) {
|
||||
case PhabricatorProjectColumnTransaction::TYPE_NAME:
|
||||
$object->setName($xaction->getNewValue());
|
||||
return;
|
||||
case PhabricatorProjectColumnTransaction::TYPE_STATUS:
|
||||
$object->setStatus($xaction->getNewValue());
|
||||
return;
|
||||
case PhabricatorProjectColumnTransaction::TYPE_LIMIT:
|
||||
$object->setPointLimit($xaction->getNewValue());
|
||||
return;
|
||||
}
|
||||
|
||||
return parent::applyCustomInternalTransaction($object, $xaction);
|
||||
}
|
||||
|
||||
protected function applyCustomExternalTransaction(
|
||||
PhabricatorLiskDAO $object,
|
||||
PhabricatorApplicationTransaction $xaction) {
|
||||
|
||||
switch ($xaction->getTransactionType()) {
|
||||
case PhabricatorProjectColumnTransaction::TYPE_NAME:
|
||||
case PhabricatorProjectColumnTransaction::TYPE_STATUS:
|
||||
case PhabricatorProjectColumnTransaction::TYPE_LIMIT:
|
||||
return;
|
||||
}
|
||||
|
||||
return parent::applyCustomExternalTransaction($object, $xaction);
|
||||
}
|
||||
|
||||
protected function validateTransaction(
|
||||
PhabricatorLiskDAO $object,
|
||||
$type,
|
||||
array $xactions) {
|
||||
|
||||
$errors = parent::validateTransaction($object, $type, $xactions);
|
||||
|
||||
switch ($type) {
|
||||
case PhabricatorProjectColumnTransaction::TYPE_LIMIT:
|
||||
foreach ($xactions as $xaction) {
|
||||
$value = $xaction->getNewValue();
|
||||
if (strlen($value) && !preg_match('/^\d+\z/', $value)) {
|
||||
$errors[] = new PhabricatorApplicationTransactionValidationError(
|
||||
$type,
|
||||
pht('Invalid'),
|
||||
pht(
|
||||
'Column point limit must either be empty or a nonnegative '.
|
||||
'integer.'),
|
||||
$xaction);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case PhabricatorProjectColumnTransaction::TYPE_NAME:
|
||||
$missing = $this->validateIsEmptyTextField(
|
||||
$object->getName(),
|
||||
$xactions);
|
||||
|
||||
// The default "Backlog" column is allowed to be unnamed, which
|
||||
// means we use the default name.
|
||||
|
||||
if ($missing && !$object->isDefaultColumn()) {
|
||||
$error = new PhabricatorApplicationTransactionValidationError(
|
||||
$type,
|
||||
pht('Required'),
|
||||
pht('Column name is required.'),
|
||||
nonempty(last($xactions), null));
|
||||
|
||||
$error->setIsMissingFieldError(true);
|
||||
$errors[] = $error;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return $errors;
|
||||
public function getCreateObjectTitleForFeed($author, $object) {
|
||||
return pht('%s created %s.', $author, $object);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorProjectTriggerEditor
|
||||
extends PhabricatorApplicationTransactionEditor {
|
||||
|
||||
public function getEditorApplicationClass() {
|
||||
return 'PhabricatorProjectApplication';
|
||||
}
|
||||
|
||||
public function getEditorObjectsDescription() {
|
||||
return pht('Triggers');
|
||||
}
|
||||
|
||||
public function getCreateObjectTitle($author, $object) {
|
||||
return pht('%s created this trigger.', $author);
|
||||
}
|
||||
|
||||
public function getCreateObjectTitleForFeed($author, $object) {
|
||||
return pht('%s created %s.', $author, $object);
|
||||
}
|
||||
|
||||
public function getTransactionTypes() {
|
||||
$types = parent::getTransactionTypes();
|
||||
|
||||
$types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
|
||||
|
||||
return $types;
|
||||
}
|
||||
|
||||
protected function supportsSearch() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
|
@ -135,52 +135,12 @@ final class PhabricatorBoardLayoutEngine extends Phobject {
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function queueAddPositionBefore(
|
||||
$board_phid,
|
||||
$column_phid,
|
||||
$object_phid,
|
||||
$before_phid) {
|
||||
|
||||
return $this->queueAddPositionRelative(
|
||||
$board_phid,
|
||||
$column_phid,
|
||||
$object_phid,
|
||||
$before_phid,
|
||||
true);
|
||||
}
|
||||
|
||||
public function queueAddPositionAfter(
|
||||
$board_phid,
|
||||
$column_phid,
|
||||
$object_phid,
|
||||
$after_phid) {
|
||||
|
||||
return $this->queueAddPositionRelative(
|
||||
$board_phid,
|
||||
$column_phid,
|
||||
$object_phid,
|
||||
$after_phid,
|
||||
false);
|
||||
}
|
||||
|
||||
public function queueAddPosition(
|
||||
$board_phid,
|
||||
$column_phid,
|
||||
$object_phid) {
|
||||
return $this->queueAddPositionRelative(
|
||||
$board_phid,
|
||||
$column_phid,
|
||||
$object_phid,
|
||||
null,
|
||||
true);
|
||||
}
|
||||
|
||||
private function queueAddPositionRelative(
|
||||
$board_phid,
|
||||
$column_phid,
|
||||
$object_phid,
|
||||
$relative_phid,
|
||||
$is_before) {
|
||||
array $after_phids,
|
||||
array $before_phids) {
|
||||
|
||||
$board_layout = idx($this->boardLayout, $board_phid, array());
|
||||
$positions = idx($board_layout, $column_phid, array());
|
||||
|
@ -196,54 +156,76 @@ final class PhabricatorBoardLayoutEngine extends Phobject {
|
|||
->setObjectPHID($object_phid);
|
||||
}
|
||||
|
||||
$found = false;
|
||||
if (!$positions) {
|
||||
$object_position->setSequence(0);
|
||||
} else {
|
||||
foreach ($positions as $position) {
|
||||
if (!$found) {
|
||||
if ($relative_phid === null) {
|
||||
$is_match = true;
|
||||
} else {
|
||||
$position_phid = $position->getObjectPHID();
|
||||
$is_match = ($relative_phid == $position_phid);
|
||||
// The user's view of the board may fall out of date, so they might
|
||||
// try to drop a card under a different card which is no longer where
|
||||
// they thought it was.
|
||||
|
||||
// When this happens, we perform the move anyway, since this is almost
|
||||
// certainly what users want when interacting with the UI. We'l try to
|
||||
// fall back to another nearby card if the client provided us one. If
|
||||
// we don't find any of the cards the client specified in the column,
|
||||
// we'll just move the card to the default position.
|
||||
|
||||
$search_phids = array();
|
||||
foreach ($after_phids as $after_phid) {
|
||||
$search_phids[] = array($after_phid, false);
|
||||
}
|
||||
|
||||
foreach ($before_phids as $before_phid) {
|
||||
$search_phids[] = array($before_phid, true);
|
||||
}
|
||||
|
||||
// This makes us fall back to the default position if we fail every
|
||||
// candidate position. The default position counts as a "before" position
|
||||
// because we want to put the new card at the top of the column.
|
||||
$search_phids[] = array(null, true);
|
||||
|
||||
$found = false;
|
||||
foreach ($search_phids as $search_position) {
|
||||
list($relative_phid, $is_before) = $search_position;
|
||||
foreach ($positions as $position) {
|
||||
if (!$found) {
|
||||
if ($relative_phid === null) {
|
||||
$is_match = true;
|
||||
} else {
|
||||
$position_phid = $position->getObjectPHID();
|
||||
$is_match = ($relative_phid === $position_phid);
|
||||
}
|
||||
|
||||
if ($is_match) {
|
||||
$found = true;
|
||||
|
||||
$sequence = $position->getSequence();
|
||||
|
||||
if (!$is_before) {
|
||||
$sequence++;
|
||||
}
|
||||
|
||||
$object_position->setSequence($sequence++);
|
||||
|
||||
if (!$is_before) {
|
||||
// If we're inserting after this position, continue the loop so
|
||||
// we don't update it.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($is_match) {
|
||||
$found = true;
|
||||
|
||||
$sequence = $position->getSequence();
|
||||
|
||||
if (!$is_before) {
|
||||
$sequence++;
|
||||
}
|
||||
|
||||
$object_position->setSequence($sequence++);
|
||||
|
||||
if (!$is_before) {
|
||||
// If we're inserting after this position, continue the loop so
|
||||
// we don't update it.
|
||||
continue;
|
||||
}
|
||||
if ($found) {
|
||||
$position->setSequence($sequence++);
|
||||
$this->addQueue[] = $position;
|
||||
}
|
||||
}
|
||||
|
||||
if ($found) {
|
||||
$position->setSequence($sequence++);
|
||||
$this->addQueue[] = $position;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($relative_phid && !$found) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Unable to find object "%s" in column "%s" on board "%s".',
|
||||
$relative_phid,
|
||||
$column_phid,
|
||||
$board_phid));
|
||||
}
|
||||
|
||||
$this->addQueue[] = $object_position;
|
||||
|
||||
$positions[$object_phid] = $object_position;
|
||||
|
@ -336,6 +318,7 @@ final class PhabricatorBoardLayoutEngine extends Phobject {
|
|||
$columns = id(new PhabricatorProjectColumnQuery())
|
||||
->setViewer($viewer)
|
||||
->withProjectPHIDs(array_keys($boards))
|
||||
->needTriggers(true)
|
||||
->execute();
|
||||
$columns = msort($columns, 'getOrderingKey');
|
||||
$columns = mpull($columns, null, 'getPHID');
|
||||
|
|
|
@ -56,6 +56,7 @@ final class PhabricatorBoardRenderingEngine extends Phobject {
|
|||
$card = id(new ProjectBoardTaskCard())
|
||||
->setViewer($viewer)
|
||||
->setTask($object)
|
||||
->setShowEditControls(true)
|
||||
->setCanEdit($this->getCanEdit($phid));
|
||||
|
||||
$owner_phid = $object->getOwnerPHID();
|
||||
|
|
|
@ -7,6 +7,7 @@ final class PhabricatorBoardResponseEngine extends Phobject {
|
|||
private $objectPHID;
|
||||
private $visiblePHIDs;
|
||||
private $ordering;
|
||||
private $sounds;
|
||||
|
||||
public function setViewer(PhabricatorUser $viewer) {
|
||||
$this->viewer = $viewer;
|
||||
|
@ -53,6 +54,15 @@ final class PhabricatorBoardResponseEngine extends Phobject {
|
|||
return $this->ordering;
|
||||
}
|
||||
|
||||
public function setSounds(array $sounds) {
|
||||
$this->sounds = $sounds;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSounds() {
|
||||
return $this->sounds;
|
||||
}
|
||||
|
||||
public function buildResponse() {
|
||||
$viewer = $this->getViewer();
|
||||
$object_phid = $this->getObjectPHID();
|
||||
|
@ -131,10 +141,7 @@ final class PhabricatorBoardResponseEngine extends Phobject {
|
|||
$card['headers'][$order_key] = $header;
|
||||
}
|
||||
|
||||
$card['properties'] = array(
|
||||
'points' => (double)$object->getPoints(),
|
||||
'status' => $object->getStatus(),
|
||||
);
|
||||
$card['properties'] = self::newTaskProperties($object);
|
||||
}
|
||||
|
||||
if ($card_phid === $object_phid) {
|
||||
|
@ -153,12 +160,22 @@ final class PhabricatorBoardResponseEngine extends Phobject {
|
|||
'columnMaps' => $natural,
|
||||
'cards' => $cards,
|
||||
'headers' => $headers,
|
||||
'sounds' => $this->getSounds(),
|
||||
);
|
||||
|
||||
return id(new AphrontAjaxResponse())
|
||||
->setContent($payload);
|
||||
}
|
||||
|
||||
public static function newTaskProperties($task) {
|
||||
return array(
|
||||
'points' => (double)$task->getPoints(),
|
||||
'status' => $task->getStatus(),
|
||||
'priority' => (int)$task->getPriority(),
|
||||
'owner' => $task->getOwnerPHID(),
|
||||
);
|
||||
}
|
||||
|
||||
private function buildTemplate($object) {
|
||||
$viewer = $this->getViewer();
|
||||
$object_phid = $this->getObjectPHID();
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorProjectTriggerUsageIndexEngineExtension
|
||||
extends PhabricatorIndexEngineExtension {
|
||||
|
||||
const EXTENSIONKEY = 'trigger.usage';
|
||||
|
||||
public function getExtensionName() {
|
||||
return pht('Trigger Usage');
|
||||
}
|
||||
|
||||
public function shouldIndexObject($object) {
|
||||
if (!($object instanceof PhabricatorProjectTrigger)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function indexObject(
|
||||
PhabricatorIndexEngine $engine,
|
||||
$object) {
|
||||
|
||||
$usage_table = new PhabricatorProjectTriggerUsage();
|
||||
$column_table = new PhabricatorProjectColumn();
|
||||
|
||||
$conn_w = $object->establishConnection('w');
|
||||
|
||||
$active_statuses = array(
|
||||
PhabricatorProjectColumn::STATUS_ACTIVE,
|
||||
);
|
||||
|
||||
// Select summary information to populate the usage index. When picking
|
||||
// an "examplePHID", we try to pick an active column.
|
||||
$row = queryfx_one(
|
||||
$conn_w,
|
||||
'SELECT phid, COUNT(*) N, SUM(IF(status IN (%Ls), 1, 0)) M FROM %R
|
||||
WHERE triggerPHID = %s
|
||||
ORDER BY IF(status IN (%Ls), 1, 0) DESC, id ASC',
|
||||
$active_statuses,
|
||||
$column_table,
|
||||
$object->getPHID(),
|
||||
$active_statuses);
|
||||
if ($row) {
|
||||
$example_phid = $row['phid'];
|
||||
$column_count = $row['N'];
|
||||
$active_count = $row['M'];
|
||||
} else {
|
||||
$example_phid = null;
|
||||
$column_count = 0;
|
||||
$active_count = 0;
|
||||
}
|
||||
|
||||
queryfx(
|
||||
$conn_w,
|
||||
'INSERT INTO %R (triggerPHID, examplePHID, columnCount, activeColumnCount)
|
||||
VALUES (%s, %ns, %d, %d)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
examplePHID = VALUES(examplePHID),
|
||||
columnCount = VALUES(columnCount),
|
||||
activeColumnCount = VALUES(activeColumnCount)',
|
||||
$usage_table,
|
||||
$object->getPHID(),
|
||||
$example_phid,
|
||||
$column_count,
|
||||
$active_count);
|
||||
}
|
||||
|
||||
}
|
|
@ -55,7 +55,7 @@ final class PhabricatorProjectsCurtainExtension
|
|||
$column_link = phutil_tag(
|
||||
'a',
|
||||
array(
|
||||
'href' => "/project/board/{$project_id}/",
|
||||
'href' => $column->getWorkboardURI(),
|
||||
'class' => 'maniphest-board-link',
|
||||
),
|
||||
$column_name);
|
||||
|
|
|
@ -81,7 +81,7 @@ final class PhabricatorProjectUIEventListener
|
|||
$column_link = phutil_tag(
|
||||
'a',
|
||||
array(
|
||||
'href' => "/project/board/{$project_id}/",
|
||||
'href' => $column->getWorkboardURI(),
|
||||
'class' => 'maniphest-board-link',
|
||||
),
|
||||
$column_name);
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorProjectTriggerCorruptionException
|
||||
extends Exception {}
|
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorProjectDropEffect
|
||||
extends Phobject {
|
||||
|
||||
private $icon;
|
||||
private $color;
|
||||
private $content;
|
||||
private $conditions = array();
|
||||
private $isTriggerEffect;
|
||||
private $isHeader;
|
||||
|
||||
public function setIcon($icon) {
|
||||
$this->icon = $icon;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getIcon() {
|
||||
return $this->icon;
|
||||
}
|
||||
|
||||
public function setColor($color) {
|
||||
$this->color = $color;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getColor() {
|
||||
return $this->color;
|
||||
}
|
||||
|
||||
public function setContent($content) {
|
||||
$this->content = $content;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getContent() {
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
public function toDictionary() {
|
||||
return array(
|
||||
'icon' => $this->getIcon(),
|
||||
'color' => $this->getColor(),
|
||||
'content' => hsprintf('%s', $this->getContent()),
|
||||
'isTriggerEffect' => $this->getIsTriggerEffect(),
|
||||
'isHeader' => $this->getIsHeader(),
|
||||
'conditions' => $this->getConditions(),
|
||||
);
|
||||
}
|
||||
|
||||
public function addCondition($field, $operator, $value) {
|
||||
$this->conditions[] = array(
|
||||
'field' => $field,
|
||||
'operator' => $operator,
|
||||
'value' => $value,
|
||||
);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getConditions() {
|
||||
return $this->conditions;
|
||||
}
|
||||
|
||||
public function setIsTriggerEffect($is_trigger_effect) {
|
||||
$this->isTriggerEffect = $is_trigger_effect;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getIsTriggerEffect() {
|
||||
return $this->isTriggerEffect;
|
||||
}
|
||||
|
||||
public function setIsHeader($is_header) {
|
||||
$this->isHeader = $is_header;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getIsHeader() {
|
||||
return $this->isHeader;
|
||||
}
|
||||
|
||||
}
|
|
@ -57,7 +57,7 @@ final class PhabricatorProjectWorkboardProfileMenuItem
|
|||
$project = $config->getProfileObject();
|
||||
|
||||
$id = $project->getID();
|
||||
$href = "/project/board/{$id}/";
|
||||
$href = $project->getWorkboardURI();
|
||||
$name = $this->getDisplayName($config);
|
||||
|
||||
$item = $this->newItem()
|
||||
|
|
|
@ -9,6 +9,7 @@ final class PhabricatorProjectColumnHeader
|
|||
private $name;
|
||||
private $icon;
|
||||
private $editProperties;
|
||||
private $dropEffects = array();
|
||||
|
||||
public function setOrderKey($order_key) {
|
||||
$this->orderKey = $order_key;
|
||||
|
@ -64,6 +65,15 @@ final class PhabricatorProjectColumnHeader
|
|||
return $this->editProperties;
|
||||
}
|
||||
|
||||
public function addDropEffect(PhabricatorProjectDropEffect $effect) {
|
||||
$this->dropEffects[] = $effect;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDropEffects() {
|
||||
return $this->dropEffects;
|
||||
}
|
||||
|
||||
public function toDictionary() {
|
||||
return array(
|
||||
'order' => $this->getOrderKey(),
|
||||
|
@ -71,6 +81,7 @@ final class PhabricatorProjectColumnHeader
|
|||
'template' => hsprintf('%s', $this->newView()),
|
||||
'vector' => $this->getSortVector(),
|
||||
'editProperties' => $this->getEditProperties(),
|
||||
'effects' => mpull($this->getDropEffects(), 'toDictionary'),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -196,6 +196,10 @@ abstract class PhabricatorProjectColumnOrder
|
|||
->setOrderKey($this->getColumnOrderKey());
|
||||
}
|
||||
|
||||
final protected function newEffect() {
|
||||
return new PhabricatorProjectDropEffect();
|
||||
}
|
||||
|
||||
final public function toDictionary() {
|
||||
return array(
|
||||
'orderKey' => $this->getColumnOrderKey(),
|
||||
|
|
|
@ -122,16 +122,23 @@ final class PhabricatorProjectColumnOwnerOrder
|
|||
$header_key = $this->newHeaderKeyForOwnerPHID($owner_phid);
|
||||
|
||||
$owner_image = null;
|
||||
$effect_content = null;
|
||||
if ($owner_phid === null) {
|
||||
$owner = null;
|
||||
$sort_vector = $this->newSortVectorForUnowned();
|
||||
$owner_name = pht('Not Assigned');
|
||||
|
||||
$effect_content = pht('Remove task assignee.');
|
||||
} else {
|
||||
$owner = idx($owner_users, $owner_phid);
|
||||
if ($owner) {
|
||||
$sort_vector = $this->newSortVectorForOwner($owner);
|
||||
$owner_name = $owner->getUsername();
|
||||
$owner_image = $owner->getProfileImageURI();
|
||||
|
||||
$effect_content = pht(
|
||||
'Assign task to %s.',
|
||||
phutil_tag('strong', array(), $owner_name));
|
||||
} else {
|
||||
$sort_vector = $this->newSortVectorForOwnerPHID($owner_phid);
|
||||
$owner_name = pht('Unknown User ("%s")', $owner_phid);
|
||||
|
@ -159,6 +166,15 @@ final class PhabricatorProjectColumnOwnerOrder
|
|||
'value' => $owner_phid,
|
||||
));
|
||||
|
||||
if ($effect_content !== null) {
|
||||
$header->addDropEffect(
|
||||
$this->newEffect()
|
||||
->setIcon($owner_icon)
|
||||
->setColor($owner_color)
|
||||
->addCondition('owner', '!=', $owner_phid)
|
||||
->setContent($effect_content));
|
||||
}
|
||||
|
||||
$headers[] = $header;
|
||||
}
|
||||
|
||||
|
|
|
@ -65,6 +65,15 @@ final class PhabricatorProjectColumnPriorityOrder
|
|||
$icon_view = id(new PHUIIconView())
|
||||
->setIcon($priority_icon, $priority_color);
|
||||
|
||||
$drop_effect = $this->newEffect()
|
||||
->setIcon($priority_icon)
|
||||
->setColor($priority_color)
|
||||
->addCondition('priority', '!=', $priority)
|
||||
->setContent(
|
||||
pht(
|
||||
'Change priority to %s.',
|
||||
phutil_tag('strong', array(), $priority_name)));
|
||||
|
||||
$header = $this->newHeader()
|
||||
->setHeaderKey($header_key)
|
||||
->setSortVector($sort_vector)
|
||||
|
@ -73,7 +82,8 @@ final class PhabricatorProjectColumnPriorityOrder
|
|||
->setEditProperties(
|
||||
array(
|
||||
'value' => (int)$priority,
|
||||
));
|
||||
))
|
||||
->addDropEffect($drop_effect);
|
||||
|
||||
$headers[] = $header;
|
||||
}
|
||||
|
|
|
@ -72,6 +72,15 @@ final class PhabricatorProjectColumnStatusOrder
|
|||
$icon_view = id(new PHUIIconView())
|
||||
->setIcon($status_icon, $status_color);
|
||||
|
||||
$drop_effect = $this->newEffect()
|
||||
->setIcon($status_icon)
|
||||
->setColor($status_color)
|
||||
->addCondition('status', '!=', $status_key)
|
||||
->setContent(
|
||||
pht(
|
||||
'Change status to %s.',
|
||||
phutil_tag('strong', array(), $status_name)));
|
||||
|
||||
$header = $this->newHeader()
|
||||
->setHeaderKey($header_key)
|
||||
->setSortVector($sort_vector)
|
||||
|
@ -80,7 +89,8 @@ final class PhabricatorProjectColumnStatusOrder
|
|||
->setEditProperties(
|
||||
array(
|
||||
'value' => $status_key,
|
||||
));
|
||||
))
|
||||
->addDropEffect($drop_effect);
|
||||
|
||||
$headers[] = $header;
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ final class PhabricatorProjectColumnPHIDType extends PhabricatorPHIDType {
|
|||
$column = $objects[$phid];
|
||||
|
||||
$handle->setName($column->getDisplayName());
|
||||
$handle->setURI('/project/board/'.$column->getProject()->getID().'/');
|
||||
$handle->setURI($column->getWorkboardURI());
|
||||
|
||||
if ($column->isHidden()) {
|
||||
$handle->setStatus(PhabricatorObjectHandle::STATUS_CLOSED);
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorProjectTriggerPHIDType
|
||||
extends PhabricatorPHIDType {
|
||||
|
||||
const TYPECONST = 'WTRG';
|
||||
|
||||
public function getTypeName() {
|
||||
return pht('Trigger');
|
||||
}
|
||||
|
||||
public function getTypeIcon() {
|
||||
return 'fa-exclamation-triangle';
|
||||
}
|
||||
|
||||
public function newObject() {
|
||||
return new PhabricatorProjectTrigger();
|
||||
}
|
||||
|
||||
public function getPHIDTypeApplicationClass() {
|
||||
return 'PhabricatorProjectApplication';
|
||||
}
|
||||
|
||||
protected function buildQueryForObjects(
|
||||
PhabricatorObjectQuery $query,
|
||||
array $phids) {
|
||||
|
||||
return id(new PhabricatorProjectTriggerQuery())
|
||||
->withPHIDs($phids);
|
||||
}
|
||||
|
||||
public function loadHandles(
|
||||
PhabricatorHandleQuery $query,
|
||||
array $handles,
|
||||
array $objects) {
|
||||
|
||||
foreach ($handles as $phid => $handle) {
|
||||
$trigger = $objects[$phid];
|
||||
|
||||
$handle->setName($trigger->getDisplayName());
|
||||
$handle->setURI($trigger->getURI());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -9,6 +9,8 @@ final class PhabricatorProjectColumnQuery
|
|||
private $proxyPHIDs;
|
||||
private $statuses;
|
||||
private $isProxyColumn;
|
||||
private $triggerPHIDs;
|
||||
private $needTriggers;
|
||||
|
||||
public function withIDs(array $ids) {
|
||||
$this->ids = $ids;
|
||||
|
@ -40,6 +42,16 @@ final class PhabricatorProjectColumnQuery
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function withTriggerPHIDs(array $trigger_phids) {
|
||||
$this->triggerPHIDs = $trigger_phids;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function needTriggers($need_triggers) {
|
||||
$this->needTriggers = true;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function newResultObject() {
|
||||
return new PhabricatorProjectColumn();
|
||||
}
|
||||
|
@ -121,6 +133,42 @@ final class PhabricatorProjectColumnQuery
|
|||
$column->attachProxy($proxy);
|
||||
}
|
||||
|
||||
if ($this->needTriggers) {
|
||||
$trigger_phids = array();
|
||||
foreach ($page as $column) {
|
||||
if ($column->canHaveTrigger()) {
|
||||
$trigger_phid = $column->getTriggerPHID();
|
||||
if ($trigger_phid) {
|
||||
$trigger_phids[] = $trigger_phid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($trigger_phids) {
|
||||
$triggers = id(new PhabricatorProjectTriggerQuery())
|
||||
->setViewer($this->getViewer())
|
||||
->setParentQuery($this)
|
||||
->withPHIDs($trigger_phids)
|
||||
->execute();
|
||||
$triggers = mpull($triggers, null, 'getPHID');
|
||||
} else {
|
||||
$triggers = array();
|
||||
}
|
||||
|
||||
foreach ($page as $column) {
|
||||
$trigger = null;
|
||||
|
||||
if ($column->canHaveTrigger()) {
|
||||
$trigger_phid = $column->getTriggerPHID();
|
||||
if ($trigger_phid) {
|
||||
$trigger = idx($triggers, $trigger_phid);
|
||||
}
|
||||
}
|
||||
|
||||
$column->attachTrigger($trigger);
|
||||
}
|
||||
}
|
||||
|
||||
return $page;
|
||||
}
|
||||
|
||||
|
@ -162,6 +210,13 @@ final class PhabricatorProjectColumnQuery
|
|||
$this->statuses);
|
||||
}
|
||||
|
||||
if ($this->triggerPHIDs !== null) {
|
||||
$where[] = qsprintf(
|
||||
$conn,
|
||||
'triggerPHID IN (%Ls)',
|
||||
$this->triggerPHIDs);
|
||||
}
|
||||
|
||||
if ($this->isProxyColumn !== null) {
|
||||
if ($this->isProxyColumn) {
|
||||
$where[] = qsprintf($conn, 'proxyPHID IS NOT NULL');
|
||||
|
|
|
@ -0,0 +1,135 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorProjectTriggerQuery
|
||||
extends PhabricatorCursorPagedPolicyAwareQuery {
|
||||
|
||||
private $ids;
|
||||
private $phids;
|
||||
private $activeColumnMin;
|
||||
private $activeColumnMax;
|
||||
|
||||
private $needUsage;
|
||||
|
||||
public function withIDs(array $ids) {
|
||||
$this->ids = $ids;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function withPHIDs(array $phids) {
|
||||
$this->phids = $phids;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function needUsage($need_usage) {
|
||||
$this->needUsage = $need_usage;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function withActiveColumnCountBetween($min, $max) {
|
||||
$this->activeColumnMin = $min;
|
||||
$this->activeColumnMax = $max;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function newResultObject() {
|
||||
return new PhabricatorProjectTrigger();
|
||||
}
|
||||
|
||||
protected function loadPage() {
|
||||
return $this->loadStandardPage($this->newResultObject());
|
||||
}
|
||||
|
||||
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
|
||||
$where = parent::buildWhereClauseParts($conn);
|
||||
|
||||
if ($this->ids !== null) {
|
||||
$where[] = qsprintf(
|
||||
$conn,
|
||||
'trigger.id IN (%Ld)',
|
||||
$this->ids);
|
||||
}
|
||||
|
||||
if ($this->phids !== null) {
|
||||
$where[] = qsprintf(
|
||||
$conn,
|
||||
'trigger.phid IN (%Ls)',
|
||||
$this->phids);
|
||||
}
|
||||
|
||||
if ($this->activeColumnMin !== null) {
|
||||
$where[] = qsprintf(
|
||||
$conn,
|
||||
'trigger_usage.activeColumnCount >= %d',
|
||||
$this->activeColumnMin);
|
||||
}
|
||||
|
||||
if ($this->activeColumnMax !== null) {
|
||||
$where[] = qsprintf(
|
||||
$conn,
|
||||
'trigger_usage.activeColumnCount <= %d',
|
||||
$this->activeColumnMax);
|
||||
}
|
||||
|
||||
return $where;
|
||||
}
|
||||
|
||||
protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) {
|
||||
$joins = parent::buildJoinClauseParts($conn);
|
||||
|
||||
if ($this->shouldJoinUsageTable()) {
|
||||
$joins[] = qsprintf(
|
||||
$conn,
|
||||
'JOIN %R trigger_usage ON trigger.phid = trigger_usage.triggerPHID',
|
||||
new PhabricatorProjectTriggerUsage());
|
||||
}
|
||||
|
||||
return $joins;
|
||||
}
|
||||
|
||||
private function shouldJoinUsageTable() {
|
||||
if ($this->activeColumnMin !== null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->activeColumnMax !== null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function didFilterPage(array $triggers) {
|
||||
if ($this->needUsage) {
|
||||
$usage_map = id(new PhabricatorProjectTriggerUsage())->loadAllWhere(
|
||||
'triggerPHID IN (%Ls)',
|
||||
mpull($triggers, 'getPHID'));
|
||||
$usage_map = mpull($usage_map, null, 'getTriggerPHID');
|
||||
|
||||
foreach ($triggers as $trigger) {
|
||||
$trigger_phid = $trigger->getPHID();
|
||||
|
||||
$usage = idx($usage_map, $trigger_phid);
|
||||
if (!$usage) {
|
||||
$usage = id(new PhabricatorProjectTriggerUsage())
|
||||
->setTriggerPHID($trigger_phid)
|
||||
->setExamplePHID(null)
|
||||
->setColumnCount(0)
|
||||
->setActiveColumnCount(0);
|
||||
}
|
||||
|
||||
$trigger->attachUsage($usage);
|
||||
}
|
||||
}
|
||||
|
||||
return $triggers;
|
||||
}
|
||||
|
||||
public function getQueryApplicationClass() {
|
||||
return 'PhabricatorProjectApplication';
|
||||
}
|
||||
|
||||
protected function getPrimaryTableAlias() {
|
||||
return 'trigger';
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,155 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorProjectTriggerSearchEngine
|
||||
extends PhabricatorApplicationSearchEngine {
|
||||
|
||||
public function getResultTypeDescription() {
|
||||
return pht('Triggers');
|
||||
}
|
||||
|
||||
public function getApplicationClassName() {
|
||||
return 'PhabricatorProjectApplication';
|
||||
}
|
||||
|
||||
public function newQuery() {
|
||||
return id(new PhabricatorProjectTriggerQuery())
|
||||
->needUsage(true);
|
||||
}
|
||||
|
||||
protected function buildCustomSearchFields() {
|
||||
return array(
|
||||
id(new PhabricatorSearchThreeStateField())
|
||||
->setLabel(pht('Active'))
|
||||
->setKey('isActive')
|
||||
->setOptions(
|
||||
pht('(Show All)'),
|
||||
pht('Show Only Active Triggers'),
|
||||
pht('Show Only Inactive Triggers')),
|
||||
);
|
||||
}
|
||||
|
||||
protected function buildQueryFromParameters(array $map) {
|
||||
$query = $this->newQuery();
|
||||
|
||||
if ($map['isActive'] !== null) {
|
||||
if ($map['isActive']) {
|
||||
$query->withActiveColumnCountBetween(1, null);
|
||||
} else {
|
||||
$query->withActiveColumnCountBetween(null, 0);
|
||||
}
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
protected function getURI($path) {
|
||||
return '/project/trigger/'.$path;
|
||||
}
|
||||
|
||||
protected function getBuiltinQueryNames() {
|
||||
$names = array();
|
||||
|
||||
$names['active'] = pht('Active Triggers');
|
||||
$names['all'] = pht('All Triggers');
|
||||
|
||||
return $names;
|
||||
}
|
||||
|
||||
public function buildSavedQueryFromBuiltin($query_key) {
|
||||
$query = $this->newSavedQuery();
|
||||
$query->setQueryKey($query_key);
|
||||
|
||||
switch ($query_key) {
|
||||
case 'active':
|
||||
return $query->setParameter('isActive', true);
|
||||
case 'all':
|
||||
return $query;
|
||||
}
|
||||
|
||||
return parent::buildSavedQueryFromBuiltin($query_key);
|
||||
}
|
||||
|
||||
protected function renderResultList(
|
||||
array $triggers,
|
||||
PhabricatorSavedQuery $query,
|
||||
array $handles) {
|
||||
assert_instances_of($triggers, 'PhabricatorProjectTrigger');
|
||||
$viewer = $this->requireViewer();
|
||||
|
||||
$example_phids = array();
|
||||
foreach ($triggers as $trigger) {
|
||||
$example_phid = $trigger->getUsage()->getExamplePHID();
|
||||
if ($example_phid) {
|
||||
$example_phids[] = $example_phid;
|
||||
}
|
||||
}
|
||||
|
||||
$handles = $viewer->loadHandles($example_phids);
|
||||
|
||||
$list = id(new PHUIObjectItemListView())
|
||||
->setViewer($viewer);
|
||||
foreach ($triggers as $trigger) {
|
||||
$usage = $trigger->getUsage();
|
||||
|
||||
$column_handle = null;
|
||||
$have_column = false;
|
||||
$example_phid = $usage->getExamplePHID();
|
||||
if ($example_phid) {
|
||||
$column_handle = $handles[$example_phid];
|
||||
if ($column_handle->isComplete()) {
|
||||
if (!$column_handle->getPolicyFiltered()) {
|
||||
$have_column = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$column_count = $usage->getColumnCount();
|
||||
$active_count = $usage->getActiveColumnCount();
|
||||
|
||||
if ($have_column) {
|
||||
if ($active_count > 1) {
|
||||
$usage_description = pht(
|
||||
'Used on %s and %s other active column(s).',
|
||||
$column_handle->renderLink(),
|
||||
new PhutilNumber($active_count - 1));
|
||||
} else if ($column_count > 1) {
|
||||
$usage_description = pht(
|
||||
'Used on %s and %s other column(s).',
|
||||
$column_handle->renderLink(),
|
||||
new PhutilNumber($column_count - 1));
|
||||
} else {
|
||||
$usage_description = pht(
|
||||
'Used on %s.',
|
||||
$column_handle->renderLink());
|
||||
}
|
||||
} else {
|
||||
if ($active_count) {
|
||||
$usage_description = pht(
|
||||
'Used on %s active column(s).',
|
||||
new PhutilNumber($active_count));
|
||||
} else if ($column_count) {
|
||||
$usage_description = pht(
|
||||
'Used on %s column(s).',
|
||||
new PhutilNumber($column_count));
|
||||
} else {
|
||||
$usage_description = pht(
|
||||
'Unused trigger.');
|
||||
}
|
||||
}
|
||||
|
||||
$item = id(new PHUIObjectItemView())
|
||||
->setObjectName($trigger->getObjectName())
|
||||
->setHeader($trigger->getDisplayName())
|
||||
->setHref($trigger->getURI())
|
||||
->addAttribute($usage_description)
|
||||
->setDisabled(!$active_count);
|
||||
|
||||
$list->addItem($item);
|
||||
}
|
||||
|
||||
return id(new PhabricatorApplicationSearchResultView())
|
||||
->setObjectList($list)
|
||||
->setNoDataString(pht('No triggers found.'));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorProjectTriggerTransactionQuery
|
||||
extends PhabricatorApplicationTransactionQuery {
|
||||
|
||||
public function getTemplateApplicationTransaction() {
|
||||
return new PhabricatorProjectTriggerTransaction();
|
||||
}
|
||||
|
||||
}
|
|
@ -392,6 +392,10 @@ final class PhabricatorProject extends PhabricatorProjectDAO
|
|||
return "/project/profile/{$id}/";
|
||||
}
|
||||
|
||||
public function getWorkboardURI() {
|
||||
return urisprintf('/project/board/%d/', $this->getID());
|
||||
}
|
||||
|
||||
public function save() {
|
||||
if (!$this->getMailKey()) {
|
||||
$this->setMailKey(Filesystem::readRandomCharacters(20));
|
||||
|
|
|
@ -18,9 +18,11 @@ final class PhabricatorProjectColumn
|
|||
protected $proxyPHID;
|
||||
protected $sequence;
|
||||
protected $properties = array();
|
||||
protected $triggerPHID;
|
||||
|
||||
private $project = self::ATTACHABLE;
|
||||
private $proxy = self::ATTACHABLE;
|
||||
private $trigger = self::ATTACHABLE;
|
||||
|
||||
public static function initializeNewColumn(PhabricatorUser $user) {
|
||||
return id(new PhabricatorProjectColumn())
|
||||
|
@ -40,6 +42,7 @@ final class PhabricatorProjectColumn
|
|||
'status' => 'uint32',
|
||||
'sequence' => 'uint32',
|
||||
'proxyPHID' => 'phid?',
|
||||
'triggerPHID' => 'phid?',
|
||||
),
|
||||
self::CONFIG_KEY_SCHEMA => array(
|
||||
'key_status' => array(
|
||||
|
@ -52,6 +55,9 @@ final class PhabricatorProjectColumn
|
|||
'columns' => array('projectPHID', 'proxyPHID'),
|
||||
'unique' => true,
|
||||
),
|
||||
'key_trigger' => array(
|
||||
'columns' => array('triggerPHID'),
|
||||
),
|
||||
),
|
||||
) + parent::getConfiguration();
|
||||
}
|
||||
|
@ -180,6 +186,72 @@ final class PhabricatorProjectColumn
|
|||
return sprintf('%s%012d', $group, $sequence);
|
||||
}
|
||||
|
||||
public function attachTrigger(PhabricatorProjectTrigger $trigger = null) {
|
||||
$this->trigger = $trigger;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTrigger() {
|
||||
return $this->assertAttached($this->trigger);
|
||||
}
|
||||
|
||||
public function canHaveTrigger() {
|
||||
// Backlog columns and proxy (subproject / milestone) columns can't have
|
||||
// triggers because cards routinely end up in these columns through tag
|
||||
// edits rather than drag-and-drop and it would likely be confusing to
|
||||
// have these triggers act only a small fraction of the time.
|
||||
|
||||
if ($this->isDefaultColumn()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->getProxy()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getWorkboardURI() {
|
||||
return $this->getProject()->getWorkboardURI();
|
||||
}
|
||||
|
||||
public function getDropEffects() {
|
||||
$effects = array();
|
||||
|
||||
$proxy = $this->getProxy();
|
||||
if ($proxy && $proxy->isMilestone()) {
|
||||
$effects[] = id(new PhabricatorProjectDropEffect())
|
||||
->setIcon($proxy->getProxyColumnIcon())
|
||||
->setColor('violet')
|
||||
->setContent(
|
||||
pht(
|
||||
'Move to milestone %s.',
|
||||
phutil_tag('strong', array(), $this->getDisplayName())));
|
||||
} else {
|
||||
$effects[] = id(new PhabricatorProjectDropEffect())
|
||||
->setIcon('fa-columns')
|
||||
->setColor('blue')
|
||||
->setContent(
|
||||
pht(
|
||||
'Move to column %s.',
|
||||
phutil_tag('strong', array(), $this->getDisplayName())));
|
||||
}
|
||||
|
||||
|
||||
if ($this->canHaveTrigger()) {
|
||||
$trigger = $this->getTrigger();
|
||||
if ($trigger) {
|
||||
foreach ($trigger->getDropEffects() as $trigger_effect) {
|
||||
$effects[] = $trigger_effect;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $effects;
|
||||
}
|
||||
|
||||
|
||||
/* -( PhabricatorConduitResultInterface )---------------------------------- */
|
||||
|
||||
public function getFieldSpecificationsForConduit() {
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorProjectColumnTransaction
|
||||
extends PhabricatorApplicationTransaction {
|
||||
|
||||
const TYPE_NAME = 'project:col:name';
|
||||
const TYPE_STATUS = 'project:col:status';
|
||||
const TYPE_LIMIT = 'project:col:limit';
|
||||
extends PhabricatorModularTransaction {
|
||||
|
||||
public function getApplicationName() {
|
||||
return 'project';
|
||||
|
@ -15,68 +11,8 @@ final class PhabricatorProjectColumnTransaction
|
|||
return PhabricatorProjectColumnPHIDType::TYPECONST;
|
||||
}
|
||||
|
||||
public function getTitle() {
|
||||
$old = $this->getOldValue();
|
||||
$new = $this->getNewValue();
|
||||
$author_handle = $this->renderHandleLink($this->getAuthorPHID());
|
||||
|
||||
switch ($this->getTransactionType()) {
|
||||
case self::TYPE_NAME:
|
||||
if ($old === null) {
|
||||
return pht(
|
||||
'%s created this column.',
|
||||
$author_handle);
|
||||
} else {
|
||||
if (!strlen($old)) {
|
||||
return pht(
|
||||
'%s named this column "%s".',
|
||||
$author_handle,
|
||||
$new);
|
||||
} else if (strlen($new)) {
|
||||
return pht(
|
||||
'%s renamed this column from "%s" to "%s".',
|
||||
$author_handle,
|
||||
$old,
|
||||
$new);
|
||||
} else {
|
||||
return pht(
|
||||
'%s removed the custom name of this column.',
|
||||
$author_handle);
|
||||
}
|
||||
}
|
||||
case self::TYPE_LIMIT:
|
||||
if (!$old) {
|
||||
return pht(
|
||||
'%s set the point limit for this column to %s.',
|
||||
$author_handle,
|
||||
$new);
|
||||
} else if (!$new) {
|
||||
return pht(
|
||||
'%s removed the point limit for this column.',
|
||||
$author_handle);
|
||||
} else {
|
||||
return pht(
|
||||
'%s changed point limit for this column from %s to %s.',
|
||||
$author_handle,
|
||||
$old,
|
||||
$new);
|
||||
}
|
||||
|
||||
case self::TYPE_STATUS:
|
||||
switch ($new) {
|
||||
case PhabricatorProjectColumn::STATUS_ACTIVE:
|
||||
return pht(
|
||||
'%s marked this column visible.',
|
||||
$author_handle);
|
||||
case PhabricatorProjectColumn::STATUS_HIDDEN:
|
||||
return pht(
|
||||
'%s marked this column hidden.',
|
||||
$author_handle);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return parent::getTitle();
|
||||
public function getBaseTransactionClass() {
|
||||
return 'PhabricatorProjectColumnTransactionType';
|
||||
}
|
||||
|
||||
}
|
||||
|
|
336
src/applications/project/storage/PhabricatorProjectTrigger.php
Normal file
336
src/applications/project/storage/PhabricatorProjectTrigger.php
Normal file
|
@ -0,0 +1,336 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorProjectTrigger
|
||||
extends PhabricatorProjectDAO
|
||||
implements
|
||||
PhabricatorApplicationTransactionInterface,
|
||||
PhabricatorPolicyInterface,
|
||||
PhabricatorIndexableInterface,
|
||||
PhabricatorDestructibleInterface {
|
||||
|
||||
protected $name;
|
||||
protected $ruleset = array();
|
||||
protected $editPolicy;
|
||||
|
||||
private $triggerRules;
|
||||
private $usage = self::ATTACHABLE;
|
||||
|
||||
public static function initializeNewTrigger() {
|
||||
$default_edit = PhabricatorPolicies::POLICY_USER;
|
||||
|
||||
return id(new self())
|
||||
->setName('')
|
||||
->setEditPolicy($default_edit);
|
||||
}
|
||||
|
||||
protected function getConfiguration() {
|
||||
return array(
|
||||
self::CONFIG_AUX_PHID => true,
|
||||
self::CONFIG_SERIALIZATION => array(
|
||||
'ruleset' => self::SERIALIZATION_JSON,
|
||||
),
|
||||
self::CONFIG_COLUMN_SCHEMA => array(
|
||||
'name' => 'text255',
|
||||
),
|
||||
self::CONFIG_KEY_SCHEMA => array(
|
||||
),
|
||||
) + parent::getConfiguration();
|
||||
}
|
||||
|
||||
public function getPHIDType() {
|
||||
return PhabricatorProjectTriggerPHIDType::TYPECONST;
|
||||
}
|
||||
|
||||
public function getDisplayName() {
|
||||
$name = $this->getName();
|
||||
if (strlen($name)) {
|
||||
return $name;
|
||||
}
|
||||
|
||||
return $this->getDefaultName();
|
||||
}
|
||||
|
||||
public function getDefaultName() {
|
||||
return pht('Custom Trigger');
|
||||
}
|
||||
|
||||
public function getURI() {
|
||||
return urisprintf(
|
||||
'/project/trigger/%d/',
|
||||
$this->getID());
|
||||
}
|
||||
|
||||
public function getObjectName() {
|
||||
return pht('Trigger %d', $this->getID());
|
||||
}
|
||||
|
||||
public function setRuleset(array $ruleset) {
|
||||
// Clear any cached trigger rules, since we're changing the ruleset
|
||||
// for the trigger.
|
||||
$this->triggerRules = null;
|
||||
|
||||
parent::setRuleset($ruleset);
|
||||
}
|
||||
|
||||
public function getTriggerRules() {
|
||||
if ($this->triggerRules === null) {
|
||||
$trigger_rules = self::newTriggerRulesFromRuleSpecifications(
|
||||
$this->getRuleset(),
|
||||
$allow_invalid = true);
|
||||
|
||||
$this->triggerRules = $trigger_rules;
|
||||
}
|
||||
|
||||
return $this->triggerRules;
|
||||
}
|
||||
|
||||
public static function newTriggerRulesFromRuleSpecifications(
|
||||
array $list,
|
||||
$allow_invalid) {
|
||||
|
||||
// NOTE: With "$allow_invalid" set, we're trying to preserve the database
|
||||
// state in the rule structure, even if it includes rule types we don't
|
||||
// ha ve implementations for, or rules with invalid rule values.
|
||||
|
||||
// If an administrator adds or removes extensions which add rules, or
|
||||
// an upgrade affects rule validity, existing rules may become invalid.
|
||||
// When they do, we still want the UI to reflect the ruleset state
|
||||
// accurately and "Edit" + "Save" shouldn't destroy data unless the
|
||||
// user explicitly modifies the ruleset.
|
||||
|
||||
// In this mode, when we run into rules which are structured correctly but
|
||||
// which have types we don't know about, we replace them with "Unknown
|
||||
// Rules". If we know about the type of a rule but the value doesn't
|
||||
// validate, we replace it with "Invalid Rules". These two rule types don't
|
||||
// take any actions when a card is dropped into the column, but they show
|
||||
// the user what's wrong with the ruleset and can be saved without causing
|
||||
// any collateral damage.
|
||||
|
||||
$rule_map = PhabricatorProjectTriggerRule::getAllTriggerRules();
|
||||
|
||||
// If the stored rule data isn't a list of rules (or we encounter other
|
||||
// fundamental structural problems, below), there isn't much we can do
|
||||
// to try to represent the state.
|
||||
if (!is_array($list)) {
|
||||
throw new PhabricatorProjectTriggerCorruptionException(
|
||||
pht(
|
||||
'Trigger ruleset is corrupt: expected a list of rule '.
|
||||
'specifications, found "%s".',
|
||||
phutil_describe_type($list)));
|
||||
}
|
||||
|
||||
$trigger_rules = array();
|
||||
foreach ($list as $key => $rule) {
|
||||
if (!is_array($rule)) {
|
||||
throw new PhabricatorProjectTriggerCorruptionException(
|
||||
pht(
|
||||
'Trigger ruleset is corrupt: rule (with key "%s") should be a '.
|
||||
'rule specification, but is actually "%s".',
|
||||
$key,
|
||||
phutil_describe_type($rule)));
|
||||
}
|
||||
|
||||
try {
|
||||
PhutilTypeSpec::checkMap(
|
||||
$rule,
|
||||
array(
|
||||
'type' => 'string',
|
||||
'value' => 'wild',
|
||||
));
|
||||
} catch (PhutilTypeCheckException $ex) {
|
||||
throw new PhabricatorProjectTriggerCorruptionException(
|
||||
pht(
|
||||
'Trigger ruleset is corrupt: rule (with key "%s") is not a '.
|
||||
'valid rule specification: %s',
|
||||
$key,
|
||||
$ex->getMessage()));
|
||||
}
|
||||
|
||||
$record = id(new PhabricatorProjectTriggerRuleRecord())
|
||||
->setType(idx($rule, 'type'))
|
||||
->setValue(idx($rule, 'value'));
|
||||
|
||||
if (!isset($rule_map[$record->getType()])) {
|
||||
if (!$allow_invalid) {
|
||||
throw new PhabricatorProjectTriggerCorruptionException(
|
||||
pht(
|
||||
'Trigger ruleset is corrupt: rule type "%s" is unknown.',
|
||||
$record->getType()));
|
||||
}
|
||||
|
||||
$rule = new PhabricatorProjectTriggerUnknownRule();
|
||||
} else {
|
||||
$rule = clone $rule_map[$record->getType()];
|
||||
}
|
||||
|
||||
try {
|
||||
$rule->setRecord($record);
|
||||
} catch (Exception $ex) {
|
||||
if (!$allow_invalid) {
|
||||
throw new PhabricatorProjectTriggerCorruptionException(
|
||||
pht(
|
||||
'Trigger ruleset is corrupt, rule (of type "%s") does not '.
|
||||
'validate: %s',
|
||||
$record->getType(),
|
||||
$ex->getMessage()));
|
||||
}
|
||||
|
||||
$rule = id(new PhabricatorProjectTriggerInvalidRule())
|
||||
->setRecord($record)
|
||||
->setException($ex);
|
||||
}
|
||||
|
||||
$trigger_rules[] = $rule;
|
||||
}
|
||||
|
||||
return $trigger_rules;
|
||||
}
|
||||
|
||||
|
||||
public function getDropEffects() {
|
||||
$effects = array();
|
||||
|
||||
$rules = $this->getTriggerRules();
|
||||
foreach ($rules as $rule) {
|
||||
foreach ($rule->getDropEffects() as $effect) {
|
||||
$effects[] = $effect;
|
||||
}
|
||||
}
|
||||
|
||||
return $effects;
|
||||
}
|
||||
|
||||
public function newDropTransactions(
|
||||
PhabricatorUser $viewer,
|
||||
PhabricatorProjectColumn $column,
|
||||
$object) {
|
||||
|
||||
$trigger_xactions = array();
|
||||
foreach ($this->getTriggerRules() as $rule) {
|
||||
$rule
|
||||
->setViewer($viewer)
|
||||
->setTrigger($this)
|
||||
->setColumn($column)
|
||||
->setObject($object);
|
||||
|
||||
$xactions = $rule->getDropTransactions(
|
||||
$object,
|
||||
$rule->getRecord()->getValue());
|
||||
|
||||
if (!is_array($xactions)) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Expected trigger rule (of class "%s") to return a list of '.
|
||||
'transactions from "newDropTransactions()", but got "%s".',
|
||||
get_class($rule),
|
||||
phutil_describe_type($xactions)));
|
||||
}
|
||||
|
||||
$expect_type = get_class($object->getApplicationTransactionTemplate());
|
||||
assert_instances_of($xactions, $expect_type);
|
||||
|
||||
foreach ($xactions as $xaction) {
|
||||
$trigger_xactions[] = $xaction;
|
||||
}
|
||||
}
|
||||
|
||||
return $trigger_xactions;
|
||||
}
|
||||
|
||||
public function getPreviewEffect() {
|
||||
$header = pht('Trigger: %s', $this->getDisplayName());
|
||||
|
||||
return id(new PhabricatorProjectDropEffect())
|
||||
->setIcon('fa-cogs')
|
||||
->setColor('blue')
|
||||
->setIsHeader(true)
|
||||
->setContent($header);
|
||||
}
|
||||
|
||||
public function getSoundEffects() {
|
||||
$sounds = array();
|
||||
|
||||
foreach ($this->getTriggerRules() as $rule) {
|
||||
foreach ($rule->getSoundEffects() as $effect) {
|
||||
$sounds[] = $effect;
|
||||
}
|
||||
}
|
||||
|
||||
return $sounds;
|
||||
}
|
||||
|
||||
public function getUsage() {
|
||||
return $this->assertAttached($this->usage);
|
||||
}
|
||||
|
||||
public function attachUsage(PhabricatorProjectTriggerUsage $usage) {
|
||||
$this->usage = $usage;
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/* -( PhabricatorApplicationTransactionInterface )------------------------- */
|
||||
|
||||
|
||||
public function getApplicationTransactionEditor() {
|
||||
return new PhabricatorProjectTriggerEditor();
|
||||
}
|
||||
|
||||
public function getApplicationTransactionTemplate() {
|
||||
return new PhabricatorProjectTriggerTransaction();
|
||||
}
|
||||
|
||||
|
||||
/* -( PhabricatorPolicyInterface )----------------------------------------- */
|
||||
|
||||
|
||||
public function getCapabilities() {
|
||||
return array(
|
||||
PhabricatorPolicyCapability::CAN_VIEW,
|
||||
PhabricatorPolicyCapability::CAN_EDIT,
|
||||
);
|
||||
}
|
||||
|
||||
public function getPolicy($capability) {
|
||||
switch ($capability) {
|
||||
case PhabricatorPolicyCapability::CAN_VIEW:
|
||||
return PhabricatorPolicies::getMostOpenPolicy();
|
||||
case PhabricatorPolicyCapability::CAN_EDIT:
|
||||
return $this->getEditPolicy();
|
||||
}
|
||||
}
|
||||
|
||||
public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/* -( PhabricatorDestructibleInterface )----------------------------------- */
|
||||
|
||||
|
||||
public function destroyObjectPermanently(
|
||||
PhabricatorDestructionEngine $engine) {
|
||||
|
||||
$this->openTransaction();
|
||||
$conn = $this->establishConnection('w');
|
||||
|
||||
// Remove the reference to this trigger from any columns which use it.
|
||||
queryfx(
|
||||
$conn,
|
||||
'UPDATE %R SET triggerPHID = null WHERE triggerPHID = %s',
|
||||
new PhabricatorProjectColumn(),
|
||||
$this->getPHID());
|
||||
|
||||
// Remove the usage index row for this trigger, if one exists.
|
||||
queryfx(
|
||||
$conn,
|
||||
'DELETE FROM %R WHERE triggerPHID = %s',
|
||||
new PhabricatorProjectTriggerUsage(),
|
||||
$this->getPHID());
|
||||
|
||||
$this->delete();
|
||||
|
||||
$this->saveTransaction();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorProjectTriggerTransaction
|
||||
extends PhabricatorModularTransaction {
|
||||
|
||||
public function getApplicationName() {
|
||||
return 'project';
|
||||
}
|
||||
|
||||
public function getApplicationTransactionType() {
|
||||
return PhabricatorProjectTriggerPHIDType::TYPECONST;
|
||||
}
|
||||
|
||||
public function getBaseTransactionClass() {
|
||||
return 'PhabricatorProjectTriggerTransactionType';
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorProjectTriggerUsage
|
||||
extends PhabricatorProjectDAO {
|
||||
|
||||
protected $triggerPHID;
|
||||
protected $examplePHID;
|
||||
protected $columnCount;
|
||||
protected $activeColumnCount;
|
||||
|
||||
protected function getConfiguration() {
|
||||
return array(
|
||||
self::CONFIG_TIMESTAMPS => false,
|
||||
self::CONFIG_COLUMN_SCHEMA => array(
|
||||
'examplePHID' => 'phid?',
|
||||
'columnCount' => 'uint32',
|
||||
'activeColumnCount' => 'uint32',
|
||||
),
|
||||
self::CONFIG_KEY_SCHEMA => array(
|
||||
'key_trigger' => array(
|
||||
'columns' => array('triggerPHID'),
|
||||
'unique' => true,
|
||||
),
|
||||
),
|
||||
) + parent::getConfiguration();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorProjectTriggerInvalidRule
|
||||
extends PhabricatorProjectTriggerRule {
|
||||
|
||||
const TRIGGERTYPE = 'invalid';
|
||||
|
||||
private $exception;
|
||||
|
||||
public function setException(Exception $exception) {
|
||||
$this->exception = $exception;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getException() {
|
||||
return $this->exception;
|
||||
}
|
||||
|
||||
public function getSelectControlName() {
|
||||
return pht('(Invalid Rule)');
|
||||
}
|
||||
|
||||
protected function isSelectableRule() {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function assertValidRuleValue($value) {
|
||||
return;
|
||||
}
|
||||
|
||||
protected function newDropTransactions($object, $value) {
|
||||
return array();
|
||||
}
|
||||
|
||||
protected function newDropEffects($value) {
|
||||
return array();
|
||||
}
|
||||
|
||||
protected function isValidRule() {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function newInvalidView() {
|
||||
return array(
|
||||
id(new PHUIIconView())
|
||||
->setIcon('fa-exclamation-triangle red'),
|
||||
' ',
|
||||
pht(
|
||||
'This is a trigger rule with a valid type ("%s") but an invalid '.
|
||||
'value.',
|
||||
$this->getRecord()->getType()),
|
||||
);
|
||||
}
|
||||
|
||||
protected function getDefaultValue() {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function getPHUIXControlType() {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function getPHUIXControlSpecification() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getRuleViewLabel() {
|
||||
return pht('Invalid Rule');
|
||||
}
|
||||
|
||||
public function getRuleViewDescription($value) {
|
||||
$record = $this->getRecord();
|
||||
$type = $record->getType();
|
||||
|
||||
$exception = $this->getException();
|
||||
if ($exception) {
|
||||
return pht(
|
||||
'This rule (of type "%s") is invalid: %s',
|
||||
$type,
|
||||
$exception->getMessage());
|
||||
} else {
|
||||
return pht(
|
||||
'This rule (of type "%s") is invalid.',
|
||||
$type);
|
||||
}
|
||||
}
|
||||
|
||||
public function getRuleViewIcon($value) {
|
||||
return id(new PHUIIconView())
|
||||
->setIcon('fa-exclamation-triangle', 'red');
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorProjectTriggerManiphestPriorityRule
|
||||
extends PhabricatorProjectTriggerRule {
|
||||
|
||||
const TRIGGERTYPE = 'task.priority';
|
||||
|
||||
public function getSelectControlName() {
|
||||
return pht('Change priority to');
|
||||
}
|
||||
|
||||
protected function assertValidRuleValue($value) {
|
||||
if (!is_string($value)) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Priority rule value should be a string, but is not (value is "%s").',
|
||||
phutil_describe_type($value)));
|
||||
}
|
||||
|
||||
$map = ManiphestTaskPriority::getTaskPriorityMap();
|
||||
if (!isset($map[$value])) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Rule value ("%s") is not a valid task priority.',
|
||||
$value));
|
||||
}
|
||||
}
|
||||
|
||||
protected function newDropTransactions($object, $value) {
|
||||
$value = ManiphestTaskPriority::getKeywordForTaskPriority($value);
|
||||
return array(
|
||||
$this->newTransaction()
|
||||
->setTransactionType(ManiphestTaskPriorityTransaction::TRANSACTIONTYPE)
|
||||
->setNewValue($value),
|
||||
);
|
||||
}
|
||||
|
||||
protected function newDropEffects($value) {
|
||||
$priority_name = ManiphestTaskPriority::getTaskPriorityName($value);
|
||||
$priority_icon = ManiphestTaskPriority::getTaskPriorityIcon($value);
|
||||
$priority_color = ManiphestTaskPriority::getTaskPriorityColor($value);
|
||||
|
||||
$content = pht(
|
||||
'Change priority to %s.',
|
||||
phutil_tag('strong', array(), $priority_name));
|
||||
|
||||
return array(
|
||||
$this->newEffect()
|
||||
->setIcon($priority_icon)
|
||||
->setColor($priority_color)
|
||||
->addCondition('priority', '!=', $value)
|
||||
->setContent($content),
|
||||
);
|
||||
}
|
||||
|
||||
protected function getDefaultValue() {
|
||||
return head_key(ManiphestTaskPriority::getTaskPriorityMap());
|
||||
}
|
||||
|
||||
protected function getPHUIXControlType() {
|
||||
return 'select';
|
||||
}
|
||||
|
||||
protected function getPHUIXControlSpecification() {
|
||||
$map = ManiphestTaskPriority::getTaskPriorityMap();
|
||||
|
||||
return array(
|
||||
'options' => $map,
|
||||
'order' => array_keys($map),
|
||||
);
|
||||
}
|
||||
|
||||
public function getRuleViewLabel() {
|
||||
return pht('Change Priority');
|
||||
}
|
||||
|
||||
public function getRuleViewDescription($value) {
|
||||
$priority_name = ManiphestTaskPriority::getTaskPriorityName($value);
|
||||
|
||||
return pht(
|
||||
'Change task priority to %s.',
|
||||
phutil_tag('strong', array(), $priority_name));
|
||||
}
|
||||
|
||||
public function getRuleViewIcon($value) {
|
||||
$priority_icon = ManiphestTaskPriority::getTaskPriorityIcon($value);
|
||||
$priority_color = ManiphestTaskPriority::getTaskPriorityColor($value);
|
||||
|
||||
return id(new PHUIIconView())
|
||||
->setIcon($priority_icon, $priority_color);
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorProjectTriggerManiphestStatusRule
|
||||
extends PhabricatorProjectTriggerRule {
|
||||
|
||||
const TRIGGERTYPE = 'task.status';
|
||||
|
||||
public function getSelectControlName() {
|
||||
return pht('Change status to');
|
||||
}
|
||||
|
||||
protected function assertValidRuleValue($value) {
|
||||
if (!is_string($value)) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Status rule value should be a string, but is not (value is "%s").',
|
||||
phutil_describe_type($value)));
|
||||
}
|
||||
|
||||
$map = ManiphestTaskStatus::getTaskStatusMap();
|
||||
if (!isset($map[$value])) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Rule value ("%s") is not a valid task status.',
|
||||
$value));
|
||||
}
|
||||
}
|
||||
|
||||
protected function newDropTransactions($object, $value) {
|
||||
return array(
|
||||
$this->newTransaction()
|
||||
->setTransactionType(ManiphestTaskStatusTransaction::TRANSACTIONTYPE)
|
||||
->setNewValue($value),
|
||||
);
|
||||
}
|
||||
|
||||
protected function newDropEffects($value) {
|
||||
$status_name = ManiphestTaskStatus::getTaskStatusName($value);
|
||||
$status_icon = ManiphestTaskStatus::getStatusIcon($value);
|
||||
$status_color = ManiphestTaskStatus::getStatusColor($value);
|
||||
|
||||
$content = pht(
|
||||
'Change status to %s.',
|
||||
phutil_tag('strong', array(), $status_name));
|
||||
|
||||
return array(
|
||||
$this->newEffect()
|
||||
->setIcon($status_icon)
|
||||
->setColor($status_color)
|
||||
->addCondition('status', '!=', $value)
|
||||
->setContent($content),
|
||||
);
|
||||
}
|
||||
|
||||
protected function getDefaultValue() {
|
||||
return head_key(ManiphestTaskStatus::getTaskStatusMap());
|
||||
}
|
||||
|
||||
protected function getPHUIXControlType() {
|
||||
return 'select';
|
||||
}
|
||||
|
||||
protected function getPHUIXControlSpecification() {
|
||||
$map = ManiphestTaskStatus::getTaskStatusMap();
|
||||
|
||||
return array(
|
||||
'options' => $map,
|
||||
'order' => array_keys($map),
|
||||
);
|
||||
}
|
||||
|
||||
public function getRuleViewLabel() {
|
||||
return pht('Change Status');
|
||||
}
|
||||
|
||||
public function getRuleViewDescription($value) {
|
||||
$status_name = ManiphestTaskStatus::getTaskStatusName($value);
|
||||
|
||||
return pht(
|
||||
'Change task status to %s.',
|
||||
phutil_tag('strong', array(), $status_name));
|
||||
}
|
||||
|
||||
public function getRuleViewIcon($value) {
|
||||
$status_icon = ManiphestTaskStatus::getStatusIcon($value);
|
||||
$status_color = ManiphestTaskStatus::getStatusColor($value);
|
||||
|
||||
return id(new PHUIIconView())
|
||||
->setIcon($status_icon, $status_color);
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorProjectTriggerPlaySoundRule
|
||||
extends PhabricatorProjectTriggerRule {
|
||||
|
||||
const TRIGGERTYPE = 'sound';
|
||||
|
||||
public function getSelectControlName() {
|
||||
return pht('Play sound');
|
||||
}
|
||||
|
||||
protected function assertValidRuleValue($value) {
|
||||
if (!is_string($value)) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Status rule value should be a string, but is not (value is "%s").',
|
||||
phutil_describe_type($value)));
|
||||
}
|
||||
|
||||
$map = self::getSoundMap();
|
||||
|
||||
if (!isset($map[$value])) {
|
||||
throw new Exception(
|
||||
pht(
|
||||
'Rule value ("%s") is not a valid sound.',
|
||||
$value));
|
||||
}
|
||||
}
|
||||
|
||||
protected function newDropTransactions($object, $value) {
|
||||
return array();
|
||||
}
|
||||
|
||||
protected function newDropEffects($value) {
|
||||
$sound_icon = 'fa-volume-up';
|
||||
$sound_color = 'blue';
|
||||
$sound_name = self::getSoundName($value);
|
||||
|
||||
$content = pht(
|
||||
'Play sound %s.',
|
||||
phutil_tag('strong', array(), $sound_name));
|
||||
|
||||
return array(
|
||||
$this->newEffect()
|
||||
->setIcon($sound_icon)
|
||||
->setColor($sound_color)
|
||||
->setContent($content),
|
||||
);
|
||||
}
|
||||
|
||||
protected function getDefaultValue() {
|
||||
return head_key(self::getSoundMap());
|
||||
}
|
||||
|
||||
protected function getPHUIXControlType() {
|
||||
return 'select';
|
||||
}
|
||||
|
||||
protected function getPHUIXControlSpecification() {
|
||||
$map = self::getSoundMap();
|
||||
$map = ipull($map, 'name');
|
||||
|
||||
return array(
|
||||
'options' => $map,
|
||||
'order' => array_keys($map),
|
||||
);
|
||||
}
|
||||
|
||||
public function getRuleViewLabel() {
|
||||
return pht('Play Sound');
|
||||
}
|
||||
|
||||
public function getRuleViewDescription($value) {
|
||||
$sound_name = self::getSoundName($value);
|
||||
|
||||
return pht(
|
||||
'Play sound %s.',
|
||||
phutil_tag('strong', array(), $sound_name));
|
||||
}
|
||||
|
||||
public function getRuleViewIcon($value) {
|
||||
$sound_icon = 'fa-volume-up';
|
||||
$sound_color = 'blue';
|
||||
|
||||
return id(new PHUIIconView())
|
||||
->setIcon($sound_icon, $sound_color);
|
||||
}
|
||||
|
||||
private static function getSoundName($value) {
|
||||
$map = self::getSoundMap();
|
||||
$spec = idx($map, $value, array());
|
||||
return idx($spec, 'name', $value);
|
||||
}
|
||||
|
||||
private static function getSoundMap() {
|
||||
return array(
|
||||
'bing' => array(
|
||||
'name' => pht('Bing'),
|
||||
'uri' => celerity_get_resource_uri('/rsrc/audio/basic/bing.mp3'),
|
||||
),
|
||||
'glass' => array(
|
||||
'name' => pht('Glass'),
|
||||
'uri' => celerity_get_resource_uri('/rsrc/audio/basic/ting.mp3'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public function getSoundEffects() {
|
||||
$value = $this->getValue();
|
||||
|
||||
$map = self::getSoundMap();
|
||||
$spec = idx($map, $value, array());
|
||||
|
||||
$uris = array();
|
||||
if (isset($spec['uri'])) {
|
||||
$uris[] = $spec['uri'];
|
||||
}
|
||||
|
||||
return $uris;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
<?php
|
||||
|
||||
abstract class PhabricatorProjectTriggerRule
|
||||
extends Phobject {
|
||||
|
||||
private $record;
|
||||
private $viewer;
|
||||
private $column;
|
||||
private $trigger;
|
||||
private $object;
|
||||
|
||||
final public function getTriggerType() {
|
||||
return $this->getPhobjectClassConstant('TRIGGERTYPE', 64);
|
||||
}
|
||||
|
||||
final public static function getAllTriggerRules() {
|
||||
return id(new PhutilClassMapQuery())
|
||||
->setAncestorClass(__CLASS__)
|
||||
->setUniqueMethod('getTriggerType')
|
||||
->execute();
|
||||
}
|
||||
|
||||
final public function setRecord(PhabricatorProjectTriggerRuleRecord $record) {
|
||||
$value = $record->getValue();
|
||||
|
||||
$this->assertValidRuleValue($value);
|
||||
|
||||
$this->record = $record;
|
||||
return $this;
|
||||
}
|
||||
|
||||
final public function getRecord() {
|
||||
return $this->record;
|
||||
}
|
||||
|
||||
final protected function getValue() {
|
||||
return $this->getRecord()->getValue();
|
||||
}
|
||||
|
||||
abstract public function getSelectControlName();
|
||||
abstract public function getRuleViewLabel();
|
||||
abstract public function getRuleViewDescription($value);
|
||||
abstract public function getRuleViewIcon($value);
|
||||
abstract protected function assertValidRuleValue($value);
|
||||
abstract protected function newDropTransactions($object, $value);
|
||||
abstract protected function newDropEffects($value);
|
||||
abstract protected function getDefaultValue();
|
||||
abstract protected function getPHUIXControlType();
|
||||
abstract protected function getPHUIXControlSpecification();
|
||||
|
||||
protected function isSelectableRule() {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function isValidRule() {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function newInvalidView() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getSoundEffects() {
|
||||
return array();
|
||||
}
|
||||
|
||||
final public function getDropTransactions($object, $value) {
|
||||
return $this->newDropTransactions($object, $value);
|
||||
}
|
||||
|
||||
final public function setViewer(PhabricatorUser $viewer) {
|
||||
$this->viewer = $viewer;
|
||||
return $this;
|
||||
}
|
||||
|
||||
final public function getViewer() {
|
||||
return $this->viewer;
|
||||
}
|
||||
|
||||
final public function setColumn(PhabricatorProjectColumn $column) {
|
||||
$this->column = $column;
|
||||
return $this;
|
||||
}
|
||||
|
||||
final public function getColumn() {
|
||||
return $this->column;
|
||||
}
|
||||
|
||||
final public function setTrigger(PhabricatorProjectTrigger $trigger) {
|
||||
$this->trigger = $trigger;
|
||||
return $this;
|
||||
}
|
||||
|
||||
final public function getTrigger() {
|
||||
return $this->trigger;
|
||||
}
|
||||
|
||||
final public function setObject(
|
||||
PhabricatorApplicationTransactionInterface $object) {
|
||||
$this->object = $object;
|
||||
return $this;
|
||||
}
|
||||
|
||||
final public function getObject() {
|
||||
return $this->object;
|
||||
}
|
||||
|
||||
final protected function newTransaction() {
|
||||
return $this->getObject()->getApplicationTransactionTemplate();
|
||||
}
|
||||
|
||||
final public function getDropEffects() {
|
||||
return $this->newDropEffects($this->getValue());
|
||||
}
|
||||
|
||||
final protected function newEffect() {
|
||||
return id(new PhabricatorProjectDropEffect())
|
||||
->setIsTriggerEffect(true);
|
||||
}
|
||||
|
||||
final public function toDictionary() {
|
||||
$record = $this->getRecord();
|
||||
|
||||
$is_valid = $this->isValidRule();
|
||||
if (!$is_valid) {
|
||||
$invalid_view = hsprintf('%s', $this->newInvalidView());
|
||||
} else {
|
||||
$invalid_view = null;
|
||||
}
|
||||
|
||||
return array(
|
||||
'type' => $record->getType(),
|
||||
'value' => $record->getValue(),
|
||||
'isValidRule' => $is_valid,
|
||||
'invalidView' => $invalid_view,
|
||||
);
|
||||
}
|
||||
|
||||
final public function newTemplate() {
|
||||
return array(
|
||||
'type' => $this->getTriggerType(),
|
||||
'name' => $this->getSelectControlName(),
|
||||
'selectable' => $this->isSelectableRule(),
|
||||
'defaultValue' => $this->getDefaultValue(),
|
||||
'control' => array(
|
||||
'type' => $this->getPHUIXControlType(),
|
||||
'specification' => $this->getPHUIXControlSpecification(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorProjectTriggerRuleRecord
|
||||
extends Phobject {
|
||||
|
||||
private $type;
|
||||
private $value;
|
||||
|
||||
public function setType($type) {
|
||||
$this->type = $type;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getType() {
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
public function setValue($value) {
|
||||
$this->value = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getValue() {
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorProjectTriggerUnknownRule
|
||||
extends PhabricatorProjectTriggerRule {
|
||||
|
||||
const TRIGGERTYPE = 'unknown';
|
||||
|
||||
public function getSelectControlName() {
|
||||
return pht('(Unknown Rule)');
|
||||
}
|
||||
|
||||
protected function isSelectableRule() {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function assertValidRuleValue($value) {
|
||||
return;
|
||||
}
|
||||
|
||||
protected function newDropTransactions($object, $value) {
|
||||
return array();
|
||||
}
|
||||
|
||||
protected function newDropEffects($value) {
|
||||
return array();
|
||||
}
|
||||
|
||||
protected function isValidRule() {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function newInvalidView() {
|
||||
return array(
|
||||
id(new PHUIIconView())
|
||||
->setIcon('fa-exclamation-triangle yellow'),
|
||||
' ',
|
||||
pht(
|
||||
'This is a trigger rule with a unknown type ("%s").',
|
||||
$this->getRecord()->getType()),
|
||||
);
|
||||
}
|
||||
|
||||
protected function getDefaultValue() {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function getPHUIXControlType() {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function getPHUIXControlSpecification() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getRuleViewLabel() {
|
||||
return pht('Unknown Rule');
|
||||
}
|
||||
|
||||
public function getRuleViewDescription($value) {
|
||||
return pht(
|
||||
'This is an unknown rule of type "%s". An administrator may have '.
|
||||
'edited or removed an extension which implements this rule type.',
|
||||
$this->getRecord()->getType());
|
||||
}
|
||||
|
||||
public function getRuleViewIcon($value) {
|
||||
return id(new PHUIIconView())
|
||||
->setIcon('fa-question-circle', 'yellow');
|
||||
}
|
||||
|
||||
}
|
|
@ -6,6 +6,7 @@ final class ProjectBoardTaskCard extends Phobject {
|
|||
private $projectHandles;
|
||||
private $task;
|
||||
private $owner;
|
||||
private $showEditControls;
|
||||
private $canEdit;
|
||||
private $coverImageFile;
|
||||
private $hideArchivedProjects;
|
||||
|
@ -70,6 +71,15 @@ final class ProjectBoardTaskCard extends Phobject {
|
|||
return $this->canEdit;
|
||||
}
|
||||
|
||||
public function setShowEditControls($show_edit_controls) {
|
||||
$this->showEditControls = $show_edit_controls;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getShowEditControls() {
|
||||
return $this->showEditControls;
|
||||
}
|
||||
|
||||
public function getItem() {
|
||||
$task = $this->getTask();
|
||||
$owner = $this->getOwner();
|
||||
|
@ -89,24 +99,26 @@ final class ProjectBoardTaskCard extends Phobject {
|
|||
->setDisabled($task->isClosed())
|
||||
->setBarColor($bar_color);
|
||||
|
||||
if ($can_edit) {
|
||||
$card
|
||||
->addSigil('draggable-card')
|
||||
->addClass('draggable-card');
|
||||
$edit_icon = 'fa-pencil';
|
||||
} else {
|
||||
$card
|
||||
->addClass('not-editable')
|
||||
->addClass('undraggable-card');
|
||||
$edit_icon = 'fa-lock red';
|
||||
}
|
||||
if ($this->getShowEditControls()) {
|
||||
if ($can_edit) {
|
||||
$card
|
||||
->addSigil('draggable-card')
|
||||
->addClass('draggable-card');
|
||||
$edit_icon = 'fa-pencil';
|
||||
} else {
|
||||
$card
|
||||
->addClass('not-editable')
|
||||
->addClass('undraggable-card');
|
||||
$edit_icon = 'fa-lock red';
|
||||
}
|
||||
|
||||
$card->addAction(
|
||||
id(new PHUIListItemView())
|
||||
->setName(pht('Edit'))
|
||||
->setIcon($edit_icon)
|
||||
->addSigil('edit-project-card')
|
||||
->setHref('/maniphest/task/edit/'.$task->getID().'/'));
|
||||
$card->addAction(
|
||||
id(new PHUIListItemView())
|
||||
->setName(pht('Edit'))
|
||||
->setIcon($edit_icon)
|
||||
->addSigil('edit-project-card')
|
||||
->setHref('/maniphest/task/edit/'.$task->getID().'/'));
|
||||
}
|
||||
|
||||
if ($owner) {
|
||||
$card->addHandleIcon($owner, $owner->getName());
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorProjectColumnLimitTransaction
|
||||
extends PhabricatorProjectColumnTransactionType {
|
||||
|
||||
const TRANSACTIONTYPE = 'project:col:limit';
|
||||
|
||||
public function generateOldValue($object) {
|
||||
return $object->getPointLimit();
|
||||
}
|
||||
|
||||
public function generateNewValue($object, $value) {
|
||||
if (strlen($value)) {
|
||||
return (int)$value;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function applyInternalEffects($object, $value) {
|
||||
$object->setPointLimit($value);
|
||||
}
|
||||
|
||||
public function getTitle() {
|
||||
$old = $this->getOldValue();
|
||||
$new = $this->getNewValue();
|
||||
|
||||
if (!$old) {
|
||||
return pht(
|
||||
'%s set the point limit for this column to %s.',
|
||||
$this->renderAuthor(),
|
||||
$this->renderNewValue());
|
||||
} else if (!$new) {
|
||||
return pht(
|
||||
'%s removed the point limit for this column.',
|
||||
$this->renderAuthor());
|
||||
} else {
|
||||
return pht(
|
||||
'%s changed the point limit for this column from %s to %s.',
|
||||
$this->renderAuthor(),
|
||||
$this->renderOldValue(),
|
||||
$this->renderNewValue());
|
||||
}
|
||||
}
|
||||
|
||||
public function validateTransactions($object, array $xactions) {
|
||||
$errors = array();
|
||||
|
||||
foreach ($xactions as $xaction) {
|
||||
$value = $xaction->getNewValue();
|
||||
if (strlen($value) && !preg_match('/^\d+\z/', $value)) {
|
||||
$errors[] = $this->newInvalidError(
|
||||
pht(
|
||||
'Column point limit must either be empty or a nonnegative '.
|
||||
'integer.'),
|
||||
$xaction);
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorProjectColumnNameTransaction
|
||||
extends PhabricatorProjectColumnTransactionType {
|
||||
|
||||
const TRANSACTIONTYPE = 'project:col:name';
|
||||
|
||||
public function generateOldValue($object) {
|
||||
return $object->getName();
|
||||
}
|
||||
|
||||
public function applyInternalEffects($object, $value) {
|
||||
$object->setName($value);
|
||||
}
|
||||
|
||||
public function getTitle() {
|
||||
$old = $this->getOldValue();
|
||||
$new = $this->getNewValue();
|
||||
|
||||
if (!strlen($old)) {
|
||||
return pht(
|
||||
'%s named this column %s.',
|
||||
$this->renderAuthor(),
|
||||
$this->renderNewValue());
|
||||
} else if (strlen($new)) {
|
||||
return pht(
|
||||
'%s renamed this column from %s to %s.',
|
||||
$this->renderAuthor(),
|
||||
$this->renderOldValue(),
|
||||
$this->renderNewValue());
|
||||
} else {
|
||||
return pht(
|
||||
'%s removed the custom name of this column.',
|
||||
$this->renderAuthor());
|
||||
}
|
||||
}
|
||||
|
||||
public function validateTransactions($object, array $xactions) {
|
||||
$errors = array();
|
||||
|
||||
if ($this->isEmptyTextTransaction($object->getName(), $xactions)) {
|
||||
// The default "Backlog" column is allowed to be unnamed, which
|
||||
// means we use the default name.
|
||||
if (!$object->isDefaultColumn()) {
|
||||
$errors[] = $this->newRequiredError(
|
||||
pht('Columns must have a name.'));
|
||||
}
|
||||
}
|
||||
|
||||
$max_length = $object->getColumnMaximumByteLength('name');
|
||||
foreach ($xactions as $xaction) {
|
||||
$new_value = $xaction->getNewValue();
|
||||
$new_length = strlen($new_value);
|
||||
if ($new_length > $max_length) {
|
||||
$errors[] = $this->newInvalidError(
|
||||
pht(
|
||||
'Column names must not be longer than %s characters.',
|
||||
new PhutilNumber($max_length)),
|
||||
$xaction);
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorProjectColumnStatusTransaction
|
||||
extends PhabricatorProjectColumnTransactionType {
|
||||
|
||||
const TRANSACTIONTYPE = 'project:col:status';
|
||||
|
||||
public function generateOldValue($object) {
|
||||
return $object->getStatus();
|
||||
}
|
||||
|
||||
public function applyInternalEffects($object, $value) {
|
||||
$object->setStatus($value);
|
||||
}
|
||||
|
||||
public function applyExternalEffects($object, $value) {
|
||||
// Update the trigger usage index, which cares about whether columns are
|
||||
// active or not.
|
||||
$trigger_phid = $object->getTriggerPHID();
|
||||
if ($trigger_phid) {
|
||||
PhabricatorSearchWorker::queueDocumentForIndexing($trigger_phid);
|
||||
}
|
||||
}
|
||||
|
||||
public function getTitle() {
|
||||
$new = $this->getNewValue();
|
||||
|
||||
switch ($new) {
|
||||
case PhabricatorProjectColumn::STATUS_ACTIVE:
|
||||
return pht(
|
||||
'%s unhid this column.',
|
||||
$this->renderAuthor());
|
||||
case PhabricatorProjectColumn::STATUS_HIDDEN:
|
||||
return pht(
|
||||
'%s hid this column.',
|
||||
$this->renderAuthor());
|
||||
}
|
||||
}
|
||||
|
||||
public function validateTransactions($object, array $xactions) {
|
||||
$errors = array();
|
||||
|
||||
$map = array(
|
||||
PhabricatorProjectColumn::STATUS_ACTIVE,
|
||||
PhabricatorProjectColumn::STATUS_HIDDEN,
|
||||
);
|
||||
$map = array_fuse($map);
|
||||
|
||||
foreach ($xactions as $xaction) {
|
||||
$value = $xaction->getNewValue();
|
||||
if (!isset($map[$value])) {
|
||||
$errors[] = $this->newInvalidError(
|
||||
pht(
|
||||
'Column status "%s" is unrecognized, valid statuses are: %s.',
|
||||
$value,
|
||||
implode(', ', array_keys($map))),
|
||||
$xaction);
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
<?php
|
||||
|
||||
abstract class PhabricatorProjectColumnTransactionType
|
||||
extends PhabricatorModularTransactionType {}
|
|
@ -0,0 +1,97 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorProjectColumnTriggerTransaction
|
||||
extends PhabricatorProjectColumnTransactionType {
|
||||
|
||||
const TRANSACTIONTYPE = 'trigger';
|
||||
|
||||
public function generateOldValue($object) {
|
||||
return $object->getTriggerPHID();
|
||||
}
|
||||
|
||||
public function applyInternalEffects($object, $value) {
|
||||
$object->setTriggerPHID($value);
|
||||
}
|
||||
|
||||
public function applyExternalEffects($object, $value) {
|
||||
// After we change the trigger attached to a column, update the search
|
||||
// indexes for the old and new triggers so we update the usage index.
|
||||
$old = $this->getOldValue();
|
||||
$new = $this->getNewValue();
|
||||
|
||||
$column_phids = array();
|
||||
if ($old) {
|
||||
$column_phids[] = $old;
|
||||
}
|
||||
if ($new) {
|
||||
$column_phids[] = $new;
|
||||
}
|
||||
|
||||
foreach ($column_phids as $phid) {
|
||||
PhabricatorSearchWorker::queueDocumentForIndexing($phid);
|
||||
}
|
||||
}
|
||||
|
||||
public function getTitle() {
|
||||
$old = $this->getOldValue();
|
||||
$new = $this->getNewValue();
|
||||
|
||||
if (!$old) {
|
||||
return pht(
|
||||
'%s set the column trigger to %s.',
|
||||
$this->renderAuthor(),
|
||||
$this->renderNewHandle());
|
||||
} else if (!$new) {
|
||||
return pht(
|
||||
'%s removed the trigger for this column (was %s).',
|
||||
$this->renderAuthor(),
|
||||
$this->renderOldHandle());
|
||||
} else {
|
||||
return pht(
|
||||
'%s changed the trigger for this column from %s to %s.',
|
||||
$this->renderAuthor(),
|
||||
$this->renderOldHandle(),
|
||||
$this->renderNewHandle());
|
||||
}
|
||||
}
|
||||
|
||||
public function validateTransactions($object, array $xactions) {
|
||||
$actor = $this->getActor();
|
||||
$errors = array();
|
||||
|
||||
foreach ($xactions as $xaction) {
|
||||
$trigger_phid = $xaction->getNewValue();
|
||||
|
||||
// You can always remove a trigger.
|
||||
if (!$trigger_phid) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// You can't put a trigger on a column that can't have triggers, like
|
||||
// a backlog column or a proxy column.
|
||||
if (!$object->canHaveTrigger()) {
|
||||
$errors[] = $this->newInvalidError(
|
||||
pht('This column can not have a trigger.'),
|
||||
$xaction);
|
||||
continue;
|
||||
}
|
||||
|
||||
$trigger = id(new PhabricatorProjectTriggerQuery())
|
||||
->setViewer($actor)
|
||||
->withPHIDs(array($trigger_phid))
|
||||
->execute();
|
||||
if (!$trigger) {
|
||||
$errors[] = $this->newInvalidError(
|
||||
pht(
|
||||
'Trigger "%s" is not a valid trigger, or you do not have '.
|
||||
'permission to view it.',
|
||||
$trigger_phid),
|
||||
$xaction);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorProjectTriggerNameTransaction
|
||||
extends PhabricatorProjectTriggerTransactionType {
|
||||
|
||||
const TRANSACTIONTYPE = 'name';
|
||||
|
||||
public function generateOldValue($object) {
|
||||
return $object->getName();
|
||||
}
|
||||
|
||||
public function applyInternalEffects($object, $value) {
|
||||
$object->setName($value);
|
||||
}
|
||||
|
||||
public function getTitle() {
|
||||
$old = $this->getOldValue();
|
||||
$new = $this->getNewValue();
|
||||
|
||||
if (strlen($old) && strlen($new)) {
|
||||
return pht(
|
||||
'%s renamed this trigger from %s to %s.',
|
||||
$this->renderAuthor(),
|
||||
$this->renderOldValue(),
|
||||
$this->renderNewValue());
|
||||
} else if (strlen($new)) {
|
||||
return pht(
|
||||
'%s named this trigger %s.',
|
||||
$this->renderAuthor(),
|
||||
$this->renderNewValue());
|
||||
} else {
|
||||
return pht(
|
||||
'%s stripped the name %s from this trigger.',
|
||||
$this->renderAuthor(),
|
||||
$this->renderOldValue());
|
||||
}
|
||||
}
|
||||
|
||||
public function validateTransactions($object, array $xactions) {
|
||||
$errors = array();
|
||||
|
||||
$max_length = $object->getColumnMaximumByteLength('name');
|
||||
foreach ($xactions as $xaction) {
|
||||
$new_value = $xaction->getNewValue();
|
||||
$new_length = strlen($new_value);
|
||||
if ($new_length > $max_length) {
|
||||
$errors[] = $this->newInvalidError(
|
||||
pht(
|
||||
'Trigger names must not be longer than %s characters.',
|
||||
new PhutilNumber($max_length)),
|
||||
$xaction);
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorProjectTriggerRulesetTransaction
|
||||
extends PhabricatorProjectTriggerTransactionType {
|
||||
|
||||
const TRANSACTIONTYPE = 'ruleset';
|
||||
|
||||
public function generateOldValue($object) {
|
||||
return $object->getRuleset();
|
||||
}
|
||||
|
||||
public function applyInternalEffects($object, $value) {
|
||||
$object->setRuleset($value);
|
||||
}
|
||||
|
||||
public function getTitle() {
|
||||
return pht(
|
||||
'%s updated the ruleset for this trigger.',
|
||||
$this->renderAuthor());
|
||||
}
|
||||
|
||||
public function validateTransactions($object, array $xactions) {
|
||||
$errors = array();
|
||||
|
||||
foreach ($xactions as $xaction) {
|
||||
$ruleset = $xaction->getNewValue();
|
||||
|
||||
try {
|
||||
PhabricatorProjectTrigger::newTriggerRulesFromRuleSpecifications(
|
||||
$ruleset,
|
||||
$allow_invalid = false);
|
||||
} catch (PhabricatorProjectTriggerCorruptionException $ex) {
|
||||
$errors[] = $this->newInvalidError(
|
||||
pht(
|
||||
'Ruleset specification is not valid. %s',
|
||||
$ex->getMessage()),
|
||||
$xaction);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
public function hasChangeDetailView() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function newChangeDetailView() {
|
||||
$viewer = $this->getViewer();
|
||||
|
||||
$old = $this->getOldValue();
|
||||
$new = $this->getNewValue();
|
||||
|
||||
$json = new PhutilJSON();
|
||||
$old_json = $json->encodeAsList($old);
|
||||
$new_json = $json->encodeAsList($new);
|
||||
|
||||
return id(new PhabricatorApplicationTransactionTextDiffDetailView())
|
||||
->setViewer($viewer)
|
||||
->setOldText($old_json)
|
||||
->setNewText($new_json);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
<?php
|
||||
|
||||
abstract class PhabricatorProjectTriggerTransactionType
|
||||
extends PhabricatorModularTransactionType {}
|
|
@ -46,6 +46,10 @@ final class PhabricatorRepositoryRepositoryPHIDType
|
|||
->setFullName("{$monogram} {$name}")
|
||||
->setURI($uri)
|
||||
->setMailStampName($monogram);
|
||||
|
||||
if ($repository->getStatus() !== PhabricatorRepository::STATUS_ACTIVE) {
|
||||
$handle->setStatus(PhabricatorObjectHandle::STATUS_CLOSED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -276,9 +276,10 @@ final class PhabricatorApplicationSearchController
|
|||
throw new Exception(
|
||||
pht(
|
||||
'SearchEngines must render a "%s" object, but this engine '.
|
||||
'(of class "%s") rendered something else.',
|
||||
'(of class "%s") rendered something else ("%s").',
|
||||
'PhabricatorApplicationSearchResultView',
|
||||
get_class($engine)));
|
||||
get_class($engine),
|
||||
phutil_describe_type($list)));
|
||||
}
|
||||
|
||||
if ($list->getObjectList()) {
|
||||
|
@ -849,19 +850,31 @@ final class PhabricatorApplicationSearchController
|
|||
));
|
||||
}
|
||||
|
||||
private function newOverheatedView(array $results) {
|
||||
if ($results) {
|
||||
public static function newOverheatedError($has_results) {
|
||||
$overheated_link = phutil_tag(
|
||||
'a',
|
||||
array(
|
||||
'href' => 'https://phurl.io/u/overheated',
|
||||
'target' => '_blank',
|
||||
),
|
||||
pht('Learn More'));
|
||||
|
||||
if ($has_results) {
|
||||
$message = pht(
|
||||
'Most objects matching your query are not visible to you, so '.
|
||||
'filtering results is taking a long time. Only some results are '.
|
||||
'shown. Refine your query to find results more quickly.');
|
||||
'This query took too long, so only some results are shown. %s',
|
||||
$overheated_link);
|
||||
} else {
|
||||
$message = pht(
|
||||
'Most objects matching your query are not visible to you, so '.
|
||||
'filtering results is taking a long time. Refine your query to '.
|
||||
'find results more quickly.');
|
||||
'This query took too long. %s',
|
||||
$overheated_link);
|
||||
}
|
||||
|
||||
return $message;
|
||||
}
|
||||
|
||||
private function newOverheatedView(array $results) {
|
||||
$message = self::newOverheatedError((bool)$results);
|
||||
|
||||
return id(new PHUIInfoView())
|
||||
->setSeverity(PHUIInfoView::SEVERITY_WARNING)
|
||||
->setFlush(true)
|
||||
|
|
|
@ -917,15 +917,16 @@ abstract class PhabricatorProfileMenuEngine extends Phobject {
|
|||
$list->addItem($view);
|
||||
}
|
||||
|
||||
$action_view = id(new PhabricatorActionListView())
|
||||
->setUser($viewer);
|
||||
|
||||
$item_types = PhabricatorProfileMenuItem::getAllMenuItems();
|
||||
$object = $this->getProfileObject();
|
||||
|
||||
$action_list = id(new PhabricatorActionListView())
|
||||
->setViewer($viewer);
|
||||
|
||||
// See T12167. This makes the "Actions" dropdown button show up in the
|
||||
// page header.
|
||||
$action_list->setID(celerity_generate_unique_node_id());
|
||||
|
||||
$action_list->addAction(
|
||||
id(new PhabricatorActionView())
|
||||
->setLabel(true)
|
||||
|
@ -970,9 +971,6 @@ abstract class PhabricatorProfileMenuEngine extends Phobject {
|
|||
->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
|
||||
->setObjectList($list);
|
||||
|
||||
$panel = id(new PHUICurtainPanelView())
|
||||
->appendChild($action_view);
|
||||
|
||||
$curtain = id(new PHUICurtainView())
|
||||
->setViewer($viewer)
|
||||
->setActionList($action_list);
|
||||
|
|
|
@ -136,7 +136,13 @@ final class PhabricatorSearchManagementIndexWorkflow
|
|||
|
||||
if ($track_skips) {
|
||||
$new_versions = $this->loadIndexVersions($phid);
|
||||
if ($old_versions !== $new_versions) {
|
||||
|
||||
if (!$old_versions && !$new_versions) {
|
||||
// If the document doesn't use an index version, both the lists
|
||||
// of versions will be empty. We still rebuild the index in this
|
||||
// case.
|
||||
$count_updated++;
|
||||
} else if ($old_versions !== $new_versions) {
|
||||
$count_updated++;
|
||||
} else {
|
||||
$count_skipped++;
|
||||
|
|
|
@ -17,6 +17,12 @@ final class PhabricatorConpherenceProfileMenuItem
|
|||
}
|
||||
|
||||
public function canAddToObject($object) {
|
||||
$application = new PhabricatorConpherenceApplication();
|
||||
|
||||
if (!$application->isInstalled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -113,6 +113,11 @@ final class PhabricatorSettingsTimezoneController
|
|||
}
|
||||
|
||||
private function formatOffset($offset) {
|
||||
// This controller works with client-side (Javascript) offsets, which have
|
||||
// the opposite sign we might expect -- for example "UTC-3" is a positive
|
||||
// offset. Invert the sign before rendering the offset.
|
||||
$offset = -1 * $offset;
|
||||
|
||||
$hours = $offset / 60;
|
||||
// Non-integer number of hours off UTC?
|
||||
if ($offset % 60) {
|
||||
|
|
|
@ -57,11 +57,11 @@ final class PhabricatorTimezoneSetting
|
|||
$groups = array();
|
||||
foreach ($timezones as $timezone) {
|
||||
$zone = new DateTimeZone($timezone);
|
||||
$offset = -($zone->getOffset($now) / (60 * 60));
|
||||
$offset = ($zone->getOffset($now) / 60);
|
||||
$groups[$offset][] = $timezone;
|
||||
}
|
||||
|
||||
krsort($groups);
|
||||
ksort($groups);
|
||||
|
||||
$option_groups = array(
|
||||
array(
|
||||
|
@ -71,10 +71,13 @@ final class PhabricatorTimezoneSetting
|
|||
);
|
||||
|
||||
foreach ($groups as $offset => $group) {
|
||||
if ($offset >= 0) {
|
||||
$label = pht('UTC-%d', $offset);
|
||||
$hours = $offset / 60;
|
||||
$minutes = abs($offset % 60);
|
||||
|
||||
if ($offset % 60) {
|
||||
$label = pht('UTC%+d:%02d', $hours, $minutes);
|
||||
} else {
|
||||
$label = pht('UTC+%d', -$offset);
|
||||
$label = pht('UTC%+d', $hours);
|
||||
}
|
||||
|
||||
sort($group);
|
||||
|
|
|
@ -29,7 +29,9 @@ final class PhabricatorApplicationTransactionCommentEditController
|
|||
$handles = $viewer->loadHandles(array($phid));
|
||||
$obj_handle = $handles[$phid];
|
||||
|
||||
if ($request->isDialogFormPost()) {
|
||||
$done_uri = $obj_handle->getURI();
|
||||
|
||||
if ($request->isFormOrHisecPost()) {
|
||||
$text = $request->getStr('text');
|
||||
|
||||
$comment = $xaction->getApplicationTransactionCommentObject();
|
||||
|
@ -41,29 +43,42 @@ final class PhabricatorApplicationTransactionCommentEditController
|
|||
$editor = id(new PhabricatorApplicationTransactionCommentEditor())
|
||||
->setActor($viewer)
|
||||
->setContentSource(PhabricatorContentSource::newFromRequest($request))
|
||||
->setRequest($request)
|
||||
->setCancelURI($done_uri)
|
||||
->applyEdit($xaction, $comment);
|
||||
|
||||
if ($request->isAjax()) {
|
||||
return id(new AphrontAjaxResponse())->setContent(array());
|
||||
} else {
|
||||
return id(new AphrontReloadResponse())->setURI($obj_handle->getURI());
|
||||
return id(new AphrontReloadResponse())->setURI($done_uri);
|
||||
}
|
||||
}
|
||||
|
||||
$errors = array();
|
||||
if ($xaction->getIsMFATransaction()) {
|
||||
$message = pht(
|
||||
'This comment was signed with MFA, so you will be required to '.
|
||||
'provide MFA credentials to make changes.');
|
||||
|
||||
$errors[] = id(new PHUIInfoView())
|
||||
->setSeverity(PHUIInfoView::SEVERITY_MFA)
|
||||
->setErrors(array($message));
|
||||
}
|
||||
|
||||
$form = id(new AphrontFormView())
|
||||
->setUser($viewer)
|
||||
->setFullWidth(true)
|
||||
->appendControl(
|
||||
id(new PhabricatorRemarkupControl())
|
||||
->setName('text')
|
||||
->setValue($xaction->getComment()->getContent()));
|
||||
->setName('text')
|
||||
->setValue($xaction->getComment()->getContent()));
|
||||
|
||||
return $this->newDialog()
|
||||
->setTitle(pht('Edit Comment'))
|
||||
->addHiddenInput('anchor', $request->getStr('anchor'))
|
||||
->appendChild($errors)
|
||||
->appendForm($form)
|
||||
->addSubmitButton(pht('Save Changes'))
|
||||
->addCancelButton($obj_handle->getURI());
|
||||
->addCancelButton($done_uri);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -30,20 +30,24 @@ final class PhabricatorApplicationTransactionCommentRemoveController
|
|||
->withPHIDs(array($obj_phid))
|
||||
->executeOne();
|
||||
|
||||
if ($request->isDialogFormPost()) {
|
||||
$done_uri = $obj_handle->getURI();
|
||||
|
||||
if ($request->isFormOrHisecPost()) {
|
||||
$comment = $xaction->getApplicationTransactionCommentObject()
|
||||
->setContent('')
|
||||
->setIsRemoved(true);
|
||||
|
||||
$editor = id(new PhabricatorApplicationTransactionCommentEditor())
|
||||
->setActor($viewer)
|
||||
->setRequest($request)
|
||||
->setCancelURI($done_uri)
|
||||
->setContentSource(PhabricatorContentSource::newFromRequest($request))
|
||||
->applyEdit($xaction, $comment);
|
||||
|
||||
if ($request->isAjax()) {
|
||||
return id(new AphrontAjaxResponse())->setContent(array());
|
||||
} else {
|
||||
return id(new AphrontReloadResponse())->setURI($obj_handle->getURI());
|
||||
return id(new AphrontReloadResponse())->setURI($done_uri);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,7 +58,6 @@ final class PhabricatorApplicationTransactionCommentRemoveController
|
|||
->setTitle(pht('Remove Comment'));
|
||||
|
||||
$dialog
|
||||
->addHiddenInput('anchor', $request->getStr('anchor'))
|
||||
->appendParagraph(
|
||||
pht(
|
||||
"Removing a comment prevents anyone (including you) from reading ".
|
||||
|
@ -65,7 +68,7 @@ final class PhabricatorApplicationTransactionCommentRemoveController
|
|||
|
||||
$dialog
|
||||
->addSubmitButton(pht('Remove Comment'))
|
||||
->addCancelButton($obj_handle->getURI());
|
||||
->addCancelButton($done_uri);
|
||||
|
||||
return $dialog;
|
||||
}
|
||||
|
|
|
@ -5,6 +5,9 @@ final class PhabricatorApplicationTransactionCommentEditor
|
|||
|
||||
private $contentSource;
|
||||
private $actingAsPHID;
|
||||
private $request;
|
||||
private $cancelURI;
|
||||
private $isNewComment;
|
||||
|
||||
public function setActingAsPHID($acting_as_phid) {
|
||||
$this->actingAsPHID = $acting_as_phid;
|
||||
|
@ -27,6 +30,33 @@ final class PhabricatorApplicationTransactionCommentEditor
|
|||
return $this->contentSource;
|
||||
}
|
||||
|
||||
public function setRequest(AphrontRequest $request) {
|
||||
$this->request = $request;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRequest() {
|
||||
return $this->request;
|
||||
}
|
||||
|
||||
public function setCancelURI($cancel_uri) {
|
||||
$this->cancelURI = $cancel_uri;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCancelURI() {
|
||||
return $this->cancelURI;
|
||||
}
|
||||
|
||||
public function setIsNewComment($is_new) {
|
||||
$this->isNewComment = $is_new;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getIsNewComment() {
|
||||
return $this->isNewComment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a transaction's comment. This method effects the required create,
|
||||
* update or delete to set the transaction's comment to the provided comment.
|
||||
|
@ -39,6 +69,8 @@ final class PhabricatorApplicationTransactionCommentEditor
|
|||
|
||||
$actor = $this->requireActor();
|
||||
|
||||
$this->applyMFAChecks($xaction, $comment);
|
||||
|
||||
$comment->setContentSource($this->getContentSource());
|
||||
$comment->setAuthorPHID($this->getActingAsPHID());
|
||||
|
||||
|
@ -160,5 +192,94 @@ final class PhabricatorApplicationTransactionCommentEditor
|
|||
}
|
||||
}
|
||||
|
||||
private function applyMFAChecks(
|
||||
PhabricatorApplicationTransaction $xaction,
|
||||
PhabricatorApplicationTransactionComment $comment) {
|
||||
$actor = $this->requireActor();
|
||||
|
||||
// We don't do any MFA checks here when you're creating a comment for the
|
||||
// first time (the parent editor handles them for us), so we can just bail
|
||||
// out if this is the creation flow.
|
||||
if ($this->getIsNewComment()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$request = $this->getRequest();
|
||||
if (!$request) {
|
||||
throw new PhutilInvalidStateException('setRequest');
|
||||
}
|
||||
|
||||
$cancel_uri = $this->getCancelURI();
|
||||
if (!strlen($cancel_uri)) {
|
||||
throw new PhutilInvalidStateException('setCancelURI');
|
||||
}
|
||||
|
||||
// If you're deleting a comment, we try to prompt you for MFA if you have
|
||||
// it configured, but do not require that you have it configured. In most
|
||||
// cases, this is administrators removing content.
|
||||
|
||||
// See PHI1173. If you're editing a comment you authored and the original
|
||||
// comment was signed with MFA, you MUST have MFA on your account and you
|
||||
// MUST sign the edit with MFA. Otherwise, we can end up with an MFA badge
|
||||
// on different content than what was signed.
|
||||
|
||||
$want_mfa = false;
|
||||
$need_mfa = false;
|
||||
|
||||
if ($comment->getIsRemoved()) {
|
||||
// Try to prompt on removal.
|
||||
$want_mfa = true;
|
||||
}
|
||||
|
||||
if ($xaction->getIsMFATransaction()) {
|
||||
if ($actor->getPHID() === $xaction->getAuthorPHID()) {
|
||||
// Strictly require MFA if the original transaction was signed and
|
||||
// you're the author.
|
||||
$want_mfa = true;
|
||||
$need_mfa = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$want_mfa) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($need_mfa) {
|
||||
$factors = id(new PhabricatorAuthFactorConfigQuery())
|
||||
->setViewer($actor)
|
||||
->withUserPHIDs(array($this->getActingAsPHID()))
|
||||
->withFactorProviderStatuses(
|
||||
array(
|
||||
PhabricatorAuthFactorProviderStatus::STATUS_ACTIVE,
|
||||
PhabricatorAuthFactorProviderStatus::STATUS_DEPRECATED,
|
||||
))
|
||||
->execute();
|
||||
if (!$factors) {
|
||||
$error = new PhabricatorApplicationTransactionValidationError(
|
||||
$xaction->getTransactionType(),
|
||||
pht('No MFA'),
|
||||
pht(
|
||||
'This comment was signed with MFA, so edits to it must also be '.
|
||||
'signed with MFA. You do not have any MFA factors attached to '.
|
||||
'your account, so you can not sign this edit. Add MFA to your '.
|
||||
'account in Settings.'),
|
||||
$xaction);
|
||||
|
||||
throw new PhabricatorApplicationTransactionValidationException(
|
||||
array(
|
||||
$error,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
$workflow_key = sprintf(
|
||||
'comment.edit(%s, %d)',
|
||||
$xaction->getPHID(),
|
||||
$xaction->getComment()->getID());
|
||||
|
||||
$hisec_token = id(new PhabricatorAuthSessionEngine())
|
||||
->setWorkflowKey($workflow_key)
|
||||
->requireHighSecurityToken($actor, $request, $cancel_uri);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1113,7 +1113,8 @@ abstract class PhabricatorApplicationTransactionEditor
|
|||
$comment_editor = id(new PhabricatorApplicationTransactionCommentEditor())
|
||||
->setActor($actor)
|
||||
->setActingAsPHID($this->getActingAsPHID())
|
||||
->setContentSource($this->getContentSource());
|
||||
->setContentSource($this->getContentSource())
|
||||
->setIsNewComment(true);
|
||||
|
||||
if (!$transaction_open) {
|
||||
$object->openTransaction();
|
||||
|
|
|
@ -153,4 +153,9 @@ final class PhabricatorStandardCustomFieldSelect
|
|||
->setOptions($this->getOptions());
|
||||
}
|
||||
|
||||
protected function newExportFieldType() {
|
||||
return id(new PhabricatorOptionExportField())
|
||||
->setOptions($this->getOptions());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
final class PhabricatorOptionExportField
|
||||
extends PhabricatorExportField {
|
||||
|
||||
private $options;
|
||||
|
||||
public function setOptions(array $options) {
|
||||
$this->options = $options;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getOptions() {
|
||||
return $this->options;
|
||||
}
|
||||
|
||||
public function getNaturalValue($value) {
|
||||
if ($value === null) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (!strlen($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$options = $this->getOptions();
|
||||
|
||||
return array(
|
||||
'value' => (string)$value,
|
||||
'name' => (string)idx($options, $value, $value),
|
||||
);
|
||||
}
|
||||
|
||||
public function getTextValue($value) {
|
||||
$natural_value = $this->getNaturalValue($value);
|
||||
if ($natural_value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $natural_value['name'];
|
||||
}
|
||||
|
||||
public function getPHPExcelValue($value) {
|
||||
return $this->getTextValue($value);
|
||||
}
|
||||
|
||||
}
|
|
@ -4,6 +4,7 @@ final class ManiphestTaskGraph
|
|||
extends PhabricatorObjectGraph {
|
||||
|
||||
private $seedMaps = array();
|
||||
private $isStandalone;
|
||||
|
||||
protected function getEdgeTypes() {
|
||||
return array(
|
||||
|
@ -24,6 +25,15 @@ final class ManiphestTaskGraph
|
|||
return $object->isClosed();
|
||||
}
|
||||
|
||||
public function setIsStandalone($is_standalone) {
|
||||
$this->isStandalone = $is_standalone;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getIsStandalone() {
|
||||
return $this->isStandalone;
|
||||
}
|
||||
|
||||
protected function newTableRow($phid, $object, $trace) {
|
||||
$viewer = $this->getViewer();
|
||||
|
||||
|
@ -132,6 +142,14 @@ final class ManiphestTaskGraph
|
|||
array(
|
||||
true,
|
||||
!$this->getRenderOnlyAdjacentNodes(),
|
||||
))
|
||||
->setDeviceVisibility(
|
||||
array(
|
||||
true,
|
||||
|
||||
// On mobile, we only show the actual graph drawing if we're on the
|
||||
// standalone page, since it can take over the screen otherwise.
|
||||
$this->getIsStandalone(),
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
@ -83,6 +83,13 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery
|
|||
|
||||
$this->applyExternalCursorConstraintsToQuery($query, $cursor);
|
||||
|
||||
// If we have a Ferret fulltext query, copy it to the subquery so that we
|
||||
// generate ranking columns appropriately, and compute the correct object
|
||||
// ranking score for the current query.
|
||||
if ($this->ferretEngine) {
|
||||
$query->withFerretConstraint($this->ferretEngine, $this->ferretTokens);
|
||||
}
|
||||
|
||||
// We're executing the subquery normally to make sure the viewer can
|
||||
// actually see the object, and that it's a completely valid object which
|
||||
// passes all filtering and policy checks. You aren't allowed to use an
|
||||
|
@ -204,6 +211,19 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery
|
|||
get_class($this)));
|
||||
}
|
||||
|
||||
if ($this->supportsFerretEngine()) {
|
||||
if ($this->getFerretTokens()) {
|
||||
$map += array(
|
||||
'rank' =>
|
||||
$cursor->getRawRowProperty(self::FULLTEXT_RANK),
|
||||
'fulltext-modified' =>
|
||||
$cursor->getRawRowProperty(self::FULLTEXT_MODIFIED),
|
||||
'fulltext-created' =>
|
||||
$cursor->getRawRowProperty(self::FULLTEXT_CREATED),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($keys as $key) {
|
||||
if (!array_key_exists($key, $map)) {
|
||||
throw new Exception(
|
||||
|
@ -295,6 +315,8 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery
|
|||
}
|
||||
|
||||
protected function didLoadRawRows(array $rows) {
|
||||
$this->rawCursorRow = last($rows);
|
||||
|
||||
if ($this->ferretEngine) {
|
||||
foreach ($rows as $row) {
|
||||
$phid = $row['phid'];
|
||||
|
@ -312,8 +334,6 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery
|
|||
}
|
||||
}
|
||||
|
||||
$this->rawCursorRow = last($rows);
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
|
@ -467,7 +487,7 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery
|
|||
*/
|
||||
protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) {
|
||||
$where = array();
|
||||
$where[] = $this->buildPagingClause($conn);
|
||||
$where[] = $this->buildPagingWhereClause($conn);
|
||||
$where[] = $this->buildEdgeLogicWhereClause($conn);
|
||||
$where[] = $this->buildSpacesWhereClause($conn);
|
||||
$where[] = $this->buildNgramsWhereClause($conn);
|
||||
|
@ -482,6 +502,7 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery
|
|||
*/
|
||||
protected function buildHavingClause(AphrontDatabaseConnection $conn) {
|
||||
$having = $this->buildHavingClauseParts($conn);
|
||||
$having[] = $this->buildPagingHavingClause($conn);
|
||||
return $this->formatHavingClause($conn, $having);
|
||||
}
|
||||
|
||||
|
@ -539,6 +560,45 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery
|
|||
/* -( Paging )------------------------------------------------------------- */
|
||||
|
||||
|
||||
private function buildPagingWhereClause(AphrontDatabaseConnection $conn) {
|
||||
if ($this->shouldPageWithHavingClause()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->buildPagingClause($conn);
|
||||
}
|
||||
|
||||
private function buildPagingHavingClause(AphrontDatabaseConnection $conn) {
|
||||
if (!$this->shouldPageWithHavingClause()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->buildPagingClause($conn);
|
||||
}
|
||||
|
||||
private function shouldPageWithHavingClause() {
|
||||
// If any of the paging conditions reference dynamic columns, we need to
|
||||
// put the paging conditions in a "HAVING" clause instead of a "WHERE"
|
||||
// clause.
|
||||
|
||||
// For example, this happens when paging on the Ferret "rank" column,
|
||||
// since the "rank" value is computed dynamically in the SELECT statement.
|
||||
|
||||
$orderable = $this->getOrderableColumns();
|
||||
$vector = $this->getOrderVector();
|
||||
|
||||
foreach ($vector as $order) {
|
||||
$key = $order->getOrderKey();
|
||||
$column = $orderable[$key];
|
||||
|
||||
if (!empty($column['having'])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @task paging
|
||||
*/
|
||||
|
@ -655,6 +715,8 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery
|
|||
'reverse' => 'optional bool',
|
||||
'unique' => 'optional bool',
|
||||
'null' => 'optional string|null',
|
||||
'requires-ferret' => 'optional bool',
|
||||
'having' => 'optional bool',
|
||||
));
|
||||
}
|
||||
|
||||
|
@ -1106,6 +1168,7 @@ abstract class PhabricatorCursorPagedPolicyAwareQuery
|
|||
'column' => self::FULLTEXT_RANK,
|
||||
'type' => 'int',
|
||||
'requires-ferret' => true,
|
||||
'having' => true,
|
||||
);
|
||||
$columns['fulltext-created'] = array(
|
||||
'table' => null,
|
||||
|
|
|
@ -12,6 +12,7 @@ final class PHUIObjectItemListView extends AphrontTagView {
|
|||
private $drag;
|
||||
private $allowEmptyList;
|
||||
private $itemClass = 'phui-oi-standard';
|
||||
private $tail = array();
|
||||
|
||||
public function setAllowEmptyList($allow_empty_list) {
|
||||
$this->allowEmptyList = $allow_empty_list;
|
||||
|
@ -72,6 +73,18 @@ final class PHUIObjectItemListView extends AphrontTagView {
|
|||
return 'ul';
|
||||
}
|
||||
|
||||
public function newTailButton() {
|
||||
$button = id(new PHUIButtonView())
|
||||
->setTag('a')
|
||||
->setColor(PHUIButtonView::GREY)
|
||||
->setIcon('fa-chevron-down')
|
||||
->setText(pht('View All Results'));
|
||||
|
||||
$this->tail[] = $button;
|
||||
|
||||
return $button;
|
||||
}
|
||||
|
||||
protected function getTagAttributes() {
|
||||
$classes = array();
|
||||
$classes[] = 'phui-oi-list-view';
|
||||
|
@ -149,9 +162,20 @@ final class PHUIObjectItemListView extends AphrontTagView {
|
|||
$pager = $this->pager;
|
||||
}
|
||||
|
||||
$tail = array();
|
||||
foreach ($this->tail as $tail_item) {
|
||||
$tail[] = phutil_tag(
|
||||
'li',
|
||||
array(
|
||||
'class' => 'phui-oi-tail',
|
||||
),
|
||||
$tail_item);
|
||||
}
|
||||
|
||||
return array(
|
||||
$header,
|
||||
$items,
|
||||
$tail,
|
||||
$pager,
|
||||
$this->renderChildren(),
|
||||
);
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue