@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Update "add panel" and "remove panel" Dashboard flows to the new panel storage format

Summary:
Depends on D20407. Ref T13272. This updates the "add panel" (which has two flavors: "add existing" and "create new") and "remove panel" flows to work with the new duplicate-friendly storage format.

- We now modify panels by "panelKey", not by panel PHID, so one dashboard may have multiple copies of the same panel and we can still figure out what's going on.
- We now work with "contextPHID", not "dashboardID", to make some flows with tab panels (or other nested panels in the future) easier.

The only major remaining flow is the Javascript "move panels around with drag-and-drop" flow.

Test Plan:
- Added panels to a dashboard with "Create New Panel".
- Added panels to a dashboard with "Add Existing Panel".
- Removed panels from a dashboard.
- Added and removed duplicate panels, got a correctly-functioning dashboard that didn't care about duplicates.

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T13272

Differential Revision: https://secure.phabricator.com/D20408

+574 -413
+8 -8
resources/celerity/map.php
··· 10 10 'conpherence.pkg.css' => '3c8a0668', 11 11 'conpherence.pkg.js' => '020aebcf', 12 12 'core.pkg.css' => '294e365c', 13 - 'core.pkg.js' => '794952ae', 13 + 'core.pkg.js' => '69247edd', 14 14 'differential.pkg.css' => '8d8360fb', 15 15 'differential.pkg.js' => '67e02996', 16 16 'diffusion.pkg.css' => '42c75c37', ··· 371 371 'rsrc/js/application/conpherence/behavior-toggle-widget.js' => '8f959ad0', 372 372 'rsrc/js/application/countdown/timer.js' => '6a162524', 373 373 'rsrc/js/application/daemon/behavior-bulk-job-reload.js' => '3829a3cf', 374 - 'rsrc/js/application/dashboard/behavior-dashboard-async-panel.js' => '09ecf50c', 374 + 'rsrc/js/application/dashboard/behavior-dashboard-async-panel.js' => 'a871fe00', 375 375 'rsrc/js/application/dashboard/behavior-dashboard-move-panels.js' => '076bd092', 376 376 'rsrc/js/application/dashboard/behavior-dashboard-query-panel-select.js' => '1e413dc9', 377 377 'rsrc/js/application/dashboard/behavior-dashboard-tab-panel.js' => '0116d3e8', ··· 594 594 'javelin-behavior-conpherence-search' => '91befbcc', 595 595 'javelin-behavior-countdown-timer' => '6a162524', 596 596 'javelin-behavior-dark-console' => 'f39d968b', 597 - 'javelin-behavior-dashboard-async-panel' => '09ecf50c', 597 + 'javelin-behavior-dashboard-async-panel' => 'a871fe00', 598 598 'javelin-behavior-dashboard-move-panels' => '076bd092', 599 599 'javelin-behavior-dashboard-query-panel-select' => '1e413dc9', 600 600 'javelin-behavior-dashboard-tab-panel' => '0116d3e8', ··· 982 982 'herald-rule-editor', 983 983 'javelin-behavior', 984 984 ), 985 - '09ecf50c' => array( 986 - 'javelin-behavior', 987 - 'javelin-dom', 988 - 'javelin-workflow', 989 - ), 990 985 '0ad8d31f' => array( 991 986 'javelin-behavior', 992 987 'javelin-stratcom', ··· 1793 1788 'a5257c4e' => array( 1794 1789 'javelin-install', 1795 1790 'javelin-dom', 1791 + ), 1792 + 'a871fe00' => array( 1793 + 'javelin-behavior', 1794 + 'javelin-dom', 1795 + 'javelin-workflow', 1796 1796 ), 1797 1797 'a9942052' => array( 1798 1798 'javelin-behavior',
+4 -6
src/__phutil_library_map__.php
··· 2906 2906 'PhabricatorDarkConsoleTabSetting' => 'applications/settings/setting/PhabricatorDarkConsoleTabSetting.php', 2907 2907 'PhabricatorDarkConsoleVisibleSetting' => 'applications/settings/setting/PhabricatorDarkConsoleVisibleSetting.php', 2908 2908 'PhabricatorDashboard' => 'applications/dashboard/storage/PhabricatorDashboard.php', 2909 - 'PhabricatorDashboardAddPanelController' => 'applications/dashboard/controller/PhabricatorDashboardAddPanelController.php', 2909 + 'PhabricatorDashboardAdjustController' => 'applications/dashboard/controller/dashboard/PhabricatorDashboardAdjustController.php', 2910 2910 'PhabricatorDashboardApplication' => 'applications/dashboard/application/PhabricatorDashboardApplication.php', 2911 2911 'PhabricatorDashboardApplicationInstallWorkflow' => 'applications/dashboard/install/PhabricatorDashboardApplicationInstallWorkflow.php', 2912 2912 'PhabricatorDashboardArchiveController' => 'applications/dashboard/controller/dashboard/PhabricatorDashboardArchiveController.php', ··· 2927 2927 'PhabricatorDashboardInstall' => 'applications/dashboard/storage/PhabricatorDashboardInstall.php', 2928 2928 'PhabricatorDashboardInstallController' => 'applications/dashboard/controller/dashboard/PhabricatorDashboardInstallController.php', 2929 2929 'PhabricatorDashboardInstallWorkflow' => 'applications/dashboard/install/PhabricatorDashboardInstallWorkflow.php', 2930 - 'PhabricatorDashboardLayoutConfig' => 'applications/dashboard/layoutconfig/PhabricatorDashboardLayoutConfig.php', 2931 2930 'PhabricatorDashboardLayoutMode' => 'applications/dashboard/layoutconfig/PhabricatorDashboardLayoutMode.php', 2932 2931 'PhabricatorDashboardLayoutTransaction' => 'applications/dashboard/xaction/dashboard/PhabricatorDashboardLayoutTransaction.php', 2933 2932 'PhabricatorDashboardListController' => 'applications/dashboard/controller/PhabricatorDashboardListController.php', ··· 2964 2963 'PhabricatorDashboardPanelType' => 'applications/dashboard/paneltype/PhabricatorDashboardPanelType.php', 2965 2964 'PhabricatorDashboardPanelUsedByObjectEdgeType' => 'applications/search/edge/PhabricatorDashboardPanelUsedByObjectEdgeType.php', 2966 2965 'PhabricatorDashboardPanelViewController' => 'applications/dashboard/controller/panel/PhabricatorDashboardPanelViewController.php', 2966 + 'PhabricatorDashboardPanelsTransaction' => 'applications/dashboard/xaction/dashboard/PhabricatorDashboardPanelsTransaction.php', 2967 2967 'PhabricatorDashboardPortal' => 'applications/dashboard/storage/PhabricatorDashboardPortal.php', 2968 2968 'PhabricatorDashboardPortalController' => 'applications/dashboard/controller/portal/PhabricatorDashboardPortalController.php', 2969 2969 'PhabricatorDashboardPortalDatasource' => 'applications/dashboard/typeahead/PhabricatorDashboardPortalDatasource.php', ··· 2998 2998 'PhabricatorDashboardQueryPanelQueryTransaction' => 'applications/dashboard/xaction/panel/PhabricatorDashboardQueryPanelQueryTransaction.php', 2999 2999 'PhabricatorDashboardQueryPanelType' => 'applications/dashboard/paneltype/PhabricatorDashboardQueryPanelType.php', 3000 3000 'PhabricatorDashboardRemarkupRule' => 'applications/dashboard/remarkup/PhabricatorDashboardRemarkupRule.php', 3001 - 'PhabricatorDashboardRemovePanelController' => 'applications/dashboard/controller/PhabricatorDashboardRemovePanelController.php', 3002 3001 'PhabricatorDashboardRenderingEngine' => 'applications/dashboard/engine/PhabricatorDashboardRenderingEngine.php', 3003 3002 'PhabricatorDashboardSchemaSpec' => 'applications/dashboard/storage/PhabricatorDashboardSchemaSpec.php', 3004 3003 'PhabricatorDashboardSearchEngine' => 'applications/dashboard/query/PhabricatorDashboardSearchEngine.php', ··· 8906 8905 'PhabricatorNgramsInterface', 8907 8906 'PhabricatorDashboardPanelContainerInterface', 8908 8907 ), 8909 - 'PhabricatorDashboardAddPanelController' => 'PhabricatorDashboardController', 8908 + 'PhabricatorDashboardAdjustController' => 'PhabricatorDashboardController', 8910 8909 'PhabricatorDashboardApplication' => 'PhabricatorApplication', 8911 8910 'PhabricatorDashboardApplicationInstallWorkflow' => 'PhabricatorDashboardInstallWorkflow', 8912 8911 'PhabricatorDashboardArchiveController' => 'PhabricatorDashboardController', ··· 8927 8926 'PhabricatorDashboardInstall' => 'PhabricatorDashboardDAO', 8928 8927 'PhabricatorDashboardInstallController' => 'PhabricatorDashboardController', 8929 8928 'PhabricatorDashboardInstallWorkflow' => 'Phobject', 8930 - 'PhabricatorDashboardLayoutConfig' => 'Phobject', 8931 8929 'PhabricatorDashboardLayoutMode' => 'Phobject', 8932 8930 'PhabricatorDashboardLayoutTransaction' => 'PhabricatorDashboardTransactionType', 8933 8931 'PhabricatorDashboardListController' => 'PhabricatorDashboardController', ··· 8971 8969 'PhabricatorDashboardPanelType' => 'Phobject', 8972 8970 'PhabricatorDashboardPanelUsedByObjectEdgeType' => 'PhabricatorEdgeType', 8973 8971 'PhabricatorDashboardPanelViewController' => 'PhabricatorDashboardController', 8972 + 'PhabricatorDashboardPanelsTransaction' => 'PhabricatorDashboardTransactionType', 8974 8973 'PhabricatorDashboardPortal' => array( 8975 8974 'PhabricatorDashboardDAO', 8976 8975 'PhabricatorApplicationTransactionInterface', ··· 9013 9012 'PhabricatorDashboardQueryPanelQueryTransaction' => 'PhabricatorDashboardPanelPropertyTransaction', 9014 9013 'PhabricatorDashboardQueryPanelType' => 'PhabricatorDashboardPanelType', 9015 9014 'PhabricatorDashboardRemarkupRule' => 'PhabricatorObjectRemarkupRule', 9016 - 'PhabricatorDashboardRemovePanelController' => 'PhabricatorDashboardController', 9017 9015 'PhabricatorDashboardRenderingEngine' => 'Phobject', 9018 9016 'PhabricatorDashboardSchemaSpec' => 'PhabricatorConfigSchemaSpec', 9019 9017 'PhabricatorDashboardSearchEngine' => 'PhabricatorApplicationSearchEngine',
+2 -2
src/applications/auth/engine/PhabricatorAuthSessionEngine.php
··· 389 389 * appropriate for one-time checks. 390 390 * 391 391 * @param PhabricatorUser User whose session needs to be in high security. 392 - * @param AphrontReqeust Current request. 392 + * @param AphrontRequest Current request. 393 393 * @param string URI to return the user to if they cancel. 394 394 * @return PhabricatorAuthHighSecurityToken Security token. 395 395 * @task hisec ··· 421 421 * use @{method:requireHighSecurityToken}. 422 422 * 423 423 * @param PhabricatorUser User whose session needs to be in high security. 424 - * @param AphrontReqeust Current request. 424 + * @param AphrontRequest Current request. 425 425 * @param string URI to return the user to if they cancel. 426 426 * @param bool True to jump partial sessions directly into high 427 427 * security instead of just upgrading them to full
+2 -3
src/applications/dashboard/application/PhabricatorDashboardApplication.php
··· 48 48 '(?:(?P<modeKey>[^/]+)/)?)?' => 49 49 'PhabricatorDashboardInstallController', 50 50 'console/' => 'PhabricatorDashboardConsoleController', 51 - 'addpanel/(?P<id>\d+)/' => 'PhabricatorDashboardAddPanelController', 52 51 'movepanel/(?P<id>\d+)/' => 'PhabricatorDashboardMovePanelController', 53 - 'removepanel/(?P<id>\d+)/' 54 - => 'PhabricatorDashboardRemovePanelController', 52 + 'adjust/(?P<op>remove|add)/' 53 + => 'PhabricatorDashboardAdjustController', 55 54 'panel/' => array( 56 55 'install/(?P<engineKey>[^/]+)/(?:(?P<queryKey>[^/]+)/)?' => 57 56 'PhabricatorDashboardQueryPanelInstallController',
-103
src/applications/dashboard/controller/PhabricatorDashboardAddPanelController.php
··· 1 - <?php 2 - 3 - final class PhabricatorDashboardAddPanelController 4 - extends PhabricatorDashboardController { 5 - 6 - public function handleRequest(AphrontRequest $request) { 7 - $viewer = $request->getViewer(); 8 - $id = $request->getURIData('id'); 9 - 10 - $dashboard = id(new PhabricatorDashboardQuery()) 11 - ->setViewer($viewer) 12 - ->withIDs(array($id)) 13 - ->requireCapabilities( 14 - array( 15 - PhabricatorPolicyCapability::CAN_VIEW, 16 - PhabricatorPolicyCapability::CAN_EDIT, 17 - )) 18 - ->executeOne(); 19 - if (!$dashboard) { 20 - return new Aphront404Response(); 21 - } 22 - 23 - $redirect_uri = $this->getApplicationURI( 24 - 'arrange/'.$dashboard->getID().'/'); 25 - 26 - $v_panel = head($request->getArr('panel')); 27 - $e_panel = true; 28 - $errors = array(); 29 - if ($request->isFormPost()) { 30 - if (strlen($v_panel)) { 31 - $panel = id(new PhabricatorDashboardPanelQuery()) 32 - ->setViewer($viewer) 33 - ->withIDs(array($v_panel)) 34 - ->executeOne(); 35 - if (!$panel) { 36 - $errors[] = pht('Not a valid panel.'); 37 - $e_panel = pht('Invalid'); 38 - } 39 - 40 - $on_dashboard = $dashboard->getPanels(); 41 - $on_ids = mpull($on_dashboard, null, 'getID'); 42 - if (array_key_exists($v_panel, $on_ids)) { 43 - $p_name = $panel->getName(); 44 - $errors[] = pht('Panel "%s" already exists on dashboard.', $p_name); 45 - $e_panel = pht('Invalid'); 46 - } 47 - 48 - } else { 49 - $errors[] = pht('Select a panel to add.'); 50 - $e_panel = pht('Required'); 51 - } 52 - 53 - if (!$errors) { 54 - PhabricatorDashboardTransactionEditor::addPanelToDashboard( 55 - $viewer, 56 - PhabricatorContentSource::newFromRequest($request), 57 - $panel, 58 - $dashboard, 59 - $request->getInt('column', 0)); 60 - 61 - return id(new AphrontRedirectResponse())->setURI($redirect_uri); 62 - } 63 - } 64 - 65 - $panels = id(new PhabricatorDashboardPanelQuery()) 66 - ->setViewer($viewer) 67 - ->withArchived(false) 68 - ->execute(); 69 - 70 - if (!$panels) { 71 - return $this->newDialog() 72 - ->setTitle(pht('No Panels Exist Yet')) 73 - ->appendParagraph( 74 - pht( 75 - 'You have not created any dashboard panels yet, so you can not '. 76 - 'add an existing panel.')) 77 - ->appendParagraph( 78 - pht('Instead, add a new panel.')) 79 - ->addCancelButton($redirect_uri); 80 - } 81 - 82 - $form = id(new AphrontFormView()) 83 - ->setUser($viewer) 84 - ->addHiddenInput('column', $request->getInt('column')) 85 - ->appendRemarkupInstructions( 86 - pht('Choose a panel to add to this dashboard:')) 87 - ->appendChild( 88 - id(new AphrontFormTokenizerControl()) 89 - ->setUser($this->getViewer()) 90 - ->setDatasource(new PhabricatorDashboardPanelDatasource()) 91 - ->setLimit(1) 92 - ->setName('panel') 93 - ->setLabel(pht('Panel'))); 94 - 95 - return $this->newDialog() 96 - ->setTitle(pht('Add Panel')) 97 - ->setErrors($errors) 98 - ->appendChild($form->buildLayoutView()) 99 - ->addCancelButton($redirect_uri) 100 - ->addSubmitButton(pht('Add Panel')); 101 - } 102 - 103 - }
-77
src/applications/dashboard/controller/PhabricatorDashboardRemovePanelController.php
··· 1 - <?php 2 - 3 - final class PhabricatorDashboardRemovePanelController 4 - extends PhabricatorDashboardController { 5 - 6 - public function handleRequest(AphrontRequest $request) { 7 - $viewer = $request->getViewer(); 8 - $id = $request->getURIData('id'); 9 - 10 - $dashboard = id(new PhabricatorDashboardQuery()) 11 - ->setViewer($viewer) 12 - ->withIDs(array($id)) 13 - ->requireCapabilities( 14 - array( 15 - PhabricatorPolicyCapability::CAN_VIEW, 16 - PhabricatorPolicyCapability::CAN_EDIT, 17 - )) 18 - ->executeOne(); 19 - if (!$dashboard) { 20 - return new Aphront404Response(); 21 - } 22 - 23 - // NOTE: If you can edit a dashboard, you can remove panels from it even 24 - // if you don't have permission to see them or they aren't valid. We only 25 - // require that the panel be present on the dashboard. 26 - 27 - $v_panel = $request->getStr('panelPHID'); 28 - 29 - $panel_on_dashboard = false; 30 - $layout = $dashboard->getLayoutConfigObject(); 31 - $columns = $layout->getPanelLocations(); 32 - foreach ($columns as $column) { 33 - foreach ($column as $column_panel_phid) { 34 - if ($column_panel_phid == $v_panel) { 35 - $panel_on_dashboard = true; 36 - break; 37 - } 38 - } 39 - } 40 - 41 - if (!$panel_on_dashboard) { 42 - return new Aphront404Response(); 43 - } 44 - 45 - $redirect_uri = $dashboard->getURI(); 46 - $layout_config = $dashboard->getLayoutConfigObject(); 47 - 48 - if ($request->isFormPost()) { 49 - $xactions = array(); 50 - 51 - $layout_config->removePanel($v_panel); 52 - $dashboard->setLayoutConfigFromObject($layout_config); 53 - 54 - $editor = id(new PhabricatorDashboardTransactionEditor()) 55 - ->setActor($viewer) 56 - ->setContentSourceFromRequest($request) 57 - ->setContinueOnMissingFields(true) 58 - ->setContinueOnNoEffect(true) 59 - ->applyTransactions($dashboard, $xactions); 60 - 61 - return id(new AphrontRedirectResponse())->setURI($redirect_uri); 62 - } 63 - 64 - $form = id(new AphrontFormView()) 65 - ->setUser($viewer) 66 - ->addHiddenInput('confirm', true) 67 - ->addHiddenInput('panelPHID', $v_panel) 68 - ->appendChild(pht('Are you sure you want to remove this panel?')); 69 - 70 - return $this->newDialog() 71 - ->setTitle(pht('Remove Panel')) 72 - ->appendChild($form->buildLayoutView()) 73 - ->addCancelButton($redirect_uri) 74 - ->addSubmitButton(pht('Remove Panel')); 75 - } 76 - 77 - }
+202
src/applications/dashboard/controller/dashboard/PhabricatorDashboardAdjustController.php
··· 1 + <?php 2 + 3 + final class PhabricatorDashboardAdjustController 4 + extends PhabricatorDashboardController { 5 + 6 + private $contextPHID; 7 + private $panelKey; 8 + private $columnKey; 9 + 10 + public function handleRequest(AphrontRequest $request) { 11 + $viewer = $this->getViewer(); 12 + 13 + $context_phid = $request->getStr('contextPHID'); 14 + 15 + $dashboard = id(new PhabricatorDashboardQuery()) 16 + ->setViewer($viewer) 17 + ->withPHIDs(array($context_phid)) 18 + ->requireCapabilities( 19 + array( 20 + PhabricatorPolicyCapability::CAN_VIEW, 21 + PhabricatorPolicyCapability::CAN_EDIT, 22 + )) 23 + ->executeOne(); 24 + if (!$dashboard) { 25 + return new Aphront404Response(); 26 + } 27 + 28 + $this->contextPHID = $context_phid; 29 + 30 + $done_uri = $dashboard->getURI(); 31 + $ref_list = $dashboard->getPanelRefList(); 32 + 33 + $panel_ref = null; 34 + $panel_key = $request->getStr('panelKey'); 35 + if (strlen($panel_key)) { 36 + $panel_ref = $ref_list->getPanelRef($panel_key); 37 + if (!$panel_ref) { 38 + return new Aphront404Response(); 39 + } 40 + 41 + $this->panelKey = $panel_key; 42 + } else { 43 + $panel_ref = null; 44 + } 45 + 46 + $column_key = $request->getStr('columnKey'); 47 + if (strlen($column_key)) { 48 + $columns = $ref_list->getColumns(); 49 + if (!isset($columns[$column_key])) { 50 + return new Aphront404Response(); 51 + } 52 + $this->columnKey = $column_key; 53 + } 54 + 55 + switch ($request->getURIData('op')) { 56 + case 'add': 57 + return $this->handleAddRequest($dashboard, $done_uri); 58 + case 'remove': 59 + if (!$panel_ref) { 60 + return new Aphront404Response(); 61 + } 62 + return $this->handleRemoveRequest($dashboard, $panel_ref, $done_uri); 63 + } 64 + } 65 + 66 + private function handleAddRequest( 67 + PhabricatorDashboard $dashboard, 68 + $done_uri) { 69 + $request = $this->getRequest(); 70 + $viewer = $this->getViewer(); 71 + 72 + $errors = array(); 73 + 74 + $panel_phid = null; 75 + $e_panel = true; 76 + if ($request->isFormPost()) { 77 + $panel_phid = head($request->getArr('panelPHIDs')); 78 + 79 + if (!$panel_phid) { 80 + $errors[] = pht('You must choose a panel to add to the dashboard.'); 81 + $e_panel = pht('Required'); 82 + } else { 83 + $panel = id(new PhabricatorDashboardPanelQuery()) 84 + ->setViewer($viewer) 85 + ->withPHIDs(array($panel_phid)) 86 + ->executeOne(); 87 + if (!$panel) { 88 + $errors[] = pht('You must choose a valid panel.'); 89 + $e_panel = pht('Invalid'); 90 + } 91 + } 92 + 93 + if (!$errors) { 94 + $xactions = array(); 95 + 96 + $ref_list = clone $dashboard->getPanelRefList(); 97 + $ref_list->newPanelRef($panel, $this->columnKey); 98 + $new_panels = $ref_list->toDictionary(); 99 + 100 + $xactions[] = $dashboard->getApplicationTransactionTemplate() 101 + ->setTransactionType( 102 + PhabricatorDashboardPanelsTransaction::TRANSACTIONTYPE) 103 + ->setNewValue($new_panels); 104 + 105 + $editor = $dashboard->getApplicationTransactionEditor() 106 + ->setActor($viewer) 107 + ->setContentSourceFromRequest($request) 108 + ->setContinueOnNoEffect(true) 109 + ->setContinueOnMissingFields(true); 110 + 111 + $editor->applyTransactions($dashboard, $xactions); 112 + 113 + return id(new AphrontRedirectResponse())->setURI($done_uri); 114 + } 115 + } 116 + 117 + if ($panel_phid) { 118 + $panel_phids = array($panel_phid); 119 + } else { 120 + $panel_phids = array(); 121 + } 122 + 123 + $form = id(new AphrontFormView()) 124 + ->setViewer($viewer) 125 + ->appendRemarkupInstructions( 126 + pht('Choose a panel to add to this dashboard:')) 127 + ->appendControl( 128 + id(new AphrontFormTokenizerControl()) 129 + ->setDatasource(new PhabricatorDashboardPanelDatasource()) 130 + ->setLimit(1) 131 + ->setName('panelPHIDs') 132 + ->setLabel(pht('Panel')) 133 + ->setError($e_panel) 134 + ->setValue($panel_phids)); 135 + 136 + return $this->newEditDialog() 137 + ->setTitle(pht('Add Panel')) 138 + ->setWidth(AphrontDialogView::WIDTH_FORM) 139 + ->setErrors($errors) 140 + ->appendForm($form) 141 + ->addCancelButton($done_uri) 142 + ->addSubmitButton(pht('Add Panel')); 143 + } 144 + 145 + private function handleRemoveRequest( 146 + PhabricatorDashboard $dashboard, 147 + PhabricatorDashboardPanelRef $panel_ref, 148 + $done_uri) { 149 + $request = $this->getRequest(); 150 + $viewer = $this->getViewer(); 151 + 152 + // NOTE: If you can edit a dashboard, you can remove panels from it even 153 + // if you don't have permission to see them or they aren't valid. We only 154 + // require that the panel be present on the dashboard. 155 + 156 + if ($request->isFormPost()) { 157 + $xactions = array(); 158 + 159 + $ref_list = clone $dashboard->getPanelRefList(); 160 + $ref_list->removePanelRef($panel_ref); 161 + $new_panels = $ref_list->toDictionary(); 162 + 163 + $xactions[] = $dashboard->getApplicationTransactionTemplate() 164 + ->setTransactionType( 165 + PhabricatorDashboardPanelsTransaction::TRANSACTIONTYPE) 166 + ->setNewValue($new_panels); 167 + 168 + $editor = $dashboard->getApplicationTransactionEditor() 169 + ->setActor($viewer) 170 + ->setContentSourceFromRequest($request) 171 + ->setContinueOnNoEffect(true) 172 + ->setContinueOnMissingFields(true); 173 + 174 + $editor->applyTransactions($dashboard, $xactions); 175 + 176 + return id(new AphrontRedirectResponse())->setURI($done_uri); 177 + } 178 + 179 + $panel_phid = $panel_ref->getPanelPHID(); 180 + $handles = $viewer->loadHandles(array($panel_phid)); 181 + $handle = $handles[$panel_phid]; 182 + 183 + $message = pht( 184 + 'Remove panel %s from dashboard %s?', 185 + phutil_tag('strong', array(), $handle->getFullName()), 186 + phutil_tag('strong', array(), $dashboard->getName())); 187 + 188 + return $this->newEditDialog() 189 + ->setTitle(pht('Remove Dashboard Panel')) 190 + ->appendParagraph($message) 191 + ->addCancelButton($done_uri) 192 + ->addSubmitButton(pht('Remove Panel')); 193 + } 194 + 195 + private function newEditDialog() { 196 + return $this->newDialog() 197 + ->addHiddenInput('contextPHID', $this->contextPHID) 198 + ->addHiddenInput('panelKey', $this->panelKey) 199 + ->addHiddenInput('columnKey', $this->columnKey); 200 + } 201 + 202 + }
+22 -16
src/applications/dashboard/controller/panel/PhabricatorDashboardPanelEditController.php
··· 9 9 $engine = id(new PhabricatorDashboardPanelEditEngine()) 10 10 ->setController($this); 11 11 12 - // We can create or edit a panel in the context of a dashboard. If we 13 - // started on a dashboard, we want to return to that dashboard when we're 14 - // done editing. 15 - $dashboard_id = $request->getStr('dashboardID'); 16 - if (strlen($dashboard_id)) { 17 - $dashboard = id(new PhabricatorDashboardQuery()) 12 + // We can create or edit a panel in the context of a dashboard or 13 + // container panel, like a tab panel. If we started this flow on some 14 + // container object, we want to return to that container when we're done 15 + // editing. 16 + 17 + $context_phid = $request->getStr('contextPHID'); 18 + if (strlen($context_phid)) { 19 + $context = id(new PhabricatorObjectQuery()) 18 20 ->setViewer($viewer) 19 - ->withIDs(array($dashboard_id)) 21 + ->withPHIDs(array($context_phid)) 20 22 ->requireCapabilities( 21 23 array( 22 24 PhabricatorPolicyCapability::CAN_VIEW, 23 25 PhabricatorPolicyCapability::CAN_EDIT, 24 26 )) 25 27 ->executeOne(); 26 - if (!$dashboard) { 28 + if (!$context) { 29 + return new Aphront404Response(); 30 + } 31 + 32 + if (!($context instanceof PhabricatorDashboardPanelContainerInterface)) { 27 33 return new Aphront404Response(); 28 34 } 29 35 30 36 $engine 31 - ->setDashboard($dashboard) 32 - ->addContextParameter('dashboardID', $dashboard_id); 37 + ->setContextObject($context) 38 + ->addContextParameter('contextPHID', $context_phid); 33 39 } else { 34 - $dashboard = null; 40 + $context = null; 35 41 } 36 42 37 43 $id = $request->getURIData('id'); 38 44 if (!$id) { 39 - $column_id = $request->getStr('columnID'); 45 + $column_key = $request->getStr('columnKey'); 40 46 41 - if ($dashboard) { 42 - $cancel_uri = $dashboard->getURI(); 47 + if ($context) { 48 + $cancel_uri = $context->getURI(); 43 49 } else { 44 50 $cancel_uri = $this->getApplicationURI('panel/'); 45 51 } ··· 52 58 53 59 $engine 54 60 ->addContextParameter('panelType', $panel_type) 55 - ->addContextParameter('columnID', $column_id) 61 + ->addContextParameter('columnKey', $column_key) 56 62 ->setPanelType($panel_type) 57 - ->setColumnID($column_id); 63 + ->setColumnKey($column_key); 58 64 } 59 65 60 66 return $engine->buildResponse();
+16 -3
src/applications/dashboard/controller/panel/PhabricatorDashboardPanelRenderController.php
··· 31 31 $parent_phids = array(); 32 32 } 33 33 34 - $rendered_panel = id(new PhabricatorDashboardPanelRenderingEngine()) 34 + $engine = id(new PhabricatorDashboardPanelRenderingEngine()) 35 35 ->setViewer($viewer) 36 36 ->setPanel($panel) 37 37 ->setPanelPHID($panel->getPHID()) 38 38 ->setParentPanelPHIDs($parent_phids) 39 39 ->setHeaderMode($request->getStr('headerMode')) 40 - ->setDashboardID($request->getInt('dashboardID')) 41 - ->renderPanel(); 40 + ->setPanelKey($request->getStr('panelKey')); 41 + 42 + $context_phid = $request->getStr('contextPHID'); 43 + if ($context_phid) { 44 + $context = id(new PhabricatorObjectQuery()) 45 + ->setViewer($viewer) 46 + ->withPHIDs(array($context_phid)) 47 + ->executeOne(); 48 + if (!$context) { 49 + return new Aphront404Response(); 50 + } 51 + $engine->setContextObject($context); 52 + } 53 + 54 + $rendered_panel = $engine->renderPanel(); 42 55 43 56 if ($request->isAjax()) { 44 57 return id(new AphrontAjaxResponse())
+43 -27
src/applications/dashboard/editor/PhabricatorDashboardPanelEditEngine.php
··· 6 6 const ENGINECONST = 'dashboard.panel'; 7 7 8 8 private $panelType; 9 - private $dashboard; 10 - private $columnID; 9 + private $contextObject; 10 + private $columnKey; 11 11 12 12 public function setPanelType($panel_type) { 13 13 $this->panelType = $panel_type; ··· 18 18 return $this->panelType; 19 19 } 20 20 21 - public function setDashboard(PhabricatorDashboard $dashboard) { 22 - $this->dashboard = $dashboard; 21 + public function setContextObject($context) { 22 + $this->contextObject = $context; 23 23 return $this; 24 24 } 25 25 26 - public function getDashboard() { 27 - return $this->dashboard; 26 + public function getContextObject() { 27 + return $this->contextObject; 28 28 } 29 29 30 - public function setColumnID($column_id) { 31 - $this->columnID = $column_id; 30 + public function setColumnKey($column_key) { 31 + $this->columnKey = $column_key; 32 32 return $this; 33 33 } 34 34 35 - public function getColumnID() { 36 - return $this->columnID; 35 + public function getColumnKey() { 36 + return $this->columnKey; 37 37 } 38 38 39 39 public function isEngineConfigurable() { ··· 84 84 } 85 85 86 86 protected function getObjectCreateCancelURI($object) { 87 - $dashboard = $this->getDashboard(); 88 - if ($dashboard) { 89 - return $dashboard->getURI(); 87 + $context = $this->getContextObject(); 88 + if ($context) { 89 + return $context->getURI(); 90 90 } 91 91 92 92 return parent::getObjectCreateCancelURI($object); 93 93 } 94 94 95 95 public function getEffectiveObjectEditDoneURI($object) { 96 - $dashboard = $this->getDashboard(); 97 - if ($dashboard) { 98 - return $dashboard->getURI(); 96 + $context = $this->getContextObject(); 97 + if ($context) { 98 + return $context->getURI(); 99 99 } 100 100 101 101 return parent::getEffectiveObjectEditDoneURI($object); 102 102 } 103 103 104 104 protected function getObjectEditCancelURI($object) { 105 - $dashboard = $this->getDashboard(); 106 - if ($dashboard) { 107 - return $dashboard->getURI(); 105 + $context = $this->getContextObject(); 106 + if ($context) { 107 + return $context->getURI(); 108 108 } 109 109 110 110 return parent::getObjectEditCancelURI($object); ··· 131 131 } 132 132 133 133 protected function didApplyTransactions($object, array $xactions) { 134 - $dashboard = $this->getDashboard(); 135 - if ($dashboard) { 134 + $context = $this->getContextObject(); 135 + 136 + if ($context instanceof PhabricatorDashboard) { 136 137 $viewer = $this->getViewer(); 137 138 $controller = $this->getController(); 138 139 $request = $controller->getRequest(); 139 140 140 - PhabricatorDashboardTransactionEditor::addPanelToDashboard( 141 - $viewer, 142 - PhabricatorContentSource::newFromRequest($request), 143 - $object, 144 - $dashboard, 145 - (int)$this->getColumnID()); 141 + $dashboard = $context; 142 + 143 + $xactions = array(); 144 + 145 + $ref_list = clone $dashboard->getPanelRefList(); 146 + 147 + $ref_list->newPanelRef($object, $this->getColumnKey()); 148 + $new_panels = $ref_list->toDictionary(); 149 + 150 + $xactions[] = $dashboard->getApplicationTransactionTemplate() 151 + ->setTransactionType( 152 + PhabricatorDashboardPanelsTransaction::TRANSACTIONTYPE) 153 + ->setNewValue($new_panels); 154 + 155 + $editor = $dashboard->getApplicationTransactionEditor() 156 + ->setActor($viewer) 157 + ->setContentSourceFromRequest($request) 158 + ->setContinueOnNoEffect(true) 159 + ->setContinueOnMissingFields(true); 160 + 161 + $editor->applyTransactions($dashboard, $xactions); 146 162 } 147 163 } 148 164
+31 -18
src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php
··· 12 12 private $enableAsyncRendering; 13 13 private $parentPanelPHIDs; 14 14 private $headerMode = self::HEADER_MODE_NORMAL; 15 - private $dashboardID; 16 15 private $movable = true; 17 16 private $panelHandle; 18 17 private $editMode; 19 18 private $contextObject; 19 + private $panelKey; 20 20 21 21 public function setContextObject($object) { 22 22 $this->contextObject = $object; ··· 27 27 return $this->contextObject; 28 28 } 29 29 30 - public function setDashboardID($id) { 31 - $this->dashboardID = $id; 30 + public function setPanelKey($panel_key) { 31 + $this->panelKey = $panel_key; 32 32 return $this; 33 33 } 34 34 35 - public function getDashboardID() { 36 - return $this->dashboardID; 35 + public function getPanelKey() { 36 + return $this->panelKey; 37 37 } 38 38 39 39 public function setHeaderMode($header_mode) { ··· 182 182 183 183 184 184 private function renderAsyncPanel() { 185 + $context_phid = $this->getContextPHID(); 185 186 $panel = $this->getPanel(); 186 187 187 188 $panel_id = celerity_generate_unique_node_id(); 188 - $dashboard_id = $this->getDashboardID(); 189 189 190 190 Javelin::initBehavior( 191 191 'dashboard-async-panel', ··· 193 193 'panelID' => $panel_id, 194 194 'parentPanelPHIDs' => $this->getParentPanelPHIDs(), 195 195 'headerMode' => $this->getHeaderMode(), 196 - 'dashboardID' => $dashboard_id, 196 + 'contextPHID' => $context_phid, 197 + 'panelKey' => $this->getPanelKey(), 197 198 'uri' => '/dashboard/panel/render/'.$panel->getID().'/', 198 199 )); 199 200 ··· 322 323 323 324 $viewer = $this->getViewer(); 324 325 $panel = $this->getPanel(); 325 - $dashboard_id = $this->getDashboardID(); 326 + $context_phid = $this->getContextPHID(); 326 327 327 328 $actions = array(); 328 329 ··· 330 331 $panel_id = $panel->getID(); 331 332 332 333 $edit_uri = "/dashboard/panel/edit/{$panel_id}/"; 333 - $edit_uri = new PhutilURI($edit_uri); 334 - if ($dashboard_id) { 335 - $edit_uri->replaceQueryParam('dashboardID', $dashboard_id); 336 - } 334 + $params = array( 335 + 'contextPHID' => $context_phid, 336 + ); 337 + $edit_uri = new PhutilURI($edit_uri, $params); 337 338 338 339 $actions[] = id(new PhabricatorActionView()) 339 340 ->setIcon('fa-pencil') 340 341 ->setName(pht('Edit Panel')) 341 - ->setHref((string)$edit_uri); 342 + ->setHref($edit_uri); 342 343 343 344 $actions[] = id(new PhabricatorActionView()) 344 345 ->setIcon('fa-window-maximize') ··· 346 347 ->setHref($panel->getURI()); 347 348 } 348 349 349 - if ($dashboard_id) { 350 + if ($context_phid) { 350 351 $panel_phid = $this->getPanelPHID(); 351 352 352 - $remove_uri = "/dashboard/removepanel/{$dashboard_id}/"; 353 - $remove_uri = id(new PhutilURI($remove_uri)) 354 - ->replaceQueryParam('panelPHID', $panel_phid); 353 + $remove_uri = urisprintf('/dashboard/adjust/remove/'); 354 + $params = array( 355 + 'contextPHID' => $context_phid, 356 + 'panelKey' => $this->getPanelKey(), 357 + ); 358 + $remove_uri = new PhutilURI($remove_uri, $params); 355 359 356 360 $actions[] = id(new PhabricatorActionView()) 357 361 ->setIcon('fa-times') 358 - ->setHref((string)$remove_uri) 362 + ->setHref($remove_uri) 359 363 ->setName(pht('Remove Panel')) 360 364 ->setWorkflow(true); 361 365 } ··· 415 419 } 416 420 } 417 421 422 + private function getContextPHID() { 423 + $context = $this->getContextObject(); 424 + 425 + if ($context) { 426 + return $context->getPHID(); 427 + } 428 + 429 + return null; 430 + } 418 431 419 432 }
+18 -10
src/applications/dashboard/engine/PhabricatorDashboardRenderingEngine.php
··· 73 73 74 74 $panel_engine = id(new PhabricatorDashboardPanelRenderingEngine()) 75 75 ->setViewer($viewer) 76 - ->setDashboardID($dashboard->getID()) 77 76 ->setEnableAsyncRendering(true) 78 77 ->setContextObject($dashboard) 78 + ->setPanelKey($panel_ref->getPanelKey()) 79 79 ->setPanelPHID($panel_phid) 80 80 ->setParentPanelPHIDs(array()) 81 81 ->setHeaderMode($h_mode) ··· 94 94 95 95 if ($is_editable) { 96 96 $column_views[] = $this->renderAddPanelPlaceHolder(); 97 - $column_views[] = $this->renderAddPanelUI($column->getColumnKey()); 97 + $column_views[] = $this->renderAddPanelUI($column); 98 98 } 99 99 100 + $sigil = 'dashboard-column'; 101 + 102 + $metadata = array( 103 + 'columnKey' => $column->getColumnKey(), 104 + ); 105 + 100 106 $result->addColumn( 101 107 $column_views, 102 108 implode(' ', $column_classes), 103 - $sigil = 'dashboard-column', 104 - $metadata = array('columnID' => $column)); 109 + $sigil, 110 + $metadata); 105 111 } 106 112 107 113 if ($is_editable) { ··· 133 139 pht('This column does not have any panels yet.')); 134 140 } 135 141 136 - private function renderAddPanelUI($column) { 137 - $dashboard_id = $this->dashboard->getID(); 142 + private function renderAddPanelUI(PhabricatorDashboardColumn $column) { 143 + $dashboard = $this->getDashboard(); 144 + $column_key = $column->getColumnKey(); 138 145 139 146 $create_uri = id(new PhutilURI('/dashboard/panel/edit/')) 140 - ->replaceQueryParam('dashboardID', $dashboard_id) 141 - ->replaceQueryParam('columnID', $column); 147 + ->replaceQueryParam('contextPHID', $dashboard->getPHID()) 148 + ->replaceQueryParam('columnKey', $column_key); 142 149 143 - $add_uri = id(new PhutilURI('/dashboard/addpanel/'.$dashboard_id.'/')) 144 - ->replaceQueryParam('columnID', $column); 150 + $add_uri = id(new PhutilURI('/dashboard/adjust/add/')) 151 + ->replaceQueryParam('contextPHID', $dashboard->getPHID()) 152 + ->replaceQueryParam('columnKey', $column_key); 145 153 146 154 $create_button = id(new PHUIButtonView()) 147 155 ->setTag('a')
-128
src/applications/dashboard/layoutconfig/PhabricatorDashboardLayoutConfig.php
··· 1 - <?php 2 - 3 - final class PhabricatorDashboardLayoutConfig extends Phobject { 4 - 5 - const MODE_FULL = 'layout-mode-full'; 6 - const MODE_HALF_AND_HALF = 'layout-mode-half-and-half'; 7 - const MODE_THIRD_AND_THIRDS = 'layout-mode-third-and-thirds'; 8 - const MODE_THIRDS_AND_THIRD = 'layout-mode-thirds-and-third'; 9 - 10 - private $layoutMode = self::MODE_FULL; 11 - private $panelLocations = array(); 12 - 13 - public function setLayoutMode($mode) { 14 - $this->layoutMode = $mode; 15 - return $this; 16 - } 17 - public function getLayoutMode() { 18 - return $this->layoutMode; 19 - } 20 - 21 - public function setPanelLocation($which_column, $panel_phid) { 22 - $this->panelLocations[$which_column][] = $panel_phid; 23 - return $this; 24 - } 25 - 26 - public function setPanelLocations(array $locations) { 27 - $this->panelLocations = $locations; 28 - return $this; 29 - } 30 - 31 - public function getPanelLocations() { 32 - return $this->panelLocations; 33 - } 34 - 35 - public function replacePanel($old_phid, $new_phid) { 36 - $locations = $this->getPanelLocations(); 37 - foreach ($locations as $column => $panel_phids) { 38 - foreach ($panel_phids as $key => $panel_phid) { 39 - if ($panel_phid == $old_phid) { 40 - $locations[$column][$key] = $new_phid; 41 - } 42 - } 43 - } 44 - return $this->setPanelLocations($locations); 45 - } 46 - 47 - public function removePanel($panel_phid) { 48 - $panel_location_grid = $this->getPanelLocations(); 49 - foreach ($panel_location_grid as $column => $panel_columns) { 50 - $found_old_column = array_search($panel_phid, $panel_columns); 51 - if ($found_old_column !== false) { 52 - $new_panel_columns = $panel_columns; 53 - array_splice( 54 - $new_panel_columns, 55 - $found_old_column, 56 - 1, 57 - array()); 58 - $panel_location_grid[$column] = $new_panel_columns; 59 - break; 60 - } 61 - } 62 - $this->setPanelLocations($panel_location_grid); 63 - } 64 - 65 - public function getDefaultPanelLocations() { 66 - switch ($this->getLayoutMode()) { 67 - case self::MODE_HALF_AND_HALF: 68 - case self::MODE_THIRD_AND_THIRDS: 69 - case self::MODE_THIRDS_AND_THIRD: 70 - $locations = array(array(), array()); 71 - break; 72 - case self::MODE_FULL: 73 - default: 74 - $locations = array(array()); 75 - break; 76 - } 77 - return $locations; 78 - } 79 - 80 - public function getColumnClass($column_index, $grippable = false) { 81 - switch ($this->getLayoutMode()) { 82 - case self::MODE_HALF_AND_HALF: 83 - $class = 'half'; 84 - break; 85 - case self::MODE_THIRD_AND_THIRDS: 86 - if ($column_index) { 87 - $class = 'thirds'; 88 - } else { 89 - $class = 'third'; 90 - } 91 - break; 92 - case self::MODE_THIRDS_AND_THIRD: 93 - if ($column_index) { 94 - $class = 'third'; 95 - } else { 96 - $class = 'thirds'; 97 - } 98 - break; 99 - case self::MODE_FULL: 100 - default: 101 - $class = null; 102 - break; 103 - } 104 - if ($grippable) { 105 - $class .= ' grippable'; 106 - } 107 - return $class; 108 - } 109 - 110 - public static function newFromDictionary(array $dict) { 111 - $layout_config = id(new PhabricatorDashboardLayoutConfig()) 112 - ->setLayoutMode(idx($dict, 'layoutMode', self::MODE_FULL)); 113 - $layout_config->setPanelLocations(idx( 114 - $dict, 115 - 'panelLocations', 116 - $layout_config->getDefaultPanelLocations())); 117 - 118 - return $layout_config; 119 - } 120 - 121 - public function toDictionary() { 122 - return array( 123 - 'layoutMode' => $this->getLayoutMode(), 124 - 'panelLocations' => $this->getPanelLocations(), 125 - ); 126 - } 127 - 128 - }
+8
src/applications/dashboard/layoutconfig/PhabricatorDashboardPanelRef.php
··· 34 34 return $this->panelKey; 35 35 } 36 36 37 + public function toDictionary() { 38 + return array( 39 + 'panelKey' => $this->getPanelKey(), 40 + 'panelPHID' => $this->getPanelPHID(), 41 + 'columnKey' => $this->getColumnKey(), 42 + ); 43 + } 44 + 37 45 }
+43
src/applications/dashboard/layoutconfig/PhabricatorDashboardPanelRefList.php
··· 73 73 return $this->refs; 74 74 } 75 75 76 + public function getPanelRef($panel_key) { 77 + foreach ($this->getPanelRefs() as $ref) { 78 + if ($ref->getPanelKey() === $panel_key) { 79 + return $ref; 80 + } 81 + } 82 + 83 + return null; 84 + } 85 + 86 + public function toDictionary() { 87 + return array_values(mpull($this->getPanelRefs(), 'toDictionary')); 88 + } 89 + 90 + public function newPanelRef(PhabricatorDashboardPanel $panel, $column_key) { 91 + $ref = id(new PhabricatorDashboardPanelRef()) 92 + ->setPanelKey($this->newPanelKey()) 93 + ->setPanelPHID($panel->getPHID()) 94 + ->setColumnKey($column_key); 95 + 96 + $this->refs[] = $ref; 97 + 98 + return $ref; 99 + } 100 + 101 + public function removePanelRef(PhabricatorDashboardPanelRef $target) { 102 + foreach ($this->refs as $key => $ref) { 103 + if ($ref->getPanelKey() !== $target->getPanelKey()) { 104 + continue; 105 + } 106 + 107 + unset($this->refs[$key]); 108 + return $ref; 109 + } 110 + 111 + return null; 112 + } 113 + 114 + private function newPanelKey() { 115 + return Filesystem::readRandomCharacters(8); 116 + } 117 + 118 + 76 119 }
+20 -11
src/applications/dashboard/storage/PhabricatorDashboard.php
··· 24 24 const STATUS_ACTIVE = 'active'; 25 25 const STATUS_ARCHIVED = 'archived'; 26 26 27 - private $panels = self::ATTACHABLE; 28 - private $edgeProjectPHIDs = self::ATTACHABLE; 29 - 30 27 private $panelRefList; 31 28 32 29 public static function initializeNewDashboard(PhabricatorUser $actor) { ··· 36 33 ->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy()) 37 34 ->setEditPolicy($actor->getPHID()) 38 35 ->setStatus(self::STATUS_ACTIVE) 39 - ->setAuthorPHID($actor->getPHID()) 40 - ->attachPanels(array()); 36 + ->setAuthorPHID($actor->getPHID()); 41 37 } 42 38 43 39 public static function getStatusNameMap() { ··· 62 58 ) + parent::getConfiguration(); 63 59 } 64 60 65 - public function generatePHID() { 66 - return PhabricatorPHID::generateNewPHID( 67 - PhabricatorDashboardDashboardPHIDType::TYPECONST); 61 + public function getPHIDType() { 62 + return PhabricatorDashboardDashboardPHIDType::TYPECONST; 68 63 } 69 64 70 65 public function getRawLayoutMode() { ··· 75 70 public function setRawLayoutMode($mode) { 76 71 $config = $this->getRawLayoutConfig(); 77 72 $config['layoutMode'] = $mode; 73 + return $this->setRawLayoutConfig($config); 74 + } 78 75 79 - // If a cached panel ref list exists, clear it. 80 - $this->panelRefList = null; 76 + public function getRawPanels() { 77 + $config = $this->getRawLayoutConfig(); 78 + return idx($config, 'panels'); 79 + } 81 80 82 - return $this->setLayoutConfig($config); 81 + public function setRawPanels(array $panels) { 82 + $config = $this->getRawLayoutConfig(); 83 + $config['panels'] = $panels; 84 + return $this->setRawLayoutConfig($config); 83 85 } 84 86 85 87 private function getRawLayoutConfig() { ··· 90 92 } 91 93 92 94 return $config; 95 + } 96 + 97 + private function setRawLayoutConfig(array $config) { 98 + // If a cached panel ref list exists, clear it. 99 + $this->panelRefList = null; 100 + 101 + return $this->setLayoutConfig($config); 93 102 } 94 103 95 104 public function isArchived() {
+153
src/applications/dashboard/xaction/dashboard/PhabricatorDashboardPanelsTransaction.php
··· 1 + <?php 2 + 3 + final class PhabricatorDashboardPanelsTransaction 4 + extends PhabricatorDashboardTransactionType { 5 + 6 + const TRANSACTIONTYPE = 'panels'; 7 + 8 + public function generateOldValue($object) { 9 + return $object->getRawPanels(); 10 + } 11 + 12 + public function applyInternalEffects($object, $value) { 13 + $object->setRawPanels($value); 14 + } 15 + 16 + public function getTitle() { 17 + return pht( 18 + '%s changed the panels on this dashboard.', 19 + $this->renderAuthor()); 20 + } 21 + 22 + public function validateTransactions($object, array $xactions) { 23 + $actor = $this->getActor(); 24 + $errors = array(); 25 + 26 + $ref_list = $object->getPanelRefList(); 27 + $columns = $ref_list->getColumns(); 28 + 29 + $old_phids = $object->getPanelPHIDs(); 30 + $old_phids = array_fuse($old_phids); 31 + 32 + foreach ($xactions as $xaction) { 33 + $new_value = $xaction->getNewValue(); 34 + if (!is_array($new_value)) { 35 + $errors[] = $this->newInvalidError( 36 + pht('Panels must be a list of panel specifications.'), 37 + $xaction); 38 + continue; 39 + } 40 + 41 + if (!phutil_is_natural_list($new_value)) { 42 + $errors[] = $this->newInvalidError( 43 + pht('Panels must be a list, not a map.'), 44 + $xaction); 45 + continue; 46 + } 47 + 48 + $new_phids = array(); 49 + $seen_keys = array(); 50 + foreach ($new_value as $idx => $spec) { 51 + if (!is_array($spec)) { 52 + $errors[] = $this->newInvalidError( 53 + pht( 54 + 'Each panel specification must be a map of panel attributes. '. 55 + 'Panel specification at index "%s" is "%s".', 56 + $idx, 57 + phutil_describe_type($spec)), 58 + $xaction); 59 + continue; 60 + } 61 + 62 + try { 63 + PhutilTypeSpec::checkMap( 64 + $spec, 65 + array( 66 + 'panelPHID' => 'string', 67 + 'columnKey' => 'string', 68 + 'panelKey' => 'string', 69 + )); 70 + } catch (PhutilTypeCheckException $ex) { 71 + $errors[] = $this->newInvalidError( 72 + pht( 73 + 'Panel specification at index "%s" is invalid: %s', 74 + $idx, 75 + $ex->getMessage()), 76 + $xaction); 77 + continue; 78 + } 79 + 80 + $panel_key = $spec['panelKey']; 81 + 82 + if (!strlen($panel_key)) { 83 + $errors[] = $this->newInvalidError( 84 + pht( 85 + 'Panel specification at index "%s" has bad panel key "%s". '. 86 + 'Panel keys must be nonempty.', 87 + $idx, 88 + $panel_key), 89 + $xaction); 90 + continue; 91 + } 92 + 93 + if (isset($seen_keys[$panel_key])) { 94 + $errors[] = $this->newInvalidError( 95 + pht( 96 + 'Panel specification at index "%s" has duplicate panel key '. 97 + '"%s". Each panel must have a unique panel key.', 98 + $idx, 99 + $panel_key), 100 + $xaction); 101 + continue; 102 + } 103 + 104 + $seen_keys[$panel_key] = true; 105 + 106 + $panel_phid = $spec['panelPHID']; 107 + $new_phids[] = $panel_phid; 108 + 109 + $column_key = $spec['columnKey']; 110 + 111 + if (!isset($columns[$column_key])) { 112 + $errors[] = $this->newInvalidError( 113 + pht( 114 + 'Panel specification at index "%s" has bad column key "%s", '. 115 + 'valid column keys are: %s.', 116 + $idx, 117 + $column_key, 118 + implode(', ', array_keys($columns))), 119 + $xaction); 120 + continue; 121 + } 122 + } 123 + 124 + $new_phids = array_fuse($new_phids); 125 + $add_phids = array_diff_key($new_phids, $old_phids); 126 + 127 + if ($add_phids) { 128 + $panels = id(new PhabricatorDashboardPanelQuery()) 129 + ->setViewer($actor) 130 + ->withPHIDs($add_phids) 131 + ->execute(); 132 + $panels = mpull($panels, null, 'getPHID'); 133 + 134 + foreach ($add_phids as $add_phid) { 135 + $panel = idx($panels, $add_phid); 136 + 137 + if (!$panel) { 138 + $errors[] = $this->newInvalidError( 139 + pht( 140 + 'Panel specification adds panel "%s", but this is not a '. 141 + 'valid panel or not a visible panel. You can only add '. 142 + 'valid panels which you have permission to see to a dashboard.', 143 + $add_phid)); 144 + continue; 145 + } 146 + } 147 + } 148 + } 149 + 150 + return $errors; 151 + } 152 + 153 + }
+2 -1
webroot/rsrc/js/application/dashboard/behavior-dashboard-async-panel.js
··· 12 12 var data = { 13 13 parentPanelPHIDs: config.parentPanelPHIDs.join(','), 14 14 headerMode: config.headerMode, 15 - dashboardID: config.dashboardID 15 + contextPHID: config.contextPHID, 16 + panelKey: config.panelKey 16 17 }; 17 18 18 19 new JX.Workflow(config.uri)