diff --git a/resources/celerity/map.php b/resources/celerity/map.php --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -52,6 +52,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' => '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 @@ '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 @@ '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 @@ '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 @@ 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 @@ 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 @@ 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 --- /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 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1456,10 +1456,13 @@ '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 @@ 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 --- a/src/applications/dashboard/application/PhabricatorApplicationDashboard.php +++ b/src/applications/dashboard/application/PhabricatorApplicationDashboard.php @@ -21,10 +21,11 @@ '(?: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 --- a/src/applications/dashboard/controller/PhabricatorDashboardAddPanelController.php +++ b/src/applications/dashboard/controller/PhabricatorDashboardAddPanelController.php @@ -26,7 +26,14 @@ 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 @@ ), )); + 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 @@ ->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 @@ ->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 --- /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 --- a/src/applications/dashboard/controller/PhabricatorDashboardEditController.php +++ b/src/applications/dashboard/controller/PhabricatorDashboardEditController.php @@ -17,6 +17,7 @@ $dashboard = id(new PhabricatorDashboardQuery()) ->setViewer($viewer) ->withIDs(array($this->id)) + ->needPanels(true) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, @@ -56,19 +57,25 @@ } $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 @@ ->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 @@ } } + $layout_mode_options = + PhabricatorDashboardLayoutConfig::getLayoutModeSelectOptions(); $form = id(new AphrontFormView()) ->setUser($viewer) ->appendChild( @@ -95,11 +108,16 @@ ->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 --- /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 --- a/src/applications/dashboard/controller/PhabricatorDashboardViewController.php +++ b/src/applications/dashboard/controller/PhabricatorDashboardViewController.php @@ -86,6 +86,14 @@ $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')) ->setIcon('fa-plus') ->setHref($this->getApplicationURI("addpanel/{$id}/")) diff --git a/src/applications/dashboard/editor/PhabricatorDashboardTransactionEditor.php b/src/applications/dashboard/editor/PhabricatorDashboardTransactionEditor.php --- a/src/applications/dashboard/editor/PhabricatorDashboardTransactionEditor.php +++ b/src/applications/dashboard/editor/PhabricatorDashboardTransactionEditor.php @@ -11,6 +11,7 @@ $types[] = PhabricatorTransactions::TYPE_EDGE; $types[] = PhabricatorDashboardTransaction::TYPE_NAME; + $types[] = PhabricatorDashboardTransaction::TYPE_LAYOUT_MODE; return $types; } @@ -24,6 +25,12 @@ 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 @@ 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 @@ 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 @@ 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 --- a/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php +++ b/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php @@ -79,6 +79,9 @@ )); 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 --- a/src/applications/dashboard/engine/PhabricatorDashboardRenderingEngine.php +++ b/src/applications/dashboard/engine/PhabricatorDashboardRenderingEngine.php @@ -4,6 +4,7 @@ private $dashboard; private $viewer; + private $arrangeMode; public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; @@ -15,20 +16,101 @@ 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 --- /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 --- a/src/applications/dashboard/paneltype/PhabricatorDashboardPanelType.php +++ b/src/applications/dashboard/paneltype/PhabricatorDashboardPanelType.php @@ -47,6 +47,9 @@ $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 --- a/src/applications/dashboard/storage/PhabricatorDashboard.php +++ b/src/applications/dashboard/storage/PhabricatorDashboard.php @@ -9,6 +9,7 @@ protected $name; protected $viewPolicy; protected $editPolicy; + protected $layoutConfig = array(); private $panelPHIDs = self::ATTACHABLE; private $panels = self::ATTACHABLE; @@ -17,12 +18,16 @@ 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 @@ 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 --- a/src/applications/dashboard/storage/PhabricatorDashboardTransaction.php +++ b/src/applications/dashboard/storage/PhabricatorDashboardTransaction.php @@ -4,6 +4,7 @@ extends PhabricatorApplicationTransaction { const TYPE_NAME = 'dashboard:name'; + const TYPE_LAYOUT_MODE = 'dashboard:layoutmode'; public function getApplicationName() { return 'dashboard'; @@ -86,4 +87,15 @@ 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 --- a/src/view/layout/AphrontMultiColumnView.php +++ b/src/view/layout/AphrontMultiColumnView.php @@ -6,14 +6,32 @@ 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 @@ $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 @@ 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 --- /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 --- /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); + } + +});