Skip to content

Commit 8b0b581

Browse files
authored
Merge branch 'develop' into undeprecate-create-tokens-endpoint
2 parents ae7d1f9 + 5e821ad commit 8b0b581

File tree

14 files changed

+319
-112
lines changed

14 files changed

+319
-112
lines changed

.changeset/calm-hounds-look.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@rocket.chat/meteor": patch
3+
"@rocket.chat/rest-typings": patch
4+
---
5+
6+
Adds deprecation warning on `livechat:returnAsInquiry` with new endpoint replacing it; `livechat/inquiries.returnAsInquiry`

.changeset/late-impalas-battle.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@rocket.chat/i18n': patch
3+
'@rocket.chat/meteor': patch
4+
---
5+
6+
Changes the feedback message when inviting external users to a federated channel

apps/meteor/app/livechat/imports/server/rest/inquiries.ts

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
11
import { LivechatInquiryStatus } from '@rocket.chat/core-typings';
2-
import { LivechatInquiry, LivechatDepartment, Users } from '@rocket.chat/models';
2+
import { LivechatInquiry, LivechatDepartment, Users, LivechatRooms } from '@rocket.chat/models';
33
import {
44
isGETLivechatInquiriesListParams,
55
isPOSTLivechatInquiriesTakeParams,
66
isGETLivechatInquiriesQueuedForUserParams,
77
isGETLivechatInquiriesGetOneParams,
8+
validateBadRequestErrorResponse,
9+
validateUnauthorizedErrorResponse,
10+
validateForbiddenErrorResponse,
11+
isPOSTLivechatInquiriesReturnAsInquiry,
12+
POSTLivechatInquiriesReturnAsInquirySuccessResponse,
813
} from '@rocket.chat/rest-typings';
914

1015
import { API } from '../../../../api/server';
16+
import type { ExtractRoutesFromAPI } from '../../../../api/server/ApiClass';
1117
import { getPaginationItems } from '../../../../api/server/helpers/getPaginationItems';
1218
import { findInquiries, findOneInquiryByRoomId } from '../../../server/api/lib/inquiries';
19+
import { returnRoomAsInquiry } from '../../../server/lib/rooms';
1320
import { takeInquiry } from '../../../server/lib/takeInquiry';
1421

1522
API.v1.addRoute(
@@ -108,3 +115,45 @@ API.v1.addRoute(
108115
},
109116
},
110117
);
118+
119+
const livechatInquiriesEndpoints = API.v1.post(
120+
'livechat/inquiries.returnAsInquiry',
121+
{
122+
response: {
123+
200: POSTLivechatInquiriesReturnAsInquirySuccessResponse,
124+
400: validateBadRequestErrorResponse,
125+
401: validateUnauthorizedErrorResponse,
126+
403: validateForbiddenErrorResponse,
127+
},
128+
authRequired: true,
129+
permissionsRequired: ['view-l-room'],
130+
body: isPOSTLivechatInquiriesReturnAsInquiry,
131+
},
132+
async function action() {
133+
const { roomId, departmentId } = this.bodyParams;
134+
135+
try {
136+
const room = await LivechatRooms.findOneById(roomId);
137+
if (!room) {
138+
return API.v1.failure('error-room-not-found');
139+
}
140+
141+
const result = await returnRoomAsInquiry(room, departmentId);
142+
143+
return API.v1.success({ result });
144+
} catch (error) {
145+
if (error instanceof Meteor.Error && typeof error.error === 'string') {
146+
return API.v1.failure(error.error as string);
147+
}
148+
149+
return API.v1.failure('error-returning-inquiry');
150+
}
151+
},
152+
);
153+
154+
type LivechatInquiriesEndpoints = ExtractRoutesFromAPI<typeof livechatInquiriesEndpoints>;
155+
156+
declare module '@rocket.chat/rest-typings' {
157+
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface
158+
interface Endpoints extends LivechatInquiriesEndpoints {}
159+
}

apps/meteor/app/livechat/server/lib/rooms.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { AppEvents, Apps } from '@rocket.chat/apps';
2+
import { Omnichannel } from '@rocket.chat/core-services';
23
import type {
34
ILivechatVisitor,
45
IMessage,
@@ -211,13 +212,17 @@ export async function saveRoomInfo(
211212
export async function returnRoomAsInquiry(room: IOmnichannelRoom, departmentId?: string, overrideTransferData: Partial<TransferData> = {}) {
212213
livechatLogger.debug({ msg: `Transfering room to ${departmentId ? 'department' : ''} queue`, room });
213214
if (!room.open) {
214-
throw new Meteor.Error('room-closed');
215+
throw new Meteor.Error('room-closed', 'Room closed');
215216
}
216217

217218
if (room.onHold) {
218219
throw new Meteor.Error('error-room-onHold');
219220
}
220221

222+
if (!(await Omnichannel.isWithinMACLimit(room))) {
223+
throw new Meteor.Error('error-mac-limit-reached');
224+
}
225+
221226
if (!room.servedBy) {
222227
return false;
223228
}

apps/meteor/app/livechat/server/methods/returnAsInquiry.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { Omnichannel } from '@rocket.chat/core-services';
21
import type { ILivechatDepartment, IRoom } from '@rocket.chat/core-typings';
32
import type { ServerMethods } from '@rocket.chat/ddp-client';
43
import { LivechatRooms } from '@rocket.chat/models';
54
import { Meteor } from 'meteor/meteor';
65

76
import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
7+
import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger';
88
import { returnRoomAsInquiry } from '../lib/rooms';
99

1010
declare module '@rocket.chat/ddp-client' {
@@ -16,6 +16,7 @@ declare module '@rocket.chat/ddp-client' {
1616

1717
Meteor.methods<ServerMethods>({
1818
async 'livechat:returnAsInquiry'(rid, departmentId) {
19+
methodDeprecationLogger.method('livechat:returnAsInquiry', '8.0.0', '/v1/livechat/inquiries.returnAsInquiry');
1920
const uid = Meteor.userId();
2021
if (!uid || !(await hasPermissionAsync(uid, 'view-l-room'))) {
2122
throw new Meteor.Error('error-not-allowed', 'Not allowed', {
@@ -30,14 +31,6 @@ Meteor.methods<ServerMethods>({
3031
});
3132
}
3233

33-
if (!(await Omnichannel.isWithinMACLimit(room))) {
34-
throw new Meteor.Error('error-mac-limit-reached', 'MAC limit reached', { method: 'livechat:returnAsInquiry' });
35-
}
36-
37-
if (!room.open) {
38-
throw new Meteor.Error('room-closed', 'Room closed', { method: 'livechat:returnAsInquiry' });
39-
}
40-
4134
return returnRoomAsInquiry(room, departmentId);
4235
},
4336
});

apps/meteor/client/views/omnichannel/directory/chats/ChatInfo/ChatInfo.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ function ChatInfo({ id, route }: ChatInfoProps) {
5353
const { data: room } = useOmnichannelRoomInfo(id); // FIXME: `room` is serialized, but we need to deserialize it
5454

5555
const {
56+
_id: roomId,
5657
ts,
5758
tags,
5859
closedAt,
@@ -121,15 +122,17 @@ function ChatInfo({ id, route }: ChatInfoProps) {
121122
{departmentId && <DepartmentField departmentId={departmentId} />}
122123
{tags && tags.length > 0 && (
123124
<InfoPanelField>
124-
<InfoPanelLabel>{t('Tags')}</InfoPanelLabel>
125+
<InfoPanelLabel id={`${roomId}-tags`}>{t('Tags')}</InfoPanelLabel>
125126
<InfoPanelText>
126-
{tags.map((tag) => (
127-
<Box key={tag} mie={4} display='inline'>
128-
<Tag style={{ display: 'inline' }} disabled>
129-
{tag}
130-
</Tag>
131-
</Box>
132-
))}
127+
<ul aria-labelledby={`${roomId}-tags`}>
128+
{tags.map((tag) => (
129+
<Box is='li' key={tag} mie={4} display='inline'>
130+
<Tag style={{ display: 'inline' }} disabled>
131+
{tag}
132+
</Tag>
133+
</Box>
134+
))}
135+
</ul>
133136
</InfoPanelText>
134137
</InfoPanelField>
135138
)}

apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useReturnChatToQueueMutation.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { IRoom } from '@rocket.chat/core-typings';
2-
import { useMethod } from '@rocket.chat/ui-contexts';
2+
import { useEndpoint } from '@rocket.chat/ui-contexts';
33
import type { UseMutationOptions, UseMutationResult } from '@tanstack/react-query';
44
import { useMutation, useQueryClient } from '@tanstack/react-query';
55

@@ -8,13 +8,13 @@ import { roomsQueryKeys, subscriptionsQueryKeys } from '../../../../../../lib/qu
88
export const useReturnChatToQueueMutation = (
99
options?: Omit<UseMutationOptions<void, Error, IRoom['_id']>, 'mutationFn'>,
1010
): UseMutationResult<void, Error, IRoom['_id']> => {
11-
const returnChatToQueue = useMethod('livechat:returnAsInquiry');
11+
const returnChatToQueue = useEndpoint('POST', '/v1/livechat/inquiries.returnAsInquiry');
1212

1313
const queryClient = useQueryClient();
1414

1515
return useMutation({
1616
mutationFn: async (rid) => {
17-
await returnChatToQueue(rid);
17+
await returnChatToQueue({ roomId: rid });
1818
},
1919
...options,
2020
onSuccess: async (data, rid, context) => {

apps/meteor/client/views/room/HeaderV2/Omnichannel/QuickActions/hooks/useReturnChatToQueueMutation.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { IRoom } from '@rocket.chat/core-typings';
2-
import { useMethod } from '@rocket.chat/ui-contexts';
2+
import { useEndpoint } from '@rocket.chat/ui-contexts';
33
import type { UseMutationOptions, UseMutationResult } from '@tanstack/react-query';
44
import { useMutation, useQueryClient } from '@tanstack/react-query';
55

@@ -8,13 +8,13 @@ import { roomsQueryKeys, subscriptionsQueryKeys } from '../../../../../../lib/qu
88
export const useReturnChatToQueueMutation = (
99
options?: Omit<UseMutationOptions<void, Error, IRoom['_id']>, 'mutationFn'>,
1010
): UseMutationResult<void, Error, IRoom['_id']> => {
11-
const returnChatToQueue = useMethod('livechat:returnAsInquiry');
11+
const returnChatToQueue = useEndpoint('POST', '/v1/livechat/inquiries.returnAsInquiry');
1212

1313
const queryClient = useQueryClient();
1414

1515
return useMutation({
1616
mutationFn: async (rid) => {
17-
await returnChatToQueue(rid);
17+
await returnChatToQueue({ roomId: rid });
1818
},
1919
...options,
2020
onSuccess: async (data, rid, context) => {

apps/meteor/client/views/room/contextualBar/RoomMembers/AddUsers/AddUsers.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ const AddUsers = ({ rid, onClickBack, reload }: AddUsersProps): ReactElement =>
3636
const dispatchToastMessage = useToastMessageDispatch();
3737
const room = useRoom();
3838
const usersFieldId = useId();
39+
const roomIsFederated = isRoomFederated(room);
40+
// we are dropping the non native federation for now
41+
const isFederationBlocked = room && !isRoomNativeFederated(room);
3942

4043
const { closeTab } = useRoomToolbox();
4144
const saveAction = useMethod('addUsersToRoom');
@@ -50,7 +53,7 @@ const AddUsers = ({ rid, onClickBack, reload }: AddUsersProps): ReactElement =>
5053
const handleSave = useEffectEvent(async ({ users }: { users: string[] }) => {
5154
try {
5255
await saveAction({ rid, users });
53-
dispatchToastMessage({ type: 'success', message: t('Users_added') });
56+
dispatchToastMessage({ type: 'success', message: t(roomIsFederated && !isFederationBlocked ? 'Users_invited' : 'Users_added') });
5457
onClickBack();
5558
reload();
5659
} catch (error) {
@@ -60,10 +63,6 @@ const AddUsers = ({ rid, onClickBack, reload }: AddUsersProps): ReactElement =>
6063

6164
const addClickHandler = useAddMatrixUsers();
6265

63-
const roomIsFederated = isRoomFederated(room);
64-
// we are dropping the non native federation for now
65-
const isFederationBlocked = room && !isRoomNativeFederated(room);
66-
6766
return (
6867
<ContextualbarDialog>
6968
<ContextualbarHeader>
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { createFakeVisitor } from '../../mocks/data';
2+
import { IS_EE } from '../config/constants';
3+
import { Users } from '../fixtures/userStates';
4+
import { HomeOmnichannel } from '../page-objects';
5+
import { createAgent } from '../utils/omnichannel/agents';
6+
import { addAgentToDepartment, createDepartment } from '../utils/omnichannel/departments';
7+
import { createConversation } from '../utils/omnichannel/rooms';
8+
import { createTag } from '../utils/omnichannel/tags';
9+
import { test, expect } from '../utils/test';
10+
11+
const visitorA = createFakeVisitor();
12+
const visitorB = createFakeVisitor();
13+
14+
test.use({ storageState: Users.user1.state });
15+
16+
test.describe('OC - Tags Visibility', () => {
17+
test.skip(!IS_EE, 'OC - Tags Visibility > Enterprise Edition Only');
18+
19+
let poOmnichannel: HomeOmnichannel;
20+
let conversations: Awaited<ReturnType<typeof createConversation>>[] = [];
21+
let departmentA: Awaited<ReturnType<typeof createDepartment>>;
22+
let departmentB: Awaited<ReturnType<typeof createDepartment>>;
23+
let agent: Awaited<ReturnType<typeof createAgent>>;
24+
let tags: Awaited<ReturnType<typeof createTag>>[] = [];
25+
26+
test.beforeAll('Create departments', async ({ api }) => {
27+
departmentA = await createDepartment(api, { name: 'Department A' });
28+
departmentB = await createDepartment(api, { name: 'Department B' });
29+
});
30+
31+
test.beforeAll('Create agent', async ({ api }) => {
32+
agent = await createAgent(api, 'user1');
33+
});
34+
35+
test.beforeAll('Add agents to departments', async ({ api }) => {
36+
await Promise.all([
37+
addAgentToDepartment(api, { department: departmentA.data, agentId: 'user1' }),
38+
addAgentToDepartment(api, { department: departmentB.data, agentId: 'user1' }),
39+
]);
40+
});
41+
42+
test.beforeAll('Create tags', async ({ api }) => {
43+
tags = await Promise.all([
44+
createTag(api, { name: 'TagA', description: 'tag A', departments: [departmentA.data._id] }),
45+
createTag(api, { name: 'TagB', description: 'tag B', departments: [departmentB.data._id] }),
46+
createTag(api, { name: 'GlobalTag', description: 'public tag', departments: [] }),
47+
createTag(api, {
48+
name: 'SharedTag',
49+
description: 'tag for both departments',
50+
departments: [departmentA.data._id, departmentB.data._id],
51+
}),
52+
]);
53+
});
54+
55+
test.beforeAll('Create conversations', async ({ api }) => {
56+
conversations = await Promise.all([
57+
createConversation(api, { visitorName: visitorA.name, agentId: 'user1', departmentId: departmentA.data._id }),
58+
createConversation(api, { visitorName: visitorB.name, agentId: 'user1', departmentId: departmentB.data._id }),
59+
]);
60+
});
61+
62+
test.beforeEach(async ({ page }) => {
63+
poOmnichannel = new HomeOmnichannel(page);
64+
await page.goto('/');
65+
});
66+
67+
test.afterAll(async () => {
68+
await Promise.all(conversations.map((conversation) => conversation.delete()));
69+
await Promise.all(tags.map((tag) => tag.delete()));
70+
await agent.delete();
71+
await departmentA.delete();
72+
await departmentB.delete();
73+
});
74+
75+
test('Verify agent should see correct tags based on department association', async () => {
76+
await test.step('Agent opens room', async () => {
77+
await poOmnichannel.sidenav.getSidebarItemByName(visitorA.name).click();
78+
});
79+
80+
await test.step('should not be able to see tags field', async () => {
81+
await expect(poOmnichannel.roomInfo.getLabel('Tags')).not.toBeVisible();
82+
});
83+
84+
await test.step('check available tags', async () => {
85+
await poOmnichannel.roomInfo.btnEditRoomInfo.click();
86+
await expect(poOmnichannel.roomInfo.dialogEditRoom).toBeVisible();
87+
await poOmnichannel.roomInfo.inputTags.click();
88+
});
89+
90+
await test.step('Should see TagA (department A specific)', async () => {
91+
await expect(poOmnichannel.roomInfo.optionTags('TagA')).toBeVisible();
92+
});
93+
94+
await test.step('Should see SharedTag (both departments)', async () => {
95+
await expect(poOmnichannel.roomInfo.optionTags('SharedTag')).toBeVisible();
96+
});
97+
98+
await test.step('Should see Public Tags for all chats (no department restriction)', async () => {
99+
await expect(poOmnichannel.roomInfo.optionTags('GlobalTag')).toBeVisible();
100+
});
101+
102+
await test.step('Should not see TagB (department B specific)', async () => {
103+
await expect(poOmnichannel.roomInfo.optionTags('TagB')).not.toBeVisible();
104+
});
105+
106+
await test.step('add tags and save', async () => {
107+
await poOmnichannel.roomInfo.selectTag('TagA');
108+
await poOmnichannel.roomInfo.selectTag('GlobalTag');
109+
await poOmnichannel.roomInfo.btnSaveEditRoom.click();
110+
});
111+
112+
await test.step('verify selected tags are displayed under room information', async () => {
113+
await expect(poOmnichannel.roomInfo.getLabel('Tags')).toBeVisible();
114+
await expect(poOmnichannel.roomInfo.getTagInfoByLabel('TagA')).toBeVisible();
115+
await expect(poOmnichannel.roomInfo.getTagInfoByLabel('GlobalTag')).toBeVisible();
116+
});
117+
});
118+
119+
test('Verify tags visibility for agent associated with multiple departments', async () => {
120+
await test.step('Open room info', async () => {
121+
await poOmnichannel.sidenav.getSidebarItemByName(visitorB.name).click();
122+
await poOmnichannel.roomInfo.btnEditRoomInfo.click();
123+
await expect(poOmnichannel.roomInfo.dialogEditRoom).toBeVisible();
124+
await poOmnichannel.roomInfo.inputTags.click();
125+
});
126+
127+
await test.step('Agent associated with DepartmentB should be able to see tags for Department B', async () => {
128+
await expect(poOmnichannel.roomInfo.optionTags('TagB')).toBeVisible();
129+
});
130+
131+
await test.step('Agent associated with DepartmentB should not be able to see tags for DepartmentA', async () => {
132+
await expect(poOmnichannel.roomInfo.optionTags('TagA')).not.toBeVisible();
133+
});
134+
});
135+
});

0 commit comments

Comments
 (0)