mirror of
https://we.phorge.it/source/phorge.git
synced 2025-01-09 14:21:02 +01:00
Allow configuration to have custom UI types
Summary: Ref T1703. This sets the stage for (but does not yet implement) custom UI types for config. In particular, a draggable list for custom fields. I might make all the builtin types go through this at some point too, but don't really want to bother for the moment. It would be very slightly cleaner but woudn't get us much of anything. Test Plan: UI now renders via custom code, although that code does nothing (produces an unadorned text field): {F45693} Reviewers: chad Reviewed By: chad CC: aran Maniphest Tasks: T1703 Differential Revision: https://secure.phabricator.com/D6154
This commit is contained in:
parent
77c03a8a42
commit
059183f6b5
7 changed files with 224 additions and 128 deletions
|
@ -894,6 +894,7 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorConfigManagementSetWorkflow' => 'applications/config/management/PhabricatorConfigManagementSetWorkflow.php',
|
'PhabricatorConfigManagementSetWorkflow' => 'applications/config/management/PhabricatorConfigManagementSetWorkflow.php',
|
||||||
'PhabricatorConfigManagementWorkflow' => 'applications/config/management/PhabricatorConfigManagementWorkflow.php',
|
'PhabricatorConfigManagementWorkflow' => 'applications/config/management/PhabricatorConfigManagementWorkflow.php',
|
||||||
'PhabricatorConfigOption' => 'applications/config/option/PhabricatorConfigOption.php',
|
'PhabricatorConfigOption' => 'applications/config/option/PhabricatorConfigOption.php',
|
||||||
|
'PhabricatorConfigOptionType' => 'applications/config/custom/PhabricatorConfigOptionType.php',
|
||||||
'PhabricatorConfigProxySource' => 'infrastructure/env/PhabricatorConfigProxySource.php',
|
'PhabricatorConfigProxySource' => 'infrastructure/env/PhabricatorConfigProxySource.php',
|
||||||
'PhabricatorConfigResponse' => 'applications/config/response/PhabricatorConfigResponse.php',
|
'PhabricatorConfigResponse' => 'applications/config/response/PhabricatorConfigResponse.php',
|
||||||
'PhabricatorConfigSource' => 'infrastructure/env/PhabricatorConfigSource.php',
|
'PhabricatorConfigSource' => 'infrastructure/env/PhabricatorConfigSource.php',
|
||||||
|
@ -918,6 +919,7 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorCrumbsView' => 'view/layout/PhabricatorCrumbsView.php',
|
'PhabricatorCrumbsView' => 'view/layout/PhabricatorCrumbsView.php',
|
||||||
'PhabricatorCursorPagedPolicyAwareQuery' => 'infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php',
|
'PhabricatorCursorPagedPolicyAwareQuery' => 'infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php',
|
||||||
'PhabricatorCustomField' => 'infrastructure/customfield/field/PhabricatorCustomField.php',
|
'PhabricatorCustomField' => 'infrastructure/customfield/field/PhabricatorCustomField.php',
|
||||||
|
'PhabricatorCustomFieldConfigOptionType' => 'infrastructure/customfield/config/PhabricatorCustomFieldConfigOptionType.php',
|
||||||
'PhabricatorCustomFieldDataNotAvailableException' => 'infrastructure/customfield/exception/PhabricatorCustomFieldDataNotAvailableException.php',
|
'PhabricatorCustomFieldDataNotAvailableException' => 'infrastructure/customfield/exception/PhabricatorCustomFieldDataNotAvailableException.php',
|
||||||
'PhabricatorCustomFieldImplementationIncompleteException' => 'infrastructure/customfield/exception/PhabricatorCustomFieldImplementationIncompleteException.php',
|
'PhabricatorCustomFieldImplementationIncompleteException' => 'infrastructure/customfield/exception/PhabricatorCustomFieldImplementationIncompleteException.php',
|
||||||
'PhabricatorCustomFieldIndexStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldIndexStorage.php',
|
'PhabricatorCustomFieldIndexStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldIndexStorage.php',
|
||||||
|
@ -2774,6 +2776,7 @@ phutil_register_library_map(array(
|
||||||
'PhabricatorCrumbView' => 'AphrontView',
|
'PhabricatorCrumbView' => 'AphrontView',
|
||||||
'PhabricatorCrumbsView' => 'AphrontView',
|
'PhabricatorCrumbsView' => 'AphrontView',
|
||||||
'PhabricatorCursorPagedPolicyAwareQuery' => 'PhabricatorPolicyAwareQuery',
|
'PhabricatorCursorPagedPolicyAwareQuery' => 'PhabricatorPolicyAwareQuery',
|
||||||
|
'PhabricatorCustomFieldConfigOptionType' => 'PhabricatorConfigOptionType',
|
||||||
'PhabricatorCustomFieldDataNotAvailableException' => 'Exception',
|
'PhabricatorCustomFieldDataNotAvailableException' => 'Exception',
|
||||||
'PhabricatorCustomFieldImplementationIncompleteException' => 'Exception',
|
'PhabricatorCustomFieldImplementationIncompleteException' => 'Exception',
|
||||||
'PhabricatorCustomFieldIndexStorage' => 'PhabricatorLiskDAO',
|
'PhabricatorCustomFieldIndexStorage' => 'PhabricatorLiskDAO',
|
||||||
|
|
|
@ -236,67 +236,72 @@ final class PhabricatorConfigEditController
|
||||||
return array($e_value, $errors, $value, $xaction);
|
return array($e_value, $errors, $value, $xaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
$type = $option->getType();
|
if ($option->isCustomType()) {
|
||||||
$set_value = null;
|
$info = $option->getCustomObject()->readRequest($option, $request);
|
||||||
|
list($e_value, $errors, $set_value, $value) = $info;
|
||||||
|
} else {
|
||||||
|
$type = $option->getType();
|
||||||
|
$set_value = null;
|
||||||
|
|
||||||
switch ($type) {
|
switch ($type) {
|
||||||
case 'int':
|
case 'int':
|
||||||
if (preg_match('/^-?[0-9]+$/', trim($value))) {
|
if (preg_match('/^-?[0-9]+$/', trim($value))) {
|
||||||
$set_value = (int)$value;
|
$set_value = (int)$value;
|
||||||
} else {
|
|
||||||
$e_value = pht('Invalid');
|
|
||||||
$errors[] = pht('Value must be an integer.');
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'string':
|
|
||||||
case 'enum':
|
|
||||||
$set_value = (string)$value;
|
|
||||||
break;
|
|
||||||
case 'list<string>':
|
|
||||||
$set_value = $request->getStrList('value');
|
|
||||||
break;
|
|
||||||
case 'set':
|
|
||||||
$set_value = array_fill_keys($request->getStrList('value'), true);
|
|
||||||
break;
|
|
||||||
case 'bool':
|
|
||||||
switch ($value) {
|
|
||||||
case 'true':
|
|
||||||
$set_value = true;
|
|
||||||
break;
|
|
||||||
case 'false':
|
|
||||||
$set_value = false;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
$e_value = pht('Invalid');
|
|
||||||
$errors[] = pht('Value must be boolean, "true" or "false".');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'class':
|
|
||||||
if (!class_exists($value)) {
|
|
||||||
$e_value = pht('Invalid');
|
|
||||||
$errors[] = pht('Class does not exist.');
|
|
||||||
} else {
|
|
||||||
$base = $option->getBaseClass();
|
|
||||||
if (!is_subclass_of($value, $base)) {
|
|
||||||
$e_value = pht('Invalid');
|
|
||||||
$errors[] = pht('Class is not of valid type.');
|
|
||||||
} else {
|
} else {
|
||||||
$set_value = $value;
|
$e_value = pht('Invalid');
|
||||||
|
$errors[] = pht('Value must be an integer.');
|
||||||
}
|
}
|
||||||
}
|
break;
|
||||||
break;
|
case 'string':
|
||||||
default:
|
case 'enum':
|
||||||
$json = json_decode($value, true);
|
$set_value = (string)$value;
|
||||||
if ($json === null && strtolower($value) != 'null') {
|
break;
|
||||||
$e_value = pht('Invalid');
|
case 'list<string>':
|
||||||
$errors[] = pht(
|
$set_value = $request->getStrList('value');
|
||||||
'The given value must be valid JSON. This means, among '.
|
break;
|
||||||
'other things, that you must wrap strings in double-quotes.');
|
case 'set':
|
||||||
} else {
|
$set_value = array_fill_keys($request->getStrList('value'), true);
|
||||||
$set_value = $json;
|
break;
|
||||||
}
|
case 'bool':
|
||||||
break;
|
switch ($value) {
|
||||||
|
case 'true':
|
||||||
|
$set_value = true;
|
||||||
|
break;
|
||||||
|
case 'false':
|
||||||
|
$set_value = false;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$e_value = pht('Invalid');
|
||||||
|
$errors[] = pht('Value must be boolean, "true" or "false".');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'class':
|
||||||
|
if (!class_exists($value)) {
|
||||||
|
$e_value = pht('Invalid');
|
||||||
|
$errors[] = pht('Class does not exist.');
|
||||||
|
} else {
|
||||||
|
$base = $option->getBaseClass();
|
||||||
|
if (!is_subclass_of($value, $base)) {
|
||||||
|
$e_value = pht('Invalid');
|
||||||
|
$errors[] = pht('Class is not of valid type.');
|
||||||
|
} else {
|
||||||
|
$set_value = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$json = json_decode($value, true);
|
||||||
|
if ($json === null && strtolower($value) != 'null') {
|
||||||
|
$e_value = pht('Invalid');
|
||||||
|
$errors[] = pht(
|
||||||
|
'The given value must be valid JSON. This means, among '.
|
||||||
|
'other things, that you must wrap strings in double-quotes.');
|
||||||
|
} else {
|
||||||
|
$set_value = $json;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$errors) {
|
if (!$errors) {
|
||||||
|
@ -320,22 +325,26 @@ final class PhabricatorConfigEditController
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$type = $option->getType();
|
if ($option->isCustomType()) {
|
||||||
$value = $entry->getValue();
|
return $option->getCustomObject()->getDisplayValue($option, $entry);
|
||||||
switch ($type) {
|
} else {
|
||||||
case 'int':
|
$type = $option->getType();
|
||||||
case 'string':
|
$value = $entry->getValue();
|
||||||
case 'enum':
|
switch ($type) {
|
||||||
case 'class':
|
case 'int':
|
||||||
return $value;
|
case 'string':
|
||||||
case 'bool':
|
case 'enum':
|
||||||
return $value ? 'true' : 'false';
|
case 'class':
|
||||||
case 'list<string>':
|
return $value;
|
||||||
return implode("\n", nonempty($value, array()));
|
case 'bool':
|
||||||
case 'set':
|
return $value ? 'true' : 'false';
|
||||||
return implode("\n", nonempty(array_keys($value), array()));
|
case 'list<string>':
|
||||||
default:
|
return implode("\n", nonempty($value, array()));
|
||||||
return PhabricatorConfigJSON::prettyPrintJSON($value);
|
case 'set':
|
||||||
|
return implode("\n", nonempty(array_keys($value), array()));
|
||||||
|
default:
|
||||||
|
return PhabricatorConfigJSON::prettyPrintJSON($value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -344,64 +353,71 @@ final class PhabricatorConfigEditController
|
||||||
$display_value,
|
$display_value,
|
||||||
$e_value) {
|
$e_value) {
|
||||||
|
|
||||||
$type = $option->getType();
|
if ($option->isCustomType()) {
|
||||||
switch ($type) {
|
$control = $option->getCustomObject()->renderControl(
|
||||||
case 'int':
|
$option,
|
||||||
case 'string':
|
$display_value,
|
||||||
$control = id(new AphrontFormTextControl());
|
$e_value);
|
||||||
break;
|
} else {
|
||||||
case 'bool':
|
$type = $option->getType();
|
||||||
$control = id(new AphrontFormSelectControl())
|
switch ($type) {
|
||||||
->setOptions(
|
case 'int':
|
||||||
|
case 'string':
|
||||||
|
$control = id(new AphrontFormTextControl());
|
||||||
|
break;
|
||||||
|
case 'bool':
|
||||||
|
$control = id(new AphrontFormSelectControl())
|
||||||
|
->setOptions(
|
||||||
|
array(
|
||||||
|
'' => pht('(Use Default)'),
|
||||||
|
'true' => idx($option->getBoolOptions(), 0),
|
||||||
|
'false' => idx($option->getBoolOptions(), 1),
|
||||||
|
));
|
||||||
|
break;
|
||||||
|
case 'enum':
|
||||||
|
$options = array_mergev(
|
||||||
array(
|
array(
|
||||||
'' => pht('(Use Default)'),
|
array('' => pht('(Use Default)')),
|
||||||
'true' => idx($option->getBoolOptions(), 0),
|
$option->getEnumOptions(),
|
||||||
'false' => idx($option->getBoolOptions(), 1),
|
|
||||||
));
|
));
|
||||||
break;
|
$control = id(new AphrontFormSelectControl())
|
||||||
case 'enum':
|
->setOptions($options);
|
||||||
$options = array_mergev(
|
break;
|
||||||
array(
|
case 'class':
|
||||||
array('' => pht('(Use Default)')),
|
$symbols = id(new PhutilSymbolLoader())
|
||||||
$option->getEnumOptions(),
|
->setType('class')
|
||||||
));
|
->setAncestorClass($option->getBaseClass())
|
||||||
$control = id(new AphrontFormSelectControl())
|
->setConcreteOnly(true)
|
||||||
->setOptions($options);
|
->selectSymbolsWithoutLoading();
|
||||||
break;
|
$names = ipull($symbols, 'name', 'name');
|
||||||
case 'class':
|
asort($names);
|
||||||
$symbols = id(new PhutilSymbolLoader())
|
$names = array(
|
||||||
->setType('class')
|
'' => pht('(Use Default)'),
|
||||||
->setAncestorClass($option->getBaseClass())
|
) + $names;
|
||||||
->setConcreteOnly(true)
|
|
||||||
->selectSymbolsWithoutLoading();
|
|
||||||
$names = ipull($symbols, 'name', 'name');
|
|
||||||
asort($names);
|
|
||||||
$names = array(
|
|
||||||
'' => pht('(Use Default)'),
|
|
||||||
) + $names;
|
|
||||||
|
|
||||||
$control = id(new AphrontFormSelectControl())
|
$control = id(new AphrontFormSelectControl())
|
||||||
->setOptions($names);
|
->setOptions($names);
|
||||||
break;
|
break;
|
||||||
case 'list<string>':
|
case 'list<string>':
|
||||||
case 'set':
|
case 'set':
|
||||||
$control = id(new AphrontFormTextAreaControl())
|
$control = id(new AphrontFormTextAreaControl())
|
||||||
->setCaption(pht('Separate values with newlines or commas.'));
|
->setCaption(pht('Separate values with newlines or commas.'));
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
$control = id(new AphrontFormTextAreaControl())
|
$control = id(new AphrontFormTextAreaControl())
|
||||||
->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL)
|
->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL)
|
||||||
->setCustomClass('PhabricatorMonospaced')
|
->setCustomClass('PhabricatorMonospaced')
|
||||||
->setCaption(pht('Enter value in JSON.'));
|
->setCaption(pht('Enter value in JSON.'));
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$control
|
||||||
|
->setLabel(pht('Value'))
|
||||||
|
->setError($e_value)
|
||||||
|
->setValue($display_value)
|
||||||
|
->setName('value');
|
||||||
}
|
}
|
||||||
|
|
||||||
$control
|
|
||||||
->setLabel(pht('Value'))
|
|
||||||
->setError($e_value)
|
|
||||||
->setValue($display_value)
|
|
||||||
->setName('value');
|
|
||||||
|
|
||||||
if ($option->getLocked()) {
|
if ($option->getLocked()) {
|
||||||
$control->setDisabled(true);
|
$control->setDisabled(true);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
abstract class PhabricatorConfigOptionType {
|
||||||
|
|
||||||
|
public function validateOption(PhabricatorConfigOption $option, $value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function readRequest(
|
||||||
|
PhabricatorConfigOption $option,
|
||||||
|
AphrontRequest $request) {
|
||||||
|
|
||||||
|
$e_value = null;
|
||||||
|
$errors = array();
|
||||||
|
$storage_value = $request->getStr('value');
|
||||||
|
$display_value = $request->getStr('value');
|
||||||
|
|
||||||
|
return array($e_value, $errors, $storage_value, $display_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDisplayValue(
|
||||||
|
PhabricatorConfigOption $option,
|
||||||
|
PhabricatorConfigEntry $entry) {
|
||||||
|
return $entry->getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function renderControl(
|
||||||
|
PhabricatorConfigOption $option,
|
||||||
|
$display_value,
|
||||||
|
$e_value) {
|
||||||
|
|
||||||
|
return id(new AphrontFormTextControl())
|
||||||
|
->setName('value')
|
||||||
|
->setLabel(pht('Value'))
|
||||||
|
->setValue($display_value)
|
||||||
|
->setError($e_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -15,6 +15,10 @@ abstract class PhabricatorApplicationConfigOptions extends Phobject {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($option->isCustomType()) {
|
||||||
|
return $option->getCustomObject()->validateOption($option, $value);
|
||||||
|
}
|
||||||
|
|
||||||
switch ($option->getType()) {
|
switch ($option->getType()) {
|
||||||
case 'bool':
|
case 'bool':
|
||||||
if ($value !== true &&
|
if ($value !== true &&
|
||||||
|
|
|
@ -17,6 +17,8 @@ final class PhabricatorConfigOption
|
||||||
private $hidden;
|
private $hidden;
|
||||||
private $masked;
|
private $masked;
|
||||||
private $baseClass;
|
private $baseClass;
|
||||||
|
private $customData;
|
||||||
|
private $customObject;
|
||||||
|
|
||||||
public function setBaseClass($base_class) {
|
public function setBaseClass($base_class) {
|
||||||
$this->baseClass = $base_class;
|
$this->baseClass = $base_class;
|
||||||
|
@ -178,6 +180,29 @@ final class PhabricatorConfigOption
|
||||||
return $this->type;
|
return $this->type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isCustomType() {
|
||||||
|
return !strncmp($this->getType(), 'custom:', 7);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCustomObject() {
|
||||||
|
if (!$this->customObject) {
|
||||||
|
if (!$this->isCustomType()) {
|
||||||
|
throw new Exception("This option does not have a custom type!");
|
||||||
|
}
|
||||||
|
$this->customObject = newv(substr($this->getType(), 7), array());
|
||||||
|
}
|
||||||
|
return $this->customObject;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCustomData() {
|
||||||
|
return $this->customData;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCustomData($data) {
|
||||||
|
$this->customData = $data;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/* -( PhabricatorMarkupInterface )----------------------------------------- */
|
/* -( PhabricatorMarkupInterface )----------------------------------------- */
|
||||||
|
|
||||||
public function getMarkupFieldKey($field) {
|
public function getMarkupFieldKey($field) {
|
||||||
|
|
|
@ -25,8 +25,11 @@ final class PhabricatorUserConfigOptions
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$custom_field_type = 'custom:PhabricatorCustomFieldConfigOptionType';
|
||||||
|
|
||||||
return array(
|
return array(
|
||||||
$this->newOption('user.fields', 'wild', $default)
|
$this->newOption('user.fields', $custom_field_type, $default)
|
||||||
|
->setCustomData(id(new PhabricatorUser())->getCustomFieldBaseClass())
|
||||||
->setDescription(pht("Select and reorder user profile fields.")),
|
->setDescription(pht("Select and reorder user profile fields.")),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
final class PhabricatorCustomFieldConfigOptionType
|
||||||
|
extends PhabricatorConfigOptionType {
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue