From 1deab6b87cbd7d9433cfdccb11e03799fa70a86d Mon Sep 17 00:00:00 2001 From: Christian Fritsch Date: Fri, 30 Aug 2024 11:34:00 +0200 Subject: [PATCH] Merge updates in 8.0.x --- .github/workflows/{test.yml => tests.yml} | 8 +- CHANGELOG.md | 21 ++ README.md | 2 +- composer.json | 14 +- .../graphql/thunder_menu.base.graphqls | 1 - .../graphql/thunder_search_api.base.graphqls | 4 + .../thunder_search_api.extension.graphqls | 0 .../GraphQL/Buffers/SearchApiResultBuffer.php | 101 +++++++ .../GraphQL/DataProducer/EntitiesWithTerm.php | 3 +- .../DataProducer/ThunderEntityList.php | 3 +- .../ThunderEntityListProducerBase.php | 92 ++++-- .../GraphQL/DataProducer/ThunderSearchApi.php | 122 ++++++++ .../ThunderSearchApiProducerBase.php | 181 ++++++++++++ .../ThunderSearchApiSchemaExtension.php | 39 +++ .../src/Wrappers/EntityListResponse.php | 47 ++- .../src/Wrappers/SearchApiResponse.php | 273 ++++++++++++++++++ .../Wrappers/SearchApiResponseInterface.php | 18 ++ .../DataProducer/ThunderSearchApiTest.php | 85 ++++++ .../thunder_gqls/thunder_gqls.services.yml | 12 + phpstan-baseline.neon | 60 ---- phpstan.neon | 7 +- .../Integration/FocalPointTest.php | 25 ++ thunder.info.yml | 2 +- 23 files changed, 1013 insertions(+), 107 deletions(-) rename .github/workflows/{test.yml => tests.yml} (99%) create mode 100644 modules/thunder_gqls/graphql/thunder_search_api.base.graphqls create mode 100644 modules/thunder_gqls/graphql/thunder_search_api.extension.graphqls create mode 100644 modules/thunder_gqls/src/GraphQL/Buffers/SearchApiResultBuffer.php create mode 100644 modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/ThunderSearchApi.php create mode 100644 modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/ThunderSearchApiProducerBase.php create mode 100644 modules/thunder_gqls/src/Plugin/GraphQL/SchemaExtension/ThunderSearchApiSchemaExtension.php create mode 100644 modules/thunder_gqls/src/Wrappers/SearchApiResponse.php create mode 100644 modules/thunder_gqls/src/Wrappers/SearchApiResponseInterface.php create mode 100644 modules/thunder_gqls/tests/src/Functional/DataProducer/ThunderSearchApiTest.php create mode 100644 modules/thunder_gqls/thunder_gqls.services.yml create mode 100644 tests/src/FunctionalJavascript/Integration/FocalPointTest.php diff --git a/.github/workflows/test.yml b/.github/workflows/tests.yml similarity index 99% rename from .github/workflows/test.yml rename to .github/workflows/tests.yml index aaf5f10ef..cb202aa6f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/tests.yml @@ -38,7 +38,7 @@ jobs: build: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 services: mysql: @@ -106,7 +106,7 @@ jobs: test-max: needs: build - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 services: mysql: @@ -170,7 +170,7 @@ jobs: test-upgrade: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 services: mysql: @@ -279,7 +279,7 @@ jobs: test-min: - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 services: mysql: diff --git a/CHANGELOG.md b/CHANGELOG.md index 989a9c641..1456541b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## [7.3.3](https://github.com/thunder/thunder-distribution/tree/7.3.3) 2024-08-22 + +[Full Changelog](https://github.com/thunder/thunder-distribution/compare/7.3.2...7.3.3) + +Add search api GraphQl schema and data producer. + +## [7.3.2](https://github.com/thunder/thunder-distribution/tree/7.3.2) 2024-08-14 + +[Full Changelog](https://github.com/thunder/thunder-distribution/compare/7.3.1...7.3.2) + +* [Issue #3462165: Add focal_point patch](https://www.drupal.org/node/3462165) + +## [7.3.1](https://github.com/thunder/thunder-distribution/tree/7.3.1) 2024-06-024 + +[Full Changelog](https://github.com/thunder/thunder-distribution/compare/7.3.0...7.3.1) + +Add patches for upstream issues. + +* [Issue #3465364: Fatal error when changing password when password_policy_history is enabled](https://www.drupal.org/project/password_policy/issues/3465364) +* [Issue #3455558: There is no visible change to a toggle when pressed (but it does trigger conditional fields, value is saved, etc)](https://www.drupal.org/project/gin/issues/3455558) + ## [7.3.0](https://github.com/thunder/thunder-distribution/tree/7.1.0) 2024-06-024 [Full Changelog](https://github.com/thunder/thunder-distribution/compare/7.2.2...7.3.0) diff --git a/README.md b/README.md index 0569bc307..35e66e0a9 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ For general help using Thunder, please refer to [the official Thunder documentat ### Community support -For additional help, you can use one of this channel to ask question: +For additional help, you can use one of these channels to ask question: * [Slack](https://thunder.org/contact-us) (highly recommended for faster support) * [Twitter](https://twitter.com/ThunderCoreTeam) diff --git a/composer.json b/composer.json index ae13260ae..1743a9190 100644 --- a/composer.json +++ b/composer.json @@ -40,6 +40,12 @@ }, "drupal/diff": { "Back button for comparison page": "https://www.drupal.org/files/issues/back_button_for-2853193-4.patch" + }, + "drupal/focal_point": { + "Issue #3462165: Preview results in Error: Call to a member function getDefinitions() on null": "https://www.drupal.org/files/issues/2024-07-18/Preview-results-in-Error-3462165.patch" + }, + "drupal/gin": { + "Issue #3455558: There is no visible change to a toggle when pressed (but it does trigger conditional fields, value is saved, etc)": "https://www.drupal.org/files/issues/2024-08-06/3455558-Refactor-toggle-styles-mr438.patch" } } }, @@ -59,10 +65,10 @@ "drupal/diff": "1.7", "drupal/dropzonejs": "^2.8", "drupal/empty_fields": "^1.0-alpha1", - "drupal/entity_reference_actions": "^1.1", + "drupal/entity_reference_actions": "^1.1.1", "drupal/entity_reference_revisions": "^1.3", "drupal/field_group": "^3.4", - "drupal/focal_point": "^2.1", + "drupal/focal_point": "2.1.1", "drupal/facets": "^2.0.7", "drupal/gin": "3.0-rc11", "drupal/gin_toolbar": "^1.0-rc6", @@ -76,14 +82,14 @@ "drupal/media_entity_slideshow": "^2.0-alpha1", "drupal/media_entity_twitter": "^2.5", "drupal/media_expire": "^2.6", - "drupal/media_library_media_modify": "^1.0.0-beta16", + "drupal/media_library_media_modify": "^1.0.0", "drupal/media_file_delete": "^1.2", "drupal/metatag": "^1.26", "drupal/metatag_async_widget": "^1.0-alpha2", "drupal/paragraphs": "^1.12", "drupal/paragraphs_features": "^2.0.0-beta3", "drupal/paragraphs_paste": "^2.0-beta3", - "drupal/password_policy": "^4.0", + "drupal/password_policy": "^4.0.3", "drupal/pathauto": "^1.12", "drupal/responsive_preview": "^2.1", "drupal/redirect": "^1.7", diff --git a/modules/thunder_gqls/graphql/thunder_menu.base.graphqls b/modules/thunder_gqls/graphql/thunder_menu.base.graphqls index a61951f9a..eadd9339d 100644 --- a/modules/thunder_gqls/graphql/thunder_menu.base.graphqls +++ b/modules/thunder_gqls/graphql/thunder_menu.base.graphqls @@ -1,7 +1,6 @@ type Menu { id: String! name: String! - items: [MenuItem] } diff --git a/modules/thunder_gqls/graphql/thunder_search_api.base.graphqls b/modules/thunder_gqls/graphql/thunder_search_api.base.graphqls new file mode 100644 index 000000000..29db90559 --- /dev/null +++ b/modules/thunder_gqls/graphql/thunder_search_api.base.graphqls @@ -0,0 +1,4 @@ +type SearchApiResult { + items: [Page!] + total: Int! +} diff --git a/modules/thunder_gqls/graphql/thunder_search_api.extension.graphqls b/modules/thunder_gqls/graphql/thunder_search_api.extension.graphqls new file mode 100644 index 000000000..e69de29bb diff --git a/modules/thunder_gqls/src/GraphQL/Buffers/SearchApiResultBuffer.php b/modules/thunder_gqls/src/GraphQL/Buffers/SearchApiResultBuffer.php new file mode 100644 index 000000000..354dfa53f --- /dev/null +++ b/modules/thunder_gqls/src/GraphQL/Buffers/SearchApiResultBuffer.php @@ -0,0 +1,101 @@ +entityTypeManager = $entityTypeManager; + } + + /** + * Add an item to the buffer. + * + * @param string|int|null $index + * The entity type of the given entity ids. + * @param array|int $id + * The entity id(s) to load. + * + * @return \Closure + * The callback to invoke to load the result for this buffer item. + */ + public function add($index, $id) { + $item = new \ArrayObject([ + 'index' => $index, + 'id' => $id, + ]); + + return $this->createBufferResolver($item); + } + + /** + * {@inheritdoc} + */ + protected function getBufferId($item) { + return $item['index']; + } + + /** + * {@inheritdoc} + */ + public function resolveBufferArray(array $buffer) { + $index = reset($buffer)['index']; + $ids = array_map(function (\ArrayObject $item) { + return (array) $item['id']; + }, $buffer); + + $ids = call_user_func_array('array_merge', $ids); + $ids = array_values(array_unique($ids)); + + // Load the buffered entities. + /** @var \Drupal\search_api\IndexInterface $index */ + $index = $this->entityTypeManager + ->getStorage('search_api_index') + ->load($index); + + $resultSet = $index->loadItemsMultiple($ids); + $entities = []; + + foreach ($resultSet as $key => $resultItem) { + if ($resultItem instanceof EntityAdapter) { + $entities[$key] = $resultItem->getEntity(); + } + } + + return array_map(function ($item) use ($entities) { + if (is_array($item['id'])) { + return array_reduce($item['id'], static function ($carry, $current) use ($entities) { + if (!empty($entities[$current])) { + $carry[] = $entities[$current]; + return $carry; + } + + return $carry; + }, []); + } + + return $entities[$item['id']] ?? NULL; + }, $buffer); + } + +} diff --git a/modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/EntitiesWithTerm.php b/modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/EntitiesWithTerm.php index 8788b2539..9ac9953ae 100644 --- a/modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/EntitiesWithTerm.php +++ b/modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/EntitiesWithTerm.php @@ -4,7 +4,6 @@ use Drupal\graphql\GraphQL\Execution\FieldContext; use Drupal\taxonomy\TermInterface; -use Drupal\thunder_gqls\Wrappers\EntityListResponse; use Drupal\thunder_gqls\Wrappers\EntityListResponseInterface; /** @@ -119,7 +118,7 @@ public function resolve(TermInterface $term, string $type, array $bundles, strin $cacheContext ); - return new EntityListResponse($query); + return $this->entityListResponse($query); } /** diff --git a/modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/ThunderEntityList.php b/modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/ThunderEntityList.php index 3afcc2965..43ea0aa03 100644 --- a/modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/ThunderEntityList.php +++ b/modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/ThunderEntityList.php @@ -3,7 +3,6 @@ namespace Drupal\thunder_gqls\Plugin\GraphQL\DataProducer; use Drupal\graphql\GraphQL\Execution\FieldContext; -use Drupal\thunder_gqls\Wrappers\EntityListResponse; use Drupal\thunder_gqls\Wrappers\EntityListResponseInterface; /** @@ -97,7 +96,7 @@ protected function resolve(string $type, array $bundles, int $offset, int $limit $cacheContext ); - return new EntityListResponse($query); + return $this->entityListResponse($query); } } diff --git a/modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/ThunderEntityListProducerBase.php b/modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/ThunderEntityListProducerBase.php index 9e3b37e59..a255547e3 100644 --- a/modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/ThunderEntityListProducerBase.php +++ b/modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/ThunderEntityListProducerBase.php @@ -2,12 +2,13 @@ namespace Drupal\thunder_gqls\Plugin\GraphQL\DataProducer; -use Drupal\Core\Entity\EntityTypeManager; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Entity\Query\QueryInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Session\AccountInterface; use Drupal\graphql\GraphQL\Execution\FieldContext; use Drupal\graphql\Plugin\GraphQL\DataProducer\DataProducerPluginBase; +use Drupal\thunder_gqls\Wrappers\EntityListResponse; use GraphQL\Error\UserError; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -16,43 +17,74 @@ */ abstract class ThunderEntityListProducerBase extends DataProducerPluginBase implements ContainerFactoryPluginInterface { - public const int MAX_ITEMS = 100; + public const MAX_ITEMS = 100; + + /** + * The entity type manager service. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected EntityTypeManagerInterface $entityTypeManager; + + /** + * The current user. + * + * @var \Drupal\Core\Session\AccountInterface + */ + protected AccountInterface $currentUser; + + /** + * The response wrapper service. + * + * @var \Drupal\thunder_gqls\Wrappers\EntityListResponse + */ + protected EntityListResponse $responseWrapper; /** * {@inheritdoc} */ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): self { - return new static( + $instance = new static( $configuration, $plugin_id, $plugin_definition, - $container->get('entity_type.manager'), - $container->get('current_user') ); + + $instance->setEntityTypeManager($container->get('entity_type.manager')); + $instance->setCurrentUser($container->get('current_user')); + $instance->setResponseWrapper($container->get('thunder_gqls.entity_list_response_wrapper')); + + return $instance; } /** - * EntityLoad constructor. + * Set the entity type manager service. * - * @param array $configuration - * The plugin configuration array. - * @param string $plugin_id - * The plugin id. - * @param array $plugin_definition - * The plugin definition array. - * @param \Drupal\Core\Entity\EntityTypeManager $entityTypeManager + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager * The entity type manager service. + */ + public function setEntityTypeManager(EntityTypeManagerInterface $entityTypeManager): void { + $this->entityTypeManager = $entityTypeManager; + } + + /** + * Set the current user. + * * @param \Drupal\Core\Session\AccountInterface $currentUser * The current user. */ - public function __construct( - array $configuration, - string $plugin_id, - array $plugin_definition, - protected readonly EntityTypeManager $entityTypeManager, - protected readonly AccountInterface $currentUser, - ) { - parent::__construct($configuration, $plugin_id, $plugin_definition); + public function setCurrentUser(AccountInterface $currentUser): void { + $this->currentUser = $currentUser; + } + + /** + * Set the response wrapper service. + * + * @param \Drupal\thunder_gqls\Wrappers\EntityListResponse $responseWrapper + * The response wrapper service. + */ + public function setResponseWrapper(EntityListResponse $responseWrapper): void { + $this->responseWrapper = $responseWrapper; } /** @@ -147,10 +179,7 @@ protected function query( $query->range($offset, $limit); $storage = $this->entityTypeManager->getStorage($type); - $entityType = $storage->getEntityType(); - - $cacheContext->addCacheTags($entityType->getListCacheTags()); - $cacheContext->addCacheContexts($entityType->getListCacheContexts()); + $cacheContext->addCacheableDependency($storage->getEntityType()); return $query; } @@ -186,4 +215,17 @@ protected function createPublishedCondition(string $type, array $conditions) { ]; } + /** + * The entity list response. + * + * @param \Drupal\Core\Entity\Query\QueryInterface $query + * The entity query. + * + * @return \Drupal\thunder_gqls\Wrappers\EntityListResponse + * The entity list response. + */ + protected function entityListResponse(QueryInterface $query): EntityListResponse { + return $this->responseWrapper->setQuery($query); + } + } diff --git a/modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/ThunderSearchApi.php b/modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/ThunderSearchApi.php new file mode 100644 index 000000000..d26ecc9c6 --- /dev/null +++ b/modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/ThunderSearchApi.php @@ -0,0 +1,122 @@ + 'status', + 'value' => TRUE, + 'operator' => '=', + ], + [ + 'field' => 'search_api_language', + 'value' => $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId(), + 'operator' => '=', + ], + ]; + + // Add default sorts. + $sortBy = $sortBy ?: [ + [ + 'field' => 'search_api_relevance', + 'direction' => QueryInterface::SORT_DESC, + ], + ]; + + $query = $this->buildBaseQuery( + $limit, + $offset, + $index, + $sortBy, + $conditions, + $search, + $cacheContext + ); + + return $this->searchApiResponse($query); + } + +} diff --git a/modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/ThunderSearchApiProducerBase.php b/modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/ThunderSearchApiProducerBase.php new file mode 100644 index 000000000..e568b6971 --- /dev/null +++ b/modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/ThunderSearchApiProducerBase.php @@ -0,0 +1,181 @@ +setEntityTypeManager($container->get('entity_type.manager')); + $instance->setLanguageManager($container->get('language_manager')); + $instance->setResponseWrapper($container->get('thunder_gqls.search_api_response_wrapper')); + + return $instance; + } + + /** + * Set the entity type manager service. + * + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager + * The entity type manager service. + */ + public function setEntityTypeManager(EntityTypeManagerInterface $entityTypeManager): void { + $this->entityTypeManager = $entityTypeManager; + } + + /** + * Set the language manager service. + * + * @param \Drupal\Core\Language\LanguageManagerInterface $languageManager + * The language manager service. + */ + public function setLanguageManager(LanguageManagerInterface $languageManager): void { + $this->languageManager = $languageManager; + } + + /** + * Set the response wrapper service. + * + * @param \Drupal\thunder_gqls\Wrappers\SearchApiResponse $responseWrapper + * The response wrapper service. + */ + public function setResponseWrapper(SearchApiResponse $responseWrapper): void { + $this->responseWrapper = $responseWrapper; + } + + /** + * Build base search api query. + * + * @param int $limit + * Limit of the query. + * @param int $offset + * Offset of the query. + * @param string $index + * Id of the search api index. + * @param array|null $sortBy + * List of sorts. + * @param array|null $conditions + * List of conditions to filter the result. + * @param string|null $search + * Query Search. + * @param \Drupal\graphql\GraphQL\Execution\FieldContext $cacheContext + * The caching context related to the current field. + * + * @return \Drupal\search_api\Query\QueryInterface|null + * The query interface. + * + * @throws \Drupal\search_api\SearchApiException + */ + protected function buildBaseQuery( + int $limit, + int $offset, + string $index, + ?array $sortBy, + ?array $conditions, + ?string $search, + FieldContext $cacheContext, + ): ?QueryInterface { + + // Make sure offset is zero or positive. + $offset = max($offset, 0); + + // Make sure limit is positive and cap the max items. + if ($limit <= 0) { + $limit = 10; + } + if ($limit > static::MAX_ITEMS) { + throw new UserError( + sprintf('Exceeded maximum query limit: %s.', static::MAX_ITEMS) + ); + } + + $searchIndex = Index::load($index); + if (!$searchIndex) { + return NULL; + } + + $query = $searchIndex->query(); + + foreach ($conditions as $condition) { + $query->addCondition($condition['field'], $condition['value'], $condition['operator']); + } + + foreach ($sortBy as $sort) { + $direction = $sort['direction'] ?? QueryInterface::SORT_ASC; + $query->sort($sort['field'], $direction); + } + + if (!empty($search)) { + $query->keys($search); + } + + $query->range($offset, $limit); + $cacheContext->addCacheableDependency($searchIndex); + + return $query; + } + + /** + * The search api response. + * + * @param \Drupal\search_api\Query\QueryInterface $query + * The search api query. + * + * @return \Drupal\thunder_gqls\Wrappers\SearchApiResponse + * The search api response. + */ + protected function searchApiResponse(QueryInterface $query): SearchApiResponse { + return $this->responseWrapper->setQuery($query); + } + +} diff --git a/modules/thunder_gqls/src/Plugin/GraphQL/SchemaExtension/ThunderSearchApiSchemaExtension.php b/modules/thunder_gqls/src/Plugin/GraphQL/SchemaExtension/ThunderSearchApiSchemaExtension.php new file mode 100644 index 000000000..dc9c2fbeb --- /dev/null +++ b/modules/thunder_gqls/src/Plugin/GraphQL/SchemaExtension/ThunderSearchApiSchemaExtension.php @@ -0,0 +1,39 @@ +addFieldResolverIfNotExists('SearchApiResult', 'total', + $this->builder->callback(function (SearchApiResponse $result) { + return $result->total(); + }) + ); + + $this->addFieldResolverIfNotExists('SearchApiResult', 'items', + $this->builder->callback(function (SearchApiResponse $result) { + return $result->items(); + }) + ); + } + +} diff --git a/modules/thunder_gqls/src/Wrappers/EntityListResponse.php b/modules/thunder_gqls/src/Wrappers/EntityListResponse.php index 90318c64d..ca62b5a87 100644 --- a/modules/thunder_gqls/src/Wrappers/EntityListResponse.php +++ b/modules/thunder_gqls/src/Wrappers/EntityListResponse.php @@ -2,21 +2,59 @@ namespace Drupal\thunder_gqls\Wrappers; +use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\Entity\Query\QueryInterface; +use Drupal\graphql\GraphQL\Buffers\EntityBuffer; use GraphQL\Deferred; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * The thunder entity list response class. */ -readonly class EntityListResponse implements EntityListResponseInterface { +class EntityListResponse implements EntityListResponseInterface, ContainerInjectionInterface { + + /** + * The query interface. + * + * @var \Drupal\Core\Entity\Query\QueryInterface + */ + protected QueryInterface $query; /** * EntityListResponse constructor. * + * @param \Drupal\Core\Entity\Query\QueryInterface|\Drupal\graphql\GraphQL\Buffers\EntityBuffer $buffer + * The query or buffer parameter. + */ + public function __construct(protected QueryInterface|EntityBuffer $buffer) { + if ($buffer instanceof QueryInterface) { + // phpcs:ignore + @trigger_error('Calling the constructor with a query parameter is deprecated in Thunder 7.3.3 and will be removed in Thunder 8.0. Use service injection and ::setQuery() instead.', E_USER_DEPRECATED); + $this->setQuery($buffer); + // phpcs:ignore + $buffer = \Drupal::service('graphql.buffer.entity'); + } + $this->buffer = $buffer; + } + + /** + * {@inheritDoc} + */ + public static function create(ContainerInterface $container): self { + return new static( + $container->get('graphql.buffer.entity'), + ); + } + + /** + * Set query. + * * @param \Drupal\Core\Entity\Query\QueryInterface $query - * The query interface. + * The query. */ - public function __construct(protected QueryInterface $query) { + public function setQuery(QueryInterface $query): EntityListResponse { + $this->query = $query; + return $this; } /** @@ -43,8 +81,7 @@ public function items(): array|Deferred { return []; } - $buffer = \Drupal::service('graphql.buffer.entity'); - $callback = $buffer->add($this->query->getEntityTypeId(), array_values($result)); + $callback = $this->buffer->add($this->query->getEntityTypeId(), array_values($result)); return new Deferred(fn() => $callback()); } diff --git a/modules/thunder_gqls/src/Wrappers/SearchApiResponse.php b/modules/thunder_gqls/src/Wrappers/SearchApiResponse.php new file mode 100644 index 000000000..612c56d03 --- /dev/null +++ b/modules/thunder_gqls/src/Wrappers/SearchApiResponse.php @@ -0,0 +1,273 @@ +get('thunder_gqls.buffer.search_api_result'), + $container->get('entity_field.manager'), + ); + } + + /** + * Set query. + * + * @param \Drupal\search_api\Query\QueryInterface $query + * The query. + */ + public function setQuery(QueryInterface $query): SearchApiResponse { + $this->query = $query; + return $this; + } + + /** + * Set Facet mapping. + * + * @param array $facetMapping + * The facet mapping. + */ + public function setFacetMapping(array $facetMapping): SearchApiResponse { + $this->facetMapping = $facetMapping; + return $this; + } + + /** + * Set bundle. + * + * @param string $bundle + * The bundle. + */ + public function setBundle(string $bundle): SearchApiResponse { + $this->bundle = $bundle; + return $this; + } + + /** + * Set facets. + * + * @param array $facets + * The facets. + */ + public function setFacets(array $facets): SearchApiResponse { + $this->facets = $facets; + return $this; + } + + /** + * {@inheritdoc} + * + * @throws \Drupal\search_api\SearchApiException + */ + public function facets(): array { + if (!$this->facets || !$this->facetMapping) { + return []; + } + + if (!$this->result) { + $this->result = $this->query->execute(); + } + + $facets = []; + + $facetData = $this->result->getExtraData('search_api_facets'); + foreach ($facetData as $facetFieldId => $facetResults) { + $facets[] = [ + 'key' => $this->facetMapping[$facetFieldId], + 'values' => $this->processFacetResults($this->facets[$facetFieldId], $facetResults), + ]; + } + + return $facets; + } + + /** + * Get search result items. + * + * @return array|\GraphQL\Deferred + * The search result items. + * + * @throws \Drupal\search_api\SearchApiException + */ + public function items(): array|Deferred { + if (!$this->result) { + $this->result = $this->query->execute(); + } + + $ids = array_map(static function ($item) { + return $item->getId(); + }, $this->result->getResultItems()); + + $ids = array_unique($ids); + + if (empty($ids)) { + return []; + } + + $callback = $this->buffer->add( + $this->query->getIndex()->id(), + array_values($ids) + ); + + return new Deferred(function () use ($callback) { + return $callback(); + }); + } + + /** + * Returns the total results. + * + * @return int + * The total results. + * + * @throws \Drupal\search_api\SearchApiException + */ + public function total(): int { + $query = clone $this->query; + $query->range(0, NULL); + $result = $query->execute(); + + return (int) $result->getResultCount(); + } + + /** + * Handles processing of facet values. + * + * @param \Drupal\facets\Entity\Facet $facet + * The facet to process. + * @param array $facetResults + * The facet results. + * + * @return array + * The processed facet results. + */ + private function processFacetResults( + Facet $facet, + array $facetResults, + ): array { + // First process facet results which contain filter like filter=""9"". + // @see Drupal\facets\Plugin\facets\query_type\SearchApiString#build(). + foreach ($facetResults as $i => $facetResult) { + $facetResult['filter'] = $facetResult['filter'] ?? ''; + + if ($facetResult['filter'][0] === '"') { + $facetResult['filter'] = substr($facetResult['filter'], 1); + } + if ($facetResult['filter'][strlen($facetResult['filter']) - 1] === '"') { + $facetResult['filter'] = substr($facetResult['filter'], 0, -1); + } + + $facetResults[$i] = $facetResult; + } + + return $this->processFacetResultsFromFieldConfig($facet, $facetResults); + } + + /** + * Populates label for facet values from allowed options field config. + * + * @param \Drupal\facets\Entity\Facet $facet + * The facet. + * @param array $facetResults + * The facet results. + * + * @return array + * The processed facet results. + */ + private function processFacetResultsFromFieldConfig( + Facet $facet, + array $facetResults, + ): array { + if (!$this->bundle) { + return $facetResults; + } + + $fieldName = $facet->getFieldIdentifier(); + $fieldConfig = $this->entityFieldManager->getFieldDefinitions('node', $this->bundle); + + if (isset($fieldConfig[$fieldName])) { + $allowedValues = options_allowed_values($fieldConfig[$fieldName]->getFieldStorageDefinition()); + + // Use order of allowedValues. + foreach ($facetResults as $key => $facetResult) { + $facetResults[$key]['label'] = $allowedValues[$facetResult['filter']] ?? $facetResult['filter']; + $facetResults[$key]['value'] = $facetResult['filter']; + } + + $allowedValueKeys = array_keys($allowedValues); + usort($facetResults, function ($a, $b) use ($allowedValueKeys) { + $indexA = array_search($a['filter'], $allowedValueKeys, TRUE); + $indexB = array_search($b['filter'], $allowedValueKeys, TRUE); + + return $indexA < $indexB ? -1 : 1; + }); + } + + return $facetResults; + } + +} diff --git a/modules/thunder_gqls/src/Wrappers/SearchApiResponseInterface.php b/modules/thunder_gqls/src/Wrappers/SearchApiResponseInterface.php new file mode 100644 index 000000000..82d2158aa --- /dev/null +++ b/modules/thunder_gqls/src/Wrappers/SearchApiResponseInterface.php @@ -0,0 +1,18 @@ +logWithRole('administrator'); + + $this->drupalGet('admin/config/search/search-api/index/content'); + $this->submitForm([], 'Index now'); + $this->assertSession()->statusCodeEquals(200); + $this->checkForMetaRefresh(); + + $options = [ + 'index' => 'content', + 'search' => 'the', + 'limit' => 10, + 'offset' => 0, + ]; + + $result = $this->executeDataProducer('thunder_search_api', $options); + $this->assertEquals(3, $result->total()); + + $items = $result->items(); + $items->runQueue(); + $this->assertEquals('Burda Launches Open-Source CMS Thunder', $items->result[0]->getTitle()); + + // Change sort order. + $options['sortBy'] = [ + [ + 'field' => 'search_api_relevance', + 'direction' => QueryInterface::SORT_ASC, + ], + ]; + + $this->container->get('kernel')->rebuildContainer(); + $result = $this->executeDataProducer('thunder_search_api', $options); + + $items = $result->items(); + $items->runQueue(); + $this->assertEquals('Legal notice', $items->result[0]->getTitle()); + + // Get articles only. + $options['conditions'] = [ + [ + 'field' => 'type', + 'value' => 'article', + 'operator' => '=', + ], + ]; + + $this->container->get('kernel')->rebuildContainer(); + $result = $this->executeDataProducer('thunder_search_api', $options); + + $items = $result->items(); + $items->runQueue(); + $this->assertEquals('Come to DrupalCon New Orleans', $items->result[0]->getTitle()); + + } + +} diff --git a/modules/thunder_gqls/thunder_gqls.services.yml b/modules/thunder_gqls/thunder_gqls.services.yml new file mode 100644 index 000000000..863299712 --- /dev/null +++ b/modules/thunder_gqls/thunder_gqls.services.yml @@ -0,0 +1,12 @@ +services: + _defaults: + autowire: true + thunder_gqls.buffer.search_api_result: + class: Drupal\thunder_gqls\GraphQL\Buffers\SearchApiResultBuffer + Drupal\thunder_gqls\GraphQL\Buffers\SearchApiResultBuffer: '@thunder_gqls.buffer.search_api_result' + thunder_gqls.search_api_response_wrapper: + class: Drupal\thunder_gqls\Wrappers\SearchApiResponse + thunder_gqls.entity_list_response_wrapper: + autowire: false + class: Drupal\thunder_gqls\Wrappers\EntityListResponse + arguments: [ '@graphql.buffer.entity' ] diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index ac571d0b1..1ddc639a4 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,55 +1,5 @@ parameters: ignoreErrors: - - - message: "#^Unsafe usage of new static\\(\\)\\.$#" - count: 1 - path: modules/thunder_article/src/Form/NodeRevisionRevertDefaultForm.php - - - - message: "#^Unsafe usage of new static\\(\\)\\.$#" - count: 1 - path: modules/thunder_article/src/Plugin/Derivative/DynamicLocalTasks.php - - - - message: "#^Unsafe usage of new static\\(\\)\\.$#" - count: 1 - path: modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/EntityLinks.php - - - - message: "#^Unsafe usage of new static\\(\\)\\.$#" - count: 1 - path: modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/FocalPoint.php - - - - message: "#^Unsafe usage of new static\\(\\)\\.$#" - count: 1 - path: modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/MenuLinksActiveTrail.php - - - - message: "#^Unsafe usage of new static\\(\\)\\.$#" - count: 1 - path: modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/MetaTags.php - - - - message: "#^Unsafe usage of new static\\(\\)\\.$#" - count: 1 - path: modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/ThunderEntityListProducerBase.php - - - - message: "#^Unsafe usage of new static\\(\\)\\.$#" - count: 1 - path: modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/ThunderEntitySubRequestBase.php - - - - message: "#^Unsafe usage of new static\\(\\)\\.$#" - count: 1 - path: modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/ThunderImage.php - - - - message: "#^Unsafe usage of new static\\(\\)\\.$#" - count: 1 - path: modules/thunder_gqls/src/Plugin/GraphQL/DataProducer/ThunderRedirect.php - - message: "#^Access to an undefined property Drupal\\\\Core\\\\Entity\\\\ContentEntityInterface\\:\\:\\$field_teaser_media\\.$#" count: 1 @@ -70,11 +20,6 @@ parameters: count: 4 path: modules/thunder_gqls/tests/src/Kernel/DataProducer/EntityLinksTest.php - - - message: "#^Unsafe usage of new static\\(\\)\\.$#" - count: 1 - path: modules/thunder_taxonomy/src/ThunderTaxonomyPermissions.php - - message: "#^Access to an undefined property Drupal\\\\Core\\\\Entity\\\\EntityInterface\\:\\:\\$status\\.$#" count: 1 @@ -220,11 +165,6 @@ parameters: count: 1 path: tests/src/TestSuites/ThunderTestSuite.php - - - message: "#^Unsafe usage of new static\\(\\)\\.$#" - count: 1 - path: tests/src/TestSuites/ThunderTestSuite.php - - message: "#^Call to method id\\(\\) on an unknown class Drupal\\\\entity_browser\\\\Entity\\\\EntityBrowser\\.$#" count: 1 diff --git a/phpstan.neon b/phpstan.neon index 654ea7304..a604bc45c 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,8 +1,11 @@ parameters: customRulesetUsed: true - checkGenericClassInNonGenericObjectType: false - checkMissingIterableValueType: false reportUnmatchedIgnoredErrors: true level: 6 + ignoreErrors: + # new static() is a best practice in Drupal, so we cannot fix that. + - "#^Unsafe usage of new static#" + - identifier: missingType.generics + - identifier: missingType.iterableValue includes: - ./phpstan-baseline.neon diff --git a/tests/src/FunctionalJavascript/Integration/FocalPointTest.php b/tests/src/FunctionalJavascript/Integration/FocalPointTest.php new file mode 100644 index 000000000..cfa49561d --- /dev/null +++ b/tests/src/FunctionalJavascript/Integration/FocalPointTest.php @@ -0,0 +1,25 @@ +loadNodeByUuid('0bd5c257-2231-450f-b4c2-ab156af7b78d'); + $this->drupalGet($node->toUrl('edit-form')); + $this->clickDrupalSelector('edit-field-teaser-media-selection-0-edit'); + $this->clickDrupalSelector('edit-field-image-0-preview-preview-link'); + + $this->assertSession()->elementExists('css', '#focal-point-derivatives .focal-point-derivative-preview-image'); + $this->assertSession()->elementExists('css', '.focal-point-original-image > #focal-point-preview-image'); + } + +} diff --git a/thunder.info.yml b/thunder.info.yml index 9e7d4e5f5..c82aa16f4 100644 --- a/thunder.info.yml +++ b/thunder.info.yml @@ -3,7 +3,7 @@ type: profile description: 'The Drupal based CMS for professional publishing.' project: thunder core_version_requirement: ~10.3.0 -version: '7.3.0' +version: '7.3.3' distribution: name: Thunder