From 72f00a6e9128df85af611794b348f66ee767787f Mon Sep 17 00:00:00 2001 From: rocketchat-github-ci Date: Mon, 19 Aug 2024 14:21:17 +0000 Subject: [PATCH 1/8] Bump 6.11.2 --- .changeset/bump-patch-1724077277110.md | 5 +++++ yarn.lock | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 10 deletions(-) create mode 100644 .changeset/bump-patch-1724077277110.md diff --git a/.changeset/bump-patch-1724077277110.md b/.changeset/bump-patch-1724077277110.md new file mode 100644 index 000000000000..e1eaa7980afb --- /dev/null +++ b/.changeset/bump-patch-1724077277110.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/yarn.lock b/yarn.lock index 84ca676c886c..93f2f41c7999 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8969,10 +8969,10 @@ __metadata: "@rocket.chat/icons": "*" "@rocket.chat/prettier-config": "*" "@rocket.chat/styled": "*" - "@rocket.chat/ui-avatar": 5.0.0 - "@rocket.chat/ui-contexts": 9.0.0 + "@rocket.chat/ui-avatar": 5.0.1 + "@rocket.chat/ui-contexts": 9.0.1 "@rocket.chat/ui-kit": 0.36.0 - "@rocket.chat/ui-video-conf": 9.0.0 + "@rocket.chat/ui-video-conf": 9.0.1 "@tanstack/react-query": "*" react: "*" react-dom: "*" @@ -9061,8 +9061,8 @@ __metadata: "@rocket.chat/fuselage-tokens": "*" "@rocket.chat/message-parser": 0.31.29 "@rocket.chat/styled": "*" - "@rocket.chat/ui-client": 9.0.0 - "@rocket.chat/ui-contexts": 9.0.0 + "@rocket.chat/ui-client": 9.0.1 + "@rocket.chat/ui-contexts": 9.0.1 katex: "*" react: "*" languageName: unknown @@ -10282,7 +10282,7 @@ __metadata: typescript: ~5.3.3 peerDependencies: "@rocket.chat/fuselage": "*" - "@rocket.chat/ui-contexts": 9.0.0 + "@rocket.chat/ui-contexts": 9.0.1 react: ~17.0.2 languageName: unknown linkType: soft @@ -10335,7 +10335,7 @@ __metadata: "@rocket.chat/fuselage": "*" "@rocket.chat/fuselage-hooks": "*" "@rocket.chat/icons": "*" - "@rocket.chat/ui-contexts": 9.0.0 + "@rocket.chat/ui-contexts": 9.0.1 react: ~17.0.2 languageName: unknown linkType: soft @@ -10511,8 +10511,8 @@ __metadata: "@rocket.chat/fuselage-hooks": "*" "@rocket.chat/icons": "*" "@rocket.chat/styled": "*" - "@rocket.chat/ui-avatar": 5.0.0 - "@rocket.chat/ui-contexts": 9.0.0 + "@rocket.chat/ui-avatar": 5.0.1 + "@rocket.chat/ui-contexts": 9.0.1 react: ^17.0.2 react-dom: ^17.0.2 languageName: unknown @@ -10602,7 +10602,7 @@ __metadata: peerDependencies: "@rocket.chat/layout": "*" "@rocket.chat/tools": 0.2.2 - "@rocket.chat/ui-contexts": 9.0.0 + "@rocket.chat/ui-contexts": 9.0.1 "@tanstack/react-query": "*" react: "*" react-hook-form: "*" From 6bcbc02ed3121c8f2c7e3fe4cccc42416abc8d87 Mon Sep 17 00:00:00 2001 From: "dionisio-bot[bot]" <117394943+dionisio-bot[bot]@users.noreply.github.com> Date: Mon, 19 Aug 2024 17:07:25 +0000 Subject: [PATCH 2/8] fix: Realtime Monitoring LineCharts not updating (#33086) Co-authored-by: Martin Schoeler <20868078+MartinSchoeler@users.noreply.github.com> --- .../RealTimeMonitoringPage.js | 57 +++++++++++++++---- 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/apps/meteor/client/views/omnichannel/realTimeMonitoring/RealTimeMonitoringPage.js b/apps/meteor/client/views/omnichannel/realTimeMonitoring/RealTimeMonitoringPage.js index 5b4d837d211c..b6e29530b5e7 100644 --- a/apps/meteor/client/views/omnichannel/realTimeMonitoring/RealTimeMonitoringPage.js +++ b/apps/meteor/client/views/omnichannel/realTimeMonitoring/RealTimeMonitoringPage.js @@ -18,11 +18,19 @@ import ChatsOverview from './overviews/ChatsOverview'; import ConversationOverview from './overviews/ConversationOverview'; import ProductivityOverview from './overviews/ProductivityOverview'; +const randomizeKeys = (keys) => { + keys.current = keys.current.map((_key, i) => { + return `${i}_${new Date().getTime()}`; + }); +}; + const dateRange = getDateRange(); const RealTimeMonitoringPage = () => { const t = useTranslation(); + const keys = useRef([...Array(10).keys()]); + const [reloadFrequency, setReloadFrequency] = useState(5); const [departmentId, setDepartment] = useState(''); @@ -43,6 +51,10 @@ const RealTimeMonitoringPage = () => { [departmentParams], ); + useEffect(() => { + randomizeKeys(keys); + }, [allParams]); + const reloadCharts = useMutableCallback(() => { Object.values(reloadRef.current).forEach((reload) => { reload(); @@ -53,6 +65,7 @@ const RealTimeMonitoringPage = () => { const interval = setInterval(reloadCharts, reloadFrequency * 1000); return () => { clearInterval(interval); + randomizeKeys(keys); }; }, [reloadCharts, reloadFrequency]); @@ -90,30 +103,54 @@ const RealTimeMonitoringPage = () => { - + - - + + - + - - + + - + - + - + - + From 9e1b9ad624e470bd60e83a9bc90c2c70504ac48a Mon Sep 17 00:00:00 2001 From: "dionisio-bot[bot]" <117394943+dionisio-bot[bot]@users.noreply.github.com> Date: Mon, 19 Aug 2024 18:45:46 +0000 Subject: [PATCH 3/8] fix: Avoid `processRoomAbandonment` callback from erroring when Business Hours config is missing for day (#33084) Co-authored-by: Kevin Aleman <11577696+KevLehman@users.noreply.github.com> --- .changeset/gentle-bugs-think.md | 5 +++++ .../app/livechat/server/hooks/processRoomAbandonment.ts | 8 +++++++- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 .changeset/gentle-bugs-think.md diff --git a/.changeset/gentle-bugs-think.md b/.changeset/gentle-bugs-think.md new file mode 100644 index 000000000000..fc4738f3043a --- /dev/null +++ b/.changeset/gentle-bugs-think.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Prevent `processRoomAbandonment` callback from erroring out when a room was inactive during a day Business Hours was not configured for. diff --git a/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.ts b/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.ts index 8a5a4c280670..8eb53fbb8fa7 100644 --- a/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.ts +++ b/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.ts @@ -43,7 +43,8 @@ const getSecondsSinceLastAgentResponse = async (room: IOmnichannelRoom, agentLas officeDays = (await businessHourManager.getBusinessHour())?.workHours.reduce(parseDays, {}); } - if (!officeDays) { + // Empty object we assume invalid config + if (!officeDays || !Object.keys(officeDays).length) { return getSecondsWhenOfficeHoursIsDisabled(room, agentLastMessage); } @@ -55,6 +56,11 @@ const getSecondsSinceLastAgentResponse = async (room: IOmnichannelRoom, agentLas for (let index = 0; index <= daysOfInactivity; index++) { const today = inactivityDay.clone().format('dddd'); const officeDay = officeDays[today]; + // Config doesnt have data for this day, we skip day + if (!officeDay) { + inactivityDay.add(1, 'days'); + continue; + } const startTodaysOfficeHour = moment(`${officeDay.start.day}:${officeDay.start.time}`, 'dddd:HH:mm').add(index, 'days'); const endTodaysOfficeHour = moment(`${officeDay.finish.day}:${officeDay.finish.time}`, 'dddd:HH:mm').add(index, 'days'); if (officeDays[today].open) { From 3d019906cc83aa6e9d646269593b3b5053861ed7 Mon Sep 17 00:00:00 2001 From: "dionisio-bot[bot]" <117394943+dionisio-bot[bot]@users.noreply.github.com> Date: Mon, 26 Aug 2024 14:30:47 +0000 Subject: [PATCH 4/8] fix: Forget session on window close (#33129) Co-authored-by: Yash Rajpal <58601732+yash-rajpal@users.noreply.github.com> --- .changeset/two-bikes-crash.md | 7 +++ apps/meteor/client/startup/accounts.ts | 13 +++++ .../externals/meteor/accounts-base.d.ts | 2 + ...account-forgetSessionOnWindowClose.spec.ts | 55 +++++++++++++++++++ 4 files changed, 77 insertions(+) create mode 100644 .changeset/two-bikes-crash.md create mode 100644 apps/meteor/tests/e2e/account-forgetSessionOnWindowClose.spec.ts diff --git a/.changeset/two-bikes-crash.md b/.changeset/two-bikes-crash.md new file mode 100644 index 000000000000..a120435e4a48 --- /dev/null +++ b/.changeset/two-bikes-crash.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixed an issue related to setting Accounts_ForgetUserSessionOnWindowClose, this setting was not working as expected. + +The new meteor 2.16 release introduced a new option to configure the Accounts package and choose between the local storage or session storage. They also changed how Meteor.\_localstorage works internally. Due to these changes in Meteor, our setting to use session storage wasn't working as expected. This PR fixes this issue and configures the Accounts package according to the workspace settings. diff --git a/apps/meteor/client/startup/accounts.ts b/apps/meteor/client/startup/accounts.ts index 3be110bc0a09..60f2de02bde0 100644 --- a/apps/meteor/client/startup/accounts.ts +++ b/apps/meteor/client/startup/accounts.ts @@ -2,6 +2,7 @@ import { Accounts } from 'meteor/accounts-base'; import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; +import { settings } from '../../app/settings/client'; import { mainReady } from '../../app/ui-utils/client'; import { sdk } from '../../app/utils/client/lib/SDKClient'; import { t } from '../../app/utils/lib/i18n'; @@ -24,3 +25,15 @@ Accounts.onEmailVerificationLink((token: string) => { }); }); }); + +Meteor.startup(() => { + Tracker.autorun(() => { + const forgetUserSessionOnWindowClose = settings.get('Accounts_ForgetUserSessionOnWindowClose'); + + if (forgetUserSessionOnWindowClose === undefined) { + return; + } + + Accounts.config({ clientStorage: forgetUserSessionOnWindowClose ? 'session' : 'local' }); + }); +}); diff --git a/apps/meteor/definition/externals/meteor/accounts-base.d.ts b/apps/meteor/definition/externals/meteor/accounts-base.d.ts index 3f0b148120e7..31b70f7b7154 100644 --- a/apps/meteor/definition/externals/meteor/accounts-base.d.ts +++ b/apps/meteor/definition/externals/meteor/accounts-base.d.ts @@ -42,6 +42,8 @@ declare module 'meteor/accounts-base' { function _clearAllLoginTokens(userId: string | null): void; + function config(options: { clientStorage: 'session' | 'local' }): void; + class ConfigError extends Error {} class LoginCancelledError extends Error { diff --git a/apps/meteor/tests/e2e/account-forgetSessionOnWindowClose.spec.ts b/apps/meteor/tests/e2e/account-forgetSessionOnWindowClose.spec.ts new file mode 100644 index 000000000000..a19b0e9866da --- /dev/null +++ b/apps/meteor/tests/e2e/account-forgetSessionOnWindowClose.spec.ts @@ -0,0 +1,55 @@ +import { DEFAULT_USER_CREDENTIALS } from './config/constants'; +import { Registration } from './page-objects'; +import { test, expect } from './utils/test'; + +test.describe.serial('Forget session on window close setting', () => { + let poRegistration: Registration; + + test.beforeEach(async ({ page }) => { + poRegistration = new Registration(page); + + await page.goto('/home'); + }); + + test.describe('Setting off', async () => { + test.beforeAll(async ({ api }) => { + await api.post('/settings/Accounts_ForgetUserSessionOnWindowClose', { value: false }); + }); + + test('Login using credentials and reload to stay logged in', async ({ page, context }) => { + await poRegistration.username.type('user1'); + await poRegistration.inputPassword.type(DEFAULT_USER_CREDENTIALS.password); + await poRegistration.btnLogin.click(); + + await expect(page.locator('role=heading[name="Welcome to Rocket.Chat"]')).toBeVisible(); + + const newPage = await context.newPage(); + await newPage.goto('/home'); + + await expect(newPage.locator('role=heading[name="Welcome to Rocket.Chat"]')).toBeVisible(); + }); + }); + + test.describe('Setting on', async () => { + test.beforeAll(async ({ api }) => { + await api.post('/settings/Accounts_ForgetUserSessionOnWindowClose', { value: true }); + }); + + test.afterAll(async ({ api }) => { + await api.post('/settings/Accounts_ForgetUserSessionOnWindowClose', { value: false }); + }); + + test('Login using credentials and reload to get logged out', async ({ page, context }) => { + await poRegistration.username.type('user1'); + await poRegistration.inputPassword.type(DEFAULT_USER_CREDENTIALS.password); + await poRegistration.btnLogin.click(); + + await expect(page.locator('role=heading[name="Welcome to Rocket.Chat"]')).toBeVisible(); + + const newPage = await context.newPage(); + await newPage.goto('/home'); + + await expect(newPage.locator('role=button[name="Login"]')).toBeVisible(); + }); + }); +}); From aa39197d1c41d04fdceec9b0340c02c3ae20ced3 Mon Sep 17 00:00:00 2001 From: "dionisio-bot[bot]" <117394943+dionisio-bot[bot]@users.noreply.github.com> Date: Mon, 26 Aug 2024 20:53:50 +0000 Subject: [PATCH 5/8] fix: imported fixes (#33153) Co-authored-by: Julio A. <52619625+julio-cfa@users.noreply.github.com> --- .changeset/orange-clocks-wait.md | 5 + .../functions/getModifiedHttpHeaders.ts | 20 ++ apps/meteor/app/lib/server/lib/debug.js | 3 +- .../meteor/app/livechat/server/api/v1/room.ts | 109 +++++---- .../app/livechat/server/api/v1/visitor.ts | 213 +++++++++--------- .../tests/end-to-end/api/miscellaneous.ts | 137 ++++++++++- .../functions/getModifiedHttpHeaders.tests.ts | 59 +++++ 7 files changed, 392 insertions(+), 154 deletions(-) create mode 100644 .changeset/orange-clocks-wait.md create mode 100644 apps/meteor/app/lib/server/functions/getModifiedHttpHeaders.ts create mode 100644 apps/meteor/tests/unit/app/lib/server/functions/getModifiedHttpHeaders.tests.ts diff --git a/.changeset/orange-clocks-wait.md b/.changeset/orange-clocks-wait.md new file mode 100644 index 000000000000..eacb88108a0f --- /dev/null +++ b/.changeset/orange-clocks-wait.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Security Hotfix (https://docs.rocket.chat/docs/security-fixes-and-updates) diff --git a/apps/meteor/app/lib/server/functions/getModifiedHttpHeaders.ts b/apps/meteor/app/lib/server/functions/getModifiedHttpHeaders.ts new file mode 100644 index 000000000000..e62727814de3 --- /dev/null +++ b/apps/meteor/app/lib/server/functions/getModifiedHttpHeaders.ts @@ -0,0 +1,20 @@ +export const getModifiedHttpHeaders = (httpHeaders: Record) => { + const modifiedHttpHeaders = { ...httpHeaders }; + + if ('x-auth-token' in modifiedHttpHeaders) { + modifiedHttpHeaders['x-auth-token'] = '[redacted]'; + } + + if (modifiedHttpHeaders.cookie) { + const cookies = modifiedHttpHeaders.cookie.split('; '); + const modifiedCookies = cookies.map((cookie: string) => { + if (cookie.startsWith('rc_token=')) { + return 'rc_token=[redacted]'; + } + return cookie; + }); + modifiedHttpHeaders.cookie = modifiedCookies.join('; '); + } + + return modifiedHttpHeaders; +}; diff --git a/apps/meteor/app/lib/server/lib/debug.js b/apps/meteor/app/lib/server/lib/debug.js index aaa492e80337..cbf38528579f 100644 --- a/apps/meteor/app/lib/server/lib/debug.js +++ b/apps/meteor/app/lib/server/lib/debug.js @@ -7,6 +7,7 @@ import _ from 'underscore'; import { getMethodArgs } from '../../../../server/lib/logger/logPayloads'; import { metrics } from '../../../metrics/server'; import { settings } from '../../../settings/server'; +import { getModifiedHttpHeaders } from '../functions/getModifiedHttpHeaders'; const logger = new Logger('Meteor'); @@ -41,7 +42,7 @@ const traceConnection = (enable, filter, prefix, name, connection, userId) => { console.log(name, { id: connection.id, clientAddress: connection.clientAddress, - httpHeaders: connection.httpHeaders, + httpHeaders: getModifiedHttpHeaders(connection.httpHeaders), userId, }); } else { diff --git a/apps/meteor/app/livechat/server/api/v1/room.ts b/apps/meteor/app/livechat/server/api/v1/room.ts index b0f45a63ff87..565f8e0bb3f4 100644 --- a/apps/meteor/app/livechat/server/api/v1/room.ts +++ b/apps/meteor/app/livechat/server/api/v1/room.ts @@ -31,63 +31,72 @@ import { findVisitorInfo } from '../lib/visitors'; const isAgentWithInfo = (agentObj: ILivechatAgent | { hiddenInfo: boolean }): agentObj is ILivechatAgent => !('hiddenInfo' in agentObj); -API.v1.addRoute('livechat/room', { - async get() { - // I'll temporary use check for validation, as validateParams doesnt support what's being done here - const extraCheckParams = await onCheckRoomParams({ - token: String, - rid: Match.Maybe(String), - agentId: Match.Maybe(String), - }); - - check(this.queryParams, extraCheckParams as any); - - const { token, rid, agentId, ...extraParams } = this.queryParams; - - const guest = token && (await findGuest(token)); - if (!guest) { - throw new Error('invalid-token'); - } - - if (!rid) { - const room = await LivechatRooms.findOneOpenByVisitorToken(token, {}); - if (room) { - return API.v1.success({ room, newRoom: false }); - } - - let agent: SelectedAgent | undefined; - const agentObj = agentId && (await findAgent(agentId)); - if (agentObj) { - if (isAgentWithInfo(agentObj)) { - const { username = undefined } = agentObj; - agent = { agentId, username }; - } else { - agent = { agentId }; - } +API.v1.addRoute( + 'livechat/room', + { + rateLimiterOptions: { + numRequestsAllowed: 5, + intervalTimeInMS: 60000, + }, + }, + { + async get() { + // I'll temporary use check for validation, as validateParams doesnt support what's being done here + const extraCheckParams = await onCheckRoomParams({ + token: String, + rid: Match.Maybe(String), + agentId: Match.Maybe(String), + }); + + check(this.queryParams, extraCheckParams as any); + + const { token, rid, agentId, ...extraParams } = this.queryParams; + + const guest = token && (await findGuest(token)); + if (!guest) { + throw new Error('invalid-token'); } - const roomInfo = { - source: { - type: isWidget(this.request.headers) ? OmnichannelSourceType.WIDGET : OmnichannelSourceType.API, - }, - }; + if (!rid) { + const room = await LivechatRooms.findOneOpenByVisitorToken(token, {}); + if (room) { + return API.v1.success({ room, newRoom: false }); + } - const newRoom = await LivechatTyped.createRoom({ visitor: guest, roomInfo, agent, extraData: extraParams }); + let agent: SelectedAgent | undefined; + const agentObj = agentId && (await findAgent(agentId)); + if (agentObj) { + if (isAgentWithInfo(agentObj)) { + const { username = undefined } = agentObj; + agent = { agentId, username }; + } else { + agent = { agentId }; + } + } - return API.v1.success({ - room: newRoom, - newRoom: true, - }); - } + const roomInfo = { + source: { + type: isWidget(this.request.headers) ? OmnichannelSourceType.WIDGET : OmnichannelSourceType.API, + }, + }; + + const newRoom = await LivechatTyped.createRoom({ visitor: guest, roomInfo, agent, extraData: extraParams }); - const froom = await LivechatRooms.findOneOpenByRoomIdAndVisitorToken(rid, token, {}); - if (!froom) { - throw new Error('invalid-room'); - } + return API.v1.success({ + room: newRoom, + newRoom: true, + }); + } + + const froom = await LivechatRooms.findOneOpenByRoomIdAndVisitorToken(rid, token, {}); + if (!froom) { + throw new Error('invalid-room'); + } - return API.v1.success({ room: froom, newRoom: false }); + return API.v1.success({ room: froom, newRoom: false }); + }, }, -}); +); // Note: use this route if a visitor is closing a room // If a RC user(like eg agent) is closing a room, use the `livechat/room.closeByUser` route diff --git a/apps/meteor/app/livechat/server/api/v1/visitor.ts b/apps/meteor/app/livechat/server/api/v1/visitor.ts index a5b3f2de35b1..ed32f0e2d279 100644 --- a/apps/meteor/app/livechat/server/api/v1/visitor.ts +++ b/apps/meteor/app/livechat/server/api/v1/visitor.ts @@ -9,119 +9,128 @@ import { settings } from '../../../../settings/server'; import { Livechat as LivechatTyped } from '../../lib/LivechatTyped'; import { findGuest, normalizeHttpHeaderData } from '../lib/livechat'; -API.v1.addRoute('livechat/visitor', { - async post() { - check(this.bodyParams, { - visitor: Match.ObjectIncluding({ - token: String, - name: Match.Maybe(String), - email: Match.Maybe(String), - department: Match.Maybe(String), - phone: Match.Maybe(String), - username: Match.Maybe(String), - customFields: Match.Maybe([ - Match.ObjectIncluding({ - key: String, - value: String, - overwrite: Boolean, - }), - ]), - }), - }); - - const { customFields, id, token, name, email, department, phone, username, connectionData } = this.bodyParams.visitor; - - if (!token?.trim()) { - throw new Meteor.Error('error-invalid-token', 'Token cannot be empty', { method: 'livechat/visitor' }); - } - - const guest = { - token, - ...(id && { id }), - ...(name && { name }), - ...(email && { email }), - ...(department && { department }), - ...(username && { username }), - ...(connectionData && { connectionData }), - ...(phone && typeof phone === 'string' && { phone: { number: phone as string } }), - connectionData: normalizeHttpHeaderData(this.request.headers), - }; - - const visitor = await LivechatTyped.registerGuest(guest); - if (!visitor) { - throw new Meteor.Error('error-livechat-visitor-registration', 'Error registering visitor', { - method: 'livechat/visitor', +API.v1.addRoute( + 'livechat/visitor', + { + rateLimiterOptions: { + numRequestsAllowed: 5, + intervalTimeInMS: 60000, + }, + }, + { + async post() { + check(this.bodyParams, { + visitor: Match.ObjectIncluding({ + token: String, + name: Match.Maybe(String), + email: Match.Maybe(String), + department: Match.Maybe(String), + phone: Match.Maybe(String), + username: Match.Maybe(String), + customFields: Match.Maybe([ + Match.ObjectIncluding({ + key: String, + value: String, + overwrite: Boolean, + }), + ]), + }), }); - } - const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); - // If it's updating an existing visitor, it must also update the roomInfo - const rooms = await LivechatRooms.findOpenByVisitorToken(visitor?.token, {}, extraQuery).toArray(); - await Promise.all( - rooms.map( - (room: IRoom) => - visitor && - LivechatTyped.saveRoomInfo(room, { - _id: visitor._id, - name: visitor.name, - phone: visitor.phone?.[0]?.phoneNumber, - livechatData: visitor.livechatData as { [k: string]: string }, - }), - ), - ); - - if (customFields && Array.isArray(customFields) && customFields.length > 0) { - const keys = customFields.map((field) => field.key); - const errors: string[] = []; - - const processedKeys = await Promise.all( - await LivechatCustomField.findByIdsAndScope>(keys, 'visitor', { - projection: { _id: 1 }, - }) - .map(async (field) => { - const customField = customFields.find((f) => f.key === field._id); - if (!customField) { - return; - } - - const { key, value, overwrite } = customField; - // TODO: Change this to Bulk update - if (!(await VisitorsRaw.updateLivechatDataByToken(token, key, value, overwrite))) { - errors.push(key); - } - - return key; - }) - .toArray(), - ); + const { customFields, id, token, name, email, department, phone, username, connectionData } = this.bodyParams.visitor; - if (processedKeys.length !== keys.length) { - LivechatTyped.logger.warn({ - msg: 'Some custom fields were not processed', - visitorId: visitor._id, - missingKeys: keys.filter((key) => !processedKeys.includes(key)), - }); + if (!token?.trim()) { + throw new Meteor.Error('error-invalid-token', 'Token cannot be empty', { method: 'livechat/visitor' }); } - if (errors.length > 0) { - LivechatTyped.logger.error({ - msg: 'Error updating custom fields', - visitorId: visitor._id, - errors, + const guest = { + token, + ...(id && { id }), + ...(name && { name }), + ...(email && { email }), + ...(department && { department }), + ...(username && { username }), + ...(connectionData && { connectionData }), + ...(phone && typeof phone === 'string' && { phone: { number: phone as string } }), + connectionData: normalizeHttpHeaderData(this.request.headers), + }; + + const visitor = await LivechatTyped.registerGuest(guest); + if (!visitor) { + throw new Meteor.Error('error-livechat-visitor-registration', 'Error registering visitor', { + method: 'livechat/visitor', }); - throw new Error('error-updating-custom-fields'); } - return API.v1.success({ visitor: await VisitorsRaw.findOneEnabledById(visitor._id) }); - } + const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); + // If it's updating an existing visitor, it must also update the roomInfo + const rooms = await LivechatRooms.findOpenByVisitorToken(visitor?.token, {}, extraQuery).toArray(); + await Promise.all( + rooms.map( + (room: IRoom) => + visitor && + LivechatTyped.saveRoomInfo(room, { + _id: visitor._id, + name: visitor.name, + phone: visitor.phone?.[0]?.phoneNumber, + livechatData: visitor.livechatData as { [k: string]: string }, + }), + ), + ); - if (!visitor) { - throw new Meteor.Error('error-saving-visitor', 'An error ocurred while saving visitor'); - } + if (customFields && Array.isArray(customFields) && customFields.length > 0) { + const keys = customFields.map((field) => field.key); + const errors: string[] = []; - return API.v1.success({ visitor }); + const processedKeys = await Promise.all( + await LivechatCustomField.findByIdsAndScope>(keys, 'visitor', { + projection: { _id: 1 }, + }) + .map(async (field) => { + const customField = customFields.find((f) => f.key === field._id); + if (!customField) { + return; + } + + const { key, value, overwrite } = customField; + // TODO: Change this to Bulk update + if (!(await VisitorsRaw.updateLivechatDataByToken(token, key, value, overwrite))) { + errors.push(key); + } + + return key; + }) + .toArray(), + ); + + if (processedKeys.length !== keys.length) { + LivechatTyped.logger.warn({ + msg: 'Some custom fields were not processed', + visitorId: visitor._id, + missingKeys: keys.filter((key) => !processedKeys.includes(key)), + }); + } + + if (errors.length > 0) { + LivechatTyped.logger.error({ + msg: 'Error updating custom fields', + visitorId: visitor._id, + errors, + }); + throw new Error('error-updating-custom-fields'); + } + + return API.v1.success({ visitor: await VisitorsRaw.findOneEnabledById(visitor._id) }); + } + + if (!visitor) { + throw new Meteor.Error('error-saving-visitor', 'An error ocurred while saving visitor'); + } + + return API.v1.success({ visitor }); + }, }, -}); +); API.v1.addRoute('livechat/visitor/:token', { async get() { diff --git a/apps/meteor/tests/end-to-end/api/miscellaneous.ts b/apps/meteor/tests/end-to-end/api/miscellaneous.ts index 613a874ecd8c..b8341f7c0994 100644 --- a/apps/meteor/tests/end-to-end/api/miscellaneous.ts +++ b/apps/meteor/tests/end-to-end/api/miscellaneous.ts @@ -5,7 +5,7 @@ import type { IInstance } from '@rocket.chat/rest-typings'; import { AssertionError, expect } from 'chai'; import { after, before, describe, it } from 'mocha'; -import { getCredentials, api, request, credentials } from '../../data/api-data'; +import { getCredentials, api, request, credentials, methodCall } from '../../data/api-data'; import { updatePermission, updateSetting } from '../../data/permissions.helper'; import { createRoom, deleteRoom } from '../../data/rooms.helper'; import { createTeam, deleteTeam } from '../../data/teams.helper'; @@ -703,4 +703,139 @@ describe('miscellaneous', () => { .end(done); }); }); + + describe('[/stdout.queue]', () => { + let testUser: TestUser; + let testUsername: string; + let testUserPassword: string; + before(async () => { + testUser = await createUser(); + testUsername = testUser.username; + testUserPassword = password; + await updateSetting('Log_Trace_Methods', true); + await updateSetting('Log_Level', '2'); + + // populate the logs by sending method calls + const populateLogsPromises = []; + populateLogsPromises.push( + request + .post(methodCall('getRoomRoles')) + .set(credentials) + .set('Cookie', `rc_token=${credentials['X-Auth-Token']}`) + .send({ + message: JSON.stringify({ + method: 'getRoomRoles', + params: ['GENERAL'], + id: 'id', + msg: 'method', + }), + }), + ); + + populateLogsPromises.push( + request + .post(methodCall('private-settings:get')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'private-settings/get', + params: [ + { + $date: new Date().getTime(), + }, + ], + id: 'id', + msg: 'method', + }), + }), + ); + + populateLogsPromises.push( + request.post(api('login')).send({ + user: { + username: testUsername, + }, + password: testUserPassword, + }), + ); + + await Promise.all(populateLogsPromises); + }); + + after(async () => { + await Promise.all([updateSetting('Log_Trace_Methods', false), updateSetting('Log_Level', '0'), deleteUser(testUser)]); + }); + + it('if log trace enabled, x-auth-token should be redacted', async () => { + await request + .get(api('stdout.queue')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('queue').that.is.an('array'); + + const { queue } = res.body; + let foundRedactedToken = false; + + for (const log of queue) { + if (log.string.includes("'x-auth-token': '[redacted]'")) { + foundRedactedToken = true; + break; + } + } + + expect(foundRedactedToken).to.be.true; + }); + }); + + it('if log trace enabled, rc_token should be redacted', async () => { + await request + .get(api('stdout.queue')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('queue').that.is.an('array'); + + const { queue } = res.body; + let foundRedactedCookie = false; + + for (const log of queue) { + if (log.string.includes('rc_token=[redacted]')) { + foundRedactedCookie = true; + break; + } + } + + expect(foundRedactedCookie).to.be.true; + }); + }); + + it('should not return user token anywhere in the log stream', async () => { + await request + .get(api('stdout.queue')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('queue').that.is.an('array'); + + const { queue } = res.body; + let foundTokenValue = false; + + for (const log of queue) { + if (log.string.includes(credentials['X-Auth-Token'])) { + foundTokenValue = true; + break; + } + } + + expect(foundTokenValue).to.be.false; + }); + }); + }); }); diff --git a/apps/meteor/tests/unit/app/lib/server/functions/getModifiedHttpHeaders.tests.ts b/apps/meteor/tests/unit/app/lib/server/functions/getModifiedHttpHeaders.tests.ts new file mode 100644 index 000000000000..5130bbe59a99 --- /dev/null +++ b/apps/meteor/tests/unit/app/lib/server/functions/getModifiedHttpHeaders.tests.ts @@ -0,0 +1,59 @@ +import { expect } from 'chai'; + +import { getModifiedHttpHeaders } from '../../../../../../app/lib/server/functions/getModifiedHttpHeaders'; + +describe('getModifiedHttpHeaders', () => { + it('should redact x-auth-token if present', () => { + const inputHeaders = { + 'x-auth-token': '12345', + 'some-other-header': 'value', + }; + const result = getModifiedHttpHeaders(inputHeaders); + expect(result['x-auth-token']).to.equal('[redacted]'); + expect(result['some-other-header']).to.equal('value'); + }); + + it('should not modify headers if x-auth-token is not present', () => { + const inputHeaders = { + 'some-other-header': 'value', + }; + const result = getModifiedHttpHeaders(inputHeaders); + expect(result).to.deep.equal(inputHeaders); + }); + + it('should redact rc_token in cookies if present', () => { + const inputHeaders = { + cookie: 'session_id=abc123; rc_token=98765; other_cookie=value', + }; + const expectedCookies = 'session_id=abc123; rc_token=[redacted]; other_cookie=value'; + const result = getModifiedHttpHeaders(inputHeaders); + expect(result.cookie).to.equal(expectedCookies); + }); + + it('should not modify cookies if rc_token is not present', () => { + const inputHeaders = { + cookie: 'session_id=abc123; other_cookie=value', + }; + const result = getModifiedHttpHeaders(inputHeaders); + expect(result.cookie).to.equal(inputHeaders.cookie); + }); + + it('should return headers unchanged if neither x-auth-token nor cookie are present', () => { + const inputHeaders = { + 'some-other-header': 'value', + }; + const result = getModifiedHttpHeaders(inputHeaders); + expect(result).to.deep.equal(inputHeaders); + }); + + it('should handle cases with both x-auth-token and rc_token in cookie', () => { + const inputHeaders = { + 'x-auth-token': '12345', + 'cookie': 'session_id=abc123; rc_token=98765; other_cookie=value', + }; + const expectedCookies = 'session_id=abc123; rc_token=[redacted]; other_cookie=value'; + const result = getModifiedHttpHeaders(inputHeaders); + expect(result['x-auth-token']).to.equal('[redacted]'); + expect(result.cookie).to.equal(expectedCookies); + }); +}); From 4baf1e4960a88fda9e47fcfe171f253663060b91 Mon Sep 17 00:00:00 2001 From: "dionisio-bot[bot]" <117394943+dionisio-bot[bot]@users.noreply.github.com> Date: Tue, 27 Aug 2024 19:29:13 +0000 Subject: [PATCH 6/8] regression: Handle live setting forget user session on window close update (#33165) Co-authored-by: Yash Rajpal <58601732+yash-rajpal@users.noreply.github.com> --- apps/meteor/client/startup/accounts.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/meteor/client/startup/accounts.ts b/apps/meteor/client/startup/accounts.ts index 60f2de02bde0..88008a606656 100644 --- a/apps/meteor/client/startup/accounts.ts +++ b/apps/meteor/client/startup/accounts.ts @@ -27,13 +27,15 @@ Accounts.onEmailVerificationLink((token: string) => { }); Meteor.startup(() => { - Tracker.autorun(() => { + Tracker.autorun((computation) => { const forgetUserSessionOnWindowClose = settings.get('Accounts_ForgetUserSessionOnWindowClose'); if (forgetUserSessionOnWindowClose === undefined) { return; } + computation.stop(); + Accounts.config({ clientStorage: forgetUserSessionOnWindowClose ? 'session' : 'local' }); }); }); From 87a59e0352c25f45c07b9b1d719d6ef7b9852763 Mon Sep 17 00:00:00 2001 From: "dionisio-bot[bot]" <117394943+dionisio-bot[bot]@users.noreply.github.com> Date: Wed, 28 Aug 2024 17:41:20 +0000 Subject: [PATCH 7/8] fix: Multi-step modals closing unexpectedly (#33178) Co-authored-by: Douglas Fabris <27704687+dougfabris@users.noreply.github.com> --- .changeset/wise-avocados-taste.md | 5 +++++ .../components/GenericModal/GenericModal.tsx | 2 +- .../GenericModal/GenericModalSkeleton.tsx | 22 +++++-------------- .../ReadReceiptsModal/ReadReceiptsModal.tsx | 8 ++----- .../ConvertToChannelModal.tsx | 2 +- .../DeleteTeam/DeleteTeamModalWithRooms.tsx | 12 +++------- .../info/LeaveTeam/LeaveTeamWithData.tsx | 12 +++------- .../RemoveUsersModal/RemoveUsersModal.js | 11 ++-------- 8 files changed, 22 insertions(+), 52 deletions(-) create mode 100644 .changeset/wise-avocados-taste.md diff --git a/.changeset/wise-avocados-taste.md b/.changeset/wise-avocados-taste.md new file mode 100644 index 000000000000..c4c9bce010b8 --- /dev/null +++ b/.changeset/wise-avocados-taste.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue where multi-step modals were closing unexpectedly diff --git a/apps/meteor/client/components/GenericModal/GenericModal.tsx b/apps/meteor/client/components/GenericModal/GenericModal.tsx index d371e1ff4ef2..5d025e05827d 100644 --- a/apps/meteor/client/components/GenericModal/GenericModal.tsx +++ b/apps/meteor/client/components/GenericModal/GenericModal.tsx @@ -111,7 +111,7 @@ const GenericModal = ({ {tagline && {tagline}} {title ?? t('Are_you_sure')} - + {onClose && } {children} diff --git a/apps/meteor/client/components/GenericModal/GenericModalSkeleton.tsx b/apps/meteor/client/components/GenericModal/GenericModalSkeleton.tsx index d56cbdd26a67..2dcdf3b3578c 100644 --- a/apps/meteor/client/components/GenericModal/GenericModalSkeleton.tsx +++ b/apps/meteor/client/components/GenericModal/GenericModalSkeleton.tsx @@ -1,25 +1,13 @@ import { Skeleton } from '@rocket.chat/fuselage'; -import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ComponentProps } from 'react'; import React from 'react'; import GenericModal from './GenericModal'; -const GenericModalSkeleton = ({ onClose, ...props }: ComponentProps) => { - const t = useTranslation(); - - return ( - } - confirmText={t('Cancel')} - onConfirm={onClose} - > - - - ); -}; +const GenericModalSkeleton = (props: ComponentProps) => ( + }> + + +); export default GenericModalSkeleton; diff --git a/apps/meteor/client/views/room/modals/ReadReceiptsModal/ReadReceiptsModal.tsx b/apps/meteor/client/views/room/modals/ReadReceiptsModal/ReadReceiptsModal.tsx index c4da16264646..ca033c2dcb0d 100644 --- a/apps/meteor/client/views/room/modals/ReadReceiptsModal/ReadReceiptsModal.tsx +++ b/apps/meteor/client/views/room/modals/ReadReceiptsModal/ReadReceiptsModal.tsx @@ -1,11 +1,11 @@ import type { IMessage, ReadReceipt } from '@rocket.chat/core-typings'; -import { Skeleton } from '@rocket.chat/fuselage'; import { useMethod, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import type { ReactElement } from 'react'; import React, { useEffect } from 'react'; import GenericModal from '../../../../components/GenericModal'; +import GenericModalSkeleton from '../../../../components/GenericModal/GenericModalSkeleton'; import ReadReceiptRow from './ReadReceiptRow'; type ReadReceiptsModalProps = { @@ -29,11 +29,7 @@ const ReadReceiptsModal = ({ messageId, onClose }: ReadReceiptsModalProps): Reac }, [dispatchToastMessage, t, onClose, readReceiptsResult.isError, readReceiptsResult.error]); if (readReceiptsResult.isLoading || readReceiptsResult.isError) { - return ( - - - - ); + return ; } const readReceipts = readReceiptsResult.data; diff --git a/apps/meteor/client/views/teams/ConvertToChannelModal/ConvertToChannelModal.tsx b/apps/meteor/client/views/teams/ConvertToChannelModal/ConvertToChannelModal.tsx index c29f6c0ec586..3efcdb89690f 100644 --- a/apps/meteor/client/views/teams/ConvertToChannelModal/ConvertToChannelModal.tsx +++ b/apps/meteor/client/views/teams/ConvertToChannelModal/ConvertToChannelModal.tsx @@ -20,7 +20,7 @@ const ConvertToChannelModal = ({ onClose, onCancel, onConfirm, teamId, userId }: }); if (phase === AsyncStatePhase.LOADING) { - return ; + return ; } return ; diff --git a/apps/meteor/client/views/teams/contextualBar/info/DeleteTeam/DeleteTeamModalWithRooms.tsx b/apps/meteor/client/views/teams/contextualBar/info/DeleteTeam/DeleteTeamModalWithRooms.tsx index 5226a3602c0f..1375ec532c91 100644 --- a/apps/meteor/client/views/teams/contextualBar/info/DeleteTeam/DeleteTeamModalWithRooms.tsx +++ b/apps/meteor/client/views/teams/contextualBar/info/DeleteTeam/DeleteTeamModalWithRooms.tsx @@ -1,11 +1,10 @@ import type { IRoom } from '@rocket.chat/core-typings'; -import { Skeleton } from '@rocket.chat/fuselage'; -import { useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; +import { useEndpoint } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import type { ReactElement } from 'react'; import React, { useMemo } from 'react'; -import GenericModal from '../../../../../components/GenericModal'; +import GenericModalSkeleton from '../../../../../components/GenericModal/GenericModalSkeleton'; import DeleteTeamModal from './DeleteTeamModal'; type DeleteTeamModalWithRoomsProps = { @@ -15,17 +14,12 @@ type DeleteTeamModalWithRoomsProps = { }; const DeleteTeamModalWithRooms = ({ teamId, onConfirm, onCancel }: DeleteTeamModalWithRoomsProps): ReactElement => { - const t = useTranslation(); const query = useMemo(() => ({ teamId }), [teamId]); const getTeamsListRooms = useEndpoint('GET', '/v1/teams.listRooms'); const { data, isLoading } = useQuery(['getTeamsListRooms', query], async () => getTeamsListRooms(query)); if (isLoading) { - return ( - } confirmText={t('Cancel')}> - - - ); + return ; } return ; }; diff --git a/apps/meteor/client/views/teams/contextualBar/info/LeaveTeam/LeaveTeamWithData.tsx b/apps/meteor/client/views/teams/contextualBar/info/LeaveTeam/LeaveTeamWithData.tsx index 9bab0acc3d86..58f98705d2bb 100644 --- a/apps/meteor/client/views/teams/contextualBar/info/LeaveTeam/LeaveTeamWithData.tsx +++ b/apps/meteor/client/views/teams/contextualBar/info/LeaveTeam/LeaveTeamWithData.tsx @@ -1,11 +1,10 @@ import type { ITeam } from '@rocket.chat/core-typings'; -import { Skeleton } from '@rocket.chat/fuselage'; -import { useUserId, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; +import { useUserId, useEndpoint } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import type { ReactElement } from 'react'; import React from 'react'; -import GenericModal from '../../../../../components/GenericModal'; +import GenericModalSkeleton from '../../../../../components/GenericModal/GenericModalSkeleton'; import LeaveTeamModal from './LeaveTeamModal/LeaveTeamModal'; type LeaveTeamWithDataProps = { @@ -15,7 +14,6 @@ type LeaveTeamWithDataProps = { }; const LeaveTeamWithData = ({ teamId, onCancel, onConfirm }: LeaveTeamWithDataProps): ReactElement => { - const t = useTranslation(); const userId = useUserId(); if (!userId) { @@ -26,11 +24,7 @@ const LeaveTeamWithData = ({ teamId, onCancel, onConfirm }: LeaveTeamWithDataPro const { data, isLoading } = useQuery(['teams.listRoomsOfUser'], () => getRoomsOfUser({ teamId, userId })); if (isLoading) { - return ( - } confirmText={t('Cancel')}> - - - ); + return ; } return ; diff --git a/apps/meteor/client/views/teams/contextualBar/members/RemoveUsersModal/RemoveUsersModal.js b/apps/meteor/client/views/teams/contextualBar/members/RemoveUsersModal/RemoveUsersModal.js index 76a38b680692..f85d5434c1d1 100644 --- a/apps/meteor/client/views/teams/contextualBar/members/RemoveUsersModal/RemoveUsersModal.js +++ b/apps/meteor/client/views/teams/contextualBar/members/RemoveUsersModal/RemoveUsersModal.js @@ -1,8 +1,6 @@ -import { Skeleton } from '@rocket.chat/fuselage'; -import { useTranslation } from '@rocket.chat/ui-contexts'; import React, { useMemo } from 'react'; -import GenericModal from '../../../../../components/GenericModal'; +import GenericModalSkeleton from '../../../../../components/GenericModal/GenericModalSkeleton'; import { useEndpointData } from '../../../../../hooks/useEndpointData'; import { AsyncStatePhase } from '../../../../../lib/asyncState'; import BaseRemoveUsersModal from './BaseRemoveUsersModal'; @@ -10,7 +8,6 @@ import BaseRemoveUsersModal from './BaseRemoveUsersModal'; const initialData = { user: { username: '' } }; const RemoveUsersModal = ({ teamId, userId, onClose, onCancel, onConfirm }) => { - const t = useTranslation(); const { value, phase } = useEndpointData('/v1/teams.listRoomsOfUser', { params: useMemo(() => ({ teamId, userId }), [teamId, userId]) }); const userDataFetch = useEndpointData('/v1/users.info', { params: useMemo(() => ({ userId }), [userId]), initialValue: initialData }); const { @@ -18,11 +15,7 @@ const RemoveUsersModal = ({ teamId, userId, onClose, onCancel, onConfirm }) => { } = userDataFetch?.value; if (phase === AsyncStatePhase.LOADING) { - return ( - } confirmText={t('Cancel')} onConfirm={onClose}> - - - ); + return ; } return ; From a9b1ca88ed36c73e90cf2b79c869efae16612405 Mon Sep 17 00:00:00 2001 From: "dionisio-bot[bot]" <117394943+dionisio-bot[bot]@users.noreply.github.com> Date: Thu, 29 Aug 2024 20:22:12 +0000 Subject: [PATCH 8/8] fix: restore tooltips to units Multiselect (#33185) Co-authored-by: Martin Schoeler <20868078+MartinSchoeler@users.noreply.github.com> --- .changeset/strong-rings-rush.md | 5 +++++ apps/meteor/client/omnichannel/units/UnitEdit.tsx | 2 +- .../meteor/tests/e2e/omnichannel/omnichannel-units.spec.ts | 7 +++++-- apps/meteor/tests/e2e/page-objects/omnichannel-units.ts | 4 ++++ packages/ui-client/src/components/TooltipComponent.tsx | 2 +- 5 files changed, 16 insertions(+), 4 deletions(-) create mode 100644 .changeset/strong-rings-rush.md diff --git a/.changeset/strong-rings-rush.md b/.changeset/strong-rings-rush.md new file mode 100644 index 000000000000..5125f47dcb3b --- /dev/null +++ b/.changeset/strong-rings-rush.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Restored tooltips to the unit edit department field selected options diff --git a/apps/meteor/client/omnichannel/units/UnitEdit.tsx b/apps/meteor/client/omnichannel/units/UnitEdit.tsx index e4bc1c0efb50..e71e8e2a94d0 100644 --- a/apps/meteor/client/omnichannel/units/UnitEdit.tsx +++ b/apps/meteor/client/omnichannel/units/UnitEdit.tsx @@ -228,7 +228,7 @@ const UnitEdit = ({ unitData, unitMonitors, unitDepartments }: UnitEditProps) => value={value} onChange={onChange} onBlur={onBlur} - withTitle={false} + withTitle filter={departmentsFilter} setFilter={setDepartmentsFilter} options={departmentsOptions} diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-units.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-units.spec.ts index 9c1b5fdd5948..cd33a56caa04 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-units.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-units.spec.ts @@ -185,11 +185,14 @@ test.describe('OC - Manage Units', () => { await poOmnichannelUnits.btnSave.click(); }); - await test.step('expect department to be in the chosen departments list', async () => { + await test.step('expect department to be in the chosen departments list and have title', async () => { await poOmnichannelUnits.search(unit.name); await poOmnichannelUnits.findRowByName(unit.name).click(); await expect(poOmnichannelUnits.contextualBar).toBeVisible(); - await expect(page.getByRole('option', { name: department2.data.name })).toBeVisible(); + await expect(poOmnichannelUnits.selectOptionChip(department2.data.name)).toBeVisible(); + await poOmnichannelUnits.selectOptionChip(department2.data.name).hover(); + + await expect(page.getByRole('tooltip', { name: department2.data.name })).toBeVisible(); await poOmnichannelUnits.btnContextualbarClose.click(); }); diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-units.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-units.ts index abcd794f8efa..2d0e24073a45 100644 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-units.ts +++ b/apps/meteor/tests/e2e/page-objects/omnichannel-units.ts @@ -43,6 +43,10 @@ export class OmnichannelUnits extends OmnichannelAdministration { return this.page.locator(`[role=option][value="${name}"]`); } + public selectOptionChip(name: string) { + return this.page.getByRole('option', { name }); + } + async selectDepartment({ name, _id }: { name: string; _id: string }) { await this.inputDepartments.click(); await this.inputDepartments.fill(name); diff --git a/packages/ui-client/src/components/TooltipComponent.tsx b/packages/ui-client/src/components/TooltipComponent.tsx index 137ec913ec78..4a46e5536a53 100644 --- a/packages/ui-client/src/components/TooltipComponent.tsx +++ b/packages/ui-client/src/components/TooltipComponent.tsx @@ -12,7 +12,7 @@ export const TooltipComponent = ({ title, anchor }: TooltipComponentProps): Reac return ( - {title} + {title} ); };