From 82c46f4b93bda377185cf91cb55aa4f900134af0 Mon Sep 17 00:00:00 2001 From: epriestley Date: Fri, 12 Apr 2019 10:39:11 -0700 Subject: [PATCH] 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 --- resources/celerity/map.php | 16 +- src/__phutil_library_map__.php | 10 +- .../engine/PhabricatorAuthSessionEngine.php | 4 +- .../PhabricatorDashboardApplication.php | 5 +- ...PhabricatorDashboardAddPanelController.php | 103 --------- ...bricatorDashboardRemovePanelController.php | 77 ------- .../PhabricatorDashboardAdjustController.php | 202 ++++++++++++++++++ ...habricatorDashboardPanelEditController.php | 38 ++-- ...bricatorDashboardPanelRenderController.php | 19 +- .../PhabricatorDashboardPanelEditEngine.php | 70 +++--- ...abricatorDashboardPanelRenderingEngine.php | 49 +++-- .../PhabricatorDashboardRenderingEngine.php | 28 ++- .../PhabricatorDashboardLayoutConfig.php | 128 ----------- .../PhabricatorDashboardPanelRef.php | 8 + .../PhabricatorDashboardPanelRefList.php | 43 ++++ .../storage/PhabricatorDashboard.php | 31 ++- .../PhabricatorDashboardPanelsTransaction.php | 153 +++++++++++++ .../behavior-dashboard-async-panel.js | 3 +- 18 files changed, 574 insertions(+), 413 deletions(-) delete mode 100644 src/applications/dashboard/controller/PhabricatorDashboardAddPanelController.php delete mode 100644 src/applications/dashboard/controller/PhabricatorDashboardRemovePanelController.php create mode 100644 src/applications/dashboard/controller/dashboard/PhabricatorDashboardAdjustController.php delete mode 100644 src/applications/dashboard/layoutconfig/PhabricatorDashboardLayoutConfig.php create mode 100644 src/applications/dashboard/xaction/dashboard/PhabricatorDashboardPanelsTransaction.php diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 0088b09935..1a97885700 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -10,7 +10,7 @@ return array( 'conpherence.pkg.css' => '3c8a0668', 'conpherence.pkg.js' => '020aebcf', 'core.pkg.css' => '294e365c', - 'core.pkg.js' => '794952ae', + 'core.pkg.js' => '69247edd', 'differential.pkg.css' => '8d8360fb', 'differential.pkg.js' => '67e02996', 'diffusion.pkg.css' => '42c75c37', @@ -371,7 +371,7 @@ return array( 'rsrc/js/application/conpherence/behavior-toggle-widget.js' => '8f959ad0', 'rsrc/js/application/countdown/timer.js' => '6a162524', 'rsrc/js/application/daemon/behavior-bulk-job-reload.js' => '3829a3cf', - 'rsrc/js/application/dashboard/behavior-dashboard-async-panel.js' => '09ecf50c', + 'rsrc/js/application/dashboard/behavior-dashboard-async-panel.js' => 'a871fe00', 'rsrc/js/application/dashboard/behavior-dashboard-move-panels.js' => '076bd092', 'rsrc/js/application/dashboard/behavior-dashboard-query-panel-select.js' => '1e413dc9', 'rsrc/js/application/dashboard/behavior-dashboard-tab-panel.js' => '0116d3e8', @@ -594,7 +594,7 @@ return array( 'javelin-behavior-conpherence-search' => '91befbcc', 'javelin-behavior-countdown-timer' => '6a162524', 'javelin-behavior-dark-console' => 'f39d968b', - 'javelin-behavior-dashboard-async-panel' => '09ecf50c', + 'javelin-behavior-dashboard-async-panel' => 'a871fe00', 'javelin-behavior-dashboard-move-panels' => '076bd092', 'javelin-behavior-dashboard-query-panel-select' => '1e413dc9', 'javelin-behavior-dashboard-tab-panel' => '0116d3e8', @@ -982,11 +982,6 @@ return array( 'herald-rule-editor', 'javelin-behavior', ), - '09ecf50c' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-workflow', - ), '0ad8d31f' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1794,6 +1789,11 @@ return array( 'javelin-install', 'javelin-dom', ), + 'a871fe00' => array( + 'javelin-behavior', + 'javelin-dom', + 'javelin-workflow', + ), 'a9942052' => array( 'javelin-behavior', 'javelin-dom', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index dcccc7c6d6..2d4900551a 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2906,7 +2906,7 @@ phutil_register_library_map(array( 'PhabricatorDarkConsoleTabSetting' => 'applications/settings/setting/PhabricatorDarkConsoleTabSetting.php', 'PhabricatorDarkConsoleVisibleSetting' => 'applications/settings/setting/PhabricatorDarkConsoleVisibleSetting.php', 'PhabricatorDashboard' => 'applications/dashboard/storage/PhabricatorDashboard.php', - 'PhabricatorDashboardAddPanelController' => 'applications/dashboard/controller/PhabricatorDashboardAddPanelController.php', + 'PhabricatorDashboardAdjustController' => 'applications/dashboard/controller/dashboard/PhabricatorDashboardAdjustController.php', 'PhabricatorDashboardApplication' => 'applications/dashboard/application/PhabricatorDashboardApplication.php', 'PhabricatorDashboardApplicationInstallWorkflow' => 'applications/dashboard/install/PhabricatorDashboardApplicationInstallWorkflow.php', 'PhabricatorDashboardArchiveController' => 'applications/dashboard/controller/dashboard/PhabricatorDashboardArchiveController.php', @@ -2927,7 +2927,6 @@ phutil_register_library_map(array( 'PhabricatorDashboardInstall' => 'applications/dashboard/storage/PhabricatorDashboardInstall.php', 'PhabricatorDashboardInstallController' => 'applications/dashboard/controller/dashboard/PhabricatorDashboardInstallController.php', 'PhabricatorDashboardInstallWorkflow' => 'applications/dashboard/install/PhabricatorDashboardInstallWorkflow.php', - 'PhabricatorDashboardLayoutConfig' => 'applications/dashboard/layoutconfig/PhabricatorDashboardLayoutConfig.php', 'PhabricatorDashboardLayoutMode' => 'applications/dashboard/layoutconfig/PhabricatorDashboardLayoutMode.php', 'PhabricatorDashboardLayoutTransaction' => 'applications/dashboard/xaction/dashboard/PhabricatorDashboardLayoutTransaction.php', 'PhabricatorDashboardListController' => 'applications/dashboard/controller/PhabricatorDashboardListController.php', @@ -2964,6 +2963,7 @@ phutil_register_library_map(array( 'PhabricatorDashboardPanelType' => 'applications/dashboard/paneltype/PhabricatorDashboardPanelType.php', 'PhabricatorDashboardPanelUsedByObjectEdgeType' => 'applications/search/edge/PhabricatorDashboardPanelUsedByObjectEdgeType.php', 'PhabricatorDashboardPanelViewController' => 'applications/dashboard/controller/panel/PhabricatorDashboardPanelViewController.php', + 'PhabricatorDashboardPanelsTransaction' => 'applications/dashboard/xaction/dashboard/PhabricatorDashboardPanelsTransaction.php', 'PhabricatorDashboardPortal' => 'applications/dashboard/storage/PhabricatorDashboardPortal.php', 'PhabricatorDashboardPortalController' => 'applications/dashboard/controller/portal/PhabricatorDashboardPortalController.php', 'PhabricatorDashboardPortalDatasource' => 'applications/dashboard/typeahead/PhabricatorDashboardPortalDatasource.php', @@ -2998,7 +2998,6 @@ phutil_register_library_map(array( 'PhabricatorDashboardQueryPanelQueryTransaction' => 'applications/dashboard/xaction/panel/PhabricatorDashboardQueryPanelQueryTransaction.php', 'PhabricatorDashboardQueryPanelType' => 'applications/dashboard/paneltype/PhabricatorDashboardQueryPanelType.php', 'PhabricatorDashboardRemarkupRule' => 'applications/dashboard/remarkup/PhabricatorDashboardRemarkupRule.php', - 'PhabricatorDashboardRemovePanelController' => 'applications/dashboard/controller/PhabricatorDashboardRemovePanelController.php', 'PhabricatorDashboardRenderingEngine' => 'applications/dashboard/engine/PhabricatorDashboardRenderingEngine.php', 'PhabricatorDashboardSchemaSpec' => 'applications/dashboard/storage/PhabricatorDashboardSchemaSpec.php', 'PhabricatorDashboardSearchEngine' => 'applications/dashboard/query/PhabricatorDashboardSearchEngine.php', @@ -8906,7 +8905,7 @@ phutil_register_library_map(array( 'PhabricatorNgramsInterface', 'PhabricatorDashboardPanelContainerInterface', ), - 'PhabricatorDashboardAddPanelController' => 'PhabricatorDashboardController', + 'PhabricatorDashboardAdjustController' => 'PhabricatorDashboardController', 'PhabricatorDashboardApplication' => 'PhabricatorApplication', 'PhabricatorDashboardApplicationInstallWorkflow' => 'PhabricatorDashboardInstallWorkflow', 'PhabricatorDashboardArchiveController' => 'PhabricatorDashboardController', @@ -8927,7 +8926,6 @@ phutil_register_library_map(array( 'PhabricatorDashboardInstall' => 'PhabricatorDashboardDAO', 'PhabricatorDashboardInstallController' => 'PhabricatorDashboardController', 'PhabricatorDashboardInstallWorkflow' => 'Phobject', - 'PhabricatorDashboardLayoutConfig' => 'Phobject', 'PhabricatorDashboardLayoutMode' => 'Phobject', 'PhabricatorDashboardLayoutTransaction' => 'PhabricatorDashboardTransactionType', 'PhabricatorDashboardListController' => 'PhabricatorDashboardController', @@ -8971,6 +8969,7 @@ phutil_register_library_map(array( 'PhabricatorDashboardPanelType' => 'Phobject', 'PhabricatorDashboardPanelUsedByObjectEdgeType' => 'PhabricatorEdgeType', 'PhabricatorDashboardPanelViewController' => 'PhabricatorDashboardController', + 'PhabricatorDashboardPanelsTransaction' => 'PhabricatorDashboardTransactionType', 'PhabricatorDashboardPortal' => array( 'PhabricatorDashboardDAO', 'PhabricatorApplicationTransactionInterface', @@ -9013,7 +9012,6 @@ phutil_register_library_map(array( 'PhabricatorDashboardQueryPanelQueryTransaction' => 'PhabricatorDashboardPanelPropertyTransaction', 'PhabricatorDashboardQueryPanelType' => 'PhabricatorDashboardPanelType', 'PhabricatorDashboardRemarkupRule' => 'PhabricatorObjectRemarkupRule', - 'PhabricatorDashboardRemovePanelController' => 'PhabricatorDashboardController', 'PhabricatorDashboardRenderingEngine' => 'Phobject', 'PhabricatorDashboardSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorDashboardSearchEngine' => 'PhabricatorApplicationSearchEngine', diff --git a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php index 38ae2201b8..e4956335a0 100644 --- a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php +++ b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php @@ -389,7 +389,7 @@ final class PhabricatorAuthSessionEngine extends Phobject { * appropriate for one-time checks. * * @param PhabricatorUser User whose session needs to be in high security. - * @param AphrontReqeust Current request. + * @param AphrontRequest Current request. * @param string URI to return the user to if they cancel. * @return PhabricatorAuthHighSecurityToken Security token. * @task hisec @@ -421,7 +421,7 @@ final class PhabricatorAuthSessionEngine extends Phobject { * use @{method:requireHighSecurityToken}. * * @param PhabricatorUser User whose session needs to be in high security. - * @param AphrontReqeust Current request. + * @param AphrontRequest Current request. * @param string URI to return the user to if they cancel. * @param bool True to jump partial sessions directly into high * security instead of just upgrading them to full diff --git a/src/applications/dashboard/application/PhabricatorDashboardApplication.php b/src/applications/dashboard/application/PhabricatorDashboardApplication.php index 78bbe41223..abe3676913 100644 --- a/src/applications/dashboard/application/PhabricatorDashboardApplication.php +++ b/src/applications/dashboard/application/PhabricatorDashboardApplication.php @@ -48,10 +48,9 @@ final class PhabricatorDashboardApplication extends PhabricatorApplication { '(?:(?P[^/]+)/)?)?' => 'PhabricatorDashboardInstallController', 'console/' => 'PhabricatorDashboardConsoleController', - 'addpanel/(?P\d+)/' => 'PhabricatorDashboardAddPanelController', 'movepanel/(?P\d+)/' => 'PhabricatorDashboardMovePanelController', - 'removepanel/(?P\d+)/' - => 'PhabricatorDashboardRemovePanelController', + 'adjust/(?Premove|add)/' + => 'PhabricatorDashboardAdjustController', 'panel/' => array( 'install/(?P[^/]+)/(?:(?P[^/]+)/)?' => 'PhabricatorDashboardQueryPanelInstallController', diff --git a/src/applications/dashboard/controller/PhabricatorDashboardAddPanelController.php b/src/applications/dashboard/controller/PhabricatorDashboardAddPanelController.php deleted file mode 100644 index d91a1a9da6..0000000000 --- a/src/applications/dashboard/controller/PhabricatorDashboardAddPanelController.php +++ /dev/null @@ -1,103 +0,0 @@ -getViewer(); - $id = $request->getURIData('id'); - - $dashboard = id(new PhabricatorDashboardQuery()) - ->setViewer($viewer) - ->withIDs(array($id)) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - if (!$dashboard) { - return new Aphront404Response(); - } - - $redirect_uri = $this->getApplicationURI( - 'arrange/'.$dashboard->getID().'/'); - - $v_panel = head($request->getArr('panel')); - $e_panel = true; - $errors = array(); - if ($request->isFormPost()) { - if (strlen($v_panel)) { - $panel = id(new PhabricatorDashboardPanelQuery()) - ->setViewer($viewer) - ->withIDs(array($v_panel)) - ->executeOne(); - if (!$panel) { - $errors[] = pht('Not a valid panel.'); - $e_panel = pht('Invalid'); - } - - $on_dashboard = $dashboard->getPanels(); - $on_ids = mpull($on_dashboard, null, 'getID'); - if (array_key_exists($v_panel, $on_ids)) { - $p_name = $panel->getName(); - $errors[] = pht('Panel "%s" already exists on dashboard.', $p_name); - $e_panel = pht('Invalid'); - } - - } else { - $errors[] = pht('Select a panel to add.'); - $e_panel = pht('Required'); - } - - if (!$errors) { - PhabricatorDashboardTransactionEditor::addPanelToDashboard( - $viewer, - PhabricatorContentSource::newFromRequest($request), - $panel, - $dashboard, - $request->getInt('column', 0)); - - return id(new AphrontRedirectResponse())->setURI($redirect_uri); - } - } - - $panels = id(new PhabricatorDashboardPanelQuery()) - ->setViewer($viewer) - ->withArchived(false) - ->execute(); - - if (!$panels) { - return $this->newDialog() - ->setTitle(pht('No Panels Exist Yet')) - ->appendParagraph( - pht( - 'You have not created any dashboard panels yet, so you can not '. - 'add an existing panel.')) - ->appendParagraph( - pht('Instead, add a new panel.')) - ->addCancelButton($redirect_uri); - } - - $form = id(new AphrontFormView()) - ->setUser($viewer) - ->addHiddenInput('column', $request->getInt('column')) - ->appendRemarkupInstructions( - pht('Choose a panel to add to this dashboard:')) - ->appendChild( - id(new AphrontFormTokenizerControl()) - ->setUser($this->getViewer()) - ->setDatasource(new PhabricatorDashboardPanelDatasource()) - ->setLimit(1) - ->setName('panel') - ->setLabel(pht('Panel'))); - - return $this->newDialog() - ->setTitle(pht('Add Panel')) - ->setErrors($errors) - ->appendChild($form->buildLayoutView()) - ->addCancelButton($redirect_uri) - ->addSubmitButton(pht('Add Panel')); - } - -} diff --git a/src/applications/dashboard/controller/PhabricatorDashboardRemovePanelController.php b/src/applications/dashboard/controller/PhabricatorDashboardRemovePanelController.php deleted file mode 100644 index 0e6c095fe9..0000000000 --- a/src/applications/dashboard/controller/PhabricatorDashboardRemovePanelController.php +++ /dev/null @@ -1,77 +0,0 @@ -getViewer(); - $id = $request->getURIData('id'); - - $dashboard = id(new PhabricatorDashboardQuery()) - ->setViewer($viewer) - ->withIDs(array($id)) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->executeOne(); - if (!$dashboard) { - return new Aphront404Response(); - } - - // NOTE: If you can edit a dashboard, you can remove panels from it even - // if you don't have permission to see them or they aren't valid. We only - // require that the panel be present on the dashboard. - - $v_panel = $request->getStr('panelPHID'); - - $panel_on_dashboard = false; - $layout = $dashboard->getLayoutConfigObject(); - $columns = $layout->getPanelLocations(); - foreach ($columns as $column) { - foreach ($column as $column_panel_phid) { - if ($column_panel_phid == $v_panel) { - $panel_on_dashboard = true; - break; - } - } - } - - if (!$panel_on_dashboard) { - return new Aphront404Response(); - } - - $redirect_uri = $dashboard->getURI(); - $layout_config = $dashboard->getLayoutConfigObject(); - - if ($request->isFormPost()) { - $xactions = array(); - - $layout_config->removePanel($v_panel); - $dashboard->setLayoutConfigFromObject($layout_config); - - $editor = id(new PhabricatorDashboardTransactionEditor()) - ->setActor($viewer) - ->setContentSourceFromRequest($request) - ->setContinueOnMissingFields(true) - ->setContinueOnNoEffect(true) - ->applyTransactions($dashboard, $xactions); - - return id(new AphrontRedirectResponse())->setURI($redirect_uri); - } - - $form = id(new AphrontFormView()) - ->setUser($viewer) - ->addHiddenInput('confirm', true) - ->addHiddenInput('panelPHID', $v_panel) - ->appendChild(pht('Are you sure you want to remove this panel?')); - - return $this->newDialog() - ->setTitle(pht('Remove Panel')) - ->appendChild($form->buildLayoutView()) - ->addCancelButton($redirect_uri) - ->addSubmitButton(pht('Remove Panel')); - } - -} diff --git a/src/applications/dashboard/controller/dashboard/PhabricatorDashboardAdjustController.php b/src/applications/dashboard/controller/dashboard/PhabricatorDashboardAdjustController.php new file mode 100644 index 0000000000..46c8a75351 --- /dev/null +++ b/src/applications/dashboard/controller/dashboard/PhabricatorDashboardAdjustController.php @@ -0,0 +1,202 @@ +getViewer(); + + $context_phid = $request->getStr('contextPHID'); + + $dashboard = id(new PhabricatorDashboardQuery()) + ->setViewer($viewer) + ->withPHIDs(array($context_phid)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$dashboard) { + return new Aphront404Response(); + } + + $this->contextPHID = $context_phid; + + $done_uri = $dashboard->getURI(); + $ref_list = $dashboard->getPanelRefList(); + + $panel_ref = null; + $panel_key = $request->getStr('panelKey'); + if (strlen($panel_key)) { + $panel_ref = $ref_list->getPanelRef($panel_key); + if (!$panel_ref) { + return new Aphront404Response(); + } + + $this->panelKey = $panel_key; + } else { + $panel_ref = null; + } + + $column_key = $request->getStr('columnKey'); + if (strlen($column_key)) { + $columns = $ref_list->getColumns(); + if (!isset($columns[$column_key])) { + return new Aphront404Response(); + } + $this->columnKey = $column_key; + } + + switch ($request->getURIData('op')) { + case 'add': + return $this->handleAddRequest($dashboard, $done_uri); + case 'remove': + if (!$panel_ref) { + return new Aphront404Response(); + } + return $this->handleRemoveRequest($dashboard, $panel_ref, $done_uri); + } + } + + private function handleAddRequest( + PhabricatorDashboard $dashboard, + $done_uri) { + $request = $this->getRequest(); + $viewer = $this->getViewer(); + + $errors = array(); + + $panel_phid = null; + $e_panel = true; + if ($request->isFormPost()) { + $panel_phid = head($request->getArr('panelPHIDs')); + + if (!$panel_phid) { + $errors[] = pht('You must choose a panel to add to the dashboard.'); + $e_panel = pht('Required'); + } else { + $panel = id(new PhabricatorDashboardPanelQuery()) + ->setViewer($viewer) + ->withPHIDs(array($panel_phid)) + ->executeOne(); + if (!$panel) { + $errors[] = pht('You must choose a valid panel.'); + $e_panel = pht('Invalid'); + } + } + + if (!$errors) { + $xactions = array(); + + $ref_list = clone $dashboard->getPanelRefList(); + $ref_list->newPanelRef($panel, $this->columnKey); + $new_panels = $ref_list->toDictionary(); + + $xactions[] = $dashboard->getApplicationTransactionTemplate() + ->setTransactionType( + PhabricatorDashboardPanelsTransaction::TRANSACTIONTYPE) + ->setNewValue($new_panels); + + $editor = $dashboard->getApplicationTransactionEditor() + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true); + + $editor->applyTransactions($dashboard, $xactions); + + return id(new AphrontRedirectResponse())->setURI($done_uri); + } + } + + if ($panel_phid) { + $panel_phids = array($panel_phid); + } else { + $panel_phids = array(); + } + + $form = id(new AphrontFormView()) + ->setViewer($viewer) + ->appendRemarkupInstructions( + pht('Choose a panel to add to this dashboard:')) + ->appendControl( + id(new AphrontFormTokenizerControl()) + ->setDatasource(new PhabricatorDashboardPanelDatasource()) + ->setLimit(1) + ->setName('panelPHIDs') + ->setLabel(pht('Panel')) + ->setError($e_panel) + ->setValue($panel_phids)); + + return $this->newEditDialog() + ->setTitle(pht('Add Panel')) + ->setWidth(AphrontDialogView::WIDTH_FORM) + ->setErrors($errors) + ->appendForm($form) + ->addCancelButton($done_uri) + ->addSubmitButton(pht('Add Panel')); + } + + private function handleRemoveRequest( + PhabricatorDashboard $dashboard, + PhabricatorDashboardPanelRef $panel_ref, + $done_uri) { + $request = $this->getRequest(); + $viewer = $this->getViewer(); + + // NOTE: If you can edit a dashboard, you can remove panels from it even + // if you don't have permission to see them or they aren't valid. We only + // require that the panel be present on the dashboard. + + if ($request->isFormPost()) { + $xactions = array(); + + $ref_list = clone $dashboard->getPanelRefList(); + $ref_list->removePanelRef($panel_ref); + $new_panels = $ref_list->toDictionary(); + + $xactions[] = $dashboard->getApplicationTransactionTemplate() + ->setTransactionType( + PhabricatorDashboardPanelsTransaction::TRANSACTIONTYPE) + ->setNewValue($new_panels); + + $editor = $dashboard->getApplicationTransactionEditor() + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true); + + $editor->applyTransactions($dashboard, $xactions); + + return id(new AphrontRedirectResponse())->setURI($done_uri); + } + + $panel_phid = $panel_ref->getPanelPHID(); + $handles = $viewer->loadHandles(array($panel_phid)); + $handle = $handles[$panel_phid]; + + $message = pht( + 'Remove panel %s from dashboard %s?', + phutil_tag('strong', array(), $handle->getFullName()), + phutil_tag('strong', array(), $dashboard->getName())); + + return $this->newEditDialog() + ->setTitle(pht('Remove Dashboard Panel')) + ->appendParagraph($message) + ->addCancelButton($done_uri) + ->addSubmitButton(pht('Remove Panel')); + } + + private function newEditDialog() { + return $this->newDialog() + ->addHiddenInput('contextPHID', $this->contextPHID) + ->addHiddenInput('panelKey', $this->panelKey) + ->addHiddenInput('columnKey', $this->columnKey); + } + +} diff --git a/src/applications/dashboard/controller/panel/PhabricatorDashboardPanelEditController.php b/src/applications/dashboard/controller/panel/PhabricatorDashboardPanelEditController.php index cc35419687..4ab76d18b5 100644 --- a/src/applications/dashboard/controller/panel/PhabricatorDashboardPanelEditController.php +++ b/src/applications/dashboard/controller/panel/PhabricatorDashboardPanelEditController.php @@ -9,37 +9,43 @@ final class PhabricatorDashboardPanelEditController $engine = id(new PhabricatorDashboardPanelEditEngine()) ->setController($this); - // We can create or edit a panel in the context of a dashboard. If we - // started on a dashboard, we want to return to that dashboard when we're - // done editing. - $dashboard_id = $request->getStr('dashboardID'); - if (strlen($dashboard_id)) { - $dashboard = id(new PhabricatorDashboardQuery()) + // We can create or edit a panel in the context of a dashboard or + // container panel, like a tab panel. If we started this flow on some + // container object, we want to return to that container when we're done + // editing. + + $context_phid = $request->getStr('contextPHID'); + if (strlen($context_phid)) { + $context = id(new PhabricatorObjectQuery()) ->setViewer($viewer) - ->withIDs(array($dashboard_id)) + ->withPHIDs(array($context_phid)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); - if (!$dashboard) { + if (!$context) { + return new Aphront404Response(); + } + + if (!($context instanceof PhabricatorDashboardPanelContainerInterface)) { return new Aphront404Response(); } $engine - ->setDashboard($dashboard) - ->addContextParameter('dashboardID', $dashboard_id); + ->setContextObject($context) + ->addContextParameter('contextPHID', $context_phid); } else { - $dashboard = null; + $context = null; } $id = $request->getURIData('id'); if (!$id) { - $column_id = $request->getStr('columnID'); + $column_key = $request->getStr('columnKey'); - if ($dashboard) { - $cancel_uri = $dashboard->getURI(); + if ($context) { + $cancel_uri = $context->getURI(); } else { $cancel_uri = $this->getApplicationURI('panel/'); } @@ -52,9 +58,9 @@ final class PhabricatorDashboardPanelEditController $engine ->addContextParameter('panelType', $panel_type) - ->addContextParameter('columnID', $column_id) + ->addContextParameter('columnKey', $column_key) ->setPanelType($panel_type) - ->setColumnID($column_id); + ->setColumnKey($column_key); } return $engine->buildResponse(); diff --git a/src/applications/dashboard/controller/panel/PhabricatorDashboardPanelRenderController.php b/src/applications/dashboard/controller/panel/PhabricatorDashboardPanelRenderController.php index 8f25a0f9bb..ebdd4bbb13 100644 --- a/src/applications/dashboard/controller/panel/PhabricatorDashboardPanelRenderController.php +++ b/src/applications/dashboard/controller/panel/PhabricatorDashboardPanelRenderController.php @@ -31,14 +31,27 @@ final class PhabricatorDashboardPanelRenderController $parent_phids = array(); } - $rendered_panel = id(new PhabricatorDashboardPanelRenderingEngine()) + $engine = id(new PhabricatorDashboardPanelRenderingEngine()) ->setViewer($viewer) ->setPanel($panel) ->setPanelPHID($panel->getPHID()) ->setParentPanelPHIDs($parent_phids) ->setHeaderMode($request->getStr('headerMode')) - ->setDashboardID($request->getInt('dashboardID')) - ->renderPanel(); + ->setPanelKey($request->getStr('panelKey')); + + $context_phid = $request->getStr('contextPHID'); + if ($context_phid) { + $context = id(new PhabricatorObjectQuery()) + ->setViewer($viewer) + ->withPHIDs(array($context_phid)) + ->executeOne(); + if (!$context) { + return new Aphront404Response(); + } + $engine->setContextObject($context); + } + + $rendered_panel = $engine->renderPanel(); if ($request->isAjax()) { return id(new AphrontAjaxResponse()) diff --git a/src/applications/dashboard/editor/PhabricatorDashboardPanelEditEngine.php b/src/applications/dashboard/editor/PhabricatorDashboardPanelEditEngine.php index d4351e8eee..da891ebd5d 100644 --- a/src/applications/dashboard/editor/PhabricatorDashboardPanelEditEngine.php +++ b/src/applications/dashboard/editor/PhabricatorDashboardPanelEditEngine.php @@ -6,8 +6,8 @@ final class PhabricatorDashboardPanelEditEngine const ENGINECONST = 'dashboard.panel'; private $panelType; - private $dashboard; - private $columnID; + private $contextObject; + private $columnKey; public function setPanelType($panel_type) { $this->panelType = $panel_type; @@ -18,22 +18,22 @@ final class PhabricatorDashboardPanelEditEngine return $this->panelType; } - public function setDashboard(PhabricatorDashboard $dashboard) { - $this->dashboard = $dashboard; + public function setContextObject($context) { + $this->contextObject = $context; return $this; } - public function getDashboard() { - return $this->dashboard; + public function getContextObject() { + return $this->contextObject; } - public function setColumnID($column_id) { - $this->columnID = $column_id; + public function setColumnKey($column_key) { + $this->columnKey = $column_key; return $this; } - public function getColumnID() { - return $this->columnID; + public function getColumnKey() { + return $this->columnKey; } public function isEngineConfigurable() { @@ -84,27 +84,27 @@ final class PhabricatorDashboardPanelEditEngine } protected function getObjectCreateCancelURI($object) { - $dashboard = $this->getDashboard(); - if ($dashboard) { - return $dashboard->getURI(); + $context = $this->getContextObject(); + if ($context) { + return $context->getURI(); } return parent::getObjectCreateCancelURI($object); } public function getEffectiveObjectEditDoneURI($object) { - $dashboard = $this->getDashboard(); - if ($dashboard) { - return $dashboard->getURI(); + $context = $this->getContextObject(); + if ($context) { + return $context->getURI(); } return parent::getEffectiveObjectEditDoneURI($object); } protected function getObjectEditCancelURI($object) { - $dashboard = $this->getDashboard(); - if ($dashboard) { - return $dashboard->getURI(); + $context = $this->getContextObject(); + if ($context) { + return $context->getURI(); } return parent::getObjectEditCancelURI($object); @@ -131,18 +131,34 @@ final class PhabricatorDashboardPanelEditEngine } protected function didApplyTransactions($object, array $xactions) { - $dashboard = $this->getDashboard(); - if ($dashboard) { + $context = $this->getContextObject(); + + if ($context instanceof PhabricatorDashboard) { $viewer = $this->getViewer(); $controller = $this->getController(); $request = $controller->getRequest(); - PhabricatorDashboardTransactionEditor::addPanelToDashboard( - $viewer, - PhabricatorContentSource::newFromRequest($request), - $object, - $dashboard, - (int)$this->getColumnID()); + $dashboard = $context; + + $xactions = array(); + + $ref_list = clone $dashboard->getPanelRefList(); + + $ref_list->newPanelRef($object, $this->getColumnKey()); + $new_panels = $ref_list->toDictionary(); + + $xactions[] = $dashboard->getApplicationTransactionTemplate() + ->setTransactionType( + PhabricatorDashboardPanelsTransaction::TRANSACTIONTYPE) + ->setNewValue($new_panels); + + $editor = $dashboard->getApplicationTransactionEditor() + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true); + + $editor->applyTransactions($dashboard, $xactions); } } diff --git a/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php b/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php index 067e9b8dad..080bf28574 100644 --- a/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php +++ b/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php @@ -12,11 +12,11 @@ final class PhabricatorDashboardPanelRenderingEngine extends Phobject { private $enableAsyncRendering; private $parentPanelPHIDs; private $headerMode = self::HEADER_MODE_NORMAL; - private $dashboardID; private $movable = true; private $panelHandle; private $editMode; private $contextObject; + private $panelKey; public function setContextObject($object) { $this->contextObject = $object; @@ -27,13 +27,13 @@ final class PhabricatorDashboardPanelRenderingEngine extends Phobject { return $this->contextObject; } - public function setDashboardID($id) { - $this->dashboardID = $id; + public function setPanelKey($panel_key) { + $this->panelKey = $panel_key; return $this; } - public function getDashboardID() { - return $this->dashboardID; + public function getPanelKey() { + return $this->panelKey; } public function setHeaderMode($header_mode) { @@ -182,10 +182,10 @@ final class PhabricatorDashboardPanelRenderingEngine extends Phobject { private function renderAsyncPanel() { + $context_phid = $this->getContextPHID(); $panel = $this->getPanel(); $panel_id = celerity_generate_unique_node_id(); - $dashboard_id = $this->getDashboardID(); Javelin::initBehavior( 'dashboard-async-panel', @@ -193,7 +193,8 @@ final class PhabricatorDashboardPanelRenderingEngine extends Phobject { 'panelID' => $panel_id, 'parentPanelPHIDs' => $this->getParentPanelPHIDs(), 'headerMode' => $this->getHeaderMode(), - 'dashboardID' => $dashboard_id, + 'contextPHID' => $context_phid, + 'panelKey' => $this->getPanelKey(), 'uri' => '/dashboard/panel/render/'.$panel->getID().'/', )); @@ -322,7 +323,7 @@ final class PhabricatorDashboardPanelRenderingEngine extends Phobject { $viewer = $this->getViewer(); $panel = $this->getPanel(); - $dashboard_id = $this->getDashboardID(); + $context_phid = $this->getContextPHID(); $actions = array(); @@ -330,15 +331,15 @@ final class PhabricatorDashboardPanelRenderingEngine extends Phobject { $panel_id = $panel->getID(); $edit_uri = "/dashboard/panel/edit/{$panel_id}/"; - $edit_uri = new PhutilURI($edit_uri); - if ($dashboard_id) { - $edit_uri->replaceQueryParam('dashboardID', $dashboard_id); - } + $params = array( + 'contextPHID' => $context_phid, + ); + $edit_uri = new PhutilURI($edit_uri, $params); $actions[] = id(new PhabricatorActionView()) ->setIcon('fa-pencil') ->setName(pht('Edit Panel')) - ->setHref((string)$edit_uri); + ->setHref($edit_uri); $actions[] = id(new PhabricatorActionView()) ->setIcon('fa-window-maximize') @@ -346,16 +347,19 @@ final class PhabricatorDashboardPanelRenderingEngine extends Phobject { ->setHref($panel->getURI()); } - if ($dashboard_id) { + if ($context_phid) { $panel_phid = $this->getPanelPHID(); - $remove_uri = "/dashboard/removepanel/{$dashboard_id}/"; - $remove_uri = id(new PhutilURI($remove_uri)) - ->replaceQueryParam('panelPHID', $panel_phid); + $remove_uri = urisprintf('/dashboard/adjust/remove/'); + $params = array( + 'contextPHID' => $context_phid, + 'panelKey' => $this->getPanelKey(), + ); + $remove_uri = new PhutilURI($remove_uri, $params); $actions[] = id(new PhabricatorActionView()) ->setIcon('fa-times') - ->setHref((string)$remove_uri) + ->setHref($remove_uri) ->setName(pht('Remove Panel')) ->setWorkflow(true); } @@ -415,5 +419,14 @@ final class PhabricatorDashboardPanelRenderingEngine extends Phobject { } } + private function getContextPHID() { + $context = $this->getContextObject(); + + if ($context) { + return $context->getPHID(); + } + + return null; + } } diff --git a/src/applications/dashboard/engine/PhabricatorDashboardRenderingEngine.php b/src/applications/dashboard/engine/PhabricatorDashboardRenderingEngine.php index 66e34f43ee..1427f58412 100644 --- a/src/applications/dashboard/engine/PhabricatorDashboardRenderingEngine.php +++ b/src/applications/dashboard/engine/PhabricatorDashboardRenderingEngine.php @@ -73,9 +73,9 @@ final class PhabricatorDashboardRenderingEngine extends Phobject { $panel_engine = id(new PhabricatorDashboardPanelRenderingEngine()) ->setViewer($viewer) - ->setDashboardID($dashboard->getID()) ->setEnableAsyncRendering(true) ->setContextObject($dashboard) + ->setPanelKey($panel_ref->getPanelKey()) ->setPanelPHID($panel_phid) ->setParentPanelPHIDs(array()) ->setHeaderMode($h_mode) @@ -94,14 +94,20 @@ final class PhabricatorDashboardRenderingEngine extends Phobject { if ($is_editable) { $column_views[] = $this->renderAddPanelPlaceHolder(); - $column_views[] = $this->renderAddPanelUI($column->getColumnKey()); + $column_views[] = $this->renderAddPanelUI($column); } + $sigil = 'dashboard-column'; + + $metadata = array( + 'columnKey' => $column->getColumnKey(), + ); + $result->addColumn( $column_views, implode(' ', $column_classes), - $sigil = 'dashboard-column', - $metadata = array('columnID' => $column)); + $sigil, + $metadata); } if ($is_editable) { @@ -133,15 +139,17 @@ final class PhabricatorDashboardRenderingEngine extends Phobject { pht('This column does not have any panels yet.')); } - private function renderAddPanelUI($column) { - $dashboard_id = $this->dashboard->getID(); + private function renderAddPanelUI(PhabricatorDashboardColumn $column) { + $dashboard = $this->getDashboard(); + $column_key = $column->getColumnKey(); $create_uri = id(new PhutilURI('/dashboard/panel/edit/')) - ->replaceQueryParam('dashboardID', $dashboard_id) - ->replaceQueryParam('columnID', $column); + ->replaceQueryParam('contextPHID', $dashboard->getPHID()) + ->replaceQueryParam('columnKey', $column_key); - $add_uri = id(new PhutilURI('/dashboard/addpanel/'.$dashboard_id.'/')) - ->replaceQueryParam('columnID', $column); + $add_uri = id(new PhutilURI('/dashboard/adjust/add/')) + ->replaceQueryParam('contextPHID', $dashboard->getPHID()) + ->replaceQueryParam('columnKey', $column_key); $create_button = id(new PHUIButtonView()) ->setTag('a') diff --git a/src/applications/dashboard/layoutconfig/PhabricatorDashboardLayoutConfig.php b/src/applications/dashboard/layoutconfig/PhabricatorDashboardLayoutConfig.php deleted file mode 100644 index 7b53c31194..0000000000 --- a/src/applications/dashboard/layoutconfig/PhabricatorDashboardLayoutConfig.php +++ /dev/null @@ -1,128 +0,0 @@ -layoutMode = $mode; - return $this; - } - public function getLayoutMode() { - return $this->layoutMode; - } - - public function setPanelLocation($which_column, $panel_phid) { - $this->panelLocations[$which_column][] = $panel_phid; - return $this; - } - - public function setPanelLocations(array $locations) { - $this->panelLocations = $locations; - return $this; - } - - public function getPanelLocations() { - return $this->panelLocations; - } - - public function replacePanel($old_phid, $new_phid) { - $locations = $this->getPanelLocations(); - foreach ($locations as $column => $panel_phids) { - foreach ($panel_phids as $key => $panel_phid) { - if ($panel_phid == $old_phid) { - $locations[$column][$key] = $new_phid; - } - } - } - return $this->setPanelLocations($locations); - } - - public function removePanel($panel_phid) { - $panel_location_grid = $this->getPanelLocations(); - foreach ($panel_location_grid as $column => $panel_columns) { - $found_old_column = array_search($panel_phid, $panel_columns); - if ($found_old_column !== false) { - $new_panel_columns = $panel_columns; - array_splice( - $new_panel_columns, - $found_old_column, - 1, - array()); - $panel_location_grid[$column] = $new_panel_columns; - break; - } - } - $this->setPanelLocations($panel_location_grid); - } - - public function getDefaultPanelLocations() { - switch ($this->getLayoutMode()) { - case self::MODE_HALF_AND_HALF: - case self::MODE_THIRD_AND_THIRDS: - case self::MODE_THIRDS_AND_THIRD: - $locations = array(array(), array()); - break; - case self::MODE_FULL: - default: - $locations = array(array()); - break; - } - return $locations; - } - - public function getColumnClass($column_index, $grippable = false) { - switch ($this->getLayoutMode()) { - case self::MODE_HALF_AND_HALF: - $class = 'half'; - break; - case self::MODE_THIRD_AND_THIRDS: - if ($column_index) { - $class = 'thirds'; - } else { - $class = 'third'; - } - break; - case self::MODE_THIRDS_AND_THIRD: - if ($column_index) { - $class = 'third'; - } else { - $class = 'thirds'; - } - break; - case self::MODE_FULL: - default: - $class = null; - break; - } - if ($grippable) { - $class .= ' grippable'; - } - return $class; - } - - public static function newFromDictionary(array $dict) { - $layout_config = id(new PhabricatorDashboardLayoutConfig()) - ->setLayoutMode(idx($dict, 'layoutMode', self::MODE_FULL)); - $layout_config->setPanelLocations(idx( - $dict, - 'panelLocations', - $layout_config->getDefaultPanelLocations())); - - return $layout_config; - } - - public function toDictionary() { - return array( - 'layoutMode' => $this->getLayoutMode(), - 'panelLocations' => $this->getPanelLocations(), - ); - } - -} diff --git a/src/applications/dashboard/layoutconfig/PhabricatorDashboardPanelRef.php b/src/applications/dashboard/layoutconfig/PhabricatorDashboardPanelRef.php index 5f43562d31..96a912e30a 100644 --- a/src/applications/dashboard/layoutconfig/PhabricatorDashboardPanelRef.php +++ b/src/applications/dashboard/layoutconfig/PhabricatorDashboardPanelRef.php @@ -34,4 +34,12 @@ final class PhabricatorDashboardPanelRef return $this->panelKey; } + public function toDictionary() { + return array( + 'panelKey' => $this->getPanelKey(), + 'panelPHID' => $this->getPanelPHID(), + 'columnKey' => $this->getColumnKey(), + ); + } + } diff --git a/src/applications/dashboard/layoutconfig/PhabricatorDashboardPanelRefList.php b/src/applications/dashboard/layoutconfig/PhabricatorDashboardPanelRefList.php index fd186887cd..5f798e4b5a 100644 --- a/src/applications/dashboard/layoutconfig/PhabricatorDashboardPanelRefList.php +++ b/src/applications/dashboard/layoutconfig/PhabricatorDashboardPanelRefList.php @@ -73,4 +73,47 @@ final class PhabricatorDashboardPanelRefList return $this->refs; } + public function getPanelRef($panel_key) { + foreach ($this->getPanelRefs() as $ref) { + if ($ref->getPanelKey() === $panel_key) { + return $ref; + } + } + + return null; + } + + public function toDictionary() { + return array_values(mpull($this->getPanelRefs(), 'toDictionary')); + } + + public function newPanelRef(PhabricatorDashboardPanel $panel, $column_key) { + $ref = id(new PhabricatorDashboardPanelRef()) + ->setPanelKey($this->newPanelKey()) + ->setPanelPHID($panel->getPHID()) + ->setColumnKey($column_key); + + $this->refs[] = $ref; + + return $ref; + } + + public function removePanelRef(PhabricatorDashboardPanelRef $target) { + foreach ($this->refs as $key => $ref) { + if ($ref->getPanelKey() !== $target->getPanelKey()) { + continue; + } + + unset($this->refs[$key]); + return $ref; + } + + return null; + } + + private function newPanelKey() { + return Filesystem::readRandomCharacters(8); + } + + } diff --git a/src/applications/dashboard/storage/PhabricatorDashboard.php b/src/applications/dashboard/storage/PhabricatorDashboard.php index c229f80736..9d9ef27ede 100644 --- a/src/applications/dashboard/storage/PhabricatorDashboard.php +++ b/src/applications/dashboard/storage/PhabricatorDashboard.php @@ -24,9 +24,6 @@ final class PhabricatorDashboard extends PhabricatorDashboardDAO const STATUS_ACTIVE = 'active'; const STATUS_ARCHIVED = 'archived'; - private $panels = self::ATTACHABLE; - private $edgeProjectPHIDs = self::ATTACHABLE; - private $panelRefList; public static function initializeNewDashboard(PhabricatorUser $actor) { @@ -36,8 +33,7 @@ final class PhabricatorDashboard extends PhabricatorDashboardDAO ->setViewPolicy(PhabricatorPolicies::getMostOpenPolicy()) ->setEditPolicy($actor->getPHID()) ->setStatus(self::STATUS_ACTIVE) - ->setAuthorPHID($actor->getPHID()) - ->attachPanels(array()); + ->setAuthorPHID($actor->getPHID()); } public static function getStatusNameMap() { @@ -62,9 +58,8 @@ final class PhabricatorDashboard extends PhabricatorDashboardDAO ) + parent::getConfiguration(); } - public function generatePHID() { - return PhabricatorPHID::generateNewPHID( - PhabricatorDashboardDashboardPHIDType::TYPECONST); + public function getPHIDType() { + return PhabricatorDashboardDashboardPHIDType::TYPECONST; } public function getRawLayoutMode() { @@ -75,11 +70,18 @@ final class PhabricatorDashboard extends PhabricatorDashboardDAO public function setRawLayoutMode($mode) { $config = $this->getRawLayoutConfig(); $config['layoutMode'] = $mode; + return $this->setRawLayoutConfig($config); + } - // If a cached panel ref list exists, clear it. - $this->panelRefList = null; + public function getRawPanels() { + $config = $this->getRawLayoutConfig(); + return idx($config, 'panels'); + } - return $this->setLayoutConfig($config); + public function setRawPanels(array $panels) { + $config = $this->getRawLayoutConfig(); + $config['panels'] = $panels; + return $this->setRawLayoutConfig($config); } private function getRawLayoutConfig() { @@ -92,6 +94,13 @@ final class PhabricatorDashboard extends PhabricatorDashboardDAO return $config; } + private function setRawLayoutConfig(array $config) { + // If a cached panel ref list exists, clear it. + $this->panelRefList = null; + + return $this->setLayoutConfig($config); + } + public function isArchived() { return ($this->getStatus() == self::STATUS_ARCHIVED); } diff --git a/src/applications/dashboard/xaction/dashboard/PhabricatorDashboardPanelsTransaction.php b/src/applications/dashboard/xaction/dashboard/PhabricatorDashboardPanelsTransaction.php new file mode 100644 index 0000000000..35248159bd --- /dev/null +++ b/src/applications/dashboard/xaction/dashboard/PhabricatorDashboardPanelsTransaction.php @@ -0,0 +1,153 @@ +getRawPanels(); + } + + public function applyInternalEffects($object, $value) { + $object->setRawPanels($value); + } + + public function getTitle() { + return pht( + '%s changed the panels on this dashboard.', + $this->renderAuthor()); + } + + public function validateTransactions($object, array $xactions) { + $actor = $this->getActor(); + $errors = array(); + + $ref_list = $object->getPanelRefList(); + $columns = $ref_list->getColumns(); + + $old_phids = $object->getPanelPHIDs(); + $old_phids = array_fuse($old_phids); + + foreach ($xactions as $xaction) { + $new_value = $xaction->getNewValue(); + if (!is_array($new_value)) { + $errors[] = $this->newInvalidError( + pht('Panels must be a list of panel specifications.'), + $xaction); + continue; + } + + if (!phutil_is_natural_list($new_value)) { + $errors[] = $this->newInvalidError( + pht('Panels must be a list, not a map.'), + $xaction); + continue; + } + + $new_phids = array(); + $seen_keys = array(); + foreach ($new_value as $idx => $spec) { + if (!is_array($spec)) { + $errors[] = $this->newInvalidError( + pht( + 'Each panel specification must be a map of panel attributes. '. + 'Panel specification at index "%s" is "%s".', + $idx, + phutil_describe_type($spec)), + $xaction); + continue; + } + + try { + PhutilTypeSpec::checkMap( + $spec, + array( + 'panelPHID' => 'string', + 'columnKey' => 'string', + 'panelKey' => 'string', + )); + } catch (PhutilTypeCheckException $ex) { + $errors[] = $this->newInvalidError( + pht( + 'Panel specification at index "%s" is invalid: %s', + $idx, + $ex->getMessage()), + $xaction); + continue; + } + + $panel_key = $spec['panelKey']; + + if (!strlen($panel_key)) { + $errors[] = $this->newInvalidError( + pht( + 'Panel specification at index "%s" has bad panel key "%s". '. + 'Panel keys must be nonempty.', + $idx, + $panel_key), + $xaction); + continue; + } + + if (isset($seen_keys[$panel_key])) { + $errors[] = $this->newInvalidError( + pht( + 'Panel specification at index "%s" has duplicate panel key '. + '"%s". Each panel must have a unique panel key.', + $idx, + $panel_key), + $xaction); + continue; + } + + $seen_keys[$panel_key] = true; + + $panel_phid = $spec['panelPHID']; + $new_phids[] = $panel_phid; + + $column_key = $spec['columnKey']; + + if (!isset($columns[$column_key])) { + $errors[] = $this->newInvalidError( + pht( + 'Panel specification at index "%s" has bad column key "%s", '. + 'valid column keys are: %s.', + $idx, + $column_key, + implode(', ', array_keys($columns))), + $xaction); + continue; + } + } + + $new_phids = array_fuse($new_phids); + $add_phids = array_diff_key($new_phids, $old_phids); + + if ($add_phids) { + $panels = id(new PhabricatorDashboardPanelQuery()) + ->setViewer($actor) + ->withPHIDs($add_phids) + ->execute(); + $panels = mpull($panels, null, 'getPHID'); + + foreach ($add_phids as $add_phid) { + $panel = idx($panels, $add_phid); + + if (!$panel) { + $errors[] = $this->newInvalidError( + pht( + 'Panel specification adds panel "%s", but this is not a '. + 'valid panel or not a visible panel. You can only add '. + 'valid panels which you have permission to see to a dashboard.', + $add_phid)); + continue; + } + } + } + } + + return $errors; + } + +} diff --git a/webroot/rsrc/js/application/dashboard/behavior-dashboard-async-panel.js b/webroot/rsrc/js/application/dashboard/behavior-dashboard-async-panel.js index 0ab1796e5b..99336f73c8 100644 --- a/webroot/rsrc/js/application/dashboard/behavior-dashboard-async-panel.js +++ b/webroot/rsrc/js/application/dashboard/behavior-dashboard-async-panel.js @@ -12,7 +12,8 @@ JX.behavior('dashboard-async-panel', function(config) { var data = { parentPanelPHIDs: config.parentPanelPHIDs.join(','), headerMode: config.headerMode, - dashboardID: config.dashboardID + contextPHID: config.contextPHID, + panelKey: config.panelKey }; new JX.Workflow(config.uri)