Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Allow managing association to business units on departments' creation and update #32682

Merged
merged 34 commits into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
cd021f7
feat: Allow monitors to manage departments in units they supervise
matheusbsilva137 Jun 27, 2024
873dd34
test: add end-to-end tests
matheusbsilva137 Jun 27, 2024
309abcf
Merge branch 'develop' of https://github.com/RocketChat/Rocket.Chat i…
matheusbsilva137 Jun 27, 2024
44fcb38
fix typecheck and lint errors
matheusbsilva137 Jun 27, 2024
f8edf18
fix lint
matheusbsilva137 Jun 27, 2024
f06e11b
Fix lint once again
matheusbsilva137 Jun 27, 2024
a454287
Improve end-to-end tests
matheusbsilva137 Jun 27, 2024
f3af7c6
tests: add end-to-end tests to livechat:saveDepartment meteor method
matheusbsilva137 Jun 27, 2024
b2df254
tests: improve tests legibility
matheusbsilva137 Jun 27, 2024
e96dfef
Create changeset
matheusbsilva137 Jun 27, 2024
c9f3624
Merge branch 'develop' into improve/monitors-manage-departments-units
matheusbsilva137 Jun 27, 2024
398aecf
fix jsdoc
matheusbsilva137 Jun 27, 2024
a46930f
do not use meteor check
matheusbsilva137 Jul 1, 2024
1d62422
Improve variable's names
matheusbsilva137 Jul 1, 2024
e249756
tests: Delete department in each test case instead of using afterEach
matheusbsilva137 Jul 3, 2024
d75d907
tests: Use existing helper to create departments
matheusbsilva137 Jul 3, 2024
9733ba6
tests: move updateDepartment function to helpers
matheusbsilva137 Jul 3, 2024
35d6c42
Fix department unit validation condition
matheusbsilva137 Jul 3, 2024
b05c1d1
tests: Improve functions used in end-to-end tests
matheusbsilva137 Jul 3, 2024
9bf8c2c
tests: add unit tests
matheusbsilva137 Jul 4, 2024
7485a05
Merge branch 'develop' of https://github.com/RocketChat/Rocket.Chat i…
matheusbsilva137 Jul 4, 2024
eb71986
improve: Do not perform any action when an invalid unit id is provided
matheusbsilva137 Jul 4, 2024
a6d0ec3
tests: improve unit tests
matheusbsilva137 Jul 4, 2024
8673b91
tests: Fix tests descriptions (using admin instead of livechat manager)
matheusbsilva137 Jul 4, 2024
fde54fc
Merge branch 'develop' of https://github.com/RocketChat/Rocket.Chat i…
matheusbsilva137 Jul 29, 2024
be49a84
Throw error when trying to remove the last department from a unit
matheusbsilva137 Jul 29, 2024
e049084
Merge branch 'develop' of https://github.com/RocketChat/Rocket.Chat i…
matheusbsilva137 Aug 12, 2024
76d252c
Add missing export
matheusbsilva137 Aug 13, 2024
20812e9
Merge branch 'develop' into improve/monitors-manage-departments-units
KevLehman Aug 23, 2024
f50d7ae
Merge branch 'develop' into improve/monitors-manage-departments-units
matheusbsilva137 Aug 23, 2024
59cf9df
Merge branch 'develop' into improve/monitors-manage-departments-units
kodiakhq[bot] Sep 16, 2024
bcad5bb
Merge branch 'develop' into improve/monitors-manage-departments-units
scuciatto Sep 16, 2024
ead6d54
Merge branch 'develop' into improve/monitors-manage-departments-units
scuciatto Sep 16, 2024
e625cb2
Merge branch 'develop' into improve/monitors-manage-departments-units
kodiakhq[bot] Sep 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/dirty-stingrays-beg.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@rocket.chat/meteor": minor
"@rocket.chat/model-typings": minor
"@rocket.chat/rest-typings": minor
---

Added support for specifying a unit on departments' creation and update
15 changes: 12 additions & 3 deletions apps/meteor/app/livechat/imports/server/rest/departments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,18 @@ API.v1.addRoute(
check(this.bodyParams, {
department: Object,
agents: Match.Maybe(Array),
departmentUnit: Match.Maybe({ _id: Match.Optional(String) }),
matheusbsilva137 marked this conversation as resolved.
Show resolved Hide resolved
});

const agents = this.bodyParams.agents ? { upsert: this.bodyParams.agents } : {};
const department = await LivechatTs.saveDepartment(null, this.bodyParams.department as ILivechatDepartment, agents);
const { departmentUnit } = this.bodyParams;
const department = await LivechatTs.saveDepartment(
this.userId,
null,
this.bodyParams.department as ILivechatDepartment,
agents,
departmentUnit || {},
);

if (department) {
return API.v1.success({
Expand Down Expand Up @@ -112,17 +120,18 @@ API.v1.addRoute(
check(this.bodyParams, {
department: Object,
agents: Match.Maybe(Array),
departmentUnit: Match.Maybe({ _id: Match.Optional(String) }),
matheusbsilva137 marked this conversation as resolved.
Show resolved Hide resolved
});

const { _id } = this.urlParams;
const { department, agents } = this.bodyParams;
const { department, agents, departmentUnit } = this.bodyParams;

if (!permissionToSave) {
throw new Error('error-not-allowed');
}

const agentParam = permissionToAddAgents && agents ? { upsert: agents } : {};
await LivechatTs.saveDepartment(_id, department, agentParam);
await LivechatTs.saveDepartment(this.userId, _id, department, agentParam, departmentUnit || {});

return API.v1.success({
department: await LivechatDepartment.findOneById(_id),
Expand Down
8 changes: 8 additions & 0 deletions apps/meteor/app/livechat/server/lib/LivechatTyped.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1879,16 +1879,20 @@ class LivechatClass {
* @param {string|null} _id - The department id
* @param {Partial<import('@rocket.chat/core-typings').ILivechatDepartment>} departmentData
* @param {{upsert?: { agentId: string; count?: number; order?: number; }[], remove?: { agentId: string; count?: number; order?: number; }}} [departmentAgents] - The department agents
* @param {{_id?: string}} [departmentUnit] - The department's unit id
*/
async saveDepartment(
userId: string,
matheusbsilva137 marked this conversation as resolved.
Show resolved Hide resolved
_id: string | null,
departmentData: LivechatDepartmentDTO,
departmentAgents?: {
upsert?: { agentId: string; count?: number; order?: number }[];
remove?: { agentId: string; count?: number; order?: number };
},
departmentUnit?: { _id?: string },
matheusbsilva137 marked this conversation as resolved.
Show resolved Hide resolved
) {
check(_id, Match.Maybe(String));
check(departmentUnit, Match.Maybe({ _id: Match.Optional(String) }));
matheusbsilva137 marked this conversation as resolved.
Show resolved Hide resolved

const department = _id ? await LivechatDepartment.findOneById(_id, { projection: { _id: 1, archived: 1, enabled: 1 } }) : null;

Expand Down Expand Up @@ -1977,6 +1981,10 @@ class LivechatClass {
await callbacks.run('livechat.afterDepartmentDisabled', departmentDB);
}

if (departmentUnit) {
await callbacks.run('livechat.manageDepartmentUnit', { userId, departmentId: departmentDB._id, unitId: departmentUnit._id });
}

return departmentDB;
}
}
Expand Down
5 changes: 3 additions & 2 deletions apps/meteor/app/livechat/server/methods/saveDepartment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,20 @@ declare module '@rocket.chat/ui-contexts' {
order?: number | undefined;
}[]
| undefined,
departmentUnit?: { _id?: string },
matheusbsilva137 marked this conversation as resolved.
Show resolved Hide resolved
) => ILivechatDepartment;
}
}

Meteor.methods<ServerMethods>({
async 'livechat:saveDepartment'(_id, departmentData, departmentAgents) {
async 'livechat:saveDepartment'(_id, departmentData, departmentAgents, departmentUnit) {
const uid = Meteor.userId();
if (!uid || !(await hasPermissionAsync(uid, 'manage-livechat-departments'))) {
throw new Meteor.Error('error-not-allowed', 'Not allowed', {
method: 'livechat:saveDepartment',
});
}

return Livechat.saveDepartment(_id, departmentData, { upsert: departmentAgents });
return Livechat.saveDepartment(uid, _id, departmentData, { upsert: departmentAgents }, departmentUnit);
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ import './afterInquiryQueued';
import './sendPdfTranscriptOnClose';
import './applyRoomRestrictions';
import './afterTagRemoved';
import './manageDepartmentUnit';
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { ILivechatDepartment, IOmnichannelBusinessUnit } from '@rocket.chat/core-typings';
import { LivechatDepartment, LivechatUnit } from '@rocket.chat/models';

import { hasAnyRoleAsync } from '../../../../../app/authorization/server/functions/hasRole';
import { callbacks } from '../../../../../lib/callbacks';
import { getUnitsFromUser } from '../methods/getUnitsFromUserRoles';

callbacks.add(
'livechat.manageDepartmentUnit',
async ({ userId, departmentId, unitId }) => {
matheusbsilva137 marked this conversation as resolved.
Show resolved Hide resolved
const units = await getUnitsFromUser(userId);
const isLivechatManager = await hasAnyRoleAsync(userId, ['admin', 'livechat-manager']);
matheusbsilva137 marked this conversation as resolved.
Show resolved Hide resolved
const department = await LivechatDepartment.findOneById<Pick<ILivechatDepartment, '_id' | 'ancestors' | 'parentId'>>(departmentId, {
projection: { ancestors: 1, parentId: 1 },
});

if (!department || (unitId && department.ancestors?.includes(unitId))) {
matheusbsilva137 marked this conversation as resolved.
Show resolved Hide resolved
return;
}

const currentDepartmentUnitId = department.parentId;
const canManageNewUnit = !unitId || isLivechatManager || (Array.isArray(units) && units.includes(unitId));
const canManageCurrentUnit =
!currentDepartmentUnitId || isLivechatManager || (Array.isArray(units) && units.includes(currentDepartmentUnitId));
if (!canManageNewUnit || !canManageCurrentUnit) {
return;
}

if (currentDepartmentUnitId) {
await LivechatUnit.decrementDepartmentsCount(currentDepartmentUnitId);
}

if (unitId) {
const unit = await LivechatUnit.findOneById<Pick<IOmnichannelBusinessUnit, '_id' | 'ancestors'>>(unitId, {
projection: { ancestors: 1 },
});

if (!unit) {
return;
}

await LivechatDepartment.addDepartmentToUnit(departmentId, unitId, [unitId, ...(unit.ancestors || [])]);
await LivechatUnit.incrementDepartmentsCount(unitId);
return;
}

await LivechatDepartment.removeDepartmentFromUnit(departmentId);
},
callbacks.priority.HIGH,
'livechat-manage-department-unit',
);
29 changes: 10 additions & 19 deletions apps/meteor/ee/server/models/raw/LivechatUnit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ const addQueryRestrictions = async (originalQuery: Filter<IOmnichannelBusinessUn

const units = await getUnitsFromUser();
if (Array.isArray(units)) {
query.ancestors = { $in: units };
const expressions = query.$and || [];
const condition = { $or: [{ ancestors: { $in: units } }, { _id: { $in: units } }] };
query.$and = [condition, ...expressions];
Expand Down Expand Up @@ -109,28 +108,12 @@ export class LivechatUnitRaw extends BaseRaw<IOmnichannelBusinessUnit> implement
// remove other departments
for await (const departmentId of savedDepartments) {
if (!departmentsToSave.includes(departmentId)) {
await LivechatDepartment.updateOne(
{ _id: departmentId },
{
$set: {
parentId: null,
ancestors: null,
},
},
);
matheusbsilva137 marked this conversation as resolved.
Show resolved Hide resolved
await LivechatDepartment.removeDepartmentFromUnit(departmentId);
}
}

for await (const departmentId of departmentsToSave) {
await LivechatDepartment.updateOne(
{ _id: departmentId },
{
$set: {
parentId: _id,
ancestors,
},
},
);
matheusbsilva137 marked this conversation as resolved.
Show resolved Hide resolved
await LivechatDepartment.addDepartmentToUnit(departmentId, _id, ancestors);
}

await LivechatRooms.associateRoomsWithDepartmentToUnit(departmentsToSave, _id);
Expand All @@ -154,6 +137,14 @@ export class LivechatUnitRaw extends BaseRaw<IOmnichannelBusinessUnit> implement
return this.updateMany(query, update);
}

incrementDepartmentsCount(_id: string): Promise<UpdateResult | Document> {
return this.updateOne({ _id }, { $inc: { numDepartments: 1 } });
}

decrementDepartmentsCount(_id: string): Promise<UpdateResult | Document> {
return this.updateOne({ _id }, { $inc: { numDepartments: -1 } });
}

async removeById(_id: string): Promise<DeleteResult> {
await LivechatUnitMonitors.removeByUnitId(_id);
await this.removeParentAndAncestorById(_id);
Expand Down
2 changes: 2 additions & 0 deletions apps/meteor/lib/callbacks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ type ChainedCallbackSignatures = {
'unarchiveRoom': (room: IRoom) => void;
'roomAvatarChanged': (room: IRoom) => void;
'beforeGetMentions': (mentionIds: string[], teamMentions: MessageMention[]) => Promise<string[]>;
'livechat.manageDepartmentUnit': (params: { userId: string; departmentId: string; unitId?: string }) => void;
};

export type Hook =
Expand All @@ -240,6 +241,7 @@ export type Hook =
| 'livechat.offlineMessage'
| 'livechat.onCheckRoomApiParams'
| 'livechat.onLoadConfigApi'
| 'livechat.manageDepartmentUnit'
| 'loginPageStateChange'
| 'mapLDAPUserData'
| 'onCreateUser'
Expand Down
8 changes: 8 additions & 0 deletions apps/meteor/server/models/raw/LivechatDepartment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,14 @@ export class LivechatDepartmentRaw extends BaseRaw<ILivechatDepartment> implemen
return this.updateOne({ _id }, { $set: { archived: true, enabled: false } });
}

addDepartmentToUnit(_id: string, unitId: string, ancestors: string[]): Promise<Document | UpdateResult> {
return this.updateOne({ _id }, { $set: { parentId: unitId, ancestors } });
}

removeDepartmentFromUnit(_id: string): Promise<Document | UpdateResult> {
return this.updateOne({ _id }, { $set: { parentId: null, ancestors: null } });
}

matheusbsilva137 marked this conversation as resolved.
Show resolved Hide resolved
async createOrUpdateDepartment(
_id: string | null,
data: {
Expand Down
22 changes: 22 additions & 0 deletions apps/meteor/tests/data/livechat/units.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,25 @@ export const createUnit = async (
});
});
};

export const deleteUnit = async (unit: IOmnichannelBusinessUnit): Promise<IOmnichannelBusinessUnit> => {
return new Promise((resolve, reject) => {
void request
.post(methodCall(`livechat:removeUnit`))
.set(credentials)
.send({
message: JSON.stringify({
method: 'livechat:removeUnit',
params: [unit._id],
id: '101',
msg: 'method',
}),
})
.end((err: Error, res: DummyResponse<string, 'wrapped'>) => {
if (err) {
return reject(err);
}
resolve(JSON.parse(res.body.message).result);
});
});
};
Loading
Loading