mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-22 14:52: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:
parent
9d5f9d5c2c
commit
6300955661
19 changed files with 719 additions and 45 deletions
|
@ -52,6 +52,7 @@ return array(
|
|||
'rsrc/css/application/conpherence/widget-pane.css' => 'bf275a6c',
|
||||
'rsrc/css/application/contentsource/content-source-view.css' => '4b8b05d4',
|
||||
'rsrc/css/application/countdown/timer.css' => '86b7b0a0',
|
||||
'rsrc/css/application/dashboard/dashboard.css' => '5b532b7b',
|
||||
'rsrc/css/application/diff/inline-comment-summary.css' => '8cfd34e8',
|
||||
'rsrc/css/application/differential/add-comment.css' => 'c478bcaa',
|
||||
'rsrc/css/application/differential/changeset-view.css' => '1570a1ff',
|
||||
|
@ -358,6 +359,7 @@ return array(
|
|||
'rsrc/js/application/conpherence/behavior-widget-pane.js' => '40b1ff90',
|
||||
'rsrc/js/application/countdown/timer.js' => '889c96f3',
|
||||
'rsrc/js/application/dashboard/behavior-dashboard-async-panel.js' => '4398eabb',
|
||||
'rsrc/js/application/dashboard/behavior-dashboard-move-panels.js' => 'aa3f313b',
|
||||
'rsrc/js/application/differential/DifferentialInlineCommentEditor.js' => 'f2441746',
|
||||
'rsrc/js/application/differential/behavior-add-reviewers-and-ccs.js' => '533a187b',
|
||||
'rsrc/js/application/differential/behavior-comment-jump.js' => '71755c79',
|
||||
|
@ -552,6 +554,7 @@ return array(
|
|||
'javelin-behavior-countdown-timer' => '889c96f3',
|
||||
'javelin-behavior-dark-console' => 'e9fdb5e5',
|
||||
'javelin-behavior-dashboard-async-panel' => '4398eabb',
|
||||
'javelin-behavior-dashboard-move-panels' => 'aa3f313b',
|
||||
'javelin-behavior-device' => '03d6ed07',
|
||||
'javelin-behavior-differential-add-reviewers-and-ccs' => '533a187b',
|
||||
'javelin-behavior-differential-comment-jump' => '71755c79',
|
||||
|
@ -698,6 +701,7 @@ return array(
|
|||
'phabricator-core-css' => '40151074',
|
||||
'phabricator-countdown-css' => '86b7b0a0',
|
||||
'phabricator-crumbs-view-css' => '6a23399c',
|
||||
'phabricator-dashboard-css' => '5b532b7b',
|
||||
'phabricator-drag-and-drop-file-upload' => 'ae6abfba',
|
||||
'phabricator-draggable-list' => '1681c4d4',
|
||||
'phabricator-fatal-config-template-css' => '25d446d6',
|
||||
|
@ -1266,6 +1270,18 @@ return array(
|
|||
2 => 'javelin-util',
|
||||
3 => 'phabricator-shaped-request',
|
||||
),
|
||||
'7319e029' =>
|
||||
array(
|
||||
0 => 'javelin-behavior',
|
||||
1 => 'javelin-dom',
|
||||
),
|
||||
'62e18640' =>
|
||||
array(
|
||||
0 => 'javelin-install',
|
||||
1 => 'javelin-util',
|
||||
2 => 'javelin-dom',
|
||||
3 => 'javelin-typeahead-normalizer',
|
||||
),
|
||||
'6453c869' =>
|
||||
array(
|
||||
0 => 'javelin-install',
|
||||
|
@ -1313,18 +1329,6 @@ return array(
|
|||
1 => 'javelin-stratcom',
|
||||
2 => 'javelin-dom',
|
||||
),
|
||||
'7319e029' =>
|
||||
array(
|
||||
0 => 'javelin-behavior',
|
||||
1 => 'javelin-dom',
|
||||
),
|
||||
'62e18640' =>
|
||||
array(
|
||||
0 => 'javelin-install',
|
||||
1 => 'javelin-util',
|
||||
2 => 'javelin-dom',
|
||||
3 => 'javelin-typeahead-normalizer',
|
||||
),
|
||||
'76f4ebed' =>
|
||||
array(
|
||||
0 => 'javelin-install',
|
||||
|
@ -1594,6 +1598,15 @@ return array(
|
|||
1 => 'javelin-stratcom',
|
||||
2 => 'javelin-dom',
|
||||
),
|
||||
'aa3f313b' =>
|
||||
array(
|
||||
0 => 'javelin-behavior',
|
||||
1 => 'javelin-dom',
|
||||
2 => 'javelin-util',
|
||||
3 => 'javelin-stratcom',
|
||||
4 => 'javelin-workflow',
|
||||
5 => 'phabricator-draggable-list',
|
||||
),
|
||||
'ad7a69ca' =>
|
||||
array(
|
||||
0 => 'javelin-install',
|
||||
|
|
|
@ -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 = '[]';
|
|
@ -1456,10 +1456,13 @@ phutil_register_library_map(array(
|
|||
'PhabricatorDaemonTaskGarbageCollector' => 'applications/daemon/garbagecollector/PhabricatorDaemonTaskGarbageCollector.php',
|
||||
'PhabricatorDashboard' => 'applications/dashboard/storage/PhabricatorDashboard.php',
|
||||
'PhabricatorDashboardAddPanelController' => 'applications/dashboard/controller/PhabricatorDashboardAddPanelController.php',
|
||||
'PhabricatorDashboardArrangeController' => 'applications/dashboard/controller/PhabricatorDashboardArrangeController.php',
|
||||
'PhabricatorDashboardController' => 'applications/dashboard/controller/PhabricatorDashboardController.php',
|
||||
'PhabricatorDashboardDAO' => 'applications/dashboard/storage/PhabricatorDashboardDAO.php',
|
||||
'PhabricatorDashboardEditController' => 'applications/dashboard/controller/PhabricatorDashboardEditController.php',
|
||||
'PhabricatorDashboardLayoutConfig' => 'applications/dashboard/layoutconfig/PhabricatorDashboardLayoutConfig.php',
|
||||
'PhabricatorDashboardListController' => 'applications/dashboard/controller/PhabricatorDashboardListController.php',
|
||||
'PhabricatorDashboardMovePanelController' => 'applications/dashboard/controller/PhabricatorDashboardMovePanelController.php',
|
||||
'PhabricatorDashboardPHIDTypeDashboard' => 'applications/dashboard/phid/PhabricatorDashboardPHIDTypeDashboard.php',
|
||||
'PhabricatorDashboardPHIDTypePanel' => 'applications/dashboard/phid/PhabricatorDashboardPHIDTypePanel.php',
|
||||
'PhabricatorDashboardPanel' => 'applications/dashboard/storage/PhabricatorDashboardPanel.php',
|
||||
|
@ -4235,10 +4238,12 @@ phutil_register_library_map(array(
|
|||
1 => 'PhabricatorPolicyInterface',
|
||||
),
|
||||
'PhabricatorDashboardAddPanelController' => 'PhabricatorDashboardController',
|
||||
'PhabricatorDashboardArrangeController' => 'PhabricatorDashboardController',
|
||||
'PhabricatorDashboardController' => 'PhabricatorController',
|
||||
'PhabricatorDashboardDAO' => 'PhabricatorLiskDAO',
|
||||
'PhabricatorDashboardEditController' => 'PhabricatorDashboardController',
|
||||
'PhabricatorDashboardListController' => 'PhabricatorDashboardController',
|
||||
'PhabricatorDashboardMovePanelController' => 'PhabricatorDashboardController',
|
||||
'PhabricatorDashboardPHIDTypeDashboard' => 'PhabricatorPHIDType',
|
||||
'PhabricatorDashboardPHIDTypePanel' => 'PhabricatorPHIDType',
|
||||
'PhabricatorDashboardPanel' =>
|
||||
|
|
|
@ -21,10 +21,11 @@ final class PhabricatorApplicationDashboard extends PhabricatorApplication {
|
|||
'(?:query/(?P<queryKey>[^/]+)/)?'
|
||||
=> 'PhabricatorDashboardListController',
|
||||
'view/(?P<id>\d+)/' => 'PhabricatorDashboardViewController',
|
||||
'arrange/(?P<id>\d+)/' => 'PhabricatorDashboardArrangeController',
|
||||
'create/' => 'PhabricatorDashboardEditController',
|
||||
'edit/(?:(?P<id>\d+)/)?' => 'PhabricatorDashboardEditController',
|
||||
'addpanel/(?P<id>\d+)/' => 'PhabricatorDashboardAddPanelController',
|
||||
|
||||
'movepanel/(?P<id>\d+)/' => 'PhabricatorDashboardMovePanelController',
|
||||
'panel/' => array(
|
||||
'(?:query/(?P<queryKey>[^/]+)/)?'
|
||||
=> 'PhabricatorDashboardPanelListController',
|
||||
|
|
|
@ -26,7 +26,14 @@ final class PhabricatorDashboardAddPanelController
|
|||
return new Aphront404Response();
|
||||
}
|
||||
|
||||
$dashboard_uri = $this->getApplicationURI('view/'.$dashboard->getID().'/');
|
||||
if ($request->getStr('src', 'edit') == 'edit') {
|
||||
$redirect_uri = $this->getApplicationURI(
|
||||
'view/'.$dashboard->getID().'/');
|
||||
} else {
|
||||
$redirect_uri = $this->getApplicationURI(
|
||||
'arrange/'.$dashboard->getID().'/');
|
||||
}
|
||||
$layout_config = $dashboard->getLayoutConfigObject();
|
||||
|
||||
$v_panel = $request->getStr('panel');
|
||||
$e_panel = true;
|
||||
|
@ -61,6 +68,13 @@ final class PhabricatorDashboardAddPanelController
|
|||
),
|
||||
));
|
||||
|
||||
if ($layout_config->isMultiColumnLayout()) {
|
||||
$layout_config->setPanelLocation(
|
||||
$request->getInt('column'),
|
||||
$panel->getPHID());
|
||||
$dashboard->setLayoutConfigFromObject($layout_config);
|
||||
}
|
||||
|
||||
$editor = id(new PhabricatorDashboardTransactionEditor())
|
||||
->setActor($viewer)
|
||||
->setContentSourceFromRequest($request)
|
||||
|
@ -68,12 +82,13 @@ final class PhabricatorDashboardAddPanelController
|
|||
->setContinueOnNoEffect(true)
|
||||
->applyTransactions($dashboard, $xactions);
|
||||
|
||||
return id(new AphrontRedirectResponse())->setURI($dashboard_uri);
|
||||
return id(new AphrontRedirectResponse())->setURI($redirect_uri);
|
||||
}
|
||||
}
|
||||
|
||||
$form = id(new AphrontFormView())
|
||||
->setUser($viewer)
|
||||
->addHiddenInput('src', $request->getStr('src', 'edit'))
|
||||
->appendRemarkupInstructions(
|
||||
pht('Enter a panel monogram like `W123`.'))
|
||||
->appendChild(
|
||||
|
@ -83,11 +98,23 @@ final class PhabricatorDashboardAddPanelController
|
|||
->setValue($v_panel)
|
||||
->setError($e_panel));
|
||||
|
||||
if ($layout_config->isMultiColumnLayout()) {
|
||||
$form
|
||||
->appendRemarkupInstructions(
|
||||
pht('Choose which column the panel should reside in.'))
|
||||
->appendChild(
|
||||
id(new AphrontFormSelectControl())
|
||||
->setName('column')
|
||||
->setLabel(pht('Column'))
|
||||
->setOptions($layout_config->getColumnSelectOptions())
|
||||
->setValue($request->getInt('column')));
|
||||
}
|
||||
|
||||
return $this->newDialog()
|
||||
->setTitle(pht('Add Panel'))
|
||||
->setErrors($errors)
|
||||
->appendChild($form->buildLayoutView())
|
||||
->addCancelButton($dashboard_uri)
|
||||
->addCancelButton($redirect_uri)
|
||||
->addSubmitButton(pht('Add Panel'));
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
|
@ -17,6 +17,7 @@ final class PhabricatorDashboardEditController
|
|||
$dashboard = id(new PhabricatorDashboardQuery())
|
||||
->setViewer($viewer)
|
||||
->withIDs(array($this->id))
|
||||
->needPanels(true)
|
||||
->requireCapabilities(
|
||||
array(
|
||||
PhabricatorPolicyCapability::CAN_VIEW,
|
||||
|
@ -56,19 +57,25 @@ final class PhabricatorDashboardEditController
|
|||
}
|
||||
|
||||
$v_name = $dashboard->getName();
|
||||
$v_layout_mode = $dashboard->getLayoutConfigObject()->getLayoutMode();
|
||||
$e_name = true;
|
||||
|
||||
$validation_exception = null;
|
||||
if ($request->isFormPost()) {
|
||||
$v_name = $request->getStr('name');
|
||||
$v_layout_mode = $request->getStr('layout_mode');
|
||||
|
||||
$xactions = array();
|
||||
|
||||
$type_name = PhabricatorDashboardTransaction::TYPE_NAME;
|
||||
$type_layout_mode = PhabricatorDashboardTransaction::TYPE_LAYOUT_MODE;
|
||||
|
||||
$xactions[] = id(new PhabricatorDashboardTransaction())
|
||||
->setTransactionType($type_name)
|
||||
->setNewValue($v_name);
|
||||
$xactions[] = id(new PhabricatorDashboardTransaction())
|
||||
->setTransactionType($type_layout_mode)
|
||||
->setNewValue($v_layout_mode);
|
||||
|
||||
try {
|
||||
$editor = id(new PhabricatorDashboardTransactionEditor())
|
||||
|
@ -77,8 +84,12 @@ final class PhabricatorDashboardEditController
|
|||
->setContentSourceFromRequest($request)
|
||||
->applyTransactions($dashboard, $xactions);
|
||||
|
||||
return id(new AphrontRedirectResponse())
|
||||
->setURI($this->getApplicationURI('view/'.$dashboard->getID().'/'));
|
||||
if ($is_new) {
|
||||
$uri = $this->getApplicationURI('arrange/'.$dashboard->getID().'/');
|
||||
} else {
|
||||
$uri = $this->getApplicationURI('view/'.$dashboard->getID().'/');
|
||||
}
|
||||
return id(new AphrontRedirectResponse())->setURI($uri);
|
||||
} catch (PhabricatorApplicationTransactionValidationException $ex) {
|
||||
$validation_exception = $ex;
|
||||
|
||||
|
@ -86,6 +97,8 @@ final class PhabricatorDashboardEditController
|
|||
}
|
||||
}
|
||||
|
||||
$layout_mode_options =
|
||||
PhabricatorDashboardLayoutConfig::getLayoutModeSelectOptions();
|
||||
$form = id(new AphrontFormView())
|
||||
->setUser($viewer)
|
||||
->appendChild(
|
||||
|
@ -94,12 +107,17 @@ final class PhabricatorDashboardEditController
|
|||
->setName('name')
|
||||
->setValue($v_name)
|
||||
->setError($e_name))
|
||||
->appendChild(
|
||||
id(new AphrontFormSelectControl())
|
||||
->setLabel(pht('Layout Mode'))
|
||||
->setName('layout_mode')
|
||||
->setValue($v_layout_mode)
|
||||
->setOptions($layout_mode_options))
|
||||
->appendChild(
|
||||
id(new AphrontFormSubmitControl())
|
||||
->setValue($button)
|
||||
->addCancelButton($cancel_uri));
|
||||
|
||||
|
||||
$box = id(new PHUIObjectBoxView())
|
||||
->setHeaderText($header)
|
||||
->setForm($form)
|
||||
|
|
|
@ -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('');
|
||||
}
|
||||
|
||||
}
|
|
@ -84,6 +84,14 @@ final class PhabricatorDashboardViewController
|
|||
->setDisabled(!$can_edit)
|
||||
->setWorkflow(!$can_edit));
|
||||
|
||||
$actions->addAction(
|
||||
id(new PhabricatorActionView())
|
||||
->setName(pht('Arrange Dashboard'))
|
||||
->setIcon('fa-arrows')
|
||||
->setHref($this->getApplicationURI("arrange/{$id}/"))
|
||||
->setDisabled(!$can_edit)
|
||||
->setWorkflow(!$can_edit));
|
||||
|
||||
$actions->addAction(
|
||||
id(new PhabricatorActionView())
|
||||
->setName(pht('Add Panel'))
|
||||
|
|
|
@ -11,6 +11,7 @@ final class PhabricatorDashboardTransactionEditor
|
|||
$types[] = PhabricatorTransactions::TYPE_EDGE;
|
||||
|
||||
$types[] = PhabricatorDashboardTransaction::TYPE_NAME;
|
||||
$types[] = PhabricatorDashboardTransaction::TYPE_LAYOUT_MODE;
|
||||
|
||||
return $types;
|
||||
}
|
||||
|
@ -24,6 +25,12 @@ final class PhabricatorDashboardTransactionEditor
|
|||
return null;
|
||||
}
|
||||
return $object->getName();
|
||||
case PhabricatorDashboardTransaction::TYPE_LAYOUT_MODE:
|
||||
if ($this->getIsNewObject()) {
|
||||
return null;
|
||||
}
|
||||
$layout_config = $object->getLayoutConfigObject();
|
||||
return $layout_config->getLayoutMode();
|
||||
}
|
||||
|
||||
return parent::getCustomTransactionOldValue($object, $xaction);
|
||||
|
@ -34,6 +41,7 @@ final class PhabricatorDashboardTransactionEditor
|
|||
PhabricatorApplicationTransaction $xaction) {
|
||||
switch ($xaction->getTransactionType()) {
|
||||
case PhabricatorDashboardTransaction::TYPE_NAME:
|
||||
case PhabricatorDashboardTransaction::TYPE_LAYOUT_MODE:
|
||||
return $xaction->getNewValue();
|
||||
}
|
||||
return parent::getCustomTransactionNewValue($object, $xaction);
|
||||
|
@ -46,6 +54,21 @@ final class PhabricatorDashboardTransactionEditor
|
|||
case PhabricatorDashboardTransaction::TYPE_NAME:
|
||||
$object->setName($xaction->getNewValue());
|
||||
return;
|
||||
case PhabricatorDashboardTransaction::TYPE_LAYOUT_MODE:
|
||||
$old_layout = $object->getLayoutConfigObject();
|
||||
$new_layout = clone $old_layout;
|
||||
$new_layout->setLayoutMode($xaction->getNewValue());
|
||||
if ($old_layout->isMultiColumnLayout() !=
|
||||
$new_layout->isMultiColumnLayout()) {
|
||||
$panel_phids = $object->getPanelPHIDs();
|
||||
$new_locations = $new_layout->getDefaultPanelLocations();
|
||||
foreach ($panel_phids as $panel_phid) {
|
||||
$new_locations[0][] = $panel_phid;
|
||||
}
|
||||
$new_layout->setPanelLocations($new_locations);
|
||||
}
|
||||
$object->setLayoutConfigFromObject($new_layout);
|
||||
return;
|
||||
case PhabricatorTransactions::TYPE_VIEW_POLICY:
|
||||
$object->setViewPolicy($xaction->getNewValue());
|
||||
return;
|
||||
|
@ -65,6 +88,7 @@ final class PhabricatorDashboardTransactionEditor
|
|||
|
||||
switch ($xaction->getTransactionType()) {
|
||||
case PhabricatorDashboardTransaction::TYPE_NAME:
|
||||
case PhabricatorDashboardTransaction::TYPE_LAYOUT_MODE:
|
||||
return;
|
||||
case PhabricatorTransactions::TYPE_EDGE:
|
||||
return;
|
||||
|
|
|
@ -79,6 +79,9 @@ final class PhabricatorDashboardPanelRenderingEngine extends Phobject {
|
|||
));
|
||||
|
||||
return id(new PHUIObjectBoxView())
|
||||
->addSigil('dashboard-panel')
|
||||
->setMetadata(array(
|
||||
'objectPHID' => $panel->getPHID()))
|
||||
->setHeaderText($panel->getName())
|
||||
->setID($panel_id)
|
||||
->appendChild(pht('Loading...'));
|
||||
|
|
|
@ -4,6 +4,7 @@ final class PhabricatorDashboardRenderingEngine extends Phobject {
|
|||
|
||||
private $dashboard;
|
||||
private $viewer;
|
||||
private $arrangeMode;
|
||||
|
||||
public function setViewer(PhabricatorUser $viewer) {
|
||||
$this->viewer = $viewer;
|
||||
|
@ -15,20 +16,101 @@ final class PhabricatorDashboardRenderingEngine extends Phobject {
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function setArrangeMode($mode) {
|
||||
$this->arrangeMode = $mode;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function renderDashboard() {
|
||||
require_celerity_resource('phabricator-dashboard-css');
|
||||
$dashboard = $this->dashboard;
|
||||
$viewer = $this->viewer;
|
||||
|
||||
$result = array();
|
||||
foreach ($dashboard->getPanels() as $panel) {
|
||||
$result[] = id(new PhabricatorDashboardPanelRenderingEngine())
|
||||
$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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -47,6 +47,9 @@ abstract class PhabricatorDashboardPanelType extends Phobject {
|
|||
$content = $this->renderPanelContent($viewer, $panel);
|
||||
|
||||
return id(new PHUIObjectBoxView())
|
||||
->addSigil('dashboard-panel')
|
||||
->setMetadata(array(
|
||||
'objectPHID' => $panel->getPHID()))
|
||||
->setHeaderText($panel->getName())
|
||||
->appendChild($content);
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ final class PhabricatorDashboard extends PhabricatorDashboardDAO
|
|||
protected $name;
|
||||
protected $viewPolicy;
|
||||
protected $editPolicy;
|
||||
protected $layoutConfig = array();
|
||||
|
||||
private $panelPHIDs = self::ATTACHABLE;
|
||||
private $panels = self::ATTACHABLE;
|
||||
|
@ -17,12 +18,16 @@ final class PhabricatorDashboard extends PhabricatorDashboardDAO
|
|||
return id(new PhabricatorDashboard())
|
||||
->setName('')
|
||||
->setViewPolicy(PhabricatorPolicies::POLICY_USER)
|
||||
->setEditPolicy($actor->getPHID());
|
||||
->setEditPolicy($actor->getPHID())
|
||||
->attachPanels(array())
|
||||
->attachPanelPHIDs(array());
|
||||
}
|
||||
|
||||
public function getConfiguration() {
|
||||
return array(
|
||||
self::CONFIG_AUX_PHID => true,
|
||||
self::CONFIG_SERIALIZATION => array(
|
||||
'layoutConfig' => self::SERIALIZATION_JSON),
|
||||
) + parent::getConfiguration();
|
||||
}
|
||||
|
||||
|
@ -31,6 +36,17 @@ final class PhabricatorDashboard extends PhabricatorDashboardDAO
|
|||
PhabricatorDashboardPHIDTypeDashboard::TYPECONST);
|
||||
}
|
||||
|
||||
public function getLayoutConfigObject() {
|
||||
return PhabricatorDashboardLayoutConfig::newFromDictionary(
|
||||
$this->getLayoutConfig());
|
||||
}
|
||||
|
||||
public function setLayoutConfigFromObject(
|
||||
PhabricatorDashboardLayoutConfig $object) {
|
||||
$this->setLayoutConfig($object->toDictionary());
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function attachPanelPHIDs(array $phids) {
|
||||
$this->panelPHIDs = $phids;
|
||||
return $this;
|
||||
|
|
|
@ -4,6 +4,7 @@ final class PhabricatorDashboardTransaction
|
|||
extends PhabricatorApplicationTransaction {
|
||||
|
||||
const TYPE_NAME = 'dashboard:name';
|
||||
const TYPE_LAYOUT_MODE = 'dashboard:layoutmode';
|
||||
|
||||
public function getApplicationName() {
|
||||
return 'dashboard';
|
||||
|
@ -86,4 +87,15 @@ final class PhabricatorDashboardTransaction
|
|||
|
||||
return parent::getColor();
|
||||
}
|
||||
|
||||
public function shouldHide() {
|
||||
$old = $this->getOldValue();
|
||||
$new = $this->getNewValue();
|
||||
|
||||
switch ($this->getTransactionType()) {
|
||||
case self::TYPE_LAYOUT_MODE:
|
||||
return true;
|
||||
}
|
||||
return parent::shouldHide();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,14 +6,32 @@ final class AphrontMultiColumnView extends AphrontView {
|
|||
const GUTTER_MEDIUM = 'mmr';
|
||||
const GUTTER_LARGE = 'mlr';
|
||||
|
||||
private $id;
|
||||
private $columns = array();
|
||||
private $fluidLayout = false;
|
||||
private $fluidishLayout = false;
|
||||
private $gutter;
|
||||
private $border;
|
||||
|
||||
public function addColumn($column) {
|
||||
$this->columns[] = $column;
|
||||
public function setID($id) {
|
||||
$this->id = $id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getID() {
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function addColumn(
|
||||
$column,
|
||||
$class = null,
|
||||
$sigil = null,
|
||||
$metadata = null) {
|
||||
$this->columns[] = array(
|
||||
'column' => $column,
|
||||
'class' => $class,
|
||||
'sigil' => $sigil,
|
||||
'metadata' => $metadata);
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
@ -55,30 +73,34 @@ final class AphrontMultiColumnView extends AphrontView {
|
|||
$classes[] = 'aphront-multi-column-'.count($this->columns).'-up';
|
||||
|
||||
$columns = array();
|
||||
$column_class = array();
|
||||
$column_class[] = 'aphront-multi-column-column';
|
||||
$outer_class = array();
|
||||
$outer_class[] = 'aphront-multi-column-column-outer';
|
||||
$i = 0;
|
||||
foreach ($this->columns as $column_data) {
|
||||
$column_class = array('aphront-multi-column-column');
|
||||
if ($this->gutter) {
|
||||
$column_class[] = $this->gutter;
|
||||
}
|
||||
$i = 0;
|
||||
foreach ($this->columns as $column) {
|
||||
$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)
|
||||
),
|
||||
'class' => implode(' ', $column_class),
|
||||
'sigil' => $column_sigil,
|
||||
'meta' => $column_metadata),
|
||||
$column);
|
||||
$columns[] = phutil_tag(
|
||||
'div',
|
||||
array(
|
||||
'class' => implode(' ', $outer_class)
|
||||
),
|
||||
'class' => implode(' ', $outer_class)),
|
||||
$column_inner);
|
||||
}
|
||||
|
||||
|
@ -120,7 +142,8 @@ final class AphrontMultiColumnView extends AphrontView {
|
|||
return phutil_tag(
|
||||
'div',
|
||||
array(
|
||||
'class' => 'aphront-multi-column-view'
|
||||
'class' => 'aphront-multi-column-view',
|
||||
'id' => $this->getID(),
|
||||
),
|
||||
$board);
|
||||
}
|
||||
|
|
59
webroot/rsrc/css/application/dashboard/dashboard.css
Normal file
59
webroot/rsrc/css/application/dashboard/dashboard.css
Normal 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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
});
|
Loading…
Reference in a new issue