Skip to content
Snippets Groups Projects
Commit 224fbb38 authored by Klaus Purer's avatar Klaus Purer
Browse files

Issue #2648326: added nested autocomplete suggestions in the UI

parent 24fc14bb
No related branches found
No related tags found
No related merge requests found
......@@ -59,6 +59,24 @@
return false;
}
/**
* Handles the autocomplete selection event.
*
* Restarts autocompleting when the selection ends in a dot, for nested data
* selectors.
*
* @param {object} event
* The event object.
* @param {object} ui
* The UI object holding the selected value.
*/
function selectHandler(event, ui) {
var input_value = ui.item.value;
if (input_value.substr(input_value.length - 1) === '.') {
$(event.target).trigger('keydown');
}
}
/**
* Override jQuery UI _renderItem function to output HTML by default.
*
......@@ -141,6 +159,7 @@
options: {
source: sourceData,
focus: focusHandler,
select: selectHandler,
renderItem: renderItem,
minLength: 0
},
......
......@@ -45,11 +45,6 @@ class AutocompleteController {
$string = $request->query->get('q');
$results = $component->autocomplete($string, $nested_expression);
// @todo the API should return the formatted results.
$results = array_map(function ($value) {
return ['value' => $value, 'label' => $value];
}, $results);
return new JsonResponse($results);
}
......
......@@ -333,8 +333,12 @@ class RulesComponent {
* The expression in which the autocompletion will be executed. All
* variables in the exection metadata state up to that point are available.
*
* @return string[]
* An array of autocomplete suggestions.
* @return array[]
* A list of autocomplete suggestions - valid property paths for one of the
* provided data definitions. Each entry is an array with the following
* keys:
* - value: the data selecor property path.
* - label: the human readable label suggestion.
*/
public function autocomplete($partial_selector, ExpressionInterface $until = NULL) {
// We use the integrity check to populate the execution metadata state with
......
......@@ -177,7 +177,7 @@ class DataFetcher implements DataFetcherInterface {
// them directly.
foreach ($data_definitions as $variable_name => $data_definition) {
if (stripos($variable_name, $partial_property_path) === 0) {
$results[] = $variable_name;
$results = array_merge($results, $this->getAutocompleteSuggestion($data_definition, $variable_name));
}
}
if (!empty($results)) {
......@@ -221,14 +221,14 @@ class DataFetcher implements DataFetcherInterface {
&& $variable_definition->getFieldStorageDefinition()->getCardinality() === 1)
) {
if ($middle_path === '') {
$results[] = "$first_part.0";
$results[] = "$first_part.1";
$results[] = "$first_part.2";
$property_path = $first_part;
}
else {
$results[] = "$first_part.$middle_path.0";
$results[] = "$first_part.$middle_path.1";
$results[] = "$first_part.$middle_path.2";
$property_path = "$first_part.$middle_path";
}
$item_definition = $variable_definition->getItemDefinition();
for ($i = 0; $i < 3; $i++) {
$results = array_merge($results, $this->getAutocompleteSuggestion($item_definition, "$property_path.$i"));
}
}
......@@ -244,18 +244,54 @@ class DataFetcher implements DataFetcherInterface {
// If the property starts with the part then we have a suggestion. If
// the part after the dot is the empty string we include all properties.
if (stripos($property_name, $last_part) === 0 || $last_part === '') {
if ($middle_path === '') {
$results[] = "$first_part.$property_name";
$property_path = "$first_part.$property_name";
}
else {
$results[] = "$first_part.$middle_path.$property_name";
$property_path = "$first_part.$middle_path.$property_name";
}
$results = array_merge($results, $this->getAutocompleteSuggestion($property_definition, $property_path));
}
}
}
natsort($results);
return array_values($results);
usort($results, function ($a, $b) {
return strnatcasecmp($a['value'], $b['value']);
});
return $results;
}
/**
* Generates autocomplete suggestions for a matched data definition.
*
* @param DataDefinitionInterface $data_definition
* The data definition to inspect.
* @param string $variable_name
* The variable name or property path.
*
* @return array[]
* A list of autocomplete suggestions - valid property paths for the
* provided data definition. Each entry is an array with the following keys:
* - value: the data selecor property path.
* - label: the human readable label suggestion.
*/
protected function getAutocompleteSuggestion(DataDefinitionInterface $data_definition, $variable_name) {
$results[] = ['value' => $variable_name, 'label' => $variable_name];
// If the data definition is just a reference then directly dereference the
// target.
if ($data_definition instanceof DataReferenceDefinitionInterface) {
$data_definition = $data_definition->getTargetDefinition();
}
if ($data_definition instanceof ListDataDefinitionInterface
|| $data_definition instanceof ComplexDataDefinitionInterface
) {
$results[] = ['value' => "$variable_name.", 'label' => "$variable_name..."];
}
return $results;
}
/**
......
......@@ -113,9 +113,12 @@ interface DataFetcherInterface {
* @param string $partial_property_path
* The partial property path, example: "node.uid.ent".
*
* @return string[]
* @return array[]
* A list of autocomplete suggestions - valid property paths for one of the
* provided data definitions.
* provided data definitions. Each entry is an array with the following
* keys:
* - value: the data selecor property path.
* - label: the human readable label suggestion.
*/
public function autocompletePropertyPath(array $data_definitions, $partial_property_path);
......
......@@ -65,7 +65,16 @@ class AutocompleteTest extends RulesDrupalTestBase {
->addContextDefinition('entity', ContextDefinition::create('entity'))
->autocomplete('e', $action);
$this->assertSame(['entity'], $results);
$this->assertSame([
[
'value' => 'entity',
'label' => 'entity',
],
[
'value' => 'entity.',
'label' => 'entity...',
],
], $results);
}
/**
......@@ -80,49 +89,233 @@ class AutocompleteTest extends RulesDrupalTestBase {
// Tests that "node.uid.en" returns the suggestion "node.uid.entity".
$results = $component->autocomplete('node.uid.en');
$this->assertSame(['node.uid.entity'], $results);
$this->assertSame([
[
'value' => 'node.uid.entity',
'label' => 'node.uid.entity',
],
[
'value' => 'node.uid.entity.',
'label' => 'node.uid.entity...',
],
], $results);
// Tests that "node." returns all available fields on a node.
$results = $component->autocomplete('node.');
$expected = [
'node.changed',
'node.created',
'node.default_langcode',
'node.field_integer',
'node.langcode',
'node.nid',
'node.promote',
'node.revision_log',
'node.revision_timestamp',
'node.revision_translation_affected',
'node.revision_uid',
'node.status',
'node.sticky',
'node.title',
'node.type',
'node.uid',
'node.uuid',
'node.vid',
[
'value' => 'node.changed',
'label' => 'node.changed',
],
[
'value' => 'node.changed.',
'label' => 'node.changed...',
],
[
'value' => 'node.created',
'label' => 'node.created',
],
[
'value' => 'node.created.',
'label' => 'node.created...',
],
[
'value' => 'node.default_langcode',
'label' => 'node.default_langcode',
],
[
'value' => 'node.default_langcode.',
'label' => 'node.default_langcode...',
],
[
'value' => 'node.field_integer',
'label' => 'node.field_integer',
],
[
'value' => 'node.field_integer.',
'label' => 'node.field_integer...',
],
[
'value' => 'node.langcode',
'label' => 'node.langcode',
],
[
'value' => 'node.langcode.',
'label' => 'node.langcode...',
],
[
'value' => 'node.nid',
'label' => 'node.nid',
],
[
'value' => 'node.nid.',
'label' => 'node.nid...',
],
[
'value' => 'node.promote',
'label' => 'node.promote',
],
[
'value' => 'node.promote.',
'label' => 'node.promote...',
],
[
'value' => 'node.revision_log',
'label' => 'node.revision_log',
],
[
'value' => 'node.revision_log.',
'label' => 'node.revision_log...',
],
[
'value' => 'node.revision_timestamp',
'label' => 'node.revision_timestamp',
],
[
'value' => 'node.revision_timestamp.',
'label' => 'node.revision_timestamp...',
],
[
'value' => 'node.revision_translation_affected',
'label' => 'node.revision_translation_affected',
],
[
'value' => 'node.revision_translation_affected.',
'label' => 'node.revision_translation_affected...',
],
[
'value' => 'node.revision_uid',
'label' => 'node.revision_uid',
],
[
'value' => 'node.revision_uid.',
'label' => 'node.revision_uid...',
],
[
'value' => 'node.status',
'label' => 'node.status',
],
[
'value' => 'node.status.',
'label' => 'node.status...',
],
[
'value' => 'node.sticky',
'label' => 'node.sticky',
],
[
'value' => 'node.sticky.',
'label' => 'node.sticky...',
],
[
'value' => 'node.title',
'label' => 'node.title',
],
[
'value' => 'node.title.',
'label' => 'node.title...',
],
[
'value' => 'node.type',
'label' => 'node.type',
],
[
'value' => 'node.type.',
'label' => 'node.type...',
],
[
'value' => 'node.uid',
'label' => 'node.uid',
],
[
'value' => 'node.uid.',
'label' => 'node.uid...',
],
[
'value' => 'node.uuid',
'label' => 'node.uuid',
],
[
'value' => 'node.uuid.',
'label' => 'node.uuid...',
],
[
'value' => 'node.vid',
'label' => 'node.vid',
],
[
'value' => 'node.vid.',
'label' => 'node.vid...',
],
];
$this->assertSame($expected, $results);
// Tests that "node.uid.entity.na" returns "node.uid.entity.name".
$results = $component->autocomplete('node.uid.entity.na');
$this->assertSame(['node.uid.entity.name'], $results);
$this->assertSame([
[
'value' => 'node.uid.entity.name',
'label' => 'node.uid.entity.name',
],
[
'value' => 'node.uid.entity.name.',
'label' => 'node.uid.entity.name...',
],
], $results);
// A multi-valued field should show numeric indices suggestions.
$results = $component->autocomplete('node.field_integer.');
$this->assertSame([
'node.field_integer.0',
'node.field_integer.1',
'node.field_integer.2',
'node.field_integer.value',
[
'value' => 'node.field_integer.0',
'label' => 'node.field_integer.0',
],
[
'value' => 'node.field_integer.0.',
'label' => 'node.field_integer.0...',
],
[
'value' => 'node.field_integer.1',
'label' => 'node.field_integer.1',
],
[
'value' => 'node.field_integer.1.',
'label' => 'node.field_integer.1...',
],
[
'value' => 'node.field_integer.2',
'label' => 'node.field_integer.2',
],
[
'value' => 'node.field_integer.2.',
'label' => 'node.field_integer.2...',
],
[
'value' => 'node.field_integer.value',
'label' => 'node.field_integer.value',
],
], $results);
// A single-valued field should not show numeric indices suggestions.
$results = $component->autocomplete('node.title.');
$this->assertSame([
'node.title.value',
[
'value' => 'node.title.value',
'label' => 'node.title.value',
],
], $results);
// A single-valued field should not show numeric indices suggestions.
$results = $component->autocomplete('n');
$this->assertSame([
[
'value' => 'node',
'label' => 'node',
],
[
'value' => 'node.',
'label' => 'node...',
],
], $results);
}
......@@ -140,9 +333,18 @@ class AutocompleteTest extends RulesDrupalTestBase {
$results = $component->autocomplete('list.');
$this->assertSame([
'list.0',
'list.1',
'list.2',
[
'value' => 'list.0',
'label' => 'list.0',
],
[
'value' => 'list.1',
'label' => 'list.1',
],
[
'value' => 'list.2',
'label' => 'list.2',
],
], $results);
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment