diff --git a/resources/celerity/map.php b/resources/celerity/map.php --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -8,7 +8,7 @@ 'names' => array( 'core.pkg.css' => 'c76b553b', - 'core.pkg.js' => '0627d27e', + 'core.pkg.js' => '8335fe3f', 'darkconsole.pkg.js' => 'ca8671ce', 'differential.pkg.css' => '4a93db37', 'differential.pkg.js' => 'eca39a2c', @@ -51,7 +51,7 @@ 'rsrc/css/application/conpherence/widget-pane.css' => 'bf275a6c', 'rsrc/css/application/contentsource/content-source-view.css' => '4b8b05d4', 'rsrc/css/application/countdown/timer.css' => '86b7b0a0', - 'rsrc/css/application/dashboard/dashboard.css' => 'f0092e3e', + 'rsrc/css/application/dashboard/dashboard.css' => '0594a469', 'rsrc/css/application/diff/inline-comment-summary.css' => '8cfd34e8', 'rsrc/css/application/differential/add-comment.css' => 'c478bcaa', 'rsrc/css/application/differential/changeset-view.css' => 'ff8eacf8', @@ -423,7 +423,7 @@ 'rsrc/js/application/uiexample/notification-example.js' => 'c51a6616', 'rsrc/js/core/Busy.js' => '6453c869', 'rsrc/js/core/DragAndDropFileUpload.js' => 'ae6abfba', - 'rsrc/js/core/DraggableList.js' => '1681c4d4', + 'rsrc/js/core/DraggableList.js' => '109e2a87', 'rsrc/js/core/FileUpload.js' => 'a4ae61bf', 'rsrc/js/core/Hovercard.js' => '4f344388', 'rsrc/js/core/KeyboardShortcut.js' => '1ae869f2', @@ -694,9 +694,9 @@ 'phabricator-core-css' => '40151074', 'phabricator-countdown-css' => '86b7b0a0', 'phabricator-crumbs-view-css' => '7fbf25b8', - 'phabricator-dashboard-css' => 'f0092e3e', + 'phabricator-dashboard-css' => '0594a469', 'phabricator-drag-and-drop-file-upload' => 'ae6abfba', - 'phabricator-draggable-list' => '1681c4d4', + 'phabricator-draggable-list' => '109e2a87', 'phabricator-fatal-config-template-css' => '25d446d6', 'phabricator-feed-css' => 'dd43ce00', 'phabricator-file-upload' => 'a4ae61bf', @@ -913,6 +913,15 @@ 1 => 'javelin-uri', 2 => 'javelin-install', ), + '109e2a87' => + array( + 0 => 'javelin-install', + 1 => 'javelin-dom', + 2 => 'javelin-stratcom', + 3 => 'javelin-util', + 4 => 'javelin-vector', + 5 => 'javelin-magical-init', + ), '127f2018' => array( 0 => 'javelin-behavior', @@ -922,15 +931,6 @@ 4 => 'javelin-util', 5 => 'phabricator-shaped-request', ), - '1681c4d4' => - array( - 0 => 'javelin-install', - 1 => 'javelin-dom', - 2 => 'javelin-stratcom', - 3 => 'javelin-util', - 4 => 'javelin-vector', - 5 => 'javelin-magical-init', - ), '1693a296' => array( 0 => 'javelin-behavior', diff --git a/src/applications/dashboard/controller/PhabricatorDashboardManageController.php b/src/applications/dashboard/controller/PhabricatorDashboardManageController.php --- a/src/applications/dashboard/controller/PhabricatorDashboardManageController.php +++ b/src/applications/dashboard/controller/PhabricatorDashboardManageController.php @@ -15,6 +15,10 @@ $id = $this->id; $dashboard_uri = $this->getApplicationURI('view/'.$id.'/'); + // TODO: This UI should drop a lot of capabilities if the user can't + // edit the dashboard, but we should still let them in for "Install" and + // "View History". + $dashboard = id(new PhabricatorDashboardQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) diff --git a/src/applications/dashboard/controller/PhabricatorDashboardPanelEditController.php b/src/applications/dashboard/controller/PhabricatorDashboardPanelEditController.php --- a/src/applications/dashboard/controller/PhabricatorDashboardPanelEditController.php +++ b/src/applications/dashboard/controller/PhabricatorDashboardPanelEditController.php @@ -38,19 +38,45 @@ if ($this->id) { $is_create = false; + if ($dashboard) { + $capabilities = array( + PhabricatorPolicyCapability::CAN_VIEW, + ); + } else { + $capabilities = array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + ); + } + $panel = id(new PhabricatorDashboardPanelQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) + ->requireCapabilities($capabilities) ->executeOne(); if (!$panel) { return new Aphront404Response(); } + if ($dashboard) { + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $panel, + PhabricatorPolicyCapability::CAN_EDIT); + if (!$can_edit) { + if ($request->isFormPost() && $request->getBool('copy')) { + $panel = $this->copyPanel( + $request, + $dashboard, + $panel); + } else { + return $this->processPanelCloneRequest( + $request, + $dashboard, + $panel); + } + } + } } else { $is_create = true; @@ -161,6 +187,10 @@ } } + // NOTE: We're setting the submit URI explicitly because we need to edit + // a different panel if we just cloned the original panel. + $submit_uri = $this->getApplicationURI('panel/edit/'.$panel->getID().'/'); + $policies = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->setObject($panel) @@ -168,6 +198,7 @@ $form = id(new AphrontFormView()) ->setUser($viewer) + ->setAction($submit_uri) ->addHiddenInput('edit', true) ->addHiddenInput('dashboardID', $request->getInt('dashboardID')) ->addHiddenInput('column', $request->getInt('column')) @@ -209,6 +240,7 @@ if ($request->isAjax()) { return $this->newDialog() ->setTitle($header) + ->setSubmitURI($submit_uri) ->setWidth(AphrontDialogView::WIDTH_FORM) ->setValidationException($validation_exception) ->appendChild($form->buildLayoutView()) @@ -317,4 +349,79 @@ )); } + private function processPanelCloneRequest( + AphrontRequest $request, + PhabricatorDashboard $dashboard, + PhabricatorDashboardPanel $panel) { + + $viewer = $request->getUser(); + + $manage_uri = $this->getApplicationURI('manage/'.$dashboard->getID().'/'); + + return $this->newDialog() + ->setTitle(pht('Copy Panel?')) + ->addHiddenInput('copy', true) + ->addHiddenInput('dashboardID', $request->getInt('dashboardID')) + ->addHiddenInput('column', $request->getInt('column')) + ->appendParagraph( + pht( + 'You do not have permission to edit this dashboard panel, but you '. + 'can make a copy and edit that instead. If you choose to copy the '. + 'panel, the original will be replaced with the new copy on this '. + 'dashboard.')) + ->appendParagraph( + pht( + 'Do you want to make a copy of this panel?')) + ->addCancelButton($manage_uri) + ->addSubmitButton(pht('Copy Panel')); + } + + private function copyPanel( + AphrontRequest $request, + PhabricatorDashboard $dashboard, + PhabricatorDashboardPanel $panel) { + + $viewer = $request->getUser(); + + $copy = PhabricatorDashboardPanel::initializeNewPanel($viewer); + $copy = PhabricatorDashboardPanel::copyPanel($copy, $panel); + + $copy->openTransaction(); + $copy->save(); + + // TODO: This should record a transaction on the panel copy, too. + + $xactions = array(); + $xactions[] = id(new PhabricatorDashboardTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) + ->setMetadataValue( + 'edge:type', + PhabricatorEdgeConfig::TYPE_DASHBOARD_HAS_PANEL) + ->setNewValue( + array( + '+' => array( + $copy->getPHID() => $copy->getPHID(), + ), + '-' => array( + $panel->getPHID() => $panel->getPHID(), + ), + )); + + $layout_config = $dashboard->getLayoutConfigObject(); + $layout_config->replacePanel($panel->getPHID(), $copy->getPHID()); + $dashboard->setLayoutConfigFromObject($layout_config); + $dashboard->save(); + + $editor = id(new PhabricatorDashboardTransactionEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnMissingFields(true) + ->setContinueOnNoEffect(true) + ->applyTransactions($dashboard, $xactions); + $copy->saveTransaction(); + + return $copy; + } + + } diff --git a/src/applications/dashboard/layoutconfig/PhabricatorDashboardLayoutConfig.php b/src/applications/dashboard/layoutconfig/PhabricatorDashboardLayoutConfig.php --- a/src/applications/dashboard/layoutconfig/PhabricatorDashboardLayoutConfig.php +++ b/src/applications/dashboard/layoutconfig/PhabricatorDashboardLayoutConfig.php @@ -32,6 +32,18 @@ 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) { diff --git a/src/applications/dashboard/storage/PhabricatorDashboardPanel.php b/src/applications/dashboard/storage/PhabricatorDashboardPanel.php --- a/src/applications/dashboard/storage/PhabricatorDashboardPanel.php +++ b/src/applications/dashboard/storage/PhabricatorDashboardPanel.php @@ -24,6 +24,14 @@ ->setEditPolicy($actor->getPHID()); } + public static function copyPanel($dst, $src) { + $dst->name = $src->name; + $dst->panelType = $src->panelType; + $dst->properties = $src->properties; + + return $dst; + } + public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, diff --git a/webroot/rsrc/js/core/DraggableList.js b/webroot/rsrc/js/core/DraggableList.js --- a/webroot/rsrc/js/core/DraggableList.js +++ b/webroot/rsrc/js/core/DraggableList.js @@ -151,6 +151,17 @@ return; } + if (e.getNode('tag:a')) { + // Never start a drag if we're somewhere inside an tag. This makes + // links unclickable in Firefox. + return; + } + + if (JX.Stratcom.pass()) { + // Let other handlers deal with this event before we do. + return; + } + e.kill(); this._dragging = e.getNode(this._sigil);