global search status

This commit is contained in:
Yuri Kuznetsov
2025-07-24 20:26:15 +03:00
parent ee38f09665
commit 74fbedfb9a
4 changed files with 150 additions and 32 deletions
@@ -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);
+70 -26
View File
@@ -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<Entity>
*/
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");
}
}
+6
View File
@@ -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: {
@@ -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 <https://www.gnu.org/licenses/>.
*
* 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}}
<span class="label label-sm label-state label-{{style}}">{{stringValue}}</span>
{{/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,
};
}
}