view stream attachments

This commit is contained in:
Yuri Kuznetsov
2025-04-16 20:46:17 +03:00
parent 39cefe99e6
commit 3fd33d3269
11 changed files with 322 additions and 28 deletions
@@ -44,6 +44,7 @@
},
"labels": {
"View Posts": "View Posts",
"View Attachments": "View Attachments",
"View Activity": "View Activity",
"Pin": "Pin",
"Unpin": "Unpin",
@@ -0,0 +1,8 @@
[
{
"name": "name",
"view": "views/attachment/fields/name-for-stream"
},
{"name": "createdBy", "width": 18},
{"name": "createdAt", "width": 18}
]
+5
View File
@@ -610,6 +610,11 @@
"action": "unfollow"
}
},
{
"route": "/:entityType/:id/streamAttachments",
"method": "get",
"actionClassName": "Espo\\Tools\\Stream\\Api\\GetStreamAttachments"
},
{
"route": "/:Note/:id/pin",
"method": "post",
@@ -167,6 +167,7 @@ class NameUtil
public const LINK_FORBIDDEN_NAME_LIST = [
'posts',
'stream',
'streamAttachments',
'subscription',
'starSubscription',
'action',
@@ -0,0 +1,85 @@
<?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\Stream\Api;
use Espo\Core\Acl;
use Espo\Core\Api\Action;
use Espo\Core\Api\Request;
use Espo\Core\Api\Response;
use Espo\Core\Api\ResponseComposer;
use Espo\Core\Exceptions\BadRequest;
use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\Record\EntityProvider;
use Espo\Core\Record\SearchParamsFetcher;
use Espo\ORM\Entity;
use Espo\Tools\Stream\RecordService;
/**
* @noinspection PhpUnused
*/
class GetStreamAttachments implements Action
{
public function __construct(
private EntityProvider $entityProvider,
private Acl $acl,
private SearchParamsFetcher $searchParamsFetcher,
private RecordService $service,
) {}
public function process(Request $request): Response
{
$entity = $this->getEntity($request);
$searchParams = $this->searchParamsFetcher->fetch($request);
$result = $this->service->findAttachments($entity, $searchParams);
return ResponseComposer::json($result->toApiOutput());
}
/**
* @throws Forbidden
* @throws NotFound
* @throws BadRequest
*/
private function getEntity(Request $request): Entity
{
$entityType = $request->getRouteParam('entityType') ?? throw new BadRequest();
$id = $request->getRouteParam('id') ?? throw new BadRequest();
$entity = $this->entityProvider->get($entityType, $id);
if (!$this->acl->checkEntityStream($entity)) {
throw new Forbidden("No 'stream' access.");
}
return $entity;
}
}
+102 -27
View File
@@ -34,8 +34,11 @@ use Espo\Core\Exceptions\Forbidden;
use Espo\Core\Exceptions\NotFound;
use Espo\Core\Name\Field;
use Espo\Core\Select\SearchParams;
use Espo\Core\Select\SelectBuilderFactory;
use Espo\Core\Utils\Metadata;
use Espo\Entities\Attachment;
use Espo\ORM\Collection;
use Espo\ORM\Entity;
use Espo\ORM\EntityManager;
use Espo\Entities\User;
use Espo\Entities\Note;
@@ -43,6 +46,8 @@ use Espo\Entities\Email;
use Espo\Core\Acl;
use Espo\Core\Acl\Table;
use Espo\Core\Record\Collection as RecordCollection;
use Espo\ORM\Query\Part\Condition;
use Espo\ORM\Query\Part\Expression;
use Espo\ORM\Query\SelectBuilder;
use Espo\Tools\Stream\RecordService\Helper;
use Espo\Tools\Stream\RecordService\NoteHelper;
@@ -62,6 +67,7 @@ class RecordService
private Metadata $metadata,
private NoteHelper $noteHelper,
private MassNotePreparator $massNotePreparator,
private SelectBuilderFactory $selectBuilderFactory,
) {}
/**
@@ -174,33 +180,7 @@ class RecordService
*/
private function findInternal(string $scope, string $id, SearchParams $searchParams): RecordCollection
{
$builder = $this->queryHelper->buildBaseQueryBuilder($searchParams);
$where = $this->user->isPortal() ?
[
'parentType' => $scope,
'parentId' => $id,
'isInternal' => false,
] :
[
'OR' => [
[
'parentType' => $scope,
'parentId' => $id,
],
[
'superParentType' => $scope,
'superParentId' => $id,
],
]
];
$this->applyPortalAccess($builder, $where);
$this->applyAccess($builder, $id, $scope, $where);
$this->applyIgnore($where);
$this->applyStatusIgnore($scope, $where);
$builder->where($where);
$builder = $this->prepareSelectBuilder($scope, $id, $searchParams);
$offset = $searchParams->getOffset();
$maxSize = $searchParams->getMaxSize();
@@ -468,4 +448,99 @@ class RecordService
throw new Forbidden("No stream access.");
}
}
/**
* @return RecordCollection<Attachment>
* @throws BadRequest
* @throws Forbidden
* @throws NotFound
* @since 9.1.0
* @internal
*/
public function findAttachments(Entity $entity, SearchParams $searchParams): RecordCollection
{
$entityType = $entity->getEntityType();
$id = $entity->getId();
$this->checkAccess($entityType, $id);
$noteBuilder = $this->prepareSelectBuilder($entityType, $id, SearchParams::create());
$noteBuilder->select(['id']);
$searchParams = $searchParams->withSelect([
'id',
'name',
'type',
'size',
'parentType',
'parentId',
'createdAt',
'createdById',
'createdByName',
]);
$query = $this->selectBuilderFactory
->create()
->from(Attachment::ENTITY_TYPE)
->withSearchParams($searchParams)
->buildQueryBuilder()
->where(
Condition::in(
Expression::column('parentId'),
$noteBuilder->build()
)
)
->where(['parentType' => Note::ENTITY_TYPE])
->build();
$collection = $this->entityManager
->getRDBRepositoryByClass(Attachment::class)
->clone($query)
->find();
$total = $this->entityManager
->getRDBRepositoryByClass(Attachment::class)
->clone($query)
->count();
return RecordCollection::create($collection, $total);
}
/**
* @throws BadRequest
* @throws Forbidden
*/
private function prepareSelectBuilder(string $scope, string $id, SearchParams $searchParams): SelectBuilder
{
$builder = $this->queryHelper->buildBaseQueryBuilder($searchParams);
$where = $this->user->isPortal() ?
[
'parentType' => $scope,
'parentId' => $id,
'isInternal' => false,
] :
[
'OR' => [
[
'parentType' => $scope,
'parentId' => $id,
],
[
'superParentType' => $scope,
'superParentId' => $id,
],
]
];
$this->applyPortalAccess($builder, $where);
$this->applyAccess($builder, $id, $scope, $where);
$this->applyIgnore($where);
$this->applyStatusIgnore($scope, $where);
$builder->where($where);
return $builder;
}
}
@@ -1,2 +1,2 @@
<span class="fas fa-paperclip small"></span> <a href="{{url}}">{{value}}</a>
<span class="fas fa-paperclip small"></span> <a href="{{url}}" target="_blank">{{value}}</a>
@@ -0,0 +1,59 @@
/************************************************************************
* 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 Model from 'model';
import FileFieldView from 'views/fields/file';
import BaseFieldView from 'views/fields/base';
export default class extends BaseFieldView {
// language=Handlebars
listTemplateContent = `
<span>{{{subField}}}</span>
`
prepare() {
const model = new Model({
fileId: this.model.id,
fileName: this.model.attributes.name,
fileType: this.model.attributes.type,
});
const view = new FileFieldView({
name: 'file',
model: model,
params: {
showPreview: true,
listPreviewSize: 'small',
},
mode: 'list',
});
return this.assignView('subField', view);
}
}
+6
View File
@@ -36,6 +36,8 @@ import CreateRelatedHelper from 'helpers/record/create-related';
/**
* A related-list modal.
*
* @todo JSDocs constructor options.
*/
class RelatedListModalView extends ModalView {
@@ -135,6 +137,10 @@ class RelatedListModalView extends ModalView {
this.panelCollection = this.options.panelCollection;
if (this.options.searchPanelDisabled) {
this.searchPanel = false;
}
if (this.panelCollection) {
this.listenTo(this.panelCollection, 'sync', (c, r, o) => {
if (o.skipCollectionSync) {
@@ -104,6 +104,7 @@ class SelectRecordsModalView extends ModalView {
* @property {function({where: Record[], searchParams: module:collection~Data})} [onMassSelect]
* On record select. As of 9.1.0.
* @property {function()} [onCreate] On create click. As of 9.0.5.
* @property {boolean} [searchPanelDisabled] Disable the search panel.
*/
/**
@@ -222,6 +223,10 @@ class SelectRecordsModalView extends ModalView {
}
}
if (this.options.searchPanelDisabled) {
this.searchPanel = false;
}
if (!this.options.headerText) {
this.$header = $('<span>');
+49
View File
@@ -781,6 +781,14 @@ class PanelStreamView extends RelationshipPanelView {
onClick: () => this.actionViewPostList(),
});
if (this.model.entityType !== 'User') {
this.actionList.push({
action: 'viewAttachmentList',
text: this.translate('View Attachments', 'labels', 'Note'),
onClick: () => this.actionViewAttachmentList(),
});
}
if (this.model.entityType === 'User') {
this.actionList.push({
action: 'viewUserActivity',
@@ -834,6 +842,47 @@ class PanelStreamView extends RelationshipPanelView {
this.actionViewRelatedList(data);
}
/**
* @private
*/
actionViewAttachmentList() {
const url = `${this.model.entityType}/${this.model.id}/streamAttachments`;
const title = `${this.translate('Stream')} @right ${this.translate('Attachment', 'scopeNamesPlural')}`;
const options = {
model: this.model,
link: 'streamAttachments',
entityType: 'Attachment',
title: title,
layoutName: 'listForStream',
defaultOrder: 'desc',
defaultOrderBy: 'createdAt',
url: url,
listViewName: this.listViewName,
createDisabled: true,
selectDisabled: true,
unlinkDisabled: true,
removeDisabled: true,
rowActionsView: null,
filtersDisabled: true,
searchPanelDisabled: true,
massActionRemoveDisabled: true,
};
Espo.Ui.notifyWait();
this.createView('modal', 'views/modals/related-list', options, view => {
Espo.Ui.notify();
view.render();
this.listenTo(view, 'action', (event, element) => {
Espo.Utils.handleAction(this, event, element);
});
});
}
actionViewUserActivity() {
const url = `User/${this.model.id}/stream/own`;