From 3b06229ba9b3487364dd4bd06b25a045a0979dbe Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Mon, 13 May 2024 15:58:46 -0300 Subject: [PATCH 01/36] refactor: PbxEvents out of DB Watcher (#32372) --- .../app/lib/server/lib/notifyListener.ts | 19 +++++++++++++++++-- .../server/database/watchCollections.ts | 2 +- .../asterisk/ami/ContinuousMonitor.ts | 14 ++++++++++---- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/apps/meteor/app/lib/server/lib/notifyListener.ts b/apps/meteor/app/lib/server/lib/notifyListener.ts index 821751f13540..c2e16117854c 100644 --- a/apps/meteor/app/lib/server/lib/notifyListener.ts +++ b/apps/meteor/app/lib/server/lib/notifyListener.ts @@ -1,6 +1,6 @@ import { api, dbWatchersDisabled } from '@rocket.chat/core-services'; -import type { IRocketChatRecord, IRoom } from '@rocket.chat/core-typings'; -import { Rooms } from '@rocket.chat/models'; +import type { IPbxEvent, IRocketChatRecord, IRoom } from '@rocket.chat/core-typings'; +import { PbxEvents, Rooms } from '@rocket.chat/models'; type ClientAction = 'inserted' | 'updated' | 'removed'; @@ -65,3 +65,18 @@ export async function notifyOnRoomChangedByUserDM( void api.broadcast('watch.rooms', { clientAction, room: item }); } } + +export async function notifyOnPbxEventChangedById( + id: T['_id'], + clientAction: ClientAction = 'updated', +): Promise { + if (!dbWatchersDisabled) { + return; + } + + const item = await PbxEvents.findOneById(id); + + if (item) { + void api.broadcast('watch.pbxevents', { clientAction, id, data: item }); + } +} diff --git a/apps/meteor/server/database/watchCollections.ts b/apps/meteor/server/database/watchCollections.ts index 984a4d0d470b..798575f999a8 100644 --- a/apps/meteor/server/database/watchCollections.ts +++ b/apps/meteor/server/database/watchCollections.ts @@ -39,7 +39,6 @@ export function getWatchCollections(): string[] { IntegrationHistory.getCollectionName(), Integrations.getCollectionName(), EmailInbox.getCollectionName(), - PbxEvents.getCollectionName(), Settings.getCollectionName(), LivechatPriority.getCollectionName(), Subscriptions.getCollectionName(), @@ -50,6 +49,7 @@ export function getWatchCollections(): string[] { collections.push(Messages.getCollectionName()); collections.push(Roles.getCollectionName()); collections.push(Rooms.getCollectionName()); + collections.push(PbxEvents.getCollectionName()); } if (onlyCollections.length > 0) { diff --git a/apps/meteor/server/services/voip-asterisk/connector/asterisk/ami/ContinuousMonitor.ts b/apps/meteor/server/services/voip-asterisk/connector/asterisk/ami/ContinuousMonitor.ts index f4d2a0d3c298..38f2ce32e99c 100644 --- a/apps/meteor/server/services/voip-asterisk/connector/asterisk/ami/ContinuousMonitor.ts +++ b/apps/meteor/server/services/voip-asterisk/connector/asterisk/ami/ContinuousMonitor.ts @@ -47,11 +47,11 @@ import { Logger } from '@rocket.chat/logger'; import { Users, PbxEvents } from '@rocket.chat/models'; import type { Db } from 'mongodb'; +import { notifyOnPbxEventChangedById } from '../../../../../../app/lib/server/lib/notifyListener'; import { Command, CommandType } from '../Command'; import { Commands } from '../Commands'; import { ACDQueue } from './ACDQueue'; import { CallbackContext } from './CallbackContext'; -// import { sendMessage } from '../../../../../../app/lib/server/functions/sendMessage'; export class ContinuousMonitor extends Command { private logger: Logger; @@ -140,13 +140,15 @@ export class ContinuousMonitor extends Command { // This event represents when an agent drops a call because of disconnection // May happen for any reason outside of our control, like closing the browswer // Or network/power issues - await PbxEvents.insertOne({ + const { insertedId } = await PbxEvents.insertOne({ event: eventName, uniqueId: `${eventName}-${event.contactstatus}-${now.getTime()}`, ts: now, agentExtension: event.aor, }); + void notifyOnPbxEventChangedById(insertedId, 'inserted'); + return; } @@ -159,7 +161,7 @@ export class ContinuousMonitor extends Command { // NOTE: using the uniqueId prop of event is not the recommented approach, since it's an opaque ID // However, since we're not using it for anything special, it's a "fair use" // uniqueId => {server}/{epoch}.{id of channel associated with this call} - await PbxEvents.insertOne({ + const { insertedId } = await PbxEvents.insertOne({ uniqueId, event: eventName, ts: now, @@ -170,6 +172,8 @@ export class ContinuousMonitor extends Command { callUniqueIdFallback: event.linkedid, agentExtension: event?.connectedlinenum, }); + + void notifyOnPbxEventChangedById(insertedId, 'inserted'); } catch (e) { this.logger.debug('Event was handled by other instance'); } @@ -282,7 +286,7 @@ export class ContinuousMonitor extends Command { * and event.calleridnum is the extension that is initiating a call. */ try { - await PbxEvents.insertOne({ + const { insertedId } = await PbxEvents.insertOne({ uniqueId: `${event.event}-${event.calleridnum}-${event.channel}-${event.destchannel}-${event.uniqueid}`, event: event.event, ts: new Date(), @@ -291,6 +295,8 @@ export class ContinuousMonitor extends Command { callUniqueIdFallback: event.linkedid, agentExtension: event.calleridnum, }); + + void notifyOnPbxEventChangedById(insertedId, 'inserted'); } catch (e) { // This could mean we received a duplicate event // This is quite common since DialEnd event happens "multiple times" at the end of the call From ff35376c37208580b3e8834860191e554c9fa397 Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Mon, 13 May 2024 17:36:03 -0300 Subject: [PATCH 02/36] fix: `GenericModal` with no ask again checkbox missing margin (#32414) --- .changeset/weak-starfishes-fail.md | 5 +++++ .../components/GenericModal/withDoNotAskAgain.tsx | 10 +++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 .changeset/weak-starfishes-fail.md diff --git a/.changeset/weak-starfishes-fail.md b/.changeset/weak-starfishes-fail.md new file mode 100644 index 000000000000..38e510229f6e --- /dev/null +++ b/.changeset/weak-starfishes-fail.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes the missing spacing on don`t ask again checkbox inside modals diff --git a/apps/meteor/client/components/GenericModal/withDoNotAskAgain.tsx b/apps/meteor/client/components/GenericModal/withDoNotAskAgain.tsx index 9d3d754d17d4..8d3644e0dc93 100644 --- a/apps/meteor/client/components/GenericModal/withDoNotAskAgain.tsx +++ b/apps/meteor/client/components/GenericModal/withDoNotAskAgain.tsx @@ -1,4 +1,5 @@ -import { Box, CheckBox } from '@rocket.chat/fuselage'; +import { Box, Label, CheckBox } from '@rocket.chat/fuselage'; +import { useUniqueId } from '@rocket.chat/fuselage-hooks'; import { useUserPreference, useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; import type { FC, ReactElement, ComponentType } from 'react'; import React, { useState } from 'react'; @@ -23,6 +24,7 @@ function withDoNotAskAgain( ): FC> { const WrappedComponent: FC> = function ({ onConfirm, dontAskAgain, ...props }) { const t = useTranslation(); + const dontAskAgainId = useUniqueId(); const dontAskAgainList = useUserPreference('dontAskAgainList'); const { action, label } = dontAskAgain; @@ -49,8 +51,10 @@ function withDoNotAskAgain( {...props} dontAskAgain={ - - + + } onConfirm={handleConfirm} From 4b6951b1d81f4884cf741e01114b42f19328d334 Mon Sep 17 00:00:00 2001 From: csuadev <72958726+csuadev@users.noreply.github.com> Date: Tue, 14 May 2024 15:45:24 +0200 Subject: [PATCH 03/36] test: Add Omnichannel Tags E2E tests (#32241) Co-authored-by: Douglas Fabris <27704687+dougfabris@users.noreply.github.com> --- .../ee/client/omnichannel/tags/TagEdit.tsx | 4 +- .../omnichannel-current-chats.spec.ts | 8 +- .../e2e/omnichannel/omnichannel-tags.spec.ts | 139 ++++++++++++++++++ .../fragments/omnichannel-sidenav.ts | 4 + apps/meteor/tests/e2e/page-objects/index.ts | 1 + .../e2e/page-objects/omnichannel-tags.ts | 71 +++++++++ .../tests/e2e/utils/omnichannel/tags.ts | 37 ++++- 7 files changed, 255 insertions(+), 9 deletions(-) create mode 100644 apps/meteor/tests/e2e/omnichannel/omnichannel-tags.spec.ts create mode 100644 apps/meteor/tests/e2e/page-objects/omnichannel-tags.ts diff --git a/apps/meteor/ee/client/omnichannel/tags/TagEdit.tsx b/apps/meteor/ee/client/omnichannel/tags/TagEdit.tsx index eb184f105c82..cd423c60b4bf 100644 --- a/apps/meteor/ee/client/omnichannel/tags/TagEdit.tsx +++ b/apps/meteor/ee/client/omnichannel/tags/TagEdit.tsx @@ -110,9 +110,7 @@ const TagEdit = ({ tagData, currentDepartments }: TagEditProps) => { ( - - )} + render={({ field }) => } /> diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-current-chats.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-current-chats.spec.ts index a3d105136efe..163a0f40f2b3 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-current-chats.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-current-chats.spec.ts @@ -23,6 +23,7 @@ test.describe('OC - Current Chats [Auto Selection]', async () => { let departments: Awaited>[]; let conversations: Awaited>[]; let agents: Awaited>[]; + let tags: Awaited>[]; // Allow manual on hold test.beforeAll(async ({ api }) => { @@ -61,9 +62,9 @@ test.describe('OC - Current Chats [Auto Selection]', async () => { // Create tags test.beforeAll(async ({ api }) => { - const promises = await Promise.all([createTag(api, 'tagA'), createTag(api, 'tagB')]); + tags = await Promise.all([createTag(api, { name: 'tagA' }), createTag(api, { name: 'tagB' })]); - promises.forEach((res) => expect(res.status()).toBe(200)); + tags.forEach((res) => expect(res.response.status()).toBe(200)); }); // Create rooms @@ -120,10 +121,11 @@ test.describe('OC - Current Chats [Auto Selection]', async () => { ...departments.map((department) => department.delete()), // Delete agents ...agents.map((agent) => agent.delete()), + // Delete tags + ...tags.map((tag) => tag.delete()), // Reset setting api.post('/settings/Livechat_allow_manual_on_hold', { value: false }), api.post('/settings/Livechat_allow_manual_on_hold_upon_agent_engagement_only', { value: true }), - // TODO: remove tags ]); }); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-tags.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-tags.spec.ts new file mode 100644 index 000000000000..151d5b8e762c --- /dev/null +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-tags.spec.ts @@ -0,0 +1,139 @@ +import { faker } from '@faker-js/faker'; +import type { Page } from '@playwright/test'; + +import { IS_EE } from '../config/constants'; +import { Users } from '../fixtures/userStates'; +import { OmnichannelTags } from '../page-objects'; +import { createAgent } from '../utils/omnichannel/agents'; +import { createDepartment } from '../utils/omnichannel/departments'; +import { createTag } from '../utils/omnichannel/tags'; +import { test, expect } from '../utils/test'; + +test.use({ storageState: Users.admin.state }); + +test.describe('OC - Manage Tags', () => { + test.skip(!IS_EE, 'OC - Manage Tags > Enterprise Edition Only'); + + let poOmnichannelTags: OmnichannelTags; + + let department: Awaited>; + let department2: Awaited>; + let agent: Awaited>; + + test.beforeAll(async ({ api }) => { + department = await createDepartment(api); + department2 = await createDepartment(api); + }); + + test.beforeAll(async ({ api }) => { + agent = await createAgent(api, 'user2'); + }); + + test.afterAll(async () => { + await department.delete(); + await agent.delete(); + }); + + test.beforeEach(async ({ page }: { page: Page }) => { + poOmnichannelTags = new OmnichannelTags(page); + }); + + test('OC - Manage Tags - Create Tag', async ({ page }) => { + const tagName = faker.string.uuid(); + + await page.goto('/omnichannel'); + await poOmnichannelTags.sidenav.linkTags.click(); + + await test.step('expect correct form default state', async () => { + await poOmnichannelTags.btnCreateTag.click(); + await expect(poOmnichannelTags.contextualBar).toBeVisible(); + await expect(poOmnichannelTags.btnSave).toBeDisabled(); + await expect(poOmnichannelTags.btnCancel).toBeEnabled(); + await poOmnichannelTags.btnCancel.click(); + await expect(poOmnichannelTags.contextualBar).not.toBeVisible(); + }); + + await test.step('expect to create new tag', async () => { + await poOmnichannelTags.btnCreateTag.click(); + await poOmnichannelTags.inputName.fill(tagName); + await poOmnichannelTags.selectDepartment(department.data); + await poOmnichannelTags.btnSave.click(); + await expect(poOmnichannelTags.contextualBar).not.toBeVisible(); + + await test.step('expect tag to have been created', async () => { + await poOmnichannelTags.search(tagName); + await expect(poOmnichannelTags.findRowByName(tagName)).toBeVisible(); + }); + }); + + await test.step('expect to delete tag', async () => { + await test.step('expect to be able to cancel delete', async () => { + await poOmnichannelTags.btnDeleteByName(tagName).click(); + await expect(poOmnichannelTags.confirmDeleteModal).toBeVisible(); + await poOmnichannelTags.btnCancelDeleteModal.click(); + await expect(poOmnichannelTags.confirmDeleteModal).not.toBeVisible(); + }); + + await test.step('expect to confirm delete', async () => { + await poOmnichannelTags.btnDeleteByName(tagName).click(); + await expect(poOmnichannelTags.confirmDeleteModal).toBeVisible(); + await poOmnichannelTags.btnConfirmDeleteModal.click(); + await expect(poOmnichannelTags.confirmDeleteModal).not.toBeVisible(); + await expect(page.locator('h3 >> text="No tags yet"')).toBeVisible(); + }); + }); + }); + + test('OC - Manage Tags - Edit tag departments', async ({ api, page }) => { + const tag = await test.step('expect to create new tag', async () => { + const { data: tag } = await createTag(api, { + name: faker.string.uuid(), + departments: [department.data._id], + }); + + return tag; + }); + + await page.goto('/omnichannel'); + await poOmnichannelTags.sidenav.linkTags.click(); + + await test.step('expect to add tag departments', async () => { + await poOmnichannelTags.search(tag.name); + await poOmnichannelTags.findRowByName(tag.name).click(); + await expect(poOmnichannelTags.contextualBar).toBeVisible(); + await poOmnichannelTags.selectDepartment({ name: department2.data.name, _id: department2.data._id }); + await poOmnichannelTags.btnSave.click(); + }); + + await test.step('expect department to be in the chosen departments list', async () => { + await poOmnichannelTags.search(tag.name); + await poOmnichannelTags.findRowByName(tag.name).click(); + await expect(poOmnichannelTags.contextualBar).toBeVisible(); + await expect(page.getByRole('option', { name: department2.data.name })).toBeVisible(); + await poOmnichannelTags.btnContextualbarClose.click(); + }); + + await test.step('expect to remove tag departments', async () => { + await poOmnichannelTags.search(tag.name); + await poOmnichannelTags.findRowByName(tag.name).click(); + await expect(poOmnichannelTags.contextualBar).toBeVisible(); + await poOmnichannelTags.selectDepartment({ name: department2.data.name, _id: department2.data._id }); + await poOmnichannelTags.btnSave.click(); + }); + + await test.step('expect department to not be in the chosen departments list', async () => { + await poOmnichannelTags.search(tag.name); + await poOmnichannelTags.findRowByName(tag.name).click(); + await expect(poOmnichannelTags.contextualBar).toBeVisible(); + await expect(page.getByRole('option', { name: department2.data.name })).toBeHidden(); + }); + + await test.step('expect to delete tag', async () => { + await poOmnichannelTags.btnDeleteByName(tag.name).click(); + await expect(poOmnichannelTags.confirmDeleteModal).toBeVisible(); + await poOmnichannelTags.btnConfirmDeleteModal.click(); + await expect(poOmnichannelTags.confirmDeleteModal).not.toBeVisible(); + await expect(page.locator('h3 >> text="No results found"')).toBeVisible(); + }); + }); +}); \ No newline at end of file diff --git a/apps/meteor/tests/e2e/page-objects/fragments/omnichannel-sidenav.ts b/apps/meteor/tests/e2e/page-objects/fragments/omnichannel-sidenav.ts index 04af530ea734..63fc3c391935 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/omnichannel-sidenav.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/omnichannel-sidenav.ts @@ -70,4 +70,8 @@ export class OmnichannelSidenav { get linkLivechatAppearance(): Locator { return this.page.locator('a[href="/omnichannel/appearance"]'); } + + get linkTags(): Locator { + return this.page.locator('a[href="/omnichannel/tags"]'); + } } diff --git a/apps/meteor/tests/e2e/page-objects/index.ts b/apps/meteor/tests/e2e/page-objects/index.ts index b8f335a6f92d..5d8284fb420b 100644 --- a/apps/meteor/tests/e2e/page-objects/index.ts +++ b/apps/meteor/tests/e2e/page-objects/index.ts @@ -16,4 +16,5 @@ export * from './omnichannel-units'; export * from './home-omnichannel'; export * from './omnichannel-monitors'; export * from './omnichannel-settings'; +export * from './omnichannel-tags'; export * from './utils'; diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-tags.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-tags.ts new file mode 100644 index 000000000000..064877aab1c6 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/omnichannel-tags.ts @@ -0,0 +1,71 @@ +import type { Locator } from '@playwright/test'; + +import { OmnichannelAdministration } from './omnichannel-administration'; + +export class OmnichannelTags extends OmnichannelAdministration { + get btnCreateTag(): Locator { + return this.page.locator('header').locator('role=button[name="Create tag"]'); + } + + get contextualBar(): Locator { + return this.page.locator('div[role="dialog"].rcx-vertical-bar'); + } + + get btnSave(): Locator { + return this.contextualBar.locator('role=button[name="Save"]'); + } + + get btnCancel(): Locator { + return this.contextualBar.locator('role=button[name="Cancel"]'); + } + + get inputName(): Locator { + return this.page.locator('[name="name"]'); + } + + get inputSearch(): Locator { + return this.page.locator('[placeholder="Search"]'); + } + + get confirmDeleteModal(): Locator { + return this.page.locator('dialog:has(h2:has-text("Are you sure?"))'); + } + + get btnCancelDeleteModal(): Locator { + return this.confirmDeleteModal.locator('role=button[name="Cancel"]'); + } + + get btnConfirmDeleteModal(): Locator { + return this.confirmDeleteModal.locator('role=button[name="Delete"]'); + } + + get btnContextualbarClose(): Locator { + return this.contextualBar.locator('button[aria-label="Close"]'); + } + + btnDeleteByName(name: string): Locator { + return this.page.locator(`role=link[name="${name} Remove"] >> role=button`); + } + + findRowByName(name: string): Locator { + return this.page.locator(`tr:has-text("${name}")`); + } + + get inputDepartments(): Locator { + return this.page.locator('input[placeholder="Select an option"]'); + } + + private selectOption(name: string): Locator { + return this.page.locator(`[role=option][value="${name}"]`); + } + + async search(text: string) { + await this.inputSearch.fill(text); + } + + async selectDepartment({ name, _id }: { name: string; _id: string }) { + await this.inputDepartments.click(); + await this.inputDepartments.fill(name); + await this.selectOption(_id).click(); + } +} diff --git a/apps/meteor/tests/e2e/utils/omnichannel/tags.ts b/apps/meteor/tests/e2e/utils/omnichannel/tags.ts index 2df9e57e1c7a..078ca8dd02f7 100644 --- a/apps/meteor/tests/e2e/utils/omnichannel/tags.ts +++ b/apps/meteor/tests/e2e/utils/omnichannel/tags.ts @@ -1,6 +1,37 @@ +import { ILivechatTag } from '@rocket.chat/core-typings'; + import { BaseTest } from '../test'; +import { parseMeteorResponse } from './utils'; + +type CreateTagParams = { + id?: string | null; + name?: string; + description?: string; + departments?: { departmentId: string }[]; +}; -export const createTag = async (api: BaseTest['api'], name: string) => - api.post('/method.call/livechat:saveTag', { - message: JSON.stringify({ msg: 'method', id: '33', method: 'livechat:saveTag', params: [null, { name, description: '' }, []] }), +const removeTag = async (api: BaseTest['api'], id: string) => + api.post('/method.call/omnichannel:removeTag', { + message: JSON.stringify({ msg: 'method', id: '33', method: 'livechat:removeTag', params: [id] }), }); + +export const createTag = async ( + api: BaseTest['api'], + { id = null, name, description = '', departments = [] }: CreateTagParams = {}, + ) => { + const response = await api.post('/method.call/livechat:saveTag', { + message: JSON.stringify({ + msg: 'method', + id: '33', + method: 'livechat:saveTag', + params: [id, { name, description }, departments]}) + }); + + const tag = await parseMeteorResponse(response); + + return { + response, + data: tag, + delete: async () => removeTag(api, tag?._id), + }; +}; From 18cae0385ca3007b70457cb4d503727d63f44b0b Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Tue, 14 May 2024 12:32:54 -0300 Subject: [PATCH 04/36] test: Fix messaging flaky e2e test (#32429) --- apps/meteor/tests/e2e/channel-management.spec.ts | 2 +- apps/meteor/tests/e2e/messaging.spec.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/meteor/tests/e2e/channel-management.spec.ts b/apps/meteor/tests/e2e/channel-management.spec.ts index a082aca17868..aeaa74773354 100644 --- a/apps/meteor/tests/e2e/channel-management.spec.ts +++ b/apps/meteor/tests/e2e/channel-management.spec.ts @@ -259,7 +259,7 @@ test.describe.serial('channel-management', () => { await expect(page.getByRole('navigation')).toBeVisible(); }); - test('should info contextualbar when clicking on roomName', async ({ page }) => { + test('should open room info when clicking on roomName', async ({ page }) => { await poHomeChannel.sidenav.openChat(targetChannel); await page.getByRole('button', { name: targetChannel }).first().focus(); await page.keyboard.press('Space'); diff --git a/apps/meteor/tests/e2e/messaging.spec.ts b/apps/meteor/tests/e2e/messaging.spec.ts index edc322c574a0..226b75ecc9a8 100644 --- a/apps/meteor/tests/e2e/messaging.spec.ts +++ b/apps/meteor/tests/e2e/messaging.spec.ts @@ -105,7 +105,7 @@ test.describe.serial('Messaging', () => { await expect(page.locator('[data-qa-type="message"]').last()).not.toBeFocused(); }); - test('should focus on the recent message when moving the focus on the list and theres no previous focus', async ({ page }) => { + test('should focus the latest message when moving the focus on the list and theres no previous focus', async ({ page }) => { await poHomeChannel.sidenav.openChat(targetChannel); await page.getByRole('button', { name: targetChannel }).first().focus(); @@ -115,7 +115,7 @@ test.describe.serial('Messaging', () => { await page.keyboard.press('Tab'); await expect(page.locator('[data-qa-type="message"]').last()).toBeFocused(); - await page.getByRole('button', { name: targetChannel }).first().click(); + await page.getByRole('button', { name: targetChannel }).first().focus(); // move focus to the list again await page.keyboard.press('Tab'); From 190d1ded2d14aa2ca59c61977353b61853f2c362 Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Tue, 14 May 2024 13:32:18 -0300 Subject: [PATCH 05/36] refactor: Permissions out of DB Watcher (#32360) Co-authored-by: Diego Sampaio <8591547+sampaiodiego@users.noreply.github.com> --- apps/meteor/app/api/server/v1/permissions.ts | 2 + apps/meteor/app/api/server/v1/roles.ts | 4 +- .../server/methods/addPermissionToRole.ts | 5 ++ .../methods/removeRoleFromPermission.ts | 5 +- .../app/lib/server/lib/notifyListener.ts | 65 ++++++++++++++++++- .../server/lib/notifyListenerOnRoleChanges.ts | 25 ------- apps/meteor/ee/server/lib/roles/insertRole.ts | 4 +- apps/meteor/ee/server/lib/roles/updateRole.ts | 4 +- .../server/database/watchCollections.ts | 2 +- .../lib/roles/createOrUpdateProtectedRole.ts | 8 +-- 10 files changed, 85 insertions(+), 39 deletions(-) delete mode 100644 apps/meteor/app/lib/server/lib/notifyListenerOnRoleChanges.ts diff --git a/apps/meteor/app/api/server/v1/permissions.ts b/apps/meteor/app/api/server/v1/permissions.ts index 4b860d6e1eac..3613cc171354 100644 --- a/apps/meteor/app/api/server/v1/permissions.ts +++ b/apps/meteor/app/api/server/v1/permissions.ts @@ -4,6 +4,7 @@ import { isBodyParamsValidPermissionUpdate } from '@rocket.chat/rest-typings'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { notifyOnPermissionChangedById } from '../../../lib/server/lib/notifyListener'; import { API } from '../api'; API.v1.addRoute( @@ -70,6 +71,7 @@ API.v1.addRoute( for await (const permission of bodyParams.permissions) { await Permissions.setRoles(permission._id, permission.roles); + void notifyOnPermissionChangedById(permission._id); } const result = (await Meteor.callAsync('permissions/get')) as IPermission[]; diff --git a/apps/meteor/app/api/server/v1/roles.ts b/apps/meteor/app/api/server/v1/roles.ts index 6727f4f970cb..66c6677a9eed 100644 --- a/apps/meteor/app/api/server/v1/roles.ts +++ b/apps/meteor/app/api/server/v1/roles.ts @@ -9,7 +9,7 @@ import { getUsersInRolePaginated } from '../../../authorization/server/functions import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { hasRoleAsync, hasAnyRoleAsync } from '../../../authorization/server/functions/hasRole'; import { apiDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; -import { notifyListenerOnRoleChanges } from '../../../lib/server/lib/notifyListenerOnRoleChanges'; +import { notifyOnRoleChanged } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server/index'; import { API } from '../api'; import { getPaginationItems } from '../helpers/getPaginationItems'; @@ -180,7 +180,7 @@ API.v1.addRoute( await Roles.removeById(role._id); - void notifyListenerOnRoleChanges(role._id, 'removed', role); + void notifyOnRoleChanged(role, 'removed'); return API.v1.success(); }, diff --git a/apps/meteor/app/authorization/server/methods/addPermissionToRole.ts b/apps/meteor/app/authorization/server/methods/addPermissionToRole.ts index cb6422a03142..13a114732bd2 100644 --- a/apps/meteor/app/authorization/server/methods/addPermissionToRole.ts +++ b/apps/meteor/app/authorization/server/methods/addPermissionToRole.ts @@ -2,6 +2,7 @@ import { Permissions } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; +import { notifyOnPermissionChangedById } from '../../../lib/server/lib/notifyListener'; import { CONSTANTS, AuthorizationUtils } from '../../lib'; import { hasPermissionAsync } from '../functions/hasPermission'; @@ -41,11 +42,15 @@ Meteor.methods({ action: 'Adding_permission', }); } + // for setting-based-permissions, authorize the group access as well if (permission.groupPermissionId) { await Permissions.addRole(permission.groupPermissionId, role); + void notifyOnPermissionChangedById(permission.groupPermissionId); } await Permissions.addRole(permission._id, role); + + void notifyOnPermissionChangedById(permission._id); }, }); diff --git a/apps/meteor/app/authorization/server/methods/removeRoleFromPermission.ts b/apps/meteor/app/authorization/server/methods/removeRoleFromPermission.ts index 30a1b2a759b6..91a4df1eddf7 100644 --- a/apps/meteor/app/authorization/server/methods/removeRoleFromPermission.ts +++ b/apps/meteor/app/authorization/server/methods/removeRoleFromPermission.ts @@ -2,6 +2,7 @@ import { Permissions } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; +import { notifyOnPermissionChangedById } from '../../../lib/server/lib/notifyListener'; import { CONSTANTS } from '../../lib'; import { hasPermissionAsync } from '../functions/hasPermission'; @@ -36,10 +37,12 @@ Meteor.methods({ // for setting based permissions, revoke the group permission once all setting permissions // related to this group have been removed - if (permission.groupPermissionId) { await Permissions.removeRole(permission.groupPermissionId, role); + void notifyOnPermissionChangedById(permission.groupPermissionId); } + await Permissions.removeRole(permission._id, role); + void notifyOnPermissionChangedById(permission._id); }, }); diff --git a/apps/meteor/app/lib/server/lib/notifyListener.ts b/apps/meteor/app/lib/server/lib/notifyListener.ts index c2e16117854c..9ec4bbf6be1b 100644 --- a/apps/meteor/app/lib/server/lib/notifyListener.ts +++ b/apps/meteor/app/lib/server/lib/notifyListener.ts @@ -1,6 +1,6 @@ import { api, dbWatchersDisabled } from '@rocket.chat/core-services'; -import type { IPbxEvent, IRocketChatRecord, IRoom } from '@rocket.chat/core-typings'; -import { PbxEvents, Rooms } from '@rocket.chat/models'; +import type { IPermission, IRocketChatRecord, IRoom, ISetting, IPbxEvent, IRole } from '@rocket.chat/core-typings'; +import { Rooms, Permissions, Settings, PbxEvents, Roles } from '@rocket.chat/models'; type ClientAction = 'inserted' | 'updated' | 'removed'; @@ -66,6 +66,43 @@ export async function notifyOnRoomChangedByUserDM( } } +export async function notifyOnSettingChanged(setting: ISetting, clientAction: ClientAction = 'updated'): Promise { + if (!dbWatchersDisabled) { + return; + } + + void api.broadcast('watch.settings', { clientAction, setting }); +} + +export async function notifyOnPermissionChanged(permission: IPermission, clientAction: ClientAction = 'updated'): Promise { + if (!dbWatchersDisabled) { + return; + } + + void api.broadcast('permission.changed', { clientAction, data: permission }); + + if (permission.level === 'settings' && permission.settingId) { + const setting = await Settings.findOneNotHiddenById(permission.settingId); + if (!setting) { + return; + } + void notifyOnSettingChanged(setting, 'updated'); + } +} + +export async function notifyOnPermissionChangedById(pid: IPermission['_id'], clientAction: ClientAction = 'updated'): Promise { + if (!dbWatchersDisabled) { + return; + } + + const permission = await Permissions.findOneById(pid); + if (!permission) { + return; + } + + return notifyOnPermissionChanged(permission, clientAction); +} + export async function notifyOnPbxEventChangedById( id: T['_id'], clientAction: ClientAction = 'updated', @@ -80,3 +117,27 @@ export async function notifyOnPbxEventChangedById( void api.broadcast('watch.pbxevents', { clientAction, id, data: item }); } } + +export async function notifyOnRoleChanged(role: T, clientAction: 'removed' | 'changed' = 'changed'): Promise { + if (!dbWatchersDisabled) { + return; + } + + void api.broadcast('watch.roles', { clientAction, role }); +} + +export async function notifyOnRoleChangedById( + id: T['_id'], + clientAction: 'removed' | 'changed' = 'changed', +): Promise { + if (!dbWatchersDisabled) { + return; + } + + const role = await Roles.findOneById(id); + if (!role) { + return; + } + + void notifyOnRoleChanged(role, clientAction); +} diff --git a/apps/meteor/app/lib/server/lib/notifyListenerOnRoleChanges.ts b/apps/meteor/app/lib/server/lib/notifyListenerOnRoleChanges.ts deleted file mode 100644 index b0a2bfb459f4..000000000000 --- a/apps/meteor/app/lib/server/lib/notifyListenerOnRoleChanges.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { api, dbWatchersDisabled } from '@rocket.chat/core-services'; -import type { IRole } from '@rocket.chat/core-typings'; -import { Roles } from '@rocket.chat/models'; - -type ClientAction = 'inserted' | 'updated' | 'removed'; - -export async function notifyListenerOnRoleChanges( - rid: IRole['_id'], - clientAction: ClientAction = 'updated', - existingRoleData?: IRole, -): Promise { - if (!dbWatchersDisabled) { - return; - } - - const role = existingRoleData || (await Roles.findOneById(rid)); - if (!role) { - return; - } - - void api.broadcast('watch.roles', { - clientAction, - role, - }); -} diff --git a/apps/meteor/ee/server/lib/roles/insertRole.ts b/apps/meteor/ee/server/lib/roles/insertRole.ts index 23cb394a913c..321490055c03 100644 --- a/apps/meteor/ee/server/lib/roles/insertRole.ts +++ b/apps/meteor/ee/server/lib/roles/insertRole.ts @@ -2,7 +2,7 @@ import { api, MeteorError } from '@rocket.chat/core-services'; import type { IRole } from '@rocket.chat/core-typings'; import { Roles } from '@rocket.chat/models'; -import { notifyListenerOnRoleChanges } from '../../../../app/lib/server/lib/notifyListenerOnRoleChanges'; +import { notifyOnRoleChanged } from '../../../../app/lib/server/lib/notifyListener'; import { isValidRoleScope } from '../../../../lib/roles/isValidRoleScope'; type InsertRoleOptions = { @@ -22,7 +22,7 @@ export const insertRoleAsync = async (roleData: Omit, options: Ins const role = await Roles.createWithRandomId(name, scope, description, false, mandatory2fa); - void notifyListenerOnRoleChanges(role._id, 'inserted', role); + void notifyOnRoleChanged(role); if (options.broadcastUpdate) { void api.broadcast('user.roleUpdate', { diff --git a/apps/meteor/ee/server/lib/roles/updateRole.ts b/apps/meteor/ee/server/lib/roles/updateRole.ts index 976abbd8921f..0cde11cd0a7a 100644 --- a/apps/meteor/ee/server/lib/roles/updateRole.ts +++ b/apps/meteor/ee/server/lib/roles/updateRole.ts @@ -2,7 +2,7 @@ import { api, MeteorError } from '@rocket.chat/core-services'; import type { IRole } from '@rocket.chat/core-typings'; import { Roles } from '@rocket.chat/models'; -import { notifyListenerOnRoleChanges } from '../../../../app/lib/server/lib/notifyListenerOnRoleChanges'; +import { notifyOnRoleChangedById } from '../../../../app/lib/server/lib/notifyListener'; import { isValidRoleScope } from '../../../../lib/roles/isValidRoleScope'; type UpdateRoleOptions = { @@ -39,7 +39,7 @@ export const updateRole = async (roleId: IRole['_id'], roleData: Omit 0) { diff --git a/apps/meteor/server/lib/roles/createOrUpdateProtectedRole.ts b/apps/meteor/server/lib/roles/createOrUpdateProtectedRole.ts index 487c74c0f76b..cdc43cdad93a 100644 --- a/apps/meteor/server/lib/roles/createOrUpdateProtectedRole.ts +++ b/apps/meteor/server/lib/roles/createOrUpdateProtectedRole.ts @@ -1,7 +1,7 @@ import type { IRole, AtLeast } from '@rocket.chat/core-typings'; import { Roles } from '@rocket.chat/models'; -import { notifyListenerOnRoleChanges } from '../../../app/lib/server/lib/notifyListenerOnRoleChanges'; +import { notifyOnRoleChanged, notifyOnRoleChangedById } from '../../../app/lib/server/lib/notifyListener'; export const createOrUpdateProtectedRoleAsync = async ( roleId: string, @@ -20,12 +20,12 @@ export const createOrUpdateProtectedRoleAsync = async ( roleData.mandatory2fa || role.mandatory2fa, ); - void notifyListenerOnRoleChanges(roleId, 'updated', updatedRole); + void notifyOnRoleChanged(updatedRole); return; } - const insertedRole = await Roles.insertOne({ + await Roles.insertOne({ _id: roleId, scope: 'Users', description: '', @@ -34,5 +34,5 @@ export const createOrUpdateProtectedRoleAsync = async ( protected: true, }); - void notifyListenerOnRoleChanges(insertedRole.insertedId, 'inserted'); + void notifyOnRoleChangedById(roleId); }; From 1c8ee7303b20ce3a30c2d8dc0d28dbfe08cfe324 Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Tue, 14 May 2024 16:42:05 -0300 Subject: [PATCH 06/36] refactor: Integration entity out of db watcher (#32378) Co-authored-by: Diego Sampaio <8591547+sampaiodiego@users.noreply.github.com> --- .../server/functions/saveRoomName.ts | 4 ++ .../integrations/server/lib/triggerHandler.js | 2 + .../incoming/addIncomingIntegration.ts | 9 ++- .../incoming/deleteIncomingIntegration.ts | 2 + .../incoming/updateIncomingIntegration.ts | 9 ++- .../outgoing/addOutgoingIntegration.ts | 10 +++- .../outgoing/deleteOutgoingIntegration.ts | 2 + .../outgoing/updateOutgoingIntegration.ts | 9 ++- .../app/lib/server/functions/deleteUser.ts | 6 +- .../app/lib/server/lib/notifyListener.ts | 57 ++++++++++++++++++- .../server/database/watchCollections.ts | 2 +- apps/meteor/server/models/raw/Integrations.ts | 12 +++- .../src/models/IIntegrationsModel.ts | 7 ++- 13 files changed, 114 insertions(+), 17 deletions(-) diff --git a/apps/meteor/app/channel-settings/server/functions/saveRoomName.ts b/apps/meteor/app/channel-settings/server/functions/saveRoomName.ts index 8aa8dc4578a5..0fc15f878bcf 100644 --- a/apps/meteor/app/channel-settings/server/functions/saveRoomName.ts +++ b/apps/meteor/app/channel-settings/server/functions/saveRoomName.ts @@ -8,6 +8,7 @@ import type { Document, UpdateResult } from 'mongodb'; import { callbacks } from '../../../../lib/callbacks'; import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; import { checkUsernameAvailability } from '../../../lib/server/functions/checkUsernameAvailability'; +import { notifyOnIntegrationChangedByChannels } from '../../../lib/server/lib/notifyListener'; import { getValidRoomName } from '../../../utils/server/lib/getValidRoomName'; const updateFName = async (rid: string, displayName: string): Promise<(UpdateResult | Document)[]> => { @@ -73,10 +74,13 @@ export async function saveRoomName( if (room.name && !isDiscussion) { await Integrations.updateRoomName(room.name, slugifiedRoomName); + void notifyOnIntegrationChangedByChannels([slugifiedRoomName]); } + if (sendMessage) { await Message.saveSystemMessage('r', rid, displayName, user); } + await callbacks.run('afterRoomNameChange', { rid, name: displayName, oldName: room.name }); return displayName; } diff --git a/apps/meteor/app/integrations/server/lib/triggerHandler.js b/apps/meteor/app/integrations/server/lib/triggerHandler.js index 07f7a3d903a2..7a2992f91510 100644 --- a/apps/meteor/app/integrations/server/lib/triggerHandler.js +++ b/apps/meteor/app/integrations/server/lib/triggerHandler.js @@ -6,6 +6,7 @@ import _ from 'underscore'; import { getRoomByNameOrIdWithOptionToJoin } from '../../../lib/server/functions/getRoomByNameOrIdWithOptionToJoin'; import { processWebhookMessage } from '../../../lib/server/functions/processWebhookMessage'; +import { notifyOnIntegrationChangedById } from '../../../lib/server/lib/notifyListener'; import { settings } from '../../../settings/server'; import { outgoingEvents } from '../../lib/outgoingEvents'; import { outgoingLogger } from '../logger'; @@ -579,6 +580,7 @@ class RocketChatIntegrationHandler { await updateHistory({ historyId, step: 'after-process-http-status-410', error: true }); outgoingLogger.error(`Disabling the Integration "${trigger.name}" because the status code was 401 (Gone).`); await Integrations.updateOne({ _id: trigger._id }, { $set: { enabled: false } }); + void notifyOnIntegrationChangedById(trigger._id); return; } diff --git a/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts b/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts index 45548a17a565..db058bec960b 100644 --- a/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/incoming/addIncomingIntegration.ts @@ -8,6 +8,7 @@ import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; import { hasPermissionAsync, hasAllPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; +import { notifyOnIntegrationChanged } from '../../../../lib/server/lib/notifyListener'; import { validateScriptEngine, isScriptEngineFrozen } from '../../lib/validateScriptEngine'; const validChannelChars = ['@', '#']; @@ -155,9 +156,13 @@ export const addIncomingIntegration = async (userId: string, integration: INewIn await Roles.addUserRoles(user._id, ['bot']); - const result = await Integrations.insertOne(integrationData); + const { insertedId } = await Integrations.insertOne(integrationData); - integrationData._id = result.insertedId; + if (insertedId) { + void notifyOnIntegrationChanged({ ...integrationData, _id: insertedId }, 'inserted'); + } + + integrationData._id = insertedId; return integrationData; }; diff --git a/apps/meteor/app/integrations/server/methods/incoming/deleteIncomingIntegration.ts b/apps/meteor/app/integrations/server/methods/incoming/deleteIncomingIntegration.ts index 06fb3e3485e3..e73a46bb27db 100644 --- a/apps/meteor/app/integrations/server/methods/incoming/deleteIncomingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/incoming/deleteIncomingIntegration.ts @@ -3,6 +3,7 @@ import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; +import { notifyOnIntegrationChangedById } from '../../../../lib/server/lib/notifyListener'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -34,6 +35,7 @@ export const deleteIncomingIntegration = async (integrationId: string, userId: s } await Integrations.removeById(integrationId); + void notifyOnIntegrationChangedById(integrationId, 'removed'); }; Meteor.methods({ diff --git a/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts b/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts index 5358e3233ce7..0ea5028130da 100644 --- a/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/incoming/updateIncomingIntegration.ts @@ -7,6 +7,7 @@ import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; import { hasAllPermissionAsync, hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; +import { notifyOnIntegrationChanged } from '../../../../lib/server/lib/notifyListener'; import { isScriptEngineFrozen, validateScriptEngine } from '../../lib/validateScriptEngine'; const validChannelChars = ['@', '#']; @@ -164,7 +165,7 @@ Meteor.methods({ await Roles.addUserRoles(user._id, ['bot']); - await Integrations.updateOne( + const updatedIntegration = await Integrations.findOneAndUpdate( { _id: integrationId }, { $set: { @@ -190,6 +191,10 @@ Meteor.methods({ }, ); - return Integrations.findOneById(integrationId); + if (updatedIntegration.value) { + void notifyOnIntegrationChanged(updatedIntegration.value); + } + + return updatedIntegration.value; }, }); diff --git a/apps/meteor/app/integrations/server/methods/outgoing/addOutgoingIntegration.ts b/apps/meteor/app/integrations/server/methods/outgoing/addOutgoingIntegration.ts index 59879f99d475..c8dc31e08446 100644 --- a/apps/meteor/app/integrations/server/methods/outgoing/addOutgoingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/outgoing/addOutgoingIntegration.ts @@ -5,6 +5,7 @@ import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; +import { notifyOnIntegrationChanged } from '../../../../lib/server/lib/notifyListener'; import { validateOutgoingIntegration } from '../../lib/validateOutgoingIntegration'; import { validateScriptEngine } from '../../lib/validateScriptEngine'; @@ -58,8 +59,13 @@ export const addOutgoingIntegration = async (userId: string, integration: INewOu const integrationData = await validateOutgoingIntegration(integration, userId); - const result = await Integrations.insertOne(integrationData); - integrationData._id = result.insertedId; + const { insertedId } = await Integrations.insertOne(integrationData); + + if (insertedId) { + void notifyOnIntegrationChanged({ ...integrationData, _id: insertedId }, 'inserted'); + } + + integrationData._id = insertedId; return integrationData; }; diff --git a/apps/meteor/app/integrations/server/methods/outgoing/deleteOutgoingIntegration.ts b/apps/meteor/app/integrations/server/methods/outgoing/deleteOutgoingIntegration.ts index 27750bca50f2..cc3d138c554a 100644 --- a/apps/meteor/app/integrations/server/methods/outgoing/deleteOutgoingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/outgoing/deleteOutgoingIntegration.ts @@ -3,6 +3,7 @@ import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; +import { notifyOnIntegrationChangedById } from '../../../../lib/server/lib/notifyListener'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention @@ -41,6 +42,7 @@ export const deleteOutgoingIntegration = async (integrationId: string, userId: s await Integrations.removeById(integrationId); await IntegrationHistory.removeByIntegrationId(integrationId); + void notifyOnIntegrationChangedById(integrationId, 'removed'); }; Meteor.methods({ diff --git a/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts b/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts index 9e62561ebf9a..116dbd043039 100644 --- a/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts +++ b/apps/meteor/app/integrations/server/methods/outgoing/updateOutgoingIntegration.ts @@ -5,6 +5,7 @@ import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; +import { notifyOnIntegrationChanged } from '../../../../lib/server/lib/notifyListener'; import { validateOutgoingIntegration } from '../../lib/validateOutgoingIntegration'; import { isScriptEngineFrozen, validateScriptEngine } from '../../lib/validateScriptEngine'; @@ -66,7 +67,7 @@ Meteor.methods({ const isFrozen = isScriptEngineFrozen(scriptEngine); - await Integrations.updateOne( + const updatedIntegration = await Integrations.findOneAndUpdate( { _id: integrationId }, { $set: { @@ -110,6 +111,10 @@ Meteor.methods({ }, ); - return Integrations.findOneById(integrationId); + if (updatedIntegration.value) { + await notifyOnIntegrationChanged(updatedIntegration.value); + } + + return updatedIntegration.value; }, }); diff --git a/apps/meteor/app/lib/server/functions/deleteUser.ts b/apps/meteor/app/lib/server/functions/deleteUser.ts index 66112bdb695b..3af123a72bf7 100644 --- a/apps/meteor/app/lib/server/functions/deleteUser.ts +++ b/apps/meteor/app/lib/server/functions/deleteUser.ts @@ -19,7 +19,7 @@ import { callbacks } from '../../../../lib/callbacks'; import { i18n } from '../../../../server/lib/i18n'; import { FileUpload } from '../../../file-upload/server'; import { settings } from '../../../settings/server'; -import { notifyOnRoomChangedById } from '../lib/notifyListener'; +import { notifyOnRoomChangedById, notifyOnIntegrationChangedByUserId } from '../lib/notifyListener'; import { getSubscribedRoomsForUserWithDetails, shouldRemoveOrChangeOwner } from './getRoomsWithSingleOwner'; import { getUserSingleOwnedRooms } from './getUserSingleOwnedRooms'; import { relinquishRoomOwnerships } from './relinquishRoomOwnerships'; @@ -114,7 +114,9 @@ export async function deleteUser(userId: string, confirmRelinquish = false, dele await FileUpload.getStore('Avatars').deleteByName(user.username); } - await Integrations.disableByUserId(userId); // Disables all the integrations which rely on the user being deleted. + // Disables all the integrations which rely on the user being deleted. + await Integrations.disableByUserId(userId); + void notifyOnIntegrationChangedByUserId(userId); // Don't broadcast user.deleted for Erasure Type of 'Keep' so that messages don't disappear from logged in sessions if (messageErasureType === 'Delete') { diff --git a/apps/meteor/app/lib/server/lib/notifyListener.ts b/apps/meteor/app/lib/server/lib/notifyListener.ts index 9ec4bbf6be1b..776b51997745 100644 --- a/apps/meteor/app/lib/server/lib/notifyListener.ts +++ b/apps/meteor/app/lib/server/lib/notifyListener.ts @@ -1,6 +1,6 @@ import { api, dbWatchersDisabled } from '@rocket.chat/core-services'; -import type { IPermission, IRocketChatRecord, IRoom, ISetting, IPbxEvent, IRole } from '@rocket.chat/core-typings'; -import { Rooms, Permissions, Settings, PbxEvents, Roles } from '@rocket.chat/models'; +import type { IPermission, IRocketChatRecord, IRoom, ISetting, IPbxEvent, IRole, IIntegration } from '@rocket.chat/core-typings'; +import { Rooms, Permissions, Settings, PbxEvents, Roles, Integrations } from '@rocket.chat/models'; type ClientAction = 'inserted' | 'updated' | 'removed'; @@ -141,3 +141,56 @@ export async function notifyOnRoleChangedById( void notifyOnRoleChanged(role, clientAction); } + +export async function notifyOnIntegrationChanged(data: T, clientAction: ClientAction = 'updated'): Promise { + if (!dbWatchersDisabled) { + return; + } + + void api.broadcast('watch.integrations', { clientAction, id: data._id, data }); +} + +export async function notifyOnIntegrationChangedById( + id: T['_id'], + clientAction: ClientAction = 'updated', +): Promise { + if (!dbWatchersDisabled) { + return; + } + + const item = await Integrations.findOneById(id); + + if (item) { + void api.broadcast('watch.integrations', { clientAction, id: item._id, data: item }); + } +} + +export async function notifyOnIntegrationChangedByUserId( + id: T['userId'], + clientAction: ClientAction = 'updated', +): Promise { + if (!dbWatchersDisabled) { + return; + } + + const items = Integrations.findByUserId(id); + + for await (const item of items) { + void api.broadcast('watch.integrations', { clientAction, id: item._id, data: item }); + } +} + +export async function notifyOnIntegrationChangedByChannels( + channels: T['channel'], + clientAction: ClientAction = 'updated', +): Promise { + if (!dbWatchersDisabled) { + return; + } + + const items = Integrations.findByChannels(channels); + + for await (const item of items) { + void api.broadcast('watch.integrations', { clientAction, id: item._id, data: item }); + } +} diff --git a/apps/meteor/server/database/watchCollections.ts b/apps/meteor/server/database/watchCollections.ts index f2bfd5799e5b..93df616df637 100644 --- a/apps/meteor/server/database/watchCollections.ts +++ b/apps/meteor/server/database/watchCollections.ts @@ -36,7 +36,6 @@ export function getWatchCollections(): string[] { LoginServiceConfiguration.getCollectionName(), InstanceStatus.getCollectionName(), IntegrationHistory.getCollectionName(), - Integrations.getCollectionName(), EmailInbox.getCollectionName(), Settings.getCollectionName(), LivechatPriority.getCollectionName(), @@ -49,6 +48,7 @@ export function getWatchCollections(): string[] { collections.push(Roles.getCollectionName()); collections.push(Rooms.getCollectionName()); collections.push(PbxEvents.getCollectionName()); + collections.push(Integrations.getCollectionName()); collections.push(Permissions.getCollectionName()); } diff --git a/apps/meteor/server/models/raw/Integrations.ts b/apps/meteor/server/models/raw/Integrations.ts index d2a536951935..aa390ec03f6b 100644 --- a/apps/meteor/server/models/raw/Integrations.ts +++ b/apps/meteor/server/models/raw/Integrations.ts @@ -1,6 +1,6 @@ import type { IIntegration, IUser, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; import type { IBaseModel, IIntegrationsModel } from '@rocket.chat/model-typings'; -import type { Collection, Db, IndexDescription } from 'mongodb'; +import type { Collection, Db, FindCursor, IndexDescription } from 'mongodb'; import { BaseRaw } from './BaseRaw'; @@ -46,7 +46,15 @@ export class IntegrationsRaw extends BaseRaw implements IIntegrati }); } - disableByUserId(userId: string): ReturnType['updateMany']> { + disableByUserId(userId: IIntegration['userId']): ReturnType['updateMany']> { return this.updateMany({ userId }, { $set: { enabled: false } }); } + + findByUserId(userId: IIntegration['userId']): FindCursor> { + return this.find({ userId }, { projection: { _id: 1 } }); + } + + findByChannels(channels: IIntegration['channel']): FindCursor { + return this.find({ channel: { $in: channels } }); + } } diff --git a/packages/model-typings/src/models/IIntegrationsModel.ts b/packages/model-typings/src/models/IIntegrationsModel.ts index 3cfe6bfcf953..5e77bdefc74f 100644 --- a/packages/model-typings/src/models/IIntegrationsModel.ts +++ b/packages/model-typings/src/models/IIntegrationsModel.ts @@ -1,10 +1,13 @@ import type { IIntegration, IUser } from '@rocket.chat/core-typings'; +import type { FindCursor } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; export interface IIntegrationsModel extends IBaseModel { + disableByUserId(userId: IIntegration['userId']): ReturnType['updateMany']>; + findByChannels(channels: IIntegration['channel']): FindCursor; + findByUserId(userId: IIntegration['userId']): FindCursor>; + findOneByIdAndCreatedByIfExists(params: { _id: IIntegration['_id']; createdBy?: IUser['_id'] }): Promise; findOneByUrl(url: string): Promise; updateRoomName(oldRoomName: string, newRoomName: string): ReturnType['updateMany']>; - findOneByIdAndCreatedByIfExists(params: { _id: IIntegration['_id']; createdBy?: IUser['_id'] }): Promise; - disableByUserId(userId: string): ReturnType['updateMany']>; } From 2d84fe2f41db60434949b5d22ef273050f13e52d Mon Sep 17 00:00:00 2001 From: csuadev <72958726+csuadev@users.noreply.github.com> Date: Tue, 14 May 2024 22:29:13 +0200 Subject: [PATCH 07/36] test: fix flaky test on OC tags (#32434) --- apps/meteor/tests/e2e/omnichannel/omnichannel-tags.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-tags.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-tags.spec.ts index 151d5b8e762c..4ad7bde6d760 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-tags.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-tags.spec.ts @@ -79,7 +79,7 @@ test.describe('OC - Manage Tags', () => { await expect(poOmnichannelTags.confirmDeleteModal).toBeVisible(); await poOmnichannelTags.btnConfirmDeleteModal.click(); await expect(poOmnichannelTags.confirmDeleteModal).not.toBeVisible(); - await expect(page.locator('h3 >> text="No tags yet"')).toBeVisible(); + await expect(page.locator('h3 >> text="No results found"')).toBeVisible(); }); }); }); From ee5cdfc3674d5ba7c7663c62c589a629ea8281f0 Mon Sep 17 00:00:00 2001 From: Tiago Evangelista Pinto Date: Wed, 15 May 2024 07:42:01 -0300 Subject: [PATCH 08/36] feat(UiKit): Channels select (#31918) --- .changeset/strong-humans-bow.md | 6 + packages/fuselage-ui-kit/jest.config.ts | 8 ++ packages/fuselage-ui-kit/jest.setup.ts | 11 ++ packages/fuselage-ui-kit/package.json | 7 +- .../ChannelsSelectElement.spec.tsx | 95 ++++++++++++++ .../ChannelsSelectElement.tsx | 78 ++++++++++++ .../MultiChannelsSelectElement.spec.tsx | 117 ++++++++++++++++++ .../MultiChannelsSelectElement.tsx | 68 ++++++++++ .../hooks/useChannelsData.ts | 40 ++++++ .../src/stories/payloads/actions.ts | 10 +- .../src/surfaces/FuselageSurfaceRenderer.tsx | 40 ++++++ .../blocks/elements/ChannelsSelectElement.ts | 3 +- .../elements/MultiChannelsSelectElement.ts | 3 +- packages/ui-kit/src/rendering/ActionOf.ts | 4 +- yarn.lock | 30 +++-- 15 files changed, 494 insertions(+), 26 deletions(-) create mode 100644 .changeset/strong-humans-bow.md create mode 100644 packages/fuselage-ui-kit/jest.setup.ts create mode 100644 packages/fuselage-ui-kit/src/elements/ChannelsSelectElement/ChannelsSelectElement.spec.tsx create mode 100644 packages/fuselage-ui-kit/src/elements/ChannelsSelectElement/ChannelsSelectElement.tsx create mode 100644 packages/fuselage-ui-kit/src/elements/ChannelsSelectElement/MultiChannelsSelectElement.spec.tsx create mode 100644 packages/fuselage-ui-kit/src/elements/ChannelsSelectElement/MultiChannelsSelectElement.tsx create mode 100644 packages/fuselage-ui-kit/src/elements/ChannelsSelectElement/hooks/useChannelsData.ts diff --git a/.changeset/strong-humans-bow.md b/.changeset/strong-humans-bow.md new file mode 100644 index 000000000000..b718cbe7fedd --- /dev/null +++ b/.changeset/strong-humans-bow.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/fuselage-ui-kit": minor +"@rocket.chat/ui-kit": minor +--- + +Introduced new elements for apps to select channels diff --git a/packages/fuselage-ui-kit/jest.config.ts b/packages/fuselage-ui-kit/jest.config.ts index 23a14f54fde9..070e217dc457 100644 --- a/packages/fuselage-ui-kit/jest.config.ts +++ b/packages/fuselage-ui-kit/jest.config.ts @@ -24,4 +24,12 @@ export default { }, ], }, + moduleNameMapper: { + '\\.css$': 'identity-obj-proxy', + '^react($|/.+)': '/../../node_modules/react$1', + }, + setupFilesAfterEnv: [ + '@testing-library/jest-dom/extend-expect', + '/jest.setup.ts', + ], }; diff --git a/packages/fuselage-ui-kit/jest.setup.ts b/packages/fuselage-ui-kit/jest.setup.ts new file mode 100644 index 000000000000..657e42bbdea1 --- /dev/null +++ b/packages/fuselage-ui-kit/jest.setup.ts @@ -0,0 +1,11 @@ +import { TextEncoder, TextDecoder } from 'util'; + +global.TextEncoder = TextEncoder; +// @ts-ignore +global.TextDecoder = TextDecoder; + +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index 0c2f7cd865fe..3bdb45003903 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -34,6 +34,7 @@ ".:build:cjs": "tsc -p tsconfig-cjs.json", "test": "jest", "lint": "eslint --ext .js,.jsx,.ts,.tsx .", + "testunit": "jest", "typecheck": "tsc --noEmit", "docs": "cross-env NODE_ENV=production build-storybook -o ../../static/fuselage-ui-kit", "storybook": "start-storybook -p 6006 --no-version-updates", @@ -63,11 +64,13 @@ "@babel/preset-react": "~7.22.15", "@babel/preset-typescript": "~7.22.15", "@rocket.chat/apps-engine": "^1.42.2", + "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/fuselage": "^0.53.6", "@rocket.chat/fuselage-hooks": "^0.33.1", "@rocket.chat/fuselage-polyfills": "~0.31.25", "@rocket.chat/icons": "^0.35.0", + "@rocket.chat/mock-providers": "workspace:^", "@rocket.chat/prettier-config": "~0.31.25", "@rocket.chat/styled": "~0.31.25", "@rocket.chat/ui-avatar": "workspace:^", @@ -82,8 +85,9 @@ "@storybook/source-loader": "~6.5.16", "@storybook/theming": "~6.5.16", "@tanstack/react-query": "^4.16.1", - "@testing-library/react": "^14.2.2", + "@testing-library/react": "^12.1.4", "@testing-library/react-hooks": "^8.0.1", + "@testing-library/user-event": "^14.5.2", "@types/babel__core": "^7.20.3", "@types/babel__preset-env": "^7.9.4", "@types/react": "~17.0.69", @@ -106,6 +110,7 @@ "typescript": "~5.3.3" }, "dependencies": { + "@rocket.chat/core-typings": "*", "@rocket.chat/gazzodown": "workspace:^", "@rocket.chat/ui-kit": "workspace:~", "tslib": "^2.5.3" diff --git a/packages/fuselage-ui-kit/src/elements/ChannelsSelectElement/ChannelsSelectElement.spec.tsx b/packages/fuselage-ui-kit/src/elements/ChannelsSelectElement/ChannelsSelectElement.spec.tsx new file mode 100644 index 000000000000..6af59dba4f63 --- /dev/null +++ b/packages/fuselage-ui-kit/src/elements/ChannelsSelectElement/ChannelsSelectElement.spec.tsx @@ -0,0 +1,95 @@ +import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; +import { MockedServerContext } from '@rocket.chat/mock-providers'; +import type { ChannelsSelectElement as ChannelsSelectElementType } from '@rocket.chat/ui-kit'; +import { BlockContext } from '@rocket.chat/ui-kit'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { contextualBarParser } from '../../surfaces'; +import ChannelsSelectElement from './ChannelsSelectElement'; +import { useChannelsData } from './hooks/useChannelsData'; + +const channelsBlock: ChannelsSelectElementType = { + type: 'channels_select', + appId: 'test', + blockId: 'test', + actionId: 'test', +}; + +jest.mock('./hooks/useChannelsData'); + +const mockedOptions: ReturnType = [ + { + value: 'channel1_id', + label: { + name: 'Channel 1', + avatarETag: 'test', + type: RoomType.CHANNEL, + }, + }, + { + value: 'channel2_id', + label: { + name: 'Channel 2', + avatarETag: 'test', + type: RoomType.CHANNEL, + }, + }, + { + value: 'channel3_id', + label: { + name: 'Channel 3', + avatarETag: 'test', + type: RoomType.CHANNEL, + }, + }, +]; + +const mockUseChannelsData = jest.mocked(useChannelsData); +mockUseChannelsData.mockReturnValue(mockedOptions); + +describe('UiKit ChannelsSelect Element', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + beforeEach(() => { + render( + + + + ); + }); + + it('should render a UiKit channel selector', async () => { + expect(await screen.findByRole('textbox')).toBeInTheDocument(); + }); + + it('should open the channel selector', async () => { + const input = await screen.findByRole('textbox'); + input.focus(); + + expect(await screen.findByRole('listbox')).toBeInTheDocument(); + }); + + it('should select a channel', async () => { + const input = await screen.findByRole('textbox'); + + input.focus(); + + const option = (await screen.findAllByRole('option'))[0]; + await userEvent.click(option, { delay: null }); + + const selected = await screen.findByRole('button'); + expect(selected).toHaveValue('channel1_id'); + }); +}); diff --git a/packages/fuselage-ui-kit/src/elements/ChannelsSelectElement/ChannelsSelectElement.tsx b/packages/fuselage-ui-kit/src/elements/ChannelsSelectElement/ChannelsSelectElement.tsx new file mode 100644 index 000000000000..51d25f7ae0d5 --- /dev/null +++ b/packages/fuselage-ui-kit/src/elements/ChannelsSelectElement/ChannelsSelectElement.tsx @@ -0,0 +1,78 @@ +import { + AutoComplete, + Option, + Box, + Options, + Chip, +} from '@rocket.chat/fuselage'; +import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; +import { RoomAvatar } from '@rocket.chat/ui-avatar'; +import type * as UiKit from '@rocket.chat/ui-kit'; +import { memo, useCallback, useState } from 'react'; + +import { useUiKitState } from '../../hooks/useUiKitState'; +import type { BlockProps } from '../../utils/BlockProps'; +import { useChannelsData } from './hooks/useChannelsData'; + +type ChannelsSelectElementProps = BlockProps; + +const ChannelsSelectElement = ({ + block, + context, +}: ChannelsSelectElementProps) => { + const [{ value, loading }, action] = useUiKitState(block, context); + + const [filter, setFilter] = useState(''); + const filterDebounced = useDebouncedValue(filter, 300); + + const options = useChannelsData({ filter: filterDebounced }); + + const handleChange = useCallback( + (value) => { + action({ target: { value } }); + }, + [action] + ); + + return ( + ( + + + + {label.name} + + + )} + renderItem={({ value, label, ...props }) => ( +