From 5a8945daf2f8c4d160c94baaedeceaf01b47ba04 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Tue, 9 Jun 2026 12:32:21 -0600 Subject: [PATCH 1/6] chore: Delegate user sync to PDP (#40836) --- apps/meteor/ee/server/api/abac/index.ts | 8 +- .../ee/server/local-services/ldap/service.ts | 5 + apps/meteor/tests/end-to-end/api/abac.ts | 164 ++++++++++++++++-- ee/packages/abac/src/index.ts | 25 +++ ee/packages/abac/src/pdp/LocalPDP.ts | 7 +- ee/packages/abac/src/pdp/VirtruPDP.spec.ts | 32 ++++ ee/packages/abac/src/pdp/VirtruPDP.ts | 28 ++- ee/packages/abac/src/pdp/types.ts | 4 + ee/packages/abac/src/service.spec.ts | 48 +++++ .../core-services/src/types/IAbacService.ts | 2 + .../core-services/src/types/ILDAPEEService.ts | 1 + packages/core-typings/src/Abac.ts | 7 + 12 files changed, 315 insertions(+), 16 deletions(-) diff --git a/apps/meteor/ee/server/api/abac/index.ts b/apps/meteor/ee/server/api/abac/index.ts index 0f1abe77f290d..72629cb2a85d2 100644 --- a/apps/meteor/ee/server/api/abac/index.ts +++ b/apps/meteor/ee/server/api/abac/index.ts @@ -1,8 +1,8 @@ import { AbacAttributeStoreExternalError, getPdpHealthErrorCode } from '@rocket.chat/abac'; -import { Abac, LDAPEnterprise } from '@rocket.chat/core-services'; +import { Abac } from '@rocket.chat/core-services'; import type { AbacActor } from '@rocket.chat/core-services'; import type { IServerEvents, IUser } from '@rocket.chat/core-typings'; -import { ServerEvents, Users } from '@rocket.chat/models'; +import { ServerEvents } from '@rocket.chat/models'; import { validateUnauthorizedErrorResponse } from '@rocket.chat/rest-typings/src/v1/Ajv'; import { convertSubObjectsIntoPaths } from '@rocket.chat/tools'; @@ -209,7 +209,7 @@ const abacEndpoints = API.v1 { authRequired: true, permissionsRequired: ['abac-management', 'manage-abac-admin-room-attributes'], - license: ['abac', 'ldap-enterprise'], + license: ['abac'], body: POSTAbacUsersSyncBodySchema, response: { 200: GenericSuccessSchema, @@ -225,7 +225,7 @@ const abacEndpoints = API.v1 const { usernames, ids, emails, ldapIds } = this.bodyParams; - await LDAPEnterprise.syncUsersAbacAttributes(Users.findUsersByIdentifiers({ usernames, ids, emails, ldapIds })); + await Abac.reevaluateUsers({ usernames, ids, emails, ldapIds }); return API.v1.success(); }, diff --git a/apps/meteor/ee/server/local-services/ldap/service.ts b/apps/meteor/ee/server/local-services/ldap/service.ts index 1f756be0d48b8..cba54b832b044 100644 --- a/apps/meteor/ee/server/local-services/ldap/service.ts +++ b/apps/meteor/ee/server/local-services/ldap/service.ts @@ -1,5 +1,6 @@ import { ServiceClassInternal, type ILDAPEEService } from '@rocket.chat/core-services'; import type { IUser } from '@rocket.chat/core-typings'; +import { Users } from '@rocket.chat/models'; import type { FindCursor } from 'mongodb'; import { LDAPEEManager } from '../../lib/ldap/Manager'; @@ -30,4 +31,8 @@ export class LDAPEEService extends ServiceClassInternal implements ILDAPEEServic async syncUsersAbacAttributes(users: FindCursor): Promise { return LDAPEEManager.syncUsersAbacAttributes(users); } + + async syncUsersAbacAttributesByIds(userIds: string[]): Promise { + return LDAPEEManager.syncUsersAbacAttributes(Users.findUsersByIdentifiers({ ids: userIds })); + } } diff --git a/apps/meteor/tests/end-to-end/api/abac.ts b/apps/meteor/tests/end-to-end/api/abac.ts index 71a71131d684e..c0eaef4208b69 100644 --- a/apps/meteor/tests/end-to-end/api/abac.ts +++ b/apps/meteor/tests/end-to-end/api/abac.ts @@ -4,7 +4,7 @@ import { expect } from 'chai'; import { before, after, describe, it } from 'mocha'; import { MongoClient } from 'mongodb'; -import { getCredentials, request, credentials, methodCall } from '../../data/api-data'; +import { api, getCredentials, request, credentials, methodCall } from '../../data/api-data'; import { sleep } from '../../data/livechat/utils'; import { mockServerHealthy, @@ -190,7 +190,7 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I it('POST /abac/users/sync should return 403', async () => { await request - .post(`${v1}/abac/users/sync`) + .post(api('abac/users/sync')) .set(credentials) .send({ usernames: ['x'] }) .expect(403); @@ -1451,6 +1451,17 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I }); }); + it('POST /abac/users/sync should fail with error-abac-not-enabled', async () => { + await request + .post(api('abac/users/sync')) + .set(credentials) + .send({ ids: ['no-such-user-id'] }) + .expect(400) + .expect((res) => { + expect(res.body.error).to.include('error-abac-not-enabled'); + }); + }); + after(async () => { await updateSetting('ABAC_Enabled', true); }); @@ -1832,6 +1843,27 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I }); }); + describe('POST /abac/users/sync (strategy-agnostic)', () => { + before(async () => { + await updateSetting('ABAC_Enabled', true); + }); + + after(async () => { + await updateSetting('ABAC_Enabled', false); + }); + + it('responds 200 with success:true when ABAC_Enabled=true and PDP type=local (no-match id)', async () => { + await request + .post(api('abac/users/sync')) + .set(credentials) + .send({ ids: ['no-such-user-id'] }) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + }); + }); + }); + describe('Room access (invite, addition)', () => { let roomWithoutAttr: IRoom; let roomWithAttr: IRoom; @@ -2503,7 +2535,7 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I it('should sync ABAC attributes for SOME users via /abac/users/sync', async () => { await request - .post(`${v1}/abac/users/sync`) + .post(api('abac/users/sync')) .set(credentials) .send({ usernames: ['david.scott', 'gene.cernan'], @@ -2533,7 +2565,7 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I it('should fail /abac/users/sync when more than 100 usernames are provided', async () => { const usernames = Array.from({ length: 101 }, (_, i) => `user_${i}@example.com`); await request - .post(`${v1}/abac/users/sync`) + .post(api('abac/users/sync')) .set(credentials) .send({ usernames, @@ -2547,7 +2579,7 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I it('should fail /abac/users/sync when more than 100 ids are provided', async () => { const ids = Array.from({ length: 101 }, (_, i) => `id_${i}`); await request - .post(`${v1}/abac/users/sync`) + .post(api('abac/users/sync')) .set(credentials) .send({ ids, @@ -2561,7 +2593,7 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I it('should fail /abac/users/sync when more than 100 emails are provided', async () => { const emails = Array.from({ length: 101 }, (_, i) => `user_${i}@example.com`); await request - .post(`${v1}/abac/users/sync`) + .post(api('abac/users/sync')) .set(credentials) .send({ emails, @@ -2575,7 +2607,7 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I it('should fail /abac/users/sync when more than 100 ldapIds are provided', async () => { const ldapIds = Array.from({ length: 101 }, (_, i) => `ldap_${i}`); await request - .post(`${v1}/abac/users/sync`) + .post(api('abac/users/sync')) .set(credentials) .send({ ldapIds, @@ -2589,7 +2621,7 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I it('should succeed /abac/users/sync when exactly 100 usernames are provided (boundary)', async () => { const usernames = Array.from({ length: 100 }, (_, i) => `boundary_user_${i}`); await request - .post(`${v1}/abac/users/sync`) + .post(api('abac/users/sync')) .set(credentials) .send({ usernames, @@ -2667,7 +2699,7 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I expect(sergeiInitialAttrs[0].values).to.include(initialDept); await request - .post(`${v1}/abac/users/sync`) + .post(api('abac/users/sync')) .set(credentials) .send({ usernames: ['david.scott', 'sergei.krikalev'], @@ -3424,6 +3456,118 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I }); }); + describe('Re-evaluation via POST /abac/users/sync', () => { + describe('PDP DENY removes the synced user', () => { + let room: IRoom; + let user: IUser; + const username = `abac-sync-deny-${Date.now()}`; + const email = `${username}@rocket.chat`; + + before(async function () { + this.timeout(15000); + + user = await createUser({ username, email }); + room = (await createRoom({ type: 'p', name: `extpdp-sync-deny-${Date.now()}` })).body.group; + await request + .post(api('groups.invite')) + .set(credentials) + .send({ roomId: room._id, usernames: [user.username] }) + .expect(200); + + await mockServerReset(); + await seedDefaultMocks(); + await seedBulkDecisionByEntity([adminEmail, email], 'DECISION_DENY'); + + await request + .post(api(`abac/rooms/${room._id}/attributes/${attrKey}`)) + .set(credentials) + .send({ values: ['alpha'] }) + .expect(200); + }); + + after(async () => { + await Promise.all([deleteRoom({ type: 'p', roomId: room._id }), deleteUser(user)]); + }); + + it('keeps the user before re-evaluation', async () => { + const res = await request.get(api('groups.members')).set(credentials).query({ roomId: room._id }).expect(200); + const usernames = res.body.members.map((m: IUser) => m.username); + expect(usernames).to.include(user.username); + }); + + it('removes the user when the Virtru PDP returns DENY', async () => { + await mockServerReset(); + await seedDefaultMocks(); + await seedBulkDecisionByEntity([adminEmail], 'DECISION_DENY'); + + await request + .post(api('abac/users/sync')) + .set(credentials) + .send({ usernames: [user.username] }) + .expect(200); + + const res = await request.get(api('groups.members')).set(credentials).query({ roomId: room._id }).expect(200); + const usernames = res.body.members.map((m: IUser) => m.username); + expect(usernames).to.not.include(user.username); + }); + + it('keeps the room creator (permitted) after re-evaluation', async () => { + const res = await request.get(api('groups.members')).set(credentials).query({ roomId: room._id }).expect(200); + const memberIds = res.body.members.map((m: IUser) => m._id); + expect(memberIds).to.include(credentials['X-User-Id']); + }); + }); + + describe('PDP PERMIT keeps the synced user', () => { + let room: IRoom; + let user: IUser; + const username = `abac-sync-permit-${Date.now()}`; + const email = `${username}@rocket.chat`; + + before(async function () { + this.timeout(15000); + + user = await createUser({ username, email }); + room = (await createRoom({ type: 'p', name: `extpdp-sync-permit-${Date.now()}` })).body.group; + await request + .post(api('groups.invite')) + .set(credentials) + .send({ roomId: room._id, usernames: [user.username] }) + .expect(200); + + await mockServerReset(); + await seedDefaultMocks(); + await seedBulkDecisionByEntity([adminEmail, email], 'DECISION_DENY'); + + await request + .post(api(`abac/rooms/${room._id}/attributes/${attrKey}`)) + .set(credentials) + .send({ values: ['alpha'] }) + .expect(200); + }); + + after(async () => { + await Promise.all([deleteRoom({ type: 'p', roomId: room._id }), deleteUser(user)]); + }); + + it('keeps the user when the Virtru PDP returns PERMIT', async () => { + await mockServerReset(); + await seedDefaultMocks(); + await seedBulkDecisionByEntity([adminEmail, email], 'DECISION_DENY'); + + await request + .post(api('abac/users/sync')) + .set(credentials) + .send({ usernames: [user.username] }) + .expect(200); + + const res = await request.get(api('groups.members')).set(credentials).query({ roomId: room._id }).expect(200); + const usernames = res.body.members.map((m: IUser) => m.username); + expect(usernames).to.include(user.username); + }); + }); + }); + describe('[GET] /abac/pdp/health', () => { beforeEach(async () => { await mockServerReset(); @@ -3945,7 +4089,7 @@ const addAbacAttributesToUserDirectly = async (userId: string, abacAttributes: I it('POST /abac/users/sync is NOT blocked by external attribute store (no error-abac-attribute-store-external)', async () => { const res = await request - .post(`${v1}/abac/users/sync`) + .post(api('abac/users/sync')) .set(credentials) .send({ usernames: ['no-such-user-vstore'] }); expect(res.body?.error).to.not.equal('error-abac-attribute-store-external'); diff --git a/ee/packages/abac/src/index.ts b/ee/packages/abac/src/index.ts index 1b73bb16b49a1..047da168c79c3 100644 --- a/ee/packages/abac/src/index.ts +++ b/ee/packages/abac/src/index.ts @@ -12,6 +12,7 @@ import type { AbacAuditReason, AbacAttributeStoreType, AbacPdpType, + AbacUserIdentifiers, } from '@rocket.chat/core-typings'; import { Rooms, AbacAttributes, Users, Subscriptions } from '@rocket.chat/models'; import { escapeRegExp } from '@rocket.chat/string-helpers'; @@ -956,6 +957,30 @@ export class AbacService extends ServiceClass implements IAbacService { logger.error({ msg: 'Failed to evaluate room membership', err }); } } + + async reevaluateUsers(identifiers: AbacUserIdentifiers): Promise { + if (!this.pdp || !(await this.pdp.isAvailable())) { + return; + } + + const users = await Users.findUsersByIdentifiers(identifiers, { + projection: { _id: 1, emails: 1, username: 1, __rooms: 1 }, + }).toArray(); + + if (!users.length) { + return; + } + + try { + const nonCompliant = await this.pdp.reevaluateUsers(users); + if (Array.isArray(nonCompliant) && nonCompliant.length) { + await Promise.all(nonCompliant.map(({ user, room }) => limit(() => this.removeUserFromRoom(room, user as IUser, 'api')))); + } + } catch (err) { + logger.error({ msg: 'Failed to reevaluate users', err }); + throw err; + } + } } export { LocalPDP, VirtruPDP } from './pdp'; diff --git a/ee/packages/abac/src/pdp/LocalPDP.ts b/ee/packages/abac/src/pdp/LocalPDP.ts index 2b1d3ed51174e..6d16d81e16997 100644 --- a/ee/packages/abac/src/pdp/LocalPDP.ts +++ b/ee/packages/abac/src/pdp/LocalPDP.ts @@ -1,9 +1,10 @@ +import { LDAPEnterprise } from '@rocket.chat/core-services'; import type { IAbacAttributeDefinition, IRoom, AtLeast, IUser } from '@rocket.chat/core-typings'; import { Rooms, Users } from '@rocket.chat/models'; import { OnlyCompliantCanBeAddedToRoomError } from '../errors'; import { buildCompliantConditions, buildNonCompliantConditions, buildRoomNonCompliantConditionsFromSubject } from '../helper'; -import type { IPolicyDecisionPoint } from './types'; +import type { IPolicyDecisionPoint, ReevaluationUser } from './types'; export class LocalPDP implements IPolicyDecisionPoint { async isAvailable(): Promise { @@ -81,6 +82,10 @@ export class LocalPDP implements IPolicyDecisionPoint { throw new Error('evaluateUserRooms is not implemented for LocalPDP'); } + async reevaluateUsers(users: ReevaluationUser[]): Promise { + await LDAPEnterprise.syncUsersAbacAttributesByIds(users.map((user) => user._id)); + } + async checkUsernamesMatchAttributes(usernames: string[], attributes: IAbacAttributeDefinition[], _object: IRoom): Promise { const nonCompliantUsersFromList = await Users.find( { diff --git a/ee/packages/abac/src/pdp/VirtruPDP.spec.ts b/ee/packages/abac/src/pdp/VirtruPDP.spec.ts index 3a4b15e722f1f..d869fa685f8ee 100644 --- a/ee/packages/abac/src/pdp/VirtruPDP.spec.ts +++ b/ee/packages/abac/src/pdp/VirtruPDP.spec.ts @@ -470,6 +470,38 @@ describe('VirtruPDP — PDP unreachable (decision call rejects)', () => { }); }); +describe('reevaluateUsers', () => { + const room = { _id: 'r1', abacAttributes: [{ key: 'clearance', values: ['secret'] }] }; + + it('returns no pairs when users have no ABAC rooms', async () => { + const pdp = new VirtruPDP(mkClient()); + const result = await pdp.reevaluateUsers([user({ _id: 'u1', __rooms: [] })]); + expect(result).toEqual([]); + expect(roomsFindPrivateRoomsByIdsWithAbacAttributes).not.toHaveBeenCalled(); + }); + + it('returns non-compliant {user, room} pairs for denied users', async () => { + roomsFindPrivateRoomsByIdsWithAbacAttributes.mockReturnValue(asyncIterable([room])); + const apiCall = jest.fn().mockResolvedValue(denyFor(['r1'])); + const pdp = new VirtruPDP(mkClient({ apiCall })); + + const u = user({ _id: 'u1', __rooms: ['r1'] }); + const result = await pdp.reevaluateUsers([u]); + + expect(result).toEqual([{ user: u, room }]); + }); + + it('returns no pairs when the user is permitted', async () => { + roomsFindPrivateRoomsByIdsWithAbacAttributes.mockReturnValue(asyncIterable([room])); + const apiCall = jest.fn().mockResolvedValue(permitFor(['r1'])); + const pdp = new VirtruPDP(mkClient({ apiCall })); + + const result = await pdp.reevaluateUsers([user({ _id: 'u1', __rooms: ['r1'] })]); + + expect(result).toEqual([]); + }); +}); + describe('VirtruPDP.getHealthStatus', () => { const platformOk = () => okJson({ status: 'SERVING' }); const authOk = () => okJson({}); diff --git a/ee/packages/abac/src/pdp/VirtruPDP.ts b/ee/packages/abac/src/pdp/VirtruPDP.ts index 671e91642daf7..86d338e6640c0 100644 --- a/ee/packages/abac/src/pdp/VirtruPDP.ts +++ b/ee/packages/abac/src/pdp/VirtruPDP.ts @@ -1,11 +1,12 @@ import type { IAbacAttributeDefinition, IRoom, IUser, AtLeast } from '@rocket.chat/core-typings'; import { Rooms, Users } from '@rocket.chat/models'; import { serverFetch } from '@rocket.chat/server-fetch'; +import { isTruthy } from '@rocket.chat/tools'; import pLimit from 'p-limit'; import { OnlyCompliantCanBeAddedToRoomError, PdpHealthCheckError } from '../errors'; import { logger } from '../logger'; -import type { IPolicyDecisionPoint, IGetDecisionBulkRequest, IGetDecisionBulkResponse, IResourceDecision } from './types'; +import type { IPolicyDecisionPoint, IGetDecisionBulkRequest, IGetDecisionBulkResponse, IResourceDecision, ReevaluationUser } from './types'; import { HEALTH_CHECK_TIMEOUT } from '../clients/virtru/VirtruClient'; import type { VirtruClient } from '../clients/virtru/VirtruClient'; import { buildEntityIdentifier, buildAttributeFqns, getUserEntityKey } from '../clients/virtru/identity'; @@ -350,6 +351,31 @@ export class VirtruPDP implements IPolicyDecisionPoint { return nonCompliant; } + async reevaluateUsers(users: ReevaluationUser[]): Promise; room: IRoom }>> { + const roomIds = [...new Set(users.flatMap((u) => u.__rooms ?? []))]; + if (!roomIds.length) { + return []; + } + + const abacRoomCursor = Rooms.findPrivateRoomsByIdsWithAbacAttributes(roomIds, { + projection: { _id: 1, abacAttributes: 1 }, + }); + + const abacRoomById = new Map(); + for await (const room of abacRoomCursor) { + abacRoomById.set(room._id, room); + } + + const entries = users + .map((user) => { + const rooms = (user.__rooms ?? []).map((rid) => abacRoomById.get(rid)).filter(isTruthy); + return rooms.length ? { user, rooms } : null; + }) + .filter(isTruthy); + + return this.evaluateUserRooms(entries); + } + async onSubjectAttributesChanged(user: IUser, _next: IAbacAttributeDefinition[]): Promise { const roomIds = user.__rooms; if (!roomIds?.length) { diff --git a/ee/packages/abac/src/pdp/types.ts b/ee/packages/abac/src/pdp/types.ts index 39073421d656c..a405104ec2d76 100644 --- a/ee/packages/abac/src/pdp/types.ts +++ b/ee/packages/abac/src/pdp/types.ts @@ -28,6 +28,8 @@ export interface IGetDecisionBulkResponse { }>; } +export type ReevaluationUser = Pick; + export interface IPolicyDecisionPoint { isAvailable(): Promise; @@ -53,6 +55,8 @@ export interface IPolicyDecisionPoint { rooms: AtLeast[]; }>, ): Promise; room: IRoom }>>; + + reevaluateUsers(users: ReevaluationUser[]): Promise; room: IRoom }>>; } export interface IVirtruPDPConfig { diff --git a/ee/packages/abac/src/service.spec.ts b/ee/packages/abac/src/service.spec.ts index 49a5726b5ad35..f815171579011 100644 --- a/ee/packages/abac/src/service.spec.ts +++ b/ee/packages/abac/src/service.spec.ts @@ -59,6 +59,8 @@ const mockCreateAuditServerEvent = jest.fn(); const mockRoomsFindAllPrivateAbac = jest.fn(); const mockUsersFindActiveByRoomIds = jest.fn(); const mockRoomRemoveUserFromRoom = jest.fn(); +const mockUsersFindUsersByIdentifiers = jest.fn(); +const mockLdapSyncByIds = jest.fn(); jest.mock('@rocket.chat/models', () => ({ Rooms: { @@ -90,6 +92,7 @@ jest.mock('@rocket.chat/models', () => ({ Users: { find: (...args: any[]) => mockUsersFind(...args), findActiveByRoomIds: (...args: any[]) => mockUsersFindActiveByRoomIds(...args), + findUsersByIdentifiers: (...args: any[]) => mockUsersFindUsersByIdentifiers(...args), setAbacAttributesById: (...args: any[]) => mockUsersSetAbacAttributesById(...args), unsetAbacAttributesById: (...args: any[]) => mockUsersUnsetAbacAttributesById(...args), findOneAndUpdate: (...args: any[]) => mockUsersUpdateOne(...args), @@ -116,6 +119,9 @@ jest.mock('@rocket.chat/core-services', () => { Room: { removeUserFromRoom: (...args: any[]) => mockRoomRemoveUserFromRoom(...args), }, + LDAPEnterprise: { + syncUsersAbacAttributesByIds: (...args: any[]) => mockLdapSyncByIds(...args), + }, api: { broadcast: jest.fn(), }, @@ -1983,4 +1989,46 @@ describe('AbacService (unit)', () => { expect(pdpStrategySpy).toHaveBeenCalledWith('local'); }); }); + + describe('reevaluateUsers', () => { + const usersCursor = (items: any[]) => ({ toArray: () => Promise.resolve(items) }); + + it('local PDP: forwards resolved user ids to the LDAP broker and removes nothing', async () => { + service.setPdpStrategy('local'); + mockUsersFindUsersByIdentifiers.mockReturnValue(usersCursor([{ _id: 'u1' }, { _id: 'u2' }])); + + await service.reevaluateUsers({ usernames: ['bob'] }); + + expect(mockUsersFindUsersByIdentifiers).toHaveBeenCalledWith( + { usernames: ['bob'] }, + { projection: { _id: 1, emails: 1, username: 1, __rooms: 1 } }, + ); + expect(mockLdapSyncByIds).toHaveBeenCalledWith(['u1', 'u2']); + expect(mockRoomRemoveUserFromRoom).not.toHaveBeenCalled(); + }); + + it('virtru PDP: removes the non-compliant pairs the PDP returns', async () => { + service.setPdpStrategy('virtru'); + const u1 = { _id: 'u1', emails: [{ address: 'u1@x.com' }], username: 'u1' }; + const room = { _id: 'r1', abacAttributes: [] }; + mockUsersFindUsersByIdentifiers.mockReturnValue(usersCursor([u1])); + mockRoomRemoveUserFromRoom.mockResolvedValue(undefined); + jest.spyOn((service as any).pdp, 'isAvailable').mockResolvedValue(true); + jest.spyOn((service as any).pdp, 'reevaluateUsers').mockResolvedValue([{ user: u1, room }]); + + await service.reevaluateUsers({ ids: ['u1'] }); + + expect(mockRoomRemoveUserFromRoom).toHaveBeenCalledTimes(1); + }); + + it('no-ops when no users match', async () => { + service.setPdpStrategy('local'); + mockUsersFindUsersByIdentifiers.mockReturnValue(usersCursor([])); + + await service.reevaluateUsers({ ids: ['missing'] }); + + expect(mockLdapSyncByIds).not.toHaveBeenCalled(); + expect(mockRoomRemoveUserFromRoom).not.toHaveBeenCalled(); + }); + }); }); diff --git a/packages/core-services/src/types/IAbacService.ts b/packages/core-services/src/types/IAbacService.ts index 4d558d50d85d1..3eac9e59b8351 100644 --- a/packages/core-services/src/types/IAbacService.ts +++ b/packages/core-services/src/types/IAbacService.ts @@ -7,6 +7,7 @@ import type { AbacAccessOperation, AbacObjectType, ILDAPEntry, + AbacUserIdentifiers, } from '@rocket.chat/core-typings'; export type AbacActor = Pick; @@ -49,6 +50,7 @@ export interface IAbacService { ): Promise; addSubjectAttributes(user: IUser, ldapUser: ILDAPEntry, map: Record, actor: AbacActor | undefined): Promise; evaluateRoomMembership(): Promise; + reevaluateUsers(identifiers: AbacUserIdentifiers): Promise; getPDPHealth(): Promise; isExternalAttributeStore(): Promise; } diff --git a/packages/core-services/src/types/ILDAPEEService.ts b/packages/core-services/src/types/ILDAPEEService.ts index 0879ea5c266b1..b8ef92d8c8a04 100644 --- a/packages/core-services/src/types/ILDAPEEService.ts +++ b/packages/core-services/src/types/ILDAPEEService.ts @@ -8,4 +8,5 @@ export interface ILDAPEEService { syncLogout(): Promise; syncAbacAttributes(): Promise; syncUsersAbacAttributes(users: FindCursor): Promise; + syncUsersAbacAttributesByIds(userIds: string[]): Promise; } diff --git a/packages/core-typings/src/Abac.ts b/packages/core-typings/src/Abac.ts index 80bff941e9adb..6cc78712e4b1e 100644 --- a/packages/core-typings/src/Abac.ts +++ b/packages/core-typings/src/Abac.ts @@ -13,3 +13,10 @@ export enum AbacObjectType { export const isAbacPdpType = (value: unknown): value is AbacPdpType => value === 'local' || value === 'virtru'; export const isAbacAttributeStoreType = (value: unknown): value is AbacAttributeStoreType => value === 'local' || value === 'virtru'; + +export type AbacUserIdentifiers = { + usernames?: string[]; + ids?: string[]; + emails?: string[]; + ldapIds?: string[]; +}; From 9a1e0f687082d0ab373397c4fa5889bf8d7958d5 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Tue, 9 Jun 2026 17:45:53 -0300 Subject: [PATCH 2/6] refactor: do not use global React namespace (#40822) --- .../client/components/message/variants/RoomMessage.tsx | 4 ++-- .../client/components/message/variants/SystemMessage.tsx | 4 ++-- .../marketplace/AppDetailsPage/AppDetailsPage.spec.tsx | 5 +++-- .../components/RadioDropDown/RadioDownAnchor.tsx | 4 ++-- .../client/views/room/MessageList/MessageList.spec.tsx | 6 +++--- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/apps/meteor/client/components/message/variants/RoomMessage.tsx b/apps/meteor/client/components/message/variants/RoomMessage.tsx index 157856ce3b087..114b8405a9e57 100644 --- a/apps/meteor/client/components/message/variants/RoomMessage.tsx +++ b/apps/meteor/client/components/message/variants/RoomMessage.tsx @@ -3,7 +3,7 @@ import { Message, MessageLeftContainer, MessageContainer, CheckBox } from '@rock import { useToggle } from '@rocket.chat/fuselage-hooks'; import { MessageAvatar } from '@rocket.chat/ui-avatar'; import { useUserId, useUserCard } from '@rocket.chat/ui-contexts'; -import type { ComponentProps } from 'react'; +import type { ComponentProps, KeyboardEvent } from 'react'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -88,7 +88,7 @@ const RoomMessage = ({ useCountSelected(); - const handleKeyDown = (e: React.KeyboardEvent) => { + const handleKeyDown = (e: KeyboardEvent) => { if (!selecting) return; if (!(e.code === 'Space' || e.code === 'Enter')) return; diff --git a/apps/meteor/client/components/message/variants/SystemMessage.tsx b/apps/meteor/client/components/message/variants/SystemMessage.tsx index 1ca0d4f8b2925..e78117e1552b4 100644 --- a/apps/meteor/client/components/message/variants/SystemMessage.tsx +++ b/apps/meteor/client/components/message/variants/SystemMessage.tsx @@ -17,7 +17,7 @@ import { UserAvatar } from '@rocket.chat/ui-avatar'; import { useUserDisplayName } from '@rocket.chat/ui-client'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useUserPresence, useUserCard } from '@rocket.chat/ui-contexts'; -import type { ComponentProps } from 'react'; +import type { ComponentProps, KeyboardEvent } from 'react'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -64,7 +64,7 @@ const SystemMessage = ({ message, showUserAvatar, ...props }: SystemMessageProps useCountSelected(); const buttonProps = useButtonPattern((e) => openUserCard(e, user.username)); - const handleKeyDown = (e: React.KeyboardEvent) => { + const handleKeyDown = (e: KeyboardEvent) => { if (!isSelecting) return; if (!(e.code === 'Space' || e.code === 'Enter')) return; diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/AppDetailsPage.spec.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/AppDetailsPage.spec.tsx index 59a544526b6b7..a713d8890d5f9 100644 --- a/apps/meteor/client/views/marketplace/AppDetailsPage/AppDetailsPage.spec.tsx +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/AppDetailsPage.spec.tsx @@ -1,6 +1,7 @@ import { mockAppRoot } from '@rocket.chat/mock-providers'; import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import type { ReactNode } from 'react'; import AppDetailsPage from './AppDetailsPage'; import { AppClientOrchestratorInstance } from '../../../apps/orchestrator'; @@ -25,8 +26,8 @@ jest.mock('@rocket.chat/ui-client', () => { const originalModule = jest.requireActual('@rocket.chat/ui-client'); return { ...originalModule, - PageHeader: ({ children }: { children: React.ReactNode }) =>
{children}
, - PageFooter: ({ children, isDirty }: { children: React.ReactNode; isDirty: boolean }) => isDirty &&
{children}
, + PageHeader: ({ children }: { children: ReactNode }) =>
{children}
, + PageFooter: ({ children, isDirty }: { children: ReactNode; isDirty: boolean }) => isDirty &&
{children}
, }; }); diff --git a/apps/meteor/client/views/marketplace/components/RadioDropDown/RadioDownAnchor.tsx b/apps/meteor/client/views/marketplace/components/RadioDropDown/RadioDownAnchor.tsx index a12f40bd16126..940e0dbccf44f 100644 --- a/apps/meteor/client/views/marketplace/components/RadioDropDown/RadioDownAnchor.tsx +++ b/apps/meteor/client/views/marketplace/components/RadioDropDown/RadioDownAnchor.tsx @@ -1,12 +1,12 @@ import type { Button } from '@rocket.chat/fuselage'; import { Box, Icon } from '@rocket.chat/fuselage'; -import type { ComponentProps } from 'react'; +import type { ComponentProps, MouseEvent } from 'react'; import { forwardRef } from 'react'; import type { RadioDropDownGroup } from '../../definitions/RadioDropDownDefinitions'; type RadioDropdownAnchorProps = { - onClick: (event: React.MouseEvent) => void; + onClick: (event: MouseEvent) => void; group: RadioDropDownGroup; } & Omit, 'onClick'>; diff --git a/apps/meteor/client/views/room/MessageList/MessageList.spec.tsx b/apps/meteor/client/views/room/MessageList/MessageList.spec.tsx index 60adcfba4c9a3..f2bbdd50f5748 100644 --- a/apps/meteor/client/views/room/MessageList/MessageList.spec.tsx +++ b/apps/meteor/client/views/room/MessageList/MessageList.spec.tsx @@ -18,15 +18,15 @@ const mockVirtualizerHandle = { }; jest.mock('virtua', () => { - const React = jest.requireActual('react'); + const { forwardRef, useImperativeHandle } = jest.requireActual('react'); return { - VList: React.forwardRef( + VList: forwardRef( ( { children, onScroll, shift: _shift, ...props }: { children: ReactNode; onScroll?: (offset: number) => void; shift?: boolean }, ref: any, ) => { - React.useImperativeHandle(ref, () => mockVirtualizerHandle); + useImperativeHandle(ref, () => mockVirtualizerHandle); return (
    onScroll?.(mockVirtualizerHandle.scrollOffset)} {...props}> {children} From 590a618cc12ea44fe8176545dc99947d443a26f5 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Tue, 9 Jun 2026 17:00:01 -0300 Subject: [PATCH 3/6] fix: Non-deterministic comparator in team's channel desertion table (#40857) --- .changeset/easy-laws-talk.md | 5 +++++ .../ChannelDesertionTable.tsx | 15 ++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 .changeset/easy-laws-talk.md diff --git a/.changeset/easy-laws-talk.md b/.changeset/easy-laws-talk.md new file mode 100644 index 0000000000000..51ff0a2702f2b --- /dev/null +++ b/.changeset/easy-laws-talk.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes non-deterministic comparator in team's channel desertion table diff --git a/apps/meteor/client/views/teams/ChannelDesertionTable/ChannelDesertionTable.tsx b/apps/meteor/client/views/teams/ChannelDesertionTable/ChannelDesertionTable.tsx index 61e6ca129e201..ec0bea58e17c6 100644 --- a/apps/meteor/client/views/teams/ChannelDesertionTable/ChannelDesertionTable.tsx +++ b/apps/meteor/client/views/teams/ChannelDesertionTable/ChannelDesertionTable.tsx @@ -39,7 +39,20 @@ const ChannelDesertionTable = ({ const direction = sortDirection === 'asc' ? 1 : -1; - return rooms.sort((a, b) => (a[sortBy] && b[sortBy] ? (a[sortBy]?.localeCompare(b[sortBy] ?? '') ?? 1) * direction : direction)); + return rooms.toSorted((a, b) => { + const aValue = a[sortBy] ?? ''; + const bValue = b[sortBy] ?? ''; + if (!aValue && !bValue) { + return 0; + } + if (!aValue) { + return 1; + } + if (!bValue) { + return -1; + } + return aValue.localeCompare(bValue) * direction; + }); }, [rooms, sortBy, sortDirection]); return ( From d485f13a1f33fe07a566a196c4313022dca39550 Mon Sep 17 00:00:00 2001 From: dougfabris Date: Tue, 9 Jun 2026 17:18:46 -0300 Subject: [PATCH 4/6] Revert "refactor: Remove unused `description` from attachments render" (#40067) (#40860) --- .../autotranslate/client/lib/autotranslate.ts | 10 ++ .../app/autotranslate/server/autotranslate.ts | 2 +- .../autotranslate/server/deeplTranslate.ts | 2 +- .../autotranslate/server/googleTranslate.ts | 2 +- .../app/autotranslate/server/msTranslate.ts | 2 +- .../server/functions/notifications/email.js | 9 +- .../server/lib/sendNotificationsOnMessage.ts | 2 + .../app/lib/server/methods/updateMessage.ts | 9 +- .../app/livechat/server/lib/sendTranscript.ts | 5 +- .../app/slackbridge/server/RocketAdapter.ts | 5 +- apps/meteor/app/ui/client/lib/ChatMessages.ts | 2 +- .../attachments/file/AudioAttachment.tsx | 5 + .../file/GenericFileAttachment.tsx | 5 + .../attachments/file/ImageAttachment.tsx | 4 + .../attachments/file/VideoAttachment.tsx | 5 + .../message/toolbar/useCopyAction.ts | 4 +- .../toolbar/useReportMessageAction.tsx | 4 +- .../client/hooks/useDecryptedMessage.spec.ts | 3 +- .../client/hooks/useDecryptedMessage.ts | 9 +- .../client/lib/normalizeThreadMessage.tsx | 6 +- .../lib/parseMessageTextToAstMarkdown.spec.ts | 111 ++++++++++++++++-- .../lib/parseMessageTextToAstMarkdown.ts | 19 ++- .../normalizeMessagePreview.spec.ts | 24 +++- .../normalizeMessagePreview.ts | 6 +- .../room/MessageList/hooks/useMessageBody.tsx | 6 +- .../useDownloadExportMutation.ts | 1 + .../useExportMessagesAsPDFMutation.tsx | 1 + .../hooks/useNormalizedThreadTitleHtml.ts | 6 +- .../EmailInbox/EmailInbox_Outgoing.ts | 6 +- .../hooks/BeforeSaveMarkdownParser.ts | 4 + .../hooks/BeforeSaveJumpToMessage.tests.ts | 11 ++ .../hooks/BeforeSaveMarkdownParser.tests.ts | 25 ++++ .../src/OmnichannelTranscript.ts | 4 +- .../MessageAttachmentBase.ts | 1 + 34 files changed, 282 insertions(+), 38 deletions(-) diff --git a/apps/meteor/app/autotranslate/client/lib/autotranslate.ts b/apps/meteor/app/autotranslate/client/lib/autotranslate.ts index bab1ed08213e1..d3207b9205930 100644 --- a/apps/meteor/app/autotranslate/client/lib/autotranslate.ts +++ b/apps/meteor/app/autotranslate/client/lib/autotranslate.ts @@ -69,6 +69,16 @@ export const AutoTranslate = { } } + if (attachment.description && attachment.translations && attachment.translations[language]) { + attachment.translations.original = attachment.description; + + if (autoTranslateShowInverse) { + attachment.description = attachment.translations.original; + } else { + attachment.description = attachment.translations[language]; + } + } + if (attachment.attachments && attachment.attachments.length > 0) { // @ts-expect-error - not sure what to do with this attachment.attachments = this.translateAttachments(attachment.attachments, language); diff --git a/apps/meteor/app/autotranslate/server/autotranslate.ts b/apps/meteor/app/autotranslate/server/autotranslate.ts index 3e04f6d39eb30..2f91e02463d58 100644 --- a/apps/meteor/app/autotranslate/server/autotranslate.ts +++ b/apps/meteor/app/autotranslate/server/autotranslate.ts @@ -320,7 +320,7 @@ export abstract class AutoTranslate { if (message.attachments && message.attachments.length > 0) { setImmediate(async () => { for (const [index, attachment] of message.attachments?.entries() ?? []) { - if (attachment.text) { + if (attachment.description || attachment.text) { // Removes the initial link `[ ](quoterl)` from quote message before translation const translatedText = attachment?.text?.replace(/\[(.*?)\]\(.*?\)/g, '$1') || attachment?.text; const attachmentMessage = { ...attachment, text: translatedText }; diff --git a/apps/meteor/app/autotranslate/server/deeplTranslate.ts b/apps/meteor/app/autotranslate/server/deeplTranslate.ts index 35f73e1755da6..d76a7ea2e4901 100644 --- a/apps/meteor/app/autotranslate/server/deeplTranslate.ts +++ b/apps/meteor/app/autotranslate/server/deeplTranslate.ts @@ -196,7 +196,7 @@ class DeeplAutoTranslate extends AutoTranslate { params: { auth_key: this.apiKey, target_lang: language, - text: attachment.text || '', + text: attachment.description || attachment.text || '', }, }); if (!result.ok) { diff --git a/apps/meteor/app/autotranslate/server/googleTranslate.ts b/apps/meteor/app/autotranslate/server/googleTranslate.ts index 53b9bb7c1d5ea..9667ae53c967a 100644 --- a/apps/meteor/app/autotranslate/server/googleTranslate.ts +++ b/apps/meteor/app/autotranslate/server/googleTranslate.ts @@ -195,7 +195,7 @@ class GoogleAutoTranslate extends AutoTranslate { key: this.apiKey, target: language, format: 'text', - q: attachment.text || '', + q: attachment.description || attachment.text || '', }, }); if (!result.ok) { diff --git a/apps/meteor/app/autotranslate/server/msTranslate.ts b/apps/meteor/app/autotranslate/server/msTranslate.ts index 6508734a1c0da..ddb345d3c895a 100644 --- a/apps/meteor/app/autotranslate/server/msTranslate.ts +++ b/apps/meteor/app/autotranslate/server/msTranslate.ts @@ -192,7 +192,7 @@ class MsAutoTranslate extends AutoTranslate { return this._translate( [ { - Text: attachment.text || '', + Text: attachment.description || attachment.text || '', }, ], targetLanguages, diff --git a/apps/meteor/app/lib/server/functions/notifications/email.js b/apps/meteor/app/lib/server/functions/notifications/email.js index 4de543abb7dd6..c41445fcf55b2 100644 --- a/apps/meteor/app/lib/server/functions/notifications/email.js +++ b/apps/meteor/app/lib/server/functions/notifications/email.js @@ -77,8 +77,13 @@ export async function getEmailContent({ message, user, room }) { } if (hasFiles) { - const fileParts = files.map((file) => { - return escapeHTML(file.name); + const attachments = message.attachments || []; + const fileParts = files.map((file, index) => { + let part = escapeHTML(file.name); + if (attachments[index]?.description) { + part += `

    ${escapeHTML(attachments[index].description)}`; + } + return part; }); contentParts.push(fileParts.join('

    ')); } diff --git a/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.ts b/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.ts index 498cef1624624..7a089abba0815 100644 --- a/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.ts +++ b/apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.ts @@ -195,6 +195,8 @@ export const sendNotification = async ({ const firstAttachment = message.attachments?.length && message.attachments.shift(); if (firstAttachment) { + firstAttachment.description = + typeof firstAttachment.description === 'string' ? emojione.shortnameToUnicode(firstAttachment.description) : undefined; firstAttachment.text = typeof firstAttachment.text === 'string' ? emojione.shortnameToUnicode(firstAttachment.text) : undefined; } diff --git a/apps/meteor/app/lib/server/methods/updateMessage.ts b/apps/meteor/app/lib/server/methods/updateMessage.ts index 833b4403c0eca..45ba42f25f000 100644 --- a/apps/meteor/app/lib/server/methods/updateMessage.ts +++ b/apps/meteor/app/lib/server/methods/updateMessage.ts @@ -34,7 +34,7 @@ export async function executeUpdateMessage( // IF the message has custom fields, always update // Ideally, we'll compare the custom fields to check for change, but since we don't know the shape of // custom fields, as it's user defined, we're gonna update - const msgText = originalMessage.msg; + const msgText = originalMessage?.attachments?.[0]?.description ?? originalMessage.msg; if (msgText === message.msg && !previewUrls && !message.customFields) { return; } @@ -86,6 +86,13 @@ export async function executeUpdateMessage( } await canSendMessageAsync(message.rid, { uid: user._id, username: user.username ?? undefined, ...user }); + // It is possible to have an empty array as the attachments property, so ensure both things exist + if (originalMessage.attachments && originalMessage.attachments.length > 0 && originalMessage.attachments[0].description !== undefined) { + originalMessage.attachments[0].description = message.msg; + message.attachments = originalMessage.attachments; + message.msg = originalMessage.msg; + } + message.u = originalMessage.u; return updateMessage(message, user, originalMessage, previewUrls); diff --git a/apps/meteor/app/livechat/server/lib/sendTranscript.ts b/apps/meteor/app/livechat/server/lib/sendTranscript.ts index f52ac3f516710..199275f6a516b 100644 --- a/apps/meteor/app/livechat/server/lib/sendTranscript.ts +++ b/apps/meteor/app/livechat/server/lib/sendTranscript.ts @@ -108,7 +108,7 @@ export async function sendTranscript({ const messageType = MessageTypes.getType(message); - const messageContent = messageType?.system + let messageContent = messageType?.system ? DOMPurify.sanitize(` ${messageType.text(i18n.cloneInstance({ interpolation: { escapeValue: false } }).t, message)}}`) : escapeHtml(message.msg); @@ -116,6 +116,9 @@ export async function sendTranscript({ let filesHTML = ''; if (message.attachments && message.attachments?.length > 0) { + messageContent = message.attachments[0].description || ''; + escapeHtml(messageContent); + for await (const attachment of message.attachments) { if (!isFileAttachment(attachment)) { continue; diff --git a/apps/meteor/app/slackbridge/server/RocketAdapter.ts b/apps/meteor/app/slackbridge/server/RocketAdapter.ts index 5c46d75368dda..100e6991c3b08 100644 --- a/apps/meteor/app/slackbridge/server/RocketAdapter.ts +++ b/apps/meteor/app/slackbridge/server/RocketAdapter.ts @@ -203,11 +203,14 @@ export default class RocketAdapter { if (rocketMessage.file.name) { let fileName = rocketMessage.file.name; - const text = rocketMessage.msg; + let text = rocketMessage.msg; const attachment = this.getMessageAttachment(rocketMessage); if (attachment) { fileName = Meteor.absoluteUrl(attachment.title_link); + if (!text) { + text = attachment.description; + } } await slack.postMessage(slack.getSlackChannel(rocketMessage.rid), { ...rocketMessage, msg: `${text} ${fileName}` }); diff --git a/apps/meteor/app/ui/client/lib/ChatMessages.ts b/apps/meteor/app/ui/client/lib/ChatMessages.ts index 70b64201979ba..a6febf3fdfef9 100644 --- a/apps/meteor/app/ui/client/lib/ChatMessages.ts +++ b/apps/meteor/app/ui/client/lib/ChatMessages.ts @@ -120,7 +120,7 @@ export class ChatMessages implements ChatAPI { }, editMessage: async (message: IMessage, { cursorAtStart = false }: { cursorAtStart?: boolean } = {}) => { this.composer?.uploads.clear(); - const text = (await this.data.getDraft(message._id)) || message.msg; + const text = (await this.data.getDraft(message._id)) || message.attachments?.[0]?.description || message.msg; await this.currentEditingMessage.stop(); diff --git a/apps/meteor/client/components/message/content/attachments/file/AudioAttachment.tsx b/apps/meteor/client/components/message/content/attachments/file/AudioAttachment.tsx index 9fa94126127e8..227874cfb8eaf 100644 --- a/apps/meteor/client/components/message/content/attachments/file/AudioAttachment.tsx +++ b/apps/meteor/client/components/message/content/attachments/file/AudioAttachment.tsx @@ -4,13 +4,17 @@ import { useMediaUrl } from '@rocket.chat/ui-contexts'; import { useMemo } from 'react'; import { useReloadOnError } from './hooks/useReloadOnError'; +import MarkdownText from '../../../../MarkdownText'; import MessageCollapsible from '../../../MessageCollapsible'; +import MessageContentBody from '../../../MessageContentBody'; const AudioAttachment = ({ title, audio_url: url, audio_type: type, audio_size: size, + description, + descriptionMd, title_link: link, title_link_download: hasDownload, collapsed, @@ -21,6 +25,7 @@ const AudioAttachment = ({ return ( <> + {descriptionMd ? : } diff --git a/apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx b/apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx index d57325334098e..f46b24d8d5634 100644 --- a/apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx +++ b/apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx @@ -13,7 +13,9 @@ import { useTranslation } from 'react-i18next'; import { getFileExtension } from '../../../../../../lib/utils/getFileExtension'; import { forAttachmentDownload, registerDownloadForUid } from '../../../../../hooks/useDownloadFromServiceWorker'; +import MarkdownText from '../../../../MarkdownText'; import MessageCollapsible from '../../../MessageCollapsible'; +import MessageContentBody from '../../../MessageContentBody'; import AttachmentSize from '../structure/AttachmentSize'; import { useOpenEncryptedPdf } from './hooks/useOpenEncryptedPdf'; @@ -23,6 +25,8 @@ type GenericFileAttachmentProps = MessageAttachmentBase; const GenericFileAttachment = ({ title, + description, + descriptionMd, title_link: link, title_link_download: hasDownload, size, @@ -81,6 +85,7 @@ const GenericFileAttachment = ({ return ( <> + {descriptionMd ? : } + {descriptionMd ? : } + {descriptionMd ? : } diff --git a/apps/meteor/client/components/message/toolbar/useCopyAction.ts b/apps/meteor/client/components/message/toolbar/useCopyAction.ts index b8275144abca1..1a03dac99936d 100644 --- a/apps/meteor/client/components/message/toolbar/useCopyAction.ts +++ b/apps/meteor/client/components/message/toolbar/useCopyAction.ts @@ -6,8 +6,8 @@ import type { MessageActionConfig } from '../../../../app/ui-utils/client/lib/Me const getMainMessageText = (message: IMessage): IMessage => { const newMessage = { ...message }; - newMessage.msg = newMessage.msg || newMessage.attachments?.[0]?.title || ''; - newMessage.md = newMessage.md || undefined; + newMessage.msg = newMessage.msg || newMessage.attachments?.[0]?.description || newMessage.attachments?.[0]?.title || ''; + newMessage.md = newMessage.md || newMessage.attachments?.[0]?.descriptionMd || undefined; return { ...newMessage }; }; diff --git a/apps/meteor/client/components/message/toolbar/useReportMessageAction.tsx b/apps/meteor/client/components/message/toolbar/useReportMessageAction.tsx index dbbb962032907..ba281c5a1f5a2 100644 --- a/apps/meteor/client/components/message/toolbar/useReportMessageAction.tsx +++ b/apps/meteor/client/components/message/toolbar/useReportMessageAction.tsx @@ -7,8 +7,8 @@ import ReportMessageModal from '../../../views/room/modals/ReportMessageModal'; const getMainMessageText = (message: IMessage): IMessage => { const newMessage = { ...message }; - newMessage.msg = newMessage.msg || newMessage.attachments?.[0]?.title || ''; - newMessage.md = newMessage.md || undefined; + newMessage.msg = newMessage.msg || newMessage.attachments?.[0]?.description || newMessage.attachments?.[0]?.title || ''; + newMessage.md = newMessage.md || newMessage.attachments?.[0]?.descriptionMd || undefined; return { ...newMessage }; }; diff --git a/apps/meteor/client/hooks/useDecryptedMessage.spec.ts b/apps/meteor/client/hooks/useDecryptedMessage.spec.ts index 3103e708910f6..5b35e8d6e3352 100644 --- a/apps/meteor/client/hooks/useDecryptedMessage.spec.ts +++ b/apps/meteor/client/hooks/useDecryptedMessage.spec.ts @@ -53,7 +53,7 @@ describe('useDecryptedMessage', () => { it('should handle E2EE messages with attachments', async () => { (isE2EEMessage as jest.MockedFunction).mockReturnValue(true); (e2e.decryptMessage as jest.Mock).mockResolvedValue({ - attachments: [{ title: 'Attachment title' }], + attachments: [{ description: 'Attachment description' }], }); const message = { msg: 'Encrypted message with attachment' }; @@ -63,6 +63,7 @@ describe('useDecryptedMessage', () => { expect(result.current).toBe('E2E_message_encrypted_placeholder'); }); + expect(result.current).toBe('Attachment description'); expect(e2e.decryptMessage).toHaveBeenCalledWith(message); }); diff --git a/apps/meteor/client/hooks/useDecryptedMessage.ts b/apps/meteor/client/hooks/useDecryptedMessage.ts index 771665dc0b631..e560aacc5b111 100644 --- a/apps/meteor/client/hooks/useDecryptedMessage.ts +++ b/apps/meteor/client/hooks/useDecryptedMessage.ts @@ -18,11 +18,14 @@ export const useDecryptedMessage = (message: IMessage): string => { e2e.decryptMessage(message).then((decryptedMsg) => { if (decryptedMsg.msg) { setDecryptedMessage(decryptedMsg.msg); - return; } - if (decryptedMsg.attachments && decryptedMsg.attachments.length > 0) { - setDecryptedMessage(t('Message_with_attachment')); + if (decryptedMsg.attachments && decryptedMsg.attachments?.length > 0) { + if (decryptedMsg.attachments[0].description) { + setDecryptedMessage(decryptedMsg.attachments[0].description); + } else { + setDecryptedMessage(t('Message_with_attachment')); + } } }); }, [message, t, setDecryptedMessage]); diff --git a/apps/meteor/client/lib/normalizeThreadMessage.tsx b/apps/meteor/client/lib/normalizeThreadMessage.tsx index 067a23f6aedfe..f2ee47688a59e 100644 --- a/apps/meteor/client/lib/normalizeThreadMessage.tsx +++ b/apps/meteor/client/lib/normalizeThreadMessage.tsx @@ -24,7 +24,11 @@ export function normalizeThreadMessage({ ...message }: Readonly attachment.title); + const attachment = message.attachments.find((attachment) => attachment.title || attachment.description); + + if (attachment?.description) { + return <>{attachment.description}; + } if (attachment?.title) { return <>{attachment.title}; diff --git a/apps/meteor/client/lib/parseMessageTextToAstMarkdown.spec.ts b/apps/meteor/client/lib/parseMessageTextToAstMarkdown.spec.ts index 0105608949b7f..e48eb15f885cf 100644 --- a/apps/meteor/client/lib/parseMessageTextToAstMarkdown.spec.ts +++ b/apps/meteor/client/lib/parseMessageTextToAstMarkdown.spec.ts @@ -178,6 +178,47 @@ describe('parseMessageTextToAstMarkdown', () => { }); it('should return correct attachment translated parsed md when translate is active', () => { + const attachmentTranslatedMessage = { + ...translatedMessage, + attachments: [ + { + description: 'description', + translations: { + en: 'description translated', + }, + }, + ], + }; + const attachmentTranslatedMessageParsed = { + ...translatedMessage, + md: translatedMessageParsed, + attachments: [ + { + description: 'description', + translations: { + en: 'description translated', + }, + md: [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'description translated', + }, + ], + }, + ], + }, + ], + }; + + expect(parseMessageTextToAstMarkdown(attachmentTranslatedMessage, parseOptions, enabledAutoTranslatedOptions)).toStrictEqual( + attachmentTranslatedMessageParsed, + ); + }); + + it('should return correct attachment quote translated parsed md when translate is active', () => { const attachmentTranslatedMessage = { ...translatedMessage, attachments: [ @@ -337,7 +378,7 @@ describe('parseMessageAttachments', () => { const attachmentMessage = [ { - text: 'message **bold** _italic_ and ~strike~', + description: 'message **bold** _italic_ and ~strike~', md: messageParserTokenMessage, }, ]; @@ -359,18 +400,46 @@ describe('parseMessageAttachments', () => { autoTranslateLanguage: 'en', }; - it('should return correct attachment text parsed md when translate is active and auto translate language is undefined', () => { - const textAttachment = [ + it('should return correct attachment description translated parsed md when translate is active', () => { + const descriptionAttachment = [ { ...attachmentMessage[0], - text: 'attachment not translated', + description: 'attachment not translated', translationProvider: 'provider', translations: { en: 'attachment translated', }, }, ]; - const textAttachmentParsed: Root = [ + const descriptionAttachmentParsed: Root = [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'attachment translated', + }, + ], + }, + ]; + + expect(parseMessageAttachments(descriptionAttachment, parseOptions, enabledAutoTranslatedOptions)[0].md).toStrictEqual( + descriptionAttachmentParsed, + ); + }); + + it('should return correct attachment description parsed md when translate is active and auto translate language is undefined', () => { + const descriptionAttachment = [ + { + ...attachmentMessage[0], + description: 'attachment not translated', + translationProvider: 'provider', + translations: { + en: 'attachment translated', + }, + }, + ]; + const descriptionAttachmentParsed: Root = [ { type: 'PARAGRAPH', value: [ @@ -383,11 +452,39 @@ describe('parseMessageAttachments', () => { ]; expect( - parseMessageAttachments(textAttachment, parseOptions, { + parseMessageAttachments(descriptionAttachment, parseOptions, { ...enabledAutoTranslatedOptions, autoTranslateLanguage: undefined, })[0].md, - ).toStrictEqual(textAttachmentParsed); + ).toStrictEqual(descriptionAttachmentParsed); + }); + + it('should return correct attachment text translated parsed md when translate is active', () => { + const textAttachment = [ + { + ...attachmentMessage[0], + text: 'attachment not translated', + translationProvider: 'provider', + translations: { + en: 'attachment translated', + }, + }, + ]; + const textAttachmentParsed: Root = [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'attachment translated', + }, + ], + }, + ]; + + expect(parseMessageAttachments(textAttachment, parseOptions, enabledAutoTranslatedOptions)[0].md).toStrictEqual( + textAttachmentParsed, + ); }); it('should return correct attachment text translated parsed md when translate is active and has multiple texts', () => { diff --git a/apps/meteor/client/lib/parseMessageTextToAstMarkdown.ts b/apps/meteor/client/lib/parseMessageTextToAstMarkdown.ts index 55d393cee38ed..df84785e26351 100644 --- a/apps/meteor/client/lib/parseMessageTextToAstMarkdown.ts +++ b/apps/meteor/client/lib/parseMessageTextToAstMarkdown.ts @@ -1,5 +1,12 @@ import type { IMessage, ITranslatedMessage, MessageAttachment } from '@rocket.chat/core-typings'; -import { isE2EEMessage, isQuoteAttachment, isTranslatedAttachment, isTranslatedMessage } from '@rocket.chat/core-typings'; +import { + isFileAttachment, + isE2EEMessage, + isQuoteAttachment, + isTranslatedAttachment, + isTranslatedMessage, + isEncryptedMessageAttachment, +} from '@rocket.chat/core-typings'; import type { Options, Root } from '@rocket.chat/message-parser'; import { parse } from '@rocket.chat/message-parser'; @@ -51,7 +58,7 @@ export const parseMessageAttachment = ( autoTranslateOptions: { autoTranslateLanguage?: string; translated: boolean }, ): T => { const { translated, autoTranslateLanguage } = autoTranslateOptions; - if (!attachment.text) { + if (!attachment.text && !attachment.description) { return attachment; } @@ -62,8 +69,16 @@ export const parseMessageAttachment = ( const text = (isTranslatedAttachment(attachment) && autoTranslateLanguage && attachment?.translations?.[autoTranslateLanguage]) || attachment.text || + attachment.description || ''; + if (isFileAttachment(attachment) && attachment.description) { + attachment.descriptionMd = + translated || isEncryptedMessageAttachment(attachment) + ? textToMessageToken(text, parseOptions) + : (attachment.descriptionMd ?? textToMessageToken(text, parseOptions)); + } + return { ...attachment, md: translated ? textToMessageToken(text, parseOptions) : (attachment.md ?? textToMessageToken(text, parseOptions)), diff --git a/apps/meteor/client/lib/utils/normalizeMessagePreview/normalizeMessagePreview.spec.ts b/apps/meteor/client/lib/utils/normalizeMessagePreview/normalizeMessagePreview.spec.ts index 9584c13531439..184145a6c4506 100644 --- a/apps/meteor/client/lib/utils/normalizeMessagePreview/normalizeMessagePreview.spec.ts +++ b/apps/meteor/client/lib/utils/normalizeMessagePreview/normalizeMessagePreview.spec.ts @@ -48,7 +48,7 @@ describe('normalizeMessagePreview', () => { }); describe('when message has attachments', () => { - it('should return attachment title when description is available', () => { + it('should return attachment description when available', () => { const message = createFakeMessageWithAttachment({ msg: '', attachments: [ @@ -60,10 +60,10 @@ describe('normalizeMessagePreview', () => { }); const result = normalizeMessagePreview(message, mockT); - expect(result).toBe('Attachment title'); + expect(result).toBe('Attachment description'); }); - it('should return attachment title when message is not provided', () => { + it('should return attachment title when description is not available', () => { const message = createFakeMessageWithAttachment({ msg: '', attachments: [ @@ -112,7 +112,7 @@ describe('normalizeMessagePreview', () => { expect(result).toBe('Second attachment title'); }); - it('should find first attachment title', () => { + it('should find first attachment description', () => { const message = createFakeMessageWithAttachment({ msg: '', attachments: [ @@ -129,7 +129,21 @@ describe('normalizeMessagePreview', () => { }); const result = normalizeMessagePreview(message, mockT); - expect(result).toBe('Third attachment title'); + expect(result).toBe('Second attachment description'); + }); + + it('should escape HTML in attachment description', () => { + const message = createFakeMessageWithAttachment({ + msg: '', + attachments: [ + { + description: '', + }, + ], + }); + const result = normalizeMessagePreview(message, mockT); + + expect(result).toBe('<script>alert("xss")</script>'); }); it('should escape HTML in attachment title', () => { diff --git a/apps/meteor/client/lib/utils/normalizeMessagePreview/normalizeMessagePreview.ts b/apps/meteor/client/lib/utils/normalizeMessagePreview/normalizeMessagePreview.ts index 0fbefea5fa48d..53bf5bf2d4056 100644 --- a/apps/meteor/client/lib/utils/normalizeMessagePreview/normalizeMessagePreview.ts +++ b/apps/meteor/client/lib/utils/normalizeMessagePreview/normalizeMessagePreview.ts @@ -11,7 +11,11 @@ export const normalizeMessagePreview = (message: IMessage, t: TFunction): string } if (message.attachments) { - const attachment = message.attachments.find((attachment) => attachment.title); + const attachment = message.attachments.find((attachment) => attachment.title || attachment.description); + + if (attachment?.description) { + return escapeHTML(attachment.description); + } if (attachment?.title) { return escapeHTML(attachment.title); diff --git a/apps/meteor/client/views/room/MessageList/hooks/useMessageBody.tsx b/apps/meteor/client/views/room/MessageList/hooks/useMessageBody.tsx index 94313e6925432..0bcbbccece740 100644 --- a/apps/meteor/client/views/room/MessageList/hooks/useMessageBody.tsx +++ b/apps/meteor/client/views/room/MessageList/hooks/useMessageBody.tsx @@ -31,7 +31,11 @@ export const useMessageBody = (message: IMessage | undefined): string | Root => } if (message.attachments) { - const attachment = message.attachments.find((attachment) => attachment.title); + const attachment = message.attachments.find((attachment) => attachment.title || attachment.description); + + if (attachment?.description) { + return attachment.description; + } if (attachment?.title) { return attachment.title; diff --git a/apps/meteor/client/views/room/contextualBar/ExportMessages/useDownloadExportMutation.ts b/apps/meteor/client/views/room/contextualBar/ExportMessages/useDownloadExportMutation.ts index 291475075f4e4..f7b53352df5a1 100644 --- a/apps/meteor/client/views/room/contextualBar/ExportMessages/useDownloadExportMutation.ts +++ b/apps/meteor/client/views/room/contextualBar/ExportMessages/useDownloadExportMutation.ts @@ -39,6 +39,7 @@ export const useDownloadExportMutation = () => { ...('image_type' in attachment && { image_type: attachment.image_type }), ...('image_size' in attachment && { image_size: attachment.image_size }), ...('type' in attachment && { type: attachment.type }), + description: attachment.description, })) ?? [], }), ); diff --git a/apps/meteor/client/views/room/contextualBar/ExportMessages/useExportMessagesAsPDFMutation.tsx b/apps/meteor/client/views/room/contextualBar/ExportMessages/useExportMessagesAsPDFMutation.tsx index 383388f0c316f..716f7f3c8bd19 100644 --- a/apps/meteor/client/views/room/contextualBar/ExportMessages/useExportMessagesAsPDFMutation.tsx +++ b/apps/meteor/client/views/room/contextualBar/ExportMessages/useExportMessagesAsPDFMutation.tsx @@ -116,6 +116,7 @@ export const useExportMessagesAsPDFMutation = () => { {parseMessage(message)} {message.attachments?.map((attachment: MessageAttachmentDefault, index) => ( + {attachment.description && {attachment.description}} {attachment.image_url && } {attachment.title} diff --git a/apps/meteor/client/views/room/contextualBar/Threads/hooks/useNormalizedThreadTitleHtml.ts b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useNormalizedThreadTitleHtml.ts index c052942a9567d..4ac708211cf47 100644 --- a/apps/meteor/client/views/room/contextualBar/Threads/hooks/useNormalizedThreadTitleHtml.ts +++ b/apps/meteor/client/views/room/contextualBar/Threads/hooks/useNormalizedThreadTitleHtml.ts @@ -33,7 +33,11 @@ export const useNormalizedThreadTitleHtml = (mainMessage: IThreadMainMessage) => } if (message.attachments) { - const attachment = message.attachments.find((attachment) => attachment.title); + const attachment = message.attachments.find((attachment) => attachment.title || attachment.description); + + if (attachment?.description) { + return escapeHTML(attachment.description); + } if (attachment?.title) { return escapeHTML(attachment.title); diff --git a/apps/meteor/server/features/EmailInbox/EmailInbox_Outgoing.ts b/apps/meteor/server/features/EmailInbox/EmailInbox_Outgoing.ts index d15ac6dbbc909..b2480acff247d 100644 --- a/apps/meteor/server/features/EmailInbox/EmailInbox_Outgoing.ts +++ b/apps/meteor/server/features/EmailInbox/EmailInbox_Outgoing.ts @@ -142,7 +142,11 @@ slashCommands.add({ return; } - const emailText = message?.msg || ''; + const emailText = + message?.attachments + ?.map((a) => a.description) + .filter(Boolean) + .join('\n\n') || ''; void sendEmail( inbox, diff --git a/apps/meteor/server/services/messages/hooks/BeforeSaveMarkdownParser.ts b/apps/meteor/server/services/messages/hooks/BeforeSaveMarkdownParser.ts index b2b47ad0b9228..088aff0c8b052 100644 --- a/apps/meteor/server/services/messages/hooks/BeforeSaveMarkdownParser.ts +++ b/apps/meteor/server/services/messages/hooks/BeforeSaveMarkdownParser.ts @@ -38,6 +38,10 @@ export class BeforeSaveMarkdownParser { if (message.msg) { message.md = parse(message.msg, config); } + + if (message.attachments?.[0]?.description) { + message.attachments[0].descriptionMd = parse(message.attachments[0].description, config); + } } catch (e) { console.error(e); // errors logged while the parser is at experimental stage } diff --git a/apps/meteor/tests/unit/server/services/messages/hooks/BeforeSaveJumpToMessage.tests.ts b/apps/meteor/tests/unit/server/services/messages/hooks/BeforeSaveJumpToMessage.tests.ts index 791404e4f1d9f..0ce7279e89b39 100644 --- a/apps/meteor/tests/unit/server/services/messages/hooks/BeforeSaveJumpToMessage.tests.ts +++ b/apps/meteor/tests/unit/server/services/messages/hooks/BeforeSaveJumpToMessage.tests.ts @@ -500,6 +500,17 @@ describe('Create attachments for message URLs', () => { image_size: 68016, type: 'file', description: 'chained 3 - file', + descriptionMd: [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'chained 3 - file', + }, + ], + }, + ], }, ], }, diff --git a/apps/meteor/tests/unit/server/services/messages/hooks/BeforeSaveMarkdownParser.tests.ts b/apps/meteor/tests/unit/server/services/messages/hooks/BeforeSaveMarkdownParser.tests.ts index c744883ce9e3c..c511b071ac1a0 100644 --- a/apps/meteor/tests/unit/server/services/messages/hooks/BeforeSaveMarkdownParser.tests.ts +++ b/apps/meteor/tests/unit/server/services/messages/hooks/BeforeSaveMarkdownParser.tests.ts @@ -82,4 +82,29 @@ describe('Markdown parser', () => { expect(message).to.have.property('md'); }); + + it('should parse markdown on the first attachment only', async () => { + const markdownParser = new BeforeSaveMarkdownParser(true); + + const message = await markdownParser.parseMarkdown({ + message: createMessage('hey', { + attachments: [ + { + description: 'hey ho', + }, + { + description: 'lets go', + }, + ], + }), + config: {}, + }); + + expect(message).to.have.property('md'); + + const [attachment1, attachment2] = message.attachments || []; + + expect(attachment1).to.have.property('descriptionMd'); + expect(attachment2).to.not.have.property('descriptionMd'); + }); }); diff --git a/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts b/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts index 67b9aa5ab16d9..4755688c9f006 100644 --- a/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts +++ b/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts @@ -312,7 +312,9 @@ export class OmnichannelTranscript extends ServiceClass implements IOmnichannelT } } - const msg = message.msg || ''; + // When you send a file message, the things you type in the modal are not "msg", they're in "description" of the attachment + // So, we'll fetch the the msg, if empty, go for the first description on an attachment, if empty, empty string + const msg = message.msg || message.attachments.find((attachment) => attachment.description)?.description || ''; // Remove nulls from final array messagesData.push({ msg, diff --git a/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentBase.ts b/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentBase.ts index 7d236bcafa2be..82ce0db3325ef 100644 --- a/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentBase.ts +++ b/packages/core-typings/src/IMessage/MessageAttachment/MessageAttachmentBase.ts @@ -7,6 +7,7 @@ export type MessageAttachmentBase = { collapsed?: boolean; /** description isn't being used on client for non-image attachments, we're keeping it for backward compatibility */ description?: string; + descriptionMd?: Root; text?: string; md?: Root; size?: number; From 50b7933ae63a1b1d5b6b832f9869a167e2b84220 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Tue, 9 Jun 2026 19:51:27 -0300 Subject: [PATCH 5/6] refactor: update event types from FormEvent to ChangeEvent in various components (#40823) --- .../NavBarSettingsToolbar/UserMenu/EditStatusModal.tsx | 4 ++-- .../MatrixFederationManageServerModal.tsx | 4 ++-- .../MatrixFederationSearchModalContent.tsx | 4 ++-- .../meteor/client/views/account/security/TwoFactorEmail.tsx | 4 ++-- apps/meteor/client/views/account/security/TwoFactorTOTP.tsx | 4 ++-- .../client/views/admin/customSounds/AddCustomSound.tsx | 4 ++-- apps/meteor/client/views/admin/customSounds/EditSound.tsx | 4 ++-- apps/meteor/client/views/admin/import/NewImportPage.tsx | 6 +++--- .../permissions/PermissionsTable/PermissionsTableFilter.tsx | 4 ++-- .../admin/settings/Setting/inputs/BooleanSettingInput.tsx | 4 ++-- .../admin/settings/Setting/inputs/FontSettingInput.tsx | 4 ++-- .../admin/settings/Setting/inputs/GenericSettingInput.tsx | 4 ++-- .../views/admin/settings/Setting/inputs/IntSettingInput.tsx | 4 ++-- .../admin/settings/Setting/inputs/PasswordSettingInput.tsx | 4 ++-- .../settings/Setting/inputs/RelativeUrlSettingInput.tsx | 4 ++-- .../admin/settings/Setting/inputs/StringSettingInput.tsx | 4 ++-- .../admin/settings/Setting/inputs/TimespanSettingInput.tsx | 4 ++-- .../admin/settings/SettingsGroupPage/SettingsGroupPage.tsx | 2 +- .../client/views/admin/settings/groups/LDAPGroupPage.tsx | 6 +++--- .../views/admin/users/UsersTable/UsersTableFilters.tsx | 6 +++--- .../client/views/audit/components/forms/DateRangePicker.tsx | 6 +++--- .../views/mediaCallHistory/CallHistoryPageFilters.tsx | 6 +++--- .../client/views/omnichannel/analytics/DateRangePicker.tsx | 6 +++--- .../contextualBar/CannedResponse/CannedResponseList.tsx | 4 ++-- .../client/views/omnichannel/departments/DepartmentTags.tsx | 4 ++-- .../omnichannel/directory/contacts/RemoveContactModal.tsx | 4 ++-- .../Header/Omnichannel/QuickActions/QuickActionOptions.tsx | 2 +- .../client/views/room/composer/messageBox/MessageBox.tsx | 2 +- .../views/room/contextualBar/RoomMembers/RoomMembers.tsx | 4 ++-- .../client/views/room/contextualBar/Threads/ThreadList.tsx | 4 ++-- .../components/MultiSelectCustom/MultiSelectCustomList.tsx | 6 +++--- packages/ui-voip/src/providers/useMediaSession.ts | 2 +- .../CallHistoryContextualbar/CallHistoryActions.stories.tsx | 6 +++--- 33 files changed, 70 insertions(+), 70 deletions(-) diff --git a/apps/meteor/client/navbar/NavBarSettingsToolbar/UserMenu/EditStatusModal.tsx b/apps/meteor/client/navbar/NavBarSettingsToolbar/UserMenu/EditStatusModal.tsx index e5cc289312bda..bbe2df4d903be 100644 --- a/apps/meteor/client/navbar/NavBarSettingsToolbar/UserMenu/EditStatusModal.tsx +++ b/apps/meteor/client/navbar/NavBarSettingsToolbar/UserMenu/EditStatusModal.tsx @@ -20,7 +20,7 @@ import { } from '@rocket.chat/fuselage'; import { useEffectEvent, useLocalStorage } from '@rocket.chat/fuselage-hooks'; import { useToastMessageDispatch, useSetting, useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; -import type { ChangeEvent, ComponentProps, FormEvent } from 'react'; +import type { ChangeEvent, ComponentProps } from 'react'; import { useState, useCallback, useId } from 'react'; import UserStatusMenu from '../../../components/UserStatusMenu'; @@ -76,7 +76,7 @@ const EditStatusModal = ({ onClose, userStatus, userStatusText }: EditStatusModa wrapperFunction={(props: ComponentProps) => ( { + onSubmit={(e) => { e.preventDefault(); handleSaveStatus(); }} diff --git a/apps/meteor/client/sidebar/header/MatrixFederationSearch/MatrixFederationManageServerModal.tsx b/apps/meteor/client/sidebar/header/MatrixFederationSearch/MatrixFederationManageServerModal.tsx index 2baaeac8753bd..c96b5d966bb5f 100644 --- a/apps/meteor/client/sidebar/header/MatrixFederationSearch/MatrixFederationManageServerModal.tsx +++ b/apps/meteor/client/sidebar/header/MatrixFederationSearch/MatrixFederationManageServerModal.tsx @@ -18,7 +18,7 @@ import { import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useSetModal, useTranslation, useEndpoint, useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import type { FormEvent } from 'react'; +import type { ChangeEvent } from 'react'; import { useState } from 'react'; import MatrixFederationRemoveServerList from './MatrixFederationRemoveServerList'; @@ -90,7 +90,7 @@ const MatrixFederationAddServerModal = ({ onClickClose }: MatrixFederationAddSer ) => { + onChange={(e: ChangeEvent) => { setServerName(e.currentTarget.value); if (errorKey) { setErrorKey(undefined); diff --git a/apps/meteor/client/sidebar/header/MatrixFederationSearch/MatrixFederationSearchModalContent.tsx b/apps/meteor/client/sidebar/header/MatrixFederationSearch/MatrixFederationSearchModalContent.tsx index 853df819ddbb5..ff5f5ab94fc15 100644 --- a/apps/meteor/client/sidebar/header/MatrixFederationSearch/MatrixFederationSearchModalContent.tsx +++ b/apps/meteor/client/sidebar/header/MatrixFederationSearch/MatrixFederationSearchModalContent.tsx @@ -2,7 +2,7 @@ import type { SelectOption } from '@rocket.chat/fuselage'; import { Box, Select, TextInput } from '@rocket.chat/fuselage'; import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; import { useSetModal } from '@rocket.chat/ui-contexts'; -import type { FormEvent } from 'react'; +import type { ChangeEvent } from 'react'; import { useCallback, useState, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -53,7 +53,7 @@ const MatrixFederationSearchModalContent = ({ defaultSelectedServer, servers }: flexGrow={4} flexShrink={0} value={roomName} - onChange={(e: FormEvent) => setRoomName(e.currentTarget.value)} + onChange={(e: ChangeEvent) => setRoomName(e.currentTarget.value)} /> diff --git a/apps/meteor/client/views/account/security/TwoFactorEmail.tsx b/apps/meteor/client/views/account/security/TwoFactorEmail.tsx index 17c261e8df5a0..5929662999cb2 100644 --- a/apps/meteor/client/views/account/security/TwoFactorEmail.tsx +++ b/apps/meteor/client/views/account/security/TwoFactorEmail.tsx @@ -1,6 +1,6 @@ import { Box, Field, FieldLabel, FieldRow, Margins, ToggleSwitch } from '@rocket.chat/fuselage'; import { useToastMessageDispatch, useUser } from '@rocket.chat/ui-contexts'; -import type { ComponentProps, FormEvent } from 'react'; +import type { ComponentProps, ChangeEvent } from 'react'; import { useCallback, useId } from 'react'; import { useTranslation } from 'react-i18next'; @@ -27,7 +27,7 @@ const TwoFactorEmail = (props: ComponentProps) => { }); const handleEnable = useCallback( - async (e: FormEvent) => { + async (e: ChangeEvent) => { if (e.currentTarget.checked) { await enable2faAction(); } else { diff --git a/apps/meteor/client/views/account/security/TwoFactorTOTP.tsx b/apps/meteor/client/views/account/security/TwoFactorTOTP.tsx index 26c51e32702b5..7bfb8cc3876d2 100644 --- a/apps/meteor/client/views/account/security/TwoFactorTOTP.tsx +++ b/apps/meteor/client/views/account/security/TwoFactorTOTP.tsx @@ -1,7 +1,7 @@ import { Box, Button, TextInput, Margins, Field, FieldRow, FieldLabel, ToggleSwitch } from '@rocket.chat/fuselage'; import { useEffectEvent, useSafely } from '@rocket.chat/fuselage-hooks'; import { useSetModal, useToastMessageDispatch, useUser, useMethod } from '@rocket.chat/ui-contexts'; -import type { ComponentPropsWithoutRef, FormEvent } from 'react'; +import type { ComponentPropsWithoutRef, ChangeEvent } from 'react'; import { useState, useCallback, useEffect, useId } from 'react'; import { useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; @@ -92,7 +92,7 @@ const TwoFactorTOTP = (props: TwoFactorTOTPProps) => { setModal(); }); - const handleToggleTotp = useEffectEvent(async (e: FormEvent) => { + const handleToggleTotp = useEffectEvent(async (e: ChangeEvent) => { if (e.currentTarget?.checked) { void enableTotp(); } else { diff --git a/apps/meteor/client/views/admin/customSounds/AddCustomSound.tsx b/apps/meteor/client/views/admin/customSounds/AddCustomSound.tsx index 187b4fb317bd5..e35ac9df9db23 100644 --- a/apps/meteor/client/views/admin/customSounds/AddCustomSound.tsx +++ b/apps/meteor/client/views/admin/customSounds/AddCustomSound.tsx @@ -2,7 +2,7 @@ import { Field, FieldLabel, FieldRow, TextInput, Box, Margins, Button, ButtonGro import { ContextualbarScrollableContent, ContextualbarFooter } from '@rocket.chat/ui-client'; import { useToastMessageDispatch, type UploadResult } from '@rocket.chat/ui-contexts'; import fileSize from 'filesize'; -import type { FormEvent } from 'react'; +import type { ChangeEvent } from 'react'; import { useState, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -86,7 +86,7 @@ const AddCustomSound = ({ goToNew, close, onChange, ...props }: AddCustomSoundPr ): void => setName(e.currentTarget.value)} + onChange={(e: ChangeEvent): void => setName(e.currentTarget.value)} placeholder={t('Name')} /> diff --git a/apps/meteor/client/views/admin/customSounds/EditSound.tsx b/apps/meteor/client/views/admin/customSounds/EditSound.tsx index 54c338e444362..9fa3043b80ee8 100644 --- a/apps/meteor/client/views/admin/customSounds/EditSound.tsx +++ b/apps/meteor/client/views/admin/customSounds/EditSound.tsx @@ -2,7 +2,7 @@ import { Box, Button, ButtonGroup, Margins, TextInput, Field, FieldLabel, FieldR import { GenericModal, ContextualbarScrollableContent, ContextualbarFooter } from '@rocket.chat/ui-client'; import { useSetModal, useToastMessageDispatch, useEndpoint } from '@rocket.chat/ui-contexts'; import fileSize from 'filesize'; -import type { SyntheticEvent } from 'react'; +import type { ChangeEvent } from 'react'; import { useCallback, useState, useMemo, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; @@ -117,7 +117,7 @@ function EditSound({ close, onChange, data, ...props }: EditSoundProps) { ): void => setName(e.currentTarget.value)} + onChange={(e: ChangeEvent): void => setName(e.currentTarget.value)} placeholder={t('Name')} /> diff --git a/apps/meteor/client/views/admin/import/NewImportPage.tsx b/apps/meteor/client/views/admin/import/NewImportPage.tsx index 8cd03322ed509..7fcc67e42cc33 100644 --- a/apps/meteor/client/views/admin/import/NewImportPage.tsx +++ b/apps/meteor/client/views/admin/import/NewImportPage.tsx @@ -19,7 +19,7 @@ import { Page, PageHeader, PageScrollableContentWithShadow } from '@rocket.chat/ import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useToastMessageDispatch, useRouter, useRouteParameter, useSetting, useEndpoint } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; -import type { ChangeEvent, DragEvent, FormEvent, Key, SyntheticEvent } from 'react'; +import type { ChangeEvent, DragEvent, Key, SyntheticEvent } from 'react'; import { useState, useMemo, useEffect, useId } from 'react'; import { useTranslation } from 'react-i18next'; @@ -146,7 +146,7 @@ function NewImportPage() { const [fileUrl, setFileUrl] = useSafely(useState('')); - const handleFileUrlChange = (event: FormEvent) => { + const handleFileUrlChange = (event: ChangeEvent) => { setFileUrl(event.currentTarget.value); }; @@ -170,7 +170,7 @@ function NewImportPage() { const [filePath, setFilePath] = useSafely(useState('')); - const handleFilePathChange = (event: FormEvent) => { + const handleFilePathChange = (event: ChangeEvent) => { setFilePath(event.currentTarget.value); }; diff --git a/apps/meteor/client/views/admin/permissions/PermissionsTable/PermissionsTableFilter.tsx b/apps/meteor/client/views/admin/permissions/PermissionsTable/PermissionsTableFilter.tsx index 0137b18a47c95..b70ed82b6d7f7 100644 --- a/apps/meteor/client/views/admin/permissions/PermissionsTable/PermissionsTableFilter.tsx +++ b/apps/meteor/client/views/admin/permissions/PermissionsTable/PermissionsTableFilter.tsx @@ -1,6 +1,6 @@ import { TextInput } from '@rocket.chat/fuselage'; import { useEffectEvent, useDebouncedValue } from '@rocket.chat/fuselage-hooks'; -import type { FormEvent } from 'react'; +import type { ChangeEvent } from 'react'; import { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; @@ -13,7 +13,7 @@ const PermissionsTableFilter = ({ onChange }: { onChange: (debouncedFilter: stri onChange(debouncedFilter); }, [debouncedFilter, onChange]); - const handleFilter = useEffectEvent(({ currentTarget: { value } }: FormEvent) => { + const handleFilter = useEffectEvent(({ currentTarget: { value } }: ChangeEvent) => { setFilter(value); }); diff --git a/apps/meteor/client/views/admin/settings/Setting/inputs/BooleanSettingInput.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/BooleanSettingInput.tsx index acb4abb5c0db3..71c4207367d83 100644 --- a/apps/meteor/client/views/admin/settings/Setting/inputs/BooleanSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/Setting/inputs/BooleanSettingInput.tsx @@ -1,5 +1,5 @@ import { Box, Field, FieldHint, FieldLabel, FieldRow, ToggleSwitch } from '@rocket.chat/fuselage'; -import type { SyntheticEvent } from 'react'; +import type { ChangeEvent } from 'react'; import ResetSettingButton from '../ResetSettingButton'; import type { SettingInputProps } from './types'; @@ -18,7 +18,7 @@ function BooleanSettingInput({ onChangeValue, onResetButtonClick, }: BooleanSettingInputProps) { - const handleChange = (event: SyntheticEvent): void => { + const handleChange = (event: ChangeEvent): void => { const value = event.currentTarget.checked; onChangeValue?.(value); }; diff --git a/apps/meteor/client/views/admin/settings/Setting/inputs/FontSettingInput.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/FontSettingInput.tsx index 815bc657b71e6..ba65bda228428 100644 --- a/apps/meteor/client/views/admin/settings/Setting/inputs/FontSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/Setting/inputs/FontSettingInput.tsx @@ -1,5 +1,5 @@ import { Field, FieldHint, FieldLabel, FieldRow, TextInput } from '@rocket.chat/fuselage'; -import type { FormEventHandler } from 'react'; +import type { ChangeEventHandler } from 'react'; import ResetSettingButton from '../ResetSettingButton'; import type { SettingInputProps } from './types'; @@ -22,7 +22,7 @@ function FontSettingInput({ onChangeValue, onResetButtonClick, }: FontSettingInputProps) { - const handleChange: FormEventHandler = (event): void => { + const handleChange: ChangeEventHandler = (event): void => { onChangeValue?.(event.currentTarget.value); }; diff --git a/apps/meteor/client/views/admin/settings/Setting/inputs/GenericSettingInput.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/GenericSettingInput.tsx index 0e5e901255d73..e2e93c8f01a54 100644 --- a/apps/meteor/client/views/admin/settings/Setting/inputs/GenericSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/Setting/inputs/GenericSettingInput.tsx @@ -1,5 +1,5 @@ import { Field, FieldHint, FieldLabel, FieldRow, TextInput } from '@rocket.chat/fuselage'; -import type { FormEventHandler } from 'react'; +import type { ChangeEventHandler } from 'react'; import ResetSettingButton from '../ResetSettingButton'; import type { SettingInputProps } from './types'; @@ -22,7 +22,7 @@ function GenericSettingInput({ onChangeValue, onResetButtonClick, }: GenericSettingInputProps) { - const handleChange: FormEventHandler = (event): void => { + const handleChange: ChangeEventHandler = (event): void => { onChangeValue?.(event.currentTarget.value); }; diff --git a/apps/meteor/client/views/admin/settings/Setting/inputs/IntSettingInput.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/IntSettingInput.tsx index 7b3747ef3f474..8fda81726d045 100644 --- a/apps/meteor/client/views/admin/settings/Setting/inputs/IntSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/Setting/inputs/IntSettingInput.tsx @@ -1,5 +1,5 @@ import { Field, FieldHint, FieldLabel, FieldRow, InputBox } from '@rocket.chat/fuselage'; -import type { FormEventHandler } from 'react'; +import type { ChangeEventHandler } from 'react'; import ResetSettingButton from '../ResetSettingButton'; import type { SettingInputProps } from './types'; @@ -22,7 +22,7 @@ function IntSettingInput({ hasResetButton, onResetButtonClick, }: IntSettingInputProps) { - const handleChange: FormEventHandler = (event) => { + const handleChange: ChangeEventHandler = (event) => { onChangeValue?.(parseInt(event.currentTarget.value, 10)); }; diff --git a/apps/meteor/client/views/admin/settings/Setting/inputs/PasswordSettingInput.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/PasswordSettingInput.tsx index c3e98e3fd551e..9bc582bc83855 100644 --- a/apps/meteor/client/views/admin/settings/Setting/inputs/PasswordSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/Setting/inputs/PasswordSettingInput.tsx @@ -1,5 +1,5 @@ import { Field, FieldHint, FieldLabel, FieldRow, PasswordInput } from '@rocket.chat/fuselage'; -import type { EventHandler, SyntheticEvent } from 'react'; +import type { ChangeEventHandler } from 'react'; import ResetSettingButton from '../ResetSettingButton'; import type { SettingInputProps } from './types'; @@ -20,7 +20,7 @@ function PasswordSettingInput({ onChangeValue, onResetButtonClick, }: PasswordSettingInputProps) { - const handleChange: EventHandler> = (event) => { + const handleChange: ChangeEventHandler = (event) => { onChangeValue?.(event.currentTarget.value); }; diff --git a/apps/meteor/client/views/admin/settings/Setting/inputs/RelativeUrlSettingInput.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/RelativeUrlSettingInput.tsx index bf2eb629b94f3..78e42d2ed0239 100644 --- a/apps/meteor/client/views/admin/settings/Setting/inputs/RelativeUrlSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/Setting/inputs/RelativeUrlSettingInput.tsx @@ -1,6 +1,6 @@ import { Field, FieldHint, FieldLabel, FieldRow, UrlInput } from '@rocket.chat/fuselage'; import { useAbsoluteUrl } from '@rocket.chat/ui-contexts'; -import type { EventHandler, SyntheticEvent } from 'react'; +import type { ChangeEventHandler } from 'react'; import ResetSettingButton from '../ResetSettingButton'; import type { SettingInputProps } from './types'; @@ -23,7 +23,7 @@ function RelativeUrlSettingInput({ }: RelativeUrlSettingInputProps) { const getAbsoluteUrl = useAbsoluteUrl(); - const handleChange: EventHandler> = (event) => { + const handleChange: ChangeEventHandler = (event) => { onChangeValue?.(event.currentTarget.value); }; diff --git a/apps/meteor/client/views/admin/settings/Setting/inputs/StringSettingInput.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/StringSettingInput.tsx index 8db88c13f514e..2c096cbc4dee7 100644 --- a/apps/meteor/client/views/admin/settings/Setting/inputs/StringSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/Setting/inputs/StringSettingInput.tsx @@ -1,5 +1,5 @@ import { Field, FieldHint, FieldLabel, FieldRow, TextAreaInput, TextInput } from '@rocket.chat/fuselage'; -import type { EventHandler, SyntheticEvent } from 'react'; +import type { ChangeEventHandler } from 'react'; import ResetSettingButton from '../ResetSettingButton'; import type { SettingInputProps } from './types'; @@ -27,7 +27,7 @@ function StringSettingInput({ onChangeValue, onResetButtonClick, }: StringSettingInputProps) { - const handleChange: EventHandler> = (event) => { + const handleChange: ChangeEventHandler = (event) => { onChangeValue?.(event.currentTarget.value); }; diff --git a/apps/meteor/client/views/admin/settings/Setting/inputs/TimespanSettingInput.tsx b/apps/meteor/client/views/admin/settings/Setting/inputs/TimespanSettingInput.tsx index 8ec08a18f22a6..7a65556a4b7b7 100644 --- a/apps/meteor/client/views/admin/settings/Setting/inputs/TimespanSettingInput.tsx +++ b/apps/meteor/client/views/admin/settings/Setting/inputs/TimespanSettingInput.tsx @@ -1,5 +1,5 @@ import { Field, FieldHint, FieldLabel, FieldRow, InputBox, Select } from '@rocket.chat/fuselage'; -import type { FormEventHandler, Key } from 'react'; +import type { ChangeEventHandler, Key } from 'react'; import { useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -55,7 +55,7 @@ function TimespanSettingInput({ const [timeUnit, setTimeUnit] = useState(getHighestTimeUnit(Number(value))); const [internalValue, setInternalValue] = useState(msToTimeUnit(timeUnit, Number(value))); - const handleChange: FormEventHandler = (event) => { + const handleChange: ChangeEventHandler = (event) => { const newValue = sanitizeInputValue(Number(event.currentTarget.value)); onChangeValue?.(timeUnitToMs(timeUnit, newValue)); diff --git a/apps/meteor/client/views/admin/settings/SettingsGroupPage/SettingsGroupPage.tsx b/apps/meteor/client/views/admin/settings/SettingsGroupPage/SettingsGroupPage.tsx index 10947f5758f28..1941bbb7e3aef 100644 --- a/apps/meteor/client/views/admin/settings/SettingsGroupPage/SettingsGroupPage.tsx +++ b/apps/meteor/client/views/admin/settings/SettingsGroupPage/SettingsGroupPage.tsx @@ -4,7 +4,7 @@ import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import { Page, PageHeader, PageScrollableContentWithShadow, PageFooter } from '@rocket.chat/ui-client'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useToastMessageDispatch, useSettingsDispatch, useSettings } from '@rocket.chat/ui-contexts'; -import type { ReactNode, FormEvent, MouseEvent } from 'react'; +import type { ReactNode, MouseEvent, FormEvent } from 'react'; import { useMemo, memo } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/apps/meteor/client/views/admin/settings/groups/LDAPGroupPage.tsx b/apps/meteor/client/views/admin/settings/groups/LDAPGroupPage.tsx index e862afaf03ecb..c2253e5564104 100644 --- a/apps/meteor/client/views/admin/settings/groups/LDAPGroupPage.tsx +++ b/apps/meteor/client/views/admin/settings/groups/LDAPGroupPage.tsx @@ -3,7 +3,7 @@ import { Button, Box, TextInput, Field, FieldLabel, FieldRow } from '@rocket.cha import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import { GenericModal } from '@rocket.chat/ui-client'; import { useSetModal, useToastMessageDispatch, useSetting, useEndpoint } from '@rocket.chat/ui-contexts'; -import type { FormEvent } from 'react'; +import type { ChangeEvent } from 'react'; import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -53,7 +53,7 @@ function LDAPGroupPage({ _id, i18nLabel, onClickBack, ...group }: LDAPGroupPageP try { await testConnection(); let username = ''; - const handleChangeUsername = (event: FormEvent): void => { + const handleChangeUsername = (event: ChangeEvent): void => { username = event.currentTarget.value; }; @@ -71,7 +71,7 @@ function LDAPGroupPage({ _id, i18nLabel, onClickBack, ...group }: LDAPGroupPageP wrapperFunction={(props) => ( { + onSubmit={(e) => { e.preventDefault(); confirmSearch(); }} diff --git a/apps/meteor/client/views/admin/users/UsersTable/UsersTableFilters.tsx b/apps/meteor/client/views/admin/users/UsersTable/UsersTableFilters.tsx index 42e007de6f508..e43bc1a6c73d4 100644 --- a/apps/meteor/client/views/admin/users/UsersTable/UsersTableFilters.tsx +++ b/apps/meteor/client/views/admin/users/UsersTable/UsersTableFilters.tsx @@ -3,7 +3,7 @@ import { Box, Icon, Margins, TextInput } from '@rocket.chat/fuselage'; import { useBreakpoints } from '@rocket.chat/fuselage-hooks'; import type { OptionProp } from '@rocket.chat/ui-client'; import { MultiSelectCustom } from '@rocket.chat/ui-client'; -import type { Dispatch, FormEvent, SetStateAction } from 'react'; +import type { ChangeEvent, Dispatch, SetStateAction } from 'react'; import { useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -21,7 +21,7 @@ const UsersTableFilters = ({ roleData, setUsersFilters }: UsersTableFiltersProps const [text, setText] = useState(''); const handleSearchTextChange = useCallback( - (event: FormEvent) => { + (event: ChangeEvent) => { setText(event.currentTarget.value); setUsersFilters({ text: event.currentTarget.value, roles: selectedRoles }); }, @@ -66,7 +66,7 @@ const UsersTableFilters = ({ roleData, setUsersFilters }: UsersTableFiltersProps ) => { + onSubmit={(event) => { event.preventDefault(); }} display='flex' diff --git a/apps/meteor/client/views/audit/components/forms/DateRangePicker.tsx b/apps/meteor/client/views/audit/components/forms/DateRangePicker.tsx index da65923a97234..80a3cc33343e0 100644 --- a/apps/meteor/client/views/audit/components/forms/DateRangePicker.tsx +++ b/apps/meteor/client/views/audit/components/forms/DateRangePicker.tsx @@ -2,7 +2,7 @@ import { Box, InputBox, Margins } from '@rocket.chat/fuselage'; import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import { GenericMenu } from '@rocket.chat/ui-client'; import { startOfDay, endOfDay, startOfWeek, endOfWeek, startOfMonth, endOfMonth, subDays, subWeeks, subMonths, parseISO } from 'date-fns'; -import type { ComponentProps, SetStateAction, FormEvent } from 'react'; +import type { ComponentProps, SetStateAction, ChangeEvent } from 'react'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -130,11 +130,11 @@ const DateRangePicker = ({ value, onChange, ...props }: DateRangePickerProps) => onChange?.(newRange); }); - const handleChangeStart = useEffectEvent(({ currentTarget }: FormEvent) => { + const handleChangeStart = useEffectEvent(({ currentTarget }: ChangeEvent) => { dispatch({ newStart: currentTarget.value }); }); - const handleChangeEnd = useEffectEvent(({ currentTarget }: FormEvent) => { + const handleChangeEnd = useEffectEvent(({ currentTarget }: ChangeEvent) => { dispatch({ newEnd: currentTarget.value }); }); diff --git a/apps/meteor/client/views/mediaCallHistory/CallHistoryPageFilters.tsx b/apps/meteor/client/views/mediaCallHistory/CallHistoryPageFilters.tsx index 56c4942227169..e1f333c571db0 100644 --- a/apps/meteor/client/views/mediaCallHistory/CallHistoryPageFilters.tsx +++ b/apps/meteor/client/views/mediaCallHistory/CallHistoryPageFilters.tsx @@ -2,7 +2,7 @@ import { Box, Icon, TextInput, Select } from '@rocket.chat/fuselage'; import type { OptionProp } from '@rocket.chat/ui-client'; import { MultiSelectCustom } from '@rocket.chat/ui-client'; import { useCallback, useMemo, useState } from 'react'; -import type { FormEvent, Key } from 'react'; +import type { ChangeEvent, Key, FormEvent } from 'react'; import { useTranslation } from 'react-i18next'; type StatesFilter = Array<'ended' | 'transferred' | 'not-answered' | 'failed'>; @@ -79,7 +79,7 @@ const CallHistoryPageFilters = ({ onChangeText, onChangeType, onChangeStates, se return ( e.preventDefault(), [])} + onSubmit={useCallback((e: FormEvent) => e.preventDefault(), [])} mb='x8' display='flex' flexWrap='wrap' @@ -92,7 +92,7 @@ const CallHistoryPageFilters = ({ onChangeText, onChangeType, onChangeStates, se alignItems='center' placeholder={t('Search_calls')} addon={} - onChange={(e: FormEvent) => onChangeText(e.currentTarget.value)} + onChange={(e: ChangeEvent) => onChangeText(e.currentTarget.value)} value={searchText} /> diff --git a/apps/meteor/client/views/omnichannel/analytics/DateRangePicker.tsx b/apps/meteor/client/views/omnichannel/analytics/DateRangePicker.tsx index 746225ee2f44a..e8307eae49b3a 100644 --- a/apps/meteor/client/views/omnichannel/analytics/DateRangePicker.tsx +++ b/apps/meteor/client/views/omnichannel/analytics/DateRangePicker.tsx @@ -2,7 +2,7 @@ import { Box, InputBox, Field, FieldLabel, FieldRow } from '@rocket.chat/fuselag import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; import { GenericMenu } from '@rocket.chat/ui-client'; import { subDays, subMonths, startOfMonth, endOfMonth, format } from 'date-fns'; -import type { ComponentProps, FormEvent } from 'react'; +import type { ComponentProps, ChangeEvent } from 'react'; import { useState, useMemo, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; @@ -39,7 +39,7 @@ const DateRangePicker = ({ onChange = () => undefined, ...props }: DateRangePick const { start, end } = range; - const handleStart = useEffectEvent(({ currentTarget }: FormEvent) => { + const handleStart = useEffectEvent(({ currentTarget }: ChangeEvent) => { const rangeObj = { start: currentTarget.value, end: range.end, @@ -48,7 +48,7 @@ const DateRangePicker = ({ onChange = () => undefined, ...props }: DateRangePick onChange(rangeObj); }); - const handleEnd = useEffectEvent(({ currentTarget }: FormEvent) => { + const handleEnd = useEffectEvent(({ currentTarget }: ChangeEvent) => { const rangeObj = { end: currentTarget.value, start: range.start, diff --git a/apps/meteor/client/views/omnichannel/cannedResponses/contextualBar/CannedResponse/CannedResponseList.tsx b/apps/meteor/client/views/omnichannel/cannedResponses/contextualBar/CannedResponse/CannedResponseList.tsx index 516a70ee0143d..041952b2e5584 100644 --- a/apps/meteor/client/views/omnichannel/cannedResponses/contextualBar/CannedResponse/CannedResponseList.tsx +++ b/apps/meteor/client/views/omnichannel/cannedResponses/contextualBar/CannedResponse/CannedResponseList.tsx @@ -11,7 +11,7 @@ import { ContextualbarDialog, } from '@rocket.chat/ui-client'; import { useRoomToolbox } from '@rocket.chat/ui-contexts'; -import type { Dispatch, FormEventHandler, MouseEvent, SetStateAction } from 'react'; +import type { Dispatch, ChangeEventHandler, MouseEvent, SetStateAction } from 'react'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { Virtuoso } from 'react-virtuoso'; @@ -27,7 +27,7 @@ type CannedResponseListProps = { onClose: () => void; options: [string, string][]; text: string; - setText: FormEventHandler; + setText: ChangeEventHandler; type: string; setType: Dispatch>; isRoomOverMacLimit: boolean; diff --git a/apps/meteor/client/views/omnichannel/departments/DepartmentTags.tsx b/apps/meteor/client/views/omnichannel/departments/DepartmentTags.tsx index 36815bde66c91..f8854c6575791 100644 --- a/apps/meteor/client/views/omnichannel/departments/DepartmentTags.tsx +++ b/apps/meteor/client/views/omnichannel/departments/DepartmentTags.tsx @@ -1,5 +1,5 @@ import { Button, Chip, FieldRow, TextInput } from '@rocket.chat/fuselage'; -import type { ComponentProps, FormEvent } from 'react'; +import type { ComponentProps, ChangeEvent } from 'react'; import { useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -33,7 +33,7 @@ const DepartmentTags = ({ error, value: tags, onChange, ...props }: DepartmentTa error={error} placeholder={t('Enter_a_tag')} value={tagText} - onChange={(e: FormEvent) => setTagText(e.currentTarget.value)} + onChange={(e: ChangeEvent) => setTagText(e.currentTarget.value)} {...props} />