diff --git a/application/Espo/Tools/GlobalSearch/Api/Get.php b/application/Espo/Tools/GlobalSearch/Api/Get.php index 87119f1fc5..f5634831ce 100644 --- a/application/Espo/Tools/GlobalSearch/Api/Get.php +++ b/application/Espo/Tools/GlobalSearch/Api/Get.php @@ -34,6 +34,7 @@ use Espo\Core\Api\Request; use Espo\Core\Api\Response; use Espo\Core\Api\ResponseComposer; use Espo\Core\Exceptions\BadRequest; +use Espo\Core\Record\SearchParamsFetcher; use Espo\Tools\GlobalSearch\Service; /** @@ -41,8 +42,10 @@ use Espo\Tools\GlobalSearch\Service; */ class Get implements Action { - public function __construct(private Service $service) - {} + public function __construct( + private Service $service, + private SearchParamsFetcher $searchParamsFetcher, + ) {} public function process(Request $request): Response { @@ -52,10 +55,10 @@ class Get implements Action throw new BadRequest("No `q` parameter."); } - $offset = intval($request->getQueryParam('offset')); - $maxSize = $request->hasQueryParam('maxSize') ? - intval($request->getQueryParam('maxSize')): - null; + $searchParams = $this->searchParamsFetcher->fetch($request); + + $offset = $searchParams->getOffset(); + $maxSize = $searchParams->getMaxSize(); $result = $this->service->find($query, $offset, $maxSize); diff --git a/application/Espo/Tools/GlobalSearch/Service.php b/application/Espo/Tools/GlobalSearch/Service.php index 8a6706a003..2dc774eb6c 100644 --- a/application/Espo/Tools/GlobalSearch/Service.php +++ b/application/Espo/Tools/GlobalSearch/Service.php @@ -30,9 +30,12 @@ namespace Espo\Tools\GlobalSearch; use Espo\Core\Acl; +use Espo\Core\Exceptions\BadRequest; +use Espo\Core\Exceptions\Forbidden; use Espo\Core\Name\Field; use Espo\Core\ORM\Type\FieldType; use Espo\Core\Record\Collection; +use Espo\Core\Record\ServiceContainer; use Espo\Core\Utils\Config; use Espo\Core\Utils\Metadata; use Espo\ORM\Entity; @@ -42,10 +45,11 @@ use Espo\ORM\Name\Attribute; use Espo\ORM\Query\Select; use Espo\ORM\Query\Part\Order; use Espo\ORM\Query\Part\Expression as Expr; - use Espo\Core\Select\Text\FullTextSearch\DataComposerFactory as FullTextSearchDataComposerFactory; use Espo\Core\Select\Text\FullTextSearch\DataComposer\Params as FullTextSearchDataComposerParams; use Espo\Core\Select\SelectBuilderFactory; +use Espo\ORM\Query\UnionBuilder; +use RuntimeException; class Service { @@ -57,17 +61,19 @@ class Service private Acl $acl, private Config $config, private Config\ApplicationConfig $applicationConfig, + private ServiceContainer $serviceContainer, ) {} /** * @param string $filter A search query. - * @param int $offset An offset. + * @param ?int $offset An offset. * @param ?int $maxSize A limit. * @return Collection */ - public function find(string $filter, int $offset = 0, ?int $maxSize = null): Collection + public function find(string $filter, ?int $offset = 0, ?int $maxSize = null): Collection { $entityTypeList = $this->config->get('globalSearchEntityList') ?? []; + $offset ??= 0; $maxSize ??= $this->applicationConfig->getRecordsPerPage(); $hasFullTextSearch = false; @@ -76,12 +82,12 @@ class Service foreach ($entityTypeList as $i => $entityType) { $query = $this->getEntityTypeQuery( - $entityType, - $i, - $filter, - $offset, - $maxSize, - $hasFullTextSearch + entityType: $entityType, + i: $i, + filter: $filter, + offset: $offset, + maxSize: $maxSize, + hasFullTextSearch: $hasFullTextSearch, ); if (!$query) { @@ -95,8 +101,7 @@ class Service return new Collection(new EntityCollection(), 0); } - $builder = $this->entityManager->getQueryBuilder() - ->union() + $builder = UnionBuilder::create() ->all() ->limit($offset, $maxSize + 1); @@ -105,11 +110,11 @@ class Service } if ($hasFullTextSearch) { - $builder->order('relevance', 'DESC'); + $builder->order('relevance', Order::DESC); } - $builder->order('order', 'DESC'); - $builder->order(Field::NAME, 'ASC'); + $builder->order('order', Order::DESC); + $builder->order(Field::NAME); $unionQuery = $builder->build(); @@ -118,9 +123,26 @@ class Service $sth = $this->entityManager->getQueryExecutor()->execute($unionQuery); while ($row = $sth->fetch()) { + $entityType = $row['entityType'] ?? null; + + if (!is_string($entityType)) { + throw new RuntimeException(); + } + + $statusField = $this->getStatusField($entityType); + + $select = [ + Attribute::ID, + Field::NAME, + ]; + + if ($statusField) { + $select[] = $statusField; + } + $entity = $this->entityManager - ->getRDBRepository($row['entityType']) - ->select([Attribute::ID, Field::NAME]) + ->getRDBRepository($entityType) + ->select($select) ->where([Attribute::ID => $row[Attribute::ID]]) ->findOne(); @@ -128,26 +150,28 @@ class Service continue; } + $this->serviceContainer->get($entityType)->prepareEntityForOutput($entity); + $collection->append($entity); } return Collection::createNoCount($collection, $maxSize); } - protected function getEntityTypeQuery( + private function getEntityTypeQuery( string $entityType, int $i, string $filter, int $offset, int $maxSize, - bool &$hasFullTextSearch + bool &$hasFullTextSearch, ): ?Select { if (!$this->acl->checkScope($entityType, Acl\Table::ACTION_READ)) { return null; } - if (!$this->metadata->get(['scopes', $entityType])) { + if (!$this->metadata->get("scopes.$entityType")) { return null; } @@ -159,8 +183,7 @@ class Service $entityDefs = $this->entityManager->getDefs()->getEntity($entityType); - $nameAttribute = $entityDefs->hasField('name') ? - 'name' : 'id'; + $nameAttribute = $entityDefs->hasField(Field::NAME) ? Field::NAME : Attribute::ID; $selectList = [ Attribute::ID, @@ -176,10 +199,7 @@ class Service FullTextSearchDataComposerParams::create() ); - $isPerson = $this->metadata - ->get(['entityDefs', $entityType, 'fields', Field::NAME, 'type']) === FieldType::PERSON_NAME; - - if ($isPerson) { + if ($this->isPerson($entityType)) { $selectList[] = 'firstName'; $selectList[] = 'lastName'; } else { @@ -187,7 +207,19 @@ class Service $selectList[] = ['VALUE:', 'lastName']; } - $query = $selectBuilder->build(); + $statusField = $this->getStatusField($entityType); + + if ($statusField) { + $selectList[] = [$statusField, 'status']; + } else { + $selectList[] = ['VALUE:', 'status']; + } + + try { + $query = $selectBuilder->build(); + } catch (BadRequest|Forbidden $e) { + throw new RuntimeException("", 0, $e); + } $queryBuilder = $this->entityManager ->getQueryBuilder() @@ -213,4 +245,16 @@ class Service return $queryBuilder->build(); } + + private function isPerson(string $entityType): bool + { + $fieldDefs = $this->entityManager->getDefs()->getEntity($entityType); + + return $fieldDefs->tryGetField(Field::NAME)?->getType() === FieldType::PERSON_NAME; + } + + private function getStatusField(string $entityType): ?string + { + return $this->metadata->get("scopes.$entityType.statusField"); + } } diff --git a/client/src/views/global-search/panel.js b/client/src/views/global-search/panel.js index 86c775ef90..c244d7f1ef 100644 --- a/client/src/views/global-search/panel.js +++ b/client/src/views/global-search/panel.js @@ -82,6 +82,12 @@ class GlobalSearchPanel extends View { name: 'name', view: 'views/global-search/name-field', } + ], + [ + { + name: 'status', + view: 'views/global-search/status-field', + } ] ], right: { diff --git a/client/src/views/global-search/status-field.js b/client/src/views/global-search/status-field.js new file mode 100644 index 0000000000..2c48b55b2b --- /dev/null +++ b/client/src/views/global-search/status-field.js @@ -0,0 +1,65 @@ +/************************************************************************ + * This file is part of EspoCRM. + * + * EspoCRM – Open Source CRM application. + * Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko + * Website: https://www.espocrm.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * The interactive user interfaces in modified source and object code versions + * of this program must display Appropriate Legal Notices, as required under + * Section 5 of the GNU Affero General Public License version 3. + * + * In accordance with Section 7(b) of the GNU Affero General Public License version 3, + * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. + ************************************************************************/ + +import BaseFieldView from 'views/fields/base'; + +export default class extends BaseFieldView { + + // language=Handlebars + listTemplateContent = ` + {{~#if stringValue}} + {{stringValue}} + {{/if~}} + ` + + data() { + /** @type {string} */ + const entityType = this.model.attributes._scope; + + const field = this.getMetadata().get(`scopes.${entityType}.statusField`); + + if (!field) { + return {}; + } + + const value = this.model.attributes[field]; + + if (!value) { + return {}; + } + + const stringValue = this.getLanguage().translateOption(value, field, entityType); + const style = this.getMetadata().get(`entityDefs.${entityType}.fields.${field}.style.${value}`) ?? 'default'; + + return { + stringValue, + style, + }; + } + +}