diff --git a/resources/celerity/map.php b/resources/celerity/map.php --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -10,7 +10,7 @@ '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 @@ '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 @@ '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 @@ 'herald-rule-editor', 'javelin-behavior', ), - '09ecf50c' => array( - 'javelin-behavior', - 'javelin-dom', - 'javelin-workflow', - ), '0ad8d31f' => array( 'javelin-behavior', 'javelin-stratcom', @@ -1794,6 +1789,11 @@ '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 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2906,7 +2906,7 @@ '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 @@ '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 @@ '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 @@ '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 @@ 'PhabricatorNgramsInterface', 'PhabricatorDashboardPanelContainerInterface', ), - 'PhabricatorDashboardAddPanelController' => 'PhabricatorDashboardController', + 'PhabricatorDashboardAdjustController' => 'PhabricatorDashboardController', 'PhabricatorDashboardApplication' => 'PhabricatorApplication', 'PhabricatorDashboardApplicationInstallWorkflow' => 'PhabricatorDashboardInstallWorkflow', 'PhabricatorDashboardArchiveController' => 'PhabricatorDashboardController', @@ -8927,7 +8926,6 @@ 'PhabricatorDashboardInstall' => 'PhabricatorDashboardDAO', 'PhabricatorDashboardInstallController' => 'PhabricatorDashboardController', 'PhabricatorDashboardInstallWorkflow' => 'Phobject', - 'PhabricatorDashboardLayoutConfig' => 'Phobject', 'PhabricatorDashboardLayoutMode' => 'Phobject', 'PhabricatorDashboardLayoutTransaction' => 'PhabricatorDashboardTransactionType', 'PhabricatorDashboardListController' => 'PhabricatorDashboardController', @@ -8971,6 +8969,7 @@ 'PhabricatorDashboardPanelType' => 'Phobject', 'PhabricatorDashboardPanelUsedByObjectEdgeType' => 'PhabricatorEdgeType', 'PhabricatorDashboardPanelViewController' => 'PhabricatorDashboardController', + 'PhabricatorDashboardPanelsTransaction' => 'PhabricatorDashboardTransactionType', 'PhabricatorDashboardPortal' => array( 'PhabricatorDashboardDAO', 'PhabricatorApplicationTransactionInterface', @@ -9013,7 +9012,6 @@ '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 --- a/src/applications/auth/engine/PhabricatorAuthSessionEngine.php +++ b/src/applications/auth/engine/PhabricatorAuthSessionEngine.php @@ -389,7 +389,7 @@ * 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 @@ * 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 --- a/src/applications/dashboard/application/PhabricatorDashboardApplication.php +++ b/src/applications/dashboard/application/PhabricatorDashboardApplication.php @@ -48,10 +48,9 @@ '(?:(?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 --- 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 --- 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 --- /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 --- a/src/applications/dashboard/controller/panel/PhabricatorDashboardPanelEditController.php +++ b/src/applications/dashboard/controller/panel/PhabricatorDashboardPanelEditController.php @@ -9,37 +9,43 @@ $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 @@ $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 --- a/src/applications/dashboard/controller/panel/PhabricatorDashboardPanelRenderController.php +++ b/src/applications/dashboard/controller/panel/PhabricatorDashboardPanelRenderController.php @@ -31,14 +31,27 @@ $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 --- a/src/applications/dashboard/editor/PhabricatorDashboardPanelEditEngine.php +++ b/src/applications/dashboard/editor/PhabricatorDashboardPanelEditEngine.php @@ -6,8 +6,8 @@ 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 @@ 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 @@ } 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 @@ } 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 --- a/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php +++ b/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php @@ -12,11 +12,11 @@ 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 @@ 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 @@ 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 @@ '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 @@ $viewer = $this->getViewer(); $panel = $this->getPanel(); - $dashboard_id = $this->getDashboardID(); + $context_phid = $this->getContextPHID(); $actions = array(); @@ -330,15 +331,15 @@ $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 @@ ->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 @@ } } + 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 --- a/src/applications/dashboard/engine/PhabricatorDashboardRenderingEngine.php +++ b/src/applications/dashboard/engine/PhabricatorDashboardRenderingEngine.php @@ -73,9 +73,9 @@ $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 @@ 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 @@ 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 --- 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 --- a/src/applications/dashboard/layoutconfig/PhabricatorDashboardPanelRef.php +++ b/src/applications/dashboard/layoutconfig/PhabricatorDashboardPanelRef.php @@ -34,4 +34,12 @@ 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 --- a/src/applications/dashboard/layoutconfig/PhabricatorDashboardPanelRefList.php +++ b/src/applications/dashboard/layoutconfig/PhabricatorDashboardPanelRefList.php @@ -73,4 +73,47 @@ 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 --- a/src/applications/dashboard/storage/PhabricatorDashboard.php +++ b/src/applications/dashboard/storage/PhabricatorDashboard.php @@ -24,9 +24,6 @@ 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 @@ ->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 @@ ) + parent::getConfiguration(); } - public function generatePHID() { - return PhabricatorPHID::generateNewPHID( - PhabricatorDashboardDashboardPHIDType::TYPECONST); + public function getPHIDType() { + return PhabricatorDashboardDashboardPHIDType::TYPECONST; } public function getRawLayoutMode() { @@ -75,11 +70,18 @@ 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 @@ 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 --- /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 --- 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 @@ var data = { parentPanelPHIDs: config.parentPanelPHIDs.join(','), headerMode: config.headerMode, - dashboardID: config.dashboardID + contextPHID: config.contextPHID, + panelKey: config.panelKey }; new JX.Workflow(config.uri)