1
0
Fork 0
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:
epriestley 2019-03-29 18:30:53 -07:00
commit bc1a879953
120 changed files with 5147 additions and 805 deletions

View file

@ -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',

View file

@ -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};

View 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};

View file

@ -0,0 +1,2 @@
ALTER TABLE {$NAMESPACE}_project.project_column
ADD triggerPHID VARBINARY(64);

View 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};

View file

@ -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',

View file

@ -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');

View file

@ -608,6 +608,7 @@ abstract class PhabricatorController extends AphrontController {
$this->setCurrentApplication($application);
$controller = new LegalpadDocumentSignController();
$controller->setIsSessionGate(true);
return $this->delegateToController($controller);
}

View file

@ -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())

View file

@ -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;
}

View file

@ -512,8 +512,7 @@ abstract class DiffusionController extends PhabricatorController {
->setIcon('fa-code')
->setHref($drequest->generateURI(
array(
'action' => 'branch',
'path' => '/',
'action' => 'browse',
)))
->setSelected($key == 'code'));

View file

@ -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')

View file

@ -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())

View file

@ -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();
}
}

View file

@ -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())) {

View file

@ -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;

View file

@ -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;

View file

@ -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
);

View file

@ -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);
}
}

View file

@ -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) {

View file

@ -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;

View file

@ -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;

View file

@ -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');

View file

@ -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);

View file

@ -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;

View file

@ -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(

View file

@ -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',

View file

@ -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'));

View file

@ -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);

View file

@ -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

View file

@ -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);

View file

@ -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);

View file

@ -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)

View file

@ -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);
}
}

View file

@ -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);

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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,
));
}
}

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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');

View file

@ -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();

View file

@ -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();

View file

@ -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);
}
}

View file

@ -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);

View file

@ -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);

View file

@ -0,0 +1,4 @@
<?php
final class PhabricatorProjectTriggerCorruptionException
extends Exception {}

View file

@ -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;
}
}

View file

@ -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()

View file

@ -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'),
);
}

View file

@ -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(),

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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);

View file

@ -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());
}
}
}

View file

@ -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');

View file

@ -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';
}
}

View file

@ -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.'));
}
}

View file

@ -0,0 +1,10 @@
<?php
final class PhabricatorProjectTriggerTransactionQuery
extends PhabricatorApplicationTransactionQuery {
public function getTemplateApplicationTransaction() {
return new PhabricatorProjectTriggerTransaction();
}
}

View file

@ -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));

View file

@ -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() {

View file

@ -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';
}
}

View 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();
}
}

View file

@ -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';
}
}

View file

@ -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();
}
}

View file

@ -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');
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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(),
),
);
}
}

View file

@ -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;
}
}

View file

@ -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');
}
}

View file

@ -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());

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1,4 @@
<?php
abstract class PhabricatorProjectColumnTransactionType
extends PhabricatorModularTransactionType {}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -0,0 +1,4 @@
<?php
abstract class PhabricatorProjectTriggerTransactionType
extends PhabricatorModularTransactionType {}

View file

@ -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);
}
}
}

View file

@ -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)

View file

@ -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);

View file

@ -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++;

View file

@ -17,6 +17,12 @@ final class PhabricatorConpherenceProfileMenuItem
}
public function canAddToObject($object) {
$application = new PhabricatorConpherenceApplication();
if (!$application->isInstalled()) {
return false;
}
return true;
}

View file

@ -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) {

View file

@ -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);

View file

@ -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);
}
}

View file

@ -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;
}

View file

@ -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);
}
}

View file

@ -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();

View file

@ -153,4 +153,9 @@ final class PhabricatorStandardCustomFieldSelect
->setOptions($this->getOptions());
}
protected function newExportFieldType() {
return id(new PhabricatorOptionExportField())
->setOptions($this->getOptions());
}
}

View file

@ -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);
}
}

View file

@ -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(),
));
}

View file

@ -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,

View file

@ -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