assigned users notes

This commit is contained in:
Yuri Kuznetsov
2024-09-19 17:55:58 +03:00
parent b752f43ee9
commit dd06a51ae0
6 changed files with 290 additions and 37 deletions
+4 -1
View File
@@ -272,7 +272,10 @@ class Note extends Entity
return $this;
}
public function setData(stdClass $data): self
/**
* @param stdClass|array<string, mixed> $data
*/
public function setData(stdClass|array $data): self
{
$this->set('data', $data);
@@ -565,6 +565,14 @@
"assignThisSelf": "{user} self-assigned this {entityType}",
"assignSelf": "{user} self-assigned {entityType} {entity}",
"assignMultiAdd": "{user} assigned {entity} to {assignee}",
"assignMultiRemove": "{user} unassigned {entity} from {removedAssignee}",
"assignMultiAddRemove": "{user} assigned {entity} to {assignee} and unassigned from {removedAssignee}",
"assignMultiAddThis": "{user} assigned this {entityType} to {assignee}",
"assignMultiRemoveThis": "{user} unassigned this {entityType} from {removedAssignee}",
"assignMultiAddRemoveThis": "{user} assigned this {entityType} to {assignee} and unassigned from {removedAssignee}",
"postThis": "{user} posted",
"attachThis": "{user} attached",
"statusThis": "{user} updated {field}",
@@ -62,6 +62,8 @@ class HookProcessor
/** @var ?array<string, ?string> */
private $statusFields = null;
private const FIELD_ASSIGNED_USERS = 'assignedUsers';
public function __construct(
private Metadata $metadata,
private EntityManager $entityManager,
@@ -425,6 +427,11 @@ class HookProcessor
if ($entity->isAttributeChanged('assignedUserId')) {
$this->afterSaveStreamNotNewAssignedUserIdChanged($entity, $options);
} else if (
$entity->hasLinkMultipleField(self::FIELD_ASSIGNED_USERS) &&
$entity->isAttributeChanged(self::FIELD_ASSIGNED_USERS . 'Ids')
) {
$this->afterSaveStreamNotNewAssignedUsersIdsChanged($entity, $options);
}
$this->service->handleAudited($entity, $options);
@@ -483,6 +490,27 @@ class HookProcessor
}
}
/**
* @param array<string, mixed> $options
*/
private function afterSaveStreamNotNewAssignedUsersIdsChanged(CoreEntity $entity, array $options): void
{
$userIds = $entity->getLinkMultipleIdList(self::FIELD_ASSIGNED_USERS);
/** @var string[] $prevUserIds */
$prevUserIds = $entity->getFetched(self::FIELD_ASSIGNED_USERS . 'Ids') ?? [];
foreach (array_diff($userIds, $prevUserIds) as $userId) {
$this->service->followEntity($entity, $userId);
}
$this->service->noteAssign($entity, $options);
if (in_array($this->user->getId(), $userIds)) {
$entity->set('isFollowed', true);
}
}
private function afterSaveStreamNotNew2(CoreEntity $entity): void
{
// Not recommended to use.
+72 -13
View File
@@ -29,6 +29,7 @@
namespace Espo\Tools\Stream;
use Espo\Core\Field\LinkMultiple;
use Espo\Core\Field\LinkParent;
use Espo\Core\ORM\Repository\Option\SaveOption;
use Espo\Core\ORM\Type\FieldType;
@@ -87,6 +88,8 @@ class Service
'Closed Lost',
];
private const FIELD_ASSIGNED_USERS = 'assignedUsers';
/**
* @var array<
* string,
@@ -581,6 +584,20 @@ class Service
$data['assignedUserId'] = $entity->get('assignedUserId');
$data['assignedUserName'] = $entity->get('assignedUserName');
} else if (
$entity instanceof CoreEntity &&
$entity->hasLinkMultipleField(self::FIELD_ASSIGNED_USERS) &&
$entity->getLinkMultipleIdList(self::FIELD_ASSIGNED_USERS) !== []
) {
/** @var LinkMultiple $users */
$users = $entity->getValueObject(self::FIELD_ASSIGNED_USERS);
$data['assignedUsers'] = array_map(function ($it) {
return [
'id' => $it->getId(),
'name' => $it->getName(),
];
}, $users->getList());
}
$field = $this->getStatusField($entityType);
@@ -754,19 +771,7 @@ class Service
$note->setParent(LinkParent::createFromEntity($entity));
$this->setSuperParent($entity, $note, true);
if ($entity->get('assignedUserId')) {
$this->loadAssignedUserName($entity);
$note->set('data', [
'assignedUserId' => $entity->get('assignedUserId'),
'assignedUserName' => $entity->get('assignedUserName'),
]);
} else {
$note->set('data', [
'assignedUserId' => null
]);
}
$this->setAssignData($entity, $note);
$noteOptions = [];
@@ -1185,4 +1190,58 @@ class Service
/** @var Note */
return $this->entityManager->getNewEntity(Note::ENTITY_TYPE);
}
private function setAssignData(Entity $entity, Note $note): void
{
if (
$entity instanceof CoreEntity &&
$entity->hasLinkMultipleField(self::FIELD_ASSIGNED_USERS)
) {
$data = [];
$newIds = $entity->getLinkMultipleIdList(self::FIELD_ASSIGNED_USERS);
/** @var array<string, ?string> $newNames */
$newNames = get_object_vars($entity->get(self::FIELD_ASSIGNED_USERS . 'Names') ?? (object) []);
/** @var string[] $prevIds */
$prevIds = $entity->getFetched(self::FIELD_ASSIGNED_USERS . 'Ids') ?? [];
/** @var array<string, ?string> $prevNames */
$prevNames = get_object_vars($entity->getFetched(self::FIELD_ASSIGNED_USERS . 'Names') ?? (object) []);
$addedIds = array_values(array_diff($newIds, $prevIds));
$removedIds = array_values(array_diff($prevIds, $newIds));
$names = array_merge($prevNames, $newNames);
$data['addedAssignedUsers'] = array_map(function ($id) use ($names) {
return [
'id' => $id,
'name' => $names[$id] ?? null,
];
}, $addedIds);
$data['removedAssignedUsers'] = array_map(function ($id) use ($names) {
return [
'id' => $id,
'name' => $names[$id] ?? null,
];
}, $removedIds);
$note->setData($data);
return;
}
if ($entity->get('assignedUserId')) {
$this->loadAssignedUserName($entity);
$note->setData([
'assignedUserId' => $entity->get('assignedUserId'),
'assignedUserName' => $entity->get('assignedUserName'),
]);
return;
}
$note->setData(['assignedUserId' => null]);
}
}
+95 -10
View File
@@ -28,6 +28,8 @@
import NoteStreamView from 'views/stream/note';
/** @module views/stream/notes/assign */
class AssignNoteStreamView extends NoteStreamView {
template = 'stream/notes/assign'
@@ -48,12 +50,34 @@ class AssignNoteStreamView extends NoteStreamView {
};
}
/**
* @typedef {{
* assignedUserId?: string,
* assignedUserName?: string,
* addedAssignedUsers?: {id: string, name: string|null}[],
* removedAssignedUsers?: {id: string, name: string|null}[],
* }} module:views/stream/notes/assign~data
*/
setup() {
const data = this.model.get('data');
this.setupData();
this.createMessage();
}
setupData() {
/** @type {module:views/stream/notes/assign~data} */
const data = this.model.get('data') || {};
this.assignedUserId = data.assignedUserId || null;
this.assignedUserName = data.assignedUserName || data.assignedUserId || null;
if (data.addedAssignedUsers) {
this.setupDataMulti(data);
return;
}
this.messageData['assignee'] =
$('<span>')
.addClass('nowrap name-avatar')
@@ -78,17 +102,78 @@ class AssignNoteStreamView extends NoteStreamView {
} else {
this.messageName += 'Void';
}
} else {
if (this.assignedUserId) {
if (this.assignedUserId === this.model.get('createdById')) {
this.messageName += 'Self';
}
} else {
this.messageName += 'Void';
}
return;
}
this.createMessage();
if (this.assignedUserId) {
if (this.assignedUserId === this.model.get('createdById')) {
this.messageName += 'Self';
}
return;
}
this.messageName += 'Void';
}
/**
* @private
* @param {module:views/stream/notes/assign~data} data
*/
setupDataMulti(data) {
this.messageName = 'assignMultiAdd';
const added = data.addedAssignedUsers;
const removed = data.removedAssignedUsers;
if (!added || !removed) {
return;
}
if (added.length && removed.length) {
this.messageName = 'assignMultiAddRemove';
} else if (removed.length) {
this.messageName = 'assignMultiRemove';
}
if (added.length) {
this.messageData['assignee'] = this.createUsersElement(added);
}
if (removed.length) {
this.messageData['removedAssignee'] = this.createUsersElement(removed);
}
}
/**
* @private
* @param {{id: string, name: ?string}[]} users
* @return {HTMLElement}
*/
createUsersElement(users) {
const wrapper = document.createElement('span');
users.forEach((it, i) => {
const a = document.createElement('a');
a.href = `#User/view/${it.id}`;
a.text = it.name || it.id;
a.dataset.id = it.id;
a.dataset.scope = 'User';
const span = document.createElement('span');
span.className = 'nowrap name-avatar';
span.innerHTML = this.getHelper().getAvatarHtml(it.id, 'small', 16, 'avatar-link');
span.appendChild(a);
wrapper.appendChild(span);
if (i < users.length - 1) {
wrapper.appendChild(document.createTextNode(', '));
}
});
return wrapper;
}
}
+83 -13
View File
@@ -28,6 +28,8 @@
import NoteStreamView from 'views/stream/note';
/** @module views/stream/notes/create */
class CreateNoteStreamView extends NoteStreamView {
template = 'stream/notes/create'
@@ -52,17 +54,56 @@ class CreateNoteStreamView extends NoteStreamView {
this.createMessage();
}
/**
* @typedef {{
* assignedUserId?: string,
* assignedUserName?: string,
* assignedUsers?: {id: string, name: string|null}[],
* statusField?: string,
* statusValue?: string|null,
* statusStyle?: string|null,
* }} module:views/stream/notes/create~data
*/
setupData() {
const data = /** @type {Object.<string, *>} */this.model.get('data');
/** @type {module:views/stream/notes/create~data} */
const data = this.model.get('data') || {};
this.setupUsersData();
if (data.statusField) {
const statusField = this.statusField = data.statusField;
const statusValue = data.statusValue;
this.statusStyle = data.statusStyle || 'default';
this.statusText = this.getLanguage()
.translateOption(statusValue, statusField, this.model.attributes.parentType);
}
}
setupUsersData() {
/** @type {module:views/stream/notes/create~data} */
const data = this.model.get('data') || {};
this.assignedUserId = data.assignedUserId || null;
this.assignedUserName = data.assignedUserName || data.assignedUserId || null;
if (data.assignedUsers) {
if (data.assignedUsers.length === 1) {
this.assignedUserId = data.assignedUsers[0].id;
this.assignedUserName = data.assignedUsers[0].name;
} else if (data.assignedUsers.length > 1) {
this.setupUsersDataMulti();
return;
}
}
this.messageData['assignee'] =
$('<span>')
.addClass('nowrap name-avatar')
.append(
this.getHelper().getAvatarHtml(data.assignedUserId, 'small', 16, 'avatar-link'),
this.getHelper().getAvatarHtml(this.assignedUserId, 'small', 16, 'avatar-link'),
$('<a>')
.attr('href', `#User/view/${this.assignedUserId}`)
.text(this.assignedUserName)
@@ -84,27 +125,56 @@ class CreateNoteStreamView extends NoteStreamView {
if (this.isThis) {
this.messageName += 'This';
if (this.assignedUserId === this.model.get('createdById')) {
if (this.assignedUserId === this.model.attributes.createdById) {
this.messageName += 'Self';
}
} else {
if (this.assignedUserId === this.model.get('createdById')) {
if (this.assignedUserId === this.model.attributes.createdById) {
this.messageName += 'Self';
}
else if (isYou) {
} else if (isYou) {
this.messageName += 'You';
}
}
}
}
if (data.statusField) {
const statusField = this.statusField = data.statusField;
const statusValue = data.statusValue;
setupUsersDataMulti() {
this.messageName = 'createAssigned';
this.statusStyle = data.statusStyle || 'default';
this.statusText = this.getLanguage()
.translateOption(statusValue, statusField, this.model.get('parentType'));
}
/** @type {module:views/stream/notes/create~data} */
const data = this.model.get('data') || {};
this.messageData['assignee'] = this.createUsersElement(data.assignedUsers);
}
/**
* @private
* @param {{id: string, name: ?string}[]} users
* @return {HTMLElement}
*/
createUsersElement(users) {
const wrapper = document.createElement('span');
users.forEach((it, i) => {
const a = document.createElement('a');
a.href = `#User/view/${it.id}`;
a.text = it.name || it.id;
a.dataset.id = it.id;
a.dataset.scope = 'User';
const span = document.createElement('span');
span.className = 'nowrap name-avatar';
span.innerHTML = this.getHelper().getAvatarHtml(it.id, 'small', 16, 'avatar-link');
span.appendChild(a);
wrapper.appendChild(span);
if (i < users.length - 1) {
wrapper.appendChild(document.createTextNode(', '));
}
});
return wrapper;
}
}