diff --git a/modules/format_strawberryfield_views/config/schema/format_strawberryfield_views.schema.yml b/modules/format_strawberryfield_views/config/schema/format_strawberryfield_views.schema.yml index 93c11b0a..0dc84e12 100644 --- a/modules/format_strawberryfield_views/config/schema/format_strawberryfield_views.schema.yml +++ b/modules/format_strawberryfield_views/config/schema/format_strawberryfield_views.schema.yml @@ -33,3 +33,27 @@ views.style.format_strawberryfield_views_leaflet: type: string label: 'Max leaflet Zoom' default: 22 + +views.filter.sbf_ado_filter: + type: views_filter + label: 'Strawberry ADO filter' + mapping: + operator: + type: string + label: 'Operator' + value: + type: array + label: 'Values' + expose: + type: mapping + label: 'Expose' + mapping: + reduce: + type: boolean + label: 'Reduce' + group_info: + mapping: + group_items: + sequence: + type: views.filter.group_item.in_operator + label: 'Group item' diff --git a/modules/format_strawberryfield_views/format_strawberryfield_views.module b/modules/format_strawberryfield_views/format_strawberryfield_views.module index 39e6aaf6..e2c7e05c 100644 --- a/modules/format_strawberryfield_views/format_strawberryfield_views.module +++ b/modules/format_strawberryfield_views/format_strawberryfield_views.module @@ -185,7 +185,7 @@ function format_strawberryfield_views_views_data_alter(array &$data) { $sbf_join_field = _search_api_views_find_field_alias('sbf_flavors_join', $table); $table[$sbf_join_field] = [ - 'title' => t('Strawberry Flavor to Node Join.'), + 'title' => t('Strawberry Flavor to Node Join'), 'group' => t('Search'), 'help' => t('Joins Strawberry Flavor Search API datasources (e.g OCR) to Nodes (ADO) when doing a Full Text Search.'), 'filter' => [ @@ -197,6 +197,25 @@ function format_strawberryfield_views_views_data_alter(array &$data) { // @TODO add also an argument ID if relationships are enabled to // $table[$advanced_fulltext_field]['argument']['id'] = 'sbf_advanced_search_api_fulltext'; // Requires a special Argument Plugin. Not needed right now. + + $ado_filter = _search_api_views_find_field_alias('sbf_ado_filter', $table); + $table[$ado_filter] = [ + 'title' => t('Similar ADO Filter (Experimental)'), + 'group' => t('Search'), + 'help' => t('Filters one or more Node Entity against Search API fields, resolving dynamically values for the input Node(s) using the destination Field property paths.'), + 'filter' => [ + 'title' => t('Similar ADO Filter'), + 'field' => 'id', + 'id' => 'sbf_ado_filter', + ], + ]; + if ($ado_filter != 'sbf_ado_filter') { + $table[$ado_filter]['real field'] = 'sbf_ado_filter'; + } + + + + } catch (\Exception $e) { $args = [ diff --git a/modules/format_strawberryfield_views/src/Plugin/views/filter/StrawberryADOfilter.php b/modules/format_strawberryfield_views/src/Plugin/views/filter/StrawberryADOfilter.php new file mode 100644 index 00000000..1d650772 --- /dev/null +++ b/modules/format_strawberryfield_views/src/Plugin/views/filter/StrawberryADOfilter.php @@ -0,0 +1,1032 @@ +setNodeStorage( + $container->get('entity_type.manager')->getStorage('node') + ); + $plugin->setFieldsHelper($container->get('search_api.fields_helper')); + $plugin->setViewStorage( + $container->get('entity_type.manager')->getStorage('view') + ); + $plugin->setCache($container->get('cache.default')); + $plugin->currentUser = $container->get('current_user'); + return $plugin; + } + + /** + * Returns information about the available operators for this filter. + * + * @return array[] + * An associative array mapping operator identifiers to their information. + * The operator information itself is an associative array with the + * following keys: + * - title: The translated title for the operator. + * - short: The translated short title for the operator. + * - values: The number of values the operator requires as input. + */ + public function operators() { + return [ + 'and' => [ + 'title' => $this->t('Contains all of the resolved values'), + 'short' => $this->t('and'), + 'values' => 1, + ], + 'or' => [ + 'title' => $this->t('Contains any of the resolved values'), + 'short' => $this->t('or'), + 'values' => 1, + ], + 'not' => [ + 'title' => $this->t('Contains none of the resolved values'), + 'short' => $this->t('not'), + 'values' => 1, + ], + ]; + } + + + /** + * {@inheritdoc} + */ + public function defineOptions() { + $options = parent::defineOptions(); + $options['value']['default'] = []; + $options['operator']['default'] = 'or'; + $options['internal_operator']['default'] = 'and'; + $options['views_source_ids'] = ['default' => []]; + $options['sbf_fields'] = ['default' => []]; + $options['expose']['contains']['value_form_type'] = ['default' => 'select']; + $options['expose']['contains']['placeholder'] = ['default' => '']; + return $options; + } + + /** + * {@inheritdoc} + */ + public function defaultExposeOptions() { + parent::defaultExposeOptions(); + $this->options['expose']['reduce'] = FALSE; + $this->options['expose']['value_form_type'] = 'select'; + $this->options['expose']['placeholder'] = '- Select a Digital Object -'; + } + + protected function valueSubmit($form, FormStateInterface $form_state) { + $form_state = $form_state; + } + + /** + * Sets the Node Storage. + * + * @param \Drupal\node\NodeStorageInterface $nodestorage + * The node storage. + * + * @return $this + */ + + public function setNodeStorage(NodeStorageInterface $nodestorage) { + $this->nodeStorage = $nodestorage; + return $this; + } + + public function setFieldsHelper(FieldsHelperInterface $fieldsHelper) { + $this->fieldsHelper = $fieldsHelper; + return $this; + } + + /** + * Sets the View Storage. + * + * @param \Drupal\Core\Entity\EntityStorageInterface $viewstorage + * The view Storage. + * + * @return $this + */ + public function setViewStorage(EntityStorageInterface $viewstorage) { + $this->viewStorage = $viewstorage; + return $this; + } + + /** + * Sets the Cache Backed. + * + * @param \Drupal\Core\Cache\CacheBackendInterface $cache + * The cache backend. Use to store complex calculations of property paths. + * + * @return $this + */ + public function setCache(CacheBackendInterface $cache) { + $this->cache = $cache; + return $this; + } + + /** + * {@inheritdoc} + */ + public function buildOptionsForm(&$form, FormStateInterface $form_state) { + parent::buildOptionsForm($form, $form_state); + + $fields = $this->getSbfNodeFields() ?? []; + // Note we can not use a manyToOne class as base + // because _search_api_views_handler_mapping() does not evaluate a fields full property path + // but just the end type. That is Ok though. I could go deeper but might affect + // the normal behavior of comparing a value against a value + // instead of what we will do there which is a Node resolved against a property path (a field) against a value + + $form['sbf_fields'] = [ + '#type' => 'select', + '#title' => $this->t( + 'Node based Entity Reference (or in its property path) Fields that need to match.' + ), + '#description' => $this->t( + 'Select the fields that will be filtered against the corresponding values resolved from the Node used as query input.' + ), + '#options' => $fields, + '#size' => min(6, count($fields)), + '#multiple' => TRUE, + '#default_value' => $this->options['sbf_fields'], + '#required' => TRUE, + ]; + } + + /** + * Shortcut to display the operator form. + */ + public function showOperatorForm(&$form, FormStateInterface $form_state) { + $this->operatorForm($form, $form_state); + $form['operator']['#prefix'] = '
'; + $form['operator']['#suffix'] = '
'; + $form['operator']['#title'] = $this->t('Resolved Values Operator'); + $form['internal_operator'] = [ + '#type' => 'radios', + '#title' => $this->t('Interfield Operator'), + '#default_value' => $this->options['internal_operator'] ?? 'or', + '#options' => [ + 'and'=>'AND', + 'or'=>'OR', + ] + ]; + $form['internal_operator']['#prefix'] = '
'; + $form['internal_operator']['#suffix'] = '
'; + } + + + public function submitOptionsForm(&$form, FormStateInterface $form_state) { + parent::submitOptionsForm( + $form, $form_state + ); + // Just a quick way of regenerating the caches, just in case. + $this->getEntityRelationsForFields( + $form_state->getValue(['options', 'sbf_fields']), FALSE + ); + } + + protected function valueForm(&$form, FormStateInterface $form_state) { + // Always remember diego. If this is exposed then the same Form shown during + // Config will be shown for the end user + // Not what we want! + // problem here. Views will return an ID, we want UUIDs ... + + /* This does not allow mixed Ids and UUIDs.. i guess that is OK */ + $nodes = []; + $this->value = is_array($this->value) ? $this->value : (array) $this->value; + if (array_filter($this->value, 'is_numeric') === $this->value) { + $nodes = $this->value ? $this->nodeStorage->loadByProperties( + ['nid' => $this->value] + ) : []; + } + else { + $nodes = $this->value ? $this->nodeStorage->loadByProperties( + ['uuid' => $this->value] + ) : []; + } + if (!$form_state->get('exposed')) { + $form['value'] = [ + '#type' => 'sbf_entity_autocomplete_uuid', + '#title' => $this->t('ADOs'), + '#description' => $this->t( + 'Enter a comma separated list of Archipelago Digital Objects.' + ), + '#target_type' => 'node', + '#tags' => TRUE, + '#default_value' => $nodes, + '#selection_handler' => 'default:nodewithstrawberry', + '#validate_reference'=> TRUE, + ]; + } + elseif ($this->isExposed()) { + if ($this->options['views_source_ids']) { + $view_parts = explode(':', $this->options['views_source_ids']); + $form['value'] = []; + if (count($view_parts) == 2) { + if ($this->options['expose']['value_form_type'] == 'select') { + // only call the options if we are going to show all of them + $options = $this->readExposedOptionsForSelectFromView(); + $form['value'] = [ + '#type' => $this->options['expose']['value_form_type'], + '#title' => $this->options['expose']['placeholder'], + '#options' => $options, + ]; + $form_value_selection = []; + } + else { + $form_value_selection = [ + '#selection_handler' => 'solr_views', + '#validate_reference' => TRUE, + '#selection_settings' => [ + 'view' => [ + 'view_name' => $view_parts[0], + 'display_name' => $view_parts[1], + 'arguments' => [], + ], + ], + ]; + } + } + else { + $form_value_selection = [ + '#target_type' => 'node', + '#tags' => TRUE, + '#selection_handler' => 'default:nodewithstrawberry', + '#validate_reference' => TRUE, + ]; + } + } + if (!empty($this->options['expose']['placeholder']) && $this->options['expose']['value_form_type'] !== 'select') { + $form['value']['#attributes']['placeholder'] = $this->options['expose']['placeholder']; + } + + $form['value'] = $form['value'] + [ + '#type' => 'entity_autocomplete', + '#title' => t('Select an ADO'), + '#target_type' => 'node', + ] + $form_value_selection; + } + } + + protected function valueValidate($form, FormStateInterface $form_state) { + $node_uuids = []; + if ($values = $form_state->getValue(['options', 'value'])) { + if (!is_array($values)) { (array) $values;} + foreach ($values as $value) { + $node_uuids_or_ids[] = $value; + } + sort($node_uuids_or_ids); + } + $form_state->setValue(['options', 'value'], $node_uuids_or_ids); + } + + public function hasExtraOptions() { + return TRUE; + } + + + /** + * {@inheritdoc} + */ + public function buildExposeForm(&$form, FormStateInterface $form_state) { + parent::buildExposeForm($form, $form_state); + + $form['expose']['placeholder'] = [ + '#type' => 'textfield', + '#default_value' => $this->options['expose']['placeholder'], + '#title' => $this->t('Placeholder'), + '#size' => 40, + '#description' => $this->t( + 'Hint text that appears inside the field when empty when using the autocomplete widget' + ), + ]; + + $form['expose']['value_form_type'] = [ + '#type' => 'radios', + '#default_value' => $this->options['expose']['value_form_type'], + '#options' => [ + 'autocomplete' => 'autocomplete', + 'select' => 'select' + ], + '#title' => $this->t('Type of exposed Widget'), + '#description'=> $this->t( + 'Either a text autocomplete field or a Select. Select requires a View driving the list. See extra settings for this field.' + ), + ]; + // No need to reduce here bc the options are driven by a View. The admin + // is responsible for reducing what is available. + unset($form['expose']['reduce']); + } + + + public function buildExtraOptionsForm(&$form, FormStateInterface $form_state + ) { + $options = $this->getApplicationViewsAsOptions() ?? []; + // We only do this when the form is displayed. + $options['null'] = '- Do not use a Views -'; + if (empty($this->definition['views'])) { + $form['views_source_ids'] = [ + '#type' => 'radios', + '#title' => $this->t('View'), + '#options' => $options, + '#description' => $this->t( + 'Select which View to use to show/generate ADOs list in the regular options .' + ), + '#default_value' => $this->options['views_source_ids'], + ]; + $form['views_source_inherit_relationship'] = [ + '#type' => 'checkbox', + '#title' => $this->t( + 'Inherit this Views\' Relationships or Context' + ), + '#description' => $this->t( + 'This allows the main View to pass its Contextual Values to the Views that generates the Exposed Options.' + ), + '#default_value' => $this->options['views_source_inherit_relationship'], + ]; + } + } + + private function getApplicationViewsAsOptions() { + $displays_entity_reference = Views::getApplicableViews( + 'entity_reference_display' + ); + // Only key that allows to me get REST and FEEDS + $displays_rest = Views::getApplicableViews('returns_response'); + $displays = $displays_entity_reference + $displays_rest; + foreach ($displays as $data) { + [$view_id, $display_id] = $data; + $view = $this->viewStorage->load($view_id); + $display = $view->get('display'); + $options[$view_id . ':' . $display_id] = $view_id . ' - ' + . $display[$display_id]['display_title']; + } + ksort($options); + return $options; + } + + + public function query() { + /* We need to resolve the Passed ADO against the data source/field + @see \Drupal\search_api\Entity\Index::indexSpecificItems + - Generate the Item using the Fields helper / I could try to do this only for the fields i need? + - Not really, i just need to create fake fields and resolve them... bc + - the applicable source paths might be valid values, but not already present fields. + - See the tests e.g \Drupal\Tests\search_api\Kernel\System\FieldValuesExtractionTest::testPropertyValuesExtraction + + This looks promising! + + $object = $this->entities[3]->getTypedData(); + /** @var \Drupal\search_api\Item\FieldInterface[][] $fields */ + /*$fields = [ + 'type' => [$this->fieldsHelper->createField($this->index, 'type')], + 'name' => [$this->fieldsHelper->createField($this->index, 'name')], + 'links:entity:name' => [ + $this->fieldsHelper->createField($this->index, 'links'), + $this->fieldsHelper->createField($this->index, 'links_1'), + ], + 'links:entity:links:entity:name' => [ + $this->fieldsHelper->createField($this->index, 'links_links'), + ], + ]; + $this->fieldsHelper->extractFields($object, $fields); + + - Call the preprocess alter/processors for these new fake fields + - Ensemble the query + - DOne. + + */ + if (empty($this->value)) { + return; + } + // Select boxes will always generate a single value. + // I could check here or cast sooner on validation? + if (!is_array($this->value)) { + $this->value = (array) $this->value; + } + + $query = $this->getQuery(); + if (array_filter($this->value, 'is_numeric') === $this->value) { + $nodes = $this->value ? $this->nodeStorage->loadByProperties( + ['nid' => $this->value] + ) : []; + } + else { + $nodes = $this->value ? $this->nodeStorage->loadByProperties( + ['uuid' => $this->value] + ) : []; + } + + $data = $this->getEntityRelationsForFields($this->options['sbf_fields']); + $resolved_values = []; + foreach ($nodes as $node) { + foreach ($data as $field_id => $field_data) { + $field = $query->getIndex()->getField($field_id); + $object = $node->getTypedData(); + if (isset($field_data['path_to_resolve'])) { + $info = [ + 'label' => 'nan', + 'type' => $field->getType(), + 'datasource_id' => $field->getDatasourceId(), + 'property_path' => $field_data['path_to_resolve'], + ]; + $fields = [ + $field_data['path_to_resolve'] + => [ + $this->fieldsHelper->createField( + $query->getIndex(), 'fake_id', $info + ), + ], + ]; + $this->fieldsHelper->extractFields($object, $fields); + foreach ($fields as $property_path => $property_fields) { + foreach ($property_fields as $field) { + $field_values = $field->getValues(); + sort($field_values); + if (!isset($field_values[$property_path])) { + $resolved_values[$field_id] = array_unique($field_values); + } + } + } + } + } + } + if (empty($resolved_values)) { + return; + } + $internal_operator = $this->options['internal_operator'] ?? 'OR'; + $internal_operator = strtoupper($internal_operator); + $condition_group = $this->getQuery()->createConditionGroup($internal_operator); + $this->getQuery()->addConditionGroup( + $condition_group, $this->options['group'] + ); + + + foreach($resolved_values as $field_id => $field_values) { + if ($this->operator !== 'and') { + $operator = $this->operator === 'not' ? 'NOT IN' : 'IN'; + if (!empty($field_values)) { + $condition_group->addCondition( + $field_id, $field_values, $operator, $this->options['group'] + ); + } + } + else { + foreach ((array) $field_values as $value) { + $condition_group->addCondition($field_id, $value, '='); + } + } + } + return; + } + + + public function validate() { + $this->getValueOptions(); + $errors = parent::validate(); + + if (!in_array($this->operator, $this->operatorValues(1))) { + $errors[] = $this->t( + 'The operator is invalid on filter: @filter.', + ['@filter' => $this->adminLabel(TRUE)] + ); + } + if (is_array($this->value)) { + if (!isset($this->valueOptions)) { + // Don't validate if there are none value options provided, for example for special handlers. + return $errors; + } + if ($this->options['exposed'] && !$this->options['expose']['required'] + && empty($this->value) + ) { + // Don't validate if the field is exposed and no default value is provided. + return $errors; + } + + // Some filter_in_operator usage uses optgroups forms, so flatten it. + $flat_options = OptGroup::flattenOptions($this->valueOptions); + + // Remove every element which is not known. + foreach ($this->value as $value) { + if (!isset($flat_options[$value])) { + unset($this->value[$value]); + } + } + // Choose different kind of output for 0, a single and multiple values. + if (count($this->value) == 0) { + $errors[] = $this->t( + 'No valid values found on filter: @filter.', + ['@filter' => $this->adminLabel(TRUE)] + ); + } + } + elseif (!empty($this->value) && !is_scalar($this->value)) { + // We allow a single scalar value to pass trough. We will cast back into array + // when/if needed. This is because we allow a select box to be used too. + $errors[] = $this->t( + 'The value @value is not an array for @operator on filter: @filter', [ + '@value' => var_export($this->value, TRUE), + '@operator' => $this->operator, + '@filter' => $this->adminLabel(TRUE), + ] + ); + } + return $errors; + } + + public function validateExposed(&$form, FormStateInterface $form_state) { + // Only validate exposed input. + if (empty($this->options['exposed']) + || empty($this->options['expose']['identifier']) + ) { + return; + } + + $identifier = $this->options['expose']['identifier']; + $input = $form_state->getValue($identifier); + + if ($this->options['is_grouped'] && isset($this->options['group_info']['group_items'][$input])) { + $this->operator = $this->options['group_info']['group_items'][$input]['operator']; + $input = $this->options['group_info']['group_items'][$input]['value']; + } + + $node_uuids_or_ids = []; + $values = (array) $form_state->getValue($identifier); + + + if ($values && + (!$this->options['is_grouped'] && ($this->options['expose']['value_form_type'] != 'select' && $input != 'All')) || + ($this->options['is_grouped'] && ($input != 'All')) + ) { + foreach ($values as $value) { + $node_uuids_or_ids[] = is_scalar($value) ? $value : NULL; + } + } + $node_uuids_or_ids = array_filter($node_uuids_or_ids); + if ($node_uuids_or_ids) { + $this->validated_exposed_input = $uids; + } + } + + + public function acceptExposedInput($input) { + $rc = parent::acceptExposedInput($input); + + if ($rc) { + // If we have previously validated input, override. + if (isset($this->validated_exposed_input)) { + $this->value = $this->validated_exposed_input; + } + } + + return $rc; + } + + /** + * Retrieves a list of all fields that contain in its path a Node Entity. + * + * @return string[] + * An options list of field identifiers mapped to their prefixed + * labels. + */ + protected function getSbfNodeFields() { + $fields = []; + /** @var \Drupal\search_api\IndexInterface $index */ + $index = Index::load(substr($this->table, 17)); + + $fields_info = $index->getFields(); + foreach ($fields_info as $field_id => $field) { + //if (($field->getDatasourceId() == 'strawberryfield_flavor_datasource') && ($field->getType() == "integer")) { + // Anything except text, fulltext or any solr_text variations. Also skip direct node id and UUIDs which would + // basically return the same ADO as input filtered, given that those are unique. + $property_path = $field->getPropertyPath(); + $datasource_id = $field->getDatasourceId(); + if (strpos($field->getType(), 'text') === FALSE + && ($property_path !== "nid" || $property_path !== "uuid") + ) { + $field->getDataDefinition(); + // Now the hard part. + // We need to know if the $field->getDatasourceId() == 'entity:node' and/or + // one of the properties, from right to left resolves to an entity reference and stop there. + // At this point, to be honest we can really only do that last part IF the + // $field->getDatasourceId() != 'entity:node' given that on the opposite eventually + // any field will resolve to a NODE. + if ($field->getDatasourceId() !== 'entity:node') { + // Also check whether the underlying property actually (still) exists. + $property = NULL; + if ($datasource_id === NULL + || $index->isValidDatasource( + $datasource_id + ) + ) { + $field->getLabel(); + $properties = $index->getPropertyDefinitions($datasource_id); + + $property = \Drupal::getContainer() + ->get('search_api.fields_helper') + ->retrieveNestedProperty($properties, $field->getPropertyPath()); + $property; + } + } + else { + if ($datasource_id === NULL + || $index->isValidDatasource( + $datasource_id + ) + ) { + $field->getLabel(); + $properties = $index->getPropertyDefinitions($datasource_id); + + $property = \Drupal::getContainer() + ->get('search_api.fields_helper') + ->retrieveNestedProperty($properties, $field->getPropertyPath()); + $property; + } + + $fields[$field_id] = $field->getPrefixedLabel() . '(' + . $field->getFieldIdentifier() . ' ' . $property_path . ')'; + } + $property_path_parts = explode(":", $property_path); + if (end($property_path_parts) == "nid" + || $property_path == 'parent_id' + ) { + $fields[$field_id] = $field->getPrefixedLabel() . '(' + . $field->getFieldIdentifier() . ')'; + } + } + } + return $fields; + } + + protected function getEntityRelationsForFields($fields, $cached = TRUE) { + $cacheid = md5( + $this->getIndex()->id() . $this->field . $this->view->id() + . $this->view->current_display + ); + $cid = "format_strawberryfield_views:{$cacheid}:property_fields"; + if ($cached) { + $cache = $this->cache->get($cid); + if ($cache) { + return $cache->data; + } + } + $cacheability = new CacheableMetadata(); + $cacheability->addCacheableDependency($index); + $field_data = []; + foreach ($fields as $field_id) { + $field_data[$field_id] = $this->calculateEntityRelationsForField( + $field_id, $cacheability + ); + } + $this->cache->set( + $cid, $field_data, $cacheability->getCacheMaxAge(), + $cacheability->getCacheTags() + ); + return $field_data; + } + + + protected function calculateEntityRelationsForField($field_id, $cacheability + ) { + /* We need to cache this folks. Too much energy to extract each + time we need to query */ + // $cacheability is passed by reference bc object and stuff. + + $index = Index::load(substr($this->table, 17)); + /** @var \Drupal\search_api\IndexInterface $index */ + + $field = $index->getField($field_id); + $data = []; + try { + $datasource = $field->getDatasource(); + } catch (SearchApiException $e) { + return []; + } + if (!$datasource) { + return []; + } + // path_to_resolve is not added here so we can actually + the arrays. + $relation_info = [ + 'datasource' => $datasource->getPluginId(), + 'entity_type' => NULL, + 'type' => $field->getType(), + 'bundles' => NULL, + ]; + $seen_path_chunks = []; + $usable_path_chunks = []; + $property_definitions = $datasource->getPropertyDefinitions(); + $field_property = \Drupal\search_api\Utility\Utility::splitPropertyPath( + $field->getPropertyPath(), FALSE + ); + for ( + ; $field_property[0]; + $field_property = \Drupal\search_api\Utility\Utility::splitPropertyPath( + $field_property[1] ?? '', FALSE + ) + ) { + $property_definition = $this->fieldsHelper->retrieveNestedProperty( + $property_definitions, $field_property[0] + ); + if (!$property_definition) { + // Seems like we could not map it from the property path to some Typed + // Data definition. In the absence of a better alternative, let's + // simply disregard this field. + break; + } + + $seen_path_chunks[] = $usable_path_chunks[] = $field_property[0]; + + if ($property_definition instanceof FieldItemDataDefinitionInterface + && $property_definition->getFieldDefinition()->isComputed() + ) { + // We cannot really deal with computed fields since we have no + // knowledge about their internal logic. Thus we cannot process + // this field any further. + break; + } + + if ($relation_info['entity_type'] + && $property_definition instanceof FieldItemDataDefinitionInterface + ) { + // Parent is an entity. Hence this level is fields of the entity. + $cacheability->addCacheableDependency( + $property_definition->getFieldDefinition() + ); + // We want only the last piece of the chunks? + $usable_path_chunks = []; + $usable_path_chunks[] = $field_property[0]; + } + + $entity_reference = $this->isEntityReferenceDataDefinition( + $property_definition, $cacheability + ); + if ($entity_reference) { + // Rethinking this: + // Once we touch an $entity_reference, only then i need to start tracking $seen_path_chunks + // In other words. I only want the property chunk piece that comes AFTER a $relation_info['entity_type'] + // or inbetween $relation_info['entity_type']s (since i can not look forward. + // Unfortunately, the nested "entity" property for entity reference + // fields comes without a bundles restriction, so we need to copy the + // bundles information from the level above (on the field itself), if + // any. + if ($relation_info['entity_type'] === $entity_reference['entity_type'] + && empty($entity_reference['bundles']) + && !empty($relation_info['bundles']) + && $field_property[0] === 'entity' + ) { + $entity_reference['bundles'] = $relation_info['bundles']; + } + $relation_info = $entity_reference; + // Not used but good for debugging + $relation_info['property_path_to_foreign_entity'] = implode( + IndexInterface::PROPERTY_PATH_SEPARATOR, $seen_path_chunks + ); + $relation_info['datasource'] = $datasource->getPluginId(); + } + + + if ($property_definition instanceof ComplexDataDefinitionInterface) { + /// Not even sure i need this? + $property_definitions = $this->fieldsHelper->getNestedProperties( + $property_definition + ); + } + else { + + if (empty($data) && count($seen_path_chunks) > 0) { + // Means we reached the root of the properties and there were no Entities/references inbetween + // But means also the property path is already connected at the base (so invisible to this) to an entity + // So we can use it directly + $relation_info['full_property_path'] = implode( + IndexInterface::PROPERTY_PATH_SEPARATOR, $seen_path_chunks + ); + $relation_info['datasource'] = $datasource->getPluginId(); + } + // This item no longer has "nested" properties in its Typed Data + // definition. Thus we cannot examine it any further than the current + // point. + break; + } + } + $data = $relation_info + [ + 'path_to_resolve' => implode( + IndexInterface::PROPERTY_PATH_SEPARATOR, $usable_path_chunks + ), + ]; + /* data looks like and we really only need `path_to_resolve` + result = {array} [6] + entity_type = "node" + bundles = {array} [0] + property_path_to_foreign_entity = "field_descriptive_metadata:sbf_entity_reference_ismemberof" + datasource = "entity:node" + full_property_path = "field_descriptive_metadata:sbf_entity_reference_ismemberof:field_descriptive_metadata:digital_object_type" + path_to_resolve = "field_descriptive_metadata:digital_object_type" + */ + + return $data; + } + + /** + * Determines whether the given property is a reference to an entity. + * + * @param \Drupal\Core\TypedData\DataDefinitionInterface $property_definition + * The property to test. + * @param \Drupal\Core\Cache\RefinableCacheableDependencyInterface $cacheability + * A cache metadata object to track any caching information necessary in + * this method call. + * + * @return array + * This method will return an empty array if $property is not an entity + * reference. Otherwise it will return an associative array with the + * following structure: + * - entity_type: (string) The entity type to which $property refers. + * - bundles: (array) A list of bundles to which $property refers. In case + * specific bundles cannot be determined or the $property points to all + * the bundles, this key will contain an empty array. + */ + protected function isEntityReferenceDataDefinition(DataDefinitionInterface $property_definition, + RefinableCacheableDependencyInterface $cacheability + ): array { + $return = []; + + if ($property_definition instanceof FieldItemDataDefinitionInterface + && $property_definition->getFieldDefinition()->getType() + === 'entity_reference' + ) { + $field = $property_definition->getFieldDefinition(); + $cacheability->addCacheableDependency($field); + + $return['entity_type'] = $field->getSetting('target_type'); + $field_settings = $field->getSetting('handler_settings'); + $return['bundles'] = $field_settings['target_bundles'] ?? []; + } + elseif ($property_definition instanceof EntityDataDefinitionInterface) { + $return['entity_type'] = $property_definition->getEntityTypeId(); + $return['bundles'] = $property_definition->getBundles() ?: []; + } + return $return; + } + + protected function readExposedOptionsForSelectFromView(): array { + $view_parts = []; + $options = []; + if ($this->options['views_source_ids']) { + $view_parts = explode(':', $this->options['views_source_ids']); + } + if (count($view_parts) == 2) { + $view = $this->viewStorage->load($view_parts[0]); + if ($view) { + $display = $view ? $view->getDisplay($view_parts[1]) : NULL; + $executable = $view->getExecutable(); + /** @var \Drupal\views\ViewExecutable $executable */ + if ($display) { + $executable->setDisplay($view_parts[1]); + //$executable->setArguments(array_values($arguments)); + $views_validation = $executable->validate(); + + // Check if we need to inherit this views arguments and pass them to + // the exposed options generating one. + $args = $this->options['views_source_inherit_relationship'] ? $this->view->args : []; + if (!empty($args)) { + $executable->setArguments($args); + } + + if (empty($views_validation)) { + try { + $this->getRenderer()->executeInRenderContext( + new RenderContext(), + function () use ($executable, $view_parts) { + // Damn view renders forms and stuff. GOSH! + $executable->execute($view_parts[1]); + } + ); + } catch (\InvalidArgumentException $exception) { + error_log('Views failed to render' . $exception->getMessage()); + $exception->getMessage(); + } + + $total = $executable->pager->getTotalItems() != 0 + ? $executable->pager->getTotalItems() + : count( + $executable->result + ); + $current_page = $executable->pager->getCurrentPage(); + $num_per_page = $executable->pager->getItemsPerPage(); + $offset = $executable->pager->getOffset(); + + foreach ($executable->result as $resultRow) { + if ($resultRow instanceof + \Drupal\search_api\Plugin\views\ResultRow + ) { + //@TODO move to its own method\ + if ($resultRow->_item) { + $node = $resultRow->_item->getOriginalObject()->getValue() ?? + NULL; + if ($node) { + $options[$node->uuid()] = $node->label(); + } + } + } + } + } + } + } + } + return $options; + } + +} diff --git a/modules/format_strawberryfield_views/src/Plugin/views/filter/StrawberryFlavorsJoin.php b/modules/format_strawberryfield_views/src/Plugin/views/filter/StrawberryFlavorsJoin.php index 7687c69d..8f72792a 100644 --- a/modules/format_strawberryfield_views/src/Plugin/views/filter/StrawberryFlavorsJoin.php +++ b/modules/format_strawberryfield_views/src/Plugin/views/filter/StrawberryFlavorsJoin.php @@ -129,7 +129,7 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) { ]; $form['sbf_type'] = [ '#type' => 'textfield', - '#title' => $this->t('Comma separeted list of processor ids to join.'), + '#title' => $this->t('Comma separated list of processor ids to join.'), '#description' => $this->t('If empty all Strawberry Flavor types will be searched. You can limit that by e.g adding here ocr,text to limit it to those two Strawberry Runner processors'), '#default_value' => $this->options['sbf_type'], '#required' => FALSE, diff --git a/src/Element/EntityAutocompleteUUID.php b/src/Element/EntityAutocompleteUUID.php index ec4ffaba..dab5fba0 100644 --- a/src/Element/EntityAutocompleteUUID.php +++ b/src/Element/EntityAutocompleteUUID.php @@ -47,13 +47,13 @@ * Usage example: * @code * $form['my_element'] = [ - * '#type' => 'entity_autocomplete', + * '#type' => 'sbf_entity_autocomplete_uuid', * '#target_type' => 'node', * '#tags' => TRUE, * '#default_value' => $node, - * '#selection_handler' => 'default', + * '#selection_handler' => 'default:nodewithstrawberry', * '#selection_settings' => [ - * 'target_bundles' => ['article', 'page'], + * 'target_bundles' => ['digital_object', 'digital_object_collection'], * ], * '#autocreate' => [ * 'bundle' => 'article', @@ -331,6 +331,8 @@ public static function validateEntityAutocomplete(array &$element, FormStateInte } } $entities = \Drupal::entityTypeManager()->getStorage($element['#target_type'])->loadMultiple($value); + // We have to reset again or we will end with both the UUID and the ID stored at the same time. + $value = []; if ($entities) { foreach($entities as $entity) { $value[] = $entity->uuid();