open current category
This commit is contained in:
@@ -33,15 +33,14 @@ use Espo\Core\Acl\Table;
|
||||
use Espo\Core\Exceptions\Forbidden;
|
||||
use Espo\Core\Exceptions\BadRequest;
|
||||
use Espo\Core\Exceptions\Error;
|
||||
|
||||
use Espo\Core\Exceptions\NotFound;
|
||||
use Espo\Core\ORM\Entity;
|
||||
use Espo\Core\Templates\Entities\CategoryTree;
|
||||
use Espo\Services\RecordTree as Service;
|
||||
use Espo\Core\Api\Request;
|
||||
|
||||
use Espo\Tools\CategoryTree\Move\MoveParams;
|
||||
use Espo\Tools\CategoryTree\MoveService;
|
||||
use Espo\Tools\CategoryTree\Record\ReadTreeParams;
|
||||
use RuntimeException;
|
||||
use stdClass;
|
||||
|
||||
@@ -56,7 +55,6 @@ class RecordTree extends Record
|
||||
* Get a category tree.
|
||||
*
|
||||
* @throws BadRequest
|
||||
* @throws Error
|
||||
* @throws Forbidden
|
||||
* @throws NotFound
|
||||
* @noinspection PhpUnused
|
||||
@@ -66,30 +64,45 @@ class RecordTree extends Record
|
||||
$selectParams = $this->fetchSearchParamsFromRequest($request);
|
||||
|
||||
$parentId = $request->getQueryParam('parentId');
|
||||
$currentId = $request->getQueryParam('currentId');
|
||||
$maxDepth = $request->getQueryParam('maxDepth');
|
||||
$onlyNotEmpty = (bool) $request->getQueryParam('onlyNotEmpty');
|
||||
|
||||
if ($parentId && $currentId) {
|
||||
throw new BadRequest("Cannot have both parentId and currentId set.");
|
||||
}
|
||||
|
||||
if ($maxDepth !== null) {
|
||||
$maxDepth = (int) $maxDepth;
|
||||
}
|
||||
|
||||
$collection = $this->getRecordTreeService()->getTree(
|
||||
$parentId,
|
||||
[
|
||||
'where' => $selectParams->getWhere(),
|
||||
'onlyNotEmpty' => $onlyNotEmpty,
|
||||
],
|
||||
$maxDepth
|
||||
$params = new ReadTreeParams(
|
||||
where: $selectParams->getWhere(),
|
||||
onlyNotEmpty: $onlyNotEmpty,
|
||||
currentId: $currentId,
|
||||
maxDepth: $maxDepth,
|
||||
parentId: $parentId,
|
||||
);
|
||||
|
||||
$service = $this->getRecordTreeService();
|
||||
|
||||
$collection = $service->getTree($params);
|
||||
|
||||
if (!$collection) {
|
||||
throw new Error();
|
||||
throw new RuntimeException();
|
||||
}
|
||||
|
||||
$openPath = null;
|
||||
|
||||
if ($params->currentId) {
|
||||
$openPath = $service->getTreeItemPath($params->currentId);
|
||||
}
|
||||
|
||||
return (object) [
|
||||
'list' => $collection->getValueMapList(),
|
||||
'path' => $this->getRecordTreeService()->getTreeItemPath($parentId),
|
||||
'data' => $this->getRecordTreeService()->getCategoryData($parentId),
|
||||
'path' => $service->getTreeItemPath($parentId),
|
||||
'data' => $service->getCategoryData($parentId),
|
||||
'openPath' => $openPath,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ use Espo\Core\Acl\Exceptions\NotImplemented;
|
||||
|
||||
use ArrayAccess;
|
||||
use Espo\Tools\CategoryTree\Move\LoopReferenceChecker;
|
||||
use Espo\Tools\CategoryTree\Record\ReadTreeParams;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
@@ -60,56 +61,45 @@ class RecordTree extends Record
|
||||
|
||||
private ?Entity $seed = null;
|
||||
|
||||
/** @var ?string */
|
||||
protected $subjectEntityType = null;
|
||||
/** @var ?string */
|
||||
protected $categoryField = null;
|
||||
protected ?string $subjectEntityType = null;
|
||||
protected ?string $categoryField = null;
|
||||
|
||||
/**
|
||||
* @param array{where?: ?WhereItem, onlyNotEmpty?: bool} $params
|
||||
* @return ?Collection<Entity>
|
||||
* @throws Forbidden
|
||||
* @throws BadRequest
|
||||
*/
|
||||
public function getTree(
|
||||
?string $parentId = null,
|
||||
array $params = [],
|
||||
?int $maxDepth = null
|
||||
): ?Collection {
|
||||
|
||||
public function getTree(ReadTreeParams $params): ?Collection
|
||||
{
|
||||
if (!$this->acl->check($this->entityType, Table::ACTION_READ)) {
|
||||
throw new Forbidden();
|
||||
}
|
||||
|
||||
/** @noinspection PhpRedundantOptionalArgumentInspection */
|
||||
return $this->getTreeInternal($parentId, $params, $maxDepth, 0);
|
||||
$path = $params->currentId ? $this->getTreeItemPath($params->currentId) : null;
|
||||
|
||||
return $this->getTreeInternal($params->parentId, $params, $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{where?: ?WhereItem, onlyNotEmpty?: bool} $params
|
||||
* @param string[] $path
|
||||
* @return ?Collection<Entity>
|
||||
* @throws BadRequest
|
||||
* @throws Forbidden
|
||||
*/
|
||||
private function getTreeInternal(
|
||||
?string $parentId = null,
|
||||
array $params = [],
|
||||
?int $maxDepth = null,
|
||||
int $level = 0
|
||||
): ?Collection {
|
||||
|
||||
if (!$maxDepth) {
|
||||
$maxDepth = self::MAX_DEPTH;
|
||||
}
|
||||
private function getTreeInternal(?string $parentId, ReadTreeParams $params, ?array $path, int $level = 0): ?Collection
|
||||
{
|
||||
$maxDepth = $params->maxDepth ?? self::MAX_DEPTH;
|
||||
|
||||
if ($level === $maxDepth) {
|
||||
return null;
|
||||
if ($path === null || !in_array($parentId, $path)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
$searchParams = SearchParams::create();
|
||||
|
||||
if (isset($params['where'])) {
|
||||
$searchParams = $searchParams->withWhere($params['where']);
|
||||
if ($params->where) {
|
||||
$searchParams = $searchParams->withWhere($params->where);
|
||||
}
|
||||
|
||||
$selectBuilder = $this->selectBuilderFactory
|
||||
@@ -118,9 +108,7 @@ class RecordTree extends Record
|
||||
->withStrictAccessControl()
|
||||
->withSearchParams($searchParams)
|
||||
->buildQueryBuilder()
|
||||
->where([
|
||||
'parentId' => $parentId,
|
||||
]);
|
||||
->where(['parentId' => $parentId]);
|
||||
|
||||
$selectBuilder->order([]);
|
||||
|
||||
@@ -141,7 +129,7 @@ class RecordTree extends Record
|
||||
->find();
|
||||
|
||||
if (
|
||||
(!empty($params['onlyNotEmpty']) || $filterItems) &&
|
||||
($params->onlyNotEmpty || $filterItems) &&
|
||||
$collection instanceof ArrayAccess
|
||||
) {
|
||||
foreach ($collection as $i => $entity) {
|
||||
@@ -152,7 +140,7 @@ class RecordTree extends Record
|
||||
}
|
||||
|
||||
foreach ($collection as $entity) {
|
||||
$childList = $this->getTreeInternal($entity->getId(), $params, $maxDepth, $level + 1);
|
||||
$childList = $this->getTreeInternal($entity->getId(), $params, $path, $level + 1);
|
||||
|
||||
$entity->set('childList', $childList?->getValueMapList());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
/************************************************************************
|
||||
* 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.
|
||||
************************************************************************/
|
||||
|
||||
namespace Espo\Tools\CategoryTree\Record;
|
||||
|
||||
use Espo\Core\Select\Where\Item;
|
||||
|
||||
readonly class ReadTreeParams
|
||||
{
|
||||
public function __construct(
|
||||
public ?Item $where = null,
|
||||
public bool $onlyNotEmpty = false,
|
||||
public ?string $currentId = null,
|
||||
public ?int $maxDepth = null,
|
||||
public ?string $parentId = null,
|
||||
) {}
|
||||
}
|
||||
@@ -32,6 +32,26 @@ import Collection from 'collection';
|
||||
|
||||
class TreeCollection extends Collection {
|
||||
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
parentId
|
||||
|
||||
/**
|
||||
* @type {string|null}
|
||||
*/
|
||||
currentId = null
|
||||
|
||||
/**
|
||||
* @type {string[]|null}
|
||||
*/
|
||||
path
|
||||
|
||||
/**
|
||||
* @type {string[]|null}
|
||||
*/
|
||||
openPath
|
||||
|
||||
/**
|
||||
* @return {TreeCollection}
|
||||
*/
|
||||
@@ -55,6 +75,8 @@ class TreeCollection extends Collection {
|
||||
seed.reset();
|
||||
|
||||
this.path = response.path;
|
||||
this.openPath = response.openPath ?? null;
|
||||
|
||||
/**
|
||||
* @type {{
|
||||
* id: string,
|
||||
@@ -111,6 +133,10 @@ class TreeCollection extends Collection {
|
||||
options.data.parentId = this.parentId;
|
||||
}
|
||||
|
||||
if (this.currentId) {
|
||||
options.data.currentId = this.currentId;
|
||||
}
|
||||
|
||||
return super.fetch(options);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
************************************************************************/
|
||||
|
||||
import RecordController from 'controllers/record';
|
||||
import TreeCollection from 'collections/tree';
|
||||
|
||||
class RecordTreeController extends RecordController {
|
||||
|
||||
@@ -49,14 +50,27 @@ class RecordTreeController extends RecordController {
|
||||
}
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
actionListTree() {
|
||||
this.getCollection().then(collection => {
|
||||
collection.url = collection.entityType + '/action/listTree';
|
||||
/**
|
||||
*
|
||||
* @param {{
|
||||
* currentId?: string,
|
||||
* }} options
|
||||
*/
|
||||
async actionListTree(options) {
|
||||
const currentId = options.currentId;
|
||||
|
||||
this.main(this.getViewName('listTree'), {
|
||||
scope: this.name,
|
||||
collection: collection
|
||||
});
|
||||
const collection = await this.getCollection();
|
||||
|
||||
if (!(collection instanceof TreeCollection)) {
|
||||
throw new Error("Wrong collection.");
|
||||
}
|
||||
|
||||
collection.url = `${collection.entityType}/action/listTree`;
|
||||
collection.currentId = currentId ?? null;
|
||||
|
||||
this.main(this.getViewName('listTree'), {
|
||||
scope: this.name,
|
||||
collection: collection,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ class ListWithCategories extends ListView {
|
||||
super.setup();
|
||||
|
||||
this.addActionHandler('toggleExpandedFromNavigation', () => this.actionToggleExpandedFromNavigation());
|
||||
this.addActionHandler('manageCategories', () => this.actionManageCategories());
|
||||
|
||||
this.defaultMaxSize = this.collection.maxSize;
|
||||
|
||||
@@ -736,14 +737,19 @@ class ListWithCategories extends ListView {
|
||||
return this.currentCategoryId;
|
||||
}
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
actionManageCategories() {
|
||||
this.clearCategoryViews();
|
||||
|
||||
this.getRouter().navigate('#' + this.categoryScope, {trigger: true});
|
||||
let url =`#${this.categoryScope}`;
|
||||
|
||||
if (this.currentCategoryId) {
|
||||
url += `/listTree/currentId=${this.currentCategoryId}`;
|
||||
}
|
||||
|
||||
this.getRouter().navigate(url, {trigger: true});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -242,7 +242,10 @@ class ListTreeRecordItemView extends View {
|
||||
});
|
||||
}
|
||||
|
||||
unfold() {
|
||||
/**
|
||||
* Unfold.
|
||||
*/
|
||||
async unfold() {
|
||||
if (this.createDisabled) {
|
||||
this.once('children-created', () => {
|
||||
if (!this.model.lastAreChecked) {
|
||||
@@ -263,34 +266,34 @@ class ListTreeRecordItemView extends View {
|
||||
return;
|
||||
}
|
||||
|
||||
this.getCollectionFactory().create(this.scope, collection => {
|
||||
collection.url = this.collection.url;
|
||||
collection.parentId = this.model.id;
|
||||
const collection = await this.getCollectionFactory().create(this.scope);
|
||||
|
||||
Espo.Ui.notifyWait();
|
||||
collection.url = this.collection.url;
|
||||
collection.parentId = this.model.id;
|
||||
|
||||
this.listenToOnce(collection, 'sync', () => {
|
||||
Espo.Ui.notify(false);
|
||||
Espo.Ui.notifyWait();
|
||||
|
||||
this.model.set('childCollection', collection);
|
||||
this.listenToOnce(collection, 'sync', () => {
|
||||
Espo.Ui.notify(false);
|
||||
|
||||
this.createChildren();
|
||||
this.model.set('childCollection', collection);
|
||||
|
||||
this.isUnfolded = true;
|
||||
this.createChildren();
|
||||
|
||||
if (collection.length || !this.createDisabled) {
|
||||
this.afterUnfold();
|
||||
this.isUnfolded = true;
|
||||
|
||||
this.trigger('after:unfold');
|
||||
} else {
|
||||
this.isEnd = true;
|
||||
if (collection.length || !this.createDisabled) {
|
||||
this.afterUnfold();
|
||||
|
||||
this.afterIsEnd();
|
||||
}
|
||||
});
|
||||
this.trigger('after:unfold');
|
||||
} else {
|
||||
this.isEnd = true;
|
||||
|
||||
collection.fetch();
|
||||
this.afterIsEnd();
|
||||
}
|
||||
});
|
||||
|
||||
await collection.fetch();
|
||||
}
|
||||
|
||||
fold() {
|
||||
@@ -376,7 +379,7 @@ class ListTreeRecordItemView extends View {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return module:views/record/list-tree
|
||||
* @return {module:views/record/list-tree}
|
||||
*/
|
||||
getChildrenView() {
|
||||
return /** @type module:views/record/list-tree */this.getView('children');
|
||||
|
||||
@@ -174,6 +174,41 @@ class ListTreeRecordView extends ListRecordView {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (this.level === 0) {
|
||||
this.once('after:render', () => {
|
||||
const collection = /** @type {import('collections/tree').default} */this.collection;
|
||||
|
||||
if (collection.openPath) {
|
||||
/**
|
||||
* @param {ListTreeRecordView} view
|
||||
* @param {string[]} path
|
||||
*/
|
||||
const open = async (view, path) => {
|
||||
path = [...path];
|
||||
const id = path.shift()
|
||||
|
||||
const itemView = view.getItemViews().find(view => view.model.id === id);
|
||||
|
||||
if (!itemView) {
|
||||
return;
|
||||
}
|
||||
|
||||
await itemView.unfold();
|
||||
|
||||
if (!path.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
await open(itemView.getChildrenView(), path);
|
||||
}
|
||||
|
||||
open(this, collection.openPath);
|
||||
|
||||
collection.openPath = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onRemove() {
|
||||
|
||||
Reference in New Issue
Block a user