@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

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

+719 -45
+25 -12
resources/celerity/map.php
··· 52 52 'rsrc/css/application/conpherence/widget-pane.css' => 'bf275a6c', 53 53 'rsrc/css/application/contentsource/content-source-view.css' => '4b8b05d4', 54 54 'rsrc/css/application/countdown/timer.css' => '86b7b0a0', 55 + 'rsrc/css/application/dashboard/dashboard.css' => '5b532b7b', 55 56 'rsrc/css/application/diff/inline-comment-summary.css' => '8cfd34e8', 56 57 'rsrc/css/application/differential/add-comment.css' => 'c478bcaa', 57 58 'rsrc/css/application/differential/changeset-view.css' => '1570a1ff', ··· 358 359 'rsrc/js/application/conpherence/behavior-widget-pane.js' => '40b1ff90', 359 360 'rsrc/js/application/countdown/timer.js' => '889c96f3', 360 361 'rsrc/js/application/dashboard/behavior-dashboard-async-panel.js' => '4398eabb', 362 + 'rsrc/js/application/dashboard/behavior-dashboard-move-panels.js' => 'aa3f313b', 361 363 'rsrc/js/application/differential/DifferentialInlineCommentEditor.js' => 'f2441746', 362 364 'rsrc/js/application/differential/behavior-add-reviewers-and-ccs.js' => '533a187b', 363 365 'rsrc/js/application/differential/behavior-comment-jump.js' => '71755c79', ··· 552 554 'javelin-behavior-countdown-timer' => '889c96f3', 553 555 'javelin-behavior-dark-console' => 'e9fdb5e5', 554 556 'javelin-behavior-dashboard-async-panel' => '4398eabb', 557 + 'javelin-behavior-dashboard-move-panels' => 'aa3f313b', 555 558 'javelin-behavior-device' => '03d6ed07', 556 559 'javelin-behavior-differential-add-reviewers-and-ccs' => '533a187b', 557 560 'javelin-behavior-differential-comment-jump' => '71755c79', ··· 698 701 'phabricator-core-css' => '40151074', 699 702 'phabricator-countdown-css' => '86b7b0a0', 700 703 'phabricator-crumbs-view-css' => '6a23399c', 704 + 'phabricator-dashboard-css' => '5b532b7b', 701 705 'phabricator-drag-and-drop-file-upload' => 'ae6abfba', 702 706 'phabricator-draggable-list' => '1681c4d4', 703 707 'phabricator-fatal-config-template-css' => '25d446d6', ··· 1266 1270 2 => 'javelin-util', 1267 1271 3 => 'phabricator-shaped-request', 1268 1272 ), 1273 + '7319e029' => 1274 + array( 1275 + 0 => 'javelin-behavior', 1276 + 1 => 'javelin-dom', 1277 + ), 1278 + '62e18640' => 1279 + array( 1280 + 0 => 'javelin-install', 1281 + 1 => 'javelin-util', 1282 + 2 => 'javelin-dom', 1283 + 3 => 'javelin-typeahead-normalizer', 1284 + ), 1269 1285 '6453c869' => 1270 1286 array( 1271 1287 0 => 'javelin-install', ··· 1313 1329 1 => 'javelin-stratcom', 1314 1330 2 => 'javelin-dom', 1315 1331 ), 1316 - '7319e029' => 1317 - array( 1318 - 0 => 'javelin-behavior', 1319 - 1 => 'javelin-dom', 1320 - ), 1321 - '62e18640' => 1322 - array( 1323 - 0 => 'javelin-install', 1324 - 1 => 'javelin-util', 1325 - 2 => 'javelin-dom', 1326 - 3 => 'javelin-typeahead-normalizer', 1327 - ), 1328 1332 '76f4ebed' => 1329 1333 array( 1330 1334 0 => 'javelin-install', ··· 1593 1597 0 => 'javelin-behavior', 1594 1598 1 => 'javelin-stratcom', 1595 1599 2 => 'javelin-dom', 1600 + ), 1601 + 'aa3f313b' => 1602 + array( 1603 + 0 => 'javelin-behavior', 1604 + 1 => 'javelin-dom', 1605 + 2 => 'javelin-util', 1606 + 3 => 'javelin-stratcom', 1607 + 4 => 'javelin-workflow', 1608 + 5 => 'phabricator-draggable-list', 1596 1609 ), 1597 1610 'ad7a69ca' => 1598 1611 array(
+4
resources/sql/autopatches/20140509.dashboardlayoutconfig.sql
··· 1 + ALTER TABLE {$NAMESPACE}_dashboard.dashboard 2 + ADD COLUMN layoutConfig LONGTEXT NOT NULL COLLATE utf8_bin AFTER name; 3 + 4 + UPDATE {$NAMESPACE}_dashboard.dashboard SET layoutConfig = '[]';
+5
src/__phutil_library_map__.php
··· 1456 1456 'PhabricatorDaemonTaskGarbageCollector' => 'applications/daemon/garbagecollector/PhabricatorDaemonTaskGarbageCollector.php', 1457 1457 'PhabricatorDashboard' => 'applications/dashboard/storage/PhabricatorDashboard.php', 1458 1458 'PhabricatorDashboardAddPanelController' => 'applications/dashboard/controller/PhabricatorDashboardAddPanelController.php', 1459 + 'PhabricatorDashboardArrangeController' => 'applications/dashboard/controller/PhabricatorDashboardArrangeController.php', 1459 1460 'PhabricatorDashboardController' => 'applications/dashboard/controller/PhabricatorDashboardController.php', 1460 1461 'PhabricatorDashboardDAO' => 'applications/dashboard/storage/PhabricatorDashboardDAO.php', 1461 1462 'PhabricatorDashboardEditController' => 'applications/dashboard/controller/PhabricatorDashboardEditController.php', 1463 + 'PhabricatorDashboardLayoutConfig' => 'applications/dashboard/layoutconfig/PhabricatorDashboardLayoutConfig.php', 1462 1464 'PhabricatorDashboardListController' => 'applications/dashboard/controller/PhabricatorDashboardListController.php', 1465 + 'PhabricatorDashboardMovePanelController' => 'applications/dashboard/controller/PhabricatorDashboardMovePanelController.php', 1463 1466 'PhabricatorDashboardPHIDTypeDashboard' => 'applications/dashboard/phid/PhabricatorDashboardPHIDTypeDashboard.php', 1464 1467 'PhabricatorDashboardPHIDTypePanel' => 'applications/dashboard/phid/PhabricatorDashboardPHIDTypePanel.php', 1465 1468 'PhabricatorDashboardPanel' => 'applications/dashboard/storage/PhabricatorDashboardPanel.php', ··· 4235 4238 1 => 'PhabricatorPolicyInterface', 4236 4239 ), 4237 4240 'PhabricatorDashboardAddPanelController' => 'PhabricatorDashboardController', 4241 + 'PhabricatorDashboardArrangeController' => 'PhabricatorDashboardController', 4238 4242 'PhabricatorDashboardController' => 'PhabricatorController', 4239 4243 'PhabricatorDashboardDAO' => 'PhabricatorLiskDAO', 4240 4244 'PhabricatorDashboardEditController' => 'PhabricatorDashboardController', 4241 4245 'PhabricatorDashboardListController' => 'PhabricatorDashboardController', 4246 + 'PhabricatorDashboardMovePanelController' => 'PhabricatorDashboardController', 4242 4247 'PhabricatorDashboardPHIDTypeDashboard' => 'PhabricatorPHIDType', 4243 4248 'PhabricatorDashboardPHIDTypePanel' => 'PhabricatorPHIDType', 4244 4249 'PhabricatorDashboardPanel' =>
+2 -1
src/applications/dashboard/application/PhabricatorApplicationDashboard.php
··· 21 21 '(?:query/(?P<queryKey>[^/]+)/)?' 22 22 => 'PhabricatorDashboardListController', 23 23 'view/(?P<id>\d+)/' => 'PhabricatorDashboardViewController', 24 + 'arrange/(?P<id>\d+)/' => 'PhabricatorDashboardArrangeController', 24 25 'create/' => 'PhabricatorDashboardEditController', 25 26 'edit/(?:(?P<id>\d+)/)?' => 'PhabricatorDashboardEditController', 26 27 'addpanel/(?P<id>\d+)/' => 'PhabricatorDashboardAddPanelController', 27 - 28 + 'movepanel/(?P<id>\d+)/' => 'PhabricatorDashboardMovePanelController', 28 29 'panel/' => array( 29 30 '(?:query/(?P<queryKey>[^/]+)/)?' 30 31 => 'PhabricatorDashboardPanelListController',
+30 -3
src/applications/dashboard/controller/PhabricatorDashboardAddPanelController.php
··· 26 26 return new Aphront404Response(); 27 27 } 28 28 29 - $dashboard_uri = $this->getApplicationURI('view/'.$dashboard->getID().'/'); 29 + if ($request->getStr('src', 'edit') == 'edit') { 30 + $redirect_uri = $this->getApplicationURI( 31 + 'view/'.$dashboard->getID().'/'); 32 + } else { 33 + $redirect_uri = $this->getApplicationURI( 34 + 'arrange/'.$dashboard->getID().'/'); 35 + } 36 + $layout_config = $dashboard->getLayoutConfigObject(); 30 37 31 38 $v_panel = $request->getStr('panel'); 32 39 $e_panel = true; ··· 61 68 ), 62 69 )); 63 70 71 + if ($layout_config->isMultiColumnLayout()) { 72 + $layout_config->setPanelLocation( 73 + $request->getInt('column'), 74 + $panel->getPHID()); 75 + $dashboard->setLayoutConfigFromObject($layout_config); 76 + } 77 + 64 78 $editor = id(new PhabricatorDashboardTransactionEditor()) 65 79 ->setActor($viewer) 66 80 ->setContentSourceFromRequest($request) ··· 68 82 ->setContinueOnNoEffect(true) 69 83 ->applyTransactions($dashboard, $xactions); 70 84 71 - return id(new AphrontRedirectResponse())->setURI($dashboard_uri); 85 + return id(new AphrontRedirectResponse())->setURI($redirect_uri); 72 86 } 73 87 } 74 88 75 89 $form = id(new AphrontFormView()) 76 90 ->setUser($viewer) 91 + ->addHiddenInput('src', $request->getStr('src', 'edit')) 77 92 ->appendRemarkupInstructions( 78 93 pht('Enter a panel monogram like `W123`.')) 79 94 ->appendChild( ··· 83 98 ->setValue($v_panel) 84 99 ->setError($e_panel)); 85 100 101 + if ($layout_config->isMultiColumnLayout()) { 102 + $form 103 + ->appendRemarkupInstructions( 104 + pht('Choose which column the panel should reside in.')) 105 + ->appendChild( 106 + id(new AphrontFormSelectControl()) 107 + ->setName('column') 108 + ->setLabel(pht('Column')) 109 + ->setOptions($layout_config->getColumnSelectOptions()) 110 + ->setValue($request->getInt('column'))); 111 + } 112 + 86 113 return $this->newDialog() 87 114 ->setTitle(pht('Add Panel')) 88 115 ->setErrors($errors) 89 116 ->appendChild($form->buildLayoutView()) 90 - ->addCancelButton($dashboard_uri) 117 + ->addCancelButton($redirect_uri) 91 118 ->addSubmitButton(pht('Add Panel')); 92 119 } 93 120
+54
src/applications/dashboard/controller/PhabricatorDashboardArrangeController.php
··· 1 + <?php 2 + 3 + final class PhabricatorDashboardArrangeController 4 + extends PhabricatorDashboardController { 5 + 6 + private $id; 7 + 8 + public function willProcessRequest(array $data) { 9 + $this->id = $data['id']; 10 + } 11 + 12 + public function processRequest() { 13 + $request = $this->getRequest(); 14 + $viewer = $request->getUser(); 15 + 16 + $dashboard = id(new PhabricatorDashboardQuery()) 17 + ->setViewer($viewer) 18 + ->withIDs(array($this->id)) 19 + ->needPanels(true) 20 + ->requireCapabilities( 21 + array( 22 + PhabricatorPolicyCapability::CAN_VIEW, 23 + PhabricatorPolicyCapability::CAN_EDIT, 24 + )) 25 + ->executeOne(); 26 + if (!$dashboard) { 27 + return new Aphront404Response(); 28 + } 29 + 30 + $title = $dashboard->getName(); 31 + $crumbs = $this->buildApplicationCrumbs(); 32 + $crumbs->addTextCrumb( 33 + pht('Dashboard %d', $dashboard->getID()), 34 + $this->getApplicationURI('view/'.$dashboard->getID().'/')); 35 + $crumbs->addTextCrumb(pht('Arrange')); 36 + 37 + $rendered_dashboard = id(new PhabricatorDashboardRenderingEngine()) 38 + ->setViewer($viewer) 39 + ->setDashboard($dashboard) 40 + ->setArrangeMode(true) 41 + ->renderDashboard(); 42 + 43 + return $this->buildApplicationPage( 44 + array( 45 + $crumbs, 46 + $rendered_dashboard, 47 + ), 48 + array( 49 + 'title' => $title, 50 + 'device' => true, 51 + )); 52 + } 53 + 54 + }
+21 -3
src/applications/dashboard/controller/PhabricatorDashboardEditController.php
··· 17 17 $dashboard = id(new PhabricatorDashboardQuery()) 18 18 ->setViewer($viewer) 19 19 ->withIDs(array($this->id)) 20 + ->needPanels(true) 20 21 ->requireCapabilities( 21 22 array( 22 23 PhabricatorPolicyCapability::CAN_VIEW, ··· 56 57 } 57 58 58 59 $v_name = $dashboard->getName(); 60 + $v_layout_mode = $dashboard->getLayoutConfigObject()->getLayoutMode(); 59 61 $e_name = true; 60 62 61 63 $validation_exception = null; 62 64 if ($request->isFormPost()) { 63 65 $v_name = $request->getStr('name'); 66 + $v_layout_mode = $request->getStr('layout_mode'); 64 67 65 68 $xactions = array(); 66 69 67 70 $type_name = PhabricatorDashboardTransaction::TYPE_NAME; 71 + $type_layout_mode = PhabricatorDashboardTransaction::TYPE_LAYOUT_MODE; 68 72 69 73 $xactions[] = id(new PhabricatorDashboardTransaction()) 70 74 ->setTransactionType($type_name) 71 75 ->setNewValue($v_name); 76 + $xactions[] = id(new PhabricatorDashboardTransaction()) 77 + ->setTransactionType($type_layout_mode) 78 + ->setNewValue($v_layout_mode); 72 79 73 80 try { 74 81 $editor = id(new PhabricatorDashboardTransactionEditor()) ··· 77 84 ->setContentSourceFromRequest($request) 78 85 ->applyTransactions($dashboard, $xactions); 79 86 80 - return id(new AphrontRedirectResponse()) 81 - ->setURI($this->getApplicationURI('view/'.$dashboard->getID().'/')); 87 + if ($is_new) { 88 + $uri = $this->getApplicationURI('arrange/'.$dashboard->getID().'/'); 89 + } else { 90 + $uri = $this->getApplicationURI('view/'.$dashboard->getID().'/'); 91 + } 92 + return id(new AphrontRedirectResponse())->setURI($uri); 82 93 } catch (PhabricatorApplicationTransactionValidationException $ex) { 83 94 $validation_exception = $ex; 84 95 ··· 86 97 } 87 98 } 88 99 100 + $layout_mode_options = 101 + PhabricatorDashboardLayoutConfig::getLayoutModeSelectOptions(); 89 102 $form = id(new AphrontFormView()) 90 103 ->setUser($viewer) 91 104 ->appendChild( ··· 95 108 ->setValue($v_name) 96 109 ->setError($e_name)) 97 110 ->appendChild( 111 + id(new AphrontFormSelectControl()) 112 + ->setLabel(pht('Layout Mode')) 113 + ->setName('layout_mode') 114 + ->setValue($v_layout_mode) 115 + ->setOptions($layout_mode_options)) 116 + ->appendChild( 98 117 id(new AphrontFormSubmitControl()) 99 118 ->setValue($button) 100 119 ->addCancelButton($cancel_uri)); 101 - 102 120 103 121 $box = id(new PHUIObjectBoxView()) 104 122 ->setHeaderText($header)
+86
src/applications/dashboard/controller/PhabricatorDashboardMovePanelController.php
··· 1 + <?php 2 + 3 + final class PhabricatorDashboardMovePanelController 4 + extends PhabricatorDashboardController { 5 + 6 + private $id; 7 + 8 + public function willProcessRequest(array $data) { 9 + $this->id = $data['id']; 10 + } 11 + 12 + public function processRequest() { 13 + $request = $this->getRequest(); 14 + $viewer = $request->getUser(); 15 + 16 + $column_id = $request->getStr('columnID'); 17 + $panel_phid = $request->getStr('objectPHID'); 18 + $after_phid = $request->getStr('afterPHID'); 19 + $before_phid = $request->getStr('beforePHID'); 20 + 21 + $dashboard = id(new PhabricatorDashboardQuery()) 22 + ->setViewer($viewer) 23 + ->withIDs(array($this->id)) 24 + ->needPanels(true) 25 + ->requireCapabilities( 26 + array( 27 + PhabricatorPolicyCapability::CAN_VIEW, 28 + PhabricatorPolicyCapability::CAN_EDIT, 29 + )) 30 + ->executeOne(); 31 + if (!$dashboard) { 32 + return new Aphront404Response(); 33 + } 34 + $panels = mpull($dashboard->getPanels(), null, 'getPHID'); 35 + $panel = idx($panels, $panel_phid); 36 + if (!$panel) { 37 + return new Aphront404Response(); 38 + } 39 + 40 + $layout_config = $dashboard->getLayoutConfigObject(); 41 + $panel_location_grid = $layout_config->getPanelLocations(); 42 + 43 + foreach ($panel_location_grid as $column => $panel_columns) { 44 + $found_old_column = array_search($panel_phid, $panel_columns); 45 + if ($found_old_column !== false) { 46 + $new_panel_columns = $panel_columns; 47 + array_splice( 48 + $new_panel_columns, 49 + $found_old_column, 50 + 1, 51 + array()); 52 + $panel_location_grid[$column] = $new_panel_columns; 53 + break; 54 + } 55 + } 56 + $panel_columns = idx($panel_location_grid, $column_id, array()); 57 + if ($panel_columns) { 58 + $insert_at = 0; 59 + $new_panel_columns = $panel_columns; 60 + foreach ($panel_columns as $index => $curr_panel_phid) { 61 + if ($curr_panel_phid === $before_phid) { 62 + $insert_at = max($index - 1, 0); 63 + break; 64 + } 65 + if ($curr_panel_phid === $after_phid) { 66 + $insert_at = $index; 67 + break; 68 + } 69 + } 70 + array_splice( 71 + $new_panel_columns, 72 + $insert_at, 73 + 0, 74 + array($panel_phid)); 75 + } else { 76 + $new_panel_columns = array(0 => $panel_phid); 77 + } 78 + $panel_location_grid[$column_id] = $new_panel_columns; 79 + $layout_config->setPanelLocations($panel_location_grid); 80 + $dashboard->setLayoutConfigFromObject($layout_config); 81 + $dashboard->save(); 82 + 83 + return id(new AphrontAjaxResponse())->setContent(''); 84 + } 85 + 86 + }
+8
src/applications/dashboard/controller/PhabricatorDashboardViewController.php
··· 86 86 87 87 $actions->addAction( 88 88 id(new PhabricatorActionView()) 89 + ->setName(pht('Arrange Dashboard')) 90 + ->setIcon('fa-arrows') 91 + ->setHref($this->getApplicationURI("arrange/{$id}/")) 92 + ->setDisabled(!$can_edit) 93 + ->setWorkflow(!$can_edit)); 94 + 95 + $actions->addAction( 96 + id(new PhabricatorActionView()) 89 97 ->setName(pht('Add Panel')) 90 98 ->setIcon('fa-plus') 91 99 ->setHref($this->getApplicationURI("addpanel/{$id}/"))
+24
src/applications/dashboard/editor/PhabricatorDashboardTransactionEditor.php
··· 11 11 $types[] = PhabricatorTransactions::TYPE_EDGE; 12 12 13 13 $types[] = PhabricatorDashboardTransaction::TYPE_NAME; 14 + $types[] = PhabricatorDashboardTransaction::TYPE_LAYOUT_MODE; 14 15 15 16 return $types; 16 17 } ··· 24 25 return null; 25 26 } 26 27 return $object->getName(); 28 + case PhabricatorDashboardTransaction::TYPE_LAYOUT_MODE: 29 + if ($this->getIsNewObject()) { 30 + return null; 31 + } 32 + $layout_config = $object->getLayoutConfigObject(); 33 + return $layout_config->getLayoutMode(); 27 34 } 28 35 29 36 return parent::getCustomTransactionOldValue($object, $xaction); ··· 34 41 PhabricatorApplicationTransaction $xaction) { 35 42 switch ($xaction->getTransactionType()) { 36 43 case PhabricatorDashboardTransaction::TYPE_NAME: 44 + case PhabricatorDashboardTransaction::TYPE_LAYOUT_MODE: 37 45 return $xaction->getNewValue(); 38 46 } 39 47 return parent::getCustomTransactionNewValue($object, $xaction); ··· 46 54 case PhabricatorDashboardTransaction::TYPE_NAME: 47 55 $object->setName($xaction->getNewValue()); 48 56 return; 57 + case PhabricatorDashboardTransaction::TYPE_LAYOUT_MODE: 58 + $old_layout = $object->getLayoutConfigObject(); 59 + $new_layout = clone $old_layout; 60 + $new_layout->setLayoutMode($xaction->getNewValue()); 61 + if ($old_layout->isMultiColumnLayout() != 62 + $new_layout->isMultiColumnLayout()) { 63 + $panel_phids = $object->getPanelPHIDs(); 64 + $new_locations = $new_layout->getDefaultPanelLocations(); 65 + foreach ($panel_phids as $panel_phid) { 66 + $new_locations[0][] = $panel_phid; 67 + } 68 + $new_layout->setPanelLocations($new_locations); 69 + } 70 + $object->setLayoutConfigFromObject($new_layout); 71 + return; 49 72 case PhabricatorTransactions::TYPE_VIEW_POLICY: 50 73 $object->setViewPolicy($xaction->getNewValue()); 51 74 return; ··· 65 88 66 89 switch ($xaction->getTransactionType()) { 67 90 case PhabricatorDashboardTransaction::TYPE_NAME: 91 + case PhabricatorDashboardTransaction::TYPE_LAYOUT_MODE: 68 92 return; 69 93 case PhabricatorTransactions::TYPE_EDGE: 70 94 return;
+3
src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php
··· 79 79 )); 80 80 81 81 return id(new PHUIObjectBoxView()) 82 + ->addSigil('dashboard-panel') 83 + ->setMetadata(array( 84 + 'objectPHID' => $panel->getPHID())) 82 85 ->setHeaderText($panel->getName()) 83 86 ->setID($panel_id) 84 87 ->appendChild(pht('Loading...'));
+89 -7
src/applications/dashboard/engine/PhabricatorDashboardRenderingEngine.php
··· 4 4 5 5 private $dashboard; 6 6 private $viewer; 7 + private $arrangeMode; 7 8 8 9 public function setViewer(PhabricatorUser $viewer) { 9 10 $this->viewer = $viewer; ··· 15 16 return $this; 16 17 } 17 18 19 + public function setArrangeMode($mode) { 20 + $this->arrangeMode = $mode; 21 + return $this; 22 + } 23 + 18 24 public function renderDashboard() { 25 + require_celerity_resource('phabricator-dashboard-css'); 19 26 $dashboard = $this->dashboard; 20 27 $viewer = $this->viewer; 21 28 22 - $result = array(); 23 - foreach ($dashboard->getPanels() as $panel) { 24 - $result[] = id(new PhabricatorDashboardPanelRenderingEngine()) 25 - ->setViewer($viewer) 26 - ->setPanel($panel) 27 - ->setEnableAsyncRendering(true) 28 - ->renderPanel(); 29 + $layout_config = $dashboard->getLayoutConfigObject(); 30 + $panel_grid_locations = $layout_config->getPanelLocations(); 31 + $panels = mpull($dashboard->getPanels(), null, 'getPHID'); 32 + $dashboard_id = celerity_generate_unique_node_id(); 33 + $result = id(new AphrontMultiColumnView()) 34 + ->setID($dashboard_id) 35 + ->setFluidlayout(true); 36 + 37 + foreach ($panel_grid_locations as $column => $panel_column_locations) { 38 + $panel_phids = $panel_column_locations; 39 + $column_panels = array_select_keys($panels, $panel_phids); 40 + $column_result = array(); 41 + foreach ($column_panels as $panel) { 42 + $column_result[] = id(new PhabricatorDashboardPanelRenderingEngine()) 43 + ->setViewer($viewer) 44 + ->setPanel($panel) 45 + ->setEnableAsyncRendering(true) 46 + ->renderPanel(); 47 + } 48 + $column_class = $layout_config->getColumnClass( 49 + $column, 50 + $this->arrangeMode); 51 + if ($this->arrangeMode) { 52 + $column_result[] = $this->renderAddPanelPlaceHolder($column); 53 + $column_result[] = $this->renderAddPanelUI($column); 54 + } 55 + $result->addColumn( 56 + $column_result, 57 + $column_class, 58 + $sigil = 'dashboard-column', 59 + $metadata = array('columnID' => $column)); 60 + } 61 + 62 + if ($this->arrangeMode) { 63 + Javelin::initBehavior( 64 + 'dashboard-move-panels', 65 + array( 66 + 'dashboardID' => $dashboard_id, 67 + 'moveURI' => '/dashboard/movepanel/'.$dashboard->getID().'/', 68 + )); 29 69 } 30 70 31 71 return $result; 72 + } 73 + 74 + private function renderAddPanelPlaceHolder($column) { 75 + $uri = $this->getAddPanelURI($column); 76 + 77 + $dashboard = $this->dashboard; 78 + $panels = $dashboard->getPanels(); 79 + $layout_config = $dashboard->getLayoutConfigObject(); 80 + if ($layout_config->isMultiColumnLayout() && count($panels)) { 81 + $text = pht('Drag a panel here or click to add a panel.'); 82 + } else { 83 + $text = pht('Click to add a panel.'); 84 + } 85 + return javelin_tag( 86 + 'a', 87 + array( 88 + 'sigil' => 'workflow', 89 + 'class' => 'drag-ghost dashboard-panel-placeholder', 90 + 'href' => (string) $uri), 91 + $text); 92 + } 93 + 94 + private function renderAddPanelUI($column) { 95 + $uri = $this->getAddPanelURI($column); 96 + 97 + return id(new PHUIButtonView()) 98 + ->setTag('a') 99 + ->setHref((string) $uri) 100 + ->setWorkflow(true) 101 + ->setColor(PHUIButtonView::GREY) 102 + ->setIcon(id(new PHUIIconView()) 103 + ->setIconFont('fa-plus')) 104 + ->setText(pht('Add Panel')) 105 + ->addClass(PHUI::MARGIN_LARGE); 106 + } 107 + 108 + private function getAddPanelURI($column) { 109 + $dashboard = $this->dashboard; 110 + $uri = id(new PhutilURI('/dashboard/addpanel/'.$dashboard->getID().'/')) 111 + ->setQueryParam('column', $column) 112 + ->setQueryParam('src', 'arrange'); 113 + return $uri; 32 114 } 33 115 34 116 }
+133
src/applications/dashboard/layoutconfig/PhabricatorDashboardLayoutConfig.php
··· 1 + <?php 2 + 3 + final class PhabricatorDashboardLayoutConfig { 4 + 5 + const MODE_FULL = 'layout-mode-full'; 6 + const MODE_HALF_AND_HALF = 'layout-mode-half-and-half'; 7 + const MODE_THIRD_AND_THIRDS = 'layout-mode-third-and-thirds'; 8 + const MODE_THIRDS_AND_THIRD = 'layout-mode-thirds-and-third'; 9 + 10 + private $layoutMode = self::MODE_FULL; 11 + private $panelLocations = array(); 12 + 13 + public function setLayoutMode($mode) { 14 + $this->layoutMode = $mode; 15 + return $this; 16 + } 17 + public function getLayoutMode() { 18 + return $this->layoutMode; 19 + } 20 + 21 + public function setPanelLocation($which_column, $panel_phid) { 22 + $this->panelLocations[$which_column][] = $panel_phid; 23 + return $this; 24 + } 25 + 26 + public function setPanelLocations(array $locations) { 27 + $this->panelLocations = $locations; 28 + return $this; 29 + } 30 + 31 + public function getPanelLocations() { 32 + return $this->panelLocations; 33 + } 34 + 35 + public function getDefaultPanelLocations() { 36 + switch ($this->getLayoutMode()) { 37 + case self::MODE_HALF_AND_HALF: 38 + case self::MODE_THIRD_AND_THIRDS: 39 + case self::MODE_THIRDS_AND_THIRD: 40 + $locations = array(array(), array()); 41 + break; 42 + case self::MODE_FULL: 43 + default: 44 + $locations = array(array()); 45 + break; 46 + } 47 + return $locations; 48 + } 49 + 50 + public function getColumnClass($column_index, $grippable = false) { 51 + switch ($this->getLayoutMode()) { 52 + case self::MODE_HALF_AND_HALF: 53 + $class = 'half'; 54 + break; 55 + case self::MODE_THIRD_AND_THIRDS: 56 + if ($column_index) { 57 + $class = 'thirds'; 58 + } else { 59 + $class = 'third'; 60 + } 61 + break; 62 + case self::MODE_THIRDS_AND_THIRD: 63 + if ($column_index) { 64 + $class = 'third'; 65 + } else { 66 + $class = 'thirds'; 67 + } 68 + break; 69 + case self::MODE_FULL: 70 + default: 71 + $class = null; 72 + break; 73 + } 74 + if ($grippable) { 75 + $class .= ' grippable'; 76 + } 77 + return $class; 78 + } 79 + 80 + public function isMultiColumnLayout() { 81 + return $this->getLayoutMode() != self::MODE_FULL; 82 + } 83 + 84 + public function getColumnSelectOptions() { 85 + $options = array(); 86 + 87 + switch ($this->getLayoutMode()) { 88 + case self::MODE_HALF_AND_HALF: 89 + case self::MODE_THIRD_AND_THIRDS: 90 + case self::MODE_THIRDS_AND_THIRD: 91 + return array( 92 + 0 => pht('Left'), 93 + 1 => pht('Right')); 94 + break; 95 + case self::MODE_FULL: 96 + throw new Exception('There is only one column in mode full.'); 97 + break; 98 + default: 99 + throw new Exception('Unknown layout mode!'); 100 + break; 101 + } 102 + 103 + return $options; 104 + } 105 + 106 + public static function getLayoutModeSelectOptions() { 107 + return array( 108 + self::MODE_FULL => pht('One full-width column'), 109 + self::MODE_HALF_AND_HALF => pht('Two columns, 1/2 and 1/2'), 110 + self::MODE_THIRD_AND_THIRDS => pht('Two columns, 1/3 and 2/3'), 111 + self::MODE_THIRDS_AND_THIRD => pht('Two columns, 2/3 and 1/3'), 112 + ); 113 + } 114 + 115 + public static function newFromDictionary(array $dict) { 116 + $layout_config = id(new PhabricatorDashboardLayoutConfig()) 117 + ->setLayoutMode(idx($dict, 'layoutMode', self::MODE_FULL)); 118 + $layout_config->setPanelLocations(idx( 119 + $dict, 120 + 'panelLocations', 121 + $layout_config->getDefaultPanelLocations())); 122 + 123 + return $layout_config; 124 + } 125 + 126 + public function toDictionary() { 127 + return array( 128 + 'layoutMode' => $this->getLayoutMode(), 129 + 'panelLocations' => $this->getPanelLocations() 130 + ); 131 + } 132 + 133 + }
+3
src/applications/dashboard/paneltype/PhabricatorDashboardPanelType.php
··· 47 47 $content = $this->renderPanelContent($viewer, $panel); 48 48 49 49 return id(new PHUIObjectBoxView()) 50 + ->addSigil('dashboard-panel') 51 + ->setMetadata(array( 52 + 'objectPHID' => $panel->getPHID())) 50 53 ->setHeaderText($panel->getName()) 51 54 ->appendChild($content); 52 55 }
+17 -1
src/applications/dashboard/storage/PhabricatorDashboard.php
··· 9 9 protected $name; 10 10 protected $viewPolicy; 11 11 protected $editPolicy; 12 + protected $layoutConfig = array(); 12 13 13 14 private $panelPHIDs = self::ATTACHABLE; 14 15 private $panels = self::ATTACHABLE; ··· 17 18 return id(new PhabricatorDashboard()) 18 19 ->setName('') 19 20 ->setViewPolicy(PhabricatorPolicies::POLICY_USER) 20 - ->setEditPolicy($actor->getPHID()); 21 + ->setEditPolicy($actor->getPHID()) 22 + ->attachPanels(array()) 23 + ->attachPanelPHIDs(array()); 21 24 } 22 25 23 26 public function getConfiguration() { 24 27 return array( 25 28 self::CONFIG_AUX_PHID => true, 29 + self::CONFIG_SERIALIZATION => array( 30 + 'layoutConfig' => self::SERIALIZATION_JSON), 26 31 ) + parent::getConfiguration(); 27 32 } 28 33 29 34 public function generatePHID() { 30 35 return PhabricatorPHID::generateNewPHID( 31 36 PhabricatorDashboardPHIDTypeDashboard::TYPECONST); 37 + } 38 + 39 + public function getLayoutConfigObject() { 40 + return PhabricatorDashboardLayoutConfig::newFromDictionary( 41 + $this->getLayoutConfig()); 42 + } 43 + 44 + public function setLayoutConfigFromObject( 45 + PhabricatorDashboardLayoutConfig $object) { 46 + $this->setLayoutConfig($object->toDictionary()); 47 + return $this; 32 48 } 33 49 34 50 public function attachPanelPHIDs(array $phids) {
+12
src/applications/dashboard/storage/PhabricatorDashboardTransaction.php
··· 4 4 extends PhabricatorApplicationTransaction { 5 5 6 6 const TYPE_NAME = 'dashboard:name'; 7 + const TYPE_LAYOUT_MODE = 'dashboard:layoutmode'; 7 8 8 9 public function getApplicationName() { 9 10 return 'dashboard'; ··· 85 86 } 86 87 87 88 return parent::getColor(); 89 + } 90 + 91 + public function shouldHide() { 92 + $old = $this->getOldValue(); 93 + $new = $this->getNewValue(); 94 + 95 + switch ($this->getTransactionType()) { 96 + case self::TYPE_LAYOUT_MODE: 97 + return true; 98 + } 99 + return parent::shouldHide(); 88 100 } 89 101 }
+41 -18
src/view/layout/AphrontMultiColumnView.php
··· 6 6 const GUTTER_MEDIUM = 'mmr'; 7 7 const GUTTER_LARGE = 'mlr'; 8 8 9 + private $id; 9 10 private $columns = array(); 10 11 private $fluidLayout = false; 11 12 private $fluidishLayout = false; 12 13 private $gutter; 13 14 private $border; 14 15 15 - public function addColumn($column) { 16 - $this->columns[] = $column; 16 + public function setID($id) { 17 + $this->id = $id; 18 + return $this; 19 + } 20 + 21 + public function getID() { 22 + return $this->id; 23 + } 24 + 25 + public function addColumn( 26 + $column, 27 + $class = null, 28 + $sigil = null, 29 + $metadata = null) { 30 + $this->columns[] = array( 31 + 'column' => $column, 32 + 'class' => $class, 33 + 'sigil' => $sigil, 34 + 'metadata' => $metadata); 17 35 return $this; 18 36 } 19 37 ··· 55 73 $classes[] = 'aphront-multi-column-'.count($this->columns).'-up'; 56 74 57 75 $columns = array(); 58 - $column_class = array(); 59 - $column_class[] = 'aphront-multi-column-column'; 60 - $outer_class = array(); 61 - $outer_class[] = 'aphront-multi-column-column-outer'; 62 - if ($this->gutter) { 63 - $column_class[] = $this->gutter; 64 - } 65 76 $i = 0; 66 - foreach ($this->columns as $column) { 77 + foreach ($this->columns as $column_data) { 78 + $column_class = array('aphront-multi-column-column'); 79 + if ($this->gutter) { 80 + $column_class[] = $this->gutter; 81 + } 82 + $outer_class = array('aphront-multi-column-column-outer'); 67 83 if (++$i === count($this->columns)) { 68 84 $column_class[] = 'aphront-multi-column-column-last'; 69 85 $outer_class[] = 'aphront-multi-colum-column-outer-last'; 70 86 } 71 - $column_inner = phutil_tag( 87 + $column = $column_data['column']; 88 + if ($column_data['class']) { 89 + $outer_class[] = $column_data['class']; 90 + } 91 + $column_sigil = idx($column_data, 'sigil'); 92 + $column_metadata = idx($column_data, 'metadata'); 93 + $column_inner = javelin_tag( 72 94 'div', 73 - array( 74 - 'class' => implode(' ', $column_class) 75 - ), 95 + array( 96 + 'class' => implode(' ', $column_class), 97 + 'sigil' => $column_sigil, 98 + 'meta' => $column_metadata), 76 99 $column); 77 100 $columns[] = phutil_tag( 78 101 'div', 79 - array( 80 - 'class' => implode(' ', $outer_class) 81 - ), 102 + array( 103 + 'class' => implode(' ', $outer_class)), 82 104 $column_inner); 83 105 } 84 106 ··· 120 142 return phutil_tag( 121 143 'div', 122 144 array( 123 - 'class' => 'aphront-multi-column-view' 145 + 'class' => 'aphront-multi-column-view', 146 + 'id' => $this->getID(), 124 147 ), 125 148 $board); 126 149 }
+59
webroot/rsrc/css/application/dashboard/dashboard.css
··· 1 + /** 2 + * @provides phabricator-dashboard-css 3 + */ 4 + 5 + .aphront-multi-column-fluid .aphront-multi-column-2-up 6 + .aphront-multi-column-column-outer.half { 7 + width: 50%; 8 + } 9 + 10 + .aphront-multi-column-fluid .aphront-multi-column-2-up 11 + .aphront-multi-column-column-outer.third { 12 + width: 33.34%; 13 + } 14 + 15 + .aphront-multi-column-fluid .aphront-multi-column-2-up 16 + .aphront-multi-column-column-outer.thirds { 17 + width: 66.66%; 18 + } 19 + 20 + .aphront-multi-column-fluid 21 + .aphront-multi-column-column-outer.grippable 22 + .aphront-multi-column-column .phui-object-box { 23 + cursor: move; 24 + } 25 + 26 + .aphront-multi-column-fluid 27 + .aphront-multi-column-column .drag-ghost { 28 + list-style-type: none; 29 + margin: 16px; 30 + } 31 + 32 + .aphront-multi-column-fluid 33 + .aphront-multi-column-column 34 + .dashboard-panel-placeholder { 35 + display: none; 36 + } 37 + 38 + .aphront-multi-column-fluid 39 + .aphront-multi-column-column.dashboard-column-empty 40 + .dashboard-panel-placeholder { 41 + color: {$greytext}; 42 + display: block; 43 + padding: 24px; 44 + margin: 16px 16px 0px 16px; 45 + } 46 + 47 + .aphront-multi-column-fluid 48 + .aphront-multi-column-column.dashboard-column-empty 49 + .dashboard-panel-placeholder:hover { 50 + text-decoration: none; 51 + border: 1px {$greyborder} dashed; 52 + color: {$darkgreytext}; 53 + } 54 + 55 + .aphront-multi-column-fluid 56 + .aphront-multi-column-column.drag-target-list 57 + .dashboard-panel-placeholder { 58 + display: none; 59 + }
+103
webroot/rsrc/js/application/dashboard/behavior-dashboard-move-panels.js
··· 1 + /** 2 + * @provides javelin-behavior-dashboard-move-panels 3 + * @requires javelin-behavior 4 + * javelin-dom 5 + * javelin-util 6 + * javelin-stratcom 7 + * javelin-workflow 8 + * phabricator-draggable-list 9 + */ 10 + 11 + JX.behavior('dashboard-move-panels', function(config) { 12 + 13 + var itemSigil = 'dashboard-panel'; 14 + 15 + function finditems(col) { 16 + return JX.DOM.scry(col, 'div', itemSigil); 17 + } 18 + 19 + function markcolempty(col, toggle) { 20 + JX.DOM.alterClass(col, 'dashboard-column-empty', toggle); 21 + } 22 + 23 + function onupdate(col) { 24 + markcolempty(col, !this.findItems().length); 25 + } 26 + 27 + function onresponse(response, item, list) { 28 + list.unlock(); 29 + JX.DOM.alterClass(item, 'drag-sending', false); 30 + } 31 + 32 + function ondrop(list, item, after, from) { 33 + list.lock(); 34 + JX.DOM.alterClass(item, 'drag-sending', true); 35 + 36 + var item_phid = JX.Stratcom.getData(item).objectPHID; 37 + var data = { 38 + objectPHID: item_phid, 39 + columnID: JX.Stratcom.getData(list.getRootNode()).columnID 40 + }; 41 + 42 + var after_phid = null; 43 + var items = finditems(list.getRootNode()); 44 + if (after) { 45 + after_phid = JX.Stratcom.getData(after).objectPHID; 46 + data.afterPHID = after_phid; 47 + } 48 + var ii; 49 + var ii_item; 50 + var ii_item_phid; 51 + var ii_prev_item_phid = null; 52 + var before_phid = null; 53 + for (ii = 0; ii < items.length; ii++) { 54 + ii_item = items[ii]; 55 + ii_item_phid = JX.Stratcom.getData(ii_item).objectPHID; 56 + if (ii_item_phid == item_phid) { 57 + // skip the item we just dropped 58 + continue; 59 + } 60 + // note this handles when there is no after phid - we are at the top of 61 + // the list - quite nicely 62 + if (ii_prev_item_phid == after_phid) { 63 + before_phid = ii_item_phid; 64 + break; 65 + } 66 + ii_prev_item_phid = ii_item_phid; 67 + } 68 + if (before_phid) { 69 + data.beforePHID = before_phid; 70 + } 71 + 72 + var workflow = new JX.Workflow(config.moveURI, data) 73 + .setHandler(function(response) { 74 + onresponse(response, item, list); 75 + }); 76 + 77 + workflow.start(); 78 + } 79 + 80 + var lists = []; 81 + var ii; 82 + var cols = JX.DOM.scry(JX.$(config.dashboardID), 'div', 'dashboard-column'); 83 + var col = null; 84 + 85 + for (ii = 0; ii < cols.length; ii++) { 86 + col = cols[ii]; 87 + var list = new JX.DraggableList(itemSigil, col) 88 + .setFindItemsHandler(JX.bind(null, finditems, col)); 89 + 90 + list.listen('didSend', JX.bind(list, onupdate, col)); 91 + list.listen('didReceive', JX.bind(list, onupdate, col)); 92 + 93 + list.listen('didDrop', JX.bind(null, ondrop, list)); 94 + 95 + lists.push(list); 96 + markcolempty(col, finditems(col).length === 0); 97 + } 98 + 99 + for (ii = 0; ii < lists.length; ii++) { 100 + lists[ii].setGroup(lists); 101 + } 102 + 103 + });