1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2025-01-11 07:11:04 +01:00

Make settings panels more modular and modern

Summary:
Currently, we have a hard-coded list of settings panels. Make them a bit more modular.

  - Allow new settings panels to be defined by third-party code (see {D2340}, for example -- @ptarjan).
  - This makes the OAuth stuff more flexible for {T887} / {T1536}.
  - Reduce the number of hard-coded URIs in various places.

Test Plan: Viewed / edited every option in every panel. Grepped for all references to these URIs.

Reviewers: btrahan, vrana, ptarjan

Reviewed By: btrahan

CC: aran

Differential Revision: https://secure.phabricator.com/D3257
This commit is contained in:
epriestley 2012-08-13 12:37:26 -07:00
parent df5bf75e36
commit 20ac900e8b
25 changed files with 548 additions and 311 deletions

View file

@ -1003,6 +1003,18 @@ phutil_register_library_map(array(
'PhabricatorSearchUserIndexer' => 'applications/search/index/indexer/PhabricatorSearchUserIndexer.php',
'PhabricatorSettingsAdjustController' => 'applications/settings/controller/PhabricatorSettingsAdjustController.php',
'PhabricatorSettingsMainController' => 'applications/settings/controller/PhabricatorSettingsMainController.php',
'PhabricatorSettingsPanel' => 'applications/settings/panel/PhabricatorSettingsPanel.php',
'PhabricatorSettingsPanelAccount' => 'applications/settings/panel/PhabricatorSettingsPanelAccount.php',
'PhabricatorSettingsPanelConduit' => 'applications/settings/panel/PhabricatorSettingsPanelConduit.php',
'PhabricatorSettingsPanelDisplayPreferences' => 'applications/settings/panel/PhabricatorSettingsPanelDisplayPreferences.php',
'PhabricatorSettingsPanelEmailAddresses' => 'applications/settings/panel/PhabricatorSettingsPanelEmailAddresses.php',
'PhabricatorSettingsPanelEmailPreferences' => 'applications/settings/panel/PhabricatorSettingsPanelEmailPreferences.php',
'PhabricatorSettingsPanelLDAP' => 'applications/settings/panel/PhabricatorSettingsPanelLDAP.php',
'PhabricatorSettingsPanelOAuth' => 'applications/settings/panel/PhabricatorSettingsPanelOAuth.php',
'PhabricatorSettingsPanelPassword' => 'applications/settings/panel/PhabricatorSettingsPanelPassword.php',
'PhabricatorSettingsPanelProfile' => 'applications/settings/panel/PhabricatorSettingsPanelProfile.php',
'PhabricatorSettingsPanelSSHKeys' => 'applications/settings/panel/PhabricatorSettingsPanelSSHKeys.php',
'PhabricatorSettingsPanelSearchPreferences' => 'applications/settings/panel/PhabricatorSettingsPanelSearchPreferences.php',
'PhabricatorSetup' => 'infrastructure/PhabricatorSetup.php',
'PhabricatorSlowvoteChoice' => 'applications/slowvote/storage/PhabricatorSlowvoteChoice.php',
'PhabricatorSlowvoteComment' => 'applications/slowvote/storage/PhabricatorSlowvoteComment.php',
@ -1053,27 +1065,15 @@ phutil_register_library_map(array(
'PhabricatorUITooltipExample' => 'applications/uiexample/examples/PhabricatorUITooltipExample.php',
'PhabricatorUnitsTestCase' => 'view/__tests__/PhabricatorUnitsTestCase.php',
'PhabricatorUser' => 'applications/people/storage/PhabricatorUser.php',
'PhabricatorUserAccountSettingsPanelController' => 'applications/settings/panel/PhabricatorUserAccountSettingsPanelController.php',
'PhabricatorUserConduitSettingsPanelController' => 'applications/settings/panel/PhabricatorUserConduitSettingsPanelController.php',
'PhabricatorUserDAO' => 'applications/people/storage/PhabricatorUserDAO.php',
'PhabricatorUserEditor' => 'applications/people/PhabricatorUserEditor.php',
'PhabricatorUserEmail' => 'applications/people/storage/PhabricatorUserEmail.php',
'PhabricatorUserEmailPreferenceSettingsPanelController' => 'applications/settings/panel/PhabricatorUserEmailPreferenceSettingsPanelController.php',
'PhabricatorUserEmailSettingsPanelController' => 'applications/settings/panel/PhabricatorUserEmailSettingsPanelController.php',
'PhabricatorUserLDAPInfo' => 'applications/people/storage/PhabricatorUserLDAPInfo.php',
'PhabricatorUserLDAPSettingsPanelController' => 'applications/settings/panel/PhabricatorUserLDAPSettingsPanelController.php',
'PhabricatorUserLog' => 'applications/people/storage/PhabricatorUserLog.php',
'PhabricatorUserOAuthInfo' => 'applications/people/storage/PhabricatorUserOAuthInfo.php',
'PhabricatorUserOAuthSettingsPanelController' => 'applications/settings/panel/PhabricatorUserOAuthSettingsPanelController.php',
'PhabricatorUserPasswordSettingsPanelController' => 'applications/settings/panel/PhabricatorUserPasswordSettingsPanelController.php',
'PhabricatorUserPreferenceSettingsPanelController' => 'applications/settings/panel/PhabricatorUserPreferenceSettingsPanelController.php',
'PhabricatorUserPreferences' => 'applications/settings/storage/PhabricatorUserPreferences.php',
'PhabricatorUserProfile' => 'applications/people/storage/PhabricatorUserProfile.php',
'PhabricatorUserProfileSettingsPanelController' => 'applications/settings/panel/PhabricatorUserProfileSettingsPanelController.php',
'PhabricatorUserSSHKey' => 'applications/settings/storage/PhabricatorUserSSHKey.php',
'PhabricatorUserSSHKeysSettingsPanelController' => 'applications/settings/panel/PhabricatorUserSSHKeysSettingsPanelController.php',
'PhabricatorUserSearchSettingsPanelController' => 'applications/settings/panel/PhabricatorUserSearchSettingsPanelController.php',
'PhabricatorUserSettingsPanelController' => 'applications/settings/panel/PhabricatorUserSettingsPanelController.php',
'PhabricatorUserStatus' => 'applications/people/storage/PhabricatorUserStatus.php',
'PhabricatorUserTestCase' => 'applications/people/storage/__tests__/PhabricatorUserTestCase.php',
'PhabricatorWorker' => 'infrastructure/daemon/workers/PhabricatorWorker.php',
@ -2074,6 +2074,17 @@ phutil_register_library_map(array(
'PhabricatorSearchUserIndexer' => 'PhabricatorSearchDocumentIndexer',
'PhabricatorSettingsAdjustController' => 'PhabricatorController',
'PhabricatorSettingsMainController' => 'PhabricatorController',
'PhabricatorSettingsPanelAccount' => 'PhabricatorSettingsPanel',
'PhabricatorSettingsPanelConduit' => 'PhabricatorSettingsPanel',
'PhabricatorSettingsPanelDisplayPreferences' => 'PhabricatorSettingsPanel',
'PhabricatorSettingsPanelEmailAddresses' => 'PhabricatorSettingsPanel',
'PhabricatorSettingsPanelEmailPreferences' => 'PhabricatorSettingsPanel',
'PhabricatorSettingsPanelLDAP' => 'PhabricatorSettingsPanel',
'PhabricatorSettingsPanelOAuth' => 'PhabricatorSettingsPanel',
'PhabricatorSettingsPanelPassword' => 'PhabricatorSettingsPanel',
'PhabricatorSettingsPanelProfile' => 'PhabricatorSettingsPanel',
'PhabricatorSettingsPanelSSHKeys' => 'PhabricatorSettingsPanel',
'PhabricatorSettingsPanelSearchPreferences' => 'PhabricatorSettingsPanel',
'PhabricatorSlowvoteChoice' => 'PhabricatorSlowvoteDAO',
'PhabricatorSlowvoteComment' => 'PhabricatorSlowvoteDAO',
'PhabricatorSlowvoteController' => 'PhabricatorController',
@ -2119,26 +2130,14 @@ phutil_register_library_map(array(
0 => 'PhabricatorUserDAO',
1 => 'PhutilPerson',
),
'PhabricatorUserAccountSettingsPanelController' => 'PhabricatorUserSettingsPanelController',
'PhabricatorUserConduitSettingsPanelController' => 'PhabricatorUserSettingsPanelController',
'PhabricatorUserDAO' => 'PhabricatorLiskDAO',
'PhabricatorUserEmail' => 'PhabricatorUserDAO',
'PhabricatorUserEmailPreferenceSettingsPanelController' => 'PhabricatorUserSettingsPanelController',
'PhabricatorUserEmailSettingsPanelController' => 'PhabricatorUserSettingsPanelController',
'PhabricatorUserLDAPInfo' => 'PhabricatorUserDAO',
'PhabricatorUserLDAPSettingsPanelController' => 'PhabricatorUserSettingsPanelController',
'PhabricatorUserLog' => 'PhabricatorUserDAO',
'PhabricatorUserOAuthInfo' => 'PhabricatorUserDAO',
'PhabricatorUserOAuthSettingsPanelController' => 'PhabricatorUserSettingsPanelController',
'PhabricatorUserPasswordSettingsPanelController' => 'PhabricatorUserSettingsPanelController',
'PhabricatorUserPreferenceSettingsPanelController' => 'PhabricatorUserSettingsPanelController',
'PhabricatorUserPreferences' => 'PhabricatorUserDAO',
'PhabricatorUserProfile' => 'PhabricatorUserDAO',
'PhabricatorUserProfileSettingsPanelController' => 'PhabricatorUserSettingsPanelController',
'PhabricatorUserSSHKey' => 'PhabricatorUserDAO',
'PhabricatorUserSSHKeysSettingsPanelController' => 'PhabricatorUserSettingsPanelController',
'PhabricatorUserSearchSettingsPanelController' => 'PhabricatorUserSettingsPanelController',
'PhabricatorUserSettingsPanelController' => 'PhabricatorPeopleController',
'PhabricatorUserStatus' => 'PhabricatorUserDAO',
'PhabricatorUserTestCase' => 'PhabricatorTestCase',
'PhabricatorWorkerDAO' => 'PhabricatorLiskDAO',

View file

@ -97,7 +97,7 @@ final class PhabricatorEmailTokenController
$request->setCookie('phsid', $session_key);
if (PhabricatorEnv::getEnvConfig('account.editable')) {
$next = (string)id(new PhutilURI('/settings/page/password/'))
$next = (string)id(new PhutilURI('/settings/panel/password/'))
->setQueryParams(
array(
'token' => $token,

View file

@ -64,12 +64,12 @@ final class PhabricatorLDAPLoginController extends PhabricatorAuthController {
'another Phabricator account. Before you can link it to a '.
'different LDAP account, you must unlink the old account.</p>'
);
$dialog->addCancelButton('/settings/page/ldap/');
$dialog->addCancelButton('/settings/panel/ldap/');
return id(new AphrontDialogResponse())->setDialog($dialog);
} else {
return id(new AphrontRedirectResponse())
->setURI('/settings/page/ldap/');
->setURI('/settings/panel/ldap/');
}
}
@ -82,7 +82,7 @@ final class PhabricatorLDAPLoginController extends PhabricatorAuthController {
$dialog->addHiddenInput('username', $request->getStr('username'));
$dialog->addHiddenInput('password', $request->getStr('password'));
$dialog->addSubmitButton('Link Accounts');
$dialog->addCancelButton('/settings/page/ldap/');
$dialog->addCancelButton('/settings/panel/ldap/');
return id(new AphrontDialogResponse())->setDialog($dialog);
}
@ -92,7 +92,7 @@ final class PhabricatorLDAPLoginController extends PhabricatorAuthController {
$this->saveLDAPInfo($ldap_info);
return id(new AphrontRedirectResponse())
->setURI('/settings/page/ldap/');
->setURI('/settings/panel/ldap/');
}
if ($ldap_info->getID()) {

View file

@ -38,7 +38,7 @@ final class PhabricatorLDAPUnlinkController extends PhabricatorAuthController {
'<p><strong>You will not be able to login</strong> using this account '.
'once you unlink it. Continue?</p>');
$dialog->addSubmitButton('Unlink Account');
$dialog->addCancelButton('/settings/page/ldap/');
$dialog->addCancelButton('/settings/panel/ldap/');
return id(new AphrontDialogResponse())->setDialog($dialog);
}
@ -46,7 +46,7 @@ final class PhabricatorLDAPUnlinkController extends PhabricatorAuthController {
$ldap_info->delete();
return id(new AphrontRedirectResponse())
->setURI('/settings/page/ldap/');
->setURI('/settings/panel/ldap/');
}
}

View file

@ -93,13 +93,13 @@ final class PhabricatorOAuthLoginController
'the Phabricator account it is currently linked to.</p>',
$provider_name,
$provider_name));
$dialog->addCancelButton('/settings/page/'.$provider_key.'/');
$dialog->addCancelButton($provider->getSettingsPanelURI());
return id(new AphrontDialogResponse())->setDialog($dialog);
} else {
$this->saveOAuthInfo($oauth_info); // Refresh token.
return id(new AphrontRedirectResponse())
->setURI('/settings/page/'.$provider_key.'/');
->setURI($provider->getSettingsPanelURI());
}
}
@ -119,7 +119,7 @@ final class PhabricatorOAuthLoginController
'must unlink the old account.</p>',
$provider_name,
$provider_name));
$dialog->addCancelButton('/settings/page/'.$provider_key.'/');
$dialog->addCancelButton($provider->getSettingsPanelURI());
return id(new AphrontDialogResponse())->setDialog($dialog);
}
@ -136,7 +136,7 @@ final class PhabricatorOAuthLoginController
$dialog->addHiddenInput('state', $this->oauthState);
$dialog->addHiddenInput('scope', $oauth_info->getTokenScope());
$dialog->addSubmitButton('Link Accounts');
$dialog->addCancelButton('/settings/page/'.$provider_key.'/');
$dialog->addCancelButton($provider->getSettingsPanelURI());
return id(new AphrontDialogResponse())->setDialog($dialog);
}
@ -146,7 +146,7 @@ final class PhabricatorOAuthLoginController
$this->saveOAuthInfo($oauth_info);
return id(new AphrontRedirectResponse())
->setURI('/settings/page/'.$provider_key.'/');
->setURI($provider->getSettingsPanelURI());
}
// Login with known auth.

View file

@ -54,7 +54,7 @@ final class PhabricatorOAuthUnlinkController extends PhabricatorAuthController {
'<p><strong>You will not be able to login</strong> using this account '.
'once you unlink it. Continue?</p>');
$dialog->addSubmitButton('Unlink Account');
$dialog->addCancelButton('/settings/page/'.$provider_key.'/');
$dialog->addCancelButton($provider->getSettingsPanelURI());
return id(new AphrontDialogResponse())->setDialog($dialog);
}
@ -62,7 +62,7 @@ final class PhabricatorOAuthUnlinkController extends PhabricatorAuthController {
$oauth_info->delete();
return id(new AphrontRedirectResponse())
->setURI('/settings/page/'.$provider_key.'/');
->setURI($provider->getSettingsPanelURI());
}
}

View file

@ -38,6 +38,12 @@ abstract class PhabricatorOAuthProvider {
abstract public function getAuthURI();
abstract public function getTestURIs();
public function getSettingsPanelURI() {
$panel = new PhabricatorSettingsPanelOAuth();
$panel->setOAuthProvider($this);
return $panel->getPanelURI();
}
/**
* If the provider needs extra stuff in the auth request, return it here.
* For example, Google needs a response_type parameter.

View file

@ -293,7 +293,7 @@ final class DifferentialChangesetListView extends AphrontView {
if ($editor_link) {
$meta['editor'] = $editor_link;
} else {
$meta['editorConfigure'] = '/settings/page/preferences/';
$meta['editorConfigure'] = '/settings/panel/display/';
}
}

View file

@ -51,7 +51,7 @@ final class PhabricatorEmailVerificationController
$settings_link = phutil_render_tag(
'a',
array(
'href' => '/settings/page/email/',
'href' => '/settings/panel/email/',
),
'Return to Email Settings');
$settings_link = '<br /><p><strong>'.$settings_link.'</strong></p>';

View file

@ -147,7 +147,7 @@ final class PhabricatorPeopleProfileController
if ($user->getPHID() == $viewer->getPHID()) {
$nav->addSpacer();
$nav->addFilter(null, 'Edit Profile...', '/settings/page/profile/');
$nav->addFilter(null, 'Edit Profile...', '/settings/panel/profile/');
}
if ($viewer->getIsAdmin()) {

View file

@ -33,7 +33,7 @@ final class PhabricatorApplicationSettings extends PhabricatorApplication {
public function getRoutes() {
return array(
'/settings/' => array(
'(?:page/(?P<page>[^/]+)/)?' => 'PhabricatorSettingsMainController',
'(?:panel/(?P<key>[^/]+)/)?' => 'PhabricatorSettingsMainController',
'adjust/' => 'PhabricatorSettingsAdjustController',
),
);

View file

@ -19,128 +19,87 @@
final class PhabricatorSettingsMainController
extends PhabricatorController {
private $page;
private $pages;
private $key;
public function willProcessRequest(array $data) {
$this->page = idx($data, 'page');
$this->key = idx($data, 'key');
}
public function processRequest() {
$request = $this->getRequest();
$oauth_providers = PhabricatorOAuthProvider::getAllProviders();
$sidenav = $this->renderSideNav($oauth_providers);
$this->page = $sidenav->selectFilter($this->page, 'account');
$panels = $this->buildPanels();
$nav = $this->renderSideNav($panels);
switch ($this->page) {
case 'account':
$delegate = new PhabricatorUserAccountSettingsPanelController($request);
break;
case 'profile':
$delegate = new PhabricatorUserProfileSettingsPanelController($request);
break;
case 'email':
$delegate = new PhabricatorUserEmailSettingsPanelController($request);
break;
case 'emailpref':
$delegate = new PhabricatorUserEmailPreferenceSettingsPanelController(
$request);
break;
case 'password':
$delegate = new PhabricatorUserPasswordSettingsPanelController(
$request);
break;
case 'conduit':
$delegate = new PhabricatorUserConduitSettingsPanelController($request);
break;
case 'sshkeys':
$delegate = new PhabricatorUserSSHKeysSettingsPanelController($request);
break;
case 'preferences':
$delegate = new PhabricatorUserPreferenceSettingsPanelController(
$request);
break;
case 'search':
$delegate = new PhabricatorUserSearchSettingsPanelController($request);
break;
case 'ldap':
$delegate = new PhabricatorUserLDAPSettingsPanelController($request);
break;
default:
$delegate = new PhabricatorUserOAuthSettingsPanelController($request);
$delegate->setOAuthProvider($oauth_providers[$this->page]);
break;
}
$key = $nav->selectFilter($this->key, head($panels)->getPanelKey());
$response = $this->delegateToController($delegate);
if ($response instanceof AphrontView) {
$sidenav->appendChild($response);
return $this->buildStandardPageResponse(
$sidenav,
array(
'title' => 'Account Settings',
));
} else {
$panel = $panels[$key];
$response = $panel->processRequest($request);
if ($response instanceof AphrontResponse) {
return $response;
}
$nav->appendChild($response);
return $this->buildApplicationPage(
$nav,
array(
'title' => $panel->getPanelName(),
));
}
private function renderSideNav($oauth_providers) {
$sidenav = new AphrontSideNavFilterView();
$sidenav
->setBaseURI(new PhutilURI('/settings/page/'))
->addLabel('Account Information')
->addFilter('account', 'Account')
->addFilter('profile', 'Profile')
->addSpacer()
->addLabel('Email')
->addFilter('email', 'Email Addresses')
->addFilter('emailpref', 'Email Preferences')
->addSpacer()
->addLabel('Authentication');
private function buildPanels() {
$panel_specs = id(new PhutilSymbolLoader())
->setAncestorClass('PhabricatorSettingsPanel')
->setConcreteOnly(true)
->selectAndLoadSymbols();
if (PhabricatorEnv::getEnvConfig('account.editable') &&
PhabricatorEnv::getEnvConfig('auth.password-auth-enabled')) {
$sidenav->addFilter('password', 'Password');
$panels = array();
foreach ($panel_specs as $spec) {
$class = newv($spec['name'], array());
$panels[] = $class->buildPanels();
}
$sidenav->addFilter('conduit', 'Conduit Certificate');
$panels = array_mergev($panels);
$panels = mpull($panels, null, 'getPanelKey');
if (PhabricatorUserSSHKeysSettingsPanelController::isEnabled()) {
$sidenav->addFilter('sshkeys', 'SSH Public Keys');
}
$sidenav->addSpacer();
$sidenav->addLabel('Application Settings');
$sidenav->addFilter('preferences', 'Display Preferences');
$sidenav->addFilter('search', 'Search Preferences');
$items = array();
foreach ($oauth_providers as $provider) {
if (!$provider->isProviderEnabled()) {
$result = array();
foreach ($panels as $key => $panel) {
if (!$panel->isEnabled()) {
continue;
}
$key = $provider->getProviderKey();
$name = $provider->getProviderName();
$items[$key] = $name.' Account';
}
$ldap_provider = new PhabricatorLDAPProvider();
if ($ldap_provider->isProviderEnabled()) {
$items['ldap'] = 'LDAP Account';
}
if ($items) {
$sidenav->addSpacer();
$sidenav->addLabel('Linked Accounts');
foreach ($items as $key => $name) {
$sidenav->addFilter($key, $name);
if (!empty($result[$key])) {
throw new Exception(
"Two settings panels share the same panel key ('{$key}'): ".
get_class($panel).', '.get_class($result[$key]).'.');
}
$result[$key] = $panel;
}
return $sidenav;
$result = msort($result, 'getPanelSortKey');
return $result;
}
private function renderSideNav(array $panels) {
$nav = new AphrontSideNavFilterView();
$nav->setBaseURI(new PhutilURI($this->getApplicationURI('/panel/')));
$group = null;
foreach ($panels as $panel) {
if ($panel->getPanelGroup() != $group) {
if ($group !== null) {
$nav->addSpacer();
}
$group = $panel->getPanelGroup();
$nav->addLabel($group);
}
$nav->addFilter($panel->getPanelKey(), $panel->getPanelName());
}
return $nav;
}
}

View file

@ -0,0 +1,156 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Defines a settings panel. Settings panels appear in the Settings application,
* and behave like lightweight controllers -- generally, they render some sort
* of form with options in it, and then update preferences when the user
* submits the form. By extending this class, you can add new settings
* panels.
*
* NOTE: This stuff is new and might not be completely stable.
*
* @task config Panel Configuration
* @task panel Panel Implementation
* @task internal Internals
*
* @group settings
*/
abstract class PhabricatorSettingsPanel {
/* -( Panel Configuration )------------------------------------------------ */
/**
* Return a unique string used in the URI to identify this panel, like
* "example".
*
* @return string Unique panel identifier (used in URIs).
* @task config
*/
abstract public function getPanelKey();
/**
* Return a human-readable description of the panel's contents, like
* "Example Settings".
*
* @return string Human-readable panel name.
* @task config
*/
abstract public function getPanelName();
/**
* Return a human-readable group name for this panel. For instance, if you
* had several related panels like "Volume Settings" and
* "Microphone Settings", you might put them in a group called "Audio".
*
* When displayed, panels are grouped with other panels that have the same
* group name.
*
* @return string Human-readable panel group name.
* @task config
*/
abstract public function getPanelGroup();
/**
* Return false to prevent this panel from being displayed or used. You can
* do, e.g., configuration checks here, to determine if the feature your
* panel controls is unavailble in this install. By default, all panels are
* enabled.
*
* @return bool True if the panel should be shown.
* @task config
*/
public function isEnabled() {
return true;
}
/**
* You can use this callback to generate multiple similar panels which all
* share the same implementation. For example, OAuth providers each have a
* separate panel, but the implementation for each panel is the same.
*
* To generate multiple panels, build them here and return a list. By default,
* the current panel (`$this`) is returned alone. For most panels, this
* is the right implementation.
*
* @return list<PhabricatorSettingsPanel> Zero or more panels.
* @task config
*/
public function buildPanels() {
return array($this);
}
/* -( Panel Implementation )----------------------------------------------- */
/**
* Process a user request for this settings panel. Implement this method like
* a lightweight controller. If you return an @{class:AphrontResponse}, the
* response will be used in whole. If you return anything else, it will be
* treated as a view and composed into a normal settings page.
*
* Generally, render your settings panel by returning a form, then return
* a redirect when the user saves settings.
*
* @param AphrontRequest Incoming request.
* @return wild Response to request, either as an
* @{class:AphrontResponse} or something which can
* be composed into a @{class:AphrontView}.
* @task panel
*/
abstract public function processRequest(AphrontRequest $request);
/**
* Get the URI for this panel.
*
* @param string? Optional path to append.
* @return string Relative URI for the panel.
* @task panel
*/
final public function getPanelURI($path = '') {
$key = $this->getPanelKey();
$key = phutil_escape_uri($key);
return '/settings/panel/'.$key.'/'.ltrim($path, '/');
}
/* -( Internals )---------------------------------------------------------- */
/**
* Generates a key to sort the list of panels.
*
* @return string Sortable key.
* @task internal
*/
final public function getPanelSortKey() {
return sprintf(
'%s'.chr(255).'%s',
$this->getPanelGroup(),
$this->getPanelName());
}
}

View file

@ -16,14 +16,24 @@
* limitations under the License.
*/
final class PhabricatorUserAccountSettingsPanelController
extends PhabricatorUserSettingsPanelController {
final class PhabricatorSettingsPanelAccount
extends PhabricatorSettingsPanel {
public function processRequest() {
public function getPanelKey() {
return 'account';
}
$request = $this->getRequest();
public function getPanelName() {
return pht('Account');
}
public function getPanelGroup() {
return pht('Account Information');
}
public function processRequest(AphrontRequest $request) {
$user = $request->getUser();
$editable = $this->getAccountEditable();
$editable = PhabricatorEnv::getEnvConfig('account.editable');
$e_realname = $editable ? true : null;
$errors = array();
@ -47,7 +57,7 @@ final class PhabricatorUserAccountSettingsPanelController
if (!$errors) {
$user->save();
return id(new AphrontRedirectResponse())
->setURI('/settings/page/account/?saved=true');
->setURI($this->getPanelURI('?saved=true'));
}
}
@ -73,7 +83,6 @@ final class PhabricatorUserAccountSettingsPanelController
$form = new AphrontFormView();
$form
->setUser($user)
->setEncType('multipart/form-data')
->appendChild(
id(new AphrontFormStaticControl())
->setLabel('Username')
@ -100,11 +109,9 @@ final class PhabricatorUserAccountSettingsPanelController
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
$panel->appendChild($form);
return id(new AphrontNullView())
->appendChild(
array(
$notice,
$panel,
));
return array(
$notice,
$panel,
);
}
}

View file

@ -16,12 +16,22 @@
* limitations under the License.
*/
final class PhabricatorUserConduitSettingsPanelController
extends PhabricatorUserSettingsPanelController {
final class PhabricatorSettingsPanelConduit
extends PhabricatorSettingsPanel {
public function processRequest() {
public function getPanelKey() {
return 'conduit';
}
$request = $this->getRequest();
public function getPanelName() {
return pht('Conduit');
}
public function getPanelGroup() {
return pht('Authentication');
}
public function processRequest(AphrontRequest $request) {
$user = $request->getUser();
if ($request->isFormPost()) {
@ -29,9 +39,9 @@ final class PhabricatorUserConduitSettingsPanelController
$dialog = new AphrontDialogView();
$dialog->setUser($user);
$dialog->setTitle('Really regenerate session?');
$dialog->setSubmitURI('/settings/page/conduit/');
$dialog->setSubmitURI($this->getPanelURI());
$dialog->addSubmitButton('Regenerate');
$dialog->addCancelbutton('/settings/page/conduit/');
$dialog->addCancelbutton($this->getPanelURI());
$dialog->appendChild(
'<p>Really destroy the old certificate? Any established '.
'sessions will be terminated.');
@ -52,7 +62,7 @@ final class PhabricatorUserConduitSettingsPanelController
$user->setConduitCertificate(null);
$user->save();
return id(new AphrontRedirectResponse())
->setURI('/settings/page/conduit/?regenerated=true');
->setURI($this->getPanelURI('?regenerated=true'));
}
if ($request->getStr('regenerated')) {
@ -89,7 +99,8 @@ final class PhabricatorUserConduitSettingsPanelController
$regen_form = new AphrontFormView();
$regen_form
->setUser($user)
->setAction('/settings/page/conduit/')
->setAction($this->getPanelURI())
->setWorkflow(true)
->appendChild(
'<p class="aphront-form-instructions">You can regenerate this '.
'certificate, which will invalidate the old certificate and create '.
@ -103,12 +114,10 @@ final class PhabricatorUserConduitSettingsPanelController
$regen->appendChild($regen_form);
$regen->setWidth(AphrontPanelView::WIDTH_FORM);
return id(new AphrontNullView())
->appendChild(
array(
$notice,
$cert,
$regen,
));
return array(
$notice,
$cert,
$regen,
);
}
}

View file

@ -16,12 +16,22 @@
* limitations under the License.
*/
final class PhabricatorUserPreferenceSettingsPanelController
extends PhabricatorUserSettingsPanelController {
final class PhabricatorSettingsPanelDisplayPreferences
extends PhabricatorSettingsPanel {
public function processRequest() {
public function getPanelKey() {
return 'display';
}
$request = $this->getRequest();
public function getPanelName() {
return pht('Display Preferences');
}
public function getPanelGroup() {
return pht('Application Settings');
}
public function processRequest(AphrontRequest $request) {
$user = $request->getUser();
$preferences = $user->loadPreferences();
@ -44,7 +54,7 @@ final class PhabricatorUserPreferenceSettingsPanelController
$preferences->save();
return id(new AphrontRedirectResponse())
->setURI('/settings/page/preferences/?saved=true');
->setURI($this->getPanelURI('?saved=true'));
}
$example_string = <<<EXAMPLE
@ -67,7 +77,6 @@ EXAMPLE;
$form = id(new AphrontFormView())
->setUser($user)
->setAction('/settings/page/preferences/')
->appendChild(
id(new AphrontFormSelectControl())
->setLabel('Page Titles')
@ -95,7 +104,7 @@ EXAMPLE;
->setLabel('Monospaced Font')
->setName($pref_monospaced)
->setCaption(
'Overrides default fonts in tools like Differential. '.
'Overrides default fonts in tools like Differential.<br />'.
'(Default: '.$font_default.')')
->setValue($preferences->getPreference($pref_monospaced)))
->appendChild(
@ -130,12 +139,10 @@ EXAMPLE;
->setErrors(array('Your preferences have been saved.'));
}
return id(new AphrontNullView())
->appendChild(
array(
$error_view,
$panel,
));
return array(
$error_view,
$panel,
);
}
}

View file

@ -16,14 +16,24 @@
* limitations under the License.
*/
final class PhabricatorUserEmailSettingsPanelController
extends PhabricatorUserSettingsPanelController {
final class PhabricatorSettingsPanelEmailAddresses
extends PhabricatorSettingsPanel {
public function processRequest() {
public function getPanelKey() {
return 'email';
}
$request = $this->getRequest();
public function getPanelName() {
return pht('Email Addresses');
}
public function getPanelGroup() {
return pht('Email');
}
public function processRequest(AphrontRequest $request) {
$user = $request->getUser();
$editable = $this->getAccountEditable();
$editable = PhabricatorEnv::getEnvConfig('account.editable');
$uri = $request->getRequestURI();
$uri->setQueryParams(array());
@ -31,23 +41,23 @@ final class PhabricatorUserEmailSettingsPanelController
if ($editable) {
$new = $request->getStr('new');
if ($new) {
return $this->returnNewAddressResponse($uri, $new);
return $this->returnNewAddressResponse($request, $uri, $new);
}
$delete = $request->getInt('delete');
if ($delete) {
return $this->returnDeleteAddressResponse($uri, $delete);
return $this->returnDeleteAddressResponse($request, $uri, $delete);
}
}
$verify = $request->getInt('verify');
if ($verify) {
return $this->returnVerifyAddressResponse($uri, $verify);
return $this->returnVerifyAddressResponse($request, $uri, $verify);
}
$primary = $request->getInt('primary');
if ($primary) {
return $this->returnPrimaryAddressResponse($uri, $primary);
return $this->returnPrimaryAddressResponse($request, $uri, $primary);
}
$emails = id(new PhabricatorUserEmail())->loadAllWhere(
@ -154,8 +164,11 @@ final class PhabricatorUserEmailSettingsPanelController
return $view;
}
private function returnNewAddressResponse(PhutilURI $uri, $new) {
$request = $this->getRequest();
private function returnNewAddressResponse(
AphrontRequest $request,
PhutilURI $uri,
$new) {
$user = $request->getUser();
$e_email = true;
@ -234,8 +247,11 @@ final class PhabricatorUserEmailSettingsPanelController
return id(new AphrontDialogResponse())->setDialog($dialog);
}
private function returnDeleteAddressResponse(PhutilURI $uri, $email_id) {
$request = $this->getRequest();
private function returnDeleteAddressResponse(
AphrontRequest $request,
PhutilURI $uri,
$email_id) {
$user = $request->getUser();
// NOTE: You can only delete your own email addresses, and you can not
@ -273,8 +289,11 @@ final class PhabricatorUserEmailSettingsPanelController
return id(new AphrontDialogResponse())->setDialog($dialog);
}
private function returnVerifyAddressResponse(PhutilURI $uri, $email_id) {
$request = $this->getRequest();
private function returnVerifyAddressResponse(
AphrontRequest $request,
PhutilURI $uri,
$email_id) {
$user = $request->getUser();
// NOTE: You can only send more email for your unverified addresses.
@ -307,8 +326,11 @@ final class PhabricatorUserEmailSettingsPanelController
return id(new AphrontDialogResponse())->setDialog($dialog);
}
private function returnPrimaryAddressResponse(PhutilURI $uri, $email_id) {
$request = $this->getRequest();
private function returnPrimaryAddressResponse(
AphrontRequest $request,
PhutilURI $uri,
$email_id) {
$user = $request->getUser();
// NOTE: You can only make your own verified addresses primary.

View file

@ -16,11 +16,22 @@
* limitations under the License.
*/
final class PhabricatorUserEmailPreferenceSettingsPanelController
extends PhabricatorUserSettingsPanelController {
final class PhabricatorSettingsPanelEmailPreferences
extends PhabricatorSettingsPanel {
public function processRequest() {
$request = $this->getRequest();
public function getPanelKey() {
return 'emailpreferences';
}
public function getPanelName() {
return pht('Email Preferences');
}
public function getPanelGroup() {
return pht('Email');
}
public function processRequest(AphrontRequest $request) {
$user = $request->getUser();
$preferences = $user->loadPreferences();
@ -64,7 +75,7 @@ final class PhabricatorUserEmailPreferenceSettingsPanelController
$preferences->save();
return id(new AphrontRedirectResponse())
->setURI('/settings/page/emailpref/?saved=true');
->setURI($this->getPanelURI('?saved=true'));
}
$notice = null;

View file

@ -16,12 +16,28 @@
* limitations under the License.
*/
final class PhabricatorUserLDAPSettingsPanelController
extends PhabricatorUserSettingsPanelController {
final class PhabricatorSettingsPanelLDAP
extends PhabricatorSettingsPanel {
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
public function getPanelKey() {
return 'ldap';
}
public function getPanelName() {
return pht('LDAP');
}
public function getPanelGroup() {
return pht('Linked Accounts');
}
public function isEnabled() {
$ldap_provider = new PhabricatorLDAPProvider();
return $ldap_provider->isProviderEnabled();
}
public function processRequest(AphrontRequest $request) {
$user = $request->getUser();
$ldap_info = id(new PhabricatorUserLDAPInfo())->loadOneWhere(
'userID = %d',
@ -78,10 +94,8 @@ final class PhabricatorUserLDAPSettingsPanelController
$panel->appendChild($form);
}
return id(new AphrontNullView())
->appendChild(
array(
$panel,
));
return array(
$panel,
);
}
}

View file

@ -16,8 +16,37 @@
* limitations under the License.
*/
final class PhabricatorUserOAuthSettingsPanelController
extends PhabricatorUserSettingsPanelController {
final class PhabricatorSettingsPanelOAuth
extends PhabricatorSettingsPanel {
public function getPanelKey() {
return 'oauth-'.$this->provider->getProviderKey();
}
public function getPanelName() {
return $this->provider->getProviderName();
}
public function getPanelGroup() {
return pht('Linked Accounts');
}
public function buildPanels() {
$panels = array();
$providers = PhabricatorOAuthProvider::getAllProviders();
foreach ($providers as $provider) {
$panel = clone $this;
$panel->setOAuthProvider($provider);
$panels[] = $panel;
}
return $panels;
}
public function isEnabled() {
return $this->provider->isProviderEnabled();
}
private $provider;
@ -48,8 +77,7 @@ final class PhabricatorUserOAuthSettingsPanelController
return $form;
}
public function processRequest() {
$request = $this->getRequest();
public function processRequest(AphrontRequest $request) {
$user = $request->getUser();
$provider = $this->provider;
$notice = null;
@ -62,7 +90,7 @@ final class PhabricatorUserOAuthSettingsPanelController
$provider->getProviderKey());
if ($request->isFormPost() && $oauth_info) {
$notice = $this->refreshProfileImage($oauth_info);
$notice = $this->refreshProfileImage($request, $oauth_info);
}
$form = new AphrontFormView();
@ -198,8 +226,11 @@ final class PhabricatorUserOAuthSettingsPanelController
));
}
private function refreshProfileImage(PhabricatorUserOAuthInfo $oauth_info) {
$user = $this->getRequest()->getUser();
private function refreshProfileImage(
AphrontRequest $request,
PhabricatorUserOAuthInfo $oauth_info) {
$user = $request->getUser();
$provider = $this->provider;
$error = false;
$userinfo_uri = new PhutilURI($provider->getUserInfoURI());

View file

@ -16,22 +16,41 @@
* limitations under the License.
*/
final class PhabricatorUserPasswordSettingsPanelController
extends PhabricatorUserSettingsPanelController {
final class PhabricatorSettingsPanelPassword
extends PhabricatorSettingsPanel {
public function processRequest() {
public function getPanelKey() {
return 'password';
}
$request = $this->getRequest();
$user = $request->getUser();
$editable = $this->getAccountEditable();
public function getPanelName() {
return pht('Password');
}
public function getPanelGroup() {
return pht('Authentication');
}
public function isEnabled() {
// There's no sense in showing a change password panel if the user
// can't change their password
if (!$editable ||
!PhabricatorEnv::getEnvConfig('auth.password-auth-enabled')) {
return new Aphront400Response();
// can't change their password...
if (!PhabricatorEnv::getEnvConfig('account.editable')) {
return false;
}
// ...or this install doesn't support password authentication at all.
if (!PhabricatorEnv::getEnvConfig('auth.password-auth-enabled')) {
return false;
}
return true;
}
public function processRequest(AphrontRequest $request) {
$user = $request->getUser();
$min_len = PhabricatorEnv::getEnvConfig('account.minimum-password-length');
$min_len = (int)$min_len;
@ -98,7 +117,7 @@ final class PhabricatorUserPasswordSettingsPanelController
// after we update their account.
$next = '/';
} else {
$next = '/settings/page/password/?saved=true';
$next = $this->getPanelURI('?saved=true');
}
return id(new AphrontRedirectResponse())->setURI($next);
@ -160,11 +179,9 @@ final class PhabricatorUserPasswordSettingsPanelController
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
$panel->appendChild($form);
return id(new AphrontNullView())
->appendChild(
array(
$notice,
$panel,
));
return array(
$notice,
$panel,
);
}
}

View file

@ -16,12 +16,22 @@
* limitations under the License.
*/
final class PhabricatorUserProfileSettingsPanelController
extends PhabricatorUserSettingsPanelController {
final class PhabricatorSettingsPanelProfile
extends PhabricatorSettingsPanel {
public function processRequest() {
public function getPanelKey() {
return 'profile';
}
$request = $this->getRequest();
public function getPanelName() {
return pht('Profile');
}
public function getPanelGroup() {
return pht('Account Information');
}
public function processRequest(AphrontRequest $request) {
$user = $request->getUser();
$profile = id(new PhabricatorUserProfile())->loadOneWhere(
@ -95,7 +105,7 @@ final class PhabricatorUserProfileSettingsPanelController
$user->save();
$profile->save();
$response = id(new AphrontRedirectResponse())
->setURI('/settings/page/profile/?saved=true');
->setURI($this->getPanelURI('?saved=true'));
return $response;
}
}
@ -143,7 +153,6 @@ final class PhabricatorUserProfileSettingsPanelController
$form = new AphrontFormView();
$form
->setUser($request->getUser())
->setAction('/settings/page/profile/')
->setEncType('multipart/form-data')
->appendChild(
id(new AphrontFormTextControl())
@ -176,7 +185,7 @@ final class PhabricatorUserProfileSettingsPanelController
->appendChild(
'<p class="aphront-form-instructions">Write something about yourself! '.
'Make sure to include <strong>important information</strong> like '.
'your favorite pokemon and which Starcraft race you play.</p>')
'your favorite Pokemon and which Starcraft race you play.</p>')
->appendChild(
id(new AphrontFormTextAreaControl())
->setLabel('Blurb')
@ -207,12 +216,10 @@ final class PhabricatorUserProfileSettingsPanelController
$panel->appendChild($form);
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
return id(new AphrontNullView())
->appendChild(
array(
$error_view,
$panel,
));
return array(
$error_view,
$panel,
);
}
}

View file

@ -16,24 +16,33 @@
* limitations under the License.
*/
final class PhabricatorUserSSHKeysSettingsPanelController
extends PhabricatorUserSettingsPanelController {
final class PhabricatorSettingsPanelSSHKeys
extends PhabricatorSettingsPanel {
const PANEL_BASE_URI = '/settings/page/sshkeys/';
public function getPanelKey() {
return 'ssh';
}
public static function isEnabled() {
public function getPanelName() {
return pht('SSH Public Keys');
}
public function getPanelGroup() {
return pht('Authentication');
}
public function isEnabled() {
return PhabricatorEnv::getEnvConfig('auth.sshkeys.enabled');
}
public function processRequest() {
public function processRequest(AphrontRequest $request) {
$request = $this->getRequest();
$user = $request->getUser();
$edit = $request->getStr('edit');
$delete = $request->getStr('delete');
if (!$edit && !$delete) {
return $this->renderKeyListView();
return $this->renderKeyListView($request);
}
$id = nonempty($edit, $delete);
@ -43,7 +52,7 @@ final class PhabricatorUserSSHKeysSettingsPanelController
$key = id(new PhabricatorUserSSHKey())->loadOneWhere(
'userPHID = %s AND id = %d',
$user->getPHID(),
$id);
(int)$id);
if (!$key) {
return new Aphront404Response();
}
@ -53,7 +62,7 @@ final class PhabricatorUserSSHKeysSettingsPanelController
}
if ($delete) {
return $this->processDelete($key);
return $this->processDelete($request, $key);
}
$e_name = true;
@ -113,7 +122,7 @@ final class PhabricatorUserSSHKeysSettingsPanelController
try {
$key->save();
return id(new AphrontRedirectResponse())
->setURI(self::PANEL_BASE_URI);
->setURI($this->getPanelURI());
} catch (AphrontQueryDuplicateKeyException $ex) {
$e_key = 'Duplicate';
$errors[] = 'This public key is already associated with a user '.
@ -156,7 +165,7 @@ final class PhabricatorUserSSHKeysSettingsPanelController
->setError($e_key))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton(self::PANEL_BASE_URI)
->addCancelButton($this->getPanelURI())
->setValue($save));
$panel = new AphrontPanelView();
@ -172,8 +181,8 @@ final class PhabricatorUserSSHKeysSettingsPanelController
));
}
private function renderKeyListView() {
$request = $this->getRequest();
private function renderKeyListView(AphrontRequest $request) {
$user = $request->getUser();
$keys = id(new PhabricatorUserSSHKey())->loadAllWhere(
@ -186,7 +195,7 @@ final class PhabricatorUserSSHKeysSettingsPanelController
phutil_render_tag(
'a',
array(
'href' => '/settings/page/sshkeys/?edit='.$key->getID(),
'href' => $this->getPanelURI('?edit='.$key->getID()),
),
phutil_escape_html($key->getName())),
phutil_escape_html($key->getKeyComment()),
@ -196,7 +205,7 @@ final class PhabricatorUserSSHKeysSettingsPanelController
javelin_render_tag(
'a',
array(
'href' => '/settings/page/sshkeys/?delete='.$key->getID(),
'href' => $this->getPanelURI('?delete='.$key->getID()),
'class' => 'small grey button',
'sigil' => 'workflow',
),
@ -230,7 +239,7 @@ final class PhabricatorUserSSHKeysSettingsPanelController
phutil_render_tag(
'a',
array(
'href' => '/settings/page/sshkeys/?edit=true',
'href' => $this->getPanelURI('?edit=true'),
'class' => 'green button',
),
'Add New Public Key'));
@ -240,8 +249,10 @@ final class PhabricatorUserSSHKeysSettingsPanelController
return $panel;
}
private function processDelete(PhabricatorUserSSHKey $key) {
$request = $this->getRequest();
private function processDelete(
AphrontRequest $request,
PhabricatorUserSSHKey $key) {
$user = $request->getUser();
$name = phutil_escape_html($key->getName());
@ -249,7 +260,7 @@ final class PhabricatorUserSSHKeysSettingsPanelController
if ($request->isDialogFormPost()) {
$key->delete();
return id(new AphrontReloadResponse())
->setURI(self::PANEL_BASE_URI);
->setURI($this->getPanelURI());
}
$dialog = id(new AphrontDialogView())
@ -261,7 +272,7 @@ final class PhabricatorUserSSHKeysSettingsPanelController
'and you will not longer be able to use the corresponding private key '.
'to authenticate.</p>')
->addSubmitButton('Delete Public Key')
->addCancelButton(self::PANEL_BASE_URI);
->addCancelButton($this->getPanelURI());
return id(new AphrontDialogResponse())
->setDialog($dialog);

View file

@ -16,12 +16,22 @@
* limitations under the License.
*/
final class PhabricatorUserSearchSettingsPanelController
extends PhabricatorUserSettingsPanelController {
final class PhabricatorSettingsPanelSearchPreferences
extends PhabricatorSettingsPanel {
public function processRequest() {
public function getPanelKey() {
return 'search';
}
$request = $this->getRequest();
public function getPanelName() {
return pht('Search Preferences');
}
public function getPanelGroup() {
return pht('Application Settings');
}
public function processRequest(AphrontRequest $request) {
$user = $request->getUser();
$preferences = $user->loadPreferences();
@ -37,12 +47,11 @@ final class PhabricatorUserSearchSettingsPanelController
$preferences->save();
return id(new AphrontRedirectResponse())
->setURI('/settings/page/search/?saved=true');
->setURI($this->getPanelURI('?saved=true'));
}
$form = id(new AphrontFormView())
->setUser($user)
->setAction('/settings/page/search/')
->appendChild(
id(new AphrontFormCheckboxControl())
->addCheckbox($pref_jump,
@ -51,7 +60,7 @@ final class PhabricatorUserSearchSettingsPanelController
$preferences->getPreference($pref_jump, 1))
->addCheckbox($pref_shortcut,
1,
'\'/\' focuses search box.',
"Press '/' to focus the search input.",
$preferences->getPreference($pref_shortcut, 1))
)
->appendChild(
@ -71,12 +80,10 @@ final class PhabricatorUserSearchSettingsPanelController
->setErrors(array('Your preferences have been saved.'));
}
return id(new AphrontNullView())
->appendChild(
array(
$error_view,
$panel,
));
return array(
$error_view,
$panel,
);
}
}

View file

@ -1,26 +0,0 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
abstract class PhabricatorUserSettingsPanelController
extends PhabricatorPeopleController {
public function getAccountEditable() {
return PhabricatorEnv::getEnvConfig('account.editable');
}
}