1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-26 08:42:41 +01:00

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
This commit is contained in:
Bob Trahan 2014-05-15 19:12:40 -07:00
parent 9d5f9d5c2c
commit 6300955661
19 changed files with 719 additions and 45 deletions

View file

@ -52,6 +52,7 @@ return array(
'rsrc/css/application/conpherence/widget-pane.css' => 'bf275a6c', 'rsrc/css/application/conpherence/widget-pane.css' => 'bf275a6c',
'rsrc/css/application/contentsource/content-source-view.css' => '4b8b05d4', 'rsrc/css/application/contentsource/content-source-view.css' => '4b8b05d4',
'rsrc/css/application/countdown/timer.css' => '86b7b0a0', '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/diff/inline-comment-summary.css' => '8cfd34e8',
'rsrc/css/application/differential/add-comment.css' => 'c478bcaa', 'rsrc/css/application/differential/add-comment.css' => 'c478bcaa',
'rsrc/css/application/differential/changeset-view.css' => '1570a1ff', '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/conpherence/behavior-widget-pane.js' => '40b1ff90',
'rsrc/js/application/countdown/timer.js' => '889c96f3', 'rsrc/js/application/countdown/timer.js' => '889c96f3',
'rsrc/js/application/dashboard/behavior-dashboard-async-panel.js' => '4398eabb', '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/DifferentialInlineCommentEditor.js' => 'f2441746',
'rsrc/js/application/differential/behavior-add-reviewers-and-ccs.js' => '533a187b', 'rsrc/js/application/differential/behavior-add-reviewers-and-ccs.js' => '533a187b',
'rsrc/js/application/differential/behavior-comment-jump.js' => '71755c79', 'rsrc/js/application/differential/behavior-comment-jump.js' => '71755c79',
@ -552,6 +554,7 @@ return array(
'javelin-behavior-countdown-timer' => '889c96f3', 'javelin-behavior-countdown-timer' => '889c96f3',
'javelin-behavior-dark-console' => 'e9fdb5e5', 'javelin-behavior-dark-console' => 'e9fdb5e5',
'javelin-behavior-dashboard-async-panel' => '4398eabb', 'javelin-behavior-dashboard-async-panel' => '4398eabb',
'javelin-behavior-dashboard-move-panels' => 'aa3f313b',
'javelin-behavior-device' => '03d6ed07', 'javelin-behavior-device' => '03d6ed07',
'javelin-behavior-differential-add-reviewers-and-ccs' => '533a187b', 'javelin-behavior-differential-add-reviewers-and-ccs' => '533a187b',
'javelin-behavior-differential-comment-jump' => '71755c79', 'javelin-behavior-differential-comment-jump' => '71755c79',
@ -698,6 +701,7 @@ return array(
'phabricator-core-css' => '40151074', 'phabricator-core-css' => '40151074',
'phabricator-countdown-css' => '86b7b0a0', 'phabricator-countdown-css' => '86b7b0a0',
'phabricator-crumbs-view-css' => '6a23399c', 'phabricator-crumbs-view-css' => '6a23399c',
'phabricator-dashboard-css' => '5b532b7b',
'phabricator-drag-and-drop-file-upload' => 'ae6abfba', 'phabricator-drag-and-drop-file-upload' => 'ae6abfba',
'phabricator-draggable-list' => '1681c4d4', 'phabricator-draggable-list' => '1681c4d4',
'phabricator-fatal-config-template-css' => '25d446d6', 'phabricator-fatal-config-template-css' => '25d446d6',
@ -1266,6 +1270,18 @@ return array(
2 => 'javelin-util', 2 => 'javelin-util',
3 => 'phabricator-shaped-request', 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' => '6453c869' =>
array( array(
0 => 'javelin-install', 0 => 'javelin-install',
@ -1313,18 +1329,6 @@ return array(
1 => 'javelin-stratcom', 1 => 'javelin-stratcom',
2 => 'javelin-dom', 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' => '76f4ebed' =>
array( array(
0 => 'javelin-install', 0 => 'javelin-install',
@ -1594,6 +1598,15 @@ return array(
1 => 'javelin-stratcom', 1 => 'javelin-stratcom',
2 => 'javelin-dom', 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' => 'ad7a69ca' =>
array( array(
0 => 'javelin-install', 0 => 'javelin-install',

View file

@ -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 = '[]';

View file

@ -1456,10 +1456,13 @@ phutil_register_library_map(array(
'PhabricatorDaemonTaskGarbageCollector' => 'applications/daemon/garbagecollector/PhabricatorDaemonTaskGarbageCollector.php', 'PhabricatorDaemonTaskGarbageCollector' => 'applications/daemon/garbagecollector/PhabricatorDaemonTaskGarbageCollector.php',
'PhabricatorDashboard' => 'applications/dashboard/storage/PhabricatorDashboard.php', 'PhabricatorDashboard' => 'applications/dashboard/storage/PhabricatorDashboard.php',
'PhabricatorDashboardAddPanelController' => 'applications/dashboard/controller/PhabricatorDashboardAddPanelController.php', 'PhabricatorDashboardAddPanelController' => 'applications/dashboard/controller/PhabricatorDashboardAddPanelController.php',
'PhabricatorDashboardArrangeController' => 'applications/dashboard/controller/PhabricatorDashboardArrangeController.php',
'PhabricatorDashboardController' => 'applications/dashboard/controller/PhabricatorDashboardController.php', 'PhabricatorDashboardController' => 'applications/dashboard/controller/PhabricatorDashboardController.php',
'PhabricatorDashboardDAO' => 'applications/dashboard/storage/PhabricatorDashboardDAO.php', 'PhabricatorDashboardDAO' => 'applications/dashboard/storage/PhabricatorDashboardDAO.php',
'PhabricatorDashboardEditController' => 'applications/dashboard/controller/PhabricatorDashboardEditController.php', 'PhabricatorDashboardEditController' => 'applications/dashboard/controller/PhabricatorDashboardEditController.php',
'PhabricatorDashboardLayoutConfig' => 'applications/dashboard/layoutconfig/PhabricatorDashboardLayoutConfig.php',
'PhabricatorDashboardListController' => 'applications/dashboard/controller/PhabricatorDashboardListController.php', 'PhabricatorDashboardListController' => 'applications/dashboard/controller/PhabricatorDashboardListController.php',
'PhabricatorDashboardMovePanelController' => 'applications/dashboard/controller/PhabricatorDashboardMovePanelController.php',
'PhabricatorDashboardPHIDTypeDashboard' => 'applications/dashboard/phid/PhabricatorDashboardPHIDTypeDashboard.php', 'PhabricatorDashboardPHIDTypeDashboard' => 'applications/dashboard/phid/PhabricatorDashboardPHIDTypeDashboard.php',
'PhabricatorDashboardPHIDTypePanel' => 'applications/dashboard/phid/PhabricatorDashboardPHIDTypePanel.php', 'PhabricatorDashboardPHIDTypePanel' => 'applications/dashboard/phid/PhabricatorDashboardPHIDTypePanel.php',
'PhabricatorDashboardPanel' => 'applications/dashboard/storage/PhabricatorDashboardPanel.php', 'PhabricatorDashboardPanel' => 'applications/dashboard/storage/PhabricatorDashboardPanel.php',
@ -4235,10 +4238,12 @@ phutil_register_library_map(array(
1 => 'PhabricatorPolicyInterface', 1 => 'PhabricatorPolicyInterface',
), ),
'PhabricatorDashboardAddPanelController' => 'PhabricatorDashboardController', 'PhabricatorDashboardAddPanelController' => 'PhabricatorDashboardController',
'PhabricatorDashboardArrangeController' => 'PhabricatorDashboardController',
'PhabricatorDashboardController' => 'PhabricatorController', 'PhabricatorDashboardController' => 'PhabricatorController',
'PhabricatorDashboardDAO' => 'PhabricatorLiskDAO', 'PhabricatorDashboardDAO' => 'PhabricatorLiskDAO',
'PhabricatorDashboardEditController' => 'PhabricatorDashboardController', 'PhabricatorDashboardEditController' => 'PhabricatorDashboardController',
'PhabricatorDashboardListController' => 'PhabricatorDashboardController', 'PhabricatorDashboardListController' => 'PhabricatorDashboardController',
'PhabricatorDashboardMovePanelController' => 'PhabricatorDashboardController',
'PhabricatorDashboardPHIDTypeDashboard' => 'PhabricatorPHIDType', 'PhabricatorDashboardPHIDTypeDashboard' => 'PhabricatorPHIDType',
'PhabricatorDashboardPHIDTypePanel' => 'PhabricatorPHIDType', 'PhabricatorDashboardPHIDTypePanel' => 'PhabricatorPHIDType',
'PhabricatorDashboardPanel' => 'PhabricatorDashboardPanel' =>

View file

@ -21,10 +21,11 @@ final class PhabricatorApplicationDashboard extends PhabricatorApplication {
'(?:query/(?P<queryKey>[^/]+)/)?' '(?:query/(?P<queryKey>[^/]+)/)?'
=> 'PhabricatorDashboardListController', => 'PhabricatorDashboardListController',
'view/(?P<id>\d+)/' => 'PhabricatorDashboardViewController', 'view/(?P<id>\d+)/' => 'PhabricatorDashboardViewController',
'arrange/(?P<id>\d+)/' => 'PhabricatorDashboardArrangeController',
'create/' => 'PhabricatorDashboardEditController', 'create/' => 'PhabricatorDashboardEditController',
'edit/(?:(?P<id>\d+)/)?' => 'PhabricatorDashboardEditController', 'edit/(?:(?P<id>\d+)/)?' => 'PhabricatorDashboardEditController',
'addpanel/(?P<id>\d+)/' => 'PhabricatorDashboardAddPanelController', 'addpanel/(?P<id>\d+)/' => 'PhabricatorDashboardAddPanelController',
'movepanel/(?P<id>\d+)/' => 'PhabricatorDashboardMovePanelController',
'panel/' => array( 'panel/' => array(
'(?:query/(?P<queryKey>[^/]+)/)?' '(?:query/(?P<queryKey>[^/]+)/)?'
=> 'PhabricatorDashboardPanelListController', => 'PhabricatorDashboardPanelListController',

View file

@ -26,7 +26,14 @@ final class PhabricatorDashboardAddPanelController
return new Aphront404Response(); 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'); $v_panel = $request->getStr('panel');
$e_panel = true; $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()) $editor = id(new PhabricatorDashboardTransactionEditor())
->setActor($viewer) ->setActor($viewer)
->setContentSourceFromRequest($request) ->setContentSourceFromRequest($request)
@ -68,12 +82,13 @@ final class PhabricatorDashboardAddPanelController
->setContinueOnNoEffect(true) ->setContinueOnNoEffect(true)
->applyTransactions($dashboard, $xactions); ->applyTransactions($dashboard, $xactions);
return id(new AphrontRedirectResponse())->setURI($dashboard_uri); return id(new AphrontRedirectResponse())->setURI($redirect_uri);
} }
} }
$form = id(new AphrontFormView()) $form = id(new AphrontFormView())
->setUser($viewer) ->setUser($viewer)
->addHiddenInput('src', $request->getStr('src', 'edit'))
->appendRemarkupInstructions( ->appendRemarkupInstructions(
pht('Enter a panel monogram like `W123`.')) pht('Enter a panel monogram like `W123`.'))
->appendChild( ->appendChild(
@ -83,11 +98,23 @@ final class PhabricatorDashboardAddPanelController
->setValue($v_panel) ->setValue($v_panel)
->setError($e_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() return $this->newDialog()
->setTitle(pht('Add Panel')) ->setTitle(pht('Add Panel'))
->setErrors($errors) ->setErrors($errors)
->appendChild($form->buildLayoutView()) ->appendChild($form->buildLayoutView())
->addCancelButton($dashboard_uri) ->addCancelButton($redirect_uri)
->addSubmitButton(pht('Add Panel')); ->addSubmitButton(pht('Add Panel'));
} }

View file

@ -0,0 +1,54 @@
<?php
final class PhabricatorDashboardArrangeController
extends PhabricatorDashboardController {
private $id;
public function willProcessRequest(array $data) {
$this->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,
));
}
}

View file

@ -17,6 +17,7 @@ final class PhabricatorDashboardEditController
$dashboard = id(new PhabricatorDashboardQuery()) $dashboard = id(new PhabricatorDashboardQuery())
->setViewer($viewer) ->setViewer($viewer)
->withIDs(array($this->id)) ->withIDs(array($this->id))
->needPanels(true)
->requireCapabilities( ->requireCapabilities(
array( array(
PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_VIEW,
@ -56,19 +57,25 @@ final class PhabricatorDashboardEditController
} }
$v_name = $dashboard->getName(); $v_name = $dashboard->getName();
$v_layout_mode = $dashboard->getLayoutConfigObject()->getLayoutMode();
$e_name = true; $e_name = true;
$validation_exception = null; $validation_exception = null;
if ($request->isFormPost()) { if ($request->isFormPost()) {
$v_name = $request->getStr('name'); $v_name = $request->getStr('name');
$v_layout_mode = $request->getStr('layout_mode');
$xactions = array(); $xactions = array();
$type_name = PhabricatorDashboardTransaction::TYPE_NAME; $type_name = PhabricatorDashboardTransaction::TYPE_NAME;
$type_layout_mode = PhabricatorDashboardTransaction::TYPE_LAYOUT_MODE;
$xactions[] = id(new PhabricatorDashboardTransaction()) $xactions[] = id(new PhabricatorDashboardTransaction())
->setTransactionType($type_name) ->setTransactionType($type_name)
->setNewValue($v_name); ->setNewValue($v_name);
$xactions[] = id(new PhabricatorDashboardTransaction())
->setTransactionType($type_layout_mode)
->setNewValue($v_layout_mode);
try { try {
$editor = id(new PhabricatorDashboardTransactionEditor()) $editor = id(new PhabricatorDashboardTransactionEditor())
@ -77,8 +84,12 @@ final class PhabricatorDashboardEditController
->setContentSourceFromRequest($request) ->setContentSourceFromRequest($request)
->applyTransactions($dashboard, $xactions); ->applyTransactions($dashboard, $xactions);
return id(new AphrontRedirectResponse()) if ($is_new) {
->setURI($this->getApplicationURI('view/'.$dashboard->getID().'/')); $uri = $this->getApplicationURI('arrange/'.$dashboard->getID().'/');
} else {
$uri = $this->getApplicationURI('view/'.$dashboard->getID().'/');
}
return id(new AphrontRedirectResponse())->setURI($uri);
} catch (PhabricatorApplicationTransactionValidationException $ex) { } catch (PhabricatorApplicationTransactionValidationException $ex) {
$validation_exception = $ex; $validation_exception = $ex;
@ -86,6 +97,8 @@ final class PhabricatorDashboardEditController
} }
} }
$layout_mode_options =
PhabricatorDashboardLayoutConfig::getLayoutModeSelectOptions();
$form = id(new AphrontFormView()) $form = id(new AphrontFormView())
->setUser($viewer) ->setUser($viewer)
->appendChild( ->appendChild(
@ -94,12 +107,17 @@ final class PhabricatorDashboardEditController
->setName('name') ->setName('name')
->setValue($v_name) ->setValue($v_name)
->setError($e_name)) ->setError($e_name))
->appendChild(
id(new AphrontFormSelectControl())
->setLabel(pht('Layout Mode'))
->setName('layout_mode')
->setValue($v_layout_mode)
->setOptions($layout_mode_options))
->appendChild( ->appendChild(
id(new AphrontFormSubmitControl()) id(new AphrontFormSubmitControl())
->setValue($button) ->setValue($button)
->addCancelButton($cancel_uri)); ->addCancelButton($cancel_uri));
$box = id(new PHUIObjectBoxView()) $box = id(new PHUIObjectBoxView())
->setHeaderText($header) ->setHeaderText($header)
->setForm($form) ->setForm($form)

View file

@ -0,0 +1,86 @@
<?php
final class PhabricatorDashboardMovePanelController
extends PhabricatorDashboardController {
private $id;
public function willProcessRequest(array $data) {
$this->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('');
}
}

View file

@ -84,6 +84,14 @@ final class PhabricatorDashboardViewController
->setDisabled(!$can_edit) ->setDisabled(!$can_edit)
->setWorkflow(!$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( $actions->addAction(
id(new PhabricatorActionView()) id(new PhabricatorActionView())
->setName(pht('Add Panel')) ->setName(pht('Add Panel'))

View file

@ -11,6 +11,7 @@ final class PhabricatorDashboardTransactionEditor
$types[] = PhabricatorTransactions::TYPE_EDGE; $types[] = PhabricatorTransactions::TYPE_EDGE;
$types[] = PhabricatorDashboardTransaction::TYPE_NAME; $types[] = PhabricatorDashboardTransaction::TYPE_NAME;
$types[] = PhabricatorDashboardTransaction::TYPE_LAYOUT_MODE;
return $types; return $types;
} }
@ -24,6 +25,12 @@ final class PhabricatorDashboardTransactionEditor
return null; return null;
} }
return $object->getName(); 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); return parent::getCustomTransactionOldValue($object, $xaction);
@ -34,6 +41,7 @@ final class PhabricatorDashboardTransactionEditor
PhabricatorApplicationTransaction $xaction) { PhabricatorApplicationTransaction $xaction) {
switch ($xaction->getTransactionType()) { switch ($xaction->getTransactionType()) {
case PhabricatorDashboardTransaction::TYPE_NAME: case PhabricatorDashboardTransaction::TYPE_NAME:
case PhabricatorDashboardTransaction::TYPE_LAYOUT_MODE:
return $xaction->getNewValue(); return $xaction->getNewValue();
} }
return parent::getCustomTransactionNewValue($object, $xaction); return parent::getCustomTransactionNewValue($object, $xaction);
@ -46,6 +54,21 @@ final class PhabricatorDashboardTransactionEditor
case PhabricatorDashboardTransaction::TYPE_NAME: case PhabricatorDashboardTransaction::TYPE_NAME:
$object->setName($xaction->getNewValue()); $object->setName($xaction->getNewValue());
return; 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: case PhabricatorTransactions::TYPE_VIEW_POLICY:
$object->setViewPolicy($xaction->getNewValue()); $object->setViewPolicy($xaction->getNewValue());
return; return;
@ -65,6 +88,7 @@ final class PhabricatorDashboardTransactionEditor
switch ($xaction->getTransactionType()) { switch ($xaction->getTransactionType()) {
case PhabricatorDashboardTransaction::TYPE_NAME: case PhabricatorDashboardTransaction::TYPE_NAME:
case PhabricatorDashboardTransaction::TYPE_LAYOUT_MODE:
return; return;
case PhabricatorTransactions::TYPE_EDGE: case PhabricatorTransactions::TYPE_EDGE:
return; return;

View file

@ -79,6 +79,9 @@ final class PhabricatorDashboardPanelRenderingEngine extends Phobject {
)); ));
return id(new PHUIObjectBoxView()) return id(new PHUIObjectBoxView())
->addSigil('dashboard-panel')
->setMetadata(array(
'objectPHID' => $panel->getPHID()))
->setHeaderText($panel->getName()) ->setHeaderText($panel->getName())
->setID($panel_id) ->setID($panel_id)
->appendChild(pht('Loading...')); ->appendChild(pht('Loading...'));

View file

@ -4,6 +4,7 @@ final class PhabricatorDashboardRenderingEngine extends Phobject {
private $dashboard; private $dashboard;
private $viewer; private $viewer;
private $arrangeMode;
public function setViewer(PhabricatorUser $viewer) { public function setViewer(PhabricatorUser $viewer) {
$this->viewer = $viewer; $this->viewer = $viewer;
@ -15,20 +16,101 @@ final class PhabricatorDashboardRenderingEngine extends Phobject {
return $this; return $this;
} }
public function setArrangeMode($mode) {
$this->arrangeMode = $mode;
return $this;
}
public function renderDashboard() { public function renderDashboard() {
require_celerity_resource('phabricator-dashboard-css');
$dashboard = $this->dashboard; $dashboard = $this->dashboard;
$viewer = $this->viewer; $viewer = $this->viewer;
$result = array(); $layout_config = $dashboard->getLayoutConfigObject();
foreach ($dashboard->getPanels() as $panel) { $panel_grid_locations = $layout_config->getPanelLocations();
$result[] = id(new PhabricatorDashboardPanelRenderingEngine()) $panels = mpull($dashboard->getPanels(), null, 'getPHID');
->setViewer($viewer) $dashboard_id = celerity_generate_unique_node_id();
->setPanel($panel) $result = id(new AphrontMultiColumnView())
->setEnableAsyncRendering(true) ->setID($dashboard_id)
->renderPanel(); ->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; 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;
}
} }

View file

@ -0,0 +1,133 @@
<?php
final class PhabricatorDashboardLayoutConfig {
const MODE_FULL = 'layout-mode-full';
const MODE_HALF_AND_HALF = 'layout-mode-half-and-half';
const MODE_THIRD_AND_THIRDS = 'layout-mode-third-and-thirds';
const MODE_THIRDS_AND_THIRD = 'layout-mode-thirds-and-third';
private $layoutMode = self::MODE_FULL;
private $panelLocations = array();
public function setLayoutMode($mode) {
$this->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()
);
}
}

View file

@ -47,6 +47,9 @@ abstract class PhabricatorDashboardPanelType extends Phobject {
$content = $this->renderPanelContent($viewer, $panel); $content = $this->renderPanelContent($viewer, $panel);
return id(new PHUIObjectBoxView()) return id(new PHUIObjectBoxView())
->addSigil('dashboard-panel')
->setMetadata(array(
'objectPHID' => $panel->getPHID()))
->setHeaderText($panel->getName()) ->setHeaderText($panel->getName())
->appendChild($content); ->appendChild($content);
} }

View file

@ -9,6 +9,7 @@ final class PhabricatorDashboard extends PhabricatorDashboardDAO
protected $name; protected $name;
protected $viewPolicy; protected $viewPolicy;
protected $editPolicy; protected $editPolicy;
protected $layoutConfig = array();
private $panelPHIDs = self::ATTACHABLE; private $panelPHIDs = self::ATTACHABLE;
private $panels = self::ATTACHABLE; private $panels = self::ATTACHABLE;
@ -17,12 +18,16 @@ final class PhabricatorDashboard extends PhabricatorDashboardDAO
return id(new PhabricatorDashboard()) return id(new PhabricatorDashboard())
->setName('') ->setName('')
->setViewPolicy(PhabricatorPolicies::POLICY_USER) ->setViewPolicy(PhabricatorPolicies::POLICY_USER)
->setEditPolicy($actor->getPHID()); ->setEditPolicy($actor->getPHID())
->attachPanels(array())
->attachPanelPHIDs(array());
} }
public function getConfiguration() { public function getConfiguration() {
return array( return array(
self::CONFIG_AUX_PHID => true, self::CONFIG_AUX_PHID => true,
self::CONFIG_SERIALIZATION => array(
'layoutConfig' => self::SERIALIZATION_JSON),
) + parent::getConfiguration(); ) + parent::getConfiguration();
} }
@ -31,6 +36,17 @@ final class PhabricatorDashboard extends PhabricatorDashboardDAO
PhabricatorDashboardPHIDTypeDashboard::TYPECONST); 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) { public function attachPanelPHIDs(array $phids) {
$this->panelPHIDs = $phids; $this->panelPHIDs = $phids;
return $this; return $this;

View file

@ -4,6 +4,7 @@ final class PhabricatorDashboardTransaction
extends PhabricatorApplicationTransaction { extends PhabricatorApplicationTransaction {
const TYPE_NAME = 'dashboard:name'; const TYPE_NAME = 'dashboard:name';
const TYPE_LAYOUT_MODE = 'dashboard:layoutmode';
public function getApplicationName() { public function getApplicationName() {
return 'dashboard'; return 'dashboard';
@ -86,4 +87,15 @@ final class PhabricatorDashboardTransaction
return parent::getColor(); 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();
}
} }

View file

@ -6,14 +6,32 @@ final class AphrontMultiColumnView extends AphrontView {
const GUTTER_MEDIUM = 'mmr'; const GUTTER_MEDIUM = 'mmr';
const GUTTER_LARGE = 'mlr'; const GUTTER_LARGE = 'mlr';
private $id;
private $columns = array(); private $columns = array();
private $fluidLayout = false; private $fluidLayout = false;
private $fluidishLayout = false; private $fluidishLayout = false;
private $gutter; private $gutter;
private $border; private $border;
public function addColumn($column) { public function setID($id) {
$this->columns[] = $column; $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; return $this;
} }
@ -55,30 +73,34 @@ final class AphrontMultiColumnView extends AphrontView {
$classes[] = 'aphront-multi-column-'.count($this->columns).'-up'; $classes[] = 'aphront-multi-column-'.count($this->columns).'-up';
$columns = array(); $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; $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)) { if (++$i === count($this->columns)) {
$column_class[] = 'aphront-multi-column-column-last'; $column_class[] = 'aphront-multi-column-column-last';
$outer_class[] = 'aphront-multi-colum-column-outer-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', 'div',
array( array(
'class' => implode(' ', $column_class) 'class' => implode(' ', $column_class),
), 'sigil' => $column_sigil,
'meta' => $column_metadata),
$column); $column);
$columns[] = phutil_tag( $columns[] = phutil_tag(
'div', 'div',
array( array(
'class' => implode(' ', $outer_class) 'class' => implode(' ', $outer_class)),
),
$column_inner); $column_inner);
} }
@ -120,7 +142,8 @@ final class AphrontMultiColumnView extends AphrontView {
return phutil_tag( return phutil_tag(
'div', 'div',
array( array(
'class' => 'aphront-multi-column-view' 'class' => 'aphront-multi-column-view',
'id' => $this->getID(),
), ),
$board); $board);
} }

View file

@ -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;
}

View file

@ -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);
}
});