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 @@ -2822,6 +2822,7 @@ 'PhabricatorObjectSelectorDialog' => 'view/control/PhabricatorObjectSelectorDialog.php', 'PhabricatorOffsetPagedQuery' => 'infrastructure/query/PhabricatorOffsetPagedQuery.php', 'PhabricatorOldWorldContentSource' => 'infrastructure/contentsource/PhabricatorOldWorldContentSource.php', + 'PhabricatorOlderInlinesSetting' => 'applications/settings/setting/PhabricatorOlderInlinesSetting.php', 'PhabricatorOneTimeTriggerClock' => 'infrastructure/daemon/workers/clock/PhabricatorOneTimeTriggerClock.php', 'PhabricatorOpcodeCacheSpec' => 'applications/cache/spec/PhabricatorOpcodeCacheSpec.php', 'PhabricatorOwnerPathQuery' => 'applications/owners/query/PhabricatorOwnerPathQuery.php', @@ -3348,11 +3349,15 @@ 'PhabricatorSecurityConfigOptions' => 'applications/config/option/PhabricatorSecurityConfigOptions.php', 'PhabricatorSecuritySetupCheck' => 'applications/config/check/PhabricatorSecuritySetupCheck.php', 'PhabricatorSelectEditField' => 'applications/transactions/editfield/PhabricatorSelectEditField.php', + 'PhabricatorSelectSetting' => 'applications/settings/setting/PhabricatorSelectSetting.php', 'PhabricatorSendGridConfigOptions' => 'applications/config/option/PhabricatorSendGridConfigOptions.php', 'PhabricatorSessionsSettingsPanel' => 'applications/settings/panel/PhabricatorSessionsSettingsPanel.php', + 'PhabricatorSetting' => 'applications/settings/setting/PhabricatorSetting.php', 'PhabricatorSettingsAddEmailAction' => 'applications/settings/action/PhabricatorSettingsAddEmailAction.php', 'PhabricatorSettingsAdjustController' => 'applications/settings/controller/PhabricatorSettingsAdjustController.php', 'PhabricatorSettingsApplication' => 'applications/settings/application/PhabricatorSettingsApplication.php', + 'PhabricatorSettingsEditController' => 'applications/settings/controller/PhabricatorSettingsEditController.php', + 'PhabricatorSettingsEditEngine' => 'applications/settings/editor/PhabricatorSettingsEditEngine.php', 'PhabricatorSettingsMainController' => 'applications/settings/controller/PhabricatorSettingsMainController.php', 'PhabricatorSettingsMainMenuBarExtension' => 'applications/settings/extension/PhabricatorSettingsMainMenuBarExtension.php', 'PhabricatorSettingsPanel' => 'applications/settings/panel/PhabricatorSettingsPanel.php', @@ -3363,6 +3368,7 @@ 'PhabricatorSetupIssueUIExample' => 'applications/uiexample/examples/PhabricatorSetupIssueUIExample.php', 'PhabricatorSetupIssueView' => 'applications/config/view/PhabricatorSetupIssueView.php', 'PhabricatorShortSite' => 'aphront/site/PhabricatorShortSite.php', + 'PhabricatorShowFiletreeSetting' => 'applications/settings/setting/PhabricatorShowFiletreeSetting.php', 'PhabricatorSimpleEditType' => 'applications/transactions/edittype/PhabricatorSimpleEditType.php', 'PhabricatorSite' => 'aphront/site/PhabricatorSite.php', 'PhabricatorSlowvoteApplication' => 'applications/slowvote/application/PhabricatorSlowvoteApplication.php', @@ -3568,6 +3574,7 @@ 'PhabricatorUIExampleRenderController' => 'applications/uiexample/controller/PhabricatorUIExampleRenderController.php', 'PhabricatorUIExamplesApplication' => 'applications/uiexample/application/PhabricatorUIExamplesApplication.php', 'PhabricatorUSEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php', + 'PhabricatorUnifiedDiffsSetting' => 'applications/settings/setting/PhabricatorUnifiedDiffsSetting.php', 'PhabricatorUnitTestContentSource' => 'infrastructure/contentsource/PhabricatorUnitTestContentSource.php', 'PhabricatorUnitsTestCase' => 'view/__tests__/PhabricatorUnitsTestCase.php', 'PhabricatorUnknownContentSource' => 'infrastructure/contentsource/PhabricatorUnknownContentSource.php', @@ -3592,6 +3599,7 @@ 'PhabricatorUserLogView' => 'applications/people/view/PhabricatorUserLogView.php', 'PhabricatorUserPHIDResolver' => 'applications/phid/resolver/PhabricatorUserPHIDResolver.php', 'PhabricatorUserPreferences' => 'applications/settings/storage/PhabricatorUserPreferences.php', + 'PhabricatorUserPreferencesEditor' => 'applications/settings/editor/PhabricatorUserPreferencesEditor.php', 'PhabricatorUserPreferencesPHIDType' => 'applications/settings/phid/PhabricatorUserPreferencesPHIDType.php', 'PhabricatorUserPreferencesQuery' => 'applications/settings/query/PhabricatorUserPreferencesQuery.php', 'PhabricatorUserPreferencesTransaction' => 'applications/settings/storage/PhabricatorUserPreferencesTransaction.php', @@ -7413,6 +7421,7 @@ 'PhabricatorObjectSelectorDialog' => 'Phobject', 'PhabricatorOffsetPagedQuery' => 'PhabricatorQuery', 'PhabricatorOldWorldContentSource' => 'PhabricatorContentSource', + 'PhabricatorOlderInlinesSetting' => 'PhabricatorSelectSetting', 'PhabricatorOneTimeTriggerClock' => 'PhabricatorTriggerClock', 'PhabricatorOpcodeCacheSpec' => 'PhabricatorCacheSpec', 'PhabricatorOwnerPathQuery' => 'Phobject', @@ -8064,11 +8073,15 @@ 'PhabricatorSecurityConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorSecuritySetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorSelectEditField' => 'PhabricatorEditField', + 'PhabricatorSelectSetting' => 'PhabricatorSetting', 'PhabricatorSendGridConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorSessionsSettingsPanel' => 'PhabricatorSettingsPanel', + 'PhabricatorSetting' => 'Phobject', 'PhabricatorSettingsAddEmailAction' => 'PhabricatorSystemAction', 'PhabricatorSettingsAdjustController' => 'PhabricatorController', 'PhabricatorSettingsApplication' => 'PhabricatorApplication', + 'PhabricatorSettingsEditController' => 'PhabricatorController', + 'PhabricatorSettingsEditEngine' => 'PhabricatorEditEngine', 'PhabricatorSettingsMainController' => 'PhabricatorController', 'PhabricatorSettingsMainMenuBarExtension' => 'PhabricatorMainMenuBarExtension', 'PhabricatorSettingsPanel' => 'Phobject', @@ -8079,6 +8092,7 @@ 'PhabricatorSetupIssueUIExample' => 'PhabricatorUIExample', 'PhabricatorSetupIssueView' => 'AphrontView', 'PhabricatorShortSite' => 'PhabricatorSite', + 'PhabricatorShowFiletreeSetting' => 'PhabricatorSelectSetting', 'PhabricatorSimpleEditType' => 'PhabricatorEditType', 'PhabricatorSite' => 'AphrontSite', 'PhabricatorSlowvoteApplication' => 'PhabricatorApplication', @@ -8305,6 +8319,7 @@ 'PhabricatorUIExampleRenderController' => 'PhabricatorController', 'PhabricatorUIExamplesApplication' => 'PhabricatorApplication', 'PhabricatorUSEnglishTranslation' => 'PhutilTranslation', + 'PhabricatorUnifiedDiffsSetting' => 'PhabricatorSelectSetting', 'PhabricatorUnitTestContentSource' => 'PhabricatorContentSource', 'PhabricatorUnitsTestCase' => 'PhabricatorTestCase', 'PhabricatorUnknownContentSource' => 'PhabricatorContentSource', @@ -8351,6 +8366,7 @@ 'PhabricatorDestructibleInterface', 'PhabricatorApplicationTransactionInterface', ), + 'PhabricatorUserPreferencesEditor' => 'AlmanacEditor', 'PhabricatorUserPreferencesPHIDType' => 'PhabricatorPHIDType', 'PhabricatorUserPreferencesQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorUserPreferencesTransaction' => 'PhabricatorApplicationTransaction', diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php --- a/src/applications/people/storage/PhabricatorUser.php +++ b/src/applications/people/storage/PhabricatorUser.php @@ -503,6 +503,7 @@ if (!$preferences) { $preferences = new PhabricatorUserPreferences(); $preferences->setUserPHID($this->getPHID()); + $preferences->attachUser($this); $default_dict = array( PhabricatorUserPreferences::PREFERENCE_TITLES => 'glyph', diff --git a/src/applications/settings/application/PhabricatorSettingsApplication.php b/src/applications/settings/application/PhabricatorSettingsApplication.php --- a/src/applications/settings/application/PhabricatorSettingsApplication.php +++ b/src/applications/settings/application/PhabricatorSettingsApplication.php @@ -34,6 +34,8 @@ 'adjust/' => 'PhabricatorSettingsAdjustController', 'timezone/(?P[^/]+)/' => 'PhabricatorSettingsTimezoneController', + '(?Puser)/(?P[^/]+)/(?:panel/(?P[^/]+)/)?' + => 'PhabricatorSettingsEditController', ), ); } diff --git a/src/applications/settings/controller/PhabricatorSettingsEditController.php b/src/applications/settings/controller/PhabricatorSettingsEditController.php new file mode 100644 --- /dev/null +++ b/src/applications/settings/controller/PhabricatorSettingsEditController.php @@ -0,0 +1,35 @@ +getViewer(); + + $engine = id(new PhabricatorSettingsEditEngine()) + ->setController($this); + + switch ($request->getURIData('type')) { + case 'user': + $user = id(new PhabricatorPeopleQuery()) + ->setViewer($viewer) + ->withUsernames(array($request->getURIData('username'))) + ->executeOne(); + + $preferences = $user->loadPreferences(); + + PhabricatorPolicyFilter::requireCapability( + $viewer, + $preferences, + PhabricatorPolicyCapability::CAN_EDIT); + + $engine->setTargetObject($preferences); + break; + default: + return new Aphront404Response(); + } + + return $engine->buildResponse(); + } + +} diff --git a/src/applications/settings/editor/PhabricatorSettingsEditEngine.php b/src/applications/settings/editor/PhabricatorSettingsEditEngine.php new file mode 100644 --- /dev/null +++ b/src/applications/settings/editor/PhabricatorSettingsEditEngine.php @@ -0,0 +1,91 @@ +getViewer()->getUsername().'/'; + } + + protected function getCreateNewObjectPolicy() { + return PhabricatorPolicies::POLICY_ADMIN; + } + + protected function buildCustomEditFields($object) { + $viewer = $this->getViewer(); + $settings = PhabricatorSetting::getAllEnabledSettings($viewer); + + $fields = array(); + foreach ($settings as $setting) { + foreach ($setting->newCustomEditFields($object) as $field) { + $fields[] = $field; + } + } + + return $fields; + } + +} diff --git a/src/applications/settings/editor/PhabricatorUserPreferencesEditor.php b/src/applications/settings/editor/PhabricatorUserPreferencesEditor.php new file mode 100644 --- /dev/null +++ b/src/applications/settings/editor/PhabricatorUserPreferencesEditor.php @@ -0,0 +1,132 @@ +getMetadataValue( + PhabricatorUserPreferencesTransaction::PROPERTY_SETTING); + + switch ($xaction->getTransactionType()) { + case PhabricatorUserPreferencesTransaction::TYPE_SETTING: + return $object->getPreference($setting_key); + } + + return parent::getCustomTransactionOldValue($object, $xaction); + } + + protected function getCustomTransactionNewValue( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + $actor = $this->getActor(); + + $setting_key = $xaction->getMetadataValue( + PhabricatorUserPreferencesTransaction::PROPERTY_SETTING); + + $settings = PhabricatorSetting::getAllEnabledSettings($actor); + $setting = $settings[$setting_key]; + + switch ($xaction->getTransactionType()) { + case PhabricatorUserPreferencesTransaction::TYPE_SETTING: + $value = $xaction->getNewValue(); + $value = $setting->getTransactionNewValue($value); + return $value; + } + + return parent::getCustomTransactionNewValue($object, $xaction); + } + + protected function applyCustomInternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + $setting_key = $xaction->getMetadataValue( + PhabricatorUserPreferencesTransaction::PROPERTY_SETTING); + + switch ($xaction->getTransactionType()) { + case PhabricatorUserPreferencesTransaction::TYPE_SETTING: + $new_value = $xaction->getNewValue(); + if ($new_value === null) { + $object->unsetPreference($setting_key); + } else { + $object->setPreference($setting_key, $new_value); + } + return; + } + + return parent::applyCustomInternalTransaction($object, $xaction); + } + + protected function applyCustomExternalTransaction( + PhabricatorLiskDAO $object, + PhabricatorApplicationTransaction $xaction) { + + switch ($xaction->getTransactionType()) { + case PhabricatorUserPreferencesTransaction::TYPE_SETTING: + return; + } + + return parent::applyCustomExternalTransaction($object, $xaction); + } + + protected function validateTransaction( + PhabricatorLiskDAO $object, + $type, + array $xactions) { + + $errors = parent::validateTransaction($object, $type, $xactions); + + $actor = $this->getActor(); + $settings = PhabricatorSetting::getAllEnabledSettings($actor); + + switch ($type) { + case PhabricatorUserPreferencesTransaction::TYPE_SETTING: + foreach ($xactions as $xaction) { + $setting_key = $xaction->getMetadataValue( + PhabricatorUserPreferencesTransaction::PROPERTY_SETTING); + + $setting = idx($settings, $setting_key); + if (!$setting) { + $errors[] = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Invalid'), + pht( + 'There is no known application setting with key "%s".', + $setting_key), + $xaction); + continue; + } + + try { + $setting->validateTransactionValue($xaction->getNewValue()); + } catch (Exception $ex) { + $errors[] = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Invalid'), + $ex->getMessage(), + $xaction); + } + } + break; + } + + return $errors; + } + +} diff --git a/src/applications/settings/phid/PhabricatorUserPreferencesPHIDType.php b/src/applications/settings/phid/PhabricatorUserPreferencesPHIDType.php --- a/src/applications/settings/phid/PhabricatorUserPreferencesPHIDType.php +++ b/src/applications/settings/phid/PhabricatorUserPreferencesPHIDType.php @@ -32,7 +32,6 @@ $viewer = $query->getViewer(); foreach ($handles as $phid => $handle) { $preferences = $objects[$phid]; - $handle->setName(pht('Settings %d', $preferences->getID())); } } diff --git a/src/applications/settings/setting/PhabricatorOlderInlinesSetting.php b/src/applications/settings/setting/PhabricatorOlderInlinesSetting.php new file mode 100644 --- /dev/null +++ b/src/applications/settings/setting/PhabricatorOlderInlinesSetting.php @@ -0,0 +1,35 @@ + pht('Enabled'), + self::VALUE_GHOST_INLINES_DISABLED => pht('Disabled'), + ); + } + + +} diff --git a/src/applications/settings/setting/PhabricatorSelectSetting.php b/src/applications/settings/setting/PhabricatorSelectSetting.php new file mode 100644 --- /dev/null +++ b/src/applications/settings/setting/PhabricatorSelectSetting.php @@ -0,0 +1,55 @@ +getSettingKey(); + $default_value = $object->getDefaultValue($setting_key); + + $options = $this->getSelectOptions(); + + if (isset($options[$default_value])) { + $default_label = pht('Default (%s)', $options[$default_value]); + } else { + $default_label = pht('Default (Unknown, "%s")', $default_value); + } + + $options = array( + '' => $default_label, + ) + $options; + + return $this->newEditField($object, new PhabricatorSelectEditField()) + ->setOptions($options); + } + + final public function validateTransactionValue($value) { + if (!strlen($value)) { + return; + } + + $options = $this->getSelectOptions(); + + if (!isset($options[$value])) { + throw new Exception( + pht( + 'Value "%s" is not valid for setting "%s": valid values are %s.', + $value, + $this->getSettingName(), + implode(', ', array_keys($options)))); + } + + return; + } + + public function getTransactionNewValue($value) { + if (!strlen($value)) { + return null; + } + + return (string)$value; + } + +} diff --git a/src/applications/settings/setting/PhabricatorSetting.php b/src/applications/settings/setting/PhabricatorSetting.php new file mode 100644 --- /dev/null +++ b/src/applications/settings/setting/PhabricatorSetting.php @@ -0,0 +1,96 @@ +viewer = $viewer; + return $this; + } + + public function getViewer() { + return $this->viewer; + } + + abstract public function getSettingName(); + + protected function getControlInstructions() { + return null; + } + + protected function isEnabledForViewer(PhabricatorUser $viewer) { + return true; + } + + public function getSettingDefaultValue() { + return null; + } + + final public function getSettingKey() { + return $this->getPhobjectClassConstant('SETTINGKEY'); + } + + public static function getAllSettings() { + return id(new PhutilClassMapQuery()) + ->setAncestorClass(__CLASS__) + ->setUniqueMethod('getSettingKey') + ->execute(); + } + + public static function getAllEnabledSettings(PhabricatorUser $viewer) { + $settings = self::getAllSettings(); + foreach ($settings as $key => $setting) { + if (!$setting->isEnabledForViewer($viewer)) { + unset($settings[$key]); + } + } + return $settings; + } + + final public function newCustomEditFields($object) { + $fields = array(); + + $field = $this->newCustomEditField($object); + if ($field) { + $fields[] = $field; + } + + return $fields; + } + + protected function newCustomEditField($object) { + return null; + } + + protected function newEditField($object, PhabricatorEditField $template) { + $setting_property = PhabricatorUserPreferencesTransaction::PROPERTY_SETTING; + $setting_key = $this->getSettingKey(); + $value = $object->getPreference($setting_key); + $xaction_type = PhabricatorUserPreferencesTransaction::TYPE_SETTING; + $label = $this->getSettingName(); + + $template + ->setKey($setting_key) + ->setLabel($label) + ->setValue($value) + ->setTransactionType($xaction_type) + ->setMetadataValue($setting_property, $setting_key); + + $instructions = $this->getControlInstructions(); + if (strlen($instructions)) { + $template->setControlInstructions($instructions); + } + + return $template; + } + + public function validateTransactionValue($value) { + return; + } + + public function getTransactionNewValue($value) { + return $value; + } + +} diff --git a/src/applications/settings/setting/PhabricatorShowFiletreeSetting.php b/src/applications/settings/setting/PhabricatorShowFiletreeSetting.php new file mode 100644 --- /dev/null +++ b/src/applications/settings/setting/PhabricatorShowFiletreeSetting.php @@ -0,0 +1,34 @@ + pht('Disable Filetree'), + self::VALUE_ENABLE_FILETREE => pht('Enable Filetree'), + ); + } + +} diff --git a/src/applications/settings/setting/PhabricatorUnifiedDiffsSetting.php b/src/applications/settings/setting/PhabricatorUnifiedDiffsSetting.php new file mode 100644 --- /dev/null +++ b/src/applications/settings/setting/PhabricatorUnifiedDiffsSetting.php @@ -0,0 +1,35 @@ + pht('On Small Screens'), + self::VALUE_ALWAYS_UNIFIED => pht('Always'), + ); + } + + +} diff --git a/src/applications/settings/storage/PhabricatorUserPreferences.php b/src/applications/settings/storage/PhabricatorUserPreferences.php --- a/src/applications/settings/storage/PhabricatorUserPreferences.php +++ b/src/applications/settings/storage/PhabricatorUserPreferences.php @@ -92,6 +92,21 @@ return $this; } + public function getDefaultValue($key) { + $setting = self::getSettingObject($key); + + if (!$setting) { + return null; + } + + return $setting->getSettingDefaultValue(); + } + + private static function getSettingObject($key) { + $settings = PhabricatorSetting::getAllSettings(); + return idx($settings, $key); + } + public function getPinnedApplications(array $apps, PhabricatorUser $viewer) { $pref_pinned = self::PREFERENCE_APP_PINNED; $pinned = $this->getPreference($pref_pinned); @@ -212,8 +227,7 @@ public function getApplicationTransactionEditor() { - // TODO: Implement. - throw new PhutilMethodNotImplementedException(); + return new PhabricatorUserPreferencesEditor(); } public function getApplicationTransactionObject() { diff --git a/src/applications/settings/storage/PhabricatorUserPreferencesTransaction.php b/src/applications/settings/storage/PhabricatorUserPreferencesTransaction.php --- a/src/applications/settings/storage/PhabricatorUserPreferencesTransaction.php +++ b/src/applications/settings/storage/PhabricatorUserPreferencesTransaction.php @@ -3,6 +3,10 @@ final class PhabricatorUserPreferencesTransaction extends PhabricatorApplicationTransaction { + const TYPE_SETTING = 'setting'; + + const PROPERTY_SETTING = 'setting.key'; + public function getApplicationName() { return 'user'; } diff --git a/src/view/form/control/AphrontFormSelectControl.php b/src/view/form/control/AphrontFormSelectControl.php --- a/src/view/form/control/AphrontFormSelectControl.php +++ b/src/view/form/control/AphrontFormSelectControl.php @@ -56,6 +56,7 @@ $disabled = array_fuse($disabled); $tags = array(); + $already_selected = false; foreach ($options as $value => $thing) { if (is_array($thing)) { $tags[] = phutil_tag( @@ -65,10 +66,22 @@ ), self::renderOptions($selected, $thing)); } else { + // When there are a list of options including similar values like + // "0" and "" (the empty string), only select the first matching + // value. Ideally this should be more precise about matching, but we + // have 2,000 of these controls at this point so hold that for a + // broader rewrite. + if (!$already_selected && ($value == $selected)) { + $is_selected = 'selected'; + $already_selected = true; + } else { + $is_selected = null; + } + $tags[] = phutil_tag( 'option', array( - 'selected' => ($value == $selected) ? 'selected' : null, + 'selected' => $is_selected, 'value' => $value, 'disabled' => isset($disabled[$value]) ? 'disabled' : null, ),