diff --git a/graphql_views.services.yml b/graphql_views.services.yml new file mode 100644 index 0000000..a06f4af --- /dev/null +++ b/graphql_views.services.yml @@ -0,0 +1,4 @@ +services: + graphql_views.token_handler: + class: Drupal\graphql_views\TokenHandler + arguments: ['@token'] diff --git a/src/Plugin/Deriver/Fields/ViewDeriver.php b/src/Plugin/Deriver/Fields/ViewDeriver.php index fe8577b..be12203 100644 --- a/src/Plugin/Deriver/Fields/ViewDeriver.php +++ b/src/Plugin/Deriver/Fields/ViewDeriver.php @@ -3,7 +3,9 @@ namespace Drupal\graphql_views\Plugin\Deriver\Fields; use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface; +use Drupal\graphql\Utility\StringHelper; use Drupal\graphql_views\Plugin\Deriver\ViewDeriverBase; +use Drupal\views\Plugin\views\display\DisplayPluginInterface; use Drupal\views\Views; /** @@ -35,7 +37,7 @@ public function getDerivativeDefinitions($basePluginDefinition) { $arguments += $this->getPagerArguments($display); $arguments += $this->getSortArguments($display, $id); $arguments += $this->getFilterArguments($display, $id); - $types = $this->getTypes($info); + $types = $this->getTypes($display, $info); $this->derivatives[$id] = [ 'id' => $id, diff --git a/src/Plugin/GraphQL/Fields/View.php b/src/Plugin/GraphQL/Fields/View.php index 4b4aa25..e5acdd1 100644 --- a/src/Plugin/GraphQL/Fields/View.php +++ b/src/Plugin/GraphQL/Fields/View.php @@ -2,6 +2,7 @@ namespace Drupal\graphql_views\Plugin\GraphQL\Fields; +use Drupal\Component\Utility\Html; use Drupal\Core\DependencyInjection\DependencySerializationTrait; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; @@ -72,10 +73,20 @@ public function resolveValues($value, array $args, ResolveContext $context, Reso // Set view contextual filters. /* @see \Drupal\graphql_views\ViewDeriverHelperTrait::getArgumentsInfo() */ if (!empty($definition['arguments_info'])) { - $arguments = $this->extractContextualFilters($value, $args); + $arguments = $this->extractContextualFilters($value, $args, $executable); $executable->setArguments($arguments); } - + // See if we can fetch the pageSize from the context. + $limit_mode = $executable->getDisplay()->getOption('limit_mode'); + if ($limit_mode == 'token') { + if (($token_string = $executable->getDisplay()->getOption('default_limit')) && ($entity_type = $executable->getDisplay()->getOption('entity_type'))) { + $token_handler = \Drupal::service('graphql_views.token_handler'); + // Now do the token replacement. + if (($pageSizeValue = current($token_handler->getArgumentsFromTokenString($token_string, $entity_type, $value))) && is_numeric($pageSizeValue)) { + $executable->setItemsPerPage((int) $pageSizeValue); + } + } + } $filters = $executable->getDisplay()->getOption('filters');; $input = $this->extractExposedInput($value, $args, $filters); $executable->setExposedInput($input); @@ -126,15 +137,37 @@ protected function getCacheDependencies(array $result, $value, array $args, Reso * The resolved parent value. * @param $args * The arguments provided to the field. + * @param \Drupal\views\ViewExecutable $executable + * The view to execute. * * @return array * An array of arguments containing the contextual filter value from the * parent or provided args if any. */ - protected function extractContextualFilters($value, $args) { + protected function extractContextualFilters($value, $args, $executable) { $definition = $this->getPluginDefinition(); $arguments = []; + // See if we can fetch the default arguments using the context. + $arg_mode = $executable->getDisplay()->getOption('argument_mode'); + + if ($arg_mode == 'token') { + if (($token_string = $executable->getDisplay()->getOption('default_argument')) && ($entity_type = $executable->getDisplay()->getOption('entity_type'))) { + $token_handler = \Drupal::service('graphql_views.token_handler'); + // Now do the token replacement. + $token_values = $token_handler->getArgumentsFromTokenString($token_string, $entity_type, $value); + $new_args = []; + // We have to be careful to only replace arguments that have + // tokens. + foreach ($token_values as $key => $value) { + $new_args[Html::escape($key)] = Html::escape($value); + } + + //$view->args = $new_args; + $arguments = $new_args; + } + } + foreach ($definition['arguments_info'] as $argumentId => $argumentInfo) { if (isset($args['contextualFilter'][$argumentId])) { $arguments[$argumentInfo['index']] = $args['contextualFilter'][$argumentId]; @@ -147,7 +180,7 @@ protected function extractContextualFilters($value, $args) { ) { $arguments[$argumentInfo['index']] = $value->id(); } - else { + elseif (!isset($arguments[$argumentInfo['index']])) { $arguments[$argumentInfo['index']] = NULL; } } diff --git a/src/Plugin/views/display/GraphQL.php b/src/Plugin/views/display/GraphQL.php index 1135191..f91d6aa 100644 --- a/src/Plugin/views/display/GraphQL.php +++ b/src/Plugin/views/display/GraphQL.php @@ -7,6 +7,7 @@ namespace Drupal\graphql_views\Plugin\views\display; +use Drupal\Component\Utility\SafeMarkup; use Drupal\Core\Cache\CacheableMetadata; use Drupal\graphql\Utility\StringHelper; use Drupal\views\Plugin\views\display\DisplayPluginBase; @@ -84,6 +85,17 @@ public function displaysExposed() { protected function defineOptions() { $options = parent::defineOptions(); + // Allow to attach the view to entity types / bundles. + // Similar to the EVA module. + $options['entity_type']['default'] = ''; + $options['bundles']['default'] = []; + // Allow to manually provide arguments or using tokens. + $options['argument_mode']['default'] = 'none'; + $options['default_argument']['default'] = ''; + // Allow to manually provide limit or using tokens. + $options['limit_mode']['default'] = 'none'; + $options['default_limit']['default'] = ''; + // Set the default plugins to 'graphql'. $options['style']['contains']['type']['default'] = 'graphql'; $options['exposed_form']['contains']['type']['default'] = 'graphql'; @@ -206,6 +218,44 @@ public function optionsSummary(&$categories, &$options) { 'title' => $this->t('Query name'), 'value' => views_ui_truncate($this->getGraphQLQueryName(), 24), ]; + + if ($entity_type = $this->getOption('entity_type')) { + $entity_info = \Drupal::entityManager()->getDefinition($entity_type); + $type_name = $entity_info->get('label'); + + $bundle_names = []; + $bundle_info = \Drupal::entityManager()->getBundleInfo($entity_type); + foreach ($this->getOption('bundles') as $bundle) { + $bundle_names[] = $bundle_info[$bundle]['label']; + } + } + + $options['entity_type'] = [ + 'category' => 'graphql', + 'title' => $this->t('Entity type'), + 'value' => empty($type_name) ? $this->t('None') : $type_name, + ]; + + $options['bundles'] = [ + 'category' => 'graphql', + 'title' => $this->t('Bundles'), + 'value' => empty($bundle_names) ? $this->t('All') : implode(', ', $bundle_names), + ]; + + $argument_mode = $this->getOption('argument_mode'); + $options['arguments'] = [ + 'category' => 'graphql', + 'title' => $this->t('Arguments'), + 'value' => empty($argument_mode) ? $this->t('GraphqlQuery') : \Drupal\Component\Utility\Html::escape($argument_mode), + ]; + + $limit_mode = $this->getOption('limit_mode'); + $options['limit'] = [ + 'category' => 'graphql', + 'title' => $this->t('Limit'), + 'value' => empty($limit_mode) ? $this->t('GraphqlQuery') : \Drupal\Component\Utility\Html::escape($limit_mode), + ]; + } /** @@ -223,6 +273,147 @@ public function buildOptionsForm(&$form, FormStateInterface $form_state) { '#default_value' => $this->getGraphQLQueryName(), ]; break; + + case 'entity_type': + $entity_info = \Drupal::entityManager()->getDefinitions(); + $entity_names = [NULL => $this->t('None')]; + foreach ($entity_info as $type => $info) { + // is this a content/front-facing entity? + if ($info instanceof \Drupal\Core\Entity\ContentEntityType) { + $entity_names[$type] = $info->get('label'); + } + } + + $form['#title'] .= $this->t('Entity type'); + $form['entity_type'] = [ + '#type' => 'radios', + '#required' => FALSE, + '#title' => $this->t('Attach this display to the following entity type'), + '#options' => $entity_names, + '#default_value' => $this->getOption('entity_type'), + ]; + break; + + case 'bundles': + $options = []; + $entity_type = $this->getOption('entity_type'); + foreach (\Drupal::entityManager()->getBundleInfo($entity_type) as $bundle => $info) { + $options[$bundle] = $info['label']; + } + $form['#title'] .= $this->t('Bundles'); + $form['bundles'] = [ + '#type' => 'checkboxes', + '#title' => $this->t('Attach this display to the following bundles. If no bundles are selected, the display will be attached to all.'), + '#options' => $options, + '#default_value' => $this->getOption('bundles'), + ]; + break; + + case 'arguments': + $form['#title'] .= $this->t('Arguments'); + $default = $this->getOption('argument_mode'); + $options = [ + 'None' => $this->t("No special handling"), + 'token' => $this->t("Use tokens from the entity the view is attached to"), + ]; + + $form['argument_mode'] = [ + '#type' => 'radios', + '#title' => $this->t("How should this display populate the view's arguments?"), + '#options' => $options, + '#default_value' => $default, + ]; + + $form['token'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Token replacement'), + '#collapsible' => TRUE, + '#states' => [ + 'visible' => [ + ':input[name=argument_mode]' => ['value' => 'token'], + ], + ], + ]; + + $form['token']['default_argument'] = [ + '#title' => $this->t('Arguments'), + '#type' => 'textfield', + '#maxlength' => 1024, + '#default_value' => $this->getOption('default_argument'), + '#description' => $this->t('You may use token replacement to provide arguments based on the current entity. Separate arguments with "/".'), + ]; + + // Add a token browser. + if (\Drupal::service('module_handler')->moduleExists('token') && $entity_type = $this->getOption('entity_type')) { + $token_types = [$entity_type => $entity_type]; + $token_mapper = \Drupal::service('token.entity_mapper'); + if (!empty($token_types)) { + $token_types = array_map(function ($type) use ($token_mapper) { + return $token_mapper->getTokenTypeForEntityType($type); + }, (array) $token_types); + } + $form['token']['browser'] = [ + '#theme' => 'token_tree_link', + '#token_types' => $token_types, + '#recursion_limit' => 5, + '#global_types' => TRUE, + '#show_nested' => FALSE, + ]; + } + break; + + case 'limit': + $form['#title'] .= $this->t('Limit'); + $default = $this->getOption('limit_mode'); + $options = [ + 'None' => $this->t("No special handling"), + 'token' => $this->t("Use tokens from the entity the view is attached to"), + ]; + + $form['limit_mode'] = [ + '#type' => 'radios', + '#title' => $this->t("How should this display populate the view's result limit?"), + '#options' => $options, + '#default_value' => $default, + ]; + + $form['token'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Token replacement'), + '#collapsible' => TRUE, + '#states' => [ + 'visible' => [ + ':input[name=limit_mode]' => ['value' => 'token'], + ], + ], + ]; + + $form['token']['default_limit'] = [ + '#title' => $this->t('Limit'), + '#type' => 'textfield', + '#maxlength' => 1024, + '#default_value' => $this->getOption('default_limit'), + '#description' => $this->t('You may use token replacement to provide the limit based on the current entity.'), + ]; + + // Add a token browser. + if (\Drupal::service('module_handler')->moduleExists('token') && $entity_type = $this->getOption('entity_type')) { + $token_types = [$entity_type => $entity_type]; + $token_mapper = \Drupal::service('token.entity_mapper'); + if (!empty($token_types)) { + $token_types = array_map(function ($type) use ($token_mapper) { + return $token_mapper->getTokenTypeForEntityType($type); + }, (array) $token_types); + } + $form['token']['browser'] = [ + '#theme' => 'token_tree_link', + '#token_types' => $token_types, + '#recursion_limit' => 5, + '#global_types' => TRUE, + '#show_nested' => FALSE, + ]; + } + break; } } @@ -236,6 +427,45 @@ public function submitOptionsForm(&$form, FormStateInterface $form_state) { case 'graphql_query_name': $this->setOption($section, $form_state->getValue($section)); break; + case 'entity_type': + $new_entity = $form_state->getValue('entity_type'); + $old_entity = $this->getOption('entity_type'); + $this->setOption('entity_type', $new_entity); + + if ($new_entity != $old_entity) { + // Each entity has its own list of bundles and view modes. If there's + // only one on the new type, we can select it automatically. Otherwise + // we need to wipe the options and start over. + $new_entity_info = \Drupal::entityManager()->getDefinition($new_entity); + $new_bundles_keys = \Drupal::entityManager()->getBundleInfo($new_entity); + $new_bundles = array(); + if (count($new_bundles_keys) == 1) { + $new_bundles[] = $new_bundles_keys[0]; + } + $this->setOption('bundles', $new_bundles); + } + break; + case 'bundles': + $this->setOption('bundles', array_values(array_filter($form_state->getValue('bundles')))); + break; + case 'arguments': + $this->setOption('argument_mode', $form_state->getValue('argument_mode')); + if ($form_state->getValue('argument_mode') == 'token') { + $this->setOption('default_argument', $form_state->getValue('default_argument')); + } + else { + $this->setOption('default_argument', NULL); + } + break; + case 'limit': + $this->setOption('limit_mode', $form_state->getValue('limit_mode')); + if ($form_state->getValue('limit_mode') == 'token') { + $this->setOption('default_limit', $form_state->getValue('default_limit')); + } + else { + $this->setOption('default_limit', NULL); + } + break; } } diff --git a/src/TokenHandler.php b/src/TokenHandler.php new file mode 100644 index 0000000..de505ec --- /dev/null +++ b/src/TokenHandler.php @@ -0,0 +1,63 @@ +token = $token; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('token') + ); + } + + /** + * Get view arguments array from string that contains tokens. + * + * @param string $string + * The token string defined by the view. + * @param string $type + * The token type. + * @param object $object + * The object being used for replacement data (typically a node). + * + * @return array + * An array of argument values. + */ + public function getArgumentsFromTokenString($string, $type, $object) { + $args = trim($string); + if (empty($args)) { + return []; + } + $args = $this->token->replace($args, [$type => $object], ['sanitize' => FALSE, 'clear' => FALSE]); + return explode('/', $args); + } + +} diff --git a/src/ViewDeriverHelperTrait.php b/src/ViewDeriverHelperTrait.php index a64325b..539f4a6 100644 --- a/src/ViewDeriverHelperTrait.php +++ b/src/ViewDeriverHelperTrait.php @@ -117,7 +117,17 @@ protected function getPagerArguments(DisplayPluginInterface $display) { * @return array * An array of additional types the view can be embedded in. */ - protected function getTypes(array $arguments, array $types = ['Root']) { + protected function getTypes(DisplayPluginInterface $display, array $arguments, array $types = ['Root']) { + + if (($entity_type = $display->getOption('entity_type'))) { + $types = array_merge($types, [StringHelper::camelCase($entity_type)]); + + if (($bundles = $display->getOption('bundles'))) { + $types = array_merge($types, array_map(function ($bundle) use ($entity_type) { + return StringHelper::camelCase($entity_type, $bundle); + }, $bundles)); + } + } if (empty($arguments)) { return $types;