From 630095566174dbf9a06bf4401905f30c6adf78f0 Mon Sep 17 00:00:00 2001 From: Bob Trahan Date: Thu, 15 May 2014 19:12:40 -0700 Subject: [PATCH] Dashboards - add layout mode to dashboards Summary: This gets us the ability to specify a "layout mode" and which column a panel should appear in at panel add time. Changing the layout mode from a multi column view to a single column view or vice versa will reset all panels to the left most column. You can also drag and drop where columns appear via the "arrange" mode. We also have a new dashboard create flow. Create dashboard -> arrange mode. (As opposed to view mode.) This could all possibly use massaging. Fixes T4996. Test Plan: made a dashboard with panels in multiple columns. verified correct widths for various layout modes re-arranged collumns like whoa. Reviewers: chad, epriestley Reviewed By: epriestley Subscribers: epriestley, Korvin Maniphest Tasks: T4996 Differential Revision: https://secure.phabricator.com/D9031 --- resources/celerity/map.php | 37 +++-- .../20140509.dashboardlayoutconfig.sql | 4 + src/__phutil_library_map__.php | 5 + .../PhabricatorApplicationDashboard.php | 3 +- ...PhabricatorDashboardAddPanelController.php | 33 ++++- .../PhabricatorDashboardArrangeController.php | 54 +++++++ .../PhabricatorDashboardEditController.php | 24 +++- ...habricatorDashboardMovePanelController.php | 86 +++++++++++ .../PhabricatorDashboardViewController.php | 8 ++ .../PhabricatorDashboardTransactionEditor.php | 24 ++++ ...abricatorDashboardPanelRenderingEngine.php | 3 + .../PhabricatorDashboardRenderingEngine.php | 96 ++++++++++++- .../PhabricatorDashboardLayoutConfig.php | 133 ++++++++++++++++++ .../PhabricatorDashboardPanelType.php | 3 + .../storage/PhabricatorDashboard.php | 18 ++- .../PhabricatorDashboardTransaction.php | 12 ++ src/view/layout/AphrontMultiColumnView.php | 59 +++++--- .../css/application/dashboard/dashboard.css | 59 ++++++++ .../behavior-dashboard-move-panels.js | 103 ++++++++++++++ 19 files changed, 719 insertions(+), 45 deletions(-) create mode 100644 resources/sql/autopatches/20140509.dashboardlayoutconfig.sql create mode 100644 src/applications/dashboard/controller/PhabricatorDashboardArrangeController.php create mode 100644 src/applications/dashboard/controller/PhabricatorDashboardMovePanelController.php create mode 100644 src/applications/dashboard/layoutconfig/PhabricatorDashboardLayoutConfig.php create mode 100644 webroot/rsrc/css/application/dashboard/dashboard.css create mode 100644 webroot/rsrc/js/application/dashboard/behavior-dashboard-move-panels.js diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 772027c10c..8fc98d8861 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -52,6 +52,7 @@ return array( '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' => '5b532b7b', 'rsrc/css/application/diff/inline-comment-summary.css' => '8cfd34e8', 'rsrc/css/application/differential/add-comment.css' => 'c478bcaa', 'rsrc/css/application/differential/changeset-view.css' => '1570a1ff', @@ -358,6 +359,7 @@ return array( 'rsrc/js/application/conpherence/behavior-widget-pane.js' => '40b1ff90', 'rsrc/js/application/countdown/timer.js' => '889c96f3', 'rsrc/js/application/dashboard/behavior-dashboard-async-panel.js' => '4398eabb', + 'rsrc/js/application/dashboard/behavior-dashboard-move-panels.js' => 'aa3f313b', 'rsrc/js/application/differential/DifferentialInlineCommentEditor.js' => 'f2441746', 'rsrc/js/application/differential/behavior-add-reviewers-and-ccs.js' => '533a187b', 'rsrc/js/application/differential/behavior-comment-jump.js' => '71755c79', @@ -552,6 +554,7 @@ return array( 'javelin-behavior-countdown-timer' => '889c96f3', 'javelin-behavior-dark-console' => 'e9fdb5e5', 'javelin-behavior-dashboard-async-panel' => '4398eabb', + 'javelin-behavior-dashboard-move-panels' => 'aa3f313b', 'javelin-behavior-device' => '03d6ed07', 'javelin-behavior-differential-add-reviewers-and-ccs' => '533a187b', 'javelin-behavior-differential-comment-jump' => '71755c79', @@ -698,6 +701,7 @@ return array( 'phabricator-core-css' => '40151074', 'phabricator-countdown-css' => '86b7b0a0', 'phabricator-crumbs-view-css' => '6a23399c', + 'phabricator-dashboard-css' => '5b532b7b', 'phabricator-drag-and-drop-file-upload' => 'ae6abfba', 'phabricator-draggable-list' => '1681c4d4', 'phabricator-fatal-config-template-css' => '25d446d6', @@ -1266,6 +1270,18 @@ return array( 2 => 'javelin-util', 3 => 'phabricator-shaped-request', ), + '7319e029' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + ), + '62e18640' => + array( + 0 => 'javelin-install', + 1 => 'javelin-util', + 2 => 'javelin-dom', + 3 => 'javelin-typeahead-normalizer', + ), '6453c869' => array( 0 => 'javelin-install', @@ -1313,18 +1329,6 @@ return array( 1 => 'javelin-stratcom', 2 => 'javelin-dom', ), - '7319e029' => - array( - 0 => 'javelin-behavior', - 1 => 'javelin-dom', - ), - '62e18640' => - array( - 0 => 'javelin-install', - 1 => 'javelin-util', - 2 => 'javelin-dom', - 3 => 'javelin-typeahead-normalizer', - ), '76f4ebed' => array( 0 => 'javelin-install', @@ -1594,6 +1598,15 @@ return array( 1 => 'javelin-stratcom', 2 => 'javelin-dom', ), + 'aa3f313b' => + array( + 0 => 'javelin-behavior', + 1 => 'javelin-dom', + 2 => 'javelin-util', + 3 => 'javelin-stratcom', + 4 => 'javelin-workflow', + 5 => 'phabricator-draggable-list', + ), 'ad7a69ca' => array( 0 => 'javelin-install', diff --git a/resources/sql/autopatches/20140509.dashboardlayoutconfig.sql b/resources/sql/autopatches/20140509.dashboardlayoutconfig.sql new file mode 100644 index 0000000000..2ffb549f0a --- /dev/null +++ b/resources/sql/autopatches/20140509.dashboardlayoutconfig.sql @@ -0,0 +1,4 @@ +ALTER TABLE {$NAMESPACE}_dashboard.dashboard + ADD COLUMN layoutConfig LONGTEXT NOT NULL COLLATE utf8_bin AFTER name; + +UPDATE {$NAMESPACE}_dashboard.dashboard SET layoutConfig = '[]'; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 3f2d4e28d9..eace6b8b1f 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1456,10 +1456,13 @@ phutil_register_library_map(array( 'PhabricatorDaemonTaskGarbageCollector' => 'applications/daemon/garbagecollector/PhabricatorDaemonTaskGarbageCollector.php', 'PhabricatorDashboard' => 'applications/dashboard/storage/PhabricatorDashboard.php', 'PhabricatorDashboardAddPanelController' => 'applications/dashboard/controller/PhabricatorDashboardAddPanelController.php', + 'PhabricatorDashboardArrangeController' => 'applications/dashboard/controller/PhabricatorDashboardArrangeController.php', 'PhabricatorDashboardController' => 'applications/dashboard/controller/PhabricatorDashboardController.php', 'PhabricatorDashboardDAO' => 'applications/dashboard/storage/PhabricatorDashboardDAO.php', 'PhabricatorDashboardEditController' => 'applications/dashboard/controller/PhabricatorDashboardEditController.php', + 'PhabricatorDashboardLayoutConfig' => 'applications/dashboard/layoutconfig/PhabricatorDashboardLayoutConfig.php', 'PhabricatorDashboardListController' => 'applications/dashboard/controller/PhabricatorDashboardListController.php', + 'PhabricatorDashboardMovePanelController' => 'applications/dashboard/controller/PhabricatorDashboardMovePanelController.php', 'PhabricatorDashboardPHIDTypeDashboard' => 'applications/dashboard/phid/PhabricatorDashboardPHIDTypeDashboard.php', 'PhabricatorDashboardPHIDTypePanel' => 'applications/dashboard/phid/PhabricatorDashboardPHIDTypePanel.php', 'PhabricatorDashboardPanel' => 'applications/dashboard/storage/PhabricatorDashboardPanel.php', @@ -4235,10 +4238,12 @@ phutil_register_library_map(array( 1 => 'PhabricatorPolicyInterface', ), 'PhabricatorDashboardAddPanelController' => 'PhabricatorDashboardController', + 'PhabricatorDashboardArrangeController' => 'PhabricatorDashboardController', 'PhabricatorDashboardController' => 'PhabricatorController', 'PhabricatorDashboardDAO' => 'PhabricatorLiskDAO', 'PhabricatorDashboardEditController' => 'PhabricatorDashboardController', 'PhabricatorDashboardListController' => 'PhabricatorDashboardController', + 'PhabricatorDashboardMovePanelController' => 'PhabricatorDashboardController', 'PhabricatorDashboardPHIDTypeDashboard' => 'PhabricatorPHIDType', 'PhabricatorDashboardPHIDTypePanel' => 'PhabricatorPHIDType', 'PhabricatorDashboardPanel' => diff --git a/src/applications/dashboard/application/PhabricatorApplicationDashboard.php b/src/applications/dashboard/application/PhabricatorApplicationDashboard.php index 71f6a289b7..6aca83a10e 100644 --- a/src/applications/dashboard/application/PhabricatorApplicationDashboard.php +++ b/src/applications/dashboard/application/PhabricatorApplicationDashboard.php @@ -21,10 +21,11 @@ final class PhabricatorApplicationDashboard extends PhabricatorApplication { '(?:query/(?P[^/]+)/)?' => 'PhabricatorDashboardListController', 'view/(?P\d+)/' => 'PhabricatorDashboardViewController', + 'arrange/(?P\d+)/' => 'PhabricatorDashboardArrangeController', 'create/' => 'PhabricatorDashboardEditController', 'edit/(?:(?P\d+)/)?' => 'PhabricatorDashboardEditController', 'addpanel/(?P\d+)/' => 'PhabricatorDashboardAddPanelController', - + 'movepanel/(?P\d+)/' => 'PhabricatorDashboardMovePanelController', 'panel/' => array( '(?:query/(?P[^/]+)/)?' => 'PhabricatorDashboardPanelListController', diff --git a/src/applications/dashboard/controller/PhabricatorDashboardAddPanelController.php b/src/applications/dashboard/controller/PhabricatorDashboardAddPanelController.php index 9fc8d9720c..76bb0804c7 100644 --- a/src/applications/dashboard/controller/PhabricatorDashboardAddPanelController.php +++ b/src/applications/dashboard/controller/PhabricatorDashboardAddPanelController.php @@ -26,7 +26,14 @@ final class PhabricatorDashboardAddPanelController return new Aphront404Response(); } - $dashboard_uri = $this->getApplicationURI('view/'.$dashboard->getID().'/'); + if ($request->getStr('src', 'edit') == 'edit') { + $redirect_uri = $this->getApplicationURI( + 'view/'.$dashboard->getID().'/'); + } else { + $redirect_uri = $this->getApplicationURI( + 'arrange/'.$dashboard->getID().'/'); + } + $layout_config = $dashboard->getLayoutConfigObject(); $v_panel = $request->getStr('panel'); $e_panel = true; @@ -61,6 +68,13 @@ final class PhabricatorDashboardAddPanelController ), )); + if ($layout_config->isMultiColumnLayout()) { + $layout_config->setPanelLocation( + $request->getInt('column'), + $panel->getPHID()); + $dashboard->setLayoutConfigFromObject($layout_config); + } + $editor = id(new PhabricatorDashboardTransactionEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) @@ -68,12 +82,13 @@ final class PhabricatorDashboardAddPanelController ->setContinueOnNoEffect(true) ->applyTransactions($dashboard, $xactions); - return id(new AphrontRedirectResponse())->setURI($dashboard_uri); + return id(new AphrontRedirectResponse())->setURI($redirect_uri); } } $form = id(new AphrontFormView()) ->setUser($viewer) + ->addHiddenInput('src', $request->getStr('src', 'edit')) ->appendRemarkupInstructions( pht('Enter a panel monogram like `W123`.')) ->appendChild( @@ -83,11 +98,23 @@ final class PhabricatorDashboardAddPanelController ->setValue($v_panel) ->setError($e_panel)); + if ($layout_config->isMultiColumnLayout()) { + $form + ->appendRemarkupInstructions( + pht('Choose which column the panel should reside in.')) + ->appendChild( + id(new AphrontFormSelectControl()) + ->setName('column') + ->setLabel(pht('Column')) + ->setOptions($layout_config->getColumnSelectOptions()) + ->setValue($request->getInt('column'))); + } + return $this->newDialog() ->setTitle(pht('Add Panel')) ->setErrors($errors) ->appendChild($form->buildLayoutView()) - ->addCancelButton($dashboard_uri) + ->addCancelButton($redirect_uri) ->addSubmitButton(pht('Add Panel')); } diff --git a/src/applications/dashboard/controller/PhabricatorDashboardArrangeController.php b/src/applications/dashboard/controller/PhabricatorDashboardArrangeController.php new file mode 100644 index 0000000000..fbbe9db239 --- /dev/null +++ b/src/applications/dashboard/controller/PhabricatorDashboardArrangeController.php @@ -0,0 +1,54 @@ +id = $data['id']; + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $dashboard = id(new PhabricatorDashboardQuery()) + ->setViewer($viewer) + ->withIDs(array($this->id)) + ->needPanels(true) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$dashboard) { + return new Aphront404Response(); + } + + $title = $dashboard->getName(); + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb( + pht('Dashboard %d', $dashboard->getID()), + $this->getApplicationURI('view/'.$dashboard->getID().'/')); + $crumbs->addTextCrumb(pht('Arrange')); + + $rendered_dashboard = id(new PhabricatorDashboardRenderingEngine()) + ->setViewer($viewer) + ->setDashboard($dashboard) + ->setArrangeMode(true) + ->renderDashboard(); + + return $this->buildApplicationPage( + array( + $crumbs, + $rendered_dashboard, + ), + array( + 'title' => $title, + 'device' => true, + )); + } + +} diff --git a/src/applications/dashboard/controller/PhabricatorDashboardEditController.php b/src/applications/dashboard/controller/PhabricatorDashboardEditController.php index cb466256f3..e13451c8c9 100644 --- a/src/applications/dashboard/controller/PhabricatorDashboardEditController.php +++ b/src/applications/dashboard/controller/PhabricatorDashboardEditController.php @@ -17,6 +17,7 @@ final class PhabricatorDashboardEditController $dashboard = id(new PhabricatorDashboardQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) + ->needPanels(true) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, @@ -56,19 +57,25 @@ final class PhabricatorDashboardEditController } $v_name = $dashboard->getName(); + $v_layout_mode = $dashboard->getLayoutConfigObject()->getLayoutMode(); $e_name = true; $validation_exception = null; if ($request->isFormPost()) { $v_name = $request->getStr('name'); + $v_layout_mode = $request->getStr('layout_mode'); $xactions = array(); $type_name = PhabricatorDashboardTransaction::TYPE_NAME; + $type_layout_mode = PhabricatorDashboardTransaction::TYPE_LAYOUT_MODE; $xactions[] = id(new PhabricatorDashboardTransaction()) ->setTransactionType($type_name) ->setNewValue($v_name); + $xactions[] = id(new PhabricatorDashboardTransaction()) + ->setTransactionType($type_layout_mode) + ->setNewValue($v_layout_mode); try { $editor = id(new PhabricatorDashboardTransactionEditor()) @@ -77,8 +84,12 @@ final class PhabricatorDashboardEditController ->setContentSourceFromRequest($request) ->applyTransactions($dashboard, $xactions); - return id(new AphrontRedirectResponse()) - ->setURI($this->getApplicationURI('view/'.$dashboard->getID().'/')); + if ($is_new) { + $uri = $this->getApplicationURI('arrange/'.$dashboard->getID().'/'); + } else { + $uri = $this->getApplicationURI('view/'.$dashboard->getID().'/'); + } + return id(new AphrontRedirectResponse())->setURI($uri); } catch (PhabricatorApplicationTransactionValidationException $ex) { $validation_exception = $ex; @@ -86,6 +97,8 @@ final class PhabricatorDashboardEditController } } + $layout_mode_options = + PhabricatorDashboardLayoutConfig::getLayoutModeSelectOptions(); $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( @@ -94,12 +107,17 @@ final class PhabricatorDashboardEditController ->setName('name') ->setValue($v_name) ->setError($e_name)) + ->appendChild( + id(new AphrontFormSelectControl()) + ->setLabel(pht('Layout Mode')) + ->setName('layout_mode') + ->setValue($v_layout_mode) + ->setOptions($layout_mode_options)) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue($button) ->addCancelButton($cancel_uri)); - $box = id(new PHUIObjectBoxView()) ->setHeaderText($header) ->setForm($form) diff --git a/src/applications/dashboard/controller/PhabricatorDashboardMovePanelController.php b/src/applications/dashboard/controller/PhabricatorDashboardMovePanelController.php new file mode 100644 index 0000000000..ed3a672df4 --- /dev/null +++ b/src/applications/dashboard/controller/PhabricatorDashboardMovePanelController.php @@ -0,0 +1,86 @@ +id = $data['id']; + } + + public function processRequest() { + $request = $this->getRequest(); + $viewer = $request->getUser(); + + $column_id = $request->getStr('columnID'); + $panel_phid = $request->getStr('objectPHID'); + $after_phid = $request->getStr('afterPHID'); + $before_phid = $request->getStr('beforePHID'); + + $dashboard = id(new PhabricatorDashboardQuery()) + ->setViewer($viewer) + ->withIDs(array($this->id)) + ->needPanels(true) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$dashboard) { + return new Aphront404Response(); + } + $panels = mpull($dashboard->getPanels(), null, 'getPHID'); + $panel = idx($panels, $panel_phid); + if (!$panel) { + return new Aphront404Response(); + } + + $layout_config = $dashboard->getLayoutConfigObject(); + $panel_location_grid = $layout_config->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; + } + } + $panel_columns = idx($panel_location_grid, $column_id, array()); + if ($panel_columns) { + $insert_at = 0; + $new_panel_columns = $panel_columns; + foreach ($panel_columns as $index => $curr_panel_phid) { + if ($curr_panel_phid === $before_phid) { + $insert_at = max($index - 1, 0); + break; + } + if ($curr_panel_phid === $after_phid) { + $insert_at = $index; + break; + } + } + array_splice( + $new_panel_columns, + $insert_at, + 0, + array($panel_phid)); + } else { + $new_panel_columns = array(0 => $panel_phid); + } + $panel_location_grid[$column_id] = $new_panel_columns; + $layout_config->setPanelLocations($panel_location_grid); + $dashboard->setLayoutConfigFromObject($layout_config); + $dashboard->save(); + + return id(new AphrontAjaxResponse())->setContent(''); + } + +} diff --git a/src/applications/dashboard/controller/PhabricatorDashboardViewController.php b/src/applications/dashboard/controller/PhabricatorDashboardViewController.php index dbda31473e..dbf4848c4b 100644 --- a/src/applications/dashboard/controller/PhabricatorDashboardViewController.php +++ b/src/applications/dashboard/controller/PhabricatorDashboardViewController.php @@ -84,6 +84,14 @@ final class PhabricatorDashboardViewController ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); + $actions->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Arrange Dashboard')) + ->setIcon('fa-arrows') + ->setHref($this->getApplicationURI("arrange/{$id}/")) + ->setDisabled(!$can_edit) + ->setWorkflow(!$can_edit)); + $actions->addAction( id(new PhabricatorActionView()) ->setName(pht('Add Panel')) diff --git a/src/applications/dashboard/editor/PhabricatorDashboardTransactionEditor.php b/src/applications/dashboard/editor/PhabricatorDashboardTransactionEditor.php index 968c79468a..1d099262d1 100644 --- a/src/applications/dashboard/editor/PhabricatorDashboardTransactionEditor.php +++ b/src/applications/dashboard/editor/PhabricatorDashboardTransactionEditor.php @@ -11,6 +11,7 @@ final class PhabricatorDashboardTransactionEditor $types[] = PhabricatorTransactions::TYPE_EDGE; $types[] = PhabricatorDashboardTransaction::TYPE_NAME; + $types[] = PhabricatorDashboardTransaction::TYPE_LAYOUT_MODE; return $types; } @@ -24,6 +25,12 @@ final class PhabricatorDashboardTransactionEditor return null; } return $object->getName(); + case PhabricatorDashboardTransaction::TYPE_LAYOUT_MODE: + if ($this->getIsNewObject()) { + return null; + } + $layout_config = $object->getLayoutConfigObject(); + return $layout_config->getLayoutMode(); } return parent::getCustomTransactionOldValue($object, $xaction); @@ -34,6 +41,7 @@ final class PhabricatorDashboardTransactionEditor PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case PhabricatorDashboardTransaction::TYPE_NAME: + case PhabricatorDashboardTransaction::TYPE_LAYOUT_MODE: return $xaction->getNewValue(); } return parent::getCustomTransactionNewValue($object, $xaction); @@ -46,6 +54,21 @@ final class PhabricatorDashboardTransactionEditor case PhabricatorDashboardTransaction::TYPE_NAME: $object->setName($xaction->getNewValue()); return; + case PhabricatorDashboardTransaction::TYPE_LAYOUT_MODE: + $old_layout = $object->getLayoutConfigObject(); + $new_layout = clone $old_layout; + $new_layout->setLayoutMode($xaction->getNewValue()); + if ($old_layout->isMultiColumnLayout() != + $new_layout->isMultiColumnLayout()) { + $panel_phids = $object->getPanelPHIDs(); + $new_locations = $new_layout->getDefaultPanelLocations(); + foreach ($panel_phids as $panel_phid) { + $new_locations[0][] = $panel_phid; + } + $new_layout->setPanelLocations($new_locations); + } + $object->setLayoutConfigFromObject($new_layout); + return; case PhabricatorTransactions::TYPE_VIEW_POLICY: $object->setViewPolicy($xaction->getNewValue()); return; @@ -65,6 +88,7 @@ final class PhabricatorDashboardTransactionEditor switch ($xaction->getTransactionType()) { case PhabricatorDashboardTransaction::TYPE_NAME: + case PhabricatorDashboardTransaction::TYPE_LAYOUT_MODE: return; case PhabricatorTransactions::TYPE_EDGE: return; diff --git a/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php b/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php index f7065a7a30..253a244b77 100644 --- a/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php +++ b/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php @@ -79,6 +79,9 @@ final class PhabricatorDashboardPanelRenderingEngine extends Phobject { )); return id(new PHUIObjectBoxView()) + ->addSigil('dashboard-panel') + ->setMetadata(array( + 'objectPHID' => $panel->getPHID())) ->setHeaderText($panel->getName()) ->setID($panel_id) ->appendChild(pht('Loading...')); diff --git a/src/applications/dashboard/engine/PhabricatorDashboardRenderingEngine.php b/src/applications/dashboard/engine/PhabricatorDashboardRenderingEngine.php index fb2629fff5..dd138a647d 100644 --- a/src/applications/dashboard/engine/PhabricatorDashboardRenderingEngine.php +++ b/src/applications/dashboard/engine/PhabricatorDashboardRenderingEngine.php @@ -4,6 +4,7 @@ final class PhabricatorDashboardRenderingEngine extends Phobject { private $dashboard; private $viewer; + private $arrangeMode; public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; @@ -15,20 +16,101 @@ final class PhabricatorDashboardRenderingEngine extends Phobject { return $this; } + public function setArrangeMode($mode) { + $this->arrangeMode = $mode; + return $this; + } + public function renderDashboard() { + require_celerity_resource('phabricator-dashboard-css'); $dashboard = $this->dashboard; $viewer = $this->viewer; - $result = array(); - foreach ($dashboard->getPanels() as $panel) { - $result[] = id(new PhabricatorDashboardPanelRenderingEngine()) - ->setViewer($viewer) - ->setPanel($panel) - ->setEnableAsyncRendering(true) - ->renderPanel(); + $layout_config = $dashboard->getLayoutConfigObject(); + $panel_grid_locations = $layout_config->getPanelLocations(); + $panels = mpull($dashboard->getPanels(), null, 'getPHID'); + $dashboard_id = celerity_generate_unique_node_id(); + $result = id(new AphrontMultiColumnView()) + ->setID($dashboard_id) + ->setFluidlayout(true); + + foreach ($panel_grid_locations as $column => $panel_column_locations) { + $panel_phids = $panel_column_locations; + $column_panels = array_select_keys($panels, $panel_phids); + $column_result = array(); + foreach ($column_panels as $panel) { + $column_result[] = id(new PhabricatorDashboardPanelRenderingEngine()) + ->setViewer($viewer) + ->setPanel($panel) + ->setEnableAsyncRendering(true) + ->renderPanel(); + } + $column_class = $layout_config->getColumnClass( + $column, + $this->arrangeMode); + if ($this->arrangeMode) { + $column_result[] = $this->renderAddPanelPlaceHolder($column); + $column_result[] = $this->renderAddPanelUI($column); + } + $result->addColumn( + $column_result, + $column_class, + $sigil = 'dashboard-column', + $metadata = array('columnID' => $column)); + } + + if ($this->arrangeMode) { + Javelin::initBehavior( + 'dashboard-move-panels', + array( + 'dashboardID' => $dashboard_id, + 'moveURI' => '/dashboard/movepanel/'.$dashboard->getID().'/', + )); } return $result; } + private function renderAddPanelPlaceHolder($column) { + $uri = $this->getAddPanelURI($column); + + $dashboard = $this->dashboard; + $panels = $dashboard->getPanels(); + $layout_config = $dashboard->getLayoutConfigObject(); + if ($layout_config->isMultiColumnLayout() && count($panels)) { + $text = pht('Drag a panel here or click to add a panel.'); + } else { + $text = pht('Click to add a panel.'); + } + return javelin_tag( + 'a', + array( + 'sigil' => 'workflow', + 'class' => 'drag-ghost dashboard-panel-placeholder', + 'href' => (string) $uri), + $text); + } + + private function renderAddPanelUI($column) { + $uri = $this->getAddPanelURI($column); + + return id(new PHUIButtonView()) + ->setTag('a') + ->setHref((string) $uri) + ->setWorkflow(true) + ->setColor(PHUIButtonView::GREY) + ->setIcon(id(new PHUIIconView()) + ->setIconFont('fa-plus')) + ->setText(pht('Add Panel')) + ->addClass(PHUI::MARGIN_LARGE); + } + + private function getAddPanelURI($column) { + $dashboard = $this->dashboard; + $uri = id(new PhutilURI('/dashboard/addpanel/'.$dashboard->getID().'/')) + ->setQueryParam('column', $column) + ->setQueryParam('src', 'arrange'); + return $uri; + } + } diff --git a/src/applications/dashboard/layoutconfig/PhabricatorDashboardLayoutConfig.php b/src/applications/dashboard/layoutconfig/PhabricatorDashboardLayoutConfig.php new file mode 100644 index 0000000000..0c9f3c383d --- /dev/null +++ b/src/applications/dashboard/layoutconfig/PhabricatorDashboardLayoutConfig.php @@ -0,0 +1,133 @@ +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 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 function isMultiColumnLayout() { + return $this->getLayoutMode() != self::MODE_FULL; + } + + public function getColumnSelectOptions() { + $options = array(); + + switch ($this->getLayoutMode()) { + case self::MODE_HALF_AND_HALF: + case self::MODE_THIRD_AND_THIRDS: + case self::MODE_THIRDS_AND_THIRD: + return array( + 0 => pht('Left'), + 1 => pht('Right')); + break; + case self::MODE_FULL: + throw new Exception('There is only one column in mode full.'); + break; + default: + throw new Exception('Unknown layout mode!'); + break; + } + + return $options; + } + + public static function getLayoutModeSelectOptions() { + return array( + self::MODE_FULL => pht('One full-width column'), + self::MODE_HALF_AND_HALF => pht('Two columns, 1/2 and 1/2'), + self::MODE_THIRD_AND_THIRDS => pht('Two columns, 1/3 and 2/3'), + self::MODE_THIRDS_AND_THIRD => pht('Two columns, 2/3 and 1/3'), + ); + } + + 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/paneltype/PhabricatorDashboardPanelType.php b/src/applications/dashboard/paneltype/PhabricatorDashboardPanelType.php index 6c6dc8151f..fe71867e35 100644 --- a/src/applications/dashboard/paneltype/PhabricatorDashboardPanelType.php +++ b/src/applications/dashboard/paneltype/PhabricatorDashboardPanelType.php @@ -47,6 +47,9 @@ abstract class PhabricatorDashboardPanelType extends Phobject { $content = $this->renderPanelContent($viewer, $panel); return id(new PHUIObjectBoxView()) + ->addSigil('dashboard-panel') + ->setMetadata(array( + 'objectPHID' => $panel->getPHID())) ->setHeaderText($panel->getName()) ->appendChild($content); } diff --git a/src/applications/dashboard/storage/PhabricatorDashboard.php b/src/applications/dashboard/storage/PhabricatorDashboard.php index 8753e0d484..48413168ab 100644 --- a/src/applications/dashboard/storage/PhabricatorDashboard.php +++ b/src/applications/dashboard/storage/PhabricatorDashboard.php @@ -9,6 +9,7 @@ final class PhabricatorDashboard extends PhabricatorDashboardDAO protected $name; protected $viewPolicy; protected $editPolicy; + protected $layoutConfig = array(); private $panelPHIDs = self::ATTACHABLE; private $panels = self::ATTACHABLE; @@ -17,12 +18,16 @@ final class PhabricatorDashboard extends PhabricatorDashboardDAO return id(new PhabricatorDashboard()) ->setName('') ->setViewPolicy(PhabricatorPolicies::POLICY_USER) - ->setEditPolicy($actor->getPHID()); + ->setEditPolicy($actor->getPHID()) + ->attachPanels(array()) + ->attachPanelPHIDs(array()); } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, + self::CONFIG_SERIALIZATION => array( + 'layoutConfig' => self::SERIALIZATION_JSON), ) + parent::getConfiguration(); } @@ -31,6 +36,17 @@ final class PhabricatorDashboard extends PhabricatorDashboardDAO PhabricatorDashboardPHIDTypeDashboard::TYPECONST); } + public function getLayoutConfigObject() { + return PhabricatorDashboardLayoutConfig::newFromDictionary( + $this->getLayoutConfig()); + } + + public function setLayoutConfigFromObject( + PhabricatorDashboardLayoutConfig $object) { + $this->setLayoutConfig($object->toDictionary()); + return $this; + } + public function attachPanelPHIDs(array $phids) { $this->panelPHIDs = $phids; return $this; diff --git a/src/applications/dashboard/storage/PhabricatorDashboardTransaction.php b/src/applications/dashboard/storage/PhabricatorDashboardTransaction.php index 8e6e8f4580..5ab0f4b688 100644 --- a/src/applications/dashboard/storage/PhabricatorDashboardTransaction.php +++ b/src/applications/dashboard/storage/PhabricatorDashboardTransaction.php @@ -4,6 +4,7 @@ final class PhabricatorDashboardTransaction extends PhabricatorApplicationTransaction { const TYPE_NAME = 'dashboard:name'; + const TYPE_LAYOUT_MODE = 'dashboard:layoutmode'; public function getApplicationName() { return 'dashboard'; @@ -86,4 +87,15 @@ final class PhabricatorDashboardTransaction return parent::getColor(); } + + public function shouldHide() { + $old = $this->getOldValue(); + $new = $this->getNewValue(); + + switch ($this->getTransactionType()) { + case self::TYPE_LAYOUT_MODE: + return true; + } + return parent::shouldHide(); + } } diff --git a/src/view/layout/AphrontMultiColumnView.php b/src/view/layout/AphrontMultiColumnView.php index b1b6b90760..ec784f4669 100644 --- a/src/view/layout/AphrontMultiColumnView.php +++ b/src/view/layout/AphrontMultiColumnView.php @@ -6,14 +6,32 @@ final class AphrontMultiColumnView extends AphrontView { const GUTTER_MEDIUM = 'mmr'; const GUTTER_LARGE = 'mlr'; + private $id; private $columns = array(); private $fluidLayout = false; private $fluidishLayout = false; private $gutter; private $border; - public function addColumn($column) { - $this->columns[] = $column; + public function setID($id) { + $this->id = $id; + return $this; + } + + public function getID() { + return $this->id; + } + + public function addColumn( + $column, + $class = null, + $sigil = null, + $metadata = null) { + $this->columns[] = array( + 'column' => $column, + 'class' => $class, + 'sigil' => $sigil, + 'metadata' => $metadata); return $this; } @@ -55,30 +73,34 @@ final class AphrontMultiColumnView extends AphrontView { $classes[] = 'aphront-multi-column-'.count($this->columns).'-up'; $columns = array(); - $column_class = array(); - $column_class[] = 'aphront-multi-column-column'; - $outer_class = array(); - $outer_class[] = 'aphront-multi-column-column-outer'; - if ($this->gutter) { - $column_class[] = $this->gutter; - } $i = 0; - foreach ($this->columns as $column) { + foreach ($this->columns as $column_data) { + $column_class = array('aphront-multi-column-column'); + if ($this->gutter) { + $column_class[] = $this->gutter; + } + $outer_class = array('aphront-multi-column-column-outer'); if (++$i === count($this->columns)) { $column_class[] = 'aphront-multi-column-column-last'; $outer_class[] = 'aphront-multi-colum-column-outer-last'; } - $column_inner = phutil_tag( + $column = $column_data['column']; + if ($column_data['class']) { + $outer_class[] = $column_data['class']; + } + $column_sigil = idx($column_data, 'sigil'); + $column_metadata = idx($column_data, 'metadata'); + $column_inner = javelin_tag( 'div', - array( - 'class' => implode(' ', $column_class) - ), + array( + 'class' => implode(' ', $column_class), + 'sigil' => $column_sigil, + 'meta' => $column_metadata), $column); $columns[] = phutil_tag( 'div', - array( - 'class' => implode(' ', $outer_class) - ), + array( + 'class' => implode(' ', $outer_class)), $column_inner); } @@ -120,7 +142,8 @@ final class AphrontMultiColumnView extends AphrontView { return phutil_tag( 'div', array( - 'class' => 'aphront-multi-column-view' + 'class' => 'aphront-multi-column-view', + 'id' => $this->getID(), ), $board); } diff --git a/webroot/rsrc/css/application/dashboard/dashboard.css b/webroot/rsrc/css/application/dashboard/dashboard.css new file mode 100644 index 0000000000..67aafd416b --- /dev/null +++ b/webroot/rsrc/css/application/dashboard/dashboard.css @@ -0,0 +1,59 @@ +/** + * @provides phabricator-dashboard-css + */ + +.aphront-multi-column-fluid .aphront-multi-column-2-up +.aphront-multi-column-column-outer.half { + width: 50%; +} + +.aphront-multi-column-fluid .aphront-multi-column-2-up +.aphront-multi-column-column-outer.third { + width: 33.34%; +} + +.aphront-multi-column-fluid .aphront-multi-column-2-up +.aphront-multi-column-column-outer.thirds { + width: 66.66%; +} + +.aphront-multi-column-fluid +.aphront-multi-column-column-outer.grippable +.aphront-multi-column-column .phui-object-box { + cursor: move; +} + +.aphront-multi-column-fluid +.aphront-multi-column-column .drag-ghost { + list-style-type: none; + margin: 16px; +} + +.aphront-multi-column-fluid +.aphront-multi-column-column +.dashboard-panel-placeholder { + display: none; +} + +.aphront-multi-column-fluid +.aphront-multi-column-column.dashboard-column-empty +.dashboard-panel-placeholder { + color: {$greytext}; + display: block; + padding: 24px; + margin: 16px 16px 0px 16px; +} + +.aphront-multi-column-fluid +.aphront-multi-column-column.dashboard-column-empty +.dashboard-panel-placeholder:hover { + text-decoration: none; + border: 1px {$greyborder} dashed; + color: {$darkgreytext}; +} + +.aphront-multi-column-fluid +.aphront-multi-column-column.drag-target-list +.dashboard-panel-placeholder { + display: none; +} diff --git a/webroot/rsrc/js/application/dashboard/behavior-dashboard-move-panels.js b/webroot/rsrc/js/application/dashboard/behavior-dashboard-move-panels.js new file mode 100644 index 0000000000..5442cc5095 --- /dev/null +++ b/webroot/rsrc/js/application/dashboard/behavior-dashboard-move-panels.js @@ -0,0 +1,103 @@ +/** + * @provides javelin-behavior-dashboard-move-panels + * @requires javelin-behavior + * javelin-dom + * javelin-util + * javelin-stratcom + * javelin-workflow + * phabricator-draggable-list + */ + +JX.behavior('dashboard-move-panels', function(config) { + + var itemSigil = 'dashboard-panel'; + + function finditems(col) { + return JX.DOM.scry(col, 'div', itemSigil); + } + + function markcolempty(col, toggle) { + JX.DOM.alterClass(col, 'dashboard-column-empty', toggle); + } + + function onupdate(col) { + markcolempty(col, !this.findItems().length); + } + + function onresponse(response, item, list) { + list.unlock(); + JX.DOM.alterClass(item, 'drag-sending', false); + } + + function ondrop(list, item, after, from) { + list.lock(); + JX.DOM.alterClass(item, 'drag-sending', true); + + var item_phid = JX.Stratcom.getData(item).objectPHID; + var data = { + objectPHID: item_phid, + columnID: JX.Stratcom.getData(list.getRootNode()).columnID + }; + + var after_phid = null; + var items = finditems(list.getRootNode()); + if (after) { + after_phid = JX.Stratcom.getData(after).objectPHID; + data.afterPHID = after_phid; + } + var ii; + var ii_item; + var ii_item_phid; + var ii_prev_item_phid = null; + var before_phid = null; + for (ii = 0; ii < items.length; ii++) { + ii_item = items[ii]; + ii_item_phid = JX.Stratcom.getData(ii_item).objectPHID; + if (ii_item_phid == item_phid) { + // skip the item we just dropped + continue; + } + // note this handles when there is no after phid - we are at the top of + // the list - quite nicely + if (ii_prev_item_phid == after_phid) { + before_phid = ii_item_phid; + break; + } + ii_prev_item_phid = ii_item_phid; + } + if (before_phid) { + data.beforePHID = before_phid; + } + + var workflow = new JX.Workflow(config.moveURI, data) + .setHandler(function(response) { + onresponse(response, item, list); + }); + + workflow.start(); + } + + var lists = []; + var ii; + var cols = JX.DOM.scry(JX.$(config.dashboardID), 'div', 'dashboard-column'); + var col = null; + + for (ii = 0; ii < cols.length; ii++) { + col = cols[ii]; + var list = new JX.DraggableList(itemSigil, col) + .setFindItemsHandler(JX.bind(null, finditems, col)); + + list.listen('didSend', JX.bind(list, onupdate, col)); + list.listen('didReceive', JX.bind(list, onupdate, col)); + + list.listen('didDrop', JX.bind(null, ondrop, list)); + + lists.push(list); + markcolempty(col, finditems(col).length === 0); + } + + for (ii = 0; ii < lists.length; ii++) { + lists[ii].setGroup(lists); + } + +}); -- 2.51.2