Skip to content

Commit 059183f

Browse files
author
epriestley
committed
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
1 parent 77c03a8 commit 059183f

File tree

7 files changed

+224
-128
lines changed

7 files changed

+224
-128
lines changed

src/__phutil_library_map__.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -894,6 +894,7 @@
894894
'PhabricatorConfigManagementSetWorkflow' => 'applications/config/management/PhabricatorConfigManagementSetWorkflow.php',
895895
'PhabricatorConfigManagementWorkflow' => 'applications/config/management/PhabricatorConfigManagementWorkflow.php',
896896
'PhabricatorConfigOption' => 'applications/config/option/PhabricatorConfigOption.php',
897+
'PhabricatorConfigOptionType' => 'applications/config/custom/PhabricatorConfigOptionType.php',
897898
'PhabricatorConfigProxySource' => 'infrastructure/env/PhabricatorConfigProxySource.php',
898899
'PhabricatorConfigResponse' => 'applications/config/response/PhabricatorConfigResponse.php',
899900
'PhabricatorConfigSource' => 'infrastructure/env/PhabricatorConfigSource.php',
@@ -918,6 +919,7 @@
918919
'PhabricatorCrumbsView' => 'view/layout/PhabricatorCrumbsView.php',
919920
'PhabricatorCursorPagedPolicyAwareQuery' => 'infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php',
920921
'PhabricatorCustomField' => 'infrastructure/customfield/field/PhabricatorCustomField.php',
922+
'PhabricatorCustomFieldConfigOptionType' => 'infrastructure/customfield/config/PhabricatorCustomFieldConfigOptionType.php',
921923
'PhabricatorCustomFieldDataNotAvailableException' => 'infrastructure/customfield/exception/PhabricatorCustomFieldDataNotAvailableException.php',
922924
'PhabricatorCustomFieldImplementationIncompleteException' => 'infrastructure/customfield/exception/PhabricatorCustomFieldImplementationIncompleteException.php',
923925
'PhabricatorCustomFieldIndexStorage' => 'infrastructure/customfield/storage/PhabricatorCustomFieldIndexStorage.php',
@@ -2774,6 +2776,7 @@
27742776
'PhabricatorCrumbView' => 'AphrontView',
27752777
'PhabricatorCrumbsView' => 'AphrontView',
27762778
'PhabricatorCursorPagedPolicyAwareQuery' => 'PhabricatorPolicyAwareQuery',
2779+
'PhabricatorCustomFieldConfigOptionType' => 'PhabricatorConfigOptionType',
27772780
'PhabricatorCustomFieldDataNotAvailableException' => 'Exception',
27782781
'PhabricatorCustomFieldImplementationIncompleteException' => 'Exception',
27792782
'PhabricatorCustomFieldIndexStorage' => 'PhabricatorLiskDAO',

src/applications/config/controller/PhabricatorConfigEditController.php

Lines changed: 143 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -236,67 +236,72 @@ private function readRequest(
236236
return array($e_value, $errors, $value, $xaction);
237237
}
238238

239-
$type = $option->getType();
240-
$set_value = null;
241-
242-
switch ($type) {
243-
case 'int':
244-
if (preg_match('/^-?[0-9]+$/', trim($value))) {
245-
$set_value = (int)$value;
246-
} else {
247-
$e_value = pht('Invalid');
248-
$errors[] = pht('Value must be an integer.');
249-
}
250-
break;
251-
case 'string':
252-
case 'enum':
253-
$set_value = (string)$value;
254-
break;
255-
case 'list<string>':
256-
$set_value = $request->getStrList('value');
257-
break;
258-
case 'set':
259-
$set_value = array_fill_keys($request->getStrList('value'), true);
260-
break;
261-
case 'bool':
262-
switch ($value) {
263-
case 'true':
264-
$set_value = true;
265-
break;
266-
case 'false':
267-
$set_value = false;
268-
break;
269-
default:
239+
if ($option->isCustomType()) {
240+
$info = $option->getCustomObject()->readRequest($option, $request);
241+
list($e_value, $errors, $set_value, $value) = $info;
242+
} else {
243+
$type = $option->getType();
244+
$set_value = null;
245+
246+
switch ($type) {
247+
case 'int':
248+
if (preg_match('/^-?[0-9]+$/', trim($value))) {
249+
$set_value = (int)$value;
250+
} else {
270251
$e_value = pht('Invalid');
271-
$errors[] = pht('Value must be boolean, "true" or "false".');
272-
break;
273-
}
274-
break;
275-
case 'class':
276-
if (!class_exists($value)) {
277-
$e_value = pht('Invalid');
278-
$errors[] = pht('Class does not exist.');
279-
} else {
280-
$base = $option->getBaseClass();
281-
if (!is_subclass_of($value, $base)) {
252+
$errors[] = pht('Value must be an integer.');
253+
}
254+
break;
255+
case 'string':
256+
case 'enum':
257+
$set_value = (string)$value;
258+
break;
259+
case 'list<string>':
260+
$set_value = $request->getStrList('value');
261+
break;
262+
case 'set':
263+
$set_value = array_fill_keys($request->getStrList('value'), true);
264+
break;
265+
case 'bool':
266+
switch ($value) {
267+
case 'true':
268+
$set_value = true;
269+
break;
270+
case 'false':
271+
$set_value = false;
272+
break;
273+
default:
274+
$e_value = pht('Invalid');
275+
$errors[] = pht('Value must be boolean, "true" or "false".');
276+
break;
277+
}
278+
break;
279+
case 'class':
280+
if (!class_exists($value)) {
282281
$e_value = pht('Invalid');
283-
$errors[] = pht('Class is not of valid type.');
282+
$errors[] = pht('Class does not exist.');
284283
} else {
285-
$set_value = $value;
284+
$base = $option->getBaseClass();
285+
if (!is_subclass_of($value, $base)) {
286+
$e_value = pht('Invalid');
287+
$errors[] = pht('Class is not of valid type.');
288+
} else {
289+
$set_value = $value;
290+
}
286291
}
287-
}
288-
break;
289-
default:
290-
$json = json_decode($value, true);
291-
if ($json === null && strtolower($value) != 'null') {
292-
$e_value = pht('Invalid');
293-
$errors[] = pht(
294-
'The given value must be valid JSON. This means, among '.
295-
'other things, that you must wrap strings in double-quotes.');
296-
} else {
297-
$set_value = $json;
298-
}
299-
break;
292+
break;
293+
default:
294+
$json = json_decode($value, true);
295+
if ($json === null && strtolower($value) != 'null') {
296+
$e_value = pht('Invalid');
297+
$errors[] = pht(
298+
'The given value must be valid JSON. This means, among '.
299+
'other things, that you must wrap strings in double-quotes.');
300+
} else {
301+
$set_value = $json;
302+
}
303+
break;
304+
}
300305
}
301306

302307
if (!$errors) {
@@ -320,22 +325,26 @@ private function getDisplayValue(
320325
return null;
321326
}
322327

323-
$type = $option->getType();
324-
$value = $entry->getValue();
325-
switch ($type) {
326-
case 'int':
327-
case 'string':
328-
case 'enum':
329-
case 'class':
330-
return $value;
331-
case 'bool':
332-
return $value ? 'true' : 'false';
333-
case 'list<string>':
334-
return implode("\n", nonempty($value, array()));
335-
case 'set':
336-
return implode("\n", nonempty(array_keys($value), array()));
337-
default:
338-
return PhabricatorConfigJSON::prettyPrintJSON($value);
328+
if ($option->isCustomType()) {
329+
return $option->getCustomObject()->getDisplayValue($option, $entry);
330+
} else {
331+
$type = $option->getType();
332+
$value = $entry->getValue();
333+
switch ($type) {
334+
case 'int':
335+
case 'string':
336+
case 'enum':
337+
case 'class':
338+
return $value;
339+
case 'bool':
340+
return $value ? 'true' : 'false';
341+
case 'list<string>':
342+
return implode("\n", nonempty($value, array()));
343+
case 'set':
344+
return implode("\n", nonempty(array_keys($value), array()));
345+
default:
346+
return PhabricatorConfigJSON::prettyPrintJSON($value);
347+
}
339348
}
340349
}
341350

@@ -344,63 +353,70 @@ private function renderControl(
344353
$display_value,
345354
$e_value) {
346355

347-
$type = $option->getType();
348-
switch ($type) {
349-
case 'int':
350-
case 'string':
351-
$control = id(new AphrontFormTextControl());
352-
break;
353-
case 'bool':
354-
$control = id(new AphrontFormSelectControl())
355-
->setOptions(
356+
if ($option->isCustomType()) {
357+
$control = $option->getCustomObject()->renderControl(
358+
$option,
359+
$display_value,
360+
$e_value);
361+
} else {
362+
$type = $option->getType();
363+
switch ($type) {
364+
case 'int':
365+
case 'string':
366+
$control = id(new AphrontFormTextControl());
367+
break;
368+
case 'bool':
369+
$control = id(new AphrontFormSelectControl())
370+
->setOptions(
371+
array(
372+
'' => pht('(Use Default)'),
373+
'true' => idx($option->getBoolOptions(), 0),
374+
'false' => idx($option->getBoolOptions(), 1),
375+
));
376+
break;
377+
case 'enum':
378+
$options = array_mergev(
356379
array(
357-
'' => pht('(Use Default)'),
358-
'true' => idx($option->getBoolOptions(), 0),
359-
'false' => idx($option->getBoolOptions(), 1),
380+
array('' => pht('(Use Default)')),
381+
$option->getEnumOptions(),
360382
));
361-
break;
362-
case 'enum':
363-
$options = array_mergev(
364-
array(
365-
array('' => pht('(Use Default)')),
366-
$option->getEnumOptions(),
367-
));
368-
$control = id(new AphrontFormSelectControl())
369-
->setOptions($options);
370-
break;
371-
case 'class':
372-
$symbols = id(new PhutilSymbolLoader())
373-
->setType('class')
374-
->setAncestorClass($option->getBaseClass())
375-
->setConcreteOnly(true)
376-
->selectSymbolsWithoutLoading();
377-
$names = ipull($symbols, 'name', 'name');
378-
asort($names);
379-
$names = array(
380-
'' => pht('(Use Default)'),
381-
) + $names;
382-
383-
$control = id(new AphrontFormSelectControl())
384-
->setOptions($names);
385-
break;
386-
case 'list<string>':
387-
case 'set':
388-
$control = id(new AphrontFormTextAreaControl())
389-
->setCaption(pht('Separate values with newlines or commas.'));
390-
break;
391-
default:
392-
$control = id(new AphrontFormTextAreaControl())
393-
->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL)
394-
->setCustomClass('PhabricatorMonospaced')
395-
->setCaption(pht('Enter value in JSON.'));
396-
break;
397-
}
383+
$control = id(new AphrontFormSelectControl())
384+
->setOptions($options);
385+
break;
386+
case 'class':
387+
$symbols = id(new PhutilSymbolLoader())
388+
->setType('class')
389+
->setAncestorClass($option->getBaseClass())
390+
->setConcreteOnly(true)
391+
->selectSymbolsWithoutLoading();
392+
$names = ipull($symbols, 'name', 'name');
393+
asort($names);
394+
$names = array(
395+
'' => pht('(Use Default)'),
396+
) + $names;
397+
398+
$control = id(new AphrontFormSelectControl())
399+
->setOptions($names);
400+
break;
401+
case 'list<string>':
402+
case 'set':
403+
$control = id(new AphrontFormTextAreaControl())
404+
->setCaption(pht('Separate values with newlines or commas.'));
405+
break;
406+
default:
407+
$control = id(new AphrontFormTextAreaControl())
408+
->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL)
409+
->setCustomClass('PhabricatorMonospaced')
410+
->setCaption(pht('Enter value in JSON.'));
411+
break;
412+
}
398413

399-
$control
400-
->setLabel(pht('Value'))
401-
->setError($e_value)
402-
->setValue($display_value)
403-
->setName('value');
414+
$control
415+
->setLabel(pht('Value'))
416+
->setError($e_value)
417+
->setValue($display_value)
418+
->setName('value');
419+
}
404420

405421
if ($option->getLocked()) {
406422
$control->setDisabled(true);
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
abstract class PhabricatorConfigOptionType {
4+
5+
public function validateOption(PhabricatorConfigOption $option, $value) {
6+
return;
7+
}
8+
9+
public function readRequest(
10+
PhabricatorConfigOption $option,
11+
AphrontRequest $request) {
12+
13+
$e_value = null;
14+
$errors = array();
15+
$storage_value = $request->getStr('value');
16+
$display_value = $request->getStr('value');
17+
18+
return array($e_value, $errors, $storage_value, $display_value);
19+
}
20+
21+
public function getDisplayValue(
22+
PhabricatorConfigOption $option,
23+
PhabricatorConfigEntry $entry) {
24+
return $entry->getValue();
25+
}
26+
27+
public function renderControl(
28+
PhabricatorConfigOption $option,
29+
$display_value,
30+
$e_value) {
31+
32+
return id(new AphrontFormTextControl())
33+
->setName('value')
34+
->setLabel(pht('Value'))
35+
->setValue($display_value)
36+
->setError($e_value);
37+
}
38+
39+
}

src/applications/config/option/PhabricatorApplicationConfigOptions.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ public function validateOption(PhabricatorConfigOption $option, $value) {
1515
return;
1616
}
1717

18+
if ($option->isCustomType()) {
19+
return $option->getCustomObject()->validateOption($option, $value);
20+
}
21+
1822
switch ($option->getType()) {
1923
case 'bool':
2024
if ($value !== true &&

0 commit comments

Comments
 (0)