diff --git a/resources/celerity/map.php b/resources/celerity/map.php --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,7 +9,7 @@ 'names' => array( 'conpherence.pkg.css' => '3c8a0668', 'conpherence.pkg.js' => '020aebcf', - 'core.pkg.css' => '2d4810eb', + 'core.pkg.css' => '671b9fae', 'core.pkg.js' => 'f9e9d770', 'differential.pkg.css' => '8d8360fb', 'differential.pkg.js' => '67e02996', @@ -134,7 +134,7 @@ 'rsrc/css/phui/object-item/phui-oi-flush-ui.css' => '490e2e2e', 'rsrc/css/phui/object-item/phui-oi-list-view.css' => 'f14f2422', 'rsrc/css/phui/object-item/phui-oi-simple-ui.css' => '6a30fa46', - 'rsrc/css/phui/phui-action-list.css' => 'c4972757', + 'rsrc/css/phui/phui-action-list.css' => 'c34af376', 'rsrc/css/phui/phui-action-panel.css' => '6c386cbf', 'rsrc/css/phui/phui-badge.css' => '666e25ad', 'rsrc/css/phui/phui-basic-nav-view.css' => '56ebd66d', @@ -757,7 +757,7 @@ 'path-typeahead' => 'ad486db3', 'people-picture-menu-item-css' => 'fe8e07cf', 'people-profile-css' => '2ea2daa1', - 'phabricator-action-list-view-css' => 'c4972757', + 'phabricator-action-list-view-css' => 'c34af376', 'phabricator-busy' => '5202e831', 'phabricator-chatlog-css' => 'abdc76ee', 'phabricator-content-source-view-css' => 'cdf0d579', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -2942,6 +2942,7 @@ 'PhabricatorDashboardPanelRenderingEngine' => 'applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php', 'PhabricatorDashboardPanelSearchEngine' => 'applications/dashboard/query/PhabricatorDashboardPanelSearchEngine.php', 'PhabricatorDashboardPanelStatusTransaction' => 'applications/dashboard/xaction/panel/PhabricatorDashboardPanelStatusTransaction.php', + 'PhabricatorDashboardPanelTabsController' => 'applications/dashboard/controller/panel/PhabricatorDashboardPanelTabsController.php', 'PhabricatorDashboardPanelTransaction' => 'applications/dashboard/storage/PhabricatorDashboardPanelTransaction.php', 'PhabricatorDashboardPanelTransactionEditor' => 'applications/dashboard/editor/PhabricatorDashboardPanelTransactionEditor.php', 'PhabricatorDashboardPanelTransactionQuery' => 'applications/dashboard/query/PhabricatorDashboardPanelTransactionQuery.php', @@ -2984,6 +2985,7 @@ 'PhabricatorDashboardRenderingEngine' => 'applications/dashboard/engine/PhabricatorDashboardRenderingEngine.php', 'PhabricatorDashboardSchemaSpec' => 'applications/dashboard/storage/PhabricatorDashboardSchemaSpec.php', 'PhabricatorDashboardSearchEngine' => 'applications/dashboard/query/PhabricatorDashboardSearchEngine.php', + 'PhabricatorDashboardTabsPanelTabsTransaction' => 'applications/dashboard/xaction/panel/PhabricatorDashboardTabsPanelTabsTransaction.php', 'PhabricatorDashboardTabsPanelType' => 'applications/dashboard/paneltype/PhabricatorDashboardTabsPanelType.php', 'PhabricatorDashboardTextPanelTextTransaction' => 'applications/dashboard/xaction/panel/PhabricatorDashboardTextPanelTextTransaction.php', 'PhabricatorDashboardTextPanelType' => 'applications/dashboard/paneltype/PhabricatorDashboardTextPanelType.php', @@ -8919,6 +8921,7 @@ 'PhabricatorDashboardPanelRenderingEngine' => 'Phobject', 'PhabricatorDashboardPanelSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorDashboardPanelStatusTransaction' => 'PhabricatorDashboardPanelTransactionType', + 'PhabricatorDashboardPanelTabsController' => 'PhabricatorDashboardController', 'PhabricatorDashboardPanelTransaction' => 'PhabricatorModularTransaction', 'PhabricatorDashboardPanelTransactionEditor' => 'PhabricatorApplicationTransactionEditor', 'PhabricatorDashboardPanelTransactionQuery' => 'PhabricatorApplicationTransactionQuery', @@ -8966,6 +8969,7 @@ 'PhabricatorDashboardRenderingEngine' => 'Phobject', 'PhabricatorDashboardSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorDashboardSearchEngine' => 'PhabricatorApplicationSearchEngine', + 'PhabricatorDashboardTabsPanelTabsTransaction' => 'PhabricatorDashboardPanelPropertyTransaction', 'PhabricatorDashboardTabsPanelType' => 'PhabricatorDashboardPanelType', 'PhabricatorDashboardTextPanelTextTransaction' => 'PhabricatorDashboardPanelPropertyTransaction', 'PhabricatorDashboardTextPanelType' => 'PhabricatorDashboardPanelType', diff --git a/src/applications/dashboard/application/PhabricatorDashboardApplication.php b/src/applications/dashboard/application/PhabricatorDashboardApplication.php --- a/src/applications/dashboard/application/PhabricatorDashboardApplication.php +++ b/src/applications/dashboard/application/PhabricatorDashboardApplication.php @@ -62,6 +62,8 @@ 'render/(?P\d+)/' => 'PhabricatorDashboardPanelRenderController', 'archive/(?P\d+)/' => 'PhabricatorDashboardPanelArchiveController', + 'tabs/(?P\d+)/(?Padd|move|remove|rename)/' + => 'PhabricatorDashboardPanelTabsController', ), ), '/portal/' => array( diff --git a/src/applications/dashboard/controller/panel/PhabricatorDashboardPanelTabsController.php b/src/applications/dashboard/controller/panel/PhabricatorDashboardPanelTabsController.php new file mode 100644 --- /dev/null +++ b/src/applications/dashboard/controller/panel/PhabricatorDashboardPanelTabsController.php @@ -0,0 +1,295 @@ +getViewer(); + + $panel = id(new PhabricatorDashboardPanelQuery()) + ->setViewer($viewer) + ->withIDs(array($request->getURIData('id'))) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$panel) { + return new Aphront404Response(); + } + + $tabs_type = id(new PhabricatorDashboardTabsPanelType()) + ->getPanelTypeKey(); + + // This controller may only be used to edit tab panels. + $panel_type = $panel->getPanelType(); + if ($panel_type !== $tabs_type) { + return new Aphront404Response(); + } + + $op = $request->getURIData('op'); + $after = $request->getStr('after'); + if (!strlen($after)) { + $after = null; + } + + $target = $request->getStr('target'); + if (!strlen($target)) { + $target = null; + } + + $impl = $panel->getImplementation(); + $config = $impl->getPanelConfiguration($panel); + + $cancel_uri = $panel->getURI(); + + if ($after !== null) { + $found = false; + foreach ($config as $key => $spec) { + if ((string)$key === $after) { + $found = true; + break; + } + } + + if (!$found) { + return $this->newDialog() + ->setTitle(pht('Adjacent Tab Not Found')) + ->appendParagraph( + pht( + 'Adjacent tab ("%s") was not found on this panel. It may have '. + 'been removed.', + $after)) + ->addCancelButton($cancel_uri); + } + } + + if ($target !== null) { + $found = false; + foreach ($config as $key => $spec) { + if ((string)$key === $target) { + $found = true; + break; + } + } + + if (!$found) { + return $this->newDialog() + ->setTitle(pht('Target Tab Not Found')) + ->appendParagraph( + pht( + 'Target tab ("%s") was not found on this panel. It may have '. + 'been removed.', + $target)) + ->addCancelButton($cancel_uri); + } + } + + switch ($op) { + case 'add': + return $this->handleAddOperation($panel, $after, $cancel_uri); + case 'remove': + return $this->handleRemoveOperation($panel, $target, $cancel_uri); + case 'move': + break; + case 'rename': + return $this->handleRenameOperation($panel, $target, $cancel_uri); + } + } + + private function handleAddOperation( + PhabricatorDashboardPanel $panel, + $after, + $cancel_uri) { + $request = $this->getRequest(); + $viewer = $this->getViewer(); + + $panel_phid = null; + $errors = array(); + if ($request->isFormPost()) { + $panel_phid = $request->getArr('panelPHID'); + $panel_phid = head($panel_phid); + + $add_panel = id(new PhabricatorDashboardPanelQuery()) + ->setViewer($viewer) + ->withPHIDs(array($panel_phid)) + ->executeOne(); + if (!$add_panel) { + $errors[] = pht('You must select a valid panel.'); + } + + if (!$errors) { + $add_panel_config = array( + 'name' => null, + 'panelID' => $add_panel->getID(), + ); + $add_panel_key = Filesystem::readRandomCharacters(12); + + $impl = $panel->getImplementation(); + $old_config = $impl->getPanelConfiguration($panel); + $new_config = array(); + if ($after === null) { + $new_config = $old_config; + $new_config[] = $add_panel_config; + } else { + foreach ($old_config as $key => $value) { + $new_config[$key] = $value; + if ((string)$key === $after) { + $new_config[$add_panel_key] = $add_panel_config; + } + } + } + + $xactions = array(); + + $xactions[] = $panel->getApplicationTransactionTemplate() + ->setTransactionType( + PhabricatorDashboardTabsPanelTabsTransaction::TRANSACTIONTYPE) + ->setNewValue($new_config); + + $editor = id(new PhabricatorDashboardPanelTransactionEditor()) + ->setContentSourceFromRequest($request) + ->setActor($viewer) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true); + + $editor->applyTransactions($panel, $xactions); + + return id(new AphrontRedirectResponse())->setURI($cancel_uri); + } + } + + if ($panel_phid) { + $v_panel = array($panel_phid); + } else { + $v_panel = array(); + } + + $form = id(new AphrontFormView()) + ->setViewer($viewer) + ->appendControl( + id(new AphrontFormTokenizerControl()) + ->setDatasource(new PhabricatorDashboardPanelDatasource()) + ->setLimit(1) + ->setName('panelPHID') + ->setLabel(pht('Panel')) + ->setValue($v_panel)); + + return $this->newDialog() + ->setTitle(pht('Choose Dashboard Panel')) + ->setErrors($errors) + ->setWidth(AphrontDialogView::WIDTH_FORM) + ->addHiddenInput('after', $after) + ->appendForm($form) + ->addCancelButton($cancel_uri) + ->addSubmitButton(pht('Add Panel')); + } + + private function handleRemoveOperation( + PhabricatorDashboardPanel $panel, + $target, + $cancel_uri) { + $request = $this->getRequest(); + $viewer = $this->getViewer(); + + $panel_phid = null; + $errors = array(); + if ($request->isFormPost()) { + $impl = $panel->getImplementation(); + $old_config = $impl->getPanelConfiguration($panel); + + $new_config = $this->removePanel($old_config, $target); + $this->writePanelConfig($panel, $new_config); + + return id(new AphrontRedirectResponse())->setURI($cancel_uri); + } + + return $this->newDialog() + ->setTitle(pht('Remove tab?')) + ->addHiddenInput('target', $target) + ->appendParagraph(pht('Really remove this tab?')) + ->addCancelButton($cancel_uri) + ->addSubmitButton(pht('Remove Tab')); + } + + private function handleRenameOperation( + PhabricatorDashboardPanel $panel, + $target, + $cancel_uri) { + $request = $this->getRequest(); + $viewer = $this->getViewer(); + + $impl = $panel->getImplementation(); + $old_config = $impl->getPanelConfiguration($panel); + + $spec = $old_config[$target]; + $name = idx($spec, 'name'); + + if ($request->isFormPost()) { + $name = $request->getStr('name'); + + $new_config = $this->renamePanel($old_config, $target, $name); + $this->writePanelConfig($panel, $new_config); + + return id(new AphrontRedirectResponse())->setURI($cancel_uri); + } + + $form = id(new AphrontFormView()) + ->setViewer($viewer) + ->appendControl( + id(new AphrontFormTextControl()) + ->setValue($name) + ->setName('name') + ->setLabel(pht('Tab Name'))); + + return $this->newDialog() + ->setTitle(pht('Rename Panel')) + ->addHiddenInput('target', $target) + ->appendForm($form) + ->addCancelButton($cancel_uri) + ->addSubmitButton(pht('Rename Tab')); + } + + + private function writePanelConfig( + PhabricatorDashboardPanel $panel, + array $config) { + $request = $this->getRequest(); + $viewer = $this->getViewer(); + + $xactions = array(); + + $xactions[] = $panel->getApplicationTransactionTemplate() + ->setTransactionType( + PhabricatorDashboardTabsPanelTabsTransaction::TRANSACTIONTYPE) + ->setNewValue($config); + + $editor = id(new PhabricatorDashboardPanelTransactionEditor()) + ->setContentSourceFromRequest($request) + ->setActor($viewer) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true); + + return $editor->applyTransactions($panel, $xactions); + } + + private function removePanel(array $config, $target) { + $result = array(); + + foreach ($config as $key => $panel_spec) { + if ((string)$key === $target) { + continue; + } + $result[$key] = $panel_spec; + } + + return $result; + } + + private function renamePanel(array $config, $target, $name) { + $config[$target]['name'] = $name; + return $config; + } + +} diff --git a/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php b/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php --- a/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php +++ b/src/applications/dashboard/engine/PhabricatorDashboardPanelRenderingEngine.php @@ -43,6 +43,10 @@ return $this->panelHandle; } + public function isEditMode() { + return ($this->getHeaderMode() === self::HEADER_MODE_EDIT); + } + /** * Allow the engine to render the panel via Ajax. */ diff --git a/src/applications/dashboard/paneltype/PhabricatorDashboardTabsPanelType.php b/src/applications/dashboard/paneltype/PhabricatorDashboardTabsPanelType.php --- a/src/applications/dashboard/paneltype/PhabricatorDashboardTabsPanelType.php +++ b/src/applications/dashboard/paneltype/PhabricatorDashboardTabsPanelType.php @@ -20,7 +20,6 @@ } protected function newEditEngineFields(PhabricatorDashboardPanel $panel) { - // TODO: Restore this using EditEngine instead of CustomField. return array(); } @@ -29,37 +28,37 @@ return false; } + public function getPanelConfiguration(PhabricatorDashboardPanel $panel) { + $config = $panel->getProperty('config'); + + if (!is_array($config)) { + // NOTE: The older version of this panel stored raw JSON. + try { + $config = phutil_json_decode($config); + } catch (PhutilJSONParserException $ex) { + $config = array(); + } + } + + return $config; + } + public function renderPanelContent( PhabricatorUser $viewer, PhabricatorDashboardPanel $panel, PhabricatorDashboardPanelRenderingEngine $engine) { - $config = $panel->getProperty('config'); - if (!is_array($config)) { - // NOTE: The older version of this panel stored raw JSON. - $config = phutil_json_decode($config); - } + $is_edit = $engine->isEditMode(); + $config = $this->getPanelConfiguration($panel); $list = id(new PHUIListView()) ->setType(PHUIListView::NAVBAR_LIST); - $selected = 0; - $node_ids = array(); foreach ($config as $idx => $tab_spec) { $node_ids[$idx] = celerity_generate_unique_node_id(); } - foreach ($config as $idx => $tab_spec) { - $list->addMenuItem( - id(new PHUIListItemView()) - ->setHref('#') - ->setSelected($idx == $selected) - ->addSigil('dashboard-tab-panel-tab') - ->setMetadata(array('idx' => $idx)) - ->setName(idx($tab_spec, 'name', pht('Nameless Tab')))); - } - $ids = ipull($config, 'panelID'); if ($ids) { $panels = id(new PhabricatorDashboardPanelQuery()) @@ -70,6 +69,135 @@ $panels = array(); } + $id = $panel->getID(); + + $add_uri = urisprintf('/dashboard/panel/tabs/%d/add/', $id); + $add_uri = new PhutilURI($add_uri); + + $remove_uri = urisprintf('/dashboard/panel/tabs/%d/remove/', $id); + $remove_uri = new PhutilURI($remove_uri); + + $rename_uri = urisprintf('/dashboard/panel/tabs/%d/rename/', $id); + $rename_uri = new PhutilURI($rename_uri); + + $selected = 0; + + $last_idx = null; + foreach ($config as $idx => $tab_spec) { + $panel_id = idx($tab_spec, 'panelID'); + $subpanel = idx($panels, $panel_id); + + $name = idx($tab_spec, 'name'); + if (!strlen($name)) { + if ($subpanel) { + $name = $subpanel->getName(); + } + } + + if (!strlen($name)) { + $name = pht('Unnamed Tab'); + } + + $tab_view = id(new PHUIListItemView()) + ->setHref('#') + ->setSelected($idx == $selected) + ->addSigil('dashboard-tab-panel-tab') + ->setMetadata(array('idx' => $idx)) + ->setName($name); + + if ($is_edit) { + $dropdown_menu = id(new PhabricatorActionListView()) + ->setViewer($viewer); + + $remove_tab_uri = id(clone $remove_uri) + ->replaceQueryParam('target', $idx); + + $rename_tab_uri = id(clone $rename_uri) + ->replaceQueryParam('target', $idx); + + if ($subpanel) { + $details_uri = $subpanel->getURI(); + } else { + $details_uri = null; + } + + $edit_uri = urisprintf( + '/dashboard/panel/edit/%d/', + $panel_id); + if ($subpanel) { + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $subpanel, + PhabricatorPolicyCapability::CAN_EDIT); + } else { + $can_edit = false; + } + + $dropdown_menu->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Rename Tab')) + ->setIcon('fa-pencil') + ->setHref($rename_tab_uri) + ->setWorkflow(true)); + + $dropdown_menu->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Remove Tab')) + ->setIcon('fa-times') + ->setHref($remove_tab_uri) + ->setWorkflow(true)); + + $dropdown_menu->addAction( + id(new PhabricatorActionView()) + ->setType(PhabricatorActionView::TYPE_DIVIDER)); + + $dropdown_menu->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Edit Panel')) + ->setIcon('fa-pencil') + ->setHref($edit_uri) + ->setWorkflow(true) + ->setDisabled(!$can_edit)); + + $dropdown_menu->addAction( + id(new PhabricatorActionView()) + ->setName(pht('View Panel Details')) + ->setIcon('fa-window-maximize') + ->setHref($details_uri) + ->setDisabled(!$subpanel)); + + $tab_view->setDropdownMenu($dropdown_menu); + } + + $list->addMenuItem($tab_view); + + $last_idx = $idx; + } + + if ($is_edit) { + $actions = id(new PhabricatorActionListView()) + ->setViewer($viewer); + + $add_last_uri = clone $add_uri; + if ($last_idx) { + $add_last_uri->replaceQueryParam('after', $last_idx); + } + + $actions->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Add Existing Panel')) + ->setIcon('fa-window-maximize') + ->setHref($add_last_uri) + ->setWorkflow(true)); + + $list->addMenuItem( + id(new PHUIListItemView()) + ->setHref('#') + ->setSelected(false) + ->setName(pht('Add Tab...')) + ->setDropdownMenu($actions)); + } + $parent_phids = $engine->getParentPanelPHIDs(); $parent_phids[] = $panel->getPHID(); @@ -83,15 +211,15 @@ $no_headers = PhabricatorDashboardPanelRenderingEngine::HEADER_MODE_NONE; foreach ($config as $idx => $tab_spec) { $panel_id = idx($tab_spec, 'panelID'); - $panel = idx($panels, $panel_id); + $subpanel = idx($panels, $panel_id); - if ($panel) { + if ($subpanel) { $panel_content = id(new PhabricatorDashboardPanelRenderingEngine()) ->setViewer($viewer) ->setEnableAsyncRendering(true) ->setParentPanelPHIDs($parent_phids) - ->setPanel($panel) - ->setPanelPHID($panel->getPHID()) + ->setPanel($subpanel) + ->setPanelPHID($subpanel->getPHID()) ->setHeaderMode($no_headers) ->setMovable(false) ->renderPanel(); @@ -108,6 +236,28 @@ $panel_content); } + if (!$content) { + if ($is_edit) { + $message = pht( + 'This tab panel does not have any tabs yet. Use "Add Tab" to '. + 'create or place a tab.'); + } else { + $message = pht( + 'This tab panel does not have any tabs yet.'); + } + + $content = id(new PHUIInfoView()) + ->setSeverity(PHUIInfoView::SEVERITY_NODATA) + ->setErrors( + array( + $message, + )); + + $content = id(new PHUIBoxView()) + ->addClass('mlt mlb') + ->appendChild($content); + } + Javelin::initBehavior('dashboard-tab-panel'); return javelin_tag( diff --git a/src/applications/dashboard/typeahead/PhabricatorDashboardPanelDatasource.php b/src/applications/dashboard/typeahead/PhabricatorDashboardPanelDatasource.php --- a/src/applications/dashboard/typeahead/PhabricatorDashboardPanelDatasource.php +++ b/src/applications/dashboard/typeahead/PhabricatorDashboardPanelDatasource.php @@ -48,13 +48,13 @@ $type_text = nonempty($panel->getPanelType(), pht('Unknown Type')); $icon = 'fa-question'; } - $id = $panel->getID(); + $phid = $panel->getPHID(); $monogram = $panel->getMonogram(); $properties = $panel->getProperties(); $result = id(new PhabricatorTypeaheadResult()) ->setName($monogram.' '.$panel->getName()) - ->setPHID($id) + ->setPHID($phid) ->setIcon($icon) ->addAttribute($type_text); @@ -66,7 +66,7 @@ $result->setClosed(pht('Archived')); } - $results[$id] = $result; + $results[$phid] = $result; } return $results; diff --git a/src/applications/dashboard/xaction/panel/PhabricatorDashboardTabsPanelTabsTransaction.php b/src/applications/dashboard/xaction/panel/PhabricatorDashboardTabsPanelTabsTransaction.php new file mode 100644 --- /dev/null +++ b/src/applications/dashboard/xaction/panel/PhabricatorDashboardTabsPanelTabsTransaction.php @@ -0,0 +1,12 @@ +openInNewWindow = $open_in_new_window; @@ -68,6 +69,7 @@ $this->addSigil('phui-dropdown-menu'); $this->setMetadata($actions->getDropdownMenuMetadata()); + $this->hasDropdown = true; return $this; } @@ -235,6 +237,10 @@ $classes[] = 'phui-list-item-has-action-icon'; } + if ($this->hasDropdown) { + $classes[] = 'dropdown'; + } + return array( 'class' => implode(' ', $classes), ); @@ -363,6 +369,12 @@ $this->count); } + if ($this->hasDropdown) { + $caret = phutil_tag('span', array('class' => 'caret'), ''); + } else { + $caret = null; + } + $icons = $this->getIcons(); $list_item = javelin_tag( @@ -381,6 +393,7 @@ $icons, $this->renderChildren(), $name, + $caret, $count, )); diff --git a/webroot/rsrc/css/phui/phui-action-list.css b/webroot/rsrc/css/phui/phui-action-list.css --- a/webroot/rsrc/css/phui/phui-action-list.css +++ b/webroot/rsrc/css/phui/phui-action-list.css @@ -213,3 +213,14 @@ .phabricator-action-view-item .phui-icon-view { color: {$sky}; } + +.phui-list-item-view.dropdown .phui-list-item-href { + padding-right: 28px; +} + +.phui-list-item-view .caret { + position: absolute; + top: 6px; + right: 12px; + border-top: 7px solid {$greytext}; +}