From 08a702c9bca3296b4ebe120ef9dad74af73287fb Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Fri, 20 Feb 2026 15:06:58 -0300 Subject: [PATCH 001/108] chore: Use extraction from AST to infer mentions (#38845) Co-authored-by: Kevin Aleman --- .../extractMentionsFromMessageAST.ts | 63 +++ apps/meteor/app/mentions/server/Mentions.ts | 39 +- .../server/services/messages/service.ts | 2 +- .../extractMentionsFromMessageAST.spec.ts | 396 ++++++++++++++++++ 4 files changed, 490 insertions(+), 10 deletions(-) create mode 100644 apps/meteor/app/lib/server/functions/extractMentionsFromMessageAST.ts create mode 100644 apps/meteor/tests/unit/app/lib/server/functions/extractMentionsFromMessageAST.spec.ts diff --git a/apps/meteor/app/lib/server/functions/extractMentionsFromMessageAST.ts b/apps/meteor/app/lib/server/functions/extractMentionsFromMessageAST.ts new file mode 100644 index 0000000000000..441aa8147f5d0 --- /dev/null +++ b/apps/meteor/app/lib/server/functions/extractMentionsFromMessageAST.ts @@ -0,0 +1,63 @@ +import type { Root, Paragraph, Blocks, Inlines, UserMention, ChannelMention, Task, ListItem, BigEmoji } from '@rocket.chat/message-parser'; + +type ExtractedMentions = { + mentions: string[]; + channels: string[]; +}; + +type MessageNode = Paragraph | Blocks | Inlines | Task | ListItem | BigEmoji; + +function isUserMention(node: MessageNode): node is UserMention { + return node.type === 'MENTION_USER'; +} + +function isChannelMention(node: MessageNode): node is ChannelMention { + return node.type === 'MENTION_CHANNEL'; +} + +function hasArrayValue(node: MessageNode): node is MessageNode & { value: MessageNode[] } { + return Array.isArray(node.value); +} + +function hasObjectValue(node: MessageNode): node is MessageNode & { value: Record } { + return typeof node.value === 'object' && node.value !== null && !Array.isArray(node.value); +} + +function traverse(node: MessageNode, mentions: Set, channels: Set): void { + if (isUserMention(node)) { + mentions.add(node.value.value); + return; + } + + if (isChannelMention(node)) { + channels.add(node.value.value); + return; + } + + if (hasArrayValue(node)) { + for (const child of node.value) { + traverse(child, mentions, channels); + } + return; + } + + if (hasObjectValue(node)) { + for (const key of Object.keys(node.value)) { + traverse(node.value[key], mentions, channels); + } + } +} + +export function extractMentionsFromMessageAST(ast: Root): ExtractedMentions { + const mentions = new Set(); + const channels = new Set(); + + for (const node of ast) { + traverse(node, mentions, channels); + } + + return { + mentions: Array.from(mentions), + channels: Array.from(channels), + }; +} diff --git a/apps/meteor/app/mentions/server/Mentions.ts b/apps/meteor/app/mentions/server/Mentions.ts index f6d40840b6488..15213c8318a3b 100644 --- a/apps/meteor/app/mentions/server/Mentions.ts +++ b/apps/meteor/app/mentions/server/Mentions.ts @@ -4,6 +4,7 @@ */ import { isE2EEMessage, type IMessage, type IRoom, type IUser } from '@rocket.chat/core-typings'; +import { extractMentionsFromMessageAST } from '../../lib/server/functions/extractMentionsFromMessageAST'; import { type MentionsParserArgs, MentionsParser } from '../lib/MentionsParser'; type MentionsServerArgs = MentionsParserArgs & { @@ -50,13 +51,25 @@ export class MentionsServer extends MentionsParser { isE2EEMessage(message) && e2eMentions?.e2eUserMentions && e2eMentions?.e2eUserMentions.length > 0 ? e2eMentions?.e2eUserMentions : this.getUserMentions(msg); + + return this.convertMentionsToUsers(mentions, rid, sender); + } + + async convertMentionsToUsers(mentions: string[], rid: string, sender: IMessage['u']): Promise { const mentionsAll: { _id: string; username: string }[] = []; - const userMentions = []; + const userMentions = new Set(); for await (const m of mentions) { - const mention = m.includes(':') ? m.trim() : m.trim().substring(1); + let mention: string; + if (m.includes(':')) { + mention = m.trim(); + } else if (m.startsWith('@')) { + mention = m.substring(1); + } else { + mention = m; + } if (mention !== 'all' && mention !== 'here') { - userMentions.push(mention); + userMentions.add(mention); continue; } if (this.messageMaxAll() > 0 && (await this.getTotalChannelMembers(rid)) > this.messageMaxAll()) { @@ -69,7 +82,7 @@ export class MentionsServer extends MentionsParser { }); } - return [...mentionsAll, ...(userMentions.length ? await this.getUsers(userMentions) : [])]; + return [...mentionsAll, ...(userMentions.size ? await this.getUsers(Array.from(userMentions)) : [])]; } async getChannelbyMentions(message: IMessage) { @@ -79,15 +92,23 @@ export class MentionsServer extends MentionsParser { isE2EEMessage(message) && e2eMentions?.e2eChannelMentions && e2eMentions?.e2eChannelMentions.length > 0 ? e2eMentions?.e2eChannelMentions : this.getChannelMentions(msg); - return this.getChannels(channels.map((c) => c.trim().substring(1))); + return this.convertMentionsToChannels(channels); + } + + async convertMentionsToChannels(channels: string[]): Promise[]> { + return this.getChannels(channels.map((c) => (c.startsWith('#') ? c.substring(1) : c))); } async execute(message: IMessage) { - const mentionsAll = await this.getUsersByMentions(message); - const channels = await this.getChannelbyMentions(message); + if (message.md) { + const { mentions, channels } = extractMentionsFromMessageAST(message.md); + message.mentions = await this.convertMentionsToUsers(mentions, message.rid, message.u); + message.channels = await this.convertMentionsToChannels(channels); + return message; + } - message.mentions = mentionsAll; - message.channels = channels; + message.mentions = await this.getUsersByMentions(message); + message.channels = await this.getChannelbyMentions(message); return message; } diff --git a/apps/meteor/server/services/messages/service.ts b/apps/meteor/server/services/messages/service.ts index 3298f953ee671..998718cf71d1a 100644 --- a/apps/meteor/server/services/messages/service.ts +++ b/apps/meteor/server/services/messages/service.ts @@ -234,10 +234,10 @@ export class MessageService extends ServiceClassInternal implements IMessageServ throw new FederationMatrixInvalidConfigurationError('Unable to send message'); } - message = await mentionServer.execute(message); message = await this.cannedResponse.replacePlaceholders({ message, room, user }); message = await this.badWords.filterBadWords({ message }); message = await this.markdownParser.parseMarkdown({ message, config: this.getMarkdownConfig() }); + message = await mentionServer.execute(message); if (parseUrls) { message.urls = parseUrlsInMessage(message, previewUrls); } diff --git a/apps/meteor/tests/unit/app/lib/server/functions/extractMentionsFromMessageAST.spec.ts b/apps/meteor/tests/unit/app/lib/server/functions/extractMentionsFromMessageAST.spec.ts new file mode 100644 index 0000000000000..5d4177a0f77ad --- /dev/null +++ b/apps/meteor/tests/unit/app/lib/server/functions/extractMentionsFromMessageAST.spec.ts @@ -0,0 +1,396 @@ +import type { Root } from '@rocket.chat/message-parser'; +import { expect } from 'chai'; + +import { extractMentionsFromMessageAST } from '../../../../../../app/lib/server/functions/extractMentionsFromMessageAST'; + +describe('extractMentionsFromMessageAST', () => { + it('should return empty arrays when AST has no mentions', () => { + const ast: Root = [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'Hello world', + }, + ], + }, + ]; + + const result = extractMentionsFromMessageAST(ast); + + expect(result.mentions).to.be.an('array').that.is.empty; + expect(result.channels).to.be.an('array').that.is.empty; + }); + + it('should extract user mentions from AST', () => { + const ast: Root = [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'Hello ', + }, + { + type: 'MENTION_USER', + value: { + type: 'PLAIN_TEXT', + value: 'john.doe', + }, + }, + ], + }, + ]; + + const result = extractMentionsFromMessageAST(ast); + + expect(result.mentions).to.deep.equal(['john.doe']); + expect(result.channels).to.be.an('array').that.is.empty; + }); + + it('should extract channel mentions from AST', () => { + const ast: Root = [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'PLAIN_TEXT', + value: 'Check ', + }, + { + type: 'MENTION_CHANNEL', + value: { + type: 'PLAIN_TEXT', + value: 'general', + }, + }, + ], + }, + ]; + + const result = extractMentionsFromMessageAST(ast); + + expect(result.mentions).to.be.an('array').that.is.empty; + expect(result.channels).to.deep.equal(['general']); + }); + + it('should extract both user and channel mentions', () => { + const ast: Root = [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'MENTION_USER', + value: { + type: 'PLAIN_TEXT', + value: 'admin', + }, + }, + { + type: 'PLAIN_TEXT', + value: ' please check ', + }, + { + type: 'MENTION_CHANNEL', + value: { + type: 'PLAIN_TEXT', + value: 'support', + }, + }, + ], + }, + ]; + + const result = extractMentionsFromMessageAST(ast); + + expect(result.mentions).to.deep.equal(['admin']); + expect(result.channels).to.deep.equal(['support']); + }); + + it('should extract multiple user mentions', () => { + const ast: Root = [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'MENTION_USER', + value: { + type: 'PLAIN_TEXT', + value: 'user1', + }, + }, + { + type: 'PLAIN_TEXT', + value: ' and ', + }, + { + type: 'MENTION_USER', + value: { + type: 'PLAIN_TEXT', + value: 'user2', + }, + }, + ], + }, + ]; + + const result = extractMentionsFromMessageAST(ast); + + expect(result.mentions).to.have.members(['user1', 'user2']); + expect(result.mentions).to.have.lengthOf(2); + }); + + it('should deduplicate repeated mentions', () => { + const ast: Root = [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'MENTION_USER', + value: { + type: 'PLAIN_TEXT', + value: 'admin', + }, + }, + { + type: 'PLAIN_TEXT', + value: ' hello ', + }, + { + type: 'MENTION_USER', + value: { + type: 'PLAIN_TEXT', + value: 'admin', + }, + }, + ], + }, + ]; + + const result = extractMentionsFromMessageAST(ast); + + expect(result.mentions).to.deep.equal(['admin']); + }); + + it('should extract mentions from nested structures like blockquotes', () => { + const ast: Root = [ + { + type: 'QUOTE', + value: [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'MENTION_USER', + value: { + type: 'PLAIN_TEXT', + value: 'quoted.user', + }, + }, + ], + }, + ], + }, + ]; + + const result = extractMentionsFromMessageAST(ast); + + expect(result.mentions).to.deep.equal(['quoted.user']); + }); + + it('should extract mentions from bold text', () => { + const ast: Root = [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'BOLD', + value: [ + { + type: 'MENTION_USER', + value: { + type: 'PLAIN_TEXT', + value: 'bold.user', + }, + }, + ], + }, + ], + }, + ]; + + const result = extractMentionsFromMessageAST(ast); + + expect(result.mentions).to.deep.equal(['bold.user']); + }); + + it('should extract mentions from italic text', () => { + const ast: Root = [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'ITALIC', + value: [ + { + type: 'MENTION_CHANNEL', + value: { + type: 'PLAIN_TEXT', + value: 'italic-channel', + }, + }, + ], + }, + ], + }, + ]; + + const result = extractMentionsFromMessageAST(ast); + + expect(result.channels).to.deep.equal(['italic-channel']); + }); + + it('should handle special mentions like all and here', () => { + const ast: Root = [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'MENTION_USER', + value: { + type: 'PLAIN_TEXT', + value: 'all', + }, + }, + { + type: 'PLAIN_TEXT', + value: ' and ', + }, + { + type: 'MENTION_USER', + value: { + type: 'PLAIN_TEXT', + value: 'here', + }, + }, + ], + }, + ]; + + const result = extractMentionsFromMessageAST(ast); + + expect(result.mentions).to.have.members(['all', 'here']); + }); + + it('should handle empty AST', () => { + const ast: Root = []; + + const result = extractMentionsFromMessageAST(ast); + + expect(result.mentions).to.be.an('array').that.is.empty; + expect(result.channels).to.be.an('array').that.is.empty; + }); + + it('should extract mentions from list items', () => { + const ast: Root = [ + { + type: 'UNORDERED_LIST', + value: [ + { + type: 'LIST_ITEM', + value: [ + { + type: 'MENTION_USER', + value: { + type: 'PLAIN_TEXT', + value: 'list.user', + }, + }, + ], + }, + ], + }, + ]; + + const result = extractMentionsFromMessageAST(ast); + + expect(result.mentions).to.deep.equal(['list.user']); + }); + + it('should extract mentions from tasks', () => { + const ast: Root = [ + { + type: 'TASKS', + value: [ + { + type: 'TASK', + status: false, + value: [ + { + type: 'MENTION_USER', + value: { + type: 'PLAIN_TEXT', + value: 'task.assignee', + }, + }, + ], + }, + ], + }, + ]; + + const result = extractMentionsFromMessageAST(ast); + + expect(result.mentions).to.deep.equal(['task.assignee']); + }); + + it('should handle BigEmoji AST structure', () => { + const ast: Root = [ + { + type: 'BIG_EMOJI', + value: [ + { + type: 'EMOJI', + value: { + type: 'PLAIN_TEXT', + value: 'smile', + }, + shortCode: ':smile:', + }, + ], + }, + ]; + + const result = extractMentionsFromMessageAST(ast); + + expect(result.mentions).to.be.an('array').that.is.empty; + expect(result.channels).to.be.an('array').that.is.empty; + }); + + it('should extract mentions from spoiler blocks', () => { + const ast: Root = [ + { + type: 'SPOILER_BLOCK', + value: [ + { + type: 'PARAGRAPH', + value: [ + { + type: 'MENTION_USER', + value: { + type: 'PLAIN_TEXT', + value: 'hidden.user', + }, + }, + ], + }, + ], + }, + ]; + + const result = extractMentionsFromMessageAST(ast); + + expect(result.mentions).to.deep.equal(['hidden.user']); + }); +}); From f661fbc11d1ab832804daf70be76622b96ae8be7 Mon Sep 17 00:00:00 2001 From: Ricardo Garim Date: Fri, 20 Feb 2026 16:06:02 -0300 Subject: [PATCH 002/108] chore: remove bodyParams-override from API router (#38580) --- apps/meteor/app/api/server/router.ts | 9 +- .../meteor/app/integrations/server/api/api.ts | 90 +++++++++---------- 2 files changed, 47 insertions(+), 52 deletions(-) diff --git a/apps/meteor/app/api/server/router.ts b/apps/meteor/app/api/server/router.ts index 41ca09d4f1a32..583976cb7efc4 100644 --- a/apps/meteor/app/api/server/router.ts +++ b/apps/meteor/app/api/server/router.ts @@ -9,10 +9,9 @@ import type { TypedOptions } from './definition'; type HonoContext = Context<{ Bindings: { incoming: IncomingMessage }; Variables: { - 'remoteAddress': string; - 'bodyParams': Record; - 'bodyParams-override': Record | undefined; - 'queryParams': Record; + remoteAddress: string; + bodyParams: Record; + queryParams: Record; }; }>; @@ -46,7 +45,7 @@ export class RocketChatAPIRouter< requestIp: c.get('remoteAddress'), urlParams: req.param(), queryParams: c.get('queryParams'), - bodyParams: c.get('bodyParams-override') || c.get('bodyParams'), + bodyParams: c.get('bodyParams'), request, path: req.path, response: res, diff --git a/apps/meteor/app/integrations/server/api/api.ts b/apps/meteor/app/integrations/server/api/api.ts index 7e72862f70588..f89152865bc83 100644 --- a/apps/meteor/app/integrations/server/api/api.ts +++ b/apps/meteor/app/integrations/server/api/api.ts @@ -3,7 +3,6 @@ import { Integrations, Users } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; import { isIntegrationsHooksAddSchema, isIntegrationsHooksRemoveSchema } from '@rocket.chat/rest-typings'; import type express from 'express'; -import type { Context, Next } from 'hono'; import { Meteor } from 'meteor/meteor'; import type { RateLimiterOptionsToCheck } from 'meteor/rate-limit'; import { WebApp } from 'meteor/webapp'; @@ -118,6 +117,42 @@ async function removeIntegration(options: { target_url: string }, user: IUser): return API.v1.success(); } +/** + * Slack/GitHub-style webhooks send JSON wrapped in a `payload` field + * with Content-Type: application/x-www-form-urlencoded (e.g. `payload={"text":"hello"}`). + * This function unwraps it so integrations receive the parsed JSON directly. + */ +function getBodyParams(bodyParams: unknown, request: Request): Record { + if (!isPlainObject(bodyParams)) { + return {}; + } + + if ( + request.headers.get('content-type')?.startsWith('application/x-www-form-urlencoded') && + Object.keys(bodyParams).length === 1 && + typeof bodyParams.payload === 'string' + ) { + try { + const parsed = JSON.parse(bodyParams.payload); + + // Valid JSON must be an object, not an array or primitive + if (!isPlainObject(parsed)) { + throw new Error('Integration payload must be a JSON object, not an array or primitive'); + } + + return parsed; + } catch (err) { + // Invalid JSON -> return original bodyParams (backward compatibility) + if (err instanceof SyntaxError) { + return bodyParams; + } + throw err; + } + } + + return bodyParams; +} + async function executeIntegrationRest( this: IntegrationThis, ): Promise< @@ -142,7 +177,13 @@ async function executeIntegrationRest( const scriptEngine = getEngine(this.request.integration); - let bodyParams = isPlainObject(this.bodyParams) ? this.bodyParams : {}; + let bodyParams: Record; + try { + bodyParams = getBodyParams(this.bodyParams, this.request); + } catch (err) { + return API.v1.failure(err instanceof Error ? err.message : String(err)); + } + const separateResponse = bodyParams.separateResponse === true; let scriptResponse: Record | undefined; @@ -388,51 +429,6 @@ Api.router .use(metricsMiddleware({ basePathRegex: new RegExp(/^\/hooks\//), api: Api, settings, summary: metrics.rocketchatRestApi })) .use(tracerSpanMiddleware); -const middleware = async (c: Context, next: Next): Promise => { - const { req } = c; - if (req.raw.headers.get('content-type') !== 'application/x-www-form-urlencoded') { - return next(); - } - - try { - const content = await req.raw.clone().text(); - const body = Object.fromEntries(new URLSearchParams(content)); - if (!body || typeof body !== 'object' || Object.keys(body).length !== 1) { - return next(); - } - - /** - * Slack/GitHub-style webhooks send JSON wrapped in a `payload` field with - * Content-Type: application/x-www-form-urlencoded (e.g. `payload={"text":"hello"}`). - * We unwrap it here so integrations receive the parsed JSON directly. - * - * Note: These webhooks only send the `payload` field with no additional form - * parameters, so we simply replace bodyParams with the parsed JSON. - */ - if (body.payload) { - if (typeof body.payload === 'string') { - try { - c.set('bodyParams-override', JSON.parse(body.payload)); - } catch { - // Keep original without unwrapping - } - } - return next(); - } - - incomingLogger.debug({ - msg: 'Body received as application/x-www-form-urlencoded without the "payload" key, parsed as string', - content, - }); - } catch (e: any) { - c.body(JSON.stringify({ success: false, error: e.message }), 400); - } - - return next(); -}; - -Api.router.use(middleware); - Api.addRoute( ':integrationId/:userId/:token', { authRequired: true }, From 5c49872ded00866d17b6e17b8f405d06064584bf Mon Sep 17 00:00:00 2001 From: Chetan Agarwal Date: Sat, 21 Feb 2026 02:00:52 +0530 Subject: [PATCH 003/108] chore(media-call-history): fix type assertions `as any` casts in `MediaCallHistoryContextualbar` (#38854) --- .../views/mediaCallHistory/MediaCallHistoryContextualbar.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/meteor/client/views/mediaCallHistory/MediaCallHistoryContextualbar.tsx b/apps/meteor/client/views/mediaCallHistory/MediaCallHistoryContextualbar.tsx index ab849910ad7d8..fd5e5b1985012 100644 --- a/apps/meteor/client/views/mediaCallHistory/MediaCallHistoryContextualbar.tsx +++ b/apps/meteor/client/views/mediaCallHistory/MediaCallHistoryContextualbar.tsx @@ -39,10 +39,10 @@ const MediaCallHistoryContextualbar = ({ queryKey: callHistoryQueryKeys.info(callId || historyId), queryFn: async () => { if (callId) { - return getCallHistory({ callId } as any); // TODO fix this type + return getCallHistory({ callId }); } if (historyId) { - return getCallHistory({ historyId } as any); // TODO fix this type + return getCallHistory({ historyId }); } throw new Error('Call ID or history ID is required'); }, From a0285d169ac4c19796c9f15867383b1bb72e6b43 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 17:55:07 -0300 Subject: [PATCH 004/108] fix: DOMPurify hook memory leak in MarkdownText (#38834) Co-authored-by: Khizarshah01 <5263975+Khizarshah01@users.noreply.github.com> Co-authored-by: Guilherme Gazzo --- .../client/components/MarkdownText.spec.tsx | 35 ++++++++ .../meteor/client/components/MarkdownText.tsx | 87 +++++++++++-------- 2 files changed, 86 insertions(+), 36 deletions(-) diff --git a/apps/meteor/client/components/MarkdownText.spec.tsx b/apps/meteor/client/components/MarkdownText.spec.tsx index a7b9da94b84e3..1ed832ee02a45 100644 --- a/apps/meteor/client/components/MarkdownText.spec.tsx +++ b/apps/meteor/client/components/MarkdownText.spec.tsx @@ -1,5 +1,6 @@ import { mockAppRoot } from '@rocket.chat/mock-providers'; import { render, screen } from '@testing-library/react'; +import dompurify from 'dompurify'; import MarkdownText, { supportedURISchemes } from './MarkdownText'; @@ -432,3 +433,37 @@ describe('code handling', () => { expect(screen.getByRole('code').outerHTML).toEqual(expected); }); }); + +describe('DOMPurify hook registration', () => { + it('should register hook only once at module level', () => { + // Import the module to trigger hook registration + + const addHookSpy = jest.spyOn(dompurify, 'addHook'); + + // Clear any previous calls from module initialization + addHookSpy.mockClear(); + + const { rerender, unmount } = render(, { + wrapper: mockAppRoot().build(), + }); + + // Hook should NOT be registered during component render (it's registered at module level) + expect(addHookSpy).toHaveBeenCalledTimes(0); + + // Re-rendering with different props should not register hook again + rerender(); + expect(addHookSpy).toHaveBeenCalledTimes(0); + + // Rendering another instance should not register hook again + render(, { + wrapper: mockAppRoot().build(), + }); + expect(addHookSpy).toHaveBeenCalledTimes(0); + + // Unmounting should not affect the module-level hook + unmount(); + expect(addHookSpy).toHaveBeenCalledTimes(0); + + addHookSpy.mockRestore(); + }); +}); diff --git a/apps/meteor/client/components/MarkdownText.tsx b/apps/meteor/client/components/MarkdownText.tsx index 8503d5ee7ed5f..c490ae5e77b48 100644 --- a/apps/meteor/client/components/MarkdownText.tsx +++ b/apps/meteor/client/components/MarkdownText.tsx @@ -101,6 +101,49 @@ type MarkdownTextProps = Partial; export const supportedURISchemes = ['http', 'https', 'notes', 'ftp', 'ftps', 'tel', 'mailto', 'sms', 'cid']; +const isElement = (node: Node): node is Element => node.nodeType === Node.ELEMENT_NODE; +const isLinkElement = (node: Node): node is HTMLAnchorElement => isElement(node) && node.tagName.toLowerCase() === 'a'; + +// Generate a unique token at runtime to prevent enumeration attacks +// This token marks internal links that need translation +const INTERNAL_LINK_TOKEN = `__INTERNAL_LINK_TITLE_${Math.random().toString(36).substring(2, 15)}__`; + +// Register the DOMPurify hook once at module level to prevent memory leaks +// This hook will be shared by all MarkdownText component instances +dompurify.addHook('afterSanitizeAttributes', (node) => { + if (!isLinkElement(node)) { + return; + } + + const href = node.getAttribute('href') || ''; + const isExternalLink = isExternal(href); + const isMailto = href.startsWith('mailto:'); + + // Set appropriate attributes based on link type + if (isExternalLink || isMailto) { + node.setAttribute('rel', 'nofollow noopener noreferrer'); + // Enforcing external links to open in new tabs is critical to assure users never navigate away from the chat + // This attribute must be preserved to guarantee users maintain their chat context + node.setAttribute('target', '_blank'); + } + + // Set appropriate title based on link type + if (isMailto) { + // For mailto links, use the email address as the title for better user experience + // Example: for href "mailto:user@example.com" the title would be "mailto:user@example.com" + node.setAttribute('title', href); + } else if (isExternalLink) { + // For external links, set an empty title to prevent tooltips + // This reduces visual clutter and lets users see the URL in the browser's status bar instead + node.setAttribute('title', ''); + } else { + // For internal links, use a token that will be replaced with translated text in the component + // This allows us to use the contextualized translation function + const relativePath = href.replace(getBaseURI(), ''); + node.setAttribute('title', `${INTERNAL_LINK_TOKEN}${relativePath}`); + } +}); + const MarkdownText = ({ content, variant = 'document', @@ -143,41 +186,16 @@ const MarkdownText = ({ } })(); - // Add a hook to make all external links open a new window - dompurify.addHook('afterSanitizeAttributes', (node) => { - if (!isLinkElement(node)) { - return; - } + const sanitizedHtml = preserveHtml + ? html + : html && sanitizer(html, { ADD_ATTR: ['target'], ALLOWED_URI_REGEXP: getRegexp(supportedURISchemes) }); - const href = node.getAttribute('href') || ''; - const isExternalLink = isExternal(href); - const isMailto = href.startsWith('mailto:'); + // Replace internal link tokens with contextualized translations + if (sanitizedHtml && typeof sanitizedHtml === 'string') { + return sanitizedHtml.replace(new RegExp(`${INTERNAL_LINK_TOKEN}([^"]*)`, 'g'), (_, href) => t('Go_to_href', { href })); + } - // Set appropriate attributes based on link type - if (isExternalLink || isMailto) { - node.setAttribute('rel', 'nofollow noopener noreferrer'); - // Enforcing external links to open in new tabs is critical to assure users never navigate away from the chat - // This attribute must be preserved to guarantee users maintain their chat context - node.setAttribute('target', '_blank'); - } - - // Set appropriate title based on link type - if (isMailto) { - // For mailto links, use the email address as the title for better user experience - // Example: for href "mailto:user@example.com" the title would be "mailto:user@example.com" - node.setAttribute('title', href); - } else if (isExternalLink) { - // For external links, set an empty title to prevent tooltips - // This reduces visual clutter and lets users see the URL in the browser's status bar instead - node.setAttribute('title', ''); - } else { - // For internal links, add a translated title with the relative path - // Example: for href "https://my-server.rocket.chat/channel/general" the title would be "Go to #general" - node.setAttribute('title', `${t('Go_to_href', { href: href.replace(getBaseURI(), '') })}`); - } - }); - - return preserveHtml ? html : html && sanitizer(html, { ADD_ATTR: ['target'], ALLOWED_URI_REGEXP: getRegexp(supportedURISchemes) }); + return sanitizedHtml; }, [preserveHtml, sanitizer, content, variant, markedOptions, parseEmoji, t]); return __html ? ( @@ -190,7 +208,4 @@ const MarkdownText = ({ ) : null; }; -const isElement = (node: Node): node is Element => node.nodeType === Node.ELEMENT_NODE; -const isLinkElement = (node: Node): node is HTMLAnchorElement => isElement(node) && node.tagName.toLowerCase() === 'a'; - export default MarkdownText; From 7d23911cf4cca1341a9c5397f6cc1ce508baa69d Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Fri, 20 Feb 2026 16:06:19 -0600 Subject: [PATCH 005/108] fix: `Production=false` not being respected by `apn` configuration (#38852) --- .changeset/tame-humans-greet.md | 5 + apps/meteor/.mocharc.js | 1 + apps/meteor/app/push/server/apn.spec.ts | 131 ++++++++++++++++++++++++ apps/meteor/app/push/server/apn.ts | 5 +- 4 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 .changeset/tame-humans-greet.md create mode 100644 apps/meteor/app/push/server/apn.spec.ts diff --git a/.changeset/tame-humans-greet.md b/.changeset/tame-humans-greet.md new file mode 100644 index 0000000000000..e5b0aa45eece6 --- /dev/null +++ b/.changeset/tame-humans-greet.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Fixes an issue where `Production` flag was not being respected when initializing Push Notifications configuration diff --git a/apps/meteor/.mocharc.js b/apps/meteor/.mocharc.js index 2ec06b5257b65..11d408b22a601 100644 --- a/apps/meteor/.mocharc.js +++ b/apps/meteor/.mocharc.js @@ -30,6 +30,7 @@ module.exports = { 'app/file-upload/server/**/*.spec.ts', 'app/statistics/server/**/*.spec.ts', 'app/livechat/server/lib/**/*.spec.ts', + 'app/push/server/**/*.spec.ts', 'app/utils/server/**/*.spec.ts', ], }; diff --git a/apps/meteor/app/push/server/apn.spec.ts b/apps/meteor/app/push/server/apn.spec.ts new file mode 100644 index 0000000000000..583eaca2ffd69 --- /dev/null +++ b/apps/meteor/app/push/server/apn.spec.ts @@ -0,0 +1,131 @@ +import { expect } from 'chai'; +import { describe, it, beforeEach } from 'mocha'; +import proxyquire from 'proxyquire'; +import sinon from 'sinon'; + +const sandbox = sinon.createSandbox(); + +const mocks = { + logger: { + debug: sandbox.stub(), + warn: sandbox.stub(), + error: sandbox.stub(), + }, + ApnProvider: sandbox.stub(), +}; + +const apnMock = { + 'Provider': mocks.ApnProvider, + 'Notification': sandbox.stub(), + '@noCallThru': true, +}; + +const { initAPN } = proxyquire.noCallThru().load('./apn', { + '@parse/node-apn': { + default: apnMock, + ...apnMock, + }, + './logger': { logger: mocks.logger }, +}); + +const baseOptions = { + apn: { + cert: 'cert-data', + key: 'key-data', + gateway: undefined as string | undefined, + }, + production: false, +}; + +const buildOptions = (overrides: Record = {}, apnOverrides: Record = {}) => ({ + ...baseOptions, + ...overrides, + apn: { + ...baseOptions.apn, + ...apnOverrides, + }, +}); + +describe('initAPN', () => { + beforeEach(() => { + sandbox.resetHistory(); + mocks.ApnProvider.reset(); + }); + + describe('APN provider initialization', () => { + it('should create apn.Provider with correct options', () => { + const options = buildOptions({ production: true }, { gateway: 'gateway.push.apple.com' }); + + initAPN({ + options, + absoluteUrl: 'https://example.com', + }); + + expect(mocks.ApnProvider.calledWithNew()).to.be.true; + }); + + it('should pass production flag from options to Provider', () => { + initAPN({ + options: buildOptions({ production: true }), + absoluteUrl: 'https://example.com', + }); + + expect(mocks.ApnProvider.firstCall.args[0]).to.have.property('production', true); + }); + + it('should pass production false when options.production is false', () => { + initAPN({ + options: buildOptions({ production: false }), + absoluteUrl: 'https://example.com', + }); + + expect(mocks.ApnProvider.firstCall.args[0]).to.have.property('production', false); + }); + + it('should pass cert and key to Provider', () => { + initAPN({ + options: buildOptions({}, { cert: 'my-cert', key: 'my-key' }), + absoluteUrl: 'https://example.com', + }); + + const providerArgs = mocks.ApnProvider.firstCall.args[0]; + expect(providerArgs).to.have.property('cert', 'my-cert'); + expect(providerArgs).to.have.property('key', 'my-key'); + }); + + it('should pass gateway to Provider when specified', () => { + initAPN({ + options: buildOptions({}, { gateway: 'gateway.push.apple.com' }), + absoluteUrl: 'https://example.com', + }); + + expect(mocks.ApnProvider.firstCall.args[0]).to.have.property('gateway', 'gateway.push.apple.com'); + }); + + it('should spread all apn options to Provider', () => { + initAPN({ + options: buildOptions({ production: true }, { cert: 'c', key: 'k', gateway: 'gateway.sandbox.push.apple.com' }), + absoluteUrl: 'https://example.com', + }); + + const providerArgs = mocks.ApnProvider.firstCall.args[0]; + expect(providerArgs).to.deep.equal({ + cert: 'c', + key: 'k', + gateway: 'gateway.sandbox.push.apple.com', + production: true, + }); + }); + + it('should not throw when Provider constructor throws', () => { + mocks.ApnProvider.throws(new Error('APN init failed')); + + expect(() => + initAPN({ + options: buildOptions(), + absoluteUrl: 'https://example.com', + }), + ).to.not.throw(); + }); + }); +}); diff --git a/apps/meteor/app/push/server/apn.ts b/apps/meteor/app/push/server/apn.ts index 634665cad9403..c03b93e9b635a 100644 --- a/apps/meteor/app/push/server/apn.ts +++ b/apps/meteor/app/push/server/apn.ts @@ -137,7 +137,10 @@ export const initAPN = ({ options, absoluteUrl }: { options: RequiredField Date: Sat, 21 Feb 2026 00:30:54 -0300 Subject: [PATCH 006/108] chore: Make this.user optional instead of nullable for non-auth endpoints (#38861) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ggazzo <5263975+ggazzo@users.noreply.github.com> Co-authored-by: Guilherme Gazzo Co-authored-by: Guilherme Gazzo --- apps/meteor/app/api/server/ApiClass.ts | 23 ++++++------- apps/meteor/app/api/server/definition.ts | 13 ++++--- .../meteor/app/integrations/server/api/api.ts | 2 ++ apps/meteor/tests/end-to-end/api/users.ts | 34 +++++++++++-------- 4 files changed, 41 insertions(+), 31 deletions(-) diff --git a/apps/meteor/app/api/server/ApiClass.ts b/apps/meteor/app/api/server/ApiClass.ts index cf7cf77ac8c1f..50b31989cd8fd 100644 --- a/apps/meteor/app/api/server/ApiClass.ts +++ b/apps/meteor/app/api/server/ApiClass.ts @@ -825,18 +825,17 @@ export class APIClass Meteor.callAsync('login', args)); - this.user = await Users.findOne( + const user = await Users.findOne( { _id: auth.id, }, @@ -1098,18 +1097,16 @@ export class APIClass = { readonly logger: Logger; userId: TOptions['authRequired'] extends true ? string : string | undefined; - user: TOptions['authRequired'] extends true ? IUser : IUser | null; token: TOptions['authRequired'] extends true ? string : string | undefined; queryParams: TOptions['query'] extends ValidateFunction ? Query : never; urlParams: UrlParams extends Record ? UrlParams : never; @@ -317,7 +316,13 @@ export type TypedThis requestIp?: string; route: string; response: Response; -}; +} & (TOptions['authRequired'] extends true + ? { + user: IUser; + } + : { + user?: IUser; + }); type PromiseOrValue = T | Promise; diff --git a/apps/meteor/app/integrations/server/api/api.ts b/apps/meteor/app/integrations/server/api/api.ts index f89152865bc83..141a0a4fedb61 100644 --- a/apps/meteor/app/integrations/server/api/api.ts +++ b/apps/meteor/app/integrations/server/api/api.ts @@ -369,6 +369,8 @@ class WebHookAPI extends APIClass<'/hooks'> { throw new Error('Invalid integration id or token provided.'); } + routeContext.request.headers.set('x-auth-token', token); + routeContext.request.integration = integration; return Users.findOneById(routeContext.request.integration.userId); diff --git a/apps/meteor/tests/end-to-end/api/users.ts b/apps/meteor/tests/end-to-end/api/users.ts index c377f977e8716..b4231f6958876 100644 --- a/apps/meteor/tests/end-to-end/api/users.ts +++ b/apps/meteor/tests/end-to-end/api/users.ts @@ -154,14 +154,17 @@ const registerUser = async ( name?: string; pass?: string; } = {}, - overrideCredentials = credentials, + overrideCredentials: Credentials | null = credentials, ) => { const username = userData.username || `user.test.${Date.now()}`; const email = userData.email || `${username}@rocket.chat`; - const result = await request - .post(api('users.register')) - .set(overrideCredentials) - .send({ email, name: username, username, pass: password, ...userData }); + + const req = request.post(api('users.register')); + + if (overrideCredentials) { + req.set(overrideCredentials); + } + const result = await req.send({ email, name: username, username, pass: password, ...userData }); return result.body.user; }; @@ -3358,12 +3361,15 @@ describe('[Users]', () => { let userCredentials: Credentials; before(async () => { - targetUser = await registerUser({ - email: `${testUsername}.@test.com`, - username: `${testUsername}test`, - name: testUsername, - pass: password, - }); + targetUser = await registerUser( + { + email: `${testUsername}.@test.com`, + username: `${testUsername}test`, + name: testUsername, + pass: password, + }, + null, + ); userCredentials = await login(targetUser.username, password); }); @@ -3388,7 +3394,7 @@ describe('[Users]', () => { let userCredentials: Credentials; before(async () => { - targetUser = await registerUser(); + targetUser = await registerUser(undefined, null); userCredentials = await login(targetUser.username, password); }); @@ -3456,7 +3462,7 @@ describe('[Users]', () => { let userCredentials: Credentials; before(async () => { - targetUser = await registerUser(); + targetUser = await registerUser(undefined, null); userCredentials = await login(targetUser.username, password); }); @@ -3634,7 +3640,7 @@ describe('[Users]', () => { let targetUser: TestUser; let room: IRoom; beforeEach(async () => { - targetUser = await registerUser(); + targetUser = await registerUser(undefined, null); room = ( await createRoom({ type: 'c', From 27448fe2498addd319e5353a47f3967e6aee3eb5 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Feb 2026 01:15:37 -0300 Subject: [PATCH 007/108] chore: replace getLoggedInUser with this.user in API endpoints (#38858) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ggazzo <5263975+ggazzo@users.noreply.github.com> --- apps/meteor/app/api/server/default/info.ts | 4 +--- .../app/api/server/helpers/getLoggedInUser.ts | 13 ------------- apps/meteor/app/api/server/index.ts | 1 - apps/meteor/app/api/server/v1/channels.ts | 5 +---- apps/meteor/app/api/server/v1/commands.ts | 4 +--- apps/meteor/app/api/server/v1/groups.ts | 8 +------- apps/meteor/app/api/server/v1/misc.ts | 3 +-- 7 files changed, 5 insertions(+), 33 deletions(-) delete mode 100644 apps/meteor/app/api/server/helpers/getLoggedInUser.ts diff --git a/apps/meteor/app/api/server/default/info.ts b/apps/meteor/app/api/server/default/info.ts index 9f2a9d79e4a54..173a733d13b47 100644 --- a/apps/meteor/app/api/server/default/info.ts +++ b/apps/meteor/app/api/server/default/info.ts @@ -1,5 +1,4 @@ import { API } from '../api'; -import { getLoggedInUser } from '../helpers/getLoggedInUser'; import { getServerInfo } from '../lib/getServerInfo'; API.default.addRoute( @@ -7,8 +6,7 @@ API.default.addRoute( { authRequired: false }, { async get() { - const user = await getLoggedInUser(this.request); - return API.v1.success(await getServerInfo(user?._id)); + return API.v1.success(await getServerInfo(this.userId)); }, }, ); diff --git a/apps/meteor/app/api/server/helpers/getLoggedInUser.ts b/apps/meteor/app/api/server/helpers/getLoggedInUser.ts deleted file mode 100644 index d3fc562eeb20f..0000000000000 --- a/apps/meteor/app/api/server/helpers/getLoggedInUser.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { IUser } from '@rocket.chat/core-typings'; -import { Users } from '@rocket.chat/models'; -import { Accounts } from 'meteor/accounts-base'; - -export async function getLoggedInUser(request: Request): Promise | null> { - const token = request.headers.get('x-auth-token'); - const userId = request.headers.get('x-user-id'); - if (!token || !userId || typeof token !== 'string' || typeof userId !== 'string') { - return null; - } - - return Users.findOneByIdAndLoginToken(userId, Accounts._hashLoginToken(token), { projection: { username: 1 } }); -} diff --git a/apps/meteor/app/api/server/index.ts b/apps/meteor/app/api/server/index.ts index 59986d6e2da87..176141af83e08 100644 --- a/apps/meteor/app/api/server/index.ts +++ b/apps/meteor/app/api/server/index.ts @@ -1,6 +1,5 @@ import './ajv'; import './helpers/composeRoomWithLastMessage'; -import './helpers/getLoggedInUser'; import './helpers/getPaginationItems'; import './helpers/getUserFromParams'; import './helpers/getUserInfo'; diff --git a/apps/meteor/app/api/server/v1/channels.ts b/apps/meteor/app/api/server/v1/channels.ts index 0aa24f5097b04..5dfdc22c237ee 100644 --- a/apps/meteor/app/api/server/v1/channels.ts +++ b/apps/meteor/app/api/server/v1/channels.ts @@ -54,7 +54,6 @@ import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMes import { API } from '../api'; import { addUserToFileObj } from '../helpers/addUserToFileObj'; import { composeRoomWithLastMessage } from '../helpers/composeRoomWithLastMessage'; -import { getLoggedInUser } from '../helpers/getLoggedInUser'; import { getPaginationItems } from '../helpers/getPaginationItems'; import { getUserFromParams, getUserListFromParams } from '../helpers/getUserFromParams'; @@ -1147,9 +1146,7 @@ API.v1.addRoute( return API.v1.failure('Channel does not exists'); } - const user = await getLoggedInUser(this.request); - - if (!room || !user || !(await canAccessRoomAsync(room, user))) { + if (!(await canAccessRoomAsync(room, this.user))) { throw new Meteor.Error('error-not-allowed', 'Not Allowed'); } diff --git a/apps/meteor/app/api/server/v1/commands.ts b/apps/meteor/app/api/server/v1/commands.ts index 59b90baafa8a1..9347536cafd52 100644 --- a/apps/meteor/app/api/server/v1/commands.ts +++ b/apps/meteor/app/api/server/v1/commands.ts @@ -8,7 +8,6 @@ import { executeSlashCommandPreview } from '../../../lib/server/methods/executeS import { getSlashCommandPreviews } from '../../../lib/server/methods/getSlashCommandPreviews'; import { slashCommands } from '../../../utils/server/slashCommand'; import { API } from '../api'; -import { getLoggedInUser } from '../helpers/getLoggedInUser'; import { getPaginationItems } from '../helpers/getPaginationItems'; API.v1.addRoute( @@ -248,7 +247,6 @@ API.v1.addRoute( // Expects these query params: command: 'giphy', params: 'mine', roomId: 'value' async get() { const query = this.queryParams; - const user = await getLoggedInUser(this.request); if (typeof query.command !== 'string') { return API.v1.failure('You must provide a command to get the previews from.'); @@ -267,7 +265,7 @@ API.v1.addRoute( return API.v1.failure('The command provided does not exist (or is disabled).'); } - if (!(await canAccessRoomIdAsync(query.roomId, user?._id))) { + if (!(await canAccessRoomIdAsync(query.roomId, this.userId))) { return API.v1.forbidden(); } diff --git a/apps/meteor/app/api/server/v1/groups.ts b/apps/meteor/app/api/server/v1/groups.ts index 3fbe9c967a8e9..c91afe561ed5d 100644 --- a/apps/meteor/app/api/server/v1/groups.ts +++ b/apps/meteor/app/api/server/v1/groups.ts @@ -34,7 +34,6 @@ import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMes import { API } from '../api'; import { addUserToFileObj } from '../helpers/addUserToFileObj'; import { composeRoomWithLastMessage } from '../helpers/composeRoomWithLastMessage'; -import { getLoggedInUser } from '../helpers/getLoggedInUser'; import { getPaginationItems } from '../helpers/getPaginationItems'; import { getUserFromParams, getUserListFromParams } from '../helpers/getUserFromParams'; @@ -839,12 +838,7 @@ API.v1.addRoute( return API.v1.failure('Group does not exists'); } - const user = await getLoggedInUser(this.request); - if (!user) { - return API.v1.failure('User does not exists'); - } - - if (!(await canAccessRoomAsync(room, user))) { + if (!(await canAccessRoomAsync(room, this.user))) { throw new Meteor.Error('error-not-allowed', 'Not Allowed'); } diff --git a/apps/meteor/app/api/server/v1/misc.ts b/apps/meteor/app/api/server/v1/misc.ts index c679a42582924..852351a3c5ded 100644 --- a/apps/meteor/app/api/server/v1/misc.ts +++ b/apps/meteor/app/api/server/v1/misc.ts @@ -29,7 +29,6 @@ import { getBaseUserFields } from '../../../utils/server/functions/getBaseUserFi import { isSMTPConfigured } from '../../../utils/server/functions/isSMTPConfigured'; import { getURL } from '../../../utils/server/getURL'; import { API } from '../api'; -import { getLoggedInUser } from '../helpers/getLoggedInUser'; import { getPaginationItems } from '../helpers/getPaginationItems'; import { getUserFromParams } from '../helpers/getUserFromParams'; import { getUserInfo } from '../helpers/getUserInfo'; @@ -244,7 +243,7 @@ API.v1.addRoute( text = `#${channel}`; break; case 'user': - if (settings.get('API_Shield_user_require_auth') && !(await getLoggedInUser(this.request))) { + if (settings.get('API_Shield_user_require_auth') && !this.user) { return API.v1.failure('You must be logged in to do this.'); } const user = await getUserFromParams(this.queryParams); From 3145c41615e719204de518f87221e5b57dc02595 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Feb 2026 01:17:09 -0300 Subject: [PATCH 008/108] test: Add API test for users.register with authenticated credentials (#38864) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ggazzo --- apps/meteor/tests/end-to-end/api/users.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/apps/meteor/tests/end-to-end/api/users.ts b/apps/meteor/tests/end-to-end/api/users.ts index b4231f6958876..049429291babe 100644 --- a/apps/meteor/tests/end-to-end/api/users.ts +++ b/apps/meteor/tests/end-to-end/api/users.ts @@ -886,6 +886,25 @@ describe('[Users]', () => { }) .end(done); }); + + it('should return an error when logged in user tries to register', (done) => { + void request + .post(api('users.register')) + .set(credentials) + .send({ + email: `newuser${Date.now()}@email.com`, + name: 'New User', + username: `newuser${Date.now()}`, + pass: 'P@ssw0rd1234.!', + }) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body).to.have.property('error').and.to.be.equal('Logged in users can not register again.'); + }) + .end(done); + }); }); describe('[/users.info]', () => { From dad0dba81eb2ea1aea4e1efff46b9b29309711aa Mon Sep 17 00:00:00 2001 From: "khizar (RinX)" <109973520+Khizarshah01@users.noreply.github.com> Date: Mon, 23 Feb 2026 00:34:15 +0530 Subject: [PATCH 009/108] fix: limit outgoing webhook response size to prevent memory exhaustion (#38760) Co-authored-by: Kevin Aleman <11577696+KevLehman@users.noreply.github.com> --- .changeset/shiny-pears-admire.md | 5 +++++ apps/meteor/app/integrations/server/lib/triggerHandler.ts | 1 + 2 files changed, 6 insertions(+) create mode 100644 .changeset/shiny-pears-admire.md diff --git a/.changeset/shiny-pears-admire.md b/.changeset/shiny-pears-admire.md new file mode 100644 index 0000000000000..0e8287d708f4e --- /dev/null +++ b/.changeset/shiny-pears-admire.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Limits `Outgoing webhook` maximum response size to 10mb. diff --git a/apps/meteor/app/integrations/server/lib/triggerHandler.ts b/apps/meteor/app/integrations/server/lib/triggerHandler.ts index 192419d6c2136..43f4826993022 100644 --- a/apps/meteor/app/integrations/server/lib/triggerHandler.ts +++ b/apps/meteor/app/integrations/server/lib/triggerHandler.ts @@ -621,6 +621,7 @@ class RocketChatIntegrationHandler { ...(opts.data && { body: opts.data }), // SECURITY: Integrations can only be configured by users with enough privileges. It's ok to disable this check here. ignoreSsrfValidation: true, + size: 10 * 1024 * 1024, }, settings.get('Allow_Invalid_SelfSigned_Certs'), ) From 133da0b146997935209e270ca72d97abe6c981f2 Mon Sep 17 00:00:00 2001 From: Shreyas Date: Mon, 23 Feb 2026 11:56:34 +0530 Subject: [PATCH 010/108] fix(team): await default-room assignments when adding team members (#38701) --- apps/meteor/server/services/team/service.ts | 10 +- .../server/services/team/service.tests.ts | 118 ++++++++++++++++++ 2 files changed, 122 insertions(+), 6 deletions(-) create mode 100644 apps/meteor/tests/unit/server/services/team/service.tests.ts diff --git a/apps/meteor/server/services/team/service.ts b/apps/meteor/server/services/team/service.ts index add7f3940574f..9d5b3bb0356ab 100644 --- a/apps/meteor/server/services/team/service.ts +++ b/apps/meteor/server/services/team/service.ts @@ -998,13 +998,11 @@ export class TeamService extends ServiceClassInternal implements ITeamService { const defaultRooms = await Rooms.findDefaultRoomsForTeam(teamId).toArray(); const users = await Users.findActiveByIds(members.map((member) => member.userId)).toArray(); - defaultRooms.map(async (room) => { + for (const room of defaultRooms) { // at this point, users are already part of the team so we won't check for membership - for await (const user of users) { - // add each user to the default room - await addUserToRoom(room._id, user, inviter, { skipSystemMessage: false }); - } - }); + // eslint-disable-next-line no-await-in-loop + await Promise.all(users.map((user) => addUserToRoom(room._id, user, inviter, { skipSystemMessage: false }))); + } } async deleteById(teamId: string): Promise { diff --git a/apps/meteor/tests/unit/server/services/team/service.tests.ts b/apps/meteor/tests/unit/server/services/team/service.tests.ts new file mode 100644 index 0000000000000..d06c34b8d46d0 --- /dev/null +++ b/apps/meteor/tests/unit/server/services/team/service.tests.ts @@ -0,0 +1,118 @@ +import { expect } from 'chai'; +import { beforeEach, describe, it } from 'mocha'; +import proxyquire from 'proxyquire'; +import sinon from 'sinon'; + +const Rooms = { + findDefaultRoomsForTeam: sinon.stub(), +}; + +const Users = { + findActiveByIds: sinon.stub(), +}; + +const addUserToRoom = sinon.stub(); + +const { TeamService } = proxyquire.noCallThru().load('../../../../../server/services/team/service', { + '@rocket.chat/core-services': { + Room: {}, + Authorization: {}, + Message: {}, + ServiceClassInternal: class {}, + api: {}, + }, + '@rocket.chat/models': { + Team: {}, + Rooms, + Subscriptions: {}, + Users, + TeamMember: {}, + }, + '@rocket.chat/string-helpers': { + escapeRegExp: (value: string) => value, + }, + '../../../app/channel-settings/server': { + saveRoomName: sinon.stub(), + }, + '../../../app/channel-settings/server/functions/saveRoomType': { + saveRoomType: sinon.stub(), + }, + '../../../app/lib/server/functions/addUserToRoom': { + addUserToRoom, + }, + '../../../app/lib/server/functions/checkUsernameAvailability': { + checkUsernameAvailability: sinon.stub(), + }, + '../../../app/lib/server/functions/getRoomsWithSingleOwner': { + getSubscribedRoomsForUserWithDetails: sinon.stub(), + }, + '../../../app/lib/server/functions/removeUserFromRoom': { + removeUserFromRoom: sinon.stub(), + }, + '../../../app/lib/server/lib/notifyListener': { + notifyOnSubscriptionChangedByRoomIdAndUserId: sinon.stub(), + notifyOnRoomChangedById: sinon.stub(), + }, + '../../../app/settings/server': { + settings: { get: sinon.stub() }, + }, +}); + +const service = new TeamService(); + +describe('Team service', () => { + beforeEach(() => { + addUserToRoom.reset(); + Rooms.findDefaultRoomsForTeam.reset(); + Users.findActiveByIds.reset(); + }); + + it('should wait for default room membership operations to finish', async function () { + this.timeout(15000); + + addUserToRoom.onFirstCall().resolves(true); + addUserToRoom.onSecondCall().returns( + new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 20); + }), + ); + + Rooms.findDefaultRoomsForTeam.returns({ + toArray: () => Promise.resolve([{ _id: 'default-room' }]), + }); + Users.findActiveByIds.returns({ + toArray: () => + Promise.resolve([ + { _id: 'user-1', username: 'user-1' }, + { _id: 'user-2', username: 'user-2' }, + ]), + }); + + await service.addMembersToDefaultRooms({ _id: 'inviter', username: 'inviter' }, 'team-id', [ + { userId: 'user-1' }, + { userId: 'user-2' }, + ]); + + expect(addUserToRoom.callCount).to.equal(2); + }); + + it('should propagate errors from default room membership operations', async function () { + this.timeout(15000); + + addUserToRoom.rejects(new Error('room-add-failed')); + Rooms.findDefaultRoomsForTeam.returns({ + toArray: () => Promise.resolve([{ _id: 'default-room' }]), + }); + Users.findActiveByIds.returns({ + toArray: () => Promise.resolve([{ _id: 'user-1', username: 'user-1' }]), + }); + + await expect( + service.addMembersToDefaultRooms({ _id: 'inviter', username: 'inviter' }, 'team-id', [{ userId: 'user-1' }]), + ).to.be.rejectedWith('room-add-failed'); + + expect(addUserToRoom.callCount).to.equal(1); + }); +}); From 40347d40fe6890a57b4f62393818719d6fb64e2d Mon Sep 17 00:00:00 2001 From: Amit Kumar Ashutosh <73929517+amitkumarashutosh@users.noreply.github.com> Date: Mon, 23 Feb 2026 21:32:58 +0530 Subject: [PATCH 011/108] refactor: rename useCannedResponse method to selectCannedResponse (#38920) --- .../omnichannel-canned-responses-usage.spec.ts | 10 +++++----- .../page-objects/fragments/home-omnichannel-content.ts | 5 +---- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-canned-responses-usage.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-canned-responses-usage.spec.ts index a6c8be054e005..636ade51487a7 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-canned-responses-usage.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-canned-responses-usage.spec.ts @@ -104,7 +104,7 @@ test.describe('OC - Canned Responses Usage', () => { }); await test.step('expect to use the canned response in the chat', async () => { - await agent.poHomeChannel.content.useCannedResponse(cannedResponseName); + await agent.poHomeChannel.content.selectCannedResponse(cannedResponseName); }); await test.step('expect canned response text to appear in message composer', async () => { @@ -134,7 +134,7 @@ test.describe('OC - Canned Responses Usage', () => { }); await test.step('expect to use the canned response with placeholder', async () => { - await agent.poHomeChannel.content.useCannedResponse(placeholderResponseName); + await agent.poHomeChannel.content.selectCannedResponse(placeholderResponseName); }); await test.step('expect placeholder to be replaced with actual visitor name', async () => { @@ -159,7 +159,7 @@ test.describe('OC - Canned Responses Usage', () => { }); await test.step('expect to use existing canned response and modify it', async () => { - await agent.poHomeChannel.content.useCannedResponse(cannedResponseName); + await agent.poHomeChannel.content.selectCannedResponse(cannedResponseName); }); await test.step('expect to modify the canned response text before sending', async () => { @@ -193,13 +193,13 @@ test.describe('OC - Canned Responses Usage', () => { }); await test.step('expect to use first canned response', async () => { - await agent.poHomeChannel.content.useCannedResponse(cannedResponseName); + await agent.poHomeChannel.content.selectCannedResponse(cannedResponseName); await expect(agent.poHomeChannel.composer.inputMessage).toHaveValue(`${cannedResponseText} `); await agent.page.keyboard.press('Enter'); }); await test.step('expect to use second canned response', async () => { - await agent.poHomeChannel.content.useCannedResponse(secondResponseName); + await agent.poHomeChannel.content.selectCannedResponse(secondResponseName); await expect(agent.poHomeChannel.composer.inputMessage).toHaveValue(`${secondResponseText} `); await agent.page.keyboard.press('Enter'); }); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-omnichannel-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-omnichannel-content.ts index 71534bdb38498..a933aebf975a2 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-omnichannel-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-omnichannel-content.ts @@ -38,10 +38,7 @@ export class HomeOmnichannelContent extends HomeContent { return this.page.locator('.rcx-room-header').getByRole('heading'); } - /** - * FIXME: useX naming convention should be exclusively for react hooks - **/ - async useCannedResponse(cannedResponseName: string): Promise { + async selectCannedResponse(cannedResponseName: string): Promise { await this.composer.inputMessage.pressSequentially('!'); await this.page.locator('[role="menu"][name="ComposerBoxPopup"]').waitFor({ state: 'visible' }); await this.composer.inputMessage.pressSequentially(cannedResponseName); From fbb5136879a54258515d70f74becd7ca4b1e3d16 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 23 Feb 2026 13:23:28 -0300 Subject: [PATCH 012/108] refactor: Update authorization service to accept IUser type (#38622) Co-authored-by: Kevin Aleman --- apps/meteor/app/api/server/v1/chat.ts | 2 +- .../server/functions/canSendMessage.ts | 24 +++++++------ .../app/lib/server/methods/sendMessage.ts | 14 ++++---- .../notifications/notifications.module.ts | 3 +- .../services/authorization/canAccessRoom.ts | 36 +++++++++++++++---- .../server/services/authorization/service.ts | 27 +++++++------- .../core-services/src/types/IAuthorization.ts | 8 ++++- 7 files changed, 75 insertions(+), 39 deletions(-) diff --git a/apps/meteor/app/api/server/v1/chat.ts b/apps/meteor/app/api/server/v1/chat.ts index f5a9250fe29b6..c9fc2ce6925ec 100644 --- a/apps/meteor/app/api/server/v1/chat.ts +++ b/apps/meteor/app/api/server/v1/chat.ts @@ -434,7 +434,7 @@ API.v1.addRoute( } const sent = await applyAirGappedRestrictionsValidation(() => - executeSendMessage(this.userId, this.bodyParams.message as Pick, { previewUrls: this.bodyParams.previewUrls }), + executeSendMessage(this.user, this.bodyParams.message as Pick, { previewUrls: this.bodyParams.previewUrls }), ); const [message] = await normalizeMessagesForUser([sent], this.userId); diff --git a/apps/meteor/app/authorization/server/functions/canSendMessage.ts b/apps/meteor/app/authorization/server/functions/canSendMessage.ts index b9d6b740c2ddd..5f5c97fda453d 100644 --- a/apps/meteor/app/authorization/server/functions/canSendMessage.ts +++ b/apps/meteor/app/authorization/server/functions/canSendMessage.ts @@ -13,9 +13,10 @@ const subscriptionOptions = { }, }; +// TODO: remove option uid and username and type export async function validateRoomMessagePermissionsAsync( room: IRoom | null, - { uid, username, type }: { uid: IUser['_id']; username: IUser['username']; type: IUser['type'] }, + args: { uid: IUser['_id']; username: IUser['username']; type: IUser['type'] } | IUser, extraData?: Record, ): Promise { if (!room) { @@ -25,33 +26,34 @@ export async function validateRoomMessagePermissionsAsync( if (room.archived) { throw new Error('room_is_archived'); } - - if (type !== 'app' && !(await canAccessRoomAsync(room, { _id: uid }, extraData))) { + if (args.type !== 'app' && !(await canAccessRoomAsync(room, 'uid' in args ? { _id: args.uid } : args, extraData))) { throw new Error('error-not-allowed'); } - if (await roomCoordinator.getRoomDirectives(room.t).allowMemberAction(room, RoomMemberActions.BLOCK, uid)) { - const subscription = await Subscriptions.findOneByRoomIdAndUserId(room._id, uid, subscriptionOptions); + if ( + await roomCoordinator.getRoomDirectives(room.t).allowMemberAction(room, RoomMemberActions.BLOCK, 'uid' in args ? args.uid : args._id) + ) { + const subscription = await Subscriptions.findOneByRoomIdAndUserId(room._id, 'uid' in args ? args.uid : args._id, subscriptionOptions); if (subscription && (subscription.blocked || subscription.blocker)) { throw new Error('room_is_blocked'); } } - if (room.ro === true && !(await hasPermissionAsync(uid, 'post-readonly', room._id))) { + if (room.ro === true && !(await hasPermissionAsync('uid' in args ? args.uid : args._id, 'post-readonly', room._id))) { // Unless the user was manually unmuted - if (username && !(room.unmuted || []).includes(username)) { + if (args.username && !(room.unmuted || []).includes(args.username)) { throw new Error("You can't send messages because the room is readonly."); } } - if (username && room?.muted?.includes(username)) { + if (args.username && room?.muted?.includes(args.username)) { throw new Error('You_have_been_muted'); } } - +// TODO: remove option uid and username and type export async function canSendMessageAsync( rid: IRoom['_id'], - { uid, username, type }: { uid: IUser['_id']; username: IUser['username']; type: IUser['type'] }, + user: { uid: IUser['_id']; username: IUser['username']; type: IUser['type'] } | IUser, extraData?: Record, ): Promise { const room = await Rooms.findOneById(rid); @@ -59,6 +61,6 @@ export async function canSendMessageAsync( throw new Error('error-invalid-room'); } - await validateRoomMessagePermissionsAsync(room, { uid, username, type }, extraData); + await validateRoomMessagePermissionsAsync(room, user, extraData); return room; } diff --git a/apps/meteor/app/lib/server/methods/sendMessage.ts b/apps/meteor/app/lib/server/methods/sendMessage.ts index 895ccc27a5b87..a1208543d6841 100644 --- a/apps/meteor/app/lib/server/methods/sendMessage.ts +++ b/apps/meteor/app/lib/server/methods/sendMessage.ts @@ -30,7 +30,7 @@ import { RateLimiter } from '../lib'; * @returns */ export async function executeSendMessage( - uid: IUser['_id'], + uid: IUser['_id'] | IUser, message: AtLeast, extraInfo?: { ts?: Date; previewUrls?: string[] }, ) { @@ -71,7 +71,7 @@ export async function executeSendMessage( } } - const user = await Users.findOneById(uid); + const user = typeof uid === 'string' ? await Users.findOneById(uid) : uid; if (!user?.username) { throw new Meteor.Error('error-invalid-user', 'Invalid user'); } @@ -95,7 +95,7 @@ export async function executeSendMessage( check(rid, String); try { - const room = await canSendMessageAsync(rid, { uid, username: user.username, type: user.type }); + const room = await canSendMessageAsync(rid, user); if (room.encrypted && settings.get('E2E_Enable') && !settings.get('E2E_Allow_Unencrypted_Messages')) { if (message.t !== 'e2e') { @@ -112,7 +112,7 @@ export async function executeSendMessage( const errorMessage: RocketchatI18nKeys = typeof err === 'string' ? err : err.error || err.message; const errorContext: TOptions = err.details ?? {}; - void api.broadcast('notify.ephemeralMessage', uid, message.rid, { + void api.broadcast('notify.ephemeralMessage', user._id, message.rid, { msg: i18n.t(errorMessage, { ...errorContext, lng: user.language }), }); @@ -151,8 +151,8 @@ Meteor.methods({ sentByEmail: Match.Maybe(Boolean), }); - const uid = Meteor.userId(); - if (!uid) { + const user = (await Meteor.userAsync()) as IUser; + if (!user) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'sendMessage', }); @@ -163,7 +163,7 @@ Meteor.methods({ } try { - return await applyAirGappedRestrictionsValidation(() => executeSendMessage(uid, message, { previewUrls })); + return await applyAirGappedRestrictionsValidation(() => executeSendMessage(user, message, { previewUrls })); } catch (error: any) { if (['error-not-allowed', 'restricted-workspace'].includes(error.error || error.message)) { throw new Meteor.Error(error.error || error.message, error.reason, { diff --git a/apps/meteor/server/modules/notifications/notifications.module.ts b/apps/meteor/server/modules/notifications/notifications.module.ts index 90262f95cf01d..cdeeb73cdecb6 100644 --- a/apps/meteor/server/modules/notifications/notifications.module.ts +++ b/apps/meteor/server/modules/notifications/notifications.module.ts @@ -98,7 +98,8 @@ export class NotificationsModule { return false; } - return Authorization.canReadRoom(room, { _id: this.userId || '' }, extraData); + const user = this.userId ? { _id: this.userId } : undefined; + return Authorization.canReadRoom(room, user, extraData); }); this.streamRoomMessage.allowRead('__my_messages__', 'all'); diff --git a/apps/meteor/server/services/authorization/canAccessRoom.ts b/apps/meteor/server/services/authorization/canAccessRoom.ts index 675336df84bc5..684d52ac477a6 100644 --- a/apps/meteor/server/services/authorization/canAccessRoom.ts +++ b/apps/meteor/server/services/authorization/canAccessRoom.ts @@ -1,8 +1,8 @@ import { Authorization, License, Abac, Settings } from '@rocket.chat/core-services'; import type { RoomAccessValidator } from '@rocket.chat/core-services'; import { TeamType, AbacAccessOperation, AbacObjectType } from '@rocket.chat/core-typings'; -import type { IUser, ITeam } from '@rocket.chat/core-typings'; -import { Subscriptions, Rooms, TeamMember, Team } from '@rocket.chat/models'; +import type { IUser, ITeam, IRoom } from '@rocket.chat/core-typings'; +import { Subscriptions, Rooms, TeamMember, Team, Users } from '@rocket.chat/models'; import { canAccessRoomLivechat } from './canAccessRoomLivechat'; @@ -15,7 +15,13 @@ async function canAccessPublicRoom(user?: Partial): Promise { return Authorization.hasPermission(user._id, 'view-c-room'); } -const roomAccessValidators: RoomAccessValidator[] = [ +type RoomAccessValidatorConverted = ( + room?: Pick, + user?: IUser, + extraData?: Record, +) => Promise; + +const roomAccessValidators: RoomAccessValidatorConverted[] = [ async function _validateAccessToPublicRoomsInTeams(room, user): Promise { if (!room) { return false; @@ -56,8 +62,8 @@ const roomAccessValidators: RoomAccessValidator[] = [ } const [canViewJoined, canViewT] = await Promise.all([ - Authorization.hasPermission(user._id, 'view-joined-room'), - Authorization.hasPermission(user._id, `view-${room.t}-room`), + Authorization.hasPermission(user, 'view-joined-room'), + Authorization.hasPermission(user, `view-${room.t}-room`), ]); // When there's no ABAC setting, license or values on the room, fallback to previous behavior @@ -89,14 +95,32 @@ const roomAccessValidators: RoomAccessValidator[] = [ canAccessRoomLivechat, ]; +const isPartialUser = (user: IUser | Pick | undefined): user is Pick => { + return Boolean(user && Object.keys(user).length === 1 && '_id' in user); +}; + export const canAccessRoom: RoomAccessValidator = async (room, user, extraData): Promise => { // TODO livechat can send both as null, so they we need to validate nevertheless // if (!room || !user) { // return false; // } + // TODO: remove this after migrations + // if user only contains _id, convert it to a full IUser object + + if (isPartialUser(user)) { + user = (await Users.findOneById(user._id)) || undefined; + if (!user) { + throw new Error('User not found'); + } + + if (process.env.NODE_ENV === 'development') { + console.log('User converted to full IUser object'); + } + } + for await (const roomAccessValidator of roomAccessValidators) { - if (await roomAccessValidator(room, user, extraData)) { + if (await roomAccessValidator(room, user as IUser, extraData)) { return true; } } diff --git a/apps/meteor/server/services/authorization/service.ts b/apps/meteor/server/services/authorization/service.ts index 3c1858b1305e4..a8984dd79c241 100644 --- a/apps/meteor/server/services/authorization/service.ts +++ b/apps/meteor/server/services/authorization/service.ts @@ -56,21 +56,21 @@ export class Authorization extends ServiceClass implements IAuthorization { } } - async hasAllPermission(userId: string, permissions: string[], scope?: string): Promise { + async hasAllPermission(userId: string | IUser, permissions: string[], scope?: string): Promise { if (!userId) { return false; } return this.all(userId, permissions, scope); } - async hasPermission(userId: string, permissionId: string, scope?: string): Promise { + async hasPermission(userId: string | IUser, permissionId: string, scope?: string): Promise { if (!userId) { return false; } return this.all(userId, [permissionId], scope); } - async hasAtLeastOnePermission(userId: string, permissions: string[], scope?: string): Promise { + async hasAtLeastOnePermission(userId: string | IUser, permissions: string[], scope?: string): Promise { if (!userId) { return false; } @@ -85,7 +85,7 @@ export class Authorization extends ServiceClass implements IAuthorization { return canReadRoom(...args); } - async canAccessRoomId(rid: IRoom['_id'], uid: IUser['_id']): Promise { + async canAccessRoomId(rid: IRoom['_id'], user: IUser['_id']): Promise { const room = await Rooms.findOneById>(rid, { projection: { _id: 1, @@ -100,7 +100,7 @@ export class Authorization extends ServiceClass implements IAuthorization { return false; } - return this.canAccessRoom(room, { _id: uid }); + return this.canAccessRoom(room, { _id: user }); } async addRoleRestrictions(role: IRole['_id'], permissions: string[]): Promise { @@ -160,17 +160,20 @@ export class Authorization extends ServiceClass implements IAuthorization { return !!result; } - private async getRoles(uid: string, scope?: IRoom['_id']): Promise { - const { roles: userRoles = [] } = (await Users.findOneById(uid, { projection: { roles: 1 } })) || {}; + private async getRoles(user: string | IUser, scope?: IRoom['_id']): Promise { + const { roles: userRoles = [] } = typeof user === 'string' ? (await Users.findOneById(user, { projection: { roles: 1 } })) || {} : user; const { roles: subscriptionsRoles = [] } = (scope && - (await Subscriptions.findOne>({ 'rid': scope, 'u._id': uid }, { projection: { roles: 1 } }))) || + (await Subscriptions.findOne>( + { 'rid': scope, 'u._id': typeof user === 'string' ? user : user._id }, + { projection: { roles: 1 } }, + ))) || {}; return [...userRoles, ...subscriptionsRoles].sort((a, b) => a.localeCompare(b)); } - private async atLeastOne(uid: string, permissions: string[] = [], scope?: string): Promise { - const sortedRoles = await this.getRolesCached(uid, scope); + private async atLeastOne(user: string | IUser, permissions: string[] = [], scope?: string): Promise { + const sortedRoles = await this.getRolesCached(user, scope); for await (const permission of permissions) { if (await this.rolesHasPermissionCached(permission, sortedRoles)) { return true; @@ -180,8 +183,8 @@ export class Authorization extends ServiceClass implements IAuthorization { return false; } - private async all(uid: string, permissions: string[] = [], scope?: string): Promise { - const sortedRoles = await this.getRolesCached(uid, scope); + private async all(user: string | IUser, permissions: string[] = [], scope?: string): Promise { + const sortedRoles = await this.getRolesCached(user, scope); for await (const permission of permissions) { if (!(await this.rolesHasPermissionCached(permission, sortedRoles))) { return false; diff --git a/packages/core-services/src/types/IAuthorization.ts b/packages/core-services/src/types/IAuthorization.ts index d4bb1c1c67d4f..d87b2a873e68f 100644 --- a/packages/core-services/src/types/IAuthorization.ts +++ b/packages/core-services/src/types/IAuthorization.ts @@ -2,13 +2,19 @@ import type { IRoom, IUser, IRole } from '@rocket.chat/core-typings'; export type RoomAccessValidator = ( room?: Pick, - user?: Pick, + user?: IUser | Pick, extraData?: Record, ) => Promise; export interface IAuthorization { + hasAllPermission(user: IUser, permissions: string[], scope?: string): Promise; + // @deprecated hasAllPermission(userId: string, permissions: string[], scope?: string): Promise; + hasPermission(user: IUser, permissionId: string, scope?: string): Promise; + // @deprecated hasPermission(userId: string, permissionId: string, scope?: string): Promise; + hasAtLeastOnePermission(user: IUser, permissions: string[], scope?: string): Promise; + // @deprecated hasAtLeastOnePermission(userId: string, permissions: string[], scope?: string): Promise; canAccessRoom: RoomAccessValidator; canReadRoom: RoomAccessValidator; From 18a1018850bf1c8ce7665832793e8d38792669c0 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 23 Feb 2026 15:12:30 -0300 Subject: [PATCH 013/108] test: set yarn testunit concurrency to 1 for improved stability (#38939) --- .github/workflows/ci-test-unit.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-test-unit.yml b/.github/workflows/ci-test-unit.yml index 7d249f60ff48e..b278eb9cd7a8a 100644 --- a/.github/workflows/ci-test-unit.yml +++ b/.github/workflows/ci-test-unit.yml @@ -53,7 +53,7 @@ jobs: tar -xzf /tmp/RocketChat-packages-build.tar.gz -C . - name: Unit Test - run: yarn testunit + run: yarn testunit --concurrency=1 - uses: codecov/codecov-action@v5 with: From 98a6c58a38c053c60db2b4d53a9df0e94fecf0ba Mon Sep 17 00:00:00 2001 From: Ahmed Nasser Date: Mon, 23 Feb 2026 20:57:30 +0200 Subject: [PATCH 014/108] chore: Add OpenAPI Support to rooms.favorite API (#35995) Co-authored-by: Guilherme Gazzo Co-authored-by: Guilherme Gazzo --- .changeset/tricky-boxes-type.md | 6 ++ apps/meteor/app/api/server/v1/rooms.ts | 143 ++++++++++++++++--------- packages/rest-typings/src/v1/rooms.ts | 14 --- 3 files changed, 101 insertions(+), 62 deletions(-) create mode 100644 .changeset/tricky-boxes-type.md diff --git a/.changeset/tricky-boxes-type.md b/.changeset/tricky-boxes-type.md new file mode 100644 index 0000000000000..084f3f79fe242 --- /dev/null +++ b/.changeset/tricky-boxes-type.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/rest-typings": patch +--- + +Add OpenAPI support for the Rocket.Chat rooms.favorite APIs endpoints by migrating to a modern chained route definition syntax and utilizing shared AJV schemas for validation to enhance API documentation and ensure type safety through response validation. diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index 686f76a7476c6..988e60002eef9 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -311,26 +311,6 @@ API.v1.addRoute( }, ); -API.v1.addRoute( - 'rooms.favorite', - { authRequired: true }, - { - async post() { - const { favorite } = this.bodyParams; - - if (!this.bodyParams.hasOwnProperty('favorite')) { - return API.v1.failure("The 'favorite' param is required"); - } - - const room = await findRoomByIdOrName({ params: this.bodyParams }); - - await toggleFavoriteMethod(this.userId, room._id, favorite); - - return API.v1.success(); - }, - }, -); - API.v1.addRoute( 'rooms.cleanHistory', { authRequired: true, validateParams: isRoomsCleanHistoryProps }, @@ -945,6 +925,16 @@ API.v1.addRoute( }, ); +type RoomsFavorite = + | { + roomId: string; + favorite: boolean; + } + | { + roomName: string; + favorite: boolean; + }; + const isRoomGetRolesPropsSchema = { type: 'object', properties: { @@ -953,6 +943,32 @@ const isRoomGetRolesPropsSchema = { additionalProperties: false, required: ['rid'], }; + +const RoomsFavoriteSchema = { + anyOf: [ + { + type: 'object', + properties: { + favorite: { type: 'boolean' }, + roomName: { type: 'string' }, + }, + required: ['roomName', 'favorite'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + favorite: { type: 'boolean' }, + roomId: { type: 'string' }, + }, + required: ['roomId', 'favorite'], + additionalProperties: false, + }, + ], +}; + +const isRoomsFavoriteProps = ajv.compile(RoomsFavoriteSchema); + export const roomEndpoints = API.v1 .get( 'rooms.roles', @@ -1066,39 +1082,70 @@ export const roomEndpoints = API.v1 total, }); }, - ); + ) + .post( + 'rooms.invite', + { + authRequired: true, + body: isRoomsInviteProps, + response: { + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + 200: ajv.compile({ + type: 'object', + properties: { + success: { type: 'boolean', enum: [true] }, + }, + required: ['success'], + additionalProperties: false, + }), + }, + }, + async function action() { + const { roomId, action } = this.bodyParams; -const roomInviteEndpoints = API.v1.post( - 'rooms.invite', - { - authRequired: true, - body: isRoomsInviteProps, - response: { - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - 200: ajv.compile({ - type: 'object', - properties: { - success: { type: 'boolean', enum: [true] }, - }, - required: ['success'], - additionalProperties: false, - }), + try { + await FederationMatrix.handleInvite(roomId, this.userId, action); + return API.v1.success(); + } catch (error) { + return API.v1.failure({ error: `Failed to handle invite: ${error instanceof Error ? error.message : String(error)}` }); + } }, - }, - async function action() { - const { roomId, action } = this.bodyParams; + ) + .post( + 'rooms.favorite', + { + authRequired: true, + body: isRoomsFavoriteProps, + response: { + 200: ajv.compile({ + type: 'object', + properties: { + success: { + type: 'boolean', + enum: [true], + description: 'Indicates if the request was successful.', + }, + }, + required: ['success'], + additionalProperties: false, + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, + }, + async function action() { + const { favorite } = this.bodyParams; + + const room = await findRoomByIdOrName({ params: this.bodyParams }); + + await toggleFavoriteMethod(this.userId, room._id, favorite); - try { - await FederationMatrix.handleInvite(roomId, this.userId, action); return API.v1.success(); - } catch (error) { - return API.v1.failure({ error: `Failed to handle invite: ${error instanceof Error ? error.message : String(error)}` }); - } - }, -); + }, + ); -type RoomEndpoints = ExtractRoutesFromAPI & ExtractRoutesFromAPI; +type RoomEndpoints = ExtractRoutesFromAPI; declare module '@rocket.chat/rest-typings' { // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface diff --git a/packages/rest-typings/src/v1/rooms.ts b/packages/rest-typings/src/v1/rooms.ts index c8d77253ecf5c..18c4574f7bb23 100644 --- a/packages/rest-typings/src/v1/rooms.ts +++ b/packages/rest-typings/src/v1/rooms.ts @@ -805,20 +805,6 @@ export type RoomsEndpoints = { POST: (params: { roomId: string; notifications: Notifications }) => void; }; - '/v1/rooms.favorite': { - POST: ( - params: - | { - roomId: string; - favorite: boolean; - } - | { - roomName: string; - favorite: boolean; - }, - ) => void; - }; - '/v1/rooms.nameExists': { GET: (params: { roomName: string }) => { exists: boolean; From c117492ad90d291a361eedc929506f557495caf7 Mon Sep 17 00:00:00 2001 From: Ahmed Nasser Date: Mon, 23 Feb 2026 22:21:20 +0200 Subject: [PATCH 015/108] chore: Add OpenAPI Support to commands.get API (#36953) --- .changeset/wet-roses-call.md | 7 ++ apps/meteor/app/api/server/v1/commands.ts | 85 +++++++++++++++++--- apps/meteor/tests/end-to-end/api/commands.ts | 2 +- packages/core-typings/src/Ajv.ts | 3 +- packages/rest-typings/src/v1/commands.ts | 5 -- 5 files changed, 82 insertions(+), 20 deletions(-) create mode 100644 .changeset/wet-roses-call.md diff --git a/.changeset/wet-roses-call.md b/.changeset/wet-roses-call.md new file mode 100644 index 0000000000000..88cdcdb45362e --- /dev/null +++ b/.changeset/wet-roses-call.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/rest-typings": patch +"@rocket.chat/core-typings": patch +--- + +Add OpenAPI support for the Rocket.Chat commands.get API endpoints by migrating to a modern chained route definition syntax and utilizing shared AJV schemas for validation to enhance API documentation and ensure type safety through response validation. diff --git a/apps/meteor/app/api/server/v1/commands.ts b/apps/meteor/app/api/server/v1/commands.ts index 9347536cafd52..6bc6711b3a654 100644 --- a/apps/meteor/app/api/server/v1/commands.ts +++ b/apps/meteor/app/api/server/v1/commands.ts @@ -1,34 +1,86 @@ import { Apps } from '@rocket.chat/apps'; +import type { SlashCommand } from '@rocket.chat/core-typings'; import { Messages } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; +import { ajv, validateUnauthorizedErrorResponse, validateBadRequestErrorResponse } from '@rocket.chat/rest-typings'; import objectPath from 'object-path'; import { canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom'; import { executeSlashCommandPreview } from '../../../lib/server/methods/executeSlashCommandPreview'; import { getSlashCommandPreviews } from '../../../lib/server/methods/getSlashCommandPreviews'; import { slashCommands } from '../../../utils/server/slashCommand'; +import type { ExtractRoutesFromAPI } from '../ApiClass'; import { API } from '../api'; import { getPaginationItems } from '../helpers/getPaginationItems'; -API.v1.addRoute( +type CommandsGetParams = { command: string }; + +const CommandsGetParamsSchema = { + type: 'object', + properties: { + command: { type: 'string' }, + }, + required: ['command'], + additionalProperties: false, +}; + +const isCommandsGetParams = ajv.compile(CommandsGetParamsSchema); + +const commandsEndpoints = API.v1.get( 'commands.get', - { authRequired: true }, { - get() { - const params = this.queryParams; + authRequired: true, + query: isCommandsGetParams, + response: { + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + 200: ajv.compile<{ + command: Pick; + success: true; + }>({ + type: 'object', + properties: { + command: { + type: 'object', + properties: { + clientOnly: { type: 'boolean' }, + command: { type: 'string' }, + description: { type: 'string' }, + params: { type: 'string' }, + providesPreview: { type: 'boolean' }, + }, + required: ['command', 'providesPreview'], + additionalProperties: false, + }, + success: { + type: 'boolean', + enum: [true], + }, + }, + required: ['command', 'success'], + additionalProperties: false, + }), + }, + }, - if (typeof params.command !== 'string') { - return API.v1.failure('The query param "command" must be provided.'); - } + async function action() { + const params = this.queryParams; - const cmd = slashCommands.commands[params.command.toLowerCase()]; + const cmd = slashCommands.commands[params.command.toLowerCase()]; - if (!cmd) { - return API.v1.failure(`There is no command in the system by the name of: ${params.command}`); - } + if (!cmd) { + return API.v1.failure(`There is no command in the system by the name of: ${params.command}`); + } - return API.v1.success({ command: cmd }); - }, + return API.v1.success({ + command: { + command: cmd.command, + description: cmd.description, + params: cmd.params, + clientOnly: cmd.clientOnly, + providesPreview: cmd.providesPreview, + }, + }); }, ); @@ -348,3 +400,10 @@ API.v1.addRoute( }, }, ); + +export type CommandsEndpoints = ExtractRoutesFromAPI; + +declare module '@rocket.chat/rest-typings' { + // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface + interface Endpoints extends CommandsEndpoints {} +} diff --git a/apps/meteor/tests/end-to-end/api/commands.ts b/apps/meteor/tests/end-to-end/api/commands.ts index 3e47d190e86bf..fbb115e87a6be 100644 --- a/apps/meteor/tests/end-to-end/api/commands.ts +++ b/apps/meteor/tests/end-to-end/api/commands.ts @@ -22,7 +22,7 @@ describe('[Commands]', () => { .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body.error).to.be.equal('The query param "command" must be provided.'); + expect(res.body.error).to.be.equal(`must have required property 'command'`); }) .end(done); }); diff --git a/packages/core-typings/src/Ajv.ts b/packages/core-typings/src/Ajv.ts index ef74c7638a30b..cea4a75b45e8f 100644 --- a/packages/core-typings/src/Ajv.ts +++ b/packages/core-typings/src/Ajv.ts @@ -7,9 +7,10 @@ import type { IMessage } from './IMessage'; import type { IOAuthApps } from './IOAuthApps'; import type { IPermission } from './IPermission'; import type { ISubscription } from './ISubscription'; +import type { SlashCommand } from './SlashCommands'; import type { IMediaCall } from './mediaCalls/IMediaCall'; export const schemas = typia.json.schemas< - [ISubscription | IInvite | ICustomSound | IMessage | IOAuthApps | IPermission | IMediaCall, CallHistoryItem], + [ISubscription | IInvite | ICustomSound | IMessage | IOAuthApps | IPermission | IMediaCall, CallHistoryItem, SlashCommand], '3.0' >(); diff --git a/packages/rest-typings/src/v1/commands.ts b/packages/rest-typings/src/v1/commands.ts index bbf3788b2b475..7476dafa88db5 100644 --- a/packages/rest-typings/src/v1/commands.ts +++ b/packages/rest-typings/src/v1/commands.ts @@ -4,11 +4,6 @@ import type { PaginatedRequest } from '../helpers/PaginatedRequest'; import type { PaginatedResult } from '../helpers/PaginatedResult'; export type CommandsEndpoints = { - '/v1/commands.get': { - GET: (params: { command: string }) => { - command: Pick; - }; - }; '/v1/commands.list': { GET: ( params?: PaginatedRequest<{ From 29b453e1def8092a8d78c28736e2bfb24229717b Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli <84046180+nazabucciarelli@users.noreply.github.com> Date: Mon, 23 Feb 2026 18:33:46 -0300 Subject: [PATCH 016/108] feat: add custom-sounds.getOne endpoint, adapt client to consume it and add Contextualbar bug fixes (#38610) Co-authored-by: gabriellsh <40830821+gabriellsh@users.noreply.github.com> --- .changeset/rude-plums-think.md | 5 + .changeset/twenty-colts-flash.md | 6 + .../meteor/app/api/server/v1/custom-sounds.ts | 165 +++++++++++------- .../admin/customSounds/AddCustomSound.tsx | 1 + .../admin/customSounds/EditCustomSound.tsx | 31 ++-- .../views/admin/customSounds/EditSound.tsx | 5 +- .../tests/end-to-end/api/custom-sounds.ts | 53 ++++++ packages/rest-typings/src/index.ts | 1 + packages/rest-typings/src/v1/customSounds.ts | 19 ++ 9 files changed, 205 insertions(+), 81 deletions(-) create mode 100644 .changeset/rude-plums-think.md create mode 100644 .changeset/twenty-colts-flash.md create mode 100644 packages/rest-typings/src/v1/customSounds.ts diff --git a/.changeset/rude-plums-think.md b/.changeset/rude-plums-think.md new file mode 100644 index 0000000000000..6b5804f013757 --- /dev/null +++ b/.changeset/rude-plums-think.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +Fixes Custom Sounds Contextualbar state and refresh behavior diff --git a/.changeset/twenty-colts-flash.md b/.changeset/twenty-colts-flash.md new file mode 100644 index 0000000000000..93729a19533f6 --- /dev/null +++ b/.changeset/twenty-colts-flash.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/rest-typings': minor +'@rocket.chat/meteor': minor +--- + +Adds new `custom-sounds.getOne` REST endpoint to retrieve a single custom sound by `_id` and updates client to consume it. diff --git a/apps/meteor/app/api/server/v1/custom-sounds.ts b/apps/meteor/app/api/server/v1/custom-sounds.ts index d41362641f75b..a2fa2b675f07f 100644 --- a/apps/meteor/app/api/server/v1/custom-sounds.ts +++ b/apps/meteor/app/api/server/v1/custom-sounds.ts @@ -2,8 +2,10 @@ import type { ICustomSound } from '@rocket.chat/core-typings'; import { CustomSounds } from '@rocket.chat/models'; import type { PaginatedRequest, PaginatedResult } from '@rocket.chat/rest-typings'; import { + isCustomSoundsGetOneProps, ajv, validateBadRequestErrorResponse, + validateNotFoundErrorResponse, validateForbiddenErrorResponse, validateUnauthorizedErrorResponse, } from '@rocket.chat/rest-typings'; @@ -45,76 +47,115 @@ const CustomSoundsListSchema = { export const isCustomSoundsListProps = ajv.compile(CustomSoundsListSchema); -const customSoundsEndpoints = API.v1.get( - 'custom-sounds.list', - { - response: { - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - 403: validateForbiddenErrorResponse, - 200: ajv.compile< - PaginatedResult<{ - sounds: ICustomSound[]; - }> - >({ - additionalProperties: false, - type: 'object', - properties: { - count: { - type: 'number', - description: 'The number of sounds returned in this response.', - }, - offset: { - type: 'number', - description: 'The number of sounds that were skipped in this response.', - }, - total: { - type: 'number', - description: 'The total number of sounds that match the query.', - }, - success: { - type: 'boolean', - description: 'Indicates if the request was successful.', +const customSoundsEndpoints = API.v1 + .get( + 'custom-sounds.list', + { + response: { + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + 403: validateForbiddenErrorResponse, + 200: ajv.compile< + PaginatedResult<{ + sounds: ICustomSound[]; + }> + >({ + additionalProperties: false, + type: 'object', + properties: { + count: { + type: 'number', + description: 'The number of sounds returned in this response.', + }, + offset: { + type: 'number', + description: 'The number of sounds that were skipped in this response.', + }, + total: { + type: 'number', + description: 'The total number of sounds that match the query.', + }, + success: { + type: 'boolean', + description: 'Indicates if the request was successful.', + }, + sounds: { + type: 'array', + items: { + $ref: '#/components/schemas/ICustomSound', + }, + }, }, - sounds: { - type: 'array', - items: { + required: ['count', 'offset', 'total', 'sounds', 'success'], + }), + }, + query: isCustomSoundsListProps, + authRequired: true, + }, + async function action() { + const { offset, count } = await getPaginationItems(this.queryParams as Record); + const { sort, query } = await this.parseJsonQuery(); + + const { name } = this.queryParams; + + const filter = { + ...query, + ...(name ? { name: { $regex: escapeRegExp(name as string), $options: 'i' } } : {}), + }; + + const { cursor, totalCount } = CustomSounds.findPaginated(filter, { + sort: sort || { name: 1 }, + skip: offset, + limit: count, + }); + const [sounds, total] = await Promise.all([cursor.toArray(), totalCount]); + + return API.v1.success({ + sounds, + count: sounds.length, + offset, + total, + }); + }, + ) + .get( + 'custom-sounds.getOne', + { + response: { + 200: ajv.compile<{ sound: ICustomSound; success: boolean }>({ + additionalProperties: false, + type: 'object', + properties: { + sound: { $ref: '#/components/schemas/ICustomSound', }, + success: { + type: 'boolean', + description: 'Indicates if the request was successful.', + }, }, - }, - required: ['count', 'offset', 'total', 'sounds', 'success'], - }), + required: ['sound', 'success'], + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + 403: validateForbiddenErrorResponse, + 404: validateNotFoundErrorResponse, + }, + query: isCustomSoundsGetOneProps, + authRequired: true, }, - query: isCustomSoundsListProps, - authRequired: true, - }, - async function action() { - const { offset, count } = await getPaginationItems(this.queryParams as Record); - const { sort, query } = await this.parseJsonQuery(); - - const { name } = this.queryParams; + async function action() { + const { _id } = this.queryParams; - const filter = { - ...query, - ...(name ? { name: { $regex: escapeRegExp(name as string), $options: 'i' } } : {}), - }; + const sound = await CustomSounds.findOneById(_id); - const { cursor, totalCount } = CustomSounds.findPaginated(filter, { - sort: sort || { name: 1 }, - skip: offset, - limit: count, - }); - const [sounds, total] = await Promise.all([cursor.toArray(), totalCount]); + if (!sound) { + return API.v1.notFound('Custom Sound not found.'); + } - return API.v1.success({ - sounds, - count: sounds.length, - offset, - total, - }); - }, -); + return API.v1.success({ sound }); + }, + ); export type CustomSoundEndpoints = ExtractRoutesFromAPI; diff --git a/apps/meteor/client/views/admin/customSounds/AddCustomSound.tsx b/apps/meteor/client/views/admin/customSounds/AddCustomSound.tsx index 3bc46c8742f1d..b3a75763c0932 100644 --- a/apps/meteor/client/views/admin/customSounds/AddCustomSound.tsx +++ b/apps/meteor/client/views/admin/customSounds/AddCustomSound.tsx @@ -63,6 +63,7 @@ const AddCustomSound = ({ goToNew, close, onChange, ...props }: AddCustomSoundPr (typeof error === 'string' || error instanceof Error) && dispatchToastMessage({ type: 'error', message: error }); } }; + close(); return soundId; } catch (error) { (typeof error === 'string' || error instanceof Error) && dispatchToastMessage({ type: 'error', message: error }); diff --git a/apps/meteor/client/views/admin/customSounds/EditCustomSound.tsx b/apps/meteor/client/views/admin/customSounds/EditCustomSound.tsx index a16c6e476505f..0319ab6db8346 100644 --- a/apps/meteor/client/views/admin/customSounds/EditCustomSound.tsx +++ b/apps/meteor/client/views/admin/customSounds/EditCustomSound.tsx @@ -1,3 +1,4 @@ +import { ContextualbarEmptyContent } from '@rocket.chat/ui-client'; import { useEndpoint } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import type { ReactElement } from 'react'; @@ -9,41 +10,37 @@ import { FormSkeleton } from '../../../components/Skeleton'; type EditCustomSoundProps = { _id: string | undefined; onChange?: () => void; - close?: () => void; + close: () => void; }; -function EditCustomSound({ _id, onChange, ...props }: EditCustomSoundProps): ReactElement | null { +function EditCustomSound({ _id, onChange, close, ...props }: EditCustomSoundProps): ReactElement | null { + const getSound = useEndpoint('GET', '/v1/custom-sounds.getOne'); const { t } = useTranslation(); - const getSounds = useEndpoint('GET', '/v1/custom-sounds.list'); - const { data, isPending, refetch } = useQuery({ - queryKey: ['custom-sounds', _id], - - queryFn: async () => { - const { sounds } = await getSounds({ query: JSON.stringify({ _id }) }); - - if (sounds.length === 0) { - throw new Error(t('No_results_found')); + const { data, isLoading } = useQuery({ + queryKey: ['custom-sound', _id], + queryFn: () => { + if (!_id) { + throw new Error('Cannot fetch custom sound: missing _id in query.'); } - return sounds[0]; + return getSound({ _id }); }, - meta: { apiErrorToastMessage: true }, + enabled: !!_id, }); - if (isPending) { + if (isLoading) { return ; } if (!data) { - return null; + return ; } const handleChange: () => void = () => { onChange?.(); - refetch?.(); }; - return ; + return ; } export default EditCustomSound; diff --git a/apps/meteor/client/views/admin/customSounds/EditSound.tsx b/apps/meteor/client/views/admin/customSounds/EditSound.tsx index 9f72df02ca7bd..f46ce0e175b61 100644 --- a/apps/meteor/client/views/admin/customSounds/EditSound.tsx +++ b/apps/meteor/client/views/admin/customSounds/EditSound.tsx @@ -9,7 +9,7 @@ import { validate, createSoundData } from './lib'; import { useSingleFileInput } from '../../../hooks/useSingleFileInput'; type EditSoundProps = { - close?: () => void; + close: () => void; onChange: () => void; data: { _id: string; @@ -82,6 +82,7 @@ function EditSound({ close, onChange, data, ...props }: EditSoundProps): ReactEl } }; } + close(); } validation.forEach((invalidFieldName) => @@ -95,7 +96,7 @@ function EditSound({ close, onChange, data, ...props }: EditSoundProps): ReactEl ); const handleSave = useCallback(async () => { - saveAction(sound); + await saveAction(sound); onChange(); }, [saveAction, sound, onChange]); diff --git a/apps/meteor/tests/end-to-end/api/custom-sounds.ts b/apps/meteor/tests/end-to-end/api/custom-sounds.ts index d21cabd7f14b6..c788b7ed0cc60 100644 --- a/apps/meteor/tests/end-to-end/api/custom-sounds.ts +++ b/apps/meteor/tests/end-to-end/api/custom-sounds.ts @@ -184,4 +184,57 @@ describe('[CustomSounds]', () => { .end(done); }); }); + + describe('[/custom-sounds.getOne]', () => { + it('should return unauthorized if not authenticated', async () => { + await request.get(api('custom-sounds.getOne')).query({ _id: fileId }).expect(401); + }); + + it('should return not found if custom sound does not exist', async () => { + await request.get(api('custom-sounds.getOne')).set(credentials).query({ _id: 'invalid-id' }).expect(404); + }); + + it('should return bad request if the _id length is not more than one', async () => { + await request.get(api('custom-sounds.getOne')).set(credentials).query({ _id: '' }).expect(400); + }); + + it('should return the custom sound successfully', async () => { + await request + .get(api('custom-sounds.getOne')) + .set(credentials) + .query({ _id: fileId }) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('sound').and.to.be.an('object'); + expect(res.body.sound).to.have.property('_id', fileId); + expect(res.body.sound).to.have.property('name').and.to.be.a('string'); + expect(res.body.sound).to.have.property('extension').and.to.be.a('string'); + }); + }); + + it('should reject regex injection via query object', async () => { + await request + .get(api('custom-sounds.getOne')) + .set(credentials) + .query({ + _id: { $regex: '.*' }, + }) + .expect(400); + }); + + it('should reject regex injection via bracket syntax', async () => { + await request.get(api('custom-sounds.getOne')).set(credentials).query('_id[$regex]=.*').expect(400); + }); + + it('should reject encoded regex injection attempt', async () => { + await request + .get(api('custom-sounds.getOne')) + .set(credentials) + .query({ + _id: '{"$regex":".*"}', + }) + .expect(404); // valid string, but it doesn't exist + }); + }); }); diff --git a/packages/rest-typings/src/index.ts b/packages/rest-typings/src/index.ts index 097ce5081b1de..5569660b41864 100644 --- a/packages/rest-typings/src/index.ts +++ b/packages/rest-typings/src/index.ts @@ -210,6 +210,7 @@ export * from './v1/teams'; export * from './v1/videoConference'; export * from './v1/assets'; export * from './v1/channels'; +export * from './v1/customSounds'; export * from './v1/customUserStatus'; export * from './v1/subscriptionsEndpoints'; export * from './v1/mailer'; diff --git a/packages/rest-typings/src/v1/customSounds.ts b/packages/rest-typings/src/v1/customSounds.ts new file mode 100644 index 0000000000000..ba87e72d0b215 --- /dev/null +++ b/packages/rest-typings/src/v1/customSounds.ts @@ -0,0 +1,19 @@ +import type { ICustomSound } from '@rocket.chat/core-typings'; + +import { ajv } from './Ajv'; + +type CustomSoundsGetOne = { _id: ICustomSound['_id'] }; + +const CustomSoundsGetOneSchema = { + type: 'object', + properties: { + _id: { + type: 'string', + minLength: 1, + }, + }, + required: ['_id'], + additionalProperties: false, +}; + +export const isCustomSoundsGetOneProps = ajv.compile(CustomSoundsGetOneSchema); From fc45f0e699b329d6b4bf9df44ac9e5dd8fd2ab06 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 23 Feb 2026 18:38:06 -0300 Subject: [PATCH 017/108] chore: update package versions to 8.3.0-develop across all relevant files --- apps/meteor/app/utils/rocketchat.info | 2 +- apps/meteor/package.json | 2 +- package.json | 2 +- packages/core-typings/package.json | 2 +- packages/rest-typings/package.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/meteor/app/utils/rocketchat.info b/apps/meteor/app/utils/rocketchat.info index f21130e88b6a1..5423a90c15f95 100644 --- a/apps/meteor/app/utils/rocketchat.info +++ b/apps/meteor/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "8.2.0-develop" + "version": "8.3.0-develop" } diff --git a/apps/meteor/package.json b/apps/meteor/package.json index af72549e69c3a..c8cd3ef402cfc 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/meteor", - "version": "8.2.0-develop", + "version": "8.3.0-develop", "private": true, "description": "The Ultimate Open Source WebChat Platform", "keywords": [ diff --git a/package.json b/package.json index 3e6b626d23017..44dd80ff3d1a8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rocket.chat", - "version": "8.2.0-develop", + "version": "8.3.0-develop", "private": true, "description": "Rocket.Chat Monorepo", "homepage": "https://github.com/RocketChat/Rocket.Chat#readme", diff --git a/packages/core-typings/package.json b/packages/core-typings/package.json index 19b1b681acb7e..818f309044683 100644 --- a/packages/core-typings/package.json +++ b/packages/core-typings/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package", "name": "@rocket.chat/core-typings", - "version": "8.2.0-develop", + "version": "8.3.0-develop", "private": true, "main": "./dist/index.js", "typings": "./dist/index.d.ts", diff --git a/packages/rest-typings/package.json b/packages/rest-typings/package.json index 724ba2246f553..2998d0dffbf16 100644 --- a/packages/rest-typings/package.json +++ b/packages/rest-typings/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/rest-typings", - "version": "8.2.0-develop", + "version": "8.3.0-develop", "main": "./dist/index.js", "typings": "./dist/index.d.ts", "files": [ From 722df6f60bc86c51b204e28a39acb3dc8710bdeb Mon Sep 17 00:00:00 2001 From: Ahmed Nasser Date: Mon, 23 Feb 2026 23:40:17 +0200 Subject: [PATCH 018/108] chore: Add OpenAPI Support to custom-user-status.list API (#36916) Co-authored-by: Guilherme Gazzo --- .changeset/sweet-terms-relax.md | 7 + .../app/api/server/v1/custom-user-status.ts | 141 ++++++++++++++---- packages/core-typings/src/Ajv.ts | 8 +- .../rest-typings/src/v1/customUserStatus.ts | 47 +----- 4 files changed, 128 insertions(+), 75 deletions(-) create mode 100644 .changeset/sweet-terms-relax.md diff --git a/.changeset/sweet-terms-relax.md b/.changeset/sweet-terms-relax.md new file mode 100644 index 0000000000000..8861e65f43250 --- /dev/null +++ b/.changeset/sweet-terms-relax.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/meteor': minor +'@rocket.chat/core-typings': minor +'@rocket.chat/rest-typings': minor +--- + +Add OpenAPI support for the Rocket.Chat custom-user-status.list API endpoints by migrating to a modern chained route definition syntax and utilizing shared AJV schemas for validation to enhance API documentation and ensure type safety through response validation diff --git a/apps/meteor/app/api/server/v1/custom-user-status.ts b/apps/meteor/app/api/server/v1/custom-user-status.ts index 037928cf1cdcf..4d4297cfe001c 100644 --- a/apps/meteor/app/api/server/v1/custom-user-status.ts +++ b/apps/meteor/app/api/server/v1/custom-user-status.ts @@ -1,46 +1,124 @@ +import type { ICustomUserStatus } from '@rocket.chat/core-typings'; import { CustomUserStatus } from '@rocket.chat/models'; -import { isCustomUserStatusListProps } from '@rocket.chat/rest-typings'; +import { ajv, validateUnauthorizedErrorResponse, validateBadRequestErrorResponse } from '@rocket.chat/rest-typings'; +import type { PaginatedRequest, PaginatedResult } from '@rocket.chat/rest-typings'; import { escapeRegExp } from '@rocket.chat/string-helpers'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { deleteCustomUserStatus } from '../../../user-status/server/methods/deleteCustomUserStatus'; import { insertOrUpdateUserStatus } from '../../../user-status/server/methods/insertOrUpdateUserStatus'; +import type { ExtractRoutesFromAPI } from '../ApiClass'; import { API } from '../api'; import { getPaginationItems } from '../helpers/getPaginationItems'; -API.v1.addRoute( - 'custom-user-status.list', - { authRequired: true, validateParams: isCustomUserStatusListProps }, - { - async get() { - const { offset, count } = await getPaginationItems(this.queryParams as Record); - const { sort, query } = await this.parseJsonQuery(); - - const { name, _id } = this.queryParams; - - const filter = { - ...query, - ...(name ? { name: { $regex: escapeRegExp(name as string), $options: 'i' } } : {}), - ...(_id ? { _id } : {}), - }; +type CustomUserStatusListProps = PaginatedRequest<{ name?: string; _id?: string; query?: string }>; - const { cursor, totalCount } = CustomUserStatus.findPaginated(filter, { - sort: sort || { name: 1 }, - skip: offset, - limit: count, - }); +const CustomUserStatusListSchema = { + type: 'object', + properties: { + count: { + type: 'number', + nullable: true, + }, + offset: { + type: 'number', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + name: { + type: 'string', + nullable: true, + }, + _id: { + type: 'string', + nullable: true, + }, + query: { + type: 'string', + nullable: true, + }, + }, + required: [], + additionalProperties: false, +}; - const [statuses, total] = await Promise.all([cursor.toArray(), totalCount]); +const isCustomUserStatusListProps = ajv.compile(CustomUserStatusListSchema); - return API.v1.success({ - statuses, - count: statuses.length, - offset, - total, - }); +const customUserStatusEndpoints = API.v1.get( + 'custom-user-status.list', + { + authRequired: true, + query: isCustomUserStatusListProps, + response: { + 200: ajv.compile< + PaginatedResult<{ + statuses: ICustomUserStatus[]; + }> + >({ + type: 'object', + properties: { + statuses: { + type: 'array', + items: { + $ref: '#/components/schemas/ICustomUserStatus', + }, + }, + count: { + type: 'number', + description: 'The number of custom user statuses returned in this response.', + }, + offset: { + type: 'number', + description: 'The number of custom user statuses that were skipped in this response.', + }, + total: { + type: 'number', + description: 'The total number of custom user statuses that match the query.', + }, + success: { + type: 'boolean', + enum: [true], + description: 'Indicates if the request was successful.', + }, + }, + required: ['success', 'statuses', 'count', 'offset', 'total'], + additionalProperties: false, + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, }, }, + async function action() { + const { offset, count } = await getPaginationItems(this.queryParams as Record); + const { sort, query } = await this.parseJsonQuery(); + + const { name, _id } = this.queryParams; + + const filter = { + ...query, + ...(name ? { name: { $regex: escapeRegExp(name as string), $options: 'i' } } : {}), + ...(_id ? { _id } : {}), + }; + + const { cursor, totalCount } = CustomUserStatus.findPaginated(filter, { + sort: sort || { name: 1 }, + skip: offset, + limit: count, + }); + + const [statuses, total] = await Promise.all([cursor.toArray(), totalCount]); + + return API.v1.success({ + statuses, + count: statuses.length, + offset, + total, + }); + }, ); API.v1.addRoute( @@ -127,3 +205,10 @@ API.v1.addRoute( }, }, ); + +export type CustomUserStatusEndpoints = ExtractRoutesFromAPI; + +declare module '@rocket.chat/rest-typings' { + // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface + interface Endpoints extends CustomUserStatusEndpoints {} +} diff --git a/packages/core-typings/src/Ajv.ts b/packages/core-typings/src/Ajv.ts index cea4a75b45e8f..eb91852edb6de 100644 --- a/packages/core-typings/src/Ajv.ts +++ b/packages/core-typings/src/Ajv.ts @@ -2,6 +2,7 @@ import typia from 'typia'; import type { CallHistoryItem } from './ICallHistoryItem'; import type { ICustomSound } from './ICustomSound'; +import type { ICustomUserStatus } from './ICustomUserStatus'; import type { IInvite } from './IInvite'; import type { IMessage } from './IMessage'; import type { IOAuthApps } from './IOAuthApps'; @@ -11,6 +12,11 @@ import type { SlashCommand } from './SlashCommands'; import type { IMediaCall } from './mediaCalls/IMediaCall'; export const schemas = typia.json.schemas< - [ISubscription | IInvite | ICustomSound | IMessage | IOAuthApps | IPermission | IMediaCall, CallHistoryItem, SlashCommand], + [ + ISubscription | IInvite | ICustomSound | IMessage | IOAuthApps | IPermission | IMediaCall, + CallHistoryItem, + ICustomUserStatus, + SlashCommand, + ], '3.0' >(); diff --git a/packages/rest-typings/src/v1/customUserStatus.ts b/packages/rest-typings/src/v1/customUserStatus.ts index 4c660bf46c6a7..742e16934e1ee 100644 --- a/packages/rest-typings/src/v1/customUserStatus.ts +++ b/packages/rest-typings/src/v1/customUserStatus.ts @@ -1,51 +1,6 @@ -import type { ICustomUserStatus, IUserStatus } from '@rocket.chat/core-typings'; - -import { ajv } from './Ajv'; -import type { PaginatedRequest } from '../helpers/PaginatedRequest'; -import type { PaginatedResult } from '../helpers/PaginatedResult'; - -type CustomUserStatusListProps = PaginatedRequest<{ name?: string; _id?: string; query?: string }>; - -const CustomUserStatusListSchema = { - type: 'object', - properties: { - count: { - type: 'number', - nullable: true, - }, - offset: { - type: 'number', - nullable: true, - }, - sort: { - type: 'string', - nullable: true, - }, - name: { - type: 'string', - nullable: true, - }, - _id: { - type: 'string', - nullable: true, - }, - query: { - type: 'string', - nullable: true, - }, - }, - required: [], - additionalProperties: false, -}; - -export const isCustomUserStatusListProps = ajv.compile(CustomUserStatusListSchema); +import type { ICustomUserStatus } from '@rocket.chat/core-typings'; export type CustomUserStatusEndpoints = { - '/v1/custom-user-status.list': { - GET: (params: CustomUserStatusListProps) => PaginatedResult<{ - statuses: IUserStatus[]; - }>; - }; '/v1/custom-user-status.create': { POST: (params: { name: string; statusType?: string }) => { customUserStatus: ICustomUserStatus; From c5b2990d410bc3ac758855cee0b5075056b59af4 Mon Sep 17 00:00:00 2001 From: Pelle <52049523+farapholch@users.noreply.github.com> Date: Mon, 23 Feb 2026 22:58:49 +0100 Subject: [PATCH 019/108] fix(i18n): correct Swedish translation for User_joined_the_team (#38904) Co-authored-by: Kevin Aleman --- packages/i18n/src/locales/sv.i18n.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/i18n/src/locales/sv.i18n.json b/packages/i18n/src/locales/sv.i18n.json index b7badc0105f80..b964ba9d2fce4 100644 --- a/packages/i18n/src/locales/sv.i18n.json +++ b/packages/i18n/src/locales/sv.i18n.json @@ -5381,7 +5381,7 @@ "User_joined_team": "anslöt till teamet", "User_joined_the_channel": "anslöt till kanalen", "User_joined_the_conversation": "anslöt till konversationen", - "User_joined_the_team": "anslut till teamet", + "User_joined_the_team": "anslöt till teamet", "User_left": "Har lämnat kanalen.", "User_left_team": "lämnade teamet", "User_left_this_channel": "lämnade kanalen", From 98d9b78cab6ab0a67d200a7bf05e4c724b25266a Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 23 Feb 2026 19:10:38 -0300 Subject: [PATCH 020/108] chore: add merge_group trigger to CI workflow for improved merge handling --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 389be8c766bdf..fc2547b873ab0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,6 +7,7 @@ on: branches: '**' paths-ignore: - '**.md' + merge_group: push: branches: - develop From 831cfe620623bd9ec1fd8c24837fb449d4e24729 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Mon, 23 Feb 2026 19:41:47 -0300 Subject: [PATCH 021/108] chore: sanitize Docker tags in CI workflow to replace '/' with '-' for compatibility --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc2547b873ab0..fed5441d44cb4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -117,6 +117,8 @@ jobs: else DOCKER_TAG=$GITHUB_REF_NAME fi + # Docker tags cannot contain '/'; merge queue refs do (e.g. gh-readonly-queue/develop/pr-123-sha) + DOCKER_TAG="${DOCKER_TAG//\//-}" echo "DOCKER_TAG: ${DOCKER_TAG}" echo "gh-docker-tag=${DOCKER_TAG}" >> $GITHUB_OUTPUT @@ -870,6 +872,7 @@ jobs: # 'develop' or 'tag' DOCKER_TAG=$GITHUB_REF_NAME + DOCKER_TAG="${DOCKER_TAG//\//-}" declare -a TAGS=() From d907668d4c3aef8f83976fbb070c192f51833aa8 Mon Sep 17 00:00:00 2001 From: Chetan Agarwal Date: Tue, 24 Feb 2026 03:37:16 +0530 Subject: [PATCH 022/108] refactor(user-info): remove `any` usage and enforce strict typing in UserInfoActions (#38859) --- .../views/room/contextualBar/UserInfo/UserInfoActions.tsx | 6 +++--- .../client/views/room/hooks/useUserInfoActions/index.ts | 1 + .../room/hooks/useUserInfoActions/useUserInfoActions.ts | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/meteor/client/views/room/contextualBar/UserInfo/UserInfoActions.tsx b/apps/meteor/client/views/room/contextualBar/UserInfo/UserInfoActions.tsx index 929c1d9c7570c..082e5c867306d 100644 --- a/apps/meteor/client/views/room/contextualBar/UserInfo/UserInfoActions.tsx +++ b/apps/meteor/client/views/room/contextualBar/UserInfo/UserInfoActions.tsx @@ -8,6 +8,7 @@ import { useTranslation } from 'react-i18next'; import { UserInfoAction } from '../../../../components/UserInfo'; import { useMemberExists } from '../../../hooks/useMemberExists'; +import type { UserInfoAction as UserInfoActionType } from '../../hooks/useUserInfoActions'; import { useUserInfoActions } from '../../hooks/useUserInfoActions'; type UserInfoActionsProps = { @@ -58,10 +59,9 @@ const UserInfoActions = ({ user, rid, isInvited, backToList }: UserInfoActionsPr ); }, [menuOptions, t]); - // TODO: sanitize Action type to avoid any const actions = useMemo(() => { - const mapAction = ([key, { content, title, icon, onClick }]: any): ReactElement => ( - + const mapAction = ([key, action]: [string, UserInfoActionType]): ReactElement => ( + ); return [...actionsDefinition.map(mapAction), menu].filter(Boolean); diff --git a/apps/meteor/client/views/room/hooks/useUserInfoActions/index.ts b/apps/meteor/client/views/room/hooks/useUserInfoActions/index.ts index 44ce5ef6c2da5..931c3ee9dd51a 100644 --- a/apps/meteor/client/views/room/hooks/useUserInfoActions/index.ts +++ b/apps/meteor/client/views/room/hooks/useUserInfoActions/index.ts @@ -1 +1,2 @@ export { useUserInfoActions } from './useUserInfoActions'; +export type { UserInfoAction, UserMenuAction } from './useUserInfoActions'; diff --git a/apps/meteor/client/views/room/hooks/useUserInfoActions/useUserInfoActions.ts b/apps/meteor/client/views/room/hooks/useUserInfoActions/useUserInfoActions.ts index f9baa1f736680..ca0c8b753ddbd 100644 --- a/apps/meteor/client/views/room/hooks/useUserInfoActions/useUserInfoActions.ts +++ b/apps/meteor/client/views/room/hooks/useUserInfoActions/useUserInfoActions.ts @@ -44,7 +44,7 @@ type UserInfoActionWithContent = { export type UserInfoAction = UserInfoActionWithContent | UserInfoActionWithOnlyIcon; -type UserMenuAction = { +export type UserMenuAction = { id: string; title: string; items: GenericMenuItemProps[]; @@ -66,7 +66,7 @@ export const useUserInfoActions = ({ size = 2, isMember, isInvited, -}: UserInfoActionsParams): { actions: [string, UserInfoAction][]; menuActions: any | undefined } => { +}: UserInfoActionsParams): { actions: [string, UserInfoAction][]; menuActions: UserMenuAction | undefined } => { const addUser = useAddUserAction(user, rid, reload); const blockUser = useBlockUserAction(user, rid); const changeLeader = useChangeLeaderAction(user, rid); From 2d95c09886baaa68c551e982e95a99a156e6ba54 Mon Sep 17 00:00:00 2001 From: Ahmadshah Donishyar Date: Tue, 24 Feb 2026 03:57:41 +0430 Subject: [PATCH 023/108] fix(ux): Align moderation console skeleton columns with header (#38716) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: Guilherme Gazzo --- .../client/views/admin/moderation/ModerationConsoleTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/client/views/admin/moderation/ModerationConsoleTable.tsx b/apps/meteor/client/views/admin/moderation/ModerationConsoleTable.tsx index 61e2af1211690..51f646d7ab0af 100644 --- a/apps/meteor/client/views/admin/moderation/ModerationConsoleTable.tsx +++ b/apps/meteor/client/views/admin/moderation/ModerationConsoleTable.tsx @@ -105,7 +105,7 @@ const ModerationConsoleTable = () => { {isLoading && ( {headers} - {isLoading && } + {isLoading && } )} {isSuccess && data.reports.length > 0 && ( From d0c3b3d7aa1da7af7c6fe031dc30333c151b8a98 Mon Sep 17 00:00:00 2001 From: "khizar (RinX)" <109973520+Khizarshah01@users.noreply.github.com> Date: Tue, 24 Feb 2026 08:07:05 +0530 Subject: [PATCH 024/108] refactor: remove redundant parent condition in ParentRoom (#38724) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: Douglas Fabris Co-authored-by: Guilherme Gazzo --- .../client/views/room/Header/ParentRoom/ParentRoom.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/apps/meteor/client/views/room/Header/ParentRoom/ParentRoom.tsx b/apps/meteor/client/views/room/Header/ParentRoom/ParentRoom.tsx index 38c6cc5f86597..2fbc31cd9f01f 100644 --- a/apps/meteor/client/views/room/Header/ParentRoom/ParentRoom.tsx +++ b/apps/meteor/client/views/room/Header/ParentRoom/ParentRoom.tsx @@ -4,12 +4,6 @@ import ParentDiscussion from './ParentDiscussion'; import ParentTeam from './ParentTeam'; const ParentRoom = ({ room }: { room: IRoom }) => { - const parentRoomId = Boolean(room.prid || (room.teamId && !room.teamMain)); - - if (!parentRoomId) { - return null; - } - if (room.prid) { return ; } @@ -17,6 +11,8 @@ const ParentRoom = ({ room }: { room: IRoom }) => { if (room.teamId && !room.teamMain) { return ; } + + return null; }; export default ParentRoom; From 50e0f906348160d666dc19da1cdf1f78c6e9e40b Mon Sep 17 00:00:00 2001 From: "khizar (RinX)" <109973520+Khizarshah01@users.noreply.github.com> Date: Tue, 24 Feb 2026 09:26:30 +0530 Subject: [PATCH 025/108] fix: limit omnichannel webhook response size (#38944) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: Kevin Aleman Co-authored-by: Guilherme Gazzo --- .changeset/tender-papayas-jam.md | 5 +++++ apps/meteor/app/livechat/server/api/v1/webhooks.ts | 1 + apps/meteor/app/livechat/server/lib/webhooks.ts | 1 + 3 files changed, 7 insertions(+) create mode 100644 .changeset/tender-papayas-jam.md diff --git a/.changeset/tender-papayas-jam.md b/.changeset/tender-papayas-jam.md new file mode 100644 index 0000000000000..d9e85e6d29425 --- /dev/null +++ b/.changeset/tender-papayas-jam.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Limits Omnichannel webhook maximum response size to 10mb. diff --git a/apps/meteor/app/livechat/server/api/v1/webhooks.ts b/apps/meteor/app/livechat/server/api/v1/webhooks.ts index 4a5fdb50f7e44..276a910502d69 100644 --- a/apps/meteor/app/livechat/server/api/v1/webhooks.ts +++ b/apps/meteor/app/livechat/server/api/v1/webhooks.ts @@ -66,6 +66,7 @@ API.v1.addRoute( body: sampleData, // SECURITY: Webhooks can only be configured by users with enough privileges. It's ok to disable this check here. ignoreSsrfValidation: true, + size: 10 * 1024 * 1024, } as ExtendedFetchOptions; const webhookUrl = settings.get('Livechat_webhookUrl'); diff --git a/apps/meteor/app/livechat/server/lib/webhooks.ts b/apps/meteor/app/livechat/server/lib/webhooks.ts index b0d2cd94f80e2..661b428cc7cb8 100644 --- a/apps/meteor/app/livechat/server/lib/webhooks.ts +++ b/apps/meteor/app/livechat/server/lib/webhooks.ts @@ -29,6 +29,7 @@ export async function sendRequest( timeout, // SECURITY: Webhooks can only be configured by users with enough privileges. It's ok to disable this check here. ignoreSsrfValidation: true, + size: 10 * 1024 * 1024, }); if (result.status === 200) { From ddc0ed34b03072362d166f1160104a9332b362e8 Mon Sep 17 00:00:00 2001 From: Ahmed Nasser Date: Tue, 24 Feb 2026 14:01:58 +0200 Subject: [PATCH 026/108] chore: Add OpenAPI Support to push.test API (#36882) --- .changeset/spicy-drinks-carry.md | 6 ++++ apps/meteor/app/api/server/v1/push.ts | 47 ++++++++++++++++++++------- packages/rest-typings/src/v1/push.ts | 5 --- 3 files changed, 42 insertions(+), 16 deletions(-) create mode 100644 .changeset/spicy-drinks-carry.md diff --git a/.changeset/spicy-drinks-carry.md b/.changeset/spicy-drinks-carry.md new file mode 100644 index 0000000000000..1b2119694d4cc --- /dev/null +++ b/.changeset/spicy-drinks-carry.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/rest-typings": patch +--- + +Add OpenAPI support for the Rocket.Chat push.test API endpoints by migrating to a modern chained route definition syntax and utilizing shared AJV schemas for validation to enhance API documentation and ensure type safety through response validation. diff --git a/apps/meteor/app/api/server/v1/push.ts b/apps/meteor/app/api/server/v1/push.ts index f84076d83d51e..5e3072759f7e7 100644 --- a/apps/meteor/app/api/server/v1/push.ts +++ b/apps/meteor/app/api/server/v1/push.ts @@ -1,6 +1,7 @@ import type { IAppsTokens } from '@rocket.chat/core-typings'; import { Messages, AppsTokens, Users, Rooms, Settings } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; +import { ajv, validateBadRequestErrorResponse, validateUnauthorizedErrorResponse } from '@rocket.chat/rest-typings'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; @@ -9,6 +10,7 @@ import { canAccessRoomAsync } from '../../../authorization/server/functions/canA import { pushUpdate } from '../../../push/server/methods'; import PushNotification from '../../../push-notifications/server/lib/PushNotification'; import { settings } from '../../../settings/server'; +import type { ExtractRoutesFromAPI } from '../ApiClass'; import { API } from '../api'; API.v1.addRoute( @@ -135,7 +137,7 @@ API.v1.addRoute( }, ); -API.v1.addRoute( +const pushEndpoints = API.v1.post( 'push.test', { authRequired: true, @@ -144,17 +146,40 @@ API.v1.addRoute( intervalTimeInMS: 1000, }, permissionsRequired: ['test-push-notifications'], + body: ajv.compile({ type: 'object', additionalProperties: false }), + response: { + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + 200: ajv.compile<{ tokensCount: number }>({ + type: 'object', + properties: { + tokensCount: { type: 'integer' }, + success: { + type: 'boolean', + enum: [true], + }, + }, + required: ['tokensCount', 'success'], + additionalProperties: false, + }), + }, }, - { - async post() { - if (settings.get('Push_enable') !== true) { - throw new Meteor.Error('error-push-disabled', 'Push is disabled', { - method: 'push_test', - }); - } - const tokensCount = await executePushTest(this.userId, this.user.username); - return API.v1.success({ tokensCount }); - }, + async function action() { + if (settings.get('Push_enable') !== true) { + throw new Meteor.Error('error-push-disabled', 'Push is disabled', { + method: 'push_test', + }); + } + + const tokensCount = await executePushTest(this.userId, this.user.username); + return API.v1.success({ tokensCount }); }, ); + +export type PushEndpoints = ExtractRoutesFromAPI; + +declare module '@rocket.chat/rest-typings' { + // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface + interface Endpoints extends PushEndpoints {} +} diff --git a/packages/rest-typings/src/v1/push.ts b/packages/rest-typings/src/v1/push.ts index 0551ce0ea8149..5fdf4172c288a 100644 --- a/packages/rest-typings/src/v1/push.ts +++ b/packages/rest-typings/src/v1/push.ts @@ -68,9 +68,4 @@ export type PushEndpoints = { defaultPushGateway: boolean; }; }; - '/v1/push.test': { - POST: () => { - tokensCount: boolean; - }; - }; }; From 803b8075514de54c9ff34ba0c9aa3ee5fc3bbe61 Mon Sep 17 00:00:00 2001 From: Ahmed Nasser Date: Tue, 24 Feb 2026 14:05:40 +0200 Subject: [PATCH 027/108] chore: Add OpenAPI Support to e2e.getUsersOfRoomWithoutKey API (#36786) --- .changeset/nice-squids-smoke.md | 6 + apps/meteor/app/api/server/v1/e2e.ts | 124 ++++++++++++------ packages/rest-typings/src/index.ts | 1 - packages/rest-typings/src/v1/e2e.ts | 5 - .../e2eGetUsersOfRoomWithoutKeyParamsGET.ts | 20 --- 5 files changed, 92 insertions(+), 64 deletions(-) create mode 100644 .changeset/nice-squids-smoke.md delete mode 100644 packages/rest-typings/src/v1/e2e/e2eGetUsersOfRoomWithoutKeyParamsGET.ts diff --git a/.changeset/nice-squids-smoke.md b/.changeset/nice-squids-smoke.md new file mode 100644 index 0000000000000..a71f10d915f97 --- /dev/null +++ b/.changeset/nice-squids-smoke.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/rest-typings": patch +--- + +Add OpenAPI support for the Rocket.Chat e2e.getUsersOfRoomWithoutKey endpoints by migrating to a modern chained route definition syntax and utilizing shared AJV schemas for validation to enhance API documentation and ensure type safety through response validation. diff --git a/apps/meteor/app/api/server/v1/e2e.ts b/apps/meteor/app/api/server/v1/e2e.ts index bfc70ba8b31a1..61a3b8075074c 100644 --- a/apps/meteor/app/api/server/v1/e2e.ts +++ b/apps/meteor/app/api/server/v1/e2e.ts @@ -1,9 +1,9 @@ +import type { IUser } from '@rocket.chat/core-typings'; import { Subscriptions, Users } from '@rocket.chat/models'; import { ajv, validateUnauthorizedErrorResponse, validateBadRequestErrorResponse, - ise2eGetUsersOfRoomWithoutKeyParamsGET, ise2eSetUserPublicAndPrivateKeysParamsPOST, ise2eUpdateGroupKeyParamsPOST, isE2EProvideUsersGroupKeyProps, @@ -34,6 +34,10 @@ type E2eSetRoomKeyIdProps = { keyID: string; }; +type e2eGetUsersOfRoomWithoutKeyParamsGET = { + rid: string; +}; + const E2eSetRoomKeyIdSchema = { type: 'object', properties: { @@ -48,60 +52,104 @@ const E2eSetRoomKeyIdSchema = { additionalProperties: false, }; +const e2eGetUsersOfRoomWithoutKeyParamsGETSchema = { + type: 'object', + properties: { + rid: { + type: 'string', + }, + }, + additionalProperties: false, + required: ['rid'], +}; + const isE2eSetRoomKeyIdProps = ajv.compile(E2eSetRoomKeyIdSchema); -const e2eEndpoints = API.v1.post( - 'e2e.setRoomKeyID', - { - authRequired: true, - body: isE2eSetRoomKeyIdProps, - response: { - 400: validateBadRequestErrorResponse, - 401: validateUnauthorizedErrorResponse, - 200: ajv.compile({ - type: 'object', - properties: { - success: { type: 'boolean', enum: [true] }, - }, - required: ['success'], - }), +const ise2eGetUsersOfRoomWithoutKeyParamsGET = ajv.compile( + e2eGetUsersOfRoomWithoutKeyParamsGETSchema, +); + +const e2eEndpoints = API.v1 + .post( + 'e2e.setRoomKeyID', + { + authRequired: true, + body: isE2eSetRoomKeyIdProps, + response: { + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + 200: ajv.compile({ + type: 'object', + properties: { + success: { type: 'boolean', enum: [true] }, + }, + required: ['success'], + }), + }, }, - }, - async function action() { - const { rid, keyID } = this.bodyParams; + async function action() { + const { rid, keyID } = this.bodyParams; - await setRoomKeyIDMethod(this.userId, rid, keyID); + await setRoomKeyIDMethod(this.userId, rid, keyID); - return API.v1.success(); - }, -); + return API.v1.success(); + }, + ) + .get( + 'e2e.getUsersOfRoomWithoutKey', + { + authRequired: true, + query: ise2eGetUsersOfRoomWithoutKeyParamsGET, + response: { + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + 200: ajv.compile<{ + users: Pick[]; + }>({ + type: 'object', + properties: { + users: { + type: 'array', + items: { + type: 'object', + properties: { + _id: { type: 'string' }, + e2e: { + type: 'object', + properties: { + private_key: { type: 'string' }, + public_key: { type: 'string' }, + }, + }, + }, + required: ['_id'], + }, + }, + success: { type: 'boolean', enum: [true] }, + }, + required: ['users', 'success'], + }), + }, + }, -API.v1.addRoute( - 'e2e.fetchMyKeys', - { - authRequired: true, - }, - { - async get() { - const result = await Users.fetchKeysByUserId(this.userId); + async function action() { + const { rid } = this.queryParams; + + const result = await getUsersOfRoomWithoutKeyMethod(this.userId, rid); return API.v1.success(result); }, - }, -); + ); API.v1.addRoute( - 'e2e.getUsersOfRoomWithoutKey', + 'e2e.fetchMyKeys', { authRequired: true, - validateParams: ise2eGetUsersOfRoomWithoutKeyParamsGET, }, { async get() { - const { rid } = this.queryParams; - - const result = await getUsersOfRoomWithoutKeyMethod(this.userId, rid); + const result = await Users.fetchKeysByUserId(this.userId); return API.v1.success(result); }, diff --git a/packages/rest-typings/src/index.ts b/packages/rest-typings/src/index.ts index 5569660b41864..278c6892afa75 100644 --- a/packages/rest-typings/src/index.ts +++ b/packages/rest-typings/src/index.ts @@ -243,7 +243,6 @@ export * from './v1/server-events'; export * from './v1/autotranslate/AutotranslateGetSupportedLanguagesParamsGET'; export * from './v1/autotranslate/AutotranslateSaveSettingsParamsPOST'; export * from './v1/autotranslate/AutotranslateTranslateMessageParamsPOST'; -export * from './v1/e2e/e2eGetUsersOfRoomWithoutKeyParamsGET'; export * from './v1/e2e/e2eSetUserPublicAndPrivateKeysParamsPOST'; export * from './v1/e2e/e2eUpdateGroupKeyParamsPOST'; export * from './v1/e2e'; diff --git a/packages/rest-typings/src/v1/e2e.ts b/packages/rest-typings/src/v1/e2e.ts index 6205a31e87640..841fc7b730240 100644 --- a/packages/rest-typings/src/v1/e2e.ts +++ b/packages/rest-typings/src/v1/e2e.ts @@ -149,11 +149,6 @@ export type E2eEndpoints = { '/v1/e2e.setUserPublicAndPrivateKeys': { POST: (params: E2eSetUserPublicAndPrivateKeysProps) => void; }; - '/v1/e2e.getUsersOfRoomWithoutKey': { - GET: (params: E2eGetUsersOfRoomWithoutKeyProps) => { - users: Pick[]; - }; - }; '/v1/e2e.updateGroupKey': { POST: (params: E2eUpdateGroupKeyProps) => void; }; diff --git a/packages/rest-typings/src/v1/e2e/e2eGetUsersOfRoomWithoutKeyParamsGET.ts b/packages/rest-typings/src/v1/e2e/e2eGetUsersOfRoomWithoutKeyParamsGET.ts deleted file mode 100644 index 071bc32e8bdbf..0000000000000 --- a/packages/rest-typings/src/v1/e2e/e2eGetUsersOfRoomWithoutKeyParamsGET.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ajv } from '../Ajv'; - -export type e2eGetUsersOfRoomWithoutKeyParamsGET = { - rid: string; -}; - -const e2eGetUsersOfRoomWithoutKeyParamsGETSchema = { - type: 'object', - properties: { - rid: { - type: 'string', - }, - }, - additionalProperties: false, - required: ['rid'], -}; - -export const ise2eGetUsersOfRoomWithoutKeyParamsGET = ajv.compile( - e2eGetUsersOfRoomWithoutKeyParamsGETSchema, -); From 139e9dc486a743181b80066fdd90c306e3bce2bc Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 24 Feb 2026 09:44:44 -0300 Subject: [PATCH 028/108] =?UTF-8?q?chore:=20add=20agents=20for=20bug=20res?= =?UTF-8?q?olution,=20feature=20development,=20and=20refact=E2=80=A6=20(#3?= =?UTF-8?q?8948)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../copilot-agents/bug-resolution-agent.md | 212 +++++++++++ .../feature-development-agent.md | 335 ++++++++++++++++++ .github/copilot-agents/refactor-agent.md | 261 ++++++++++++++ .github/workflows/todo.yml | 32 ++ 4 files changed, 840 insertions(+) create mode 100644 .github/copilot-agents/bug-resolution-agent.md create mode 100644 .github/copilot-agents/feature-development-agent.md create mode 100644 .github/copilot-agents/refactor-agent.md create mode 100644 .github/workflows/todo.yml diff --git a/.github/copilot-agents/bug-resolution-agent.md b/.github/copilot-agents/bug-resolution-agent.md new file mode 100644 index 0000000000000..45af5ac8ca190 --- /dev/null +++ b/.github/copilot-agents/bug-resolution-agent.md @@ -0,0 +1,212 @@ +--- +name: Bug Resolution Agent +description: | + A focused agent that resolves GitHub issues by applying minimal, test-driven fixes. + It prioritizes reproducible tests (when feasible), enforces lint and TypeScript compliance, + and avoids refactoring or unrelated changes. +--- + +# Bug Resolution Agent + +## Purpose + +This agent resolves bugs in a precise and minimal manner. +Its goal is to: + +- Reproduce the issue (preferably with an automated test) +- Apply the smallest possible fix +- Ensure quality gates (tests, lint, TypeScript) pass +- Create a clear changeset +- Keep the PR easy to review + +The agent must **not introduce refactors, performance optimizations, or scope expansion**. + +--- + +## Operating Principles + +1. **Minimal Surface Area** + - Only modify what is strictly necessary to resolve the issue. + - Do not refactor unrelated code. + - Do not introduce structural improvements unless required to fix the bug. + +2. **Test-First When Feasible** + - If the issue can be reproduced via automated test (especially API or black-box behavior), write a failing test first. + - The test must fail before the fix and pass after the fix. + +3. **Quality Gates Are Mandatory** + - All existing tests must pass. + - Lint must pass with no new warnings or errors. + - TypeScript type checking must pass without errors. + - Build must succeed. + +4. **Scope Discipline** + - If additional problems are discovered, do not fix them in the same PR. + - Document them as TODOs for future issues (see Section: Documenting Out-of-Scope Findings). + +--- + +## Documenting Out-of-Scope Findings + +When you discover problems outside the current scope during your work, **do not fix them**. Instead, create a detailed TODO comment or document them in the PR description so they can become separate issues. + +### TODO Format + +Add a TODO comment in the code near where the problem was found. + +**Type prefixes** (same as PR conventions): +- `bug` - A bug that needs to be fixed +- `feat` - A new feature opportunity +- `refactor` - Code that needs refactoring +- `chore` - Maintenance tasks (dependencies, configs, etc.) +- `test` - Missing or incomplete tests + +**Optional labels** (GitHub labels in brackets): +- `[security]` - Security-related issues +- `[performance]` - Performance improvements +- `[a11y]` - Accessibility issues +- `[i18n]` - Internationalization issues +- `[breaking]` - Breaking changes +- Any other GitHub label relevant to the issue + +```typescript +// TODO: type [optional-label] +// Problem: +// Location: +// Impact: +// Suggested fix: +// Discovered while: +``` + +### Example + +```typescript +// TODO: bug [high-priority] Race condition in message delivery +// Problem: When multiple messages are sent rapidly, the order is not guaranteed +// due to async handling without proper sequencing. +// Location: apps/meteor/server/services/messages/sendMessage.ts - sendToChannel() +// Impact: Medium - Users may see messages out of order in high-traffic channels +// Suggested fix: Implement a message queue with sequence numbers or use +// optimistic locking on the channel's lastMessageAt timestamp. +// Discovered while: Fixing #12345 - Message duplication bug +``` + +### In PR Description + +Also list discovered issues in the PR description under a "Discovered Issues" section: + +```markdown +## Discovered Issues (Out of Scope) + +The following issues were discovered during this work and should be addressed in separate PRs: + +- `bug` [high-priority]: **Race condition in message delivery** - See TODO in `sendMessage.ts:142` +- `test`: **Missing input validation tests** - See TODO in `userController.ts:87` +``` + +--- + +## Step-by-Step Execution Flow + +### 1. Analyze the Issue + +- Carefully read the issue description. +- Identify: + - Expected behavior + - Actual behavior + - Reproduction steps +- Do not assume undocumented requirements. + +--- + +### 2. Determine Test Feasibility + +- Can the bug be reproduced through: + - Unit tests? + - Integration tests? + - API black-box tests? + +If **yes** → proceed to Step 3. +If **no** → proceed directly to Step 4. + +--- + +### 3. Write a Failing Test (Preferred Path) + +- Implement a test that reproduces the bug. +- The test must: + - Reflect the reported behavior + - Fail under the current implementation +- Use the project's existing test framework and conventions. +- Avoid introducing new testing patterns unless strictly required. + +--- + +### 4. Apply the Minimal Fix + +- Implement the smallest change that resolves the failing behavior. +- Do not: + - Refactor unrelated modules + - Rename symbols without necessity + - Change formatting beyond what lint enforces + - Improve performance unless directly tied to the bug + +--- + +### 5. Validate the Fix + +Ensure: + +- The newly created test passes. +- All existing tests pass. +- Lint passes. +- TypeScript compilation passes. +- Build succeeds. + +If any of these fail, adjust only what is required to restore compliance. + +--- + +### 6. Create a Changeset + +Create a concise changeset entry including: + +- What was broken +- What was changed +- How it was validated (test reference) + +Keep it factual and objective. + +--- + +### 7. Open the Pull Request + +The PR must: + +- Clearly reference the original issue. +- Highlight the test added (if applicable). +- Describe the minimal fix applied. +- Avoid mentioning improvements outside the issue scope. + +--- + +## Non-Goals + +This agent must NOT: + +- Perform refactoring +- Improve unrelated code quality +- Introduce stylistic changes +- Expand scope beyond the issue +- Combine multiple bug fixes in one PR + +--- + +## Success Criteria + +A successful run results in: + +- A minimal diff +- A reproducible test (when feasible) +- Passing CI (tests, lint, TypeScript) +- A clear and review-friendly Pull Request diff --git a/.github/copilot-agents/feature-development-agent.md b/.github/copilot-agents/feature-development-agent.md new file mode 100644 index 0000000000000..f7df896de6dcc --- /dev/null +++ b/.github/copilot-agents/feature-development-agent.md @@ -0,0 +1,335 @@ +--- +name: Feature Development Agent +description: | + A comprehensive agent that implements new features following best practices, + with proper planning, testing, documentation, and incremental delivery. + It ensures features are well-designed, tested, and maintainable. +--- + +# Feature Development Agent + +## Purpose + +This agent implements new features in a structured and maintainable way. +Its goal is to: + +- Understand and clarify feature requirements +- Design a clean, extensible implementation +- Write comprehensive tests for new functionality +- Ensure quality gates pass +- Create well-documented, reviewable PRs +- Follow existing patterns and conventions + +The agent must **focus only on the specified feature scope** and avoid scope creep. + +--- + +## Operating Principles + +1. **Requirements First** + - Fully understand the feature before writing code. + - Clarify ambiguities before implementation. + - Define acceptance criteria upfront. + +2. **Design Before Code** + - Plan the architecture and approach. + - Consider edge cases and error handling. + - Identify integration points with existing code. + +3. **Test-Driven Development** + - Write tests that define expected behavior. + - Tests should cover happy paths and edge cases. + - Tests serve as living documentation. + +4. **Quality Gates Are Mandatory** + - All tests (new and existing) must pass. + - Lint must pass with no new warnings or errors. + - TypeScript type checking must pass without errors. + - Build must succeed. + +5. **Incremental Delivery** + - Break features into deliverable increments. + - Each increment should be functional and valuable. + - Prefer feature flags for large features. + +6. **Consistency** + - Follow existing code patterns and conventions. + - Maintain consistency with the codebase style. + - Reuse existing utilities and components. + +7. **Scope Discipline** + - Focus only on the specified feature requirements. + - Do not fix unrelated bugs discovered during implementation. + - Do not refactor existing code beyond what's needed for the feature. + - Document discovered problems as TODOs for future issues (see Section: Documenting Out-of-Scope Findings). + +--- + +## Documenting Out-of-Scope Findings + +When you discover bugs, technical debt, or improvement opportunities outside the current feature scope, **do not fix them**. Instead, create a detailed TODO comment or document them in the PR description so they can become separate issues. + +### TODO Format + +Add a TODO comment in the code near where the problem was found. + +**Type prefixes** (same as PR conventions): +- `bug` - A bug that needs to be fixed +- `feat` - A new feature opportunity +- `refactor` - Code that needs refactoring +- `chore` - Maintenance tasks (dependencies, configs, etc.) +- `test` - Missing or incomplete tests + +**Optional labels** (GitHub labels in brackets): +- `[security]` - Security-related issues +- `[performance]` - Performance improvements +- `[a11y]` - Accessibility issues +- `[i18n]` - Internationalization issues +- `[breaking]` - Breaking changes +- Any other GitHub label relevant to the issue + +```typescript +// TODO: type [optional-label] +// Problem: +// Location: +// Impact: +// Suggested fix: +// Discovered while: +``` + +### Example + +```typescript +// TODO: bug [security] Missing rate limiting on user search endpoint +// Problem: The /api/v1/users.search endpoint has no rate limiting, +// allowing potential abuse through rapid sequential requests. +// Location: apps/meteor/app/api/server/v1/users.ts - searchUsers() +// Impact: High - Security vulnerability, potential DoS vector +// Suggested fix: Add rate limiting middleware similar to login endpoints, +// suggest 10 requests per minute per user. +// Discovered while: Implementing user mention autocomplete feature #54321 +``` + +### In PR Description + +Also list discovered issues in the PR description under a "Discovered Issues" section: + +```markdown +## Discovered Issues (Out of Scope) + +The following issues were discovered during this feature implementation and should be addressed in separate PRs: + +- `bug` [security]: **Missing rate limiting on user search** - See TODO in `users.ts:234` +- `refactor`: **Inconsistent error handling in API** - See TODO in `channels.ts:156` +- `chore`: **Outdated TypeScript types** - See TODO in `types/user.d.ts:12` +``` + +--- + +## Step-by-Step Execution Flow + +### 1. Analyze Feature Requirements + +- Carefully read the feature request/specification. +- Identify: + - Core functionality + - User stories/use cases + - Acceptance criteria + - Non-functional requirements (performance, security) +- List any unclear or ambiguous requirements. +- Do not assume undocumented behavior. + +--- + +### 2. Research Existing Codebase + +- Identify related existing functionality. +- Find: + - Similar features to reference + - Existing patterns to follow + - Utilities and helpers to reuse + - Integration points +- Understand the architectural context. + +--- + +### 3. Design the Implementation + +Create a technical design covering: + +- **Data Model**: New types, interfaces, schemas +- **API Design**: Endpoints, methods, signatures +- **Component Structure**: Files, modules, classes +- **State Management**: How data flows +- **Error Handling**: Expected failures and responses +- **Security**: Authentication, authorization, validation +- **Performance**: Considerations for scale + +--- + +### 4. Define Test Strategy + +Plan tests at multiple levels: + +- **Unit Tests**: Individual functions and components +- **Integration Tests**: Component interactions +- **API Tests**: Endpoint behavior (if applicable) +- **E2E Tests**: User workflows (if applicable) + +Define: +- Happy path scenarios +- Edge cases +- Error scenarios +- Boundary conditions + +--- + +### 5. Implement Incrementally + +For each implementation increment: + +#### 5.1 Write Tests First +- Define expected behavior through tests. +- Tests should initially fail (TDD red phase). + +#### 5.2 Implement the Code +- Write the minimum code to pass tests. +- Follow existing patterns and conventions. +- Add proper TypeScript types. +- Include error handling. + +#### 5.3 Refactor if Needed +- Clean up the implementation. +- Ensure code quality standards are met. +- Keep tests passing. + +#### 5.4 Verify Quality Gates +- Run all tests. +- Run lint. +- Run TypeScript compilation. +- Build the project. + +--- + +### 6. Add Documentation + +Document the new feature: + +- **Code Comments**: Complex logic explanation +- **JSDoc/TSDoc**: Public APIs and functions +- **README Updates**: If feature affects setup/usage +- **API Documentation**: New endpoints (if applicable) +- **Inline Documentation**: Configuration options + +--- + +### 7. Create a Changeset + +Create a changeset entry including: + +- Feature name and description +- Key functionality added +- Any breaking changes +- Migration notes (if applicable) + +--- + +### 8. Open the Pull Request + +The PR must include: + +- Clear description of the feature +- Link to the original issue/specification +- Summary of implementation approach +- List of new tests added +- Screenshots/recordings (if UI changes) +- Testing instructions for reviewers +- Any deployment considerations + +--- + +## Implementation Guidelines + +### Code Structure +- Place code in appropriate directories following project conventions. +- Create new files for distinct functionality. +- Keep files focused and reasonably sized. + +### Type Safety +- Define explicit types for all new code. +- Avoid `any` types. +- Use generics where appropriate. +- Export types that consumers need. + +### Error Handling +- Handle all expected error cases. +- Provide meaningful error messages. +- Use appropriate error types/codes. +- Log errors appropriately. + +### Security +- Validate all inputs. +- Sanitize outputs where needed. +- Follow authentication/authorization patterns. +- Never expose sensitive data. + +### Performance +- Consider performance implications. +- Avoid unnecessary computations. +- Use appropriate data structures. +- Add caching where beneficial. + +--- + +## Feature Categories + +### API Features +- Define clear endpoint contracts. +- Follow REST/GraphQL conventions. +- Include proper validation. +- Document request/response schemas. + +### UI Features +- Follow design system patterns. +- Ensure accessibility (a11y). +- Support internationalization (i18n). +- Handle loading and error states. + +### Backend Features +- Design for scalability. +- Consider data migration needs. +- Handle edge cases gracefully. +- Add appropriate logging. + +### Integration Features +- Define clear interfaces. +- Handle external service failures. +- Add retry logic where appropriate. +- Document integration requirements. + +--- + +## Non-Goals + +This agent must NOT: + +- Implement features beyond the defined scope +- Fix unrelated bugs (create separate issues) +- Refactor existing code unrelated to the feature +- Skip test coverage +- Ignore existing patterns and conventions +- Make breaking changes without explicit approval + +--- + +## Success Criteria + +A successful feature implementation results in: + +- Fully functional feature matching requirements +- Comprehensive test coverage +- Passing CI (tests, lint, TypeScript) +- Clear documentation +- Easy-to-review Pull Request +- No regressions in existing functionality +- Ready for production deployment diff --git a/.github/copilot-agents/refactor-agent.md b/.github/copilot-agents/refactor-agent.md new file mode 100644 index 0000000000000..9e855f3302d58 --- /dev/null +++ b/.github/copilot-agents/refactor-agent.md @@ -0,0 +1,261 @@ +--- +name: Refactor Agent +description: | + A disciplined agent that performs code refactoring with a focus on improving code quality, + maintainability, and readability without changing external behavior. It ensures all tests + pass before and after changes, and creates incremental, reviewable PRs. +--- + +# Refactor Agent + +## Purpose + +This agent performs controlled refactoring operations to improve code quality. +Its goal is to: + +- Improve code readability, maintainability, and structure +- Preserve existing behavior (no functional changes) +- Ensure all quality gates pass throughout the process +- Create incremental, easy-to-review changes +- Document the rationale behind refactoring decisions + +The agent must **not introduce new features, fix bugs, or change external behavior**. + +--- + +## Operating Principles + +1. **Behavior Preservation** + - External behavior must remain identical before and after refactoring. + - All existing tests must continue to pass. + - If tests fail, the refactoring approach must be reconsidered. + +2. **Incremental Changes** + - Break large refactors into smaller, atomic commits. + - Each commit should be independently valid and reviewable. + - Prefer multiple small PRs over one large PR when appropriate. + +3. **Quality Gates Are Mandatory** + - All existing tests must pass. + - Lint must pass with no new warnings or errors. + - TypeScript type checking must pass without errors. + - Build must succeed. + +4. **Clear Rationale** + - Every refactoring decision must have a documented reason. + - Common reasons include: reducing duplication, improving type safety, enhancing readability, simplifying complexity. + +5. **Scope Discipline** + - Stay focused on the refactoring goal. + - Do not fix unrelated bugs discovered during refactoring. + - Do not add new features or optimizations. + - Document discovered problems as TODOs for future issues (see Section: Documenting Out-of-Scope Findings). + +--- + +## Documenting Out-of-Scope Findings + +When you discover bugs, technical debt, or improvement opportunities outside the current refactoring scope, **do not fix them**. Instead, create a detailed TODO comment or document them in the PR description so they can become separate issues. + +### TODO Format + +Add a TODO comment in the code near where the problem was found. + +**Type prefixes** (same as PR conventions): +- `bug` - A bug that needs to be fixed +- `feat` - A new feature opportunity +- `refactor` - Code that needs refactoring +- `chore` - Maintenance tasks (dependencies, configs, etc.) +- `test` - Missing or incomplete tests + +**Optional labels** (GitHub labels in brackets): +- `[security]` - Security-related issues +- `[performance]` - Performance improvements +- `[a11y]` - Accessibility issues +- `[i18n]` - Internationalization issues +- `[breaking]` - Breaking changes +- Any other GitHub label relevant to the issue + +```typescript +// TODO: type [optional-label] +// Problem: +// Location: +// Impact: +// Suggested fix: +// Discovered while: +``` + +### Example + +```typescript +// TODO: chore [breaking] Deprecated API usage in notification service +// Problem: The notifyUser() function uses the deprecated Meteor.defer() API +// which will be removed in the next major version. +// Location: apps/meteor/server/services/notifications/notifyUser.ts:45 +// Impact: High - Will break notifications after Meteor upgrade +// Suggested fix: Replace with Promise-based async/await pattern using +// the new queueMicrotask() or setImmediate() alternatives. +// Discovered while: Refactoring notification module structure +``` + +### In PR Description + +Also list discovered issues in the PR description under a "Discovered Issues" section: + +```markdown +## Discovered Issues (Out of Scope) + +The following issues were discovered during this refactoring and should be addressed in separate PRs: + +- `chore` [breaking]: **Deprecated API usage** - See TODO in `notifyUser.ts:45` +- `bug`: **Missing error boundary** - See TODO in `MessageList.tsx:23` +- `bug` [performance]: **Potential memory leak** - See TODO in `subscriptionManager.ts:112` +``` + +--- + +## Step-by-Step Execution Flow + +### 1. Understand the Refactoring Goal + +- Clearly define what needs to be improved: + - Code duplication? + - Complex conditionals? + - Poor naming? + - Missing type safety? + - Tight coupling? + - Large files/functions? +- Identify the scope and boundaries of the refactoring. + +--- + +### 2. Analyze Current State + +- Review the existing code structure. +- Identify: + - Existing test coverage + - Dependencies and dependents + - Potential risk areas +- Document the current state for reference. + +--- + +### 3. Ensure Test Coverage + +- Verify existing tests adequately cover the code being refactored. +- If coverage is insufficient: + - Add characterization tests that capture current behavior. + - These tests act as a safety net during refactoring. +- Tests must pass before any refactoring begins. + +--- + +### 4. Plan the Refactoring Steps + +- Break down the refactoring into discrete steps. +- Each step should: + - Be independently verifiable + - Maintain a working codebase + - Be easy to review +- Order steps to minimize risk. + +--- + +### 5. Execute Refactoring Incrementally + +For each step: + +1. Make the targeted change. +2. Run tests to verify behavior preservation. +3. Run lint and type checking. +4. Commit with a clear message explaining the change. + +Common refactoring patterns to apply: + +- **Extract Function/Method**: Break down large functions +- **Rename**: Improve clarity of names +- **Move**: Relocate code to better locations +- **Inline**: Remove unnecessary abstractions +- **Extract Interface/Type**: Improve type definitions +- **Consolidate Duplicates**: DRY principle application +- **Simplify Conditionals**: Reduce complexity + +--- + +### 6. Validate Final State + +After all refactoring steps: + +- All tests pass. +- Lint passes. +- TypeScript compilation passes. +- Build succeeds. +- Code review checklist: + - Is the code more readable? + - Is the code more maintainable? + - Is the structure improved? + - Are there any regressions? + +--- + +### 7. Open the Pull Request + +The PR must: + +- Clearly describe the refactoring goal and rationale. +- List the refactoring patterns applied. +- Confirm that no behavioral changes were introduced. +- Reference any related issues or technical debt items. +- Include before/after examples if helpful. + +--- + +## Refactoring Categories + +### Structural Refactoring +- File/folder reorganization +- Module extraction +- Dependency restructuring + +### Code Quality Refactoring +- Naming improvements +- Function decomposition +- Duplication removal +- Complexity reduction + +### Type Safety Refactoring +- Adding explicit types +- Removing `any` types +- Improving generic usage +- Adding type guards + +### Pattern Application +- Applying design patterns +- Removing anti-patterns +- Standardizing approaches + +--- + +## Non-Goals + +This agent must NOT: + +- Change external behavior +- Fix bugs (create separate issues) +- Add new features +- Optimize performance (unless part of explicit refactoring goal) +- Make changes outside the defined scope +- Skip quality gate verification + +--- + +## Success Criteria + +A successful refactoring results in: + +- Improved code quality metrics (readability, maintainability) +- All tests passing (no behavioral changes) +- Passing CI (tests, lint, TypeScript) +- Clear documentation of changes +- Easy-to-review Pull Request +- No new bugs introduced diff --git a/.github/workflows/todo.yml b/.github/workflows/todo.yml new file mode 100644 index 0000000000000..3eedd9857a8ab --- /dev/null +++ b/.github/workflows/todo.yml @@ -0,0 +1,32 @@ +name: Create issues from TODOs + +on: + workflow_dispatch: + inputs: + importAll: + default: false + required: false + type: boolean + description: Enable, if you want to import all TODOs. Runs on checked out branch! Only use if you're sure what you are doing. + push: + branches: # do not set multiple branches, todos might be added and then get referenced by themselves in case of a merge + - develop + +permissions: + issues: write + repository-projects: read + contents: read + +jobs: + todos: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - name: Run Issue Bot + uses: juulsn/todo-issue@v1.1.4 + with: + excludePattern: '(^|/)node_modules/' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 304847762136f2aec047ea2af56e40d86f463718 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 09:56:22 -0300 Subject: [PATCH 029/108] chore: prevent undefined `supportSchemesForLink` in filtered markdown parser (#38958) Co-authored-by: ggazzo <5263975+ggazzo@users.noreply.github.com> --- apps/meteor/app/markdown/lib/parser/filtered/filtered.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/app/markdown/lib/parser/filtered/filtered.js b/apps/meteor/app/markdown/lib/parser/filtered/filtered.js index 260fc835d8a0a..868baaee80903 100644 --- a/apps/meteor/app/markdown/lib/parser/filtered/filtered.js +++ b/apps/meteor/app/markdown/lib/parser/filtered/filtered.js @@ -9,7 +9,7 @@ export const filtered = ( supportSchemesForLink: 'http,https', }, ) => { - const schemes = options.supportSchemesForLink.split(',').join('|'); + const schemes = (options.supportSchemesForLink || 'http,https').split(',').join('|'); // Remove block code backticks message = message.replace(/```/g, ''); From b77fe135a62148209648bc517dc06c91f382510d Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli <84046180+nazabucciarelli@users.noreply.github.com> Date: Tue, 24 Feb 2026 11:17:01 -0300 Subject: [PATCH 030/108] fix: avoid to get files with other extensions than the proper ones for custom-sounds and emoji-custom endpoints (#38531) Co-authored-by: Guilherme Gazzo --- .changeset/clean-ears-fly.md | 5 ++ .../server/startup/custom-sounds.js | 11 +++ .../server/startup/emoji-custom.js | 68 +++++++++++-------- .../src/models/IEmojiCustomModel.ts | 1 + packages/models/src/models/EmojiCustom.ts | 5 ++ 5 files changed, 61 insertions(+), 29 deletions(-) create mode 100644 .changeset/clean-ears-fly.md diff --git a/.changeset/clean-ears-fly.md b/.changeset/clean-ears-fly.md new file mode 100644 index 0000000000000..781a8a4da4466 --- /dev/null +++ b/.changeset/clean-ears-fly.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes a cross-resource access issue that allowed users to retrieve emojis from the Custom Sounds endpoint and sounds from the Custom Emojis endpoint when using the FileSystem storage mode. diff --git a/apps/meteor/app/custom-sounds/server/startup/custom-sounds.js b/apps/meteor/app/custom-sounds/server/startup/custom-sounds.js index 117a7d3c9e759..94101b8622c6b 100644 --- a/apps/meteor/app/custom-sounds/server/startup/custom-sounds.js +++ b/apps/meteor/app/custom-sounds/server/startup/custom-sounds.js @@ -1,3 +1,4 @@ +import { CustomSounds } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { WebApp } from 'meteor/webapp'; @@ -47,7 +48,17 @@ Meteor.startup(() => { return; } + const sound = await CustomSounds.findOneById(fileId.split('.')[0], { projection: { _id: 1 } }); + + if (!sound) { + res.writeHead(404); + res.write('Not found'); + res.end(); + return; + } + const file = await RocketChatFileCustomSoundsInstance.getFileWithReadStream(fileId); + if (!file) { res.writeHead(404); res.write('Not found'); diff --git a/apps/meteor/app/emoji-custom/server/startup/emoji-custom.js b/apps/meteor/app/emoji-custom/server/startup/emoji-custom.js index fed5123b115f4..1512bb0314c15 100644 --- a/apps/meteor/app/emoji-custom/server/startup/emoji-custom.js +++ b/apps/meteor/app/emoji-custom/server/startup/emoji-custom.js @@ -1,3 +1,4 @@ +import { EmojiCustom } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { WebApp } from 'meteor/webapp'; import _ from 'underscore'; @@ -8,6 +9,35 @@ import { settings } from '../../../settings/server'; export let RocketChatFileEmojiCustomInstance; +const writeSvgFallback = (res, req) => { + res.setHeader('Content-Type', 'image/svg+xml'); + res.setHeader('Cache-Control', 'public, max-age=0'); + res.setHeader('Expires', '-1'); + res.setHeader('Last-Modified', 'Thu, 01 Jan 2015 00:00:00 GMT'); + + const reqModifiedHeader = req.headers['if-modified-since']; + if (reqModifiedHeader != null) { + if (reqModifiedHeader === 'Thu, 01 Jan 2015 00:00:00 GMT') { + res.writeHead(304); + res.end(); + return; + } + } + + const color = '#000'; + const initials = '?'; + + const svg = ` + + + ${initials} + +`; + + res.write(svg); + res.end(); +}; + Meteor.startup(() => { let storeType = 'GridFS'; @@ -48,39 +78,19 @@ Meteor.startup(() => { return; } - const file = await RocketChatFileEmojiCustomInstance.getFileWithReadStream(encodeURIComponent(params.emoji)); - res.setHeader('Content-Disposition', 'inline'); + const emoji = await EmojiCustom.findOneByName(params.emoji.split('.')[0], { projection: { _id: 1 } }); + + if (!emoji) { + return writeSvgFallback(res, req); + } + + const file = await RocketChatFileEmojiCustomInstance.getFileWithReadStream(encodeURIComponent(params.emoji)); + if (!file) { // use code from username initials renderer until file upload is complete - res.setHeader('Content-Type', 'image/svg+xml'); - res.setHeader('Cache-Control', 'public, max-age=0'); - res.setHeader('Expires', '-1'); - res.setHeader('Last-Modified', 'Thu, 01 Jan 2015 00:00:00 GMT'); - - const reqModifiedHeader = req.headers['if-modified-since']; - if (reqModifiedHeader != null) { - if (reqModifiedHeader === 'Thu, 01 Jan 2015 00:00:00 GMT') { - res.writeHead(304); - res.end(); - return; - } - } - - const color = '#000'; - const initials = '?'; - - const svg = ` - - - ${initials} - -`; - - res.write(svg); - res.end(); - return; + return writeSvgFallback(res, req); } const fileUploadDate = file.uploadDate != null ? file.uploadDate.toUTCString() : undefined; diff --git a/packages/model-typings/src/models/IEmojiCustomModel.ts b/packages/model-typings/src/models/IEmojiCustomModel.ts index 5bae4d9eb9baf..0ed54f36ab393 100644 --- a/packages/model-typings/src/models/IEmojiCustomModel.ts +++ b/packages/model-typings/src/models/IEmojiCustomModel.ts @@ -12,4 +12,5 @@ export interface IEmojiCustomModel extends IBaseModel { setETagByName(name: string, etag: string): Promise; create(data: InsertionModel): Promise>>; countByNameOrAlias(name: string): Promise; + findOneByName(name: string, options?: FindOptions): Promise; } diff --git a/packages/models/src/models/EmojiCustom.ts b/packages/models/src/models/EmojiCustom.ts index 49bc7322be1f5..679ee0d3ab037 100644 --- a/packages/models/src/models/EmojiCustom.ts +++ b/packages/models/src/models/EmojiCustom.ts @@ -90,4 +90,9 @@ export class EmojiCustomRaw extends BaseRaw implements IEmojiCusto return this.countDocuments(query); } + + // TODO: convert name: string to branded type using to enforce validation also replace this type cross the models/apis + findOneByName(name: string, options?: FindOptions): Promise { + return this.findOne({ name }, options); + } } From 55bf0717033d85644d9773ea822bbca8b7e16d77 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Feb 2026 11:32:49 -0300 Subject: [PATCH 031/108] fix: MarkdownText variant document ignoring \n (#38983) Co-authored-by: Guilherme Gazzo --- .changeset/fix-webhook-newline.md | 5 +++ .../client/components/MarkdownText.spec.tsx | 32 +++++++++++++++++++ .../meteor/client/components/MarkdownText.tsx | 1 + 3 files changed, 38 insertions(+) create mode 100644 .changeset/fix-webhook-newline.md diff --git a/.changeset/fix-webhook-newline.md b/.changeset/fix-webhook-newline.md new file mode 100644 index 0000000000000..c622c57eb235b --- /dev/null +++ b/.changeset/fix-webhook-newline.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes incoming webhook messages ignoring literal `\n` escape sequences, and fixes the `MarkdownText` `document` variant not rendering newlines as line breaks. diff --git a/apps/meteor/client/components/MarkdownText.spec.tsx b/apps/meteor/client/components/MarkdownText.spec.tsx index 1ed832ee02a45..a59c53cc14012 100644 --- a/apps/meteor/client/components/MarkdownText.spec.tsx +++ b/apps/meteor/client/components/MarkdownText.spec.tsx @@ -434,6 +434,38 @@ describe('code handling', () => { }); }); +describe('line breaks handling', () => { + it('should convert newlines to
in document variant', () => { + const content = 'First Line\nSecond Line\nThird Line'; + const { container } = render(, { + wrapper: mockAppRoot().build(), + }); + + const html = container.innerHTML; + expect(html).toContain('First Line
Second Line
Third Line'); + }); + + it('should convert newlines to
in inline variant', () => { + const content = 'First Line\nSecond Line\nThird Line'; + const { container } = render(, { + wrapper: mockAppRoot().build(), + }); + + const html = container.innerHTML; + expect(html).not.toContain('
'); + }); + + it('should not convert newlines to
in inlineWithoutBreaks variant', () => { + const content = 'First Line\nSecond Line\nThird Line'; + const { container } = render(, { + wrapper: mockAppRoot().build(), + }); + + const html = container.innerHTML; + expect(html).not.toContain('
'); + }); +}); + describe('DOMPurify hook registration', () => { it('should register hook only once at module level', () => { // Import the module to trigger hook registration diff --git a/apps/meteor/client/components/MarkdownText.tsx b/apps/meteor/client/components/MarkdownText.tsx index c490ae5e77b48..faefcf806d54b 100644 --- a/apps/meteor/client/components/MarkdownText.tsx +++ b/apps/meteor/client/components/MarkdownText.tsx @@ -78,6 +78,7 @@ const defaultOptions = { const options = { ...defaultOptions, + breaks: true, renderer: documentRenderer, }; From 4c0244d2076cef20eedb18b87243325b004465f3 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 24 Feb 2026 16:32:41 -0300 Subject: [PATCH 032/108] ci: add workflows for auto-closing duplicate issues (#38993) --- .../bug-resolution-agent.md | 0 .../feature-development-agent.md | 0 .../refactor-agent.md | 0 .github/workflows/auto-close-duplicates.yml | 31 +++ .github/workflows/dedupe-issues.yml | 83 +++++++ scripts/auto-close-duplicates.ts | 204 ++++++++++++++++++ 6 files changed, 318 insertions(+) rename .github/{copilot-agents => agents}/bug-resolution-agent.md (100%) rename .github/{copilot-agents => agents}/feature-development-agent.md (100%) rename .github/{copilot-agents => agents}/refactor-agent.md (100%) create mode 100644 .github/workflows/auto-close-duplicates.yml create mode 100644 .github/workflows/dedupe-issues.yml create mode 100644 scripts/auto-close-duplicates.ts diff --git a/.github/copilot-agents/bug-resolution-agent.md b/.github/agents/bug-resolution-agent.md similarity index 100% rename from .github/copilot-agents/bug-resolution-agent.md rename to .github/agents/bug-resolution-agent.md diff --git a/.github/copilot-agents/feature-development-agent.md b/.github/agents/feature-development-agent.md similarity index 100% rename from .github/copilot-agents/feature-development-agent.md rename to .github/agents/feature-development-agent.md diff --git a/.github/copilot-agents/refactor-agent.md b/.github/agents/refactor-agent.md similarity index 100% rename from .github/copilot-agents/refactor-agent.md rename to .github/agents/refactor-agent.md diff --git a/.github/workflows/auto-close-duplicates.yml b/.github/workflows/auto-close-duplicates.yml new file mode 100644 index 0000000000000..6f24ce2028b8a --- /dev/null +++ b/.github/workflows/auto-close-duplicates.yml @@ -0,0 +1,31 @@ +name: Auto-close duplicate issues +description: Auto-closes issues that are duplicates of existing issues +on: + schedule: + - cron: '0 9 * * *' + workflow_dispatch: + +jobs: + auto-close-duplicates: + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + issues: write + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Auto-close duplicate issues + run: bun run scripts/auto-close-duplicates.ts + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }} + GITHUB_REPOSITORY_NAME: ${{ github.event.repository.name }} + STATSIG_API_KEY: ${{ secrets.STATSIG_API_KEY }} diff --git a/.github/workflows/dedupe-issues.yml b/.github/workflows/dedupe-issues.yml new file mode 100644 index 0000000000000..3c44d475ef671 --- /dev/null +++ b/.github/workflows/dedupe-issues.yml @@ -0,0 +1,83 @@ +name: Rocket.Chat Issue Dedupe +description: Automatically dedupe GitHub issues using AI +on: + issues: + types: [opened] + workflow_dispatch: + inputs: + issue_number: + description: 'Issue number to process for duplicate detection' + required: true + type: string + +jobs: + dedupe-issues: + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read + issues: write + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Run Claude Code slash command + uses: anthropics/claude-code-base-action@beta + with: + prompt: '/dedupe ${{ github.repository }}/issues/${{ github.event.issue.number || inputs.issue_number }}' + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + claude_args: '--model claude-sonnet-4-5-20250929' + claude_env: | + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Log duplicate comment event to Statsig + if: always() + env: + STATSIG_API_KEY: ${{ secrets.STATSIG_API_KEY }} + run: | + ISSUE_NUMBER=${{ github.event.issue.number || inputs.issue_number }} + REPO=${{ github.repository }} + + if [ -z "$STATSIG_API_KEY" ]; then + echo "STATSIG_API_KEY not found, skipping Statsig logging" + exit 0 + fi + + # Prepare the event payload + EVENT_PAYLOAD=$(jq -n \ + --arg issue_number "$ISSUE_NUMBER" \ + --arg repo "$REPO" \ + --arg triggered_by "${{ github.event_name }}" \ + --arg actor "${{ github.actor }}" \ + '{ + events: [{ + eventName: "github_duplicate_comment_added", + value: 1, + metadata: { + repository: $repo, + issue_number: ($issue_number | tonumber), + triggered_by: $triggered_by, + workflow_run_id: "${{ github.run_id }}", + actor: $actor + }, + time: (now | floor | tostring) + }] + }') + + # Send to Statsig API + echo "Logging duplicate comment event to Statsig for issue #${ISSUE_NUMBER}" + + RESPONSE=$(curl -s -w "\n%{http_code}" -X POST https://events.statsigapi.net/v1/log_event \ + -H "Content-Type: application/json" \ + -H "STATSIG-API-KEY: ${STATSIG_API_KEY}" \ + -d "$EVENT_PAYLOAD") + + HTTP_CODE=$(echo "$RESPONSE" | tail -n1) + BODY=$(echo "$RESPONSE" | head -n-1) + + if [ "$HTTP_CODE" -eq 200 ] || [ "$HTTP_CODE" -eq 202 ]; then + echo "Successfully logged duplicate comment event for issue #${ISSUE_NUMBER}" + else + echo "Failed to log duplicate comment event for issue #${ISSUE_NUMBER}. HTTP ${HTTP_CODE}: ${BODY}" + fi diff --git a/scripts/auto-close-duplicates.ts b/scripts/auto-close-duplicates.ts new file mode 100644 index 0000000000000..3d1c7f22b7347 --- /dev/null +++ b/scripts/auto-close-duplicates.ts @@ -0,0 +1,204 @@ +#!/usr/bin/env bun + +interface GitHubIssue { + number: number; + title: string; + user: { id: number }; + created_at: string; +} + +interface GitHubComment { + id: number; + body: string; + created_at: string; + user: { type: string; id: number }; +} + +interface GitHubReaction { + user: { id: number }; + content: string; +} + +async function githubRequest(endpoint: string, token: string, method: string = 'GET', body?: any): Promise { + const response = await fetch(`https://api.github.com${endpoint}`, { + method, + headers: { + 'Authorization': `Bearer ${token}`, + 'Accept': 'application/vnd.github.v3+json', + 'User-Agent': 'auto-close-duplicates-script', + ...(body && { 'Content-Type': 'application/json' }), + }, + ...(body && { body: JSON.stringify(body) }), + }); + + if (!response.ok) { + throw new Error(`GitHub API request failed: ${response.status} ${response.statusText}`); + } + + return response.json(); +} + +function extractDuplicateIssueNumber(commentBody: string): number | null { + // Try to match #123 format first + let match = commentBody.match(/#(\d+)/); + if (match) { + return parseInt(match[1], 10); + } + + // Try to match GitHub issue URL format: https://github.com/owner/repo/issues/123 + match = commentBody.match(/github\.com\/[^\/]+\/[^\/]+\/issues\/(\d+)/); + if (match) { + return parseInt(match[1], 10); + } + + return null; +} + +async function closeIssueAsDuplicate( + owner: string, + repo: string, + issueNumber: number, + duplicateOfNumber: number, + token: string, +): Promise { + await githubRequest(`/repos/${owner}/${repo}/issues/${issueNumber}`, token, 'PATCH', { + state: 'closed', + state_reason: 'duplicate', + labels: ['duplicate'], + }); + + await githubRequest(`/repos/${owner}/${repo}/issues/${issueNumber}/comments`, token, 'POST', { + body: `This issue has been automatically closed as a duplicate of #${duplicateOfNumber}. + + If this is incorrect, please re-open this issue or create a new one. + + 🤖 Generated with [Claude Code](https://claude.ai/code)`, + }); +} + +async function autoCloseDuplicates(): Promise { + console.log('[DEBUG] Starting auto-close duplicates script'); + + const token = process.env.GITHUB_TOKEN; + if (!token) { + throw new Error('GITHUB_TOKEN environment variable is required'); + } + console.log('[DEBUG] GitHub token found'); + + const owner = process.env.GITHUB_REPOSITORY_OWNER || 'RocketChat'; + const repo = process.env.GITHUB_REPOSITORY_NAME || 'Rocket.Chat'; + console.log(`[DEBUG] Repository: ${owner}/${repo}`); + + const threeDaysAgo = new Date(); + threeDaysAgo.setDate(threeDaysAgo.getDate() - 3); + console.log(`[DEBUG] Checking for duplicate comments older than: ${threeDaysAgo.toISOString()}`); + + console.log('[DEBUG] Fetching open issues created more than 3 days ago...'); + const allIssues: GitHubIssue[] = []; + let page = 1; + const perPage = 100; + + while (true) { + const pageIssues: GitHubIssue[] = await githubRequest( + `/repos/${owner}/${repo}/issues?state=open&per_page=${perPage}&page=${page}`, + token, + ); + + if (pageIssues.length === 0) break; + + // Filter for issues created more than 3 days ago + const oldEnoughIssues = pageIssues.filter((issue) => new Date(issue.created_at) <= threeDaysAgo); + + allIssues.push(...oldEnoughIssues); + page++; + + // Safety limit to avoid infinite loops + if (page > 20) break; + } + + const issues = allIssues; + console.log(`[DEBUG] Found ${issues.length} open issues`); + + let processedCount = 0; + let candidateCount = 0; + + for (const issue of issues) { + processedCount++; + console.log(`[DEBUG] Processing issue #${issue.number} (${processedCount}/${issues.length}): ${issue.title}`); + + console.log(`[DEBUG] Fetching comments for issue #${issue.number}...`); + const comments: GitHubComment[] = await githubRequest(`/repos/${owner}/${repo}/issues/${issue.number}/comments`, token); + console.log(`[DEBUG] Issue #${issue.number} has ${comments.length} comments`); + + const dupeComments = comments.filter( + (comment) => comment.body.includes('Found') && comment.body.includes('possible duplicate') && comment.user.type === 'Bot', + ); + console.log(`[DEBUG] Issue #${issue.number} has ${dupeComments.length} duplicate detection comments`); + + if (dupeComments.length === 0) { + console.log(`[DEBUG] Issue #${issue.number} - no duplicate comments found, skipping`); + continue; + } + + const lastDupeComment = dupeComments[dupeComments.length - 1]; + const dupeCommentDate = new Date(lastDupeComment.created_at); + console.log(`[DEBUG] Issue #${issue.number} - most recent duplicate comment from: ${dupeCommentDate.toISOString()}`); + + if (dupeCommentDate > threeDaysAgo) { + console.log(`[DEBUG] Issue #${issue.number} - duplicate comment is too recent, skipping`); + continue; + } + console.log( + `[DEBUG] Issue #${issue.number} - duplicate comment is old enough (${Math.floor( + (Date.now() - dupeCommentDate.getTime()) / (1000 * 60 * 60 * 24), + )} days)`, + ); + + const commentsAfterDupe = comments.filter((comment) => new Date(comment.created_at) > dupeCommentDate); + console.log(`[DEBUG] Issue #${issue.number} - ${commentsAfterDupe.length} comments after duplicate detection`); + + if (commentsAfterDupe.length > 0) { + console.log(`[DEBUG] Issue #${issue.number} - has activity after duplicate comment, skipping`); + continue; + } + + console.log(`[DEBUG] Issue #${issue.number} - checking reactions on duplicate comment...`); + const reactions: GitHubReaction[] = await githubRequest( + `/repos/${owner}/${repo}/issues/comments/${lastDupeComment.id}/reactions`, + token, + ); + console.log(`[DEBUG] Issue #${issue.number} - duplicate comment has ${reactions.length} reactions`); + + const authorThumbsDown = reactions.some((reaction) => reaction.user.id === issue.user.id && reaction.content === '-1'); + console.log(`[DEBUG] Issue #${issue.number} - author thumbs down reaction: ${authorThumbsDown}`); + + if (authorThumbsDown) { + console.log(`[DEBUG] Issue #${issue.number} - author disagreed with duplicate detection, skipping`); + continue; + } + + const duplicateIssueNumber = extractDuplicateIssueNumber(lastDupeComment.body); + if (!duplicateIssueNumber) { + console.log(`[DEBUG] Issue #${issue.number} - could not extract duplicate issue number from comment, skipping`); + continue; + } + + candidateCount++; + const issueUrl = `https://github.com/${owner}/${repo}/issues/${issue.number}`; + + try { + console.log(`[INFO] Auto-closing issue #${issue.number} as duplicate of #${duplicateIssueNumber}: ${issueUrl}`); + await closeIssueAsDuplicate(owner, repo, issue.number, duplicateIssueNumber, token); + console.log(`[SUCCESS] Successfully closed issue #${issue.number} as duplicate of #${duplicateIssueNumber}`); + } catch (error) { + console.error(`[ERROR] Failed to close issue #${issue.number} as duplicate: ${error}`); + } + } + + console.log(`[DEBUG] Script completed. Processed ${processedCount} issues, found ${candidateCount} candidates for auto-close`); +} + +autoCloseDuplicates().catch(console.error); + +// Make it a module +export {}; From 99d6b3126724c5df2f89dc84fe01a3573cba7ad9 Mon Sep 17 00:00:00 2001 From: Sahil Singh Date: Wed, 25 Feb 2026 02:39:09 +0530 Subject: [PATCH 033/108] fix(createRoom): avoid mutating members input parameter (#38885) --- apps/meteor/app/lib/server/functions/createRoom.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/meteor/app/lib/server/functions/createRoom.ts b/apps/meteor/app/lib/server/functions/createRoom.ts index bbd971e667e66..a9cd1330d6148 100644 --- a/apps/meteor/app/lib/server/functions/createRoom.ts +++ b/apps/meteor/app/lib/server/functions/createRoom.ts @@ -193,7 +193,9 @@ export const createRoom = async ( return createDirectRoom(members as IUser[], extraData, { ...options, creator: options?.creator || owner?._id }); } - if (!onlyUsernames(members)) { + const memberList = [...members]; + + if (!onlyUsernames(memberList)) { throw new Meteor.Error( 'error-invalid-members', 'members should be an array of usernames if provided for rooms other than direct messages', @@ -218,8 +220,8 @@ export const createRoom = async ( }); } - if (!excludeSelf && owner.username && !members.includes(owner.username)) { - members.push(owner.username); + if (!excludeSelf && owner.username && !memberList.includes(owner.username)) { + memberList.push(owner.username); } if (extraData.broadcast) { @@ -258,7 +260,7 @@ export const createRoom = async ( const tmp = { ...roomProps, - _USERNAMES: members, + _USERNAMES: memberList, }; const prevent = await Apps.self?.triggerEvent(AppEvents.IPreRoomCreatePrevent, tmp).catch((error) => { @@ -299,10 +301,10 @@ export const createRoom = async ( if (shouldBeHandledByFederation) { // Reusing unused callback to create Matrix room. // We should discuss the opportunity to rename it to something with "before" prefix. - await callbacks.run('federation.afterCreateFederatedRoom', room, { owner, originalMemberList: members, options }); + await callbacks.run('federation.afterCreateFederatedRoom', room, { owner, originalMemberList: memberList, options }); } - await createUsersSubscriptions({ room, members, now, owner, options, shouldBeHandledByFederation }); + await createUsersSubscriptions({ room, members: memberList, now, owner, options, shouldBeHandledByFederation }); if (type === 'c') { if (room.teamId) { From 88b2151df8ecc748a0ae68b965baae66b7ecce84 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Tue, 24 Feb 2026 18:09:38 -0300 Subject: [PATCH 034/108] refactor: Stricten types over UiKit implementation APIs (#38804) --- .../meteor/app/integrations/server/api/api.ts | 2 +- .../integrations/server/lib/triggerHandler.ts | 2 +- .../server/functions/processWebhookMessage.ts | 6 +- .../ee/server/apps/communication/uikit.ts | 142 +++++++++--------- .../server/modules/core-apps/banner.module.ts | 4 +- .../core-apps/cloudAnnouncements.module.ts | 30 ++-- .../cloudSubscriptionCommunication.module.ts | 4 +- .../modules/core-apps/mention.module.ts | 36 +++-- .../server/modules/core-apps/nps.module.ts | 9 +- .../modules/core-apps/videoconf.module.ts | 8 +- .../server/services/uikit-core-app/service.ts | 22 +-- .../services/video-conference/service.ts | 4 +- packages/core-services/src/index.ts | 12 +- .../core-services/src/types/IUiKitCoreApp.ts | 79 +++++----- .../src/types/IVideoConfService.ts | 2 +- packages/rest-typings/src/apps/index.ts | 2 +- 16 files changed, 202 insertions(+), 162 deletions(-) diff --git a/apps/meteor/app/integrations/server/api/api.ts b/apps/meteor/app/integrations/server/api/api.ts index 141a0a4fedb61..511b22f3f97d0 100644 --- a/apps/meteor/app/integrations/server/api/api.ts +++ b/apps/meteor/app/integrations/server/api/api.ts @@ -47,7 +47,7 @@ type IntegrationThis = GenericRouteExecutionContext & { request: Request & { integration: IIncomingIntegration; }; - user: IUser & { username: RequiredField }; + user: RequiredField; }; async function createIntegration(options: IntegrationOptions, user: IUser): Promise { diff --git a/apps/meteor/app/integrations/server/lib/triggerHandler.ts b/apps/meteor/app/integrations/server/lib/triggerHandler.ts index 43f4826993022..135c88bdde228 100644 --- a/apps/meteor/app/integrations/server/lib/triggerHandler.ts +++ b/apps/meteor/app/integrations/server/lib/triggerHandler.ts @@ -181,7 +181,7 @@ class RocketChatIntegrationHandler { channel: tmpRoom.t === 'd' ? `@${tmpRoom._id}` : `#${tmpRoom._id}`, }; - return processWebhookMessage(message, user as IUser & { username: RequiredField }, defaultValues); + return processWebhookMessage(message, user as RequiredField, defaultValues); } eventNameArgumentsToObject(...args: unknown[]) { diff --git a/apps/meteor/app/lib/server/functions/processWebhookMessage.ts b/apps/meteor/app/lib/server/functions/processWebhookMessage.ts index 374cc091e0634..e3e9903a6527e 100644 --- a/apps/meteor/app/lib/server/functions/processWebhookMessage.ts +++ b/apps/meteor/app/lib/server/functions/processWebhookMessage.ts @@ -131,13 +131,13 @@ const buildMessage = (messageObj: Payload, defaultValues: DefaultValues) => { export function processWebhookMessage( messageObj: Payload & { separateResponse: true }, - user: IUser & { username: RequiredField }, + user: RequiredField, defaultValues?: DefaultValues, ): Promise; export function processWebhookMessage( messageObj: Payload & { separateResponse?: false | undefined }, - user: IUser & { username: RequiredField }, + user: RequiredField, defaultValues?: DefaultValues, ): Promise; @@ -148,7 +148,7 @@ export async function processWebhookMessage( */ separateResponse?: boolean; }, - user: IUser & { username: RequiredField }, + user: RequiredField, defaultValues: DefaultValues = { channel: '', alias: '', avatar: '', emoji: '' }, ) { const rooms: ({ channel: string } & ({ room: IRoom } | { room: IRoom | null; error?: any }))[] = []; diff --git a/apps/meteor/ee/server/apps/communication/uikit.ts b/apps/meteor/ee/server/apps/communication/uikit.ts index e5e851ef8f2b1..1dcd1c169d578 100644 --- a/apps/meteor/ee/server/apps/communication/uikit.ts +++ b/apps/meteor/ee/server/apps/communication/uikit.ts @@ -1,7 +1,6 @@ import { AppEvents, type IAppServerOrchestrator } from '@rocket.chat/apps'; -import type { UiKitCoreAppPayload } from '@rocket.chat/core-services'; import { UiKitCoreApp } from '@rocket.chat/core-services'; -import type { OperationParams, UrlParams } from '@rocket.chat/rest-typings'; +import type { OperationParams, OperationResult, UrlParams } from '@rocket.chat/rest-typings'; import bodyParser from 'body-parser'; import cors from 'cors'; import type { Request, Response } from 'express'; @@ -94,7 +93,7 @@ apiServer.use('/api/apps/ui.interaction/', bodyParser.json(), cors(corsOptions), type UiKitUserInteractionRequest = Request< UrlParams<'/apps/ui.interaction/:id'>, - any, + OperationResult<'POST', '/apps/ui.interaction/:id'> | { error: unknown }, OperationParams<'POST', '/apps/ui.interaction/:id'> & { visitor?: { id: string; @@ -111,81 +110,87 @@ type UiKitUserInteractionRequest = Request< } >; -const getCoreAppPayload = (req: UiKitUserInteractionRequest): UiKitCoreAppPayload => { +router.post('/:id', async (req: UiKitUserInteractionRequest, res, next) => { const { id: appId } = req.params; - if (req.body.type === 'blockAction') { - const { user } = req; - const { type, actionId, triggerId, payload, container, visitor } = req.body; - const message = 'mid' in req.body ? req.body.mid : undefined; - const room = 'rid' in req.body ? req.body.rid : undefined; - - return { - appId, - type, - actionId, - triggerId, - container, - message, - payload, - user, - visitor, - room, - }; + const isCoreApp = await UiKitCoreApp.isRegistered(appId); + if (!isCoreApp) { + return next(); } - if (req.body.type === 'viewClosed') { + try { const { user } = req; - const { - type, - payload: { view, isCleared }, - triggerId, - } = req.body; - - return { - appId, - triggerId, - type, - user, - payload: { - view, - isCleared, - }, - }; - } - if (req.body.type === 'viewSubmit') { - const { user } = req; - const { type, actionId, triggerId, payload } = req.body; - - return { - appId, - type, - actionId, - triggerId, - payload, - user, - }; - } + switch (req.body.type) { + case 'blockAction': { + const { type, actionId, triggerId, payload, container, visitor } = req.body; + const message = 'mid' in req.body ? req.body.mid : undefined; + const room = 'rid' in req.body ? req.body.rid : undefined; - throw new Error('Type not supported'); -}; + const result = await UiKitCoreApp.blockAction({ + appId, + type, + actionId, + triggerId, + container, + message, + payload, + user, + visitor, + room, + }); -router.post('/:id', async (req: UiKitUserInteractionRequest, res, next) => { - const { id: appId } = req.params; + // Using ?? to always send something in the response, even if the app had no result. + res.send(result ?? {}); - const isCoreApp = await UiKitCoreApp.isRegistered(appId); - if (!isCoreApp) { - return next(); - } + return; + } - try { - const payload = getCoreAppPayload(req); + case 'viewSubmit': { + const { type, actionId, triggerId, payload } = req.body; + + const result = await UiKitCoreApp.viewSubmit({ + appId, + type, + actionId, + triggerId, + payload, + user, + }); + + // Using ?? to always send something in the response, even if the app had no result. + res.send(result ?? {}); + + return; + } + + case 'viewClosed': { + const { + type, + payload: { view, isCleared }, + triggerId, + } = req.body; + + const result = await UiKitCoreApp.viewClosed({ + appId, + triggerId, + type, + user, + payload: { + view, + isCleared, + }, + }); + + // Using ?? to always send something in the response, even if the app had no result. + res.send(result ?? {}); - const result = await UiKitCoreApp[payload.type](payload); + return; + } - // Using ?? to always send something in the response, even if the app had no result. - res.send(result ?? {}); + default: + throw new Error('Type not supported'); + } } catch (e) { const error = e instanceof Error ? e.message : e; res.status(500).send({ error }); @@ -201,7 +206,10 @@ export class AppUIKitInteractionApi { router.post('/:id', this.routeHandler); } - private routeHandler = async (req: UiKitUserInteractionRequest, res: Response): Promise => { + private routeHandler = async ( + req: UiKitUserInteractionRequest, + res: Response | { error: unknown }>, + ): Promise => { const { orch } = this; const { id: appId } = req.params; diff --git a/apps/meteor/server/modules/core-apps/banner.module.ts b/apps/meteor/server/modules/core-apps/banner.module.ts index cf5bdebd50fe2..2af041134d065 100644 --- a/apps/meteor/server/modules/core-apps/banner.module.ts +++ b/apps/meteor/server/modules/core-apps/banner.module.ts @@ -1,12 +1,12 @@ import { Banner } from '@rocket.chat/core-services'; -import type { IUiKitCoreApp, UiKitCoreAppPayload } from '@rocket.chat/core-services'; +import type { IUiKitCoreApp, UiKitCoreAppViewClosedPayload } from '@rocket.chat/core-services'; import type * as UiKit from '@rocket.chat/ui-kit'; export class BannerModule implements IUiKitCoreApp { appId = 'banner-core'; // when banner view is closed we need to dismiss that banner for that user - async viewClosed(payload: UiKitCoreAppPayload): Promise { + async viewClosed(payload: UiKitCoreAppViewClosedPayload): Promise { const { payload: { view: { viewId: bannerId } = {} }, user: { _id: userId } = {}, diff --git a/apps/meteor/server/modules/core-apps/cloudAnnouncements.module.ts b/apps/meteor/server/modules/core-apps/cloudAnnouncements.module.ts index f13957b729813..00ce92c3b6fe1 100644 --- a/apps/meteor/server/modules/core-apps/cloudAnnouncements.module.ts +++ b/apps/meteor/server/modules/core-apps/cloudAnnouncements.module.ts @@ -1,5 +1,10 @@ import { Banner } from '@rocket.chat/core-services'; -import type { IUiKitCoreApp, UiKitCoreAppPayload } from '@rocket.chat/core-services'; +import type { + IUiKitCoreApp, + UiKitCoreAppBlockActionPayload, + UiKitCoreAppViewClosedPayload, + UiKitCoreAppViewSubmitPayload, +} from '@rocket.chat/core-services'; import type { Cloud, IUser } from '@rocket.chat/core-typings'; import { Banners } from '@rocket.chat/models'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; @@ -19,7 +24,7 @@ type CloudAnnouncementInteractant = user: Pick; } | { - visitor: Pick['visitor'], 'id' | 'username' | 'name' | 'department' | 'phone'>; + visitor: Pick, 'id' | 'username' | 'name' | 'department' | 'phone'>; }; type CloudAnnouncementInteractionRequest = UiKit.UserInteraction & CloudAnnouncementInteractant; @@ -35,15 +40,15 @@ export class CloudAnnouncementsModule implements IUiKitCoreApp { return settings.get('Cloud_Url'); } - blockAction(payload: UiKitCoreAppPayload): Promise { + blockAction(payload: UiKitCoreAppBlockActionPayload): Promise { return this.handlePayload(payload); } - viewSubmit(payload: UiKitCoreAppPayload): Promise { + viewSubmit(payload: UiKitCoreAppViewSubmitPayload): Promise { return this.handlePayload(payload); } - async viewClosed(payload: UiKitCoreAppPayload): Promise { + async viewClosed(payload: UiKitCoreAppViewClosedPayload): Promise { const { payload: { view: { viewId, id } = {} }, user: { _id: userId } = {}, @@ -86,7 +91,9 @@ export class CloudAnnouncementsModule implements IUiKitCoreApp { }; } - protected async handlePayload(payload: UiKitCoreAppPayload): Promise { + protected async handlePayload( + payload: UiKitCoreAppBlockActionPayload | UiKitCoreAppViewSubmitPayload | UiKitCoreAppViewClosedPayload, + ): Promise { const interactant = this.getInteractant(payload); const interaction = this.getInteraction(payload); @@ -104,10 +111,13 @@ export class CloudAnnouncementsModule implements IUiKitCoreApp { return serverInteraction; } catch (err) { SystemLogger.error({ err }); + return undefined; } } - protected getInteractant(payload: UiKitCoreAppPayload): CloudAnnouncementInteractant { + protected getInteractant( + payload: UiKitCoreAppBlockActionPayload | UiKitCoreAppViewSubmitPayload | UiKitCoreAppViewClosedPayload, + ): CloudAnnouncementInteractant { if (payload.user) { return { user: { @@ -118,7 +128,7 @@ export class CloudAnnouncementsModule implements IUiKitCoreApp { }; } - if (payload.visitor) { + if ('visitor' in payload && payload.visitor) { return { visitor: { id: payload.visitor.id, @@ -136,7 +146,9 @@ export class CloudAnnouncementsModule implements IUiKitCoreApp { /** * Transform the payload received from the Core App back to the format the UI sends from the client */ - protected getInteraction(payload: UiKitCoreAppPayload): UiKit.UserInteraction { + protected getInteraction( + payload: UiKitCoreAppBlockActionPayload | UiKitCoreAppViewSubmitPayload | UiKitCoreAppViewClosedPayload, + ): UiKit.UserInteraction { if (payload.type === 'blockAction' && payload.container?.type === 'message') { const { actionId, diff --git a/apps/meteor/server/modules/core-apps/cloudSubscriptionCommunication.module.ts b/apps/meteor/server/modules/core-apps/cloudSubscriptionCommunication.module.ts index e4b540416d4cc..37becfb041fab 100644 --- a/apps/meteor/server/modules/core-apps/cloudSubscriptionCommunication.module.ts +++ b/apps/meteor/server/modules/core-apps/cloudSubscriptionCommunication.module.ts @@ -1,4 +1,4 @@ -import type { UiKitCoreAppPayload } from '@rocket.chat/core-services'; +import type { UiKitCoreAppViewClosedPayload } from '@rocket.chat/core-services'; import type * as UiKit from '@rocket.chat/ui-kit'; import { CloudAnnouncementsModule } from './cloudAnnouncements.module'; @@ -6,7 +6,7 @@ import { CloudAnnouncementsModule } from './cloudAnnouncements.module'; export class CloudSubscriptionCommunication extends CloudAnnouncementsModule { override appId = 'cloud-communication-core'; - override async viewClosed(payload: UiKitCoreAppPayload): Promise { + override async viewClosed(payload: UiKitCoreAppViewClosedPayload): Promise { const { payload: { view: { viewId } = {} }, user: { _id: userId } = {}, diff --git a/apps/meteor/server/modules/core-apps/mention.module.ts b/apps/meteor/server/modules/core-apps/mention.module.ts index 8bd5bec4ce090..7d3785b4aaccd 100644 --- a/apps/meteor/server/modules/core-apps/mention.module.ts +++ b/apps/meteor/server/modules/core-apps/mention.module.ts @@ -1,6 +1,6 @@ import { api } from '@rocket.chat/core-services'; -import type { IUiKitCoreApp } from '@rocket.chat/core-services'; -import type { IMessage } from '@rocket.chat/core-typings'; +import type { IUiKitCoreApp, UiKitCoreAppBlockActionPayload } from '@rocket.chat/core-services'; +import type { IMessage, IUser } from '@rocket.chat/core-typings'; import { Subscriptions, Messages } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; @@ -24,13 +24,16 @@ const retrieveMentionsFromPayload = (stringifiedMentions: string): Exclude { + async blockAction(payload: UiKitCoreAppBlockActionPayload): Promise { const { actionId, payload: { value: stringifiedMentions, blockId: referenceMessageId }, } = payload; - const mentions = retrieveMentionsFromPayload(stringifiedMentions); + const user = payload.user!; + const room = payload.room!; + + const mentions = retrieveMentionsFromPayload(stringifiedMentions as string); const usernames = mentions.map(({ username }) => username); @@ -43,34 +46,34 @@ export class MentionModule implements IUiKitCoreApp { const joinedUsernames = `@${usernames.join(', @')}`; if (actionId === 'dismiss') { - void api.broadcast('notify.ephemeralMessage', payload.user._id, payload.room, { + void api.broadcast('notify.ephemeralMessage', user._id, room, { msg: i18n.t('You_mentioned___mentions__but_theyre_not_in_this_room', { mentions: joinedUsernames, - lng: payload.user.language, + lng: user.language, }), _id: payload.message, tmid: message.tmid, mentions, }); - return; + return undefined; } if (actionId === 'add-users') { - void addUsersToRoomMethod(payload.user._id, { rid: payload.room, users: usernames as string[] }, payload.user); - void api.broadcast('notify.ephemeralMessage', payload.user._id, payload.room, { + void addUsersToRoomMethod(user._id, { rid: room, users: usernames as string[] }, user); + void api.broadcast('notify.ephemeralMessage', user._id, room, { msg: i18n.t('You_mentioned___mentions__but_theyre_not_in_this_room', { mentions: joinedUsernames, - lng: payload.user.language, + lng: user.language, }), tmid: message.tmid, _id: payload.message, mentions, }); - return; + return undefined; } if (actionId === 'share-message') { - const sub = await Subscriptions.findOneByRoomIdAndUserId(payload.room, payload.user._id, { projection: { t: 1, rid: 1, name: 1 } }); + const sub = await Subscriptions.findOneByRoomIdAndUserId(room, user._id, { projection: { t: 1, rid: 1, name: 1 } }); // this should exist since the event is fired from withing the room (e.g the user sent a message) if (!sub) { throw new Error('Mention bot - Failed to retrieve room information'); @@ -83,7 +86,7 @@ export class MentionModule implements IUiKitCoreApp { const messageText = i18n.t('Youre_not_a_part_of__channel__and_I_mentioned_you_there', { channel: `#${sub.name}`, - lng: payload.user.language, + lng: user.language, }); const link = new URL(Meteor.absoluteUrl(roomPath)); @@ -97,18 +100,19 @@ export class MentionModule implements IUiKitCoreApp { text, separateResponse: true, // so that messages are sent to other DMs even if one or more fails }, - payload.user, + user as IUser & { username: string }, ); - void api.broadcast('notify.ephemeralMessage', payload.user._id, payload.room, { + void api.broadcast('notify.ephemeralMessage', user._id, room, { msg: i18n.t('You_mentioned___mentions__but_theyre_not_in_this_room_You_let_them_know_via_dm', { mentions: joinedUsernames, - lng: payload.user.language, + lng: user.language, }), tmid: message.tmid, _id: payload.message, mentions, }); + return undefined; } } } diff --git a/apps/meteor/server/modules/core-apps/nps.module.ts b/apps/meteor/server/modules/core-apps/nps.module.ts index 6e8965122df33..16e1763453f68 100644 --- a/apps/meteor/server/modules/core-apps/nps.module.ts +++ b/apps/meteor/server/modules/core-apps/nps.module.ts @@ -1,12 +1,13 @@ -import type { IUiKitCoreApp, UiKitCoreAppPayload } from '@rocket.chat/core-services'; +import type { IUiKitCoreApp, UiKitCoreAppBlockActionPayload, UiKitCoreAppViewSubmitPayload } from '@rocket.chat/core-services'; import { Banner, NPS } from '@rocket.chat/core-services'; +import type * as UiKit from '@rocket.chat/ui-kit'; import { createModal } from './nps/createModal'; export class Nps implements IUiKitCoreApp { appId = 'nps-core'; - async blockAction(payload: UiKitCoreAppPayload) { + async blockAction(payload: UiKitCoreAppBlockActionPayload): Promise { const { triggerId, actionId, @@ -32,7 +33,7 @@ export class Nps implements IUiKitCoreApp { }); } - async viewSubmit(payload: UiKitCoreAppPayload) { + async viewSubmit(payload: UiKitCoreAppViewSubmitPayload): Promise { if (!payload.payload?.view?.state || !payload.payload?.view?.id) { throw new Error('Invalid payload'); } @@ -65,7 +66,5 @@ export class Nps implements IUiKitCoreApp { } await Banner.dismiss(userId, bannerId); - - return true; } } diff --git a/apps/meteor/server/modules/core-apps/videoconf.module.ts b/apps/meteor/server/modules/core-apps/videoconf.module.ts index 694a0fac9b8e3..1f2765be0ce30 100644 --- a/apps/meteor/server/modules/core-apps/videoconf.module.ts +++ b/apps/meteor/server/modules/core-apps/videoconf.module.ts @@ -1,12 +1,13 @@ -import type { IUiKitCoreApp, UiKitCoreAppPayload } from '@rocket.chat/core-services'; +import type { IUiKitCoreApp, UiKitCoreAppBlockActionPayload } from '@rocket.chat/core-services'; import { VideoConf } from '@rocket.chat/core-services'; +import type * as UiKit from '@rocket.chat/ui-kit'; import { i18n } from '../../lib/i18n'; export class VideoConfModule implements IUiKitCoreApp { appId = 'videoconf-core'; - async blockAction(payload: UiKitCoreAppPayload) { + async blockAction(payload: UiKitCoreAppBlockActionPayload): Promise { const { triggerId, actionId, @@ -31,7 +32,6 @@ export class VideoConfModule implements IUiKitCoreApp { appId: this.appId, view: { appId: this.appId, - type: 'modal', id: `${callId}-info`, title: { type: 'plain_text', @@ -40,6 +40,8 @@ export class VideoConfModule implements IUiKitCoreApp { }, close: { type: 'button', + appId: this.appId, + blockId: callId, text: { type: 'plain_text', text: i18n.t('Close'), diff --git a/apps/meteor/server/services/uikit-core-app/service.ts b/apps/meteor/server/services/uikit-core-app/service.ts index a842a4854c6a3..7f07eb0a65fa6 100644 --- a/apps/meteor/server/services/uikit-core-app/service.ts +++ b/apps/meteor/server/services/uikit-core-app/service.ts @@ -1,5 +1,11 @@ import { ServiceClassInternal } from '@rocket.chat/core-services'; -import type { IUiKitCoreApp, IUiKitCoreAppService, UiKitCoreAppPayload } from '@rocket.chat/core-services'; +import type { + IUiKitCoreApp, + IUiKitCoreAppService, + UiKitCoreAppBlockActionPayload, + UiKitCoreAppViewClosedPayload, + UiKitCoreAppViewSubmitPayload, +} from '@rocket.chat/core-services'; const registeredApps = new Map(); @@ -24,29 +30,25 @@ export class UiKitCoreAppService extends ServiceClassInternal implements IUiKitC return registeredApps.has(appId); } - async blockAction(payload: UiKitCoreAppPayload) { + async blockAction(payload: UiKitCoreAppBlockActionPayload) { const { appId } = payload; const service = getAppModule(appId); - if (!service) { - return; - } + if (!service) return undefined; return service.blockAction?.(payload); } - async viewClosed(payload: UiKitCoreAppPayload) { + async viewClosed(payload: UiKitCoreAppViewClosedPayload) { const { appId } = payload; const service = getAppModule(appId); - if (!service) { - return; - } + if (!service) return undefined; return service.viewClosed?.(payload); } - async viewSubmit(payload: UiKitCoreAppPayload) { + async viewSubmit(payload: UiKitCoreAppViewSubmitPayload) { const { appId } = payload; const service = getAppModule(appId); diff --git a/apps/meteor/server/services/video-conference/service.ts b/apps/meteor/server/services/video-conference/service.ts index 1ea8c03f95c18..a5610b690f5de 100644 --- a/apps/meteor/server/services/video-conference/service.ts +++ b/apps/meteor/server/services/video-conference/service.ts @@ -172,7 +172,7 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf }); } - public async getInfo(callId: VideoConference['_id'], uid: IUser['_id'] | undefined): Promise { + public async getInfo(callId: VideoConference['_id'], uid: IUser['_id'] | undefined): Promise { const call = await VideoConferenceModel.findOneById(callId); if (!call) { throw new Error('invalid-call'); @@ -202,7 +202,7 @@ export class VideoConfService extends ServiceClassInternal implements IVideoConf }); if (blocks?.length) { - return blocks as UiKit.LayoutBlock[]; + return blocks as UiKit.ModalSurfaceLayout; } return [ diff --git a/packages/core-services/src/index.ts b/packages/core-services/src/index.ts index 817316a345b72..c0f2aeb517611 100644 --- a/packages/core-services/src/index.ts +++ b/packages/core-services/src/index.ts @@ -43,7 +43,13 @@ import type { IListRoomsFilter, } from './types/ITeamService'; import type { ITelemetryEvent, TelemetryMap, TelemetryEvents } from './types/ITelemetryEvent'; -import type { UiKitCoreAppPayload, IUiKitCoreApp, IUiKitCoreAppService } from './types/IUiKitCoreApp'; +import type { + UiKitCoreAppBlockActionPayload, + UiKitCoreAppViewClosedPayload, + UiKitCoreAppViewSubmitPayload, + IUiKitCoreApp, + IUiKitCoreAppService, +} from './types/IUiKitCoreApp'; import type { ISendFileLivechatMessageParams, ISendFileMessageParams, IUploadFileParams, IUploadService } from './types/IUploadService'; import type { IUserService } from './types/IUserService'; import type { IVideoConfService, VideoConferenceJoinOptions } from './types/IVideoConfService'; @@ -118,7 +124,9 @@ export { ITeamService, ITeamUpdateData, ITelemetryEvent, - UiKitCoreAppPayload, + UiKitCoreAppBlockActionPayload, + UiKitCoreAppViewClosedPayload, + UiKitCoreAppViewSubmitPayload, IUiKitCoreApp, IUiKitCoreAppService, IVideoConfService, diff --git a/packages/core-services/src/types/IUiKitCoreApp.ts b/packages/core-services/src/types/IUiKitCoreApp.ts index 5ba521b73642c..330fcbdff3e68 100644 --- a/packages/core-services/src/types/IUiKitCoreApp.ts +++ b/packages/core-services/src/types/IUiKitCoreApp.ts @@ -1,56 +1,61 @@ -import type { IUser } from '@rocket.chat/core-typings'; +import type { IMessage, IRoom, IUser } from '@rocket.chat/core-typings'; +import type * as UiKit from '@rocket.chat/ui-kit'; import type { IServiceClass } from './ServiceClass'; -// TODO: Fix this type to match `UiKit.UserInteraction` from `@rocket.chat/core-typings` -export type UiKitCoreAppPayload = { +export type UiKitCoreAppBlockActionPayload = Pick< + UiKit.ViewBlockActionUserInteraction | UiKit.MessageBlockActionUserInteraction, + 'type' | 'actionId' | 'triggerId' | 'container' | 'payload' +> & { appId: string; - type: 'blockAction' | 'viewClosed' | 'viewSubmit'; - actionId?: string; - triggerId?: string; - container?: { - id: string; - [key: string]: unknown; - }; - message?: unknown; + message: IMessage['_id'] | undefined; + user: IUser | undefined; + visitor: + | { + id: string; + username: string; + name?: string; + department?: string; + updatedAt?: Date; + token: string; + phone?: { phoneNumber: string }[] | null; + visitorEmails?: { address: string }[]; + livechatData?: Record; + status?: 'online' | 'away' | 'offline' | 'busy' | 'disabled'; + } + | undefined; + room: IRoom['_id'] | undefined; +}; + +export type UiKitCoreAppViewClosedPayload = Pick & { + appId: string; + user: IUser | undefined; payload: { - blockId?: string; - value?: unknown; - view?: { + view: UiKit.View & { viewId?: string; id?: string; - state?: { [blockId: string]: { [key: string]: unknown } }; - [key: string]: unknown; + state: { [blockId: string]: { [key: string]: unknown } }; }; - isCleared?: unknown; + isCleared?: boolean; }; - user?: IUser; - visitor?: { - id: string; - username: string; - name?: string; - department?: string; - updatedAt?: Date; - token: string; - phone?: { phoneNumber: string }[] | null; - visitorEmails?: { address: string }[]; - livechatData?: Record; - status?: 'online' | 'away' | 'offline' | 'busy' | 'disabled'; - }; - room?: unknown; +}; + +export type UiKitCoreAppViewSubmitPayload = Pick & { + appId: string; + user: IUser | undefined; }; export interface IUiKitCoreApp { appId: string; - blockAction?(payload: UiKitCoreAppPayload): Promise; - viewClosed?(payload: UiKitCoreAppPayload): Promise; - viewSubmit?(payload: UiKitCoreAppPayload): Promise; + blockAction?(payload: UiKitCoreAppBlockActionPayload): Promise; + viewClosed?(payload: UiKitCoreAppViewClosedPayload): Promise; + viewSubmit?(payload: UiKitCoreAppViewSubmitPayload): Promise; } export interface IUiKitCoreAppService extends IServiceClass { isRegistered(appId: string): Promise; - blockAction(payload: UiKitCoreAppPayload): Promise; - viewClosed(payload: UiKitCoreAppPayload): Promise; - viewSubmit(payload: UiKitCoreAppPayload): Promise; + blockAction(payload: UiKitCoreAppBlockActionPayload): Promise; + viewClosed(payload: UiKitCoreAppViewClosedPayload): Promise; + viewSubmit(payload: UiKitCoreAppViewSubmitPayload): Promise; } diff --git a/packages/core-services/src/types/IVideoConfService.ts b/packages/core-services/src/types/IVideoConfService.ts index 3c21db2f677b0..6d74413ccf211 100644 --- a/packages/core-services/src/types/IVideoConfService.ts +++ b/packages/core-services/src/types/IVideoConfService.ts @@ -21,7 +21,7 @@ export interface IVideoConfService { create(data: VideoConferenceCreateData, useAppUser?: boolean): Promise; start(caller: IUser['_id'], rid: string, options: { title?: string; allowRinging?: boolean }): Promise; join(uid: IUser['_id'] | undefined, callId: VideoConference['_id'], options: VideoConferenceJoinOptions): Promise; - getInfo(callId: VideoConference['_id'], uid: IUser['_id'] | undefined): Promise; + getInfo(callId: VideoConference['_id'], uid: IUser['_id'] | undefined): Promise; cancel(uid: IUser['_id'], callId: VideoConference['_id']): Promise; get(callId: VideoConference['_id']): Promise | null>; getUnfiltered(callId: VideoConference['_id']): Promise; diff --git a/packages/rest-typings/src/apps/index.ts b/packages/rest-typings/src/apps/index.ts index a012d251b9801..89d34deeb4bea 100644 --- a/packages/rest-typings/src/apps/index.ts +++ b/packages/rest-typings/src/apps/index.ts @@ -257,6 +257,6 @@ export type AppsEndpoints = { }; '/apps/ui.interaction/:id': { - POST: (params: UiKit.UserInteraction) => any; + POST: (params: UiKit.UserInteraction) => UiKit.ServerInteraction | Record; }; }; From 389837ca066d8b1307dd94fa8880be936c3bf102 Mon Sep 17 00:00:00 2001 From: Yashika soni Date: Wed, 25 Feb 2026 02:40:01 +0530 Subject: [PATCH 035/108] fix(livechat): support query and fields parameters on livechat rooms endpoint (#38059) Co-authored-by: Guilherme Gazzo --- apps/meteor/app/livechat/imports/server/rest/rooms.ts | 3 ++- apps/meteor/app/livechat/server/api/lib/rooms.ts | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/meteor/app/livechat/imports/server/rest/rooms.ts b/apps/meteor/app/livechat/imports/server/rest/rooms.ts index 917647da06334..f7462495013bf 100644 --- a/apps/meteor/app/livechat/imports/server/rest/rooms.ts +++ b/apps/meteor/app/livechat/imports/server/rest/rooms.ts @@ -29,7 +29,7 @@ API.v1.addRoute( { async get() { const { offset, count } = await getPaginationItems(this.queryParams); - const { sort, fields } = await this.parseJsonQuery(); + const { sort, fields, query } = await this.parseJsonQuery(); const { agents, departmentId, open, tags, roomName, onhold, queued, units } = this.queryParams; const { createdAt, customFields, closedAt } = this.queryParams; @@ -71,6 +71,7 @@ API.v1.addRoute( onhold, queued, units, + query, options: { offset, count, sort, fields }, callerId: this.userId, }), diff --git a/apps/meteor/app/livechat/server/api/lib/rooms.ts b/apps/meteor/app/livechat/server/api/lib/rooms.ts index f8b4a5d4acd10..5632a56d6411d 100644 --- a/apps/meteor/app/livechat/server/api/lib/rooms.ts +++ b/apps/meteor/app/livechat/server/api/lib/rooms.ts @@ -16,6 +16,7 @@ export async function findRooms({ onhold, queued, units, + query, options: { offset, count, fields, sort }, callerId, }: { @@ -36,10 +37,12 @@ export async function findRooms({ onhold?: string | boolean; queued?: string | boolean; units?: Array; + query?: Record; options: { offset: number; count: number; fields: Record; sort: Record }; callerId: string; }): Promise }>> { - const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}, { unitsFilter: units, userId: callerId }); + const extraQueryBase = await callbacks.run('livechat.applyRoomRestrictions', {}, { unitsFilter: units, userId: callerId }); + const extraQuery = { ...extraQueryBase, ...(query || {}) }; const { cursor, totalCount } = LivechatRooms.findRoomsWithCriteria({ agents, roomName, From 85c0ac7d8c7a5b7b89ef58f4a42b18467a8e2dd4 Mon Sep 17 00:00:00 2001 From: Isha <2025eb1100207@bitspilani-digital.edu.in> Date: Wed, 25 Feb 2026 02:40:24 +0530 Subject: [PATCH 036/108] fix(ux): show error message for invalid email domain on signup (#38955) Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Guilherme Gazzo Co-authored-by: Ricardo Garim --- .changeset/nasty-candles-invent.md | 6 ++++++ packages/i18n/src/locales/af.i18n.json | 1 + packages/i18n/src/locales/ar.i18n.json | 1 + packages/i18n/src/locales/az.i18n.json | 1 + packages/i18n/src/locales/be-BY.i18n.json | 1 + packages/i18n/src/locales/bg.i18n.json | 1 + packages/i18n/src/locales/bs.i18n.json | 1 + packages/i18n/src/locales/ca.i18n.json | 1 + packages/i18n/src/locales/cs.i18n.json | 1 + packages/i18n/src/locales/cy.i18n.json | 1 + packages/i18n/src/locales/da.i18n.json | 1 + packages/i18n/src/locales/de-AT.i18n.json | 1 + packages/i18n/src/locales/de.i18n.json | 1 + packages/i18n/src/locales/en.i18n.json | 1 + packages/web-ui-registration/src/RegisterForm.tsx | 3 +++ 15 files changed, 22 insertions(+) create mode 100644 .changeset/nasty-candles-invent.md diff --git a/.changeset/nasty-candles-invent.md b/.changeset/nasty-candles-invent.md new file mode 100644 index 0000000000000..2af4dcf9cebf1 --- /dev/null +++ b/.changeset/nasty-candles-invent.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/web-ui-registration': patch +'@rocket.chat/i18n': patch +--- + +Fixes invalid email domain error not being displayed on the registration form. diff --git a/packages/i18n/src/locales/af.i18n.json b/packages/i18n/src/locales/af.i18n.json index 7d5475f6b113e..b549e4e385ae2 100644 --- a/packages/i18n/src/locales/af.i18n.json +++ b/packages/i18n/src/locales/af.i18n.json @@ -2600,6 +2600,7 @@ "registration.component.form.emailOrUsername": "E-pos of gebruikersnaam", "registration.component.form.invalidConfirmPass": "Die wagwoord bevestiging pas nie by die wagwoord nie", "registration.component.form.invalidEmail": "Die ingevoerde e-pos is ongeldig", + "registration.component.form.invalidEmailDomain": "Die ingevoerde e-posadres het 'n ongeldige e-posdomein", "registration.component.form.name": "naam", "registration.component.form.password": "wagwoord", "registration.component.form.reasonToJoin": "Rede om deel te neem", diff --git a/packages/i18n/src/locales/ar.i18n.json b/packages/i18n/src/locales/ar.i18n.json index ccec8ea37a0f8..0c1a41d23c515 100644 --- a/packages/i18n/src/locales/ar.i18n.json +++ b/packages/i18n/src/locales/ar.i18n.json @@ -4588,6 +4588,7 @@ "registration.component.form.emailOrUsername": "البريد الإلكتروني أو اسم المستخدم", "registration.component.form.invalidConfirmPass": "تأكيد كلمة السر لا تطابق كلمة السر", "registration.component.form.invalidEmail": "البريد الإلكتروني الذي تم إدخاله غير صالح", + "registration.component.form.invalidEmailDomain": "البريد الإلكتروني المُدخل يحتوي على نطاق غير صالح", "registration.component.form.name": "الاسم", "registration.component.form.password": "كلمة المرور", "registration.component.form.reasonToJoin": "سبب الانضمام", diff --git a/packages/i18n/src/locales/az.i18n.json b/packages/i18n/src/locales/az.i18n.json index 31d596b498810..f9b65a7e21865 100644 --- a/packages/i18n/src/locales/az.i18n.json +++ b/packages/i18n/src/locales/az.i18n.json @@ -2603,6 +2603,7 @@ "registration.component.form.emailOrUsername": "E-poçt və ya istifadəçi adı", "registration.component.form.invalidConfirmPass": "Şifrənin təsdiqlənməsi şifrə uyğun gəlmir", "registration.component.form.invalidEmail": "Girilən e-poçt etibarsızdır", + "registration.component.form.invalidEmailDomain":"Daxil edilən e-poçt ünvanının domeni etibarsızdır", "registration.component.form.name": "Adı", "registration.component.form.password": "Şifrə", "registration.component.form.reasonToJoin": "Qoşulma səbəbi", diff --git a/packages/i18n/src/locales/be-BY.i18n.json b/packages/i18n/src/locales/be-BY.i18n.json index 856b44c4eb435..cea9625fc8c18 100644 --- a/packages/i18n/src/locales/be-BY.i18n.json +++ b/packages/i18n/src/locales/be-BY.i18n.json @@ -2625,6 +2625,7 @@ "registration.component.form.emailOrUsername": "Email або імя карыстальніка", "registration.component.form.invalidConfirmPass": "Пацвярджэнне пароля не супадае пароль", "registration.component.form.invalidEmail": "Адрас электроннай пошты ня дзейнічае", + "registration.component.form.invalidEmailDomain":"Уведзены адрас электроннай пошты мае несапраўдны дамен", "registration.component.form.name": "імя", "registration.component.form.password": "пароль", "registration.component.form.reasonToJoin": "прычына рэгістрацыі", diff --git a/packages/i18n/src/locales/bg.i18n.json b/packages/i18n/src/locales/bg.i18n.json index 2a8e0fb5fb6d4..eb435c8a99968 100644 --- a/packages/i18n/src/locales/bg.i18n.json +++ b/packages/i18n/src/locales/bg.i18n.json @@ -2594,6 +2594,7 @@ "registration.component.form.emailAlreadyExists": "Електроната поща вече съсществува", "registration.component.form.invalidConfirmPass": "Потвърждението на паролата не съвпада с паролата", "registration.component.form.invalidEmail": "Въведеният имейл адрес е невалиден", + "registration.component.form.invalidEmailDomain":"Въведеният имейл адрес има невалиден домейн", "registration.component.form.name": "Име", "registration.component.form.password": "Парола", "registration.component.form.reasonToJoin": "Причина за присъединяване", diff --git a/packages/i18n/src/locales/bs.i18n.json b/packages/i18n/src/locales/bs.i18n.json index 3ceac1accd223..8c1b13dcb7bf9 100644 --- a/packages/i18n/src/locales/bs.i18n.json +++ b/packages/i18n/src/locales/bs.i18n.json @@ -2591,6 +2591,7 @@ "registration.component.form.emailAlreadyExists": "Email već postoji", "registration.component.form.invalidConfirmPass": "Potvrda lozinke se ne slaže sa lozinkom", "registration.component.form.invalidEmail": "Uneseni e-mail nije valjan", + "registration.component.form.invalidEmailDomain":"Unesena e-mail adresa ima nevažeću domenu", "registration.component.form.name": "Ime", "registration.component.form.password": "Lozinka", "registration.component.form.reasonToJoin": "Razlog pridruživanja", diff --git a/packages/i18n/src/locales/ca.i18n.json b/packages/i18n/src/locales/ca.i18n.json index 9b7c5a9bb08ab..8200829718b78 100644 --- a/packages/i18n/src/locales/ca.i18n.json +++ b/packages/i18n/src/locales/ca.i18n.json @@ -4421,6 +4421,7 @@ "registration.component.form.emailAlreadyExists": "L'adreça de correu electrònic ja existeix", "registration.component.form.invalidConfirmPass": "La confirmació de la contrasenya no coincideix amb la contrasenya", "registration.component.form.invalidEmail": "L'adreça de correu-e és invàlida", + "registration.component.form.invalidEmailDomain":"El correu electrònic introduït té un domini no vàlid", "registration.component.form.name": "Nom", "registration.component.form.password": "Contrasenya", "registration.component.form.reasonToJoin": "Motiu per unir-se", diff --git a/packages/i18n/src/locales/cs.i18n.json b/packages/i18n/src/locales/cs.i18n.json index 6ee0346b7fc2c..9e0add1b82323 100644 --- a/packages/i18n/src/locales/cs.i18n.json +++ b/packages/i18n/src/locales/cs.i18n.json @@ -3763,6 +3763,7 @@ "registration.component.form.emailAlreadyExists": "Email již existuje", "registration.component.form.invalidConfirmPass": "Hesla nesouhlasí", "registration.component.form.invalidEmail": "Zadaný e-mail je neplatný", + "registration.component.form.invalidEmailDomain":"Zadaná e-mailová adresa má neplatnou doménu", "registration.component.form.name": "Jméno", "registration.component.form.password": "Heslo", "registration.component.form.reasonToJoin": "Důvod připojení", diff --git a/packages/i18n/src/locales/cy.i18n.json b/packages/i18n/src/locales/cy.i18n.json index 7f473b852e6fc..a987c848123b6 100644 --- a/packages/i18n/src/locales/cy.i18n.json +++ b/packages/i18n/src/locales/cy.i18n.json @@ -2593,6 +2593,7 @@ "registration.component.form.emailAlreadyExists": "Ebost eisoes yn bodoli", "registration.component.form.invalidConfirmPass": "Nid yw'r cadarnhad cyfrinair yn cyfateb i'r cyfrinair", "registration.component.form.invalidEmail": "Mae'r e-bost a roddwyd yn annilys", + "registration.component.form.invalidEmailDomain":"Mae gan y cyfeiriad e-bost a roddwyd barth annilys", "registration.component.form.name": "Enw", "registration.component.form.password": "Cyfrinair", "registration.component.form.reasonToJoin": "Rheswm i Ymuno", diff --git a/packages/i18n/src/locales/da.i18n.json b/packages/i18n/src/locales/da.i18n.json index 5dc221d6e6d95..46588c79f6406 100644 --- a/packages/i18n/src/locales/da.i18n.json +++ b/packages/i18n/src/locales/da.i18n.json @@ -3867,6 +3867,7 @@ "registration.component.form.emailAlreadyExists": "E-mailen eksisterer allerede", "registration.component.form.invalidConfirmPass": "Adgangskodebekræftelsen stemmer ikke overens med adgangskoden", "registration.component.form.invalidEmail": "Den indtastede e-mail er ugyldig", + "registration.component.form.invalidEmailDomain":"Den indtastede e-mailadresse har et ugyldigt domæne", "registration.component.form.name": "Navn", "registration.component.form.password": "Adgangskode", "registration.component.form.reasonToJoin": "Årsag til at deltage", diff --git a/packages/i18n/src/locales/de-AT.i18n.json b/packages/i18n/src/locales/de-AT.i18n.json index e305f8fbc6963..0ea013f277548 100644 --- a/packages/i18n/src/locales/de-AT.i18n.json +++ b/packages/i18n/src/locales/de-AT.i18n.json @@ -2599,6 +2599,7 @@ "registration.component.form.emailAlreadyExists": "Die E-Mail-Adresse existiert bereits.", "registration.component.form.invalidConfirmPass": "Die Passwörter stimmen nicht überein.", "registration.component.form.invalidEmail": "Die eingegebene E-Mail-Adresse ist ungültig.", + "registration.component.form.invalidEmailDomain": "Die eingegebene E-Mail-Adresse hat eine ungültige Domain.", "registration.component.form.name": "Name", "registration.component.form.password": "Passwort", "registration.component.form.reasonToJoin": "Grund zu Join", diff --git a/packages/i18n/src/locales/de.i18n.json b/packages/i18n/src/locales/de.i18n.json index 4a00676e5b8ab..5008477292050 100644 --- a/packages/i18n/src/locales/de.i18n.json +++ b/packages/i18n/src/locales/de.i18n.json @@ -5108,6 +5108,7 @@ "registration.component.form.emailOrUsername": "E-Mail-Adresse oder Nutzername", "registration.component.form.invalidConfirmPass": "Die Passwörter stimmen nicht überein.", "registration.component.form.invalidEmail": "Die eingegebene E-Mail-Adresse ist ungültig.", + "registration.component.form.invalidEmailDomain":"Die eingegebene E-Mail-Adresse hat eine ungültige Domain", "registration.component.form.name": "Name", "registration.component.form.password": "Passwort", "registration.component.form.reasonToJoin": "Info für den Admin, warum Sie beitreten möchten", diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 465ee29d3896b..52a27e071c3d2 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -6693,6 +6693,7 @@ "registration.component.form.emailPlaceholder": "example@example.com", "registration.component.form.invalidConfirmPass": "The password confirmation does not match password", "registration.component.form.invalidEmail": "The email entered is invalid", + "registration.component.form.invalidEmailDomain":"The entered email has invalid email domain", "registration.component.form.joinYourTeam": "Join your team", "registration.component.form.name": "Name", "registration.component.form.nameContainsInvalidChars": "Name contains invalid characters", diff --git a/packages/web-ui-registration/src/RegisterForm.tsx b/packages/web-ui-registration/src/RegisterForm.tsx index 9b7f3e548d3eb..4efbb0fdaff6b 100644 --- a/packages/web-ui-registration/src/RegisterForm.tsx +++ b/packages/web-ui-registration/src/RegisterForm.tsx @@ -93,6 +93,9 @@ export const RegisterForm = ({ setLoginRoute }: { setLoginRoute: DispatchLoginRo if (error.errorType === 'error-user-already-exists') { setError('username', { type: 'user-already-exists', message: t('registration.component.form.usernameAlreadyExists') }); } + if (error.errorType === 'error-invalid-domain') { + setError('email', { type: 'invalid-domain', message: t('registration.component.form.invalidEmailDomain') }); + } if (/Email already exists/.test(error.error)) { setError('email', { type: 'email-already-exists', message: t('registration.component.form.emailAlreadyExists') }); } From 35239b2b1df63b30b09690e19c02df37a1a32008 Mon Sep 17 00:00:00 2001 From: Shreyas Date: Wed, 25 Feb 2026 02:40:44 +0530 Subject: [PATCH 037/108] refactor: make reducePlainTexts linear and allocation-light (#38901) Co-authored-by: Guilherme Gazzo --- packages/message-parser/src/utils.ts | 35 ++++++++++++++-------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/packages/message-parser/src/utils.ts b/packages/message-parser/src/utils.ts index 252cc67274818..692971e153d8e 100644 --- a/packages/message-parser/src/utils.ts +++ b/packages/message-parser/src/utils.ts @@ -182,23 +182,24 @@ const joinEmoji = (current: Inlines, previous: Inlines | undefined, next: Inline return current; }; -export const reducePlainTexts = (values: Paragraph['value']): Paragraph['value'] => - values.flat().reduce( - (result, item, index, values) => { - const next = values[index + 1]; - const current = joinEmoji(item, values[index - 1], next); - const previous: Inlines = result[result.length - 1]; - - if (previous) { - if (current.type === 'PLAIN_TEXT' && current.type === previous.type) { - previous.value += current.value; - return result; - } - } - return [...result, current]; - }, - [] as Paragraph['value'], - ); +export const reducePlainTexts = (values: Paragraph['value']): Paragraph['value'] => { + const flattenedValues = values.flat(); + const result: Paragraph['value'] = []; + + for (let index = 0; index < flattenedValues.length; index++) { + const current = joinEmoji(flattenedValues[index], flattenedValues[index - 1], flattenedValues[index + 1]); + const previous = result[result.length - 1]; + + if (previous && current.type === 'PLAIN_TEXT' && previous.type === 'PLAIN_TEXT') { + previous.value += current.value; + continue; + } + + result.push(current); + } + + return result; +}; export const lineBreak = (): LineBreak => ({ type: 'LINE_BREAK', value: undefined, From 19bf55c8449e79de00548de279dde3cab7e58c23 Mon Sep 17 00:00:00 2001 From: Shreyas Date: Wed, 25 Feb 2026 02:41:12 +0530 Subject: [PATCH 038/108] fix(streamer): await sendToManySubscriptions async dispatch (#38681) --- .../modules/streamer/streamer.module.ts | 35 ++++-- .../modules/streamer/streamer.module.spec.ts | 112 ++++++++++++++++++ 2 files changed, 135 insertions(+), 12 deletions(-) create mode 100644 apps/meteor/tests/unit/server/modules/streamer/streamer.module.spec.ts diff --git a/apps/meteor/server/modules/streamer/streamer.module.ts b/apps/meteor/server/modules/streamer/streamer.module.ts index 26996e96bf431..ef35622ea5f2c 100644 --- a/apps/meteor/server/modules/streamer/streamer.module.ts +++ b/apps/meteor/server/modules/streamer/streamer.module.ts @@ -290,19 +290,30 @@ export abstract class Streamer extends EventEmit args: any[], getMsg: string | TransformMessage, ): Promise { - subscriptions.forEach(async (subscription) => { - if (this.retransmitToSelf === false && origin && origin === subscription.subscription.connection) { - return; - } - - const allowed = await this.isEmitAllowed(subscription.subscription, eventName, ...args); - if (allowed) { - const msg = typeof getMsg === 'string' ? getMsg : getMsg(this, subscription, eventName, args, allowed); - if (msg) { - subscription.subscription._session.socket?.send(msg); + await Promise.all( + [...subscriptions].map(async (subscription) => { + try { + if (this.retransmitToSelf === false && origin && origin === subscription.subscription.connection) { + return; + } + + const allowed = await this.isEmitAllowed(subscription.subscription, eventName, ...args); + if (allowed) { + const msg = typeof getMsg === 'string' ? getMsg : getMsg(this, subscription, eventName, args, allowed); + if (msg) { + subscription.subscription._session.socket?.send(msg); + } + } + } catch (err) { + SystemLogger.error({ + msg: 'Error while delivering streamer event', + eventName, + streamName: this.name, + err, + }); } - } - }); + }), + ); } override emit(eventName: string | symbol, ...args: any[]): boolean { diff --git a/apps/meteor/tests/unit/server/modules/streamer/streamer.module.spec.ts b/apps/meteor/tests/unit/server/modules/streamer/streamer.module.spec.ts new file mode 100644 index 0000000000000..2cf64b7857a97 --- /dev/null +++ b/apps/meteor/tests/unit/server/modules/streamer/streamer.module.spec.ts @@ -0,0 +1,112 @@ +import { expect } from 'chai'; +import sinon from 'sinon'; + +import { Streamer, StreamerCentral } from '../../../../../server/modules/streamer/streamer.module'; + +class TestStreamer extends Streamer { + registerPublication(): void { + // no-op for unit test subclass + } + + registerMethod(): void { + // no-op for unit test subclass + } + + changedPayload(): string { + return 'payload'; + } +} + +type TestSubscription = { + entry: any; + connection: Record; + send: sinon.SinonSpy; +}; + +const makeSubscription = (connectionId: string): TestSubscription => { + const send = sinon.spy(); + const connection = { id: connectionId }; + + return { + connection, + send, + entry: { + eventName: 'event', + subscription: { + connection, + _session: { + socket: { send }, + }, + }, + }, + }; +}; + +describe('Streamer.sendToManySubscriptions', () => { + let streamer: TestStreamer; + let streamerNameSeed = 0; + + beforeEach(() => { + streamer = new TestStreamer(`streamer-test-${streamerNameSeed++}`); + }); + + afterEach(() => { + sinon.restore(); + delete StreamerCentral.instances[streamer.name]; + }); + + it('waits for async permission checks before resolving', async () => { + const sub = makeSubscription('conn-1'); + + const isEmitAllowed = sinon.stub(streamer, 'isEmitAllowed').resolves(true); + + const sendPromise = streamer.sendToManySubscriptions(new Set([sub.entry]), undefined, 'event', [], 'test-msg'); + + expect(sub.send.called).to.equal(false); + + await sendPromise; + + expect(isEmitAllowed.calledOnceWithExactly(sub.entry.subscription, 'event')).to.equal(true); + expect(sub.send.calledOnceWithExactly('test-msg')).to.equal(true); + }); + + it('skips origin subscription and sends only to allowed subscriptions', async () => { + const originSub = makeSubscription('origin'); + const allowedSub = makeSubscription('allowed'); + const deniedSub = makeSubscription('denied'); + + const isEmitAllowed = sinon.stub(streamer, 'isEmitAllowed'); + isEmitAllowed.onFirstCall().resolves(true); + isEmitAllowed.onSecondCall().resolves(false); + + await streamer.sendToManySubscriptions( + new Set([originSub.entry, allowedSub.entry, deniedSub.entry]), + originSub.connection as any, + 'event', + [], + 'test-msg', + ); + + expect(originSub.send.called).to.equal(false); + expect(allowedSub.send.calledOnceWithExactly('test-msg')).to.equal(true); + expect(deniedSub.send.called).to.equal(false); + }); + + it('continues dispatching to other subscribers when a permission check rejects', async () => { + const failingSub = makeSubscription('failing'); + const successSub = makeSubscription('success'); + const error = new Error('boom'); + + const isEmitAllowed = sinon.stub(streamer, 'isEmitAllowed'); + isEmitAllowed.onFirstCall().rejects(error); + isEmitAllowed.onSecondCall().resolves(true); + + await streamer.sendToManySubscriptions(new Set([failingSub.entry, successSub.entry]), undefined, 'event-name', [], 'test-msg'); + + expect(isEmitAllowed.calledTwice).to.equal(true); + expect(isEmitAllowed.firstCall.calledWithExactly(failingSub.entry.subscription, 'event-name')).to.equal(true); + expect(isEmitAllowed.secondCall.calledWithExactly(successSub.entry.subscription, 'event-name')).to.equal(true); + expect(failingSub.send.called).to.equal(false); + expect(successSub.send.calledOnceWithExactly('test-msg')).to.equal(true); + }); +}); From fb188dc2ef823f827e3516f56ada1edc9a09c66b Mon Sep 17 00:00:00 2001 From: ASHWANI YADAV <22ashwaniyadav@gmail.com> Date: Wed, 25 Feb 2026 02:41:57 +0530 Subject: [PATCH 039/108] chore(omnichannel): fix unsafe type assertions in CannedResponsesComposer (#38704) --- .../CannedResponsesComposer.tsx | 19 ++++++++++++++----- .../InsertPlaceholderDropdown.tsx | 4 ++-- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/apps/meteor/client/views/omnichannel/cannedResponses/components/CannedResponsesComposer/CannedResponsesComposer.tsx b/apps/meteor/client/views/omnichannel/cannedResponses/components/CannedResponsesComposer/CannedResponsesComposer.tsx index b2abf4a612a3f..db3e855cebce9 100644 --- a/apps/meteor/client/views/omnichannel/cannedResponses/components/CannedResponsesComposer/CannedResponsesComposer.tsx +++ b/apps/meteor/client/views/omnichannel/cannedResponses/components/CannedResponsesComposer/CannedResponsesComposer.tsx @@ -8,7 +8,7 @@ import { MessageComposerActionsDivider, } from '@rocket.chat/ui-composer'; import { useUserPreference } from '@rocket.chat/ui-contexts'; -import type { ComponentProps } from 'react'; +import type { ComponentProps, ChangeEvent } from 'react'; import { memo, useCallback, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -16,7 +16,11 @@ import InsertPlaceholderDropdown from './InsertPlaceholderDropdown'; import { Backdrop } from '../../../../../components/Backdrop'; import { useEmojiPicker } from '../../../../../contexts/EmojiPickerContext'; -const CannedResponsesComposer = ({ onChange, ...props }: ComponentProps) => { +type CannedResponsesComposerProps = Omit, 'onChange'> & { + onChange: (value: string) => void; +}; + +const CannedResponsesComposer = ({ onChange, ...props }: CannedResponsesComposerProps) => { const { t } = useTranslation(); const useEmojisPreference = useUserPreference('useEmojis'); @@ -53,7 +57,7 @@ const CannedResponsesComposer = ({ onChange, ...props }: ComponentProps - + ): void => onChange(e.target.value)} + {...props} + /> diff --git a/apps/meteor/client/views/omnichannel/cannedResponses/components/CannedResponsesComposer/InsertPlaceholderDropdown.tsx b/apps/meteor/client/views/omnichannel/cannedResponses/components/CannedResponsesComposer/InsertPlaceholderDropdown.tsx index c8972618745d8..0bff09fa387c2 100644 --- a/apps/meteor/client/views/omnichannel/cannedResponses/components/CannedResponsesComposer/InsertPlaceholderDropdown.tsx +++ b/apps/meteor/client/views/omnichannel/cannedResponses/components/CannedResponsesComposer/InsertPlaceholderDropdown.tsx @@ -5,7 +5,7 @@ import { memo } from 'react'; import { useTranslation } from 'react-i18next'; type InsertPlaceholderDropdownProps = { - onChange: any; + onChange: (value: string) => void; textAreaRef: RefObject; setVisible: Dispatch>; }; @@ -17,7 +17,7 @@ const InsertPlaceholderDropdown = ({ onChange, textAreaRef, setVisible }: Insert cursor: pointer; `; - const setPlaceholder = (name: any): void => { + const setPlaceholder = (name: string): void => { if (textAreaRef?.current) { const text = textAreaRef.current.value; const startPos = textAreaRef.current.selectionStart; From 37acece030bc9f39bdaa86ab0130eb818332033e Mon Sep 17 00:00:00 2001 From: Ahmed Nasser Date: Tue, 24 Feb 2026 23:14:46 +0200 Subject: [PATCH 040/108] chore: Add OpenAPI Support to users.getAvatarSuggestion API (#36402) --- .changeset/rare-waves-help.md | 6 + apps/meteor/app/api/server/v1/users.ts | 188 ++++++++++++++++--------- packages/rest-typings/src/v1/users.ts | 14 -- yarn.lock | 2 +- 4 files changed, 125 insertions(+), 85 deletions(-) create mode 100644 .changeset/rare-waves-help.md diff --git a/.changeset/rare-waves-help.md b/.changeset/rare-waves-help.md new file mode 100644 index 0000000000000..476f7e0839153 --- /dev/null +++ b/.changeset/rare-waves-help.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/rest-typings": minor +--- + +Add OpenAPI support for the Rocket.Chat users.getAvatarSuggestion API endpoints by migrating to a modern chained route definition syntax and utilizing shared AJV schemas for validation to enhance API documentation and ensure type safety through response validation. diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts index 88b6b67e3b442..d9e37b1f7b8b2 100644 --- a/apps/meteor/app/api/server/v1/users.ts +++ b/apps/meteor/app/api/server/v1/users.ts @@ -19,6 +19,8 @@ import { isUsersCheckUsernameAvailabilityParamsGET, isUsersSendConfirmationEmailParamsPOST, ajv, + validateBadRequestErrorResponse, + validateUnauthorizedErrorResponse, } from '@rocket.chat/rest-typings'; import { getLoginExpirationInMs, wrapExceptions } from '@rocket.chat/tools'; import { Accounts } from 'meteor/accounts-base'; @@ -97,20 +99,6 @@ API.v1.addRoute( }, ); -API.v1.addRoute( - 'users.getAvatarSuggestion', - { - authRequired: true, - }, - { - async get() { - const suggestions = await getAvatarSuggestionForUser(this.user); - - return API.v1.success({ suggestions }); - }, - }, -); - API.v1.addRoute( 'users.update', { authRequired: true, twoFactorRequired: true, validateParams: isUsersUpdateParamsPOST }, @@ -764,72 +752,132 @@ API.v1.addRoute( }, ); -const usersEndpoints = API.v1.post( - 'users.createToken', - { - authRequired: true, - body: ajv.compile<{ userId: string; secret: string }>({ - type: 'object', - properties: { - userId: { - type: 'string', - minLength: 1, - }, - secret: { - type: 'string', - minLength: 1, - }, - }, - required: ['userId', 'secret'], - additionalProperties: false, - }), - response: { - 200: ajv.compile<{ data: { userId: string; authToken: string } }>({ +const usersEndpoints = API.v1 + .post( + 'users.createToken', + { + authRequired: true, + body: ajv.compile<{ userId: string; secret: string }>({ type: 'object', properties: { - data: { - type: 'object', - properties: { - userId: { - type: 'string', - minLength: 1, - }, - authToken: { - type: 'string', - minLength: 1, - }, - }, - required: ['userId'], - additionalProperties: false, + userId: { + type: 'string', + minLength: 1, }, - success: { - type: 'boolean', - enum: [true], + secret: { + type: 'string', + minLength: 1, }, }, - required: ['data', 'success'], - additionalProperties: false, - }), - 400: ajv.compile({ - type: 'object', - properties: { - success: { type: 'boolean', enum: [false] }, - error: { type: 'string' }, - errorType: { type: 'string' }, - }, - required: ['success'], + required: ['userId', 'secret'], additionalProperties: false, }), + response: { + 200: ajv.compile<{ data: { userId: string; authToken: string } }>({ + type: 'object', + properties: { + data: { + type: 'object', + properties: { + userId: { + type: 'string', + minLength: 1, + }, + authToken: { + type: 'string', + minLength: 1, + }, + }, + required: ['userId'], + additionalProperties: false, + }, + success: { + type: 'boolean', + enum: [true], + }, + }, + required: ['data', 'success'], + additionalProperties: false, + }), + 400: ajv.compile({ + type: 'object', + properties: { + success: { type: 'boolean', enum: [false] }, + error: { type: 'string' }, + errorType: { type: 'string' }, + }, + required: ['success'], + additionalProperties: false, + }), + }, }, - }, - async function action() { - const user = await getUserFromParams(this.bodyParams); + async function action() { + const user = await getUserFromParams(this.bodyParams); - const data = await generateAccessToken(user._id, this.bodyParams.secret); + const data = await generateAccessToken(user._id, this.bodyParams.secret); - return API.v1.success({ data }); - }, -); + return API.v1.success({ data }); + }, + ) + .get( + 'users.getAvatarSuggestion', + { + authRequired: true, + response: { + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + 200: ajv.compile<{ + suggestions: Record< + string, + { + blob: string; + contentType: string; + service: string; + url: string; + } + >; + }>({ + type: 'object', + properties: { + success: { + type: 'boolean', + enum: [true], + }, + suggestions: { + type: 'object', + additionalProperties: { + type: 'object', + properties: { + blob: { + type: 'string', + }, + contentType: { + type: 'string', + }, + service: { + type: 'string', + }, + url: { + type: 'string', + format: 'uri', + }, + }, + required: ['blob', 'contentType', 'service', 'url'], + additionalProperties: false, + }, + }, + }, + required: ['success', 'suggestions'], + additionalProperties: false, + }), + }, + }, + async function action() { + const suggestions = await getAvatarSuggestionForUser(this.user); + + return API.v1.success({ suggestions }); + }, + ); API.v1.addRoute( 'users.getPreferences', diff --git a/packages/rest-typings/src/v1/users.ts b/packages/rest-typings/src/v1/users.ts index f0aec0a85aff4..565620d31ba5e 100644 --- a/packages/rest-typings/src/v1/users.ts +++ b/packages/rest-typings/src/v1/users.ts @@ -255,20 +255,6 @@ export type UsersEndpoints = { }; }; - '/v1/users.getAvatarSuggestion': { - GET: () => { - suggestions: Record< - string, - { - blob: string; - contentType: string; - service: string; - url: string; - } - >; - }; - }; - '/v1/users.checkUsernameAvailability': { GET: (params: { username: string }) => { result: boolean; diff --git a/yarn.lock b/yarn.lock index a93a67032f4eb..f219de83d458d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10437,7 +10437,7 @@ __metadata: peerDependencies: "@rocket.chat/layout": "*" "@rocket.chat/tools": 0.2.4 - "@rocket.chat/ui-contexts": 27.0.0 + "@rocket.chat/ui-contexts": 27.0.1 "@tanstack/react-query": "*" react: "*" react-hook-form: "*" From 7c7324184589a15bf3e67b4f0c1cc222f8d48db3 Mon Sep 17 00:00:00 2001 From: Ahmed Nasser Date: Tue, 24 Feb 2026 23:16:41 +0200 Subject: [PATCH 041/108] chore: Add OpenAPI Support to dm.close/im.close API (#38974) --- .changeset/wicked-buckets-thank.md | 6 + apps/meteor/app/api/server/v1/im.ts | 115 ++++++++++++------ .../rest-typings/src/v1/dm/DmCloseProps.ts | 18 --- packages/rest-typings/src/v1/dm/dm.ts | 1 - packages/rest-typings/src/v1/dm/im.ts | 10 +- 5 files changed, 90 insertions(+), 60 deletions(-) create mode 100644 .changeset/wicked-buckets-thank.md delete mode 100644 packages/rest-typings/src/v1/dm/DmCloseProps.ts diff --git a/.changeset/wicked-buckets-thank.md b/.changeset/wicked-buckets-thank.md new file mode 100644 index 0000000000000..cc6f8af59fcce --- /dev/null +++ b/.changeset/wicked-buckets-thank.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/rest-typings": patch +--- + +Add OpenAPI support for the Rocket.Chat dm.close/im.close API endpoints by migrating to a modern chained route definition syntax and utilizing shared AJV schemas for validation to enhance API documentation and ensure type safety through response validation. diff --git a/apps/meteor/app/api/server/v1/im.ts b/apps/meteor/app/api/server/v1/im.ts index 9972de8ce10cb..f97bd6e7e9161 100644 --- a/apps/meteor/app/api/server/v1/im.ts +++ b/apps/meteor/app/api/server/v1/im.ts @@ -6,6 +6,7 @@ import { Subscriptions, Uploads, Messages, Rooms, Users } from '@rocket.chat/mod import { ajv, validateUnauthorizedErrorResponse, + validateForbiddenErrorResponse, validateBadRequestErrorResponse, isDmFileProps, isDmMemberProps, @@ -99,6 +100,10 @@ type DmDeleteProps = username: string; }; +type DmCloseProps = { + roomId: string; +}; + const isDmDeleteProps = ajv.compile({ oneOf: [ { @@ -144,6 +149,43 @@ const dmDeleteEndpointsProps = { }, } as const; +const DmClosePropsSchema = { + type: 'object', + properties: { + roomId: { + type: 'string', + }, + userId: { + type: 'string', + }, + }, + required: ['roomId', 'userId'], + additionalProperties: false, +}; + +const isDmCloseProps = ajv.compile(DmClosePropsSchema); + +const dmCloseEndpointsProps = { + authRequired: true, + body: isDmCloseProps, + response: { + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + 403: validateForbiddenErrorResponse, + 200: ajv.compile({ + type: 'object', + properties: { + success: { + type: 'boolean', + enum: [true], + }, + }, + required: ['success'], + additionalProperties: false, + }), + }, +}; + const dmDeleteAction = (_path: Path): TypedAction => async function action() { const { room } = await findDirectMessageRoom(this.bodyParams, this.userId); @@ -160,51 +202,52 @@ const dmDeleteAction = (_path: Path): TypedAction(_path: Path): TypedAction => + async function action() { + const { roomId } = this.bodyParams; + if (!roomId) { + throw new Meteor.Error('error-room-param-not-provided', 'Body param "roomId" is required'); + } + if (!this.userId) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'dm.close', + }); + } + let subscription; -API.v1.addRoute( - ['dm.close', 'im.close'], - { authRequired: true }, - { - async post() { - const { roomId } = this.bodyParams; - if (!roomId) { - throw new Meteor.Error('error-room-param-not-provided', 'Body param "roomId" is required'); + const roomExists = !!(await Rooms.findOneById(roomId)); + if (!roomExists) { + // even if the room doesn't exist, we should allow the user to close the subscription anyways + subscription = await Subscriptions.findOneByRoomIdAndUserId(roomId, this.userId); + } else { + const canAccess = await canAccessRoomIdAsync(roomId, this.userId); + if (!canAccess) { + return API.v1.forbidden('error-not-allowed'); } - let subscription; - - const roomExists = !!(await Rooms.findOneById(roomId)); - if (!roomExists) { - // even if the room doesn't exist, we should allow the user to close the subscription anyways - subscription = await Subscriptions.findOneByRoomIdAndUserId(roomId, this.userId); - } else { - const canAccess = await canAccessRoomIdAsync(roomId, this.userId); - if (!canAccess) { - return API.v1.forbidden(); - } + const { subscription: subs } = await findDirectMessageRoom({ roomId }, this.userId); - const { subscription: subs } = await findDirectMessageRoom({ roomId }, this.userId); + subscription = subs; + } - subscription = subs; - } + if (!subscription) { + return API.v1.failure(`The user is not subscribed to the room`); + } - if (!subscription) { - return API.v1.failure(`The user is not subscribed to the room`); - } + if (!subscription.open) { + return API.v1.failure(`The direct message room, is already closed to the sender`); + } - if (!subscription.open) { - return API.v1.failure(`The direct message room, is already closed to the sender`); - } + await hideRoomMethod(this.userId, roomId); - await hideRoomMethod(this.userId, roomId); + return API.v1.success(); + }; - return API.v1.success(); - }, - }, -); +const dmEndpoints = API.v1 + .post('im.delete', dmDeleteEndpointsProps, dmDeleteAction('im.delete')) + .post('dm.delete', dmDeleteEndpointsProps, dmDeleteAction('dm.delete')) + .post('dm.close', dmCloseEndpointsProps, dmCloseAction('dm.close')) + .post('im.close', dmCloseEndpointsProps, dmCloseAction('im.close')); // https://github.com/RocketChat/Rocket.Chat/pull/9679 as reference API.v1.addRoute( diff --git a/packages/rest-typings/src/v1/dm/DmCloseProps.ts b/packages/rest-typings/src/v1/dm/DmCloseProps.ts deleted file mode 100644 index c2b0caae5a653..0000000000000 --- a/packages/rest-typings/src/v1/dm/DmCloseProps.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ajv } from '../Ajv'; - -export type DmCloseProps = { - roomId: string; -}; - -const DmClosePropsSchema = { - type: 'object', - properties: { - roomId: { - type: 'string', - }, - }, - required: ['roomId'], - additionalProperties: false, -}; - -export const isDmCloseProps = ajv.compile(DmClosePropsSchema); diff --git a/packages/rest-typings/src/v1/dm/dm.ts b/packages/rest-typings/src/v1/dm/dm.ts index ccaf19c7810ef..bbc4af18659a8 100644 --- a/packages/rest-typings/src/v1/dm/dm.ts +++ b/packages/rest-typings/src/v1/dm/dm.ts @@ -2,7 +2,6 @@ import type { ImEndpoints } from './im'; export type DmEndpoints = { '/v1/dm.create': ImEndpoints['/v1/im.create']; - '/v1/dm.close': ImEndpoints['/v1/im.close']; '/v1/dm.counters': ImEndpoints['/v1/im.counters']; '/v1/dm.files': ImEndpoints['/v1/im.files']; '/v1/dm.history': ImEndpoints['/v1/im.history']; diff --git a/packages/rest-typings/src/v1/dm/im.ts b/packages/rest-typings/src/v1/dm/im.ts index ae87b7d619ca9..82b9e5b909f3a 100644 --- a/packages/rest-typings/src/v1/dm/im.ts +++ b/packages/rest-typings/src/v1/dm/im.ts @@ -1,6 +1,5 @@ import type { IMessage, IRoom, IUser, IUploadWithUser, ISubscription } from '@rocket.chat/core-typings'; -import type { DmCloseProps } from './DmCloseProps'; import type { DmCreateProps } from './DmCreateProps'; import type { DmFileProps } from './DmFileProps'; import type { DmHistoryProps } from './DmHistoryProps'; @@ -10,17 +9,18 @@ import type { DmMessagesProps } from './DmMessagesProps'; import type { PaginatedRequest } from '../../helpers/PaginatedRequest'; import type { PaginatedResult } from '../../helpers/PaginatedResult'; +type DmKickProps = { + roomId: string; +}; + export type ImEndpoints = { '/v1/im.create': { POST: (params: DmCreateProps) => { room: IRoom & { rid: IRoom['_id'] }; }; }; - '/v1/im.close': { - POST: (params: DmCloseProps) => void; - }; '/v1/im.kick': { - POST: (params: DmCloseProps) => void; + POST: (params: DmKickProps) => void; }; '/v1/im.leave': { POST: (params: DmLeaveProps) => void; From a4e3c1635d55ec4ce04cbde741426770e43581fb Mon Sep 17 00:00:00 2001 From: Ahmed Nasser Date: Tue, 24 Feb 2026 23:19:38 +0200 Subject: [PATCH 042/108] chore: Add OpenAPI support for the Rocket.Chat autotranslate translateMessage API (#38978) --- .changeset/hungry-monkeys-hang.md | 6 ++ .../meteor/app/api/server/v1/autotranslate.ts | 88 +++++++++++++++---- .../server/functions/translateMessage.ts | 10 ++- .../server/methods/translateMessage.ts | 2 +- packages/rest-typings/src/index.ts | 1 - packages/rest-typings/src/v1/autoTranslate.ts | 8 +- ...AutotranslateTranslateMessageParamsPOST.ts | 25 ------ 7 files changed, 86 insertions(+), 54 deletions(-) create mode 100644 .changeset/hungry-monkeys-hang.md delete mode 100644 packages/rest-typings/src/v1/autotranslate/AutotranslateTranslateMessageParamsPOST.ts diff --git a/.changeset/hungry-monkeys-hang.md b/.changeset/hungry-monkeys-hang.md new file mode 100644 index 0000000000000..f128167d7cee7 --- /dev/null +++ b/.changeset/hungry-monkeys-hang.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/rest-typings": minor +--- + +Add OpenAPI support for the Rocket.Chat autotranslate translateMessage API endpoints by migrating to a modern chained route definition syntax and utilizing shared AJV schemas for validation to enhance API documentation and ensure type safety through response validation diff --git a/apps/meteor/app/api/server/v1/autotranslate.ts b/apps/meteor/app/api/server/v1/autotranslate.ts index a5c167f7d0a89..76f5ea1debbc3 100644 --- a/apps/meteor/app/api/server/v1/autotranslate.ts +++ b/apps/meteor/app/api/server/v1/autotranslate.ts @@ -1,7 +1,10 @@ +import type { IMessage } from '@rocket.chat/core-typings'; import { Messages } from '@rocket.chat/models'; import { + ajv, + validateUnauthorizedErrorResponse, + validateBadRequestErrorResponse, isAutotranslateSaveSettingsParamsPOST, - isAutotranslateTranslateMessageParamsPOST, isAutotranslateGetSupportedLanguagesParamsGET, } from '@rocket.chat/rest-typings'; @@ -9,6 +12,7 @@ import { getSupportedLanguages } from '../../../autotranslate/server/functions/g import { saveAutoTranslateSettings } from '../../../autotranslate/server/functions/saveSettings'; import { translateMessage } from '../../../autotranslate/server/functions/translateMessage'; import { settings } from '../../../settings/server'; +import type { ExtractRoutesFromAPI } from '../ApiClass'; import { API } from '../api'; API.v1.addRoute( @@ -69,29 +73,75 @@ API.v1.addRoute( }, ); -API.v1.addRoute( +type AutotranslateTranslateMessageParamsPOST = { + messageId: string; + targetLanguage?: string; +}; + +const AutotranslateTranslateMessageParamsPostSchema = { + type: 'object', + properties: { + messageId: { + type: 'string', + }, + targetLanguage: { + type: 'string', + nullable: true, + }, + }, + required: ['messageId'], + additionalProperties: false, +}; + +const isAutotranslateTranslateMessageParamsPOST = ajv.compile( + AutotranslateTranslateMessageParamsPostSchema, +); + +const autotranslateEndpoints = API.v1.post( 'autotranslate.translateMessage', { authRequired: true, - validateParams: isAutotranslateTranslateMessageParamsPOST, + body: isAutotranslateTranslateMessageParamsPOST, + response: { + 200: ajv.compile<{ message: IMessage }>({ + type: 'object', + properties: { + message: { $ref: '#/components/schemas/IMessage' }, + success: { type: 'boolean', enum: [true] }, + }, + required: ['message', 'success'], + additionalProperties: false, + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, }, - { - async post() { - const { messageId, targetLanguage } = this.bodyParams; - if (!settings.get('AutoTranslate_Enabled')) { - return API.v1.failure('AutoTranslate is disabled.'); - } - if (!messageId) { - return API.v1.failure('The bodyParam "messageId" is required.'); - } - const message = await Messages.findOneById(messageId); - if (!message) { - return API.v1.failure('Message not found.'); - } + async function action() { + const { messageId, targetLanguage } = this.bodyParams; + if (!settings.get('AutoTranslate_Enabled')) { + return API.v1.failure('AutoTranslate is disabled.'); + } + if (!messageId) { + return API.v1.failure('The bodyParam "messageId" is required.'); + } + const message = await Messages.findOneById(messageId); + if (!message) { + return API.v1.failure('Message not found.'); + } - const translatedMessage = await translateMessage(targetLanguage, message); + const translatedMessage = await translateMessage(targetLanguage, message); - return API.v1.success({ message: translatedMessage }); - }, + if (!translatedMessage) { + return API.v1.failure('Failed to translate message.'); + } + + return API.v1.success({ message: translatedMessage }); }, ); + +type AutotranslateEndpoints = ExtractRoutesFromAPI; + +declare module '@rocket.chat/rest-typings' { + // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface + interface Endpoints extends AutotranslateEndpoints {} +} diff --git a/apps/meteor/app/autotranslate/server/functions/translateMessage.ts b/apps/meteor/app/autotranslate/server/functions/translateMessage.ts index c3c9b0d8709df..8c0545369dcca 100644 --- a/apps/meteor/app/autotranslate/server/functions/translateMessage.ts +++ b/apps/meteor/app/autotranslate/server/functions/translateMessage.ts @@ -12,7 +12,15 @@ export const translateMessage = async (targetLanguage?: string, message?: IMessa } const room = await Rooms.findOneById(message?.rid); + let translatedMessage; + if (message && room) { - await TranslationProviderRegistry.translateMessage(message, room, targetLanguage); + translatedMessage = await TranslationProviderRegistry.translateMessage(message, room, targetLanguage); } + + if (!translatedMessage) { + return; + } + + return translatedMessage; }; diff --git a/apps/meteor/app/autotranslate/server/methods/translateMessage.ts b/apps/meteor/app/autotranslate/server/methods/translateMessage.ts index 7c8741782647c..c9d2cc43996be 100644 --- a/apps/meteor/app/autotranslate/server/methods/translateMessage.ts +++ b/apps/meteor/app/autotranslate/server/methods/translateMessage.ts @@ -7,7 +7,7 @@ import { translateMessage } from '../functions/translateMessage'; declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { - 'autoTranslate.translateMessage'(message: IMessage | undefined, targetLanguage: string): Promise; + 'autoTranslate.translateMessage'(message: IMessage | undefined, targetLanguage: string): Promise; } } diff --git a/packages/rest-typings/src/index.ts b/packages/rest-typings/src/index.ts index 278c6892afa75..76a36bffc4b45 100644 --- a/packages/rest-typings/src/index.ts +++ b/packages/rest-typings/src/index.ts @@ -242,7 +242,6 @@ export * from './v1/server-events'; export * from './v1/autotranslate/AutotranslateGetSupportedLanguagesParamsGET'; export * from './v1/autotranslate/AutotranslateSaveSettingsParamsPOST'; -export * from './v1/autotranslate/AutotranslateTranslateMessageParamsPOST'; export * from './v1/e2e/e2eSetUserPublicAndPrivateKeysParamsPOST'; export * from './v1/e2e/e2eUpdateGroupKeyParamsPOST'; export * from './v1/e2e'; diff --git a/packages/rest-typings/src/v1/autoTranslate.ts b/packages/rest-typings/src/v1/autoTranslate.ts index cf17f5bbf6ac5..9679dddfcd286 100644 --- a/packages/rest-typings/src/v1/autoTranslate.ts +++ b/packages/rest-typings/src/v1/autoTranslate.ts @@ -1,8 +1,7 @@ -import type { IMessage, ISupportedLanguage } from '@rocket.chat/core-typings'; +import type { ISupportedLanguage } from '@rocket.chat/core-typings'; import type { AutotranslateGetSupportedLanguagesParamsGET } from './autotranslate/AutotranslateGetSupportedLanguagesParamsGET'; import type { AutotranslateSaveSettingsParamsPOST } from './autotranslate/AutotranslateSaveSettingsParamsPOST'; -import type { AutotranslateTranslateMessageParamsPOST } from './autotranslate/AutotranslateTranslateMessageParamsPOST'; export type AutoTranslateEndpoints = { '/v1/autotranslate.getSupportedLanguages': { @@ -11,9 +10,4 @@ export type AutoTranslateEndpoints = { '/v1/autotranslate.saveSettings': { POST: (params: AutotranslateSaveSettingsParamsPOST) => void; }; - '/v1/autotranslate.translateMessage': { - POST: (params: AutotranslateTranslateMessageParamsPOST) => { - message: IMessage; - }; - }; }; diff --git a/packages/rest-typings/src/v1/autotranslate/AutotranslateTranslateMessageParamsPOST.ts b/packages/rest-typings/src/v1/autotranslate/AutotranslateTranslateMessageParamsPOST.ts deleted file mode 100644 index 8da798a801204..0000000000000 --- a/packages/rest-typings/src/v1/autotranslate/AutotranslateTranslateMessageParamsPOST.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { ajv } from '../Ajv'; - -export type AutotranslateTranslateMessageParamsPOST = { - messageId: string; - targetLanguage?: string; -}; - -const AutotranslateTranslateMessageParamsPostSchema = { - type: 'object', - properties: { - messageId: { - type: 'string', - }, - targetLanguage: { - type: 'string', - nullable: true, - }, - }, - required: ['messageId'], - additionalProperties: false, -}; - -export const isAutotranslateTranslateMessageParamsPOST = ajv.compile( - AutotranslateTranslateMessageParamsPostSchema, -); From 39f2e87e1caa6842e69155f033205cfdc4767b9e Mon Sep 17 00:00:00 2001 From: Ahmed Nasser Date: Wed, 25 Feb 2026 01:01:12 +0200 Subject: [PATCH 043/108] chore: Add OpenAPI Support to emoji-custom.create API (#36523) Co-authored-by: Matheus Cardoso Co-authored-by: Guilherme Gazzo --- .changeset/weak-terms-shave.md | 6 ++ apps/meteor/app/api/server/definition.ts | 2 +- apps/meteor/app/api/server/v1/emoji-custom.ts | 100 ++++++++++++------ packages/rest-typings/src/v1/emojiCustom.ts | 3 - 4 files changed, 76 insertions(+), 35 deletions(-) create mode 100644 .changeset/weak-terms-shave.md diff --git a/.changeset/weak-terms-shave.md b/.changeset/weak-terms-shave.md new file mode 100644 index 0000000000000..1813edcdb2b5b --- /dev/null +++ b/.changeset/weak-terms-shave.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/rest-typings": patch +--- + +Add OpenAPI support for the Rocket.Chat emoji-custom.create API endpoints by migrating to a modern chained route definition syntax and utilizing shared AJV schemas for validation to enhance API documentation and ensure type safety through response validation. diff --git a/apps/meteor/app/api/server/definition.ts b/apps/meteor/app/api/server/definition.ts index 702fd0e4fe3a1..34232aa197256 100644 --- a/apps/meteor/app/api/server/definition.ts +++ b/apps/meteor/app/api/server/definition.ts @@ -312,7 +312,7 @@ export type TypedThis query: Record; }>; bodyParams: TOptions['body'] extends ValidateFunction ? Body : never; - + request: Request; requestIp?: string; route: string; response: Response; diff --git a/apps/meteor/app/api/server/v1/emoji-custom.ts b/apps/meteor/app/api/server/v1/emoji-custom.ts index 15764b7f74e7d..0cc6bb53720d5 100644 --- a/apps/meteor/app/api/server/v1/emoji-custom.ts +++ b/apps/meteor/app/api/server/v1/emoji-custom.ts @@ -1,7 +1,7 @@ import { Media } from '@rocket.chat/core-services'; import type { IEmojiCustom } from '@rocket.chat/core-typings'; import { EmojiCustom } from '@rocket.chat/models'; -import { isEmojiCustomList } from '@rocket.chat/rest-typings'; +import { ajv, isEmojiCustomList } from '@rocket.chat/rest-typings'; import { escapeRegExp } from '@rocket.chat/string-helpers'; import { Meteor } from 'meteor/meteor'; @@ -11,6 +11,7 @@ import { insertOrUpdateEmoji } from '../../../emoji-custom/server/lib/insertOrUp import { uploadEmojiCustomWithBuffer } from '../../../emoji-custom/server/lib/uploadEmojiCustom'; import { deleteEmojiCustom } from '../../../emoji-custom/server/methods/deleteEmojiCustom'; import { settings } from '../../../settings/server'; +import type { ExtractRoutesFromAPI } from '../ApiClass'; import { API } from '../api'; import { getPaginationItems } from '../helpers/getPaginationItems'; import { findEmojisCustom } from '../lib/emoji-custom'; @@ -103,45 +104,73 @@ API.v1.addRoute( }, ); -API.v1.addRoute( +const emojiCustomCreateEndpoints = API.v1.post( 'emoji-custom.create', - { authRequired: true }, { - async post() { - const emoji = await getUploadFormData( - { - request: this.request, + authRequired: true, + response: { + 400: ajv.compile({ + type: 'object', + properties: { + success: { type: 'boolean', enum: [false] }, + stack: { type: 'string' }, + error: { type: 'string' }, + errorType: { type: 'string' }, + details: { type: 'string' }, }, - { field: 'emoji', sizeLimit: settings.get('FileUpload_MaxFileSize') }, - ); + required: ['success'], + additionalProperties: false, + }), + 200: ajv.compile({ + type: 'object', + properties: { + success: { + type: 'boolean', + enum: [true], + }, + }, + required: ['success'], + additionalProperties: false, + }), + }, + }, + async function action() { + const emoji = await getUploadFormData( + { + request: this.request, + }, + { + field: 'emoji', + sizeLimit: settings.get('FileUpload_MaxFileSize'), + }, + ); - const { fields, fileBuffer, mimetype } = emoji; + const { fields, fileBuffer, mimetype } = emoji; - const isUploadable = await Media.isImage(fileBuffer); - if (!isUploadable) { - throw new Meteor.Error('emoji-is-not-image', "Emoji file provided cannot be uploaded since it's not an image"); - } + const isUploadable = await Media.isImage(fileBuffer); + if (!isUploadable) { + throw new Meteor.Error('emoji-is-not-image', "Emoji file provided cannot be uploaded since it's not an image"); + } - const [, extension] = mimetype.split('/'); - fields.extension = extension; + const [, extension] = mimetype.split('/'); + fields.extension = extension; - try { - const emojiData = await insertOrUpdateEmoji(this.userId, { - ...fields, - newFile: true, - aliases: fields.aliases || '', - name: fields.name, - extension: fields.extension, - }); + try { + const emojiData = await insertOrUpdateEmoji(this.userId, { + ...fields, + newFile: true, + aliases: fields.aliases || '', + name: fields.name, + extension: fields.extension, + }); - await uploadEmojiCustomWithBuffer(this.userId, fileBuffer, mimetype, emojiData); - } catch (err) { - SystemLogger.error({ err }); - return API.v1.failure(); - } + await uploadEmojiCustomWithBuffer(this.userId, fileBuffer, mimetype, emojiData); + } catch (err) { + SystemLogger.error({ err }); + return API.v1.failure(); + } - return API.v1.success(); - }, + return API.v1.success(); }, ); @@ -219,3 +248,12 @@ API.v1.addRoute( }, }, ); + +type EmojiCustomCreateEndpoints = ExtractRoutesFromAPI; + +export type EmojiCustomEndpoints = EmojiCustomCreateEndpoints; + +declare module '@rocket.chat/rest-typings' { + // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface + interface Endpoints extends EmojiCustomCreateEndpoints {} +} diff --git a/packages/rest-typings/src/v1/emojiCustom.ts b/packages/rest-typings/src/v1/emojiCustom.ts index ae990573875c1..923af25859d26 100644 --- a/packages/rest-typings/src/v1/emojiCustom.ts +++ b/packages/rest-typings/src/v1/emojiCustom.ts @@ -63,9 +63,6 @@ export type EmojiCustomEndpoints = { '/v1/emoji-custom.delete': { POST: (params: emojiCustomDeleteProps) => void; }; - '/v1/emoji-custom.create': { - POST: (params: { emoji: ICustomEmojiDescriptor }) => void; - }; '/v1/emoji-custom.update': { POST: (params: { emoji: ICustomEmojiDescriptor }) => void; }; From d87c3e5595ab62d4b6974bcaf4b0abd1b670e8d5 Mon Sep 17 00:00:00 2001 From: Suryansh Mishra Date: Wed, 25 Feb 2026 07:17:45 +0530 Subject: [PATCH 044/108] test(message-parser): add benchmark suite for parser performance baseline (#38905) --- .../message-parser/benchmarks/parser.bench.ts | 280 ++++++++++++++++++ packages/message-parser/jest.config.ts | 1 + packages/message-parser/package.json | 4 + yarn.lock | 11 +- 4 files changed, 295 insertions(+), 1 deletion(-) create mode 100644 packages/message-parser/benchmarks/parser.bench.ts diff --git a/packages/message-parser/benchmarks/parser.bench.ts b/packages/message-parser/benchmarks/parser.bench.ts new file mode 100644 index 0000000000000..948c08763c625 --- /dev/null +++ b/packages/message-parser/benchmarks/parser.bench.ts @@ -0,0 +1,280 @@ +/** + * Benchmark suite for @rocket.chat/message-parser + * + * Measures parsing performance (ops/sec) across various message categories. + * Run with: `yarn bench` from packages/message-parser/ + * + * This file runs via Jest (to leverage the .pegjs transform) with a long timeout. + * + * Categories tested: + * - Plain text (simple, no formatting) + * - Rich formatting (bold, italic, strike, nested emphasis) + * - URLs and auto-linking + * - Emoji (shortcodes, unicode, big emoji) + * - Mentions (@user, #channel) + * - Code blocks and inline code + * - Adversarial / stress-test inputs + * - Mixed real-world messages + */ + +import { Bench, type Task } from 'tinybench'; + +import { parse } from '../src'; +import type { Options } from '../src'; + +// ── Test Fixtures ────────────────────────────────────────────────────────── + +const fixtures = { + // Category 1: Plain text + plainShort: 'Hello world', + plainMedium: 'The quick brown fox jumps over the lazy dog. This is a typical message one might send in a chat application.', + plainLong: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. '.repeat(20).trim(), + + // Category 2: Rich formatting + boldSimple: '**Hello world**', + italicSimple: '_Hello world_', + strikeSimple: '~~Hello world~~', + nestedEmphasis: '**bold _italic_ and ~~strike~~**', + deepNesting: '**bold _italic ~~strike _deep italic_~~_**', + multipleEmphasis: '**bold** normal _italic_ normal ~~strike~~ **more bold** _more italic_', + + // Category 3: URLs + singleUrl: 'Check out https://rocket.chat for more info', + multipleUrls: 'Visit https://rocket.chat or https://github.com/RocketChat/Rocket.Chat or https://open.rocket.chat', + markdownLink: '[Rocket.Chat](https://rocket.chat)', + autolinkedDomain: 'Visit rocket.chat for more info', + urlWithPath: 'See https://github.com/RocketChat/Rocket.Chat/tree/develop/packages/message-parser for details', + + // Category 4: Emoji + singleEmoji: ':smile:', + tripleEmoji: ':smile::heart::rocket:', + unicodeEmoji: '😀', + tripleUnicodeEmoji: '😀🚀🌈', + emojiInText: 'Hello :smile: world :heart: test :rocket: done', + mixedEmoji: 'Great job :thumbsup: 🎉 keep going :rocket:', + + // Category 5: Mentions + singleMention: '@admin', + multipleMentions: '@admin @user1 @moderator', + channelMention: '#general', + mixedMentions: 'Hey @admin check #general and @user1', + + // Category 6: Code + inlineCode: 'Use `console.log()` for debugging', + codeBlock: '```javascript\nconst x = 1;\nconsole.log(x);\n```', + multiInlineCode: 'Use `Array.map()` and `Array.filter()` and `Array.reduce()`', + + // Category 7: Lists + orderedList: '1. First item\n2. Second item\n3. Third item', + unorderedList: '- First item\n- Second item\n- Third item', + taskList: '- [x] Done task\n- [ ] Pending task\n- [x] Another done', + + // Category 8: Blockquote + blockquote: '> This is a quoted message\n> with multiple lines', + + // Category 9: KaTeX (math) + katexInline: 'The formula is $E = mc^2$ in physics', + katexBlock: '$$\\sum_{i=1}^{n} x_i = x_1 + x_2 + ... + x_n$$', + + // Category 10: Adversarial / stress test + adversarialEmphasis: '**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__', + adversarialMixed: + 'This a message designed to stress test !!@#$%^&*()_+, overloading the symbols {}:"|<>?, some more text ,./;\'\\[], numbers too 1234567890-= let it call s o s ok~', + repeatedSpecials: '****____~~~~||||````####>>>>', + longWithFormatting: '**bold** _italic_ ~~strike~~ `code` @user #channel :smile: https://example.com '.repeat(10).trim(), + + // Category 11: Real-world mixed messages + realWorldSimple: 'Hey team, the deploy is done ✅', + realWorldMedium: + '@admin I pushed the fix to `develop` branch. Check https://github.com/RocketChat/Rocket.Chat/pull/12345 for details. :thumbsup:', + realWorldComplex: + '**Release Notes v7.0**\n- [x] Fix #12345\n- [ ] Update docs\n\n> Important: check https://docs.rocket.chat\n\ncc @admin @devlead #releases :rocket:', + + // Category 12: Heading + heading: '# Hello World', + headingMultiLevel: '# H1\n## H2\n### H3\n#### H4', + + // Category 13: Spoiler + spoiler: '||This is a spoiler||', + spoilerWithFormatting: '||**bold** and _italic_ spoiler||', + + // Category 14: Timestamps + timestampUnix: '', +}; + +// ── Options presets ──────────────────────────────────────────────────────── + +const fullOptions: Options = { + colors: true, + emoticons: true, + katex: { + dollarSyntax: true, + parenthesisSyntax: true, + }, +}; + +// ── Benchmark runner ─────────────────────────────────────────────────────── + +async function runBenchmarks() { + console.log('='.repeat(72)); + console.log(' @rocket.chat/message-parser — Performance Benchmark Suite'); + console.log('='.repeat(72)); + console.log(); + + // Group 1: Plain text parsing + const plainTextBench = new Bench({ time: 1000, warmupTime: 200 }); + plainTextBench + .add('plain: short', () => parse(fixtures.plainShort)) + .add('plain: medium', () => parse(fixtures.plainMedium)) + .add('plain: long', () => parse(fixtures.plainLong)); + + // Group 2: Emphasis / formatting + const emphasisBench = new Bench({ time: 1000, warmupTime: 200 }); + emphasisBench + .add('emphasis: bold', () => parse(fixtures.boldSimple)) + .add('emphasis: italic', () => parse(fixtures.italicSimple)) + .add('emphasis: strike', () => parse(fixtures.strikeSimple)) + .add('emphasis: nested', () => parse(fixtures.nestedEmphasis)) + .add('emphasis: deep nesting', () => parse(fixtures.deepNesting)) + .add('emphasis: multiple', () => parse(fixtures.multipleEmphasis)); + + // Group 3: URLs + const urlBench = new Bench({ time: 1000, warmupTime: 200 }); + urlBench + .add('url: single', () => parse(fixtures.singleUrl)) + .add('url: multiple', () => parse(fixtures.multipleUrls)) + .add('url: markdown link', () => parse(fixtures.markdownLink)) + .add('url: autolinked domain', () => parse(fixtures.autolinkedDomain)) + .add('url: with path', () => parse(fixtures.urlWithPath)); + + // Group 4: Emoji + const emojiBench = new Bench({ time: 1000, warmupTime: 200 }); + emojiBench + .add('emoji: single shortcode', () => parse(fixtures.singleEmoji)) + .add('emoji: triple shortcode (BigEmoji)', () => parse(fixtures.tripleEmoji)) + .add('emoji: single unicode', () => parse(fixtures.unicodeEmoji)) + .add('emoji: triple unicode (BigEmoji)', () => parse(fixtures.tripleUnicodeEmoji)) + .add('emoji: in text', () => parse(fixtures.emojiInText)) + .add('emoji: mixed', () => parse(fixtures.mixedEmoji)); + + // Group 5: Mentions + const mentionBench = new Bench({ time: 1000, warmupTime: 200 }); + mentionBench + .add('mention: single user', () => parse(fixtures.singleMention)) + .add('mention: multiple users', () => parse(fixtures.multipleMentions)) + .add('mention: channel', () => parse(fixtures.channelMention)) + .add('mention: mixed', () => parse(fixtures.mixedMentions)); + + // Group 6: Code + const codeBench = new Bench({ time: 1000, warmupTime: 200 }); + codeBench + .add('code: inline', () => parse(fixtures.inlineCode)) + .add('code: block', () => parse(fixtures.codeBlock)) + .add('code: multi inline', () => parse(fixtures.multiInlineCode)); + + // Group 7: Structured blocks + const blockBench = new Bench({ time: 1000, warmupTime: 200 }); + blockBench + .add('block: ordered list', () => parse(fixtures.orderedList)) + .add('block: unordered list', () => parse(fixtures.unorderedList)) + .add('block: task list', () => parse(fixtures.taskList)) + .add('block: blockquote', () => parse(fixtures.blockquote)) + .add('block: heading', () => parse(fixtures.heading)) + .add('block: heading multi-level', () => parse(fixtures.headingMultiLevel)) + .add('block: spoiler', () => parse(fixtures.spoiler)) + .add('block: spoiler with formatting', () => parse(fixtures.spoilerWithFormatting)); + + // Group 8: KaTeX (with options enabled) + const katexBench = new Bench({ time: 1000, warmupTime: 200 }); + katexBench + .add('katex: inline', () => parse(fixtures.katexInline, fullOptions)) + .add('katex: block', () => parse(fixtures.katexBlock, fullOptions)); + + // Group 9: Adversarial / stress + const stressBench = new Bench({ time: 2000, warmupTime: 500 }); + stressBench + .add('stress: adversarial emphasis', () => parse(fixtures.adversarialEmphasis)) + .add('stress: adversarial mixed', () => parse(fixtures.adversarialMixed)) + .add('stress: repeated specials', () => parse(fixtures.repeatedSpecials)) + .add('stress: long with formatting', () => parse(fixtures.longWithFormatting)); + + // Group 10: Real-world + const realWorldBench = new Bench({ time: 1000, warmupTime: 200 }); + realWorldBench + .add('real-world: simple', () => parse(fixtures.realWorldSimple)) + .add('real-world: medium', () => parse(fixtures.realWorldMedium)) + .add('real-world: complex', () => parse(fixtures.realWorldComplex, fullOptions)); + + // Group 11: Timestamps + const timestampBench = new Bench({ time: 1000, warmupTime: 200 }); + timestampBench.add('timestamp: unix format', () => parse(fixtures.timestampUnix)); + + // ── Run all benchmarks ───────────────────────────────────────────────── + + const groups = [ + { name: 'Plain Text', bench: plainTextBench }, + { name: 'Emphasis / Formatting', bench: emphasisBench }, + { name: 'URLs & Links', bench: urlBench }, + { name: 'Emoji', bench: emojiBench }, + { name: 'Mentions', bench: mentionBench }, + { name: 'Code', bench: codeBench }, + { name: 'Structured Blocks', bench: blockBench }, + { name: 'KaTeX (Math)', bench: katexBench }, + { name: 'Adversarial / Stress', bench: stressBench }, + { name: 'Real-World Messages', bench: realWorldBench }, + { name: 'Timestamps', bench: timestampBench }, + ]; + + // Benchmarks must run sequentially to avoid interference + // eslint-disable-next-line no-await-in-loop + for (const { name, bench } of groups) { + console.log(`── ${name} ${'─'.repeat(Math.max(0, 56 - name.length))}`); + // eslint-disable-next-line no-await-in-loop + await bench.run(); + console.table( + bench.tasks.map((task: Task) => ({ + 'Task': task.name, + 'ops/sec': Math.round(task.result?.hz ?? 0).toLocaleString(), + 'Avg (ms)': ((task.result?.mean ?? 0) * 1000).toFixed(4), + 'Min (ms)': ((task.result?.min ?? 0) * 1000).toFixed(4), + 'Max (ms)': ((task.result?.max ?? 0) * 1000).toFixed(4), + 'P99 (ms)': ((task.result?.p99 ?? 0) * 1000).toFixed(4), + 'Samples': task.result?.samples?.length ?? 0, + })), + ); + console.log(); + } + + // ── Summary ──────────────────────────────────────────────────────────── + + console.log('='.repeat(72)); + console.log(' Summary'); + console.log('='.repeat(72)); + + const allTasks = groups.flatMap(({ bench }) => bench.tasks); + const sorted = [...allTasks].sort((a, b) => (a.result?.hz ?? 0) - (b.result?.hz ?? 0)); + + console.log('\n Slowest operations (potential optimization targets):'); + for (const task of sorted.slice(0, 5)) { + console.log( + ` ${Math.round(task.result?.hz ?? 0) + .toLocaleString() + .padStart(12)} ops/sec │ ${task.name}`, + ); + } + + console.log('\n Fastest operations:'); + for (const task of sorted.slice(-5).reverse()) { + console.log( + ` ${Math.round(task.result?.hz ?? 0) + .toLocaleString() + .padStart(12)} ops/sec │ ${task.name}`, + ); + } + + console.log(); +} + +it('benchmark: message-parser performance', async () => { + await runBenchmarks(); +}, 120_000); diff --git a/packages/message-parser/jest.config.ts b/packages/message-parser/jest.config.ts index 4bbab0385d903..3b5a3f5d7af89 100644 --- a/packages/message-parser/jest.config.ts +++ b/packages/message-parser/jest.config.ts @@ -9,4 +9,5 @@ export default { '\\.pegjs$': resolve(__dirname, './loaders/pegtransform.js'), }, moduleFileExtensions: ['js', 'ts', 'pegjs'], + testPathIgnorePatterns: ['/node_modules/', '\\.bench\\.ts$'], } satisfies Config; diff --git a/packages/message-parser/package.json b/packages/message-parser/package.json index a39a5ddf49516..50378b9ff810c 100644 --- a/packages/message-parser/package.json +++ b/packages/message-parser/package.json @@ -35,6 +35,8 @@ ".:build:bundle": "webpack-cli", ".:build:clean": "rimraf dist", "build": "run-s .:build:clean .:build:bundle", + "bench": "jest --testMatch='**/benchmarks/**/*.bench.ts' --no-coverage --verbose", + "bench:size": "node -e \"const fs=require('fs'),zlib=require('zlib'),p='dist/messageParser.js';try{const s=fs.statSync(p),c=fs.readFileSync(p),g=zlib.gzipSync(c);console.log('Bundle:',p);console.log('Raw:',s.size,'bytes ('+(s.size/1024).toFixed(1),'KB)');console.log('Gzip:',g.length,'bytes ('+(g.length/1024).toFixed(1),'KB)')}catch(e){console.error('Build first: yarn build');process.exit(1)}\"", "lint": "eslint .", "test": "jest", "testunit": "jest", @@ -57,7 +59,9 @@ "peggy": "4.1.1", "prettier-plugin-pegjs": "~0.5.4", "rimraf": "^6.0.1", + "tinybench": "^3.0.5", "ts-loader": "~9.5.4", + "ts-node": "~10.9.2", "typescript": "~5.9.3", "webpack": "~5.99.9", "webpack-cli": "~5.1.4" diff --git a/yarn.lock b/yarn.lock index f219de83d458d..529b3a99ee19c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8916,8 +8916,10 @@ __metadata: peggy: "npm:4.1.1" prettier-plugin-pegjs: "npm:~0.5.4" rimraf: "npm:^6.0.1" + tinybench: "npm:^3.0.5" tldts: "npm:~6.1.86" ts-loader: "npm:~9.5.4" + ts-node: "npm:~10.9.2" typescript: "npm:~5.9.3" webpack: "npm:~5.99.9" webpack-cli: "npm:~5.1.4" @@ -34822,6 +34824,13 @@ __metadata: languageName: node linkType: hard +"tinybench@npm:^3.0.5": + version: 3.1.1 + resolution: "tinybench@npm:3.1.1" + checksum: 10/1d97035722dd91fc8a496cb2a97c375a06ce2ec03e57586ee5bf4b861bbc8a3ab52839d285ca5948c094ac61c1529fbdf7f47c9cb7e8dce11daafd4239eb47b0 + languageName: node + linkType: hard + "tinyglobby@npm:^0.2.14": version: 0.2.15 resolution: "tinyglobby@npm:0.2.15" @@ -35131,7 +35140,7 @@ __metadata: languageName: node linkType: hard -"ts-node@npm:^10.9.2": +"ts-node@npm:^10.9.2, ts-node@npm:~10.9.2": version: 10.9.2 resolution: "ts-node@npm:10.9.2" dependencies: From af1446ae970a786d43e3c5b8c54f98fb9ea781a6 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Tue, 24 Feb 2026 23:03:45 -0300 Subject: [PATCH 045/108] refactor(message-parser): update benchmark suite to use ts-node and add custom .pegjs loader --- .../message-parser/benchmarks/parser.bench.ts | 417 ++++++++---------- .../message-parser/loaders/pegjs-register.js | 13 + packages/message-parser/package.json | 2 +- 3 files changed, 192 insertions(+), 240 deletions(-) create mode 100644 packages/message-parser/loaders/pegjs-register.js diff --git a/packages/message-parser/benchmarks/parser.bench.ts b/packages/message-parser/benchmarks/parser.bench.ts index 948c08763c625..14851b1865577 100644 --- a/packages/message-parser/benchmarks/parser.bench.ts +++ b/packages/message-parser/benchmarks/parser.bench.ts @@ -1,20 +1,11 @@ +#!/usr/bin/env npx ts-node /** * Benchmark suite for @rocket.chat/message-parser * * Measures parsing performance (ops/sec) across various message categories. * Run with: `yarn bench` from packages/message-parser/ * - * This file runs via Jest (to leverage the .pegjs transform) with a long timeout. - * - * Categories tested: - * - Plain text (simple, no formatting) - * - Rich formatting (bold, italic, strike, nested emphasis) - * - URLs and auto-linking - * - Emoji (shortcodes, unicode, big emoji) - * - Mentions (@user, #channel) - * - Code blocks and inline code - * - Adversarial / stress-test inputs - * - Mixed real-world messages + * Uses a custom loader (pegjs-register.js) to compile .pegjs at runtime — no build needed. */ import { Bench, type Task } from 'tinybench'; @@ -22,86 +13,6 @@ import { Bench, type Task } from 'tinybench'; import { parse } from '../src'; import type { Options } from '../src'; -// ── Test Fixtures ────────────────────────────────────────────────────────── - -const fixtures = { - // Category 1: Plain text - plainShort: 'Hello world', - plainMedium: 'The quick brown fox jumps over the lazy dog. This is a typical message one might send in a chat application.', - plainLong: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. '.repeat(20).trim(), - - // Category 2: Rich formatting - boldSimple: '**Hello world**', - italicSimple: '_Hello world_', - strikeSimple: '~~Hello world~~', - nestedEmphasis: '**bold _italic_ and ~~strike~~**', - deepNesting: '**bold _italic ~~strike _deep italic_~~_**', - multipleEmphasis: '**bold** normal _italic_ normal ~~strike~~ **more bold** _more italic_', - - // Category 3: URLs - singleUrl: 'Check out https://rocket.chat for more info', - multipleUrls: 'Visit https://rocket.chat or https://github.com/RocketChat/Rocket.Chat or https://open.rocket.chat', - markdownLink: '[Rocket.Chat](https://rocket.chat)', - autolinkedDomain: 'Visit rocket.chat for more info', - urlWithPath: 'See https://github.com/RocketChat/Rocket.Chat/tree/develop/packages/message-parser for details', - - // Category 4: Emoji - singleEmoji: ':smile:', - tripleEmoji: ':smile::heart::rocket:', - unicodeEmoji: '😀', - tripleUnicodeEmoji: '😀🚀🌈', - emojiInText: 'Hello :smile: world :heart: test :rocket: done', - mixedEmoji: 'Great job :thumbsup: 🎉 keep going :rocket:', - - // Category 5: Mentions - singleMention: '@admin', - multipleMentions: '@admin @user1 @moderator', - channelMention: '#general', - mixedMentions: 'Hey @admin check #general and @user1', - - // Category 6: Code - inlineCode: 'Use `console.log()` for debugging', - codeBlock: '```javascript\nconst x = 1;\nconsole.log(x);\n```', - multiInlineCode: 'Use `Array.map()` and `Array.filter()` and `Array.reduce()`', - - // Category 7: Lists - orderedList: '1. First item\n2. Second item\n3. Third item', - unorderedList: '- First item\n- Second item\n- Third item', - taskList: '- [x] Done task\n- [ ] Pending task\n- [x] Another done', - - // Category 8: Blockquote - blockquote: '> This is a quoted message\n> with multiple lines', - - // Category 9: KaTeX (math) - katexInline: 'The formula is $E = mc^2$ in physics', - katexBlock: '$$\\sum_{i=1}^{n} x_i = x_1 + x_2 + ... + x_n$$', - - // Category 10: Adversarial / stress test - adversarialEmphasis: '**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__', - adversarialMixed: - 'This a message designed to stress test !!@#$%^&*()_+, overloading the symbols {}:"|<>?, some more text ,./;\'\\[], numbers too 1234567890-= let it call s o s ok~', - repeatedSpecials: '****____~~~~||||````####>>>>', - longWithFormatting: '**bold** _italic_ ~~strike~~ `code` @user #channel :smile: https://example.com '.repeat(10).trim(), - - // Category 11: Real-world mixed messages - realWorldSimple: 'Hey team, the deploy is done ✅', - realWorldMedium: - '@admin I pushed the fix to `develop` branch. Check https://github.com/RocketChat/Rocket.Chat/pull/12345 for details. :thumbsup:', - realWorldComplex: - '**Release Notes v7.0**\n- [x] Fix #12345\n- [ ] Update docs\n\n> Important: check https://docs.rocket.chat\n\ncc @admin @devlead #releases :rocket:', - - // Category 12: Heading - heading: '# Hello World', - headingMultiLevel: '# H1\n## H2\n### H3\n#### H4', - - // Category 13: Spoiler - spoiler: '||This is a spoiler||', - spoilerWithFormatting: '||**bold** and _italic_ spoiler||', - - // Category 14: Timestamps - timestampUnix: '', -}; - // ── Options presets ──────────────────────────────────────────────────────── const fullOptions: Options = { @@ -113,168 +24,196 @@ const fullOptions: Options = { }, }; -// ── Benchmark runner ─────────────────────────────────────────────────────── - -async function runBenchmarks() { - console.log('='.repeat(72)); - console.log(' @rocket.chat/message-parser — Performance Benchmark Suite'); - console.log('='.repeat(72)); - console.log(); - - // Group 1: Plain text parsing - const plainTextBench = new Bench({ time: 1000, warmupTime: 200 }); - plainTextBench - .add('plain: short', () => parse(fixtures.plainShort)) - .add('plain: medium', () => parse(fixtures.plainMedium)) - .add('plain: long', () => parse(fixtures.plainLong)); - - // Group 2: Emphasis / formatting - const emphasisBench = new Bench({ time: 1000, warmupTime: 200 }); - emphasisBench - .add('emphasis: bold', () => parse(fixtures.boldSimple)) - .add('emphasis: italic', () => parse(fixtures.italicSimple)) - .add('emphasis: strike', () => parse(fixtures.strikeSimple)) - .add('emphasis: nested', () => parse(fixtures.nestedEmphasis)) - .add('emphasis: deep nesting', () => parse(fixtures.deepNesting)) - .add('emphasis: multiple', () => parse(fixtures.multipleEmphasis)); - - // Group 3: URLs - const urlBench = new Bench({ time: 1000, warmupTime: 200 }); - urlBench - .add('url: single', () => parse(fixtures.singleUrl)) - .add('url: multiple', () => parse(fixtures.multipleUrls)) - .add('url: markdown link', () => parse(fixtures.markdownLink)) - .add('url: autolinked domain', () => parse(fixtures.autolinkedDomain)) - .add('url: with path', () => parse(fixtures.urlWithPath)); - - // Group 4: Emoji - const emojiBench = new Bench({ time: 1000, warmupTime: 200 }); - emojiBench - .add('emoji: single shortcode', () => parse(fixtures.singleEmoji)) - .add('emoji: triple shortcode (BigEmoji)', () => parse(fixtures.tripleEmoji)) - .add('emoji: single unicode', () => parse(fixtures.unicodeEmoji)) - .add('emoji: triple unicode (BigEmoji)', () => parse(fixtures.tripleUnicodeEmoji)) - .add('emoji: in text', () => parse(fixtures.emojiInText)) - .add('emoji: mixed', () => parse(fixtures.mixedEmoji)); +// ── Fixture type ─────────────────────────────────────────────────────────── - // Group 5: Mentions - const mentionBench = new Bench({ time: 1000, warmupTime: 200 }); - mentionBench - .add('mention: single user', () => parse(fixtures.singleMention)) - .add('mention: multiple users', () => parse(fixtures.multipleMentions)) - .add('mention: channel', () => parse(fixtures.channelMention)) - .add('mention: mixed', () => parse(fixtures.mixedMentions)); - - // Group 6: Code - const codeBench = new Bench({ time: 1000, warmupTime: 200 }); - codeBench - .add('code: inline', () => parse(fixtures.inlineCode)) - .add('code: block', () => parse(fixtures.codeBlock)) - .add('code: multi inline', () => parse(fixtures.multiInlineCode)); - - // Group 7: Structured blocks - const blockBench = new Bench({ time: 1000, warmupTime: 200 }); - blockBench - .add('block: ordered list', () => parse(fixtures.orderedList)) - .add('block: unordered list', () => parse(fixtures.unorderedList)) - .add('block: task list', () => parse(fixtures.taskList)) - .add('block: blockquote', () => parse(fixtures.blockquote)) - .add('block: heading', () => parse(fixtures.heading)) - .add('block: heading multi-level', () => parse(fixtures.headingMultiLevel)) - .add('block: spoiler', () => parse(fixtures.spoiler)) - .add('block: spoiler with formatting', () => parse(fixtures.spoilerWithFormatting)); +type Fixture = { + name: string; + input: string; + options?: Options; +}; - // Group 8: KaTeX (with options enabled) - const katexBench = new Bench({ time: 1000, warmupTime: 200 }); - katexBench - .add('katex: inline', () => parse(fixtures.katexInline, fullOptions)) - .add('katex: block', () => parse(fixtures.katexBlock, fullOptions)); +type BenchCategory = { + name: string; + time?: number; + warmupTime?: number; + fixtures: Fixture[]; +}; - // Group 9: Adversarial / stress - const stressBench = new Bench({ time: 2000, warmupTime: 500 }); - stressBench - .add('stress: adversarial emphasis', () => parse(fixtures.adversarialEmphasis)) - .add('stress: adversarial mixed', () => parse(fixtures.adversarialMixed)) - .add('stress: repeated specials', () => parse(fixtures.repeatedSpecials)) - .add('stress: long with formatting', () => parse(fixtures.longWithFormatting)); +// ── Categories ───────────────────────────────────────────────────────────── + +const categories: BenchCategory[] = [ + { + name: 'Plain Text', + fixtures: [ + { name: 'short', input: 'Hello world' }, + { + name: 'medium', + input: 'The quick brown fox jumps over the lazy dog. This is a typical message one might send in a chat application.', + }, + { name: 'long', input: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. '.repeat(20).trim() }, + ], + }, + { + name: 'Emphasis / Formatting', + fixtures: [ + { name: 'bold', input: '**Hello world**' }, + { name: 'italic', input: '_Hello world_' }, + { name: 'strike', input: '~~Hello world~~' }, + { name: 'nested', input: '**bold _italic_ and ~~strike~~**' }, + { name: 'deep nesting', input: '**bold _italic ~~strike _deep italic_~~_**' }, + { name: 'multiple', input: '**bold** normal _italic_ normal ~~strike~~ **more bold** _more italic_' }, + ], + }, + { + name: 'URLs & Links', + fixtures: [ + { name: 'single', input: 'Check out https://rocket.chat for more info' }, + { name: 'multiple', input: 'Visit https://rocket.chat or https://github.com/RocketChat/Rocket.Chat or https://open.rocket.chat' }, + { name: 'markdown link', input: '[Rocket.Chat](https://rocket.chat)' }, + { name: 'autolinked domain', input: 'Visit rocket.chat for more info' }, + { name: 'with path', input: 'See https://github.com/RocketChat/Rocket.Chat/tree/develop/packages/message-parser for details' }, + ], + }, + { + name: 'Emoji', + fixtures: [ + { name: 'single shortcode', input: ':smile:' }, + { name: 'triple shortcode (BigEmoji)', input: ':smile::heart::rocket:' }, + { name: 'single unicode', input: '😀' }, + { name: 'triple unicode (BigEmoji)', input: '😀🚀🌈' }, + { name: 'in text', input: 'Hello :smile: world :heart: test :rocket: done' }, + { name: 'mixed', input: 'Great job :thumbsup: 🎉 keep going :rocket:' }, + ], + }, + { + name: 'Mentions', + fixtures: [ + { name: 'single user', input: '@admin' }, + { name: 'multiple users', input: '@admin @user1 @moderator' }, + { name: 'channel', input: '#general' }, + { name: 'mixed', input: 'Hey @admin check #general and @user1' }, + ], + }, + { + name: 'Code', + fixtures: [ + { name: 'inline', input: 'Use `console.log()` for debugging' }, + { name: 'block', input: '```javascript\nconst x = 1;\nconsole.log(x);\n```' }, + { name: 'multi inline', input: 'Use `Array.map()` and `Array.filter()` and `Array.reduce()`' }, + ], + }, + { + name: 'Structured Blocks', + fixtures: [ + { name: 'ordered list', input: '1. First item\n2. Second item\n3. Third item' }, + { name: 'unordered list', input: '- First item\n- Second item\n- Third item' }, + { name: 'task list', input: '- [x] Done task\n- [ ] Pending task\n- [x] Another done' }, + { name: 'blockquote', input: '> This is a quoted message\n> with multiple lines' }, + { name: 'heading', input: '# Hello World' }, + { name: 'heading multi-level', input: '# H1\n## H2\n### H3\n#### H4' }, + { name: 'spoiler', input: '||This is a spoiler||' }, + { name: 'spoiler with formatting', input: '||**bold** and _italic_ spoiler||' }, + ], + }, + { + name: 'KaTeX (Math)', + fixtures: [ + { name: 'inline', input: 'The formula is $E = mc^2$ in physics', options: fullOptions }, + { name: 'block', input: '$$\\sum_{i=1}^{n} x_i = x_1 + x_2 + ... + x_n$$', options: fullOptions }, + ], + }, + { + name: 'Adversarial / Stress', + time: 2000, + warmupTime: 500, + fixtures: [ + { + name: 'adversarial emphasis', + input: '**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__**_**__', + }, + { + name: 'adversarial mixed', + input: + 'This a message designed to stress test !!@#$%^&*()_+, overloading the symbols {}:"|<>?, some more text ,./;\'\\[], numbers too 1234567890-= let it call s o s ok~', + }, + { name: 'repeated specials', input: '****____~~~~||||````####>>>>' }, + { + name: 'long with formatting', + input: '**bold** _italic_ ~~strike~~ `code` @user #channel :smile: https://example.com '.repeat(10).trim(), + }, + ], + }, + { + name: 'Real-World Messages', + fixtures: [ + { name: 'simple', input: 'Hey team, the deploy is done ✅' }, + { + name: 'medium', + input: + '@admin I pushed the fix to `develop` branch. Check https://github.com/RocketChat/Rocket.Chat/pull/12345 for details. :thumbsup:', + }, + { + name: 'complex', + input: + '**Release Notes v7.0**\n- [x] Fix #12345\n- [ ] Update docs\n\n> Important: check https://docs.rocket.chat\n\ncc @admin @devlead #releases :rocket:', + options: fullOptions, + }, + ], + }, + { + name: 'Timestamps', + fixtures: [{ name: 'unix format', input: '' }], + }, +]; + +// ── Helpers ──────────────────────────────────────────────────────────────── + +function formatResults(tasks: Task[]) { + return tasks.map((task) => ({ + 'Task': task.name, + 'ops/sec': Math.round(task.result?.hz ?? 0).toLocaleString(), + 'Avg (ms)': ((task.result?.mean ?? 0) * 1000).toFixed(4), + 'Min (ms)': ((task.result?.min ?? 0) * 1000).toFixed(4), + 'Max (ms)': ((task.result?.max ?? 0) * 1000).toFixed(4), + 'P99 (ms)': ((task.result?.p99 ?? 0) * 1000).toFixed(4), + 'Samples': task.result?.samples?.length ?? 0, + })); +} - // Group 10: Real-world - const realWorldBench = new Bench({ time: 1000, warmupTime: 200 }); - realWorldBench - .add('real-world: simple', () => parse(fixtures.realWorldSimple)) - .add('real-world: medium', () => parse(fixtures.realWorldMedium)) - .add('real-world: complex', () => parse(fixtures.realWorldComplex, fullOptions)); +// ── Runner ───────────────────────────────────────────────────────────────── - // Group 11: Timestamps - const timestampBench = new Bench({ time: 1000, warmupTime: 200 }); - timestampBench.add('timestamp: unix format', () => parse(fixtures.timestampUnix)); +async function run() { + console.log('='.repeat(72)); + console.log(' @rocket.chat/message-parser — Performance Benchmark Suite'); + console.log('='.repeat(72)); + console.log(); - // ── Run all benchmarks ───────────────────────────────────────────────── + // Benchmarks must run sequentially to avoid interference + // eslint-disable-next-line no-restricted-syntax + for (const category of categories) { + const bench = new Bench({ + time: category.time ?? 1000, + warmupTime: category.warmupTime ?? 200, + }); - const groups = [ - { name: 'Plain Text', bench: plainTextBench }, - { name: 'Emphasis / Formatting', bench: emphasisBench }, - { name: 'URLs & Links', bench: urlBench }, - { name: 'Emoji', bench: emojiBench }, - { name: 'Mentions', bench: mentionBench }, - { name: 'Code', bench: codeBench }, - { name: 'Structured Blocks', bench: blockBench }, - { name: 'KaTeX (Math)', bench: katexBench }, - { name: 'Adversarial / Stress', bench: stressBench }, - { name: 'Real-World Messages', bench: realWorldBench }, - { name: 'Timestamps', bench: timestampBench }, - ]; + for (const fixture of category.fixtures) { + bench.add(fixture.name, () => parse(fixture.input, fixture.options)); + } - // Benchmarks must run sequentially to avoid interference - // eslint-disable-next-line no-await-in-loop - for (const { name, bench } of groups) { - console.log(`── ${name} ${'─'.repeat(Math.max(0, 56 - name.length))}`); // eslint-disable-next-line no-await-in-loop await bench.run(); - console.table( - bench.tasks.map((task: Task) => ({ - 'Task': task.name, - 'ops/sec': Math.round(task.result?.hz ?? 0).toLocaleString(), - 'Avg (ms)': ((task.result?.mean ?? 0) * 1000).toFixed(4), - 'Min (ms)': ((task.result?.min ?? 0) * 1000).toFixed(4), - 'Max (ms)': ((task.result?.max ?? 0) * 1000).toFixed(4), - 'P99 (ms)': ((task.result?.p99 ?? 0) * 1000).toFixed(4), - 'Samples': task.result?.samples?.length ?? 0, - })), - ); + + console.log(`── ${category.name} ${'─'.repeat(Math.max(0, 56 - category.name.length))}`); + console.table(formatResults(bench.tasks)); console.log(); } - // ── Summary ──────────────────────────────────────────────────────────── - console.log('='.repeat(72)); - console.log(' Summary'); + console.log(' Done.'); console.log('='.repeat(72)); - - const allTasks = groups.flatMap(({ bench }) => bench.tasks); - const sorted = [...allTasks].sort((a, b) => (a.result?.hz ?? 0) - (b.result?.hz ?? 0)); - - console.log('\n Slowest operations (potential optimization targets):'); - for (const task of sorted.slice(0, 5)) { - console.log( - ` ${Math.round(task.result?.hz ?? 0) - .toLocaleString() - .padStart(12)} ops/sec │ ${task.name}`, - ); - } - - console.log('\n Fastest operations:'); - for (const task of sorted.slice(-5).reverse()) { - console.log( - ` ${Math.round(task.result?.hz ?? 0) - .toLocaleString() - .padStart(12)} ops/sec │ ${task.name}`, - ); - } - - console.log(); } -it('benchmark: message-parser performance', async () => { - await runBenchmarks(); -}, 120_000); +run().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/packages/message-parser/loaders/pegjs-register.js b/packages/message-parser/loaders/pegjs-register.js new file mode 100644 index 0000000000000..9a4de3c665268 --- /dev/null +++ b/packages/message-parser/loaders/pegjs-register.js @@ -0,0 +1,13 @@ +const fs = require('fs'); +const Module = require('module'); + +const peggy = require('peggy'); + +Module._extensions['.pegjs'] = function (mod, filename) { + const content = fs.readFileSync(filename, 'utf-8'); + const code = peggy.generate(content, { + output: 'source', + format: 'commonjs', + }); + mod._compile(code, filename); +}; diff --git a/packages/message-parser/package.json b/packages/message-parser/package.json index 50378b9ff810c..b146fb9b80698 100644 --- a/packages/message-parser/package.json +++ b/packages/message-parser/package.json @@ -35,7 +35,7 @@ ".:build:bundle": "webpack-cli", ".:build:clean": "rimraf dist", "build": "run-s .:build:clean .:build:bundle", - "bench": "jest --testMatch='**/benchmarks/**/*.bench.ts' --no-coverage --verbose", + "bench": "ts-node --compiler-options '{\"module\":\"commonjs\"}' -r ./loaders/pegjs-register.js benchmarks/parser.bench.ts", "bench:size": "node -e \"const fs=require('fs'),zlib=require('zlib'),p='dist/messageParser.js';try{const s=fs.statSync(p),c=fs.readFileSync(p),g=zlib.gzipSync(c);console.log('Bundle:',p);console.log('Raw:',s.size,'bytes ('+(s.size/1024).toFixed(1),'KB)');console.log('Gzip:',g.length,'bytes ('+(g.length/1024).toFixed(1),'KB)')}catch(e){console.error('Build first: yarn build');process.exit(1)}\"", "lint": "eslint .", "test": "jest", From fd5b687ca70c14d9dd39329a5787a69b5eb2d150 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Wed, 25 Feb 2026 00:56:14 -0300 Subject: [PATCH 046/108] ci(todo-issue): implement TODO issue creation and management workflow (#39015) --- .github/workflows/todo.yml | 32 +++- scripts/todo-issue/README.md | 225 +++++++++++++++++++++++ scripts/todo-issue/bun.lock | 25 +++ scripts/todo-issue/package.json | 11 ++ scripts/todo-issue/src/diff.ts | 127 +++++++++++++ scripts/todo-issue/src/github.ts | 260 +++++++++++++++++++++++++++ scripts/todo-issue/src/index.ts | 143 +++++++++++++++ scripts/todo-issue/src/matcher.ts | 70 ++++++++ scripts/todo-issue/src/similarity.ts | 24 +++ scripts/todo-issue/src/types.ts | 36 ++++ scripts/todo-issue/tsconfig.json | 11 ++ 11 files changed, 961 insertions(+), 3 deletions(-) create mode 100644 scripts/todo-issue/README.md create mode 100644 scripts/todo-issue/bun.lock create mode 100644 scripts/todo-issue/package.json create mode 100644 scripts/todo-issue/src/diff.ts create mode 100644 scripts/todo-issue/src/github.ts create mode 100644 scripts/todo-issue/src/index.ts create mode 100644 scripts/todo-issue/src/matcher.ts create mode 100644 scripts/todo-issue/src/similarity.ts create mode 100644 scripts/todo-issue/src/types.ts create mode 100644 scripts/todo-issue/tsconfig.json diff --git a/.github/workflows/todo.yml b/.github/workflows/todo.yml index 3eedd9857a8ab..cee1e97a864e0 100644 --- a/.github/workflows/todo.yml +++ b/.github/workflows/todo.yml @@ -8,6 +8,16 @@ on: required: false type: boolean description: Enable, if you want to import all TODOs. Runs on checked out branch! Only use if you're sure what you are doing. + sha: + default: '' + required: false + type: string + description: 'A commit SHA or range (e.g. "abc123" or "abc123...def456"). Single SHA compares against its parent.' + path: + default: '' + required: false + type: string + description: 'Import TODOs from a specific path (e.g. "apps/meteor/client" or "packages/core-typings/src/IMessage.ts").' push: branches: # do not set multiple branches, todos might be added and then get referenced by themselves in case of a merge - develop @@ -20,13 +30,29 @@ permissions: jobs: todos: runs-on: ubuntu-latest + timeout-minutes: 10 steps: - uses: actions/checkout@v6 - - name: Run Issue Bot - uses: juulsn/todo-issue@v1.1.4 + - name: Setup Bun + uses: oven-sh/setup-bun@v2 with: - excludePattern: '(^|/)node_modules/' + bun-version: latest + + - name: Install dependencies + run: bun install --frozen-lockfile + working-directory: scripts/todo-issue + + - name: Create issues from TODOs + run: bun run src/index.ts + working-directory: scripts/todo-issue env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }} + GITHUB_REPOSITORY_NAME: ${{ github.event.repository.name }} + GITHUB_SHA: ${{ github.sha }} + BEFORE_SHA: ${{ github.event.before }} + IMPORT_ALL: ${{ inputs.importAll || 'false' }} + SHA_INPUT: ${{ inputs.sha || '' }} + PATH_FILTER: ${{ inputs.path || '' }} diff --git a/scripts/todo-issue/README.md b/scripts/todo-issue/README.md new file mode 100644 index 0000000000000..fab191b7d2dfe --- /dev/null +++ b/scripts/todo-issue/README.md @@ -0,0 +1,225 @@ +# todo-issue + +Scans code for `TODO` comments and automatically creates, closes, updates, and references GitHub Issues. + +Runs as a GitHub Actions workflow via [Bun](https://bun.sh/) with a single dependency (`parse-diff`). + +## How it works + +When code is pushed to `develop`, the script diffs the changes and: + +- **Creates** a new issue for each new `TODO` comment +- **Closes** the issue when a `TODO` is removed +- **Updates** the issue title when a `TODO` is edited (detected via similarity matching) +- **References** an existing issue when a duplicate `TODO` is added (adds a comment instead of creating a duplicate) + +Every issue created by the script receives the `todo` label, which is also used to efficiently query only relevant issues. + +## TODO syntax + +Any comment style works (`//`, `#`, `--`, `/* */`, etc.) as long as only symbols and whitespace appear before the `TODO` keyword. The keyword is case-insensitive. + +### Examples + +**Minimal -- title only:** + +```ts +// TODO: Fix the race condition in token refresh +``` + +Extracted data: + +| Field | Value | +| ------ | ----------------------------------------- | +| Title | `Fix the race condition in token refresh` | +| Body | _(none)_ | +| Labels | `todo` | + +--- + +**With body -- continuation lines using the same comment prefix:** + +```ts +// TODO: Refactor the auth flow +// The current token refresh logic has race conditions +// when multiple tabs are open simultaneously +``` + +Extracted data: + +| Field | Value | +| ------ | ----------------------------------------------------------------------------------------------------- | +| Title | `Refactor the auth flow` | +| Body | `The current token refresh logic has race conditions`
`when multiple tabs are open simultaneously` | +| Labels | `todo` | + +The body stops at the first line that doesn't share the same comment prefix or is empty. + +--- + +**With custom labels:** + +```ts +// TODO: Make this button red [frontend] [ui] +``` + +Extracted data: + +| Field | Value | +| ------ | ------------------------ | +| Title | `Make this button red` | +| Body | _(none)_ | +| Labels | `todo`, `frontend`, `ui` | + +Labels are extracted from `[square brackets]` at the end of the title. The `todo` label is always included. + +--- + +**Full example -- body + labels in different languages:** + +```python +# TODO: Add retry logic for S3 uploads [infra] [backend] +# Currently fails silently on timeout +# See https://github.com/org/repo/issues/42 +``` + +Extracted data: + +| Field | Value | +| ------ | ------------------------------------------------------------------------------------ | +| Title | `Add retry logic for S3 uploads` | +| Body | `Currently fails silently on timeout`
`See https://github.com/org/repo/issues/42` | +| Labels | `todo`, `infra`, `backend` | + +--- + +**Block comments:** + +```ts +/** + * TODO: Should we reinvent the wheel here? + * We already have a good one in @rocket.chat/core + */ +``` + +Extracted data: + +| Field | Value | +| ------ | ------------------------------------------------- | +| Title | `Should we reinvent the wheel here?` | +| Body | `We already have a good one in @rocket.chat/core` | +| Labels | `todo` | + +--- + +### Assignees + +Mention GitHub usernames with `@` to automatically assign the issue: + +```ts +// TODO: Fix flaky test @john-doe [testing] +``` + +Extracted data: + +| Field | Value | +| --------- | ----------------- | +| Title | `Fix flaky test` | +| Body | _(none)_ | +| Labels | `todo`, `testing` | +| Assignees | `john-doe` | + +Mentions in the title are removed from the issue title. Mentions in the body are also collected as assignees but kept in the body text: + +```ts +// TODO: Migrate to new payments API +// @alice should review the Stripe integration +// @bob handles the webhook setup +``` + +Extracted data: + +| Field | Value | +| --------- | --------------------------------------------------------------------------------- | +| Title | `Migrate to new payments API` | +| Body | `@alice should review the Stripe integration`
`@bob handles the webhook setup` | +| Labels | `todo` | +| Assignees | `alice`, `bob` | + +--- + +### What is NOT captured + +```ts +// This is not a TODO because the keyword is not at the start after the prefix +// See the TODO documentation for more info + +const todo = 'strings containing TODO are ignored'; + +// TODONT -- partial keyword matches are ignored +``` + +## Trigger modes + +The workflow supports four modes, configured via `workflow_dispatch` inputs or automatic push events. + +### Push (automatic) + +Triggers on every push to `develop`. Compares `BEFORE_SHA...GITHUB_SHA` to detect added/removed TODOs. + +### SHA (manual) + +Process a specific commit or range of commits. + +| Input | Behavior | +| ------------------- | ------------------------------------------------------- | +| `a1b2c3d` | Diffs commit against its parent (`a1b2c3d~1...a1b2c3d`) | +| `a1b2c3d...f4e5d6a` | Diffs the full range | + +### Path (manual) + +Import all TODOs from a specific file or directory: + +``` +apps/meteor/client +packages/core-typings/src/IMessage.ts +``` + +Only creates issues (no close/update). Skips TODOs that already have a matching issue. + +### Import all (manual) + +Scans the entire codebase for TODOs. Only creates issues for TODOs that don't already have a matching issue. Use with caution on large codebases. + +## Matching logic + +The script compares found TODOs against existing issues using: + +1. **Exact title match** -- identical titles are considered the same TODO +2. **Similarity match** -- titles within 80% Levenshtein similarity are considered the same TODO (catches typo fixes and small edits) + +When a TODO is both added and deleted in the same diff: + +- **Same title** -- treated as a move (no action) +- **Similar title** -- treated as an edit (updates the existing issue title) + +When a new TODO matches an existing open issue, a reference comment is added to the issue instead of creating a duplicate. + +## Rate limiting + +The script reads `x-ratelimit-remaining` and `x-ratelimit-reset` from GitHub API response headers and automatically waits when approaching the limit. + +## Project structure + +``` +scripts/todo-issue/ +├── package.json +├── tsconfig.json +└── src/ + ├── index.ts # Entry point, config loading, diff resolution, orchestration + ├── types.ts # Shared interfaces (Config, TodoItem, GitHubIssue, MatchResult) + ├── diff.ts # Diff parsing (parse-diff), TODO extraction, body/label parsing + ├── matcher.ts # Match found TODOs against existing issues + ├── similarity.ts # Levenshtein distance and similarity check + └── github.ts # GitHub API (REST + GraphQL), rate limiting, issue CRUD +``` diff --git a/scripts/todo-issue/bun.lock b/scripts/todo-issue/bun.lock new file mode 100644 index 0000000000000..18b2de8e52056 --- /dev/null +++ b/scripts/todo-issue/bun.lock @@ -0,0 +1,25 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "todo-issue", + "dependencies": { + "parse-diff": "^0.11.1", + }, + "devDependencies": { + "@types/bun": "latest", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.3.9", "", { "dependencies": { "bun-types": "1.3.9" } }, "sha512-KQ571yULOdWJiMH+RIWIOZ7B2RXQGpL1YQrBtLIV3FqDcCu6FsbFUBwhdKUlCKUpS3PJDsHlJ1QKlpxoVR+xtw=="], + + "@types/node": ["@types/node@25.3.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A=="], + + "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], + + "parse-diff": ["parse-diff@0.11.1", "", {}, "sha512-Oq4j8LAOPOcssanQkIjxosjATBIEJhCxMCxPhMu+Ci4wdNmAEdx0O+a7gzbR2PyKXgKPvRLIN5g224+dJAsKHA=="], + + "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + } +} diff --git a/scripts/todo-issue/package.json b/scripts/todo-issue/package.json new file mode 100644 index 0000000000000..2692a73cb9a7f --- /dev/null +++ b/scripts/todo-issue/package.json @@ -0,0 +1,11 @@ +{ + "name": "todo-issue", + "private": true, + "type": "module", + "dependencies": { + "parse-diff": "^0.11.1" + }, + "devDependencies": { + "@types/bun": "latest" + } +} diff --git a/scripts/todo-issue/src/diff.ts b/scripts/todo-issue/src/diff.ts new file mode 100644 index 0000000000000..21ca1affab033 --- /dev/null +++ b/scripts/todo-issue/src/diff.ts @@ -0,0 +1,127 @@ +import parseDiff from 'parse-diff'; +import type { Change } from 'parse-diff'; +import type { TodoItem } from './types'; + +const KEYWORD = 'TODO'; +const EXCLUDE_PATTERN = /(^|\/)node_modules\//; +const DEFAULT_LABEL = 'todo'; + +const MENTION_REGEX = /\B@([a-zA-Z0-9](?:[a-zA-Z0-9]|-(?=[a-zA-Z0-9])){0,38})/g; + +function extractMentions(text: string): { cleaned: string; mentions: string[] } { + const mentions: string[] = []; + let match; + + while ((match = MENTION_REGEX.exec(text)) !== null) { + const username = match[1]; + if (!mentions.includes(username)) { + mentions.push(username); + } + } + MENTION_REGEX.lastIndex = 0; + + const cleaned = text.replace(MENTION_REGEX, '').replace(/\s{2,}/g, ' ').trim(); + return { cleaned, mentions }; +} + +function extractLabels(title: string): { cleaned: string; labels: string[] } { + const labels: string[] = []; + const tagRegex = /\[([^\]]+)\]$/; + let match = title.match(tagRegex); + + while (match) { + labels.push(match[1].trim()); + title = title.replace(tagRegex, '').trimEnd(); + match = title.match(tagRegex); + } + + return { cleaned: title, labels }; +} + +const TODO_LINE_REGEX = new RegExp(`^\\s*\\W+\\s*${KEYWORD}\\b`, 'i'); + +function extractBody(changes: Change[], startIndex: number, prefix: string): string | false { + const bodyLines: string[] = []; + const trimmedPrefix = prefix.replace(/\s+$/, '').trim(); + const startType = changes[startIndex].type; + + for (let j = startIndex + 1; j < changes.length; j++) { + const next = changes[j]; + if (next.type !== startType) break; + + const content = next.content.slice(1); + + if (TODO_LINE_REGEX.test(content)) break; + + const prefixMatch = content.match(new RegExp(`^\\s*${trimmedPrefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`)); + if (!prefixMatch) break; + + const lineText = content.slice(prefixMatch[0].length).trim(); + if (!lineText) break; + + bodyLines.push(lineText); + } + + return bodyLines.length ? bodyLines.join('\n') : false; +} + +export function extractTodos(diffText: string): TodoItem[] { + const files = parseDiff(diffText); + const todos: TodoItem[] = []; + const regex = new RegExp(`^(?\\s*\\W+\\s*)${KEYWORD}\\b\\s*:?\\s*(?.+)`, 'i'); + + for (const file of files) { + const filename = file.to ?? file.from ?? ''; + if (!filename || filename === '/dev/null' || EXCLUDE_PATTERN.test(filename)) continue; + + for (const chunk of file.chunks) { + for (let i = 0; i < chunk.changes.length; i++) { + const change = chunk.changes[i]; + if (change.type === 'normal') continue; + + const raw = change.content.slice(1); + const match = regex.exec(raw); + if (!match?.groups) continue; + + let title = match.groups.title.trim(); + if (!title) continue; + + const prefix = match.groups.prefix; + const body = extractBody(chunk.changes, i, prefix); + + const { cleaned: labelCleaned, labels } = extractLabels(title); + const { cleaned: mentionCleaned, mentions: titleMentions } = extractMentions(labelCleaned); + title = mentionCleaned; + if (!title) continue; + + const assignees = [...titleMentions]; + if (body) { + const { mentions: bodyMentions } = extractMentions(body); + for (const m of bodyMentions) { + if (!assignees.includes(m)) assignees.push(m); + } + } + + if (title.length > 256) { + title = title.slice(0, 253) + '...'; + } + + const line = change.type === 'add' ? (change as { ln: number }).ln : (change as { ln: number }).ln; + + const allLabels = labels.includes(DEFAULT_LABEL) ? labels : [DEFAULT_LABEL, ...labels]; + + todos.push({ + type: change.type === 'add' ? 'add' : 'del', + title, + body, + filename, + line, + labels: allLabels, + assignees, + }); + } + } + } + + return todos; +} diff --git a/scripts/todo-issue/src/github.ts b/scripts/todo-issue/src/github.ts new file mode 100644 index 0000000000000..22d1806661003 --- /dev/null +++ b/scripts/todo-issue/src/github.ts @@ -0,0 +1,260 @@ +import type { Config, GitHubIssue, TodoItem } from './types'; + +const BLOB_LINES = 5; +const DEFAULT_LABEL_COLOR = '00B0D8'; +const RATE_LIMIT_BUFFER = 5; + +let rateLimitRemaining = Infinity; +let rateLimitResetAt = 0; + +async function waitForRateLimit(): Promise<void> { + if (rateLimitRemaining > RATE_LIMIT_BUFFER) return; + + const waitMs = rateLimitResetAt * 1000 - Date.now() + 1000; + if (waitMs <= 0) return; + + console.log(`[RATE] ${rateLimitRemaining} requests left, waiting ${Math.ceil(waitMs / 1000)}s`); + await new Promise((r) => setTimeout(r, waitMs)); +} + +function trackRateLimit(response: Response): void { + const remaining = response.headers.get('x-ratelimit-remaining'); + const reset = response.headers.get('x-ratelimit-reset'); + + if (remaining) rateLimitRemaining = parseInt(remaining, 10); + if (reset) rateLimitResetAt = parseInt(reset, 10); +} + +async function githubRequest<T>(endpoint: string, token: string, method = 'GET', body?: unknown): Promise<T> { + await waitForRateLimit(); + + const headers: Record<string, string> = { + Authorization: `Bearer ${token}`, + Accept: 'application/vnd.github.v3+json', + 'User-Agent': 'todo-issue-script', + }; + if (body) { + headers['Content-Type'] = 'application/json'; + } + + const response = await fetch(`https://api.github.com${endpoint}`, { + method, + headers, + body: body ? JSON.stringify(body) : undefined, + }); + + trackRateLimit(response); + + if (!response.ok) { + const text = await response.text(); + throw new Error(`GitHub API ${method} ${endpoint} failed: ${response.status} ${text}`); + } + + return response.json(); +} + +async function graphqlRequest<T>(query: string, variables: Record<string, unknown>, token: string): Promise<T> { + await waitForRateLimit(); + + const response = await fetch('https://api.github.com/graphql', { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + 'User-Agent': 'todo-issue-script', + }, + body: JSON.stringify({ query, variables }), + }); + + trackRateLimit(response); + + if (!response.ok) { + const text = await response.text(); + throw new Error(`GitHub GraphQL failed: ${response.status} ${text}`); + } + + const json = (await response.json()) as { data?: T; errors?: { message: string }[] }; + if (json.errors?.length) { + throw new Error(`GitHub GraphQL errors: ${json.errors.map((e) => e.message).join(', ')}`); + } + + return json.data as T; +} + +export async function getDiffFromApi(endpoint: string, token: string): Promise<string> { + await waitForRateLimit(); + + const response = await fetch(`https://api.github.com${endpoint}`, { + headers: { + Authorization: `Bearer ${token}`, + Accept: 'application/vnd.github.diff', + 'User-Agent': 'todo-issue-script', + }, + }); + + trackRateLimit(response); + + if (!response.ok) { + throw new Error(`GitHub API GET ${endpoint} failed: ${response.status}`); + } + + return response.text(); +} + +export async function fetchExistingIssues(config: Config): Promise<GitHubIssue[]> { + console.log('[INFO] Fetching existing issues via GraphQL...'); + + const allIssues: GitHubIssue[] = []; + let cursor: string | null = null; + + const query = ` + query($owner: String!, $repo: String!, $cursor: String) { + repository(owner: $owner, name: $repo) { + issues(first: 100, after: $cursor, labels: ["todo"], orderBy: { field: CREATED_AT, direction: DESC }) { + pageInfo { hasNextPage endCursor } + nodes { + number + title + state + assignees(first: 10) { nodes { login } } + } + } + } + } + `; + + interface IssuesResponse { + repository: { + issues: { + pageInfo: { hasNextPage: boolean; endCursor: string | null }; + nodes: { number: number; title: string; state: string; assignees: { nodes: { login: string }[] } }[]; + }; + }; + } + + while (true) { + const data: IssuesResponse = await graphqlRequest<IssuesResponse>( + query, + { owner: config.owner, repo: config.repo, cursor }, + config.token, + ); + + const { nodes, pageInfo } = data.repository.issues; + + for (const node of nodes) { + allIssues.push({ + number: node.number, + title: node.title, + state: node.state === 'OPEN' ? 'open' : 'closed', + assignees: node.assignees.nodes, + }); + } + + if (!pageInfo.hasNextPage) break; + cursor = pageInfo.endCursor; + + if (allIssues.length > 10_000) { + console.log('[WARN] Reached issue limit (10k), stopping pagination'); + break; + } + } + + console.log(`[INFO] Fetched ${allIssues.length} existing issues`); + return allIssues; +} + +function buildIssueBody(todo: TodoItem, owner: string, repo: string, sha: string): string { + const blobUrl = `https://github.com/${owner}/${repo}/blob/${sha}/${encodeURI(todo.filename)}#L${todo.line}-L${todo.line + BLOB_LINES}`; + const lines = [ + todo.body || '', + '', + `<!-- todo-issue -->`, + `📝 Found in [\`${todo.filename}#L${todo.line}\`](${blobUrl})`, + '', + `Commit: ${sha}`, + ]; + return lines.join('\n'); +} + +async function ensureLabelExists(owner: string, repo: string, label: string, token: string): Promise<void> { + try { + await githubRequest(`/repos/${owner}/${repo}/labels`, token, 'POST', { + name: label, + color: DEFAULT_LABEL_COLOR, + }); + } catch (err) { + if (!String(err).includes('422')) throw err; + } +} + +export async function createIssue(todo: TodoItem, config: Config, sha: string): Promise<void> { + for (const label of todo.labels) { + await ensureLabelExists(config.owner, config.repo, label, config.token); + } + + const body = buildIssueBody(todo, config.owner, config.repo, sha); + + console.log(`[CREATE] "${todo.title}" (${todo.filename}#L${todo.line})`); + + const payload: Record<string, unknown> = { + title: todo.title, + body, + labels: todo.labels, + ...(todo.assignees.length && { assignees: todo.assignees }), + }; + + try { + await githubRequest(`/repos/${config.owner}/${config.repo}/issues`, config.token, 'POST', payload); + } catch (err) { + if (todo.assignees.length && String(err).includes('422')) { + console.log(`[WARN] Assignees ${todo.assignees.join(', ')} may be invalid, retrying without them`); + delete payload.assignees; + await githubRequest(`/repos/${config.owner}/${config.repo}/issues`, config.token, 'POST', payload); + } else { + throw err; + } + } +} + +export async function closeIssue(todo: TodoItem, config: Config, sha: string): Promise<void> { + if (!todo.issueId) return; + + console.log(`[CLOSE] #${todo.issueId}: "${todo.title}"`); + + await githubRequest(`/repos/${config.owner}/${config.repo}/issues/${todo.issueId}/comments`, config.token, 'POST', { + body: `This TODO was removed in commit ${sha}.\n\nClosing automatically.`, + }); + + await githubRequest(`/repos/${config.owner}/${config.repo}/issues/${todo.issueId}`, config.token, 'PATCH', { + state: 'closed', + state_reason: 'completed', + }); +} + +export async function updateIssue(todo: TodoItem, config: Config): Promise<void> { + if (!todo.issueId) return; + + console.log(`[UPDATE] #${todo.issueId} → "${todo.title}"`); + + await githubRequest(`/repos/${config.owner}/${config.repo}/issues/${todo.issueId}`, config.token, 'PATCH', { + title: todo.title, + }); +} + +export async function addReferenceToIssue(todo: TodoItem, config: Config, sha: string): Promise<void> { + if (!todo.similarIssueId) return; + + const blobUrl = `https://github.com/${config.owner}/${config.repo}/blob/${sha}/${encodeURI(todo.filename)}#L${todo.line}-L${todo.line + BLOB_LINES}`; + + console.log(`[REFERENCE] #${todo.similarIssueId} ← ${todo.filename}#L${todo.line}`); + + await githubRequest(`/repos/${config.owner}/${config.repo}/issues/${todo.similarIssueId}/comments`, config.token, 'POST', { + body: [ + `Also referenced in [\`${todo.filename}#L${todo.line}\`](${blobUrl})`, + '', + `> ${todo.title}`, + '', + `Commit: ${sha}`, + ].join('\n'), + }); +} diff --git a/scripts/todo-issue/src/index.ts b/scripts/todo-issue/src/index.ts new file mode 100644 index 0000000000000..9bf3a6619a03a --- /dev/null +++ b/scripts/todo-issue/src/index.ts @@ -0,0 +1,143 @@ +#!/usr/bin/env bun + +import type { Config } from './types'; +import { extractTodos } from './diff'; +import { matchTodos } from './matcher'; +import { isSimilar } from './similarity'; +import { fetchExistingIssues, getDiffFromApi, createIssue, closeIssue, updateIssue, addReferenceToIssue } from './github'; + +function loadConfig(): Config { + const token = process.env.GITHUB_TOKEN; + if (!token) throw new Error('GITHUB_TOKEN is required'); + + return { + token, + owner: process.env.GITHUB_REPOSITORY_OWNER || 'RocketChat', + repo: process.env.GITHUB_REPOSITORY_NAME || 'Rocket.Chat', + importAll: process.env.IMPORT_ALL === 'true', + shaInput: process.env.SHA_INPUT || '', + pathFilter: process.env.PATH_FILTER || '', + baseSha: process.env.BEFORE_SHA, + headSha: process.env.GITHUB_SHA, + }; +} + +function execGitDiff(args: string[]): string { + const { spawnSync } = require('child_process'); + const result = spawnSync('git', ['diff', '--no-index', ...args], { + encoding: 'utf-8', + maxBuffer: 50 * 1024 * 1024, + }); + return result.stdout ?? ''; +} + +async function getDiff(config: Config): Promise<{ diffText: string; resolvedHeadSha: string }> { + if (config.importAll) { + console.log('[INFO] Import-all mode: scanning entire codebase'); + return { + diffText: execGitDiff(['/dev/null', '.']), + resolvedHeadSha: config.headSha || 'HEAD', + }; + } + + if (config.pathFilter) { + console.log(`[INFO] Path mode: scanning "${config.pathFilter}"`); + return { + diffText: execGitDiff(['/dev/null', '--', config.pathFilter]), + resolvedHeadSha: config.headSha || 'HEAD', + }; + } + + if (config.shaInput) { + let fromSha: string; + let toSha: string; + + if (config.shaInput.includes('...')) { + [fromSha, toSha] = config.shaInput.split('...'); + } else { + fromSha = `${config.shaInput}~1`; + toSha = config.shaInput; + } + + console.log(`[INFO] SHA mode: comparing ${fromSha}...${toSha}`); + return { + diffText: await getDiffFromApi(`/repos/${config.owner}/${config.repo}/compare/${fromSha}...${toSha}`, config.token), + resolvedHeadSha: toSha, + }; + } + + if (!config.baseSha || !config.headSha) { + throw new Error('BEFORE_SHA and GITHUB_SHA are required for push mode'); + } + + console.log(`[INFO] Push mode: comparing ${config.baseSha.slice(0, 7)}...${config.headSha.slice(0, 7)}`); + return { + diffText: await getDiffFromApi( + `/repos/${config.owner}/${config.repo}/compare/${config.baseSha}...${config.headSha}`, + config.token, + ), + resolvedHeadSha: config.headSha, + }; +} + +async function run(): Promise<void> { + console.log('[INFO] Starting todo-issue'); + + const config = loadConfig(); + console.log(`[INFO] Repository: ${config.owner}/${config.repo}`); + + const { diffText, resolvedHeadSha } = await getDiff(config); + + const todos = extractTodos(diffText); + const added = todos.filter((t) => t.type === 'add').length; + const deleted = todos.filter((t) => t.type === 'del').length; + console.log(`[INFO] Found ${todos.length} TODOs (${added} added, ${deleted} deleted)`); + + if (todos.length === 0) { + console.log('[INFO] No TODOs found, exiting'); + return; + } + + const existingIssues = await fetchExistingIssues(config); + + if (config.importAll || config.pathFilter) { + const toCreate = todos.filter((t) => t.type === 'add').filter((todo) => { + return !existingIssues.find((i) => i.title === todo.title || isSimilar(i.title, todo.title)); + }); + + console.log(`[INFO] Import: ${toCreate.length} new issues to create`); + + for (const todo of toCreate) { + await createIssue(todo, config, resolvedHeadSha); + } + } else { + const { toCreate, toClose, toUpdate, toReference } = matchTodos(todos, existingIssues); + + console.log( + `[INFO] Actions: ${toCreate.length} create, ${toClose.length} close, ${toUpdate.length} update, ${toReference.length} reference`, + ); + + for (const todo of toCreate) { + await createIssue(todo, config, resolvedHeadSha); + } + + for (const todo of toClose) { + await closeIssue(todo, config, resolvedHeadSha); + } + + for (const todo of toUpdate) { + await updateIssue(todo, config); + } + + for (const todo of toReference) { + await addReferenceToIssue(todo, config, resolvedHeadSha); + } + } + + console.log('[INFO] Done'); +} + +run().catch((err) => { + console.error('[FATAL]', err); + process.exit(1); +}); diff --git a/scripts/todo-issue/src/matcher.ts b/scripts/todo-issue/src/matcher.ts new file mode 100644 index 0000000000000..26c5ebd9502d4 --- /dev/null +++ b/scripts/todo-issue/src/matcher.ts @@ -0,0 +1,70 @@ +import type { GitHubIssue, MatchResult, TodoItem } from './types'; +import { isSimilar } from './similarity'; + +export function matchTodos(found: TodoItem[], existing: GitHubIssue[]): MatchResult { + const toCreate: TodoItem[] = []; + const toClose: TodoItem[] = []; + const toUpdate: TodoItem[] = []; + const toReference: TodoItem[] = []; + + const addTodos = found.filter((t) => t.type === 'add'); + const delTodos = found.filter((t) => t.type === 'del'); + + for (const del of delTodos) { + const exactMatch = existing.find((e) => e.title === del.title && e.state === 'open'); + if (exactMatch) { + del.issueId = exactMatch.number; + } else { + const similarMatch = existing.find((e) => isSimilar(e.title, del.title) && e.state === 'open'); + if (similarMatch) { + del.issueId = similarMatch.number; + } + } + } + + const consumedDels = new Set<TodoItem>(); + + for (const add of addTodos) { + const movedDel = delTodos.find((d) => !consumedDels.has(d) && d.title === add.title); + if (movedDel) { + movedDel.issueId = undefined; + consumedDels.add(movedDel); + continue; + } + + const updatedDel = delTodos.find((d) => !consumedDels.has(d) && d.issueId && isSimilar(d.title, add.title)); + if (updatedDel) { + add.issueId = updatedDel.issueId; + updatedDel.issueId = undefined; + consumedDels.add(updatedDel); + toUpdate.push(add); + continue; + } + + const exactExisting = existing.find((e) => e.title === add.title); + if (exactExisting) { + add.similarIssueId = exactExisting.number; + toReference.push(add); + continue; + } + + const similarExisting = existing.find((e) => isSimilar(e.title, add.title)); + if (similarExisting) { + add.similarIssueId = similarExisting.number; + toReference.push(add); + continue; + } + + toCreate.push(add); + } + + const closedIssueIds = new Set<number>(); + for (const del of delTodos) { + if (del.issueId && !closedIssueIds.has(del.issueId)) { + closedIssueIds.add(del.issueId); + toClose.push(del); + } + } + + return { toCreate, toClose, toUpdate, toReference }; +} diff --git a/scripts/todo-issue/src/similarity.ts b/scripts/todo-issue/src/similarity.ts new file mode 100644 index 0000000000000..a95362b57272e --- /dev/null +++ b/scripts/todo-issue/src/similarity.ts @@ -0,0 +1,24 @@ +const SIMILARITY_THRESHOLD = 0.8; + +function levenshtein(a: string, b: string): number { + const m = a.length; + const n = b.length; + const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0)); + + for (let i = 0; i <= m; i++) dp[i][0] = i; + for (let j = 0; j <= n; j++) dp[0][j] = j; + + for (let i = 1; i <= m; i++) { + for (let j = 1; j <= n; j++) { + dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]); + } + } + + return dp[m][n]; +} + +export function isSimilar(a: string, b: string): boolean { + const avgLen = (a.length + b.length) / 2; + const maxDistance = avgLen * (1 - SIMILARITY_THRESHOLD); + return levenshtein(a, b) <= maxDistance; +} diff --git a/scripts/todo-issue/src/types.ts b/scripts/todo-issue/src/types.ts new file mode 100644 index 0000000000000..7bbde522d342e --- /dev/null +++ b/scripts/todo-issue/src/types.ts @@ -0,0 +1,36 @@ +export interface Config { + token: string; + owner: string; + repo: string; + importAll: boolean; + shaInput: string; + pathFilter: string; + baseSha?: string; + headSha?: string; +} + +export interface GitHubIssue { + number: number; + title: string; + state: 'open' | 'closed'; + assignees: { login: string }[]; +} + +export interface TodoItem { + type: 'add' | 'del'; + title: string; + body: string | false; + filename: string; + line: number; + labels: string[]; + assignees: string[]; + issueId?: number; + similarIssueId?: number; +} + +export interface MatchResult { + toCreate: TodoItem[]; + toClose: TodoItem[]; + toUpdate: TodoItem[]; + toReference: TodoItem[]; +} diff --git a/scripts/todo-issue/tsconfig.json b/scripts/todo-issue/tsconfig.json new file mode 100644 index 0000000000000..052fdddd3ccbe --- /dev/null +++ b/scripts/todo-issue/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "strict": true, + "module": "esnext", + "moduleResolution": "bundler", + "target": "esnext", + "skipLibCheck": true, + "noEmit": true + }, + "include": ["src"] +} From 40253146de8d8f83737e71b0ade7c67e0c295a28 Mon Sep 17 00:00:00 2001 From: Daniel Nwachukwu <dannyclassic56@gmail.com> Date: Wed, 25 Feb 2026 05:42:13 +0100 Subject: [PATCH 047/108] chore: migrate rooms.leave endpoint to new OpenAPI pattern with AJV validation (#38957) Co-authored-by: Guilherme Gazzo <guilhermegazzo@gmail.com> --- .changeset/migrate-rooms-leave-endpoint.md | 6 ++ apps/meteor/app/api/server/v1/rooms.ts | 79 +++++++++++++++++----- packages/rest-typings/src/v1/rooms.ts | 5 -- 3 files changed, 68 insertions(+), 22 deletions(-) create mode 100644 .changeset/migrate-rooms-leave-endpoint.md diff --git a/.changeset/migrate-rooms-leave-endpoint.md b/.changeset/migrate-rooms-leave-endpoint.md new file mode 100644 index 0000000000000..4f9a6263a9a19 --- /dev/null +++ b/.changeset/migrate-rooms-leave-endpoint.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/meteor': minor +'@rocket.chat/rest-typings': minor +--- + +Migrated rooms.leave endpoint to new OpenAPI pattern with AJV validation diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index 988e60002eef9..70875e47fedd9 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -390,23 +390,6 @@ API.v1.addRoute( }, ); -API.v1.addRoute( - 'rooms.leave', - { authRequired: true }, - { - async post() { - const room = await findRoomByIdOrName({ params: this.bodyParams }); - const user = await Users.findOneById(this.userId); - if (!user) { - return API.v1.failure('Invalid user'); - } - await leaveRoomMethod(user, room._id); - - return API.v1.success(); - }, - }, -); - /* TO-DO: 8.0.0 should use the ajv validation which will change this endpoint's @@ -935,6 +918,14 @@ type RoomsFavorite = favorite: boolean; }; +type RoomsLeave = + | { + roomId: string; + } + | { + roomName: string; + }; + const isRoomGetRolesPropsSchema = { type: 'object', properties: { @@ -967,7 +958,29 @@ const RoomsFavoriteSchema = { ], }; +const isRoomsLeavePropsSchema = { + anyOf: [ + { + type: 'object', + properties: { + roomId: { type: 'string' }, + }, + required: ['roomId'], + additionalProperties: false, + }, + { + type: 'object', + properties: { + roomName: { type: 'string' }, + }, + required: ['roomName'], + additionalProperties: false, + }, + ], +}; + const isRoomsFavoriteProps = ajv.compile<RoomsFavorite>(RoomsFavoriteSchema); +const isRoomsLeaveProps = ajv.compile<RoomsLeave>(isRoomsLeavePropsSchema); export const roomEndpoints = API.v1 .get( @@ -1141,6 +1154,38 @@ export const roomEndpoints = API.v1 await toggleFavoriteMethod(this.userId, room._id, favorite); + return API.v1.success(); + }, + ) + .post( + 'rooms.leave', + { + authRequired: true, + body: isRoomsLeaveProps, + response: { + 200: ajv.compile<void>({ + type: 'object', + properties: { + success: { type: 'boolean', enum: [true] }, + }, + required: ['success'], + additionalProperties: false, + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, + }, + async function action() { + const room = await findRoomByIdOrName({ params: this.bodyParams }); + + const user = await Users.findOneById(this.userId); + + if (!user) { + return API.v1.failure('error-invalid-user'); + } + + await leaveRoomMethod(user, room._id); + return API.v1.success(); }, ); diff --git a/packages/rest-typings/src/v1/rooms.ts b/packages/rest-typings/src/v1/rooms.ts index 18c4574f7bb23..be2314d65bd41 100644 --- a/packages/rest-typings/src/v1/rooms.ts +++ b/packages/rest-typings/src/v1/rooms.ts @@ -90,7 +90,6 @@ export const isRoomsAutocompleteAdminRoomsPayload = ajv.compile<RoomsAutocomplet type BaseRoomsProps = { roomId: string } | { roomName: string }; type RoomsInfoProps = BaseRoomsProps; -type RoomsLeaveProps = BaseRoomsProps; const RoomsInfoSchema = { oneOf: [ @@ -822,10 +821,6 @@ export type RoomsEndpoints = { }; }; - '/v1/rooms.leave': { - POST: (params: RoomsLeaveProps) => void; - }; - '/v1/rooms.getDiscussions': { GET: (params: RoomsGetDiscussionsProps) => PaginatedResult<{ discussions: IRoom[]; From f6dce5f22e1cdc1518d4c105b53e7f5db87bfd53 Mon Sep 17 00:00:00 2001 From: Diego Sampaio <chinello@gmail.com> Date: Wed, 25 Feb 2026 01:53:37 -0300 Subject: [PATCH 048/108] fix: message being marked as sent before the request completes (#39003) --- .changeset/nice-penguins-rhyme.md | 5 +++++ apps/meteor/app/lib/client/methods/sendMessage.ts | 8 +------- apps/meteor/client/lib/chats/flows/sendMessage.ts | 15 +++++++++++---- 3 files changed, 17 insertions(+), 11 deletions(-) create mode 100644 .changeset/nice-penguins-rhyme.md diff --git a/.changeset/nice-penguins-rhyme.md b/.changeset/nice-penguins-rhyme.md new file mode 100644 index 0000000000000..5e89a31ef9739 --- /dev/null +++ b/.changeset/nice-penguins-rhyme.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fix marking a message as sent before the request finishes diff --git a/apps/meteor/app/lib/client/methods/sendMessage.ts b/apps/meteor/app/lib/client/methods/sendMessage.ts index ee67688a0b6ae..0969ea8d9cf36 100644 --- a/apps/meteor/app/lib/client/methods/sendMessage.ts +++ b/apps/meteor/app/lib/client/methods/sendMessage.ts @@ -44,13 +44,7 @@ Meteor.methods<ServerMethods>({ await onClientMessageReceived(message as IMessage).then((message) => { Messages.state.store(message); - void clientCallbacks.run('afterSaveMessage', message, { room, user }); - - // Now that the message is stored, we can go ahead and mark as sent - Messages.state.update( - (record) => record._id === message._id && record.temp === true, - ({ temp: _, ...record }) => record, - ); + return clientCallbacks.run('afterSaveMessage', message, { room, user }); }); }, }); diff --git a/apps/meteor/client/lib/chats/flows/sendMessage.ts b/apps/meteor/client/lib/chats/flows/sendMessage.ts index d592729e295b6..16a184de04e81 100644 --- a/apps/meteor/client/lib/chats/flows/sendMessage.ts +++ b/apps/meteor/client/lib/chats/flows/sendMessage.ts @@ -1,14 +1,15 @@ import type { IMessage } from '@rocket.chat/core-typings'; +import { processMessageEditing } from './processMessageEditing'; +import { processSetReaction } from './processSetReaction'; +import { processSlashCommand } from './processSlashCommand'; +import { processTooLongMessage } from './processTooLongMessage'; import { sdk } from '../../../../app/utils/client/lib/SDKClient'; import { t } from '../../../../app/utils/lib/i18n'; +import { Messages } from '../../../stores'; import { onClientBeforeSendMessage } from '../../onClientBeforeSendMessage'; import { dispatchToastMessage } from '../../toast'; import type { ChatAPI } from '../ChatAPI'; -import { processMessageEditing } from './processMessageEditing'; -import { processSetReaction } from './processSetReaction'; -import { processSlashCommand } from './processSlashCommand'; -import { processTooLongMessage } from './processTooLongMessage'; const process = async (chat: ChatAPI, message: IMessage, previewUrls?: string[], isSlashCommandAllowed?: boolean): Promise<void> => { const mid = chat.currentEditingMessage.getMID(); @@ -36,6 +37,12 @@ const process = async (chat: ChatAPI, message: IMessage, previewUrls?: string[], } await sdk.call('sendMessage', message, previewUrls); + + // after the request is complete we can go ahead and mark as sent + Messages.state.update( + (record) => record._id === message._id && record.temp === true, + ({ temp: _, ...record }) => record, + ); }; export const sendMessage = async ( From 813d57a4161dbac6243e2ecb3351a33b47397b4b Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 02:14:44 -0300 Subject: [PATCH 049/108] regression: Codebock autoclose unclosed markdown code blocks on message send (#39007) Co-authored-by: Guilherme Gazzo <guilherme@gazzo.xyz> --- .../client/lib/chats/flows/sendMessage.ts | 2 ++ .../lib/utils/closeUnclosedCodeBlock.spec.ts | 36 +++++++++++++++++++ .../lib/utils/closeUnclosedCodeBlock.ts | 7 ++++ .../server/services/messages/service.ts | 7 ++++ apps/meteor/tests/end-to-end/api/chat.ts | 22 ++++++++++++ 5 files changed, 74 insertions(+) create mode 100644 apps/meteor/client/lib/utils/closeUnclosedCodeBlock.spec.ts create mode 100644 apps/meteor/lib/utils/closeUnclosedCodeBlock.ts diff --git a/apps/meteor/client/lib/chats/flows/sendMessage.ts b/apps/meteor/client/lib/chats/flows/sendMessage.ts index 16a184de04e81..dd4fae1deb952 100644 --- a/apps/meteor/client/lib/chats/flows/sendMessage.ts +++ b/apps/meteor/client/lib/chats/flows/sendMessage.ts @@ -6,6 +6,7 @@ import { processSlashCommand } from './processSlashCommand'; import { processTooLongMessage } from './processTooLongMessage'; import { sdk } from '../../../../app/utils/client/lib/SDKClient'; import { t } from '../../../../app/utils/lib/i18n'; +import { closeUnclosedCodeBlock } from '../../../../lib/utils/closeUnclosedCodeBlock'; import { Messages } from '../../../stores'; import { onClientBeforeSendMessage } from '../../onClientBeforeSendMessage'; import { dispatchToastMessage } from '../../toast'; @@ -66,6 +67,7 @@ export const sendMessage = async ( chat.readStateManager.clearUnreadMark(); text = text.trim(); + text = closeUnclosedCodeBlock(text); const mid = chat.currentEditingMessage.getMID(); if (!text && !mid) { // Nothing to do diff --git a/apps/meteor/client/lib/utils/closeUnclosedCodeBlock.spec.ts b/apps/meteor/client/lib/utils/closeUnclosedCodeBlock.spec.ts new file mode 100644 index 0000000000000..7131585a53289 --- /dev/null +++ b/apps/meteor/client/lib/utils/closeUnclosedCodeBlock.spec.ts @@ -0,0 +1,36 @@ +import { closeUnclosedCodeBlock } from '../../../lib/utils/closeUnclosedCodeBlock'; + +describe('closeUnclosedCodeBlock', () => { + it('should return the text unchanged if there are no backticks', () => { + expect(closeUnclosedCodeBlock('hello world')).toBe('hello world'); + }); + + it('should return the text unchanged if code block is already closed', () => { + const text = '```\ncode\n```'; + expect(closeUnclosedCodeBlock(text)).toBe(text); + }); + + it('should append closing backticks when code block is unclosed', () => { + const text = '```\ncode'; + expect(closeUnclosedCodeBlock(text)).toBe('```\ncode\n```'); + }); + + it('should handle code block with language specifier unclosed', () => { + const text = '```javascript\nconst x = 1;'; + expect(closeUnclosedCodeBlock(text)).toBe('```javascript\nconst x = 1;\n```'); + }); + + it('should return unchanged text when there are multiple closed code blocks', () => { + const text = '```\ncode1\n```\ntext\n```\ncode2\n```'; + expect(closeUnclosedCodeBlock(text)).toBe(text); + }); + + it('should close unclosed block when there is one closed block followed by an unclosed block', () => { + const text = '```\ncode1\n```\ntext\n```\ncode2'; + expect(closeUnclosedCodeBlock(text)).toBe('```\ncode1\n```\ntext\n```\ncode2\n```'); + }); + + it('should return the text unchanged if it is empty', () => { + expect(closeUnclosedCodeBlock('')).toBe(''); + }); +}); diff --git a/apps/meteor/lib/utils/closeUnclosedCodeBlock.ts b/apps/meteor/lib/utils/closeUnclosedCodeBlock.ts new file mode 100644 index 0000000000000..c1423f0bc42c8 --- /dev/null +++ b/apps/meteor/lib/utils/closeUnclosedCodeBlock.ts @@ -0,0 +1,7 @@ +export const closeUnclosedCodeBlock = (text: string): string => { + const backtickCount = (text.match(/```/g) || []).length; + if (backtickCount % 2 !== 0) { + return `${text}\n\`\`\``; + } + return text; +}; diff --git a/apps/meteor/server/services/messages/service.ts b/apps/meteor/server/services/messages/service.ts index 998718cf71d1a..7a6fd09f5f04c 100644 --- a/apps/meteor/server/services/messages/service.ts +++ b/apps/meteor/server/services/messages/service.ts @@ -26,6 +26,8 @@ import { BeforeSaveMarkdownParser } from './hooks/BeforeSaveMarkdownParser'; import { mentionServer } from './hooks/BeforeSaveMentions'; import { BeforeSavePreventMention } from './hooks/BeforeSavePreventMention'; import { BeforeSaveSpotify } from './hooks/BeforeSaveSpotify'; +import { closeUnclosedCodeBlock } from '../../../lib/utils/closeUnclosedCodeBlock'; +import { shouldBreakInVersion } from '../../lib/shouldBreakInVersion'; const disableMarkdownParser = ['yes', 'true'].includes(String(process.env.DISABLE_MESSAGE_PARSER).toLowerCase()); @@ -236,6 +238,11 @@ export class MessageService extends ServiceClassInternal implements IMessageServ message = await this.cannedResponse.replacePlaceholders({ message, room, user }); message = await this.badWords.filterBadWords({ message }); + // TODO: Auto-close unclosed markdown code blocks for server versions below 9.0.0 + // In 9.0.0, this behavior is handled on the client side, so this block should be removed. + if (!shouldBreakInVersion('9.0.0') && message.msg) { + message = { ...message, msg: closeUnclosedCodeBlock(message.msg) }; + } message = await this.markdownParser.parseMarkdown({ message, config: this.getMarkdownConfig() }); message = await mentionServer.execute(message); if (parseUrls) { diff --git a/apps/meteor/tests/end-to-end/api/chat.ts b/apps/meteor/tests/end-to-end/api/chat.ts index 4652ae424193e..15ab7e4c083cb 100644 --- a/apps/meteor/tests/end-to-end/api/chat.ts +++ b/apps/meteor/tests/end-to-end/api/chat.ts @@ -1825,6 +1825,28 @@ describe('[Chat]', () => { }); }); }); + + // TODO: Auto-close unclosed markdown code blocks on backend - Remove in 9.0.0 + // In 9.0.0, this behavior is handled entirely on the client side and should no longer be done on the backend. + it('should auto-close an unclosed code block when sending a message', async () => { + const unclosedMsg = '```\nsome code'; + const expectedMsg = '```\nsome code\n```'; + await request + .post(api('chat.sendMessage')) + .set(credentials) + .send({ + message: { + rid: testChannel._id, + msg: unclosedMsg, + }, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('message.msg', expectedMsg); + }); + }); }); describe('/chat.update', () => { From 0e45814ff6733faa322e9154afd621f9df1d34cc Mon Sep 17 00:00:00 2001 From: R Pratheek <111165032+Pratheek555@users.noreply.github.com> Date: Wed, 25 Feb 2026 19:25:53 +0530 Subject: [PATCH 050/108] chore(omnichannel): remove 'any' usage by applying required Type safety (#38982) --- .../additionalForms/CurrentChatTags.tsx | 14 +++----------- .../client/views/omnichannel/components/Tags.tsx | 2 +- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/apps/meteor/client/views/omnichannel/additionalForms/CurrentChatTags.tsx b/apps/meteor/client/views/omnichannel/additionalForms/CurrentChatTags.tsx index e298f3cb3dbc4..0df8f5d14a2ec 100644 --- a/apps/meteor/client/views/omnichannel/additionalForms/CurrentChatTags.tsx +++ b/apps/meteor/client/views/omnichannel/additionalForms/CurrentChatTags.tsx @@ -4,8 +4,8 @@ import { useHasLicenseModule } from '../../../hooks/useHasLicenseModule'; import AutoCompleteTagsMultiple from '../tags/AutoCompleteTagsMultiple'; type CurrentChatTagsProps = Pick<ComponentProps<typeof AutoCompleteTagsMultiple>, 'id' | 'aria-labelledby'> & { - value: Array<{ value: string; label: string }>; - handler: (value: { label: string; value: string }[]) => void; + value: NonNullable<ComponentProps<typeof AutoCompleteTagsMultiple>['value']>; + handler: NonNullable<ComponentProps<typeof AutoCompleteTagsMultiple>['onChange']>; department?: string; viewAll?: boolean; }; @@ -17,15 +17,7 @@ const CurrentChatTags = ({ value, handler, department, viewAll, ...props }: Curr return null; } - return ( - <AutoCompleteTagsMultiple - {...props} - onChange={handler as any} // FIXME: any - value={value} - department={department} - viewAll={viewAll} - /> - ); + return <AutoCompleteTagsMultiple {...props} onChange={handler} value={value} department={department} viewAll={viewAll} />; }; export default CurrentChatTags; diff --git a/apps/meteor/client/views/omnichannel/components/Tags.tsx b/apps/meteor/client/views/omnichannel/components/Tags.tsx index c85d8d9994181..f5a8e96ee6455 100644 --- a/apps/meteor/client/views/omnichannel/components/Tags.tsx +++ b/apps/meteor/client/views/omnichannel/components/Tags.tsx @@ -76,7 +76,7 @@ const Tags = ({ tags = [], handler, error, tagRequired, department }: TagsProps) <CurrentChatTags id={tagsFieldId} value={paginatedTagValue} - handler={(tags: { label: string; value: string }[]): void => { + handler={(tags): void => { handler(tags.map((tag) => tag.label)); }} department={department} From cf99fe5dea0b62a337a4ebd77cb087d22b4e2688 Mon Sep 17 00:00:00 2001 From: ANIRUDDHA ADAK <aniruddhaadak80@gmail.com> Date: Wed, 25 Feb 2026 19:57:39 +0530 Subject: [PATCH 051/108] docs: update YouTube capitalization in README (#38871) Co-authored-by: Kevin Aleman <kaleman960@gmail.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f663b0113e1a0..51c049b0fcfee 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ We're hiring developers, technical support, and product managers all the time. C - [Twitter](https://twitter.com/RocketChat) - [Facebook](https://www.facebook.com/RocketChatApp) - [LinkedIn](https://www.linkedin.com/company/rocket-chat) -- [Youtube](https://www.youtube.com/channel/UCin9nv7mUjoqrRiwrzS5UVQ) +- [YouTube](https://www.youtube.com/channel/UCin9nv7mUjoqrRiwrzS5UVQ) # 🗒️ Credits From b1b1d6ccd81c90d231a7e594f834965c6e5f4fae Mon Sep 17 00:00:00 2001 From: Shreyas <shreyaswagh2004@gmail.com> Date: Wed, 25 Feb 2026 20:56:31 +0530 Subject: [PATCH 052/108] fix(message-parser): preserve ordered list index 0 in LIST_ITEM nodes (#39052) --- .changeset/loud-weeks-protect.md | 5 +++++ packages/message-parser/src/utils.ts | 2 +- packages/message-parser/tests/orderedList.test.ts | 2 ++ 3 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 .changeset/loud-weeks-protect.md diff --git a/.changeset/loud-weeks-protect.md b/.changeset/loud-weeks-protect.md new file mode 100644 index 0000000000000..3317177f72765 --- /dev/null +++ b/.changeset/loud-weeks-protect.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/message-parser': patch +--- + +Fixes ordered list AST generation to preserve `number: 0` for list items that start at index `0`. diff --git a/packages/message-parser/src/utils.ts b/packages/message-parser/src/utils.ts index 692971e153d8e..5d14c36e47512 100644 --- a/packages/message-parser/src/utils.ts +++ b/packages/message-parser/src/utils.ts @@ -132,7 +132,7 @@ export const unorderedList = generate('UNORDERED_LIST'); export const listItem = (text: Inlines[], number?: number): ListItem => ({ type: 'LIST_ITEM', value: text, - ...(number && { number }), + ...(number !== undefined && { number }), }); export const mentionUser = (() => { diff --git a/packages/message-parser/tests/orderedList.test.ts b/packages/message-parser/tests/orderedList.test.ts index e7a453d32adb2..fe292cee168f6 100644 --- a/packages/message-parser/tests/orderedList.test.ts +++ b/packages/message-parser/tests/orderedList.test.ts @@ -4,6 +4,7 @@ import { bold, plain, orderedList, listItem, emoji } from '../src/utils'; test.each([ [ ` +0. Zeroth item 7. First item 2. Second item 8. Third item @@ -13,6 +14,7 @@ test.each([ `.trim(), [ orderedList([ + listItem([plain('Zeroth item')], 0), listItem([plain('First item')], 7), listItem([plain('Second item')], 2), listItem([plain('Third item')], 8), From 02b1e6e6a184850d21e335077ca30382a1c7a66b Mon Sep 17 00:00:00 2001 From: Suryansh Mishra <suryansh.mishra.sm6@gmail.com> Date: Wed, 25 Feb 2026 21:42:30 +0530 Subject: [PATCH 053/108] chore(message-parser): replace wasteful filter().shift() with find() in extractFirstResult (#39046) --- .changeset/fix-message-parser-reduce-perf.md | 5 +++++ packages/message-parser/src/utils.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-message-parser-reduce-perf.md diff --git a/.changeset/fix-message-parser-reduce-perf.md b/.changeset/fix-message-parser-reduce-perf.md new file mode 100644 index 0000000000000..6601f8f205c43 --- /dev/null +++ b/.changeset/fix-message-parser-reduce-perf.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/message-parser': patch +--- + +Replaces wasteful `filter().shift()` with `find(Boolean)` in `extractFirstResult` to avoid allocating an intermediate filtered array just to get the first truthy element. diff --git a/packages/message-parser/src/utils.ts b/packages/message-parser/src/utils.ts index 5d14c36e47512..586a1baf6c069 100644 --- a/packages/message-parser/src/utils.ts +++ b/packages/message-parser/src/utils.ts @@ -275,5 +275,5 @@ export const extractFirstResult = (value: Types[keyof Types]['value']): Types[ke return value; } - return value.filter((item) => item).shift() as Types[keyof Types]['value']; + return value.find(Boolean) as Types[keyof Types]['value']; }; From 9d8c4104dd828c24d55e595f1f130517c2fc8a15 Mon Sep 17 00:00:00 2001 From: Suryansh Mishra <suryansh.mishra.sm6@gmail.com> Date: Wed, 25 Feb 2026 21:47:31 +0530 Subject: [PATCH 054/108] chore: migrate instances.get endpoint to new chained API pattern (#38881) Co-authored-by: Guilherme Gazzo <guilhermegazzo@gmail.com> --- .../refactor-instances-api-chained-pattern.md | 5 + apps/meteor/app/api/server/v1/instances.ts | 96 ++++++++++++++----- 2 files changed, 78 insertions(+), 23 deletions(-) create mode 100644 .changeset/refactor-instances-api-chained-pattern.md diff --git a/.changeset/refactor-instances-api-chained-pattern.md b/.changeset/refactor-instances-api-chained-pattern.md new file mode 100644 index 0000000000000..e38ef1235e7ef --- /dev/null +++ b/.changeset/refactor-instances-api-chained-pattern.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + + adds `instances.get` API endpoint to new chained pattern with response schemas diff --git a/apps/meteor/app/api/server/v1/instances.ts b/apps/meteor/app/api/server/v1/instances.ts index 47f98c856f446..6fad69d1c33b9 100644 --- a/apps/meteor/app/api/server/v1/instances.ts +++ b/apps/meteor/app/api/server/v1/instances.ts @@ -1,4 +1,5 @@ import { InstanceStatus } from '@rocket.chat/models'; +import { ajv, validateUnauthorizedErrorResponse, validateForbiddenErrorResponse } from '@rocket.chat/rest-typings'; import { isRunningMs } from '../../../../server/lib/isRunningMs'; import { API } from '../api'; @@ -12,33 +13,82 @@ const getConnections = (() => { return () => getInstanceList(); })(); -API.v1.addRoute( +API.v1.get( 'instances.get', - { authRequired: true, permissionsRequired: ['view-statistics'] }, { - async get() { - const instanceRecords = await InstanceStatus.find().toArray(); - - const connections = await getConnections(); - - const result = instanceRecords.map((instanceRecord) => { - const connection = connections.find((c) => c.id === instanceRecord._id); - - return { - address: connection?.ipList[0], + authRequired: true, + permissionsRequired: ['view-statistics'], + response: { + 200: ajv.compile<{ + instances: { + address?: string; currentStatus: { - connected: connection?.available || false, - lastHeartbeatTime: connection?.lastHeartbeatTime, - local: connection?.local, + connected: boolean; + lastHeartbeatTime?: number; + local?: boolean; + }; + instanceRecord: object; + broadcastAuth: boolean; + }[]; + success: true; + }>({ + type: 'object', + properties: { + instances: { + type: 'array', + items: { + type: 'object', + properties: { + address: { type: 'string' }, + currentStatus: { + type: 'object', + properties: { + connected: { type: 'boolean' }, + lastHeartbeatTime: { type: 'number' }, + local: { type: 'boolean' }, + }, + required: ['connected'], + }, + instanceRecord: { type: 'object' }, + broadcastAuth: { type: 'boolean' }, + }, + required: ['currentStatus', 'instanceRecord', 'broadcastAuth'], + }, }, - instanceRecord, - broadcastAuth: true, - }; - }); - - return API.v1.success({ - instances: result, - }); + success: { + type: 'boolean', + enum: [true], + }, + }, + required: ['instances', 'success'], + additionalProperties: false, + }), + 401: validateUnauthorizedErrorResponse, + 403: validateForbiddenErrorResponse, }, }, + async function action() { + const instanceRecords = await InstanceStatus.find().toArray(); + + const connections = await getConnections(); + + const result = instanceRecords.map((instanceRecord) => { + const connection = connections.find((c) => c.id === instanceRecord._id); + + return { + address: connection?.ipList[0], + currentStatus: { + connected: connection?.available || false, + lastHeartbeatTime: connection?.lastHeartbeatTime, + local: connection?.local, + }, + instanceRecord, + broadcastAuth: true, + }; + }); + + return API.v1.success({ + instances: result, + }); + }, ); From 8d73ce5ce403a12acc6916e975a053935473fcdd Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 13:28:15 -0300 Subject: [PATCH 055/108] fix: inclusive query parameter handling in groups.history REST API (#39012) Co-authored-by: ggazzo <5263975+ggazzo@users.noreply.github.com> Co-authored-by: Guilherme Gazzo <guilherme@gazzo.xyz> --- apps/meteor/app/api/server/v1/groups.ts | 4 +- apps/meteor/tests/end-to-end/api/channels.ts | 100 +++++++++++++++++ .../tests/end-to-end/api/direct-message.ts | 102 ++++++++++++++++++ apps/meteor/tests/end-to-end/api/groups.ts | 100 +++++++++++++++++ .../src/v1/groups/GroupsHistoryProps.ts | 8 +- 5 files changed, 311 insertions(+), 3 deletions(-) diff --git a/apps/meteor/app/api/server/v1/groups.ts b/apps/meteor/app/api/server/v1/groups.ts index c91afe561ed5d..2437813860836 100644 --- a/apps/meteor/app/api/server/v1/groups.ts +++ b/apps/meteor/app/api/server/v1/groups.ts @@ -509,7 +509,7 @@ API.v1.addRoute( oldestDate = new Date(this.queryParams.oldest); } - const inclusive = this.queryParams.inclusive || false; + const inclusive = this.queryParams.inclusive === 'true'; let count = 20; if (this.queryParams.count) { @@ -521,7 +521,7 @@ API.v1.addRoute( offset = parseInt(String(this.queryParams.offset)); } - const unreads = this.queryParams.unreads || false; + const unreads = this.queryParams.unreads === 'true'; const showThreadMessages = this.queryParams.showThreadMessages !== 'false'; diff --git a/apps/meteor/tests/end-to-end/api/channels.ts b/apps/meteor/tests/end-to-end/api/channels.ts index f2d348240f3cf..98224d6babd3e 100644 --- a/apps/meteor/tests/end-to-end/api/channels.ts +++ b/apps/meteor/tests/end-to-end/api/channels.ts @@ -1771,6 +1771,106 @@ describe('[Channels]', () => { }) .end(done); }); + + describe('inclusive parameter', () => { + let testChannel: IRoom; + let oldestMessage: IMessage; + let middleMessage: IMessage; + let latestMessage: IMessage; + + before(async () => { + const channelRes = await request + .post(api('channels.create')) + .set(credentials) + .send({ name: `inclusive-test-channel-${Date.now()}` }); + testChannel = channelRes.body.channel; + + // Send messages with small delays to ensure distinct timestamps + const msg1 = await sendMessage({ message: { rid: testChannel._id, msg: 'oldest message' } }); + oldestMessage = msg1.body.message; + + // Small delay to ensure timestamps are different + await new Promise((resolve) => setTimeout(resolve, 50)); + + const msg2 = await sendMessage({ message: { rid: testChannel._id, msg: 'middle message' } }); + middleMessage = msg2.body.message; + + await new Promise((resolve) => setTimeout(resolve, 50)); + + const msg3 = await sendMessage({ message: { rid: testChannel._id, msg: 'latest message' } }); + latestMessage = msg3.body.message; + }); + + after(async () => { + if (testChannel?._id) { + await deleteRoom({ type: 'c', roomId: testChannel._id }); + } + }); + + it('should include boundary messages when inclusive=true', async () => { + const res = await request + .get(api('channels.history')) + .set(credentials) + .query({ + roomId: testChannel._id, + oldest: oldestMessage.ts, + latest: latestMessage.ts, + inclusive: 'true', + }) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('messages').that.is.an('array'); + + const messageIds = res.body.messages.map((m: IMessage) => m._id); + expect(messageIds).to.include(oldestMessage._id, 'oldest message should be included'); + expect(messageIds).to.include(latestMessage._id, 'latest message should be included'); + }); + + it('should exclude boundary messages when inclusive=false', async () => { + const res = await request + .get(api('channels.history')) + .set(credentials) + .query({ + roomId: testChannel._id, + oldest: oldestMessage.ts, + latest: latestMessage.ts, + inclusive: 'false', + }) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('messages').that.is.an('array'); + + const messageIds = res.body.messages.map((m: IMessage) => m._id); + expect(messageIds).to.not.include(oldestMessage._id, 'oldest message should be excluded'); + expect(messageIds).to.not.include(latestMessage._id, 'latest message should be excluded'); + // Middle message should still be included if it exists in the range + expect(messageIds).to.include(middleMessage._id, 'middle message should be included'); + }); + + it('should exclude boundary messages by default (no inclusive param)', async () => { + const res = await request + .get(api('channels.history')) + .set(credentials) + .query({ + roomId: testChannel._id, + oldest: oldestMessage.ts, + latest: latestMessage.ts, + }) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('messages').that.is.an('array'); + + const messageIds = res.body.messages.map((m: IMessage) => m._id); + expect(messageIds).to.not.include(oldestMessage._id, 'oldest message should be excluded by default'); + expect(messageIds).to.not.include(latestMessage._id, 'latest message should be excluded by default'); + }); + }); }); describe('/channels.members', () => { diff --git a/apps/meteor/tests/end-to-end/api/direct-message.ts b/apps/meteor/tests/end-to-end/api/direct-message.ts index c449ee29e8ac9..67ce007aaadab 100644 --- a/apps/meteor/tests/end-to-end/api/direct-message.ts +++ b/apps/meteor/tests/end-to-end/api/direct-message.ts @@ -194,6 +194,108 @@ describe('[Direct Messages]', () => { .end(done); }); + describe('/im.history inclusive parameter', () => { + let testDMRoom: IRoom; + let testUser2: TestUser<IUser>; + let oldestMessage: IMessage; + let middleMessage: IMessage; + let latestMessage: IMessage; + + before(async () => { + testUser2 = await createUser(); + const dmRes = await request.post(api('im.create')).set(credentials).send({ username: testUser2.username }); + testDMRoom = dmRes.body.room; + + // Send messages with small delays to ensure distinct timestamps + const msg1 = await sendMessage({ message: { rid: testDMRoom._id, msg: 'oldest message' } }); + oldestMessage = msg1.body.message; + + // Small delay to ensure timestamps are different + await new Promise((resolve) => setTimeout(resolve, 50)); + + const msg2 = await sendMessage({ message: { rid: testDMRoom._id, msg: 'middle message' } }); + middleMessage = msg2.body.message; + + await new Promise((resolve) => setTimeout(resolve, 50)); + + const msg3 = await sendMessage({ message: { rid: testDMRoom._id, msg: 'latest message' } }); + latestMessage = msg3.body.message; + }); + + after(async () => { + if (testDMRoom?._id) { + await deleteRoom({ type: 'd', roomId: testDMRoom._id }); + } + if (testUser2) { + await deleteUser(testUser2); + } + }); + + it('should include boundary messages when inclusive=true', async () => { + const res = await request + .get(api('im.history')) + .set(credentials) + .query({ + roomId: testDMRoom._id, + oldest: oldestMessage.ts, + latest: latestMessage.ts, + inclusive: 'true', + }) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('messages').that.is.an('array'); + + const messageIds = res.body.messages.map((m: IMessage) => m._id); + expect(messageIds).to.include(oldestMessage._id, 'oldest message should be included'); + expect(messageIds).to.include(latestMessage._id, 'latest message should be included'); + }); + + it('should exclude boundary messages when inclusive=false', async () => { + const res = await request + .get(api('im.history')) + .set(credentials) + .query({ + roomId: testDMRoom._id, + oldest: oldestMessage.ts, + latest: latestMessage.ts, + inclusive: 'false', + }) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('messages').that.is.an('array'); + + const messageIds = res.body.messages.map((m: IMessage) => m._id); + expect(messageIds).to.not.include(oldestMessage._id, 'oldest message should be excluded'); + expect(messageIds).to.not.include(latestMessage._id, 'latest message should be excluded'); + // Middle message should still be included if it exists in the range + expect(messageIds).to.include(middleMessage._id, 'middle message should be included'); + }); + + it('should exclude boundary messages by default (no inclusive param)', async () => { + const res = await request + .get(api('im.history')) + .set(credentials) + .query({ + roomId: testDMRoom._id, + oldest: oldestMessage.ts, + latest: latestMessage.ts, + }) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('messages').that.is.an('array'); + + const messageIds = res.body.messages.map((m: IMessage) => m._id); + expect(messageIds).to.not.include(oldestMessage._id, 'oldest message should be excluded by default'); + expect(messageIds).to.not.include(latestMessage._id, 'latest message should be excluded by default'); + }); + }); + it('/im.list', (done) => { void request .get(api('im.list')) diff --git a/apps/meteor/tests/end-to-end/api/groups.ts b/apps/meteor/tests/end-to-end/api/groups.ts index b92ac79bd3046..6eecca278af35 100644 --- a/apps/meteor/tests/end-to-end/api/groups.ts +++ b/apps/meteor/tests/end-to-end/api/groups.ts @@ -1200,6 +1200,106 @@ describe('[Groups]', () => { }) .end(done); }); + + describe('inclusive parameter', () => { + let testGroup: IRoom; + let oldestMessage: IMessage; + let middleMessage: IMessage; + let latestMessage: IMessage; + + before(async () => { + const groupRes = await request + .post(api('groups.create')) + .set(credentials) + .send({ name: `inclusive-test-group-${Date.now()}` }); + testGroup = groupRes.body.group; + + // Send messages with small delays to ensure distinct timestamps + const msg1 = await sendMessage({ message: { rid: testGroup._id, msg: 'oldest message' } }); + oldestMessage = msg1.body.message; + + // Small delay to ensure timestamps are different + await new Promise((resolve) => setTimeout(resolve, 50)); + + const msg2 = await sendMessage({ message: { rid: testGroup._id, msg: 'middle message' } }); + middleMessage = msg2.body.message; + + await new Promise((resolve) => setTimeout(resolve, 50)); + + const msg3 = await sendMessage({ message: { rid: testGroup._id, msg: 'latest message' } }); + latestMessage = msg3.body.message; + }); + + after(async () => { + if (testGroup?._id) { + await request.post(api('groups.delete')).set(credentials).send({ roomId: testGroup._id }); + } + }); + + it('should include boundary messages when inclusive=true', async () => { + const res = await request + .get(api('groups.history')) + .set(credentials) + .query({ + roomId: testGroup._id, + oldest: oldestMessage.ts, + latest: latestMessage.ts, + inclusive: 'true', + }) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('messages').that.is.an('array'); + + const messageIds = res.body.messages.map((m: IMessage) => m._id); + expect(messageIds).to.include(oldestMessage._id, 'oldest message should be included'); + expect(messageIds).to.include(latestMessage._id, 'latest message should be included'); + }); + + it('should exclude boundary messages when inclusive=false', async () => { + const res = await request + .get(api('groups.history')) + .set(credentials) + .query({ + roomId: testGroup._id, + oldest: oldestMessage.ts, + latest: latestMessage.ts, + inclusive: 'false', + }) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('messages').that.is.an('array'); + + const messageIds = res.body.messages.map((m: IMessage) => m._id); + expect(messageIds).to.not.include(oldestMessage._id, 'oldest message should be excluded'); + expect(messageIds).to.not.include(latestMessage._id, 'latest message should be excluded'); + // Middle message should still be included if it exists in the range + expect(messageIds).to.include(middleMessage._id, 'middle message should be included'); + }); + + it('should exclude boundary messages by default (no inclusive param)', async () => { + const res = await request + .get(api('groups.history')) + .set(credentials) + .query({ + roomId: testGroup._id, + oldest: oldestMessage.ts, + latest: latestMessage.ts, + }) + .expect('Content-Type', 'application/json') + .expect(200); + + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('messages').that.is.an('array'); + + const messageIds = res.body.messages.map((m: IMessage) => m._id); + expect(messageIds).to.not.include(oldestMessage._id, 'oldest message should be excluded by default'); + expect(messageIds).to.not.include(latestMessage._id, 'latest message should be excluded by default'); + }); + }); }); describe('/groups.archive', () => { diff --git a/packages/rest-typings/src/v1/groups/GroupsHistoryProps.ts b/packages/rest-typings/src/v1/groups/GroupsHistoryProps.ts index 4a056c3d840eb..60ec96875c5b3 100644 --- a/packages/rest-typings/src/v1/groups/GroupsHistoryProps.ts +++ b/packages/rest-typings/src/v1/groups/GroupsHistoryProps.ts @@ -4,7 +4,13 @@ import { withGroupBaseProperties } from './BaseProps'; import type { PaginatedRequest } from '../../helpers/PaginatedRequest'; export type GroupsHistoryProps = PaginatedRequest< - GroupsBaseProps & { latest?: string; oldest?: string; inclusive?: boolean; unreads?: boolean; showThreadMessages?: string } + GroupsBaseProps & { + latest?: string; + oldest?: string; + inclusive?: 'true' | 'false'; + unreads?: 'true' | 'false'; + showThreadMessages?: string; + } >; const groupsHistoryPropsSchema = withGroupBaseProperties({ latest: { From b1925d6220ac2050e208cf252d78e596ccb652ba Mon Sep 17 00:00:00 2001 From: Suryansh Mishra <suryansh.mishra.sm6@gmail.com> Date: Wed, 25 Feb 2026 22:53:26 +0530 Subject: [PATCH 056/108] chore: rename misspelled `useDrowdownVisibility` hook filename (#39036) Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> --- .../room/Header/Omnichannel/QuickActions/QuickActionOptions.tsx | 2 +- .../{useDrowdownVisibility.ts => useDropdownVisibility.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/{useDrowdownVisibility.ts => useDropdownVisibility.ts} (100%) diff --git a/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/QuickActionOptions.tsx b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/QuickActionOptions.tsx index 0e86696253cdb..2c4bb57ca5026 100644 --- a/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/QuickActionOptions.tsx +++ b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/QuickActionOptions.tsx @@ -5,7 +5,7 @@ import { HeaderToolbarAction } from '@rocket.chat/ui-client'; import { memo, useRef } from 'react'; import { useTranslation } from 'react-i18next'; -import { useDropdownVisibility } from './hooks/useDrowdownVisibility'; +import { useDropdownVisibility } from './hooks/useDropdownVisibility'; import type { QuickActionsActionOptions } from '../../../lib/quickActions'; type QuickActionOptionsProps = { diff --git a/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useDrowdownVisibility.ts b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useDropdownVisibility.ts similarity index 100% rename from apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useDrowdownVisibility.ts rename to apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useDropdownVisibility.ts From 29d2454779619c6f3e6b6a08cae799b5ef0ca8c4 Mon Sep 17 00:00:00 2001 From: Amit Kumar Ashutosh <73929517+amitkumarashutosh@users.noreply.github.com> Date: Thu, 26 Feb 2026 00:39:13 +0530 Subject: [PATCH 057/108] chore: add fullOptions to Emoji benchmark fixtures (#39035) --- packages/message-parser/benchmarks/parser.bench.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/message-parser/benchmarks/parser.bench.ts b/packages/message-parser/benchmarks/parser.bench.ts index 14851b1865577..7e41d975bb08f 100644 --- a/packages/message-parser/benchmarks/parser.bench.ts +++ b/packages/message-parser/benchmarks/parser.bench.ts @@ -77,12 +77,12 @@ const categories: BenchCategory[] = [ { name: 'Emoji', fixtures: [ - { name: 'single shortcode', input: ':smile:' }, - { name: 'triple shortcode (BigEmoji)', input: ':smile::heart::rocket:' }, - { name: 'single unicode', input: '😀' }, - { name: 'triple unicode (BigEmoji)', input: '😀🚀🌈' }, - { name: 'in text', input: 'Hello :smile: world :heart: test :rocket: done' }, - { name: 'mixed', input: 'Great job :thumbsup: 🎉 keep going :rocket:' }, + { name: 'single shortcode', input: ':smile:', options: fullOptions }, + { name: 'triple shortcode (BigEmoji)', input: ':smile::heart::rocket:', options: fullOptions }, + { name: 'single unicode', input: '😀', options: fullOptions }, + { name: 'triple unicode (BigEmoji)', input: '😀🚀🌈', options: fullOptions }, + { name: 'in text', input: 'Hello :smile: world :heart: test :rocket: done', options: fullOptions }, + { name: 'mixed', input: 'Great job :thumbsup: 🎉 keep going :rocket:', options: fullOptions }, ], }, { From d1bf2cc675e80403659d388a1fbbdc6f73889dad Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 18:00:11 -0300 Subject: [PATCH 058/108] fix(message-parser): Merge blockquotes separated by empty `>` lines into a single block (#39062) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ggazzo <5263975+ggazzo@users.noreply.github.com> --- .changeset/fix-blockquote-empty-lines.md | 5 +++++ packages/message-parser/src/grammar.pegjs | 4 +++- .../message-parser/tests/blockquotes.test.ts | 16 ++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-blockquote-empty-lines.md diff --git a/.changeset/fix-blockquote-empty-lines.md b/.changeset/fix-blockquote-empty-lines.md new file mode 100644 index 0000000000000..b3b463f3913a6 --- /dev/null +++ b/.changeset/fix-blockquote-empty-lines.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/message-parser': patch +--- + +Fixed blockquotes with empty lines between paragraphs not rendering as a single blockquote. Lines like `> ` or `>` (empty quote lines) are now treated as part of the surrounding blockquote rather than breaking it into separate quotes. diff --git a/packages/message-parser/src/grammar.pegjs b/packages/message-parser/src/grammar.pegjs index f197b80be41a1..d48c2e7d257d7 100644 --- a/packages/message-parser/src/grammar.pegjs +++ b/packages/message-parser/src/grammar.pegjs @@ -76,7 +76,9 @@ Blocks */ Blockquote = b:BlockquoteLine+ { return quote(b); } -BlockquoteLine = ">" [ \t]* @Paragraph +BlockquoteLine + = ">" [ \t]* EndOfLine { return paragraph([plain('')]); } + / ">" [ \t]* @Paragraph /** * Block Spoiler diff --git a/packages/message-parser/tests/blockquotes.test.ts b/packages/message-parser/tests/blockquotes.test.ts index b42368daacc64..f20fc33462116 100644 --- a/packages/message-parser/tests/blockquotes.test.ts +++ b/packages/message-parser/tests/blockquotes.test.ts @@ -26,6 +26,22 @@ As Rocket Cat said: `.trim(), [paragraph([plain('As Rocket Cat said:')]), quote([paragraph([plain('meowww')]), paragraph([plain('grr.')])])], ], + [ + ` +> meowww +> +> grr. +`.trim(), + [quote([paragraph([plain('meowww')]), paragraph([plain('')]), paragraph([plain('grr.')])])], + ], + [ + ` +> meowww +> +> grr. +`.trim(), + [quote([paragraph([plain('meowww')]), paragraph([plain('')]), paragraph([plain('grr.')])])], + ], ])('parses %p', (input, output) => { expect(parse(input)).toMatchObject(output); }); From 1b7d4d8fde47725e9606804de6e240de5fe18757 Mon Sep 17 00:00:00 2001 From: Suryansh Mishra <suryansh.mishra.sm6@gmail.com> Date: Thu, 26 Feb 2026 02:31:43 +0530 Subject: [PATCH 059/108] chore: migrate presence endpoints to new API pattern (#38882) Co-authored-by: Guilherme Gazzo <guilhermegazzo@gmail.com> --- .../refactor-presence-api-chained-pattern.md | 5 ++ apps/meteor/app/api/server/v1/presence.ts | 60 +++++++++++++++---- 2 files changed, 53 insertions(+), 12 deletions(-) create mode 100644 .changeset/refactor-presence-api-chained-pattern.md diff --git a/.changeset/refactor-presence-api-chained-pattern.md b/.changeset/refactor-presence-api-chained-pattern.md new file mode 100644 index 0000000000000..cec1816fce98b --- /dev/null +++ b/.changeset/refactor-presence-api-chained-pattern.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +Migrates `presence.getConnections` and `presence.enableBroadcast` REST API endpoints from legacy `addRoute` pattern to the new chained `.get()`/`.post()` API pattern with typed response schemas. diff --git a/apps/meteor/app/api/server/v1/presence.ts b/apps/meteor/app/api/server/v1/presence.ts index 019137569f610..28e83ecb68f80 100644 --- a/apps/meteor/app/api/server/v1/presence.ts +++ b/apps/meteor/app/api/server/v1/presence.ts @@ -1,27 +1,63 @@ import { Presence } from '@rocket.chat/core-services'; +import { ajv, validateUnauthorizedErrorResponse, validateForbiddenErrorResponse } from '@rocket.chat/rest-typings'; import { API } from '../api'; -API.v1.addRoute( +API.v1.get( 'presence.getConnections', - { authRequired: true, permissionsRequired: ['manage-user-status'] }, { - async get() { - const result = await Presence.getConnectionCount(); - - return API.v1.success(result); + authRequired: true, + permissionsRequired: ['manage-user-status'], + response: { + 200: ajv.compile<{ current: number; max: number; success: true }>({ + type: 'object', + properties: { + current: { type: 'number' }, + max: { type: 'number' }, + success: { + type: 'boolean', + enum: [true], + }, + }, + required: ['current', 'max', 'success'], + additionalProperties: false, + }), + 401: validateUnauthorizedErrorResponse, + 403: validateForbiddenErrorResponse, }, }, + async function action() { + const result = await Presence.getConnectionCount(); + + return API.v1.success(result); + }, ); -API.v1.addRoute( +API.v1.post( 'presence.enableBroadcast', - { authRequired: true, permissionsRequired: ['manage-user-status'], twoFactorRequired: true }, { - async post() { - await Presence.toggleBroadcast(true); - - return API.v1.success(); + authRequired: true, + permissionsRequired: ['manage-user-status'], + twoFactorRequired: true, + response: { + 200: ajv.compile<{ success: true }>({ + type: 'object', + properties: { + success: { + type: 'boolean', + enum: [true], + }, + }, + required: ['success'], + additionalProperties: false, + }), + 401: validateUnauthorizedErrorResponse, + 403: validateForbiddenErrorResponse, }, }, + async function action() { + await Presence.toggleBroadcast(true); + + return API.v1.success({}); + }, ); From 46f38c920a2ebc33652fd0f1f2eb5fde2acfc9c8 Mon Sep 17 00:00:00 2001 From: Suryansh Mishra <suryansh.mishra.sm6@gmail.com> Date: Thu, 26 Feb 2026 02:33:40 +0530 Subject: [PATCH 060/108] chore: migrate ldap endpoints to new API pattern (#38883) Co-authored-by: Guilherme Gazzo <guilhermegazzo@gmail.com> --- .../refactor-ldap-api-chained-pattern.md | 5 + apps/meteor/app/api/server/v1/ldap.ts | 110 +++++++++++------- packages/rest-typings/src/index.ts | 1 + 3 files changed, 73 insertions(+), 43 deletions(-) create mode 100644 .changeset/refactor-ldap-api-chained-pattern.md diff --git a/.changeset/refactor-ldap-api-chained-pattern.md b/.changeset/refactor-ldap-api-chained-pattern.md new file mode 100644 index 0000000000000..e402e8609cb46 --- /dev/null +++ b/.changeset/refactor-ldap-api-chained-pattern.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +Migrates `ldap.testConnection` and `ldap.testSearch` REST API endpoints from legacy `addRoute` pattern to the new chained `.post()` API pattern with typed response schemas and AJV body validation (replacing Meteor `check()`). diff --git a/apps/meteor/app/api/server/v1/ldap.ts b/apps/meteor/app/api/server/v1/ldap.ts index 3f9a2c29deded..efbf9a898d02c 100644 --- a/apps/meteor/app/api/server/v1/ldap.ts +++ b/apps/meteor/app/api/server/v1/ldap.ts @@ -1,62 +1,86 @@ import { LDAP } from '@rocket.chat/core-services'; -import { Match, check } from 'meteor/check'; +import { ajv, isLdapTestSearch, validateUnauthorizedErrorResponse, validateForbiddenErrorResponse } from '@rocket.chat/rest-typings'; import { SystemLogger } from '../../../../server/lib/logger/system'; import { settings } from '../../../settings/server'; import { API } from '../api'; -API.v1.addRoute( +const messageResponseSchema = { + type: 'object' as const, + properties: { + message: { type: 'string' as const }, + success: { + type: 'boolean' as const, + enum: [true] as const, + }, + }, + required: ['message', 'success'] as const, + additionalProperties: false, +}; + +API.v1.post( 'ldap.testConnection', - { authRequired: true, permissionsRequired: ['test-admin-options'] }, { - async post() { - if (!this.userId) { - throw new Error('error-invalid-user'); - } - - if (settings.get<boolean>('LDAP_Enable') !== true) { - throw new Error('LDAP_disabled'); - } - - try { - await LDAP.testConnection(); - } catch (err) { - SystemLogger.error({ err }); - throw new Error('Connection_failed'); - } - - return API.v1.success({ - message: 'LDAP_Connection_successful' as const, - }); + authRequired: true, + permissionsRequired: ['test-admin-options'], + response: { + 200: ajv.compile<{ message: string; success: true }>(messageResponseSchema), + 401: validateUnauthorizedErrorResponse, + 403: validateForbiddenErrorResponse, }, }, + async function action() { + if (!this.userId) { + throw new Error('error-invalid-user'); + } + + if (settings.get<boolean>('LDAP_Enable') !== true) { + throw new Error('LDAP_disabled'); + } + + try { + await LDAP.testConnection(); + } catch (err) { + SystemLogger.error({ err }); + throw new Error('Connection_failed'); + } + + return API.v1.success({ + message: 'LDAP_Connection_successful' as const, + }); + }, ); -API.v1.addRoute( +API.v1.post( 'ldap.testSearch', - { authRequired: true, permissionsRequired: ['test-admin-options'] }, { - async post() { - check( - this.bodyParams, - Match.ObjectIncluding({ - username: String, - }), - ); - - if (!this.userId) { - throw new Error('error-invalid-user'); - } - - if (settings.get('LDAP_Enable') !== true) { - throw new Error('LDAP_disabled'); - } + authRequired: true, + permissionsRequired: ['test-admin-options'], + body: isLdapTestSearch, + response: { + 200: ajv.compile<{ message: string; success: true }>(messageResponseSchema), + 401: validateUnauthorizedErrorResponse, + 403: validateForbiddenErrorResponse, + }, + }, + async function action() { + if (!this.userId) { + throw new Error('error-invalid-user'); + } + if (settings.get<boolean>('LDAP_Enable') !== true) { + throw new Error('LDAP_disabled'); + } + + try { await LDAP.testSearch(this.bodyParams.username); + } catch (err) { + SystemLogger.error({ err }); + throw new Error('LDAP_search_failed'); + } - return API.v1.success({ - message: 'LDAP_User_Found' as const, - }); - }, + return API.v1.success({ + message: 'LDAP_User_Found' as const, + }); }, ); diff --git a/packages/rest-typings/src/index.ts b/packages/rest-typings/src/index.ts index 76a36bffc4b45..3edd726658537 100644 --- a/packages/rest-typings/src/index.ts +++ b/packages/rest-typings/src/index.ts @@ -230,6 +230,7 @@ export * from './helpers/ReplacePlaceholders'; export * from './helpers/WithItemCount'; export * from './v1/emojiCustom'; export * from './v1/instances'; +export * from './v1/ldap'; export * from './v1/users'; export * from './v1/users/UsersSetAvatarParamsPOST'; export * from './v1/users/UsersSetPreferenceParamsPOST'; From a71273834e7a66bb9db4170faa3008215a951022 Mon Sep 17 00:00:00 2001 From: Harmeet Kour <87123067+Harmeet221@users.noreply.github.com> Date: Thu, 26 Feb 2026 02:36:04 +0530 Subject: [PATCH 061/108] test: Add automated tests for custom fields functionality. (#38743) Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: Aleksander Nicacio da Silva <aleksander.silva@rocket.chat> Co-authored-by: juliajforesti <juliajforesti@gmail.com> --- .../omnichannel-custom-field-usage.spec.ts | 154 ++++++++++++++++++ .../fragments/edit-room-flextab.ts | 4 + .../omnichannel/omnichannel-info.ts | 12 ++ .../e2e/utils/omnichannel/custom-field.ts | 17 ++ .../src/components/CustomFieldsForm.tsx | 20 ++- 5 files changed, 199 insertions(+), 8 deletions(-) create mode 100644 apps/meteor/tests/e2e/omnichannel/omnichannel-custom-field-usage.spec.ts diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-custom-field-usage.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-custom-field-usage.spec.ts new file mode 100644 index 0000000000000..ff9e1ff384e74 --- /dev/null +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-custom-field-usage.spec.ts @@ -0,0 +1,154 @@ +import { faker } from '@faker-js/faker'; + +import { createFakeVisitor } from '../../mocks/data'; +import { Users } from '../fixtures/userStates'; +import { HomeOmnichannel } from '../page-objects'; +import { createAgent } from '../utils/omnichannel/agents'; +import { createCustomField, setVisitorCustomFieldValue } from '../utils/omnichannel/custom-field'; +import { createManager } from '../utils/omnichannel/managers'; +import { createConversation } from '../utils/omnichannel/rooms'; +import { test, expect } from '../utils/test'; + +const visitor = createFakeVisitor(); + +test.use({ storageState: Users.user1.state }); + +test.describe.serial('OC - Custom fields usage, scope : room and visitor', () => { + let poHomeChannel: HomeOmnichannel; + + const roomCustomFieldLabel = `room_cf_${faker.string.alpha(8)}`; + const roomCustomFieldName = roomCustomFieldLabel; + const roomCustomFieldValue = faker.lorem.words(3); + + const visitorCustomFieldLabel = `visitor_cf_${faker.string.alpha(8)}`; + const visitorCustomFieldName = visitorCustomFieldLabel; + const visitorCustomFieldValue = faker.lorem.words(3); + const visitorToken = faker.string.uuid(); + + let agent: Awaited<ReturnType<typeof createAgent>>; + let manager: Awaited<ReturnType<typeof createManager>>; + let conversation: Awaited<ReturnType<typeof createConversation>>; + let roomCustomField: Awaited<ReturnType<typeof createCustomField>>; + let visitorCustomField: Awaited<ReturnType<typeof createCustomField>>; + + test.beforeAll('Set up agent, manager and custom fields', async ({ api }) => { + [agent, manager] = await Promise.all([createAgent(api, 'user1'), createManager(api, 'user1')]); + + [roomCustomField, visitorCustomField, conversation] = await Promise.all([ + createCustomField(api, { + field: roomCustomFieldLabel, + label: roomCustomFieldName, + scope: 'room', + }), + createCustomField(api, { + field: visitorCustomFieldLabel, + label: visitorCustomFieldName, + scope: 'visitor', + }), + createConversation(api, { + visitorName: visitor.name, + agentId: 'user1', + visitorToken, + }), + ]); + + await setVisitorCustomFieldValue(api, { + token: visitorToken, + customFieldId: visitorCustomField.customField._id, + value: visitorCustomFieldValue, + }); + }); + + test.beforeEach(async ({ page }) => { + poHomeChannel = new HomeOmnichannel(page); + await page.goto('/'); + await poHomeChannel.waitForHome(); + }); + + test.afterAll('Remove agent, manager, custom fields and conversation', async () => { + await Promise.all([agent.delete(), manager.delete(), roomCustomField.delete(), visitorCustomField.delete(), conversation.delete()]); + }); + + test('Should be allowed to set room custom field for a conversation', async () => { + await test.step('Agent opens the conversation', async () => { + await poHomeChannel.sidebar.getSidebarItemByName(visitor.name).click(); + }); + + await test.step('Agent opens edit room', async () => { + await poHomeChannel.roomInfo.waitForDisplay(); + await poHomeChannel.roomInfo.btnEdit.click(); + await poHomeChannel.editRoomInfo.waitForDisplay(); + }); + + await test.step('Agent fills room custom field and saves', async () => { + await poHomeChannel.editRoomInfo.getRoomCustomField(roomCustomFieldLabel).fill(roomCustomFieldValue); + await poHomeChannel.editRoomInfo.btnSave.click(); + }); + + await test.step('Custom field should be updated successfully', async () => { + await poHomeChannel.roomInfo.btnEdit.click(); + await poHomeChannel.editRoomInfo.waitForDisplay(); + await expect(poHomeChannel.editRoomInfo.getRoomCustomField(roomCustomFieldLabel)).toHaveValue(roomCustomFieldValue); + }); + }); + + test('Should be allowed to update existing room custom field', async () => { + const updatedValue = faker.lorem.words(2); + + await test.step('Agent opens the conversation', async () => { + await poHomeChannel.sidebar.getSidebarItemByName(visitor.name).click(); + }); + + await test.step('Agent opens edit room and updates custom field', async () => { + await poHomeChannel.roomInfo.waitForDisplay(); + await poHomeChannel.roomInfo.btnEdit.click(); + await poHomeChannel.editRoomInfo.waitForDisplay(); + await poHomeChannel.editRoomInfo.getRoomCustomField(roomCustomFieldLabel).fill(updatedValue); + await poHomeChannel.editRoomInfo.btnSave.click(); + }); + + await test.step('Room Information displays the updated custom field value', async () => { + await poHomeChannel.roomInfo.btnEdit.click(); + await poHomeChannel.editRoomInfo.waitForDisplay(); + await expect(poHomeChannel.editRoomInfo.getRoomCustomField(roomCustomFieldLabel)).toHaveValue(updatedValue); + }); + }); + + test('Should verify that the visitor custom field is set', async () => { + await test.step('Agent opens the conversation', async () => { + await poHomeChannel.sidebar.getSidebarItemByName(visitor.name).click(); + }); + + await test.step('Agent opens Contact Information', async () => { + await poHomeChannel.roomToolbar.openContactInfo(); + await poHomeChannel.contacts.contactInfo.waitForDisplay(); + }); + + await test.step('Assert custom field is set successfully', async () => { + await expect(poHomeChannel.contacts.contactInfo.getInfoByValue(visitorCustomFieldValue)).toBeVisible(); + }); + }); + + test('Should be allowed to update existing visitor custom field', async () => { + const updatedVisitorCustomFieldValue = faker.lorem.words(2); + + await test.step('Agent opens the conversation', async () => { + await poHomeChannel.sidebar.getSidebarItemByName(visitor.name).click(); + }); + + await test.step('Agent opens Contact Information', async () => { + await poHomeChannel.roomToolbar.openContactInfo(); + await poHomeChannel.contacts.contactInfo.waitForDisplay(); + }); + + await test.step('Agent clicks edit and updates visitor custom field', async () => { + await poHomeChannel.contacts.contactInfo.btnEdit.click(); + await poHomeChannel.contacts.contactInfo.getVisitorCustomField(visitorCustomFieldLabel).fill(updatedVisitorCustomFieldValue); + await poHomeChannel.contacts.contactInfo.btnSave.click(); + }); + + await test.step('Assert custom field is updated successfully', async () => { + await expect(poHomeChannel.contacts.contactInfo.getInfoByValue(updatedVisitorCustomFieldValue)).toBeVisible(); + }); + }); +}); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/edit-room-flextab.ts b/apps/meteor/tests/e2e/page-objects/fragments/edit-room-flextab.ts index bfc88661991bb..8fa085c271e89 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/edit-room-flextab.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/edit-room-flextab.ts @@ -84,4 +84,8 @@ export class OmnichannelEditRoomFlexTab extends EditRoomFlexTab { get inputTags(): Locator { return this.root.getByRole('textbox', { name: 'Select an option' }); } + + getRoomCustomField(label: string): Locator { + return this.root.getByRole('group', { name: 'Custom Fields' }).getByLabel(label); + } } diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-info.ts b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-info.ts index d911a81305d00..5d6a877e71022 100644 --- a/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-info.ts +++ b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-info.ts @@ -39,6 +39,18 @@ export class OmnichannelContactInfo extends FlexTab { return this.root.getByRole('button', { name: 'See conflicts' }); } + private get customFieldsGroup() { + return this.root.getByRole('group', { name: 'Custom Fields' }); + } + + getInfoByValue(value: string): Locator { + return this.root.getByText(value, { exact: true }); + } + + getVisitorCustomField(label: string): Locator { + return this.customFieldsGroup.getByLabel(label); + } + async solveConflict(field: string, value: string) { await this.btnSeeConflicts.click(); await this.contactReviewModal.solveConfirmation(field, value); diff --git a/apps/meteor/tests/e2e/utils/omnichannel/custom-field.ts b/apps/meteor/tests/e2e/utils/omnichannel/custom-field.ts index fca092c4133a5..7aa2f914f6ef3 100644 --- a/apps/meteor/tests/e2e/utils/omnichannel/custom-field.ts +++ b/apps/meteor/tests/e2e/utils/omnichannel/custom-field.ts @@ -10,6 +10,23 @@ export const removeCustomField = (api: BaseTest['api'], id: string) => { }); }; +export const setVisitorCustomFieldValue = async ( + api: BaseTest['api'], + params: { token: string; customFieldId: string; value: string; overwrite?: boolean }, +) => { + const response = await api.post('/livechat/custom.field', { + token: params.token, + key: params.customFieldId, + value: params.value, + overwrite: params.overwrite ?? true, + }); + if (!response.ok()) { + throw new Error(`Failed to set visitor custom field [http status: ${response.status()}]`); + } + const { field } = await response.json(); + return { response, field }; +}; + export const createCustomField = async (api: BaseTest['api'], overwrites: Partial<CustomField>) => { const response = await api.post('/livechat/custom-fields.save', { customFieldId: null, diff --git a/packages/ui-client/src/components/CustomFieldsForm.tsx b/packages/ui-client/src/components/CustomFieldsForm.tsx index bd57e573e6657..37d49df3e61d2 100644 --- a/packages/ui-client/src/components/CustomFieldsForm.tsx +++ b/packages/ui-client/src/components/CustomFieldsForm.tsx @@ -1,6 +1,6 @@ import type { CustomFieldMetadata } from '@rocket.chat/core-typings'; import type { SelectOption } from '@rocket.chat/fuselage'; -import { Field, FieldLabel, FieldRow, FieldError, Select, TextInput } from '@rocket.chat/fuselage'; +import { Field, FieldLabel, FieldRow, FieldError, Select, TextInput, FieldGroup } from '@rocket.chat/fuselage'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useCallback, useId, useMemo } from 'react'; import type { Control, FieldValues, FieldError as RHFFieldError } from 'react-hook-form'; @@ -104,10 +104,14 @@ const CustomField = <T extends FieldValues>({ }; // eslint-disable-next-line react/no-multi-comp -export const CustomFieldsForm = <T extends FieldValues>({ formName, formControl, metadata }: CustomFieldFormProps<T>) => ( - <> - {metadata.map(({ name: fieldName, label, ...props }) => ( - <CustomField key={fieldName} name={`${formName}.${fieldName}`} control={formControl} label={label ?? fieldName} {...props} /> - ))} - </> -); +export const CustomFieldsForm = <T extends FieldValues>({ formName, formControl, metadata }: CustomFieldFormProps<T>) => { + const { t } = useTranslation(); + + return ( + <FieldGroup aria-label={t('Custom_Fields')}> + {metadata.map(({ name: fieldName, label, ...props }) => ( + <CustomField key={fieldName} name={`${formName}.${fieldName}`} control={formControl} label={label ?? fieldName} {...props} /> + ))} + </FieldGroup> + ); +}; From fea32db761c0ca289b1d69dd260026605bfd2fd6 Mon Sep 17 00:00:00 2001 From: "khizar (RinX)" <109973520+Khizarshah01@users.noreply.github.com> Date: Thu, 26 Feb 2026 02:38:18 +0530 Subject: [PATCH 062/108] chore: remove 'as any' usage by applying required Type safety (#39061) --- .../components/CategoryFilter/CategoryDropDown.tsx | 2 +- .../components/RadioDropDown/RadioDownAnchor.tsx | 6 +++--- .../marketplace/components/RadioDropDown/RadioDropDown.tsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/meteor/client/views/marketplace/components/CategoryFilter/CategoryDropDown.tsx b/apps/meteor/client/views/marketplace/components/CategoryFilter/CategoryDropDown.tsx index 0a315e8266308..f8b8962518a29 100644 --- a/apps/meteor/client/views/marketplace/components/CategoryFilter/CategoryDropDown.tsx +++ b/apps/meteor/client/views/marketplace/components/CategoryFilter/CategoryDropDown.tsx @@ -36,7 +36,7 @@ const CategoryDropDown = ({ categories, onSelected, selectedCategories, ...props <> <CategoryDropDownAnchor ref={reference} - onClick={toggleCollapsed as any} + onClick={() => toggleCollapsed()} selectedCategoriesCount={selectedCategories.length} {...props} /> diff --git a/apps/meteor/client/views/marketplace/components/RadioDropDown/RadioDownAnchor.tsx b/apps/meteor/client/views/marketplace/components/RadioDropDown/RadioDownAnchor.tsx index 6b3bb5e4c28fe..a12f40bd16126 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, SetStateAction } from 'react'; +import type { ComponentProps } from 'react'; import { forwardRef } from 'react'; import type { RadioDropDownGroup } from '../../definitions/RadioDropDownDefinitions'; type RadioDropdownAnchorProps = { - onClick: (forcedValue?: SetStateAction<boolean> | undefined) => void; + onClick: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void; group: RadioDropDownGroup; } & Omit<ComponentProps<typeof Button>, 'onClick'>; @@ -17,7 +17,7 @@ const RadioDownAnchor = forwardRef<HTMLElement, RadioDropdownAnchorProps>(functi <Box is='button' ref={ref} - onClick={onClick as any} + onClick={onClick} alignItems='center' bg='light' borderColor='light' diff --git a/apps/meteor/client/views/marketplace/components/RadioDropDown/RadioDropDown.tsx b/apps/meteor/client/views/marketplace/components/RadioDropDown/RadioDropDown.tsx index 3e69e431cb74a..7b5a738302257 100644 --- a/apps/meteor/client/views/marketplace/components/RadioDropDown/RadioDropDown.tsx +++ b/apps/meteor/client/views/marketplace/components/RadioDropDown/RadioDropDown.tsx @@ -30,7 +30,7 @@ const RadioDropDown = ({ group, onSelected, ...props }: RadioDropDownProps & Com return ( <> - <RadioDropDownAnchor ref={reference} group={group} onClick={toggleCollapsed as any} {...props} /> + <RadioDropDownAnchor ref={reference} group={group} onClick={() => toggleCollapsed()} {...props} /> {collapsed && ( <DropDownListWrapper ref={reference} onClose={onClose}> <RadioButtonList group={group} onSelected={onSelected} /> From 96b2a45461012d9dd63f954c59311410fd3f4c53 Mon Sep 17 00:00:00 2001 From: Sandra Nymark-Brand <143543945+sandranymark@users.noreply.github.com> Date: Thu, 26 Feb 2026 01:37:36 +0100 Subject: [PATCH 063/108] i18n: Update inline error messages in registration form (#36442) Co-authored-by: Douglas Fabris <devfabris@gmail.com> --- apps/meteor/tests/e2e/register.spec.ts | 2 +- packages/i18n/src/locales/af.i18n.json | 2 -- packages/i18n/src/locales/ar.i18n.json | 2 -- packages/i18n/src/locales/az.i18n.json | 2 -- packages/i18n/src/locales/be-BY.i18n.json | 2 -- packages/i18n/src/locales/bg.i18n.json | 2 -- packages/i18n/src/locales/bs.i18n.json | 2 -- packages/i18n/src/locales/ca.i18n.json | 2 -- packages/i18n/src/locales/cs.i18n.json | 2 -- packages/i18n/src/locales/cy.i18n.json | 2 -- packages/i18n/src/locales/da.i18n.json | 2 -- packages/i18n/src/locales/de-AT.i18n.json | 2 -- packages/i18n/src/locales/de.i18n.json | 2 -- packages/i18n/src/locales/el.i18n.json | 2 -- packages/i18n/src/locales/en.i18n.json | 4 ++-- packages/i18n/src/locales/eo.i18n.json | 2 -- packages/i18n/src/locales/es.i18n.json | 2 -- packages/i18n/src/locales/fa.i18n.json | 2 -- packages/i18n/src/locales/fi.i18n.json | 2 -- packages/i18n/src/locales/fr.i18n.json | 2 -- packages/i18n/src/locales/he.i18n.json | 1 - packages/i18n/src/locales/hi-IN.i18n.json | 2 -- packages/i18n/src/locales/hr.i18n.json | 2 -- packages/i18n/src/locales/hu.i18n.json | 2 -- packages/i18n/src/locales/id.i18n.json | 2 -- packages/i18n/src/locales/it.i18n.json | 2 -- packages/i18n/src/locales/ja.i18n.json | 2 -- packages/i18n/src/locales/ka-GE.i18n.json | 2 -- packages/i18n/src/locales/km.i18n.json | 2 -- packages/i18n/src/locales/ko.i18n.json | 2 -- packages/i18n/src/locales/ku.i18n.json | 2 -- packages/i18n/src/locales/lo.i18n.json | 2 -- packages/i18n/src/locales/lt.i18n.json | 2 -- packages/i18n/src/locales/lv.i18n.json | 2 -- packages/i18n/src/locales/mn.i18n.json | 2 -- packages/i18n/src/locales/ms-MY.i18n.json | 2 -- packages/i18n/src/locales/nb.i18n.json | 2 -- packages/i18n/src/locales/nl.i18n.json | 2 -- packages/i18n/src/locales/nn.i18n.json | 2 -- packages/i18n/src/locales/pl.i18n.json | 2 -- packages/i18n/src/locales/pt-BR.i18n.json | 2 -- packages/i18n/src/locales/pt.i18n.json | 2 -- packages/i18n/src/locales/ro.i18n.json | 2 -- packages/i18n/src/locales/ru.i18n.json | 2 -- packages/i18n/src/locales/sk-SK.i18n.json | 2 -- packages/i18n/src/locales/sl-SI.i18n.json | 2 -- packages/i18n/src/locales/sq.i18n.json | 2 -- packages/i18n/src/locales/sr.i18n.json | 2 -- packages/i18n/src/locales/sv.i18n.json | 2 -- packages/i18n/src/locales/ta-IN.i18n.json | 2 -- packages/i18n/src/locales/th-TH.i18n.json | 2 -- packages/i18n/src/locales/tr.i18n.json | 2 -- packages/i18n/src/locales/ug.i18n.json | 1 - packages/i18n/src/locales/uk.i18n.json | 2 -- packages/i18n/src/locales/vi-VN.i18n.json | 2 -- packages/i18n/src/locales/zh-HK.i18n.json | 2 -- packages/i18n/src/locales/zh-TW.i18n.json | 2 -- packages/i18n/src/locales/zh.i18n.json | 2 -- packages/web-ui-registration/src/RegisterForm.tsx | 4 ++-- 59 files changed, 5 insertions(+), 115 deletions(-) diff --git a/apps/meteor/tests/e2e/register.spec.ts b/apps/meteor/tests/e2e/register.spec.ts index 1cd36d94c355b..eb081b62bb708 100644 --- a/apps/meteor/tests/e2e/register.spec.ts +++ b/apps/meteor/tests/e2e/register.spec.ts @@ -162,7 +162,7 @@ test.describe.parallel('register', () => { await poRegistration.inputPasswordConfirm.fill('P@ssw0rd1234.!'); await poRegistration.btnRegister.click(); - await expect(page.getByRole('alert').filter({ hasText: 'Email already exists' })).toBeVisible(); + await expect(page.getByRole('alert').filter({ hasText: 'Email already in use' })).toBeVisible(); }); }); }); diff --git a/packages/i18n/src/locales/af.i18n.json b/packages/i18n/src/locales/af.i18n.json index b549e4e385ae2..591808fb2fb14 100644 --- a/packages/i18n/src/locales/af.i18n.json +++ b/packages/i18n/src/locales/af.i18n.json @@ -2596,7 +2596,6 @@ "registration.component.form.confirmPassword": "Bevestig jou wagwoord", "registration.component.form.divider": "of", "registration.component.form.email": "e-pos", - "registration.component.form.emailAlreadyExists": "E-pos bestaan ​​reeds", "registration.component.form.emailOrUsername": "E-pos of gebruikersnaam", "registration.component.form.invalidConfirmPass": "Die wagwoord bevestiging pas nie by die wagwoord nie", "registration.component.form.invalidEmail": "Die ingevoerde e-pos is ongeldig", @@ -2606,7 +2605,6 @@ "registration.component.form.reasonToJoin": "Rede om deel te neem", "registration.component.form.sendConfirmationEmail": "Stuur bevestiging e-pos", "registration.component.form.submit": "Indien", - "registration.component.form.userAlreadyExist": "Gebruikersnaam bestaan ​​reeds. Probeer asseblief 'n ander gebruikersnaam.", "registration.component.form.username": "Gebruikersnaam", "registration.component.form.usernameAlreadyExists": "Gebruikersnaam bestaan ​​reeds. Probeer asseblief 'n ander gebruikersnaam.", "registration.component.login": "Teken aan", diff --git a/packages/i18n/src/locales/ar.i18n.json b/packages/i18n/src/locales/ar.i18n.json index 0c1a41d23c515..18a47db364a56 100644 --- a/packages/i18n/src/locales/ar.i18n.json +++ b/packages/i18n/src/locales/ar.i18n.json @@ -4584,7 +4584,6 @@ "registration.component.form.confirmation": "التأكيد", "registration.component.form.divider": "أو", "registration.component.form.email": "البريد الإلكتروني", - "registration.component.form.emailAlreadyExists": "سبق وجود البريد الإلكتروني", "registration.component.form.emailOrUsername": "البريد الإلكتروني أو اسم المستخدم", "registration.component.form.invalidConfirmPass": "تأكيد كلمة السر لا تطابق كلمة السر", "registration.component.form.invalidEmail": "البريد الإلكتروني الذي تم إدخاله غير صالح", @@ -4596,7 +4595,6 @@ "registration.component.form.requiredField": "هذا الحقل مطلوب", "registration.component.form.sendConfirmationEmail": "إرسال رسالة تأكيد", "registration.component.form.submit": "إرسال", - "registration.component.form.userAlreadyExist": "سبق وجود اسم المستخدم. تُرجى تجربة اسم مستخدم آخر.", "registration.component.form.username": "اسم المستخدم", "registration.component.form.usernameAlreadyExists": "سبق وجود اسم المستخدم. تُرجى تجربة اسم مستخدم آخر.", "registration.component.login": "تسجيل الدخول", diff --git a/packages/i18n/src/locales/az.i18n.json b/packages/i18n/src/locales/az.i18n.json index f9b65a7e21865..b7aa18d3e13c0 100644 --- a/packages/i18n/src/locales/az.i18n.json +++ b/packages/i18n/src/locales/az.i18n.json @@ -2599,7 +2599,6 @@ "registration.component.form.confirmPassword": "Şifrənizi təsdiqləyin", "registration.component.form.divider": "və ya", "registration.component.form.email": "E-poçt", - "registration.component.form.emailAlreadyExists": "Elektron poçt ünvanı artıq mövcuddur", "registration.component.form.emailOrUsername": "E-poçt və ya istifadəçi adı", "registration.component.form.invalidConfirmPass": "Şifrənin təsdiqlənməsi şifrə uyğun gəlmir", "registration.component.form.invalidEmail": "Girilən e-poçt etibarsızdır", @@ -2609,7 +2608,6 @@ "registration.component.form.reasonToJoin": "Qoşulma səbəbi", "registration.component.form.sendConfirmationEmail": "Təsdiq e-poçt göndər", "registration.component.form.submit": "təqdim", - "registration.component.form.userAlreadyExist": "İstifadəçi adı artıq mövcuddur. Başqa bir istifadəçi adı cəhd edin.", "registration.component.form.username": "İstifadəçi adı", "registration.component.form.usernameAlreadyExists": "İstifadəçi adı artıq mövcuddur. Başqa bir istifadəçi adı cəhd edin.", "registration.component.login": "Daxil ol", diff --git a/packages/i18n/src/locales/be-BY.i18n.json b/packages/i18n/src/locales/be-BY.i18n.json index cea9625fc8c18..96c4922a5744f 100644 --- a/packages/i18n/src/locales/be-BY.i18n.json +++ b/packages/i18n/src/locales/be-BY.i18n.json @@ -2621,7 +2621,6 @@ "registration.component.form.confirmPassword": "Пацвердзіць пароль", "registration.component.form.divider": "або", "registration.component.form.email": "E-mail", - "registration.component.form.emailAlreadyExists": "E-mail ўжо існуе", "registration.component.form.emailOrUsername": "Email або імя карыстальніка", "registration.component.form.invalidConfirmPass": "Пацвярджэнне пароля не супадае пароль", "registration.component.form.invalidEmail": "Адрас электроннай пошты ня дзейнічае", @@ -2632,7 +2631,6 @@ "registration.component.form.register": "Рэгістрацыя новага карыстальніка", "registration.component.form.sendConfirmationEmail": "Адправіць па электроннай пошце пацвярджэнне", "registration.component.form.submit": "прадставіць", - "registration.component.form.userAlreadyExist": "Імя карыстальніка ўжо існуе. Калі ласка, паспрабуйце іншую назву.", "registration.component.form.username": "імя карыстальніка", "registration.component.form.usernameAlreadyExists": "Імя карыстальніка ўжо існуе. Калі ласка, паспрабуйце іншую назву.", "registration.component.login": "Увайсці", diff --git a/packages/i18n/src/locales/bg.i18n.json b/packages/i18n/src/locales/bg.i18n.json index eb435c8a99968..6ccb1440e8cdd 100644 --- a/packages/i18n/src/locales/bg.i18n.json +++ b/packages/i18n/src/locales/bg.i18n.json @@ -2591,7 +2591,6 @@ "registration.component.form.confirmPassword": "Потвърдите паролата", "registration.component.form.divider": "или", "registration.component.form.email": "Електрона поща", - "registration.component.form.emailAlreadyExists": "Електроната поща вече съсществува", "registration.component.form.invalidConfirmPass": "Потвърждението на паролата не съвпада с паролата", "registration.component.form.invalidEmail": "Въведеният имейл адрес е невалиден", "registration.component.form.invalidEmailDomain":"Въведеният имейл адрес има невалиден домейн", @@ -2600,7 +2599,6 @@ "registration.component.form.reasonToJoin": "Причина за присъединяване", "registration.component.form.sendConfirmationEmail": "Изпратете имейл за потвърждение", "registration.component.form.submit": "Подайте", - "registration.component.form.userAlreadyExist": "Потребителското име вече съществува. Моля, опитайте с друго потребителско име.", "registration.component.form.username": "Потребителско име", "registration.component.form.usernameAlreadyExists": "Потребителското име вече съществува. Моля, опитайте с друго потребителско име.", "registration.component.login": "Влез", diff --git a/packages/i18n/src/locales/bs.i18n.json b/packages/i18n/src/locales/bs.i18n.json index 8c1b13dcb7bf9..26d471e8302de 100644 --- a/packages/i18n/src/locales/bs.i18n.json +++ b/packages/i18n/src/locales/bs.i18n.json @@ -2588,7 +2588,6 @@ "quote": "citat", "registration.component.form.confirmPassword": "Potvrdi svoju lozinku", "registration.component.form.divider": "ili", - "registration.component.form.emailAlreadyExists": "Email već postoji", "registration.component.form.invalidConfirmPass": "Potvrda lozinke se ne slaže sa lozinkom", "registration.component.form.invalidEmail": "Uneseni e-mail nije valjan", "registration.component.form.invalidEmailDomain":"Unesena e-mail adresa ima nevažeću domenu", @@ -2597,7 +2596,6 @@ "registration.component.form.reasonToJoin": "Razlog pridruživanja", "registration.component.form.sendConfirmationEmail": "Pošalji potvrdni email", "registration.component.form.submit": "Pošalji", - "registration.component.form.userAlreadyExist": "Korisničko ime već postoji. Pokušajte s drugim korisničkim imenom.", "registration.component.form.username": "Korisničko ime", "registration.component.form.usernameAlreadyExists": "Korisničko ime već postoji. Pokušajte s drugim korisničkim imenom.", "registration.component.login": "Prijava", diff --git a/packages/i18n/src/locales/ca.i18n.json b/packages/i18n/src/locales/ca.i18n.json index 8200829718b78..e5f7f6a13fbc2 100644 --- a/packages/i18n/src/locales/ca.i18n.json +++ b/packages/i18n/src/locales/ca.i18n.json @@ -4418,7 +4418,6 @@ "registration.component.form.confirmation": "Confirmació", "registration.component.form.divider": "o", "registration.component.form.email": "Correu electrònic", - "registration.component.form.emailAlreadyExists": "L'adreça de correu electrònic ja existeix", "registration.component.form.invalidConfirmPass": "La confirmació de la contrasenya no coincideix amb la contrasenya", "registration.component.form.invalidEmail": "L'adreça de correu-e és invàlida", "registration.component.form.invalidEmailDomain":"El correu electrònic introduït té un domini no vàlid", @@ -4427,7 +4426,6 @@ "registration.component.form.reasonToJoin": "Motiu per unir-se", "registration.component.form.sendConfirmationEmail": "Envia correu-e de confirmació", "registration.component.form.submit": "Envia", - "registration.component.form.userAlreadyExist": "Nom d'usuari ja existeix. Proveu amb un altre nom d'usuari.", "registration.component.form.username": "Nom d'usuari", "registration.component.form.usernameAlreadyExists": "Nom d'usuari ja existeix. Proveu amb un altre nom d'usuari.", "registration.component.login": "Inicia sessió", diff --git a/packages/i18n/src/locales/cs.i18n.json b/packages/i18n/src/locales/cs.i18n.json index 9e0add1b82323..a38cb1b85b460 100644 --- a/packages/i18n/src/locales/cs.i18n.json +++ b/packages/i18n/src/locales/cs.i18n.json @@ -3760,7 +3760,6 @@ "registration.component.form.confirmPassword": "Potvrďte heslo", "registration.component.form.divider": "nebo", "registration.component.form.email": "E-mail", - "registration.component.form.emailAlreadyExists": "Email již existuje", "registration.component.form.invalidConfirmPass": "Hesla nesouhlasí", "registration.component.form.invalidEmail": "Zadaný e-mail je neplatný", "registration.component.form.invalidEmailDomain":"Zadaná e-mailová adresa má neplatnou doménu", @@ -3769,7 +3768,6 @@ "registration.component.form.reasonToJoin": "Důvod připojení", "registration.component.form.sendConfirmationEmail": "Zaslat potvrzovací e-mail", "registration.component.form.submit": "Odeslat", - "registration.component.form.userAlreadyExist": "Uživatelské jméno již existuje. Použijte prosím jiné.", "registration.component.form.username": "Uživatelské jméno", "registration.component.form.usernameAlreadyExists": "Uživatelské jméno již existuje. Použijte prosím jiné.", "registration.component.login": "Přihlásit se", diff --git a/packages/i18n/src/locales/cy.i18n.json b/packages/i18n/src/locales/cy.i18n.json index a987c848123b6..1fe7e3a5e8514 100644 --- a/packages/i18n/src/locales/cy.i18n.json +++ b/packages/i18n/src/locales/cy.i18n.json @@ -2590,7 +2590,6 @@ "registration.component.form.confirmPassword": "Cadarnhau eich cyfrinair", "registration.component.form.divider": "neu", "registration.component.form.email": "E-bost", - "registration.component.form.emailAlreadyExists": "Ebost eisoes yn bodoli", "registration.component.form.invalidConfirmPass": "Nid yw'r cadarnhad cyfrinair yn cyfateb i'r cyfrinair", "registration.component.form.invalidEmail": "Mae'r e-bost a roddwyd yn annilys", "registration.component.form.invalidEmailDomain":"Mae gan y cyfeiriad e-bost a roddwyd barth annilys", @@ -2599,7 +2598,6 @@ "registration.component.form.reasonToJoin": "Rheswm i Ymuno", "registration.component.form.sendConfirmationEmail": "Anfon ebost cadarnhad", "registration.component.form.submit": "Cyflwyno", - "registration.component.form.userAlreadyExist": "Mae enw defnyddiwr eisoes yn bodoli. Rhowch gynnig ar enw defnyddiwr arall.", "registration.component.form.username": "Enw Defnyddiwr", "registration.component.form.usernameAlreadyExists": "Mae enw defnyddiwr eisoes yn bodoli. Rhowch gynnig ar enw defnyddiwr arall.", "registration.component.login": "Mewngofnodi", diff --git a/packages/i18n/src/locales/da.i18n.json b/packages/i18n/src/locales/da.i18n.json index 46588c79f6406..fd2e9f7af1e5a 100644 --- a/packages/i18n/src/locales/da.i18n.json +++ b/packages/i18n/src/locales/da.i18n.json @@ -3864,7 +3864,6 @@ "registration.component.form.createAnAccount": "Opret en konto", "registration.component.form.divider": "eller", "registration.component.form.email": "E-mail", - "registration.component.form.emailAlreadyExists": "E-mailen eksisterer allerede", "registration.component.form.invalidConfirmPass": "Adgangskodebekræftelsen stemmer ikke overens med adgangskoden", "registration.component.form.invalidEmail": "Den indtastede e-mail er ugyldig", "registration.component.form.invalidEmailDomain":"Den indtastede e-mailadresse har et ugyldigt domæne", @@ -3873,7 +3872,6 @@ "registration.component.form.reasonToJoin": "Årsag til at deltage", "registration.component.form.sendConfirmationEmail": "Send bekræftelses-email", "registration.component.form.submit": "Indsend", - "registration.component.form.userAlreadyExist": "Brugernavnet eksisterer allerede. Prøv venligst et andet brugernavn.", "registration.component.form.username": "Brugernavn", "registration.component.form.usernameAlreadyExists": "Brugernavnet eksisterer allerede. Prøv venligst et andet brugernavn.", "registration.component.login": "Login", diff --git a/packages/i18n/src/locales/de-AT.i18n.json b/packages/i18n/src/locales/de-AT.i18n.json index 0ea013f277548..4691aa7c3856d 100644 --- a/packages/i18n/src/locales/de-AT.i18n.json +++ b/packages/i18n/src/locales/de-AT.i18n.json @@ -2596,7 +2596,6 @@ "registration.component.form.confirmPassword": "Bestätigen Sie Ihr Passwort.", "registration.component.form.divider": "oder", "registration.component.form.email": "E-Mail", - "registration.component.form.emailAlreadyExists": "Die E-Mail-Adresse existiert bereits.", "registration.component.form.invalidConfirmPass": "Die Passwörter stimmen nicht überein.", "registration.component.form.invalidEmail": "Die eingegebene E-Mail-Adresse ist ungültig.", "registration.component.form.invalidEmailDomain": "Die eingegebene E-Mail-Adresse hat eine ungültige Domain.", @@ -2605,7 +2604,6 @@ "registration.component.form.reasonToJoin": "Grund zu Join", "registration.component.form.sendConfirmationEmail": "Bestätigungsmail versenden", "registration.component.form.submit": "Abschicken", - "registration.component.form.userAlreadyExist": "Benutzername existiert bereits. Bitte versuchen Sie es mit einem anderen Benutzernamen.", "registration.component.form.username": "Nutzername", "registration.component.form.usernameAlreadyExists": "Benutzername existiert bereits. Bitte versuchen Sie es mit einem anderen Benutzernamen.", "registration.component.login": "Anmelden", diff --git a/packages/i18n/src/locales/de.i18n.json b/packages/i18n/src/locales/de.i18n.json index 5008477292050..4463d25e27b10 100644 --- a/packages/i18n/src/locales/de.i18n.json +++ b/packages/i18n/src/locales/de.i18n.json @@ -5104,7 +5104,6 @@ "registration.component.form.createAnAccount": "Ein Konto erstellen", "registration.component.form.divider": "oder", "registration.component.form.email": "E-Mail", - "registration.component.form.emailAlreadyExists": "Die E-Mail-Adresse existiert bereits.", "registration.component.form.emailOrUsername": "E-Mail-Adresse oder Nutzername", "registration.component.form.invalidConfirmPass": "Die Passwörter stimmen nicht überein.", "registration.component.form.invalidEmail": "Die eingegebene E-Mail-Adresse ist ungültig.", @@ -5116,7 +5115,6 @@ "registration.component.form.requiredField": "Dies ist ein Pflichtfeld", "registration.component.form.sendConfirmationEmail": "Bestätigungsmail versenden", "registration.component.form.submit": "Abssenden", - "registration.component.form.userAlreadyExist": "Benutzername existiert bereits. Bitte versuchen Sie es mit einem anderen Benutzernamen.", "registration.component.form.username": "Benutzername", "registration.component.form.usernameAlreadyExists": "Benutzername existiert bereits. Bitte versuchen Sie es mit einem anderen Benutzernamen.", "registration.component.login": "Anmelden", diff --git a/packages/i18n/src/locales/el.i18n.json b/packages/i18n/src/locales/el.i18n.json index c410c9e4ef583..8874e9d6904af 100644 --- a/packages/i18n/src/locales/el.i18n.json +++ b/packages/i18n/src/locales/el.i18n.json @@ -2599,7 +2599,6 @@ "registration.component.form.confirmPassword": "Επιβεβαιώστε τον κωδικό σας", "registration.component.form.divider": "ή", "registration.component.form.email": "Email", - "registration.component.form.emailAlreadyExists": "Το email υπάρχει ήδη", "registration.component.form.emailOrUsername": "Email ή όνομα χρήστη", "registration.component.form.invalidConfirmPass": "Η επιβεβαίωση κωδικού δεν ταιριάζει με τον αρχικό κωδικό", "registration.component.form.invalidEmail": "Το e-mail που δώσατε δεν είναι έγκυρο", @@ -2608,7 +2607,6 @@ "registration.component.form.reasonToJoin": "Λόγος για συμμετοχή", "registration.component.form.sendConfirmationEmail": "Αποστολή email επιβεβαίωσης", "registration.component.form.submit": "Υποβολή", - "registration.component.form.userAlreadyExist": "Το όνομα χρήστη υπάρχει ήδη. Δοκιμάστε ένα άλλο όνομα χρήστη.", "registration.component.form.username": "Όνομα Χρήστη", "registration.component.form.usernameAlreadyExists": "Το όνομα χρήστη υπάρχει ήδη. Δοκιμάστε ένα άλλο όνομα χρήστη.", "registration.component.login": "Είσοδος", diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 52a27e071c3d2..ca424d5169b69 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -6688,7 +6688,7 @@ "registration.component.form.createAnAccount": "Create an account", "registration.component.form.divider": "or", "registration.component.form.email": "Email", - "registration.component.form.emailAlreadyExists": "Email already exists", + "registration.component.form.emailAlreadyInUse": "Email already in use", "registration.component.form.emailOrUsername": "Email or username", "registration.component.form.emailPlaceholder": "example@example.com", "registration.component.form.invalidConfirmPass": "The password confirmation does not match password", @@ -6704,9 +6704,9 @@ "registration.component.form.requiredField": "This field is required", "registration.component.form.sendConfirmationEmail": "Send confirmation email", "registration.component.form.submit": "Submit", - "registration.component.form.userAlreadyExist": "Username already exists. Please try another username.", "registration.component.form.username": "Username", "registration.component.form.usernameAlreadyExists": "Username already exists. Please try another username.", + "registration.component.form.usernameAlreadyInUse": "Username already in use", "registration.component.form.usernameContainsInvalidChars": "Username contains invalid characters", "registration.component.login": "Login", "registration.component.login.incorrectPassword": "Incorrect password", diff --git a/packages/i18n/src/locales/eo.i18n.json b/packages/i18n/src/locales/eo.i18n.json index 95335390f46b3..8682d22de9907 100644 --- a/packages/i18n/src/locales/eo.i18n.json +++ b/packages/i18n/src/locales/eo.i18n.json @@ -2594,7 +2594,6 @@ "registration.component.form.confirmPassword": "Konfirmu vian pasvorton", "registration.component.form.divider": "aŭ", "registration.component.form.email": "Retpoŝto", - "registration.component.form.emailAlreadyExists": "Retpoŝto jam ekzistas", "registration.component.form.emailOrUsername": "Retpoŝtadreso aŭ uzantnomo", "registration.component.form.invalidConfirmPass": "La konfirmilo de pasvorto ne kongruas kun pasvorto", "registration.component.form.invalidEmail": "La retpoŝta eniro estas nevalida", @@ -2603,7 +2602,6 @@ "registration.component.form.reasonToJoin": "Kialo por aliĝi", "registration.component.form.sendConfirmationEmail": "Sendu konfirman retpoŝton", "registration.component.form.submit": "Afiŝu", - "registration.component.form.userAlreadyExist": "Uzantnomo jam ekzistas. Bonvolu provi alian uzantnomon.", "registration.component.form.username": "Uzulnomo", "registration.component.form.usernameAlreadyExists": "Uzantnomo jam ekzistas. Bonvolu provi alian uzantnomon.", "registration.component.login": "Ensaluti", diff --git a/packages/i18n/src/locales/es.i18n.json b/packages/i18n/src/locales/es.i18n.json index bafc601ea69b7..1eb2fddf29bbc 100644 --- a/packages/i18n/src/locales/es.i18n.json +++ b/packages/i18n/src/locales/es.i18n.json @@ -4749,7 +4749,6 @@ "registration.component.form.createAnAccount": "Crear una cuenta", "registration.component.form.divider": "o", "registration.component.form.email": "Correo electrónico", - "registration.component.form.emailAlreadyExists": "El correo electrónico ya existe", "registration.component.form.emailPlaceholder": "ejemplo@ejemplo.com", "registration.component.form.invalidConfirmPass": "La confirmación de la contraseña no coincide con la contraseña", "registration.component.form.invalidEmail": "El correo electrónico introducido no es válido", @@ -4761,7 +4760,6 @@ "registration.component.form.requiredField": "Este campo es obligatorio", "registration.component.form.sendConfirmationEmail": "Enviar correo electrónico de confirmación", "registration.component.form.submit": "Enviar", - "registration.component.form.userAlreadyExist": "El nombre de usuario ya existe. Pruebe con otro.", "registration.component.form.username": "Nombre de usuario", "registration.component.form.usernameAlreadyExists": "El nombre de usuario ya existe. Prueba a usar otro nombre.", "registration.component.login": "Iniciar sesión", diff --git a/packages/i18n/src/locales/fa.i18n.json b/packages/i18n/src/locales/fa.i18n.json index 59e01b25d7a88..33602178c797a 100644 --- a/packages/i18n/src/locales/fa.i18n.json +++ b/packages/i18n/src/locales/fa.i18n.json @@ -2919,7 +2919,6 @@ "registration.component.form.confirmPassword": "رمز عبور خود را تأیید کنید", "registration.component.form.divider": "یا", "registration.component.form.email": "ایمیل", - "registration.component.form.emailAlreadyExists": "ایمیل از قبل وجود دارد", "registration.component.form.emailOrUsername": "ایمیل یا نام کاربری", "registration.component.form.invalidConfirmPass": "تأییدیه رمز عبور با رمز عبور اصلی مطابقت ندارد.", "registration.component.form.invalidEmail": "ایمیل وارد شده نامعتبر است", @@ -2928,7 +2927,6 @@ "registration.component.form.reasonToJoin": "دلیل پیوستن", "registration.component.form.sendConfirmationEmail": "ارسال ایمیل تایید", "registration.component.form.submit": "ارسال", - "registration.component.form.userAlreadyExist": "نام کاربری از قبل وجود دارد. لطفا نام کاربری دیگری را امتحان کنید", "registration.component.form.username": "نام کاربری", "registration.component.form.usernameAlreadyExists": "نام کاربری از قبل وجود دارد. لطفا نام کاربری دیگری را امتحان کنید", "registration.component.login": "ورود", diff --git a/packages/i18n/src/locales/fi.i18n.json b/packages/i18n/src/locales/fi.i18n.json index d42cfe5624810..a88b941705ca4 100644 --- a/packages/i18n/src/locales/fi.i18n.json +++ b/packages/i18n/src/locales/fi.i18n.json @@ -5302,7 +5302,6 @@ "registration.component.form.createAnAccount": "Luo tili", "registration.component.form.divider": "tai", "registration.component.form.email": "Sähköpostiosoite", - "registration.component.form.emailAlreadyExists": "Sähköpostiosoite on jo olemassa", "registration.component.form.emailOrUsername": "Sähköpostiosoite tai käyttäjätunnus", "registration.component.form.emailPlaceholder": "esimerkki@esimerkki.com", "registration.component.form.invalidConfirmPass": "Salasanat eivät täsmää", @@ -5316,7 +5315,6 @@ "registration.component.form.requiredField": "Tämä kenttä on pakollinen", "registration.component.form.sendConfirmationEmail": "Lähetä vahvistussähköposti", "registration.component.form.submit": "Lähetä", - "registration.component.form.userAlreadyExist": "Käyttäjätunnus on jo olemassa. Kokeile jotain muuta käyttäjätunnusta.", "registration.component.form.username": "Käyttäjätunnus", "registration.component.form.usernameAlreadyExists": "Käyttäjätunnus on jo olemassa. Kokeile toista käyttäjätunnusta.", "registration.component.login": "Kirjaudu", diff --git a/packages/i18n/src/locales/fr.i18n.json b/packages/i18n/src/locales/fr.i18n.json index 32468da48439b..6316080187dd6 100644 --- a/packages/i18n/src/locales/fr.i18n.json +++ b/packages/i18n/src/locales/fr.i18n.json @@ -4587,7 +4587,6 @@ "registration.component.form.confirmation": "Confirmation", "registration.component.form.divider": "ou", "registration.component.form.email": "E-mail", - "registration.component.form.emailAlreadyExists": "L'adresse e-mail existe déjà", "registration.component.form.invalidConfirmPass": "La confirmation de mot du passe ne correspond pas au mot de passe", "registration.component.form.invalidEmail": "L'adresse e-mail saisie est invalide", "registration.component.form.name": "Nom", @@ -4597,7 +4596,6 @@ "registration.component.form.requiredField": "Ce champ est requis", "registration.component.form.sendConfirmationEmail": "Envoyer un e-mail de confirmation", "registration.component.form.submit": "Soumettre", - "registration.component.form.userAlreadyExist": "Ce nom d'utilisateur existe déjà. Essayez avec un autre nom d'utilisateur.", "registration.component.form.username": "Nom d'utilisateur", "registration.component.form.usernameAlreadyExists": "Ce nom d'utilisateur existe déjà. Essayez avec un autre nom d'utilisateur.", "registration.component.login": "Connexion", diff --git a/packages/i18n/src/locales/he.i18n.json b/packages/i18n/src/locales/he.i18n.json index 31ab008d3147d..1442bcd386bc1 100644 --- a/packages/i18n/src/locales/he.i18n.json +++ b/packages/i18n/src/locales/he.i18n.json @@ -1482,7 +1482,6 @@ "quote": "ציטוט", "registration.component.form.confirmPassword": "אמת את הסיסמה שלך", "registration.component.form.email": "דוא״ל", - "registration.component.form.emailAlreadyExists": "כתובת הדוא״ל כבר קיימת", "registration.component.form.invalidConfirmPass": "אימות הססמה אינו זהה לססמה", "registration.component.form.invalidEmail": "כתובת הדוא״ל שהוזנה אינה תקינה", "registration.component.form.name": "שם", diff --git a/packages/i18n/src/locales/hi-IN.i18n.json b/packages/i18n/src/locales/hi-IN.i18n.json index 49e593aeaf763..9b1854c98bdf6 100644 --- a/packages/i18n/src/locales/hi-IN.i18n.json +++ b/packages/i18n/src/locales/hi-IN.i18n.json @@ -5650,7 +5650,6 @@ "registration.component.form.createAnAccount": "खाता बनाएं", "registration.component.form.divider": "या", "registration.component.form.email": "ईमेल", - "registration.component.form.emailAlreadyExists": "ईमेल पहले से ही मौजूद है", "registration.component.form.emailOrUsername": "ईमेल या उपयोगकर्ता का नाम", "registration.component.form.emailPlaceholder": "example@example.com", "registration.component.form.invalidConfirmPass": "पासवर्ड पुष्टिकरण पासवर्ड से मेल नहीं खाता", @@ -5664,7 +5663,6 @@ "registration.component.form.requiredField": "यह फ़ील्ड आवश्यक है", "registration.component.form.sendConfirmationEmail": "पुष्टिकरण ईमेल भेजें", "registration.component.form.submit": "जमा करना", - "registration.component.form.userAlreadyExist": "उपयोगकर्ता का नाम पहले से मौजूद है। कृपया कोई अन्य उपयोक्तानाम आज़माएँ.", "registration.component.form.username": "उपयोगकर्ता नाम", "registration.component.form.usernameAlreadyExists": "उपयोगकर्ता का नाम पहले से मौजूद है। कृपया कोई अन्य उपयोक्तानाम आज़माएँ.", "registration.component.login": "लॉग इन करें", diff --git a/packages/i18n/src/locales/hr.i18n.json b/packages/i18n/src/locales/hr.i18n.json index 942a0e841f75a..318b1e7eb31c6 100644 --- a/packages/i18n/src/locales/hr.i18n.json +++ b/packages/i18n/src/locales/hr.i18n.json @@ -2723,7 +2723,6 @@ "registration.component.form.confirmPassword": "Potvrdi svoju lozinku", "registration.component.form.divider": "ili", "registration.component.form.email": "Email", - "registration.component.form.emailAlreadyExists": "Email već postoji", "registration.component.form.emailOrUsername": "Email or username", "registration.component.form.invalidConfirmPass": "Potvrda lozinke se ne slaže sa lozinkom", "registration.component.form.invalidEmail": "Uneseni e-mail nije valjan", @@ -2732,7 +2731,6 @@ "registration.component.form.reasonToJoin": "Razlog pridruživanja", "registration.component.form.sendConfirmationEmail": "Pošalji potvrdni email", "registration.component.form.submit": "Pošalji", - "registration.component.form.userAlreadyExist": "Korisničko ime već postoji. Pokušajte s drugim korisničkim imenom.", "registration.component.form.username": "Korisničko ime", "registration.component.form.usernameAlreadyExists": "Korisničko ime već postoji. Pokušajte s drugim korisničkim imenom.", "registration.component.login": "Prijava", diff --git a/packages/i18n/src/locales/hu.i18n.json b/packages/i18n/src/locales/hu.i18n.json index 227be546916e4..beae536ccca9e 100644 --- a/packages/i18n/src/locales/hu.i18n.json +++ b/packages/i18n/src/locales/hu.i18n.json @@ -4991,7 +4991,6 @@ "registration.component.form.createAnAccount": "Fiók létrehozása", "registration.component.form.divider": "vagy", "registration.component.form.email": "E-mail-cím", - "registration.component.form.emailAlreadyExists": "Az e-mail cím már létezik", "registration.component.form.emailOrUsername": "E-mail-cím vagy felhasználónév", "registration.component.form.emailPlaceholder": "pelda@example.com", "registration.component.form.invalidConfirmPass": "A két jelszó nem eggyezik", @@ -5003,7 +5002,6 @@ "registration.component.form.requiredField": "Ez a mező kötelező", "registration.component.form.sendConfirmationEmail": "Megerősítő email elküldése", "registration.component.form.submit": "Elküldés", - "registration.component.form.userAlreadyExist": "A felhasználónév már létezik. Próbáljon más felhasználónevet.", "registration.component.form.username": "Felhasználónév", "registration.component.form.usernameAlreadyExists": "Felhasználónév már létezik. Próbálj meg egy másik felhasználónevet.", "registration.component.login": "Bejelentkezés", diff --git a/packages/i18n/src/locales/id.i18n.json b/packages/i18n/src/locales/id.i18n.json index 2adbbce8c8494..d887962b5239b 100644 --- a/packages/i18n/src/locales/id.i18n.json +++ b/packages/i18n/src/locales/id.i18n.json @@ -2601,7 +2601,6 @@ "registration.component.form.confirmPassword": "Konfirmasikan kata sandi anda", "registration.component.form.divider": "atau", "registration.component.form.email": "E-mail", - "registration.component.form.emailAlreadyExists": "Email sudah terdaftar", "registration.component.form.emailOrUsername": "Email atau username", "registration.component.form.invalidConfirmPass": "Kata sandi konfirmasi tidak cocok dengan kata sandi utama", "registration.component.form.invalidEmail": "Email yang anda masukkan tidak valid", @@ -2610,7 +2609,6 @@ "registration.component.form.reasonToJoin": "Alasan untuk bergabung", "registration.component.form.sendConfirmationEmail": "Kirim email konfirmasi", "registration.component.form.submit": "Submit", - "registration.component.form.userAlreadyExist": "Nama pengguna sudah ada. Silakan coba nama pengguna lain", "registration.component.form.username": "Nama pengguna", "registration.component.form.usernameAlreadyExists": "Nama pengguna sudah ada. Silakan coba nama pengguna lain", "registration.component.login": "Login", diff --git a/packages/i18n/src/locales/it.i18n.json b/packages/i18n/src/locales/it.i18n.json index 69ca883a7bcf6..bb3a2d4ca96af 100644 --- a/packages/i18n/src/locales/it.i18n.json +++ b/packages/i18n/src/locales/it.i18n.json @@ -3214,7 +3214,6 @@ "registration.component.form.confirmPassword": "Conferma la tua password", "registration.component.form.divider": "o", "registration.component.form.email": "Email", - "registration.component.form.emailAlreadyExists": "Email già esistente", "registration.component.form.emailOrUsername": "Email o nome utente", "registration.component.form.emailPlaceholder": "example@example.com", "registration.component.form.invalidConfirmPass": "La password di conferma non corrisponde con la password", @@ -3224,7 +3223,6 @@ "registration.component.form.reasonToJoin": "Motivo per partecipare", "registration.component.form.sendConfirmationEmail": "Invia email di conferma", "registration.component.form.submit": "Invia", - "registration.component.form.userAlreadyExist": "Il nome utente esiste già. Si prega di provare un altro nome utente.", "registration.component.form.username": "Nome utente", "registration.component.form.usernameAlreadyExists": "Il nome utente esiste già. Si prega di provare un altro nome utente.", "registration.component.login": "Login", diff --git a/packages/i18n/src/locales/ja.i18n.json b/packages/i18n/src/locales/ja.i18n.json index c4111af955f98..f86c3a847a235 100644 --- a/packages/i18n/src/locales/ja.i18n.json +++ b/packages/i18n/src/locales/ja.i18n.json @@ -4535,7 +4535,6 @@ "registration.component.form.confirmation": "確認", "registration.component.form.divider": "または", "registration.component.form.email": "メール", - "registration.component.form.emailAlreadyExists": "メールはすでに存在します", "registration.component.form.emailOrUsername": "メールアドレスまたはユーザー名", "registration.component.form.invalidConfirmPass": "入力されたパスワードとパスワードの確認が一致していません", "registration.component.form.invalidEmail": "入力されたメールアドレスは無効です", @@ -4546,7 +4545,6 @@ "registration.component.form.requiredField": "このフィールドは必須です", "registration.component.form.sendConfirmationEmail": "確認メールを送信", "registration.component.form.submit": "送信", - "registration.component.form.userAlreadyExist": "ユーザー名はすでに存在します。別のユーザー名をお試しください。", "registration.component.form.username": "ユーザー名", "registration.component.form.usernameAlreadyExists": "ユーザー名はすでに存在します。別のユーザー名をお試しください。", "registration.component.login": "ログイン", diff --git a/packages/i18n/src/locales/ka-GE.i18n.json b/packages/i18n/src/locales/ka-GE.i18n.json index e8ae17a32a8fa..620bfb3af146d 100644 --- a/packages/i18n/src/locales/ka-GE.i18n.json +++ b/packages/i18n/src/locales/ka-GE.i18n.json @@ -3489,7 +3489,6 @@ "registration.component.form.confirmPassword": "დაადასტურეთ თქვენი პაროლი", "registration.component.form.divider": "ან", "registration.component.form.email": "ელ.ფოსტა", - "registration.component.form.emailAlreadyExists": "იმეილი უკვე არსებობს", "registration.component.form.emailOrUsername": "ელ.ფოსტის ან მომხმარებლის სახელი", "registration.component.form.invalidConfirmPass": "პაროლის დასტური არ შეესაბამება პაროლს", "registration.component.form.invalidEmail": "შეყვანილი ელ.ფოსტა არასწორია", @@ -3498,7 +3497,6 @@ "registration.component.form.reasonToJoin": "გაწევრიანების მიზეზი", "registration.component.form.sendConfirmationEmail": "დადასტურების ელ.ფოსტის გაგზავნა", "registration.component.form.submit": "წარდგენა", - "registration.component.form.userAlreadyExist": "სახელი უკვე არსებობს. გთხოვთ, სცადოთ სხვა სახელი.", "registration.component.form.username": "მომხმარებლის სახელი", "registration.component.form.usernameAlreadyExists": "სახელი უკვე არსებობს. გთხოვთ, სცადოთ სხვა სახელი.", "registration.component.login": "შესვლა", diff --git a/packages/i18n/src/locales/km.i18n.json b/packages/i18n/src/locales/km.i18n.json index 05d6cfb68999c..04cb756547014 100644 --- a/packages/i18n/src/locales/km.i18n.json +++ b/packages/i18n/src/locales/km.i18n.json @@ -2935,7 +2935,6 @@ "registration.component.form.confirmPassword": "បញ្ជាក់​ពាក្យ​សម្ងាត់", "registration.component.form.divider": "ឬ", "registration.component.form.email": "អ៊ីម៉ែល", - "registration.component.form.emailAlreadyExists": "អ៊ី​ម៉ែ​ល​ដែល​មាន​រួច​ហើយ", "registration.component.form.invalidConfirmPass": "ពាក្យ​សម្ងាត់​បញ្ជាក់​មិន​ដូច​ពាក្យ​សម្ងាត់​បាន​បញ្ចូល​", "registration.component.form.invalidEmail": "អ៊ី​មែល​ដែល​បញ្ចូល​មិន​ត្រឹម​ត្រូវ", "registration.component.form.name": "Name", @@ -2943,7 +2942,6 @@ "registration.component.form.reasonToJoin": "ហេតុផលដើម្បីចូលរួម", "registration.component.form.sendConfirmationEmail": "ផ្ញើរអ៊ីម៉ែល​បញ្ជាក់", "registration.component.form.submit": "បញ្ចូល", - "registration.component.form.userAlreadyExist": "ឈ្មោះ​របស់​អ្នកប្រើប្រាស់​ធ្លាប់​មាន​ហើយ។ សូមសាកល្បងឈ្មោះអ្នកប្រើប្រាស់ផ្សេងទៀត។", "registration.component.form.username": "ឈ្មោះ​អ្នកប្រើប្រាស់", "registration.component.form.usernameAlreadyExists": "ឈ្មោះ​របស់​អ្នកប្រើប្រាស់​ធ្លាប់​មាន​ហើយ។ សូមសាកល្បងឈ្មោះអ្នកប្រើប្រាស់ផ្សេងទៀត។", "registration.component.login": "ឡុក​ចូល", diff --git a/packages/i18n/src/locales/ko.i18n.json b/packages/i18n/src/locales/ko.i18n.json index 90ea1ffce6dd3..00ee3bc3e3ed0 100644 --- a/packages/i18n/src/locales/ko.i18n.json +++ b/packages/i18n/src/locales/ko.i18n.json @@ -3831,7 +3831,6 @@ "registration.component.form.confirmPassword": "비밀번호를 확인하세요", "registration.component.form.divider": "또는", "registration.component.form.email": "이메일", - "registration.component.form.emailAlreadyExists": "이메일이 이미 있습니다.", "registration.component.form.invalidConfirmPass": "비밀번호가 일치하지 않습니다.", "registration.component.form.invalidEmail": "입력한 이메일이 잘못되었습니다.", "registration.component.form.name": "이름", @@ -3839,7 +3838,6 @@ "registration.component.form.reasonToJoin": "가입 이유", "registration.component.form.sendConfirmationEmail": "확인 메일 보내기", "registration.component.form.submit": "제출", - "registration.component.form.userAlreadyExist": "사용자명이 이미 존재합니다. 다른 사용자명을 입력해보세요.", "registration.component.form.username": "사용자명", "registration.component.form.usernameAlreadyExists": "사용자명이 이미 존재합니다. 다른 사용자명을 입력해보세요.", "registration.component.login": "로그인", diff --git a/packages/i18n/src/locales/ku.i18n.json b/packages/i18n/src/locales/ku.i18n.json index 98ec9ff45ba42..20529a65341f2 100644 --- a/packages/i18n/src/locales/ku.i18n.json +++ b/packages/i18n/src/locales/ku.i18n.json @@ -2585,7 +2585,6 @@ "quote": "pêşnîyarîya bedelê", "registration.component.form.confirmPassword": "تێپەڕەوشەکەت پشتڕاستکەوە", "registration.component.form.divider": "an", - "registration.component.form.emailAlreadyExists": "Email jixwe heye", "registration.component.form.invalidConfirmPass": "دووبارەکراوەی تێپەڕەوشە یەکناگرێتەوە لەگەڵ تێپەڕەوشە", "registration.component.form.invalidEmail": "ئیمەیڵی نوسراو هەڵەیە.", "registration.component.form.name": "ناو", @@ -2593,7 +2592,6 @@ "registration.component.form.reasonToJoin": "Reason to join", "registration.component.form.sendConfirmationEmail": "ئیمەیڵی پشتڕاستکردنەوە بنێرە", "registration.component.form.submit": "ناردن", - "registration.component.form.userAlreadyExist": "Ev nav tê bikaranîn. Ji kerema xwe bikarhênerek din bixwazin.", "registration.component.form.username": "Navê bikarhêner", "registration.component.form.usernameAlreadyExists": "Ev nav tê bikaranîn. Ji kerema xwe bikarhênerek din bixwazin.", "registration.component.login": "چوونەژوور", diff --git a/packages/i18n/src/locales/lo.i18n.json b/packages/i18n/src/locales/lo.i18n.json index e434b1efe860d..3c0cd45ef4045 100644 --- a/packages/i18n/src/locales/lo.i18n.json +++ b/packages/i18n/src/locales/lo.i18n.json @@ -2626,7 +2626,6 @@ "registration.component.form.confirmPassword": "ຢືນຢັນລະຫັດຜ່ານຂອງທ່ານ", "registration.component.form.divider": "ຫຼື", "registration.component.form.email": "Email", - "registration.component.form.emailAlreadyExists": "Email ຢູ່ແລ້ວ", "registration.component.form.emailOrUsername": "ອີ​ເມລ​ຫຼື​ຊື່​ຜູ້​ໃຊ້", "registration.component.form.invalidConfirmPass": "ການຢືນຢັນລະຫັດຜ່ານບໍ່ກົງກັບລະຫັດຜ່ານ", "registration.component.form.invalidEmail": "ອີເມວນັ້ນບໍ່ຖືກຕ້ອງ", @@ -2635,7 +2634,6 @@ "registration.component.form.reasonToJoin": "ເຫດຜົນທີ່ຈະເຂົ້າຮ່ວມ", "registration.component.form.sendConfirmationEmail": "ສົ່ງອີເມວການຢືນຢັນ", "registration.component.form.submit": "ຍື່ນສະເຫນີ", - "registration.component.form.userAlreadyExist": "ຊື່ຜູ້ໃຊ້ມີຢູ່ແລ້ວ. ກະລຸນາລອງໃຊ້ຊື່ຜູ້ໃຊ້ອີກຄັ້ງຫນຶ່ງ.", "registration.component.form.username": "ຊື່ຜູ້ໃຊ້", "registration.component.form.usernameAlreadyExists": "ຊື່ຜູ້ໃຊ້ມີຢູ່ແລ້ວ. ກະລຸນາລອງໃຊ້ຊື່ຜູ້ໃຊ້ອີກຄັ້ງຫນຶ່ງ.", "registration.component.login": "ເຂົ້າ​ສູ່​ລະ​ບົບ", diff --git a/packages/i18n/src/locales/lt.i18n.json b/packages/i18n/src/locales/lt.i18n.json index 1883c762921e3..6c9f1f5db8f39 100644 --- a/packages/i18n/src/locales/lt.i18n.json +++ b/packages/i18n/src/locales/lt.i18n.json @@ -2645,7 +2645,6 @@ "registration.component.form.confirmPassword": "Patvirtinkite savo slaptažodį", "registration.component.form.divider": "arba", "registration.component.form.email": "Paštas", - "registration.component.form.emailAlreadyExists": "El. Laiškas jau egzistuoja", "registration.component.form.emailOrUsername": "El. pastas arba vartotojo vardas", "registration.component.form.invalidConfirmPass": "Slaptažodžio patvirtinimas nesutampa su slaptažodžiu", "registration.component.form.invalidEmail": "Įvestas el. Pašto adresas neteisingas", @@ -2654,7 +2653,6 @@ "registration.component.form.reasonToJoin": "Priežastys prisijungti", "registration.component.form.sendConfirmationEmail": "Siųsti patvirtinimo el. Laišką", "registration.component.form.submit": "Pateikti", - "registration.component.form.userAlreadyExist": "Vardas jau egzistuoja. Pabandykite kitą vartotojo vardą.", "registration.component.form.username": "Vartotojo vardas", "registration.component.form.usernameAlreadyExists": "Vardas jau egzistuoja. Pabandykite kitą vartotojo vardą.", "registration.component.login": "Prisijungti", diff --git a/packages/i18n/src/locales/lv.i18n.json b/packages/i18n/src/locales/lv.i18n.json index 158010f517c57..65de5facf07ec 100644 --- a/packages/i18n/src/locales/lv.i18n.json +++ b/packages/i18n/src/locales/lv.i18n.json @@ -2597,7 +2597,6 @@ "registration.component.form.confirmPassword": "Apstipriniet savu paroli", "registration.component.form.divider": "vai", "registration.component.form.email": "E-pasts", - "registration.component.form.emailAlreadyExists": "E-pasta adrese jau pastāv", "registration.component.form.emailOrUsername": "E-pasts vai lietotājvārds", "registration.component.form.invalidConfirmPass": "Paroles apstiprinājums neatbilst parolei", "registration.component.form.invalidEmail": "Ievadītais e-pasts nav derīgs", @@ -2606,7 +2605,6 @@ "registration.component.form.reasonToJoin": "Iemesls pievienoties", "registration.component.form.sendConfirmationEmail": "Nosūtīt apstiprinājuma e-pastu", "registration.component.form.submit": "Iesniegt", - "registration.component.form.userAlreadyExist": "Lietotājvārds jau eksistē. Lūdzu, izmēģiniet citu lietotājvārdu.", "registration.component.form.username": "Lietotājvārds", "registration.component.form.usernameAlreadyExists": "Lietotājvārds jau eksistē. Lūdzu, izmēģiniet citu lietotājvārdu.", "registration.component.login": "Pieteikties", diff --git a/packages/i18n/src/locales/mn.i18n.json b/packages/i18n/src/locales/mn.i18n.json index 40fac0d97c7e4..69aa1f59152d6 100644 --- a/packages/i18n/src/locales/mn.i18n.json +++ b/packages/i18n/src/locales/mn.i18n.json @@ -2588,7 +2588,6 @@ "registration.component.form.confirmPassword": "Нууц үгээ батлах", "registration.component.form.divider": "эсвэл", "registration.component.form.email": "И-мэйл хаяг", - "registration.component.form.emailAlreadyExists": "Имэйл аль хэдийн байна", "registration.component.form.emailOrUsername": "И-мэйл эсвэл Хэрэглэгчийн нэр", "registration.component.form.invalidConfirmPass": "Нууц үг баталгаажуулалт нь нууц үгтэй таарахгүй байна", "registration.component.form.invalidEmail": "Оруулсан имэйл буруу байна", @@ -2597,7 +2596,6 @@ "registration.component.form.reasonToJoin": "Яагаад болохгүй гэж?", "registration.component.form.sendConfirmationEmail": "Баталгаажуулах имэйл илгээх", "registration.component.form.submit": "Илгээх", - "registration.component.form.userAlreadyExist": "Хэрэглэгчийн нэр өмнө байна. Өөр хэрэглэгчийн нэрийг оруулна уу.", "registration.component.form.username": "Хэрэглэгчийн нэр", "registration.component.form.usernameAlreadyExists": "Хэрэглэгчийн нэр өмнө байна. Өөр хэрэглэгчийн нэрийг оруулна уу.", "registration.component.login": "Нэвтрэх", diff --git a/packages/i18n/src/locales/ms-MY.i18n.json b/packages/i18n/src/locales/ms-MY.i18n.json index 517fcae8acca4..8b2825ee9f137 100644 --- a/packages/i18n/src/locales/ms-MY.i18n.json +++ b/packages/i18n/src/locales/ms-MY.i18n.json @@ -2605,7 +2605,6 @@ "registration.component.form.confirmPassword": "Sahkan kata laluan anda", "registration.component.form.divider": "atau", "registration.component.form.email": "e-mel", - "registration.component.form.emailAlreadyExists": "E-mel telah wujud", "registration.component.form.emailOrUsername": "E-mel atau nama pengguna", "registration.component.form.invalidConfirmPass": "Pengesahan kata laluan tidak sepadan dengan kata laluan", "registration.component.form.invalidEmail": "E-mel yang dimasukkan tidak sah", @@ -2614,7 +2613,6 @@ "registration.component.form.reasonToJoin": "Sebab untuk Bergabung", "registration.component.form.sendConfirmationEmail": "Hantar e-mel pengesahan", "registration.component.form.submit": "Hantar", - "registration.component.form.userAlreadyExist": "Nama pengguna sudah wujud. Sila cuba nama pengguna lain.", "registration.component.form.username": "Nama pengguna", "registration.component.form.usernameAlreadyExists": "Nama pengguna sudah wujud. Sila cuba nama pengguna lain.", "registration.component.login": "Log masuk", diff --git a/packages/i18n/src/locales/nb.i18n.json b/packages/i18n/src/locales/nb.i18n.json index ba51f7669bfed..258a89841ae05 100644 --- a/packages/i18n/src/locales/nb.i18n.json +++ b/packages/i18n/src/locales/nb.i18n.json @@ -6507,7 +6507,6 @@ "registration.component.form.createAnAccount": "Opprett en konto", "registration.component.form.divider": "eller", "registration.component.form.email": "E-post", - "registration.component.form.emailAlreadyExists": "E-post eksisterer allerede", "registration.component.form.emailOrUsername": "E-post eller brukernavn", "registration.component.form.emailPlaceholder": "eksempel@eksempel.no", "registration.component.form.invalidConfirmPass": "Passordbekreftelsen stemmer ikke overens med passordet", @@ -6522,7 +6521,6 @@ "registration.component.form.requiredField": "Dette feltet er obligatorisk", "registration.component.form.sendConfirmationEmail": "Send bekreftelses-e-post", "registration.component.form.submit": "Send inn", - "registration.component.form.userAlreadyExist": "Brukernavn finnes allerede. Vennligst prøv et annet brukernavn.", "registration.component.form.username": "Brukernavn", "registration.component.form.usernameAlreadyExists": "Brukernavn finnes allerede. Vennligst prøv et nytt brukernavn.", "registration.component.form.usernameContainsInvalidChars": "Brukernavnet inneholder ugyldige tegn", diff --git a/packages/i18n/src/locales/nl.i18n.json b/packages/i18n/src/locales/nl.i18n.json index a1d2215602956..1e2d56e90735f 100644 --- a/packages/i18n/src/locales/nl.i18n.json +++ b/packages/i18n/src/locales/nl.i18n.json @@ -4570,7 +4570,6 @@ "registration.component.form.confirmation": "Bevestiging", "registration.component.form.divider": "of", "registration.component.form.email": "E-mail", - "registration.component.form.emailAlreadyExists": "E-mailadres bestaat al", "registration.component.form.invalidConfirmPass": "De wachtwoordbevestiging komt niet overeen met het wachtwoord", "registration.component.form.invalidEmail": "Het ingevoerde e-mailadres is ongeldig", "registration.component.form.name": "Naam", @@ -4580,7 +4579,6 @@ "registration.component.form.requiredField": "Dit veld is verplicht", "registration.component.form.sendConfirmationEmail": "Stuur een bevestigingsmail", "registration.component.form.submit": "Verzenden", - "registration.component.form.userAlreadyExist": "Gebruikersnaam bestaat al. Probeer een andere gebruikersnaam.", "registration.component.form.username": "Gebruikersnaam", "registration.component.form.usernameAlreadyExists": "Gebruikersnaam bestaat al. Probeer een andere gebruikersnaam.", "registration.component.login": "Log in", diff --git a/packages/i18n/src/locales/nn.i18n.json b/packages/i18n/src/locales/nn.i18n.json index 67245089d6740..d407a541dfec8 100644 --- a/packages/i18n/src/locales/nn.i18n.json +++ b/packages/i18n/src/locales/nn.i18n.json @@ -5855,7 +5855,6 @@ "registration.component.form.createAnAccount": "Opprett en konto", "registration.component.form.divider": "eller", "registration.component.form.email": "E-post", - "registration.component.form.emailAlreadyExists": "E-post finnes allerede", "registration.component.form.emailPlaceholder": "eksempel@eksempel.no", "registration.component.form.invalidConfirmPass": "Passordbekreftelsen stemmer ikke overens med passordet", "registration.component.form.invalidEmail": "E-posten som er oppgitt, er ugyldig", @@ -5867,7 +5866,6 @@ "registration.component.form.reasonToJoin": "Årsak til å bli med", "registration.component.form.sendConfirmationEmail": "Send bekreftelses-e-post", "registration.component.form.submit": "Sende inn", - "registration.component.form.userAlreadyExist": "Brukernavn finnes allerede. Vennligst prøv et annet brukernavn.", "registration.component.form.username": "Brukernavn", "registration.component.form.usernameAlreadyExists": "Brukernavn finnes allerede. Vennligst prøv et nytt brukernavn.", "registration.component.form.usernameContainsInvalidChars": "Brukernavnet inneholder ugyldige tegn", diff --git a/packages/i18n/src/locales/pl.i18n.json b/packages/i18n/src/locales/pl.i18n.json index f0b6973589e8d..a70cc674aaec6 100644 --- a/packages/i18n/src/locales/pl.i18n.json +++ b/packages/i18n/src/locales/pl.i18n.json @@ -4996,7 +4996,6 @@ "registration.component.form.createAnAccount": "Utwórz konto", "registration.component.form.divider": "lub", "registration.component.form.email": "E-mail", - "registration.component.form.emailAlreadyExists": "Ten email jest zajęty", "registration.component.form.emailOrUsername": "Adres e-mail lub nazwa użytkownika", "registration.component.form.invalidConfirmPass": "Podane hasła nie są jednakowe", "registration.component.form.invalidEmail": "E-mail jest nieprawidłowy", @@ -5008,7 +5007,6 @@ "registration.component.form.requiredField": "To pole jest wymagane", "registration.component.form.sendConfirmationEmail": "Wyślij e-mail z potwierdzeniem", "registration.component.form.submit": "Prześlij", - "registration.component.form.userAlreadyExist": "Nazwa użytkownika już istnieje. Spróbuj użyć innej nazwy użytkownika.", "registration.component.form.username": "Nazwa użytkownika", "registration.component.form.usernameAlreadyExists": "Nazwa użytkownika już istnieje. Spróbuj użyć innej nazwy użytkownika.", "registration.component.login": "Zaloguj się", diff --git a/packages/i18n/src/locales/pt-BR.i18n.json b/packages/i18n/src/locales/pt-BR.i18n.json index a900f91c981c6..f2454e40f97ba 100644 --- a/packages/i18n/src/locales/pt-BR.i18n.json +++ b/packages/i18n/src/locales/pt-BR.i18n.json @@ -6367,7 +6367,6 @@ "registration.component.form.createAnAccount": "Criar uma conta", "registration.component.form.divider": "ou", "registration.component.form.email": "E-mail", - "registration.component.form.emailAlreadyExists": "E-mail já existe", "registration.component.form.emailOrUsername": "E-mail ou nome de usuário", "registration.component.form.emailPlaceholder": "exemplo@exemplo.com", "registration.component.form.invalidConfirmPass": "A confirmação de senha não é igual à senha", @@ -6382,7 +6381,6 @@ "registration.component.form.requiredField": "Este campo é obrigatório", "registration.component.form.sendConfirmationEmail": "Enviar email de confirmação", "registration.component.form.submit": "Enviar", - "registration.component.form.userAlreadyExist": "O nome de usuário já existe. Tente outro nome de usuário.", "registration.component.form.username": "Nome de usuário", "registration.component.form.usernameAlreadyExists": "O nome de usuário já existe. Tente outro nome de usuário.", "registration.component.form.usernameContainsInvalidChars": "O nome de usuário contém caracteres inválidos", diff --git a/packages/i18n/src/locales/pt.i18n.json b/packages/i18n/src/locales/pt.i18n.json index 1f5501f324e78..94b60392fe21e 100644 --- a/packages/i18n/src/locales/pt.i18n.json +++ b/packages/i18n/src/locales/pt.i18n.json @@ -2987,7 +2987,6 @@ "registration.component.form.confirmPassword": "Confirmar a senha", "registration.component.form.divider": "ou", "registration.component.form.email": "Email", - "registration.component.form.emailAlreadyExists": "Email já registado", "registration.component.form.emailOrUsername": "Email ou nome de utilizador", "registration.component.form.invalidConfirmPass": "A confirmação da senha não é igual à senha", "registration.component.form.invalidEmail": "O email introduzido é inválido", @@ -2997,7 +2996,6 @@ "registration.component.form.reasonToJoin": "Razão para se juntar", "registration.component.form.sendConfirmationEmail": "Enviar email de confirmação", "registration.component.form.submit": "Submeter", - "registration.component.form.userAlreadyExist": "O nome de utilizador já existe. Por favor, tente outro nome de utilizador.", "registration.component.form.username": "Nome de utilizador", "registration.component.form.usernameAlreadyExists": "O nome de utilizador já existe. Por favor, tente outro nome de utilizador.", "registration.component.login": "Entrar", diff --git a/packages/i18n/src/locales/ro.i18n.json b/packages/i18n/src/locales/ro.i18n.json index 60ab831cbd3fe..6215d4ac4fbe7 100644 --- a/packages/i18n/src/locales/ro.i18n.json +++ b/packages/i18n/src/locales/ro.i18n.json @@ -2591,7 +2591,6 @@ "registration.component.form.confirmPassword": "Confirmați parola", "registration.component.form.divider": "sau", "registration.component.form.email": "E-mail", - "registration.component.form.emailAlreadyExists": "Adresa de e-mail există deja", "registration.component.form.invalidConfirmPass": "Confirmarea parolei nu se potrivește cu parola introdusă", "registration.component.form.invalidEmail": "Adresa de email folosită este invalidă", "registration.component.form.name": "Nume", @@ -2599,7 +2598,6 @@ "registration.component.form.reasonToJoin": "Motivul de a te alătura", "registration.component.form.sendConfirmationEmail": "Trimite email de confirmare", "registration.component.form.submit": "Trimite", - "registration.component.form.userAlreadyExist": "Nume de utilizator deja existent. Încercați un alt nume de utilizator.", "registration.component.form.username": "Utilizator", "registration.component.form.usernameAlreadyExists": "Nume de utilizator deja existent. Încercați un alt nume de utilizator.", "registration.component.login": "Autentificare", diff --git a/packages/i18n/src/locales/ru.i18n.json b/packages/i18n/src/locales/ru.i18n.json index e767af0ebcefb..1941ac8545edd 100644 --- a/packages/i18n/src/locales/ru.i18n.json +++ b/packages/i18n/src/locales/ru.i18n.json @@ -4753,7 +4753,6 @@ "registration.component.form.confirmation": "Подтверждение", "registration.component.form.divider": "или", "registration.component.form.email": "Электронная почта", - "registration.component.form.emailAlreadyExists": "Такой адрес электронной почты уже используется", "registration.component.form.invalidConfirmPass": "Пароли не совпадают", "registration.component.form.invalidEmail": "Введен некорректный адрес электронной почты", "registration.component.form.name": "Имя", @@ -4763,7 +4762,6 @@ "registration.component.form.requiredField": "Это обязательное поле", "registration.component.form.sendConfirmationEmail": "Отправить электронное письмо с подтверждением", "registration.component.form.submit": "Отправить", - "registration.component.form.userAlreadyExist": "Такой пользователь уже существует. Пожалуйста, выберите другое имя.", "registration.component.form.username": "Имя пользователя", "registration.component.form.usernameAlreadyExists": "Такой пользователь уже существует. Пожалуйста, выберите другое имя.", "registration.component.login": "Авторизация", diff --git a/packages/i18n/src/locales/sk-SK.i18n.json b/packages/i18n/src/locales/sk-SK.i18n.json index 84ff28bf7b9af..6f131cbbd4b7e 100644 --- a/packages/i18n/src/locales/sk-SK.i18n.json +++ b/packages/i18n/src/locales/sk-SK.i18n.json @@ -2601,7 +2601,6 @@ "registration.component.form.confirmPassword": "Potvrďte svoje heslo", "registration.component.form.divider": "alebo", "registration.component.form.email": "e-mail", - "registration.component.form.emailAlreadyExists": "Email už existuje", "registration.component.form.invalidConfirmPass": "Potvrdenie hesla sa nezhoduje s heslom", "registration.component.form.invalidEmail": "Zadaný e-mail je neplatný", "registration.component.form.name": "Názov", @@ -2609,7 +2608,6 @@ "registration.component.form.reasonToJoin": "Dôvod pripojenia", "registration.component.form.sendConfirmationEmail": "Pošlite potvrdzovací e-mail", "registration.component.form.submit": "Predložiť", - "registration.component.form.userAlreadyExist": "Užívateľské meno už existuje. Skúste iné používateľské meno.", "registration.component.form.username": "Používateľské meno", "registration.component.form.usernameAlreadyExists": "Užívateľské meno už existuje. Skúste iné používateľské meno.", "registration.component.login": "Prihlásiť sa", diff --git a/packages/i18n/src/locales/sl-SI.i18n.json b/packages/i18n/src/locales/sl-SI.i18n.json index 5ab895f3a464a..99ea2b8585cb6 100644 --- a/packages/i18n/src/locales/sl-SI.i18n.json +++ b/packages/i18n/src/locales/sl-SI.i18n.json @@ -2587,7 +2587,6 @@ "registration.component.form.confirmPassword": "Potrdi geslo", "registration.component.form.divider": "ali", "registration.component.form.email": "E-poštni naslov", - "registration.component.form.emailAlreadyExists": "E-potšni naslov že obstaja", "registration.component.form.invalidConfirmPass": "Potrditev gesla se ne ujema z geslom", "registration.component.form.invalidEmail": "Vneseni e-poštni naslov je neveljaven", "registration.component.form.name": "Ime", @@ -2596,7 +2595,6 @@ "registration.component.form.register": "Registriraj novi račun", "registration.component.form.sendConfirmationEmail": "Pošlji potrditveno e-poštno sporočilo", "registration.component.form.submit": "Pošlji", - "registration.component.form.userAlreadyExist": "Uporabniško ime že obstaja. Poskusite drugo uporabniško ime.", "registration.component.form.username": "Uporabniško ime", "registration.component.form.usernameAlreadyExists": "Uporabniško ime že obstaja. Poskusite drugo uporabniško ime.", "registration.component.login": "Prijava", diff --git a/packages/i18n/src/locales/sq.i18n.json b/packages/i18n/src/locales/sq.i18n.json index 3ae8a0a5dcb73..c67e899e79496 100644 --- a/packages/i18n/src/locales/sq.i18n.json +++ b/packages/i18n/src/locales/sq.i18n.json @@ -2597,7 +2597,6 @@ "quote": "citoj", "registration.component.form.confirmPassword": "Konfirmoni fjalëkalimin tuaj", "registration.component.form.divider": "ose", - "registration.component.form.emailAlreadyExists": "Emaili ekziston", "registration.component.form.invalidConfirmPass": "Konfirmimi Fjalëkalimi nuk përputhet me fjalëkalimin", "registration.component.form.invalidEmail": "Email-i vendosur është i pavlefshëm", "registration.component.form.name": "Emër", @@ -2606,7 +2605,6 @@ "registration.component.form.register": "Regjistro një adresë të re", "registration.component.form.sendConfirmationEmail": "Dërgo email konfirmimi", "registration.component.form.submit": "Paraqit", - "registration.component.form.userAlreadyExist": "Emri i përdoruesit tashmë ekziston. Të lutem provo një tjetër emër përdoruesi.", "registration.component.form.username": "Emri i përdoruesit", "registration.component.form.usernameAlreadyExists": "Emri i përdoruesit tashmë ekziston. Të lutem provo një tjetër emër përdoruesi.", "registration.component.login": "Hyrje", diff --git a/packages/i18n/src/locales/sr.i18n.json b/packages/i18n/src/locales/sr.i18n.json index ab54814b70937..da11e029178cd 100644 --- a/packages/i18n/src/locales/sr.i18n.json +++ b/packages/i18n/src/locales/sr.i18n.json @@ -2408,7 +2408,6 @@ "registration.component.form.confirmPassword": "Потврдите лозинку", "registration.component.form.divider": "или", "registration.component.form.email": "Е-пошта", - "registration.component.form.emailAlreadyExists": "Е-пошта већ постоји", "registration.component.form.invalidConfirmPass": "Потврдна лозинка се не поклапа са лозинком", "registration.component.form.invalidEmail": "Унета је неисправна адреса е-поште", "registration.component.form.name": "Име", @@ -2417,7 +2416,6 @@ "registration.component.form.register": "Направите нови налог", "registration.component.form.sendConfirmationEmail": "Пошаљи потврдну поруку", "registration.component.form.submit": "Пошаљи", - "registration.component.form.userAlreadyExist": "Корисничко име већ постоји. Молимо покушајте друго корисничко име.", "registration.component.form.username": "Корисничко име", "registration.component.form.usernameAlreadyExists": "Корисничко име већ постоји. Молимо покушајте друго корисничко име.", "registration.component.login": "Пријава", diff --git a/packages/i18n/src/locales/sv.i18n.json b/packages/i18n/src/locales/sv.i18n.json index b964ba9d2fce4..222b57d348d97 100644 --- a/packages/i18n/src/locales/sv.i18n.json +++ b/packages/i18n/src/locales/sv.i18n.json @@ -6404,7 +6404,6 @@ "registration.component.form.createAnAccount": "Skapa ett konto", "registration.component.form.divider": "eller", "registration.component.form.email": "E-post", - "registration.component.form.emailAlreadyExists": "E-postadressen finns redan", "registration.component.form.emailOrUsername": "E-postadress eller användarnamn", "registration.component.form.emailPlaceholder": "din@epost.com", "registration.component.form.invalidConfirmPass": "Lösenorden matchar inte varandra", @@ -6419,7 +6418,6 @@ "registration.component.form.requiredField": "Fältet är obligatoriskt", "registration.component.form.sendConfirmationEmail": "Skicka e-postbekräftelse", "registration.component.form.submit": "Skicka", - "registration.component.form.userAlreadyExist": "Användarnamn existerar redan. Vänligen försök med ett annat användarnamn.", "registration.component.form.username": "Användarnamn", "registration.component.form.usernameAlreadyExists": "Användarnamn existerar redan. Vänligen försök med ett annat användarnamn.", "registration.component.form.usernameContainsInvalidChars": "Användarnamnet innehåller ogiltiga tecken", diff --git a/packages/i18n/src/locales/ta-IN.i18n.json b/packages/i18n/src/locales/ta-IN.i18n.json index 3b992211871cf..58d0505fe833c 100644 --- a/packages/i18n/src/locales/ta-IN.i18n.json +++ b/packages/i18n/src/locales/ta-IN.i18n.json @@ -2594,7 +2594,6 @@ "registration.component.form.confirmPassword": "உங்கள் கடவுச்சொல்லை உறுதிப்படுத்துக", "registration.component.form.divider": "அல்லது", "registration.component.form.email": "மின்னஞ்சல்", - "registration.component.form.emailAlreadyExists": "மின்னஞ்சல் ஏற்கனவே உள்ளது", "registration.component.form.emailOrUsername": "மின்னஞ்சல் அல்லது பயனர் பெயர்", "registration.component.form.invalidConfirmPass": "கடவுச்சொல்லை உறுதிப்படுத்தும் கடவுச்சொல் பொருந்தவில்லை", "registration.component.form.invalidEmail": "உள்ளிட்ட மின்னஞ்சல் தவறானது", @@ -2604,7 +2603,6 @@ "registration.component.form.register": "ஒரு புதிய கணக்கு பதிவு", "registration.component.form.sendConfirmationEmail": "உறுதிப்படுத்தும் மின்னஞ்சல் அனுப்பவும்", "registration.component.form.submit": "சமர்ப்பிக்கவும்", - "registration.component.form.userAlreadyExist": "பெயர் ஏற்கனவே உள்ளது. மற்றொரு பயனர்பெயரை முயற்சிக்கவும்.", "registration.component.form.username": "பயனர் பெயர்", "registration.component.form.usernameAlreadyExists": "பெயர் ஏற்கனவே உள்ளது. மற்றொரு பயனர்பெயரை முயற்சிக்கவும்.", "registration.component.login": "உள் நுழை", diff --git a/packages/i18n/src/locales/th-TH.i18n.json b/packages/i18n/src/locales/th-TH.i18n.json index a97133c7474fc..ea433a65a9865 100644 --- a/packages/i18n/src/locales/th-TH.i18n.json +++ b/packages/i18n/src/locales/th-TH.i18n.json @@ -2581,7 +2581,6 @@ "registration.component.form.confirmPassword": "ยืนยันรหัสผ่านของคุณ", "registration.component.form.divider": "หรือ", "registration.component.form.email": "อีเมล์", - "registration.component.form.emailAlreadyExists": "มีอีเมลอยู่แล้ว", "registration.component.form.invalidConfirmPass": "การยืนยันรหัสผ่านไม่ตรงกับรหัสผ่าน", "registration.component.form.invalidEmail": "อีเมลที่ป้อนไม่ถูกต้อง", "registration.component.form.name": "ชื่อ", @@ -2589,7 +2588,6 @@ "registration.component.form.reasonToJoin": "เหตุผลที่เข้าร่วม", "registration.component.form.sendConfirmationEmail": "ส่งอีเมลยืนยัน", "registration.component.form.submit": "เสนอ", - "registration.component.form.userAlreadyExist": "ชื่อผู้ใช้อยู่แล้ว. โปรดลองชื่อผู้ใช้อื่น", "registration.component.form.username": "ชื่อผู้ใช้", "registration.component.form.usernameAlreadyExists": "ชื่อผู้ใช้อยู่แล้ว. โปรดลองชื่อผู้ใช้อื่น", "registration.component.login": "เข้าสู่ระบบ", diff --git a/packages/i18n/src/locales/tr.i18n.json b/packages/i18n/src/locales/tr.i18n.json index 293703c1eb71d..d618bea8999ae 100644 --- a/packages/i18n/src/locales/tr.i18n.json +++ b/packages/i18n/src/locales/tr.i18n.json @@ -3083,7 +3083,6 @@ "registration.component.form.confirmPassword": "Parolanızı onaylayın", "registration.component.form.divider": "ya da", "registration.component.form.email": "E-posta", - "registration.component.form.emailAlreadyExists": "Bu e-posta zaten var", "registration.component.form.invalidConfirmPass": "Şifre doğrulaması şifre ile eşleşmiyor", "registration.component.form.invalidEmail": "Girilen e-posta geçersiz", "registration.component.form.name": "Ad", @@ -3092,7 +3091,6 @@ "registration.component.form.register": "Yeni hesap oluştur", "registration.component.form.sendConfirmationEmail": "Doğrulama e-postası gönder", "registration.component.form.submit": "Gönder", - "registration.component.form.userAlreadyExist": "Kullanıcı adı zaten var. Lütfen başka bir kullanıcı adı deneyin.", "registration.component.form.username": "Kullanıcı Adı", "registration.component.form.usernameAlreadyExists": "Kullanıcı adı zaten var. Lütfen başka bir kullanıcı adı deneyin.", "registration.component.login": "Oturum aç", diff --git a/packages/i18n/src/locales/ug.i18n.json b/packages/i18n/src/locales/ug.i18n.json index 137825534e32c..0f2d583b2aaa3 100644 --- a/packages/i18n/src/locales/ug.i18n.json +++ b/packages/i18n/src/locales/ug.i18n.json @@ -1190,7 +1190,6 @@ "quote": "نەقىل كەلتۈرۈش", "registration.component.form.confirmPassword": "مەخپىي نومۇرنى جەزملەشتۈرۈش", "registration.component.form.email": "ئىلخەت", - "registration.component.form.emailAlreadyExists": "ئىلخەت ئاللىبۇرۇن بار", "registration.component.form.emailOrUsername": "ئىلخەت ياكى ئەزا نامى", "registration.component.form.invalidConfirmPass": "ئىككىنچى قېتىم كىرگۈزۈلگەن مەخپىي نومۇر بىلەن بىرىنچى قېتىم كىرگۈزۈلگىنى بىلەن ماس كەلمىدى.", "registration.component.form.invalidEmail": "كىرگۈزۈلگەن ئىلخەت ئادرېسى ئىناۋەتسىز", diff --git a/packages/i18n/src/locales/uk.i18n.json b/packages/i18n/src/locales/uk.i18n.json index 1a6a66fef1613..50f5786288876 100644 --- a/packages/i18n/src/locales/uk.i18n.json +++ b/packages/i18n/src/locales/uk.i18n.json @@ -3176,7 +3176,6 @@ "registration.component.form.confirmPassword": "Підтвердити новий пароль", "registration.component.form.divider": "або", "registration.component.form.email": "Email", - "registration.component.form.emailAlreadyExists": "Email вже існує", "registration.component.form.emailOrUsername": "Адреса електронної пошти або логін", "registration.component.form.invalidConfirmPass": "Підтвердження пароля не збігаються пароль", "registration.component.form.invalidEmail": "Невірний email", @@ -3186,7 +3185,6 @@ "registration.component.form.register": "Зареєструвати новий обліковий запис ", "registration.component.form.sendConfirmationEmail": "Надіслати електронною поштою підтвердження", "registration.component.form.submit": "Підтвердити", - "registration.component.form.userAlreadyExist": "Ім'я користувача вже існує. Будь ласка, спробуйте інше ім'я користувача.", "registration.component.form.username": "Ім'я користувача", "registration.component.form.usernameAlreadyExists": "Ім'я користувача вже існує. Будь ласка, спробуйте інше ім'я користувача.", "registration.component.login": "Логін", diff --git a/packages/i18n/src/locales/vi-VN.i18n.json b/packages/i18n/src/locales/vi-VN.i18n.json index d079aa74e4989..cd768487cf1b2 100644 --- a/packages/i18n/src/locales/vi-VN.i18n.json +++ b/packages/i18n/src/locales/vi-VN.i18n.json @@ -2697,7 +2697,6 @@ "registration.component.form.confirmPassword": "Xác nhận mật khẩu của bạn", "registration.component.form.divider": "hoặc là", "registration.component.form.email": "E-mail", - "registration.component.form.emailAlreadyExists": "Email đã tồn tại", "registration.component.form.invalidConfirmPass": "Xác nhận mật khẩu không khớp với mật khẩu", "registration.component.form.invalidEmail": "Email nhập vào không hợp lệ", "registration.component.form.name": "Tên", @@ -2705,7 +2704,6 @@ "registration.component.form.reasonToJoin": "Lý do tham gia", "registration.component.form.sendConfirmationEmail": "Gửi email xác nhận", "registration.component.form.submit": "Gửi đi", - "registration.component.form.userAlreadyExist": "Tên này đã có người dùng. Vui lòng thử tên người dùng khác.", "registration.component.form.username": "Tên đăng nhập", "registration.component.form.usernameAlreadyExists": "Tên này đã có người dùng. Vui lòng thử tên người dùng khác.", "registration.component.login": "Đăng nhập", diff --git a/packages/i18n/src/locales/zh-HK.i18n.json b/packages/i18n/src/locales/zh-HK.i18n.json index 49eb9b842a3ee..d5d2370d7113b 100644 --- a/packages/i18n/src/locales/zh-HK.i18n.json +++ b/packages/i18n/src/locales/zh-HK.i18n.json @@ -2613,7 +2613,6 @@ "registration.component.form.confirmPassword": "确认密码", "registration.component.form.divider": "要么", "registration.component.form.email": "电子邮件", - "registration.component.form.emailAlreadyExists": "邮件已存在", "registration.component.form.invalidConfirmPass": "第二次密码输入与第一次不匹配", "registration.component.form.invalidEmail": "无效的电子邮件", "registration.component.form.name": "姓名", @@ -2621,7 +2620,6 @@ "registration.component.form.reasonToJoin": "加入的理由", "registration.component.form.sendConfirmationEmail": "已发送确认电子邮件", "registration.component.form.submit": "提交", - "registration.component.form.userAlreadyExist": "此用户名已存在。请尝试其他用户名。", "registration.component.form.username": "用户名", "registration.component.form.usernameAlreadyExists": "此用户名已存在。请尝试其他用户名。", "registration.component.login": "登录", diff --git a/packages/i18n/src/locales/zh-TW.i18n.json b/packages/i18n/src/locales/zh-TW.i18n.json index 6b1e537e827a0..c31c4a931cf00 100644 --- a/packages/i18n/src/locales/zh-TW.i18n.json +++ b/packages/i18n/src/locales/zh-TW.i18n.json @@ -4324,7 +4324,6 @@ "registration.component.form.confirmation": "確認", "registration.component.form.divider": "或", "registration.component.form.email": "電子郵件", - "registration.component.form.emailAlreadyExists": "電子郵件已存在", "registration.component.form.invalidConfirmPass": "確認密碼與密碼不相符", "registration.component.form.invalidEmail": "電子郵件無效", "registration.component.form.name": "姓名", @@ -4334,7 +4333,6 @@ "registration.component.form.requiredField": "此為必填欄位", "registration.component.form.sendConfirmationEmail": "已傳送確認電子郵件", "registration.component.form.submit": "送出", - "registration.component.form.userAlreadyExist": "此使用者名稱已存在。請嘗試其他使用者名稱。", "registration.component.form.username": "使用者名稱", "registration.component.form.usernameAlreadyExists": "此使用者名稱已存在。請嘗試其他使用者名稱。", "registration.component.login": "登入", diff --git a/packages/i18n/src/locales/zh.i18n.json b/packages/i18n/src/locales/zh.i18n.json index 7016955437aa8..eafcf9600928d 100644 --- a/packages/i18n/src/locales/zh.i18n.json +++ b/packages/i18n/src/locales/zh.i18n.json @@ -6667,7 +6667,6 @@ "registration.component.form.createAnAccount": "创建账户", "registration.component.form.divider": "或", "registration.component.form.email": "电子邮箱", - "registration.component.form.emailAlreadyExists": "邮箱已存在", "registration.component.form.emailOrUsername": "电子邮件或用户名", "registration.component.form.emailPlaceholder": "示例:example@example.com", "registration.component.form.invalidConfirmPass": "两次输入的密码不一致", @@ -6682,7 +6681,6 @@ "registration.component.form.requiredField": "此字段为必填项", "registration.component.form.sendConfirmationEmail": "已发送确认电子邮件", "registration.component.form.submit": "提交", - "registration.component.form.userAlreadyExist": "此用户名已存在。请尝试其他用户名。", "registration.component.form.username": "用户名", "registration.component.form.usernameAlreadyExists": "此用户名已存在。请尝试其他用户名。", "registration.component.form.usernameContainsInvalidChars": "用户名包含无效字符", diff --git a/packages/web-ui-registration/src/RegisterForm.tsx b/packages/web-ui-registration/src/RegisterForm.tsx index 4efbb0fdaff6b..c86b53a34e306 100644 --- a/packages/web-ui-registration/src/RegisterForm.tsx +++ b/packages/web-ui-registration/src/RegisterForm.tsx @@ -97,10 +97,10 @@ export const RegisterForm = ({ setLoginRoute }: { setLoginRoute: DispatchLoginRo setError('email', { type: 'invalid-domain', message: t('registration.component.form.invalidEmailDomain') }); } if (/Email already exists/.test(error.error)) { - setError('email', { type: 'email-already-exists', message: t('registration.component.form.emailAlreadyExists') }); + setError('email', { type: 'email-already-exists', message: t('registration.component.form.emailAlreadyInUse') }); } if (/Username is already in use/.test(error.error)) { - setError('username', { type: 'username-already-exists', message: t('registration.component.form.userAlreadyExist') }); + setError('username', { type: 'username-already-exists', message: t('registration.component.form.usernameAlreadyInUse') }); } if (/The username provided is not valid/.test(error.error)) { setError('username', { From 37a7602de855efef3ae7800780eaaa5f5215240b Mon Sep 17 00:00:00 2001 From: Ricardo Garim <rswarovsky@gmail.com> Date: Wed, 25 Feb 2026 21:38:09 -0300 Subject: [PATCH 064/108] fix: filter versionUpdate banners where version <= current installed (#38932) --- .changeset/olive-hairs-report.md | 5 + .../api/server/helpers/getUserInfo.spec.ts | 56 ++++++++ .../app/api/server/helpers/getUserInfo.ts | 20 +++ .../buildVersionUpdateMessage.spec.ts | 123 ++++++++++++++++++ .../functions/buildVersionUpdateMessage.ts | 43 +++++- apps/meteor/jest.config.ts | 1 + .../model-typings/src/models/IUsersModel.ts | 2 + packages/models/src/models/Users.ts | 12 ++ 8 files changed, 259 insertions(+), 3 deletions(-) create mode 100644 .changeset/olive-hairs-report.md create mode 100644 apps/meteor/app/version-check/server/functions/buildVersionUpdateMessage.spec.ts diff --git a/.changeset/olive-hairs-report.md b/.changeset/olive-hairs-report.md new file mode 100644 index 0000000000000..fff0535e67cbc --- /dev/null +++ b/.changeset/olive-hairs-report.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes version update banner showing outdated versions after server upgrade. diff --git a/apps/meteor/app/api/server/helpers/getUserInfo.spec.ts b/apps/meteor/app/api/server/helpers/getUserInfo.spec.ts index eaf6122fb99c4..4d00e1292f62f 100644 --- a/apps/meteor/app/api/server/helpers/getUserInfo.spec.ts +++ b/apps/meteor/app/api/server/helpers/getUserInfo.spec.ts @@ -1,6 +1,16 @@ import { getUserInfo } from './getUserInfo'; import type { CachedSettings } from '../../../settings/server/CachedSettings'; +const mockInfoVersion = jest.fn(() => '7.5.0'); + +jest.mock('../../../utils/rocketchat.info', () => ({ + Info: { + get version() { + return mockInfoVersion(); + }, + }, +})); + jest.mock('@rocket.chat/models', () => ({ Users: { findOneById: jest.fn().mockResolvedValue({ @@ -198,4 +208,50 @@ describe('getUserInfo', () => { }); }); }); + + describe('version update banner filtering', () => { + beforeEach(() => { + mockInfoVersion.mockReturnValue('7.5.0'); + }); + + it('should filter out versionUpdate banners for versions <= current installed', async () => { + user.banners = { + 'versionUpdate-6_2_0': { + id: 'versionUpdate-6_2_0', + priority: 10, + title: 'Update', + text: 'New version', + modifiers: [], + link: '', + read: false, + }, + }; + const userInfo = await getUserInfo(user); + expect(userInfo.banners).toEqual({}); + }); + + it('should keep versionUpdate banners for versions > current installed', async () => { + user.banners = { + 'versionUpdate-8_0_0': { + id: 'versionUpdate-8_0_0', + priority: 10, + title: 'Update', + text: 'New version', + modifiers: [], + link: '', + read: false, + }, + }; + const userInfo = await getUserInfo(user); + expect(userInfo.banners).toHaveProperty('versionUpdate-8_0_0'); + }); + + it('should keep non-versionUpdate banners unchanged', async () => { + user.banners = { + 'other-banner': { id: 'other-banner', priority: 10, title: 'Other', text: 'Other banner', modifiers: [], link: '', read: false }, + }; + const userInfo = await getUserInfo(user); + expect(userInfo.banners).toHaveProperty('other-banner'); + }); + }); }); diff --git a/apps/meteor/app/api/server/helpers/getUserInfo.ts b/apps/meteor/app/api/server/helpers/getUserInfo.ts index beed975452c7a..4c0e338d69ae1 100644 --- a/apps/meteor/app/api/server/helpers/getUserInfo.ts +++ b/apps/meteor/app/api/server/helpers/getUserInfo.ts @@ -1,6 +1,8 @@ import { isOAuthUser, type IUser, type IUserEmail, type IUserCalendar } from '@rocket.chat/core-typings'; +import semver from 'semver'; import { settings } from '../../../settings/server'; +import { Info } from '../../../utils/rocketchat.info'; import { getURL } from '../../../utils/server/getURL'; import { getUserPreference } from '../../../utils/server/lib/getUserPreference'; @@ -25,6 +27,23 @@ const getUserPreferences = async (me: IUser): Promise<Record<string, unknown>> = return accumulator; }; +const filterOutdatedVersionUpdateBanners = (banners: NonNullable<IUser['banners']>): IUser['banners'] => { + return Object.fromEntries( + Object.entries(banners).filter(([id]) => { + if (!id.startsWith('versionUpdate-')) { + return true; + } + + const version = id.replace('versionUpdate-', '').replace(/_/g, '.'); + if (!semver.valid(version) || semver.lte(version, Info.version)) { + return false; + } + + return true; + }), + ); +}; + /** * Returns the user's calendar settings based on their email domain and the configured mapping. * If the email is not provided or the domain is not found in the mapping, @@ -80,6 +99,7 @@ export async function getUserInfo( return { ...me, + ...(me.banners && { banners: filterOutdatedVersionUpdateBanners(me.banners) }), email: verifiedEmail ? verifiedEmail.address : undefined, settings: { profile: {}, diff --git a/apps/meteor/app/version-check/server/functions/buildVersionUpdateMessage.spec.ts b/apps/meteor/app/version-check/server/functions/buildVersionUpdateMessage.spec.ts new file mode 100644 index 0000000000000..0f8f8198e2d5f --- /dev/null +++ b/apps/meteor/app/version-check/server/functions/buildVersionUpdateMessage.spec.ts @@ -0,0 +1,123 @@ +import { buildVersionUpdateMessage } from './buildVersionUpdateMessage'; +import { sendMessagesToAdmins } from '../../../../server/lib/sendMessagesToAdmins'; + +const originalTestMode = process.env.TEST_MODE; + +const mockInfoVersion = jest.fn(() => '7.5.0'); + +jest.mock('../../../utils/rocketchat.info', () => ({ + Info: { + get version() { + return mockInfoVersion(); + }, + }, +})); + +const mockSetBannersInBulk = jest.fn(); +const mockFindUsersInRolesWithQuery = jest.fn(); + +jest.mock('@rocket.chat/models', () => ({ + Settings: { + updateValueById: jest.fn().mockResolvedValue({ modifiedCount: 0 }), + }, + Users: { + findUsersInRolesWithQuery: () => mockFindUsersInRolesWithQuery(), + setBannersInBulk: (updates: unknown) => mockSetBannersInBulk(updates), + }, +})); + +const mockSettingsGet = jest.fn(); + +jest.mock('../../../settings/server', () => ({ + settings: { + get: (key: string) => mockSettingsGet(key), + }, +})); + +jest.mock('../../../../server/lib/i18n', () => ({ + i18n: { + t: jest.fn((key) => key), + }, +})); + +jest.mock('../../../../server/lib/sendMessagesToAdmins', () => ({ + sendMessagesToAdmins: jest.fn(), +})); + +jest.mock('../../../../server/settings/lib/auditedSettingUpdates', () => ({ + updateAuditedBySystem: jest.fn(() => () => Promise.resolve({ modifiedCount: 0 })), +})); + +jest.mock('../../../lib/server/lib/notifyListener', () => ({ + notifyOnSettingChangedById: jest.fn(), +})); + +describe('buildVersionUpdateMessage', () => { + // Delete the TEST_MODE environment variable so buildVersionUpdateMessage() + // doesn't return early (see line 40 in buildVersionUpdateMessage.ts) + beforeAll(() => { + delete process.env.TEST_MODE; + }); + + afterAll(() => { + process.env.TEST_MODE = originalTestMode; + }); + + beforeEach(() => { + jest.clearAllMocks(); + mockInfoVersion.mockReturnValue('7.5.0'); + mockSettingsGet.mockReturnValue('7.0.0'); + }); + + describe('cleanupOutdatedVersionUpdateBanners', () => { + it('should remove outdated version banners (<= current installed)', async () => { + const admin = { _id: 'admin1', banners: { 'versionUpdate-6_2_0': { id: 'versionUpdate-6_2_0' } } }; + mockFindUsersInRolesWithQuery.mockReturnValue([admin]); + + await buildVersionUpdateMessage([]); + + expect(mockSetBannersInBulk).toHaveBeenCalledWith([{ userId: 'admin1', banners: {} }]); + }); + + it('should keep version banners > current installed', async () => { + const admin = { _id: 'admin1', banners: { 'versionUpdate-8_0_0': { id: 'versionUpdate-8_0_0' } } }; + mockFindUsersInRolesWithQuery.mockReturnValue([admin]); + + await buildVersionUpdateMessage([]); + + expect(mockSetBannersInBulk).not.toHaveBeenCalled(); + }); + + it('should remove banners with invalid semver version IDs', async () => { + const admin = { _id: 'admin1', banners: { 'versionUpdate-invalid_version': { id: 'versionUpdate-invalid_version' } } }; + mockFindUsersInRolesWithQuery.mockReturnValue([admin]); + + await buildVersionUpdateMessage([]); + + expect(mockSetBannersInBulk).toHaveBeenCalledWith([{ userId: 'admin1', banners: {} }]); + }); + }); + + describe('version sorting', () => { + it('should process versions in descending order (highest first)', async () => { + mockFindUsersInRolesWithQuery.mockReturnValue([]); + + await buildVersionUpdateMessage([ + { version: '7.6.0', security: false, infoUrl: 'https://example.com/7.6.0' }, + { version: '8.0.0', security: false, infoUrl: 'https://example.com/8.0.0' }, + { version: '7.8.0', security: false, infoUrl: 'https://example.com/7.8.0' }, + ]); + + expect(sendMessagesToAdmins).toHaveBeenCalledTimes(1); + expect(sendMessagesToAdmins).toHaveBeenCalledWith( + expect.objectContaining({ + banners: expect.arrayContaining([ + expect.objectContaining({ + id: 'versionUpdate-8_0_0', + }), + ]), + }), + ); + }); + }); +}); diff --git a/apps/meteor/app/version-check/server/functions/buildVersionUpdateMessage.ts b/apps/meteor/app/version-check/server/functions/buildVersionUpdateMessage.ts index be53cee5959af..9811ae1ec94a6 100644 --- a/apps/meteor/app/version-check/server/functions/buildVersionUpdateMessage.ts +++ b/apps/meteor/app/version-check/server/functions/buildVersionUpdateMessage.ts @@ -1,4 +1,5 @@ -import { Settings } from '@rocket.chat/models'; +import type { IUser } from '@rocket.chat/core-typings'; +import { Settings, Users } from '@rocket.chat/models'; import semver from 'semver'; import { i18n } from '../../../../server/lib/i18n'; @@ -8,6 +9,39 @@ import { notifyOnSettingChangedById } from '../../../lib/server/lib/notifyListen import { settings } from '../../../settings/server'; import { Info } from '../../../utils/rocketchat.info'; +const cleanupOutdatedVersionUpdateBanners = async (): Promise<void> => { + const admins = Users.findUsersInRolesWithQuery('admin', { banners: { $exists: true } }, { projection: { _id: 1, banners: 1 } }); + + const updates: { userId: IUser['_id']; banners: NonNullable<IUser['banners']> }[] = []; + + for await (const admin of admins) { + if (!admin.banners) { + continue; + } + + const filteredBanners = Object.fromEntries( + Object.entries(admin.banners).filter(([bannerId]) => { + if (!bannerId.startsWith('versionUpdate-')) { + return true; + } + const version = bannerId.replace('versionUpdate-', '').replace(/_/g, '.'); + if (!semver.valid(version) || semver.lte(version, Info.version)) { + return false; + } + return true; + }), + ); + + if (Object.keys(filteredBanners).length !== Object.keys(admin.banners).length) { + updates.push({ userId: admin._id, banners: filteredBanners }); + } + } + + if (updates.length > 0) { + await Users.setBannersInBulk(updates); + } +}; + export const buildVersionUpdateMessage = async ( versions: { version: string; @@ -25,8 +59,11 @@ export const buildVersionUpdateMessage = async ( return; } - for await (const version of versions) { - // Ignore prerelease versions + const sortedVersions = [...versions].sort((a, b) => semver.rcompare(a.version, b.version)); + + await cleanupOutdatedVersionUpdateBanners(); + + for await (const version of sortedVersions) { if (semver.prerelease(version.version)) { continue; } diff --git a/apps/meteor/jest.config.ts b/apps/meteor/jest.config.ts index e7b2746d65fae..cb6261662258a 100644 --- a/apps/meteor/jest.config.ts +++ b/apps/meteor/jest.config.ts @@ -48,6 +48,7 @@ export default { '<rootDir>/app/api/server/**.spec.ts', '<rootDir>/app/api/server/helpers/**.spec.ts', '<rootDir>/app/api/server/middlewares/**.spec.ts', + '<rootDir>/app/version-check/server/**/*.spec.ts', ], coveragePathIgnorePatterns: ['/node_modules/'], }, diff --git a/packages/model-typings/src/models/IUsersModel.ts b/packages/model-typings/src/models/IUsersModel.ts index 174a4115c9955..31b723e24c63e 100644 --- a/packages/model-typings/src/models/IUsersModel.ts +++ b/packages/model-typings/src/models/IUsersModel.ts @@ -21,6 +21,7 @@ import type { WithId, UpdateOptions, UpdateFilter, + BulkWriteResult, } from 'mongodb'; import type { FindPaginated, IBaseModel } from './IBaseModel'; @@ -397,6 +398,7 @@ export interface IUsersModel extends IBaseModel<IUser> { bannerExistsById(userId: string, bannerId: string): Promise<boolean>; setBannerReadById(userId: string, bannerId: string): Promise<UpdateResult>; removeBannerById(userId: string, bannerId: string): Promise<UpdateResult>; + setBannersInBulk(updates: { userId: IUser['_id']; banners: NonNullable<IUser['banners']> }[]): Promise<BulkWriteResult>; removeSamlServiceSession(userId: string): Promise<UpdateResult>; updateDefaultStatus(userId: string, status: string): Promise<UpdateResult>; setSamlInResponseTo(userId: string, inResponseTo: string): Promise<UpdateResult>; diff --git a/packages/models/src/models/Users.ts b/packages/models/src/models/Users.ts index 40dd0b0b4e0ae..d8ff89c93fa7e 100644 --- a/packages/models/src/models/Users.ts +++ b/packages/models/src/models/Users.ts @@ -26,6 +26,7 @@ import type { FindCursor, SortDirection, FindOneAndUpdateOptions, + AnyBulkWriteOperation, } from 'mongodb'; import { Rooms, Subscriptions } from '../index'; @@ -3155,6 +3156,17 @@ export class UsersRaw extends BaseRaw<IUser, DefaultFields<IUser>> implements IU return this.updateOne({ _id }, update); } + async setBannersInBulk(updates: { userId: IUser['_id']; banners: NonNullable<IUser['banners']> }[]) { + const ops: AnyBulkWriteOperation<IUser>[] = updates.map(({ userId, banners }) => ({ + updateOne: { + filter: { _id: userId }, + update: { $set: { banners } }, + }, + })); + + return this.col.bulkWrite(ops); + } + removeSamlServiceSession(_id: IUser['_id']) { const update = { $unset: { From 7bd82140d9e7d3767b878f1acb2bdcb2c489faee Mon Sep 17 00:00:00 2001 From: Robin Schneider <45321827+robinschneider@users.noreply.github.com> Date: Thu, 26 Feb 2026 01:38:37 +0100 Subject: [PATCH 065/108] chore: add opencontainers source label for renovate (#38582) Co-authored-by: Guilherme Gazzo <guilherme@gazzo.xyz> --- .github/actions/build-docker/action.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/actions/build-docker/action.yml b/.github/actions/build-docker/action.yml index 12bd993c665da..274cb7ac98c49 100644 --- a/.github/actions/build-docker/action.yml +++ b/.github/actions/build-docker/action.yml @@ -108,6 +108,7 @@ runs: --allow=fs.read=/tmp/build \ --set "*.tags+=${IMAGE}-gha-run-${{ github.run_id }}" \ --set "*.labels.org.opencontainers.image.description=Build run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \ + --set "*.labels.org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}" \ --set *.platform=linux/${{ inputs.arch }} \ --set *.cache-from=type=gha \ --set *.cache-to=type=gha,mode=max \ From 6c2bc719098262313cb53414312a84249af9b3dd Mon Sep 17 00:00:00 2001 From: ergot-rp <erik.gothe@redpill-linpro.com> Date: Thu, 26 Feb 2026 01:39:08 +0100 Subject: [PATCH 066/108] =?UTF-8?q?fix:=20WCAG=20(1.1.1)=20Add=20`alt`=1Bp?= =?UTF-8?q?roperty=20to=20user=20avatar=20(#35969)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: erik <erik.gothe@fotbollsdata.se> Co-authored-by: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Co-authored-by: Douglas Fabris <devfabris@gmail.com> --- .../__snapshots__/UserAvatarChip.spec.tsx.snap | 6 +++--- .../UserInfo/__snapshots__/UserInfo.spec.tsx.snap | 8 ++++---- .../__snapshots__/UsersInRoleTable.spec.tsx.snap | 10 +++++----- .../UsersTable/__snapshots__/UsersTable.spec.tsx.snap | 10 +++++----- .../SecurityLogDisplayModal.spec.tsx.snap | 2 +- .../body/__snapshots__/RoomInviteBody.spec.tsx.snap | 6 +++--- packages/ui-avatar/src/components/UserAvatar.tsx | 6 ++++-- 7 files changed, 25 insertions(+), 23 deletions(-) diff --git a/apps/meteor/client/components/UserAutoCompleteMultiple/__snapshots__/UserAvatarChip.spec.tsx.snap b/apps/meteor/client/components/UserAutoCompleteMultiple/__snapshots__/UserAvatarChip.spec.tsx.snap index a55dbb1d84acd..6b7320236d10d 100644 --- a/apps/meteor/client/components/UserAutoCompleteMultiple/__snapshots__/UserAvatarChip.spec.tsx.snap +++ b/apps/meteor/client/components/UserAutoCompleteMultiple/__snapshots__/UserAvatarChip.spec.tsx.snap @@ -14,7 +14,7 @@ exports[`UserAvatarChip renders Default without crashing 1`] = ` class="rcx-box rcx-box--full rcx-avatar rcx-avatar--x20" > <img - alt="" + alt="Avatar" aria-hidden="true" class="rcx-avatar__element rcx-avatar__element--x20" data-username="johndoe" @@ -87,7 +87,7 @@ exports[`UserAvatarChip renders WithoutClickEvent without crashing 1`] = ` class="rcx-box rcx-box--full rcx-avatar rcx-avatar--x20" > <img - alt="" + alt="Avatar" aria-hidden="true" class="rcx-avatar__element rcx-avatar__element--x20" data-username="johndoe" @@ -120,7 +120,7 @@ exports[`UserAvatarChip renders WithoutName without crashing 1`] = ` class="rcx-box rcx-box--full rcx-avatar rcx-avatar--x20" > <img - alt="" + alt="Avatar" aria-hidden="true" class="rcx-avatar__element rcx-avatar__element--x20" data-username="johndoe" diff --git a/apps/meteor/client/components/UserInfo/__snapshots__/UserInfo.spec.tsx.snap b/apps/meteor/client/components/UserInfo/__snapshots__/UserInfo.spec.tsx.snap index b8990e18c6234..7f31acf80c25b 100644 --- a/apps/meteor/client/components/UserInfo/__snapshots__/UserInfo.spec.tsx.snap +++ b/apps/meteor/client/components/UserInfo/__snapshots__/UserInfo.spec.tsx.snap @@ -50,7 +50,7 @@ exports[`renders Default without crashing 1`] = ` class="rcx-box rcx-box--full rcx-avatar rcx-avatar--x332" > <img - alt="" + alt="Avatar" aria-hidden="true" class="rcx-avatar__element rcx-avatar__element--x332" data-username="guilherme.gazzo" @@ -353,7 +353,7 @@ exports[`renders InvitedUser without crashing 1`] = ` class="rcx-box rcx-box--full rcx-avatar rcx-avatar--x332" > <img - alt="" + alt="Avatar" aria-hidden="true" class="rcx-avatar__element rcx-avatar__element--x332" data-username="guilherme.gazzo" @@ -670,7 +670,7 @@ exports[`renders WithABACAttributes without crashing 1`] = ` class="rcx-box rcx-box--full rcx-avatar rcx-avatar--x332" > <img - alt="" + alt="Avatar" aria-hidden="true" class="rcx-avatar__element rcx-avatar__element--x332" data-username="guilherme.gazzo" @@ -1033,7 +1033,7 @@ exports[`renders WithVoiceCallExtension without crashing 1`] = ` class="rcx-box rcx-box--full rcx-avatar rcx-avatar--x332" > <img - alt="" + alt="Avatar" aria-hidden="true" class="rcx-avatar__element rcx-avatar__element--x332" data-username="guilherme.gazzo" diff --git a/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRoleTable/__snapshots__/UsersInRoleTable.spec.tsx.snap b/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRoleTable/__snapshots__/UsersInRoleTable.spec.tsx.snap index e1a1e54446fca..6a47d55c4a414 100644 --- a/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRoleTable/__snapshots__/UsersInRoleTable.spec.tsx.snap +++ b/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRoleTable/__snapshots__/UsersInRoleTable.spec.tsx.snap @@ -88,7 +88,7 @@ exports[`renders Default without crashing 1`] = ` class="rcx-box rcx-box--full rcx-avatar rcx-avatar--x40" > <img - alt="" + alt="Avatar" aria-hidden="true" class="rcx-avatar__element rcx-avatar__element--x40" data-username="user.1" @@ -149,7 +149,7 @@ exports[`renders Default without crashing 1`] = ` class="rcx-box rcx-box--full rcx-avatar rcx-avatar--x40" > <img - alt="" + alt="Avatar" aria-hidden="true" class="rcx-avatar__element rcx-avatar__element--x40" data-username="user.2" @@ -210,7 +210,7 @@ exports[`renders Default without crashing 1`] = ` class="rcx-box rcx-box--full rcx-avatar rcx-avatar--x40" > <img - alt="" + alt="Avatar" aria-hidden="true" class="rcx-avatar__element rcx-avatar__element--x40" data-username="user.3" @@ -271,7 +271,7 @@ exports[`renders Default without crashing 1`] = ` class="rcx-box rcx-box--full rcx-avatar rcx-avatar--x40" > <img - alt="" + alt="Avatar" aria-hidden="true" class="rcx-avatar__element rcx-avatar__element--x40" data-username="user.4" @@ -332,7 +332,7 @@ exports[`renders Default without crashing 1`] = ` class="rcx-box rcx-box--full rcx-avatar rcx-avatar--x40" > <img - alt="" + alt="Avatar" aria-hidden="true" class="rcx-avatar__element rcx-avatar__element--x40" data-username="user.5" diff --git a/apps/meteor/client/views/admin/users/UsersTable/__snapshots__/UsersTable.spec.tsx.snap b/apps/meteor/client/views/admin/users/UsersTable/__snapshots__/UsersTable.spec.tsx.snap index 8354272d00c79..918b954a49ac7 100644 --- a/apps/meteor/client/views/admin/users/UsersTable/__snapshots__/UsersTable.spec.tsx.snap +++ b/apps/meteor/client/views/admin/users/UsersTable/__snapshots__/UsersTable.spec.tsx.snap @@ -175,7 +175,7 @@ exports[`renders Default without crashing 1`] = ` class="rcx-box rcx-box--full rcx-avatar rcx-avatar--x28" > <img - alt="" + alt="Avatar" aria-hidden="true" class="rcx-avatar__element rcx-avatar__element--x28" data-username="example.user" @@ -259,7 +259,7 @@ exports[`renders Default without crashing 1`] = ` class="rcx-box rcx-box--full rcx-avatar rcx-avatar--x28" > <img - alt="" + alt="Avatar" aria-hidden="true" class="rcx-avatar__element rcx-avatar__element--x28" data-username="john.doe" @@ -348,7 +348,7 @@ exports[`renders Default without crashing 1`] = ` class="rcx-box rcx-box--full rcx-avatar rcx-avatar--x28" > <img - alt="" + alt="Avatar" aria-hidden="true" class="rcx-avatar__element rcx-avatar__element--x28" data-username="sarah.smith" @@ -434,7 +434,7 @@ exports[`renders Default without crashing 1`] = ` class="rcx-box rcx-box--full rcx-avatar rcx-avatar--x28" > <img - alt="" + alt="Avatar" aria-hidden="true" class="rcx-avatar__element rcx-avatar__element--x28" data-username="mike.wilson" @@ -520,7 +520,7 @@ exports[`renders Default without crashing 1`] = ` class="rcx-box rcx-box--full rcx-avatar rcx-avatar--x28" > <img - alt="" + alt="Avatar" aria-hidden="true" class="rcx-avatar__element rcx-avatar__element--x28" data-username="emma.davis" diff --git a/apps/meteor/client/views/audit/components/__snapshots__/SecurityLogDisplayModal.spec.tsx.snap b/apps/meteor/client/views/audit/components/__snapshots__/SecurityLogDisplayModal.spec.tsx.snap index 8f1183e87a237..caeb092cd80ce 100644 --- a/apps/meteor/client/views/audit/components/__snapshots__/SecurityLogDisplayModal.spec.tsx.snap +++ b/apps/meteor/client/views/audit/components/__snapshots__/SecurityLogDisplayModal.spec.tsx.snap @@ -65,7 +65,7 @@ exports[`renders Default without crashing 1`] = ` class="rcx-box rcx-box--full rcx-avatar rcx-avatar--x24" > <img - alt="" + alt="Avatar" aria-hidden="true" class="rcx-avatar__element rcx-avatar__element--x24" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQYV2Oora39DwAFaQJ3y3rKeAAAAABJRU5ErkJggg==" diff --git a/apps/meteor/client/views/room/body/__snapshots__/RoomInviteBody.spec.tsx.snap b/apps/meteor/client/views/room/body/__snapshots__/RoomInviteBody.spec.tsx.snap index 8e9de74d07856..a5c317118b8ff 100644 --- a/apps/meteor/client/views/room/body/__snapshots__/RoomInviteBody.spec.tsx.snap +++ b/apps/meteor/client/views/room/body/__snapshots__/RoomInviteBody.spec.tsx.snap @@ -40,7 +40,7 @@ exports[`RoomInvite renders Default without crashing 1`] = ` class="rcx-box rcx-box--full rcx-avatar rcx-avatar--x16" > <img - alt="" + alt="Avatar" aria-hidden="true" class="rcx-avatar__element rcx-avatar__element--x16" data-username="rocket.cat" @@ -124,7 +124,7 @@ exports[`RoomInvite renders Loading without crashing 1`] = ` class="rcx-box rcx-box--full rcx-avatar rcx-avatar--x16" > <img - alt="" + alt="Avatar" aria-hidden="true" class="rcx-avatar__element rcx-avatar__element--x16" data-username="rocket.cat" @@ -222,7 +222,7 @@ exports[`RoomInvite renders WithInfoLink without crashing 1`] = ` class="rcx-box rcx-box--full rcx-avatar rcx-avatar--x16" > <img - alt="" + alt="Avatar" aria-hidden="true" class="rcx-avatar__element rcx-avatar__element--x16" data-username="rocket.cat" diff --git a/packages/ui-avatar/src/components/UserAvatar.tsx b/packages/ui-avatar/src/components/UserAvatar.tsx index b4f5ae85a6587..a82916a555e30 100644 --- a/packages/ui-avatar/src/components/UserAvatar.tsx +++ b/packages/ui-avatar/src/components/UserAvatar.tsx @@ -1,5 +1,6 @@ import { useUserAvatarPath } from '@rocket.chat/ui-contexts'; import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; import type { BaseAvatarProps } from './BaseAvatar'; import BaseAvatar from './BaseAvatar'; @@ -21,14 +22,15 @@ type UserAvatarProps = Omit<BaseAvatarProps, 'url' | 'title'> & { const UserAvatar = ({ username, userId, etag, ...rest }: UserAvatarProps) => { const getUserAvatarPath = useUserAvatarPath(); + const { t } = useTranslation(); if (userId) { const { url = getUserAvatarPath({ userId, etag }), ...props } = rest; - return <BaseAvatar url={url} {...props} />; + return <BaseAvatar url={url} alt={t('Avatar')} {...props} />; } if (username) { const { url = getUserAvatarPath({ username, etag }), ...props } = rest; - return <BaseAvatar url={url} data-username={username} title={username} {...props} />; + return <BaseAvatar url={url} data-username={username} title={username} alt={t('Avatar')} {...props} />; } // TODO: We should throw an Error after fixing the issue in Composer passing the username undefined From 898d785bb86a5c2abd064b9c3c900b15dc2530c0 Mon Sep 17 00:00:00 2001 From: Amit Kumar Ashutosh <73929517+amitkumarashutosh@users.noreply.github.com> Date: Thu, 26 Feb 2026 06:09:30 +0530 Subject: [PATCH 067/108] chore: toggle option for formatting buttons and shortcuts (#35336) --- .../app/ui-message/client/messageBox/createComposerAPI.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts b/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts index a7d92ed3f02f3..ed282ce3c75aa 100644 --- a/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts +++ b/apps/meteor/app/ui-message/client/messageBox/createComposerAPI.ts @@ -230,11 +230,11 @@ export const createComposerAPI = ( focus(); const startPattern = pattern.slice(0, pattern.indexOf('{{text}}')); - const startPatternFound = [...startPattern].reverse().every((char, index) => input.value.slice(selectionStart - index - 1, 1) === char); + const startPatternFound = input.value.slice(selectionStart - startPattern.length, selectionStart) === startPattern; if (startPatternFound) { const endPattern = pattern.slice(pattern.indexOf('{{text}}') + '{{text}}'.length); - const endPatternFound = [...endPattern].every((char, index) => input.value.slice(selectionEnd + index, 1) === char); + const endPatternFound = input.value.slice(selectionEnd, selectionEnd + endPattern.length) === endPattern; if (endPatternFound) { insertText(selectedText); From 9a70095296dbf516b0113a9a65e09f25137b2eaf Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 21:48:51 -0300 Subject: [PATCH 068/108] fix: strip trailing punctuation from URLs at end of message (#39069) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ggazzo <5263975+ggazzo@users.noreply.github.com> --- .changeset/fix-trailing-punctuation-url.md | 5 +++++ packages/message-parser/src/grammar.pegjs | 4 ++-- packages/message-parser/tests/url.test.ts | 4 ++++ 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 .changeset/fix-trailing-punctuation-url.md diff --git a/.changeset/fix-trailing-punctuation-url.md b/.changeset/fix-trailing-punctuation-url.md new file mode 100644 index 0000000000000..f55255a46e574 --- /dev/null +++ b/.changeset/fix-trailing-punctuation-url.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/message-parser": patch +--- + +Fixes trailing punctuation (e.g. periods, exclamation marks) being incorrectly included in parsed URLs when they appear at the end of a message. For example, `go to https://www.google.com.` now correctly parses the URL as `https://www.google.com` without the trailing period. diff --git a/packages/message-parser/src/grammar.pegjs b/packages/message-parser/src/grammar.pegjs index d48c2e7d257d7..4ea4e755a700d 100644 --- a/packages/message-parser/src/grammar.pegjs +++ b/packages/message-parser/src/grammar.pegjs @@ -351,7 +351,7 @@ URLScheme = $([A-Za-z0-9+-] |1..32| ":") URLBody = ( - !(Extra+ (Whitespace / EndOfLine) / Whitespace) + !(Extra+ (Whitespace / EndOfLine / !.) / Whitespace) (AnyText / [*\[\/\]\^_`{}~(]) )+ @@ -435,7 +435,7 @@ AutoLinkURL = $(URLScheme URLAuthority AutoLinkURLBody*) / $(URLAuthorityHost AutoLinkURLBody*) -AutoLinkURLBody = !(Extra* (Whitespace / EndOfLine)) . +AutoLinkURLBody = !(Extra* (Whitespace / EndOfLine / !.)) . /** * diff --git a/packages/message-parser/tests/url.test.ts b/packages/message-parser/tests/url.test.ts index 1595f92f8fc44..aaeba7eb2292c 100644 --- a/packages/message-parser/tests/url.test.ts +++ b/packages/message-parser/tests/url.test.ts @@ -125,6 +125,10 @@ test.each([ ]), ], ], + ['go to https://www.google.com.', [paragraph([plain('go to '), link('https://www.google.com'), plain('.')])]], + ['https://www.google.com.', [paragraph([link('https://www.google.com'), plain('.')])]], + ['https://www.google.com!', [paragraph([link('https://www.google.com'), plain('!')])]], + ['visit www.google.com.', [paragraph([plain('visit '), link('//www.google.com', [plain('www.google.com')]), plain('.')])]], ])('parses %p', (input, output) => { expect(parse(input)).toMatchObject(output); }); From f6ba6a719538362aa27352553f0f4ee21dae8a00 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 25 Feb 2026 22:06:37 -0300 Subject: [PATCH 069/108] fix(message-parser): decimal numbers and underscore-wrapped text render as italic (#38981) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ggazzo <5263975+ggazzo@users.noreply.github.com> Co-authored-by: Guilherme Gazzo <guilhermegazzo@gmail.com> --- packages/message-parser/src/grammar.pegjs | 2 +- packages/message-parser/tests/emphasis.test.ts | 11 +++++------ packages/message-parser/tests/strongEmphasis.test.ts | 6 ++++++ 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/message-parser/src/grammar.pegjs b/packages/message-parser/src/grammar.pegjs index 4ea4e755a700d..57084728715d5 100644 --- a/packages/message-parser/src/grammar.pegjs +++ b/packages/message-parser/src/grammar.pegjs @@ -374,7 +374,7 @@ URLAuthorityPort DomainName = "localhost" - / $(DomainNameLabel ("." DomainChar DomainNameLabel*)+) + / $(![\x5F] DomainNameLabel ("." DomainChar DomainNameLabel*)+) DomainNameLabel = $(DomainChar+ ("-" DomainChar+)*) diff --git a/packages/message-parser/tests/emphasis.test.ts b/packages/message-parser/tests/emphasis.test.ts index 5c26f1184289d..eab036e542dfc 100644 --- a/packages/message-parser/tests/emphasis.test.ts +++ b/packages/message-parser/tests/emphasis.test.ts @@ -64,6 +64,11 @@ test.each([ ['paragraph@test__', [paragraph([plain('paragraph@test__')])]], ['_ @guilherme_gazzo_ _', [paragraph([italic([plain(' '), mentionUser('guilherme_gazzo_'), plain(' ')])])]], ['_ @guilherme.gazzo _', [paragraph([italic([plain(' '), mentionUser('guilherme.gazzo'), plain(' ')])])]], + ['_9797.76_', [paragraph([italic([plain('9797.76')])])]], + ['_3.14_', [paragraph([italic([plain('3.14')])])]], + ['_example.com_', [paragraph([italic([plain('example.com')])])]], + ['_rocket.chat_', [paragraph([italic([plain('rocket.chat')])])]], + ['_example.com', [paragraph([plain('_example.com')])]], [ '**reference link inside [emphasis with more [references](https://rocket.chat)](https://rocket.chat)**', [ @@ -120,12 +125,6 @@ test.each([ [paragraph([plain('some__double__snake__case__text and some '), italic([plain('italic')]), plain(' text')])], ], ['something__ __and italic__', [paragraph([plain('something__ '), italic([plain('and italic')])])]], - ['*test:*', [paragraph([bold([plain('test:')])])]], - ['*bold ending with colon:*', [paragraph([bold([plain('bold ending with colon:')])])]], - ['*bold ending with colon:* and some more text', [paragraph([bold([plain('bold ending with colon:')]), plain(' and some more text')])]], - ['*bold ending with colon :*and some more text', [paragraph([bold([plain('bold ending with colon :')]), plain('and some more text')])]], - ['*bold with a kissing emoji :* *', [paragraph([bold([plain('bold with a kissing emoji :')]), plain(' *')])]], - ['*bold with a kissing emoji :* ', [paragraph([bold([plain('bold with a kissing emoji :')]), plain(' ')])]], ])('parses %p', (input, output) => { expect(parse(input, { emoticons: false })).toMatchObject(output); }); diff --git a/packages/message-parser/tests/strongEmphasis.test.ts b/packages/message-parser/tests/strongEmphasis.test.ts index 52a1b1d30c1a3..bd9aff809421e 100644 --- a/packages/message-parser/tests/strongEmphasis.test.ts +++ b/packages/message-parser/tests/strongEmphasis.test.ts @@ -57,6 +57,12 @@ test.each([ ['*(teste*', [paragraph([bold([plain('(teste')])])]], ['*(teste)*', [paragraph([bold([plain('(teste)')])])]], ['*__~bolditalicstrike~_*', [paragraph([bold([plain('_'), italic([strike([plain('bolditalicstrike')])])])])]], + ['*test:*', [paragraph([bold([plain('test:')])])]], + ['*bold ending with colon:*', [paragraph([bold([plain('bold ending with colon:')])])]], + ['*bold ending with colon:* and some more text', [paragraph([bold([plain('bold ending with colon:')]), plain(' and some more text')])]], + ['*bold ending with colon :*and some more text', [paragraph([bold([plain('bold ending with colon :')]), plain('and some more text')])]], + ['*bold with a kissing emoji :* *', [paragraph([bold([plain('bold with a kissing emoji :')]), plain(' *')])]], + ['*bold with a kissing emoji :* ', [paragraph([bold([plain('bold with a kissing emoji :')]), plain(' ')])]], ])('parses %p', (input, output) => { expect(parse(input)).toMatchObject(output); }); From afaabea922f820812a29cb643e912aeac2e2003e Mon Sep 17 00:00:00 2001 From: "Rahian S.S." <85490097+TheRazorbill@users.noreply.github.com> Date: Wed, 25 Feb 2026 22:12:53 -0300 Subject: [PATCH 070/108] fix(i18n): use correct i18n key for workspace confirmation (#38662) Co-authored-by: Guilherme Gazzo <guilhermegazzo@gmail.com> --- .changeset/fix-register-workspace-i18n.md | 5 +++++ .../RegisterWorkspaceSetupStepTwoModal.tsx | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-register-workspace-i18n.md diff --git a/.changeset/fix-register-workspace-i18n.md b/.changeset/fix-register-workspace-i18n.md new file mode 100644 index 0000000000000..62eed988444d9 --- /dev/null +++ b/.changeset/fix-register-workspace-i18n.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes wrong i18n key in RegisterWorkspace confirmation step so the text is translated instead of showing a missing key. diff --git a/apps/meteor/client/views/admin/workspace/VersionCard/modals/RegisterWorkspaceSetupModal/RegisterWorkspaceSetupStepTwoModal.tsx b/apps/meteor/client/views/admin/workspace/VersionCard/modals/RegisterWorkspaceSetupModal/RegisterWorkspaceSetupStepTwoModal.tsx index b483b3374912e..676ebd286a485 100644 --- a/apps/meteor/client/views/admin/workspace/VersionCard/modals/RegisterWorkspaceSetupModal/RegisterWorkspaceSetupStepTwoModal.tsx +++ b/apps/meteor/client/views/admin/workspace/VersionCard/modals/RegisterWorkspaceSetupModal/RegisterWorkspaceSetupStepTwoModal.tsx @@ -83,7 +83,7 @@ const RegisterWorkspaceSetupStepTwoModal = ({ email, step, setStep, onClose, int <ModalContent> <Box fontSize='p2'> <Box> - <Trans i18nKey='RegisterWorkspace_Setup_Email_Confirmation'> + <Trans i18nKey='cloud.RegisterWorkspace_Setup_Email_Confirmation'> <Box is='p'> Email sent to{' '} <Box is='span' fontScale='p2b'> From 5f6b887de9c8f98361f2249e0c993aab500f83ac Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva <aleksander.silva@rocket.chat> Date: Thu, 26 Feb 2026 08:15:35 -0300 Subject: [PATCH 071/108] fix: Mentions popup remains open after canceling message edit (#38635) --- .../composer/hooks/useComposerBoxPopup.ts | 4 ++ .../room/composer/messageBox/MessageBox.tsx | 12 ++-- .../meteor/tests/e2e/message-composer.spec.ts | 56 +++++++++++++++++++ .../e2e/page-objects/fragments/composer.ts | 4 ++ 4 files changed, 72 insertions(+), 4 deletions(-) diff --git a/apps/meteor/client/views/room/composer/hooks/useComposerBoxPopup.ts b/apps/meteor/client/views/room/composer/hooks/useComposerBoxPopup.ts index 6ec1502dda217..b4e24c3fff608 100644 --- a/apps/meteor/client/views/room/composer/hooks/useComposerBoxPopup.ts +++ b/apps/meteor/client/views/room/composer/hooks/useComposerBoxPopup.ts @@ -28,6 +28,7 @@ type ComposerBoxPopupResult<T extends { _id: string; sort?: number }> = suspended: boolean; filter: unknown; clear: () => void; + update: () => void; } | { option: undefined; @@ -39,6 +40,7 @@ type ComposerBoxPopupResult<T extends { _id: string; sort?: number }> = suspended: undefined; filter: unknown; clear: () => void; + update: () => void; }; const keys = { @@ -269,6 +271,7 @@ export const useComposerBoxPopup = <T extends { _id: string; sort?: number }>( suspended: undefined, filter: undefined, clear, + update: setOptionByInput, }; } @@ -282,5 +285,6 @@ export const useComposerBoxPopup = <T extends { _id: string; sort?: number }>( suspended, filter, clear, + update: setOptionByInput, }; }; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx index dd804ab3f6dd8..8ad2997ef917a 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBox.tsx @@ -178,10 +178,15 @@ const MessageBox = ({ event.stopPropagation(); chat.currentEditingMessage.reset().then((reset) => { - if (!reset) { - chat.currentEditingMessage.cancel(); - chat.currentEditingMessage.stop(); + // NOTE: if the message was reset (i.e. content changed), we just update the popup (to re-apply/remove the preview) + if (reset) { + popup.update(); + return; } + + chat.currentEditingMessage.cancel(); + chat.currentEditingMessage.stop(); + popup.clear(); }); } }; @@ -378,7 +383,6 @@ const MessageBox = ({ ); const shouldPopupPreview = useEnablePopupPreview(popup.filter, popup.option); - return ( <> {chat.composer?.quotedMessages && <MessageBoxReplies />} diff --git a/apps/meteor/tests/e2e/message-composer.spec.ts b/apps/meteor/tests/e2e/message-composer.spec.ts index 14d700e5f21d0..8b203d4d93fcf 100644 --- a/apps/meteor/tests/e2e/message-composer.spec.ts +++ b/apps/meteor/tests/e2e/message-composer.spec.ts @@ -116,6 +116,62 @@ test.describe.serial('message-composer', () => { }); }); + test('should close mention popup when canceling a message edit via "Cancel" button', async ({ page }) => { + await poHomeChannel.navbar.openChat(targetChannel); + await poHomeChannel.content.sendMessage('hello composer'); + + await test.step('expect to edit last message', async () => { + await expect(poHomeChannel.composer.inputMessage).toHaveValue(''); + await poHomeChannel.content.openLastMessageMenu(); + await poHomeChannel.content.btnOptionEditMessage.click(); + await expect(poHomeChannel.composer.inputMessage).toHaveValue('hello composer'); + }); + + await test.step('expect to open popup on mention', async () => { + await page.keyboard.type(' @'); + await expect(poHomeChannel.composer.boxPopup).toBeVisible(); + }); + + await test.step('expect popup to close after the first edit is cancelled', async () => { + await poHomeChannel.composer.btnCancel.click(); + await expect(poHomeChannel.composer.inputMessage).toHaveValue('hello composer'); + await expect(poHomeChannel.composer.boxPopup).not.toBeVisible(); + }); + + await test.step('expect to leave editing mode', async () => { + await poHomeChannel.composer.btnCancel.click(); + await expect(poHomeChannel.composer.inputMessage).toHaveValue(''); + }); + }); + + test('should close mention popup when canceling a message edit via keyboard', async ({ page }) => { + await poHomeChannel.navbar.openChat(targetChannel); + await poHomeChannel.content.sendMessage('hello composer'); + + await test.step('expect to edit last message', async () => { + await expect(poHomeChannel.composer.inputMessage).toHaveValue(''); + await poHomeChannel.content.openLastMessageMenu(); + await poHomeChannel.content.btnOptionEditMessage.click(); + await expect(poHomeChannel.composer.inputMessage).toHaveValue('hello composer'); + }); + + await test.step('expect to open popup on mention', async () => { + await page.keyboard.type(' @'); + await expect(poHomeChannel.composer.boxPopup).toBeVisible(); + }); + + await test.step('expect popup to close after the first edit is cancelled', async () => { + await page.keyboard.press('Escape'); + await expect(poHomeChannel.composer.inputMessage).toHaveValue('hello composer'); + await expect(poHomeChannel.composer.boxPopup).not.toBeVisible(); + }); + + await test.step('expect to leave editing mode', async () => { + await page.keyboard.press('Escape'); + await expect(poHomeChannel.composer.inputMessage).toHaveValue(''); + }); + }); + test.describe('audio recorder', () => { test('should open audio recorder', async () => { await poHomeChannel.navbar.openChat(targetChannel); diff --git a/apps/meteor/tests/e2e/page-objects/fragments/composer.ts b/apps/meteor/tests/e2e/page-objects/fragments/composer.ts index e0dfbc6e54821..e3ecc115e52b1 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/composer.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/composer.ts @@ -33,6 +33,10 @@ export abstract class Composer { return this.root.getByRole('button', { name: 'Send' }); } + get btnCancel(): Locator { + return this.root.getByRole('button', { name: 'Cancel', exact: true }); + } + get btnOptionFileUpload(): Locator { return this.toolbarPrimaryActions.getByRole('button', { name: 'Upload file' }); } From 5f01b242bc93a378b7ea6d6e9c2798396b7f354a Mon Sep 17 00:00:00 2001 From: Sandra Nymark-Brand <143543945+sandranymark@users.noreply.github.com> Date: Thu, 26 Feb 2026 16:25:29 +0100 Subject: [PATCH 072/108] fix(ux): Inline-Errors in LoginForm (#36469) Co-authored-by: Matheus Cardoso <matheus@cardo.so> Co-authored-by: Douglas Fabris <devfabris@gmail.com> --- .../web-ui-registration/src/LoginForm.tsx | 45 ++++++++++++++----- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/packages/web-ui-registration/src/LoginForm.tsx b/packages/web-ui-registration/src/LoginForm.tsx index f429a8faa2636..39aeac58e7c8e 100644 --- a/packages/web-ui-registration/src/LoginForm.tsx +++ b/packages/web-ui-registration/src/LoginForm.tsx @@ -33,10 +33,6 @@ const LOGIN_SUBMIT_ERRORS = { type: 'danger', i18n: 'registration.page.login.errors.AppUserNotAllowedToLogin', }, - 'user-not-found': { - type: 'danger', - i18n: 'registration.page.login.errors.wrongCredentials', - }, 'error-login-blocked-for-ip': { type: 'danger', i18n: 'registration.page.login.errors.loginBlockedForIp', @@ -64,13 +60,30 @@ export const LoginForm = ({ setLoginRoute }: { setLoginRoute: DispatchLoginRoute register, handleSubmit, setError, + watch, clearErrors, getValues, formState: { errors }, } = useForm<{ usernameOrEmail: string; password: string }>({ - mode: 'onBlur', + mode: 'onSubmit', + reValidateMode: 'onChange', }); + const watchUsernameOrEmail = watch('usernameOrEmail'); + const watchPassword = watch('password'); + + useEffect(() => { + if (watchUsernameOrEmail) { + clearErrors('password'); + } + }, [watchUsernameOrEmail, clearErrors]); + + useEffect(() => { + if (watchPassword) { + clearErrors('usernameOrEmail'); + } + }, [watchPassword, clearErrors]); + const { t } = useTranslation(); const formLabelId = useId(); const [errorOnSubmit, setErrorOnSubmit] = useState<LoginErrorState>(undefined); @@ -88,16 +101,22 @@ export const LoginForm = ({ setLoginRoute }: { setLoginRoute: DispatchLoginRoute return login(formData.usernameOrEmail, formData.password); }, onError: (error: any) => { + if (error.error === 'user-not-found' || error.error === 401 || error.reason === 'User not found') { + setError('password', { + type: 'user-not-found', + message: t('registration.page.login.errors.wrongCredentials'), + }); + return; + } + if ([error.error, error.errorType].includes('error-invalid-email')) { setError('usernameOrEmail', { type: 'invalid-email', message: t('registration.page.login.errors.invalidEmail') }); + return; } if ('error' in error && error.error !== 403) { setErrorOnSubmit([error.error, error.reason]); - return; } - - setErrorOnSubmit(['user-not-found']); }, }); @@ -139,6 +158,8 @@ export const LoginForm = ({ setLoginRoute }: { setLoginRoute: DispatchLoginRoute return <EmailConfirmationForm onBackToLogin={() => clearErrors('usernameOrEmail')} email={getValues('usernameOrEmail')} />; } + const hasAuthError = errors.password?.type === 'user-not-found'; + return ( <Form tabIndex={-1} @@ -164,14 +185,14 @@ export const LoginForm = ({ setLoginRoute }: { setLoginRoute: DispatchLoginRoute required: t('Required_field', { field: t('registration.component.form.emailOrUsername') }), })} placeholder={usernameOrEmailPlaceholder || t('registration.component.form.emailPlaceholder')} - error={errors.usernameOrEmail?.message} - aria-invalid={errors.usernameOrEmail || errorOnSubmit ? 'true' : 'false'} + error={errors.usernameOrEmail?.message || (hasAuthError ? errors.password?.message : undefined)} + aria-invalid={errors.usernameOrEmail || hasAuthError || errorOnSubmit ? 'true' : 'false'} aria-describedby={`${usernameId}-error`} id={usernameId} /> </FieldRow> {errors.usernameOrEmail && ( - <FieldError aria-live='assertive' id={`${usernameId}-error`}> + <FieldError role='alert' id={`${usernameId}-error`}> {errors.usernameOrEmail.message} </FieldError> )} @@ -193,7 +214,7 @@ export const LoginForm = ({ setLoginRoute }: { setLoginRoute: DispatchLoginRoute /> </FieldRow> {errors.password && ( - <FieldError aria-live='assertive' id={`${passwordId}-error`}> + <FieldError role='alert' id={`${passwordId}-error`}> {errors.password.message} </FieldError> )} From 2affa1798a7e2f476f68cbd6268cbfcbc6849669 Mon Sep 17 00:00:00 2001 From: Shreyas <shreyaswagh2004@gmail.com> Date: Thu, 26 Feb 2026 22:11:43 +0530 Subject: [PATCH 073/108] test(message-parser): decouple parser tests from production helpers (#39086) --- packages/message-parser/tests/abuse.test.ts | 2 +- packages/message-parser/tests/any.test.ts | 2 +- .../message-parser/tests/blockquotes.test.ts | 2 +- .../message-parser/tests/codeFence.test.ts | 2 +- packages/message-parser/tests/color.test.ts | 2 +- packages/message-parser/tests/email.test.ts | 2 +- packages/message-parser/tests/emoji.test.ts | 2 +- .../message-parser/tests/emoticons.test.ts | 2 +- .../message-parser/tests/emphasis.test.ts | 2 +- .../tests/emphasisWithEmoticons.test.ts | 2 +- packages/message-parser/tests/escaped.test.ts | 2 +- packages/message-parser/tests/heading.test.ts | 2 +- packages/message-parser/tests/helpers.ts | 102 ++++++++++++++++++ packages/message-parser/tests/image.test.ts | 2 +- .../message-parser/tests/inlineCode.test.ts | 2 +- .../tests/inlineCodeStrike.test.ts | 2 +- packages/message-parser/tests/katex.test.ts | 2 +- .../message-parser/tests/lineBreak.test.ts | 2 +- packages/message-parser/tests/link.test.ts | 2 +- packages/message-parser/tests/mention.test.ts | 2 +- .../message-parser/tests/orderedList.test.ts | 2 +- .../message-parser/tests/phoneNumber.test.ts | 2 +- packages/message-parser/tests/spoiler.test.ts | 2 +- .../message-parser/tests/spoilerBlock.test.ts | 2 +- .../tests/strikethrough.test.ts | 2 +- .../tests/strongEmphasis.test.ts | 2 +- packages/message-parser/tests/tasks.test.ts | 2 +- .../message-parser/tests/timestamp.test.ts | 63 +++++++---- .../tests/unorderedList.test.ts | 2 +- packages/message-parser/tests/url.test.ts | 19 ++-- 30 files changed, 184 insertions(+), 54 deletions(-) create mode 100644 packages/message-parser/tests/helpers.ts diff --git a/packages/message-parser/tests/abuse.test.ts b/packages/message-parser/tests/abuse.test.ts index 525efe37fc7c1..1485c8c7dc894 100644 --- a/packages/message-parser/tests/abuse.test.ts +++ b/packages/message-parser/tests/abuse.test.ts @@ -1,5 +1,5 @@ import { parse } from '../src'; -import { paragraph, plain, bold, italic, strike } from '../src/utils'; +import { paragraph, plain, bold, italic, strike } from './helpers'; test.each([ [ diff --git a/packages/message-parser/tests/any.test.ts b/packages/message-parser/tests/any.test.ts index 7dca7bbdde527..4b1e61923e49f 100644 --- a/packages/message-parser/tests/any.test.ts +++ b/packages/message-parser/tests/any.test.ts @@ -1,5 +1,5 @@ import { parse } from '../src'; -import { paragraph, plain } from '../src/utils'; +import { paragraph, plain } from './helpers'; test.each([ ['free text', [paragraph([plain('free text')])]], diff --git a/packages/message-parser/tests/blockquotes.test.ts b/packages/message-parser/tests/blockquotes.test.ts index f20fc33462116..de718cb220f72 100644 --- a/packages/message-parser/tests/blockquotes.test.ts +++ b/packages/message-parser/tests/blockquotes.test.ts @@ -1,5 +1,5 @@ import { parse } from '../src'; -import { paragraph, plain, quote, bold } from '../src/utils'; +import { paragraph, plain, quote, bold } from './helpers'; test.each([ [ diff --git a/packages/message-parser/tests/codeFence.test.ts b/packages/message-parser/tests/codeFence.test.ts index c84f515983a70..1d9ba256df234 100644 --- a/packages/message-parser/tests/codeFence.test.ts +++ b/packages/message-parser/tests/codeFence.test.ts @@ -1,5 +1,5 @@ import { parse } from '../src'; -import { paragraph, plain, codeLine, code } from '../src/utils'; +import { paragraph, plain, codeLine, code } from './helpers'; const multiply = <T>(a: number, element: T): Array<T> => Array.from({ length: a }, () => element); diff --git a/packages/message-parser/tests/color.test.ts b/packages/message-parser/tests/color.test.ts index 099fe41779c0e..9182150743dd9 100644 --- a/packages/message-parser/tests/color.test.ts +++ b/packages/message-parser/tests/color.test.ts @@ -1,5 +1,5 @@ import { parse } from '../src'; -import { color, paragraph, plain } from '../src/utils'; +import { color, paragraph, plain } from './helpers'; test.each([ ['color:#ccc', [paragraph([color(0xcc, 0xcc, 0xcc)])], [paragraph([plain('color:#ccc')])]], diff --git a/packages/message-parser/tests/email.test.ts b/packages/message-parser/tests/email.test.ts index 30aade6151109..61c8a98f3eb8f 100644 --- a/packages/message-parser/tests/email.test.ts +++ b/packages/message-parser/tests/email.test.ts @@ -1,5 +1,5 @@ import { parse } from '../src'; -import { link, paragraph, plain } from '../src/utils'; +import { link, paragraph, plain } from './helpers'; test.each([ ['joe@joe.com', [paragraph([link('mailto:joe@joe.com', [plain('joe@joe.com')])])]], diff --git a/packages/message-parser/tests/emoji.test.ts b/packages/message-parser/tests/emoji.test.ts index 325f29a4f7b53..60e1d4fe13777 100644 --- a/packages/message-parser/tests/emoji.test.ts +++ b/packages/message-parser/tests/emoji.test.ts @@ -1,5 +1,5 @@ import { parse } from '../src'; -import { emoji, bigEmoji, paragraph, plain, emojiUnicode } from '../src/utils'; +import { emoji, bigEmoji, paragraph, plain, emojiUnicode } from './helpers'; test.each([ [':smile: asd', [paragraph([emoji('smile'), plain(' asd')])]], diff --git a/packages/message-parser/tests/emoticons.test.ts b/packages/message-parser/tests/emoticons.test.ts index 715712ae71517..3f2b15793663e 100644 --- a/packages/message-parser/tests/emoticons.test.ts +++ b/packages/message-parser/tests/emoticons.test.ts @@ -1,5 +1,5 @@ import { parse } from '../src'; -import { bigEmoji, paragraph, plain, emoticon } from '../src/utils'; +import { bigEmoji, paragraph, plain, emoticon } from './helpers'; test.each([ // Should render normal Emojis diff --git a/packages/message-parser/tests/emphasis.test.ts b/packages/message-parser/tests/emphasis.test.ts index eab036e542dfc..84f01eebb907e 100644 --- a/packages/message-parser/tests/emphasis.test.ts +++ b/packages/message-parser/tests/emphasis.test.ts @@ -12,7 +12,7 @@ import { mentionChannel, mentionUser, inlineCode, -} from '../src/utils'; +} from './helpers'; test.each([ ['_:smile:_', [paragraph([italic([emoji('smile')])])]], diff --git a/packages/message-parser/tests/emphasisWithEmoticons.test.ts b/packages/message-parser/tests/emphasisWithEmoticons.test.ts index 324ef9076c8ce..5086c3be8863e 100644 --- a/packages/message-parser/tests/emphasisWithEmoticons.test.ts +++ b/packages/message-parser/tests/emphasisWithEmoticons.test.ts @@ -1,5 +1,5 @@ import { parse } from '../src'; -import { paragraph, plain, bold, italic, emoticon } from '../src/utils'; +import { paragraph, plain, bold, italic, emoticon } from './helpers'; test.each([ ['*test:*', [paragraph([bold([plain('test:')])])]], diff --git a/packages/message-parser/tests/escaped.test.ts b/packages/message-parser/tests/escaped.test.ts index 7c0a3a2df35be..78a759f7b2c6c 100644 --- a/packages/message-parser/tests/escaped.test.ts +++ b/packages/message-parser/tests/escaped.test.ts @@ -1,5 +1,5 @@ import { parse } from '../src'; -import { paragraph, plain, bold } from '../src/utils'; +import { paragraph, plain, bold } from './helpers'; test.each([ ['¯\\\\_(ツ)_/¯', [paragraph([plain('¯\\_(ツ)_/¯')])]], diff --git a/packages/message-parser/tests/heading.test.ts b/packages/message-parser/tests/heading.test.ts index abcbed075ef6b..8324d3742ad16 100644 --- a/packages/message-parser/tests/heading.test.ts +++ b/packages/message-parser/tests/heading.test.ts @@ -1,5 +1,5 @@ import { parse } from '../src'; -import { heading, lineBreak, mentionChannel, paragraph, plain } from '../src/utils'; +import { heading, lineBreak, mentionChannel, paragraph, plain } from './helpers'; test.each([ ['# h1', [heading([plain('h1')], 1)]], diff --git a/packages/message-parser/tests/helpers.ts b/packages/message-parser/tests/helpers.ts new file mode 100644 index 0000000000000..8c8bf448f08a5 --- /dev/null +++ b/packages/message-parser/tests/helpers.ts @@ -0,0 +1,102 @@ +const generate = + <Type extends string>(type: Type) => + <Value>(value: Value) => + ({ type, value }) as const; + +export const paragraph = generate('PARAGRAPH'); +export const bold = generate('BOLD'); + +export const color = (r: number, g: number, b: number, a = 255) => ({ + type: 'COLOR' as const, + value: { r, g, b, a }, +}); + +export const heading = (value: unknown[], level = 1) => ({ + type: 'HEADING' as const, + level, + value, +}); + +export const code = (value: unknown[], language = 'none') => ({ + type: 'CODE' as const, + language, + value, +}); + +export const bigEmoji = generate('BIG_EMOJI'); +export const task = (value: unknown[], status: boolean) => ({ type: 'TASK' as const, status, value }); +export const inlineCode = generate('INLINE_CODE'); +export const tasks = generate('TASKS'); + +export const italic = generate('ITALIC'); +export const spoiler = generate('SPOILER'); + +export const plain = generate('PLAIN_TEXT'); +export const strike = generate('STRIKE'); + +export const codeLine = generate('CODE_LINE'); + +export const link = (src: string, label?: unknown[]) => ({ + type: 'LINK' as const, + value: { src: plain(src), label: label ?? [plain(src)] }, +}); + +export const image = (src: string, label?: unknown) => ({ + type: 'IMAGE' as const, + value: { src: plain(src), label: label ?? plain(src) }, +}); + +export const quote = generate('QUOTE'); +export const spoilerBlock = generate('SPOILER_BLOCK'); + +export const mentionChannel = (value: string) => ({ + type: 'MENTION_CHANNEL' as const, + value: plain(value), +}); + +export const orderedList = generate('ORDERED_LIST'); +export const unorderedList = generate('UNORDERED_LIST'); + +export const listItem = (value: unknown[], number?: number) => ({ + type: 'LIST_ITEM' as const, + value, + ...(number !== undefined ? { number } : {}), +}); + +export const mentionUser = (value: string) => ({ + type: 'MENTION_USER' as const, + value: plain(value), +}); + +export const emoji = (shortCode: string) => ({ + type: 'EMOJI' as const, + value: plain(shortCode), + shortCode, +}); + +export const emojiUnicode = (unicode: string) => ({ + type: 'EMOJI' as const, + value: undefined, + unicode, +}); + +export const emoticon = (value: string, shortCode: string) => ({ + type: 'EMOJI' as const, + value: plain(value), + shortCode, +}); + +export const lineBreak = () => ({ + type: 'LINE_BREAK' as const, + value: undefined, +}); + +export const katex = (value: string) => ({ + type: 'KATEX' as const, + value, +}); + +export const inlineKatex = (value: string) => ({ + type: 'INLINE_KATEX' as const, + value, +}); diff --git a/packages/message-parser/tests/image.test.ts b/packages/message-parser/tests/image.test.ts index c68c1f70fe6ce..3dff039496a8a 100644 --- a/packages/message-parser/tests/image.test.ts +++ b/packages/message-parser/tests/image.test.ts @@ -1,5 +1,5 @@ import { parse } from '../src'; -import { image, paragraph, plain } from '../src/utils'; +import { image, paragraph, plain } from './helpers'; test.each([ [ diff --git a/packages/message-parser/tests/inlineCode.test.ts b/packages/message-parser/tests/inlineCode.test.ts index 457bc16e18872..06b0aacd82ba9 100644 --- a/packages/message-parser/tests/inlineCode.test.ts +++ b/packages/message-parser/tests/inlineCode.test.ts @@ -1,5 +1,5 @@ import { parse } from '../src'; -import { inlineCode, paragraph, plain } from '../src/utils'; +import { inlineCode, paragraph, plain } from './helpers'; test.each([ ['`[asd](https://localhost)`', [paragraph([inlineCode(plain('[asd](https://localhost)'))])]], diff --git a/packages/message-parser/tests/inlineCodeStrike.test.ts b/packages/message-parser/tests/inlineCodeStrike.test.ts index 84d82f7e4e473..c782f4c21e7f4 100644 --- a/packages/message-parser/tests/inlineCodeStrike.test.ts +++ b/packages/message-parser/tests/inlineCodeStrike.test.ts @@ -1,5 +1,5 @@ import { parse } from '../src'; -import { bold, inlineCode, italic, paragraph, plain, strike } from '../src/utils'; +import { bold, inlineCode, italic, paragraph, plain, strike } from './helpers'; test.each([ ['~~`Striking Inline Code`~~', [paragraph([strike([inlineCode(plain('Striking Inline Code'))])])]], diff --git a/packages/message-parser/tests/katex.test.ts b/packages/message-parser/tests/katex.test.ts index 10bb47243de21..e743d01bc477e 100644 --- a/packages/message-parser/tests/katex.test.ts +++ b/packages/message-parser/tests/katex.test.ts @@ -1,5 +1,5 @@ import { parse } from '../src'; -import { inlineKatex, katex, paragraph, plain } from '../src/utils'; +import { inlineKatex, katex, paragraph, plain } from './helpers'; test.each([ [ diff --git a/packages/message-parser/tests/lineBreak.test.ts b/packages/message-parser/tests/lineBreak.test.ts index 082cf7b548517..4dd16880f5c85 100644 --- a/packages/message-parser/tests/lineBreak.test.ts +++ b/packages/message-parser/tests/lineBreak.test.ts @@ -1,5 +1,5 @@ import { parse } from '../src'; -import { lineBreak, paragraph, plain } from '../src/utils'; +import { lineBreak, paragraph, plain } from './helpers'; test.each([ [ diff --git a/packages/message-parser/tests/link.test.ts b/packages/message-parser/tests/link.test.ts index e46b4886076d3..54201bc4633c6 100644 --- a/packages/message-parser/tests/link.test.ts +++ b/packages/message-parser/tests/link.test.ts @@ -1,5 +1,5 @@ import { parse } from '../src'; -import { link, paragraph, plain, bold, strike, italic, quote, lineBreak, unorderedList, listItem, orderedList } from '../src/utils'; +import { link, paragraph, plain, bold, strike, italic, quote, lineBreak, unorderedList, listItem, orderedList } from './helpers'; test.each([ ['<https://domain.com|Test>', [paragraph([link('https://domain.com', [plain('Test')])])]], diff --git a/packages/message-parser/tests/mention.test.ts b/packages/message-parser/tests/mention.test.ts index 77a64d504945f..c6915bdeffa04 100644 --- a/packages/message-parser/tests/mention.test.ts +++ b/packages/message-parser/tests/mention.test.ts @@ -1,5 +1,5 @@ import { parse } from '../src'; -import { paragraph, plain, mentionUser, mentionChannel } from '../src/utils'; +import { paragraph, plain, mentionUser, mentionChannel } from './helpers'; test.each([ ['@guilherme.gazzo', [paragraph([mentionUser('guilherme.gazzo')])]], diff --git a/packages/message-parser/tests/orderedList.test.ts b/packages/message-parser/tests/orderedList.test.ts index fe292cee168f6..ed98bf781e86e 100644 --- a/packages/message-parser/tests/orderedList.test.ts +++ b/packages/message-parser/tests/orderedList.test.ts @@ -1,5 +1,5 @@ import { parse } from '../src'; -import { bold, plain, orderedList, listItem, emoji } from '../src/utils'; +import { bold, plain, orderedList, listItem, emoji } from './helpers'; test.each([ [ diff --git a/packages/message-parser/tests/phoneNumber.test.ts b/packages/message-parser/tests/phoneNumber.test.ts index f9faade94b62d..d210064f9956a 100644 --- a/packages/message-parser/tests/phoneNumber.test.ts +++ b/packages/message-parser/tests/phoneNumber.test.ts @@ -1,5 +1,5 @@ import { parse } from '../src'; -import { link, paragraph, plain, bold } from '../src/utils'; +import { link, paragraph, plain, bold } from './helpers'; test.each([ ['+07563546725', [paragraph([link('tel:07563546725', [plain('+07563546725')])])]], diff --git a/packages/message-parser/tests/spoiler.test.ts b/packages/message-parser/tests/spoiler.test.ts index d3a8d706afd47..1c0032ab9ed9a 100644 --- a/packages/message-parser/tests/spoiler.test.ts +++ b/packages/message-parser/tests/spoiler.test.ts @@ -1,5 +1,5 @@ import { parse } from '../src'; -import { bold, emoji, italic, link, mentionChannel, mentionUser, paragraph, plain, spoiler, strike } from '../src/utils'; +import { bold, emoji, italic, link, mentionChannel, mentionUser, paragraph, plain, spoiler, strike } from './helpers'; describe('spoiler parsing', () => { test.each([ diff --git a/packages/message-parser/tests/spoilerBlock.test.ts b/packages/message-parser/tests/spoilerBlock.test.ts index bb918efa3da76..9dd0623707f2b 100644 --- a/packages/message-parser/tests/spoilerBlock.test.ts +++ b/packages/message-parser/tests/spoilerBlock.test.ts @@ -1,5 +1,5 @@ import { parse } from '../src'; -import { lineBreak, paragraph, plain, spoilerBlock } from '../src/utils'; +import { lineBreak, paragraph, plain, spoilerBlock } from './helpers'; describe('block spoiler parsing', () => { test.each([ diff --git a/packages/message-parser/tests/strikethrough.test.ts b/packages/message-parser/tests/strikethrough.test.ts index f8d9e2d1e45b2..2000bee9d5bdb 100644 --- a/packages/message-parser/tests/strikethrough.test.ts +++ b/packages/message-parser/tests/strikethrough.test.ts @@ -1,5 +1,5 @@ import { parse } from '../src'; -import { emoji, emojiUnicode, link, mentionChannel, mentionUser, paragraph, plain, strike } from '../src/utils'; +import { emoji, emojiUnicode, link, mentionChannel, mentionUser, paragraph, plain, strike } from './helpers'; test.each([ ['~:smile:~', [paragraph([strike([emoji('smile')])])]], diff --git a/packages/message-parser/tests/strongEmphasis.test.ts b/packages/message-parser/tests/strongEmphasis.test.ts index bd9aff809421e..8888788fec190 100644 --- a/packages/message-parser/tests/strongEmphasis.test.ts +++ b/packages/message-parser/tests/strongEmphasis.test.ts @@ -1,5 +1,5 @@ import { parse } from '../src'; -import { bold, link, paragraph, plain, italic, strike, emoji, emojiUnicode, mentionChannel, mentionUser } from '../src/utils'; +import { bold, link, paragraph, plain, italic, strike, emoji, emojiUnicode, mentionChannel, mentionUser } from './helpers'; test.each([ ['*:smile:*', [paragraph([bold([emoji('smile')])])]], diff --git a/packages/message-parser/tests/tasks.test.ts b/packages/message-parser/tests/tasks.test.ts index 8258aeeddf2d7..2a06630ed0a0e 100644 --- a/packages/message-parser/tests/tasks.test.ts +++ b/packages/message-parser/tests/tasks.test.ts @@ -1,5 +1,5 @@ import { parse } from '../src'; -import { plain, tasks, task, mentionUser, mentionChannel, link, bold, emoji } from '../src/utils'; +import { plain, tasks, task, mentionUser, mentionChannel, link, bold, emoji } from './helpers'; test.each([ [ diff --git a/packages/message-parser/tests/timestamp.test.ts b/packages/message-parser/tests/timestamp.test.ts index c121318a7ff61..6652c280ee466 100644 --- a/packages/message-parser/tests/timestamp.test.ts +++ b/packages/message-parser/tests/timestamp.test.ts @@ -1,10 +1,26 @@ import { parse } from '../src'; -import { bold, paragraph, plain, strike, timestamp, timestampFromHours } from '../src/utils'; + +const plain = (value: string) => ({ type: 'PLAIN_TEXT' as const, value }); + +const paragraph = (value: Array<Record<string, unknown>>) => ({ type: 'PARAGRAPH' as const, value }); + +const bold = (value: Array<Record<string, unknown>>) => ({ type: 'BOLD' as const, value }); + +const strike = (value: Array<Record<string, unknown>>) => ({ type: 'STRIKE' as const, value }); + +const timestampNode = (value: string, format: 't' | 'T' | 'd' | 'D' | 'f' | 'F' | 'R' = 't') => ({ + type: 'TIMESTAMP' as const, + value: { + timestamp: value, + format, + }, + fallback: plain(`<t:${value}:${format}>`), +}); test.each([ - [`<t:1708551317>`, [paragraph([timestamp('1708551317')])]], - [`<t:1708551317:R>`, [paragraph([timestamp('1708551317', 'R')])]], - ['hello <t:1708551317>', [paragraph([plain('hello '), timestamp('1708551317')])]], + [`<t:1708551317>`, [paragraph([timestampNode('1708551317')])]], + [`<t:1708551317:R>`, [paragraph([timestampNode('1708551317', 'R')])]], + ['hello <t:1708551317>', [paragraph([plain('hello '), timestampNode('1708551317')])]], ])('parses %p', (input, output) => { expect(parse(input)).toMatchObject(output); }); @@ -17,26 +33,37 @@ test.each([ }); test.each([ - ['~<t:1708551317>~', [paragraph([strike([timestamp('1708551317')])])]], + ['~<t:1708551317>~', [paragraph([strike([timestampNode('1708551317')])])]], ['*<t:1708551317>*', [paragraph([bold([plain('<t:1708551317>')])])]], ])('parses %p', (input, output) => { expect(parse(input)).toMatchObject(output); }); test.each([ - ['<t:2025-07-22T10:00:00.000+00:00:R>', [paragraph([timestamp((Date.parse('2025-07-22T10:00:00.000+00:00') / 1000).toString(), 'R')])]], - ['<t:2025-07-22T10:00:00.000+00:00:R>', [paragraph([timestamp((Date.parse('2025-07-22T10:00:00.000+00:00') / 1000).toString(), 'R')])]], - ['<t:2025-07-22T10:00:00.000+00:00:R>', [paragraph([timestamp((Date.parse('2025-07-22T10:00:00.000+00:00') / 1000).toString(), 'R')])]], - ['<t:2025-07-22T10:00:00+00:00:R>', [paragraph([timestamp((Date.parse('2025-07-22T10:00:00+00:00') / 1000).toString(), 'R')])]], - ['<t:10:00:00+00:00:R>', [paragraph([timestamp(timestampFromHours('10', '00', '00', '+00:00'), 'R')])]], - ['<t:10:00+00:00:R>', [paragraph([timestamp(timestampFromHours('10', '00', '00', '+00:00'), 'R')])]], - ['<t:10:00:05+00:00>', [paragraph([timestamp(timestampFromHours('10', '00', '05', '+00:00'), 't')])]], - ['<t:10:00+00:00>', [paragraph([timestamp(timestampFromHours('10', '00', '00', '+00:00'), 't')])]], - - [ - '<t:2025-07-24T20:19:58.154+00:00:R>', - [paragraph([timestamp(((Date.parse('2025-07-24T20:19:58.154+00:00') / 1000) | 0).toString(), 'R')])], - ], + ['<t:2025-07-22T10:00:00.000+00:00:R>', [paragraph([timestampNode('1753178400', 'R')])]], + ['<t:2025-07-22T10:00:00+00:00:R>', [paragraph([timestampNode('1753178400', 'R')])]], + + ['<t:2025-07-24T20:19:58.154+00:00:R>', [paragraph([timestampNode('1753388398', 'R')])]], ])('parses %p', (input, output) => { expect(parse(input)).toMatchObject(output); }); + +describe('relative hour timestamp parsing', () => { + beforeAll(() => { + jest.useFakeTimers(); + jest.setSystemTime(new Date('2025-07-22T00:00:00.000Z')); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + test.each([ + ['<t:10:00:00+00:00:R>', [paragraph([timestampNode('1753178400', 'R')])]], + ['<t:10:00+00:00:R>', [paragraph([timestampNode('1753178400', 'R')])]], + ['<t:10:00:05+00:00>', [paragraph([timestampNode('1753178405')])]], + ['<t:10:00+00:00>', [paragraph([timestampNode('1753178400')])]], + ])('parses %p', (input, output) => { + expect(parse(input)).toMatchObject(output); + }); +}); diff --git a/packages/message-parser/tests/unorderedList.test.ts b/packages/message-parser/tests/unorderedList.test.ts index 2ef56884e4aa3..9bd3ba4b04d4c 100644 --- a/packages/message-parser/tests/unorderedList.test.ts +++ b/packages/message-parser/tests/unorderedList.test.ts @@ -1,5 +1,5 @@ import { parse } from '../src'; -import { unorderedList, plain, listItem, bold, emoji } from '../src/utils'; +import { unorderedList, plain, listItem, bold, emoji } from './helpers'; test.each([ [ diff --git a/packages/message-parser/tests/url.test.ts b/packages/message-parser/tests/url.test.ts index aaeba7eb2292c..9cfc0e38479e1 100644 --- a/packages/message-parser/tests/url.test.ts +++ b/packages/message-parser/tests/url.test.ts @@ -1,23 +1,24 @@ import { parse } from '../src'; -import { lineBreak, autoLink, paragraph, plain, link } from '../src/utils'; +import { lineBreak, paragraph, plain, link } from './helpers'; +import { autoLink } from '../src/utils'; test.each([ [ 'https://pt.wikipedia.org/wiki/Condi%C3%A7%C3%A3o_de_corrida#:~:text=Uma%20condi%C3%A7%C3%A3o%20de%20corrida%20%C3%A9,sequ%C3%AAncia%20ou%20sincronia%20doutros%20eventos', [ paragraph([ - autoLink( + link( 'https://pt.wikipedia.org/wiki/Condi%C3%A7%C3%A3o_de_corrida#:~:text=Uma%20condi%C3%A7%C3%A3o%20de%20corrida%20%C3%A9,sequ%C3%AAncia%20ou%20sincronia%20doutros%20eventos', ), ]), ], ], - ['https://pt.wikipedia.org/', [paragraph([autoLink('https://pt.wikipedia.org/')])]], - ['https://pt.wikipedia.org/with-hyphen', [paragraph([autoLink('https://pt.wikipedia.org/with-hyphen')])]], - ['https://pt.wikipedia.org/with_underscore', [paragraph([autoLink('https://pt.wikipedia.org/with_underscore')])]], + ['https://pt.wikipedia.org/', [paragraph([link('https://pt.wikipedia.org/')])]], + ['https://pt.wikipedia.org/with-hyphen', [paragraph([link('https://pt.wikipedia.org/with-hyphen')])]], + ['https://pt.wikipedia.org/with_underscore', [paragraph([link('https://pt.wikipedia.org/with_underscore')])]], [ 'https://www.npmjs.com/package/@rocket.chat/message-parser', - [paragraph([autoLink('https://www.npmjs.com/package/@rocket.chat/message-parser')])], + [paragraph([link('https://www.npmjs.com/package/@rocket.chat/message-parser')])], ], ['http:/rocket.chat/teste', [paragraph([plain('http:/rocket.chat/teste')])]], ['https:/rocket.chat/', [paragraph([plain('https:/rocket.chat/')])]], @@ -135,9 +136,9 @@ test.each([ describe('autoLink with custom hosts settings comming from Rocket.Chat', () => { test.each([ - ['http://gitlab.local', [paragraph([autoLink('http://gitlab.local', ['local'])])]], - ['gitlab.local', [paragraph([autoLink('gitlab.local', ['local'])])]], - ['internaltool.intranet', [paragraph([autoLink('internaltool.intranet', ['local', 'intranet'])])]], + ['http://gitlab.local', [paragraph([link('http://gitlab.local', [plain('http://gitlab.local')])])]], + ['gitlab.local', [paragraph([link('//gitlab.local', [plain('gitlab.local')])])]], + ['internaltool.intranet', [paragraph([link('//internaltool.intranet', [plain('internaltool.intranet')])])]], ])('parses %p', (input, output) => { expect(parse(input, { customDomains: ['local', 'intranet'] })).toMatchObject(output); }); From 602b20a8c570b895eb296ecfe39c9b7fcb12fabd Mon Sep 17 00:00:00 2001 From: Kevin Aleman <kaleman960@gmail.com> Date: Thu, 26 Feb 2026 11:31:10 -0600 Subject: [PATCH 074/108] fix: Check ownership of uploaded files before confirming/sending (#39010) --- .changeset/blue-seals-leave.md | 7 ++ apps/meteor/app/api/server/v1/rooms.ts | 2 +- .../server/methods/sendFileMessage.ts | 7 ++ apps/meteor/tests/data/uploads.helper.ts | 81 ++++++++++++++++++- .../src/models/IBaseUploadsModel.ts | 2 + packages/models/src/models/BaseUploadModel.ts | 4 + 6 files changed, 99 insertions(+), 4 deletions(-) create mode 100644 .changeset/blue-seals-leave.md diff --git a/.changeset/blue-seals-leave.md b/.changeset/blue-seals-leave.md new file mode 100644 index 0000000000000..62f6e7e9003fd --- /dev/null +++ b/.changeset/blue-seals-leave.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/model-typings": patch +"@rocket.chat/models": patch +--- + +Fixes an authorization issue that allowed users to confirm uploads from other users diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index 70875e47fedd9..0e05a9a966ab9 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -257,7 +257,7 @@ API.v1.addRoute( return API.v1.forbidden(); } - const file = await Uploads.findOneById(this.urlParams.fileId); + const file = await Uploads.findOneByIdAndUserIdAndRoomId(this.urlParams.fileId, this.userId, this.urlParams.rid); if (!file) { throw new Meteor.Error('invalid-file'); diff --git a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts index 15fcba1875388..e105d1962d895 100644 --- a/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts +++ b/apps/meteor/app/file-upload/server/methods/sendFileMessage.ts @@ -38,6 +38,13 @@ export const parseFileIntoMessageAttachments = async ( ): Promise<FilesAndAttachments> => { validateFileRequiredFields(file); + const upload = await Uploads.findOneByIdAndUserIdAndRoomId(file._id, user._id, roomId, { projection: { _id: 1 } }); + if (!upload) { + throw new Meteor.Error('error-invalid-file', 'Invalid file', { + method: 'sendFileMessage', + }); + } + await Uploads.updateFileComplete(file._id, user._id, omit(file, '_id')); const fileUrl = FileUpload.getPath(`${file._id}/${encodeURI(file.name || '')}`); diff --git a/apps/meteor/tests/data/uploads.helper.ts b/apps/meteor/tests/data/uploads.helper.ts index 20e015700cea0..b0c356ef53dd2 100644 --- a/apps/meteor/tests/data/uploads.helper.ts +++ b/apps/meteor/tests/data/uploads.helper.ts @@ -1,4 +1,5 @@ -import type { IRoom } from '@rocket.chat/core-typings'; +import type { Credentials } from '@rocket.chat/api-client'; +import type { IRoom, IUser } from '@rocket.chat/core-typings'; import { expect } from 'chai'; import { after, before, it } from 'mocha'; import type { Response } from 'supertest'; @@ -6,8 +7,9 @@ import type { Response } from 'supertest'; import { api, request, credentials } from './api-data'; import { imgURL, soundURL } from './interactions'; import { updateSetting } from './permissions.helper'; -import { createRoom, deleteRoom } from './rooms.helper'; -import { createUser, deleteUser } from './users.helper'; +import { addUserToRoom, createRoom, deleteRoom } from './rooms.helper'; +import { password } from './user'; +import { createUser, deleteUser, login } from './users.helper'; export async function testFileUploads( filesEndpoint: 'channels.files' | 'groups.files' | 'im.files', @@ -301,4 +303,77 @@ export async function testFileUploads( await Promise.all([nameFilterTest, typeGroupFilterTest]); }); + + describe('with another user', () => { + let anotherUserCreds: Credentials; + let anotherUser: IUser; + let extraRoom: IRoom; + + before(async () => { + anotherUser = await createUser(); + anotherUserCreds = await login(anotherUser.username, password); + + extraRoom = ( + await createRoom({ + type: roomType, + ...(roomType === 'd' ? { username: user.username } : { name: `channel-files-${Date.now()}` }), + credentials: anotherUserCreds, + } as any) + ).body[propertyMap[roomType]]; + + if (roomType === 'p') { + await addUserToRoom({ + rid: testRoom._id, + usernames: [anotherUser.username!], + }); + } + }); + + after(() => Promise.all([deleteUser(anotherUser), deleteRoom({ type: roomType, roomId: extraRoom._id })])); + + it('should not allow to confirm a file from another user', async function () { + if (roomType === 'd') { + this.skip(); + } + + let fileId: string; + await request + .post(api(`rooms.media/${testRoom._id}`)) + .set(anotherUserCreds) + .attach('file', imgURL) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + fileId = res.body.file._id; + }); + + await request + .post(api(`rooms.mediaConfirm/${testRoom._id}/${fileId!}`)) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(400); + }); + + it('should not allow to confirm a file that was not uploaded to the same room', async () => { + let fileId: string; + + await request + .post(api(`rooms.media/${extraRoom._id}`)) + .set(anotherUserCreds) + .attach('file', imgURL) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success', true); + fileId = res.body.file._id; + }); + + await request + .post(api(`rooms.mediaConfirm/${testRoom._id}/${fileId!}`)) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(400); + }); + }); } diff --git a/packages/model-typings/src/models/IBaseUploadsModel.ts b/packages/model-typings/src/models/IBaseUploadsModel.ts index 1161ab0fe86d9..de47c9a7d8b02 100644 --- a/packages/model-typings/src/models/IBaseUploadsModel.ts +++ b/packages/model-typings/src/models/IBaseUploadsModel.ts @@ -21,4 +21,6 @@ export interface IBaseUploadsModel<T extends IUpload> extends IBaseModel<T> { updateFileNameById(fileId: string, name: string): Promise<Document | UpdateResult>; deleteFile(fileId: string, options?: { session?: ClientSession }): Promise<DeleteResult>; + + findOneByIdAndUserIdAndRoomId(fileId: string, userId: string, rid: string, options?: FindOptions<T>): Promise<T | null>; } diff --git a/packages/models/src/models/BaseUploadModel.ts b/packages/models/src/models/BaseUploadModel.ts index ba4165534c00d..055f9c1db483a 100644 --- a/packages/models/src/models/BaseUploadModel.ts +++ b/packages/models/src/models/BaseUploadModel.ts @@ -134,4 +134,8 @@ export abstract class BaseUploadModelRaw extends BaseRaw<T> implements IBaseUplo async deleteFile(fileId: string, options?: { session?: ClientSession }): Promise<DeleteResult> { return this.deleteOne({ _id: fileId }, { session: options?.session }); } + + async findOneByIdAndUserIdAndRoomId(fileId: string, userId: string, rid: string, options?: FindOptions<T>): Promise<T | null> { + return this.findOne({ _id: fileId, userId, rid }, options); + } } From dbf125c8f0013c2e05bb1de2c27ec3560a2c53c0 Mon Sep 17 00:00:00 2001 From: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> Date: Thu, 26 Feb 2026 15:19:54 -0300 Subject: [PATCH 075/108] chore: modernize code related to pushToken management (#39011) --- apps/meteor/app/api/server/v1/push.ts | 234 ++++++++++++++---- apps/meteor/app/push/server/apn.ts | 4 +- apps/meteor/app/push/server/methods.ts | 118 ++------- apps/meteor/app/push/server/push.ts | 34 +-- apps/meteor/server/lib/pushConfig.ts | 4 +- apps/meteor/server/models.ts | 2 - apps/meteor/server/services/push/logger.ts | 3 + apps/meteor/server/services/push/service.ts | 23 ++ .../tokenManagement/findDocumentToUpdate.ts | 17 ++ .../push/tokenManagement/registerPushToken.ts | 41 +++ apps/meteor/tests/end-to-end/api/push.ts | 76 ++++-- packages/core-services/src/index.ts | 1 + .../core-services/src/types/IPushService.ts | 8 +- packages/core-typings/src/IAppsTokens.ts | 13 - packages/core-typings/src/IPushToken.ts | 6 +- packages/core-typings/src/index.ts | 1 - packages/model-typings/src/index.ts | 1 - .../src/models/IAppsTokensModel.ts | 9 - .../src/models/IPushTokenModel.ts | 20 +- packages/models/src/index.ts | 2 - packages/models/src/modelClasses.ts | 1 - packages/models/src/models/AppsTokens.ts | 36 --- packages/models/src/models/PushToken.ts | 93 ++++++- packages/rest-typings/src/v1/push.ts | 6 +- 24 files changed, 484 insertions(+), 269 deletions(-) create mode 100644 apps/meteor/server/services/push/logger.ts create mode 100644 apps/meteor/server/services/push/tokenManagement/findDocumentToUpdate.ts create mode 100644 apps/meteor/server/services/push/tokenManagement/registerPushToken.ts delete mode 100644 packages/core-typings/src/IAppsTokens.ts delete mode 100644 packages/model-typings/src/models/IAppsTokensModel.ts delete mode 100644 packages/models/src/models/AppsTokens.ts diff --git a/apps/meteor/app/api/server/v1/push.ts b/apps/meteor/app/api/server/v1/push.ts index 5e3072759f7e7..0d5e21e3f97eb 100644 --- a/apps/meteor/app/api/server/v1/push.ts +++ b/apps/meteor/app/api/server/v1/push.ts @@ -1,87 +1,211 @@ -import type { IAppsTokens } from '@rocket.chat/core-typings'; -import { Messages, AppsTokens, Users, Rooms, Settings } from '@rocket.chat/models'; -import { Random } from '@rocket.chat/random'; -import { ajv, validateBadRequestErrorResponse, validateUnauthorizedErrorResponse } from '@rocket.chat/rest-typings'; +import { Push } from '@rocket.chat/core-services'; +import type { IPushToken } from '@rocket.chat/core-typings'; +import { Messages, PushToken, Users, Rooms, Settings } from '@rocket.chat/models'; +import { + ajv, + validateNotFoundErrorResponse, + validateBadRequestErrorResponse, + validateUnauthorizedErrorResponse, + validateForbiddenErrorResponse, +} from '@rocket.chat/rest-typings'; +import type { JSONSchemaType } from 'ajv'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { executePushTest } from '../../../../server/lib/pushConfig'; import { canAccessRoomAsync } from '../../../authorization/server/functions/canAccessRoom'; -import { pushUpdate } from '../../../push/server/methods'; import PushNotification from '../../../push-notifications/server/lib/PushNotification'; import { settings } from '../../../settings/server'; import type { ExtractRoutesFromAPI } from '../ApiClass'; import { API } from '../api'; +import type { SuccessResult } from '../definition'; -API.v1.addRoute( - 'push.token', - { authRequired: true }, - { - async post() { - const { id, type, value, appName } = this.bodyParams; +type PushTokenPOST = { + id?: string; + type: 'apn' | 'gcm'; + value: string; + appName: string; +}; - if (id && typeof id !== 'string') { - throw new Meteor.Error('error-id-param-not-valid', 'The required "id" body param is invalid.'); - } +const PushTokenPOSTSchema: JSONSchemaType<PushTokenPOST> = { + type: 'object', + properties: { + id: { + type: 'string', + nullable: true, + }, + type: { + type: 'string', + enum: ['apn', 'gcm'], + }, + value: { + type: 'string', + minLength: 1, + }, + appName: { + type: 'string', + minLength: 1, + }, + }, + required: ['type', 'value', 'appName'], + additionalProperties: false, +}; - const deviceId = id || Random.id(); +export const isPushTokenPOSTProps = ajv.compile<PushTokenPOST>(PushTokenPOSTSchema); - if (!type || (type !== 'apn' && type !== 'gcm')) { - throw new Meteor.Error('error-type-param-not-valid', 'The required "type" body param is missing or invalid.'); - } +type PushTokenDELETE = { + token: string; +}; - if (!value || typeof value !== 'string') { - throw new Meteor.Error('error-token-param-not-valid', 'The required "value" body param is missing or invalid.'); - } +const PushTokenDELETESchema: JSONSchemaType<PushTokenDELETE> = { + type: 'object', + properties: { + token: { + type: 'string', + minLength: 1, + }, + }, + required: ['token'], + additionalProperties: false, +}; - if (!appName || typeof appName !== 'string') { - throw new Meteor.Error('error-appName-param-not-valid', 'The required "appName" body param is missing or invalid.'); - } +export const isPushTokenDELETEProps = ajv.compile<PushTokenDELETE>(PushTokenDELETESchema); + +type PushTokenResult = Pick<IPushToken, '_id' | 'token' | 'appName' | 'userId' | 'enabled' | 'createdAt' | '_updatedAt'>; + +/** + * Pick only the attributes we actually want to return on the endpoint, ensuring nothing from older schemas get mixed in + */ +function cleanTokenResult(result: Omit<IPushToken, 'authToken'>): PushTokenResult { + const { _id, token, appName, userId, enabled, createdAt, _updatedAt } = result; + + return { + _id, + token, + appName, + userId, + enabled, + createdAt, + _updatedAt, + }; +} - const authToken = this.request.headers.get('x-auth-token'); - if (!authToken) { +const pushTokenEndpoints = API.v1 + .post( + 'push.token', + { + response: { + 200: ajv.compile<SuccessResult<{ result: PushTokenResult }>['body']>({ + additionalProperties: false, + type: 'object', + properties: { + success: { + type: 'boolean', + description: 'Indicates if the request was successful.', + }, + result: { + type: 'object', + description: 'The updated token data for this device', + properties: { + _id: { + type: 'string', + }, + token: { + type: 'object', + properties: { + apn: { + type: 'string', + }, + gcm: { + type: 'string', + }, + }, + required: [], + additionalProperties: false, + }, + appName: { + type: 'string', + }, + userId: { + type: 'string', + nullable: true, + }, + enabled: { + type: 'boolean', + }, + createdAt: { + type: 'string', + }, + _updatedAt: { + type: 'string', + }, + }, + additionalProperties: false, + }, + }, + required: ['success', 'result'], + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + 403: validateForbiddenErrorResponse, + }, + body: isPushTokenPOSTProps, + authRequired: true, + }, + async function action() { + const { id, type, value, appName } = this.bodyParams; + + const rawToken = this.request.headers.get('x-auth-token'); + if (!rawToken) { throw new Meteor.Error('error-authToken-param-not-valid', 'The required "authToken" header param is missing or invalid.'); } + const authToken = Accounts._hashLoginToken(rawToken); - const result = await pushUpdate({ - id: deviceId, - token: { [type]: value } as IAppsTokens['token'], + const result = await Push.registerPushToken({ + ...(id && { _id: id }), + token: { [type]: value } as IPushToken['token'], authToken, appName, userId: this.userId, }); - return API.v1.success({ result }); + return API.v1.success({ result: cleanTokenResult(result) }); }, - async delete() { + ) + .delete( + 'push.token', + { + response: { + 200: ajv.compile<void>({ + additionalProperties: false, + type: 'object', + properties: { + success: { + type: 'boolean', + }, + }, + required: ['success'], + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + 403: validateForbiddenErrorResponse, + 404: validateNotFoundErrorResponse, + }, + body: isPushTokenDELETEProps, + authRequired: true, + }, + async function action() { const { token } = this.bodyParams; - if (!token || typeof token !== 'string') { - throw new Meteor.Error('error-token-param-not-valid', 'The required "token" body param is missing or invalid.'); - } - - const affectedRecords = ( - await AppsTokens.deleteMany({ - $or: [ - { - 'token.apn': token, - }, - { - 'token.gcm': token, - }, - ], - userId: this.userId, - }) - ).deletedCount; + const removeResult = await PushToken.removeAllByTokenStringAndUserId(token, this.userId); - if (affectedRecords === 0) { + if (removeResult.deletedCount === 0) { return API.v1.notFound(); } return API.v1.success(); }, - }, -); + ); API.v1.addRoute( 'push.get', @@ -137,7 +261,7 @@ API.v1.addRoute( }, ); -const pushEndpoints = API.v1.post( +const pushTestEndpoints = API.v1.post( 'push.test', { authRequired: true, @@ -177,7 +301,11 @@ const pushEndpoints = API.v1.post( }, ); -export type PushEndpoints = ExtractRoutesFromAPI<typeof pushEndpoints>; +type PushTestEndpoints = ExtractRoutesFromAPI<typeof pushTestEndpoints>; + +type PushTokenEndpoints = ExtractRoutesFromAPI<typeof pushTokenEndpoints>; + +type PushEndpoints = PushTestEndpoints & PushTokenEndpoints; declare module '@rocket.chat/rest-typings' { // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface diff --git a/apps/meteor/app/push/server/apn.ts b/apps/meteor/app/push/server/apn.ts index c03b93e9b635a..e8732a9daae5f 100644 --- a/apps/meteor/app/push/server/apn.ts +++ b/apps/meteor/app/push/server/apn.ts @@ -1,5 +1,5 @@ import apn from '@parse/node-apn'; -import type { IAppsTokens, RequiredField } from '@rocket.chat/core-typings'; +import type { IPushToken, RequiredField } from '@rocket.chat/core-typings'; import EJSON from 'ejson'; import type { PushOptions, PendingPushNotification } from './definition'; @@ -24,7 +24,7 @@ export const sendAPN = ({ }: { userToken: string; notification: PendingPushNotification & { topic: string }; - _removeToken: (token: IAppsTokens['token']) => void; + _removeToken: (token: IPushToken['token']) => void; }) => { if (!apnConnection) { throw new Error('Apn Connection not initialized.'); diff --git a/apps/meteor/app/push/server/methods.ts b/apps/meteor/app/push/server/methods.ts index bc5102848756a..e4dddc8910dfc 100644 --- a/apps/meteor/app/push/server/methods.ts +++ b/apps/meteor/app/push/server/methods.ts @@ -1,7 +1,7 @@ -import type { IAppsTokens } from '@rocket.chat/core-typings'; +import { Push } from '@rocket.chat/core-services'; +import type { IPushToken } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { AppsTokens } from '@rocket.chat/models'; -import { Random } from '@rocket.chat/random'; +import { PushToken } from '@rocket.chat/models'; import { Accounts } from 'meteor/accounts-base'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; @@ -11,7 +11,7 @@ import { _matchToken } from './push'; type PushUpdateOptions = { id?: string; - token: IAppsTokens['token']; + token: IPushToken['token']; authToken: string; appName: string; userId: string | null; @@ -20,96 +20,11 @@ type PushUpdateOptions = { declare module '@rocket.chat/ddp-client' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { - 'raix:push-update'(options: PushUpdateOptions): Promise<Omit<IAppsTokens, '_updatedAt'>>; + 'raix:push-update'(options: PushUpdateOptions): Promise<Omit<IPushToken, 'authToken'>>; 'raix:push-setuser'(options: { id: string; userId: string }): Promise<boolean>; } } -export const pushUpdate = async (options: PushUpdateOptions): Promise<Omit<IAppsTokens, '_updatedAt'>> => { - // we always store the hashed token to protect users - const hashedToken = Accounts._hashLoginToken(options.authToken); - - let doc; - - // lookup app by id if one was included - if (options.id) { - doc = await AppsTokens.findOne({ _id: options.id }); - } else if (options.userId) { - doc = await AppsTokens.findOne({ userId: options.userId }); - } - - // No doc was found - we check the database to see if - // we can find a match for the app via token and appName - if (!doc) { - doc = await AppsTokens.findOne({ - $and: [ - { token: options.token }, // Match token - { appName: options.appName }, // Match appName - { token: { $exists: true } }, // Make sure token exists - ], - }); - } - - // if we could not find the id or token then create it - if (!doc) { - // Rig default doc - doc = { - token: options.token, - authToken: hashedToken, - appName: options.appName, - userId: options.userId, - enabled: true, - createdAt: new Date(), - updatedAt: new Date(), - metadata: options.metadata || {}, - - // XXX: We might want to check the id - Why isnt there a match for id - // in the Meteor check... Normal length 17 (could be larger), and - // numbers+letters are used in Random.id() with exception of 0 and 1 - _id: options.id || Random.id(), - // The user wanted us to use a specific id, we didn't find this while - // searching. The client could depend on the id eg. as reference so - // we respect this and try to create a document with the selected id; - }; - - await AppsTokens.insertOne(doc); - } else { - // We found the app so update the updatedAt and set the token - await AppsTokens.updateOne( - { _id: doc._id }, - { - $set: { - updatedAt: new Date(), - token: options.token, - authToken: hashedToken, - }, - }, - ); - } - - if (doc.token) { - const removed = ( - await AppsTokens.deleteMany({ - $and: [ - { _id: { $ne: doc._id } }, - { token: doc.token }, // Match token - { appName: doc.appName }, // Match appName - { token: { $exists: true } }, // Make sure token exists - ], - }) - ).deletedCount; - - if (removed) { - logger.debug({ msg: 'Removed existing app items', removed }); - } - } - - logger.debug({ msg: 'Push token updated', doc }); - - // Return the doc we want to use - return doc; -}; - Meteor.methods<ServerMethods>({ async 'raix:push-update'(options) { logger.debug({ msg: 'Got push token from app', options }); @@ -124,11 +39,28 @@ Meteor.methods<ServerMethods>({ }); // The if user id is set then user id should match on client and connection - if (options.userId && options.userId !== this.userId) { + if (!this.userId || (options.userId && options.userId !== this.userId)) { throw new Meteor.Error(403, 'Forbidden access'); } - return pushUpdate(options); + // Retain old behavior: if id is not specified but userId is explicitly set, then update the user's first token + if (!options.id && options.userId) { + const firstDoc = await PushToken.findFirstByUserId(options.userId, { projection: { _id: 1 } }); + if (firstDoc) { + options.id = firstDoc._id; + } + } + + const authToken = Accounts._hashLoginToken(options.authToken); + + return Push.registerPushToken({ + ...(options.id && { _id: options.id }), + token: options.token, + appName: options.appName, + authToken, + userId: this.userId, + ...(options.metadata && { metadata: options.metadata }), + }); }, // Deprecated async 'raix:push-setuser'(id) { @@ -138,7 +70,7 @@ Meteor.methods<ServerMethods>({ } logger.debug({ msg: 'Setting userId for app', userId: this.userId, appId: id }); - const found = await AppsTokens.updateOne({ _id: id }, { $set: { userId: this.userId } }); + const found = await PushToken.updateOne({ _id: id }, { $set: { userId: this.userId } }); return !!found; }, diff --git a/apps/meteor/app/push/server/push.ts b/apps/meteor/app/push/server/push.ts index 04e217822156a..860900a92471c 100644 --- a/apps/meteor/app/push/server/push.ts +++ b/apps/meteor/app/push/server/push.ts @@ -1,5 +1,5 @@ -import type { IAppsTokens, RequiredField, Optional, IPushNotificationConfig } from '@rocket.chat/core-typings'; -import { AppsTokens } from '@rocket.chat/models'; +import type { IPushToken, RequiredField, Optional, IPushNotificationConfig } from '@rocket.chat/core-typings'; +import { PushToken } from '@rocket.chat/models'; import { ajv } from '@rocket.chat/rest-typings'; import type { ExtendedFetchOptions } from '@rocket.chat/server-fetch'; import { serverFetch as fetch } from '@rocket.chat/server-fetch'; @@ -112,8 +112,8 @@ type GatewayNotification = { query?: { userId: any; }; - token?: IAppsTokens['token']; - tokens?: IAppsTokens['token'][]; + token?: IPushToken['token']; + tokens?: IPushToken['token'][]; payload?: Record<string, any>; delayUntil?: Date; createdAt: Date; @@ -123,8 +123,8 @@ type GatewayNotification = { export type NativeNotificationParameters = { userTokens: string | string[]; notification: PendingPushNotification; - _replaceToken: (currentToken: IAppsTokens['token'], newToken: IAppsTokens['token']) => void; - _removeToken: (token: IAppsTokens['token']) => void; + _replaceToken: (currentToken: IPushToken['token'], newToken: IPushToken['token']) => void; + _removeToken: (token: IPushToken['token']) => void; options: RequiredField<PushOptions, 'gcm'>; }; @@ -167,12 +167,12 @@ class PushClass { } } - private replaceToken(currentToken: IAppsTokens['token'], newToken: IAppsTokens['token']): void { - void AppsTokens.updateMany({ token: currentToken }, { $set: { token: newToken } }); + private replaceToken(currentToken: IPushToken['token'], newToken: IPushToken['token']): void { + void PushToken.updateMany({ token: currentToken }, { $set: { token: newToken } }); } - private removeToken(token: IAppsTokens['token']): void { - void AppsTokens.deleteOne({ token }); + private removeToken(token: IPushToken['token']): void { + void PushToken.deleteOne({ token }); } private shouldUseGateway(): boolean { @@ -180,7 +180,7 @@ class PushClass { } private async sendNotificationNative( - app: IAppsTokens, + app: IPushToken, notification: PendingPushNotification, countApn: string[], countGcm: string[], @@ -275,7 +275,7 @@ class PushClass { if (result.status === 406) { logger.info({ msg: 'removing push token', token }); - await AppsTokens.deleteMany({ + await PushToken.deleteMany({ $or: [ { 'token.apn': token, @@ -325,7 +325,7 @@ class PushClass { } private async sendNotificationGateway( - app: IAppsTokens, + app: IPushToken, notification: PendingPushNotification, countApn: string[], countGcm: string[], @@ -378,7 +378,7 @@ class PushClass { $or: [{ 'token.apn': { $exists: true } }, { 'token.gcm': { $exists: true } }], }; - const appTokens = AppsTokens.find(query); + const appTokens = PushToken.find(query); for await (const app of appTokens) { logger.debug({ msg: 'send to token', token: app.token }); @@ -402,15 +402,15 @@ class PushClass { // Add some verbosity about the send result, making sure the developer // understands what just happened. if (!countApn.length && !countGcm.length) { - if ((await AppsTokens.estimatedDocumentCount()) === 0) { + if ((await PushToken.estimatedDocumentCount()) === 0) { logger.debug('GUIDE: The "AppsTokens" is empty - No clients have registered on the server yet...'); } } else if (!countApn.length) { - if ((await AppsTokens.countApnTokens()) === 0) { + if ((await PushToken.countApnTokens()) === 0) { logger.debug('GUIDE: The "AppsTokens" - No APN clients have registered on the server yet...'); } } else if (!countGcm.length) { - if ((await AppsTokens.countGcmTokens()) === 0) { + if ((await PushToken.countGcmTokens()) === 0) { logger.debug('GUIDE: The "AppsTokens" - No GCM clients have registered on the server yet...'); } } diff --git a/apps/meteor/server/lib/pushConfig.ts b/apps/meteor/server/lib/pushConfig.ts index 62cd6a4bbc68c..95719d2e9c415 100644 --- a/apps/meteor/server/lib/pushConfig.ts +++ b/apps/meteor/server/lib/pushConfig.ts @@ -1,6 +1,6 @@ import type { IUser } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; -import { AppsTokens } from '@rocket.chat/models'; +import { PushToken } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { i18n } from './i18n'; @@ -10,7 +10,7 @@ import { Push } from '../../app/push/server'; import { settings } from '../../app/settings/server'; export const executePushTest = async (userId: IUser['_id'], username: IUser['username']): Promise<number> => { - const tokens = await AppsTokens.countTokensByUserId(userId); + const tokens = await PushToken.countTokensByUserId(userId); if (tokens === 0) { throw new Meteor.Error('error-no-tokens-for-this-user', 'There are no tokens for this user', { diff --git a/apps/meteor/server/models.ts b/apps/meteor/server/models.ts index 9549477407ad7..49b5b98b19f08 100644 --- a/apps/meteor/server/models.ts +++ b/apps/meteor/server/models.ts @@ -4,7 +4,6 @@ import { AppsLogsModel, AppsModel, AppsPersistenceModel, - AppsTokensRaw, AvatarsRaw, BannersDismissRaw, BannersRaw, @@ -86,7 +85,6 @@ registerModel('IAnalyticsModel', new AnalyticsRaw(db)); registerModel('IAppLogsModel', new AppsLogsModel(db)); registerModel('IAppsModel', new AppsModel(db)); registerModel('IAppsPersistenceModel', new AppsPersistenceModel(db)); -registerModel('IAppsTokensModel', new AppsTokensRaw(db)); registerModel('IAvatarsModel', new AvatarsRaw(db)); registerModel('IBannersDismissModel', new BannersDismissRaw(db)); registerModel('IBannersModel', new BannersRaw(db)); diff --git a/apps/meteor/server/services/push/logger.ts b/apps/meteor/server/services/push/logger.ts new file mode 100644 index 0000000000000..5793949089dd8 --- /dev/null +++ b/apps/meteor/server/services/push/logger.ts @@ -0,0 +1,3 @@ +import { Logger } from '@rocket.chat/logger'; + +export const logger = new Logger('Push'); diff --git a/apps/meteor/server/services/push/service.ts b/apps/meteor/server/services/push/service.ts index 50f977c0d7606..b6ac954f9356f 100644 --- a/apps/meteor/server/services/push/service.ts +++ b/apps/meteor/server/services/push/service.ts @@ -1,7 +1,11 @@ import type { IPushService } from '@rocket.chat/core-services'; import { ServiceClassInternal } from '@rocket.chat/core-services'; +import type { IPushToken, Optional } from '@rocket.chat/core-typings'; import { PushToken } from '@rocket.chat/models'; +import { logger } from './logger'; +import { registerPushToken } from './tokenManagement/registerPushToken'; + export class PushService extends ServiceClassInternal implements IPushService { protected name = 'push'; @@ -26,4 +30,23 @@ export class PushService extends ServiceClassInternal implements IPushService { } }); } + + async registerPushToken( + data: Optional<Pick<IPushToken, '_id' | 'token' | 'authToken' | 'appName' | 'userId' | 'metadata'>, '_id' | 'metadata'>, + ): Promise<Omit<IPushToken, 'authToken'>> { + const tokenId = await registerPushToken(data); + + const removeResult = await PushToken.removeByTokenAndAppNameExceptId(data.token, data.appName, tokenId); + if (removeResult.deletedCount) { + logger.debug({ msg: 'Removed existing app items', removed: removeResult.deletedCount }); + } + + const updatedDoc = await PushToken.findOneById<Omit<IPushToken, 'authToken'>>(tokenId, { projection: { authToken: 0 } }); + if (!updatedDoc) { + logger.error({ msg: 'Could not find PushToken document on mongo after successful operation', tokenId }); + throw new Error('could-not-find-token-document'); + } + + return updatedDoc; + } } diff --git a/apps/meteor/server/services/push/tokenManagement/findDocumentToUpdate.ts b/apps/meteor/server/services/push/tokenManagement/findDocumentToUpdate.ts new file mode 100644 index 0000000000000..9e7fa5967f183 --- /dev/null +++ b/apps/meteor/server/services/push/tokenManagement/findDocumentToUpdate.ts @@ -0,0 +1,17 @@ +import type { IPushToken } from '@rocket.chat/core-typings'; +import { PushToken } from '@rocket.chat/models'; + +export async function findDocumentToUpdate(data: Partial<IPushToken>): Promise<IPushToken | null> { + if (data._id) { + const existingDoc = await PushToken.findOneById(data._id); + if (existingDoc) { + return existingDoc; + } + } + + if (data.token && data.appName) { + return PushToken.findOneByTokenAndAppName(data.token, data.appName); + } + + return null; +} diff --git a/apps/meteor/server/services/push/tokenManagement/registerPushToken.ts b/apps/meteor/server/services/push/tokenManagement/registerPushToken.ts new file mode 100644 index 0000000000000..c5ae10059e4d8 --- /dev/null +++ b/apps/meteor/server/services/push/tokenManagement/registerPushToken.ts @@ -0,0 +1,41 @@ +import type { IPushToken, Optional } from '@rocket.chat/core-typings'; +import { PushToken } from '@rocket.chat/models'; + +import { logger } from '../logger'; +import { findDocumentToUpdate } from './findDocumentToUpdate'; + +export async function registerPushToken( + data: Optional<Pick<IPushToken, '_id' | 'token' | 'authToken' | 'appName' | 'userId' | 'metadata'>, '_id' | 'metadata'>, +): Promise<IPushToken['_id']> { + const doc = await findDocumentToUpdate(data); + + if (!doc) { + const insertResult = await PushToken.insertToken({ + ...(data._id && { _id: data._id }), + token: data.token, + authToken: data.authToken, + appName: data.appName, + userId: data.userId, + ...(data.metadata && { metadata: data.metadata }), + }); + + const { authToken: _, ...dataWithNoAuthToken } = data; + logger.debug({ msg: 'Push token added', dataWithNoAuthToken, insertResult }); + + return insertResult.insertedId; + } + + const updateResult = await PushToken.refreshTokenById(doc._id, { + token: data.token, + authToken: data.authToken, + appName: data.appName, + userId: data.userId, + }); + + if (updateResult.modifiedCount) { + const { authToken: _, ...dataWithNoAuthToken } = data; + logger.debug({ msg: 'Push token updated', dataWithNoAuthToken, updateResult }); + } + + return doc._id; +} diff --git a/apps/meteor/tests/end-to-end/api/push.ts b/apps/meteor/tests/end-to-end/api/push.ts index 8319862fa5a7f..a5a8183b1a614 100644 --- a/apps/meteor/tests/end-to-end/api/push.ts +++ b/apps/meteor/tests/end-to-end/api/push.ts @@ -8,9 +8,46 @@ describe('[Push]', () => { before((done) => getCredentials(done)); describe('POST [/push.token]', () => { + it('should succeed with a valid gcm token', async () => { + await request + .post(api('push.token')) + .set(credentials) + .send({ + type: 'gcm', + value: 'token', + appName: 'com.example.rocketchat', + }) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('result').and.to.be.an('object'); + }); + }); + + it('should succeed with a valid apn token', async () => { + await request + .post(api('push.token')) + .set(credentials) + .send({ + type: 'apn', + value: 'token', + appName: 'com.example.rocketchat', + }) + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('result').and.to.be.an('object'); + }); + }); + it('should fail if not logged in', async () => { await request .post(api('push.token')) + .send({ + type: 'gcm', + value: 'token', + appName: 'com.example.rocketchat', + }) .expect(401) .expect((res) => { expect(res.body).to.have.property('status', 'error'); @@ -29,7 +66,8 @@ describe('[Push]', () => { .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('errorType', 'error-type-param-not-valid'); + expect(res.body).to.have.property('errorType', 'invalid-params'); + expect(res.body).to.have.property('error').that.includes(`must have required property 'type'`); }); }); @@ -44,7 +82,8 @@ describe('[Push]', () => { .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('errorType', 'error-token-param-not-valid'); + expect(res.body).to.have.property('errorType', 'invalid-params'); + expect(res.body).to.have.property('error').that.includes(`must have required property 'value'`); }); }); @@ -59,7 +98,8 @@ describe('[Push]', () => { .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('errorType', 'error-appName-param-not-valid'); + expect(res.body).to.have.property('errorType', 'invalid-params'); + expect(res.body).to.have.property('error').that.includes(`must have required property 'appName'`); }); }); @@ -69,11 +109,14 @@ describe('[Push]', () => { .set(credentials) .send({ type: 'unknownPlatform', + value: 'token', + appName: 'com.example.rocketchat', }) .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('errorType', 'error-type-param-not-valid'); + expect(res.body).to.have.property('errorType', 'invalid-params'); + expect(res.body).to.have.property('error').that.includes(`must be equal to one of the allowed values`); }); }); @@ -89,23 +132,8 @@ describe('[Push]', () => { .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('errorType', 'error-token-param-not-valid'); - }); - }); - - it('should add a token if valid', async () => { - await request - .post(api('push.token')) - .set(credentials) - .send({ - type: 'gcm', - value: 'token', - appName: 'com.example.rocketchat', - }) - .expect(200) - .expect((res) => { - expect(res.body).to.have.property('success', true); - expect(res.body).to.have.property('result').and.to.be.an('object'); + expect(res.body).to.have.property('errorType', 'invalid-params'); + expect(res.body).to.have.property('error').that.includes(`must NOT have fewer than 1 characters`); }); }); }); @@ -132,7 +160,8 @@ describe('[Push]', () => { .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('errorType', 'error-token-param-not-valid'); + expect(res.body).to.have.property('errorType', 'invalid-params'); + expect(res.body).to.have.property('error').that.includes(`must have required property 'token'`); }); }); @@ -146,7 +175,8 @@ describe('[Push]', () => { .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('errorType', 'error-token-param-not-valid'); + expect(res.body).to.have.property('errorType', 'invalid-params'); + expect(res.body).to.have.property('error').that.includes(`must NOT have fewer than 1 characters`); }); }); diff --git a/packages/core-services/src/index.ts b/packages/core-services/src/index.ts index c0f2aeb517611..d427b375a0836 100644 --- a/packages/core-services/src/index.ts +++ b/packages/core-services/src/index.ts @@ -189,6 +189,7 @@ export const OmnichannelEEService = proxify<IOmnichannelEEService>('omnichannel- export const Import = proxify<IImportService>('import'); export const OmnichannelAnalytics = proxify<IOmnichannelAnalyticsService>('omnichannel-analytics'); export const User = proxify<IUserService>('user'); +export const Push = proxify<IPushService>('push'); // Calls without wait. Means that the service is optional and the result may be an error // of service/method not available diff --git a/packages/core-services/src/types/IPushService.ts b/packages/core-services/src/types/IPushService.ts index 72ed75a82b78c..0978c284e63fb 100644 --- a/packages/core-services/src/types/IPushService.ts +++ b/packages/core-services/src/types/IPushService.ts @@ -1,3 +1,9 @@ +import type { IPushToken, Optional } from '@rocket.chat/core-typings'; + import type { IServiceClass } from './ServiceClass'; -export type IPushService = IServiceClass; +export interface IPushService extends IServiceClass { + registerPushToken( + data: Optional<Pick<IPushToken, '_id' | 'token' | 'authToken' | 'appName' | 'userId' | 'metadata'>, '_id' | 'metadata'>, + ): Promise<Omit<IPushToken, 'authToken'>>; +} diff --git a/packages/core-typings/src/IAppsTokens.ts b/packages/core-typings/src/IAppsTokens.ts deleted file mode 100644 index 5e28f6163f35f..0000000000000 --- a/packages/core-typings/src/IAppsTokens.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { IRocketChatRecord } from './IRocketChatRecord'; - -// The above to a ts interface -export interface IAppsTokens extends IRocketChatRecord { - token: { apn: string } | { gcm: string }; - authToken: string; - appName: string; - userId: string | null; - metadata: Record<string, any>; - enabled: boolean; - createdAt: Date; - updatedAt: Date; -} diff --git a/packages/core-typings/src/IPushToken.ts b/packages/core-typings/src/IPushToken.ts index bc8888397afed..ee6280fa27832 100644 --- a/packages/core-typings/src/IPushToken.ts +++ b/packages/core-typings/src/IPushToken.ts @@ -1,12 +1,14 @@ import type { IRocketChatRecord } from './IRocketChatRecord'; +import type { ILoginToken } from './IUser'; export type IPushTokenTypes = 'gcm' | 'apn'; export interface IPushToken extends IRocketChatRecord { - token: Record<IPushTokenTypes, string>; + token: Partial<Record<IPushTokenTypes, string>>; appName: string; userId: string; enabled: boolean; + authToken: ILoginToken['hashedToken']; + metadata?: Record<string, unknown>; createdAt: Date; - updatedAt: Date; } diff --git a/packages/core-typings/src/index.ts b/packages/core-typings/src/index.ts index 5e0522cd0cf2d..9d62860b19821 100644 --- a/packages/core-typings/src/index.ts +++ b/packages/core-typings/src/index.ts @@ -112,7 +112,6 @@ export * from './ICalendarEvent'; export * from './search'; export * from './omnichannel'; -export * from './IAppsTokens'; export * from './ILivechatUnitMonitor'; export * from './ICronHistoryItem'; diff --git a/packages/model-typings/src/index.ts b/packages/model-typings/src/index.ts index ee99ac2aa4f99..4b93cc6100621 100644 --- a/packages/model-typings/src/index.ts +++ b/packages/model-typings/src/index.ts @@ -70,7 +70,6 @@ export * from './models/IAppsModel'; export * from './models/IAppsPersistenceModel'; export * from './models/IImportsModel'; export * from './models/IFederationRoomEventsModel'; -export * from './models/IAppsTokensModel'; export * from './models/IAuditLogModel'; export * from './models/ICronHistoryModel'; export * from './models/IMigrationsModel'; diff --git a/packages/model-typings/src/models/IAppsTokensModel.ts b/packages/model-typings/src/models/IAppsTokensModel.ts deleted file mode 100644 index 4043ec7193938..0000000000000 --- a/packages/model-typings/src/models/IAppsTokensModel.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { IAppsTokens, IUser } from '@rocket.chat/core-typings'; - -import type { IBaseModel } from './IBaseModel'; - -export interface IAppsTokensModel extends IBaseModel<IAppsTokens> { - countTokensByUserId(userId: IUser['_id']): Promise<number>; - countGcmTokens(): Promise<number>; - countApnTokens(): Promise<number>; -} diff --git a/packages/model-typings/src/models/IPushTokenModel.ts b/packages/model-typings/src/models/IPushTokenModel.ts index 6295c3e6ea655..e9be625e47705 100644 --- a/packages/model-typings/src/models/IPushTokenModel.ts +++ b/packages/model-typings/src/models/IPushTokenModel.ts @@ -1,10 +1,24 @@ -import type { IPushToken } from '@rocket.chat/core-typings'; -import type { DeleteResult } from 'mongodb'; +import type { AtLeast, IPushToken, IUser } from '@rocket.chat/core-typings'; +import type { DeleteResult, FindOptions, InsertOneResult, UpdateResult } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; export interface IPushTokenModel extends IBaseModel<IPushToken> { - removeByUserIdExceptTokens(userId: string, tokens: string[]): Promise<DeleteResult>; + countTokensByUserId(userId: IUser['_id']): Promise<number>; + countGcmTokens(): Promise<number>; + countApnTokens(): Promise<number>; + findOneByTokenAndAppName(token: IPushToken['token'], appName: IPushToken['appName']): Promise<IPushToken | null>; + findFirstByUserId<T extends IPushToken>(userId: IUser['_id'], options?: FindOptions<IPushToken>): Promise<T | null>; + + insertToken(data: AtLeast<IPushToken, 'token' | 'authToken' | 'appName' | 'userId'>): Promise<InsertOneResult<IPushToken>>; + refreshTokenById( + id: IPushToken['_id'], + data: Pick<IPushToken, 'token' | 'appName' | 'authToken' | 'userId'>, + ): Promise<UpdateResult<IPushToken>>; + + removeByUserIdExceptTokens(userId: string, tokens: IPushToken['authToken'][]): Promise<DeleteResult>; + removeByTokenAndAppNameExceptId(token: IPushToken['token'], appName: IPushToken['appName'], id: IPushToken['_id']): Promise<DeleteResult>; removeAllByUserId(userId: string): Promise<DeleteResult>; + removeAllByTokenStringAndUserId(token: string, userId: string): Promise<DeleteResult>; } diff --git a/packages/models/src/index.ts b/packages/models/src/index.ts index 8548a8b1e176f..b36f38a9cfaa3 100644 --- a/packages/models/src/index.ts +++ b/packages/models/src/index.ts @@ -70,7 +70,6 @@ import type { IAppLogsModel, IImportsModel, IFederationRoomEventsModel, - IAppsTokensModel, IAuditLogModel, ICronHistoryModel, IMigrationsModel, @@ -123,7 +122,6 @@ export { registerModel } from './proxify'; export { type Updater, UpdaterImpl } from './updater'; export const Apps = proxify<IAppsModel>('IAppsModel'); -export const AppsTokens = proxify<IAppsTokensModel>('IAppsTokensModel'); export const AppsPersistence = proxify<IAppsPersistenceModel>('IAppsPersistenceModel'); export const AppLogs = proxify<IAppLogsModel>('IAppLogsModel'); export const Analytics = proxify<IAnalyticsModel>('IAnalyticsModel'); diff --git a/packages/models/src/modelClasses.ts b/packages/models/src/modelClasses.ts index bb4172e3ec9e9..c554fc9698550 100644 --- a/packages/models/src/modelClasses.ts +++ b/packages/models/src/modelClasses.ts @@ -3,7 +3,6 @@ export * from './models/AbacAttributes'; export * from './models/Analytics'; export * from './models/Apps'; export * from './models/AppsPersistence'; -export * from './models/AppsTokens'; export * from './models/AppLogsModel'; export * from './models/Avatars'; export * from './models/Banners'; diff --git a/packages/models/src/models/AppsTokens.ts b/packages/models/src/models/AppsTokens.ts deleted file mode 100644 index b151b3de6310d..0000000000000 --- a/packages/models/src/models/AppsTokens.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { IAppsTokens, IUser } from '@rocket.chat/core-typings'; -import type { IAppsTokensModel } from '@rocket.chat/model-typings'; -import type { Db } from 'mongodb'; - -import { BaseRaw } from './BaseRaw'; - -export class AppsTokensRaw extends BaseRaw<IAppsTokens> implements IAppsTokensModel { - constructor(db: Db) { - super(db, '_raix_push_app_tokens', undefined, { collectionNameResolver: (name) => name }); - } - - countApnTokens() { - const query = { - 'token.apn': { $exists: true }, - }; - - return this.countDocuments(query); - } - - countGcmTokens() { - const query = { - 'token.gcm': { $exists: true }, - }; - - return this.countDocuments(query); - } - - countTokensByUserId(userId: IUser['_id']) { - const query = { - userId, - $or: [{ 'token.apn': { $exists: true } }, { 'token.gcm': { $exists: true } }], - }; - - return this.countDocuments(query); - } -} diff --git a/packages/models/src/models/PushToken.ts b/packages/models/src/models/PushToken.ts index 3ada0d55a2d98..e7af5131fcfe2 100644 --- a/packages/models/src/models/PushToken.ts +++ b/packages/models/src/models/PushToken.ts @@ -1,6 +1,6 @@ -import type { IPushToken } from '@rocket.chat/core-typings'; +import type { IPushToken, IUser, AtLeast } from '@rocket.chat/core-typings'; import type { IPushTokenModel } from '@rocket.chat/model-typings'; -import type { Db, DeleteResult, IndexDescription } from 'mongodb'; +import type { Db, DeleteResult, FindOptions, IndexDescription, InsertOneResult, UpdateResult } from 'mongodb'; import { BaseRaw } from './BaseRaw'; @@ -17,16 +17,103 @@ export class PushTokenRaw extends BaseRaw<IPushToken> implements IPushTokenModel return [{ key: { userId: 1, authToken: 1 } }, { key: { appName: 1, token: 1 } }]; } - removeByUserIdExceptTokens(userId: string, tokens: string[]): Promise<DeleteResult> { + countApnTokens() { + const query = { + 'token.apn': { $exists: true }, + }; + + return this.countDocuments(query); + } + + countGcmTokens() { + const query = { + 'token.gcm': { $exists: true }, + }; + + return this.countDocuments(query); + } + + countTokensByUserId(userId: IUser['_id']) { + const query = { + userId, + $or: [{ 'token.apn': { $exists: true } }, { 'token.gcm': { $exists: true } }], + }; + + return this.countDocuments(query); + } + + async findFirstByUserId<T extends IPushToken>(userId: IUser['_id'], options: FindOptions<IPushToken> = {}): Promise<T | null> { + return this.findOne<T>({ userId }, options); + } + + async insertToken(data: AtLeast<IPushToken, 'token' | 'authToken' | 'appName' | 'userId'>): Promise<InsertOneResult<IPushToken>> { + return this.insertOne({ + enabled: true, + createdAt: new Date(), + ...data, + }); + } + + async refreshTokenById( + id: IPushToken['_id'], + data: Pick<IPushToken, 'token' | 'appName' | 'authToken' | 'userId'>, + ): Promise<UpdateResult<IPushToken>> { + return this.updateOne( + { _id: id }, + { + $set: { + token: data.token, + authToken: data.authToken, + appName: data.appName, + userId: data.userId, + }, + }, + ); + } + + findOneByTokenAndAppName(token: IPushToken['token'], appName: IPushToken['appName']): Promise<IPushToken | null> { + return this.findOne({ + token, + appName, + }); + } + + removeByUserIdExceptTokens(userId: string, tokens: IPushToken['authToken'][]): Promise<DeleteResult> { return this.deleteMany({ userId, authToken: { $nin: tokens }, }); } + removeByTokenAndAppNameExceptId( + token: IPushToken['token'], + appName: IPushToken['appName'], + id: IPushToken['_id'], + ): Promise<DeleteResult> { + return this.deleteMany({ + token, + appName, + _id: { $ne: id }, + }); + } + removeAllByUserId(userId: string): Promise<DeleteResult> { return this.deleteMany({ userId, }); } + + removeAllByTokenStringAndUserId(token: string, userId: string): Promise<DeleteResult> { + return this.deleteMany({ + $or: [ + { + 'token.apn': token, + }, + { + 'token.gcm': token, + }, + ], + userId, + }); + } } diff --git a/packages/rest-typings/src/v1/push.ts b/packages/rest-typings/src/v1/push.ts index 5fdf4172c288a..f09cfc892a611 100644 --- a/packages/rest-typings/src/v1/push.ts +++ b/packages/rest-typings/src/v1/push.ts @@ -1,4 +1,4 @@ -import type { IMessage, IPushNotificationConfig, IPushTokenTypes, IPushToken } from '@rocket.chat/core-typings'; +import type { IMessage, IPushNotificationConfig, IPushTokenTypes } from '@rocket.chat/core-typings'; import { ajv } from './Ajv'; @@ -50,10 +50,6 @@ const PushGetPropsSchema = { export const isPushGetProps = ajv.compile<PushGetProps>(PushGetPropsSchema); export type PushEndpoints = { - '/v1/push.token': { - POST: (payload: PushTokenProps) => { result: IPushToken }; - DELETE: (payload: { token: string }) => void; - }; '/v1/push.get': { GET: (params: PushGetProps) => { data: { From b4b9303d316702503dbab8629e63203209049365 Mon Sep 17 00:00:00 2001 From: Sandra Nymark-Brand <143543945+sandranymark@users.noreply.github.com> Date: Thu, 26 Feb 2026 22:46:23 +0100 Subject: [PATCH 076/108] fix(ux): Room header encrypted key icon - very low contrast (#36058) Co-authored-by: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Co-authored-by: Douglas Fabris <devfabris@gmail.com> --- apps/meteor/client/views/room/Header/icons/Encrypted.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/meteor/client/views/room/Header/icons/Encrypted.tsx b/apps/meteor/client/views/room/Header/icons/Encrypted.tsx index 65ffd3ecac502..5386c4beb90fe 100644 --- a/apps/meteor/client/views/room/Header/icons/Encrypted.tsx +++ b/apps/meteor/client/views/room/Header/icons/Encrypted.tsx @@ -1,5 +1,4 @@ import type { IRoom } from '@rocket.chat/core-typings'; -import colors from '@rocket.chat/fuselage-tokens/colors.json'; import { HeaderState } from '@rocket.chat/ui-client'; import { useSetting } from '@rocket.chat/ui-contexts'; import { memo } from 'react'; @@ -8,7 +7,7 @@ import { useTranslation } from 'react-i18next'; const Encrypted = ({ room }: { room: IRoom }) => { const { t } = useTranslation(); const e2eEnabled = useSetting('E2E_Enable'); - return e2eEnabled && room?.encrypted ? <HeaderState title={t('Encrypted')} icon='key' color={colors.g500} /> : null; + return e2eEnabled && room?.encrypted ? <HeaderState title={t('Encrypted')} icon='key' color='status-font-on-success' /> : null; }; export default memo(Encrypted); From 539659af22bc19880eda047dfc0b152472ccb65c Mon Sep 17 00:00:00 2001 From: Tasso Evangelista <tasso.evangelista@rocket.chat> Date: Fri, 27 Feb 2026 11:16:24 -0300 Subject: [PATCH 077/108] chore(eslint): Upgrade ESLint configuration (#38989) --- .changeset/little-eyes-kneel.md | 76 + .github/workflows/ci-code-check.yml | 2 - .vscode/settings.json | 18 - apps/meteor/.eslintrc.json | 156 -- apps/meteor/package.json | 21 +- apps/meteor/packages/.eslintrc.json | 6 - .../packages/rocketchat-i18n/.eslintrc.json | 5 - apps/meteor/tests/e2e/.eslintrc.json | 37 - apps/uikit-playground/.eslintrc.json | 32 - apps/uikit-playground/package.json | 9 +- ee/apps/account-service/.eslintrc.json | 16 - ee/apps/account-service/package.json | 5 +- ee/apps/authorization-service/.eslintrc.json | 16 - ee/apps/authorization-service/package.json | 5 +- ee/apps/ddp-streamer/.eslintrc.json | 16 - ee/apps/ddp-streamer/package.json | 5 +- ee/apps/omnichannel-transcript/.eslintrc.json | 4 - ee/apps/omnichannel-transcript/package.json | 5 +- ee/apps/presence-service/.eslintrc.json | 16 - ee/apps/presence-service/package.json | 5 +- ee/apps/queue-worker/.eslintrc.json | 4 - ee/apps/queue-worker/package.json | 5 +- ee/packages/abac/.eslintrc.json | 4 - ee/packages/abac/package.json | 7 +- ee/packages/federation-matrix/.eslintrc.json | 4 - ee/packages/federation-matrix/package.json | 3 +- ee/packages/license/.eslintrc.json | 4 - ee/packages/license/package.json | 6 +- ee/packages/media-calls/.eslintrc.json | 4 - ee/packages/media-calls/package.json | 6 +- ee/packages/network-broker/.eslintrc.json | 4 - ee/packages/network-broker/package.json | 7 +- ee/packages/omni-core-ee/.eslintrc.json | 4 - ee/packages/omni-core-ee/package.json | 7 +- .../omnichannel-services/.eslintrc.json | 4 - ee/packages/omnichannel-services/package.json | 7 +- ee/packages/pdf-worker/.eslintrc.json | 5 - ee/packages/pdf-worker/package.json | 7 +- ee/packages/presence/.eslintrc.json | 4 - ee/packages/presence/package.json | 3 +- ee/packages/ui-theming/.eslintrc.json | 4 - ee/packages/ui-theming/package.json | 10 +- eslint.config.mjs | 613 +++++++ package.json | 6 +- packages/account-utils/.eslintrc.json | 4 - packages/account-utils/package.json | 6 +- packages/agenda/.eslintrc.json | 4 - packages/agenda/package.json | 6 +- packages/api-client/.eslintrc.json | 4 - packages/api-client/package.json | 6 +- packages/apps-engine/.eslintrc.json | 59 - packages/apps-engine/package.json | 7 +- packages/apps/.eslintrc.json | 4 - packages/apps/package.json | 6 +- packages/base64/.eslintrc.json | 4 - packages/base64/package.json | 4 +- packages/cas-validate/.eslintrc.json | 4 - packages/cas-validate/package.json | 6 +- packages/core-services/.eslintrc.json | 4 - packages/core-services/package.json | 3 +- packages/core-typings/.eslintrc.json | 15 - packages/core-typings/package.json | 7 +- packages/core-typings/src/utils.ts | 1 - packages/cron/.eslintrc.json | 4 - packages/cron/package.json | 6 +- packages/ddp-client/.eslintrc.json | 26 - packages/ddp-client/package.json | 6 +- packages/desktop-api/.eslintrc.json | 4 - packages/desktop-api/package.json | 5 +- packages/eslint-config/.eslintrc.json | 3 - .../eslint-config/best-practices/index.js | 303 ---- packages/eslint-config/errors/index.js | 148 -- packages/eslint-config/es6/index.js | 189 -- packages/eslint-config/imports/index.js | 257 --- packages/eslint-config/index.js | 565 ++++++ packages/eslint-config/node/index.js | 33 - packages/eslint-config/original/index.js | 148 -- packages/eslint-config/package.json | 44 +- packages/eslint-config/react.js | 35 - packages/eslint-config/standard/index.js | 173 -- packages/eslint-config/style/index.js | 473 ----- packages/eslint-config/variables/index.js | 51 - packages/favicon/.eslintrc.json | 7 - packages/favicon/package.json | 6 +- packages/fuselage-ui-kit/.eslintrc.json | 12 - packages/fuselage-ui-kit/package.json | 7 +- packages/gazzodown/.eslintrc.json | 4 - packages/gazzodown/package.json | 12 +- packages/http-router/.eslintrc.json | 4 - packages/http-router/package.json | 7 +- packages/i18n/.eslintrc.json | 8 - packages/i18n/package.json | 6 +- packages/instance-status/.eslintrc.json | 12 - packages/instance-status/package.json | 7 +- packages/jest-presets/.eslintrc.json | 5 - packages/jest-presets/package.json | 3 +- packages/jwt/.eslintrc.json | 4 - packages/jwt/package.json | 6 +- packages/livechat/.eslintrc.json | 104 -- packages/livechat/package.json | 12 +- packages/log-format/.eslintrc.json | 4 - packages/log-format/package.json | 6 +- packages/logger/.eslintrc.json | 4 - packages/logger/package.json | 6 +- packages/media-signaling/.eslintrc.json | 4 - packages/media-signaling/package.json | 6 +- packages/message-parser/.eslintrc.json | 4 - packages/message-parser/package.json | 4 +- packages/message-types/.eslintrc.json | 4 - packages/message-types/package.json | 3 +- packages/mock-providers/.eslintrc.json | 4 - packages/mock-providers/package.json | 6 +- packages/model-typings/.eslintrc.json | 4 - packages/model-typings/package.json | 6 +- packages/models/.eslintrc.json | 4 - packages/models/package.json | 6 +- packages/mongo-adapter/.eslintrc.json | 4 - packages/mongo-adapter/package.json | 6 +- packages/node-poplib/package.json | 1 - packages/omni-core/.eslintrc.json | 4 - packages/omni-core/package.json | 7 +- packages/password-policies/.eslintrc.json | 8 - packages/password-policies/package.json | 6 +- packages/patch-injection/.eslintrc.json | 4 - packages/patch-injection/package.json | 6 +- packages/peggy-loader/.eslintrc.json | 4 - packages/peggy-loader/package.json | 5 +- packages/random/.eslintrc.json | 4 - packages/random/package.json | 5 +- packages/release-action/.eslintrc.json | 4 - packages/release-action/package.json | 5 +- packages/release-changelog/.eslintrc.json | 8 - packages/release-changelog/package.json | 5 +- packages/rest-typings/.eslintrc.json | 4 - packages/rest-typings/package.json | 7 +- .../server-cloud-communication/.eslintrc.json | 4 - .../server-cloud-communication/package.json | 6 +- packages/server-fetch/.eslintrc.json | 4 - packages/server-fetch/package.json | 6 +- packages/sha256/.eslintrc.json | 4 - packages/sha256/package.json | 5 +- packages/storybook-config/.eslintrc.json | 4 - packages/storybook-config/package.json | 7 +- packages/tools/.eslintrc.json | 4 - packages/tools/package.json | 6 +- packages/tracing/.eslintrc.json | 4 - packages/tracing/package.json | 6 +- packages/ui-avatar/.eslintrc.json | 4 - packages/ui-avatar/package.json | 10 +- packages/ui-client/.eslintrc.json | 12 - packages/ui-client/package.json | 11 +- packages/ui-composer/.eslintrc.json | 4 - packages/ui-composer/package.json | 10 +- packages/ui-contexts/.eslintrc.json | 12 - packages/ui-contexts/package.json | 7 +- packages/ui-kit/.eslintrc.json | 4 - packages/ui-kit/package.json | 5 +- packages/ui-video-conf/.eslintrc.json | 4 - packages/ui-video-conf/package.json | 10 +- packages/ui-voip/.eslintrc.json | 12 - packages/ui-voip/package.json | 10 +- packages/web-ui-registration/.eslintrc.json | 12 - packages/web-ui-registration/package.json | 7 +- yarn.lock | 1551 +++++++++-------- 164 files changed, 2325 insertions(+), 3632 deletions(-) create mode 100644 .changeset/little-eyes-kneel.md delete mode 100644 apps/meteor/.eslintrc.json delete mode 100644 apps/meteor/packages/.eslintrc.json delete mode 100644 apps/meteor/packages/rocketchat-i18n/.eslintrc.json delete mode 100644 apps/meteor/tests/e2e/.eslintrc.json delete mode 100644 apps/uikit-playground/.eslintrc.json delete mode 100644 ee/apps/account-service/.eslintrc.json delete mode 100644 ee/apps/authorization-service/.eslintrc.json delete mode 100644 ee/apps/ddp-streamer/.eslintrc.json delete mode 100644 ee/apps/omnichannel-transcript/.eslintrc.json delete mode 100644 ee/apps/presence-service/.eslintrc.json delete mode 100644 ee/apps/queue-worker/.eslintrc.json delete mode 100644 ee/packages/abac/.eslintrc.json delete mode 100644 ee/packages/federation-matrix/.eslintrc.json delete mode 100644 ee/packages/license/.eslintrc.json delete mode 100644 ee/packages/media-calls/.eslintrc.json delete mode 100644 ee/packages/network-broker/.eslintrc.json delete mode 100644 ee/packages/omni-core-ee/.eslintrc.json delete mode 100644 ee/packages/omnichannel-services/.eslintrc.json delete mode 100644 ee/packages/pdf-worker/.eslintrc.json delete mode 100644 ee/packages/presence/.eslintrc.json delete mode 100644 ee/packages/ui-theming/.eslintrc.json create mode 100644 eslint.config.mjs delete mode 100644 packages/account-utils/.eslintrc.json delete mode 100644 packages/agenda/.eslintrc.json delete mode 100644 packages/api-client/.eslintrc.json delete mode 100644 packages/apps-engine/.eslintrc.json delete mode 100644 packages/apps/.eslintrc.json delete mode 100644 packages/base64/.eslintrc.json delete mode 100644 packages/cas-validate/.eslintrc.json delete mode 100644 packages/core-services/.eslintrc.json delete mode 100644 packages/core-typings/.eslintrc.json delete mode 100644 packages/cron/.eslintrc.json delete mode 100644 packages/ddp-client/.eslintrc.json delete mode 100644 packages/desktop-api/.eslintrc.json delete mode 100644 packages/eslint-config/.eslintrc.json delete mode 100644 packages/eslint-config/best-practices/index.js delete mode 100644 packages/eslint-config/errors/index.js delete mode 100644 packages/eslint-config/es6/index.js delete mode 100644 packages/eslint-config/imports/index.js create mode 100644 packages/eslint-config/index.js delete mode 100644 packages/eslint-config/node/index.js delete mode 100644 packages/eslint-config/original/index.js delete mode 100644 packages/eslint-config/react.js delete mode 100644 packages/eslint-config/standard/index.js delete mode 100644 packages/eslint-config/style/index.js delete mode 100644 packages/eslint-config/variables/index.js delete mode 100644 packages/favicon/.eslintrc.json delete mode 100644 packages/fuselage-ui-kit/.eslintrc.json delete mode 100644 packages/gazzodown/.eslintrc.json delete mode 100644 packages/http-router/.eslintrc.json delete mode 100644 packages/i18n/.eslintrc.json delete mode 100644 packages/instance-status/.eslintrc.json delete mode 100644 packages/jest-presets/.eslintrc.json delete mode 100644 packages/jwt/.eslintrc.json delete mode 100644 packages/livechat/.eslintrc.json delete mode 100644 packages/log-format/.eslintrc.json delete mode 100644 packages/logger/.eslintrc.json delete mode 100644 packages/media-signaling/.eslintrc.json delete mode 100644 packages/message-parser/.eslintrc.json delete mode 100644 packages/message-types/.eslintrc.json delete mode 100644 packages/mock-providers/.eslintrc.json delete mode 100644 packages/model-typings/.eslintrc.json delete mode 100644 packages/models/.eslintrc.json delete mode 100644 packages/mongo-adapter/.eslintrc.json delete mode 100644 packages/omni-core/.eslintrc.json delete mode 100644 packages/password-policies/.eslintrc.json delete mode 100644 packages/patch-injection/.eslintrc.json delete mode 100644 packages/peggy-loader/.eslintrc.json delete mode 100644 packages/random/.eslintrc.json delete mode 100644 packages/release-action/.eslintrc.json delete mode 100644 packages/release-changelog/.eslintrc.json delete mode 100644 packages/rest-typings/.eslintrc.json delete mode 100644 packages/server-cloud-communication/.eslintrc.json delete mode 100644 packages/server-fetch/.eslintrc.json delete mode 100644 packages/sha256/.eslintrc.json delete mode 100644 packages/storybook-config/.eslintrc.json delete mode 100644 packages/tools/.eslintrc.json delete mode 100644 packages/tracing/.eslintrc.json delete mode 100644 packages/ui-avatar/.eslintrc.json delete mode 100644 packages/ui-client/.eslintrc.json delete mode 100644 packages/ui-composer/.eslintrc.json delete mode 100644 packages/ui-contexts/.eslintrc.json delete mode 100644 packages/ui-kit/.eslintrc.json delete mode 100644 packages/ui-video-conf/.eslintrc.json delete mode 100644 packages/ui-voip/.eslintrc.json delete mode 100644 packages/web-ui-registration/.eslintrc.json diff --git a/.changeset/little-eyes-kneel.md b/.changeset/little-eyes-kneel.md new file mode 100644 index 0000000000000..0ae80efde92b3 --- /dev/null +++ b/.changeset/little-eyes-kneel.md @@ -0,0 +1,76 @@ +--- +'@rocket.chat/eslint-config': minor +'@rocket.chat/server-cloud-communication': patch +'@rocket.chat/omnichannel-services': patch +'@rocket.chat/omnichannel-transcript': patch +'@rocket.chat/authorization-service': patch +'@rocket.chat/federation-matrix': patch +'@rocket.chat/web-ui-registration': patch +'@rocket.chat/network-broker': patch +'@rocket.chat/password-policies': patch +'@rocket.chat/release-changelog': patch +'@rocket.chat/storybook-config': patch +'@rocket.chat/presence-service': patch +'@rocket.chat/omni-core-ee': patch +'@rocket.chat/fuselage-ui-kit': patch +'@rocket.chat/instance-status': patch +'@rocket.chat/media-signaling': patch +'@rocket.chat/patch-injection': patch +'@rocket.chat/account-service': patch +'@rocket.chat/media-calls': patch +'@rocket.chat/message-parser': patch +'@rocket.chat/mock-providers': patch +'@rocket.chat/release-action': patch +'@rocket.chat/pdf-worker': patch +'@rocket.chat/ui-theming': patch +'@rocket.chat/account-utils': patch +'@rocket.chat/core-services': patch +'@rocket.chat/message-types': patch +'@rocket.chat/model-typings': patch +'@rocket.chat/mongo-adapter': patch +'@rocket.chat/ui-video-conf': patch +'@rocket.chat/uikit-playground': patch +'@rocket.chat/cas-validate': patch +'@rocket.chat/core-typings': patch +'@rocket.chat/jest-presets': patch +'@rocket.chat/peggy-loader': patch +'@rocket.chat/rest-typings': patch +'@rocket.chat/server-fetch': patch +'@rocket.chat/ddp-streamer': patch +'@rocket.chat/queue-worker': patch +'@rocket.chat/presence': patch +'@rocket.chat/apps-engine': patch +'@rocket.chat/desktop-api': patch +'@rocket.chat/http-router': patch +'@rocket.chat/poplib': patch +'@rocket.chat/ui-composer': patch +'@rocket.chat/ui-contexts': patch +'@rocket.chat/license': patch +'@rocket.chat/api-client': patch +'@rocket.chat/ddp-client': patch +'@rocket.chat/log-format': patch +'@rocket.chat/gazzodown': patch +'@rocket.chat/omni-core': patch +'@rocket.chat/ui-avatar': patch +'@rocket.chat/ui-client': patch +'@rocket.chat/livechat': patch +'@rocket.chat/abac': patch +'@rocket.chat/favicon': patch +'@rocket.chat/tracing': patch +'@rocket.chat/ui-voip': patch +'@rocket.chat/agenda': patch +'@rocket.chat/base64': patch +'@rocket.chat/logger': patch +'@rocket.chat/models': patch +'@rocket.chat/random': patch +'@rocket.chat/sha256': patch +'@rocket.chat/ui-kit': patch +'@rocket.chat/tools': patch +'@rocket.chat/apps': patch +'@rocket.chat/cron': patch +'@rocket.chat/i18n': patch +'@rocket.chat/jwt': patch +'@rocket.chat/meteor': patch +--- + +chore(eslint): Upgrades ESLint and its configuration diff --git a/.github/workflows/ci-code-check.yml b/.github/workflows/ci-code-check.yml index 52e3b7c085d9d..5555cead5e580 100644 --- a/.github/workflows/ci-code-check.yml +++ b/.github/workflows/ci-code-check.yml @@ -56,7 +56,6 @@ jobs: - name: Cache TypeCheck uses: actions/cache@v5 - if: matrix.check == 'ts' with: path: ./apps/meteor/tsconfig.typecheck.tsbuildinfo key: typecheck-cache-${{ runner.OS }}-${{ hashFiles('yarn.lock') }}-${{ github.event.issue.number }} @@ -66,7 +65,6 @@ jobs: typecheck-cache - name: Install Meteor - if: matrix.check == 'ts' shell: bash run: | # Restore bin from cache diff --git a/.vscode/settings.json b/.vscode/settings.json index 2dcd055310d14..9bc41d08b8468 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,22 +1,4 @@ { - "eslint.workingDirectories": [ - { - "pattern": "packages/*", - "changeProcessCWD": true - }, - { - "pattern": "apps/*", - "changeProcessCWD": true - }, - { - "pattern": "ee/apps/*", - "changeProcessCWD": true - }, - { - "pattern": "ee/packages/*", - "changeProcessCWD": true - } - ], "typescript.tsdk": "./node_modules/typescript/lib", "cSpell.words": [ "autotranslate", diff --git a/apps/meteor/.eslintrc.json b/apps/meteor/.eslintrc.json deleted file mode 100644 index 47376a4e7fddf..0000000000000 --- a/apps/meteor/.eslintrc.json +++ /dev/null @@ -1,156 +0,0 @@ -{ - "extends": [ - "@rocket.chat/eslint-config", - "@rocket.chat/eslint-config/react", - "plugin:you-dont-need-lodash-underscore/compatible", - "plugin:storybook/recommended" - ], - "globals": { - "__meteor_bootstrap__": false, - "__meteor_runtime_config__": false, - "Assets": false, - "chrome": false, - "jscolor": false - }, - "rules": { - "import/named": "error", - "react-hooks/exhaustive-deps": [ - "warn", - { - "additionalHooks": "(useComponentDidUpdate)" - } - ], - "prefer-arrow-callback": [ - "error", - { - "allowNamedFunctions": true - } - ] - }, - "ignorePatterns": [ - "app/emoji-emojione/generateEmojiIndex.js", - "public", - "private/moment-locales", - "imports", - "ee/server/services/dist", - "!.mocharc.js", - "!.mocharc.*.js", - "!.scripts", - "!.storybook", - "!client/.eslintrc.js", - "!ee/client/.eslintrc.js", - "storybook-static", - "packages" - ], - "overrides": [ - { - "files": ["**/*.ts", "**/*.tsx"], - "rules": { - "@typescript-eslint/naming-convention": [ - "error", - { - "selector": ["function", "parameter", "variable"], - "modifiers": ["destructured"], - "format": null - }, - { - "selector": ["variable"], - "format": ["camelCase", "UPPER_CASE", "PascalCase"], - "leadingUnderscore": "allowSingleOrDouble" - }, - { - "selector": ["function"], - "format": ["camelCase", "PascalCase"], - "leadingUnderscore": "allowSingleOrDouble" - }, - { - "selector": ["parameter"], - "format": ["PascalCase"], - "filter": { - "regex": "Component$", - "match": true - } - }, - { - "selector": ["parameter"], - "format": ["camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": ["parameter"], - "format": ["camelCase"], - "modifiers": ["unused"], - "leadingUnderscore": "require" - }, - { - "selector": "parameter", - "format": null, - "filter": { - "regex": "^Story$", - "match": true - } - }, - { - "selector": ["interface"], - "format": ["PascalCase"], - "custom": { - "regex": "^I[A-Z]", - "match": true - } - } - ], - "no-unreachable-loop": "error" - }, - "parserOptions": { - "project": ["./tsconfig.json"] - }, - "excludedFiles": [".scripts/*.ts"] - }, - { - "files": ["**/*.tests.js", "**/*.tests.ts", "**/*.spec.ts"], - "env": { - "mocha": true - } - }, - { - "files": ["**/*.spec.ts", "**/*.spec.tsx"], - "extends": ["plugin:testing-library/react"], - "rules": { - "testing-library/no-await-sync-events": "warn", - "testing-library/no-manual-cleanup": "warn", - "testing-library/prefer-explicit-assert": "warn", - "testing-library/prefer-user-event": "warn" - }, - "env": { - "mocha": true - } - }, - { - "files": ["**/*.stories.js", "**/*.stories.jsx", "**/*.stories.ts", "**/*.stories.tsx", "**/*.spec.tsx"], - "rules": { - "react/display-name": "off", - "react/no-multi-comp": "off" - } - }, - { - "files": ["**/*.stories.ts", "**/*.stories.tsx"], - "rules": { - "@typescript-eslint/explicit-function-return-type": "off", - "@typescript-eslint/explicit-module-boundary-types": "off" - } - }, - { - "files": ["client/**/*.ts", "client/**/*.tsx", "ee/client/**/*.ts", "ee/client/**/*.tsx"], - "rules": { - "@typescript-eslint/no-misused-promises": "off", - "@typescript-eslint/no-floating-promises": "off" - } - }, - { - "files": ["**/*.d.ts"], - "rules": { - "@typescript-eslint/naming-convention": "off" - } - } - ] -} diff --git a/apps/meteor/package.json b/apps/meteor/package.json index c8cd3ef402cfc..4f56db912a2e2 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -34,12 +34,12 @@ "dev": "NODE_OPTIONS=\"--trace-warnings\" meteor --exclude-archs \"web.browser.legacy, web.cordova\"", "docker:start": "docker-compose up", "dsv": "meteor npm run dev", - "eslint": "NODE_OPTIONS=\"--max-old-space-size=8192\" eslint --ext .js,.jsx,.ts,.tsx . --cache", + "eslint": "NODE_OPTIONS=\"--max-old-space-size=8192\" eslint --cache", "eslint:fix": "yarn eslint --fix", "ha": "meteor npm run ha:start", "ha:add": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' ts-node .scripts/run-ha.ts instance", "ha:start": "TS_NODE_COMPILER_OPTIONS='{\"module\": \"commonjs\"}' ts-node .scripts/run-ha.ts main", - "lint": "yarn stylelint && yarn eslint", + "lint": "yarn stylelint && meteor lint && yarn eslint .", "migration:add": "ts-node-transpile-only --skip-project .scripts/make-migration.ts", "ms": "TRANSPORTER=${TRANSPORTER:-TCP} meteor npm run dev", "obj:dev": "TEST_MODE=true yarn dev", @@ -309,14 +309,12 @@ "devDependencies": { "@axe-core/playwright": "^4.10.2", "@babel/core": "~7.28.6", - "@babel/eslint-parser": "~7.28.6", "@babel/preset-env": "~7.28.6", "@babel/preset-react": "~7.27.1", "@babel/register": "~7.28.6", "@faker-js/faker": "~8.0.2", "@playwright/test": "^1.52.0", "@rocket.chat/desktop-api": "workspace:~", - "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/livechat": "workspace:^", "@rocket.chat/mock-providers": "workspace:^", @@ -400,8 +398,6 @@ "@types/underscore": "^1.13.0", "@types/xml-crypto": "~1.4.6", "@types/xml-encryption": "~1.2.4", - "@typescript-eslint/eslint-plugin": "~5.60.1", - "@typescript-eslint/parser": "~5.60.1", "autoprefixer": "^9.8.8", "babel-loader": "~10.0.0", "babel-plugin-array-includes": "^2.0.3", @@ -414,18 +410,7 @@ "cross-env": "^7.0.3", "docker-compose": "^0.24.8", "emojione-assets": "^4.5.0", - "eslint": "~8.45.0", - "eslint-config-prettier": "~9.1.2", - "eslint-plugin-anti-trojan-source": "~1.1.2", - "eslint-plugin-import": "~2.31.0", - "eslint-plugin-no-floating-promise": "~2.0.0", - "eslint-plugin-playwright": "~2.2.2", - "eslint-plugin-prettier": "~5.2.6", - "eslint-plugin-react": "~7.37.5", - "eslint-plugin-react-hooks": "~5.0.0", - "eslint-plugin-storybook": "~0.11.6", - "eslint-plugin-testing-library": "~6.4.0", - "eslint-plugin-you-dont-need-lodash-underscore": "~6.14.0", + "eslint": "~9.39.3", "fast-glob": "^3.3.3", "i18next": "~23.4.9", "jest": "~30.2.0", diff --git a/apps/meteor/packages/.eslintrc.json b/apps/meteor/packages/.eslintrc.json deleted file mode 100644 index 0e3406639dc1c..0000000000000 --- a/apps/meteor/packages/.eslintrc.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "globals": { - "Package": false, - "Npm": false - } -} diff --git a/apps/meteor/packages/rocketchat-i18n/.eslintrc.json b/apps/meteor/packages/rocketchat-i18n/.eslintrc.json deleted file mode 100644 index e891ae3e268d1..0000000000000 --- a/apps/meteor/packages/rocketchat-i18n/.eslintrc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "globals": { - "Npm" : false - } -} diff --git a/apps/meteor/tests/e2e/.eslintrc.json b/apps/meteor/tests/e2e/.eslintrc.json deleted file mode 100644 index aa970258b4c7e..0000000000000 --- a/apps/meteor/tests/e2e/.eslintrc.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "root": true, - "extends": ["@rocket.chat/eslint-config", "@rocket.chat/eslint-config/react", "prettier", "plugin:@typescript-eslint/recommended"], - "parser": "@typescript-eslint/parser", - "plugins": ["prettier", "testing-library", "anti-trojan-source", "no-floating-promise"], - "rules": { - "@typescript-eslint/no-unused-vars": [ - "error", - { - "argsIgnorePattern": "^_", - "ignoreRestSiblings": true - } - ], - "@typescript-eslint/no-floating-promises": "error", - "import/named": "error", - "import/order": [ - "error", - { - "newlines-between": "always", - "groups": ["builtin", "external", "internal", ["parent", "sibling", "index"]], - "alphabetize": { - "order": "asc" - } - } - ] - }, - "settings": { - "import/resolver": { - "node": { - "extensions": [".js", ".ts", ".tsx"] - } - } - }, - "parserOptions": { - "project": ["./tsconfig.json"] - } -} diff --git a/apps/uikit-playground/.eslintrc.json b/apps/uikit-playground/.eslintrc.json deleted file mode 100644 index bf9b095684aee..0000000000000 --- a/apps/uikit-playground/.eslintrc.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "env": { "browser": true, "es2020": true }, - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:react/recommended", - "plugin:react/jsx-runtime", - "plugin:react-hooks/recommended" - ], - "parser": "@typescript-eslint/parser", - "parserOptions": { "ecmaVersion": "latest", "sourceType": "module" }, - "plugins": ["react-refresh"], - "rules": { - "react-refresh/only-export-components": "warn" - }, - "ignorePatterns": [ - "dist", - "build", - "storybook-static", - "!.jest", - "!.storybook", - ".storybook/jest-results.json", - ".DS_Store", - ".env.local", - ".env.development.local", - ".env.test.local", - ".env.production.local", - "npm-debug.log*", - "yarn-debug.log*", - "yarn-error.log*" - ] -} diff --git a/apps/uikit-playground/package.json b/apps/uikit-playground/package.json index e3c696c69b3b2..88ee5ad0811e7 100644 --- a/apps/uikit-playground/package.json +++ b/apps/uikit-playground/package.json @@ -7,7 +7,7 @@ ".:build-preview-move": "mkdir -p ../../.preview/ && cp -r ./dist ../../.preview/uikit-playground", "build-preview": "tsc && vite build", "dev": "vite", - "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "lint": "eslint .", "preview": "vite preview", "typecheck": "tsc --noEmit" }, @@ -48,13 +48,8 @@ "@types/react": "~18.3.27", "@types/react-beautiful-dnd": "^13.1.8", "@types/react-dom": "~18.3.7", - "@typescript-eslint/eslint-plugin": "~5.60.1", - "@typescript-eslint/parser": "~5.60.1", "@vitejs/plugin-react": "~4.5.2", - "eslint": "~8.45.0", - "eslint-plugin-react": "~7.37.5", - "eslint-plugin-react-hooks": "^5.0.0", - "eslint-plugin-react-refresh": "^0.4.26", + "eslint": "~9.39.3", "typescript": "~5.9.3", "vite": "^6.2.4" }, diff --git a/ee/apps/account-service/.eslintrc.json b/ee/apps/account-service/.eslintrc.json deleted file mode 100644 index 7d1aa549ad6e3..0000000000000 --- a/ee/apps/account-service/.eslintrc.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "overrides": [ - { - "files": ["**/*.spec.js", "**/*.spec.jsx"], - "env": { - "jest": true - } - } - ], - "ignorePatterns": ["dist"], - "plugins": ["jest"], - "env": { - "jest/globals": true - } -} diff --git a/ee/apps/account-service/package.json b/ee/apps/account-service/package.json index 018e7af331aa7..d28b5ef257570 100644 --- a/ee/apps/account-service/package.json +++ b/ee/apps/account-service/package.json @@ -13,7 +13,7 @@ ], "scripts": { "build": "tsc -p tsconfig.json", - "lint": "eslint src", + "lint": "eslint .", "ms": "TRANSPORTER=${TRANSPORTER:-TCP} MONGO_URL=${MONGO_URL:-mongodb://localhost:3001/meteor} ts-node --files src/service.ts", "test": "echo \"Error: no test specified\" && exit 1", "typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json" @@ -43,12 +43,11 @@ "prometheus-gc-stats": "^1.1.0" }, "devDependencies": { - "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/tsconfig": "workspace:*", "@types/bcrypt": "^5.0.2", "@types/polka": "^0.5.8", "@types/prometheus-gc-stats": "^0.6.4", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "ts-node": "^10.9.2", "typescript": "~5.9.3" }, diff --git a/ee/apps/authorization-service/.eslintrc.json b/ee/apps/authorization-service/.eslintrc.json deleted file mode 100644 index 7d1aa549ad6e3..0000000000000 --- a/ee/apps/authorization-service/.eslintrc.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "overrides": [ - { - "files": ["**/*.spec.js", "**/*.spec.jsx"], - "env": { - "jest": true - } - } - ], - "ignorePatterns": ["dist"], - "plugins": ["jest"], - "env": { - "jest/globals": true - } -} diff --git a/ee/apps/authorization-service/package.json b/ee/apps/authorization-service/package.json index 23eb464440346..cfc089aa73702 100644 --- a/ee/apps/authorization-service/package.json +++ b/ee/apps/authorization-service/package.json @@ -13,7 +13,7 @@ ], "scripts": { "build": "tsc -p tsconfig.json", - "lint": "eslint src", + "lint": "eslint .", "ms": "TRANSPORTER=${TRANSPORTER:-TCP} MONGO_URL=${MONGO_URL:-mongodb://localhost:3001/meteor} ts-node --files src/service.ts", "test": "echo \"Error: no test specified\" && exit 1", "typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json" @@ -42,11 +42,10 @@ "prometheus-gc-stats": "^1.1.0" }, "devDependencies": { - "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/tsconfig": "workspace:*", "@types/polka": "^0.5.8", "@types/prometheus-gc-stats": "^0.6.4", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "ts-node": "^10.9.2", "typescript": "~5.9.3" }, diff --git a/ee/apps/ddp-streamer/.eslintrc.json b/ee/apps/ddp-streamer/.eslintrc.json deleted file mode 100644 index 7d1aa549ad6e3..0000000000000 --- a/ee/apps/ddp-streamer/.eslintrc.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "overrides": [ - { - "files": ["**/*.spec.js", "**/*.spec.jsx"], - "env": { - "jest": true - } - } - ], - "ignorePatterns": ["dist"], - "plugins": ["jest"], - "env": { - "jest/globals": true - } -} diff --git a/ee/apps/ddp-streamer/package.json b/ee/apps/ddp-streamer/package.json index 12b0fa078a599..9ba9e9047876e 100644 --- a/ee/apps/ddp-streamer/package.json +++ b/ee/apps/ddp-streamer/package.json @@ -13,7 +13,7 @@ ], "scripts": { "build": "tsc -p tsconfig.json", - "lint": "eslint src", + "lint": "eslint .", "ms": "TRANSPORTER=${TRANSPORTER:-TCP} MONGO_URL=${MONGO_URL:-mongodb://localhost:3001/meteor} ts-node --files src/service.ts", "test": "echo \"Error: no test specified\" && exit 1", "typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json" @@ -49,7 +49,6 @@ "devDependencies": { "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/ddp-client": "workspace:~", - "@rocket.chat/eslint-config": "workspace:^", "@types/ejson": "^2.2.2", "@types/node": "~22.16.5", "@types/polka": "^0.5.8", @@ -57,7 +56,7 @@ "@types/underscore": "^1.13.0", "@types/uuid": "^10.0.0", "@types/ws": "^8.5.13", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "pino-pretty": "^7.6.1", "ts-node": "^10.9.2", "typescript": "~5.9.3" diff --git a/ee/apps/omnichannel-transcript/.eslintrc.json b/ee/apps/omnichannel-transcript/.eslintrc.json deleted file mode 100644 index 9ec331f09b5e3..0000000000000 --- a/ee/apps/omnichannel-transcript/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "ignorePatterns": ["dist"] -} diff --git a/ee/apps/omnichannel-transcript/package.json b/ee/apps/omnichannel-transcript/package.json index 4a11f7770e0a4..bd8511da43f01 100644 --- a/ee/apps/omnichannel-transcript/package.json +++ b/ee/apps/omnichannel-transcript/package.json @@ -13,7 +13,7 @@ ], "scripts": { "build": "tsc -p tsconfig.json", - "lint": "eslint src", + "lint": "eslint .", "ms": "TRANSPORTER=${TRANSPORTER:-TCP} MONGO_URL=${MONGO_URL:-mongodb://localhost:3001/meteor} ts-node --files src/service.ts", "test": "echo \"Error: no test specified\" && exit 1", "typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json" @@ -49,13 +49,12 @@ "prometheus-gc-stats": "^1.1.0" }, "devDependencies": { - "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/tsconfig": "workspace:*", "@types/i18next-sprintf-postprocessor": "^0.2.3", "@types/polka": "^0.5.8", "@types/prometheus-gc-stats": "^0.6.4", "@types/react": "~18.3.27", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "react": "~18.3.1", "ts-node": "^10.9.2", "typescript": "~5.9.3" diff --git a/ee/apps/presence-service/.eslintrc.json b/ee/apps/presence-service/.eslintrc.json deleted file mode 100644 index 7d1aa549ad6e3..0000000000000 --- a/ee/apps/presence-service/.eslintrc.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "overrides": [ - { - "files": ["**/*.spec.js", "**/*.spec.jsx"], - "env": { - "jest": true - } - } - ], - "ignorePatterns": ["dist"], - "plugins": ["jest"], - "env": { - "jest/globals": true - } -} diff --git a/ee/apps/presence-service/package.json b/ee/apps/presence-service/package.json index d3df6feb340b1..c3646a81e2af4 100644 --- a/ee/apps/presence-service/package.json +++ b/ee/apps/presence-service/package.json @@ -13,7 +13,7 @@ ], "scripts": { "build": "tsc -p tsconfig.json", - "lint": "eslint src", + "lint": "eslint .", "ms": "TRANSPORTER=${TRANSPORTER:-TCP} MONGO_URL=${MONGO_URL:-mongodb://localhost:3001/meteor} ts-node --files src/service.ts", "test": "echo \"Error: no test specified\" && exit 1", "typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json" @@ -41,11 +41,10 @@ "prometheus-gc-stats": "^1.1.0" }, "devDependencies": { - "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/tsconfig": "workspace:*", "@types/polka": "^0.5.8", "@types/prometheus-gc-stats": "^0.6.4", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "ts-node": "^10.9.2", "typescript": "~5.9.3" }, diff --git a/ee/apps/queue-worker/.eslintrc.json b/ee/apps/queue-worker/.eslintrc.json deleted file mode 100644 index 9ec331f09b5e3..0000000000000 --- a/ee/apps/queue-worker/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "ignorePatterns": ["dist"] -} diff --git a/ee/apps/queue-worker/package.json b/ee/apps/queue-worker/package.json index 2741289c23d97..38d09153d80a5 100644 --- a/ee/apps/queue-worker/package.json +++ b/ee/apps/queue-worker/package.json @@ -13,7 +13,7 @@ ], "scripts": { "build": "tsc -p tsconfig.json", - "lint": "eslint src", + "lint": "eslint .", "ms": "TRANSPORTER=${TRANSPORTER:-TCP} MONGO_URL=${MONGO_URL:-mongodb://localhost:3001/meteor} ts-node --files src/service.ts", "test": "echo \"Error: no test specified\" && exit 1", "typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json" @@ -43,11 +43,10 @@ "prometheus-gc-stats": "^1.1.0" }, "devDependencies": { - "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/tsconfig": "workspace:*", "@types/polka": "^0.5.8", "@types/prometheus-gc-stats": "^0.6.4", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "ts-node": "^10.9.2", "typescript": "~5.9.3" }, diff --git a/ee/packages/abac/.eslintrc.json b/ee/packages/abac/.eslintrc.json deleted file mode 100644 index 6744bc0544383..0000000000000 --- a/ee/packages/abac/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "ignorePatterns": ["**/dist"] -} diff --git a/ee/packages/abac/package.json b/ee/packages/abac/package.json index a2dbbe932ef80..f65aa0f0fd4f7 100644 --- a/ee/packages/abac/package.json +++ b/ee/packages/abac/package.json @@ -11,8 +11,8 @@ "scripts": { "build": "tsc -p tsconfig.build.json", "dev": "tsc --watch --preserveWatchOutput", - "lint": "eslint --ext .js,.jsx,.ts,.tsx src", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx src --fix", + "lint": "eslint .", + "lint:fix": "eslint --fix .", "test": "jest", "testunit": "jest", "typecheck": "tsc --noEmit --skipLibCheck" @@ -36,11 +36,10 @@ "p-limit": "3.1.0" }, "devDependencies": { - "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/tsconfig": "workspace:*", "@types/jest": "~30.0.0", "@types/node": "~22.16.5", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "jest": "~30.2.0", "mongodb-memory-server": "^10.1.4", "typescript": "~5.9.3" diff --git a/ee/packages/federation-matrix/.eslintrc.json b/ee/packages/federation-matrix/.eslintrc.json deleted file mode 100644 index 9ec331f09b5e3..0000000000000 --- a/ee/packages/federation-matrix/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "ignorePatterns": ["dist"] -} diff --git a/ee/packages/federation-matrix/package.json b/ee/packages/federation-matrix/package.json index 67c076ccae906..99ab6c1e4786c 100644 --- a/ee/packages/federation-matrix/package.json +++ b/ee/packages/federation-matrix/package.json @@ -39,11 +39,10 @@ }, "devDependencies": { "@rocket.chat/ddp-client": "workspace:^", - "@rocket.chat/eslint-config": "workspace:^", "@types/emojione": "^2.2.9", "@types/node": "~22.16.5", "@types/sanitize-html": "~2.16.0", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "jest": "~30.2.0", "jest-qase-reporter": "^2.1.4", "matrix-js-sdk": "^38.4.0", diff --git a/ee/packages/license/.eslintrc.json b/ee/packages/license/.eslintrc.json deleted file mode 100644 index 9ec331f09b5e3..0000000000000 --- a/ee/packages/license/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "ignorePatterns": ["dist"] -} diff --git a/ee/packages/license/package.json b/ee/packages/license/package.json index 346381b76a27e..5e5b68b861aa2 100644 --- a/ee/packages/license/package.json +++ b/ee/packages/license/package.json @@ -10,8 +10,8 @@ "scripts": { "build": "tsc", "dev": "tsc --watch --preserveWatchOutput", - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "lint": "eslint .", + "lint:fix": "eslint --fix .", "test": "jest", "testunit": "jest", "typecheck": "tsc --noEmit" @@ -28,7 +28,7 @@ "@types/bcrypt": "^5.0.2", "@types/jest": "~30.0.0", "@types/ws": "^8.5.13", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "jest": "~30.2.0", "jest-websocket-mock": "~2.5.0", "typescript": "~5.9.3" diff --git a/ee/packages/media-calls/.eslintrc.json b/ee/packages/media-calls/.eslintrc.json deleted file mode 100644 index 9ec331f09b5e3..0000000000000 --- a/ee/packages/media-calls/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "ignorePatterns": ["dist"] -} diff --git a/ee/packages/media-calls/package.json b/ee/packages/media-calls/package.json index 4b20ff01664a7..3b9b1081da686 100644 --- a/ee/packages/media-calls/package.json +++ b/ee/packages/media-calls/package.json @@ -11,8 +11,8 @@ "build": "rm -rf dist && tsc -p tsconfig.json", "build-preview": "mkdir -p ../../.preview && cp -r ./dist ../../.preview/media-calls", "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput", - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "lint": "eslint .", + "lint:fix": "eslint --fix .", "typecheck": "tsc --noEmit" }, "dependencies": { @@ -26,7 +26,7 @@ "devDependencies": { "@rocket.chat/tsconfig": "workspace:*", "@types/jest": "~30.0.0", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "typescript": "~5.9.3" } } diff --git a/ee/packages/network-broker/.eslintrc.json b/ee/packages/network-broker/.eslintrc.json deleted file mode 100644 index 9ec331f09b5e3..0000000000000 --- a/ee/packages/network-broker/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "ignorePatterns": ["dist"] -} diff --git a/ee/packages/network-broker/package.json b/ee/packages/network-broker/package.json index ee15503db1566..62102a08de406 100644 --- a/ee/packages/network-broker/package.json +++ b/ee/packages/network-broker/package.json @@ -9,8 +9,8 @@ ], "scripts": { "build": "tsc", - "lint": "eslint src", - "lint:fix": "eslint src --fix", + "lint": "eslint .", + "lint:fix": "eslint --fix .", "test": "jest", "testunit": "jest", "typecheck": "tsc --noEmit --skipLibCheck" @@ -22,14 +22,13 @@ "pino": "^8.21.0" }, "devDependencies": { - "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/tsconfig": "workspace:*", "@types/chai": "~4.3.20", "@types/ejson": "^2.2.2", "@types/node": "~22.16.5", "@types/sinon": "^10.0.20", "chai": "^4.5.0", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "jest": "~30.2.0", "sinon": "^19.0.5", "typescript": "~5.9.3" diff --git a/ee/packages/omni-core-ee/.eslintrc.json b/ee/packages/omni-core-ee/.eslintrc.json deleted file mode 100644 index 9ec331f09b5e3..0000000000000 --- a/ee/packages/omni-core-ee/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "ignorePatterns": ["dist"] -} diff --git a/ee/packages/omni-core-ee/package.json b/ee/packages/omni-core-ee/package.json index 67c04aa497d48..1698e1a375d85 100644 --- a/ee/packages/omni-core-ee/package.json +++ b/ee/packages/omni-core-ee/package.json @@ -10,8 +10,8 @@ "scripts": { "build": "rm -rf dist && tsc -p tsconfig.json", "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput", - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "lint": "eslint .", + "lint:fix": "eslint --fix .", "test": "jest", "testunit": "jest" }, @@ -24,11 +24,10 @@ "mongodb": "6.16.0" }, "devDependencies": { - "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/tsconfig": "workspace:*", "@types/jest": "~30.0.0", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "jest": "~30.2.0", "typescript": "~5.9.3" }, diff --git a/ee/packages/omnichannel-services/.eslintrc.json b/ee/packages/omnichannel-services/.eslintrc.json deleted file mode 100644 index 9ec331f09b5e3..0000000000000 --- a/ee/packages/omnichannel-services/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "ignorePatterns": ["dist"] -} diff --git a/ee/packages/omnichannel-services/package.json b/ee/packages/omnichannel-services/package.json index 5bdefc781e760..7ac6af64d0cc5 100644 --- a/ee/packages/omnichannel-services/package.json +++ b/ee/packages/omnichannel-services/package.json @@ -10,8 +10,8 @@ "scripts": { "build": "rm -rf dist && tsc -p tsconfig.build.json", "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput", - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "lint": "eslint .", + "lint:fix": "eslint --fix .", "test": "jest", "testunit": "jest", "typecheck": "tsc --noEmit --skipLibCheck" @@ -42,11 +42,10 @@ "pino": "^8.21.0" }, "devDependencies": { - "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/tsconfig": "workspace:*", "@types/jest": "~30.0.0", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "jest": "~30.2.0", "typescript": "~5.9.3" }, diff --git a/ee/packages/pdf-worker/.eslintrc.json b/ee/packages/pdf-worker/.eslintrc.json deleted file mode 100644 index 7c5ecdf982fe6..0000000000000 --- a/ee/packages/pdf-worker/.eslintrc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config", "@rocket.chat/eslint-config/react", "plugin:storybook/recommended"], - "parser": "@typescript-eslint/parser", - "ignorePatterns": ["dist", "!.storybook"] -} diff --git a/ee/packages/pdf-worker/package.json b/ee/packages/pdf-worker/package.json index 1d81713b7364f..e040323b2973e 100644 --- a/ee/packages/pdf-worker/package.json +++ b/ee/packages/pdf-worker/package.json @@ -10,8 +10,8 @@ "scripts": { "build": "tsc -p tsconfig.build.json && cp -r src/public dist/public", "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput", - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "lint": "eslint .", + "lint:fix": "eslint --fix .", "storybook": "storybook dev -p 6006", "test": "jest", "testunit": "jest", @@ -42,8 +42,7 @@ "@types/react": "~18.3.27", "@types/react-dom": "~18.3.7", "buffer": "~6.0.3", - "eslint": "~8.45.0", - "eslint-plugin-storybook": "~0.11.6", + "eslint": "~9.39.3", "i18next": "~23.4.9", "jest": "~30.2.0", "react-dom": "~18.3.1", diff --git a/ee/packages/presence/.eslintrc.json b/ee/packages/presence/.eslintrc.json deleted file mode 100644 index 9ec331f09b5e3..0000000000000 --- a/ee/packages/presence/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "ignorePatterns": ["dist"] -} diff --git a/ee/packages/presence/package.json b/ee/packages/presence/package.json index b2479372d4382..ceb33d6225af7 100644 --- a/ee/packages/presence/package.json +++ b/ee/packages/presence/package.json @@ -23,10 +23,9 @@ }, "devDependencies": { "@rocket.chat/apps-engine": "workspace:^", - "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", "@types/node": "~22.16.5", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "jest": "~30.2.0", "typescript": "~5.9.3" }, diff --git a/ee/packages/ui-theming/.eslintrc.json b/ee/packages/ui-theming/.eslintrc.json deleted file mode 100644 index 9a131836901c4..0000000000000 --- a/ee/packages/ui-theming/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config", "@rocket.chat/eslint-config/react"], - "ignorePatterns": ["dist"] -} diff --git a/ee/packages/ui-theming/package.json b/ee/packages/ui-theming/package.json index c821cd71bcdbd..36eccdba5c44f 100644 --- a/ee/packages/ui-theming/package.json +++ b/ee/packages/ui-theming/package.json @@ -10,8 +10,8 @@ "scripts": { "build": "rm -rf dist && tsc -p tsconfig-build.json", "dev": "tsc -p tsconfig-build.json --watch --preserveWatchOutput", - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix" + "lint": "eslint .", + "lint:fix": "eslint --fix ." }, "devDependencies": { "@rocket.chat/css-in-js": "~0.31.25", @@ -23,11 +23,7 @@ "@rocket.chat/ui-contexts": "workspace:~", "@types/react": "~18.3.27", "@types/react-dom": "~18.3.7", - "eslint": "~8.45.0", - "eslint-plugin-anti-trojan-source": "~1.1.2", - "eslint-plugin-react": "~7.37.5", - "eslint-plugin-react-hooks": "~5.0.0", - "eslint-plugin-testing-library": "^6.4.0", + "eslint": "~9.39.3", "react": "~18.3.1", "react-docgen-typescript-plugin": "~1.0.8", "react-dom": "~18.3.1", diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000000000..c100011518493 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,613 @@ +import { fileURLToPath } from 'node:url'; + +import rocketChatConfig from '@rocket.chat/eslint-config'; +import reactRefreshPlugin from 'eslint-plugin-react-refresh'; +import youDontNeedLodashUnderscorePlugin from 'eslint-plugin-you-dont-need-lodash-underscore'; +import globals from 'globals'; + +function getAbsolutePath(path) { + return fileURLToPath(new URL(path, import.meta.url)); +} + +/** @type {import('eslint').Linter.FlatConfig[]} */ +export default [ + ...rocketChatConfig, + { + ignores: [ + 'apps/meteor/app/emoji-emojione/generateEmojiIndex.js', + 'apps/meteor/**/public', + 'apps/meteor/**/private/moment-locales', + 'apps/meteor/**/imports', + 'apps/meteor/**/packages', + 'apps/meteor/.meteor/**', + ], + }, + { + files: ['apps/meteor/**/*'], + languageOptions: { + globals: { + __meteor_runtime_config__: 'readonly', + Assets: 'readonly', + chrome: 'readonly', + jscolor: 'readonly', + ...globals.browser, + }, + }, + plugins: { + 'you-dont-need-lodash-underscore': youDontNeedLodashUnderscorePlugin, + }, + settings: { + 'import/ignore': ['meteor/.+'], + }, + rules: { + 'import/named': 'error', + 'import/no-unresolved': [ + 'error', + { + commonjs: true, + caseSensitive: true, + ignore: ['meteor/.+'], + }, + ], + 'react-hooks/exhaustive-deps': 'warn', + 'you-dont-need-lodash-underscore/concat': 'error', + 'you-dont-need-lodash-underscore/drop': 'error', + 'you-dont-need-lodash-underscore/drop-right': 'error', + 'you-dont-need-lodash-underscore/index-of': 'error', + 'you-dont-need-lodash-underscore/join': 'error', + 'you-dont-need-lodash-underscore/last': 'error', + 'you-dont-need-lodash-underscore/last-index-of': 'error', + 'you-dont-need-lodash-underscore/reverse': 'error', + 'you-dont-need-lodash-underscore/fill': 'error', + 'you-dont-need-lodash-underscore/detect': 'error', + 'you-dont-need-lodash-underscore/first': 'error', + 'you-dont-need-lodash-underscore/is-array': 'error', + 'you-dont-need-lodash-underscore/slice': 'error', + 'you-dont-need-lodash-underscore/bind': 'error', + 'you-dont-need-lodash-underscore/is-finite': 'error', + 'you-dont-need-lodash-underscore/is-integer': 'error', + 'you-dont-need-lodash-underscore/is-nan': 'error', + 'you-dont-need-lodash-underscore/is-nil': 'error', + 'you-dont-need-lodash-underscore/is-null': 'error', + 'you-dont-need-lodash-underscore/is-undefined': 'error', + 'you-dont-need-lodash-underscore/keys': 'error', + 'you-dont-need-lodash-underscore/extend-own': 'error', + 'you-dont-need-lodash-underscore/assign': 'error', + 'you-dont-need-lodash-underscore/values': 'error', + 'you-dont-need-lodash-underscore/entries': 'error', + 'you-dont-need-lodash-underscore/to-pairs': 'error', + 'you-dont-need-lodash-underscore/pairs': 'error', + 'you-dont-need-lodash-underscore/split': 'error', + 'you-dont-need-lodash-underscore/starts-with': 'error', + 'you-dont-need-lodash-underscore/ends-with': 'error', + 'you-dont-need-lodash-underscore/to-lower': 'error', + 'you-dont-need-lodash-underscore/to-upper': 'error', + 'you-dont-need-lodash-underscore/trim': 'error', + 'you-dont-need-lodash-underscore/pad-start': 'error', + 'you-dont-need-lodash-underscore/pad-end': 'error', + 'you-dont-need-lodash-underscore/repeat': 'error', + 'you-dont-need-lodash-underscore/uniq': 'error', + 'you-dont-need-lodash-underscore/replace': 'error', + 'you-dont-need-lodash-underscore/omit': 'error', + 'you-dont-need-lodash-underscore/flatten': 'error', + 'you-dont-need-lodash-underscore/throttle': 'error', + 'you-dont-need-lodash-underscore/is-string': 'error', + 'you-dont-need-lodash-underscore/cast-array': 'error', + 'you-dont-need-lodash-underscore/clone-deep': 'error', + 'you-dont-need-lodash-underscore/is-function': 'error', + 'you-dont-need-lodash-underscore/capitalize': 'error', + 'you-dont-need-lodash-underscore/is-date': 'error', + 'you-dont-need-lodash-underscore/defaults': 'error', + 'you-dont-need-lodash-underscore/head': 'error', + 'you-dont-need-lodash-underscore/find': 'warn', + 'you-dont-need-lodash-underscore/find-index': 'warn', + 'you-dont-need-lodash-underscore/each': 'warn', + 'you-dont-need-lodash-underscore/for-each': 'warn', + 'you-dont-need-lodash-underscore/every': 'warn', + 'you-dont-need-lodash-underscore/all': 'warn', + 'you-dont-need-lodash-underscore/filter': 'warn', + 'you-dont-need-lodash-underscore/select': 'warn', + 'you-dont-need-lodash-underscore/map': 'warn', + 'you-dont-need-lodash-underscore/collect': 'warn', + 'you-dont-need-lodash-underscore/reduce': 'warn', + 'you-dont-need-lodash-underscore/inject': 'warn', + 'you-dont-need-lodash-underscore/foldl': 'warn', + 'you-dont-need-lodash-underscore/reduce-right': 'warn', + 'you-dont-need-lodash-underscore/foldr': 'warn', + 'you-dont-need-lodash-underscore/size': 'warn', + 'you-dont-need-lodash-underscore/some': 'warn', + 'you-dont-need-lodash-underscore/any': 'warn', + 'you-dont-need-lodash-underscore/includes': 'warn', + 'you-dont-need-lodash-underscore/contains': 'warn', + 'you-dont-need-lodash-underscore/take-right': 'warn', + 'you-dont-need-lodash-underscore/get': 'warn', + 'you-dont-need-lodash-underscore/union-by': 'warn', + 'you-dont-need-lodash-underscore/is-array-buffer': 'warn', + 'new-cap': [ + 'error', + { + capIsNewExceptions: [ + 'Match.Optional', + 'Match.Maybe', + 'Match.OneOf', + 'Match.Where', + 'Match.ObjectIncluding', + 'Push.Configure', + 'SHA256', + ], + }, + ], + 'prefer-arrow-callback': [ + 'error', + { + allowNamedFunctions: true, + }, + ], + }, + }, + { + files: ['apps/meteor/**/*.@(ts|tsx)'], + ignores: ['apps/meteor/.scripts/*.ts', 'apps/meteor/**/*.d.ts'], + rules: { + '@typescript-eslint/naming-convention': [ + 'error', + { + selector: ['function', 'parameter', 'variable'], + modifiers: ['destructured'], + format: null, + }, + { + selector: ['variable'], + format: ['camelCase', 'UPPER_CASE', 'PascalCase'], + leadingUnderscore: 'allowSingleOrDouble', + }, + { + selector: ['function'], + format: ['camelCase', 'PascalCase'], + leadingUnderscore: 'allowSingleOrDouble', + }, + { + selector: ['parameter'], + format: ['PascalCase'], + filter: { + regex: 'Component$', + match: true, + }, + }, + { + selector: ['parameter'], + format: ['camelCase'], + leadingUnderscore: 'allow', + }, + { + selector: ['parameter'], + format: ['camelCase'], + modifiers: ['unused'], + leadingUnderscore: 'require', + }, + { + selector: 'parameter', + format: null, + filter: { + regex: '^Story$', + match: true, + }, + }, + { + selector: ['interface'], + format: ['PascalCase'], + custom: { + regex: '^I[A-Z]', + match: true, + }, + }, + ], + 'no-unreachable-loop': 'error', + }, + }, + { + files: ['apps/meteor/**/*.tests.js'], + languageOptions: { + globals: { + ...globals.mocha, + }, + }, + }, + { + files: ['apps/meteor/tests/@(end-to-end|unit)/**/*.spec.ts'], + rules: { + 'jest/expect-expect': 'off', + 'jest/no-conditional-expect': 'off', + 'jest/no-done-callback': 'off', + 'jest/no-export': 'off', + 'jest/no-identical-title': 'off', + 'jest/no-standalone-expect': 'off', + 'jest/no-test-prefixes': 'off', + 'jest/valid-describe-callback': 'off', + 'jest/valid-expect-in-promise': 'off', + 'jest/valid-expect': 'off', + 'jest/valid-title': 'off', + }, + }, + { + files: ['apps/meteor/tests/@(end-to-end|unit)/**/*.ts'], + rules: { + '@typescript-eslint/no-unused-expressions': 'off', + }, + }, + { + files: [ + 'apps/meteor/client/**/*.@(ts|tsx)', + 'apps/meteor/server/**/*.ts', + 'apps/meteor/ee/app/**/*.ts', + 'apps/meteor/ee/client/**/*.@(ts|tsx)', + 'apps/meteor/ee/server/**/*.ts', + 'packages/ui-contexts/**/*.@(ts|tsx)', + 'packages/i18n/src/scripts/common.mts', + ], + rules: { + '@typescript-eslint/no-floating-promises': 'off', + }, + }, + { + files: ['apps/meteor/tests/e2e/**/*'], + languageOptions: { + parserOptions: { + projectService: true, + }, + }, + rules: { + '@typescript-eslint/no-floating-promises': 'error', + 'import/named': 'error', + 'import/order': [ + 'error', + { + 'newlines-between': 'always', + 'groups': ['builtin', 'external', 'internal', ['parent', 'sibling', 'index']], + 'alphabetize': { + order: 'asc', + }, + }, + ], + }, + }, + { + files: ['apps/meteor/packages/**/*'], + languageOptions: { + globals: { + Package: 'readonly', + Npm: 'readonly', + }, + }, + }, + { + files: ['apps/uikit-playground/**/*'], + languageOptions: { + globals: { + ...globals.browser, + ...globals.es2020, + }, + }, + plugins: { + 'react-refresh': reactRefreshPlugin, + }, + rules: { + '@typescript-eslint/consistent-type-imports': 'off', + 'import/order': 'off', + 'prettier/prettier': 'off', + 'react-refresh/only-export-components': 'warn', + 'spaced-comment': 'off', + 'array-callback-return': 'off', + '@typescript-eslint/no-floating-promises': 'off', + 'import/export': 'off', + 'jsx-quotes': 'off', + 'import/newline-after-import': 'off', + 'react/jsx-curly-brace-presence': 'warn', + 'prefer-destructuring': 'off', + 'object-shorthand': 'off', + 'import/no-duplicates': 'off', + '@typescript-eslint/naming-convention': 'off', + 'react/jsx-key': 'warn', + 'jsx-a11y/click-events-have-key-events': 'off', + 'jsx-a11y/no-static-element-interactions': 'off', + 'no-nested-ternary': 'off', + 'new-cap': 'off', + }, + }, + { + ignores: [ + 'apps/uikit-playground/build', + 'apps/uikit-playground/.storybook/jest-results.json', + 'apps/uikit-playground/.DS_Store', + 'apps/uikit-playground/.env*.local', + 'apps/uikit-playground/@(npm-debug|yarn-debug|yarn-error).log*', + ], + }, + { + files: ['packages/apps-engine/**/*.ts'], + languageOptions: { + parserOptions: { + project: getAbsolutePath('./packages/apps-engine/tsconfig-lint.json'), + }, + }, + rules: { + '@typescript-eslint/no-empty-object-type': 'off', + '@typescript-eslint/no-unsafe-function-type': 'error', + '@typescript-eslint/no-wrapper-object-types': 'error', + '@typescript-eslint/naming-convention': [ + 'error', + { + selector: ['function', 'parameter', 'variable'], + modifiers: ['destructured'], + format: null, + }, + { + selector: ['variable'], + format: ['camelCase', 'UPPER_CASE', 'PascalCase'], + leadingUnderscore: 'allowSingleOrDouble', + }, + { + selector: ['function'], + format: ['camelCase', 'PascalCase'], + leadingUnderscore: 'allowSingleOrDouble', + }, + { + selector: ['parameter'], + format: ['camelCase'], + leadingUnderscore: 'allow', + }, + { + selector: ['parameter'], + format: ['camelCase'], + modifiers: ['unused'], + leadingUnderscore: 'allow', + }, + { + selector: ['interface'], + format: ['PascalCase'], + custom: { + regex: '^I[A-Z]', + match: true, + }, + }, + ], + '@typescript-eslint/no-empty-function': 'off', + '@typescript-eslint/no-unused-vars': ['error', { args: 'none' }], + 'new-cap': 'off', + 'no-await-in-loop': 'off', + }, + }, + { + ignores: ['packages/apps-engine/**/@(client|definition|docs|server|lib|deno-runtime|.deno|.deno-cache)/**'], + }, + { + files: ['packages/core-typings/**/*'], + rules: { + '@typescript-eslint/no-empty-interface': 'off', + }, + }, + { + files: ['packages/ddp-client/**/*'], + rules: { + '@typescript-eslint/naming-convention': [ + 'error', + { selector: 'variableLike', format: ['camelCase'], leadingUnderscore: 'allow' }, + { + selector: ['variable'], + format: ['camelCase', 'UPPER_CASE', 'PascalCase'], + leadingUnderscore: 'allowSingleOrDouble', + }, + { + selector: ['function'], + format: ['camelCase', 'PascalCase'], + leadingUnderscore: 'allowSingleOrDouble', + }, + { + selector: 'parameter', + format: ['camelCase'], + modifiers: ['unused'], + leadingUnderscore: 'require', + }, + ], + }, + }, + { + ignores: ['packages/jest-presets/@(client|server)/**'], + }, + { + files: ['packages/livechat/**/*'], + languageOptions: { + globals: { + ...globals.browser, + }, + }, + settings: { + react: { + pragma: 'h', + pragmaFrag: 'Fragment', + version: 'detect', + }, + }, + rules: { + 'import/order': [ + 'error', + { + 'newlines-between': 'always', + 'groups': ['builtin', 'external', 'internal', ['parent', 'sibling', 'index']], + 'alphabetize': { + order: 'asc', + }, + }, + ], + 'jsx-a11y/alt-text': 'off', + 'jsx-a11y/click-events-have-key-events': 'off', + 'jsx-a11y/media-has-caption': 'off', + 'jsx-a11y/no-static-element-interactions': 'off', + 'jsx-quotes': ['error', 'prefer-single'], + 'react/jsx-curly-brace-presence': 'off', + 'react/display-name': [ + 'warn', + { + ignoreTranspilerName: false, + }, + ], + 'react/jsx-fragments': ['error', 'syntax'], + 'react/jsx-key': 'off', + 'react/jsx-no-bind': [ + 'warn', + { + ignoreRefs: true, + allowFunctions: true, + allowArrowFunctions: true, + }, + ], + 'react/jsx-no-comment-textnodes': 'error', + 'react/jsx-no-duplicate-props': 'error', + 'react/jsx-no-target-blank': 'error', + 'react/jsx-no-undef': 'error', + 'react/jsx-tag-spacing': [ + 'error', + { + beforeSelfClosing: 'always', + }, + ], + 'react/jsx-uses-react': 'error', + 'react/jsx-uses-vars': 'error', + 'react/no-children-prop': 'error', + 'react/no-danger': 'warn', + 'react/no-deprecated': 'error', + 'react/no-did-mount-set-state': 'error', + 'react/no-did-update-set-state': 'error', + 'react/no-direct-mutation-state': 'warn', + 'react/no-find-dom-node': 'error', + 'react/no-is-mounted': 'error', + 'react/no-multi-comp': 'off', + 'react/no-string-refs': 'error', + 'react/no-unknown-property': ['error', { ignore: ['class'] }], + 'react/prefer-es6-class': 'error', + 'react/prefer-stateless-function': 'warn', + 'react/require-render-return': 'error', + 'react/self-closing-comp': 'error', + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'warn', + 'no-sequences': 'off', + }, + }, + { + files: ['packages/livechat/**/*.@(ts|tsx)'], + rules: { + '@typescript-eslint/naming-convention': [ + 'error', + { selector: 'variableLike', format: ['camelCase'], leadingUnderscore: 'allow' }, + { + selector: ['variable'], + format: ['camelCase', 'UPPER_CASE', 'PascalCase'], + leadingUnderscore: 'allowSingleOrDouble', + }, + { + selector: ['function'], + format: ['camelCase', 'PascalCase'], + leadingUnderscore: 'allowSingleOrDouble', + }, + { + selector: 'parameter', + format: ['camelCase'], + modifiers: ['unused'], + leadingUnderscore: 'require', + }, + ], + }, + }, + { + ignores: ['packages/node-poplib/**', 'packages/storybook-config/*.@(d.ts|js)', 'scripts/**', '.github/**', '.houston/**'], + }, + { + files: [ + 'apps/meteor/client/**/*.@(ts|tsx)', + 'apps/meteor/app/**/*.ts', + 'apps/meteor/ee/app/**/*.ts', + 'apps/meteor/ee/client/**/*.@(ts|tsx)', + 'apps/meteor/ee/server/**/*.ts', + 'apps/meteor/server/**/*.ts', + 'packages/fuselage-ui-kit/**/*.@(ts|tsx)', + 'packages/livechat/**/*.@(ts|tsx)', + 'packages/ui-client/**/*.@(ts|tsx)', + 'packages/ui-contexts/**/*.@(ts|tsx)', + 'packages/ui-voip/**/*.@(ts|tsx)', + 'packages/web-ui-registration/**/*.@(ts|tsx)', + ], + rules: { + '@typescript-eslint/no-misused-promises': 'warn', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + varsIgnorePattern: 'Endpoints?$|Routes$|^invites$|^livechatVisitorDepartmentTransfer$', // FIXME? + argsIgnorePattern: '^_', + ignoreRestSiblings: true, + caughtErrors: 'none', + }, + ], + }, + }, + { + files: ['packages/@(sha256|models)/**/*'], + rules: { + 'new-cap': [ + 'error', + { + capIsNewExceptions: ['SHA256'], + }, + ], + }, + }, + // FIXME: React 19 useEffectEvent conflicts with fuselage-hooks + { + files: ['**/*.@(ts|tsx)'], + rules: { + 'react-hooks/exhaustive-deps': 'warn', + 'react-hooks/rules-of-hooks': 'warn', + }, + }, + // FIXME: these rules require type information and the files are not included in the main tsconfig.json + { + files: [ + '**/*.d.ts', + '**/__tests__/**', + '**/*.@(spec|test).@(ts|tsx)', + '**/tests/**', + '**/.storybook/**', + '**/jest.config.@(ts|js)', + '**/jest.config.*.@(ts|js)', + '**/webpack.config.@(ts|js)', + '**/vite.config.@(ts|js)', + '**/rollup.config.@(ts|js)', + + 'apps/meteor/.storybook/logo.svg.d.ts', + 'packages/fuselage-ui-kit/.storybook/logo.svg.d.ts', + 'packages/storybook-config/src/logo.svg.d.ts', + + '@(ee/packages|packages)/*/jest.config.ts', + + 'ee/packages/pdf-worker/.storybook/*.@(ts|tsx)', + 'packages/@(gazzodown|ui-client|ui-composer|ui-voip|web-ui-registration)/.storybook/*.@(ts|tsx)', + ], + rules: { + '@typescript-eslint/prefer-optional-chain': 'off', + '@typescript-eslint/no-misused-promises': 'off', + }, + }, + // FIXME: these storybook rules cannot be enabled until Storybook is upgraded to >=9 + { + files: ['**/*.stories.@(ts|tsx|js|jsx|mjs|cjs)'], + rules: { + 'storybook/no-renderer-packages': 'off', + }, + }, + // FIXME + { + files: ['ee/packages/federation-matrix/src/api/.well-known/server.ts'], + rules: { + 'import/order': 'warn', + }, + }, +]; diff --git a/package.json b/package.json index 44dd80ff3d1a8..9718c07cbb810 100644 --- a/package.json +++ b/package.json @@ -63,11 +63,15 @@ }, "devDependencies": { "@changesets/cli": "^2.27.11", + "@rocket.chat/eslint-config": "workspace:~", "@types/chart.js": "^2.9.41", "@types/js-yaml": "^4.0.9", "@types/node": "~22.16.5", + "eslint": "~9.39.3", + "eslint-plugin-react-refresh": "~0.5.2", + "eslint-plugin-you-dont-need-lodash-underscore": "~6.14.0", "ts-node": "^10.9.2", - "turbo": "~2.7.6", + "turbo": "~2.8.11", "typescript": "~5.9.3" }, "packageManager": "yarn@4.12.0", diff --git a/packages/account-utils/.eslintrc.json b/packages/account-utils/.eslintrc.json deleted file mode 100644 index 9ec331f09b5e3..0000000000000 --- a/packages/account-utils/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "ignorePatterns": ["dist"] -} diff --git a/packages/account-utils/package.json b/packages/account-utils/package.json index 011601ccddef8..f1b84a771d844 100644 --- a/packages/account-utils/package.json +++ b/packages/account-utils/package.json @@ -10,11 +10,11 @@ "scripts": { "build": "rm -rf dist && tsc -p tsconfig.json", "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput", - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix" + "lint": "eslint .", + "lint:fix": "eslint --fix ." }, "devDependencies": { - "eslint": "~8.45.0", + "eslint": "~9.39.3", "typescript": "~5.9.3" }, "volta": { diff --git a/packages/agenda/.eslintrc.json b/packages/agenda/.eslintrc.json deleted file mode 100644 index 9ec331f09b5e3..0000000000000 --- a/packages/agenda/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "ignorePatterns": ["dist"] -} diff --git a/packages/agenda/package.json b/packages/agenda/package.json index 07ba287eda6a4..c11e16d470eef 100644 --- a/packages/agenda/package.json +++ b/packages/agenda/package.json @@ -10,8 +10,8 @@ ], "scripts": { "build": "rm -rf dist && tsc -p tsconfig.json", - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix" + "lint": "eslint .", + "lint:fix": "eslint --fix ." }, "dependencies": { "cron": "~1.8.2", @@ -23,7 +23,7 @@ }, "devDependencies": { "@types/debug": "^4.1.12", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "typescript": "~5.9.3" }, "volta": { diff --git a/packages/api-client/.eslintrc.json b/packages/api-client/.eslintrc.json deleted file mode 100644 index 9ec331f09b5e3..0000000000000 --- a/packages/api-client/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "ignorePatterns": ["dist"] -} diff --git a/packages/api-client/package.json b/packages/api-client/package.json index 1dfae4c06108e..7ff645504cbfb 100644 --- a/packages/api-client/package.json +++ b/packages/api-client/package.json @@ -9,8 +9,8 @@ "scripts": { "build": "tsc", "dev": "tsc --watch --preserveWatchOutput", - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "lint": "eslint .", + "lint:fix": "eslint --fix .", "test": "jest", "testunit": "jest" }, @@ -27,7 +27,7 @@ "@rocket.chat/tsconfig": "workspace:*", "@types/jest": "~30.0.0", "@types/strict-uri-encode": "^2.0.2", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "jest": "~30.2.0", "jest-fetch-mock": "~3.0.3", "typescript": "~5.9.3" diff --git a/packages/apps-engine/.eslintrc.json b/packages/apps-engine/.eslintrc.json deleted file mode 100644 index 0e15f5fe9fb02..0000000000000 --- a/packages/apps-engine/.eslintrc.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "extends": "@rocket.chat/eslint-config", - "parser": "@typescript-eslint/parser", - "parserOptions": { - "project": "./tsconfig-lint.json" - }, - "rules": { - "@typescript-eslint/ban-types": [ - "error", - { - "types": { - "{}": false - } - } - ], - "@typescript-eslint/naming-convention": [ - "error", - { - "selector": ["function", "parameter", "variable"], - "modifiers": ["destructured"], - "format": null - }, - { - "selector": ["variable"], - "format": ["camelCase", "UPPER_CASE", "PascalCase"], - "leadingUnderscore": "allowSingleOrDouble" - }, - { - "selector": ["function"], - "format": ["camelCase", "PascalCase"], - "leadingUnderscore": "allowSingleOrDouble" - }, - { - "selector": ["parameter"], - "format": ["camelCase"], - "leadingUnderscore": "allow" - }, - { - "selector": ["parameter"], - "format": ["camelCase"], - "modifiers": ["unused"], - "leadingUnderscore": "allow" - }, - { - "selector": ["interface"], - "format": ["PascalCase"], - "custom": { - "regex": "^I[A-Z]", - "match": true - } - } - ], - "@typescript-eslint/no-empty-function": "off", - "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], - "new-cap": "off", - "no-await-in-loop": "off" - }, - "ignorePatterns": ["client", "definition", "docs", "server", "lib", "deno-runtime", ".deno"] -} diff --git a/packages/apps-engine/package.json b/packages/apps-engine/package.json index f2dd78c7308fd..405374c40dc30 100644 --- a/packages/apps-engine/package.json +++ b/packages/apps-engine/package.json @@ -47,7 +47,7 @@ ".:build:default": "tsc -p tsconfig.json", ".:build:deno-cache": "node scripts/deno-cache.js", ".:deno-fmt:fix": "cd deno-runtime && deno fmt", - ".:eslint:fix": "eslint . --fix", + ".:eslint:fix": "eslint --fix .", ".:lint:deno": "deno lint --ignore=deno-runtime/.deno deno-runtime/", ".:lint:eslint": "eslint .", ".:test:deno": "cd deno-runtime && deno task test", @@ -91,7 +91,6 @@ "uuid": "~11.0.5" }, "devDependencies": { - "@rocket.chat/eslint-config": "workspace:~", "@rocket.chat/ui-kit": "workspace:~", "@seald-io/nedb": "^4.1.2", "@types/adm-zip": "^0.5.7", @@ -101,11 +100,9 @@ "@types/semver": "^7.5.8", "@types/stack-trace": "0.0.33", "@types/uuid": "~10.0.0", - "@typescript-eslint/eslint-plugin": "~5.60.1", - "@typescript-eslint/parser": "~5.60.1", "alsatian": "^2.4.0", "browserify": "^16.5.2", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "npm-run-all": "^4.1.5", "nyc": "^17.1.0", "rimraf": "^6.0.1", diff --git a/packages/apps/.eslintrc.json b/packages/apps/.eslintrc.json deleted file mode 100644 index 9ec331f09b5e3..0000000000000 --- a/packages/apps/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "ignorePatterns": ["dist"] -} diff --git a/packages/apps/package.json b/packages/apps/package.json index 8be18c4cbdca1..06ea0e9fe186a 100644 --- a/packages/apps/package.json +++ b/packages/apps/package.json @@ -10,8 +10,8 @@ "scripts": { "build": "rm -rf dist && tsc -p tsconfig.json", "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput", - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix" + "lint": "eslint .", + "lint:fix": "eslint --fix ." }, "dependencies": { "@rocket.chat/apps-engine": "workspace:^", @@ -20,7 +20,7 @@ }, "devDependencies": { "@rocket.chat/tsconfig": "workspace:*", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "typescript": "~5.9.3" }, "volta": { diff --git a/packages/base64/.eslintrc.json b/packages/base64/.eslintrc.json deleted file mode 100644 index 9ec331f09b5e3..0000000000000 --- a/packages/base64/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "ignorePatterns": ["dist"] -} diff --git a/packages/base64/package.json b/packages/base64/package.json index 73f63dadafa9a..69f40bf6bda29 100644 --- a/packages/base64/package.json +++ b/packages/base64/package.json @@ -9,15 +9,13 @@ "build": "rm -rf dist && tsc -p tsconfig.build.json", "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput", "lint": "eslint .", - "lint:fix": "eslint . --fix", + "lint:fix": "eslint --fix .", "test": "jest", "testunit": "jest" }, "devDependencies": { - "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/tsconfig": "workspace:*", - "eslint": "~8.45.0", "jest": "~30.2.0", "typescript": "~5.9.3" }, diff --git a/packages/cas-validate/.eslintrc.json b/packages/cas-validate/.eslintrc.json deleted file mode 100644 index 9ec331f09b5e3..0000000000000 --- a/packages/cas-validate/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "ignorePatterns": ["dist"] -} diff --git a/packages/cas-validate/package.json b/packages/cas-validate/package.json index ee63960004b21..aeef6f4faf2ae 100644 --- a/packages/cas-validate/package.json +++ b/packages/cas-validate/package.json @@ -11,15 +11,15 @@ "scripts": { "build": "rm -rf dist && tsc -p tsconfig.json", "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput", - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "lint": "eslint .", + "lint:fix": "eslint --fix .", "testunit": "jest" }, "dependencies": { "cheerio": "1.0.0" }, "devDependencies": { - "eslint": "~8.45.0", + "eslint": "~9.39.3", "jest": "~30.2.0", "typescript": "~5.9.3" }, diff --git a/packages/core-services/.eslintrc.json b/packages/core-services/.eslintrc.json deleted file mode 100644 index 9ec331f09b5e3..0000000000000 --- a/packages/core-services/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "ignorePatterns": ["dist"] -} diff --git a/packages/core-services/package.json b/packages/core-services/package.json index 023200f3393fa..323d474c7cf95 100644 --- a/packages/core-services/package.json +++ b/packages/core-services/package.json @@ -30,11 +30,10 @@ }, "devDependencies": { "@rocket.chat/apps-engine": "workspace:^", - "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/tsconfig": "workspace:*", "@types/jest": "~30.0.0", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "jest": "~30.2.0", "mongodb": "6.16.0", "prettier": "~3.3.3", diff --git a/packages/core-typings/.eslintrc.json b/packages/core-typings/.eslintrc.json deleted file mode 100644 index 373fe99379a44..0000000000000 --- a/packages/core-typings/.eslintrc.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "overrides": [ - { - "files": ["**/*.spec.js", "**/*.spec.jsx"], - "env": { - "jest": true - } - } - ], - "ignorePatterns": ["dist"], - "rules": { - "@typescript-eslint/no-empty-interface": "off" - } -} diff --git a/packages/core-typings/package.json b/packages/core-typings/package.json index 818f309044683..24fc235563153 100644 --- a/packages/core-typings/package.json +++ b/packages/core-typings/package.json @@ -14,8 +14,8 @@ ".:build:prepare": "ts-patch install && typia patch", "build": "run-s .:build:prepare .:build:clean .:build:build", "dev": "tsc --watch --preserveWatchOutput -p tsconfig.json", - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "lint": "eslint .", + "lint:fix": "eslint --fix .", "test": "echo \"no tests\" && exit 1" }, "dependencies": { @@ -27,9 +27,8 @@ }, "devDependencies": { "@rocket.chat/apps-engine": "workspace:^", - "@rocket.chat/eslint-config": "workspace:^", "@types/express": "^4.17.25", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "mongodb": "6.16.0", "npm-run-all": "~4.1.5", "prettier": "~3.3.3", diff --git a/packages/core-typings/src/utils.ts b/packages/core-typings/src/utils.ts index e314a791e20d0..5832a65eccd48 100644 --- a/packages/core-typings/src/utils.ts +++ b/packages/core-typings/src/utils.ts @@ -48,7 +48,6 @@ export type Branded<T, B> = T & Brand<B>; type SerializablePrimitive = boolean | number | string | null; -// eslint-disable-next-line @typescript-eslint/ban-types type UnserializablePrimitive = bigint | symbol | undefined; type CustomSerializable<T> = { diff --git a/packages/cron/.eslintrc.json b/packages/cron/.eslintrc.json deleted file mode 100644 index 9ec331f09b5e3..0000000000000 --- a/packages/cron/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "ignorePatterns": ["dist"] -} diff --git a/packages/cron/package.json b/packages/cron/package.json index 5ea04f5d211d7..4d02ce8f5c389 100644 --- a/packages/cron/package.json +++ b/packages/cron/package.json @@ -10,8 +10,8 @@ "scripts": { "build": "rm -rf dist && tsc -p tsconfig.json", "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput", - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix" + "lint": "eslint .", + "lint:fix": "eslint --fix ." }, "dependencies": { "@rocket.chat/agenda": "workspace:^", @@ -23,7 +23,7 @@ }, "devDependencies": { "@rocket.chat/tsconfig": "workspace:*", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "typescript": "~5.9.3" }, "volta": { diff --git a/packages/ddp-client/.eslintrc.json b/packages/ddp-client/.eslintrc.json deleted file mode 100644 index 8404218fed0a7..0000000000000 --- a/packages/ddp-client/.eslintrc.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "ignorePatterns": ["**/dist", "**/coverage"], - "rules": { - "@typescript-eslint/naming-convention": [ - "error", - { "selector": "variableLike", "format": ["camelCase"], "leadingUnderscore": "allow" }, - { - "selector": ["variable"], - "format": ["camelCase", "UPPER_CASE", "PascalCase"], - "leadingUnderscore": "allowSingleOrDouble" - }, - { - "selector": ["function"], - "format": ["camelCase", "PascalCase"], - "leadingUnderscore": "allowSingleOrDouble" - }, - { - "selector": "parameter", - "format": ["camelCase"], - "modifiers": ["unused"], - "leadingUnderscore": "require" - } - ] - } -} diff --git a/packages/ddp-client/package.json b/packages/ddp-client/package.json index 48ffe80c98370..c84402536220d 100644 --- a/packages/ddp-client/package.json +++ b/packages/ddp-client/package.json @@ -9,8 +9,8 @@ "scripts": { "build": "tsc -p tsconfig.build.json", "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput", - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "lint": "eslint .", + "lint:fix": "eslint --fix .", "test": "jest", "testunit": "jest", "typecheck": "tsc --noEmit" @@ -26,7 +26,7 @@ "@rocket.chat/tsconfig": "workspace:*", "@types/jest": "~30.0.0", "@types/ws": "^8.5.13", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "jest": "~30.2.0", "jest-websocket-mock": "~2.5.0", "typescript": "~5.9.3", diff --git a/packages/desktop-api/.eslintrc.json b/packages/desktop-api/.eslintrc.json deleted file mode 100644 index 9ec331f09b5e3..0000000000000 --- a/packages/desktop-api/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "ignorePatterns": ["dist"] -} diff --git a/packages/desktop-api/package.json b/packages/desktop-api/package.json index 315bb3818f0b4..3a3399c02e402 100644 --- a/packages/desktop-api/package.json +++ b/packages/desktop-api/package.json @@ -12,12 +12,11 @@ "types": "./dist/index.d.ts", "scripts": { "build": "rimraf dist && tsc -p tsconfig.json", - "lint": "eslint", + "lint": "eslint .", "typecheck": "tsc -p tsconfig.json --noEmit" }, "devDependencies": { - "@rocket.chat/eslint-config": "workspace:~", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "rimraf": "~6.0.1", "typescript": "~5.9.3" }, diff --git a/packages/eslint-config/.eslintrc.json b/packages/eslint-config/.eslintrc.json deleted file mode 100644 index 7e5c5710d9304..0000000000000 --- a/packages/eslint-config/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "./standard/index.js" -} diff --git a/packages/eslint-config/best-practices/index.js b/packages/eslint-config/best-practices/index.js deleted file mode 100644 index 28f86b76a508f..0000000000000 --- a/packages/eslint-config/best-practices/index.js +++ /dev/null @@ -1,303 +0,0 @@ -module.exports = { - rules: { - // // enforces getter/setter pairs in objects - // 'accessor-pairs': 'off', - - // enforces return statements in callbacks of array's methods - // https://eslint.org/docs/rules/array-callback-return - 'array-callback-return': ['error', { allowImplicit: true }], - - // treat var statements as if they were block scoped - 'block-scoped-var': 'error', - - // specify the maximum cyclomatic complexity allowed in a program - 'complexity': ['warn', 31], - - // enforce that class methods use "this" - // https://eslint.org/docs/rules/class-methods-use-this - // 'class-methods-use-this': ['error', { - // exceptMethods: [], - // }], - - // require return statements to either always or never specify values - // 'consistent-return': 'error', - - // specify curly brace conventions for all control statements - 'curly': ['error', 'all'], - - // // require default case in switch statements - // 'default-case': ['error', { commentPattern: '^no default$' }], - - // encourages use of dot notation whenever possible - 'dot-notation': ['error', { allowKeywords: true }], - - // enforces consistent newlines before or after dots - // https://eslint.org/docs/rules/dot-location - 'dot-location': ['error', 'property'], - - // require the use of === and !== - // https://eslint.org/docs/rules/eqeqeq - 'eqeqeq': ['error', 'allow-null'], - - // make sure for-in loops have an if statement - 'guard-for-in': 'error', - - // // enforce a maximum number of classes per file - // // https://eslint.org/docs/rules/max-classes-per-file - // // TODO: semver-major (eslint 5): enable - // 'max-classes-per-file': ['off', 1], - - // // disallow the use of alert, confirm, and prompt - // 'no-alert': 'warn', - - // disallow use of arguments.caller or arguments.callee - 'no-caller': 'error', - - // disallow lexical declarations in case/default clauses - // https://eslint.org/docs/rules/no-case-declarations.html - // 'no-case-declarations': 'error', - - // disallow division operators explicitly at beginning of regular expression - // https://eslint.org/docs/rules/no-div-regex - 'no-div-regex': 'off', - - // disallow else after a return in an if - // https://eslint.org/docs/rules/no-else-return - 'no-else-return': ['error', { allowElseIf: false }], - - // disallow empty functions, except for standalone funcs/arrows - // https://eslint.org/docs/rules/no-empty-function - 'no-empty-function': [ - 'error', - { - allow: ['arrowFunctions', 'functions', 'methods'], - }, - ], - - // disallow empty destructuring patterns - // https://eslint.org/docs/rules/no-empty-pattern - 'no-empty-pattern': 'error', - - // // disallow comparisons to null without a type-checking operator - // 'no-eq-null': 'off', - - // disallow use of eval() - 'no-eval': 'error', - - // disallow adding to native types - 'no-extend-native': 'error', - - // disallow unnecessary function binding - 'no-extra-bind': 'error', - - // disallow Unnecessary Labels - // https://eslint.org/docs/rules/no-extra-label - 'no-extra-label': 'error', - - // disallow fallthrough of case statements - 'no-fallthrough': 'error', - - // disallow the use of leading or trailing decimal points in numeric literals - 'no-floating-decimal': 'error', - - // // disallow reassignments of native objects or read-only globals - // // https://eslint.org/docs/rules/no-global-assign - // 'no-global-assign': ['error', { exceptions: [] }], - // // deprecated in favor of no-global-assign - // 'no-native-reassign': 'off', - - // // disallow implicit type conversions - // // https://eslint.org/docs/rules/no-implicit-coercion - // 'no-implicit-coercion': ['off', { - // boolean: false, - // number: true, - // string: true, - // allow: [], - // }], - - // // disallow var and named functions in global scope - // // https://eslint.org/docs/rules/no-implicit-globals - // 'no-implicit-globals': 'off', - - // disallow use of eval()-like methods - 'no-implied-eval': 'error', - - // disallow this keywords outside of classes or class-like objects - 'no-invalid-this': 'off', - - // disallow usage of __iterator__ property - 'no-iterator': 'error', - - // // disallow use of labels for anything other then loops and switches - // 'no-labels': ['error', { allowLoop: false, allowSwitch: false }], - - // disallow unnecessary nested blocks - 'no-lone-blocks': 'error', - - // disallow creation of functions within loops - 'no-loop-func': 'error', - - // // disallow magic numbers - // // https://eslint.org/docs/rules/no-magic-numbers - // 'no-magic-numbers': ['off', { - // ignore: [], - // ignoreArrayIndexes: true, - // enforceConst: true, - // detectObjects: false, - // }], - - // disallow use of multiple spaces - 'no-multi-spaces': 'error', - - // disallow use of multiline strings - 'no-multi-str': 'error', - - // disallow use of new operator when not part of the assignment or comparison - // 'no-new': 'error', - - // disallow use of new operator for Function object - // 'no-new-func': 'error', - - // disallows creating new instances of String, Number, and Boolean - 'no-new-wrappers': 'error', - - // disallow use of (old style) octal literals - 'no-octal': 'error', - - // // disallow use of octal escape sequences in string literals, such as - // // var foo = 'Copyright \251'; - // 'no-octal-escape': 'error', - - // // disallow reassignment of function parameters - // // disallow parameter object manipulation except for specific exclusions - // // rule: https://eslint.org/docs/rules/no-param-reassign.html - // 'no-param-reassign': ['error', { - // props: true, - // ignorePropertyModificationsFor: [ - // 'acc', // for reduce accumulators - // 'accumulator', // for reduce accumulators - // 'e', // for e.returnvalue - // 'ctx', // for Koa routing - // 'req', // for Express requests - // 'request', // for Express requests - // 'res', // for Express responses - // 'response', // for Express responses - // '$scope', // for Angular 1 scopes - // ] - // }], - - // disallow usage of __proto__ property - 'no-proto': 'error', - - // disallow declaring the same variable more then once - 'no-redeclare': 'error', - - // disallow certain object properties - // https://eslint.org/docs/rules/no-restricted-properties - 'no-restricted-properties': [ - 'error', - { - object: 'describe', - property: 'only', - }, - { - object: 'it', - property: 'only', - }, - { - object: 'context', - property: 'only', - }, - ], - - // disallow use of assignment in return statement - 'no-return-assign': ['error', 'always'], - - // disallow redundant `return await` - 'no-return-await': 'error', - - // // disallow use of `javascript:` urls. - // 'no-script-url': 'error', - - // // disallow self assignment - // // https://eslint.org/docs/rules/no-self-assign - // // TODO: semver-major: props -> true - // 'no-self-assign': ['error', { - // props: false, - // }], - - // disallow comparisons where both sides are exactly the same - 'no-self-compare': 'error', - - // disallow use of comma operator - 'no-sequences': 'error', - - // restrict what can be thrown as an exception - 'no-throw-literal': 'error', - - // // disallow unmodified conditions of loops - // // https://eslint.org/docs/rules/no-unmodified-loop-condition - // 'no-unmodified-loop-condition': 'off', - - // // disallow usage of expressions in statement position - // 'no-unused-expressions': ['error', { - // allowShortCircuit: false, - // allowTernary: false, - // allowTaggedTemplates: false, - // }], - - // disallow unused labels - // https://eslint.org/docs/rules/no-unused-labels - 'no-unused-labels': 'error', - - // disallow unnecessary .call() and .apply() - 'no-useless-call': 'off', - - // disallow useless string concatenation - // https://eslint.org/docs/rules/no-useless-concat - 'no-useless-concat': 'error', - - // // disallow unnecessary string escaping - // // https://eslint.org/docs/rules/no-useless-escape - // 'no-useless-escape': 'error', - - // disallow redundant return; keywords - // https://eslint.org/docs/rules/no-useless-return - 'no-useless-return': 'error', - - // disallow use of void operator - // https://eslint.org/docs/rules/no-void - 'no-void': 'off', - - // // disallow usage of configurable warning terms in comments: e.g. todo - // 'no-warning-comments': ['off', { terms: ['todo', 'fixme', 'xxx'], location: 'start' }], - - // // disallow use of the with statement - // 'no-with': 'error', - - // // require using Error objects as Promise rejection reasons - // // https://eslint.org/docs/rules/prefer-promise-reject-errors - // 'prefer-promise-reject-errors': ['error', { allowEmptyReject: true }], - - // // require use of the second argument for parseInt() - // radix: 'error', - - // // require `await` in `async function` (note: this is a horrible rule that should never be used) - // // https://eslint.org/docs/rules/require-await - // 'require-await': 'off', - - // // Enforce the use of u flag on RegExp - // // https://eslint.org/docs/rules/require-unicode-regexp - // 'require-unicode-regexp': 'off', - - // // requires to declare all vars on top of their containing scope - // 'vars-on-top': 'error', - - // require immediate function invocation to be wrapped in parentheses - // https://eslint.org/docs/rules/wrap-iife.html - 'wrap-iife': ['error', 'outside', { functionPrototypeMethods: false }], - - // require or disallow Yoda conditions - 'yoda': 'error', - }, -}; diff --git a/packages/eslint-config/errors/index.js b/packages/eslint-config/errors/index.js deleted file mode 100644 index 3223c31e34b27..0000000000000 --- a/packages/eslint-config/errors/index.js +++ /dev/null @@ -1,148 +0,0 @@ -module.exports = { - rules: { - // Enforce “for” loop update clause moving the counter in the right direction - // https://eslint.org/docs/rules/for-direction - 'for-direction': 'error', - - // Enforces that a return statement is present in property getters - // https://eslint.org/docs/rules/getter-return - 'getter-return': ['error', { allowImplicit: true }], - - // disallow using an async function as a Promise executor - // https://eslint.org/docs/rules/no-async-promise-executor - // TODO: enable, semver-major - 'no-async-promise-executor': 'off', - - // Disallow await inside of loops - // https://eslint.org/docs/rules/no-await-in-loop - 'no-await-in-loop': 'error', - - // Disallow comparisons to negative zero - // https://eslint.org/docs/rules/no-compare-neg-zero - 'no-compare-neg-zero': 'error', - - // disallow assignment in conditional expressions - 'no-cond-assign': 'error', - - // disallow use of console - // 'no-console': 'warn', - - // disallow use of constant expressions in conditions - 'no-constant-condition': 'error', - - // disallow control characters in regular expressions - 'no-control-regex': 'error', - - // disallow use of debugger - 'no-debugger': 'error', - - // disallow duplicate arguments in functions - 'no-dupe-args': 'error', - - // disallow duplicate keys when creating object literals - 'no-dupe-keys': 'error', - - // disallow a duplicate case label. - 'no-duplicate-case': 'error', - - // disallow empty statements - 'no-empty': 'error', - - // disallow the use of empty character classes in regular expressions - 'no-empty-character-class': 'error', - - // disallow assigning to the exception in a catch block - 'no-ex-assign': 'error', - - // disallow double-negation boolean casts in a boolean context - // https://eslint.org/docs/rules/no-extra-boolean-cast - 'no-extra-boolean-cast': 'error', - - // disallow unnecessary parentheses - // https://eslint.org/docs/rules/no-extra-parens - 'no-extra-parens': [ - 'error', - 'all', - { - conditionalAssign: true, - nestedBinaryExpressions: false, - returnAssign: true, - ignoreJSX: 'all', // delegate to eslint-plugin-react - enforceForArrowConditionals: false, - }, - ], - - // disallow unnecessary semicolons - 'no-extra-semi': 'error', - - // disallow overwriting functions written as function declarations - 'no-func-assign': 'error', - - // disallow function declarations in nested blocks - 'no-inner-declarations': ['error', 'functions'], - - // disallow invalid regular expression strings in the RegExp constructor - 'no-invalid-regexp': 'error', - - // disallow irregular whitespace outside of strings and comments - 'no-irregular-whitespace': 'error', - - // // Disallow characters which are made with multiple code points in character class syntax - // // https://eslint.org/docs/rules/no-misleading-character-class - // // TODO: enable, semver-major - // 'no-misleading-character-class': 'off', - - // disallow the use of object properties of the global object (Math and JSON) as functions - 'no-obj-calls': 'error', - - // // disallow use of Object.prototypes builtins directly - // // https://eslint.org/docs/rules/no-prototype-builtins - // 'no-prototype-builtins': 'error', - - // disallow multiple spaces in a regular expression literal - 'no-regex-spaces': 'error', - - // disallow sparse arrays - 'no-sparse-arrays': 'error', - - // // Disallow template literal placeholder syntax in regular strings - // // https://eslint.org/docs/rules/no-template-curly-in-string - // 'no-template-curly-in-string': 'error', - - // Avoid code that looks like two expressions but is actually one - // https://eslint.org/docs/rules/no-unexpected-multiline - 'no-unexpected-multiline': 'error', - - // disallow unreachable statements after a return, throw, continue, or break statement - 'no-unreachable': 'error', - - // disallow return/throw/break/continue inside finally blocks - // https://eslint.org/docs/rules/no-unsafe-finally - 'no-unsafe-finally': 'error', - - // disallow negating the left operand of relational operators - // https://eslint.org/docs/rules/no-unsafe-negation - 'no-unsafe-negation': 'error', - - // disallow negation of the left operand of an in expression - // deprecated in favor of no-unsafe-negation - // TODO: turn off - 'no-negated-in-lhs': 'error', - - // Disallow assignments that can lead to race conditions due to usage of await or yield - // https://eslint.org/docs/rules/require-atomic-updates - // TODO: enable, semver-major - 'require-atomic-updates': 'off', - - // disallow comparisons with the value NaN - 'use-isnan': 'error', - - // // ensure JSDoc comments are valid - // // https://eslint.org/docs/rules/valid-jsdoc - // 'valid-jsdoc': 'off', - - // ensure that the results of typeof are compared against a valid string - // https://eslint.org/docs/rules/valid-typeof - 'valid-typeof': ['error', { requireStringLiterals: true }], - }, -}; diff --git a/packages/eslint-config/es6/index.js b/packages/eslint-config/es6/index.js deleted file mode 100644 index b066791920857..0000000000000 --- a/packages/eslint-config/es6/index.js +++ /dev/null @@ -1,189 +0,0 @@ -module.exports = { - env: { - es6: true, - }, - parserOptions: { - ecmaVersion: 6, - sourceType: 'module', - ecmaFeatures: { - generators: false, - objectLiteralDuplicateProperties: false, - }, - }, - - rules: { - // enforces no braces where they can be omitted - // https://eslint.org/docs/rules/arrow-body-style - // TODO: enable requireReturnForObjectLiteral? - 'arrow-body-style': [ - 'error', - 'as-needed', - { - requireReturnForObjectLiteral: false, - }, - ], - - // require parens in arrow function arguments - // https://eslint.org/docs/rules/arrow-parens - 'arrow-parens': ['error', 'always'], - - // require space before/after arrow function's arrow - // https://eslint.org/docs/rules/arrow-spacing - 'arrow-spacing': ['error', { before: true, after: true }], - - // // verify super() callings in constructors - // 'constructor-super': 'error', - - // // enforce the spacing around the * in generator functions - // // https://eslint.org/docs/rules/generator-star-spacing - // 'generator-star-spacing': ['error', { before: false, after: true }], - - // // disallow modifying variables of class declarations - // // https://eslint.org/docs/rules/no-class-assign - // 'no-class-assign': 'error', - - // disallow arrow functions where they could be confused with comparisons - // https://eslint.org/docs/rules/no-confusing-arrow - 'no-confusing-arrow': [ - 'error', - { - allowParens: true, - }, - ], - - // disallow modifying variables that are declared using const - 'no-const-assign': 'error', - - // disallow duplicate class members - // https://eslint.org/docs/rules/no-dupe-class-members - 'no-dupe-class-members': 'error', - - // disallow importing from the same path more than once - // https://eslint.org/docs/rules/no-duplicate-imports - // replaced by https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-duplicates.md - 'no-duplicate-imports': 'off', - - // // disallow symbol constructor - // // https://eslint.org/docs/rules/no-new-symbol - // 'no-new-symbol': 'error', - - // // disallow specific imports - // // https://eslint.org/docs/rules/no-restricted-imports - // 'no-restricted-imports': ['off', { - // paths: [], - // patterns: [] - // }], - - // disallow to use this/super before super() calling in constructors. - // https://eslint.org/docs/rules/no-this-before-super - 'no-this-before-super': 'error', - - // disallow useless computed property keys - // https://eslint.org/docs/rules/no-useless-computed-key - 'no-useless-computed-key': 'error', - - // disallow unnecessary constructor - // https://eslint.org/docs/rules/no-useless-constructor - 'no-useless-constructor': 'error', - - // disallow renaming import, export, and destructured assignments to the same name - // https://eslint.org/docs/rules/no-useless-rename - 'no-useless-rename': [ - 'error', - { - ignoreDestructuring: false, - ignoreImport: false, - ignoreExport: false, - }, - ], - - // require let or const instead of var - 'no-var': 'error', - - // require method and property shorthand syntax for object literals - // https://eslint.org/docs/rules/object-shorthand - 'object-shorthand': 'error', - - // // suggest using arrow functions as callbacks - // 'prefer-arrow-callback': ['error', { - // allowNamedFunctions: false, - // allowUnboundThis: true, - // }], - - // suggest using of const declaration for variables that are never modified after declared - 'prefer-const': [ - 'error', - { - destructuring: 'any', - ignoreReadBeforeAssign: true, - }, - ], - - // Prefer destructuring from arrays and objects - // https://eslint.org/docs/rules/prefer-destructuring - 'prefer-destructuring': [ - 'error', - { - VariableDeclarator: { - array: false, - object: true, - }, - AssignmentExpression: { - array: false, - object: false, - }, - }, - { - enforceForRenamedProperties: false, - }, - ], - - // // disallow parseInt() in favor of binary, octal, and hexadecimal literals - // // https://eslint.org/docs/rules/prefer-numeric-literals - // 'prefer-numeric-literals': 'error', - - // // suggest using Reflect methods where applicable - // // https://eslint.org/docs/rules/prefer-reflect - // 'prefer-reflect': 'off', - - // use rest parameters instead of arguments - // https://eslint.org/docs/rules/prefer-rest-params - 'prefer-rest-params': 'error', - - // // suggest using the spread operator instead of .apply() - // // https://eslint.org/docs/rules/prefer-spread - // 'prefer-spread': 'error', - - // suggest using template literals instead of string concatenation - // https://eslint.org/docs/rules/prefer-template - 'prefer-template': 'error', - - // // disallow generator functions that do not have yield - // // https://eslint.org/docs/rules/require-yield - // 'require-yield': 'error', - - // enforce spacing between object rest-spread - // https://eslint.org/docs/rules/rest-spread-spacing - 'rest-spread-spacing': ['error', 'never'], - - // // import sorting - // // https://eslint.org/docs/rules/sort-imports - // 'sort-imports': ['off', { - // ignoreCase: false, - // ignoreMemberSort: false, - // memberSyntaxSortOrder: ['none', 'all', 'multiple', 'single'], - // }], - - // // require a Symbol description - // // https://eslint.org/docs/rules/symbol-description - // 'symbol-description': 'error', - - // enforce usage of spacing in template strings - // https://eslint.org/docs/rules/template-curly-spacing - 'template-curly-spacing': ['error', 'always'], - - // // enforce spacing around the * in yield* expressions - // // https://eslint.org/docs/rules/yield-star-spacing - // 'yield-star-spacing': ['error', 'after'] - }, -}; diff --git a/packages/eslint-config/imports/index.js b/packages/eslint-config/imports/index.js deleted file mode 100644 index 5b0b65ec48697..0000000000000 --- a/packages/eslint-config/imports/index.js +++ /dev/null @@ -1,257 +0,0 @@ -module.exports = { - env: { - es6: true, - }, - parserOptions: { - ecmaVersion: 6, - sourceType: 'module', - }, - plugins: ['import'], - - settings: { - 'import/resolver': { - node: { - extensions: ['.mjs', '.js', '.json'], - }, - }, - 'import/extensions': ['.js', '.mjs', '.jsx'], - 'import/core-modules': [], - 'import/ignore': ['node_modules', '\\.(coffee|scss|css|less|hbs|svg|json)$'], - }, - - rules: { - // Static analysis: - - // ensure imports point to files/modules that can be resolved - // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-unresolved.md - 'import/no-unresolved': [ - 'error', - { - commonjs: true, - caseSensitive: true, - amd: true, - ignore: ['^meteor/.+$'], - }, - ], - - // ensure named imports coupled with named exports - // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/named.md#when-not-to-use-it - 'import/named': 'off', - - // ensure default import coupled with default export - // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/default.md#when-not-to-use-it - 'import/default': 'off', - - // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/namespace.md - 'import/namespace': 'off', - - // Helpful warnings: - - // disallow invalid exports, e.g. multiple defaults - // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/export.md - 'import/export': 'error', - - // do not allow a default import name to match a named export - // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-named-as-default.md - // 'import/no-named-as-default': 'error', - 'import/no-named-as-default': 'off', - - // warn on accessing default export property names that are also named exports - // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-named-as-default-member.md - // 'import/no-named-as-default-member': 'error', - 'import/no-named-as-default-member': 'off', - - // // disallow use of jsdoc-marked-deprecated imports - // // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-deprecated.md - // 'import/no-deprecated': 'off', - - // // Forbid the use of extraneous packages - // // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-extraneous-dependencies.md - // // paths are treated both as absolute paths, and relative to process.cwd() - // 'import/no-extraneous-dependencies': ['error', { - // devDependencies: [ - // 'test/**', // tape, common npm pattern - // 'tests/**', // also common npm pattern - // 'spec/**', // mocha, rspec-like pattern - // '**/__tests__/**', // jest pattern - // '**/__mocks__/**', // jest pattern - // 'test.{js,jsx}', // repos with a single test file - // 'test-*.{js,jsx}', // repos with multiple top-level test files - // '**/*{.,_}{test,spec}.{js,jsx}', // tests where the extension or filename suffix denotes that it is a test - // '**/jest.config.js', // jest config - // '**/vue.config.js', // vue-cli config - // '**/webpack.config.js', // webpack config - // '**/webpack.config.*.js', // webpack config - // '**/rollup.config.js', // rollup config - // '**/rollup.config.*.js', // rollup config - // '**/gulpfile.js', // gulp config - // '**/gulpfile.*.js', // gulp config - // '**/Gruntfile{,.js}', // grunt config - // '**/protractor.conf.js', // protractor config - // '**/protractor.conf.*.js', // protractor config - // ], - // optionalDependencies: false, - // }], - - // // Forbid mutable exports - // // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-mutable-exports.md - // 'import/no-mutable-exports': 'error', - - // Module systems: - - // // disallow require() - // // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-commonjs.md - // 'import/no-commonjs': 'off', - - // // disallow AMD require/define - // // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-amd.md - // 'import/no-amd': 'error', - - // // No Node.js builtin modules - // // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-nodejs-modules.md - // // TODO: enable? - // 'import/no-nodejs-modules': 'off', - - // Style guide: - - // disallow non-import statements appearing before import statements - // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/first.md - 'import/first': 'error', - - // // disallow non-import statements appearing before import statements - // // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/imports-first.md - // // deprecated: use `import/first` - // 'import/imports-first': 'off', - - // disallow duplicate imports - // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-duplicates.md - 'import/no-duplicates': 'error', - - // // disallow namespace imports - // // TODO: enable? - // // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-namespace.md - // 'import/no-namespace': 'off', - - // // Ensure consistent use of file extension within the import path - // // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/extensions.md - // 'import/extensions': ['error', 'ignorePackages', { - // js: 'never', - // mjs: 'never', - // jsx: 'never', - // }], - - // ensure absolute imports are above relative imports and that unassigned imports are ignored - // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/order.md - // TODO: enforce a stricter convention in module import order? - 'import/order': [ - 'error', - { - 'newlines-between': 'always', - 'groups': ['builtin', ['external', 'internal'], ['parent', 'sibling', 'index']], - 'alphabetize': { - order: 'asc', - }, - }, - ], - - // Require a newline after the last import/require in a group - // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/newline-after-import.md - 'import/newline-after-import': 'error', - - // // Require modules with a single export to use a default export - // // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/prefer-default-export.md - // 'import/prefer-default-export': 'error', - - // // Restrict which files can be imported in a given folder - // // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-restricted-paths.md - // 'import/no-restricted-paths': 'off', - - // // Forbid modules to have too many dependencies - // // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/max-dependencies.md - // 'import/max-dependencies': ['off', { max: 10 }], - - // Forbid import of modules using absolute paths - // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-absolute-path.md - 'import/no-absolute-path': 'error', - - // Forbid require() calls with expressions - // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-dynamic-require.md - 'import/no-dynamic-require': 'error', - - // // prevent importing the submodules of other modules - // // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-internal-modules.md - // 'import/no-internal-modules': ['off', { - // allow: [], - // }], - - // // Warn if a module could be mistakenly parsed as a script by a consumer - // // leveraging Unambiguous JavaScript Grammar - // // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/unambiguous.md - // // this should not be enabled until this proposal has at least been *presented* to TC39. - // // At the moment, it's not a thing. - // 'import/unambiguous': 'off', - - // // Forbid Webpack loader syntax in imports - // // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-webpack-loader-syntax.md - // 'import/no-webpack-loader-syntax': 'error', - - // // Prevent unassigned imports - // // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-unassigned-import.md - // // importing for side effects is perfectly acceptable, if you need side effects. - // 'import/no-unassigned-import': 'off', - - // // Prevent importing the default as if it were named - // // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/no-named-default.md - // 'import/no-named-default': 'error', - - // // Reports if a module's default export is unnamed - // // https://github.com/benmosher/eslint-plugin-import/blob/d9b712ac7fd1fddc391f7b234827925c160d956f/docs/rules/no-anonymous-default-export.md - // 'import/no-anonymous-default-export': ['off', { - // allowArray: false, - // allowArrowFunction: false, - // allowAnonymousClass: false, - // allowAnonymousFunction: false, - // allowLiteral: false, - // allowObject: false, - // }], - - // // This rule enforces that all exports are declared at the bottom of the file. - // // https://github.com/benmosher/eslint-plugin-import/blob/98acd6afd04dcb6920b81330114e146dc8532ea4/docs/rules/exports-last.md - // // TODO: enable? - // 'import/exports-last': 'off', - - // // Reports when named exports are not grouped together in a single export declaration - // // or when multiple assignments to CommonJS module.exports or exports object are present - // // in a single file. - // // https://github.com/benmosher/eslint-plugin-import/blob/44a038c06487964394b1e15b64f3bd34e5d40cde/docs/rules/group-exports.md - // 'import/group-exports': 'off', - - // // forbid default exports. this is a terrible rule, do not use it. - // // https://github.com/benmosher/eslint-plugin-import/blob/44a038c06487964394b1e15b64f3bd34e5d40cde/docs/rules/no-default-export.md - // 'import/no-default-export': 'off', - - // Forbid a module from importing itself - // https://github.com/benmosher/eslint-plugin-import/blob/44a038c06487964394b1e15b64f3bd34e5d40cde/docs/rules/no-self-import.md - 'import/no-self-import': 'error', - - // Forbid cyclical dependencies between modules - // https://github.com/benmosher/eslint-plugin-import/blob/d81f48a2506182738409805f5272eff4d77c9348/docs/rules/no-cycle.md - // 'import/no-cycle': ['error', { maxDepth: Infinity }], - 'import/no-cycle': 'off', - - // Ensures that there are no useless path segments - // https://github.com/benmosher/eslint-plugin-import/blob/ebafcbf59ec9f653b2ac2a0156ca3bcba0a7cf57/docs/rules/no-useless-path-segments.md - 'import/no-useless-path-segments': 'error', - - // // dynamic imports require a leading comment with a webpackChunkName - // // https://github.com/benmosher/eslint-plugin-import/blob/ebafcbf59ec9f653b2ac2a0156ca3bcba0a7cf57/docs/rules/dynamic-import-chunkname.md - // 'import/dynamic-import-chunkname': ['off', { - // importFunctions: [], - // webpackChunknameFormat: '[0-9a-zA-Z-_/.]+', - // }], - - // // Use this rule to prevent imports to folders in relative parent paths. - // // https://github.com/benmosher/eslint-plugin-import/blob/c34f14f67f077acd5a61b3da9c0b0de298d20059/docs/rules/no-relative-parent-imports.md - // 'import/no-relative-parent-imports': 'off', - }, -}; diff --git a/packages/eslint-config/index.js b/packages/eslint-config/index.js new file mode 100644 index 0000000000000..af35dc0d6c437 --- /dev/null +++ b/packages/eslint-config/index.js @@ -0,0 +1,565 @@ +import eslint from '@eslint/js'; +import { defineConfig } from 'eslint/config'; +import antiTrojanSourcePlugin from 'eslint-plugin-anti-trojan-source'; +import importPlugin from 'eslint-plugin-import'; +import jestPlugin from 'eslint-plugin-jest'; +import jsxA11yPlugin from 'eslint-plugin-jsx-a11y'; +import prettierPluginRecommended from 'eslint-plugin-prettier/recommended'; +import reactPlugin from 'eslint-plugin-react'; +import reactHooksPlugin from 'eslint-plugin-react-hooks'; +import storybookPlugin from 'eslint-plugin-storybook'; +import testingLibraryPlugin from 'eslint-plugin-testing-library'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; + +export default defineConfig( + { + name: 'rocket.chat/linter', + linterOptions: { + reportUnusedDisableDirectives: true, + }, + }, + { + name: 'rocket.chat/ignored', + ignores: ['**/dist', '**/coverage', '**/storybook-static'], + }, + eslint.configs.recommended, + tseslint.configs.recommendedTypeChecked, + { + languageOptions: { + parserOptions: { + projectService: true, + }, + }, + }, + importPlugin.flatConfigs.recommended, + importPlugin.flatConfigs.typescript, + jsxA11yPlugin.flatConfigs.recommended, + { + name: 'rocket.chat/jsx-a11y', + rules: { + 'jsx-a11y/no-autofocus': ['error', { ignoreNonDOM: true }], + }, + }, + reactPlugin.configs.flat.recommended, + reactPlugin.configs.flat['jsx-runtime'], + { + name: 'rocket.chat/react', + settings: { + react: { + version: 'detect', + }, + }, + rules: { + 'react/jsx-curly-brace-presence': 'error', + 'react/jsx-fragments': ['error', 'syntax'], + 'react/jsx-key': ['error', { checkFragmentShorthand: true, checkKeyMustBeforeSpread: true, warnOnDuplicates: true }], + 'react/jsx-no-target-blank': 'warn', + 'react/no-multi-comp': 'error', + 'react/no-unescaped-entities': 'warn', + 'react/prop-types': 'warn', + }, + }, + reactHooksPlugin.configs.flat.recommended, + { + name: 'rocket.chat/react-hooks', + rules: { + // Core hooks rules + 'react-hooks/exhaustive-deps': 'error', + + // React Compiler rules (currently not in use) + 'react-hooks/component-hook-factories': 'off', + 'react-hooks/config': 'off', + 'react-hooks/error-boundaries': 'off', + 'react-hooks/gating': 'off', + 'react-hooks/globals': 'off', + 'react-hooks/immutability': 'off', + 'react-hooks/incompatible-library': 'off', + 'react-hooks/preserve-manual-memoization': 'off', + 'react-hooks/purity': 'off', + 'react-hooks/refs': 'off', + 'react-hooks/set-state-in-effect': 'off', + 'react-hooks/set-state-in-render': 'off', + 'react-hooks/static-components': 'off', + 'react-hooks/unsupported-syntax': 'off', + 'react-hooks/use-memo': 'off', + }, + }, + { + files: ['**/*.@(spec|test).@(ts|tsx|js|jsx|mjs|cjs)'], + ...jestPlugin.configs['flat/recommended'], + ...testingLibraryPlugin.configs['flat/react'], + plugins: { + ...jestPlugin.configs['flat/recommended'].plugins, + ...testingLibraryPlugin.configs['flat/react'].plugins, + }, + rules: { + ...jestPlugin.configs['flat/recommended'].rules, + ...testingLibraryPlugin.configs['flat/react'].rules, + 'jest/no-conditional-expect': 'warn', + 'jest/no-done-callback': 'warn', + 'jest/no-export': 'warn', + 'jest/no-identical-title': 'warn', + 'jest/no-standalone-expect': 'warn', + 'jest/no-test-prefixes': 'warn', + 'jest/valid-describe-callback': 'warn', + 'jest/valid-expect-in-promise': 'warn', + 'jest/valid-expect': 'warn', + 'jest/valid-title': 'warn', + 'testing-library/no-await-sync-events': 'warn', + 'testing-library/no-container': 'warn', + 'testing-library/no-manual-cleanup': 'warn', + 'testing-library/no-node-access': 'warn', + 'testing-library/no-render-in-lifecycle': 'warn', + 'testing-library/prefer-explicit-assert': 'warn', + 'testing-library/prefer-find-by': 'warn', + 'testing-library/prefer-screen-queries': 'warn', + 'testing-library/prefer-user-event': 'warn', + 'testing-library/render-result-naming-convention': 'warn', + }, + }, + ...storybookPlugin.configs['flat/recommended'], + { + name: 'rocket.chat/anti-trojan', + plugins: { + 'anti-trojan-source': antiTrojanSourcePlugin, + }, + rules: { + 'anti-trojan-source/no-bidi': 'error', + }, + }, + prettierPluginRecommended, + { + name: 'rocket.chat/ecmascript', + languageOptions: { + ecmaVersion: 2024, + sourceType: 'module', + }, + }, + { + name: 'rocket.chat/disable-typescript-rules-for-js', + files: ['**/*.@(js|jsx|mjs|cjs)'], + rules: { + '@typescript-eslint/ban-ts-comment': 'off', + '@typescript-eslint/no-array-constructor': 'off', + '@typescript-eslint/no-duplicate-enum-values': 'off', + '@typescript-eslint/no-empty-object-type': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-extra-non-null-assertion': 'off', + '@typescript-eslint/no-misused-new': 'off', + '@typescript-eslint/no-namespace': 'off', + '@typescript-eslint/no-non-null-asserted-optional-chain': 'off', + '@typescript-eslint/no-require-imports': 'off', + '@typescript-eslint/no-this-alias': 'off', + '@typescript-eslint/no-unnecessary-type-constraint': 'off', + '@typescript-eslint/no-unsafe-declaration-merging': 'off', + '@typescript-eslint/no-unsafe-function-type': 'off', + '@typescript-eslint/no-unused-expressions': 'off', + '@typescript-eslint/no-unused-vars': 'off', + '@typescript-eslint/no-wrapper-object-types': 'off', + '@typescript-eslint/prefer-as-const': 'off', + '@typescript-eslint/prefer-namespace-keyword': 'off', + '@typescript-eslint/triple-slash-reference': 'off', + }, + }, + { + name: 'rocket.chat/disable-type-checked-rules-for-js', + files: ['**/*.@(js|jsx|mjs|cjs)'], + ...tseslint.configs.disableTypeChecked, + }, + { + name: 'rocket.chat/best-practices', + rules: { + 'array-callback-return': ['error', { allowImplicit: true }], + 'block-scoped-var': 'error', + 'complexity': ['warn', 31], + 'dot-notation': ['error', { allowKeywords: true }], + 'eqeqeq': ['error', 'allow-null'], + 'guard-for-in': 'error', + 'no-caller': 'error', + 'no-div-regex': 'off', + 'no-else-return': ['error', { allowElseIf: false }], + 'no-empty-function': [ + 'error', + { + allow: ['arrowFunctions', 'functions', 'methods'], + }, + ], + 'no-eval': 'error', + 'no-extend-native': 'error', + 'no-extra-bind': 'error', + 'no-extra-label': 'error', + 'no-implied-eval': 'error', + 'no-invalid-this': 'off', + 'no-iterator': 'error', + 'no-lone-blocks': 'error', + 'no-loop-func': 'error', + 'no-multi-str': 'error', + 'no-new-wrappers': 'error', + 'no-proto': 'error', + 'no-restricted-properties': [ + 'error', + { + object: 'describe', + property: 'only', + }, + { + object: 'it', + property: 'only', + }, + { + object: 'context', + property: 'only', + }, + ], + 'no-return-assign': ['error', 'always'], + 'no-return-await': 'error', + 'no-self-compare': 'error', + 'no-sequences': 'error', + 'no-throw-literal': 'error', + 'no-useless-call': 'off', + 'no-useless-catch': 'warn', + 'no-useless-concat': 'error', + 'no-useless-return': 'error', + 'no-void': 'off', + 'preserve-caught-error': 'warn', + 'yoda': 'error', + }, + }, + { + name: 'rocket.chat/common-mistakes', + rules: { + 'getter-return': ['error', { allowImplicit: true }], + 'no-async-promise-executor': 'warn', + 'no-await-in-loop': 'error', + 'no-case-declarations': 'warn', + 'no-constant-binary-expression': 'warn', + 'no-debugger': 'error', + 'no-inner-declarations': ['error', 'functions'], + 'no-negated-in-lhs': 'error', + 'no-prototype-builtins': 'warn', + 'no-unsafe-optional-chaining': 'warn', + 'no-useless-assignment': 'warn', + 'require-atomic-updates': 'off', + 'valid-typeof': ['error', { requireStringLiterals: true }], + }, + }, + // TODO: disable, as they are not available in all environments + { + name: 'rocket.chat/node-globals', + languageOptions: { + globals: { + ...globals.node, + }, + }, + }, + { + name: 'rocket.chat/stylistic', + rules: { + 'lines-between-class-members': ['error', 'always', { exceptAfterSingleLine: false }], + 'lines-around-directive': [ + 'error', + { + before: 'always', + after: 'always', + }, + ], + 'max-depth': ['off', 4], + 'new-cap': 'error', + 'no-array-constructor': 'error', + 'no-lonely-if': 'error', + 'no-multi-assign': 'error', + 'no-nested-ternary': 'error', + 'no-unneeded-ternary': ['error', { defaultAssignment: false }], + 'no-useless-escape': 'warn', + 'one-var': ['error', 'never'], + 'operator-assignment': ['error', 'always'], + 'prefer-object-spread': 'off', + 'spaced-comment': 'error', + }, + }, + { + name: 'rocket.chat/variables', + rules: { + 'no-unused-vars': [ + 'error', + { + vars: 'all', + args: 'after-used', + ignoreRestSiblings: true, + caughtErrors: 'none', + }, + ], + 'no-use-before-define': ['error', { functions: true, classes: true, variables: true }], + }, + }, + { + name: 'rocket.chat/es2015', + rules: { + 'no-duplicate-imports': 'off', + 'no-useless-computed-key': 'error', + 'no-useless-constructor': 'error', + 'no-useless-rename': [ + 'error', + { + ignoreDestructuring: false, + ignoreImport: false, + ignoreExport: false, + }, + ], + 'no-var': 'error', + 'object-shorthand': 'error', + 'prefer-const': [ + 'error', + { + destructuring: 'any', + ignoreReadBeforeAssign: true, + }, + ], + 'prefer-destructuring': [ + 'error', + { + VariableDeclarator: { + array: false, + object: true, + }, + AssignmentExpression: { + array: false, + object: false, + }, + }, + { + enforceForRenamedProperties: false, + }, + ], + 'prefer-rest-params': 'error', + 'prefer-template': 'error', + }, + }, + { + name: 'rocket.chat/import', + settings: { + 'import/resolver': { + node: true, + typescript: true, + }, + }, + rules: { + 'import/no-unresolved': [ + 'error', + { + commonjs: true, + caseSensitive: true, + }, + ], + 'import/named': 'off', + 'import/default': 'off', + 'import/namespace': 'off', + 'import/export': 'error', + 'import/no-named-as-default': 'off', + 'import/no-named-as-default-member': 'off', + 'import/first': 'error', + 'import/no-duplicates': 'error', + 'import/order': [ + 'error', + { + 'newlines-between': 'always', + 'groups': ['builtin', ['external', 'internal'], ['parent', 'sibling', 'index']], + 'alphabetize': { + order: 'asc', + }, + }, + ], + 'import/newline-after-import': 'error', + 'import/no-absolute-path': 'error', + 'import/no-dynamic-require': 'error', + 'import/no-self-import': 'error', + 'import/no-cycle': 'off', + 'import/no-useless-path-segments': 'error', + }, + }, + { + files: ['**/*.@(ts|tsx|cts|mts)'], + rules: { + '@typescript-eslint/no-empty-object-type': 'warn', + '@typescript-eslint/no-unsafe-function-type': 'warn', + '@typescript-eslint/no-wrapper-object-types': 'warn', + '@typescript-eslint/no-restricted-types': [ + 'warn', + { + types: { + 'FC': 'Useless and has some drawbacks, see https://adr.rocket.chat/0094', + 'React.FC': 'Useless and has some drawbacks, see https://adr.rocket.chat/0094', + 'VFC': 'Useless and has some drawbacks, see https://adr.rocket.chat/0094', + 'React.VFC': 'Useless and has some drawbacks, see https://adr.rocket.chat/0094', + 'FunctionComponent': 'Useless and has some drawbacks, see https://adr.rocket.chat/0094', + 'React.FunctionComponent': 'Useless and has some drawbacks, see https://adr.rocket.chat/0094', + }, + }, + ], + '@typescript-eslint/ban-ts-comment': 'warn', + '@typescript-eslint/consistent-type-imports': [ + 'warn', + { + disallowTypeAnnotations: false, + fixStyle: 'inline-type-imports', + }, + ], + '@typescript-eslint/naming-convention': [ + 'error', + { selector: 'variableLike', format: ['camelCase'], leadingUnderscore: 'allow' }, + { + selector: ['variable'], + format: ['camelCase', 'UPPER_CASE', 'PascalCase'], + leadingUnderscore: 'allowSingleOrDouble', + }, + { + selector: ['function'], + format: ['camelCase', 'PascalCase'], + leadingUnderscore: 'allowSingleOrDouble', + }, + { + selector: 'parameter', + format: null, + filter: { + regex: '^Story$', + match: true, + }, + }, + { + selector: 'parameter', + format: ['camelCase'], + modifiers: ['unused'], + leadingUnderscore: 'require', + }, + { + selector: 'interface', + format: ['PascalCase'], + custom: { + regex: '^I[A-Z]', + match: true, + }, + }, + ], + '@typescript-eslint/await-thenable': 'warn', + '@typescript-eslint/no-array-delete': 'warn', + '@typescript-eslint/no-base-to-string': 'warn', + '@typescript-eslint/no-dupe-class-members': 'error', + '@typescript-eslint/no-duplicate-enum-values': 'warn', + '@typescript-eslint/no-duplicate-type-constituents': 'warn', + '@typescript-eslint/no-empty-function': 'error', + '@typescript-eslint/no-empty-interface': 'warn', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-for-in-array': 'warn', + '@typescript-eslint/no-implied-eval': 'warn', + '@typescript-eslint/no-non-null-assertion': 'warn', + '@typescript-eslint/no-redeclare': 'error', + '@typescript-eslint/no-redundant-type-constituents': 'off', + '@typescript-eslint/no-require-imports': 'warn', + '@typescript-eslint/no-this-alias': 'error', + '@typescript-eslint/no-unnecessary-type-assertion': 'warn', + '@typescript-eslint/no-unused-expressions': 'warn', + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + ignoreRestSiblings: true, + caughtErrors: 'none', + }, + ], + '@typescript-eslint/no-unsafe-argument': 'off', + '@typescript-eslint/no-unsafe-assignment': 'off', + '@typescript-eslint/no-unsafe-call': 'warn', + '@typescript-eslint/no-unsafe-declaration-merging': 'warn', + '@typescript-eslint/no-unsafe-enum-comparison': 'warn', + '@typescript-eslint/no-unsafe-member-access': 'off', + '@typescript-eslint/no-unsafe-return': 'warn', + '@typescript-eslint/only-throw-error': 'warn', + '@typescript-eslint/prefer-promise-reject-errors': 'warn', + '@typescript-eslint/prefer-optional-chain': 'warn', + '@typescript-eslint/require-await': 'off', + '@typescript-eslint/restrict-plus-operands': 'warn', + '@typescript-eslint/restrict-template-expressions': 'warn', + '@typescript-eslint/unbound-method': 'off', + 'no-dupe-class-members': 'off', + 'no-empty-function': 'off', + 'no-redeclare': 'off', + 'no-undef': 'off', + 'no-unused-vars': 'off', + 'no-use-before-define': 'off', + 'no-useless-constructor': 'off', + }, + }, + { + files: [ + '**/*.d.ts', + '**/__tests__/**', + '**/*.spec.ts', + '**/*.spec.tsx', + '**/*.test.ts', + '**/*.test.tsx', + '**/tests/**', + '**/.storybook/**', + '**/jest.config.ts', + '**/jest.config.js', + '**/jest.config.*.ts', + '**/jest.config.*.js', + '**/webpack.config.ts', + '**/webpack.config.js', + '**/vite.config.ts', + '**/vite.config.js', + '**/rollup.config.ts', + '**/rollup.config.js', + ], + ...tseslint.configs.disableTypeChecked, + }, + { + files: ['**/*.ts', '**/*.tsx', '**/*.cts', '**/*.mts'], + ignores: [ + '**/*.d.ts', + '**/__tests__/**', + '**/*.spec.ts', + '**/*.spec.tsx', + '**/*.test.ts', + '**/*.test.tsx', + '**/tests/**', + '**/.storybook/**', + '**/jest.config.ts', + '**/jest.config.js', + '**/jest.config.*.ts', + '**/jest.config.*.js', + '**/webpack.config.ts', + '**/webpack.config.js', + '**/vite.config.ts', + '**/vite.config.js', + '**/rollup.config.ts', + '**/rollup.config.js', + ], + rules: { + '@typescript-eslint/no-misused-promises': [ + 'error', + { + checksVoidReturn: { + arguments: false, + inheritedMethods: false, + }, + }, + ], + '@typescript-eslint/no-floating-promises': 'error', + }, + }, + { + files: ['**/*.d.ts'], + rules: { + '@typescript-eslint/naming-convention': 'off', + }, + }, + { + name: 'rocket.chat/react-testing', + files: ['**/*.stories.@(ts|tsx|mts|cts|js|jsx|mjs|cjs)', '**/*.spec.@(ts|tsx|js|jsx|mjs|cjs)'], + rules: { + 'react/display-name': 'off', + 'react/no-multi-comp': 'off', + }, + }, +); diff --git a/packages/eslint-config/node/index.js b/packages/eslint-config/node/index.js deleted file mode 100644 index 396bb969c7df3..0000000000000 --- a/packages/eslint-config/node/index.js +++ /dev/null @@ -1,33 +0,0 @@ -module.exports = { - env: { - node: true, - }, - - rules: { - // enforce return after a callback - // 'callback-return': 'off', - // // require all requires be top-level - // // https://eslint.org/docs/rules/global-require - // 'global-require': 'error', - // // enforces error handling in callbacks (node environment) - // 'handle-callback-err': 'off', - // // disallow use of the Buffer() constructor - // // https://eslint.org/docs/rules/no-buffer-constructor - // 'no-buffer-constructor': 'error', - // // disallow mixing regular variable and require declarations - // 'no-mixed-requires': ['off', false], - // // disallow use of new operator with the require function - // 'no-new-require': 'error', - // // disallow string concatenation with __dirname and __filename - // // https://eslint.org/docs/rules/no-path-concat - // 'no-path-concat': 'error', - // // disallow use of process.env - // 'no-process-env': 'off', - // // disallow process.exit() - // 'no-process-exit': 'off', - // // restrict usage of specified node modules - // 'no-restricted-modules': 'off', - // // disallow use of synchronous methods (off by default) - // 'no-sync': 'off', - }, -}; diff --git a/packages/eslint-config/original/index.js b/packages/eslint-config/original/index.js deleted file mode 100644 index 0f01f4c570c56..0000000000000 --- a/packages/eslint-config/original/index.js +++ /dev/null @@ -1,148 +0,0 @@ -module.exports = { - extends: [ - '../best-practices/index.js', - '../errors/index.js', - '../node/index.js', - '../style/index.js', - '../variables/index.js', - '../es6/index.js', - '../imports/index.js', - ], - - parserOptions: { - sourceType: 'module', - ecmaVersion: 2018, - ecmaFeatures: { - generators: false, - objectLiteralDuplicateProperties: false, - }, - }, - env: { - browser: true, - commonjs: true, - es6: true, - node: true, - }, - // rules: { - // // 'no-multi-spaces': 2, - // // 'no-eval': 2, - // // 'no-extend-native': 2, - // // 'no-multi-str': 2, - // // 'no-use-before-define': 2, - // // 'no-const-assign': 2, - // // 'no-cond-assign': 2, - // // 'no-constant-condition': 2, - // // 'no-control-regex': 2, - // // 'no-debugger': 2, - // // 'no-delete-var': 2, - // // 'no-dupe-keys': 2, - // // 'no-dupe-args': 2, - // // 'no-dupe-class-members': 2, - // // 'no-duplicate-case': 2, - // // 'no-empty': 2, - // // 'no-empty-character-class': 2, - // // 'no-ex-assign': 2, - // // 'no-extra-boolean-cast': 2, - // // 'no-extra-semi': 2, - // // 'no-fallthrough': 2, - // // 'no-func-assign': 2, - // // 'no-inner-declarations': [2, 'functions'], - // // 'no-invalid-regexp': 2, - // // 'no-irregular-whitespace': 2, - // // 'no-mixed-operators': [2, { - // // 'groups': [ - // // ['%', '**'], - // // ['%', '+'], - // // ['%', '-'], - // // ['%', '*'], - // // ['%', '/'], - // // ['**', '+'], - // // ['**', '-'], - // // ['**', '*'], - // // ['**', '/'], - // // ['&', '|', '^', '~', '<<', '>>', '>>>'], - // // ['==', '!=', '===', '!==', '>', '>=', '<', '<='], - // // ['&&', '||'], - // // ['in', 'instanceof'] - // // ], - // // 'allowSamePrecedence': false - // // }], - // // 'no-mixed-spaces-and-tabs': 2, - // // 'no-sparse-arrays': 2, - // // 'no-negated-in-lhs': 2, - // // 'no-obj-calls': 2, - // // 'no-octal': 2, - // // 'no-redeclare': 2, - // // 'no-regex-spaces': 2, - // // 'no-undef': 2, - // // 'no-unreachable': 2, - // // 'no-unused-vars': [2, { - // // 'vars': 'all', - // // 'args': 'after-used' - // // }], - // // 'no-void': 2, - // // 'no-var': 2, - // // 'no-multiple-empty-lines': [2, { 'max': 2 }], - // // 'no-nested-ternary': 2, - // // 'prefer-rest-params': 2, - // // 'array-callback-return': 2, - // // 'prefer-destructuring': [2, { - // // 'VariableDeclarator': { - // // 'array': false, - // // 'object': true - // // }, - // // 'AssignmentExpression': { - // // 'array': false, - // // 'object': false - // // } - // // }, { - // // 'enforceForRenamedProperties': false - // // }], - // // 'no-duplicate-imports': 2, - // // 'arrow-parens': [2, 'always'], - // // 'quote-props': [2, 'as-needed'], - // // 'no-array-constructor': 2, - // // 'arrow-spacing': 2, - // // 'arrow-body-style': [2, 'as-needed'], - // // 'no-confusing-arrow': [2, { 'allowParens': true }], - // // 'dot-notation': 2, - // // 'no-unneeded-ternary': 2, - // // 'spaced-comment': 2, - // // 'space-infix-ops': 2, - // // 'array-bracket-spacing': [2, 'never'], - // // 'object-curly-spacing': [2, 'always'], - // // 'one-var': [2, 'never'], - // // 'no-lonely-if': 2, - // // 'no-trailing-spaces': 2, - // // 'complexity': [1, 31], - // // 'space-in-parens': [2, 'never'], - // // 'space-before-function-paren': [2, 'never'], - // // 'space-before-blocks': [2, 'always'], - // // 'indent': [2, 'tab', {'SwitchCase': 1}], - // // 'eol-last': [2, 'always'], - // // 'comma-dangle': [2, 'always-multiline'], - // // 'keyword-spacing': 2, - // // 'block-spacing': 2, - // // 'brace-style': [2, '1tbs', { 'allowSingleLine': true }], - // // 'computed-property-spacing': 2, - // // 'comma-spacing': 2, - // // 'comma-style': 2, - // // 'guard-for-in': 2, - // // 'wrap-iife': 2, - // // 'block-scoped-var': 2, - // // 'curly': [2, 'all'], - // // 'eqeqeq': [2, 'allow-null'], - // // 'new-cap': [2, { - // // 'capIsNewExceptions': ['Match.Optional', 'Match.Maybe', 'Match.OneOf', 'Match.Where', 'Match.ObjectIncluding', 'Push.Configure', 'SHA256'] - // // }], - // // 'use-isnan': 2, - // // 'valid-typeof': 2, - // // 'linebreak-style': [2, 'unix'], - // // 'prefer-template': 2, - // // 'template-curly-spacing': [2, 'always'], - // // 'quotes': [2, 'single'], - // // 'semi': [2, 'always'], - // // 'prefer-const': 2, - // // 'object-shorthand': 2 - // } -}; diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index c248461897d53..c7adb4c32642c 100644 --- a/packages/eslint-config/package.json +++ b/packages/eslint-config/package.json @@ -2,39 +2,39 @@ "name": "@rocket.chat/eslint-config", "version": "0.7.0", "description": "Rocket.Chat's JS/TS ESLint config", - "main": "./standard/index.js", + "type": "module", + "main": "./index.js", + "exports": { + ".": "./index.js" + }, "files": [ - "/standard", - "/original", - "/best-practices", - "/errors", - "/es6", - "/imports", - "/node", - "/style", - "/variables", - "react.js" + "/index.js" ], "scripts": { - "lint": "eslint --ext .js .", - "lint:fix": "eslint --ext .js . --fix" + "lint": "eslint .", + "lint:fix": "eslint --fix ." }, "dependencies": { "@babel/core": "~7.28.6", "@babel/eslint-parser": "~7.28.6", + "@eslint/js": "~9.39.3", "@types/eslint": "~8.44.9", "@types/prettier": "^2.7.3", - "@typescript-eslint/eslint-plugin": "~5.60.1", - "@typescript-eslint/parser": "~5.60.1", - "eslint": "~8.45.0", - "eslint-config-prettier": "~9.1.2", + "eslint": "~9.39.3", + "eslint-config-prettier": "~10.1.8", "eslint-import-resolver-typescript": "~4.4.4", "eslint-plugin-anti-trojan-source": "~1.1.2", - "eslint-plugin-import": "~2.31.0", - "eslint-plugin-jest": "~28.8.3", - "eslint-plugin-jsx-a11y": "^6.10.2", - "eslint-plugin-prettier": "~5.2.6", - "prettier": "~3.3.3" + "eslint-plugin-import": "~2.32.0", + "eslint-plugin-jest": "~29.15.0", + "eslint-plugin-jsx-a11y": "~6.10.2", + "eslint-plugin-prettier": "~5.5.5", + "eslint-plugin-react": "~7.37.5", + "eslint-plugin-react-hooks": "~7.0.1", + "eslint-plugin-storybook": "~10.2.12", + "eslint-plugin-testing-library": "~7.16.0", + "globals": "~17.3.0", + "prettier": "~3.3.3", + "typescript-eslint": "~8.56.1" }, "devDependencies": { "typescript": "~5.9.3" diff --git a/packages/eslint-config/react.js b/packages/eslint-config/react.js deleted file mode 100644 index a5b7d88652309..0000000000000 --- a/packages/eslint-config/react.js +++ /dev/null @@ -1,35 +0,0 @@ -/** @type {import('eslint').ESLint.ConfigData} */ -const config = { - plugins: ['react', 'react-hooks', 'jsx-a11y'], - extends: ['plugin:jsx-a11y/recommended'], - rules: { - 'react-hooks/exhaustive-deps': 'error', - 'react-hooks/rules-of-hooks': 'error', - 'react/display-name': 'error', - 'react/jsx-curly-brace-presence': 'error', - 'react/jsx-fragments': ['error', 'syntax'], - 'react/jsx-key': ['error', { checkFragmentShorthand: true, checkKeyMustBeforeSpread: true, warnOnDuplicates: true }], - 'react/jsx-no-undef': 'error', - 'react/jsx-uses-react': 'error', - 'react/jsx-uses-vars': 'error', - 'react/no-children-prop': 'error', - 'react/no-multi-comp': 'error', - 'jsx-a11y/no-autofocus': [2, { ignoreNonDOM: true }], - }, - settings: { - react: { - version: 'detect', - }, - }, - overrides: [ - { - files: ['**/*.stories.js', '**/*.stories.jsx', '**/*.stories.ts', '**/*.stories.tsx', '**/*.spec.tsx'], - rules: { - 'react/display-name': 'off', - 'react/no-multi-comp': 'off', - }, - }, - ], -}; - -module.exports = config; diff --git a/packages/eslint-config/standard/index.js b/packages/eslint-config/standard/index.js deleted file mode 100644 index 9fd44f875cb20..0000000000000 --- a/packages/eslint-config/standard/index.js +++ /dev/null @@ -1,173 +0,0 @@ -module.exports = { - extends: ['../original/index.js', 'plugin:prettier/recommended', 'plugin:import/typescript'], - parser: '@babel/eslint-parser', - parserOptions: { - requireConfigFile: false, - }, - settings: { - 'import/resolver': { - node: { - extensions: ['.js', '.ts', '.tsx', '.cts', '.mts'], - }, - }, - }, - rules: { - 'jsx-quotes': ['error', 'prefer-single'], - }, - overrides: [ - { - files: ['**/*.ts', '**/*.tsx', '**/*.cts', '**/*.mts'], - extends: [ - 'plugin:@typescript-eslint/recommended', - 'plugin:@typescript-eslint/eslint-recommended', - '../original/index.js', - 'plugin:prettier/recommended', - ], - parser: '@typescript-eslint/parser', - parserOptions: { - sourceType: 'module', - ecmaVersion: 2018, - warnOnUnsupportedTypeScriptVersion: false, - ecmaFeatures: { - experimentalObjectRestSpread: true, - legacyDecorators: true, - }, - }, - plugins: ['@typescript-eslint', 'anti-trojan-source'], - rules: { - '@typescript-eslint/ban-types': [ - 'warn', - { - types: { - 'FC': 'Useless and has some drawbacks, see https://adr.rocket.chat/0094', - 'React.FC': 'Useless and has some drawbacks, see https://adr.rocket.chat/0094', - 'VFC': 'Useless and has some drawbacks, see https://adr.rocket.chat/0094', - 'React.VFC': 'Useless and has some drawbacks, see https://adr.rocket.chat/0094', - 'FunctionComponent': 'Useless and has some drawbacks, see https://adr.rocket.chat/0094', - 'React.FunctionComponent': 'Useless and has some drawbacks, see https://adr.rocket.chat/0094', - }, - }, - ], - '@typescript-eslint/ban-ts-comment': 'warn', - '@typescript-eslint/consistent-type-imports': 'error', - '@typescript-eslint/naming-convention': [ - 'error', - { selector: 'variableLike', format: ['camelCase'], leadingUnderscore: 'allow' }, - { - selector: ['variable'], - format: ['camelCase', 'UPPER_CASE', 'PascalCase'], - leadingUnderscore: 'allowSingleOrDouble', - }, - { - selector: ['function'], - format: ['camelCase', 'PascalCase'], - leadingUnderscore: 'allowSingleOrDouble', - }, - { - selector: 'parameter', - format: null, - filter: { - regex: '^Story$', - match: true, - }, - }, - { - selector: 'parameter', - format: ['camelCase'], - modifiers: ['unused'], - leadingUnderscore: 'require', - }, - { - selector: 'interface', - format: ['PascalCase'], - custom: { - regex: '^I[A-Z]', - match: true, - }, - }, - ], - '@typescript-eslint/no-dupe-class-members': 'error', - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-redeclare': 'error', - '@typescript-eslint/no-unused-vars': [ - 'error', - { - argsIgnorePattern: '^_', - ignoreRestSiblings: true, - }, - ], - '@typescript-eslint/prefer-optional-chain': 'warn', - 'anti-trojan-source/no-bidi': 'error', - 'func-call-spacing': 'off', - 'indent': 'off', - 'jsx-quotes': ['error', 'prefer-single'], - 'no-dupe-class-members': 'off', - 'no-empty-function': 'off', - 'no-extra-parens': 'off', - 'no-redeclare': 'off', - 'no-spaced-func': 'off', - 'no-undef': 'off', - 'no-unused-vars': 'off', - 'no-use-before-define': 'off', - 'no-useless-constructor': 'off', - }, - env: { - browser: true, - commonjs: true, - es6: true, - node: true, - }, - settings: { - 'import/resolver': { - node: { - extensions: ['.js', '.ts', '.tsx', '.cts', '.mts'], - }, - typescript: {}, - }, - }, - }, - { - files: ['**/*.ts', '**/*.tsx'], - excludedFiles: [ - '**/*.d.ts', - '**/__tests__/**', - '**/*.spec.ts', - '**/*.spec.tsx', - '**/*.test.ts', - '**/*.test.tsx', - '**/tests/**', - '**/.storybook/**', - 'jest.config.ts', - 'jest.config.js', - 'jest.config.*.ts', - 'jest.config.*.js', - 'webpack.config.ts', - 'webpack.config.js', - 'vite.config.ts', - 'vite.config.js', - 'rollup.config.ts', - 'rollup.config.js', - ], - parserOptions: { - project: true, - }, - rules: { - '@typescript-eslint/no-misused-promises': [ - 'error', - { - checksVoidReturn: { - arguments: false, - }, - }, - ], - '@typescript-eslint/no-floating-promises': 'error', - }, - }, - { - files: ['**/*.d.ts'], - rules: { - '@typescript-eslint/naming-convention': 'off', - }, - }, - ], -}; diff --git a/packages/eslint-config/style/index.js b/packages/eslint-config/style/index.js deleted file mode 100644 index ca875683dbeb2..0000000000000 --- a/packages/eslint-config/style/index.js +++ /dev/null @@ -1,473 +0,0 @@ -module.exports = { - rules: { - // enforce line breaks after opening and before closing array brackets - // https://eslint.org/docs/rules/array-bracket-newline - // TODO: enable? semver-major - // 'array-bracket-newline': ['off', 'consistent'], // object option alternative: { multiline: true, minItems: 3 } - - // // enforce line breaks between array elements - // // https://eslint.org/docs/rules/array-element-newline - // // TODO: enable? semver-major - // 'array-element-newline': ['off', { multiline: true, minItems: 3 }], - - // enforce spacing inside array brackets - 'array-bracket-spacing': ['error', 'never'], - - // enforce spacing inside single-line blocks - // https://eslint.org/docs/rules/block-spacing - 'block-spacing': 'error', - - // enforce one true brace style - 'brace-style': ['error', '1tbs', { allowSingleLine: true }], - - // // require camel case names - // // TODO: semver-major (eslint 5): add ignoreDestructuring: false option - // camelcase: ['error', { properties: 'never' }], - - // // enforce or disallow capitalization of the first letter of a comment - // // https://eslint.org/docs/rules/capitalized-comments - // 'capitalized-comments': ['off', 'never', { - // line: { - // ignorePattern: '.*', - // ignoreInlineComments: true, - // ignoreConsecutiveComments: true, - // }, - // block: { - // ignorePattern: '.*', - // ignoreInlineComments: true, - // ignoreConsecutiveComments: true, - // }, - // }], - - // require trailing commas in multiline object literals - 'comma-dangle': ['error', 'always-multiline'], - - // enforce spacing before and after comma - 'comma-spacing': 'error', - - // enforce one true comma style - 'comma-style': 'error', - - // disallow padding inside computed properties - 'computed-property-spacing': ['error', 'never'], - - // // enforces consistent naming when capturing the current execution context - // 'consistent-this': 'off', - - // enforce newline at the end of file, with no multiple empty lines - 'eol-last': ['error', 'always'], - - // enforce spacing between functions and their invocations - // https://eslint.org/docs/rules/func-call-spacing - 'func-call-spacing': ['error', 'never'], - - // // requires function names to match the name of the variable or property to which they are - // // assigned - // // https://eslint.org/docs/rules/func-name-matching - // // TODO: semver-major (eslint 5): add considerPropertyDescriptor: true - // 'func-name-matching': ['off', 'always', { - // includeCommonJSModuleExports: false - // }], - - // // require function expressions to have a name - // // https://eslint.org/docs/rules/func-names - // 'func-names': 'warn', - - // // enforces use of function declarations or expressions - // // https://eslint.org/docs/rules/func-style - // // TODO: enable - // 'func-style': ['off', 'expression'], - - // // enforce consistent line breaks inside function parentheses - // // https://eslint.org/docs/rules/function-paren-newline - // 'function-paren-newline': ['error', 'consistent'], - - // // Blacklist certain identifiers to prevent them being used - // // https://eslint.org/docs/rules/id-blacklist - // 'id-blacklist': 'off', - - // // this option enforces minimum and maximum identifier lengths - // // (variable names, property names etc.) - // 'id-length': 'off', - - // // require identifiers to match the provided regular expression - // 'id-match': 'off', - - // Enforce the location of arrow function bodies with implicit returns - // https://eslint.org/docs/rules/implicit-arrow-linebreak - // 'implicit-arrow-linebreak': ['error', 'beside'], - - // this option sets a specific tab width for your code - // https://eslint.org/docs/rules/indent - 'indent': ['error', 'tab', { SwitchCase: 1 }], - - // // specify whether double or single quotes should be used in JSX attributes - // // https://eslint.org/docs/rules/jsx-quotes - // 'jsx-quotes': ['off', 'prefer-double'], - - // enforces spacing between keys and values in object literal properties - 'key-spacing': ['error', { beforeColon: false, afterColon: true }], - - // require a space before & after certain keywords - 'keyword-spacing': 'error', - - // // enforce position of line comments - // // https://eslint.org/docs/rules/line-comment-position - // // TODO: enable? - // 'line-comment-position': ['off', { - // position: 'above', - // ignorePattern: '', - // applyDefaultPatterns: true, - // }], - - // disallow mixed 'LF' and 'CRLF' as linebreaks - // https://eslint.org/docs/rules/linebreak-style - 'linebreak-style': ['error', 'unix'], - - // require or disallow an empty line between class members - // https://eslint.org/docs/rules/lines-between-class-members - 'lines-between-class-members': ['error', 'always', { exceptAfterSingleLine: false }], - - // // enforces empty lines around comments - // 'lines-around-comment': 'off', - - // require or disallow newlines around directives - // https://eslint.org/docs/rules/lines-around-directive - 'lines-around-directive': [ - 'error', - { - before: 'always', - after: 'always', - }, - ], - - // specify the maximum depth that blocks can be nested - 'max-depth': ['off', 4], - - // // specify the maximum length of a line in your program - // // https://eslint.org/docs/rules/max-len - // 'max-len': ['error', 100, 2, { - // ignoreUrls: true, - // ignoreComments: false, - // ignoreRegExpLiterals: true, - // ignoreStrings: true, - // ignoreTemplateLiterals: true, - // }], - - // // specify the max number of lines in a file - // // https://eslint.org/docs/rules/max-lines - // 'max-lines': ['off', { - // max: 300, - // skipBlankLines: true, - // skipComments: true - // }], - - // // enforce a maximum function length - // // https://eslint.org/docs/rules/max-lines-per-function - // 'max-lines-per-function': ['off', { - // max: 50, - // skipBlankLines: true, - // skipComments: true, - // IIFEs: true, - // }], - - // // specify the maximum depth callbacks can be nested - // 'max-nested-callbacks': 'off', - - // // limits the number of parameters that can be used in the function declaration. - // 'max-params': ['off', 3], - - // // specify the maximum number of statement allowed in a function - // 'max-statements': ['off', 10], - - // // restrict the number of statements per line - // // https://eslint.org/docs/rules/max-statements-per-line - // 'max-statements-per-line': ['off', { max: 1 }], - - // // enforce a particular style for multiline comments - // // https://eslint.org/docs/rules/multiline-comment-style - // 'multiline-comment-style': ['off', 'starred-block'], - - // // require multiline ternary - // // https://eslint.org/docs/rules/multiline-ternary - // // TODO: enable? - // 'multiline-ternary': ['off', 'never'], - - // require a capital letter for constructors - 'new-cap': [ - 'error', - { - capIsNewExceptions: [ - 'Match.Optional', - 'Match.Maybe', - 'Match.OneOf', - 'Match.Where', - 'Match.ObjectIncluding', - 'Push.Configure', - 'SHA256', - ], - }, - ], - - // disallow the omission of parentheses when invoking a constructor with no arguments - // https://eslint.org/docs/rules/new-parens - 'new-parens': 'error', - - // // allow/disallow an empty newline after var statement - // 'newline-after-var': 'off', - - // // https://eslint.org/docs/rules/newline-before-return - // 'newline-before-return': 'off', - - // // enforces new line after each method call in the chain to make it - // // more readable and easy to maintain - // // https://eslint.org/docs/rules/newline-per-chained-call - // 'newline-per-chained-call': ['error', { ignoreChainWithDepth: 4 }], - - // disallow use of the Array constructor - 'no-array-constructor': 'error', - - // // disallow use of bitwise operators - // // https://eslint.org/docs/rules/no-bitwise - // 'no-bitwise': 'error', - - // // disallow use of the continue statement - // // https://eslint.org/docs/rules/no-continue - // 'no-continue': 'error', - - // // disallow comments inline after code - // 'no-inline-comments': 'off', - - // disallow if as the only statement in an else block - // https://eslint.org/docs/rules/no-lonely-if - 'no-lonely-if': 'error', - - // disallow un-paren'd mixes of different operators - // https://eslint.org/docs/rules/no-mixed-operators - 'no-mixed-operators': [ - 'error', - { - // the list of arthmetic groups disallows mixing `%` and `**` - // with other arithmetic operators. - groups: [ - ['%', '**'], - ['%', '+'], - ['%', '-'], - ['%', '*'], - ['%', '/'], - ['**', '+'], - ['**', '-'], - ['**', '*'], - ['**', '/'], - ['&', '|', '^', '~', '<<', '>>', '>>>'], - ['==', '!=', '===', '!==', '>', '>=', '<', '<='], - ['&&', '||'], - ['in', 'instanceof'], - ], - allowSamePrecedence: false, - }, - ], - - // disallow mixed spaces and tabs for indentation - 'no-mixed-spaces-and-tabs': 'error', - - // disallow use of chained assignment expressions - // https://eslint.org/docs/rules/no-multi-assign - 'no-multi-assign': ['error'], - - // disallow multiple empty lines and only one newline at the end - 'no-multiple-empty-lines': ['error', { max: 2, maxEOF: 0 }], - - // // disallow negated conditions - // // https://eslint.org/docs/rules/no-negated-condition - // 'no-negated-condition': 'off', - - // disallow nested ternary expressions - 'no-nested-ternary': 'error', - - // // disallow use of the Object constructor - // 'no-new-object': 'error', - - // // disallow use of unary operators, ++ and -- - // // https://eslint.org/docs/rules/no-plusplus - // 'no-plusplus': 'error', - - // // disallow certain syntax forms - // // https://eslint.org/docs/rules/no-restricted-syntax - // 'no-restricted-syntax': [ - // 'error', - // { - // selector: 'ForInStatement', - // message: 'for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array.', - // }, - // { - // selector: 'ForOfStatement', - // message: 'iterators/generators require regenerator-runtime, which is too heavyweight for this guide to allow them. Separately, loops should be avoided in favor of array iterations.', - // }, - // { - // selector: 'LabeledStatement', - // message: 'Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand.', - // }, - // { - // selector: 'WithStatement', - // message: '`with` is disallowed in strict mode because it makes code impossible to predict and optimize.', - // }, - // ], - - // disallow space between function identifier and application - 'no-spaced-func': 'error', - - // // disallow tab characters entirely - // 'no-tabs': 'error', - - // // disallow the use of ternary operators - // 'no-ternary': 'off', - - // disallow trailing whitespace at the end of lines - 'no-trailing-spaces': [ - 'error', - { - skipBlankLines: false, - ignoreComments: false, - }, - ], - - // // disallow dangling underscores in identifiers - // // https://eslint.org/docs/rules/no-underscore-dangle - // 'no-underscore-dangle': ['error', { - // allow: [], - // allowAfterThis: false, - // allowAfterSuper: false, - // enforceInMethodNames: true, - // }], - - // disallow the use of Boolean literals in conditional expressions - // also, prefer `a || b` over `a ? a : b` - // https://eslint.org/docs/rules/no-unneeded-ternary - 'no-unneeded-ternary': ['error', { defaultAssignment: false }], - - // disallow whitespace before properties - // https://eslint.org/docs/rules/no-whitespace-before-property - 'no-whitespace-before-property': 'error', - - // // enforce the location of single-line statements - // // https://eslint.org/docs/rules/nonblock-statement-body-position - // 'nonblock-statement-body-position': ['error', 'beside', { overrides: {} }], - - // require padding inside curly braces - 'object-curly-spacing': ['error', 'always'], - - // // enforce line breaks between braces - // // https://eslint.org/docs/rules/object-curly-newline - // 'object-curly-newline': ['error', { - // ObjectExpression: { minProperties: 4, multiline: true, consistent: true }, - // ObjectPattern: { minProperties: 4, multiline: true, consistent: true }, - // ImportDeclaration: { minProperties: 4, multiline: true, consistent: true }, - // ExportDeclaration: { minProperties: 4, multiline: true, consistent: true }, - // }], - - // enforce "same line" or "multiple line" on object properties. - // https://eslint.org/docs/rules/object-property-newline - 'object-property-newline': [ - 'error', - { - allowAllPropertiesOnSameLine: true, - }, - ], - - // allow just one var statement per function - 'one-var': ['error', 'never'], - - // // require a newline around variable declaration - // // https://eslint.org/docs/rules/one-var-declaration-per-line - // 'one-var-declaration-per-line': ['error', 'always'], - - // require assignment operator shorthand where possible or prohibit it entirely - // https://eslint.org/docs/rules/operator-assignment - 'operator-assignment': ['error', 'always'], - - // Requires operator at the beginning of the line in multiline statements - // https://eslint.org/docs/rules/operator-linebreak - 'operator-linebreak': ['error', 'before', { overrides: { '=': 'none' } }], - - // disallow padding within blocks - 'padded-blocks': ['error', { blocks: 'never', classes: 'never', switches: 'never' }], - - // // Require or disallow padding lines between statements - // // https://eslint.org/docs/rules/padding-line-between-statements - // 'padding-line-between-statements': 'off', - - // Prefer use of an object spread over Object.assign - // https://eslint.org/docs/rules/prefer-object-spread - // TODO: semver-major (eslint 5): enable - 'prefer-object-spread': 'off', - - // require quotes around object literal property names - // https://eslint.org/docs/rules/quote-props.html - 'quote-props': ['error', 'as-needed', { keywords: false, unnecessary: true, numbers: false }], - - // specify whether double or single quotes should be used - 'quotes': ['error', 'single', { avoidEscape: true }], - - // // do not require jsdoc - // // https://eslint.org/docs/rules/require-jsdoc - // 'require-jsdoc': 'off', - - // require or disallow use of semicolons instead of ASI - 'semi': ['error', 'always'], - - // enforce spacing before and after semicolons - 'semi-spacing': ['error', { before: false, after: true }], - - // Enforce location of semicolons - // https://eslint.org/docs/rules/semi-style - 'semi-style': ['error', 'last'], - - // // requires object keys to be sorted - // 'sort-keys': ['off', 'asc', { caseSensitive: false, natural: true }], - - // // sort variables within the same declaration block - // 'sort-vars': 'off', - - // require or disallow space before blocks - 'space-before-blocks': ['error', 'always'], - - // require or disallow space before function opening parenthesis - // https://eslint.org/docs/rules/space-before-function-paren - 'space-before-function-paren': ['error', { anonymous: 'never', named: 'never', asyncArrow: 'always' }], - - // require or disallow spaces inside parentheses - 'space-in-parens': ['error', 'never'], - - // require spaces around operators - 'space-infix-ops': 'error', - - // // Require or disallow spaces before/after unary operators - // // https://eslint.org/docs/rules/space-unary-ops - // 'space-unary-ops': ['error', { - // words: true, - // nonwords: false, - // overrides: { - // }, - // }], - - // require or disallow a space immediately following the // or /* in a comment - // https://eslint.org/docs/rules/spaced-comment - 'spaced-comment': 'error', - - // Enforce spacing around colons of switch statements - // https://eslint.org/docs/rules/switch-colon-spacing - 'switch-colon-spacing': ['error', { after: true, before: false }], - - // // Require or disallow spacing between template tags and their literals - // // https://eslint.org/docs/rules/template-tag-spacing - // 'template-tag-spacing': ['error', 'never'], - - // // require or disallow the Unicode Byte Order Mark - // // https://eslint.org/docs/rules/unicode-bom - // 'unicode-bom': ['error', 'never'], - - // // require regex literals to be wrapped in parentheses - // 'wrap-regex': 'off' - }, -}; diff --git a/packages/eslint-config/variables/index.js b/packages/eslint-config/variables/index.js deleted file mode 100644 index a6b1e24f88321..0000000000000 --- a/packages/eslint-config/variables/index.js +++ /dev/null @@ -1,51 +0,0 @@ -// const restrictedGlobals = require('eslint-restricted-globals'); - -module.exports = { - rules: { - // enforce or disallow variable initializations at definition - // 'init-declarations': 'off', - - // // disallow the catch clause parameter name being the same as a variable in the outer scope - // 'no-catch-shadow': 'off', - - // disallow deletion of variables - 'no-delete-var': 'error', - - // // disallow labels that share a name with a variable - // // https://eslint.org/docs/rules/no-label-var - // 'no-label-var': 'error', - - // // disallow specific globals - // 'no-restricted-globals': ['error', 'isFinite', 'isNaN'].concat(restrictedGlobals), - - // // disallow declaration of variables already declared in the outer scope - // 'no-shadow': 'error', - - // // disallow shadowing of names such as arguments - // 'no-shadow-restricted-names': 'error', - - // disallow use of undeclared variables unless mentioned in a /*global */ block - 'no-undef': 'error', - - // // disallow use of undefined when initializing variables - // 'no-undef-init': 'error', - - // // disallow use of undefined variable - // // https://eslint.org/docs/rules/no-undefined - // // TODO: enable? - // 'no-undefined': 'off', - - // disallow declaration of variables that are not used in the code - 'no-unused-vars': [ - 'error', - { - vars: 'all', - args: 'after-used', - ignoreRestSiblings: true, - }, - ], - - // disallow use of variables before they are defined - 'no-use-before-define': ['error', { functions: true, classes: true, variables: true }], - }, -}; diff --git a/packages/favicon/.eslintrc.json b/packages/favicon/.eslintrc.json deleted file mode 100644 index c5735e3561386..0000000000000 --- a/packages/favicon/.eslintrc.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "ignorePatterns": ["dist"], - "rules": { - "@typescript-eslint/explicit-function-return-type": "off" - } -} diff --git a/packages/favicon/package.json b/packages/favicon/package.json index a210db686a298..47a3e8dabe421 100644 --- a/packages/favicon/package.json +++ b/packages/favicon/package.json @@ -10,11 +10,11 @@ "scripts": { "build": "rm -rf dist && tsc -p tsconfig.json", "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput", - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix" + "lint": "eslint .", + "lint:fix": "eslint --fix ." }, "devDependencies": { - "eslint": "~8.45.0", + "eslint": "~9.39.3", "typescript": "~5.9.3" }, "volta": { diff --git a/packages/fuselage-ui-kit/.eslintrc.json b/packages/fuselage-ui-kit/.eslintrc.json deleted file mode 100644 index 96574c6de8d41..0000000000000 --- a/packages/fuselage-ui-kit/.eslintrc.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config", "@rocket.chat/eslint-config/react", "plugin:storybook/recommended"], - "ignorePatterns": ["dist", "storybook-static", "!.storybook"], - "overrides": [ - { - "files": ["**/*.ts", "**/*.tsx"], - "rules": { - "@typescript-eslint/no-misused-promises": "off" - } - } - ] -} diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index 63f543d04baef..8e93d55562fcb 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -31,7 +31,7 @@ "build-storybook": "NODE_ENV=production storybook build", "dev": "tsc --watch --preserveWatchOutput -p tsconfig.build.json", "docs": "cross-env NODE_ENV=production storybook build -o ../../static/fuselage-ui-kit", - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", + "lint": "eslint .", "storybook": "storybook dev -p 6006 --no-version-updates", "test": "jest", "testunit": "jest", @@ -46,7 +46,6 @@ "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/emitter": "^0.32.0", - "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/fuselage": "^0.71.0", "@rocket.chat/fuselage-hooks": "^0.39.0", "@rocket.chat/fuselage-tokens": "~0.33.2", @@ -73,8 +72,7 @@ "@types/react": "~18.3.27", "@types/react-dom": "~18.3.7", "cross-env": "^7.0.3", - "eslint": "~8.45.0", - "eslint-plugin-storybook": "~0.11.6", + "eslint": "~9.39.3", "i18next": "~23.4.9", "jest": "~30.2.0", "normalize.css": "^8.0.1", @@ -92,7 +90,6 @@ }, "peerDependencies": { "@rocket.chat/apps-engine": "workspace:^", - "@rocket.chat/eslint-config": "0.7.0", "@rocket.chat/fuselage": "*", "@rocket.chat/fuselage-hooks": "*", "@rocket.chat/icons": "*", diff --git a/packages/gazzodown/.eslintrc.json b/packages/gazzodown/.eslintrc.json deleted file mode 100644 index 6e0406abd46a7..0000000000000 --- a/packages/gazzodown/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config", "@rocket.chat/eslint-config/react", "plugin:storybook/recommended"], - "ignorePatterns": ["dist", "storybook-static", "!.storybook"] -} diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index 59f9b871c240f..8b526b0831ba5 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -13,8 +13,8 @@ "build-preview": "storybook build --quiet", "build-storybook": "storybook build", "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput", - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "lint": "eslint .", + "lint:fix": "eslint --fix .", "storybook": "storybook dev -p 6006", "test": "jest", "testunit": "jest", @@ -56,13 +56,7 @@ "@types/katex": "~0.16.8", "@types/react": "~18.3.27", "@types/react-dom": "~18.3.7", - "@typescript-eslint/eslint-plugin": "~5.60.1", - "@typescript-eslint/parser": "~5.60.1", - "eslint": "~8.45.0", - "eslint-plugin-anti-trojan-source": "~1.1.2", - "eslint-plugin-react": "~7.37.5", - "eslint-plugin-react-hooks": "~5.0.0", - "eslint-plugin-storybook": "~0.11.6", + "eslint": "~9.39.3", "i18next": "~23.4.9", "identity-obj-proxy": "^3.0.0", "jest": "~30.2.0", diff --git a/packages/http-router/.eslintrc.json b/packages/http-router/.eslintrc.json deleted file mode 100644 index 9ec331f09b5e3..0000000000000 --- a/packages/http-router/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "ignorePatterns": ["dist"] -} diff --git a/packages/http-router/package.json b/packages/http-router/package.json index 9a83dfbcbcded..c7ebf1f164176 100644 --- a/packages/http-router/package.json +++ b/packages/http-router/package.json @@ -10,8 +10,8 @@ "scripts": { "build": "rm -rf dist && tsc", "dev": "tsc --watch --preserveWatchOutput", - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "lint": "eslint .", + "lint:fix": "eslint --fix .", "test": "jest", "testunit": "jest", "typecheck": "tsc --noEmit" @@ -26,13 +26,12 @@ "qs": "^6.14.1" }, "devDependencies": { - "@rocket.chat/eslint-config": "workspace:~", "@rocket.chat/jest-presets": "workspace:^", "@rocket.chat/tsconfig": "workspace:*", "@types/express": "^4.17.25", "@types/jest": "~30.0.0", "@types/supertest": "~6.0.3", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "jest": "~30.2.0", "supertest": "~7.1.4", "ts-jest": "~29.4.6", diff --git a/packages/i18n/.eslintrc.json b/packages/i18n/.eslintrc.json deleted file mode 100644 index f27fe59dc9222..0000000000000 --- a/packages/i18n/.eslintrc.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "plugins": ["jest"], - "env": { - "jest/globals": true - }, - "ignorePatterns": ["dist"] -} diff --git a/packages/i18n/package.json b/packages/i18n/package.json index db74e1a400a6d..481040a80d538 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -26,8 +26,8 @@ "scripts": { "build": "rm -rf dist && tsc -p tsconfig.build.json && node --experimental-transform-types ./src/scripts/build.mts", "check": "node --experimental-transform-types ./src/scripts/check.mts", - "lint": "eslint . && node --experimental-transform-types ./src/scripts/check.mts", - "lint:fix": "eslint . --fix && node --experimental-transform-types ./src/scripts/check.mts --fix", + "lint": "eslint && node --experimental-transform-types ./src/scripts/check.mts", + "lint:fix": "eslint --fix && node --experimental-transform-types ./src/scripts/check.mts --fix", "replace-sprintf": "node --experimental-transform-types ./src/scripts/replace-sprintf.mts", "test": "jest", "testunit": "jest" @@ -36,7 +36,7 @@ "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/tools": "workspace:~", "@rocket.chat/tsconfig": "workspace:*", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "i18next": "~23.4.9", "jest": "~30.2.0", "typescript": "~5.9.3" diff --git a/packages/instance-status/.eslintrc.json b/packages/instance-status/.eslintrc.json deleted file mode 100644 index c36ef5941c077..0000000000000 --- a/packages/instance-status/.eslintrc.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "overrides": [ - { - "files": ["**/*.spec.js", "**/*.spec.jsx"], - "env": { - "jest": true - } - } - ], - "ignorePatterns": ["dist"] -} diff --git a/packages/instance-status/package.json b/packages/instance-status/package.json index 515dd6de6f4b8..ed726c27fd98f 100644 --- a/packages/instance-status/package.json +++ b/packages/instance-status/package.json @@ -10,8 +10,8 @@ "scripts": { "build": "rm -rf dist && tsc -p tsconfig.json", "dev": "tsc --watch --preserveWatchOutput -p tsconfig.json", - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "lint": "eslint .", + "lint:fix": "eslint --fix .", "test": "echo \"Error: no test specified\" && exit 1" }, "dependencies": { @@ -19,9 +19,8 @@ "@rocket.chat/tracing": "workspace:^" }, "devDependencies": { - "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/tsconfig": "workspace:*", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "mongodb": "6.16.0", "prettier": "~3.3.3", "typescript": "~5.9.3" diff --git a/packages/jest-presets/.eslintrc.json b/packages/jest-presets/.eslintrc.json deleted file mode 100644 index 7250665ca644a..0000000000000 --- a/packages/jest-presets/.eslintrc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/eslintrc", - "extends": ["@rocket.chat/eslint-config"], - "ignorePatterns": ["/dist/**", "/client/**", "/server/**"] -} diff --git a/packages/jest-presets/package.json b/packages/jest-presets/package.json index 55c6d3282cc1c..9b5c00394f06f 100644 --- a/packages/jest-presets/package.json +++ b/packages/jest-presets/package.json @@ -38,11 +38,10 @@ "uuid": "~11.0.5" }, "devDependencies": { - "@rocket.chat/eslint-config": "workspace:~", "@types/identity-obj-proxy": "^3.0.2", "@types/jest": "~30.0.0", "@types/uuid": "^10.0.0", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "jest": "~30.2.0", "typescript": "~5.9.3" }, diff --git a/packages/jwt/.eslintrc.json b/packages/jwt/.eslintrc.json deleted file mode 100644 index 9ec331f09b5e3..0000000000000 --- a/packages/jwt/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "ignorePatterns": ["dist"] -} diff --git a/packages/jwt/package.json b/packages/jwt/package.json index 49bb98ce0ac7c..9dc5f11803b44 100644 --- a/packages/jwt/package.json +++ b/packages/jwt/package.json @@ -10,8 +10,8 @@ "scripts": { "build": "rm -rf dist && tsc", "dev": "tsc --watch --preserveWatchOutput", - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "lint": "eslint .", + "lint:fix": "eslint --fix .", "test": "jest", "testunit": "jest", "typecheck": "tsc --noEmit" @@ -23,7 +23,7 @@ "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/tsconfig": "workspace:*", "@types/jest": "~30.0.0", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "jest": "~30.2.0", "typescript": "~5.9.3" }, diff --git a/packages/livechat/.eslintrc.json b/packages/livechat/.eslintrc.json deleted file mode 100644 index 3dd68c20eec6c..0000000000000 --- a/packages/livechat/.eslintrc.json +++ /dev/null @@ -1,104 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config", "prettier", "plugin:storybook/recommended"], - "plugins": ["react", "react-hooks", "prettier"], - "env": { - "jest": true - }, - "rules": { - "import/order": [ - "error", - { - "newlines-between": "always", - "groups": ["builtin", "external", "internal", ["parent", "sibling", "index"]], - "alphabetize": { - "order": "asc" - } - } - ], - "jsx-quotes": ["error", "prefer-single"], - "react/display-name": [ - "warn", - { - "ignoreTranspilerName": false - } - ], - "react/jsx-fragments": ["error", "syntax"], - "react/jsx-no-bind": [ - "warn", - { - "ignoreRefs": true, - "allowFunctions": true, - "allowArrowFunctions": true - } - ], - "react/jsx-no-comment-textnodes": "error", - "react/jsx-no-duplicate-props": "error", - "react/jsx-no-target-blank": "error", - "react/jsx-no-undef": "error", - "react/jsx-tag-spacing": [ - "error", - { - "beforeSelfClosing": "always" - } - ], - "react/jsx-uses-react": "error", - "react/jsx-uses-vars": "error", - "react/no-children-prop": "error", - "react/no-danger": "warn", - "react/no-deprecated": "error", - "react/no-did-mount-set-state": "error", - "react/no-did-update-set-state": "error", - "react/no-find-dom-node": "error", - "react/no-is-mounted": "error", - "react/no-string-refs": "error", - "react/prefer-es6-class": "error", - "react/prefer-stateless-function": "warn", - "react/require-render-return": "error", - "react/self-closing-comp": "error", - "react-hooks/rules-of-hooks": "error", - "react-hooks/exhaustive-deps": "warn", - "no-sequences": "off", - "no-extra-parens": "off" - }, - "settings": { - "import/resolver": { - "node": { - "extensions": [".js", ".ts", ".tsx"] - } - }, - "react": { - "pragma": "h", - "pragmaFrag": "Fragment", - "version": "detect" - } - }, - "ignorePatterns": ["dist", "build", "!.storybook"], - "overrides": [ - { - "files": ["*.ts", "*.tsx"], - "rules": { - "@typescript-eslint/naming-convention": [ - "error", - { "selector": "variableLike", "format": ["camelCase"], "leadingUnderscore": "allow" }, - { - "selector": ["variable"], - "format": ["camelCase", "UPPER_CASE", "PascalCase"], - "leadingUnderscore": "allowSingleOrDouble" - }, - { - "selector": ["function"], - "format": ["camelCase", "PascalCase"], - "leadingUnderscore": "allowSingleOrDouble" - }, - { - "selector": "parameter", - "format": ["camelCase"], - "modifiers": ["unused"], - "leadingUnderscore": "require" - } - ], - "@typescript-eslint/no-misused-promises": "off" - } - } - ] -} diff --git a/packages/livechat/package.json b/packages/livechat/package.json index 2fb4ad1c092fc..ec8ef8a9b60db 100644 --- a/packages/livechat/package.json +++ b/packages/livechat/package.json @@ -16,7 +16,7 @@ "build-storybook": "storybook build", "clean": "rimraf dist", "dev": "yarn build", - "eslint": "eslint --ext .js,.jsx,.ts,.tsx .", + "eslint": "eslint .", "lint": "yarn run eslint && yarn run stylelint", "start": "cross-env TS_NODE_PROJECT=\"tsconfig.webpack.json\" webpack-dev-server --mode development", "storybook": "storybook dev -p 9001 -c .storybook", @@ -53,13 +53,11 @@ }, "devDependencies": { "@babel/core": "~7.28.6", - "@babel/eslint-parser": "~7.28.6", "@babel/preset-env": "~7.28.6", "@babel/preset-react": "~7.27.1", "@babel/preset-typescript": "~7.27.1", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/ddp-client": "workspace:^", - "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/fuselage-hooks": "^0.39.0", "@rocket.chat/fuselage-tokens": "~0.33.2", "@rocket.chat/logo": "^0.32.4", @@ -75,19 +73,13 @@ "@types/react": "~18.3.27", "@types/webpack-env": "~1.18.8", "@types/whatwg-fetch": "~0.0.33", - "@typescript-eslint/eslint-plugin": "~5.60.1", - "@typescript-eslint/parser": "~5.60.1", "autoprefixer": "^9.8.8", "babel-loader": "~10.0.0", "cross-env": "^7.0.3", "css-loader": "^4.3.0", "cssnano": "^7.0.7", "desvg-loader": "^0.1.0", - "eslint": "~8.45.0", - "eslint-plugin-import": "~2.31.0", - "eslint-plugin-react": "~7.37.5", - "eslint-plugin-react-hooks": "~5.0.0", - "eslint-plugin-storybook": "~0.11.6", + "eslint": "~9.39.3", "file-loader": "^6.2.0", "html-webpack-plugin": "~5.6.6", "if-env": "^1.0.4", diff --git a/packages/log-format/.eslintrc.json b/packages/log-format/.eslintrc.json deleted file mode 100644 index 9ec331f09b5e3..0000000000000 --- a/packages/log-format/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "ignorePatterns": ["dist"] -} diff --git a/packages/log-format/package.json b/packages/log-format/package.json index d54f35d51f85f..afb43d3c03d49 100644 --- a/packages/log-format/package.json +++ b/packages/log-format/package.json @@ -10,8 +10,8 @@ "scripts": { "build": "rm -rf dist && tsc -p tsconfig.json", "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput", - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix" + "lint": "eslint .", + "lint:fix": "eslint --fix ." }, "dependencies": { "chalk": "^4.1.2", @@ -20,7 +20,7 @@ "devDependencies": { "@types/chalk": "^2.2.4", "@types/ejson": "^2.2.2", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "typescript": "~5.9.3" }, "volta": { diff --git a/packages/logger/.eslintrc.json b/packages/logger/.eslintrc.json deleted file mode 100644 index 9ec331f09b5e3..0000000000000 --- a/packages/logger/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "ignorePatterns": ["dist"] -} diff --git a/packages/logger/package.json b/packages/logger/package.json index f3badf5003616..9e5e10396edab 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -10,15 +10,15 @@ "scripts": { "build": "rm -rf dist && tsc -p tsconfig.json", "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput", - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix" + "lint": "eslint .", + "lint:fix": "eslint --fix ." }, "dependencies": { "@rocket.chat/emitter": "^0.32.0", "pino": "^8.21.0" }, "devDependencies": { - "eslint": "~8.45.0", + "eslint": "~9.39.3", "typescript": "~5.9.3" }, "volta": { diff --git a/packages/media-signaling/.eslintrc.json b/packages/media-signaling/.eslintrc.json deleted file mode 100644 index 9ec331f09b5e3..0000000000000 --- a/packages/media-signaling/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "ignorePatterns": ["dist"] -} diff --git a/packages/media-signaling/package.json b/packages/media-signaling/package.json index a6f3b5ce88fa8..93cc0d1c56446 100644 --- a/packages/media-signaling/package.json +++ b/packages/media-signaling/package.json @@ -11,8 +11,8 @@ "build": "rm -rf dist && tsc -p tsconfig.json", "build-preview": "mkdir -p ../../.preview && cp -r ./dist ../../.preview/media-signaling", "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput", - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "lint": "eslint .", + "lint:fix": "eslint --fix .", "test": "jest" }, "dependencies": { @@ -23,7 +23,7 @@ "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/tsconfig": "workspace:*", "@types/jest": "~30.0.0", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "jest": "~30.2.0", "typescript": "~5.9.3" } diff --git a/packages/message-parser/.eslintrc.json b/packages/message-parser/.eslintrc.json deleted file mode 100644 index 0ae720da7f0f4..0000000000000 --- a/packages/message-parser/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "@rocket.chat/eslint-config", - "ignorePatterns": ["dist"] -} diff --git a/packages/message-parser/package.json b/packages/message-parser/package.json index b146fb9b80698..680874f3ff7b4 100644 --- a/packages/message-parser/package.json +++ b/packages/message-parser/package.json @@ -46,14 +46,12 @@ "tldts": "~6.1.86" }, "devDependencies": { - "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/peggy-loader": "workspace:~", "@rocket.chat/prettier-config": "~0.31.25", "@types/jest": "~30.0.0", "@types/node": "~22.16.5", - "@typescript-eslint/parser": "~5.60.1", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "jest": "~30.2.0", "npm-run-all": "^4.1.5", "peggy": "4.1.1", diff --git a/packages/message-types/.eslintrc.json b/packages/message-types/.eslintrc.json deleted file mode 100644 index 9a131836901c4..0000000000000 --- a/packages/message-types/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config", "@rocket.chat/eslint-config/react"], - "ignorePatterns": ["dist"] -} diff --git a/packages/message-types/package.json b/packages/message-types/package.json index 7bb4f2e7e19a5..13def43629c9a 100644 --- a/packages/message-types/package.json +++ b/packages/message-types/package.json @@ -15,9 +15,8 @@ }, "devDependencies": { "@rocket.chat/core-typings": "workspace:~", - "@rocket.chat/eslint-config": "workspace:~", "date-fns": "~4.1.0", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "i18next": "~23.4.9", "jest": "~30.2.0", "moment": "^2.30.1", diff --git a/packages/mock-providers/.eslintrc.json b/packages/mock-providers/.eslintrc.json deleted file mode 100644 index 9a131836901c4..0000000000000 --- a/packages/mock-providers/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config", "@rocket.chat/eslint-config/react"], - "ignorePatterns": ["dist"] -} diff --git a/packages/mock-providers/package.json b/packages/mock-providers/package.json index 0ca1e77c1953f..fd84e7cd7bb7a 100644 --- a/packages/mock-providers/package.json +++ b/packages/mock-providers/package.json @@ -10,8 +10,8 @@ "scripts": { "build": "rm -rf dist && tsc -p tsconfig.json", "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput", - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix" + "lint": "eslint .", + "lint:fix": "eslint --fix ." }, "dependencies": { "@rocket.chat/emitter": "^0.32.0", @@ -33,7 +33,7 @@ "@testing-library/react": "~16.3.2", "@types/react": "~18.3.27", "@types/react-dom": "~18.3.7", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "jest": "~30.2.0", "react": "~18.3.1", "react-dom": "~18.3.1", diff --git a/packages/model-typings/.eslintrc.json b/packages/model-typings/.eslintrc.json deleted file mode 100644 index 9ec331f09b5e3..0000000000000 --- a/packages/model-typings/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "ignorePatterns": ["dist"] -} diff --git a/packages/model-typings/package.json b/packages/model-typings/package.json index 030d73117c92c..548eb2bbb8b03 100644 --- a/packages/model-typings/package.json +++ b/packages/model-typings/package.json @@ -9,15 +9,15 @@ ], "scripts": { "build": "rm -rf dist && tsc -p tsconfig.json", - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix" + "lint": "eslint .", + "lint:fix": "eslint --fix ." }, "dependencies": { "@rocket.chat/core-typings": "workspace:^" }, "devDependencies": { "@types/node-rsa": "^1.1.4", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "mongodb": "6.16.0", "typescript": "~5.9.3" }, diff --git a/packages/models/.eslintrc.json b/packages/models/.eslintrc.json deleted file mode 100644 index 9ec331f09b5e3..0000000000000 --- a/packages/models/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "ignorePatterns": ["dist"] -} diff --git a/packages/models/package.json b/packages/models/package.json index 6ed2cc1e45c52..074ca526b8e52 100644 --- a/packages/models/package.json +++ b/packages/models/package.json @@ -10,8 +10,8 @@ "scripts": { "build": "rm -rf dist && tsc", "dev": "tsc --watch --preserveWatchOutput", - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "lint": "eslint .", + "lint:fix": "eslint --fix .", "test": "jest", "testunit": "jest", "typecheck": "tsc --noEmit", @@ -32,7 +32,7 @@ "@rocket.chat/tsconfig": "workspace:*", "@types/jest": "~30.0.0", "@types/node-rsa": "^1.1.4", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "jest": "~30.2.0", "typescript": "~5.9.3" }, diff --git a/packages/mongo-adapter/.eslintrc.json b/packages/mongo-adapter/.eslintrc.json deleted file mode 100644 index 9ec331f09b5e3..0000000000000 --- a/packages/mongo-adapter/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "ignorePatterns": ["dist"] -} diff --git a/packages/mongo-adapter/package.json b/packages/mongo-adapter/package.json index e8ee09a9101ce..151220525c203 100644 --- a/packages/mongo-adapter/package.json +++ b/packages/mongo-adapter/package.json @@ -10,14 +10,14 @@ "scripts": { "build": "rm -rf dist && tsc -p tsconfig.json", "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput", - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "lint": "eslint .", + "lint:fix": "eslint --fix .", "test": "jest", "testunit": "jest" }, "devDependencies": { "@rocket.chat/jest-presets": "workspace:~", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "jest": "~30.2.0", "mongodb": "6.16.0", "typescript": "~5.9.3" diff --git a/packages/node-poplib/package.json b/packages/node-poplib/package.json index 0229da7444d44..57853a798ec94 100644 --- a/packages/node-poplib/package.json +++ b/packages/node-poplib/package.json @@ -4,7 +4,6 @@ "private": true, "main": "./src/index.js", "devDependencies": { - "eslint": "~8.45.0", "typescript": "~5.9.3" }, "volta": { diff --git a/packages/omni-core/.eslintrc.json b/packages/omni-core/.eslintrc.json deleted file mode 100644 index 9ec331f09b5e3..0000000000000 --- a/packages/omni-core/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "ignorePatterns": ["dist"] -} diff --git a/packages/omni-core/package.json b/packages/omni-core/package.json index 22f880c08e879..5e8a138d17f04 100644 --- a/packages/omni-core/package.json +++ b/packages/omni-core/package.json @@ -10,8 +10,8 @@ "scripts": { "build": "rm -rf dist && tsc -p tsconfig.json", "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput", - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "lint": "eslint .", + "lint:fix": "eslint --fix .", "test": "jest", "testunit": "jest" }, @@ -21,12 +21,11 @@ "mongodb": "6.16.0" }, "devDependencies": { - "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/tools": "workspace:*", "@rocket.chat/tsconfig": "workspace:*", "@types/jest": "~30.0.0", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "jest": "~30.2.0", "typescript": "~5.9.3" }, diff --git a/packages/password-policies/.eslintrc.json b/packages/password-policies/.eslintrc.json deleted file mode 100644 index f27fe59dc9222..0000000000000 --- a/packages/password-policies/.eslintrc.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "plugins": ["jest"], - "env": { - "jest/globals": true - }, - "ignorePatterns": ["dist"] -} diff --git a/packages/password-policies/package.json b/packages/password-policies/package.json index 30fea3ced0373..30d768b97e4ab 100644 --- a/packages/password-policies/package.json +++ b/packages/password-policies/package.json @@ -10,8 +10,8 @@ "scripts": { "build": "rm -rf dist && tsc", "dev": "tsc --watch --preserveWatchOutput", - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "lint": "eslint .", + "lint:fix": "eslint --fix .", "test": "jest", "testunit": "jest", "typecheck": "tsc --noEmit" @@ -20,7 +20,7 @@ "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/tsconfig": "workspace:*", "@types/jest": "~30.0.0", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "jest": "~30.2.0", "typescript": "~5.9.3" }, diff --git a/packages/patch-injection/.eslintrc.json b/packages/patch-injection/.eslintrc.json deleted file mode 100644 index 9ec331f09b5e3..0000000000000 --- a/packages/patch-injection/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "ignorePatterns": ["dist"] -} diff --git a/packages/patch-injection/package.json b/packages/patch-injection/package.json index 7b369b7b78072..0c7e2764ea869 100644 --- a/packages/patch-injection/package.json +++ b/packages/patch-injection/package.json @@ -10,8 +10,8 @@ "scripts": { "build": "rm -rf dist && tsc", "dev": "tsc --watch --preserveWatchOutput", - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "lint": "eslint .", + "lint:fix": "eslint --fix .", "test": "jest", "testunit": "jest", "typecheck": "tsc --noEmit" @@ -20,7 +20,7 @@ "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/tsconfig": "workspace:*", "@types/jest": "~30.0.0", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "jest": "~30.2.0", "typescript": "~5.9.3" }, diff --git a/packages/peggy-loader/.eslintrc.json b/packages/peggy-loader/.eslintrc.json deleted file mode 100644 index 9ec331f09b5e3..0000000000000 --- a/packages/peggy-loader/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "ignorePatterns": ["dist"] -} diff --git a/packages/peggy-loader/package.json b/packages/peggy-loader/package.json index 5ee3652f21746..f4818858c7c50 100644 --- a/packages/peggy-loader/package.json +++ b/packages/peggy-loader/package.json @@ -30,13 +30,12 @@ ".:build:clean": "rimraf dist", ".:build:tsc": "tsc -p tsconfig.build.json", "build": "run-s .:build:clean .:build:tsc", - "lint": "eslint src" + "lint": "eslint ." }, "devDependencies": { - "@rocket.chat/eslint-config": "workspace:~", "@rocket.chat/prettier-config": "~0.31.25", "@types/node": "~22.16.5", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "npm-run-all": "^4.1.5", "peggy": "4.1.1", "prettier": "~3.3.3", diff --git a/packages/random/.eslintrc.json b/packages/random/.eslintrc.json deleted file mode 100644 index 9ec331f09b5e3..0000000000000 --- a/packages/random/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "ignorePatterns": ["dist"] -} diff --git a/packages/random/package.json b/packages/random/package.json index 4a53e488131fd..f484d4843ce48 100644 --- a/packages/random/package.json +++ b/packages/random/package.json @@ -10,16 +10,15 @@ "build": "rm -rf dist && tsc -p tsconfig.build.json", "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput", "lint": "eslint .", - "lint:fix": "eslint . --fix", + "lint:fix": "eslint --fix .", "test": "jest", "testunit": "jest", "typecheck": "tsc --noEmit" }, "devDependencies": { - "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/tsconfig": "workspace:*", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "jest": "~30.2.0", "typescript": "~5.9.3" }, diff --git a/packages/release-action/.eslintrc.json b/packages/release-action/.eslintrc.json deleted file mode 100644 index 9ec331f09b5e3..0000000000000 --- a/packages/release-action/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "ignorePatterns": ["dist"] -} diff --git a/packages/release-action/package.json b/packages/release-action/package.json index 18ec4e372fb04..e1dbfdb837692 100644 --- a/packages/release-action/package.json +++ b/packages/release-action/package.json @@ -5,7 +5,7 @@ "main": "dist/index.js", "scripts": { "build": "tsc", - "lint": "eslint src", + "lint": "eslint .", "lint:fix": "eslint --fix src" }, "dependencies": { @@ -14,8 +14,6 @@ "@actions/github": "^6.0.1", "@octokit/core": "^5.0.1", "@octokit/plugin-throttling": "^6.1.0", - "@rocket.chat/eslint-config": "workspace:^", - "eslint": "~8.45.0", "mdast-util-to-string": "2.0.0", "remark-parse": "9.0.0", "remark-stringify": "9.0.1", @@ -24,6 +22,7 @@ }, "devDependencies": { "@types/node": "~22.16.5", + "eslint": "~9.39.3", "typescript": "~5.9.3" }, "packageManager": "yarn@4.12.0", diff --git a/packages/release-changelog/.eslintrc.json b/packages/release-changelog/.eslintrc.json deleted file mode 100644 index f27fe59dc9222..0000000000000 --- a/packages/release-changelog/.eslintrc.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "plugins": ["jest"], - "env": { - "jest/globals": true - }, - "ignorePatterns": ["dist"] -} diff --git a/packages/release-changelog/package.json b/packages/release-changelog/package.json index 4e4566bf3a89d..83c58df8ac45a 100644 --- a/packages/release-changelog/package.json +++ b/packages/release-changelog/package.json @@ -5,7 +5,7 @@ "main": "dist/index.js", "scripts": { "build": "tsc", - "lint": "eslint src" + "lint": "eslint ." }, "dependencies": { "dataloader": "^2.2.3", @@ -13,9 +13,8 @@ }, "devDependencies": { "@changesets/types": "^6.0.0", - "@rocket.chat/eslint-config": "workspace:^", "@types/node": "~22.16.5", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "typescript": "~5.9.3" }, "volta": { diff --git a/packages/rest-typings/.eslintrc.json b/packages/rest-typings/.eslintrc.json deleted file mode 100644 index 9ec331f09b5e3..0000000000000 --- a/packages/rest-typings/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "ignorePatterns": ["dist"] -} diff --git a/packages/rest-typings/package.json b/packages/rest-typings/package.json index 2998d0dffbf16..a217392c65673 100644 --- a/packages/rest-typings/package.json +++ b/packages/rest-typings/package.json @@ -9,8 +9,8 @@ "scripts": { "build": "rm -rf dist && tsc", "dev": "tsc --watch --preserveWatchOutput", - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "lint": "eslint .", + "lint:fix": "eslint --fix .", "typecheck": "tsc --noEmit" }, "dependencies": { @@ -22,9 +22,8 @@ }, "devDependencies": { "@rocket.chat/apps-engine": "workspace:^", - "@rocket.chat/eslint-config": "workspace:~", "@types/jest": "~30.0.0", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "mongodb": "6.16.0", "typescript": "~5.9.3" }, diff --git a/packages/server-cloud-communication/.eslintrc.json b/packages/server-cloud-communication/.eslintrc.json deleted file mode 100644 index 9ec331f09b5e3..0000000000000 --- a/packages/server-cloud-communication/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "ignorePatterns": ["dist"] -} diff --git a/packages/server-cloud-communication/package.json b/packages/server-cloud-communication/package.json index 75ed78f672525..22271f6378421 100644 --- a/packages/server-cloud-communication/package.json +++ b/packages/server-cloud-communication/package.json @@ -9,12 +9,12 @@ ], "scripts": { "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput", - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix" + "lint": "eslint .", + "lint:fix": "eslint --fix ." }, "devDependencies": { "@rocket.chat/license": "workspace:^", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "typescript": "~5.9.3" }, "volta": { diff --git a/packages/server-fetch/.eslintrc.json b/packages/server-fetch/.eslintrc.json deleted file mode 100644 index 9ec331f09b5e3..0000000000000 --- a/packages/server-fetch/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "ignorePatterns": ["dist"] -} diff --git a/packages/server-fetch/package.json b/packages/server-fetch/package.json index 0ef1b947a0b43..39951bb9d083d 100644 --- a/packages/server-fetch/package.json +++ b/packages/server-fetch/package.json @@ -10,8 +10,8 @@ "scripts": { "build": "rm -rf dist && tsc -p tsconfig.build.json", "dev": "tsc --watch --preserveWatchOutput", - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "lint": "eslint .", + "lint:fix": "eslint --fix .", "typecheck": "tsc --noEmit", "testunit": "jest" }, @@ -28,7 +28,7 @@ "devDependencies": { "@types/jest": "^29.5.5", "@types/node-fetch": "~2.6.13", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "jest": "^29.7.0", "typescript": "~5.9.3" }, diff --git a/packages/sha256/.eslintrc.json b/packages/sha256/.eslintrc.json deleted file mode 100644 index 9ec331f09b5e3..0000000000000 --- a/packages/sha256/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "ignorePatterns": ["dist"] -} diff --git a/packages/sha256/package.json b/packages/sha256/package.json index 3e780df716cb4..62e9df1194fba 100644 --- a/packages/sha256/package.json +++ b/packages/sha256/package.json @@ -9,16 +9,15 @@ "build": "rm -rf dist && tsc -p tsconfig.build.json", "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput", "lint": "eslint .", - "lint:fix": "eslint . --fix", + "lint:fix": "eslint --fix .", "test": "jest", "testunit": "jest", "typecheck": "tsc --noEmit" }, "devDependencies": { - "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/tsconfig": "workspace:*", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "jest": "~30.2.0", "typescript": "~5.9.3" }, diff --git a/packages/storybook-config/.eslintrc.json b/packages/storybook-config/.eslintrc.json deleted file mode 100644 index cc6ff6d22bd83..0000000000000 --- a/packages/storybook-config/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "ignorePatterns": ["**/dist", "*.d.ts", "*.js"] -} diff --git a/packages/storybook-config/package.json b/packages/storybook-config/package.json index 7870763257554..3803e5a5e78b6 100644 --- a/packages/storybook-config/package.json +++ b/packages/storybook-config/package.json @@ -15,8 +15,8 @@ "build": "rm -rf dist && tsc && yarn run copy-svg", "copy-svg": "cp -r ./src/logo.svg ./dist/logo.svg", "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput", - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix" + "lint": "eslint .", + "lint:fix": "eslint --fix ." }, "dependencies": { "@rocket.chat/emitter": "^0.32.0", @@ -36,12 +36,11 @@ "webpack": "~5.99.9" }, "devDependencies": { - "@rocket.chat/eslint-config": "workspace:~", "@rocket.chat/fuselage": "^0.71.0", "@rocket.chat/icons": "~0.46.0", "@rocket.chat/tsconfig": "workspace:*", "@storybook/react": "^8.6.15", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "react": "~18.3.1", "react-dom": "~18.3.1", "typescript": "~5.9.3" diff --git a/packages/tools/.eslintrc.json b/packages/tools/.eslintrc.json deleted file mode 100644 index 9ec331f09b5e3..0000000000000 --- a/packages/tools/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "ignorePatterns": ["dist"] -} diff --git a/packages/tools/package.json b/packages/tools/package.json index 9e6198af058ec..e14fbd9ceff95 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -10,8 +10,8 @@ "scripts": { "build": "rm -rf dist && tsc -p tsconfig.build.json", "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput", - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "lint": "eslint .", + "lint:fix": "eslint --fix .", "test": "jest", "test:cov": "jest --coverage", "testunit": "jest", @@ -24,7 +24,7 @@ "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/tsconfig": "workspace:*", "@types/jest": "~30.0.0", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "jest": "~30.2.0", "typescript": "~5.9.3" }, diff --git a/packages/tracing/.eslintrc.json b/packages/tracing/.eslintrc.json deleted file mode 100644 index 9ec331f09b5e3..0000000000000 --- a/packages/tracing/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config"], - "ignorePatterns": ["dist"] -} diff --git a/packages/tracing/package.json b/packages/tracing/package.json index c0c3edc0487bd..6feea40f3c6ea 100644 --- a/packages/tracing/package.json +++ b/packages/tracing/package.json @@ -10,8 +10,8 @@ "scripts": { "build": "rm -rf dist && tsc", "dev": "tsc --watch --preserveWatchOutput", - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "lint": "eslint .", + "lint:fix": "eslint --fix .", "testunit": "jest --passWithNoTests" }, "dependencies": { @@ -21,7 +21,7 @@ }, "devDependencies": { "@types/jest": "~30.0.0", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "jest": "~30.2.0", "ts-jest": "~29.4.6", "typescript": "~5.9.3" diff --git a/packages/ui-avatar/.eslintrc.json b/packages/ui-avatar/.eslintrc.json deleted file mode 100644 index 341a302833736..0000000000000 --- a/packages/ui-avatar/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config", "@rocket.chat/eslint-config/react"], - "ignorePatterns": ["dist", "storybook-static", "!.storybook"] -} diff --git a/packages/ui-avatar/package.json b/packages/ui-avatar/package.json index e766be6870aa1..7ef4980b0e950 100644 --- a/packages/ui-avatar/package.json +++ b/packages/ui-avatar/package.json @@ -10,8 +10,8 @@ "scripts": { "build": "rm -rf dist && tsc -p tsconfig.build.json", "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput", - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "lint": "eslint .", + "lint:fix": "eslint --fix .", "typecheck": "tsc -p tsconfig.json --noEmit" }, "devDependencies": { @@ -24,11 +24,7 @@ "@rocket.chat/ui-contexts": "workspace:^", "@types/react": "~18.3.27", "@types/react-dom": "~18.3.7", - "eslint": "~8.45.0", - "eslint-plugin-react": "~7.37.5", - "eslint-plugin-react-hooks": "~5.0.0", - "eslint-plugin-storybook": "~0.11.6", - "eslint-plugin-testing-library": "~6.4.0", + "eslint": "~9.39.3", "react": "~18.3.1", "react-dom": "~18.3.1", "react-virtuoso": "^4.12.0", diff --git a/packages/ui-client/.eslintrc.json b/packages/ui-client/.eslintrc.json deleted file mode 100644 index 96574c6de8d41..0000000000000 --- a/packages/ui-client/.eslintrc.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config", "@rocket.chat/eslint-config/react", "plugin:storybook/recommended"], - "ignorePatterns": ["dist", "storybook-static", "!.storybook"], - "overrides": [ - { - "files": ["**/*.ts", "**/*.tsx"], - "rules": { - "@typescript-eslint/no-misused-promises": "off" - } - } - ] -} diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index e1e401b4d664a..72b9384a0a412 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -10,8 +10,8 @@ "scripts": { "build": "rm -rf dist && tsc -p tsconfig.build.json", "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput", - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "lint": "eslint .", + "lint:fix": "eslint --fix .", "storybook": "storybook dev -p 6006", "test": "jest", "testunit": "jest", @@ -44,12 +44,7 @@ "@types/jest": "~30.0.0", "@types/react": "~18.3.27", "@types/react-dom": "~18.3.7", - "eslint": "~8.45.0", - "eslint-plugin-anti-trojan-source": "~1.1.2", - "eslint-plugin-react": "~7.37.5", - "eslint-plugin-react-hooks": "~5.0.0", - "eslint-plugin-storybook": "~0.11.6", - "eslint-plugin-testing-library": "~6.4.0", + "eslint": "~9.39.3", "jest": "~30.2.0", "overlayscrollbars": "^2.11.4", "overlayscrollbars-react": "^0.5.6", diff --git a/packages/ui-composer/.eslintrc.json b/packages/ui-composer/.eslintrc.json deleted file mode 100644 index 6e0406abd46a7..0000000000000 --- a/packages/ui-composer/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config", "@rocket.chat/eslint-config/react", "plugin:storybook/recommended"], - "ignorePatterns": ["dist", "storybook-static", "!.storybook"] -} diff --git a/packages/ui-composer/package.json b/packages/ui-composer/package.json index 187c4600eb2ec..c4501dd4b8625 100644 --- a/packages/ui-composer/package.json +++ b/packages/ui-composer/package.json @@ -12,8 +12,8 @@ "build": "rm -rf dist && tsc -p tsconfig.build.json", "build-preview": "storybook build", "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput", - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "lint": "eslint .", + "lint:fix": "eslint --fix .", "storybook": "storybook dev -p 6006", "test": "jest", "testunit": "jest", @@ -22,7 +22,6 @@ "devDependencies": { "@react-aria/toolbar": "^3.0.0-nightly.5042", "@rocket.chat/emitter": "^0.32.0", - "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/fuselage": "^0.71.0", "@rocket.chat/fuselage-hooks": "^0.39.0", "@rocket.chat/fuselage-tokens": "~0.33.2", @@ -40,10 +39,7 @@ "@types/jest": "~30.0.0", "@types/react": "~18.3.27", "@types/react-dom": "~18.3.7", - "eslint": "~8.45.0", - "eslint-plugin-react": "~7.37.5", - "eslint-plugin-react-hooks": "~5.0.0", - "eslint-plugin-storybook": "~0.11.6", + "eslint": "~9.39.3", "jest": "~30.2.0", "react": "~18.3.1", "react-dom": "~18.3.1", diff --git a/packages/ui-contexts/.eslintrc.json b/packages/ui-contexts/.eslintrc.json deleted file mode 100644 index db2a901f7069b..0000000000000 --- a/packages/ui-contexts/.eslintrc.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config", "@rocket.chat/eslint-config/react"], - "ignorePatterns": ["dist"], - "overrides": [ - { - "files": ["**/*.ts", "**/*.tsx"], - "rules": { - "@typescript-eslint/no-misused-promises": "off" - } - } - ] -} diff --git a/packages/ui-contexts/package.json b/packages/ui-contexts/package.json index e976080070cd0..fcb99572f308c 100644 --- a/packages/ui-contexts/package.json +++ b/packages/ui-contexts/package.json @@ -10,8 +10,8 @@ "scripts": { "build": "rm -rf dist && tsc -p tsconfig.build.json", "dev": "tsc --watch --preserveWatchOutput -p tsconfig.build.json", - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "lint": "eslint .", + "lint:fix": "eslint --fix .", "test": "jest", "testunit": "jest" }, @@ -33,8 +33,7 @@ "@types/jest": "~30.0.0", "@types/react": "~18.3.27", "@types/react-dom": "~18.3.7", - "eslint": "~8.45.0", - "eslint-plugin-react-hooks": "^5.0.0", + "eslint": "~9.39.3", "i18next": "~23.4.9", "jest": "~30.2.0", "mongodb": "6.16.0", diff --git a/packages/ui-kit/.eslintrc.json b/packages/ui-kit/.eslintrc.json deleted file mode 100644 index 0ae720da7f0f4..0000000000000 --- a/packages/ui-kit/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "@rocket.chat/eslint-config", - "ignorePatterns": ["dist"] -} diff --git a/packages/ui-kit/package.json b/packages/ui-kit/package.json index 48d99de76e1bc..ba9616264d1d3 100644 --- a/packages/ui-kit/package.json +++ b/packages/ui-kit/package.json @@ -25,7 +25,7 @@ ".:build:prepare": "ts-patch install && typia patch", ".:build:tsc": "tsc -p tsconfig.build.json", "build": "run-s .:build:prepare .:build:clean .:build:tsc", - "lint": "eslint . --ext .ts,.tsx", + "lint": "eslint .", "test": "jest", "testunit": "jest", "typecheck": "tsc --noEmit" @@ -34,12 +34,11 @@ "typia": "~9.7.2" }, "devDependencies": { - "@rocket.chat/eslint-config": "workspace:~", "@rocket.chat/icons": "~0.46.0", "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/tsconfig": "workspace:*", "@types/jest": "~30.0.0", - "eslint": "~8.45.0", + "eslint": "~9.39.3", "jest": "~30.2.0", "npm-run-all": "~4.1.5", "prettier": "~3.3.3", diff --git a/packages/ui-video-conf/.eslintrc.json b/packages/ui-video-conf/.eslintrc.json deleted file mode 100644 index 6e0406abd46a7..0000000000000 --- a/packages/ui-video-conf/.eslintrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config", "@rocket.chat/eslint-config/react", "plugin:storybook/recommended"], - "ignorePatterns": ["dist", "storybook-static", "!.storybook"] -} diff --git a/packages/ui-video-conf/package.json b/packages/ui-video-conf/package.json index f30de41a07cd2..d42e8900dc106 100644 --- a/packages/ui-video-conf/package.json +++ b/packages/ui-video-conf/package.json @@ -10,8 +10,8 @@ "scripts": { "build": "rm -rf dist && tsc -p tsconfig.build.json", "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput", - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "lint": "eslint .", + "lint:fix": "eslint --fix .", "storybook": "storybook dev -p 6006", "test": "jest", "testunit": "jest", @@ -22,7 +22,6 @@ }, "devDependencies": { "@rocket.chat/css-in-js": "~0.31.25", - "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/fuselage": "^0.71.0", "@rocket.chat/fuselage-hooks": "^0.39.0", "@rocket.chat/fuselage-tokens": "~0.33.2", @@ -44,10 +43,7 @@ "@types/jest-axe": "~3.5.9", "@types/react": "~18.3.27", "@types/react-dom": "~18.3.7", - "eslint": "~8.45.0", - "eslint-plugin-react": "~7.37.5", - "eslint-plugin-react-hooks": "~5.0.0", - "eslint-plugin-storybook": "~0.11.6", + "eslint": "~9.39.3", "jest": "~30.2.0", "jest-axe": "~10.0.0", "react": "~18.3.1", diff --git a/packages/ui-voip/.eslintrc.json b/packages/ui-voip/.eslintrc.json deleted file mode 100644 index 96574c6de8d41..0000000000000 --- a/packages/ui-voip/.eslintrc.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config", "@rocket.chat/eslint-config/react", "plugin:storybook/recommended"], - "ignorePatterns": ["dist", "storybook-static", "!.storybook"], - "overrides": [ - { - "files": ["**/*.ts", "**/*.tsx"], - "rules": { - "@typescript-eslint/no-misused-promises": "off" - } - } - ] -} diff --git a/packages/ui-voip/package.json b/packages/ui-voip/package.json index 599d0973dfe72..952a93f4d0e4b 100644 --- a/packages/ui-voip/package.json +++ b/packages/ui-voip/package.json @@ -10,8 +10,8 @@ "scripts": { "build": "tsc -p tsconfig.build.json", "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput", - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "lint": "eslint .", + "lint:fix": "eslint --fix .", "storybook": "storybook dev -p 6006", "test": "yarn testunit", "test-storybook": "npx concurrently -k -s first -n \"SB,TEST\" \"yarn storybook --ci\" \"npx wait-on tcp:127.0.0.1:6006 && yarn exec test-storybook\"", @@ -30,7 +30,6 @@ "@react-spectrum/test-utils": "~1.0.0-alpha.8", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/css-in-js": "~0.31.25", - "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/fuselage": "^0.71.0", "@rocket.chat/fuselage-hooks": "^0.39.0", "@rocket.chat/fuselage-tokens": "~0.33.2", @@ -62,10 +61,7 @@ "@types/react": "~18.3.27", "@types/react-dom": "~18.3.7", "date-fns": "~4.1.0", - "eslint": "~8.45.0", - "eslint-plugin-react": "~7.37.5", - "eslint-plugin-react-hooks": "~5.0.0", - "eslint-plugin-storybook": "~0.11.6", + "eslint": "~9.39.3", "i18next": "~23.4.9", "jest": "~30.2.0", "jest-axe": "~10.0.0", diff --git a/packages/web-ui-registration/.eslintrc.json b/packages/web-ui-registration/.eslintrc.json deleted file mode 100644 index 96574c6de8d41..0000000000000 --- a/packages/web-ui-registration/.eslintrc.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": ["@rocket.chat/eslint-config", "@rocket.chat/eslint-config/react", "plugin:storybook/recommended"], - "ignorePatterns": ["dist", "storybook-static", "!.storybook"], - "overrides": [ - { - "files": ["**/*.ts", "**/*.tsx"], - "rules": { - "@typescript-eslint/no-misused-promises": "off" - } - } - ] -} diff --git a/packages/web-ui-registration/package.json b/packages/web-ui-registration/package.json index 20218a762af42..4559294b3e3fa 100644 --- a/packages/web-ui-registration/package.json +++ b/packages/web-ui-registration/package.json @@ -11,8 +11,8 @@ "scripts": { "build": "tsc -p tsconfig.build.json", "dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput", - "lint": "eslint --ext .js,.jsx,.ts,.tsx .", - "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "lint": "eslint .", + "lint:fix": "eslint --fix .", "storybook": "storybook dev -p 6006 --no-version-updates", "typecheck": "tsc --noEmit" }, @@ -46,8 +46,7 @@ "@testing-library/react": "~16.3.2", "@types/react": "~18.3.27", "@types/react-dom": "~18.3.7", - "eslint": "~8.45.0", - "eslint-plugin-storybook": "~0.11.6", + "eslint": "~9.39.3", "i18next": "~23.4.9", "react": "~18.3.1", "react-dom": "~18.3.1", diff --git a/yarn.lock b/yarn.lock index 529b3a99ee19c..88f1024df3206 100644 --- a/yarn.lock +++ b/yarn.lock @@ -133,6 +133,17 @@ __metadata: languageName: node linkType: hard +"@babel/code-frame@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/code-frame@npm:7.29.0" + dependencies: + "@babel/helper-validator-identifier": "npm:^7.28.5" + js-tokens: "npm:^4.0.0" + picocolors: "npm:^1.1.1" + checksum: 10/199e15ff89007dd30675655eec52481cb245c9fdf4f81e4dc1f866603b0217b57aff25f5ffa0a95bbc8e31eb861695330cd7869ad52cc211aa63016320ef72c5 + languageName: node + linkType: hard + "@babel/compat-data@npm:^7.27.2": version: 7.27.5 resolution: "@babel/compat-data@npm:7.27.5" @@ -177,6 +188,29 @@ __metadata: languageName: node linkType: hard +"@babel/core@npm:^7.24.4": + version: 7.29.0 + resolution: "@babel/core@npm:7.29.0" + dependencies: + "@babel/code-frame": "npm:^7.29.0" + "@babel/generator": "npm:^7.29.0" + "@babel/helper-compilation-targets": "npm:^7.28.6" + "@babel/helper-module-transforms": "npm:^7.28.6" + "@babel/helpers": "npm:^7.28.6" + "@babel/parser": "npm:^7.29.0" + "@babel/template": "npm:^7.28.6" + "@babel/traverse": "npm:^7.29.0" + "@babel/types": "npm:^7.29.0" + "@jridgewell/remapping": "npm:^2.3.5" + convert-source-map: "npm:^2.0.0" + debug: "npm:^4.1.0" + gensync: "npm:^1.0.0-beta.2" + json5: "npm:^2.2.3" + semver: "npm:^6.3.1" + checksum: 10/25f4e91688cdfbaf1365831f4f245b436cdaabe63d59389b75752013b8d61819ee4257101b52fc328b0546159fd7d0e74457ed7cf12c365fea54be4fb0a40229 + languageName: node + linkType: hard + "@babel/core@npm:~7.28.6": version: 7.28.6 resolution: "@babel/core@npm:7.28.6" @@ -279,6 +313,19 @@ __metadata: languageName: node linkType: hard +"@babel/generator@npm:^7.29.0": + version: 7.29.1 + resolution: "@babel/generator@npm:7.29.1" + dependencies: + "@babel/parser": "npm:^7.29.0" + "@babel/types": "npm:^7.29.0" + "@jridgewell/gen-mapping": "npm:^0.3.12" + "@jridgewell/trace-mapping": "npm:^0.3.28" + jsesc: "npm:^3.0.2" + checksum: 10/61fe4ddd6e817aa312a14963ccdbb5c9a8c57e8b97b98d19a8a99ccab2215fda1a5f52bc8dd8d2e3c064497ddeb3ab8ceb55c76fa0f58f8169c34679d2256fe0 + languageName: node + linkType: hard + "@babel/helper-annotate-as-pure@npm:^7.25.9, @babel/helper-annotate-as-pure@npm:^7.27.1, @babel/helper-annotate-as-pure@npm:^7.27.3": version: 7.27.3 resolution: "@babel/helper-annotate-as-pure@npm:7.27.3" @@ -641,6 +688,17 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.24.4, @babel/parser@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/parser@npm:7.29.0" + dependencies: + "@babel/types": "npm:^7.29.0" + bin: + parser: ./bin/babel-parser.js + checksum: 10/b1576dca41074997a33ee740d87b330ae2e647f4b7da9e8d2abd3772b18385d303b0cee962b9b88425e0f30d58358dbb8d63792c1a2d005c823d335f6a029747 + languageName: node + linkType: hard + "@babel/parser@npm:^7.25.7, @babel/parser@npm:^7.25.9, @babel/parser@npm:^7.27.1, @babel/parser@npm:^7.28.3, @babel/parser@npm:^7.28.4": version: 7.28.4 resolution: "@babel/parser@npm:7.28.4" @@ -2002,6 +2060,21 @@ __metadata: languageName: node linkType: hard +"@babel/traverse@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/traverse@npm:7.29.0" + dependencies: + "@babel/code-frame": "npm:^7.29.0" + "@babel/generator": "npm:^7.29.0" + "@babel/helper-globals": "npm:^7.28.0" + "@babel/parser": "npm:^7.29.0" + "@babel/template": "npm:^7.28.6" + "@babel/types": "npm:^7.29.0" + debug: "npm:^4.3.1" + checksum: 10/3a0d0438f1ba9fed4fbe1706ea598a865f9af655a16ca9517ab57bda526e224569ca1b980b473fb68feea5e08deafbbf2cf9febb941f92f2d2533310c3fc4abc + languageName: node + linkType: hard + "@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.5, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4": version: 7.23.5 resolution: "@babel/types@npm:7.23.5" @@ -2094,6 +2167,16 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.29.0": + version: 7.29.0 + resolution: "@babel/types@npm:7.29.0" + dependencies: + "@babel/helper-string-parser": "npm:^7.27.1" + "@babel/helper-validator-identifier": "npm:^7.28.5" + checksum: 10/bfc2b211210f3894dcd7e6a33b2d1c32c93495dc1e36b547376aa33441abe551ab4bc1640d4154ee2acd8e46d3bbc925c7224caae02fcaf0e6a771e97fccc661 + languageName: node + linkType: hard + "@bcoe/v8-coverage@npm:^0.2.3": version: 0.2.3 resolution: "@bcoe/v8-coverage@npm:0.2.3" @@ -3229,45 +3312,91 @@ __metadata: languageName: node linkType: hard -"@eslint-community/eslint-utils@npm:^4.2.0, @eslint-community/eslint-utils@npm:^4.4.0": - version: 4.4.0 - resolution: "@eslint-community/eslint-utils@npm:4.4.0" +"@eslint-community/eslint-utils@npm:^4.8.0, @eslint-community/eslint-utils@npm:^4.9.1": + version: 4.9.1 + resolution: "@eslint-community/eslint-utils@npm:4.9.1" dependencies: - eslint-visitor-keys: "npm:^3.3.0" + eslint-visitor-keys: "npm:^3.4.3" peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - checksum: 10/8d70bcdcd8cd279049183aca747d6c2ed7092a5cf0cf5916faac1ef37ffa74f0c245c2a3a3d3b9979d9dfdd4ca59257b4c5621db699d637b847a2c5e02f491c2 + checksum: 10/863b5467868551c9ae34d03eefe634633d08f623fc7b19d860f8f26eb6f303c1a5934253124163bee96181e45ed22bf27473dccc295937c3078493a4a8c9eddd languageName: node linkType: hard -"@eslint-community/regexpp@npm:^4.4.0": - version: 4.5.1 - resolution: "@eslint-community/regexpp@npm:4.5.1" - checksum: 10/e31e456d44e9bf98d59c8ac445549098e1a6d9c4e22053cad58e86a9f78a1e64104ef7f7f46255c442e0c878fe0e566ffba287787d070196c83510ef30d1d197 +"@eslint-community/regexpp@npm:^4.12.1, @eslint-community/regexpp@npm:^4.12.2": + version: 4.12.2 + resolution: "@eslint-community/regexpp@npm:4.12.2" + checksum: 10/049b280fddf71dd325514e0a520024969431dc3a8b02fa77476e6820e9122f28ab4c9168c11821f91a27982d2453bcd7a66193356ea84e84fb7c8d793be1ba0c languageName: node linkType: hard -"@eslint/eslintrc@npm:^2.1.0": - version: 2.1.0 - resolution: "@eslint/eslintrc@npm:2.1.0" +"@eslint/config-array@npm:^0.21.1": + version: 0.21.1 + resolution: "@eslint/config-array@npm:0.21.1" dependencies: - ajv: "npm:^6.12.4" + "@eslint/object-schema": "npm:^2.1.7" + debug: "npm:^4.3.1" + minimatch: "npm:^3.1.2" + checksum: 10/6eaa0435972f735ce52d581f355a0b616e50a9b8a73304a7015398096e252798b9b3b968a67b524eefb0fdeacc57c4d960f0ec6432abe1c1e24be815b88c5d18 + languageName: node + linkType: hard + +"@eslint/config-helpers@npm:^0.4.2": + version: 0.4.2 + resolution: "@eslint/config-helpers@npm:0.4.2" + dependencies: + "@eslint/core": "npm:^0.17.0" + checksum: 10/3f2b4712d8e391c36ec98bc200f7dea423dfe518e42956569666831b89ede83b33120c761dfd3ab6347d8e8894a6d4af47254a18d464a71c6046fd88065f6daf + languageName: node + linkType: hard + +"@eslint/core@npm:^0.17.0": + version: 0.17.0 + resolution: "@eslint/core@npm:0.17.0" + dependencies: + "@types/json-schema": "npm:^7.0.15" + checksum: 10/f9a428cc651ec15fb60d7d60c2a7bacad4666e12508320eafa98258e976fafaa77d7be7be91519e75f801f15f830105420b14a458d4aab121a2b0a59bc43517b + languageName: node + linkType: hard + +"@eslint/eslintrc@npm:^3.3.1": + version: 3.3.4 + resolution: "@eslint/eslintrc@npm:3.3.4" + dependencies: + ajv: "npm:^6.14.0" debug: "npm:^4.3.2" - espree: "npm:^9.6.0" - globals: "npm:^13.19.0" + espree: "npm:^10.0.1" + globals: "npm:^14.0.0" ignore: "npm:^5.2.0" import-fresh: "npm:^3.2.1" - js-yaml: "npm:^4.1.0" - minimatch: "npm:^3.1.2" + js-yaml: "npm:^4.1.1" + minimatch: "npm:^3.1.3" strip-json-comments: "npm:^3.1.1" - checksum: 10/923adf0fbadbe1548b2cbf6d020cc135fcd3bafee073b937a4c2e15b971cff607d987cc82e076d19d86d660dc0b992f688e0f5cf5eabfb5045c8ecdc3e50bd63 + checksum: 10/537e6bddb55d37a6b128910d54eaa2c1851992781f82dbf36294583de50386ca92bd669eadc99db9181ab4d735f7e6fa286cba10dab1327b1ea88599a2c5e6a7 languageName: node linkType: hard -"@eslint/js@npm:8.44.0": - version: 8.44.0 - resolution: "@eslint/js@npm:8.44.0" - checksum: 10/06adec291c023cf1415d5c8dc0b14608d770ffb42b29c65dcbf092051580e1f6080483979c87b2067580b4566e281c0f588efb571303a092b34bca911eca8fb9 +"@eslint/js@npm:9.39.3, @eslint/js@npm:~9.39.3": + version: 9.39.3 + resolution: "@eslint/js@npm:9.39.3" + checksum: 10/91a1a1822cfeb2eb8a89aae86be5dfabad0b66b0915946516690a8485ddd80b91f43eee346789313fea1acbb7390a4958119ca7dc9a684a5c4014f12fcb3aaf3 + languageName: node + linkType: hard + +"@eslint/object-schema@npm:^2.1.7": + version: 2.1.7 + resolution: "@eslint/object-schema@npm:2.1.7" + checksum: 10/946ef5d6235b4d1c0907c6c6e6429c8895f535380c562b7705c131f63f2e961b06e8785043c86a293da48e0a60c6286d98ba395b8b32ea55561fe6e4417cb7e4 + languageName: node + linkType: hard + +"@eslint/plugin-kit@npm:^0.4.1": + version: 0.4.1 + resolution: "@eslint/plugin-kit@npm:0.4.1" + dependencies: + "@eslint/core": "npm:^0.17.0" + levn: "npm:^0.4.1" + checksum: 10/c5947d0ffeddca77d996ac1b886a66060c1a15ed1d5e425d0c7e7d7044a4bd3813fc968892d03950a7831c9b89368a2f7b281e45dd3c74a048962b74bf3a1cb4 languageName: node linkType: hard @@ -3459,14 +3588,20 @@ __metadata: languageName: node linkType: hard -"@humanwhocodes/config-array@npm:^0.11.10": - version: 0.11.10 - resolution: "@humanwhocodes/config-array@npm:0.11.10" +"@humanfs/core@npm:^0.19.1": + version: 0.19.1 + resolution: "@humanfs/core@npm:0.19.1" + checksum: 10/270d936be483ab5921702623bc74ce394bf12abbf57d9145a69e8a0d1c87eb1c768bd2d93af16c5705041e257e6d9cc7529311f63a1349f3678abc776fc28523 + languageName: node + linkType: hard + +"@humanfs/node@npm:^0.16.6": + version: 0.16.7 + resolution: "@humanfs/node@npm:0.16.7" dependencies: - "@humanwhocodes/object-schema": "npm:^1.2.1" - debug: "npm:^4.1.1" - minimatch: "npm:^3.0.5" - checksum: 10/f93086ae6a340e739a6bb23d4575b69f52acc4e4e3d62968eaaf77a77db4ba69d6d3e50c0028ba19b634ef6b241553a9d9a13d91b797b3ea33d5d711bb3362fb + "@humanfs/core": "npm:^0.19.1" + "@humanwhocodes/retry": "npm:^0.4.0" + checksum: 10/b3633d3dce898592cac515ba5e6693c78e6be92863541d3eaf2c009b10f52b2fa62ff6e6e06f240f2447ddbe7b5f1890bc34e9308470675c876eee207553a08d languageName: node linkType: hard @@ -3477,10 +3612,10 @@ __metadata: languageName: node linkType: hard -"@humanwhocodes/object-schema@npm:^1.2.1": - version: 1.2.1 - resolution: "@humanwhocodes/object-schema@npm:1.2.1" - checksum: 10/b48a8f87fcd5fdc4ac60a31a8bf710d19cc64556050575e6a35a4a48a8543cf8cde1598a65640ff2cdfbfd165b38f9db4fa3782bea7848eb585cc3db824002e6 +"@humanwhocodes/retry@npm:^0.4.0, @humanwhocodes/retry@npm:^0.4.2": + version: 0.4.3 + resolution: "@humanwhocodes/retry@npm:0.4.3" + checksum: 10/0b32cfd362bea7a30fbf80bb38dcaf77fee9c2cae477ee80b460871d03590110ac9c77d654f04ec5beaf71b6f6a89851bdf6c1e34ccdf2f686bd86fcd97d9e61 languageName: node linkType: hard @@ -5069,7 +5204,7 @@ __metadata: languageName: node linkType: hard -"@nodelib/fs.walk@npm:^1.2.3, @nodelib/fs.walk@npm:^1.2.8": +"@nodelib/fs.walk@npm:^1.2.3": version: 1.2.8 resolution: "@nodelib/fs.walk@npm:1.2.8" dependencies: @@ -5937,6 +6072,13 @@ __metadata: languageName: node linkType: hard +"@pkgr/core@npm:^0.2.9": + version: 0.2.9 + resolution: "@pkgr/core@npm:0.2.9" + checksum: 10/bb2fb86977d63f836f8f5b09015d74e6af6488f7a411dcd2bfdca79d76b5a681a9112f41c45bdf88a9069f049718efc6f3900d7f1de66a2ec966068308ae517f + languageName: node + linkType: hard + "@playwright/test@npm:^1.52.0": version: 1.52.0 resolution: "@playwright/test@npm:1.52.0" @@ -7901,13 +8043,12 @@ __metadata: dependencies: "@rocket.chat/core-services": "workspace:^" "@rocket.chat/core-typings": "workspace:^" - "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/logger": "workspace:^" "@rocket.chat/models": "workspace:^" "@rocket.chat/tsconfig": "workspace:*" "@types/jest": "npm:~30.0.0" "@types/node": "npm:~22.16.5" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" jest: "npm:~30.2.0" mem: "npm:^8.1.1" mongodb: "npm:6.10.0" @@ -7924,7 +8065,6 @@ __metadata: "@rocket.chat/core-services": "workspace:^" "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/emitter": "npm:^0.32.0" - "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/logger": "workspace:^" "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" @@ -7940,7 +8080,7 @@ __metadata: "@types/prometheus-gc-stats": "npm:^0.6.4" bcrypt: "npm:^5.1.1" ejson: "npm:^2.2.3" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" eventemitter3: "npm:^5.0.4" mem: "npm:^8.1.1" moleculer: "npm:^0.14.35" @@ -7958,7 +8098,7 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/account-utils@workspace:packages/account-utils" dependencies: - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" typescript: "npm:~5.9.3" languageName: unknown linkType: soft @@ -7971,7 +8111,7 @@ __metadata: cron: "npm:~1.8.2" date.js: "npm:~0.3.3" debug: "npm:~4.3.7" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" human-interval: "npm:^2.0.1" moment-timezone: "npm:~0.5.48" mongodb: "npm:6.16.0" @@ -7989,7 +8129,7 @@ __metadata: "@rocket.chat/tsconfig": "workspace:*" "@types/jest": "npm:~30.0.0" "@types/strict-uri-encode": "npm:^2.0.2" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" filter-obj: "npm:^3.0.0" jest: "npm:~30.2.0" jest-fetch-mock: "npm:~3.0.3" @@ -8005,7 +8145,6 @@ __metadata: resolution: "@rocket.chat/apps-engine@workspace:packages/apps-engine" dependencies: "@msgpack/msgpack": "npm:3.0.0-beta2" - "@rocket.chat/eslint-config": "workspace:~" "@rocket.chat/ui-kit": "workspace:~" "@seald-io/nedb": "npm:^4.1.2" "@types/adm-zip": "npm:^0.5.7" @@ -8015,14 +8154,12 @@ __metadata: "@types/semver": "npm:^7.5.8" "@types/stack-trace": "npm:0.0.33" "@types/uuid": "npm:~10.0.0" - "@typescript-eslint/eslint-plugin": "npm:~5.60.1" - "@typescript-eslint/parser": "npm:~5.60.1" adm-zip: "npm:^0.5.16" alsatian: "npm:^2.4.0" browserify: "npm:^16.5.2" debug: "npm:^4.3.7" esbuild: "npm:~0.25.12" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" jose: "npm:^4.15.9" jsonrpc-lite: "npm:^2.2.0" lodash.clonedeep: "npm:^4.5.0" @@ -8048,7 +8185,7 @@ __metadata: "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/tsconfig": "workspace:*" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" typescript: "npm:~5.9.3" languageName: unknown linkType: soft @@ -8061,7 +8198,6 @@ __metadata: "@rocket.chat/core-services": "workspace:^" "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/emitter": "npm:^0.32.0" - "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/logger": "workspace:^" "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" @@ -8074,7 +8210,7 @@ __metadata: "@types/polka": "npm:^0.5.8" "@types/prometheus-gc-stats": "npm:^0.6.4" ejson: "npm:^2.2.3" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" eventemitter3: "npm:^5.0.4" mem: "npm:^8.1.1" moleculer: "npm:^0.14.35" @@ -8092,10 +8228,8 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/base64@workspace:packages/base64" dependencies: - "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/tsconfig": "workspace:*" - eslint: "npm:~8.45.0" jest: "npm:~30.2.0" typescript: "npm:~5.9.3" languageName: unknown @@ -8106,7 +8240,7 @@ __metadata: resolution: "@rocket.chat/cas-validate@workspace:packages/cas-validate" dependencies: cheerio: "npm:1.0.0" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" jest: "npm:~30.2.0" typescript: "npm:~5.9.3" languageName: unknown @@ -8118,7 +8252,6 @@ __metadata: dependencies: "@rocket.chat/apps-engine": "workspace:^" "@rocket.chat/core-typings": "workspace:^" - "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/federation-sdk": "npm:0.3.9" "@rocket.chat/http-router": "workspace:^" "@rocket.chat/icons": "npm:~0.46.0" @@ -8131,7 +8264,7 @@ __metadata: "@rocket.chat/tsconfig": "workspace:*" "@rocket.chat/ui-kit": "workspace:~" "@types/jest": "npm:~30.0.0" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" jest: "npm:~30.2.0" mongodb: "npm:6.16.0" prettier: "npm:~3.3.3" @@ -8144,12 +8277,11 @@ __metadata: resolution: "@rocket.chat/core-typings@workspace:packages/core-typings" dependencies: "@rocket.chat/apps-engine": "workspace:^" - "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/icons": "npm:~0.46.0" "@rocket.chat/message-parser": "workspace:^" "@rocket.chat/ui-kit": "workspace:~" "@types/express": "npm:^4.17.25" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" mongodb: "npm:6.16.0" npm-run-all: "npm:~4.1.5" prettier: "npm:~3.3.3" @@ -8171,7 +8303,7 @@ __metadata: "@rocket.chat/models": "workspace:^" "@rocket.chat/random": "workspace:^" "@rocket.chat/tsconfig": "workspace:*" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" mongodb: "npm:6.16.0" typescript: "npm:~5.9.3" languageName: unknown @@ -8211,7 +8343,7 @@ __metadata: "@rocket.chat/tsconfig": "workspace:*" "@types/jest": "npm:~30.0.0" "@types/ws": "npm:^8.5.13" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" jest: "npm:~30.2.0" jest-websocket-mock: "npm:~2.5.0" typescript: "npm:~5.9.3" @@ -8230,7 +8362,6 @@ __metadata: "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/ddp-client": "workspace:~" "@rocket.chat/emitter": "npm:^0.32.0" - "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/instance-status": "workspace:^" "@rocket.chat/logger": "workspace:^" "@rocket.chat/model-typings": "workspace:^" @@ -8248,7 +8379,7 @@ __metadata: "@types/ws": "npm:^8.5.13" colorette: "npm:^1.4.0" ejson: "npm:^2.2.3" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" eventemitter3: "npm:^5.0.4" jaeger-client: "npm:^3.19.0" mem: "npm:^8.1.1" @@ -8272,8 +8403,7 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/desktop-api@workspace:packages/desktop-api" dependencies: - "@rocket.chat/eslint-config": "workspace:~" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" rimraf: "npm:~6.0.1" typescript: "npm:~5.9.3" languageName: unknown @@ -8293,26 +8423,31 @@ __metadata: languageName: node linkType: hard -"@rocket.chat/eslint-config@workspace:^, @rocket.chat/eslint-config@workspace:packages/eslint-config, @rocket.chat/eslint-config@workspace:~": +"@rocket.chat/eslint-config@workspace:packages/eslint-config, @rocket.chat/eslint-config@workspace:~": version: 0.0.0-use.local resolution: "@rocket.chat/eslint-config@workspace:packages/eslint-config" dependencies: "@babel/core": "npm:~7.28.6" "@babel/eslint-parser": "npm:~7.28.6" + "@eslint/js": "npm:~9.39.3" "@types/eslint": "npm:~8.44.9" "@types/prettier": "npm:^2.7.3" - "@typescript-eslint/eslint-plugin": "npm:~5.60.1" - "@typescript-eslint/parser": "npm:~5.60.1" - eslint: "npm:~8.45.0" - eslint-config-prettier: "npm:~9.1.2" + eslint: "npm:~9.39.3" + eslint-config-prettier: "npm:~10.1.8" eslint-import-resolver-typescript: "npm:~4.4.4" eslint-plugin-anti-trojan-source: "npm:~1.1.2" - eslint-plugin-import: "npm:~2.31.0" - eslint-plugin-jest: "npm:~28.8.3" - eslint-plugin-jsx-a11y: "npm:^6.10.2" - eslint-plugin-prettier: "npm:~5.2.6" + eslint-plugin-import: "npm:~2.32.0" + eslint-plugin-jest: "npm:~29.15.0" + eslint-plugin-jsx-a11y: "npm:~6.10.2" + eslint-plugin-prettier: "npm:~5.5.5" + eslint-plugin-react: "npm:~7.37.5" + eslint-plugin-react-hooks: "npm:~7.0.1" + eslint-plugin-storybook: "npm:~10.2.12" + eslint-plugin-testing-library: "npm:~7.16.0" + globals: "npm:~17.3.0" prettier: "npm:~3.3.3" typescript: "npm:~5.9.3" + typescript-eslint: "npm:~8.56.1" languageName: unknown linkType: soft @@ -8320,7 +8455,7 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/favicon@workspace:packages/favicon" dependencies: - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" typescript: "npm:~5.9.3" languageName: unknown linkType: soft @@ -8333,7 +8468,6 @@ __metadata: "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/ddp-client": "workspace:^" "@rocket.chat/emitter": "npm:^0.32.0" - "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/federation-sdk": "npm:0.3.9" "@rocket.chat/http-router": "workspace:^" "@rocket.chat/license": "workspace:^" @@ -8344,7 +8478,7 @@ __metadata: "@types/node": "npm:~22.16.5" "@types/sanitize-html": "npm:~2.16.0" emojione: "npm:^4.5.0" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" jest: "npm:~30.2.0" jest-qase-reporter: "npm:^2.1.4" marked: "npm:^16.1.2" @@ -8434,7 +8568,6 @@ __metadata: "@rocket.chat/apps-engine": "workspace:^" "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/emitter": "npm:^0.32.0" - "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/fuselage": "npm:^0.71.0" "@rocket.chat/fuselage-hooks": "npm:^0.39.0" "@rocket.chat/fuselage-tokens": "npm:~0.33.2" @@ -8462,8 +8595,7 @@ __metadata: "@types/react": "npm:~18.3.27" "@types/react-dom": "npm:~18.3.7" cross-env: "npm:^7.0.3" - eslint: "npm:~8.45.0" - eslint-plugin-storybook: "npm:~0.11.6" + eslint: "npm:~9.39.3" i18next: "npm:~23.4.9" jest: "npm:~30.2.0" normalize.css: "npm:^8.0.1" @@ -8480,7 +8612,6 @@ __metadata: webpack: "npm:~5.99.9" peerDependencies: "@rocket.chat/apps-engine": "workspace:^" - "@rocket.chat/eslint-config": 0.7.0 "@rocket.chat/fuselage": "*" "@rocket.chat/fuselage-hooks": "*" "@rocket.chat/icons": "*" @@ -8552,15 +8683,9 @@ __metadata: "@types/katex": "npm:~0.16.8" "@types/react": "npm:~18.3.27" "@types/react-dom": "npm:~18.3.7" - "@typescript-eslint/eslint-plugin": "npm:~5.60.1" - "@typescript-eslint/parser": "npm:~5.60.1" date-fns: "npm:~4.1.0" dompurify: "npm:~3.3.1" - eslint: "npm:~8.45.0" - eslint-plugin-anti-trojan-source: "npm:~1.1.2" - eslint-plugin-react: "npm:~7.37.5" - eslint-plugin-react-hooks: "npm:~5.0.0" - eslint-plugin-storybook: "npm:~0.11.6" + eslint: "npm:~9.39.3" highlight.js: "npm:11.8.0" i18next: "npm:~23.4.9" identity-obj-proxy: "npm:^3.0.0" @@ -8596,7 +8721,6 @@ __metadata: resolution: "@rocket.chat/http-router@workspace:packages/http-router" dependencies: "@rocket.chat/core-typings": "workspace:^" - "@rocket.chat/eslint-config": "workspace:~" "@rocket.chat/jest-presets": "workspace:^" "@rocket.chat/logger": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" @@ -8605,7 +8729,7 @@ __metadata: "@types/jest": "npm:~30.0.0" "@types/supertest": "npm:~6.0.3" ajv: "npm:^8.17.1" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" express: "npm:^4.21.2" hono: "npm:^4.11.9" jest: "npm:~30.2.0" @@ -8623,7 +8747,7 @@ __metadata: "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/tools": "workspace:~" "@rocket.chat/tsconfig": "workspace:*" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" i18next: "npm:~23.4.9" jest: "npm:~30.2.0" typescript: "npm:~5.9.3" @@ -8644,11 +8768,10 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/instance-status@workspace:packages/instance-status" dependencies: - "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/models": "workspace:^" "@rocket.chat/tracing": "workspace:^" "@rocket.chat/tsconfig": "workspace:*" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" mongodb: "npm:6.16.0" prettier: "npm:~3.3.3" typescript: "npm:~5.9.3" @@ -8659,7 +8782,6 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/jest-presets@workspace:packages/jest-presets" dependencies: - "@rocket.chat/eslint-config": "workspace:~" "@swc/core": "npm:1.15.11" "@swc/jest": "npm:~0.2.39" "@testing-library/jest-dom": "npm:~6.8.0" @@ -8667,7 +8789,7 @@ __metadata: "@types/jest": "npm:~30.0.0" "@types/jest-axe": "npm:~3.5.9" "@types/uuid": "npm:^10.0.0" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" identity-obj-proxy: "npm:~3.0.0" jest: "npm:~30.2.0" jest-axe: "npm:~10.0.0" @@ -8685,7 +8807,7 @@ __metadata: "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/tsconfig": "workspace:*" "@types/jest": "npm:~30.0.0" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" jest: "npm:~30.2.0" jose: "npm:^4.15.9" typescript: "npm:~5.9.3" @@ -8717,7 +8839,7 @@ __metadata: "@types/jest": "npm:~30.0.0" "@types/ws": "npm:^8.5.13" bcrypt: "npm:^5.1.1" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" jest: "npm:~30.2.0" jest-websocket-mock: "npm:~2.5.0" typescript: "npm:~5.9.3" @@ -8729,14 +8851,12 @@ __metadata: resolution: "@rocket.chat/livechat@workspace:packages/livechat" dependencies: "@babel/core": "npm:~7.28.6" - "@babel/eslint-parser": "npm:~7.28.6" "@babel/preset-env": "npm:~7.28.6" "@babel/preset-react": "npm:~7.27.1" "@babel/preset-typescript": "npm:~7.27.1" "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/ddp-client": "workspace:^" "@rocket.chat/emitter": "npm:^0.32.0" - "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/fuselage-hooks": "npm:^0.39.0" "@rocket.chat/fuselage-tokens": "npm:~0.33.2" "@rocket.chat/gazzodown": "workspace:^" @@ -8756,8 +8876,6 @@ __metadata: "@types/react": "npm:~18.3.27" "@types/webpack-env": "npm:~1.18.8" "@types/whatwg-fetch": "npm:~0.0.33" - "@typescript-eslint/eslint-plugin": "npm:~5.60.1" - "@typescript-eslint/parser": "npm:~5.60.1" ajv: "npm:^8.17.1" ajv-formats: "npm:^3.0.1" autoprefixer: "npm:^9.8.8" @@ -8770,11 +8888,7 @@ __metadata: desvg-loader: "npm:^0.1.0" dompurify: "npm:~3.3.1" emoji-mart: "npm:^3.0.1" - eslint: "npm:~8.45.0" - eslint-plugin-import: "npm:~2.31.0" - eslint-plugin-react: "npm:~7.37.5" - eslint-plugin-react-hooks: "npm:~5.0.0" - eslint-plugin-storybook: "npm:~0.11.6" + eslint: "npm:~9.39.3" file-loader: "npm:^6.2.0" history: "npm:~5.3.0" html-webpack-plugin: "npm:~5.6.6" @@ -8831,7 +8945,7 @@ __metadata: "@types/ejson": "npm:^2.2.2" chalk: "npm:^4.1.2" ejson: "npm:^2.2.3" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" typescript: "npm:~5.9.3" languageName: unknown linkType: soft @@ -8841,7 +8955,7 @@ __metadata: resolution: "@rocket.chat/logger@workspace:packages/logger" dependencies: "@rocket.chat/emitter": "npm:^0.32.0" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" pino: "npm:^8.21.0" typescript: "npm:~5.9.3" languageName: unknown @@ -8872,7 +8986,7 @@ __metadata: "@rocket.chat/tsconfig": "workspace:*" "@types/jest": "npm:~30.0.0" drachtio-srf: "patch:drachtio-srf@npm%3A5.0.12#~/.yarn/patches/drachtio-srf-npm-5.0.12-b0b1afaad6.patch" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" typescript: "npm:~5.9.3" languageName: unknown linkType: soft @@ -8886,7 +9000,7 @@ __metadata: "@rocket.chat/tsconfig": "workspace:*" "@types/jest": "npm:~30.0.0" ajv: "npm:^8.17.1" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" jest: "npm:~30.2.0" typescript: "npm:~5.9.3" languageName: unknown @@ -8903,14 +9017,12 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/message-parser@workspace:packages/message-parser" dependencies: - "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/peggy-loader": "workspace:~" "@rocket.chat/prettier-config": "npm:~0.31.25" "@types/jest": "npm:~30.0.0" "@types/node": "npm:~22.16.5" - "@typescript-eslint/parser": "npm:~5.60.1" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" jest: "npm:~30.2.0" npm-run-all: "npm:^4.1.5" peggy: "npm:4.1.1" @@ -8931,9 +9043,8 @@ __metadata: resolution: "@rocket.chat/message-types@workspace:packages/message-types" dependencies: "@rocket.chat/core-typings": "workspace:~" - "@rocket.chat/eslint-config": "workspace:~" date-fns: "npm:~4.1.0" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" i18next: "npm:~23.4.9" jest: "npm:~30.2.0" moment: "npm:^2.30.1" @@ -8950,7 +9061,6 @@ __metadata: dependencies: "@axe-core/playwright": "npm:^4.10.2" "@babel/core": "npm:~7.28.6" - "@babel/eslint-parser": "npm:~7.28.6" "@babel/preset-env": "npm:~7.28.6" "@babel/preset-react": "npm:~7.27.1" "@babel/register": "npm:~7.28.6" @@ -8989,7 +9099,6 @@ __metadata: "@rocket.chat/css-in-js": "npm:~0.31.25" "@rocket.chat/desktop-api": "workspace:~" "@rocket.chat/emitter": "npm:^0.32.0" - "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/favicon": "workspace:^" "@rocket.chat/federation-matrix": "workspace:^" "@rocket.chat/federation-sdk": "npm:0.3.9" @@ -9134,8 +9243,6 @@ __metadata: "@types/underscore": "npm:^1.13.0" "@types/xml-crypto": "npm:~1.4.6" "@types/xml-encryption": "npm:~1.2.4" - "@typescript-eslint/eslint-plugin": "npm:~5.60.1" - "@typescript-eslint/parser": "npm:~5.60.1" "@xmldom/xmldom": "npm:~0.8.11" adm-zip: "npm:0.5.16" ajv: "npm:^8.17.1" @@ -9185,18 +9292,7 @@ __metadata: emojione: "npm:^4.5.0" emojione-assets: "npm:^4.5.0" esl: "npm:^11.2.1" - eslint: "npm:~8.45.0" - eslint-config-prettier: "npm:~9.1.2" - eslint-plugin-anti-trojan-source: "npm:~1.1.2" - eslint-plugin-import: "npm:~2.31.0" - eslint-plugin-no-floating-promise: "npm:~2.0.0" - eslint-plugin-playwright: "npm:~2.2.2" - eslint-plugin-prettier: "npm:~5.2.6" - eslint-plugin-react: "npm:~7.37.5" - eslint-plugin-react-hooks: "npm:~5.0.0" - eslint-plugin-storybook: "npm:~0.11.6" - eslint-plugin-testing-library: "npm:~6.4.0" - eslint-plugin-you-dont-need-lodash-underscore: "npm:~6.14.0" + eslint: "npm:~9.39.3" eventemitter3: "npm:^5.0.4" exif-be-gone: "npm:^1.5.1" expiry-map: "npm:^2.0.0" @@ -9365,7 +9461,7 @@ __metadata: "@testing-library/react": "npm:~16.3.2" "@types/react": "npm:~18.3.27" "@types/react-dom": "npm:~18.3.7" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" i18next: "npm:~23.4.9" jest: "npm:~30.2.0" react: "npm:~18.3.1" @@ -9385,7 +9481,7 @@ __metadata: dependencies: "@rocket.chat/core-typings": "workspace:^" "@types/node-rsa": "npm:^1.1.4" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" mongodb: "npm:6.16.0" typescript: "npm:~5.9.3" languageName: unknown @@ -9406,7 +9502,7 @@ __metadata: "@types/jest": "npm:~30.0.0" "@types/node-rsa": "npm:^1.1.4" date-fns: "npm:~4.1.0" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" jest: "npm:~30.2.0" node-rsa: "npm:^1.1.1" typescript: "npm:~5.9.3" @@ -9418,7 +9514,7 @@ __metadata: resolution: "@rocket.chat/mongo-adapter@workspace:packages/mongo-adapter" dependencies: "@rocket.chat/jest-presets": "workspace:~" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" jest: "npm:~30.2.0" mongodb: "npm:6.16.0" typescript: "npm:~5.9.3" @@ -9439,7 +9535,6 @@ __metadata: resolution: "@rocket.chat/network-broker@workspace:ee/packages/network-broker" dependencies: "@rocket.chat/core-services": "workspace:^" - "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/tsconfig": "workspace:*" "@types/chai": "npm:~4.3.20" "@types/ejson": "npm:^2.2.2" @@ -9447,7 +9542,7 @@ __metadata: "@types/sinon": "npm:^10.0.20" chai: "npm:^4.5.0" ejson: "npm:^2.2.3" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" jest: "npm:~30.2.0" moleculer: "npm:^0.14.35" pino: "npm:^8.21.0" @@ -9461,14 +9556,13 @@ __metadata: resolution: "@rocket.chat/omni-core-ee@workspace:ee/packages/omni-core-ee" dependencies: "@rocket.chat/core-services": "workspace:^" - "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/logger": "workspace:^" "@rocket.chat/models": "workspace:^" "@rocket.chat/omni-core": "workspace:^" "@rocket.chat/tsconfig": "workspace:*" "@types/jest": "npm:~30.0.0" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" jest: "npm:~30.2.0" mem: "npm:^8.1.1" mongodb: "npm:6.16.0" @@ -9480,14 +9574,13 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/omni-core@workspace:packages/omni-core" dependencies: - "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/models": "workspace:^" "@rocket.chat/patch-injection": "workspace:^" "@rocket.chat/tools": "workspace:*" "@rocket.chat/tsconfig": "workspace:*" "@types/jest": "npm:~30.0.0" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" jest: "npm:~30.2.0" mongodb: "npm:6.16.0" typescript: "npm:~5.9.3" @@ -9501,7 +9594,6 @@ __metadata: "@rocket.chat/core-services": "workspace:^" "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/emitter": "npm:^0.32.0" - "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/i18n": "workspace:^" "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/logger": "workspace:^" @@ -9518,7 +9610,7 @@ __metadata: date-fns: "npm:~4.1.0" ejson: "npm:^2.2.3" emoji-toolkit: "npm:^7.0.1" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" eventemitter3: "npm:^5.0.4" i18next: "npm:~23.4.9" jest: "npm:~30.2.0" @@ -9539,7 +9631,6 @@ __metadata: "@rocket.chat/core-services": "workspace:^" "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/emitter": "npm:^0.32.0" - "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/i18n": "workspace:^" "@rocket.chat/logger": "workspace:^" "@rocket.chat/model-typings": "workspace:^" @@ -9557,7 +9648,7 @@ __metadata: "@types/react": "npm:~18.3.27" ejson: "npm:^2.2.3" emoji-toolkit: "npm:^7.0.1" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" eventemitter3: "npm:^5.0.4" i18next: "npm:~23.4.9" i18next-sprintf-postprocessor: "npm:^0.2.2" @@ -9603,7 +9694,7 @@ __metadata: "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/tsconfig": "workspace:*" "@types/jest": "npm:~30.0.0" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" jest: "npm:~30.2.0" typescript: "npm:~5.9.3" languageName: unknown @@ -9616,7 +9707,7 @@ __metadata: "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/tsconfig": "workspace:*" "@types/jest": "npm:~30.0.0" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" jest: "npm:~30.2.0" typescript: "npm:~5.9.3" languageName: unknown @@ -9644,8 +9735,7 @@ __metadata: "@types/react-dom": "npm:~18.3.7" buffer: "npm:~6.0.3" emoji-toolkit: "npm:^7.0.1" - eslint: "npm:~8.45.0" - eslint-plugin-storybook: "npm:~0.11.6" + eslint: "npm:~9.39.3" i18next: "npm:~23.4.9" jest: "npm:~30.2.0" moment: "npm:^2.30.1" @@ -9662,10 +9752,9 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/peggy-loader@workspace:packages/peggy-loader" dependencies: - "@rocket.chat/eslint-config": "workspace:~" "@rocket.chat/prettier-config": "npm:~0.31.25" "@types/node": "npm:~22.16.5" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" npm-run-all: "npm:^4.1.5" peggy: "npm:4.1.1" prettier: "npm:~3.3.3" @@ -9682,7 +9771,6 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/poplib@workspace:packages/node-poplib" dependencies: - eslint: "npm:~8.45.0" typescript: "npm:~5.9.3" languageName: unknown linkType: soft @@ -9694,7 +9782,6 @@ __metadata: "@rocket.chat/core-services": "workspace:^" "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/emitter": "npm:^0.32.0" - "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/logger": "workspace:^" "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" @@ -9707,7 +9794,7 @@ __metadata: "@types/polka": "npm:^0.5.8" "@types/prometheus-gc-stats": "npm:^0.6.4" ejson: "npm:^2.2.3" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" eventemitter3: "npm:^5.0.4" mem: "npm:^8.1.1" moleculer: "npm:^0.14.35" @@ -9728,11 +9815,10 @@ __metadata: "@rocket.chat/apps-engine": "workspace:^" "@rocket.chat/core-services": "workspace:^" "@rocket.chat/core-typings": "workspace:^" - "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/models": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" "@types/node": "npm:~22.16.5" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" jest: "npm:~30.2.0" mongodb: "npm:6.16.0" typescript: "npm:~5.9.3" @@ -9755,7 +9841,6 @@ __metadata: "@rocket.chat/core-services": "workspace:^" "@rocket.chat/core-typings": "workspace:^" "@rocket.chat/emitter": "npm:^0.32.0" - "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/logger": "workspace:^" "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" @@ -9768,7 +9853,7 @@ __metadata: "@types/prometheus-gc-stats": "npm:^0.6.4" ejson: "npm:^2.2.3" emoji-toolkit: "npm:^7.0.1" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" eventemitter3: "npm:^5.0.4" mem: "npm:^8.1.1" moleculer: "npm:^0.14.35" @@ -9788,10 +9873,9 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/random@workspace:packages/random" dependencies: - "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/tsconfig": "workspace:*" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" jest: "npm:~30.2.0" typescript: "npm:~5.9.3" languageName: unknown @@ -9806,9 +9890,8 @@ __metadata: "@actions/github": "npm:^6.0.1" "@octokit/core": "npm:^5.0.1" "@octokit/plugin-throttling": "npm:^6.1.0" - "@rocket.chat/eslint-config": "workspace:^" "@types/node": "npm:~22.16.5" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" mdast-util-to-string: "npm:2.0.0" remark-parse: "npm:9.0.0" remark-stringify: "npm:9.0.1" @@ -9823,10 +9906,9 @@ __metadata: resolution: "@rocket.chat/release-changelog@workspace:packages/release-changelog" dependencies: "@changesets/types": "npm:^6.0.0" - "@rocket.chat/eslint-config": "workspace:^" "@types/node": "npm:~22.16.5" dataloader: "npm:^2.2.3" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" node-fetch: "npm:^2.7.0" typescript: "npm:~5.9.3" languageName: unknown @@ -9838,13 +9920,12 @@ __metadata: dependencies: "@rocket.chat/apps-engine": "workspace:^" "@rocket.chat/core-typings": "workspace:^" - "@rocket.chat/eslint-config": "workspace:~" "@rocket.chat/message-parser": "workspace:^" "@rocket.chat/ui-kit": "workspace:~" "@types/jest": "npm:~30.0.0" ajv: "npm:^8.17.1" ajv-formats: "npm:^3.0.1" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" mongodb: "npm:6.16.0" typescript: "npm:~5.9.3" languageName: unknown @@ -9855,7 +9936,7 @@ __metadata: resolution: "@rocket.chat/server-cloud-communication@workspace:packages/server-cloud-communication" dependencies: "@rocket.chat/license": "workspace:^" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" typescript: "npm:~5.9.3" languageName: unknown linkType: soft @@ -9870,7 +9951,7 @@ __metadata: "@types/jest": "npm:^29.5.5" "@types/node-fetch": "npm:~2.6.13" "@types/proxy-from-env": "npm:^1.0.4" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" http-proxy-agent: "npm:^7.0.2" https-proxy-agent: "npm:^7.0.6" jest: "npm:^29.7.0" @@ -9884,10 +9965,9 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/sha256@workspace:packages/sha256" dependencies: - "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/tsconfig": "workspace:*" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" jest: "npm:~30.2.0" typescript: "npm:~5.9.3" languageName: unknown @@ -9898,7 +9978,6 @@ __metadata: resolution: "@rocket.chat/storybook-config@workspace:packages/storybook-config" dependencies: "@rocket.chat/emitter": "npm:^0.32.0" - "@rocket.chat/eslint-config": "workspace:~" "@rocket.chat/fuselage": "npm:^0.71.0" "@rocket.chat/fuselage-hooks": "npm:^0.39.0" "@rocket.chat/fuselage-tokens": "npm:~0.33.2" @@ -9913,7 +9992,7 @@ __metadata: "@storybook/react": "npm:^8.6.15" "@storybook/react-webpack5": "npm:^8.6.15" "@storybook/theming": "npm:^8.6.15" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" react: "npm:~18.3.1" react-dom: "npm:~18.3.1" react-virtuoso: "npm:^4.12.0" @@ -9963,7 +10042,7 @@ __metadata: "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/tsconfig": "workspace:*" "@types/jest": "npm:~30.0.0" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" jest: "npm:~30.2.0" moment-timezone: "npm:^0.5.48" typescript: "npm:~5.9.3" @@ -9978,7 +10057,7 @@ __metadata: "@opentelemetry/exporter-trace-otlp-grpc": "npm:^0.54.2" "@opentelemetry/sdk-node": "npm:^0.54.2" "@types/jest": "npm:~30.0.0" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" jest: "npm:~30.2.0" ts-jest: "npm:~29.4.6" typescript: "npm:~5.9.3" @@ -10004,11 +10083,7 @@ __metadata: "@rocket.chat/ui-contexts": "workspace:^" "@types/react": "npm:~18.3.27" "@types/react-dom": "npm:~18.3.7" - eslint: "npm:~8.45.0" - eslint-plugin-react: "npm:~7.37.5" - eslint-plugin-react-hooks: "npm:~5.0.0" - eslint-plugin-storybook: "npm:~0.11.6" - eslint-plugin-testing-library: "npm:~6.4.0" + eslint: "npm:~9.39.3" react: "npm:~18.3.1" react-dom: "npm:~18.3.1" react-virtuoso: "npm:^4.12.0" @@ -10048,12 +10123,7 @@ __metadata: "@types/react": "npm:~18.3.27" "@types/react-dom": "npm:~18.3.7" dompurify: "npm:~3.3.1" - eslint: "npm:~8.45.0" - eslint-plugin-anti-trojan-source: "npm:~1.1.2" - eslint-plugin-react: "npm:~7.37.5" - eslint-plugin-react-hooks: "npm:~5.0.0" - eslint-plugin-storybook: "npm:~0.11.6" - eslint-plugin-testing-library: "npm:~6.4.0" + eslint: "npm:~9.39.3" jest: "npm:~30.2.0" overlayscrollbars: "npm:^2.11.4" overlayscrollbars-react: "npm:^0.5.6" @@ -10086,7 +10156,6 @@ __metadata: dependencies: "@react-aria/toolbar": "npm:^3.0.0-nightly.5042" "@rocket.chat/emitter": "npm:^0.32.0" - "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/fuselage": "npm:^0.71.0" "@rocket.chat/fuselage-hooks": "npm:^0.39.0" "@rocket.chat/fuselage-tokens": "npm:~0.33.2" @@ -10104,10 +10173,7 @@ __metadata: "@types/jest": "npm:~30.0.0" "@types/react": "npm:~18.3.27" "@types/react-dom": "npm:~18.3.7" - eslint: "npm:~8.45.0" - eslint-plugin-react: "npm:~7.37.5" - eslint-plugin-react-hooks: "npm:~5.0.0" - eslint-plugin-storybook: "npm:~0.11.6" + eslint: "npm:~9.39.3" jest: "npm:~30.2.0" react: "npm:~18.3.1" react-dom: "npm:~18.3.1" @@ -10143,8 +10209,7 @@ __metadata: "@types/jest": "npm:~30.0.0" "@types/react": "npm:~18.3.27" "@types/react-dom": "npm:~18.3.7" - eslint: "npm:~8.45.0" - eslint-plugin-react-hooks: "npm:^5.0.0" + eslint: "npm:~9.39.3" i18next: "npm:~23.4.9" jest: "npm:~30.2.0" mongodb: "npm:6.16.0" @@ -10165,12 +10230,11 @@ __metadata: version: 0.0.0-use.local resolution: "@rocket.chat/ui-kit@workspace:packages/ui-kit" dependencies: - "@rocket.chat/eslint-config": "workspace:~" "@rocket.chat/icons": "npm:~0.46.0" "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/tsconfig": "workspace:*" "@types/jest": "npm:~30.0.0" - eslint: "npm:~8.45.0" + eslint: "npm:~9.39.3" jest: "npm:~30.2.0" npm-run-all: "npm:~4.1.5" prettier: "npm:~3.3.3" @@ -10197,11 +10261,7 @@ __metadata: "@rocket.chat/ui-contexts": "workspace:~" "@types/react": "npm:~18.3.27" "@types/react-dom": "npm:~18.3.7" - eslint: "npm:~8.45.0" - eslint-plugin-anti-trojan-source: "npm:~1.1.2" - eslint-plugin-react: "npm:~7.37.5" - eslint-plugin-react-hooks: "npm:~5.0.0" - eslint-plugin-testing-library: "npm:^6.4.0" + eslint: "npm:~9.39.3" react: "npm:~18.3.1" react-docgen-typescript-plugin: "npm:~1.0.8" react-dom: "npm:~18.3.1" @@ -10223,7 +10283,6 @@ __metadata: dependencies: "@rocket.chat/css-in-js": "npm:~0.31.25" "@rocket.chat/emitter": "npm:^0.32.0" - "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/fuselage": "npm:^0.71.0" "@rocket.chat/fuselage-hooks": "npm:^0.39.0" "@rocket.chat/fuselage-tokens": "npm:~0.33.2" @@ -10245,10 +10304,7 @@ __metadata: "@types/jest-axe": "npm:~3.5.9" "@types/react": "npm:~18.3.27" "@types/react-dom": "npm:~18.3.7" - eslint: "npm:~8.45.0" - eslint-plugin-react: "npm:~7.37.5" - eslint-plugin-react-hooks: "npm:~5.0.0" - eslint-plugin-storybook: "npm:~0.11.6" + eslint: "npm:~9.39.3" jest: "npm:~30.2.0" jest-axe: "npm:~10.0.0" react: "npm:~18.3.1" @@ -10280,7 +10336,6 @@ __metadata: "@rocket.chat/css-in-js": "npm:~0.31.25" "@rocket.chat/desktop-api": "workspace:^" "@rocket.chat/emitter": "npm:^0.32.0" - "@rocket.chat/eslint-config": "workspace:^" "@rocket.chat/fuselage": "npm:^0.71.0" "@rocket.chat/fuselage-hooks": "npm:^0.39.0" "@rocket.chat/fuselage-tokens": "npm:~0.33.2" @@ -10314,10 +10369,7 @@ __metadata: "@types/react": "npm:~18.3.27" "@types/react-dom": "npm:~18.3.7" date-fns: "npm:~4.1.0" - eslint: "npm:~8.45.0" - eslint-plugin-react: "npm:~7.37.5" - eslint-plugin-react-hooks: "npm:~5.0.0" - eslint-plugin-storybook: "npm:~0.11.6" + eslint: "npm:~9.39.3" i18next: "npm:~23.4.9" jest: "npm:~30.2.0" jest-axe: "npm:~10.0.0" @@ -10370,14 +10422,9 @@ __metadata: "@types/react": "npm:~18.3.27" "@types/react-beautiful-dnd": "npm:^13.1.8" "@types/react-dom": "npm:~18.3.7" - "@typescript-eslint/eslint-plugin": "npm:~5.60.1" - "@typescript-eslint/parser": "npm:~5.60.1" "@vitejs/plugin-react": "npm:~4.5.2" codemirror: "npm:^6.0.2" - eslint: "npm:~8.45.0" - eslint-plugin-react: "npm:~7.37.5" - eslint-plugin-react-hooks: "npm:^5.0.0" - eslint-plugin-react-refresh: "npm:^0.4.26" + eslint: "npm:~9.39.3" eslint4b-prebuilt: "npm:^6.7.2" moment: "npm:^2.30.1" prettier: "npm:~3.3.3" @@ -10425,8 +10472,7 @@ __metadata: "@types/react": "npm:~18.3.27" "@types/react-dom": "npm:~18.3.7" dompurify: "npm:~3.3.1" - eslint: "npm:~8.45.0" - eslint-plugin-storybook: "npm:~0.11.6" + eslint: "npm:~9.39.3" i18next: "npm:~23.4.9" react: "npm:~18.3.1" react-dom: "npm:~18.3.1" @@ -13531,7 +13577,7 @@ __metadata: languageName: node linkType: hard -"@types/semver@npm:^7.3.12, @types/semver@npm:^7.3.4, @types/semver@npm:^7.5.8": +"@types/semver@npm:^7.3.4, @types/semver@npm:^7.5.8": version: 7.5.8 resolution: "@types/semver@npm:7.5.8" checksum: 10/3496808818ddb36deabfe4974fd343a78101fa242c4690044ccdc3b95dcf8785b494f5d628f2f47f38a702f8db9c53c67f47d7818f2be1b79f2efb09692e1178 @@ -13902,247 +13948,138 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/eslint-plugin@npm:~5.60.1": - version: 5.60.1 - resolution: "@typescript-eslint/eslint-plugin@npm:5.60.1" +"@typescript-eslint/eslint-plugin@npm:8.56.1": + version: 8.56.1 + resolution: "@typescript-eslint/eslint-plugin@npm:8.56.1" dependencies: - "@eslint-community/regexpp": "npm:^4.4.0" - "@typescript-eslint/scope-manager": "npm:5.60.1" - "@typescript-eslint/type-utils": "npm:5.60.1" - "@typescript-eslint/utils": "npm:5.60.1" - debug: "npm:^4.3.4" - grapheme-splitter: "npm:^1.0.4" - ignore: "npm:^5.2.0" - natural-compare-lite: "npm:^1.4.0" - semver: "npm:^7.3.7" - tsutils: "npm:^3.21.0" + "@eslint-community/regexpp": "npm:^4.12.2" + "@typescript-eslint/scope-manager": "npm:8.56.1" + "@typescript-eslint/type-utils": "npm:8.56.1" + "@typescript-eslint/utils": "npm:8.56.1" + "@typescript-eslint/visitor-keys": "npm:8.56.1" + ignore: "npm:^7.0.5" + natural-compare: "npm:^1.4.0" + ts-api-utils: "npm:^2.4.0" peerDependencies: - "@typescript-eslint/parser": ^5.0.0 - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - typescript: - optional: true - checksum: 10/0d05b59a14a96f72feb32060d7776387d1d447a49cdfd796291e98354c118f1e60ff8088945ce18060d66496e61b37346fdd84d1077f25b56755402448a99a80 + "@typescript-eslint/parser": ^8.56.1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.0.0" + checksum: 10/669d19cff91fcad5fe34dba97cc8c0c2df3160ae14646759fb23dfd6ffbb861d00d8d081e74d1060d544bfba0ea4d05588c5b73ae104907af26cc18189c0d139 languageName: node linkType: hard -"@typescript-eslint/parser@npm:~5.60.1": - version: 5.60.1 - resolution: "@typescript-eslint/parser@npm:5.60.1" +"@typescript-eslint/parser@npm:8.56.1": + version: 8.56.1 + resolution: "@typescript-eslint/parser@npm:8.56.1" dependencies: - "@typescript-eslint/scope-manager": "npm:5.60.1" - "@typescript-eslint/types": "npm:5.60.1" - "@typescript-eslint/typescript-estree": "npm:5.60.1" - debug: "npm:^4.3.4" + "@typescript-eslint/scope-manager": "npm:8.56.1" + "@typescript-eslint/types": "npm:8.56.1" + "@typescript-eslint/typescript-estree": "npm:8.56.1" + "@typescript-eslint/visitor-keys": "npm:8.56.1" + debug: "npm:^4.4.3" peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - typescript: - optional: true - checksum: 10/4117ce76b1b177278e6e1554f10978c4ae558f5b12d758626979dd78f63228aefabb36148eddd2a050877e8d013bfc33bc2451a8c9a63ed2f208911cbb34a3d5 - languageName: node - linkType: hard - -"@typescript-eslint/scope-manager@npm:5.60.1": - version: 5.60.1 - resolution: "@typescript-eslint/scope-manager@npm:5.60.1" - dependencies: - "@typescript-eslint/types": "npm:5.60.1" - "@typescript-eslint/visitor-keys": "npm:5.60.1" - checksum: 10/0fb618d6895e5a742ec61cb0d0f3a5e3d9e5c76e6aa876b90eaacd546295ef9e8f43d8e72331d4daf7b4fd926364a78d4d6a5e8e29579a35361fd5f13903fce8 - languageName: node - linkType: hard - -"@typescript-eslint/scope-manager@npm:5.62.0": - version: 5.62.0 - resolution: "@typescript-eslint/scope-manager@npm:5.62.0" - dependencies: - "@typescript-eslint/types": "npm:5.62.0" - "@typescript-eslint/visitor-keys": "npm:5.62.0" - checksum: 10/e827770baa202223bc0387e2fd24f630690809e460435b7dc9af336c77322290a770d62bd5284260fa881c86074d6a9fd6c97b07382520b115f6786b8ed499da + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.0.0" + checksum: 10/280b041a69153caf9e721b307781830483dd39d881b02d993156635bd8600e30e6a816aaead8bdd662ae5149b8870aef7b3823d3b98ec974d924c23a786fb6d9 languageName: node linkType: hard -"@typescript-eslint/scope-manager@npm:8.12.2": - version: 8.12.2 - resolution: "@typescript-eslint/scope-manager@npm:8.12.2" +"@typescript-eslint/project-service@npm:8.56.1": + version: 8.56.1 + resolution: "@typescript-eslint/project-service@npm:8.56.1" dependencies: - "@typescript-eslint/types": "npm:8.12.2" - "@typescript-eslint/visitor-keys": "npm:8.12.2" - checksum: 10/a2cd6ad4b31f4d0ca6f94c4df8a94bdee762abd556686817ab4143d80a27506f43fbf96769b44e698d573784a464bfd78e0cbc17ac61c36a868e02311c754ce1 - languageName: node - linkType: hard - -"@typescript-eslint/type-utils@npm:5.60.1": - version: 5.60.1 - resolution: "@typescript-eslint/type-utils@npm:5.60.1" - dependencies: - "@typescript-eslint/typescript-estree": "npm:5.60.1" - "@typescript-eslint/utils": "npm:5.60.1" - debug: "npm:^4.3.4" - tsutils: "npm:^3.21.0" + "@typescript-eslint/tsconfig-utils": "npm:^8.56.1" + "@typescript-eslint/types": "npm:^8.56.1" + debug: "npm:^4.4.3" peerDependencies: - eslint: "*" - peerDependenciesMeta: - typescript: - optional: true - checksum: 10/415ef8a618379bdd241b1bdb699e842ce695e301fcbecbabdbe328b744217130b05a5464fdbfc0863a3d58c7fe9483e3c98f7115823692c5c8fda3ce0eb0554e - languageName: node - linkType: hard - -"@typescript-eslint/types@npm:5.60.1": - version: 5.60.1 - resolution: "@typescript-eslint/types@npm:5.60.1" - checksum: 10/69c2b31e6fb2a2bbe1b60e9470e39c2e0840e8e499d9ed7dab158f9191b0bc84a81ed7d437658ff9677372ca5b2b23dd9690265801c4c7ce03b20a7c60a252fa - languageName: node - linkType: hard - -"@typescript-eslint/types@npm:5.62.0": - version: 5.62.0 - resolution: "@typescript-eslint/types@npm:5.62.0" - checksum: 10/24e8443177be84823242d6729d56af2c4b47bfc664dd411a1d730506abf2150d6c31bdefbbc6d97c8f91043e3a50e0c698239dcb145b79bb6b0c34469aaf6c45 - languageName: node - linkType: hard - -"@typescript-eslint/types@npm:8.12.2": - version: 8.12.2 - resolution: "@typescript-eslint/types@npm:8.12.2" - checksum: 10/57981e5fa45b03a0398ffb82418fdb716f476aa0b9c17d96edeb7fd3e3f4a720466868af7c2a02ddca65c27e70bfaff50c523b2a570582c4645a2702e17dc94a + typescript: ">=4.8.4 <6.0.0" + checksum: 10/5e7fdc95aebcefc72fec77806bb0821a9a59e5e88f86d72b15ad011eb6110da05419b803875f021716a219fc7fb8517331a6736364344c8613a90209539a6d32 languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:5.60.1": - version: 5.60.1 - resolution: "@typescript-eslint/typescript-estree@npm:5.60.1" +"@typescript-eslint/scope-manager@npm:8.56.1, @typescript-eslint/scope-manager@npm:^8.56.0": + version: 8.56.1 + resolution: "@typescript-eslint/scope-manager@npm:8.56.1" dependencies: - "@typescript-eslint/types": "npm:5.60.1" - "@typescript-eslint/visitor-keys": "npm:5.60.1" - debug: "npm:^4.3.4" - globby: "npm:^11.1.0" - is-glob: "npm:^4.0.3" - semver: "npm:^7.3.7" - tsutils: "npm:^3.21.0" - peerDependenciesMeta: - typescript: - optional: true - checksum: 10/f67ad9bc86b17351d8ee9f1737521f8be39c8a73cb2f9b55be2c1e30c6d8d9f7f33293f450d24bef1ad7e18635007e01de4948a9b25ffc3be381b3cb1ba3353b + "@typescript-eslint/types": "npm:8.56.1" + "@typescript-eslint/visitor-keys": "npm:8.56.1" + checksum: 10/f358cf8bd32952eed005d4f34c1e95805baefe35abee96d866222b0eff8027cc02f831cee04b308707d74db2b415437a134191207b4213ee8acbc6d67a435616 languageName: node linkType: hard -"@typescript-eslint/typescript-estree@npm:5.62.0": - version: 5.62.0 - resolution: "@typescript-eslint/typescript-estree@npm:5.62.0" - dependencies: - "@typescript-eslint/types": "npm:5.62.0" - "@typescript-eslint/visitor-keys": "npm:5.62.0" - debug: "npm:^4.3.4" - globby: "npm:^11.1.0" - is-glob: "npm:^4.0.3" - semver: "npm:^7.3.7" - tsutils: "npm:^3.21.0" - peerDependenciesMeta: - typescript: - optional: true - checksum: 10/06c975eb5f44b43bd19fadc2e1023c50cf87038fe4c0dd989d4331c67b3ff509b17fa60a3251896668ab4d7322bdc56162a9926971218d2e1a1874d2bef9a52e - languageName: node - linkType: hard - -"@typescript-eslint/typescript-estree@npm:8.12.2": - version: 8.12.2 - resolution: "@typescript-eslint/typescript-estree@npm:8.12.2" - dependencies: - "@typescript-eslint/types": "npm:8.12.2" - "@typescript-eslint/visitor-keys": "npm:8.12.2" - debug: "npm:^4.3.4" - fast-glob: "npm:^3.3.2" - is-glob: "npm:^4.0.3" - minimatch: "npm:^9.0.4" - semver: "npm:^7.6.0" - ts-api-utils: "npm:^1.3.0" - peerDependenciesMeta: - typescript: - optional: true - checksum: 10/9995929ec4b66afa53d52c16f5cecd7c9aa45994f943c41e9ec91fe178593e83d9049ff056fe2638c3cf7da01476861eff0dc3cb76c314cc130458d3f828930d - languageName: node - linkType: hard - -"@typescript-eslint/utils@npm:5.60.1": - version: 5.60.1 - resolution: "@typescript-eslint/utils@npm:5.60.1" - dependencies: - "@eslint-community/eslint-utils": "npm:^4.2.0" - "@types/json-schema": "npm:^7.0.9" - "@types/semver": "npm:^7.3.12" - "@typescript-eslint/scope-manager": "npm:5.60.1" - "@typescript-eslint/types": "npm:5.60.1" - "@typescript-eslint/typescript-estree": "npm:5.60.1" - eslint-scope: "npm:^5.1.1" - semver: "npm:^7.3.7" +"@typescript-eslint/tsconfig-utils@npm:8.56.1, @typescript-eslint/tsconfig-utils@npm:^8.56.1": + version: 8.56.1 + resolution: "@typescript-eslint/tsconfig-utils@npm:8.56.1" peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - checksum: 10/7658936e2fc9bc0586efb40b372de77dc88f752208b94d3df1a0a4c236dfb438fa46796b392bc0e9e7fdf8b45002542c3a21d143442435bc9c6161f2c1ff9695 + typescript: ">=4.8.4 <6.0.0" + checksum: 10/d509f1ae4b14969173e498db6d15c833b6407db456c7fca9e25396798a35014229a73754691f353c4a99f5f0bbb4535b4144f42f84e596645a16d88a2022135f languageName: node linkType: hard -"@typescript-eslint/utils@npm:^5.62.0": - version: 5.62.0 - resolution: "@typescript-eslint/utils@npm:5.62.0" +"@typescript-eslint/type-utils@npm:8.56.1": + version: 8.56.1 + resolution: "@typescript-eslint/type-utils@npm:8.56.1" dependencies: - "@eslint-community/eslint-utils": "npm:^4.2.0" - "@types/json-schema": "npm:^7.0.9" - "@types/semver": "npm:^7.3.12" - "@typescript-eslint/scope-manager": "npm:5.62.0" - "@typescript-eslint/types": "npm:5.62.0" - "@typescript-eslint/typescript-estree": "npm:5.62.0" - eslint-scope: "npm:^5.1.1" - semver: "npm:^7.3.7" + "@typescript-eslint/types": "npm:8.56.1" + "@typescript-eslint/typescript-estree": "npm:8.56.1" + "@typescript-eslint/utils": "npm:8.56.1" + debug: "npm:^4.4.3" + ts-api-utils: "npm:^2.4.0" peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 - checksum: 10/15ef13e43998a082b15f85db979f8d3ceb1f9ce4467b8016c267b1738d5e7cdb12aa90faf4b4e6dd6486c236cf9d33c463200465cf25ff997dbc0f12358550a1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.0.0" + checksum: 10/2b07c674c26d797d05c05779ac5c89761b6b96680ecaf01440957727d12c6d06a2e48f0b139e45752eb4b53bf13c03940628656c519d362082b716d6a0ece6d9 languageName: node linkType: hard -"@typescript-eslint/utils@npm:^6.0.0 || ^7.0.0 || ^8.0.0, @typescript-eslint/utils@npm:^8.8.1": - version: 8.12.2 - resolution: "@typescript-eslint/utils@npm:8.12.2" - dependencies: - "@eslint-community/eslint-utils": "npm:^4.4.0" - "@typescript-eslint/scope-manager": "npm:8.12.2" - "@typescript-eslint/types": "npm:8.12.2" - "@typescript-eslint/typescript-estree": "npm:8.12.2" - peerDependencies: - eslint: ^8.57.0 || ^9.0.0 - checksum: 10/4588866ca43314692a0e685d8936c470dca4e6d119a4a1adefbc2fd54682ff081bc21d60bf4e8077d3668aa680bada851b88566264d09c92a840fe2e4feb331b +"@typescript-eslint/types@npm:8.56.1, @typescript-eslint/types@npm:^8.56.1": + version: 8.56.1 + resolution: "@typescript-eslint/types@npm:8.56.1" + checksum: 10/4bcffab5b0fd48adb731fcade86a776ca4a66e229defa5a282b58ba9c95af16ffc459a7d188e27c988a35be1f6fb5b812f9cf0952692eac38d5b3e87daafb20a languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:5.60.1": - version: 5.60.1 - resolution: "@typescript-eslint/visitor-keys@npm:5.60.1" +"@typescript-eslint/typescript-estree@npm:8.56.1": + version: 8.56.1 + resolution: "@typescript-eslint/typescript-estree@npm:8.56.1" dependencies: - "@typescript-eslint/types": "npm:5.60.1" - eslint-visitor-keys: "npm:^3.3.0" - checksum: 10/15826a8373f09885e9a4c761932580cadad1b596bf3638675bd23c3e0708d03e5d6337a7df925d702f1869684989c468ca2303600fb49672a5238c4a75b0db50 + "@typescript-eslint/project-service": "npm:8.56.1" + "@typescript-eslint/tsconfig-utils": "npm:8.56.1" + "@typescript-eslint/types": "npm:8.56.1" + "@typescript-eslint/visitor-keys": "npm:8.56.1" + debug: "npm:^4.4.3" + minimatch: "npm:^10.2.2" + semver: "npm:^7.7.3" + tinyglobby: "npm:^0.2.15" + ts-api-utils: "npm:^2.4.0" + peerDependencies: + typescript: ">=4.8.4 <6.0.0" + checksum: 10/af39dae0a8fada72295a11f0efb49f311241134b0a3d819100eeda6a2f92368844645873ba785de5513ad541cd9c2ba17b9bfed2679daac4682fa2a3b627f087 languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:5.62.0": - version: 5.62.0 - resolution: "@typescript-eslint/visitor-keys@npm:5.62.0" +"@typescript-eslint/utils@npm:8.56.1, @typescript-eslint/utils@npm:^8.0.0, @typescript-eslint/utils@npm:^8.48.0, @typescript-eslint/utils@npm:^8.56.0": + version: 8.56.1 + resolution: "@typescript-eslint/utils@npm:8.56.1" dependencies: - "@typescript-eslint/types": "npm:5.62.0" - eslint-visitor-keys: "npm:^3.3.0" - checksum: 10/dc613ab7569df9bbe0b2ca677635eb91839dfb2ca2c6fa47870a5da4f160db0b436f7ec0764362e756d4164e9445d49d5eb1ff0b87f4c058946ae9d8c92eb388 + "@eslint-community/eslint-utils": "npm:^4.9.1" + "@typescript-eslint/scope-manager": "npm:8.56.1" + "@typescript-eslint/types": "npm:8.56.1" + "@typescript-eslint/typescript-estree": "npm:8.56.1" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.0.0" + checksum: 10/528cbd187d8288a8cfce24a043f993b93711087f53d2b6f95cdd615a1a4087af1dab083a747761af97474a621c7b14f11c84ee50c250f31566ebc64cf73867fe languageName: node linkType: hard -"@typescript-eslint/visitor-keys@npm:8.12.2": - version: 8.12.2 - resolution: "@typescript-eslint/visitor-keys@npm:8.12.2" +"@typescript-eslint/visitor-keys@npm:8.56.1": + version: 8.56.1 + resolution: "@typescript-eslint/visitor-keys@npm:8.56.1" dependencies: - "@typescript-eslint/types": "npm:8.12.2" - eslint-visitor-keys: "npm:^3.4.3" - checksum: 10/42795ad1c71520a367e2b53c3511b6cf922dcee05d61f6b0ec56b71c0b89a58889e0c3282b1bb13befc69df07204d0e4e053436d0c2b808460ce310b58a2a92e + "@typescript-eslint/types": "npm:8.56.1" + eslint-visitor-keys: "npm:^5.0.0" + checksum: 10/efed6a9867e7be203eec543e5a65da5aaec96aae55fba6fe74a305bf600e57c707764835e82bb8eb541f49a9b70442ff1e1a0ecf3543c78c91b84dda43b95c53 languageName: node linkType: hard @@ -14760,7 +14697,7 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.12.1, acorn@npm:^8.14.0, acorn@npm:^8.4.1, acorn@npm:^8.8.2, acorn@npm:^8.9.0": +"acorn@npm:^8.12.1, acorn@npm:^8.14.0, acorn@npm:^8.4.1, acorn@npm:^8.8.2": version: 8.14.0 resolution: "acorn@npm:8.14.0" bin: @@ -14769,6 +14706,15 @@ __metadata: languageName: node linkType: hard +"acorn@npm:^8.15.0": + version: 8.16.0 + resolution: "acorn@npm:8.16.0" + bin: + acorn: bin/acorn + checksum: 10/690c673bb4d61b38ef82795fab58526471ad7f7e67c0e40c4ff1e10ecd80ce5312554ef633c9995bfc4e6d170cef165711f9ca9e49040b62c0c66fbf2dd3df2b + languageName: node + linkType: hard + "add-px-to-style@npm:1.0.0": version: 1.0.0 resolution: "add-px-to-style@npm:1.0.0" @@ -14867,7 +14813,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^6.10.0, ajv@npm:^6.12.3, ajv@npm:^6.12.4, ajv@npm:^6.12.5": +"ajv@npm:^6.12.3, ajv@npm:^6.12.4, ajv@npm:^6.12.5": version: 6.12.6 resolution: "ajv@npm:6.12.6" dependencies: @@ -14879,6 +14825,18 @@ __metadata: languageName: node linkType: hard +"ajv@npm:^6.14.0": + version: 6.14.0 + resolution: "ajv@npm:6.14.0" + dependencies: + fast-deep-equal: "npm:^3.1.1" + fast-json-stable-stringify: "npm:^2.0.0" + json-schema-traverse: "npm:^0.4.1" + uri-js: "npm:^4.2.2" + checksum: 10/c71f14dd2b6f2535d043f74019c8169f7aeb1106bafbb741af96f34fdbf932255c919ddd46344043d03b62ea0ccb319f83667ec5eedf612393f29054fe5ce4a5 + languageName: node + linkType: hard + "ajv@npm:^8.0.0, ajv@npm:^8.0.1, ajv@npm:^8.17.1, ajv@npm:^8.9.0": version: 8.17.1 resolution: "ajv@npm:8.17.1" @@ -15201,6 +15159,22 @@ __metadata: languageName: node linkType: hard +"array-includes@npm:^3.1.9": + version: 3.1.9 + resolution: "array-includes@npm:3.1.9" + dependencies: + call-bind: "npm:^1.0.8" + call-bound: "npm:^1.0.4" + define-properties: "npm:^1.2.1" + es-abstract: "npm:^1.24.0" + es-object-atoms: "npm:^1.1.1" + get-intrinsic: "npm:^1.3.0" + is-string: "npm:^1.1.1" + math-intrinsics: "npm:^1.1.0" + checksum: 10/8bfe9a58df74f326b4a76b04ee05c13d871759e888b4ee8f013145297cf5eb3c02cfa216067ebdaac5d74eb9763ac5cad77cdf2773b8ab475833701e032173aa + languageName: node + linkType: hard + "array-timsort@npm:^1.0.3": version: 1.0.3 resolution: "array-timsort@npm:1.0.3" @@ -15252,21 +15226,22 @@ __metadata: languageName: node linkType: hard -"array.prototype.findlastindex@npm:^1.2.5": - version: 1.2.5 - resolution: "array.prototype.findlastindex@npm:1.2.5" +"array.prototype.findlastindex@npm:^1.2.6": + version: 1.2.6 + resolution: "array.prototype.findlastindex@npm:1.2.6" dependencies: - call-bind: "npm:^1.0.7" + call-bind: "npm:^1.0.8" + call-bound: "npm:^1.0.4" define-properties: "npm:^1.2.1" - es-abstract: "npm:^1.23.2" + es-abstract: "npm:^1.23.9" es-errors: "npm:^1.3.0" - es-object-atoms: "npm:^1.0.0" - es-shim-unscopables: "npm:^1.0.2" - checksum: 10/7c5c821f357cd53ab6cc305de8086430dd8d7a2485db87b13f843e868055e9582b1fd338f02338f67fc3a1603ceaf9610dd2a470b0b506f9d18934780f95b246 + es-object-atoms: "npm:^1.1.1" + es-shim-unscopables: "npm:^1.1.0" + checksum: 10/5ddb6420e820bef6ddfdcc08ce780d0fd5e627e97457919c27e32359916de5a11ce12f7c55073555e503856618eaaa70845d6ca11dcba724766f38eb1c22f7a2 languageName: node linkType: hard -"array.prototype.flat@npm:^1.3.1, array.prototype.flat@npm:^1.3.2": +"array.prototype.flat@npm:^1.3.1": version: 1.3.2 resolution: "array.prototype.flat@npm:1.3.2" dependencies: @@ -15278,6 +15253,18 @@ __metadata: languageName: node linkType: hard +"array.prototype.flat@npm:^1.3.3": + version: 1.3.3 + resolution: "array.prototype.flat@npm:1.3.3" + dependencies: + call-bind: "npm:^1.0.8" + define-properties: "npm:^1.2.1" + es-abstract: "npm:^1.23.5" + es-shim-unscopables: "npm:^1.0.2" + checksum: 10/f9b992fa0775d8f7c97abc91eb7f7b2f0ed8430dd9aeb9fdc2967ac4760cdd7fc2ef7ead6528fef40c7261e4d790e117808ce0d3e7e89e91514d4963a531cd01 + languageName: node + linkType: hard + "array.prototype.flatmap@npm:^1.3.2, array.prototype.flatmap@npm:^1.3.3": version: 1.3.3 resolution: "array.prototype.flatmap@npm:1.3.3" @@ -15930,6 +15917,13 @@ __metadata: languageName: node linkType: hard +"balanced-match@npm:^4.0.2": + version: 4.0.4 + resolution: "balanced-match@npm:4.0.4" + checksum: 10/fb07bb66a0959c2843fc055838047e2a95ccebb837c519614afb067ebfdf2fa967ca8d712c35ced07f2cd26fc6f07964230b094891315ad74f11eba3d53178a0 + languageName: node + linkType: hard + "bare-addon-resolve@npm:^1.3.0": version: 1.9.4 resolution: "bare-addon-resolve@npm:1.9.4" @@ -16280,6 +16274,15 @@ __metadata: languageName: node linkType: hard +"brace-expansion@npm:^5.0.2": + version: 5.0.3 + resolution: "brace-expansion@npm:5.0.3" + dependencies: + balanced-match: "npm:^4.0.2" + checksum: 10/8ba7deae4ca333d52418d2cde3287ac23f44f7330d92c3ecd96a8941597bea8aab02227bd990944d6711dd549bcc6e550fe70be5d94aa02e2fdc88942f480c9b + languageName: node + linkType: hard + "braces@npm:^3.0.3, braces@npm:~3.0.2": version: 3.0.3 resolution: "braces@npm:3.0.3" @@ -18720,7 +18723,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4.4.3": +"debug@npm:4.4.3, debug@npm:^4.4.3": version: 4.4.3 resolution: "debug@npm:4.4.3" dependencies: @@ -19879,7 +19882,69 @@ __metadata: languageName: node linkType: hard -"es-abstract@npm:^1.17.5, es-abstract@npm:^1.19.1, es-abstract@npm:^1.22.1, es-abstract@npm:^1.23.2, es-abstract@npm:^1.23.3, es-abstract@npm:^1.23.5, es-abstract@npm:^1.23.6, es-abstract@npm:^1.23.9": +"es-abstract@npm:^1.17.5, es-abstract@npm:^1.24.0, es-abstract@npm:^1.24.1": + version: 1.24.1 + resolution: "es-abstract@npm:1.24.1" + dependencies: + array-buffer-byte-length: "npm:^1.0.2" + arraybuffer.prototype.slice: "npm:^1.0.4" + available-typed-arrays: "npm:^1.0.7" + call-bind: "npm:^1.0.8" + call-bound: "npm:^1.0.4" + data-view-buffer: "npm:^1.0.2" + data-view-byte-length: "npm:^1.0.2" + data-view-byte-offset: "npm:^1.0.1" + es-define-property: "npm:^1.0.1" + es-errors: "npm:^1.3.0" + es-object-atoms: "npm:^1.1.1" + es-set-tostringtag: "npm:^2.1.0" + es-to-primitive: "npm:^1.3.0" + function.prototype.name: "npm:^1.1.8" + get-intrinsic: "npm:^1.3.0" + get-proto: "npm:^1.0.1" + get-symbol-description: "npm:^1.1.0" + globalthis: "npm:^1.0.4" + gopd: "npm:^1.2.0" + has-property-descriptors: "npm:^1.0.2" + has-proto: "npm:^1.2.0" + has-symbols: "npm:^1.1.0" + hasown: "npm:^2.0.2" + internal-slot: "npm:^1.1.0" + is-array-buffer: "npm:^3.0.5" + is-callable: "npm:^1.2.7" + is-data-view: "npm:^1.0.2" + is-negative-zero: "npm:^2.0.3" + is-regex: "npm:^1.2.1" + is-set: "npm:^2.0.3" + is-shared-array-buffer: "npm:^1.0.4" + is-string: "npm:^1.1.1" + is-typed-array: "npm:^1.1.15" + is-weakref: "npm:^1.1.1" + math-intrinsics: "npm:^1.1.0" + object-inspect: "npm:^1.13.4" + object-keys: "npm:^1.1.1" + object.assign: "npm:^4.1.7" + own-keys: "npm:^1.0.1" + regexp.prototype.flags: "npm:^1.5.4" + safe-array-concat: "npm:^1.1.3" + safe-push-apply: "npm:^1.0.0" + safe-regex-test: "npm:^1.1.0" + set-proto: "npm:^1.0.0" + stop-iteration-iterator: "npm:^1.1.0" + string.prototype.trim: "npm:^1.2.10" + string.prototype.trimend: "npm:^1.0.9" + string.prototype.trimstart: "npm:^1.0.8" + typed-array-buffer: "npm:^1.0.3" + typed-array-byte-length: "npm:^1.0.3" + typed-array-byte-offset: "npm:^1.0.4" + typed-array-length: "npm:^1.0.7" + unbox-primitive: "npm:^1.1.0" + which-typed-array: "npm:^1.1.19" + checksum: 10/c84cb69ebae36781309a3ed70ff40b4767a921d3b3518060fac4e08f14ede04491b68e9f318aedf186e349d4af4a40f5d0e4111e46513800e8368551fd09de8c + languageName: node + linkType: hard + +"es-abstract@npm:^1.19.1, es-abstract@npm:^1.22.1, es-abstract@npm:^1.23.2, es-abstract@npm:^1.23.3, es-abstract@npm:^1.23.5, es-abstract@npm:^1.23.6, es-abstract@npm:^1.23.9": version: 1.23.9 resolution: "es-abstract@npm:1.23.9" dependencies: @@ -19977,26 +20042,26 @@ __metadata: linkType: hard "es-iterator-helpers@npm:^1.2.1": - version: 1.2.1 - resolution: "es-iterator-helpers@npm:1.2.1" + version: 1.2.2 + resolution: "es-iterator-helpers@npm:1.2.2" dependencies: call-bind: "npm:^1.0.8" - call-bound: "npm:^1.0.3" + call-bound: "npm:^1.0.4" define-properties: "npm:^1.2.1" - es-abstract: "npm:^1.23.6" + es-abstract: "npm:^1.24.1" es-errors: "npm:^1.3.0" - es-set-tostringtag: "npm:^2.0.3" + es-set-tostringtag: "npm:^2.1.0" function-bind: "npm:^1.1.2" - get-intrinsic: "npm:^1.2.6" + get-intrinsic: "npm:^1.3.0" globalthis: "npm:^1.0.4" gopd: "npm:^1.2.0" has-property-descriptors: "npm:^1.0.2" has-proto: "npm:^1.2.0" has-symbols: "npm:^1.1.0" internal-slot: "npm:^1.1.0" - iterator.prototype: "npm:^1.1.4" + iterator.prototype: "npm:^1.1.5" safe-array-concat: "npm:^1.1.3" - checksum: 10/802e0e8427a05ff4a5b0c70c7fdaaeff37cdb81a28694aeb7bfb831c6ab340d8f3deeb67b96732ff9e9699ea240524d5ea8a9a6a335fcd15aa3983b27b06113f + checksum: 10/17b5b2834c4f5719d6ce0e837a4d11c6ba4640bee28290d22ec4daf7106ec3d5fe0ff4f7e5dbaa2b4612e8335934360e964a8f08608d43f2889da106b25481ee languageName: node linkType: hard @@ -20016,7 +20081,7 @@ __metadata: languageName: node linkType: hard -"es-set-tostringtag@npm:^2.0.3, es-set-tostringtag@npm:^2.1.0": +"es-set-tostringtag@npm:^2.1.0": version: 2.1.0 resolution: "es-set-tostringtag@npm:2.1.0" dependencies: @@ -20037,6 +20102,15 @@ __metadata: languageName: node linkType: hard +"es-shim-unscopables@npm:^1.1.0": + version: 1.1.0 + resolution: "es-shim-unscopables@npm:1.1.0" + dependencies: + hasown: "npm:^2.0.2" + checksum: 10/c351f586c30bbabc62355be49564b2435468b52c3532b8a1663672e3d10dc300197e69c247869dd173e56d86423ab95fc0c10b0939cdae597094e0fdca078cba + languageName: node + linkType: hard + "es-to-primitive@npm:^1.3.0": version: 1.3.0 resolution: "es-to-primitive@npm:1.3.0" @@ -20379,14 +20453,14 @@ __metadata: languageName: node linkType: hard -"eslint-config-prettier@npm:~9.1.2": - version: 9.1.2 - resolution: "eslint-config-prettier@npm:9.1.2" +"eslint-config-prettier@npm:~10.1.8": + version: 10.1.8 + resolution: "eslint-config-prettier@npm:10.1.8" peerDependencies: eslint: ">=7.0.0" bin: eslint-config-prettier: bin/cli.js - checksum: 10/e4bba2d76df9dc6e2adca2866e544bfd1ff32232fc483743c04cedd93918a90a327b56d4a7e3f9d3fa32d90bd50b034d09df987860260064b18c026b8bbd15aa + checksum: 10/03f8e6ea1a6a9b8f9eeaf7c8c52a96499ec4b275b9ded33331a6cc738ed1d56de734097dbd0091f136f0e84bc197388bd8ec22a52a4658105883f8c8b7d8921a languageName: node linkType: hard @@ -20440,15 +20514,15 @@ __metadata: languageName: node linkType: hard -"eslint-module-utils@npm:^2.12.0": - version: 2.12.0 - resolution: "eslint-module-utils@npm:2.12.0" +"eslint-module-utils@npm:^2.12.1": + version: 2.12.1 + resolution: "eslint-module-utils@npm:2.12.1" dependencies: debug: "npm:^3.2.7" peerDependenciesMeta: eslint: optional: true - checksum: 10/dd27791147eca17366afcb83f47d6825b6ce164abb256681e5de4ec1d7e87d8605641eb869298a0dbc70665e2446dbcc2f40d3e1631a9475dd64dd23d4ca5dee + checksum: 10/bd25d6610ec3abaa50e8f1beb0119541562bbb8dd02c035c7e887976fe1e0c5dd8175f4607ca8d86d1146df24d52a071bd3d1dd329f6902bd58df805a8ca16d3 languageName: node linkType: hard @@ -20461,54 +20535,57 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-import@npm:~2.31.0": - version: 2.31.0 - resolution: "eslint-plugin-import@npm:2.31.0" +"eslint-plugin-import@npm:~2.32.0": + version: 2.32.0 + resolution: "eslint-plugin-import@npm:2.32.0" dependencies: "@rtsao/scc": "npm:^1.1.0" - array-includes: "npm:^3.1.8" - array.prototype.findlastindex: "npm:^1.2.5" - array.prototype.flat: "npm:^1.3.2" - array.prototype.flatmap: "npm:^1.3.2" + array-includes: "npm:^3.1.9" + array.prototype.findlastindex: "npm:^1.2.6" + array.prototype.flat: "npm:^1.3.3" + array.prototype.flatmap: "npm:^1.3.3" debug: "npm:^3.2.7" doctrine: "npm:^2.1.0" eslint-import-resolver-node: "npm:^0.3.9" - eslint-module-utils: "npm:^2.12.0" + eslint-module-utils: "npm:^2.12.1" hasown: "npm:^2.0.2" - is-core-module: "npm:^2.15.1" + is-core-module: "npm:^2.16.1" is-glob: "npm:^4.0.3" minimatch: "npm:^3.1.2" object.fromentries: "npm:^2.0.8" object.groupby: "npm:^1.0.3" - object.values: "npm:^1.2.0" + object.values: "npm:^1.2.1" semver: "npm:^6.3.1" - string.prototype.trimend: "npm:^1.0.8" + string.prototype.trimend: "npm:^1.0.9" tsconfig-paths: "npm:^3.15.0" peerDependencies: eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 - checksum: 10/6b76bd009ac2db0615d9019699d18e2a51a86cb8c1d0855a35fb1b418be23b40239e6debdc6e8c92c59f1468ed0ea8d7b85c817117a113d5cc225be8a02ad31c + checksum: 10/1bacf4967e9ebf99e12176a795f0d6d3a87d1c9a030c2207f27b267e10d96a1220be2647504c7fc13ab543cdf13ffef4b8f5620e0447032dba4ff0d3922f7c9e languageName: node linkType: hard -"eslint-plugin-jest@npm:~28.8.3": - version: 28.8.3 - resolution: "eslint-plugin-jest@npm:28.8.3" +"eslint-plugin-jest@npm:~29.15.0": + version: 29.15.0 + resolution: "eslint-plugin-jest@npm:29.15.0" dependencies: - "@typescript-eslint/utils": "npm:^6.0.0 || ^7.0.0 || ^8.0.0" + "@typescript-eslint/utils": "npm:^8.0.0" peerDependencies: - "@typescript-eslint/eslint-plugin": ^6.0.0 || ^7.0.0 || ^8.0.0 - eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 + "@typescript-eslint/eslint-plugin": ^8.0.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 jest: "*" + typescript: ">=4.8.4 <6.0.0" peerDependenciesMeta: "@typescript-eslint/eslint-plugin": optional: true jest: optional: true - checksum: 10/3f1798c61e143981eefcfb2fbc4b2e5b329378ebaafdec6485f443c79ee0d3304e9409e8ea8ce089ac15abb4e700d8d838e0a1da29feb528d77a2b3cce6989ec + typescript: + optional: true + checksum: 10/552361326c55564fe09092adeb34bf4aae179fdd39b4de6aa561511cebedee370b4ed53f98683af6c695a30891589f54e6fa9ed03ae87957fbe16a506732d0ab languageName: node linkType: hard -"eslint-plugin-jsx-a11y@npm:^6.10.2": +"eslint-plugin-jsx-a11y@npm:~6.10.2": version: 6.10.2 resolution: "eslint-plugin-jsx-a11y@npm:6.10.2" dependencies: @@ -20533,32 +20610,12 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-no-floating-promise@npm:~2.0.0": - version: 2.0.0 - resolution: "eslint-plugin-no-floating-promise@npm:2.0.0" +"eslint-plugin-prettier@npm:~5.5.5": + version: 5.5.5 + resolution: "eslint-plugin-prettier@npm:5.5.5" dependencies: - requireindex: "npm:1.2.0" - checksum: 10/4f56c5acfc7ba3189b6a5428cdc97aa1004f7ba69784949d193a9a88ef9766e2b2070463eedbb26f17e3852ea7362c4205eaa026ea2e0af5471a9daa1469096e - languageName: node - linkType: hard - -"eslint-plugin-playwright@npm:~2.2.2": - version: 2.2.2 - resolution: "eslint-plugin-playwright@npm:2.2.2" - dependencies: - globals: "npm:^13.23.0" - peerDependencies: - eslint: ">=8.40.0" - checksum: 10/861bb486e30e607a3c38bf46b69b01e759a2e7db3d7e79eabb18aa006c505b632b6c6f24e8402b59b8a9b5f94a72ed454fd5a934265081028a1ca0dd4582019f - languageName: node - linkType: hard - -"eslint-plugin-prettier@npm:~5.2.6": - version: 5.2.6 - resolution: "eslint-plugin-prettier@npm:5.2.6" - dependencies: - prettier-linter-helpers: "npm:^1.0.0" - synckit: "npm:^0.11.0" + prettier-linter-helpers: "npm:^1.0.1" + synckit: "npm:^0.11.12" peerDependencies: "@types/eslint": ">=8.0.0" eslint: ">=8.0.0" @@ -20569,25 +20626,31 @@ __metadata: optional: true eslint-config-prettier: optional: true - checksum: 10/8f82a3c6bbf2db358476e745501349c8f3d5f0976f15c4af2a07dd62bb70291d29500ad09a354bb33e645c98a378d35544a92e9758aeb65530b1ec6e2dc8b8f9 + checksum: 10/36c22c2fa2fd7c61ed292af1280e1d8f94dfe1671eacc5a503a249ca4b27fd226dbf6a1820457d611915926946f42729488d2dc7a5c320601e6cf1fad0d28f66 languageName: node linkType: hard -"eslint-plugin-react-hooks@npm:^5.0.0, eslint-plugin-react-hooks@npm:~5.0.0": - version: 5.0.0 - resolution: "eslint-plugin-react-hooks@npm:5.0.0" +"eslint-plugin-react-hooks@npm:~7.0.1": + version: 7.0.1 + resolution: "eslint-plugin-react-hooks@npm:7.0.1" + dependencies: + "@babel/core": "npm:^7.24.4" + "@babel/parser": "npm:^7.24.4" + hermes-parser: "npm:^0.25.1" + zod: "npm:^3.25.0 || ^4.0.0" + zod-validation-error: "npm:^3.5.0 || ^4.0.0" peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 - checksum: 10/b762789832806b6981e2d910994e72aa7a85136fe0880572334b26cf1274ba37bd3b1365e77d2c2f92465337c4a65c84ef647bc499d33b86fc1110f2df7ef1bb + checksum: 10/12e96c68d58c6588305fd17d660524a1ef1e872650ec591d5b138f059431290831c373d4b1c9ae8991fb25f96c43935497d2149678c027e65d0417d3d99ecc85 languageName: node linkType: hard -"eslint-plugin-react-refresh@npm:^0.4.26": - version: 0.4.26 - resolution: "eslint-plugin-react-refresh@npm:0.4.26" +"eslint-plugin-react-refresh@npm:~0.5.2": + version: 0.5.2 + resolution: "eslint-plugin-react-refresh@npm:0.5.2" peerDependencies: - eslint: ">=8.40" - checksum: 10/068da7edcd39d802dda4c03b7ea8923891145eb95f4dfd4cfce82a53a5eb55f5cbe66b33cf8e57b71f1525a684517bda4bfca1bc5c6b12c82ba03c9e8197764d + eslint: ^9 || ^10 + checksum: 10/155a2e66d74866352f023b2a2d9b0daf1fb3033638851321caafa48fe9c9984830acc089d3832348ff2df2848791db96d17bd43639860b045b9f7ee4e5f86dcc languageName: node linkType: hard @@ -20619,27 +20682,27 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-storybook@npm:~0.11.6": - version: 0.11.6 - resolution: "eslint-plugin-storybook@npm:0.11.6" +"eslint-plugin-storybook@npm:~10.2.12": + version: 10.2.12 + resolution: "eslint-plugin-storybook@npm:10.2.12" dependencies: - "@storybook/csf": "npm:^0.1.11" - "@typescript-eslint/utils": "npm:^8.8.1" - ts-dedent: "npm:^2.2.0" + "@typescript-eslint/utils": "npm:^8.48.0" peerDependencies: eslint: ">=8" - checksum: 10/9872dd37386bed244b56589348949d54cad0a526ef81229681a5c918fe1a74dbef56fd7a6cf1c8014b5b38744ba759d1b24a3f94fdbec5d0fddfc6a54f5e6eb7 + storybook: ^10.2.12 + checksum: 10/b412630703b86b7630550e3adaa809889de630a52fe04ae070457bfd94aa6d500ee36498f62dc4b3c9515221c05bbf082fe19d8cbfe61267fedc7e31b527e9bf languageName: node linkType: hard -"eslint-plugin-testing-library@npm:^6.4.0, eslint-plugin-testing-library@npm:~6.4.0": - version: 6.4.0 - resolution: "eslint-plugin-testing-library@npm:6.4.0" +"eslint-plugin-testing-library@npm:~7.16.0": + version: 7.16.0 + resolution: "eslint-plugin-testing-library@npm:7.16.0" dependencies: - "@typescript-eslint/utils": "npm:^5.62.0" + "@typescript-eslint/scope-manager": "npm:^8.56.0" + "@typescript-eslint/utils": "npm:^8.56.0" peerDependencies: - eslint: ^7.5.0 || ^8.0.0 || ^9.0.0 - checksum: 10/b9461e6662542fbfb3c0b768e6038bbaa56e345cb0600c7f07e26ff8154166abb9f7dcdc08d565c314cd9ccf0416eca8c3ce3c235b375c5fbf3f152a81142969 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + checksum: 10/6581b41d25838c61b818f6b80e998ff2d72a646531c1c017023842fe5523a9ea3e9e75e7ce0d84abee9d73309602d64911985cbaace8dfbb0a94415da1b213a8 languageName: node linkType: hard @@ -20652,7 +20715,7 @@ __metadata: languageName: node linkType: hard -"eslint-scope@npm:5.1.1, eslint-scope@npm:^5.1.1": +"eslint-scope@npm:5.1.1": version: 5.1.1 resolution: "eslint-scope@npm:5.1.1" dependencies: @@ -20662,13 +20725,13 @@ __metadata: languageName: node linkType: hard -"eslint-scope@npm:^7.2.0": - version: 7.2.0 - resolution: "eslint-scope@npm:7.2.0" +"eslint-scope@npm:^8.4.0": + version: 8.4.0 + resolution: "eslint-scope@npm:8.4.0" dependencies: esrecurse: "npm:^4.3.0" estraverse: "npm:^5.2.0" - checksum: 10/94d8942840b35bf5e6559bd0f0a8b10610d65b1e44e41295e66ed1fe82f83bc51756e7af607d611b75f435adf821122bd901aa565701596ca1a628db41c0cd87 + checksum: 10/e8e611701f65375e034c62123946e628894f0b54aa8cb11abe224816389abe5cd74cf16b62b72baa36504f22d1a958b9b8b0169b82397fe2e7997674c0d09b06 languageName: node linkType: hard @@ -20679,13 +20742,27 @@ __metadata: languageName: node linkType: hard -"eslint-visitor-keys@npm:^3.3.0, eslint-visitor-keys@npm:^3.4.1, eslint-visitor-keys@npm:^3.4.3": +"eslint-visitor-keys@npm:^3.4.3": version: 3.4.3 resolution: "eslint-visitor-keys@npm:3.4.3" checksum: 10/3f357c554a9ea794b094a09bd4187e5eacd1bc0d0653c3adeb87962c548e6a1ab8f982b86963ae1337f5d976004146536dcee5d0e2806665b193fbfbf1a9231b languageName: node linkType: hard +"eslint-visitor-keys@npm:^4.2.1": + version: 4.2.1 + resolution: "eslint-visitor-keys@npm:4.2.1" + checksum: 10/3ee00fc6a7002d4b0ffd9dc99e13a6a7882c557329e6c25ab254220d71e5c9c4f89dca4695352949ea678eb1f3ba912a18ef8aac0a7fe094196fd92f441bfce2 + languageName: node + linkType: hard + +"eslint-visitor-keys@npm:^5.0.0": + version: 5.0.1 + resolution: "eslint-visitor-keys@npm:5.0.1" + checksum: 10/f9cc1a57b75e0ef949545cac33d01e8367e302de4c1483266ed4d8646ee5c306376660196bbb38b004e767b7043d1e661cb4336b49eff634a1bbe75c1db709ec + languageName: node + linkType: hard + "eslint4b-prebuilt@npm:^6.7.2": version: 6.7.2 resolution: "eslint4b-prebuilt@npm:6.7.2" @@ -20693,61 +20770,63 @@ __metadata: languageName: node linkType: hard -"eslint@npm:~8.45.0": - version: 8.45.0 - resolution: "eslint@npm:8.45.0" +"eslint@npm:~9.39.3": + version: 9.39.3 + resolution: "eslint@npm:9.39.3" dependencies: - "@eslint-community/eslint-utils": "npm:^4.2.0" - "@eslint-community/regexpp": "npm:^4.4.0" - "@eslint/eslintrc": "npm:^2.1.0" - "@eslint/js": "npm:8.44.0" - "@humanwhocodes/config-array": "npm:^0.11.10" + "@eslint-community/eslint-utils": "npm:^4.8.0" + "@eslint-community/regexpp": "npm:^4.12.1" + "@eslint/config-array": "npm:^0.21.1" + "@eslint/config-helpers": "npm:^0.4.2" + "@eslint/core": "npm:^0.17.0" + "@eslint/eslintrc": "npm:^3.3.1" + "@eslint/js": "npm:9.39.3" + "@eslint/plugin-kit": "npm:^0.4.1" + "@humanfs/node": "npm:^0.16.6" "@humanwhocodes/module-importer": "npm:^1.0.1" - "@nodelib/fs.walk": "npm:^1.2.8" - ajv: "npm:^6.10.0" + "@humanwhocodes/retry": "npm:^0.4.2" + "@types/estree": "npm:^1.0.6" + ajv: "npm:^6.12.4" chalk: "npm:^4.0.0" - cross-spawn: "npm:^7.0.2" + cross-spawn: "npm:^7.0.6" debug: "npm:^4.3.2" - doctrine: "npm:^3.0.0" escape-string-regexp: "npm:^4.0.0" - eslint-scope: "npm:^7.2.0" - eslint-visitor-keys: "npm:^3.4.1" - espree: "npm:^9.6.0" - esquery: "npm:^1.4.2" + eslint-scope: "npm:^8.4.0" + eslint-visitor-keys: "npm:^4.2.1" + espree: "npm:^10.4.0" + esquery: "npm:^1.5.0" esutils: "npm:^2.0.2" fast-deep-equal: "npm:^3.1.3" - file-entry-cache: "npm:^6.0.1" + file-entry-cache: "npm:^8.0.0" find-up: "npm:^5.0.0" glob-parent: "npm:^6.0.2" - globals: "npm:^13.19.0" - graphemer: "npm:^1.4.0" ignore: "npm:^5.2.0" imurmurhash: "npm:^0.1.4" is-glob: "npm:^4.0.0" - is-path-inside: "npm:^3.0.3" - js-yaml: "npm:^4.1.0" json-stable-stringify-without-jsonify: "npm:^1.0.1" - levn: "npm:^0.4.1" lodash.merge: "npm:^4.6.2" minimatch: "npm:^3.1.2" natural-compare: "npm:^1.4.0" optionator: "npm:^0.9.3" - strip-ansi: "npm:^6.0.1" - text-table: "npm:^0.2.0" + peerDependencies: + jiti: "*" + peerDependenciesMeta: + jiti: + optional: true bin: eslint: bin/eslint.js - checksum: 10/54820753ae1fb85affe48d001ea0cdf87e48b863bc423f717f4ca6a12ea0db65f171de58732ef51e94eacff33ac4e2c4f4717ec93014e759ed8adfcd6dc9402a + checksum: 10/1c95c92983ddf435e7f7d54edd06d703a15773a7d189583d3388e5b5ac714f0a2450b91c0b3bb9b9ccec9bd20994fd8e48d231ed6dabca0be56ef314b32820ff languageName: node linkType: hard -"espree@npm:^9.6.0": - version: 9.6.0 - resolution: "espree@npm:9.6.0" +"espree@npm:^10.0.1, espree@npm:^10.4.0": + version: 10.4.0 + resolution: "espree@npm:10.4.0" dependencies: - acorn: "npm:^8.9.0" + acorn: "npm:^8.15.0" acorn-jsx: "npm:^5.3.2" - eslint-visitor-keys: "npm:^3.4.1" - checksum: 10/870834c0ab188213ba56fae7003ff9fadbad2b9285dae941840c3d425cedbb2221ad3cffaabd217bc36b96eb80d651c2a2d9b0b1f3b9394b2358b27052c942e2 + eslint-visitor-keys: "npm:^4.2.1" + checksum: 10/9b355b32dbd1cc9f57121d5ee3be258fab87ebeb7c83fc6c02e5af1a74fc8c5ba79fe8c663e69ea112c3e84a1b95e6a2067ac4443ee7813bb85ac7581acb8bf9 languageName: node linkType: hard @@ -20761,12 +20840,12 @@ __metadata: languageName: node linkType: hard -"esquery@npm:^1.4.2": - version: 1.5.0 - resolution: "esquery@npm:1.5.0" +"esquery@npm:^1.5.0": + version: 1.7.0 + resolution: "esquery@npm:1.7.0" dependencies: estraverse: "npm:^5.1.0" - checksum: 10/e65fcdfc1e0ff5effbf50fb4f31ea20143ae5df92bb2e4953653d8d40aa4bc148e0d06117a592ce4ea53eeab1dafdfded7ea7e22a5be87e82d73757329a1b01d + checksum: 10/4afaf3089367e1f5885caa116ef386dffd8bfd64da21fd3d0e56e938d2667cfb2e5400ab4a825aa70e799bb3741e5b5d63c0b94d86e2d4cf3095c9e64b2f5a15 languageName: node linkType: hard @@ -21335,12 +21414,12 @@ __metadata: languageName: node linkType: hard -"file-entry-cache@npm:^6.0.1": - version: 6.0.1 - resolution: "file-entry-cache@npm:6.0.1" +"file-entry-cache@npm:^8.0.0": + version: 8.0.0 + resolution: "file-entry-cache@npm:8.0.0" dependencies: - flat-cache: "npm:^3.0.4" - checksum: 10/099bb9d4ab332cb93c48b14807a6918a1da87c45dce91d4b61fd40e6505d56d0697da060cb901c729c90487067d93c9243f5da3dc9c41f0358483bfdebca736b + flat-cache: "npm:^4.0.0" + checksum: 10/afe55c4de4e0d226a23c1eae62a7219aafb390859122608a89fa4df6addf55c7fd3f1a2da6f5b41e7cdff496e4cf28bbd215d53eab5c817afa96d2b40c81bfb0 languageName: node linkType: hard @@ -21591,6 +21670,16 @@ __metadata: languageName: node linkType: hard +"flat-cache@npm:^4.0.0": + version: 4.0.1 + resolution: "flat-cache@npm:4.0.1" + dependencies: + flatted: "npm:^3.2.9" + keyv: "npm:^4.5.4" + checksum: 10/58ce851d9045fffc7871ce2bd718bc485ad7e777bf748c054904b87c351ff1080c2c11da00788d78738bfb51b71e4d5ea12d13b98eb36e3358851ffe495b62dc + languageName: node + linkType: hard + "flat-cache@npm:^5.0.0": version: 5.0.0 resolution: "flat-cache@npm:5.0.0" @@ -21617,6 +21706,13 @@ __metadata: languageName: node linkType: hard +"flatted@npm:^3.2.9": + version: 3.3.3 + resolution: "flatted@npm:3.3.3" + checksum: 10/8c96c02fbeadcf4e8ffd0fa24983241e27698b0781295622591fc13585e2f226609d95e422bcf2ef044146ffacb6b68b1f20871454eddf75ab3caa6ee5f4a1fe + languageName: node + linkType: hard + "follow-redirects@npm:^1.0.0, follow-redirects@npm:^1.15.4, follow-redirects@npm:^1.15.6, follow-redirects@npm:^1.15.9": version: 1.15.9 resolution: "follow-redirects@npm:1.15.9" @@ -21653,6 +21749,15 @@ __metadata: languageName: node linkType: hard +"for-each@npm:^0.3.5": + version: 0.3.5 + resolution: "for-each@npm:0.3.5" + dependencies: + is-callable: "npm:^1.2.7" + checksum: 10/330cc2439f85c94f4609de3ee1d32c5693ae15cdd7fe3d112c4fd9efd4ce7143f2c64ef6c2c9e0cfdb0058437f33ef05b5bdae5b98fcc903fb2143fbaf0fea0f + languageName: node + linkType: hard + "foreground-child@npm:^2.0.0": version: 2.0.0 resolution: "foreground-child@npm:2.0.0" @@ -22276,12 +22381,17 @@ __metadata: languageName: node linkType: hard -"globals@npm:^13.19.0, globals@npm:^13.23.0": - version: 13.24.0 - resolution: "globals@npm:13.24.0" - dependencies: - type-fest: "npm:^0.20.2" - checksum: 10/62c5b1997d06674fc7191d3e01e324d3eda4d65ac9cc4e78329fa3b5c4fd42a0e1c8722822497a6964eee075255ce21ccf1eec2d83f92ef3f06653af4d0ee28e +"globals@npm:^14.0.0": + version: 14.0.0 + resolution: "globals@npm:14.0.0" + checksum: 10/03939c8af95c6df5014b137cac83aa909090c3a3985caef06ee9a5a669790877af8698ab38007e4c0186873adc14c0b13764acc754b16a754c216cc56aa5f021 + languageName: node + linkType: hard + +"globals@npm:~17.3.0": + version: 17.3.0 + resolution: "globals@npm:17.3.0" + checksum: 10/44ba2b7db93eb6a2531dfba09219845e21f2e724a4f400eb59518b180b7d5bcf7f65580530e3d3023d7dc2bdbacf5d265fd87c393f567deb9a2b0472b51c9d5e languageName: node linkType: hard @@ -22378,20 +22488,6 @@ __metadata: languageName: node linkType: hard -"grapheme-splitter@npm:^1.0.4": - version: 1.0.4 - resolution: "grapheme-splitter@npm:1.0.4" - checksum: 10/fdb2f51fd430ce881e18e44c4934ad30e59736e46213f7ad35ea5970a9ebdf7d0fe56150d15cc98230d55d2fd48c73dc6781494c38d8cf2405718366c36adb88 - languageName: node - linkType: hard - -"graphemer@npm:^1.4.0": - version: 1.4.0 - resolution: "graphemer@npm:1.4.0" - checksum: 10/6dd60dba97007b21e3a829fab3f771803cc1292977fe610e240ea72afd67e5690ac9eeaafc4a99710e78962e5936ab5a460787c2a1180f1cb0ccfac37d29f897 - languageName: node - linkType: hard - "gravatar@npm:^1.8.2": version: 1.8.2 resolution: "gravatar@npm:1.8.2" @@ -22644,6 +22740,22 @@ __metadata: languageName: node linkType: hard +"hermes-estree@npm:0.25.1": + version: 0.25.1 + resolution: "hermes-estree@npm:0.25.1" + checksum: 10/7b1eca98b264a25632064cffa5771360d30cf452e77db1e191f9913ee45cf78c292b2dbca707e92fb71b0870abb97e94b506a5ab80abd96ba237fee169b601fe + languageName: node + linkType: hard + +"hermes-parser@npm:^0.25.1": + version: 0.25.1 + resolution: "hermes-parser@npm:0.25.1" + dependencies: + hermes-estree: "npm:0.25.1" + checksum: 10/805efc05691420f236654349872c70731121791fa54de521c7ee51059eae34f84dd19f22ee846741dcb60372f8fb5335719b96b4ecb010d2aed7d872f2eff9cc + languageName: node + linkType: hard + "hexer@npm:^1.5.0": version: 1.5.0 resolution: "hexer@npm:1.5.0" @@ -23259,6 +23371,13 @@ __metadata: languageName: node linkType: hard +"ignore@npm:^7.0.5": + version: 7.0.5 + resolution: "ignore@npm:7.0.5" + checksum: 10/f134b96a4de0af419196f52c529d5c6120c4456ff8a6b5a14ceaaa399f883e15d58d2ce651c9b69b9388491d4669dda47285d307e827de9304a53a1824801bc6 + languageName: node + linkType: hard + "image-size@npm:^1.2.1": version: 1.2.1 resolution: "image-size@npm:1.2.1" @@ -23658,7 +23777,7 @@ __metadata: languageName: node linkType: hard -"is-core-module@npm:^2.12.0, is-core-module@npm:^2.16.0": +"is-core-module@npm:^2.12.0, is-core-module@npm:^2.16.0, is-core-module@npm:^2.16.1": version: 2.16.1 resolution: "is-core-module@npm:2.16.1" dependencies: @@ -23667,7 +23786,7 @@ __metadata: languageName: node linkType: hard -"is-core-module@npm:^2.13.0, is-core-module@npm:^2.15.1, is-core-module@npm:^2.5.0": +"is-core-module@npm:^2.13.0, is-core-module@npm:^2.5.0": version: 2.15.1 resolution: "is-core-module@npm:2.15.1" dependencies: @@ -23826,6 +23945,13 @@ __metadata: languageName: node linkType: hard +"is-negative-zero@npm:^2.0.3": + version: 2.0.3 + resolution: "is-negative-zero@npm:2.0.3" + checksum: 10/8fe5cffd8d4fb2ec7b49d657e1691889778d037494c6f40f4d1a524cadd658b4b53ad7b6b73a59bcb4b143ae9a3d15829af864b2c0f9d65ac1e678c4c80f17e5 + languageName: node + linkType: hard + "is-network-error@npm:^1.0.0": version: 1.1.0 resolution: "is-network-error@npm:1.1.0" @@ -23864,13 +23990,6 @@ __metadata: languageName: node linkType: hard -"is-path-inside@npm:^3.0.3": - version: 3.0.3 - resolution: "is-path-inside@npm:3.0.3" - checksum: 10/abd50f06186a052b349c15e55b182326f1936c89a78bf6c8f2b707412517c097ce04bc49a0ca221787bc44e1049f51f09a2ffb63d22899051988d3a618ba13e9 - languageName: node - linkType: hard - "is-plain-obj@npm:^1.1.0": version: 1.1.0 resolution: "is-plain-obj@npm:1.1.0" @@ -24040,7 +24159,7 @@ __metadata: languageName: node linkType: hard -"is-weakref@npm:^1.0.2, is-weakref@npm:^1.1.0": +"is-weakref@npm:^1.0.2, is-weakref@npm:^1.1.0, is-weakref@npm:^1.1.1": version: 1.1.1 resolution: "is-weakref@npm:1.1.1" dependencies: @@ -24285,7 +24404,7 @@ __metadata: languageName: node linkType: hard -"iterator.prototype@npm:^1.1.4": +"iterator.prototype@npm:^1.1.5": version: 1.1.5 resolution: "iterator.prototype@npm:1.1.5" dependencies: @@ -25515,6 +25634,17 @@ __metadata: languageName: node linkType: hard +"js-yaml@npm:^4.1.1": + version: 4.1.1 + resolution: "js-yaml@npm:4.1.1" + dependencies: + argparse: "npm:^2.0.1" + bin: + js-yaml: bin/js-yaml.js + checksum: 10/a52d0519f0f4ef5b4adc1cde466cb54c50d56e2b4a983b9d5c9c0f2f99462047007a6274d7e95617a21d3c91fde3ee6115536ed70991cd645ba8521058b78f77 + languageName: node + linkType: hard + "jsbn@npm:1.1.0": version: 1.1.0 resolution: "jsbn@npm:1.1.0" @@ -27235,7 +27365,16 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:^3.0.0, minimatch@npm:^3.0.4, minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": +"minimatch@npm:^10.2.2": + version: 10.2.4 + resolution: "minimatch@npm:10.2.4" + dependencies: + brace-expansion: "npm:^5.0.2" + checksum: 10/aea4874e521c55bb60744685bbffe3d152e5460f84efac3ea936e6bbe2ceba7deb93345fec3f9bb17f7b6946776073a64d40ae32bf5f298ad690308121068a1f + languageName: node + linkType: hard + +"minimatch@npm:^3.0.0, minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": version: 3.1.2 resolution: "minimatch@npm:3.1.2" dependencies: @@ -27244,6 +27383,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:^3.1.3": + version: 3.1.5 + resolution: "minimatch@npm:3.1.5" + dependencies: + brace-expansion: "npm:^1.1.7" + checksum: 10/b11a7ee5773cd34c1a0c8436cdbe910901018fb4b6cb47aa508a18d567f6efd2148507959e35fba798389b161b8604a2d704ccef751ea36bd4582f9852b7d63f + languageName: node + linkType: hard + "minimatch@npm:^5.1.0": version: 5.1.6 resolution: "minimatch@npm:5.1.6" @@ -27851,13 +27999,6 @@ __metadata: languageName: node linkType: hard -"natural-compare-lite@npm:^1.4.0": - version: 1.4.0 - resolution: "natural-compare-lite@npm:1.4.0" - checksum: 10/5222ac3986a2b78dd6069ac62cbb52a7bf8ffc90d972ab76dfe7b01892485d229530ed20d0c62e79a6b363a663b273db3bde195a1358ce9e5f779d4453887225 - languageName: node - linkType: hard - "natural-compare@npm:^1.4.0": version: 1.4.0 resolution: "natural-compare@npm:1.4.0" @@ -27982,6 +28123,18 @@ __metadata: languageName: node linkType: hard +"node-exports-info@npm:^1.6.0": + version: 1.6.0 + resolution: "node-exports-info@npm:1.6.0" + dependencies: + array.prototype.flatmap: "npm:^1.3.3" + es-errors: "npm:^1.3.0" + object.entries: "npm:^1.1.9" + semver: "npm:^6.3.1" + checksum: 10/0a1667d535f499ac1fe6c6d22f8146bc8b68abc76fa355856219202f6cf5f386027e0ff054e66a22d08be02acbc63fcdc9f98d0fbc97993f5eabc66408fdadad + languageName: node + linkType: hard + "node-fetch@npm:2.6.7": version: 2.6.7 resolution: "node-fetch@npm:2.6.7" @@ -28372,7 +28525,7 @@ __metadata: languageName: node linkType: hard -"object-inspect@npm:^1.13.3": +"object-inspect@npm:^1.13.3, object-inspect@npm:^1.13.4": version: 1.13.4 resolution: "object-inspect@npm:1.13.4" checksum: 10/aa13b1190ad3e366f6c83ad8a16ed37a19ed57d267385aa4bfdccda833d7b90465c057ff6c55d035a6b2e52c1a2295582b294217a0a3a1ae7abdd6877ef781fb @@ -28452,7 +28605,7 @@ __metadata: languageName: node linkType: hard -"object.values@npm:^1.1.6, object.values@npm:^1.2.0, object.values@npm:^1.2.1": +"object.values@npm:^1.1.6, object.values@npm:^1.2.1": version: 1.2.1 resolution: "object.values@npm:1.2.1" dependencies: @@ -30336,12 +30489,12 @@ __metadata: languageName: node linkType: hard -"prettier-linter-helpers@npm:^1.0.0": - version: 1.0.0 - resolution: "prettier-linter-helpers@npm:1.0.0" +"prettier-linter-helpers@npm:^1.0.1": + version: 1.0.1 + resolution: "prettier-linter-helpers@npm:1.0.1" dependencies: fast-diff: "npm:^1.1.2" - checksum: 10/00ce8011cf6430158d27f9c92cfea0a7699405633f7f1d4a45f07e21bf78e99895911cbcdc3853db3a824201a7c745bd49bfea8abd5fb9883e765a90f74f8392 + checksum: 10/2dc35f5036a35f4c4f5e645887edda1436acb63687a7f12b2383e0a6f3c1f76b8a0a4709fe4d82e19157210feb5984b159bb714d43290022911ab53d606474ec languageName: node linkType: hard @@ -31748,7 +31901,7 @@ __metadata: languageName: node linkType: hard -"regexp.prototype.flags@npm:^1.5.3": +"regexp.prototype.flags@npm:^1.5.3, regexp.prototype.flags@npm:^1.5.4": version: 1.5.4 resolution: "regexp.prototype.flags@npm:1.5.4" dependencies: @@ -31975,13 +32128,6 @@ __metadata: languageName: node linkType: hard -"requireindex@npm:1.2.0": - version: 1.2.0 - resolution: "requireindex@npm:1.2.0" - checksum: 10/266d1cb31f6cbc4b6cf2e898f5bbc45581f7919bcf61bba5c45d0adb69b722b9ff5a13727be3350cde4520d7cd37f39df45d58a29854baaa4552cd6b05ae4a1a - languageName: node - linkType: hard - "requires-port@npm:^1.0.0": version: 1.0.0 resolution: "requires-port@npm:1.0.0" @@ -32086,15 +32232,18 @@ __metadata: linkType: hard "resolve@npm:^2.0.0-next.5": - version: 2.0.0-next.5 - resolution: "resolve@npm:2.0.0-next.5" + version: 2.0.0-next.6 + resolution: "resolve@npm:2.0.0-next.6" dependencies: - is-core-module: "npm:^2.13.0" + es-errors: "npm:^1.3.0" + is-core-module: "npm:^2.16.1" + node-exports-info: "npm:^1.6.0" + object-keys: "npm:^1.1.1" path-parse: "npm:^1.0.7" supports-preserve-symlinks-flag: "npm:^1.0.0" bin: resolve: bin/resolve - checksum: 10/2d6fd28699f901744368e6f2032b4268b4c7b9185fd8beb64f68c93ac6b22e52ae13560ceefc96241a665b985edf9ffd393ae26d2946a7d3a07b7007b7d51e79 + checksum: 10/c95cb98b8d3f9e2a979e6eb8b7e0b0e13f08da62607a45207275f151d640152244568a9a9cd01662a21e3746792177cbf9be1dacb88f7355edf4db49d9ee27e5 languageName: node linkType: hard @@ -32125,15 +32274,18 @@ __metadata: linkType: hard "resolve@patch:resolve@npm%3A^2.0.0-next.5#optional!builtin<compat/resolve>": - version: 2.0.0-next.5 - resolution: "resolve@patch:resolve@npm%3A2.0.0-next.5#optional!builtin<compat/resolve>::version=2.0.0-next.5&hash=c3c19d" + version: 2.0.0-next.6 + resolution: "resolve@patch:resolve@npm%3A2.0.0-next.6#optional!builtin<compat/resolve>::version=2.0.0-next.6&hash=c3c19d" dependencies: - is-core-module: "npm:^2.13.0" + es-errors: "npm:^1.3.0" + is-core-module: "npm:^2.16.1" + node-exports-info: "npm:^1.6.0" + object-keys: "npm:^1.1.1" path-parse: "npm:^1.0.7" supports-preserve-symlinks-flag: "npm:^1.0.0" bin: resolve: bin/resolve - checksum: 10/05fa778de9d0347c8b889eb7a18f1f06bf0f801b0eb4610b4871a4b2f22e220900cf0ad525e94f990bb8d8921c07754ab2122c0c225ab4cdcea98f36e64fa4c2 + checksum: 10/1b26738af76c80b341075e6bf4b202ef85f85f4a2cbf2934246c3b5f20c682cf352833fc6e32579c6967419226d3ab63e8d321328da052c87a31eaad91e3571a languageName: node linkType: hard @@ -32256,13 +32408,17 @@ __metadata: resolution: "rocket.chat@workspace:." dependencies: "@changesets/cli": "npm:^2.27.11" + "@rocket.chat/eslint-config": "workspace:~" "@types/chart.js": "npm:^2.9.41" "@types/js-yaml": "npm:^4.0.9" "@types/node": "npm:~22.16.5" "@types/stream-buffers": "npm:^3.0.8" + eslint: "npm:~9.39.3" + eslint-plugin-react-refresh: "npm:~0.5.2" + eslint-plugin-you-dont-need-lodash-underscore: "npm:~6.14.0" node-gyp: "npm:^10.2.0" ts-node: "npm:^10.9.2" - turbo: "npm:~2.7.6" + turbo: "npm:~2.8.11" typescript: "npm:~5.9.3" languageName: unknown linkType: soft @@ -32725,7 +32881,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.3.2, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.2, semver@npm:^7.6.3, semver@npm:^7.7.1, semver@npm:^7.7.2": +"semver@npm:^7.3.2, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.2, semver@npm:^7.6.3, semver@npm:^7.7.1, semver@npm:^7.7.2": version: 7.7.2 resolution: "semver@npm:7.7.2" bin: @@ -33695,7 +33851,7 @@ __metadata: languageName: node linkType: hard -"stop-iteration-iterator@npm:^1.0.0": +"stop-iteration-iterator@npm:^1.0.0, stop-iteration-iterator@npm:^1.1.0": version: 1.1.0 resolution: "stop-iteration-iterator@npm:1.1.0" dependencies: @@ -33976,7 +34132,7 @@ __metadata: languageName: node linkType: hard -"string.prototype.trimend@npm:^1.0.8, string.prototype.trimend@npm:^1.0.9": +"string.prototype.trimend@npm:^1.0.9": version: 1.0.9 resolution: "string.prototype.trimend@npm:1.0.9" dependencies: @@ -34455,7 +34611,16 @@ __metadata: languageName: node linkType: hard -"synckit@npm:^0.11.0, synckit@npm:^0.11.8": +"synckit@npm:^0.11.12": + version: 0.11.12 + resolution: "synckit@npm:0.11.12" + dependencies: + "@pkgr/core": "npm:^0.2.9" + checksum: 10/2f51978bfed81aaf0b093f596709a72c49b17909020f42b43c5549f9c0fe18b1fe29f82e41ef771172d729b32e9ce82900a85d2b87fa14d59f886d4df8d7a329 + languageName: node + linkType: hard + +"synckit@npm:^0.11.8": version: 0.11.8 resolution: "synckit@npm:0.11.8" dependencies: @@ -34723,13 +34888,6 @@ __metadata: languageName: node linkType: hard -"text-table@npm:^0.2.0": - version: 0.2.0 - resolution: "text-table@npm:0.2.0" - checksum: 10/4383b5baaeffa9bb4cda2ac33a4aa2e6d1f8aaf811848bf73513a9b88fd76372dc461f6fd6d2e9cb5100f48b473be32c6f95bd983509b7d92bb4d92c10747452 - languageName: node - linkType: hard - "textarea-caret@npm:^3.1.0": version: 3.1.0 resolution: "textarea-caret@npm:3.1.0" @@ -34831,7 +34989,7 @@ __metadata: languageName: node linkType: hard -"tinyglobby@npm:^0.2.14": +"tinyglobby@npm:^0.2.14, tinyglobby@npm:^0.2.15": version: 0.2.15 resolution: "tinyglobby@npm:0.2.15" dependencies: @@ -35068,12 +35226,12 @@ __metadata: languageName: node linkType: hard -"ts-api-utils@npm:^1.3.0": - version: 1.3.0 - resolution: "ts-api-utils@npm:1.3.0" +"ts-api-utils@npm:^2.4.0": + version: 2.4.0 + resolution: "ts-api-utils@npm:2.4.0" peerDependencies: - typescript: ">=4.2.0" - checksum: 10/3ee44faa24410cd649b5c864e068d438aa437ef64e9e4a66a41646a6d3024d3097a695eeb3fb26ee364705d3cb9653a65756d009e6a53badb6066a5f447bf7ed + typescript: ">=4.8.4" + checksum: 10/d6b2b3b6caad8d2f4ddc0c3785d22bb1a6041773335a1c71d73a5d67d11d993763fe8e4faefc4a4d03bb42b26c6126bbcf2e34826baed1def5369d0ebad358fa languageName: node linkType: hard @@ -35261,7 +35419,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^1.8.1, tslib@npm:^1.9.3": +"tslib@npm:^1.9.3": version: 1.14.1 resolution: "tslib@npm:1.14.1" checksum: 10/7dbf34e6f55c6492637adb81b555af5e3b4f9cc6b998fb440dac82d3b42bdc91560a35a5fb75e20e24a076c651438234da6743d139e4feabf0783f3cdfe1dddb @@ -35289,17 +35447,6 @@ __metadata: languageName: node linkType: hard -"tsutils@npm:^3.21.0": - version: 3.21.0 - resolution: "tsutils@npm:3.21.0" - dependencies: - tslib: "npm:^1.8.1" - peerDependencies: - typescript: ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" - checksum: 10/ea036bec1dd024e309939ffd49fda7a351c0e87a1b8eb049570dd119d447250e2c56e0e6c00554e8205760e7417793fdebff752a46e573fbe07d4f375502a5b2 - languageName: node - linkType: hard - "tsx@npm:~4.20.6": version: 4.20.6 resolution: "tsx@npm:4.20.6" @@ -35355,58 +35502,58 @@ __metadata: languageName: node linkType: hard -"turbo-darwin-64@npm:2.7.6": - version: 2.7.6 - resolution: "turbo-darwin-64@npm:2.7.6" +"turbo-darwin-64@npm:2.8.11": + version: 2.8.11 + resolution: "turbo-darwin-64@npm:2.8.11" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"turbo-darwin-arm64@npm:2.7.6": - version: 2.7.6 - resolution: "turbo-darwin-arm64@npm:2.7.6" +"turbo-darwin-arm64@npm:2.8.11": + version: 2.8.11 + resolution: "turbo-darwin-arm64@npm:2.8.11" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"turbo-linux-64@npm:2.7.6": - version: 2.7.6 - resolution: "turbo-linux-64@npm:2.7.6" +"turbo-linux-64@npm:2.8.11": + version: 2.8.11 + resolution: "turbo-linux-64@npm:2.8.11" conditions: os=linux & cpu=x64 languageName: node linkType: hard -"turbo-linux-arm64@npm:2.7.6": - version: 2.7.6 - resolution: "turbo-linux-arm64@npm:2.7.6" +"turbo-linux-arm64@npm:2.8.11": + version: 2.8.11 + resolution: "turbo-linux-arm64@npm:2.8.11" conditions: os=linux & cpu=arm64 languageName: node linkType: hard -"turbo-windows-64@npm:2.7.6": - version: 2.7.6 - resolution: "turbo-windows-64@npm:2.7.6" +"turbo-windows-64@npm:2.8.11": + version: 2.8.11 + resolution: "turbo-windows-64@npm:2.8.11" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"turbo-windows-arm64@npm:2.7.6": - version: 2.7.6 - resolution: "turbo-windows-arm64@npm:2.7.6" +"turbo-windows-arm64@npm:2.8.11": + version: 2.8.11 + resolution: "turbo-windows-arm64@npm:2.8.11" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"turbo@npm:~2.7.6": - version: 2.7.6 - resolution: "turbo@npm:2.7.6" +"turbo@npm:~2.8.11": + version: 2.8.11 + resolution: "turbo@npm:2.8.11" dependencies: - turbo-darwin-64: "npm:2.7.6" - turbo-darwin-arm64: "npm:2.7.6" - turbo-linux-64: "npm:2.7.6" - turbo-linux-arm64: "npm:2.7.6" - turbo-windows-64: "npm:2.7.6" - turbo-windows-arm64: "npm:2.7.6" + turbo-darwin-64: "npm:2.8.11" + turbo-darwin-arm64: "npm:2.8.11" + turbo-linux-64: "npm:2.8.11" + turbo-linux-arm64: "npm:2.8.11" + turbo-windows-64: "npm:2.8.11" + turbo-windows-arm64: "npm:2.8.11" dependenciesMeta: turbo-darwin-64: optional: true @@ -35422,7 +35569,7 @@ __metadata: optional: true bin: turbo: bin/turbo - checksum: 10/88dd2f3bedf238f8f2d55416cd77179d9708a283528a4d3d9752c950e08c8bbfd81e8fe05837426891f02eb8574da07385d84ef75769f2a71ba41b2e26f5f3a6 + checksum: 10/5ac765e3f35b69ad9daf398a35c44b5db237640f17a234bbbd344594e4b1da014ed592dbc0e87d908548a8c6ad72037c2526baba57091e99765160b14ed4cd2a languageName: node linkType: hard @@ -35489,13 +35636,6 @@ __metadata: languageName: node linkType: hard -"type-fest@npm:^0.20.2": - version: 0.20.2 - resolution: "type-fest@npm:0.20.2" - checksum: 10/8907e16284b2d6cfa4f4817e93520121941baba36b39219ea36acfe64c86b9dbc10c9941af450bd60832c8f43464974d51c0957f9858bc66b952b66b6914cbb9 - languageName: node - linkType: hard - "type-fest@npm:^0.21.3": version: 0.21.3 resolution: "type-fest@npm:0.21.3" @@ -35638,6 +35778,21 @@ __metadata: languageName: node linkType: hard +"typescript-eslint@npm:~8.56.1": + version: 8.56.1 + resolution: "typescript-eslint@npm:8.56.1" + dependencies: + "@typescript-eslint/eslint-plugin": "npm:8.56.1" + "@typescript-eslint/parser": "npm:8.56.1" + "@typescript-eslint/typescript-estree": "npm:8.56.1" + "@typescript-eslint/utils": "npm:8.56.1" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: ">=4.8.4 <6.0.0" + checksum: 10/e18cd347ddce0f0e5b28121346f27736a418adf68e73d613690ea3a1d0adfe03bc393f77a8872c2cef77ca74bcc0974212d1775c360de33a9987a94cda11a05b + languageName: node + linkType: hard + "typescript@npm:~5.9.3": version: 5.9.3 resolution: "typescript@npm:5.9.3" @@ -36923,6 +37078,21 @@ __metadata: languageName: node linkType: hard +"which-typed-array@npm:^1.1.19": + version: 1.1.20 + resolution: "which-typed-array@npm:1.1.20" + dependencies: + available-typed-arrays: "npm:^1.0.7" + call-bind: "npm:^1.0.8" + call-bound: "npm:^1.0.4" + for-each: "npm:^0.3.5" + get-proto: "npm:^1.0.1" + gopd: "npm:^1.2.0" + has-tostringtag: "npm:^1.0.2" + checksum: 10/e56da3fc995d330ff012f682476f7883c16b12d36c6717c87c7ca23eb5a5ef957fa89115dacb389b11a9b4e99d5dbe2d12689b4d5d08c050b5aed0eae385b840 + languageName: node + linkType: hard + "which@npm:2.0.2, which@npm:^2.0.1": version: 2.0.2 resolution: "which@npm:2.0.2" @@ -37441,6 +37611,15 @@ __metadata: languageName: node linkType: hard +"zod-validation-error@npm:^3.5.0 || ^4.0.0": + version: 4.0.2 + resolution: "zod-validation-error@npm:4.0.2" + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + checksum: 10/5e35ca8ebb4602dcb526e122d7e9fca695c4a479bd97535f3400a732d49160f24f7213a9ed64986fc9dc3a2e8a6c4e1241ec0c4d8a4e3e69ea91a0328ded2192 + languageName: node + linkType: hard + "zod@npm:^3.24.1": version: 3.24.1 resolution: "zod@npm:3.24.1" @@ -37448,7 +37627,7 @@ __metadata: languageName: node linkType: hard -"zod@npm:~4.3.6": +"zod@npm:^3.25.0 || ^4.0.0, zod@npm:~4.3.6": version: 4.3.6 resolution: "zod@npm:4.3.6" checksum: 10/25fc0f62e01b557b4644bf0b393bbaf47542ab30877c37837ea8caf314a8713d220c7d7fe51f68ffa72f0e1018ddfa34d96f1973d23033f5a2a5a9b6b9d9da01 From 1361a1f4f1e3c0cc3f2a191cef8eccc12a714cde Mon Sep 17 00:00:00 2001 From: Rohit Giri <rohitgiri0204@gmail.com> Date: Fri, 27 Feb 2026 20:42:05 +0530 Subject: [PATCH 078/108] refactor: migrate rooms.delete endpoint to new API format (#38549) --- .changeset/nine-otters-hug.md | 6 ++ apps/meteor/app/api/server/v1/rooms.ts | 69 +++++++++++++++-------- apps/meteor/tests/end-to-end/api/rooms.ts | 4 +- packages/rest-typings/src/v1/rooms.ts | 4 -- 4 files changed, 55 insertions(+), 28 deletions(-) create mode 100644 .changeset/nine-otters-hug.md diff --git a/.changeset/nine-otters-hug.md b/.changeset/nine-otters-hug.md new file mode 100644 index 0000000000000..78868e3057732 --- /dev/null +++ b/.changeset/nine-otters-hug.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/rest-typings': minor +'@rocket.chat/meteor': minor +--- + +migrated rooms.delete endpoint to new OpenAPI pattern with AJV validation diff --git a/apps/meteor/app/api/server/v1/rooms.ts b/apps/meteor/app/api/server/v1/rooms.ts index 0e05a9a966ab9..930e9cd8168cb 100644 --- a/apps/meteor/app/api/server/v1/rooms.ts +++ b/apps/meteor/app/api/server/v1/rooms.ts @@ -121,37 +121,58 @@ API.v1.addRoute( }, ); -API.v1.addRoute( +const roomDeleteEndpoint = API.v1.post( 'rooms.delete', { authRequired: true, + body: ajv.compile<{ roomId: string }>({ + type: 'object', + properties: { + roomId: { + type: 'string', + description: 'The ID of the room to delete.', + }, + }, + required: ['roomId'], + additionalProperties: false, + }), + response: { + 200: ajv.compile<void>({ + type: 'object', + properties: { + success: { + type: 'boolean', + enum: [true], + description: 'Indicates if the request was successful.', + }, + }, + required: ['success'], + additionalProperties: false, + }), + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + }, }, - { - async post() { - const { roomId } = this.bodyParams; + async function action() { + const { roomId } = this.bodyParams; - if (!roomId) { - return API.v1.failure("The 'roomId' param is required"); - } + const room = await Rooms.findOneById(roomId); - const room = await Rooms.findOneById(roomId); - - if (!room) { - throw new MeteorError('error-invalid-room', 'Invalid room', { - method: 'eraseRoom', - }); - } + if (!room) { + throw new MeteorError('error-invalid-room', 'Invalid room', { + method: 'eraseRoom', + }); + } - if (room.teamMain) { - throw new Meteor.Error('error-cannot-delete-team-channel', 'Cannot delete a team channel', { - method: 'eraseRoom', - }); - } + if (room.teamMain) { + throw new Meteor.Error('error-cannot-delete-team-channel', 'Cannot delete a team channel', { + method: 'eraseRoom', + }); + } - await eraseRoom(room, this.user); + await eraseRoom(room, this.user); - return API.v1.success(); - }, + return API.v1.success(); }, ); @@ -1190,7 +1211,9 @@ export const roomEndpoints = API.v1 }, ); -type RoomEndpoints = ExtractRoutesFromAPI<typeof roomEndpoints>; +type RoomEndpoints = ExtractRoutesFromAPI<typeof roomEndpoints> & + ExtractRoutesFromAPI<typeof roomEndpoints> & + ExtractRoutesFromAPI<typeof roomDeleteEndpoint>; declare module '@rocket.chat/rest-typings' { // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-empty-interface diff --git a/apps/meteor/tests/end-to-end/api/rooms.ts b/apps/meteor/tests/end-to-end/api/rooms.ts index f20b56e563df7..8bf1bb3ab08c7 100644 --- a/apps/meteor/tests/end-to-end/api/rooms.ts +++ b/apps/meteor/tests/end-to-end/api/rooms.ts @@ -2421,10 +2421,12 @@ describe('[Rooms]', () => { .expect(400) .expect((res) => { expect(res.body).to.have.property('success', false); - expect(res.body).to.have.property('error', "The 'roomId' param is required"); + expect(res.body).to.have.property('errorType', 'invalid-params'); + expect(res.body).to.have.property('error').include("must have required property 'roomId'"); }) .end(done); }); + it('should delete a room when the request is correct', (done) => { void request .post(api('rooms.delete')) diff --git a/packages/rest-typings/src/v1/rooms.ts b/packages/rest-typings/src/v1/rooms.ts index be2314d65bd41..5afad26ca5817 100644 --- a/packages/rest-typings/src/v1/rooms.ts +++ b/packages/rest-typings/src/v1/rooms.ts @@ -810,10 +810,6 @@ export type RoomsEndpoints = { }; }; - '/v1/rooms.delete': { - POST: (params: { roomId: string }) => void; - }; - '/v1/rooms.get': { GET: (params: { updatedSince: string }) => { update: IRoom[]; From d8baf395181b70fef9ce448eb509f65b66049615 Mon Sep 17 00:00:00 2001 From: Ahmed Nasser <ahmed.n.abdeltwab@gmail.com> Date: Fri, 27 Feb 2026 17:17:58 +0200 Subject: [PATCH 079/108] chore: Add OpenAPI Support to e2e.fetchMyKeys API (#36779) Co-authored-by: Matheus Cardoso <matheus@cardo.so> --- .changeset/short-starfishes-provide.md | 6 ++++ apps/meteor/app/api/server/v1/e2e.ts | 39 +++++++++++++++++--------- packages/rest-typings/src/v1/e2e.ts | 3 -- 3 files changed, 31 insertions(+), 17 deletions(-) create mode 100644 .changeset/short-starfishes-provide.md diff --git a/.changeset/short-starfishes-provide.md b/.changeset/short-starfishes-provide.md new file mode 100644 index 0000000000000..2d70c789a69cd --- /dev/null +++ b/.changeset/short-starfishes-provide.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/rest-typings": minor +--- + +Add OpenAPI support for the Rocket.Chat e2e.fetchMyKeys endpoints by migrating to a modern chained route definition syntax and utilizing shared AJV schemas for validation to enhance API documentation and ensure type safety through response validation. diff --git a/apps/meteor/app/api/server/v1/e2e.ts b/apps/meteor/app/api/server/v1/e2e.ts index 61a3b8075074c..0b125b6cde153 100644 --- a/apps/meteor/app/api/server/v1/e2e.ts +++ b/apps/meteor/app/api/server/v1/e2e.ts @@ -96,6 +96,31 @@ const e2eEndpoints = API.v1 return API.v1.success(); }, ) + .get( + 'e2e.fetchMyKeys', + { + authRequired: true, + query: undefined, + response: { + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + 200: ajv.compile<{ public_key?: string; private_key?: string }>({ + type: 'object', + properties: { + public_key: { type: 'string' }, + private_key: { type: 'string' }, + success: { type: 'boolean', enum: [true] }, + }, + required: ['success'], + }), + }, + }, + async function action() { + const result = await Users.fetchKeysByUserId(this.userId); + + return API.v1.success(result); + }, + ) .get( 'e2e.getUsersOfRoomWithoutKey', { @@ -142,20 +167,6 @@ const e2eEndpoints = API.v1 }, ); -API.v1.addRoute( - 'e2e.fetchMyKeys', - { - authRequired: true, - }, - { - async get() { - const result = await Users.fetchKeysByUserId(this.userId); - - return API.v1.success(result); - }, - }, -); - /** * @openapi * /api/v1/e2e.setUserPublicAndPrivateKeys: diff --git a/packages/rest-typings/src/v1/e2e.ts b/packages/rest-typings/src/v1/e2e.ts index 841fc7b730240..c6b393e55e3dc 100644 --- a/packages/rest-typings/src/v1/e2e.ts +++ b/packages/rest-typings/src/v1/e2e.ts @@ -158,9 +158,6 @@ export type E2eEndpoints = { '/v1/e2e.rejectSuggestedGroupKey': { POST: (params: E2eGetUsersOfRoomWithoutKeyProps) => void; }; - '/v1/e2e.fetchMyKeys': { - GET: () => { public_key: string; private_key: string }; - }; '/v1/e2e.fetchUsersWaitingForGroupKey': { GET: (params: E2EFetchUsersWaitingForGroupKeyProps) => { usersWaitingForE2EKeys: Record<IRoom['_id'], { _id: IUser['_id']; public_key: string }[]>; From 2a2701098536b32143003be8d267891978c708c9 Mon Sep 17 00:00:00 2001 From: Ahmed Nasser <ahmed.n.abdeltwab@gmail.com> Date: Fri, 27 Feb 2026 17:32:32 +0200 Subject: [PATCH 080/108] chore: Add OpenAPI support for the Rocket.Chat e2e.updateGroupKey endpoints (#39094) Co-authored-by: Guilherme Gazzo <guilhermegazzo@gmail.com> --- .changeset/pretty-jobs-juggle.md | 6 ++ apps/meteor/app/api/server/v1/e2e.ts | 101 +++++++++--------- packages/rest-typings/src/index.ts | 1 - packages/rest-typings/src/v1/e2e.ts | 28 ----- .../src/v1/e2e/e2eUpdateGroupKeyParamsPOST.ts | 26 ----- 5 files changed, 56 insertions(+), 106 deletions(-) create mode 100644 .changeset/pretty-jobs-juggle.md delete mode 100644 packages/rest-typings/src/v1/e2e/e2eUpdateGroupKeyParamsPOST.ts diff --git a/.changeset/pretty-jobs-juggle.md b/.changeset/pretty-jobs-juggle.md new file mode 100644 index 0000000000000..028fc592dd034 --- /dev/null +++ b/.changeset/pretty-jobs-juggle.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/rest-typings': minor +'@rocket.chat/meteor': minor +--- + +Adds OpenAPI support for the Rocket.Chat e2e.updateGroupKey endpoints by migrating to a modern chained route definition syntax and utilizing shared AJV schemas for validation to enhance API documentation and ensure type safety through response validation. diff --git a/apps/meteor/app/api/server/v1/e2e.ts b/apps/meteor/app/api/server/v1/e2e.ts index 0b125b6cde153..6c36b97d04cde 100644 --- a/apps/meteor/app/api/server/v1/e2e.ts +++ b/apps/meteor/app/api/server/v1/e2e.ts @@ -5,7 +5,6 @@ import { validateUnauthorizedErrorResponse, validateBadRequestErrorResponse, ise2eSetUserPublicAndPrivateKeysParamsPOST, - ise2eUpdateGroupKeyParamsPOST, isE2EProvideUsersGroupKeyProps, isE2EFetchUsersWaitingForGroupKeyProps, isE2EResetRoomKeyProps, @@ -38,6 +37,12 @@ type e2eGetUsersOfRoomWithoutKeyParamsGET = { rid: string; }; +type e2eUpdateGroupKeyParamsPOST = { + uid: string; + rid: string; + key: string; +}; + const E2eSetRoomKeyIdSchema = { type: 'object', properties: { @@ -63,12 +68,31 @@ const e2eGetUsersOfRoomWithoutKeyParamsGETSchema = { required: ['rid'], }; +const e2eUpdateGroupKeyParamsPOSTSchema = { + type: 'object', + properties: { + uid: { + type: 'string', + }, + rid: { + type: 'string', + }, + key: { + type: 'string', + }, + }, + additionalProperties: false, + required: ['uid', 'rid', 'key'], +}; + const isE2eSetRoomKeyIdProps = ajv.compile<E2eSetRoomKeyIdProps>(E2eSetRoomKeyIdSchema); const ise2eGetUsersOfRoomWithoutKeyParamsGET = ajv.compile<e2eGetUsersOfRoomWithoutKeyParamsGET>( e2eGetUsersOfRoomWithoutKeyParamsGETSchema, ); +const ise2eUpdateGroupKeyParamsPOST = ajv.compile<e2eUpdateGroupKeyParamsPOST>(e2eUpdateGroupKeyParamsPOSTSchema); + const e2eEndpoints = API.v1 .post( 'e2e.setRoomKeyID', @@ -165,6 +189,31 @@ const e2eEndpoints = API.v1 return API.v1.success(result); }, + ) + .post( + 'e2e.updateGroupKey', + { + authRequired: true, + body: ise2eUpdateGroupKeyParamsPOST, + response: { + 400: validateBadRequestErrorResponse, + 401: validateUnauthorizedErrorResponse, + 200: ajv.compile<void>({ + type: 'object', + properties: { + success: { type: 'boolean', enum: [true] }, + }, + required: ['success'], + }), + }, + }, + async function action() { + const { uid, rid, key } = this.bodyParams; + + await updateGroupKey(rid, uid, key, this.userId); + + return API.v1.success(); + }, ); /** @@ -222,56 +271,6 @@ API.v1.addRoute( }, ); -/** - * @openapi - * /api/v1/e2e.updateGroupKey: - * post: - * description: Updates the end-to-end encryption key for a user on a room - * security: - * - autenticated: {} - * requestBody: - * description: A tuple containing the user ID, the room ID, and the key - * content: - * application/json: - * schema: - * type: object - * properties: - * uid: - * type: string - * rid: - * type: string - * key: - * type: string - * responses: - * 200: - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/ApiSuccessV1' - * default: - * description: Unexpected error - * content: - * application/json: - * schema: - * $ref: '#/components/schemas/ApiFailureV1' - */ -API.v1.addRoute( - 'e2e.updateGroupKey', - { - authRequired: true, - validateParams: ise2eUpdateGroupKeyParamsPOST, - }, - { - async post() { - const { uid, rid, key } = this.bodyParams; - - await updateGroupKey(rid, uid, key, this.userId); - - return API.v1.success(); - }, - }, -); - API.v1.addRoute( 'e2e.acceptSuggestedGroupKey', { diff --git a/packages/rest-typings/src/index.ts b/packages/rest-typings/src/index.ts index 3edd726658537..b0e2dacff7a85 100644 --- a/packages/rest-typings/src/index.ts +++ b/packages/rest-typings/src/index.ts @@ -244,7 +244,6 @@ export * from './v1/server-events'; export * from './v1/autotranslate/AutotranslateGetSupportedLanguagesParamsGET'; export * from './v1/autotranslate/AutotranslateSaveSettingsParamsPOST'; export * from './v1/e2e/e2eSetUserPublicAndPrivateKeysParamsPOST'; -export * from './v1/e2e/e2eUpdateGroupKeyParamsPOST'; export * from './v1/e2e'; export * from './v1/import'; export * from './v1/email-inbox'; diff --git a/packages/rest-typings/src/v1/e2e.ts b/packages/rest-typings/src/v1/e2e.ts index c6b393e55e3dc..9e1de684e1b31 100644 --- a/packages/rest-typings/src/v1/e2e.ts +++ b/packages/rest-typings/src/v1/e2e.ts @@ -39,31 +39,6 @@ const E2eGetUsersOfRoomWithoutKeySchema = { export const isE2eGetUsersOfRoomWithoutKeyProps = ajv.compile<E2eGetUsersOfRoomWithoutKeyProps>(E2eGetUsersOfRoomWithoutKeySchema); -type E2eUpdateGroupKeyProps = { - uid: string; - rid: string; - key: string; -}; - -const E2eUpdateGroupKeySchema = { - type: 'object', - properties: { - uid: { - type: 'string', - }, - rid: { - type: 'string', - }, - key: { - type: 'string', - }, - }, - required: ['uid', 'rid', 'key'], - additionalProperties: false, -}; - -export const isE2eUpdateGroupKeyProps = ajv.compile<E2eUpdateGroupKeyProps>(E2eUpdateGroupKeySchema); - type E2EProvideUsersGroupKeyProps = { usersSuggestedGroupKeys: Record<IRoom['_id'], { _id: IUser['_id']; key: string; oldKeys: ISubscription['suggestedOldRoomKeys'] }[]>; }; @@ -149,9 +124,6 @@ export type E2eEndpoints = { '/v1/e2e.setUserPublicAndPrivateKeys': { POST: (params: E2eSetUserPublicAndPrivateKeysProps) => void; }; - '/v1/e2e.updateGroupKey': { - POST: (params: E2eUpdateGroupKeyProps) => void; - }; '/v1/e2e.acceptSuggestedGroupKey': { POST: (params: E2eGetUsersOfRoomWithoutKeyProps) => void; }; diff --git a/packages/rest-typings/src/v1/e2e/e2eUpdateGroupKeyParamsPOST.ts b/packages/rest-typings/src/v1/e2e/e2eUpdateGroupKeyParamsPOST.ts deleted file mode 100644 index dc0f4c3d6268e..0000000000000 --- a/packages/rest-typings/src/v1/e2e/e2eUpdateGroupKeyParamsPOST.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ajv } from '../Ajv'; - -export type e2eUpdateGroupKeyParamsPOST = { - uid: string; - rid: string; - key: string; -}; - -const e2eUpdateGroupKeyParamsPOSTSchema = { - type: 'object', - properties: { - uid: { - type: 'string', - }, - rid: { - type: 'string', - }, - key: { - type: 'string', - }, - }, - additionalProperties: false, - required: ['uid', 'rid', 'key'], -}; - -export const ise2eUpdateGroupKeyParamsPOST = ajv.compile<e2eUpdateGroupKeyParamsPOST>(e2eUpdateGroupKeyParamsPOSTSchema); From 2a3255329e8144d6902464cce29c72fb75791a41 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 12:40:16 -0300 Subject: [PATCH 081/108] chore(deps): bump actions/upload-artifact from 6 to 7 (#39122) Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-test-e2e.yml | 4 ++-- .github/workflows/ci.yml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml index 0e15ee6c9fb5a..c3c3c9068de4b 100644 --- a/.github/workflows/ci-test-e2e.yml +++ b/.github/workflows/ci-test-e2e.yml @@ -263,7 +263,7 @@ jobs: - name: Store playwright test trace if: inputs.type == 'ui' && always() - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: playwright-test-trace-${{ inputs.release }}-${{ matrix.mongodb-version }}-${{ matrix.shard }}${{ inputs.db-watcher-disabled == 'true' && '-no-watcher' || '' }} path: ./apps/meteor/tests/e2e/.playwright* @@ -279,7 +279,7 @@ jobs: - name: Store coverage if: inputs.coverage == matrix.mongodb-version - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: coverage-${{ inputs.type }}-${{ matrix.shard }} path: /tmp/coverage diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fed5441d44cb4..a7eaf612bde45 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -223,7 +223,7 @@ jobs: $(git ls-files -oi --exclude-standard -- ':(exclude)node_modules/*' ':(exclude)**/node_modules/*' ':(exclude)**/.meteor/*' ':(exclude)**/.turbo/*' ':(exclude).turbo/*' ':(exclude)**/.yarn/*' ':(exclude).yarn/*' ':(exclude).git/*') - name: Upload packages build artifact - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: packages-build path: /tmp/RocketChat-packages-build.tar.gz @@ -231,7 +231,7 @@ jobs: - name: Store turbo build if: steps.packages-cache-build.outputs.cache-hit != 'true' - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: turbo-build path: .turbo/cache @@ -707,7 +707,7 @@ jobs: npx nyc report --reporter=lcovonly --report-dir=/tmp/coverage_report/ui --temp-dir=/tmp/coverage/ui - name: Store coverage-reports - uses: actions/upload-artifact@v6 + uses: actions/upload-artifact@v7 with: name: reports-coverage path: /tmp/coverage_report From c3abfa28dbf7a402c0d1d495dce40c3861cd22fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?= <60678893+juliajforesti@users.noreply.github.com> Date: Fri, 27 Feb 2026 13:55:08 -0300 Subject: [PATCH 082/108] chore: replace messages `data-qa` (#38493) --- .../components/message/IgnoredContent.tsx | 2 +- .../components/message/MessageContentBody.tsx | 23 +++--- .../components/message/MessageHeader.tsx | 10 +-- .../file/GenericFileAttachment.tsx | 7 +- .../attachments/structure/AttachmentText.tsx | 8 +- .../message/toolbar/MessageToolbarItem.tsx | 14 +--- .../items/actions/ForwardMessageAction.tsx | 1 - .../items/actions/JumpToMessageAction.tsx | 1 - .../items/actions/QuoteMessageAction.tsx | 1 - .../items/actions/ReactionMessageAction.tsx | 1 - .../actions/ReplyInThreadMessageAction.tsx | 1 - .../message/variants/RoomMessage.tsx | 3 - .../message/variants/SystemMessage.tsx | 8 +- .../message/variants/ThreadMessage.tsx | 2 - .../message/variants/ThreadMessagePreview.tsx | 1 - .../variants/room/RoomMessageContent.tsx | 11 ++- .../variants/thread/ThreadMessageContent.tsx | 6 +- .../tests/e2e/channel-management.spec.ts | 10 ++- .../e2ee-encrypted-channels.spec.ts | 18 +++-- .../e2ee-encryption-decryption.spec.ts | 6 +- .../e2ee-file-encryption.spec.ts | 10 +-- .../page-objects/fragments/home-content.ts | 60 +++++++------- apps/meteor/tests/e2e/file-upload.spec.ts | 6 +- .../meteor/tests/e2e/files-management.spec.ts | 4 +- apps/meteor/tests/e2e/message-actions.spec.ts | 47 ++++------- apps/meteor/tests/e2e/messaging.spec.ts | 36 +++++---- .../omnichannel-send-pdf-transcript.spec.ts | 3 +- .../tests/e2e/page-objects/encrypted-room.ts | 4 +- .../page-objects/fragments/home-content.ts | 81 ++++++++++++------- .../fragments/home-flextab-pruneMessages.ts | 4 +- .../page-objects/fragments/home-flextab.ts | 4 - .../e2e/page-objects/fragments/message.ts | 6 +- .../omnichannel/omnichannel-transcript.ts | 4 - apps/meteor/tests/e2e/prune-messages.spec.ts | 13 ++- .../meteor/tests/e2e/quote-attachment.spec.ts | 2 +- apps/meteor/tests/e2e/system-messages.spec.ts | 3 +- apps/meteor/tests/e2e/team-management.spec.ts | 6 +- apps/meteor/tests/e2e/threads.spec.ts | 22 +++-- packages/i18n/src/locales/en.i18n.json | 5 +- 39 files changed, 222 insertions(+), 232 deletions(-) diff --git a/apps/meteor/client/components/message/IgnoredContent.tsx b/apps/meteor/client/components/message/IgnoredContent.tsx index 0e5e05bc65519..9a33d46508b82 100644 --- a/apps/meteor/client/components/message/IgnoredContent.tsx +++ b/apps/meteor/client/components/message/IgnoredContent.tsx @@ -17,7 +17,7 @@ const IgnoredContent = ({ onShowMessageIgnored }: IgnoredContentProps): ReactEle }; return ( - <MessageBody data-qa-type='message-body' dir='auto'> + <MessageBody role='document' aria-roledescription={t('message_body')} dir='auto'> <Box display='flex' alignItems='center' fontSize='c2' color='hint'> <span tabIndex={0} diff --git a/apps/meteor/client/components/message/MessageContentBody.tsx b/apps/meteor/client/components/message/MessageContentBody.tsx index ae9d52c3cd3e2..ff0735539a63e 100644 --- a/apps/meteor/client/components/message/MessageContentBody.tsx +++ b/apps/meteor/client/components/message/MessageContentBody.tsx @@ -2,6 +2,7 @@ import { MessageBody, Skeleton } from '@rocket.chat/fuselage'; import { Markup } from '@rocket.chat/gazzodown'; import type { ComponentProps } from 'react'; import { Suspense } from 'react'; +import { useTranslation } from 'react-i18next'; import type { MessageWithMdEnforced } from '../../lib/parseMessageTextToAstMarkdown'; import GazzodownText from '../GazzodownText'; @@ -10,14 +11,18 @@ type MessageContentBodyProps = Pick<MessageWithMdEnforced, 'mentions' | 'channel searchText?: string; } & ComponentProps<typeof MessageBody>; -const MessageContentBody = ({ mentions, channels, md, searchText, ...props }: MessageContentBodyProps) => ( - <MessageBody data-qa-type='message-body' dir='auto' {...props}> - <Suspense fallback={<Skeleton />}> - <GazzodownText channels={channels} mentions={mentions} searchText={searchText}> - <Markup tokens={md} /> - </GazzodownText> - </Suspense> - </MessageBody> -); +const MessageContentBody = ({ mentions, channels, md, searchText, ...props }: MessageContentBodyProps) => { + const { t } = useTranslation(); + + return ( + <MessageBody role='document' aria-roledescription={t('message_body')} dir='auto' {...props}> + <Suspense fallback={<Skeleton />}> + <GazzodownText channels={channels} mentions={mentions} searchText={searchText}> + <Markup tokens={md} /> + </GazzodownText> + </Suspense> + </MessageBody> + ); +}; export default MessageContentBody; diff --git a/apps/meteor/client/components/message/MessageHeader.tsx b/apps/meteor/client/components/message/MessageHeader.tsx index 427f61803b584..23196a6c52cec 100644 --- a/apps/meteor/client/components/message/MessageHeader.tsx +++ b/apps/meteor/client/components/message/MessageHeader.tsx @@ -56,19 +56,13 @@ const MessageHeader = ({ message }: MessageHeaderProps): ReactElement => { {...buttonProps} {...triggerProps} > - <MessageName - {...(!showUsername && { 'data-qa-type': 'username' })} - title={!showUsername && !usernameAndRealNameAreSame ? `@${user.username}` : undefined} - data-username={user.username} - > + <MessageName title={!showUsername && !usernameAndRealNameAreSame ? `@${user.username}` : undefined} data-username={user.username}> {message.alias || displayName} </MessageName> {showUsername && ( <> {' '} - <MessageUsername data-username={user.username} data-qa-type='username'> - @{user.username} - </MessageUsername> + <MessageUsername data-username={user.username}>@{user.username}</MessageUsername> </> )} </MessageNameContainer> 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 6d4955fb73424..d2dbd4f888257 100644 --- a/apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx +++ b/apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx @@ -78,12 +78,7 @@ const GenericFileAttachment = ({ <MessageGenericPreviewContent thumb={<MessageGenericPreviewIcon name='attachment-file' type={format || getFileExtension(title)} />} > - <MessageGenericPreviewTitle - download={!!openDocumentViewer} - externalUrl={getExternalUrl()} - onClick={handleTitleClick} - data-qa-type='attachment-title-link' - > + <MessageGenericPreviewTitle download={!!openDocumentViewer} externalUrl={getExternalUrl()} onClick={handleTitleClick}> {title} </MessageGenericPreviewTitle> {size && ( diff --git a/apps/meteor/client/components/message/content/attachments/structure/AttachmentText.tsx b/apps/meteor/client/components/message/content/attachments/structure/AttachmentText.tsx index 75153d1d57eb5..05a694df24997 100644 --- a/apps/meteor/client/components/message/content/attachments/structure/AttachmentText.tsx +++ b/apps/meteor/client/components/message/content/attachments/structure/AttachmentText.tsx @@ -1,10 +1,12 @@ import { Box } from '@rocket.chat/fuselage'; import type { ComponentPropsWithoutRef } from 'react'; +import { useTranslation } from 'react-i18next'; type AttachmentTextProps = ComponentPropsWithoutRef<typeof Box>; -const AttachmentText = (props: AttachmentTextProps) => ( - <Box data-qa-type='message-attachment' mbe={4} mi={2} fontScale='p2' color='default' {...props} /> -); +const AttachmentText = (props: AttachmentTextProps) => { + const { t } = useTranslation(); + return <Box role='document' aria-roledescription={t('message_attachment')} mbe={4} mi={2} fontScale='p2' color='default' {...props} />; +}; export default AttachmentText; diff --git a/apps/meteor/client/components/message/toolbar/MessageToolbarItem.tsx b/apps/meteor/client/components/message/toolbar/MessageToolbarItem.tsx index 633f3fc72d679..6cb9f2d351d0b 100644 --- a/apps/meteor/client/components/message/toolbar/MessageToolbarItem.tsx +++ b/apps/meteor/client/components/message/toolbar/MessageToolbarItem.tsx @@ -8,27 +8,17 @@ type MessageToolbarItemProps = { icon: IconName; title: string; disabled?: boolean; - qa: string; onClick: MouseEventHandler; }; -const MessageToolbarItem = ({ id, icon, title, disabled, qa, onClick }: MessageToolbarItemProps) => { +const MessageToolbarItem = ({ id, icon, title, disabled, onClick }: MessageToolbarItemProps) => { const hiddenActions = useLayoutHiddenActions().messageToolbox; if (hiddenActions.includes(id)) { return null; } - return ( - <FuselageMessageToolbarItem - icon={icon} - title={title} - disabled={disabled} - data-qa-id={qa} - data-qa-type='message-action-menu' - onClick={onClick} - /> - ); + return <FuselageMessageToolbarItem icon={icon} title={title} disabled={disabled} onClick={onClick} />; }; export default MessageToolbarItem; diff --git a/apps/meteor/client/components/message/toolbar/items/actions/ForwardMessageAction.tsx b/apps/meteor/client/components/message/toolbar/items/actions/ForwardMessageAction.tsx index 64e11d578272c..6c5720990b128 100644 --- a/apps/meteor/client/components/message/toolbar/items/actions/ForwardMessageAction.tsx +++ b/apps/meteor/client/components/message/toolbar/items/actions/ForwardMessageAction.tsx @@ -35,7 +35,6 @@ const ForwardMessageAction = ({ message, room }: ForwardMessageActionProps) => { id='forward-message' icon='arrow-forward' title={getTitle} - qa='Forward_message' disabled={encrypted || isABACEnabled} onClick={async () => { const permalink = await getPermaLink(message._id); diff --git a/apps/meteor/client/components/message/toolbar/items/actions/JumpToMessageAction.tsx b/apps/meteor/client/components/message/toolbar/items/actions/JumpToMessageAction.tsx index 1f1d757287f3d..802b505a55045 100644 --- a/apps/meteor/client/components/message/toolbar/items/actions/JumpToMessageAction.tsx +++ b/apps/meteor/client/components/message/toolbar/items/actions/JumpToMessageAction.tsx @@ -17,7 +17,6 @@ const JumpToMessageAction = ({ id, message }: JumpToMessageActionProps) => { id={id} icon='jump' title={t('Jump_to_message')} - qa='Jump_to_message' onClick={() => { setMessageJumpQueryStringParameter(message._id); }} diff --git a/apps/meteor/client/components/message/toolbar/items/actions/QuoteMessageAction.tsx b/apps/meteor/client/components/message/toolbar/items/actions/QuoteMessageAction.tsx index d73f193cbb1d5..088fb1f61028b 100644 --- a/apps/meteor/client/components/message/toolbar/items/actions/QuoteMessageAction.tsx +++ b/apps/meteor/client/components/message/toolbar/items/actions/QuoteMessageAction.tsx @@ -40,7 +40,6 @@ const QuoteMessageAction = ({ message, subscription }: QuoteMessageActionProps) id='quote-message' icon='quote' title={t('Quote')} - qa='Quote' onClick={() => { if (message && autoTranslateOptions?.autoTranslateEnabled && autoTranslateOptions.showAutoTranslate(message)) { message.msg = diff --git a/apps/meteor/client/components/message/toolbar/items/actions/ReactionMessageAction.tsx b/apps/meteor/client/components/message/toolbar/items/actions/ReactionMessageAction.tsx index 8772672c707ec..6ce3dc1be51d6 100644 --- a/apps/meteor/client/components/message/toolbar/items/actions/ReactionMessageAction.tsx +++ b/apps/meteor/client/components/message/toolbar/items/actions/ReactionMessageAction.tsx @@ -72,7 +72,6 @@ const ReactionMessageAction = ({ message, room, subscription }: ReactionMessageA id='reaction-message' icon='add-reaction' title={t('Add_Reaction')} - qa='Add_Reaction' onClick={(event) => { event.stopPropagation(); chat?.emojiPicker.open(event.currentTarget, (emoji) => { diff --git a/apps/meteor/client/components/message/toolbar/items/actions/ReplyInThreadMessageAction.tsx b/apps/meteor/client/components/message/toolbar/items/actions/ReplyInThreadMessageAction.tsx index d46f01935d582..d962cceffbe99 100644 --- a/apps/meteor/client/components/message/toolbar/items/actions/ReplyInThreadMessageAction.tsx +++ b/apps/meteor/client/components/message/toolbar/items/actions/ReplyInThreadMessageAction.tsx @@ -37,7 +37,6 @@ const ReplyInThreadMessageAction = ({ message, room, subscription }: ReplyInThre id='reply-in-thread' icon='thread' title={t('Reply_in_thread')} - qa='Reply_in_thread' onClick={(event) => { event.stopPropagation(); const routeName = router.getRouteName(); diff --git a/apps/meteor/client/components/message/variants/RoomMessage.tsx b/apps/meteor/client/components/message/variants/RoomMessage.tsx index 43d885f6839f6..ec96ff8fc1fc1 100644 --- a/apps/meteor/client/components/message/variants/RoomMessage.tsx +++ b/apps/meteor/client/components/message/variants/RoomMessage.tsx @@ -74,14 +74,11 @@ const RoomMessage = ({ isEditing={editing} isPending={message.temp} sequential={sequential} - data-qa-editing={editing} - data-qa-selected={selected} data-id={message._id} data-mid={message._id} data-unread={unread} data-sequential={sequential} data-own={message.u._id === uid} - data-qa-type='message' aria-busy={message.temp} {...props} > diff --git a/apps/meteor/client/components/message/variants/SystemMessage.tsx b/apps/meteor/client/components/message/variants/SystemMessage.tsx index 1577007c06097..ff19c38a5003b 100644 --- a/apps/meteor/client/components/message/variants/SystemMessage.tsx +++ b/apps/meteor/client/components/message/variants/SystemMessage.tsx @@ -68,8 +68,6 @@ const SystemMessage = ({ message, showUserAvatar, ...props }: SystemMessageProps tabIndex={0} onClick={isSelecting ? toggleSelected : undefined} isSelected={isSelected} - data-qa-selected={isSelected} - data-qa='system-message' data-system-message-type={message.t} {...props} > @@ -88,7 +86,11 @@ const SystemMessage = ({ message, showUserAvatar, ...props }: SystemMessageProps </> )} </MessageNameContainer> - {messageType && <MessageSystemBody data-qa-type='system-message-body'>{messageType.text(t, message)}</MessageSystemBody>} + {messageType && ( + <MessageSystemBody role='document' aria-roledescription={t('system_message_body')}> + {messageType.text(t, message)} + </MessageSystemBody> + )} <MessageSystemTimestamp title={formatDateAndTime(message.ts)}>{formatTime(message.ts)}</MessageSystemTimestamp> </MessageSystemBlock> {message.attachments && ( diff --git a/apps/meteor/client/components/message/variants/ThreadMessage.tsx b/apps/meteor/client/components/message/variants/ThreadMessage.tsx index 86a0442a69948..3acb7653ab625 100644 --- a/apps/meteor/client/components/message/variants/ThreadMessage.tsx +++ b/apps/meteor/client/components/message/variants/ThreadMessage.tsx @@ -45,13 +45,11 @@ const ThreadMessage = ({ message, sequential, unread, showUserAvatar }: ThreadMe isEditing={editing} isPending={message.temp} sequential={sequential} - data-qa-editing={editing} data-id={message._id} data-mid={message._id} data-unread={unread} data-sequential={sequential} data-own={message.u._id === uid} - data-qa-type='message' > <MessageLeftContainer> {!sequential && message.u.username && showUserAvatar && ( diff --git a/apps/meteor/client/components/message/variants/ThreadMessagePreview.tsx b/apps/meteor/client/components/message/variants/ThreadMessagePreview.tsx index 393f8b16e01a5..8822dcbfab712 100644 --- a/apps/meteor/client/components/message/variants/ThreadMessagePreview.tsx +++ b/apps/meteor/client/components/message/variants/ThreadMessagePreview.tsx @@ -77,7 +77,6 @@ const ThreadMessagePreview = ({ message, showUserAvatar, sequential, ...props }: onClick={handleThreadClick} onKeyDown={(e) => e.code === 'Enter' && handleThreadClick()} isSelected={isSelected} - data-qa-selected={isSelected} {...props} > {!sequential && ( diff --git a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx index a8fbfb2e345e4..813d63e9f9130 100644 --- a/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/room/RoomMessageContent.tsx @@ -2,9 +2,10 @@ import type { IMessage } from '@rocket.chat/core-typings'; import { isDiscussionMessage, isThreadMainMessage, isE2EEMessage, isQuoteAttachment } from '@rocket.chat/core-typings'; import { MessageBody } from '@rocket.chat/fuselage'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; -import { useTranslation, useUserId, useUserPresence } from '@rocket.chat/ui-contexts'; +import { useUserId, useUserPresence } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import { memo } from 'react'; +import { useTranslation } from 'react-i18next'; import { useChat } from '../../../../views/room/contexts/ChatContext'; import MessageContentBody from '../../MessageContentBody'; @@ -40,7 +41,7 @@ const RoomMessageContent = ({ message, unread, all, mention, searchText }: RoomM const { enabled: readReceiptEnabled } = useMessageListReadReceipts(); const messageUser = { ...message.u, roles: [], ...useUserPresence(message.u._id) }; const chat = useChat(); - const t = useTranslation(); + const { t } = useTranslation(); const normalizedMessage = useNormalizedMessage(message); const isMessageEncrypted = encrypted && normalizedMessage?.e2e === 'pending'; @@ -51,7 +52,11 @@ const RoomMessageContent = ({ message, unread, all, mention, searchText }: RoomM return ( <> - {isMessageEncrypted && <MessageBody data-qa-type='message-body'>{t('E2E_message_encrypted_placeholder')}</MessageBody>} + {isMessageEncrypted && ( + <MessageBody role='document' aria-roledescription={t('message_body')}> + {t('E2E_message_encrypted_placeholder')} + </MessageBody> + )} {!!quotes?.length && <Attachments attachments={quotes} />} diff --git a/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx b/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx index bb422e1f20b90..d137162d0b06d 100644 --- a/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx +++ b/apps/meteor/client/components/message/variants/thread/ThreadMessageContent.tsx @@ -46,7 +46,11 @@ const ThreadMessageContent = ({ message }: ThreadMessageContentProps): ReactElem return ( <> - {isMessageEncrypted && <MessageBody data-qa-type='message-body'>{t('E2E_message_encrypted_placeholder')}</MessageBody>} + {isMessageEncrypted && ( + <MessageBody role='document' aria-roledescription={t('message_body')}> + {t('E2E_message_encrypted_placeholder')} + </MessageBody> + )} {!!quotes?.length && <Attachments attachments={quotes} />} diff --git a/apps/meteor/tests/e2e/channel-management.spec.ts b/apps/meteor/tests/e2e/channel-management.spec.ts index bd76bafb4ec16..9823ee6850498 100644 --- a/apps/meteor/tests/e2e/channel-management.spec.ts +++ b/apps/meteor/tests/e2e/channel-management.spec.ts @@ -155,7 +155,7 @@ test.describe.serial('channel-management', () => { test('should access targetTeam through discussion header', async ({ page }) => { await poHomeChannel.navbar.openChat(targetChannel); - await page.locator('[data-qa-type="message"]', { hasText: discussionName }).locator('button').first().click(); + await page.getByRole('listitem', { name: discussionName }).getByRole('button', { name: 'Reply' }).click(); await page.getByRole('button', { name: `Back to ${targetChannel} channel`, exact: true }).focus(); await page.keyboard.press('Space'); @@ -249,7 +249,7 @@ test.describe.serial('channel-management', () => { await user1Page.close(); }); - test('should ignore user1 messages', async () => { + test('should ignore user1 messages', async ({ page }) => { await poHomeChannel.navbar.openChat(targetChannel); await poHomeChannel.roomToolbar.openMembersTab(); await poHomeChannel.tabs.members.showAllUsers(); @@ -257,13 +257,14 @@ test.describe.serial('channel-management', () => { await poHomeChannel.tabs.members.openMoreActions(); await expect(poHomeChannel.tabs.members.getMenuItemAction('Unignore')).toBeVisible(); + await page.keyboard.press('Escape'); const user1Channel = new HomeChannel(user1Page); await user1Page.goto(`/channel/${targetChannel}`); await user1Channel.content.waitForChannel(); await user1Channel.content.sendMessage('message to check ignore'); - await expect(poHomeChannel.content.lastUserMessageBody).toContainText('This message was ignored'); + await expect(poHomeChannel.content.lastUserMessageBody.getByRole('button', { name: 'This message was ignored' })).toBeVisible(); }); test('should unignore single user1 message', async () => { @@ -281,7 +282,7 @@ test.describe.serial('channel-management', () => { await expect(poHomeChannel.content.lastUserMessageBody).toContainText('only message to be unignored'); }); - test('should unignore user1 messages', async () => { + test('should unignore user1 messages', async ({ page }) => { const user1Channel = new HomeChannel(user1Page); await user1Page.goto(`/channel/${targetChannel}`); await user1Channel.content.waitForChannel(); @@ -296,6 +297,7 @@ test.describe.serial('channel-management', () => { await poHomeChannel.tabs.members.openMoreActions(); await expect(poHomeChannel.tabs.members.getMenuItemAction('Ignore')).toBeVisible(); + await page.keyboard.press('Escape'); await user1Channel.content.sendMessage('message after being unignored'); diff --git a/apps/meteor/tests/e2e/e2e-encryption/e2ee-encrypted-channels.spec.ts b/apps/meteor/tests/e2e/e2e-encryption/e2ee-encrypted-channels.spec.ts index 4e5708d56aa5d..bc5e101dbf3e4 100644 --- a/apps/meteor/tests/e2e/e2e-encryption/e2ee-encrypted-channels.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption/e2ee-encrypted-channels.spec.ts @@ -92,9 +92,7 @@ test.describe('E2EE Encrypted Channels', () => { await expect(poHomeChannel.content.lastUserMessageBody).toHaveText('This is the thread main message.'); await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); - - await page.locator('[data-qa-type="message"]').last().hover(); - await page.locator('role=button[name="Reply in thread"]').click(); + await poHomeChannel.content.openReplyInThread(); await expect(page).toHaveURL(/.*thread/); @@ -106,7 +104,7 @@ test.describe('E2EE Encrypted Channels', () => { await page.keyboard.press('Enter'); await expect(poHomeChannel.content.lastThreadMessageText).toContainText('This is an encrypted thread message also sent in channel'); await expect(poHomeChannel.content.lastThreadMessageText.locator('.rcx-icon--name-key')).toBeVisible(); - await expect(poHomeChannel.content.lastUserMessage).toContainText('This is an encrypted thread message also sent in channel'); + await expect(poHomeChannel.content.lastThreadMessagePreview).toContainText('This is an encrypted thread message also sent in channel'); await expect(poHomeChannel.content.mainThreadMessageText).toContainText('This is the thread main message.'); await expect(poHomeChannel.content.mainThreadMessageText.locator('.rcx-icon--name-key')).toBeVisible(); }); @@ -125,7 +123,7 @@ test.describe('E2EE Encrypted Channels', () => { await expect(poHomeChannel.content.lastUserMessageBody).toHaveText('This is an encrypted message.'); await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); - await page.locator('[data-qa-type="message"]').last().hover(); + await poHomeChannel.content.lastUserMessage.hover(); await expect(page.locator('role=button[name="Forward message not available on encrypted content"]')).toBeDisabled(); await poHomeChannel.content.openLastMessageMenu(); @@ -308,7 +306,10 @@ test.describe('E2EE Encrypted Channels', () => { await expect(page.getByRole('dialog', { name: 'Pinned Messages' })).toBeVisible(); - const lastPinnedMessage = page.getByRole('dialog', { name: 'Pinned Messages' }).locator('[data-qa-type="message"]').last(); + const lastPinnedMessage = page + .getByRole('dialog', { name: 'Pinned Messages' }) + .locator('[role="listitem"][aria-roledescription="message"]') + .last(); await expect(lastPinnedMessage).toContainText('This message should be pinned and stared.'); await lastPinnedMessage.hover(); await lastPinnedMessage.locator('role=button[name="More"]').waitFor(); @@ -320,7 +321,10 @@ test.describe('E2EE Encrypted Channels', () => { await poHomeChannel.tabs.kebab.click(); await poHomeChannel.tabs.btnStarredMessageList.click(); - const lastStarredMessage = page.getByRole('dialog', { name: 'Starred Messages' }).locator('[data-qa-type="message"]').last(); + const lastStarredMessage = page + .getByRole('dialog', { name: 'Starred Messages' }) + .locator('[role="listitem"][aria-roledescription="message"]') + .last(); await expect(page.getByRole('dialog', { name: 'Starred Messages' })).toBeVisible(); await expect(lastStarredMessage).toContainText('This message should be pinned and stared.'); await lastStarredMessage.hover(); diff --git a/apps/meteor/tests/e2e/e2e-encryption/e2ee-encryption-decryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption/e2ee-encryption-decryption.spec.ts index e60a2777ff596..330b306cb4a86 100644 --- a/apps/meteor/tests/e2e/e2e-encryption/e2ee-encryption-decryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption/e2ee-encryption-decryption.spec.ts @@ -102,7 +102,7 @@ test.describe('E2EE Encryption and Decryption - Basic Features', () => { // Check the file upload await expect(encryptedRoomPage.lastMessage.encryptedIcon).toBeVisible(); - await expect(encryptedRoomPage.lastMessage.fileUploadName).toContainText(fileName); + await expect(encryptedRoomPage.lastMessage.getFileUploadByName(fileName)).toBeVisible(); await expect(encryptedRoomPage.lastMessage.body).toHaveText(fileDescription); }); @@ -118,7 +118,7 @@ test.describe('E2EE Encryption and Decryption - Basic Features', () => { await fileUploadModal.send(); await expect(encryptedRoomPage.lastMessage.encryptedIcon).not.toBeVisible(); - await expect(encryptedRoomPage.lastMessage.fileUploadName).toContainText(fileName); + await expect(encryptedRoomPage.lastMessage.getFileUploadByName(fileName)).toBeVisible(); await expect(encryptedRoomPage.lastMessage.body).toHaveText(fileDescription); }); @@ -144,7 +144,7 @@ test.describe('E2EE Encryption and Decryption - Basic Features', () => { await expect(encryptedRoomPage.lastNthMessage(1).encryptedIcon).toBeVisible(); await expect(encryptedRoomPage.lastMessage.encryptedIcon).not.toBeVisible(); - await expect(encryptedRoomPage.lastMessage.fileUploadName).toContainText(fileName); + await expect(encryptedRoomPage.lastMessage.getFileUploadByName(fileName)).toBeVisible(); await expect(encryptedRoomPage.lastMessage.body).toHaveText(fileDescription); }); diff --git a/apps/meteor/tests/e2e/e2e-encryption/e2ee-file-encryption.spec.ts b/apps/meteor/tests/e2e/e2e-encryption/e2ee-file-encryption.spec.ts index 173ae4cf2036f..da41810380978 100644 --- a/apps/meteor/tests/e2e/e2e-encryption/e2ee-file-encryption.spec.ts +++ b/apps/meteor/tests/e2e/e2e-encryption/e2ee-file-encryption.spec.ts @@ -63,7 +63,7 @@ test.describe('E2EE File Encryption', () => { await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); await expect(poHomeChannel.content.getFileDescription).toHaveText('any_description'); - await expect(poHomeChannel.content.lastMessageFileName).toContainText('any_file1.txt'); + await expect(poHomeChannel.content.getLastMessageByFileName('any_file1.txt')).toBeVisible(); }); await test.step('edit the description', async () => { @@ -97,7 +97,7 @@ test.describe('E2EE File Encryption', () => { await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); await expect(poHomeChannel.content.getFileDescription).toHaveText('message 1'); - await expect(poHomeChannel.content.lastMessageFileName).toContainText('any_file1.txt'); + await expect(poHomeChannel.content.getLastMessageByFileName('any_file1.txt')).toBeVisible(); }); await test.step('set whitelisted media type setting', async () => { @@ -112,7 +112,7 @@ test.describe('E2EE File Encryption', () => { await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); await expect(poHomeChannel.content.getFileDescription).toHaveText('message 2'); - await expect(poHomeChannel.content.lastMessageFileName).toContainText('any_file2.txt'); + await expect(poHomeChannel.content.getLastMessageByFileName('any_file2.txt')).toBeVisible(); }); await test.step('set blacklisted media type setting to not accept application/octet-stream media type', async () => { @@ -127,7 +127,7 @@ test.describe('E2EE File Encryption', () => { await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).toBeVisible(); await expect(poHomeChannel.content.getFileDescription).toHaveText('message 2'); - await expect(poHomeChannel.content.lastMessageFileName).toContainText('any_file2.txt'); + await expect(poHomeChannel.content.getLastMessageByFileName('any_file2.txt')).toBeVisible(); }); }); @@ -158,7 +158,7 @@ test.describe('E2EE File Encryption', () => { await expect(poHomeChannel.content.lastUserMessage.locator('.rcx-icon--name-key')).not.toBeVisible(); await expect(poHomeChannel.content.getFileDescription).toHaveText('any_description'); - await expect(poHomeChannel.content.lastMessageFileName).toContainText('any_file1.txt'); + await expect(poHomeChannel.content.getLastMessageByFileName('any_file1.txt')).toBeVisible(); }); }); }); diff --git a/apps/meteor/tests/e2e/federation/page-objects/fragments/home-content.ts b/apps/meteor/tests/e2e/federation/page-objects/fragments/home-content.ts index a60c5c4955b92..4fb82d858afd6 100644 --- a/apps/meteor/tests/e2e/federation/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/federation/page-objects/fragments/home-content.ts @@ -15,12 +15,16 @@ export class FederationHomeContent { return this.page.locator('role=menu[name="People"]'); } + get messageListItems(): Locator { + return this.page.locator('[role="listitem"][aria-roledescription="message"]'); + } + get lastUserMessage(): Locator { - return this.page.locator('[data-qa-type="message"]').last(); + return this.messageListItems.last(); } get lastUserMessageBody(): Locator { - return this.lastUserMessage.locator('[data-qa-type="message-body"]'); + return this.lastUserMessage.locator('[role="document"][aria-roledescription="message body"]'); } async sendMessage(text: string): Promise<void> { @@ -96,30 +100,36 @@ export class FederationHomeContent { } get lastMessageFileName(): Locator { - return this.page.locator('[data-qa-type="message"]:last-child'); + return this.page.locator('[role="listitem"][aria-roledescription="message"]:last-child'); } async getLastFileMessageByFileName(filename: string): Promise<Locator> { - return this.page.locator('[data-qa-type="message"]:last-child .rcx-message-container').last().locator(`div[title="${filename}"]`); + return this.page + .locator('[role="listitem"][aria-roledescription="message"]:last-child .rcx-message-container') + .last() + .locator(`div[title="${filename}"]`); } async getLastFileThreadMessageByFileName(filename: string): Promise<Locator> { return this.page - .locator('div.thread-list ul.thread [data-qa-type="message"]:last-child .rcx-message-container') + .locator('div.thread-list ul.thread [role="listitem"][aria-roledescription="message"]:last-child .rcx-message-container') .last() .locator(`div[title="${filename}"]`); } get lastFileMessage(): Locator { - return this.page.locator('[data-qa-type="message"]:last-child .rcx-message-container').last(); + return this.page.locator('[role="listitem"][aria-roledescription="message"]:last-child .rcx-message-container').last(); } get waitForLastMessageTextAttachmentEqualsText(): Locator { - return this.page.locator('[data-qa-type="message"]:last-child .rcx-attachment__details .rcx-message-body'); + return this.page.locator('[role="listitem"][aria-roledescription="message"]:last-child .rcx-attachment__details .rcx-message-body'); } get waitForLastThreadMessageTextAttachmentEqualsText(): Locator { - return this.page.locator('div.thread-list ul.thread [data-qa-type="message"]').last().locator('.rcx-attachment__details'); + return this.page + .locator('div.thread-list ul.thread [role="listitem"][aria-roledescription="message"]') + .last() + .locator('.rcx-attachment__details'); } get btnOptionEditMessage(): Locator { @@ -167,7 +177,7 @@ export class FederationHomeContent { } get lastThreadMessageText(): Locator { - return this.page.locator('div.thread-list ul.thread [data-qa-type="message"]').last(); + return this.page.locator('div.thread-list ul.thread [role="listitem"][aria-roledescription="message"]').last(); } async sendFileMessage(fileName: string): Promise<void> { @@ -180,9 +190,9 @@ export class FederationHomeContent { } async openLastMessageMenu(): Promise<void> { - await this.page.locator('[data-qa-type="message"]').last().hover(); - await this.page.locator('[data-qa-type="message"]').last().locator('[data-qa-type="message-action-menu"][data-qa-id="menu"]').waitFor(); - await this.page.locator('[data-qa-type="message"]').last().locator('[data-qa-type="message-action-menu"][data-qa-id="menu"]').click(); + await this.lastUserMessage.hover(); + await this.lastUserMessage.getByRole('button', { name: 'More', exact: true }).waitFor(); + await this.lastUserMessage.getByRole('button', { name: 'More', exact: true }).click(); } threadSendToChannelAlso(): Locator { @@ -197,19 +207,9 @@ export class FederationHomeContent { } async openLastThreadMessageMenu(): Promise<void> { - await this.page.getByRole('dialog').locator('[data-qa-type="message"]').last().hover(); - await this.page - .getByRole('dialog') - .locator('[data-qa-type="message"]') - .last() - .locator('[data-qa-type="message-action-menu"][data-qa-id="menu"]') - .waitFor(); - await this.page - .getByRole('dialog') - .locator('[data-qa-type="message"]') - .last() - .locator('[data-qa-type="message-action-menu"][data-qa-id="menu"]') - .click(); + await this.lastThreadMessageText.hover(); + await this.lastThreadMessageText.getByRole('button', { name: 'More', exact: true }).waitFor(); + await this.lastThreadMessageText.getByRole('button', { name: 'More', exact: true }).click(); } async quoteMessageInsideThread(message: string): Promise<void> { @@ -226,19 +226,19 @@ export class FederationHomeContent { } async unreactLastMessage(): Promise<void> { - await this.page.locator('[data-qa-type="message"]').last().locator('.rcx-message-reactions__reaction').nth(1).waitFor(); - await this.page.locator('[data-qa-type="message"]').last().locator('.rcx-message-reactions__reaction').nth(1).click(); + await this.lastUserMessage.locator('.rcx-message-reactions__reaction').nth(1).waitFor(); + await this.lastUserMessage.locator('.rcx-message-reactions__reaction').nth(1).click(); } async getSystemMessageByText(text: string): Promise<Locator> { - return this.page.locator('div[data-qa="system-message"] div[data-qa-type="system-message-body"]', { hasText: text }); + return this.page.locator('[role="document"][aria-roledescription="system message body"]', { hasText: text }); } async getLastSystemMessageName(): Promise<Locator> { - return this.page.locator('div[data-qa="system-message"]:last-child span.rcx-message-system__name'); + return this.page.locator('[role="listitem"][aria-roledescription="system message"]').last().getByRole('button'); } async getAllReactions(): Promise<Locator> { - return this.page.locator('[data-qa-type="message"]').last().locator('.rcx-message-reactions__reaction'); + return this.lastUserMessage.locator('.rcx-message-reactions__reaction'); } } diff --git a/apps/meteor/tests/e2e/file-upload.spec.ts b/apps/meteor/tests/e2e/file-upload.spec.ts index 7603332fb0d7a..a7a77ec903726 100644 --- a/apps/meteor/tests/e2e/file-upload.spec.ts +++ b/apps/meteor/tests/e2e/file-upload.spec.ts @@ -48,7 +48,7 @@ test.describe.serial('file-upload', () => { await poHomeChannel.content.btnModalConfirm.click(); await expect(poHomeChannel.content.getFileDescription).toHaveText('any_description'); - await expect(poHomeChannel.content.lastMessageFileName).toContainText('any_file1.txt'); + await expect(poHomeChannel.content.getLastMessageByFileName('any_file1.txt')).toBeVisible(); }); test('should send lst file successfully', async () => { @@ -57,7 +57,7 @@ test.describe.serial('file-upload', () => { await poHomeChannel.content.btnModalConfirm.click(); await expect(poHomeChannel.content.getFileDescription).toHaveText('lst_description'); - await expect(poHomeChannel.content.lastMessageFileName).toContainText('lst-test.lst'); + await expect(poHomeChannel.content.getLastMessageByFileName('lst-test.lst')).toBeVisible(); }); test('should send drawio (unknown media type) file successfully', async ({ page }) => { @@ -67,7 +67,7 @@ test.describe.serial('file-upload', () => { await poHomeChannel.content.btnModalConfirm.click(); await expect(poHomeChannel.content.getFileDescription).toHaveText('drawio_description'); - await expect(poHomeChannel.content.lastMessageFileName).toContainText('diagram.drawio'); + await expect(poHomeChannel.content.getLastMessageByFileName('diagram.drawio')).toBeVisible(); }); test('should not to send drawio file (unknown media type) when the default media type is blocked', async ({ api, page }) => { diff --git a/apps/meteor/tests/e2e/files-management.spec.ts b/apps/meteor/tests/e2e/files-management.spec.ts index 5ee95a3a787e0..2f193e614437d 100644 --- a/apps/meteor/tests/e2e/files-management.spec.ts +++ b/apps/meteor/tests/e2e/files-management.spec.ts @@ -29,7 +29,7 @@ test.describe.serial('files-management', () => { test('should send a file and manage it in the list', async () => { await poHomeChannel.content.dragAndDropTxtFile(); await poHomeChannel.content.btnModalConfirm.click(); - await expect(poHomeChannel.content.lastMessageFileName).toHaveText(TEST_FILE_TXT); + await expect(poHomeChannel.content.getLastMessageByFileName(TEST_FILE_TXT)).toBeVisible(); await poHomeChannel.roomToolbar.openMoreOptions(); await poHomeChannel.roomToolbar.menuItemFiles.click(); @@ -49,7 +49,7 @@ test.describe.serial('files-management', () => { await poHomeChannel.tabs.files.deleteFile(TEST_FILE_TXT); await expect(poHomeChannel.tabs.files.getFileByName(TEST_FILE_TXT)).toHaveCount(0); - await expect(poHomeChannel.content.lastUserMessage).not.toBeVisible(); + await expect(poHomeChannel.content.getLastMessageByFileName(TEST_FILE_TXT)).not.toBeVisible(); }); }); }); diff --git a/apps/meteor/tests/e2e/message-actions.spec.ts b/apps/meteor/tests/e2e/message-actions.spec.ts index 5e043d198d37e..787a6fd7df311 100644 --- a/apps/meteor/tests/e2e/message-actions.spec.ts +++ b/apps/meteor/tests/e2e/message-actions.spec.ts @@ -34,47 +34,35 @@ test.describe.serial('message-actions', () => { test('expect reply the message', async ({ page }) => { await poHomeChannel.content.sendMessage('this is a message for reply'); - await page.locator('[data-qa-type="message"]').last().hover(); - await page.locator('role=button[name="Reply in thread"]').click(); + await poHomeChannel.content.openReplyInThread(); await page.locator('.rcx-vertical-bar').locator(`role=textbox[name="Message #${targetChannel}"]`).type('this is a reply message'); await page.keyboard.press('Enter'); - await expect(poHomeChannel.tabs.flexTabViewThreadMessage).toHaveText('this is a reply message'); + await expect(poHomeChannel.content.lastThreadMessageText).toHaveText('this is a reply message'); }); // with thread open we listen to the subscription and update the collection from there test('expect follow/unfollow message with thread open', async ({ page }) => { await test.step('start thread', async () => { await poHomeChannel.content.sendMessage('this is a message for reply'); - await page.locator('[data-qa-type="message"]').last().hover(); - await page.locator('role=button[name="Reply in thread"]').click(); + await poHomeChannel.content.openReplyInThread(); await page.getByRole('dialog').locator(`role=textbox[name="Message #${targetChannel}"]`).fill('this is a reply message'); await page.keyboard.press('Enter'); - await expect(poHomeChannel.tabs.flexTabViewThreadMessage).toHaveText('this is a reply message'); + await expect(poHomeChannel.content.lastThreadMessageText).toHaveText('this is a reply message'); }); await test.step('unfollow thread', async () => { - const unFollowButton = page - .locator('[data-qa-type="message"]', { has: page.getByRole('button', { name: 'Following' }) }) - .last() - .getByRole('button', { name: 'Following' }); + const unFollowButton = poHomeChannel.content.lastUserMessage.getByRole('button', { name: 'Following' }); await expect(unFollowButton).toBeVisible(); + await unFollowButton.click(); }); await test.step('follow thread', async () => { - const followButton = page - .locator('[data-qa-type="message"]', { has: page.getByRole('button', { name: 'Not following' }) }) - .last() - .getByRole('button', { name: 'Not following' }); + const followButton = poHomeChannel.content.lastUserMessage.getByRole('button', { name: 'Not following' }); await expect(followButton).toBeVisible(); await followButton.click(); - await expect( - page - .locator('[data-qa-type="message"]', { has: page.getByRole('button', { name: 'Following' }) }) - .last() - .getByRole('button', { name: 'Following' }), - ).toBeVisible(); + await expect(poHomeChannel.content.lastUserMessage.getByRole('button', { name: 'Following' })).toBeVisible(); }); }); @@ -82,27 +70,26 @@ test.describe.serial('message-actions', () => { test('expect follow/unfollow message with thread closed', async ({ page }) => { await test.step('start thread', async () => { await poHomeChannel.content.sendMessage('this is a message for reply'); - await page.locator('[data-qa-type="message"]').last().hover(); - await page.locator('role=button[name="Reply in thread"]').click(); + await poHomeChannel.content.openReplyInThread(); await page.locator('.rcx-vertical-bar').locator(`role=textbox[name="Message #${targetChannel}"]`).fill('this is a reply message'); await page.keyboard.press('Enter'); - await expect(poHomeChannel.tabs.flexTabViewThreadMessage).toHaveText('this is a reply message'); + await expect(poHomeChannel.content.lastThreadMessageText).toHaveText('this is a reply message'); }); // close thread before testing because the behavior changes await page.getByRole('dialog').getByRole('button', { name: 'Close', exact: true }).click(); await test.step('unfollow thread', async () => { - const unFollowButton = page.locator('[data-qa-type="message"]').last().getByTitle('Following'); + const unFollowButton = poHomeChannel.content.lastUserMessage.getByRole('button', { name: 'Following' }); await expect(unFollowButton).toBeVisible(); await unFollowButton.click(); }); await test.step('follow thread', async () => { - const followButton = page.locator('[data-qa-type="message"]').last().getByTitle('Not following'); + const followButton = poHomeChannel.content.lastUserMessage.getByRole('button', { name: 'Not following' }); await expect(followButton).toBeVisible(); await followButton.click(); - await expect(page.locator('[data-qa-type="message"]').last().getByTitle('Following')).toBeVisible(); + await expect(poHomeChannel.content.lastUserMessage.getByRole('button', { name: 'Following' })).toBeVisible(); }); }); @@ -120,17 +107,15 @@ test.describe.serial('message-actions', () => { await poHomeChannel.content.sendMessage('Message to delete'); await poHomeChannel.content.deleteLastMessage(); - await expect(poHomeChannel.content.lastUserMessage.locator('[data-qa-type="message-body"]:has-text("Message to delete")')).toHaveCount( - 0, - ); + await expect(poHomeChannel.content.lastUserMessageBody).not.toHaveText('Message to delete'); }); test('expect quote the message', async ({ page }) => { const message = `Message for quote - ${Date.now()}`; await poHomeChannel.content.sendMessage(message); - await page.locator('[data-qa-type="message"]').last().hover(); - await page.locator('role=button[name="Quote"]').click(); + await poHomeChannel.content.lastUserMessage.hover(); + await poHomeChannel.content.lastUserMessage.getByRole('button', { name: 'Quote' }).click(); await page.locator('[name="msg"]').fill('this is a quote message'); await page.keyboard.press('Enter'); diff --git a/apps/meteor/tests/e2e/messaging.spec.ts b/apps/meteor/tests/e2e/messaging.spec.ts index c6b781aaa2fca..fafc19da35073 100644 --- a/apps/meteor/tests/e2e/messaging.spec.ts +++ b/apps/meteor/tests/e2e/messaging.spec.ts @@ -36,18 +36,18 @@ test.describe('Messaging', () => { await test.step('move focus to the second message', async () => { await page.keyboard.press('Shift+Tab'); - await expect(page.locator('[data-qa-type="message"]').last()).toBeFocused(); + await expect(poHomeChannel.content.lastUserMessage).toBeFocused(); }); await test.step('move focus to the first system message', async () => { await page.keyboard.press('ArrowUp'); await page.keyboard.press('ArrowUp'); - await expect(page.locator('[data-qa="system-message"]').first()).toBeFocused(); + await expect(poHomeChannel.content.systemMessageListItems.first()).toBeFocused(); }); await test.step('move focus to the first typed message', async () => { await page.keyboard.press('ArrowDown'); - await expect(page.locator('[data-qa-type="message"]:has-text("msg1")')).toBeFocused(); + await expect(poHomeChannel.content.getMessageByText('msg1')).toBeFocused(); }); await test.step('move focus to the room title', async () => { @@ -59,20 +59,21 @@ test.describe('Messaging', () => { await page.keyboard.press('Tab'); await page.keyboard.press('Tab'); await page.keyboard.press('Tab'); - await expect(page.locator('[data-qa-type="message"]:has-text("msg1")')).toBeFocused(); + await expect(poHomeChannel.content.getMessageByText('msg1')).toBeFocused(); }); await test.step('move focus to the message toolbar', async () => { - await page - .locator('[data-qa-type="message"]:has-text("msg1")') + await poHomeChannel.content + .getMessageByText('msg1') .locator('[role=toolbar][aria-label="Message actions"]') .getByRole('button', { name: 'Add reaction' }) .waitFor(); + await page.keyboard.press('Tab'); await page.keyboard.press('Tab'); await expect( - page - .locator('[data-qa-type="message"]:has-text("msg1")') + poHomeChannel.content + .getMessageByText('msg1') .locator('[role=toolbar][aria-label="Message actions"]') .getByRole('button', { name: 'Add reaction' }), ).toBeFocused(); @@ -80,8 +81,8 @@ test.describe('Messaging', () => { await test.step('move focus to the composer', async () => { await page.keyboard.press('Tab'); - await page - .locator('[data-qa-type="message"]:has-text("msg2")') + await poHomeChannel.content + .getMessageByText('msg2') .locator('[role=toolbar][aria-label="Message actions"]') .getByRole('button', { name: 'Add reaction' }) .waitFor(); @@ -123,18 +124,19 @@ test.describe('Messaging', () => { test('should not restore focus on the last focused if it was triggered by click', async ({ page }) => { await poHomeChannel.navbar.openChat(targetChannel); - await page.locator('[data-qa-type="message"]:has-text("msg1")').click(); + await poHomeChannel.content.getMessageByText('msg1').click(); + await poHomeChannel.composer.inputMessage.click(); await page.keyboard.press('Shift+Tab'); - await expect(page.locator('[data-qa-type="message"]:has-text("msg2")')).toBeFocused(); + await expect(poHomeChannel.content.getMessageByText('msg2')).toBeFocused(); }); - test('should not focus on the last message when focusing by click', async ({ page }) => { + test('should not focus on the last message when focusing by click', async () => { await poHomeChannel.navbar.openChat(targetChannel); - await page.locator('[data-qa-type="message"]:has-text("msg1")').click(); + await poHomeChannel.content.getMessageByText('msg1').click(); - await expect(page.locator('[data-qa-type="message"]').last()).not.toBeFocused(); + await expect(poHomeChannel.content.lastUserMessage).not.toBeFocused(); }); test('should focus the latest message when moving the focus on the list and theres no previous focus', async ({ page }) => { @@ -145,7 +147,7 @@ test.describe('Messaging', () => { await page.keyboard.press('Tab'); await page.keyboard.press('Tab'); await page.keyboard.press('Tab'); - await expect(page.locator('[data-qa-type="message"]').last()).toBeFocused(); + await expect(poHomeChannel.content.lastUserMessage).toBeFocused(); }); await test.step('move focus to the list again', async () => { @@ -153,7 +155,7 @@ test.describe('Messaging', () => { await page.keyboard.press('Tab'); await page.keyboard.press('Tab'); await page.keyboard.press('Tab'); - await expect(page.locator('[data-qa-type="message"]').last()).toBeFocused(); + await expect(poHomeChannel.content.lastUserMessage).toBeFocused(); }); }); }); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-send-pdf-transcript.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-send-pdf-transcript.spec.ts index 5171e1c4739f6..381cf4c8eca3a 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-send-pdf-transcript.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-send-pdf-transcript.spec.ts @@ -63,7 +63,8 @@ test.describe('omnichannel- export chat transcript as PDF', () => { await test.step('Expect to have exported PDF in rocket.cat', async () => { await page.waitForTimeout(3000); await agent.poHomeChannel.navbar.openChat('rocket.cat'); - await expect(agent.poHomeChannel.transcript.DownloadedPDF).toBeVisible(); + await expect(agent.poHomeChannel.content.lastUserMessage.getByText('PDF Transcript successfully generated')).toBeVisible(); + await expect(agent.poHomeChannel.content.lastUserMessage.getByRole('link', { name: 'Transcript' })).toBeVisible(); }); // PDF can be exported from Omnichannel Contact Center diff --git a/apps/meteor/tests/e2e/page-objects/encrypted-room.ts b/apps/meteor/tests/e2e/page-objects/encrypted-room.ts index 4b8a4e98c1e59..e54f92906043c 100644 --- a/apps/meteor/tests/e2e/page-objects/encrypted-room.ts +++ b/apps/meteor/tests/e2e/page-objects/encrypted-room.ts @@ -12,11 +12,11 @@ export class EncryptedRoomPage extends HomeContent { } get lastMessage() { - return new Message(this.page.locator('[data-qa-type="message"]').last()); + return new Message(this.lastUserMessage); } lastNthMessage(index: number) { - return new Message(this.page.locator(`[data-qa-type="message"]`).nth(-index - 1)); + return new Message(this.nthMessage(-index - 1)); } async enableEncryption() { diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts index eba1a2f986a26..2577cd557216e 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts @@ -41,28 +41,48 @@ export class HomeContent { return this.page.locator('role=menu[name="People"]'); } + get mainMessageList(): Locator { + return this.page.getByRole('list', { name: 'Message list', exact: true }); + } + + get threadMessageList(): Locator { + return this.page.getByRole('list', { name: 'Thread message list', exact: true }); + } + + get messageListItems(): Locator { + return this.mainMessageList.locator('[role="listitem"][aria-roledescription="message"]'); + } + + get systemMessageListItems(): Locator { + return this.mainMessageList.locator('[role="listitem"][aria-roledescription="system message"]'); + } + + get threadMessageListItems(): Locator { + return this.threadMessageList.locator('[role="listitem"][aria-roledescription="thread message"]'); + } + get lastUserMessage(): Locator { - return this.page.locator('[data-qa-type="message"]').last(); + return this.messageListItems.last(); } - nthMessage(index: number): Locator { - return this.page.locator('[data-qa-type="message"]').nth(index); + get lastThreadMessagePreview(): Locator { + return this.page.getByRole('listitem').locator('[role="link"][aria-roledescription="thread message preview"]').last(); } - get lastUserMessageNotThread(): Locator { - return this.page.locator('div.messages-box [data-qa-type="message"]').last(); + nthMessage(index: number): Locator { + return this.messageListItems.nth(index); } get lastUserMessageBody(): Locator { - return this.lastUserMessage.locator('[data-qa-type="message-body"]'); + return this.lastUserMessage.locator('[role="document"][aria-roledescription="message body"]'); } get lastUserMessageAttachment(): Locator { - return this.page.locator('[data-qa-type="message-attachment"]').last(); + return this.page.locator('[role="document"][aria-roledescription="message attachment"]').last(); } get lastUserMessageNotSequential(): Locator { - return this.page.locator('[data-qa-type="message"][data-sequential="false"]').last(); + return this.mainMessageList.locator('[role="listitem"][aria-roledescription="message"][data-sequential="false"]').last(); } get encryptedRoomHeaderIcon(): Locator { @@ -119,7 +139,7 @@ export class HomeContent { } async forwardMessage(chatName: string) { - await this.page.locator('[data-qa-type="message"]').last().hover(); + await this.messageListItems.last().hover(); await this.page.locator('role=button[name="Forward message"]').click(); await this.page.getByRole('textbox', { name: 'Person or Channel', exact: true }).click(); @@ -173,7 +193,7 @@ export class HomeContent { } get getFileDescription(): Locator { - return this.page.locator('[data-qa-type="message"]:last-child [data-qa-type="message-body"]'); + return this.lastUserMessage.locator('[role="document"][aria-roledescription="message body"]'); } get fileNameInput(): Locator { @@ -182,16 +202,19 @@ export class HomeContent { // ----------------------------------------- - get lastMessageFileName(): Locator { - return this.page.locator('[data-qa-type="message"]:last-child [data-qa-type="attachment-title-link"]'); + getLastMessageByFileName(filename: string): Locator { + return this.messageListItems + .filter({ has: this.page.getByRole('link', { name: filename }) }) + .last() + .getByRole('link', { name: filename }); } get lastMessageTextAttachment(): Locator { - return this.page.locator('[data-qa-type="message"]:last-child [data-qa-type="message-attachment"]'); + return this.messageListItems.last().locator('[role="document"][aria-roledescription="message attachment"]'); } get lastMessageTextAttachmentEqualsText(): Locator { - return this.page.locator('[data-qa-type="message"]:last-child .rcx-attachment__details .rcx-message-body'); + return this.messageListItems.last().locator('.rcx-attachment__details .rcx-message-body'); } get btnQuoteMessage(): Locator { @@ -239,15 +262,15 @@ export class HomeContent { } get lastThreadMessageTextAttachmentEqualsText(): Locator { - return this.page.locator('div.thread-list ul.thread [data-qa-type="message"]').last().locator('.rcx-attachment__details'); + return this.threadMessageListItems.last().locator('.rcx-attachment__details'); } get mainThreadMessageText(): Locator { - return this.page.locator('div.thread-list ul.thread [data-qa-type="message"]').first(); + return this.threadMessageListItems.first(); } get lastThreadMessageText(): Locator { - return this.page.locator('div.thread-list ul.thread [data-qa-type="message"]').last(); + return this.threadMessageListItems.last(); } get lastThreadMessagePreviewText(): Locator { @@ -255,11 +278,11 @@ export class HomeContent { } get lastThreadMessageFileDescription(): Locator { - return this.page.locator('div.thread-list ul.thread [data-qa-type="message"]').last().locator('[data-qa-type="message-body"]'); + return this.threadMessageListItems.last().locator('[role="document"][aria-roledescription="message body"]'); } - get lastThreadMessageFileName(): Locator { - return this.page.locator('div.thread-list ul.thread [data-qa-type="message"]').last().locator('[data-qa-type="attachment-title-link"]'); + getLastThreadMessageByFileName(filename: string): Locator { + return this.threadMessageListItems.last().getByRole('link', { name: filename }); } // TODO: improve locator specificity @@ -268,7 +291,7 @@ export class HomeContent { } get lastThreadMessageTextAttachment(): Locator { - return this.page.locator('div.thread-list ul.thread [data-qa-type="message"]').last().locator('[data-qa-type="message-attachment"]'); + return this.threadMessageListItems.last().locator('[role="document"][aria-roledescription="message attachment"]'); } get btnOptionEditMessage(): Locator { @@ -393,10 +416,6 @@ export class HomeContent { await this.lastUserMessage.getByRole('button', { name: 'More', exact: true }).click(); } - get threadMessageList(): Locator { - return this.page.getByRole('list', { name: 'Thread message list' }); - } - async openLastThreadMessageMenu(): Promise<void> { await this.threadMessageList.last().hover(); await this.threadMessageList.last().getByRole('button', { name: 'More', exact: true }).waitFor(); @@ -411,7 +430,7 @@ export class HomeContent { } get lastSystemMessageBody(): Locator { - return this.page.locator('[data-qa-type="system-message-body"]').last(); + return this.page.locator('[role=document][aria-roledescription="system message body"]').last(); } get resumeOnHoldOmnichannelChatButton(): Locator { @@ -480,7 +499,7 @@ export class HomeContent { // TODO: use getSystemMessageByText instead findSystemMessage(text: string): Locator { - return this.page.locator(`[data-qa-type="system-message-body"] >> text="${text}"`); + return this.page.locator(`[role="document"][aria-roledescription="system message body"]`, { hasText: text }); } getSystemMessageByText(text: string): Locator { @@ -492,7 +511,7 @@ export class HomeContent { } getMessageById(id: string): Locator { - return this.page.locator(`[data-qa-type="message"][id="${id}"]`); + return this.page.locator(`[role="listitem"][aria-roledescription="message"][id="${id}"]`); } async waitForChannel(): Promise<void> { @@ -505,9 +524,9 @@ export class HomeContent { } async openReplyInThread(): Promise<void> { - await this.page.locator('[data-qa-type="message"]').last().hover(); - await this.page.locator('[data-qa-type="message"]').last().locator('role=button[name="Reply in thread"]').waitFor(); - await this.page.locator('[data-qa-type="message"]').last().locator('role=button[name="Reply in thread"]').click(); + await this.lastUserMessage.hover(); + await this.lastUserMessage.getByRole('button', { name: 'Reply in thread' }).waitFor(); + await this.lastUserMessage.getByRole('button', { name: 'Reply in thread' }).click(); } async sendMessageInThread(text: string): Promise<void> { diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-flextab-pruneMessages.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-flextab-pruneMessages.ts index d73896abbf9ba..7d5f933be8203 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-flextab-pruneMessages.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-flextab-pruneMessages.ts @@ -12,11 +12,11 @@ export class HomeFlextabPruneMessages { } get doNotPrunePinned(): Locator { - return this.form.getByRole('checkbox', { name: 'Do not prune pinned messages', exact: true }); + return this.form.locator('label', { hasText: 'Do not prune pinned messages' }); } get filesOnly(): Locator { - return this.form.getByRole('checkbox', { name: 'Only remove the attached files, keep messages', exact: true }); + return this.form.locator('label', { hasText: 'Only remove the attached files, keep messages' }); } async prune(): Promise<void> { diff --git a/apps/meteor/tests/e2e/page-objects/fragments/home-flextab.ts b/apps/meteor/tests/e2e/page-objects/fragments/home-flextab.ts index be9185d755cab..af808345e597b 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-flextab.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-flextab.ts @@ -72,10 +72,6 @@ export class HomeFlextab { return this.page.locator('role=menuitem[name="Enable E2E encryption"]'); } - get flexTabViewThreadMessage(): Locator { - return this.page.locator('div.thread-list ul.thread [data-qa-type="message"]').last().locator('[data-qa-type="message-body"]'); - } - get userInfoUsername(): Locator { return this.page.locator('[data-qa="UserInfoUserName"]'); } diff --git a/apps/meteor/tests/e2e/page-objects/fragments/message.ts b/apps/meteor/tests/e2e/page-objects/fragments/message.ts index 77dee24c85ca0..cb2bc36db5281 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/message.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/message.ts @@ -4,11 +4,11 @@ export class Message { constructor(public readonly root: Locator) {} get body() { - return this.root.locator('[data-qa-type="message-body"]'); + return this.root.locator('[role="document"][aria-roledescription="message body"]'); } - get fileUploadName() { - return this.root.locator('[data-qa-type="attachment-title-link"]'); + getFileUploadByName(filename: string) { + return this.root.getByRole('link', { name: filename }); } get encryptedIcon() { diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-transcript.ts b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-transcript.ts index a05a185849168..851c338626d6e 100644 --- a/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-transcript.ts +++ b/apps/meteor/tests/e2e/page-objects/omnichannel/omnichannel-transcript.ts @@ -18,8 +18,4 @@ export class OmnichannelTranscript extends OmnichannelAdmin { get btnOpenChat(): Locator { return this.page.getByRole('dialog').getByRole('button', { name: 'Open chat', exact: true }); } - - get DownloadedPDF(): Locator { - return this.page.locator('[data-qa-type="attachment-title-link"]').last(); - } } diff --git a/apps/meteor/tests/e2e/prune-messages.spec.ts b/apps/meteor/tests/e2e/prune-messages.spec.ts index d56856bfff51c..0845ebcdf2911 100644 --- a/apps/meteor/tests/e2e/prune-messages.spec.ts +++ b/apps/meteor/tests/e2e/prune-messages.spec.ts @@ -44,7 +44,7 @@ test.describe('prune-messages', () => { await content.sendFileMessage('any_file.txt'); await content.descriptionInput.fill('a message with a file'); await content.btnModalConfirm.click(); - await expect(content.lastMessageFileName).toHaveText('any_file.txt'); + await expect(content.getLastMessageByFileName('any_file.txt')).toBeVisible(); await sendTargetChannelMessage(api, targetChannel.fname as string, { msg: 'a message without files', @@ -112,7 +112,7 @@ test.describe('prune-messages', () => { await content.sendFileMessage('any_file.txt'); await content.descriptionInput.fill('a message with a file'); await content.btnModalConfirm.click(); - await expect(content.lastMessageFileName).toHaveText('any_file.txt'); + await expect(content.getLastMessageByFileName('any_file.txt')).toBeVisible(); await test.step('prune files only', async () => { await pruneMessages.filesOnly.check({ force: true }); @@ -124,7 +124,7 @@ test.describe('prune-messages', () => { }); await test.step('check message list for prune message-attachment', async () => { - await expect(content.lastMessageFileName).not.toBeVisible(); + await expect(content.getLastMessageByFileName('any_file.txt')).not.toBeVisible(); await expect(content.lastMessageTextAttachment, 'Prune message attachment replaces file attachment').toHaveText( 'File removed by prune', ); @@ -147,10 +147,9 @@ test.describe('prune-messages', () => { await content.sendFileMessage('any_file.txt'); await content.descriptionInput.fill('a message with a file'); await content.btnModalConfirm.click(); - await expect(content.lastMessageFileName).toHaveText('any_file.txt'); + await expect(content.getLastMessageByFileName('any_file.txt')).toBeVisible(); - await content.lastUserMessage.hover(); - await content.lastUserMessage.getByTitle('Reply in thread').click(); + await content.openReplyInThread(); expect( ( await api.post('/rooms.cleanHistory', { @@ -163,7 +162,7 @@ test.describe('prune-messages', () => { ).toBe(200); await test.step('check main thread message for prune message-attachment', async () => { - await expect(content.lastThreadMessageFileName).not.toBeVisible(); + await expect(content.getLastThreadMessageByFileName('any_file.txt')).not.toBeVisible(); await expect(content.lastThreadMessageTextAttachment, 'Prune message attachment replaces file attachment').toHaveText( 'File removed by prune', ); diff --git a/apps/meteor/tests/e2e/quote-attachment.spec.ts b/apps/meteor/tests/e2e/quote-attachment.spec.ts index ddf505d5600c9..8b20af3ae54c7 100644 --- a/apps/meteor/tests/e2e/quote-attachment.spec.ts +++ b/apps/meteor/tests/e2e/quote-attachment.spec.ts @@ -76,7 +76,7 @@ test.describe.parallel('Quote Attachment', () => { await poHomeChannel.content.btnModalConfirm.click(); await expect(poHomeChannel.content.lastThreadMessageFileDescription).toHaveText(fileDescription); - await expect(poHomeChannel.content.lastThreadMessageFileName).toContainText(textFileName); + await expect(poHomeChannel.content.getLastThreadMessageByFileName(textFileName)).toBeVisible(); }); await test.step('Quote the message with attachment in thread', async () => { diff --git a/apps/meteor/tests/e2e/system-messages.spec.ts b/apps/meteor/tests/e2e/system-messages.spec.ts index c8f5669439288..bc787f6e822bc 100644 --- a/apps/meteor/tests/e2e/system-messages.spec.ts +++ b/apps/meteor/tests/e2e/system-messages.spec.ts @@ -16,7 +16,8 @@ const userData = { password: faker.internet.password(), }; -const findSysMes = (page: Page, id: string): Locator => page.locator(`[data-qa="system-message"][data-system-message-type="${id}"]`); +const findSysMes = (page: Page, id: string): Locator => + page.locator(`[role="listitem"][aria-roledescription="system message"][data-system-message-type="${id}"]`); // There currently are over 33 system messages. Testing only a couple due to test being too slow right now. // Ideally, we should test all. diff --git a/apps/meteor/tests/e2e/team-management.spec.ts b/apps/meteor/tests/e2e/team-management.spec.ts index 81f76ef60e628..a633d7748bfb4 100644 --- a/apps/meteor/tests/e2e/team-management.spec.ts +++ b/apps/meteor/tests/e2e/team-management.spec.ts @@ -146,12 +146,10 @@ test.describe.serial('teams-management', () => { test('should send hello in the targetTeam and reply in a thread', async ({ page }) => { await poHomeTeam.navbar.openChat(targetTeam); await poHomeTeam.content.sendMessage('hello'); - await page.locator('[data-qa-type="message"]').last().hover(); - - await page.locator('role=button[name="Reply in thread"]').click(); + await poHomeTeam.content.openReplyInThread(); await page.locator('.rcx-vertical-bar').locator(`role=textbox[name="Message #${targetTeam}"]`).type('any-reply-message'); await page.keyboard.press('Enter'); - await expect(poHomeTeam.tabs.flexTabViewThreadMessage).toHaveText('any-reply-message'); + await expect(poHomeTeam.content.lastThreadMessageText).toHaveText('any-reply-message'); }); test('should set targetTeam as readonly', async () => { diff --git a/apps/meteor/tests/e2e/threads.spec.ts b/apps/meteor/tests/e2e/threads.spec.ts index f7ddbdd51a906..f52212e92ba37 100644 --- a/apps/meteor/tests/e2e/threads.spec.ts +++ b/apps/meteor/tests/e2e/threads.spec.ts @@ -29,8 +29,7 @@ test.describe.serial('Threads', () => { test('expect thread message preview if alsoSendToChannel checkbox is checked', async ({ page }) => { await poHomeChannel.content.sendMessage('this is a message for reply'); - await page.locator('[data-qa-type="message"]').last().hover(); - await page.locator('role=button[name="Reply in thread"]').click(); + await poHomeChannel.content.openReplyInThread(); await expect(page).toHaveURL(/.*thread/); @@ -38,7 +37,7 @@ test.describe.serial('Threads', () => { await page.getByRole('dialog').locator('[name="msg"]').last().fill('This is a thread message also sent in channel'); await page.keyboard.press('Enter'); await expect(poHomeChannel.content.lastThreadMessageText).toContainText('This is a thread message also sent in channel'); - await expect(poHomeChannel.content.lastUserMessage).toContainText('This is a thread message also sent in channel'); + await expect(poHomeChannel.content.lastThreadMessagePreview).toContainText('This is a thread message also sent in channel'); }); test('expect open threads contextual bar when clicked on thread preview', async ({ page }) => { await poHomeChannel.content.lastThreadMessagePreviewText.click(); @@ -59,7 +58,7 @@ test.describe.serial('Threads', () => { test('expect to close thread contextual bar on clicking outside', async ({ page }) => { await poHomeChannel.content.lastThreadMessagePreviewText.click(); await expect(page).toHaveURL(/.*thread/); - await poHomeChannel.content.lastUserMessageNotThread.click(); + await poHomeChannel.content.lastUserMessage.click(); await expect(page).not.toHaveURL(/.*thread/); }); test('expect open threads contextual bar when clicked on thread preview', async ({ page }) => { @@ -90,7 +89,7 @@ test.describe.serial('Threads', () => { await poHomeChannel.content.btnModalConfirm.click(); await expect(poHomeChannel.content.lastThreadMessageFileDescription).toHaveText('any_description'); - await expect(poHomeChannel.content.lastThreadMessageFileName).toContainText('any_file1.txt'); + await expect(poHomeChannel.content.getLastThreadMessageByFileName('any_file1.txt')).toBeVisible(); }); test.describe('thread message actions', () => { @@ -99,8 +98,7 @@ test.describe.serial('Threads', () => { await page.goto('/home'); await poHomeChannel.navbar.openChat(targetChannel); await poHomeChannel.content.sendMessage('this is a message for reply'); - await page.locator('[data-qa-type="message"]').last().hover(); - await page.locator('role=button[name="Reply in thread"]').click(); + await poHomeChannel.content.openReplyInThread(); }); test('expect delete the thread message and close thread if has only one message', async ({ page }) => { @@ -133,8 +131,8 @@ test.describe.serial('Threads', () => { }); test('expect quote the thread message', async ({ page }) => { - await page.getByRole('dialog').locator('[data-qa-type="message"]').last().hover(); - await page.locator('role=button[name="Quote"]').click(); + await poHomeChannel.content.lastThreadMessageText.hover(); + await poHomeChannel.content.lastThreadMessageText.getByRole('button', { name: 'Quote' }).click(); await page.locator('[name="msg"]').last().fill('this is a quote message'); await page.keyboard.press('Enter'); @@ -170,7 +168,7 @@ test.describe.serial('Threads', () => { test('expect close thread if has only one message and user press escape', async ({ page }) => { await expect(page).toHaveURL(/.*thread/); - await expect(page.getByRole('dialog').locator('[data-qa-type="message"]')).toBeVisible(); + await expect(poHomeChannel.content.lastThreadMessageText).toBeVisible(); await expect(page.locator('[name="msg"]').last()).toBeFocused(); await page.keyboard.press('Escape'); await expect(page).not.toHaveURL(/.*thread/); @@ -178,7 +176,7 @@ test.describe.serial('Threads', () => { test('expect reset the thread composer to original message if user presses escape', async ({ page }) => { await expect(page).toHaveURL(/.*thread/); - await expect(page.getByRole('dialog').locator('[data-qa-type="message"]')).toBeVisible(); + await expect(poHomeChannel.content.lastThreadMessageText).toBeVisible(); await expect(page.locator('[name="msg"]').last()).toBeFocused(); await page.locator('[name="msg"]').last().fill('message to be edited'); @@ -195,7 +193,7 @@ test.describe.serial('Threads', () => { test('expect clean composer and keep the thread open if user is editing message and presses escape', async ({ page }) => { await expect(page).toHaveURL(/.*thread/); - await expect(page.getByRole('dialog').locator('[data-qa-type="message"]')).toBeVisible(); + await expect(poHomeChannel.content.lastThreadMessageText).toBeVisible(); await expect(page.locator('[name="msg"]').last()).toBeFocused(); await page.locator('[name="msg"]').last().fill('message to be edited'); diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index ca424d5169b69..c2cc8d1e6cbf6 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -7108,5 +7108,8 @@ "Message_composer": "Message composer", "Thread_composer": "Thread composer", "Room_composer": "Room composer", - "Date_range_presets": "Date range presets" + "Date_range_presets": "Date range presets", + "message_body": "message body", + "message_attachment": "message attachment", + "system_message_body": "system message body" } \ No newline at end of file From e31776aa5f1704b29c4b951d797e394b5aad1c3a Mon Sep 17 00:00:00 2001 From: Kartik <dodakartik26@gmail.com> Date: Fri, 27 Feb 2026 22:25:31 +0530 Subject: [PATCH 083/108] chore: Incorrect validation message label for 'Post as' field (#39103) Co-authored-by: dodaa08 <dodaa08@users.noreply.github.com> --- .../views/admin/integrations/incoming/IncomingWebhookForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/client/views/admin/integrations/incoming/IncomingWebhookForm.tsx b/apps/meteor/client/views/admin/integrations/incoming/IncomingWebhookForm.tsx index aa8e77f5b2746..3fe9c96326cd3 100644 --- a/apps/meteor/client/views/admin/integrations/incoming/IncomingWebhookForm.tsx +++ b/apps/meteor/client/views/admin/integrations/incoming/IncomingWebhookForm.tsx @@ -202,7 +202,7 @@ const IncomingWebhookForm = ({ webhookData }: { webhookData?: Serialized<IIncomi <Controller name='username' control={control} - rules={{ required: t('Required_field', { field: t('Post_to_Channel') }) }} + rules={{ required: t('Required_field', { field: t('Post_as') }) }} render={({ field }) => ( <TextInput id={usernameField} From 1334dc842b67697fc4f83337e9040053a8b88b5d Mon Sep 17 00:00:00 2001 From: Nazareno Bucciarelli <84046180+nazabucciarelli@users.noreply.github.com> Date: Fri, 27 Feb 2026 13:55:54 -0300 Subject: [PATCH 084/108] fix: make custom sounds/emojis storage settings reactive (#38954) --- .changeset/tough-steaks-beam.md | 5 + .../server/startup/custom-sounds.js | 7 +- .../server/startup/emoji-custom.js | 7 +- .../tests/end-to-end/api/custom-sounds.ts | 94 ++++++++++++- .../tests/end-to-end/api/emoji-custom.ts | 129 ++++++++++++++++++ 5 files changed, 239 insertions(+), 3 deletions(-) create mode 100644 .changeset/tough-steaks-beam.md diff --git a/.changeset/tough-steaks-beam.md b/.changeset/tough-steaks-beam.md new file mode 100644 index 0000000000000..cd0263fb496ed --- /dev/null +++ b/.changeset/tough-steaks-beam.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes reactivity of Custom Sounds and Custom Emojis storage settings diff --git a/apps/meteor/app/custom-sounds/server/startup/custom-sounds.js b/apps/meteor/app/custom-sounds/server/startup/custom-sounds.js index 94101b8622c6b..601157a884de6 100644 --- a/apps/meteor/app/custom-sounds/server/startup/custom-sounds.js +++ b/apps/meteor/app/custom-sounds/server/startup/custom-sounds.js @@ -8,7 +8,7 @@ import { settings } from '../../../settings/server'; export let RocketChatFileCustomSoundsInstance; -Meteor.startup(() => { +const initializeCustomSoundsStorage = () => { let storeType = 'GridFS'; if (settings.get('CustomSounds_Storage_Type')) { @@ -37,7 +37,10 @@ Meteor.startup(() => { name: 'custom_sounds', absolutePath: path, }); +}; +Meteor.startup(() => { + initializeCustomSoundsStorage(); return WebApp.connectHandlers.use('/custom-sounds/', async (req, res /* , next*/) => { const fileId = decodeURIComponent(req.url.replace(/^\//, '').replace(/\?.*$/, '')); @@ -97,3 +100,5 @@ Meteor.startup(() => { file.readStream.pipe(res); }); }); + +settings.watchMultiple(['CustomSounds_Storage_Type', 'CustomSounds_FileSystemPath'], initializeCustomSoundsStorage); diff --git a/apps/meteor/app/emoji-custom/server/startup/emoji-custom.js b/apps/meteor/app/emoji-custom/server/startup/emoji-custom.js index 1512bb0314c15..d784630013a57 100644 --- a/apps/meteor/app/emoji-custom/server/startup/emoji-custom.js +++ b/apps/meteor/app/emoji-custom/server/startup/emoji-custom.js @@ -38,7 +38,7 @@ const writeSvgFallback = (res, req) => { res.end(); }; -Meteor.startup(() => { +const initializeEmojiCustomStorage = () => { let storeType = 'GridFS'; if (settings.get('EmojiUpload_Storage_Type')) { @@ -67,7 +67,10 @@ Meteor.startup(() => { name: 'custom_emoji', absolutePath: path, }); +}; +Meteor.startup(() => { + initializeEmojiCustomStorage(); return WebApp.connectHandlers.use('/emoji-custom/', async (req, res /* , next*/) => { const params = { emoji: decodeURIComponent(req.url.replace(/^\//, '').replace(/\?.*$/, '')) }; @@ -118,3 +121,5 @@ Meteor.startup(() => { file.readStream.pipe(res); }); }); + +settings.watchMultiple(['EmojiUpload_Storage_Type', 'EmojiUpload_FileSystemPath'], initializeEmojiCustomStorage); diff --git a/apps/meteor/tests/end-to-end/api/custom-sounds.ts b/apps/meteor/tests/end-to-end/api/custom-sounds.ts index c788b7ed0cc60..2abb62b8fae76 100644 --- a/apps/meteor/tests/end-to-end/api/custom-sounds.ts +++ b/apps/meteor/tests/end-to-end/api/custom-sounds.ts @@ -6,6 +6,7 @@ import { expect } from 'chai'; import { before, describe, it, after } from 'mocha'; import { getCredentials, api, request, credentials } from '../../data/api-data'; +import { updateSetting } from '../../data/permissions.helper'; async function insertOrUpdateSound(fileName: string, fileId?: string): Promise<string> { fileId = fileId ?? ''; @@ -44,17 +45,33 @@ async function uploadCustomSound(binary: string, fileName: string, fileId: strin .expect(200); } +async function deleteCustomSound(_id: string) { + await request + .post(api('method.call/deleteCustomSound')) + .set(credentials) + .send({ + message: JSON.stringify({ + msg: 'method', + id: '1', + method: 'deleteCustomSound', + params: [_id], + }), + }) + .expect(200); +} + describe('[CustomSounds]', () => { const fileName = `test-file-${randomUUID()}`; let fileId: string; let fileId2: string; let uploadDate: string | undefined; + let binary: string; before((done) => getCredentials(done)); before(async () => { const data = readFileSync(path.resolve(__dirname, '../../mocks/files/audio_mock.wav')); - const binary = data.toString('binary'); + binary = data.toString('binary'); fileId = await insertOrUpdateSound(fileName); fileId2 = await insertOrUpdateSound(`${fileName}-2`); @@ -237,4 +254,79 @@ describe('[CustomSounds]', () => { .expect(404); // valid string, but it doesn't exist }); }); + + describe('Sounds storage settings reactivity', () => { + let fsFileId: string; + let gridFsFileId: string; + + before(async () => { + await updateSetting('CustomSounds_Storage_Type', 'FileSystem'); + fsFileId = await insertOrUpdateSound(`${fileName}-3`); + await uploadCustomSound(binary, `${fileName}-3`, fsFileId); + + await updateSetting('CustomSounds_Storage_Type', 'GridFS'); + gridFsFileId = await insertOrUpdateSound(`${fileName}-4`); + await uploadCustomSound(binary, `${fileName}-4`, gridFsFileId); + + await updateSetting('CustomSounds_FileSystemPath', ''); + }); + + after(async () => { + await updateSetting('CustomSounds_Storage_Type', 'FileSystem', false); + await updateSetting('CustomSounds_FileSystemPath', ''); + await deleteCustomSound(fsFileId); + await updateSetting('CustomSounds_Storage_Type', 'GridFS'); + await deleteCustomSound(gridFsFileId); + }); + + describe('CustomSounds_Storage_Type', () => { + describe('when storage is GridFS', () => { + before(async () => { + await updateSetting('CustomSounds_Storage_Type', 'GridFS'); + }); + + it('should resolve GridFS files only', async () => { + await request.get(`/custom-sounds/${gridFsFileId}.wav`).set(credentials).expect(200); + await request.get(`/custom-sounds/${fsFileId}.wav`).set(credentials).expect(404); + }); + }); + + describe('when storage is FileSystem', () => { + before(async () => { + await updateSetting('CustomSounds_Storage_Type', 'FileSystem'); + }); + + it('should resolve FileSystem files only', async () => { + await request.get(`/custom-sounds/${gridFsFileId}.wav`).set(credentials).expect(404); + await request.get(`/custom-sounds/${fsFileId}.wav`).set(credentials).expect(200); + }); + }); + }); + + describe('CustomSounds_FileSystemPath', () => { + before(async () => { + await updateSetting('CustomSounds_Storage_Type', 'FileSystem'); + }); + + describe('when file system path is the default one', () => { + it('should resolve files', async () => { + await request.get(`/custom-sounds/${fsFileId}.wav`).set(credentials).expect(200); + }); + }); + + describe('when file system path is NOT the default one', () => { + before(async () => { + await updateSetting('CustomSounds_FileSystemPath', '~/sounds'); + }); + + after(async () => { + await updateSetting('CustomSounds_FileSystemPath', ''); + }); + + it('should NOT resolve files', async () => { + await request.get(`/custom-sounds/${fsFileId}.wav`).set(credentials).expect(404); + }); + }); + }); + }); }); diff --git a/apps/meteor/tests/end-to-end/api/emoji-custom.ts b/apps/meteor/tests/end-to-end/api/emoji-custom.ts index 744480ea6f716..57ec2570eb200 100644 --- a/apps/meteor/tests/end-to-end/api/emoji-custom.ts +++ b/apps/meteor/tests/end-to-end/api/emoji-custom.ts @@ -4,6 +4,7 @@ import { before, describe, it, after } from 'mocha'; import { getCredentials, api, request, credentials } from '../../data/api-data'; import { imgURL } from '../../data/interactions'; +import { updateSetting } from '../../data/permissions.helper'; describe('[EmojiCustom]', () => { const customEmojiName = `my-custom-emoji-${Date.now()}`; @@ -481,4 +482,132 @@ describe('[EmojiCustom]', () => { .end(done); }); }); + + describe('Emoji storage settings reactivity', () => { + const normalizeSvg = (svg: string) => svg.replace(/\r\n/g, '\n').trim(); + + const now = Date.now(); + const fsEmojiName = `emoji-fs-${now}`; + const gridFsEmojiName = `emoji-gridfs-${now}`; + const svgFallback = normalizeSvg(`<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg xmlns="http://www.w3.org/2000/svg" pointer-events="none" width="50" height="50" style="width: 50px; height: 50px; background-color: #000;"> + <text text-anchor="middle" y="50%" x="50%" dy="0.36em" pointer-events="auto" fill="#ffffff" font-family="Helvetica, Arial, Lucida Grande, sans-serif" style="font-weight: 400; font-size: 28px;"> + ? + </text> +</svg>`); + + before(async () => { + await updateSetting('EmojiUpload_Storage_Type', 'FileSystem'); + await request.post(api('emoji-custom.create')).set(credentials).attach('emoji', imgURL).field({ name: fsEmojiName }).expect(200); + + await updateSetting('EmojiUpload_Storage_Type', 'GridFS'); + await request.post(api('emoji-custom.create')).set(credentials).attach('emoji', imgURL).field({ name: gridFsEmojiName }).expect(200); + + await updateSetting('EmojiUpload_FileSystemPath', ''); + }); + + after(async () => { + const list = await request.get(api('emoji-custom.all')).set(credentials); + const fsEmoji = list.body.emojis.find((e: IEmojiCustom) => e.name === fsEmojiName); + const gridEmoji = list.body.emojis.find((e: IEmojiCustom) => e.name === gridFsEmojiName); + + await updateSetting('EmojiUpload_Storage_Type', 'FileSystem', false); + await updateSetting('EmojiUpload_FileSystemPath', ''); + if (fsEmoji) { + await request.post(api('emoji-custom.delete')).set(credentials).send({ emojiId: fsEmoji._id }); + } + + await updateSetting('EmojiUpload_Storage_Type', 'GridFS'); + if (gridEmoji) { + await request.post(api('emoji-custom.delete')).set(credentials).send({ emojiId: gridEmoji._id }); + } + }); + + describe('EmojiUpload_Storage_Type', () => { + describe('when storage is GridFs', () => { + before(async () => { + await updateSetting('EmojiUpload_Storage_Type', 'GridFS'); + }); + + it('should resolve GridFS files only', async () => { + await request + .get(`/emoji-custom/${fsEmojiName}.png`) + .set(credentials) + .expect(200) + .expect((res) => { + const received = normalizeSvg(res.body.toString()); + expect(received).to.equal(svgFallback); + }); + await request + .get(`/emoji-custom/${gridFsEmojiName}.png`) + .set(credentials) + .expect(200) + .expect((res) => expect(res.headers).to.have.property('content-type', 'image/png')); + }); + }); + + describe('when storage is FileSystem', () => { + before(async () => { + await updateSetting('EmojiUpload_Storage_Type', 'FileSystem'); + }); + + it('should resolve FileSystem files only', async () => { + await request + .get(`/emoji-custom/${fsEmojiName}.png`) + .set(credentials) + .expect(200) + .expect((res) => expect(res.headers).to.have.property('content-type', 'image/png')); + await request + .get(`/emoji-custom/${gridFsEmojiName}.png`) + .set(credentials) + .expect(200) + .expect((res) => { + const received = normalizeSvg(res.body.toString()); + expect(received).to.equal(svgFallback); + }); + }); + }); + }); + + describe('EmojiUpload_FileSystemPath', () => { + before(async () => { + await updateSetting('EmojiUpload_Storage_Type', 'FileSystem'); + }); + + describe('when file system path is the default one', () => { + before(async () => { + await updateSetting('EmojiUpload_FileSystemPath', ''); + }); + + it('should resolve files', async () => { + await request + .get(`/emoji-custom/${fsEmojiName}.png`) + .set(credentials) + .expect(200) + .expect((res) => expect(res.headers).to.have.property('content-type', 'image/png')); + }); + }); + + describe('when file system path is NOT the default one', () => { + before(async () => { + await updateSetting('EmojiUpload_FileSystemPath', '~/emoji-test'); + }); + + after(async () => { + await updateSetting('CustomSounds_FileSystemPath', ''); + }); + + it('should NOT resolve files', async () => { + await request + .get(`/emoji-custom/${fsEmojiName}.png`) + .set(credentials) + .expect(200) + .expect((res) => { + const received = normalizeSvg(res.body.toString()); + expect(received).to.equal(svgFallback); + }); + }); + }); + }); + }); }); From 2655c8e7a000b6947f807510fa95cd2c4676d5a6 Mon Sep 17 00:00:00 2001 From: Kaustubh2k5 <115872134+Kaustubh2k5@users.noreply.github.com> Date: Fri, 27 Feb 2026 22:26:18 +0530 Subject: [PATCH 085/108] refactor(parseJsonQuery): Avoid repeated permission checks per request (#39014) Co-authored-by: Kaustubh <kaus@localhost.localdomain> --- .../app/api/server/helpers/parseJsonQuery.ts | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/apps/meteor/app/api/server/helpers/parseJsonQuery.ts b/apps/meteor/app/api/server/helpers/parseJsonQuery.ts index 9dd6c9c0448af..fafaebcf59c94 100644 --- a/apps/meteor/app/api/server/helpers/parseJsonQuery.ts +++ b/apps/meteor/app/api/server/helpers/parseJsonQuery.ts @@ -26,7 +26,8 @@ export async function parseJsonQuery(api: GenericRouteExecutionContext): Promise query: Record<string, unknown>; }> { const { userId = '', response, route, logger } = api; - + const isUsersRoute = route.includes('/v1/users.'); + const canViewFullOtherUserInfo = isUsersRoute && (await hasPermissionAsync(userId, 'view-full-other-user-info')); const params = isPlainObject(api.queryParams) ? api.queryParams : {}; const queryFields = Array.isArray(api.queryFields) ? (api.queryFields as string[]) : []; const queryOperations = Array.isArray(api.queryOperations) ? (api.queryOperations as string[]) : []; @@ -85,13 +86,9 @@ export async function parseJsonQuery(api: GenericRouteExecutionContext): Promise // Verify the user's selected fields only contains ones which their role allows if (typeof fields === 'object') { let nonSelectableFields = Object.keys(API.v1.defaultFieldsToExclude); - if (route.includes('/v1/users.')) { + if (isUsersRoute) { nonSelectableFields = nonSelectableFields.concat( - Object.keys( - (await hasPermissionAsync(userId, 'view-full-other-user-info')) - ? API.v1.limitedUserFieldsToExcludeIfIsPrivilegedUser - : API.v1.limitedUserFieldsToExclude, - ), + Object.keys(canViewFullOtherUserInfo ? API.v1.limitedUserFieldsToExcludeIfIsPrivilegedUser : API.v1.limitedUserFieldsToExclude), ); } @@ -104,8 +101,8 @@ export async function parseJsonQuery(api: GenericRouteExecutionContext): Promise // Limit the fields by default fields = Object.assign({}, fields, API.v1.defaultFieldsToExclude); - if (route.includes('/v1/users.')) { - if (await hasPermissionAsync(userId, 'view-full-other-user-info')) { + if (isUsersRoute) { + if (canViewFullOtherUserInfo) { fields = Object.assign(fields, API.v1.limitedUserFieldsToExcludeIfIsPrivilegedUser); } else { fields = Object.assign(fields, API.v1.limitedUserFieldsToExclude); @@ -134,8 +131,8 @@ export async function parseJsonQuery(api: GenericRouteExecutionContext): Promise if (typeof query === 'object') { let nonQueryableFields = Object.keys(API.v1.defaultFieldsToExclude); - if (route.includes('/v1/users.')) { - if (await hasPermissionAsync(userId, 'view-full-other-user-info')) { + if (isUsersRoute) { + if (canViewFullOtherUserInfo) { nonQueryableFields = nonQueryableFields.concat(Object.keys(API.v1.limitedUserFieldsToExcludeIfIsPrivilegedUser)); } else { nonQueryableFields = nonQueryableFields.concat(Object.keys(API.v1.limitedUserFieldsToExclude)); From 86af4e2ff53b0ee555c354549a87ea8cbaf1cce3 Mon Sep 17 00:00:00 2001 From: Shreyas <shreyaswagh2004@gmail.com> Date: Fri, 27 Feb 2026 22:27:29 +0530 Subject: [PATCH 086/108] refactor: reduce hot-path allocations in message parser utils (#39075) --- packages/message-parser/src/utils.ts | 46 +++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/packages/message-parser/src/utils.ts b/packages/message-parser/src/utils.ts index 586a1baf6c069..f2158a3623553 100644 --- a/packages/message-parser/src/utils.ts +++ b/packages/message-parser/src/utils.ts @@ -174,7 +174,7 @@ const joinEmoji = (current: Inlines, previous: Inlines | undefined, next: Inline } return { - ...current.value, + type: 'PLAIN_TEXT', value: `:${current.value.value}:`, }; } @@ -183,19 +183,55 @@ const joinEmoji = (current: Inlines, previous: Inlines | undefined, next: Inline }; export const reducePlainTexts = (values: Paragraph['value']): Paragraph['value'] => { - const flattenedValues = values.flat(); const result: Paragraph['value'] = []; + const flattenableValues = values as Array<Inlines | Inlines[]>; - for (let index = 0; index < flattenedValues.length; index++) { - const current = joinEmoji(flattenedValues[index], flattenedValues[index - 1], flattenedValues[index + 1]); + let previousInline = undefined as Inlines | undefined; + let pendingInline = undefined as Inlines | undefined; + + const appendJoinedInline = (inline: Inlines, nextInline: Inlines | undefined): void => { + const current = joinEmoji(inline, previousInline, nextInline); const previous = result[result.length - 1]; if (previous && current.type === 'PLAIN_TEXT' && previous.type === 'PLAIN_TEXT') { previous.value += current.value; + } else { + result.push(current); + } + + previousInline = inline; + }; + + for (let index = 0; index < flattenableValues.length; index++) { + const entry = flattenableValues[index]; + + if (Array.isArray(entry)) { + for (let nestedIndex = 0; nestedIndex < entry.length; nestedIndex++) { + const currentInline = entry[nestedIndex]; + + if (pendingInline === undefined) { + pendingInline = currentInline; + continue; + } + + appendJoinedInline(pendingInline, currentInline); + pendingInline = currentInline; + } + continue; } - result.push(current); + if (pendingInline === undefined) { + pendingInline = entry; + continue; + } + + appendJoinedInline(pendingInline, entry); + pendingInline = entry; + } + + if (pendingInline !== undefined) { + appendJoinedInline(pendingInline, undefined); } return result; From d0e706b27b13c0a8f96ecf555077160903fea763 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 17:42:11 -0300 Subject: [PATCH 087/108] chore(deps): bump axios from 1.12.0 to 1.13.5 in /.github/actions/update-version-durability (#38585) --- .../package-lock.json | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/.github/actions/update-version-durability/package-lock.json b/.github/actions/update-version-durability/package-lock.json index 6fb692c311a1d..5ac99a59a922a 100644 --- a/.github/actions/update-version-durability/package-lock.json +++ b/.github/actions/update-version-durability/package-lock.json @@ -205,16 +205,17 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" }, "node_modules/axios": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.0.tgz", - "integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==", + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, @@ -253,6 +254,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" }, @@ -264,6 +266,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", "engines": { "node": ">=0.4.0" } @@ -352,15 +355,16 @@ "license": "MIT" }, "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "funding": [ { "type": "individual", "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -371,9 +375,9 @@ } }, "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -496,6 +500,7 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -504,6 +509,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, From 3bfd40134703146623adddda65684f43cdd42f0c Mon Sep 17 00:00:00 2001 From: Divyanshu Patil <161197941+divyanshu-patil@users.noreply.github.com> Date: Sat, 28 Feb 2026 02:13:59 +0530 Subject: [PATCH 088/108] chore: adding validation when adding link to message composer (#37573) --- .../messageBox/AddLinkComposerActionModal.tsx | 23 +++++++++++++++---- packages/i18n/src/locales/en.i18n.json | 2 ++ packages/i18n/src/locales/es.i18n.json | 2 ++ packages/i18n/src/locales/hi-IN.i18n.json | 3 +++ packages/i18n/src/locales/pt-BR.i18n.json | 2 ++ 5 files changed, 28 insertions(+), 4 deletions(-) diff --git a/apps/meteor/app/ui-message/client/messageBox/AddLinkComposerActionModal.tsx b/apps/meteor/app/ui-message/client/messageBox/AddLinkComposerActionModal.tsx index ce75e12ee68df..58edd971d1e1c 100644 --- a/apps/meteor/app/ui-message/client/messageBox/AddLinkComposerActionModal.tsx +++ b/apps/meteor/app/ui-message/client/messageBox/AddLinkComposerActionModal.tsx @@ -1,9 +1,11 @@ -import { Field, FieldGroup, TextInput, FieldLabel, FieldRow, Box } from '@rocket.chat/fuselage'; +import { Field, FieldGroup, TextInput, FieldLabel, FieldRow, Box, FieldError } from '@rocket.chat/fuselage'; import { GenericModal } from '@rocket.chat/ui-client'; import { useEffect, useId } from 'react'; import { useForm, Controller } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; +import { isValidLink } from '../../../../client/views/room/MessageList/lib/isValidLink'; + type AddLinkComposerActionModalProps = { selectedText?: string; onConfirm: (url: string, text: string) => void; @@ -15,8 +17,8 @@ const AddLinkComposerActionModal = ({ selectedText, onClose, onConfirm }: AddLin const textField = useId(); const urlField = useId(); - const { handleSubmit, setFocus, control } = useForm({ - mode: 'onBlur', + const { handleSubmit, setFocus, control, formState } = useForm({ + mode: 'onChange', defaultValues: { text: selectedText || '', url: '', @@ -40,6 +42,7 @@ const AddLinkComposerActionModal = ({ selectedText, onClose, onConfirm }: AddLin confirmText={t('Add')} onCancel={onClose} wrapperFunction={(props) => <Box is='form' onSubmit={(e) => void submit(e)} {...props} />} + confirmDisabled={!formState.isValid} title={t('Add_link')} > <FieldGroup> @@ -52,8 +55,20 @@ const AddLinkComposerActionModal = ({ selectedText, onClose, onConfirm }: AddLin <Field> <FieldLabel htmlFor={urlField}>{t('URL')}</FieldLabel> <FieldRow> - <Controller control={control} name='url' render={({ field }) => <TextInput autoComplete='off' id={urlField} {...field} />} /> + <Controller + control={control} + name='url' + rules={{ + validate: (value) => isValidLink(value) || t('Invalid_URL'), + required: { + value: true, + message: t(`URL_is_required`), + }, + }} + render={({ field }) => <TextInput autoComplete='off' id={urlField} {...field} />} + /> </FieldRow> + <FieldError>{formState.errors.url?.message}</FieldError> </Field> </FieldGroup> </GenericModal> diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index c2cc8d1e6cbf6..9255e9ed033f4 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -5422,6 +5422,8 @@ "UI_Use_Real_Name": "Use Real Name", "URL": "URL", "URLs": "URLs", + "URL_is_required": "URL is required", + "URL_must_start_with_'http://'_or_'https://'": "URL must start with 'http://' or 'https://'", "UTC_Timezone": "UTC Timezone", "UTF8_Channel_Names_Validation": "UTF8 Channel Names Validation", "UTF8_Channel_Names_Validation_Description": "RegExp that will be used to validate channel names", diff --git a/packages/i18n/src/locales/es.i18n.json b/packages/i18n/src/locales/es.i18n.json index 1eb2fddf29bbc..03e4cc9666ba7 100644 --- a/packages/i18n/src/locales/es.i18n.json +++ b/packages/i18n/src/locales/es.i18n.json @@ -3866,6 +3866,8 @@ "UI_Use_Name_Avatar": "Usar iniciales del nombre completo para generar un avatar por defecto", "UI_Use_Real_Name": "Usar nombre real", "URL": "URL", + "URL_is_required": "La URL es obligatoria", + "URL_must_start_with_'http://'_or_'https://'": "La URL debe comenzar con 'http://' o 'https://'", "UTC_Timezone": "Zona horaria UTC", "UTF8_Channel_Names_Validation": "Validación de nombres de Channel UTF8", "UTF8_Channel_Names_Validation_Description": "Regex que se usará para validar los nombres de canal", diff --git a/packages/i18n/src/locales/hi-IN.i18n.json b/packages/i18n/src/locales/hi-IN.i18n.json index 9b1854c98bdf6..4f7f08bfae298 100644 --- a/packages/i18n/src/locales/hi-IN.i18n.json +++ b/packages/i18n/src/locales/hi-IN.i18n.json @@ -4405,6 +4405,7 @@ "Test_Connection": "परीक्षण कनेक्शन", "Test_Desktop_Notifications": "डेस्कटॉप सूचनाओं का परीक्षण करें", "Test_LDAP_Search": "एलडीएपी खोज का परीक्षण करें", + "Text": "टेक्स्ट", "Texts": "ग्रंथों", "Thank_You_For_Choosing_RocketChat": "रॉकेट.चैट चुनने के लिए धन्यवाद!", "Thank_you_exclamation_mark": "धन्यवाद!", @@ -4594,6 +4595,8 @@ "UI_Use_Real_Name": "वास्तविक नाम का प्रयोग करें", "URL": "यूआरएल", "URLs": "यूआरएल", + "URL_is_required": "यूआरएल आवश्यक है", + "URL_must_start_with_'http://'_or_'https://'": "यूआरएल की शुरुआत 'http://' या 'https://' से होनी चाहिए।", "UTC_Timezone": "यूटीसी समय क्षेत्र", "UTF8_Channel_Names_Validation": "UTF8 चैनल नाम सत्यापन", "UTF8_Channel_Names_Validation_Description": "रेगएक्सपी जिसका उपयोग चैनल नामों को मान्य करने के लिए किया जाएगा", diff --git a/packages/i18n/src/locales/pt-BR.i18n.json b/packages/i18n/src/locales/pt-BR.i18n.json index f2454e40f97ba..67e62c75e2991 100644 --- a/packages/i18n/src/locales/pt-BR.i18n.json +++ b/packages/i18n/src/locales/pt-BR.i18n.json @@ -5154,6 +5154,8 @@ "UI_Use_Real_Name": "Usar o nome verdadeiro", "URL": "URL", "URLs": "URLs", + "URL_is_required": "URL é obrigatório", + "URL_must_start_with_'http://'_or_'https://'": "A URL deve começar com 'http://' ou 'https://'", "UTC_Timezone": "Fuso horário UTC", "UTF8_Channel_Names_Validation": "Validação de nomes de canal UTF8", "UTF8_Channel_Names_Validation_Description": "RegExp que será usado para validar nomes de canais", From 119c4d65edaaad45a05ac22908a62d169ecbd688 Mon Sep 17 00:00:00 2001 From: Julio Araujo <julio.araujo@rocket.chat> Date: Fri, 27 Feb 2026 21:38:20 +0100 Subject: [PATCH 089/108] chore(deps): bump storybook-related dependencies (#39131) --- apps/meteor/package.json | 12 +- ee/packages/pdf-worker/package.json | 8 +- packages/fuselage-ui-kit/package.json | 12 +- packages/gazzodown/package.json | 16 +- packages/livechat/package.json | 10 +- packages/mock-providers/package.json | 4 +- packages/storybook-config/package.json | 16 +- packages/ui-client/package.json | 6 +- packages/ui-composer/package.json | 14 +- packages/ui-video-conf/package.json | 14 +- packages/ui-voip/package.json | 18 +- packages/web-ui-registration/package.json | 14 +- yarn.lock | 530 +++++++++++----------- 13 files changed, 337 insertions(+), 337 deletions(-) diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 4f56db912a2e2..b45abd588ed70 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -319,13 +319,13 @@ "@rocket.chat/livechat": "workspace:^", "@rocket.chat/mock-providers": "workspace:^", "@rocket.chat/tsconfig": "workspace:*", - "@storybook/addon-a11y": "^8.6.15", - "@storybook/addon-essentials": "^8.6.15", - "@storybook/addon-interactions": "^8.6.15", + "@storybook/addon-a11y": "^8.6.17", + "@storybook/addon-essentials": "^8.6.17", + "@storybook/addon-interactions": "^8.6.17", "@storybook/addon-styling-webpack": "^1.0.1", "@storybook/addon-webpack5-compiler-swc": "~3.0.0", - "@storybook/react": "^8.6.15", - "@storybook/react-webpack5": "^8.6.15", + "@storybook/react": "^8.6.17", + "@storybook/react-webpack5": "^8.6.17", "@testing-library/dom": "~10.4.1", "@testing-library/react": "~16.3.2", "@testing-library/user-event": "~14.6.1", @@ -435,7 +435,7 @@ "react-docgen-typescript-plugin": "^1.0.8", "sinon": "^19.0.5", "source-map": "~0.7.6", - "storybook": "^8.6.15", + "storybook": "^8.6.17", "stylelint": "^16.10.0", "stylelint-config-standard": "^36.0.1", "stylelint-order": "^6.0.4", diff --git a/ee/packages/pdf-worker/package.json b/ee/packages/pdf-worker/package.json index e040323b2973e..7bf8e85451288 100644 --- a/ee/packages/pdf-worker/package.json +++ b/ee/packages/pdf-worker/package.json @@ -31,10 +31,10 @@ "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/message-parser": "workspace:~", "@rocket.chat/tsconfig": "workspace:*", - "@storybook/addon-essentials": "^8.6.15", + "@storybook/addon-essentials": "^8.6.17", "@storybook/addon-webpack5-compiler-swc": "~3.0.0", - "@storybook/react": "^8.6.15", - "@storybook/react-webpack5": "^8.6.15", + "@storybook/react": "^8.6.17", + "@storybook/react-webpack5": "^8.6.17", "@testing-library/dom": "~10.4.1", "@testing-library/react": "~16.3.2", "@types/emojione": "^2.2.9", @@ -46,7 +46,7 @@ "i18next": "~23.4.9", "jest": "~30.2.0", "react-dom": "~18.3.1", - "storybook": "^8.6.15", + "storybook": "^8.6.17", "typescript": "~5.9.3" }, "volta": { diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json index 8e93d55562fcb..69d7469461770 100644 --- a/packages/fuselage-ui-kit/package.json +++ b/packages/fuselage-ui-kit/package.json @@ -58,13 +58,13 @@ "@rocket.chat/ui-contexts": "workspace:^", "@rocket.chat/ui-kit": "workspace:~", "@rocket.chat/ui-video-conf": "workspace:^", - "@storybook/addon-essentials": "^8.6.15", + "@storybook/addon-essentials": "^8.6.17", "@storybook/addon-styling-webpack": "^1.0.1", "@storybook/addon-webpack5-compiler-swc": "~3.0.0", - "@storybook/blocks": "^8.6.15", - "@storybook/react": "^8.6.15", - "@storybook/react-webpack5": "^8.6.15", - "@storybook/theming": "^8.6.15", + "@storybook/blocks": "^8.6.17", + "@storybook/react": "^8.6.17", + "@storybook/react-webpack5": "^8.6.17", + "@storybook/theming": "^8.6.17", "@tanstack/react-query": "~5.65.1", "@testing-library/dom": "~10.4.1", "@testing-library/react": "~16.3.2", @@ -83,7 +83,7 @@ "react-i18next": "~13.2.2", "react-virtuoso": "^4.12.0", "rimraf": "^6.0.1", - "storybook": "^8.6.15", + "storybook": "^8.6.17", "storybook-dark-mode": "^4.0.2", "typescript": "~5.9.3", "webpack": "~5.99.9" diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json index 8b526b0831ba5..7603b67b4c9a7 100644 --- a/packages/gazzodown/package.json +++ b/packages/gazzodown/package.json @@ -41,15 +41,15 @@ "@rocket.chat/tsconfig": "workspace:*", "@rocket.chat/ui-client": "workspace:^", "@rocket.chat/ui-contexts": "workspace:^", - "@storybook/addon-actions": "^8.6.15", - "@storybook/addon-docs": "^8.6.15", - "@storybook/addon-essentials": "^8.6.15", - "@storybook/addon-interactions": "^8.6.15", - "@storybook/addon-links": "^8.6.15", + "@storybook/addon-actions": "^8.6.17", + "@storybook/addon-docs": "^8.6.17", + "@storybook/addon-essentials": "^8.6.17", + "@storybook/addon-interactions": "^8.6.17", + "@storybook/addon-links": "^8.6.17", "@storybook/addon-styling-webpack": "^1.0.1", "@storybook/addon-webpack5-compiler-swc": "~3.0.0", - "@storybook/react": "^8.6.15", - "@storybook/react-webpack5": "^8.6.15", + "@storybook/react": "^8.6.17", + "@storybook/react-webpack5": "^8.6.17", "@testing-library/dom": "~10.4.1", "@testing-library/react": "~16.3.2", "@types/jest": "~30.0.0", @@ -67,7 +67,7 @@ "react-dom": "~18.3.1", "react-i18next": "~13.2.2", "react-virtuoso": "^4.12.0", - "storybook": "^8.6.15", + "storybook": "^8.6.17", "typescript": "~5.9.3", "webpack": "~5.99.9" }, diff --git a/packages/livechat/package.json b/packages/livechat/package.json index ec8ef8a9b60db..bf49e41b1ccc0 100644 --- a/packages/livechat/package.json +++ b/packages/livechat/package.json @@ -62,12 +62,12 @@ "@rocket.chat/fuselage-tokens": "~0.33.2", "@rocket.chat/logo": "^0.32.4", "@rocket.chat/ui-contexts": "workspace:^", - "@storybook/addon-essentials": "^8.6.15", + "@storybook/addon-essentials": "^8.6.17", "@storybook/addon-styling-webpack": "^1.0.1", "@storybook/addon-webpack5-compiler-swc": "~3.0.0", - "@storybook/preact": "^8.6.15", - "@storybook/preact-webpack5": "^8.6.15", - "@storybook/theming": "^8.6.15", + "@storybook/preact": "^8.6.17", + "@storybook/preact-webpack5": "^8.6.17", + "@storybook/theming": "^8.6.17", "@types/crypto-js": "~4.2.2", "@types/mini-css-extract-plugin": "~2.5.1", "@types/react": "~18.3.27", @@ -100,7 +100,7 @@ "rimraf": "^6.0.1", "sass": "~1.80.7", "sass-loader": "~16.0.6", - "storybook": "^8.6.15", + "storybook": "^8.6.17", "style-loader": "^1.3.0", "stylelint": "^16.10.0", "stylelint-order": "^6.0.4", diff --git a/packages/mock-providers/package.json b/packages/mock-providers/package.json index fd84e7cd7bb7a..307f54783cd1f 100644 --- a/packages/mock-providers/package.json +++ b/packages/mock-providers/package.json @@ -17,7 +17,7 @@ "@rocket.chat/emitter": "^0.32.0", "@rocket.chat/i18n": "workspace:~", "@rocket.chat/ui-contexts": "workspace:^", - "@storybook/react": "^8.6.15", + "@storybook/react": "^8.6.17", "i18next": "~23.4.9", "react-i18next": "~13.2.2" }, @@ -37,7 +37,7 @@ "jest": "~30.2.0", "react": "~18.3.1", "react-dom": "~18.3.1", - "storybook": "^8.6.15", + "storybook": "^8.6.17", "typescript": "~5.9.3" }, "peerDependencies": { diff --git a/packages/storybook-config/package.json b/packages/storybook-config/package.json index 3803e5a5e78b6..559008ec391c2 100644 --- a/packages/storybook-config/package.json +++ b/packages/storybook-config/package.json @@ -22,16 +22,16 @@ "@rocket.chat/emitter": "^0.32.0", "@rocket.chat/fuselage-hooks": "^0.39.0", "@rocket.chat/fuselage-tokens": "~0.33.2", - "@storybook/addon-a11y": "^8.6.15", - "@storybook/addon-essentials": "^8.6.15", + "@storybook/addon-a11y": "^8.6.17", + "@storybook/addon-essentials": "^8.6.17", "@storybook/addon-styling-webpack": "^1.0.1", "@storybook/addon-webpack5-compiler-swc": "~3.0.0", - "@storybook/blocks": "^8.6.15", - "@storybook/preview-api": "^8.6.15", - "@storybook/react-webpack5": "^8.6.15", - "@storybook/theming": "^8.6.15", + "@storybook/blocks": "^8.6.17", + "@storybook/preview-api": "^8.6.17", + "@storybook/react-webpack5": "^8.6.17", + "@storybook/theming": "^8.6.17", "react-virtuoso": "^4.12.0", - "storybook": "^8.6.15", + "storybook": "^8.6.17", "storybook-dark-mode": "^4.0.2", "webpack": "~5.99.9" }, @@ -39,7 +39,7 @@ "@rocket.chat/fuselage": "^0.71.0", "@rocket.chat/icons": "~0.46.0", "@rocket.chat/tsconfig": "workspace:*", - "@storybook/react": "^8.6.15", + "@storybook/react": "^8.6.17", "eslint": "~9.39.3", "react": "~18.3.1", "react-dom": "~18.3.1", diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json index 72b9384a0a412..5fd016d3cfc8c 100644 --- a/packages/ui-client/package.json +++ b/packages/ui-client/package.json @@ -37,8 +37,8 @@ "@rocket.chat/tsconfig": "workspace:*", "@rocket.chat/ui-avatar": "workspace:~", "@rocket.chat/ui-contexts": "workspace:~", - "@storybook/react": "^8.6.15", - "@storybook/react-webpack5": "^8.6.15", + "@storybook/react": "^8.6.17", + "@storybook/react-webpack5": "^8.6.17", "@testing-library/dom": "~10.4.1", "@testing-library/react": "~16.3.2", "@types/jest": "~30.0.0", @@ -53,7 +53,7 @@ "react-dom": "~18.3.1", "react-hook-form": "~7.45.4", "react-virtuoso": "^4.12.0", - "storybook": "^8.6.15", + "storybook": "^8.6.17", "typescript": "~5.9.3" }, "peerDependencies": { diff --git a/packages/ui-composer/package.json b/packages/ui-composer/package.json index c4501dd4b8625..81262f340e8d3 100644 --- a/packages/ui-composer/package.json +++ b/packages/ui-composer/package.json @@ -28,14 +28,14 @@ "@rocket.chat/icons": "~0.46.0", "@rocket.chat/jest-presets": "workspace:~", "@rocket.chat/tsconfig": "workspace:*", - "@storybook/addon-a11y": "^8.6.15", - "@storybook/addon-actions": "^8.6.15", - "@storybook/addon-docs": "^8.6.15", - "@storybook/addon-essentials": "^8.6.15", + "@storybook/addon-a11y": "^8.6.17", + "@storybook/addon-actions": "^8.6.17", + "@storybook/addon-docs": "^8.6.17", + "@storybook/addon-essentials": "^8.6.17", "@storybook/addon-styling-webpack": "^1.0.1", "@storybook/addon-webpack5-compiler-swc": "~3.0.0", - "@storybook/react": "^8.6.15", - "@storybook/react-webpack5": "^8.6.15", + "@storybook/react": "^8.6.17", + "@storybook/react-webpack5": "^8.6.17", "@types/jest": "~30.0.0", "@types/react": "~18.3.27", "@types/react-dom": "~18.3.7", @@ -44,7 +44,7 @@ "react": "~18.3.1", "react-dom": "~18.3.1", "react-virtuoso": "^4.12.0", - "storybook": "^8.6.15", + "storybook": "^8.6.17", "typescript": "~5.9.3", "webpack": "~5.99.9" }, diff --git a/packages/ui-video-conf/package.json b/packages/ui-video-conf/package.json index d42e8900dc106..c169cb5d5e632 100644 --- a/packages/ui-video-conf/package.json +++ b/packages/ui-video-conf/package.json @@ -31,14 +31,14 @@ "@rocket.chat/tsconfig": "workspace:*", "@rocket.chat/ui-avatar": "workspace:^", "@rocket.chat/ui-contexts": "workspace:^", - "@storybook/addon-a11y": "^8.6.15", - "@storybook/addon-actions": "^8.6.15", - "@storybook/addon-docs": "^8.6.15", - "@storybook/addon-essentials": "^8.6.15", + "@storybook/addon-a11y": "^8.6.17", + "@storybook/addon-actions": "^8.6.17", + "@storybook/addon-docs": "^8.6.17", + "@storybook/addon-essentials": "^8.6.17", "@storybook/addon-styling-webpack": "^1.0.1", "@storybook/addon-webpack5-compiler-swc": "~3.0.0", - "@storybook/react": "^8.6.15", - "@storybook/react-webpack5": "^8.6.15", + "@storybook/react": "^8.6.17", + "@storybook/react-webpack5": "^8.6.17", "@types/jest": "~30.0.0", "@types/jest-axe": "~3.5.9", "@types/react": "~18.3.27", @@ -49,7 +49,7 @@ "react": "~18.3.1", "react-dom": "~18.3.1", "react-virtuoso": "^4.12.0", - "storybook": "^8.6.15", + "storybook": "^8.6.17", "typescript": "~5.9.3", "webpack": "~5.99.9" }, diff --git a/packages/ui-voip/package.json b/packages/ui-voip/package.json index 952a93f4d0e4b..df4bd2e225db4 100644 --- a/packages/ui-voip/package.json +++ b/packages/ui-voip/package.json @@ -43,15 +43,15 @@ "@rocket.chat/ui-client": "workspace:^", "@rocket.chat/ui-contexts": "workspace:^", "@rocket.chat/ui-kit": "workspace:^", - "@storybook/addon-a11y": "^8.6.15", - "@storybook/addon-actions": "^8.6.15", - "@storybook/addon-docs": "^8.6.15", - "@storybook/addon-essentials": "^8.6.15", - "@storybook/addon-interactions": "^8.6.15", + "@storybook/addon-a11y": "^8.6.17", + "@storybook/addon-actions": "^8.6.17", + "@storybook/addon-docs": "^8.6.17", + "@storybook/addon-essentials": "^8.6.17", + "@storybook/addon-interactions": "^8.6.17", "@storybook/addon-webpack5-compiler-swc": "~3.0.0", - "@storybook/react": "^8.6.15", - "@storybook/react-webpack5": "^8.6.15", - "@storybook/test": "^8.6.15", + "@storybook/react": "^8.6.17", + "@storybook/react-webpack5": "^8.6.17", + "@storybook/test": "^8.6.17", "@storybook/test-runner": "^0.22.1", "@testing-library/dom": "~10.4.1", "@testing-library/react": "~16.3.2", @@ -68,7 +68,7 @@ "react": "~18.3.1", "react-dom": "~18.3.1", "react-virtuoso": "^4.12.0", - "storybook": "^8.6.15", + "storybook": "^8.6.17", "typescript": "~5.9.3" }, "peerDependencies": { diff --git a/packages/web-ui-registration/package.json b/packages/web-ui-registration/package.json index 4559294b3e3fa..09662271ff880 100644 --- a/packages/web-ui-registration/package.json +++ b/packages/web-ui-registration/package.json @@ -34,13 +34,13 @@ "@rocket.chat/tools": "workspace:~", "@rocket.chat/ui-client": "workspace:^", "@rocket.chat/ui-contexts": "workspace:^", - "@storybook/addon-actions": "^8.6.15", - "@storybook/addon-docs": "^8.6.15", - "@storybook/addon-essentials": "^8.6.15", + "@storybook/addon-actions": "^8.6.17", + "@storybook/addon-docs": "^8.6.17", + "@storybook/addon-essentials": "^8.6.17", "@storybook/addon-webpack5-compiler-swc": "~3.0.0", - "@storybook/react": "^8.6.15", - "@storybook/react-webpack5": "^8.6.15", - "@storybook/theming": "^8.6.15", + "@storybook/react": "^8.6.17", + "@storybook/react-webpack5": "^8.6.17", + "@storybook/theming": "^8.6.17", "@tanstack/react-query": "~5.65.1", "@testing-library/dom": "~10.4.1", "@testing-library/react": "~16.3.2", @@ -53,7 +53,7 @@ "react-hook-form": "~7.45.4", "react-i18next": "~13.2.2", "react-virtuoso": "^4.12.0", - "storybook": "^8.6.15", + "storybook": "^8.6.17", "storybook-dark-mode": "^4.0.2", "typescript": "~5.9.3" }, diff --git a/yarn.lock b/yarn.lock index 88f1024df3206..1598ae0fd7522 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8581,13 +8581,13 @@ __metadata: "@rocket.chat/ui-contexts": "workspace:^" "@rocket.chat/ui-kit": "workspace:~" "@rocket.chat/ui-video-conf": "workspace:^" - "@storybook/addon-essentials": "npm:^8.6.15" + "@storybook/addon-essentials": "npm:^8.6.17" "@storybook/addon-styling-webpack": "npm:^1.0.1" "@storybook/addon-webpack5-compiler-swc": "npm:~3.0.0" - "@storybook/blocks": "npm:^8.6.15" - "@storybook/react": "npm:^8.6.15" - "@storybook/react-webpack5": "npm:^8.6.15" - "@storybook/theming": "npm:^8.6.15" + "@storybook/blocks": "npm:^8.6.17" + "@storybook/react": "npm:^8.6.17" + "@storybook/react-webpack5": "npm:^8.6.17" + "@storybook/theming": "npm:^8.6.17" "@tanstack/react-query": "npm:~5.65.1" "@testing-library/dom": "npm:~10.4.1" "@testing-library/react": "npm:~16.3.2" @@ -8606,7 +8606,7 @@ __metadata: react-i18next: "npm:~13.2.2" react-virtuoso: "npm:^4.12.0" rimraf: "npm:^6.0.1" - storybook: "npm:^8.6.15" + storybook: "npm:^8.6.17" storybook-dark-mode: "npm:^4.0.2" typescript: "npm:~5.9.3" webpack: "npm:~5.99.9" @@ -8668,15 +8668,15 @@ __metadata: "@rocket.chat/tsconfig": "workspace:*" "@rocket.chat/ui-client": "workspace:^" "@rocket.chat/ui-contexts": "workspace:^" - "@storybook/addon-actions": "npm:^8.6.15" - "@storybook/addon-docs": "npm:^8.6.15" - "@storybook/addon-essentials": "npm:^8.6.15" - "@storybook/addon-interactions": "npm:^8.6.15" - "@storybook/addon-links": "npm:^8.6.15" + "@storybook/addon-actions": "npm:^8.6.17" + "@storybook/addon-docs": "npm:^8.6.17" + "@storybook/addon-essentials": "npm:^8.6.17" + "@storybook/addon-interactions": "npm:^8.6.17" + "@storybook/addon-links": "npm:^8.6.17" "@storybook/addon-styling-webpack": "npm:^1.0.1" "@storybook/addon-webpack5-compiler-swc": "npm:~3.0.0" - "@storybook/react": "npm:^8.6.15" - "@storybook/react-webpack5": "npm:^8.6.15" + "@storybook/react": "npm:^8.6.17" + "@storybook/react-webpack5": "npm:^8.6.17" "@testing-library/dom": "npm:~10.4.1" "@testing-library/react": "npm:~16.3.2" "@types/jest": "npm:~30.0.0" @@ -8699,7 +8699,7 @@ __metadata: react-i18next: "npm:~13.2.2" react-stately: "npm:~3.17.0" react-virtuoso: "npm:^4.12.0" - storybook: "npm:^8.6.15" + storybook: "npm:^8.6.17" typescript: "npm:~5.9.3" webpack: "npm:~5.99.9" peerDependencies: @@ -8865,12 +8865,12 @@ __metadata: "@rocket.chat/random": "workspace:~" "@rocket.chat/ui-contexts": "workspace:^" "@rocket.chat/ui-kit": "workspace:~" - "@storybook/addon-essentials": "npm:^8.6.15" + "@storybook/addon-essentials": "npm:^8.6.17" "@storybook/addon-styling-webpack": "npm:^1.0.1" "@storybook/addon-webpack5-compiler-swc": "npm:~3.0.0" - "@storybook/preact": "npm:^8.6.15" - "@storybook/preact-webpack5": "npm:^8.6.15" - "@storybook/theming": "npm:^8.6.15" + "@storybook/preact": "npm:^8.6.17" + "@storybook/preact-webpack5": "npm:^8.6.17" + "@storybook/theming": "npm:^8.6.17" "@types/crypto-js": "npm:~4.2.2" "@types/mini-css-extract-plugin": "npm:~2.5.1" "@types/react": "npm:~18.3.27" @@ -8918,7 +8918,7 @@ __metadata: rimraf: "npm:^6.0.1" sass: "npm:~1.80.7" sass-loader: "npm:~16.0.6" - storybook: "npm:^8.6.15" + storybook: "npm:^8.6.17" storybook-dark-mode: "npm:^4.0.2" style-loader: "npm:^1.3.0" stylelint: "npm:^16.10.0" @@ -9162,13 +9162,13 @@ __metadata: "@rocket.chat/web-ui-registration": "workspace:^" "@slack/bolt": "npm:^3.22.0" "@slack/rtm-api": "npm:~7.0.4" - "@storybook/addon-a11y": "npm:^8.6.15" - "@storybook/addon-essentials": "npm:^8.6.15" - "@storybook/addon-interactions": "npm:^8.6.15" + "@storybook/addon-a11y": "npm:^8.6.17" + "@storybook/addon-essentials": "npm:^8.6.17" + "@storybook/addon-interactions": "npm:^8.6.17" "@storybook/addon-styling-webpack": "npm:^1.0.1" "@storybook/addon-webpack5-compiler-swc": "npm:~3.0.0" - "@storybook/react": "npm:^8.6.15" - "@storybook/react-webpack5": "npm:^8.6.15" + "@storybook/react": "npm:^8.6.17" + "@storybook/react-webpack5": "npm:^8.6.17" "@tanstack/react-query": "npm:~5.65.1" "@testing-library/dom": "npm:~10.4.1" "@testing-library/react": "npm:~16.3.2" @@ -9405,7 +9405,7 @@ __metadata: sodium-plus: "npm:^0.9.0" source-map: "npm:~0.7.6" speakeasy: "npm:^2.0.0" - storybook: "npm:^8.6.15" + storybook: "npm:^8.6.17" stream-buffers: "npm:^3.0.3" strict-uri-encode: "npm:^2.0.0" string-strip-html: "npm:^8.5.0" @@ -9454,7 +9454,7 @@ __metadata: "@rocket.chat/tools": "workspace:~" "@rocket.chat/ui-contexts": "workspace:^" "@rocket.chat/ui-video-conf": "workspace:*" - "@storybook/react": "npm:^8.6.15" + "@storybook/react": "npm:^8.6.17" "@tanstack/react-query": "npm:~5.65.1" "@testing-library/dom": "npm:~10.4.1" "@testing-library/jest-dom": "npm:~6.8.0" @@ -9467,7 +9467,7 @@ __metadata: react: "npm:~18.3.1" react-dom: "npm:~18.3.1" react-i18next: "npm:~13.2.2" - storybook: "npm:^8.6.15" + storybook: "npm:^8.6.17" typescript: "npm:~5.9.3" peerDependencies: "@tanstack/react-query": "*" @@ -9723,10 +9723,10 @@ __metadata: "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/message-parser": "workspace:~" "@rocket.chat/tsconfig": "workspace:*" - "@storybook/addon-essentials": "npm:^8.6.15" + "@storybook/addon-essentials": "npm:^8.6.17" "@storybook/addon-webpack5-compiler-swc": "npm:~3.0.0" - "@storybook/react": "npm:^8.6.15" - "@storybook/react-webpack5": "npm:^8.6.15" + "@storybook/react": "npm:^8.6.17" + "@storybook/react-webpack5": "npm:^8.6.17" "@testing-library/dom": "npm:~10.4.1" "@testing-library/react": "npm:~16.3.2" "@types/emojione": "npm:^2.2.9" @@ -9743,7 +9743,7 @@ __metadata: react: "npm:~18.3.1" react-dom: "npm:~18.3.1" react-i18next: "npm:~13.2.2" - storybook: "npm:^8.6.15" + storybook: "npm:^8.6.17" typescript: "npm:~5.9.3" languageName: unknown linkType: soft @@ -9983,20 +9983,20 @@ __metadata: "@rocket.chat/fuselage-tokens": "npm:~0.33.2" "@rocket.chat/icons": "npm:~0.46.0" "@rocket.chat/tsconfig": "workspace:*" - "@storybook/addon-a11y": "npm:^8.6.15" - "@storybook/addon-essentials": "npm:^8.6.15" + "@storybook/addon-a11y": "npm:^8.6.17" + "@storybook/addon-essentials": "npm:^8.6.17" "@storybook/addon-styling-webpack": "npm:^1.0.1" "@storybook/addon-webpack5-compiler-swc": "npm:~3.0.0" - "@storybook/blocks": "npm:^8.6.15" - "@storybook/preview-api": "npm:^8.6.15" - "@storybook/react": "npm:^8.6.15" - "@storybook/react-webpack5": "npm:^8.6.15" - "@storybook/theming": "npm:^8.6.15" + "@storybook/blocks": "npm:^8.6.17" + "@storybook/preview-api": "npm:^8.6.17" + "@storybook/react": "npm:^8.6.17" + "@storybook/react-webpack5": "npm:^8.6.17" + "@storybook/theming": "npm:^8.6.17" eslint: "npm:~9.39.3" react: "npm:~18.3.1" react-dom: "npm:~18.3.1" react-virtuoso: "npm:^4.12.0" - storybook: "npm:^8.6.15" + storybook: "npm:^8.6.17" storybook-dark-mode: "npm:^4.0.2" typescript: "npm:~5.9.3" webpack: "npm:~5.99.9" @@ -10115,8 +10115,8 @@ __metadata: "@rocket.chat/tsconfig": "workspace:*" "@rocket.chat/ui-avatar": "workspace:~" "@rocket.chat/ui-contexts": "workspace:~" - "@storybook/react": "npm:^8.6.15" - "@storybook/react-webpack5": "npm:^8.6.15" + "@storybook/react": "npm:^8.6.17" + "@storybook/react-webpack5": "npm:^8.6.17" "@testing-library/dom": "npm:~10.4.1" "@testing-library/react": "npm:~16.3.2" "@types/jest": "npm:~30.0.0" @@ -10132,7 +10132,7 @@ __metadata: react-dom: "npm:~18.3.1" react-hook-form: "npm:~7.45.4" react-virtuoso: "npm:^4.12.0" - storybook: "npm:^8.6.15" + storybook: "npm:^8.6.17" typescript: "npm:~5.9.3" peerDependencies: "@react-aria/toolbar": "*" @@ -10162,14 +10162,14 @@ __metadata: "@rocket.chat/icons": "npm:~0.46.0" "@rocket.chat/jest-presets": "workspace:~" "@rocket.chat/tsconfig": "workspace:*" - "@storybook/addon-a11y": "npm:^8.6.15" - "@storybook/addon-actions": "npm:^8.6.15" - "@storybook/addon-docs": "npm:^8.6.15" - "@storybook/addon-essentials": "npm:^8.6.15" + "@storybook/addon-a11y": "npm:^8.6.17" + "@storybook/addon-actions": "npm:^8.6.17" + "@storybook/addon-docs": "npm:^8.6.17" + "@storybook/addon-essentials": "npm:^8.6.17" "@storybook/addon-styling-webpack": "npm:^1.0.1" "@storybook/addon-webpack5-compiler-swc": "npm:~3.0.0" - "@storybook/react": "npm:^8.6.15" - "@storybook/react-webpack5": "npm:^8.6.15" + "@storybook/react": "npm:^8.6.17" + "@storybook/react-webpack5": "npm:^8.6.17" "@types/jest": "npm:~30.0.0" "@types/react": "npm:~18.3.27" "@types/react-dom": "npm:~18.3.7" @@ -10178,7 +10178,7 @@ __metadata: react: "npm:~18.3.1" react-dom: "npm:~18.3.1" react-virtuoso: "npm:^4.12.0" - storybook: "npm:^8.6.15" + storybook: "npm:^8.6.17" typescript: "npm:~5.9.3" webpack: "npm:~5.99.9" peerDependencies: @@ -10292,14 +10292,14 @@ __metadata: "@rocket.chat/tsconfig": "workspace:*" "@rocket.chat/ui-avatar": "workspace:^" "@rocket.chat/ui-contexts": "workspace:^" - "@storybook/addon-a11y": "npm:^8.6.15" - "@storybook/addon-actions": "npm:^8.6.15" - "@storybook/addon-docs": "npm:^8.6.15" - "@storybook/addon-essentials": "npm:^8.6.15" + "@storybook/addon-a11y": "npm:^8.6.17" + "@storybook/addon-actions": "npm:^8.6.17" + "@storybook/addon-docs": "npm:^8.6.17" + "@storybook/addon-essentials": "npm:^8.6.17" "@storybook/addon-styling-webpack": "npm:^1.0.1" "@storybook/addon-webpack5-compiler-swc": "npm:~3.0.0" - "@storybook/react": "npm:^8.6.15" - "@storybook/react-webpack5": "npm:^8.6.15" + "@storybook/react": "npm:^8.6.17" + "@storybook/react-webpack5": "npm:^8.6.17" "@types/jest": "npm:~30.0.0" "@types/jest-axe": "npm:~3.5.9" "@types/react": "npm:~18.3.27" @@ -10310,7 +10310,7 @@ __metadata: react: "npm:~18.3.1" react-dom: "npm:~18.3.1" react-virtuoso: "npm:^4.12.0" - storybook: "npm:^8.6.15" + storybook: "npm:^8.6.17" typescript: "npm:~5.9.3" webpack: "npm:~5.99.9" peerDependencies: @@ -10350,15 +10350,15 @@ __metadata: "@rocket.chat/ui-client": "workspace:^" "@rocket.chat/ui-contexts": "workspace:^" "@rocket.chat/ui-kit": "workspace:^" - "@storybook/addon-a11y": "npm:^8.6.15" - "@storybook/addon-actions": "npm:^8.6.15" - "@storybook/addon-docs": "npm:^8.6.15" - "@storybook/addon-essentials": "npm:^8.6.15" - "@storybook/addon-interactions": "npm:^8.6.15" + "@storybook/addon-a11y": "npm:^8.6.17" + "@storybook/addon-actions": "npm:^8.6.17" + "@storybook/addon-docs": "npm:^8.6.17" + "@storybook/addon-essentials": "npm:^8.6.17" + "@storybook/addon-interactions": "npm:^8.6.17" "@storybook/addon-webpack5-compiler-swc": "npm:~3.0.0" - "@storybook/react": "npm:^8.6.15" - "@storybook/react-webpack5": "npm:^8.6.15" - "@storybook/test": "npm:^8.6.15" + "@storybook/react": "npm:^8.6.17" + "@storybook/react-webpack5": "npm:^8.6.17" + "@storybook/test": "npm:^8.6.17" "@storybook/test-runner": "npm:^0.22.1" "@tanstack/react-query": "npm:~5.65.1" "@testing-library/dom": "npm:~10.4.1" @@ -10377,7 +10377,7 @@ __metadata: react-dom: "npm:~18.3.1" react-i18next: "npm:~13.2.2" react-virtuoso: "npm:^4.12.0" - storybook: "npm:^8.6.15" + storybook: "npm:^8.6.17" typescript: "npm:~5.9.3" peerDependencies: "@rocket.chat/css-in-js": "*" @@ -10459,13 +10459,13 @@ __metadata: "@rocket.chat/tools": "workspace:~" "@rocket.chat/ui-client": "workspace:^" "@rocket.chat/ui-contexts": "workspace:^" - "@storybook/addon-actions": "npm:^8.6.15" - "@storybook/addon-docs": "npm:^8.6.15" - "@storybook/addon-essentials": "npm:^8.6.15" + "@storybook/addon-actions": "npm:^8.6.17" + "@storybook/addon-docs": "npm:^8.6.17" + "@storybook/addon-essentials": "npm:^8.6.17" "@storybook/addon-webpack5-compiler-swc": "npm:~3.0.0" - "@storybook/react": "npm:^8.6.15" - "@storybook/react-webpack5": "npm:^8.6.15" - "@storybook/theming": "npm:^8.6.15" + "@storybook/react": "npm:^8.6.17" + "@storybook/react-webpack5": "npm:^8.6.17" + "@storybook/theming": "npm:^8.6.17" "@tanstack/react-query": "npm:~5.65.1" "@testing-library/dom": "npm:~10.4.1" "@testing-library/react": "npm:~16.3.2" @@ -10479,7 +10479,7 @@ __metadata: react-hook-form: "npm:~7.45.4" react-i18next: "npm:~13.2.2" react-virtuoso: "npm:^4.12.0" - storybook: "npm:^8.6.15" + storybook: "npm:^8.6.17" storybook-dark-mode: "npm:^4.0.2" typescript: "npm:~5.9.3" peerDependencies: @@ -10978,23 +10978,23 @@ __metadata: languageName: node linkType: hard -"@storybook/addon-a11y@npm:^8.6.15": - version: 8.6.15 - resolution: "@storybook/addon-a11y@npm:8.6.15" +"@storybook/addon-a11y@npm:^8.6.17": + version: 8.6.17 + resolution: "@storybook/addon-a11y@npm:8.6.17" dependencies: - "@storybook/addon-highlight": "npm:8.6.15" + "@storybook/addon-highlight": "npm:8.6.17" "@storybook/global": "npm:^5.0.0" - "@storybook/test": "npm:8.6.15" + "@storybook/test": "npm:8.6.17" axe-core: "npm:^4.2.0" peerDependencies: - storybook: ^8.6.15 - checksum: 10/558fcb105486112118bfc5f0068efcb5d4b66b508820f8e2a6d4041e7b06029b64d2d3a8b51a7a6049c836b900ead67c33fa9f89e0a1b550f0e5eedbab18e33c + storybook: ^8.6.17 + checksum: 10/020f1a57ac360f36ba0cfaa2218782a228695664a8577e753ab39d774fb73f1793002810687cd7fe3237ea79472f03ba4fbb82a3f0411bc6897888b8e2ddea49 languageName: node linkType: hard -"@storybook/addon-actions@npm:8.6.15, @storybook/addon-actions@npm:^8.6.15": - version: 8.6.15 - resolution: "@storybook/addon-actions@npm:8.6.15" +"@storybook/addon-actions@npm:8.6.17, @storybook/addon-actions@npm:^8.6.17": + version: 8.6.17 + resolution: "@storybook/addon-actions@npm:8.6.17" dependencies: "@storybook/global": "npm:^5.0.0" "@types/uuid": "npm:^9.0.1" @@ -11002,137 +11002,137 @@ __metadata: polished: "npm:^4.2.2" uuid: "npm:^9.0.0" peerDependencies: - storybook: ^8.6.15 - checksum: 10/4d47e3ce9319d282e5abb44e7694748792c29f37c883afbcfc8939353be47bcc004ab41ce1f421c3d5bafbaa0366935f0b90272e5e6542a2229db55f63bef82c + storybook: ^8.6.17 + checksum: 10/3df7a2f0f772b5baaea193d721c4a5568ac7c23202f7afed32114a7334918ae473d4569a14043fb28f5b949ab3ff1b4a9ab50fa62ea0a2af3d0cd0dffa4036df languageName: node linkType: hard -"@storybook/addon-backgrounds@npm:8.6.15": - version: 8.6.15 - resolution: "@storybook/addon-backgrounds@npm:8.6.15" +"@storybook/addon-backgrounds@npm:8.6.17": + version: 8.6.17 + resolution: "@storybook/addon-backgrounds@npm:8.6.17" dependencies: "@storybook/global": "npm:^5.0.0" memoizerific: "npm:^1.11.3" ts-dedent: "npm:^2.0.0" peerDependencies: - storybook: ^8.6.15 - checksum: 10/c96107e892d39d5841e7f64f53a619527268636c61184770ca07684432fb3cee30c224a35b716ff30ba9bcdb4326649cd4e7873359d65a7fa8889b0fe7a15ad4 + storybook: ^8.6.17 + checksum: 10/470ca47276ea5c561bfbb8b9d90549b3fa87a7f1bb3c620f0f15c682cf6b41ebd282e1b98d1c24596af86af2b0db8de5e4508a80702e72ff3b5b9cfc762dd251 languageName: node linkType: hard -"@storybook/addon-controls@npm:8.6.15": - version: 8.6.15 - resolution: "@storybook/addon-controls@npm:8.6.15" +"@storybook/addon-controls@npm:8.6.17": + version: 8.6.17 + resolution: "@storybook/addon-controls@npm:8.6.17" dependencies: "@storybook/global": "npm:^5.0.0" dequal: "npm:^2.0.2" ts-dedent: "npm:^2.0.0" peerDependencies: - storybook: ^8.6.15 - checksum: 10/ee7ea1e4d6cdb47c233ff3dd48196e649bea62bb88816261f65eb0ecd4f83e1593f09e657fc91ed8595b6279a04a1267c01d82020ef749d90c28d4f71b879224 + storybook: ^8.6.17 + checksum: 10/cc388392a62ec88fef04ecc581202747856fc80f402b327de7a9fb94b87e388aaf0addc59029728f0a299cc952f205a127e412be165685e9f315abf6271cd257 languageName: node linkType: hard -"@storybook/addon-docs@npm:8.6.15, @storybook/addon-docs@npm:^8.6.15": - version: 8.6.15 - resolution: "@storybook/addon-docs@npm:8.6.15" +"@storybook/addon-docs@npm:8.6.17, @storybook/addon-docs@npm:^8.6.17": + version: 8.6.17 + resolution: "@storybook/addon-docs@npm:8.6.17" dependencies: "@mdx-js/react": "npm:^3.0.0" - "@storybook/blocks": "npm:8.6.15" - "@storybook/csf-plugin": "npm:8.6.15" - "@storybook/react-dom-shim": "npm:8.6.15" + "@storybook/blocks": "npm:8.6.17" + "@storybook/csf-plugin": "npm:8.6.17" + "@storybook/react-dom-shim": "npm:8.6.17" react: "npm:^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" react-dom: "npm:^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" ts-dedent: "npm:^2.0.0" peerDependencies: - storybook: ^8.6.15 - checksum: 10/ec18d166ebab276258098ef86e9ad9bcaa163b357766bcef957a8f8c2260d4c128834497304cd0cde8fed1b0dfa02b9e79d82b88d61e21eab35437b2d17fa163 + storybook: ^8.6.17 + checksum: 10/ce35b8b990f3183c983d4b75b631f7b07f5bb1d3a147518e047fe2b513fee77c223e78db4c8d2c87e09ebb8e000a30b7060c5316d7cc7ffa39bb66a7139a787c languageName: node linkType: hard -"@storybook/addon-essentials@npm:^8.6.15": - version: 8.6.15 - resolution: "@storybook/addon-essentials@npm:8.6.15" +"@storybook/addon-essentials@npm:^8.6.17": + version: 8.6.17 + resolution: "@storybook/addon-essentials@npm:8.6.17" dependencies: - "@storybook/addon-actions": "npm:8.6.15" - "@storybook/addon-backgrounds": "npm:8.6.15" - "@storybook/addon-controls": "npm:8.6.15" - "@storybook/addon-docs": "npm:8.6.15" - "@storybook/addon-highlight": "npm:8.6.15" - "@storybook/addon-measure": "npm:8.6.15" - "@storybook/addon-outline": "npm:8.6.15" - "@storybook/addon-toolbars": "npm:8.6.15" - "@storybook/addon-viewport": "npm:8.6.15" + "@storybook/addon-actions": "npm:8.6.17" + "@storybook/addon-backgrounds": "npm:8.6.17" + "@storybook/addon-controls": "npm:8.6.17" + "@storybook/addon-docs": "npm:8.6.17" + "@storybook/addon-highlight": "npm:8.6.17" + "@storybook/addon-measure": "npm:8.6.17" + "@storybook/addon-outline": "npm:8.6.17" + "@storybook/addon-toolbars": "npm:8.6.17" + "@storybook/addon-viewport": "npm:8.6.17" ts-dedent: "npm:^2.0.0" peerDependencies: - storybook: ^8.6.15 - checksum: 10/0416693b5f0b7f727deaa2c575e1ad826b93852f41fbd0d905c7bad54c31cf312278575cacc28498db81b35b027373e98c5980b307876297e075e74f9267e7f8 + storybook: ^8.6.17 + checksum: 10/28faaee33ebe4794952618ac52944cf23458d9b61c76556a02660b9173519f690ceefe42eb49b77a0d5e1922cc9fa7bdc10ca16302de1e2b46e7ba8372574ccf languageName: node linkType: hard -"@storybook/addon-highlight@npm:8.6.15": - version: 8.6.15 - resolution: "@storybook/addon-highlight@npm:8.6.15" +"@storybook/addon-highlight@npm:8.6.17": + version: 8.6.17 + resolution: "@storybook/addon-highlight@npm:8.6.17" dependencies: "@storybook/global": "npm:^5.0.0" peerDependencies: - storybook: ^8.6.15 - checksum: 10/51f0a7fbf6c81e78e74f71943f8cf2b5f7e9ec08712e9f186381ecaa15f011c4c7df9d8d99e1e3b44e37bd646ff4d162cf91024d6b6a8b881fe988ccab47f786 + storybook: ^8.6.17 + checksum: 10/022effa867a8171a9500806513da4228a62c1ce54625f89856783252f17e5adf594e3bf28f473b44af8d2161fc08337329e09d61a065ef00d74e48a704a8d249 languageName: node linkType: hard -"@storybook/addon-interactions@npm:^8.6.15": - version: 8.6.15 - resolution: "@storybook/addon-interactions@npm:8.6.15" +"@storybook/addon-interactions@npm:^8.6.17": + version: 8.6.17 + resolution: "@storybook/addon-interactions@npm:8.6.17" dependencies: "@storybook/global": "npm:^5.0.0" - "@storybook/instrumenter": "npm:8.6.15" - "@storybook/test": "npm:8.6.15" + "@storybook/instrumenter": "npm:8.6.17" + "@storybook/test": "npm:8.6.17" polished: "npm:^4.2.2" ts-dedent: "npm:^2.2.0" peerDependencies: - storybook: ^8.6.15 - checksum: 10/a179f83bde02d3e42b9df0e52906855f7055a16df64043c7181e47bd9fe1a1b6af3bc0716b56af9d2fc19fc3b3cd268134757c6f766a045260e5ea018a9acc5f + storybook: ^8.6.17 + checksum: 10/1da114800aca04a9bf7f8658a63a0ba7b192712762f060bd0c8ce9cf6058ce4c529e365fcfa0243529468b92a4c66699815aa2471985130e154aadfd3db79ee8 languageName: node linkType: hard -"@storybook/addon-links@npm:^8.6.15": - version: 8.6.15 - resolution: "@storybook/addon-links@npm:8.6.15" +"@storybook/addon-links@npm:^8.6.17": + version: 8.6.17 + resolution: "@storybook/addon-links@npm:8.6.17" dependencies: "@storybook/global": "npm:^5.0.0" ts-dedent: "npm:^2.0.0" peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.6.15 + storybook: ^8.6.17 peerDependenciesMeta: react: optional: true - checksum: 10/3b39fdf0f7a61d209d957b6003def911c0d000ccc03cec417f31c7ee49395c7c06d3a39241e912ac4dd07d76a2a1f14b1f376462b58aeb04ad45adc2cd540127 + checksum: 10/8df86a8115c49ec0ab92f97dd19356ac4264cd9c176fcc0974fe9be1d41d096a3bb21a80de828b0823ef9657622f067759ceae4d7c3a9529b021406ea148f2a2 languageName: node linkType: hard -"@storybook/addon-measure@npm:8.6.15": - version: 8.6.15 - resolution: "@storybook/addon-measure@npm:8.6.15" +"@storybook/addon-measure@npm:8.6.17": + version: 8.6.17 + resolution: "@storybook/addon-measure@npm:8.6.17" dependencies: "@storybook/global": "npm:^5.0.0" tiny-invariant: "npm:^1.3.1" peerDependencies: - storybook: ^8.6.15 - checksum: 10/62b899f873e0024ed21e081759d731ff978fe917e8739dab0de3ebc857bb43641c9c9074f4a562864b353550fafd17950ab8dbd17b20a97371501ebe5f188a05 + storybook: ^8.6.17 + checksum: 10/3a79d1ed56bfa5aa47ecd57e60b89eff1e2208e570047e10c2f51a9776d255fc112c268e4c54c279876be9177982e3a1640a6366bd8eeb749ee2c2055818e8ad languageName: node linkType: hard -"@storybook/addon-outline@npm:8.6.15": - version: 8.6.15 - resolution: "@storybook/addon-outline@npm:8.6.15" +"@storybook/addon-outline@npm:8.6.17": + version: 8.6.17 + resolution: "@storybook/addon-outline@npm:8.6.17" dependencies: "@storybook/global": "npm:^5.0.0" ts-dedent: "npm:^2.0.0" peerDependencies: - storybook: ^8.6.15 - checksum: 10/9439e6ab319475df7fed652ea4265916699df45d4ba4a04ec6b576a604f4d0917b2b5c8f1a48cabfd174f7baf3b943d3789ba80efb9e299ecefb388f8bf22f79 + storybook: ^8.6.17 + checksum: 10/1777f59f654b1cdc401ad438eef7023c610768bbafd48af5e18de5c7478bcf711be6fea06db1cb321890a03890711580b8c3f1e8edf1a8aa87bfbd3d095ee131 languageName: node linkType: hard @@ -11147,23 +11147,23 @@ __metadata: languageName: node linkType: hard -"@storybook/addon-toolbars@npm:8.6.15": - version: 8.6.15 - resolution: "@storybook/addon-toolbars@npm:8.6.15" +"@storybook/addon-toolbars@npm:8.6.17": + version: 8.6.17 + resolution: "@storybook/addon-toolbars@npm:8.6.17" peerDependencies: - storybook: ^8.6.15 - checksum: 10/27cfba470fd0f85d8bd236a7929b77cc50c89948926a4463f3473bcd04f823cebbf08f8e240a4055d77aac9636d32cfaaee3ca912ff32ee5806e3250df53dff7 + storybook: ^8.6.17 + checksum: 10/9cd8ea339792e584d080a1a9524fcd0a41d9d176a49271a9b740efda7678dc1137a2545d1e61d0adb4ef50ff48c9cb80d6525bad5febb216fa0562629e839f8d languageName: node linkType: hard -"@storybook/addon-viewport@npm:8.6.15": - version: 8.6.15 - resolution: "@storybook/addon-viewport@npm:8.6.15" +"@storybook/addon-viewport@npm:8.6.17": + version: 8.6.17 + resolution: "@storybook/addon-viewport@npm:8.6.17" dependencies: memoizerific: "npm:^1.11.3" peerDependencies: - storybook: ^8.6.15 - checksum: 10/2b3359ac92e9a131c7dc7c4b6bc9d131786c86bead9490d464e4b275278e309a579cd67756d4d9ea5764aae502e3e65104e98486933f7f57b583ab8a3757db8b + storybook: ^8.6.17 + checksum: 10/08399a158d4fd20c81816589beb722d093154f48b6bcd07aa2344a4d392120fa73f88ea74a4df1db749f9de8cfe14893b1f2013d289029cfad4e5510a7ab2c0c languageName: node linkType: hard @@ -11177,30 +11177,30 @@ __metadata: languageName: node linkType: hard -"@storybook/blocks@npm:8.6.15, @storybook/blocks@npm:^8.6.15": - version: 8.6.15 - resolution: "@storybook/blocks@npm:8.6.15" +"@storybook/blocks@npm:8.6.17, @storybook/blocks@npm:^8.6.17": + version: 8.6.17 + resolution: "@storybook/blocks@npm:8.6.17" dependencies: "@storybook/icons": "npm:^1.2.12" ts-dedent: "npm:^2.0.0" peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - storybook: ^8.6.15 + storybook: ^8.6.17 peerDependenciesMeta: react: optional: true react-dom: optional: true - checksum: 10/7598b9fe3c5dcabc02b22eee3780055ae7ca259bc51520c5bf12c34395d11dbac4b0b3f817c46492b407a16f67ff5ba36879fd30c91299ddb053e9f274664e21 + checksum: 10/51855cda3b6a8e770f36ead12e35fa4ef650bb50c38d3dcce5cdcbbf6d1f556f50172fd05dc09eec808854adc4516b480871a18106ab16e6bbc08102e3957aee languageName: node linkType: hard -"@storybook/builder-webpack5@npm:8.6.15": - version: 8.6.15 - resolution: "@storybook/builder-webpack5@npm:8.6.15" +"@storybook/builder-webpack5@npm:8.6.17": + version: 8.6.17 + resolution: "@storybook/builder-webpack5@npm:8.6.17" dependencies: - "@storybook/core-webpack": "npm:8.6.15" + "@storybook/core-webpack": "npm:8.6.17" "@types/semver": "npm:^7.3.4" browser-assert: "npm:^1.2.1" case-sensitive-paths-webpack-plugin: "npm:^2.4.0" @@ -11225,20 +11225,20 @@ __metadata: webpack-hot-middleware: "npm:^2.25.1" webpack-virtual-modules: "npm:^0.6.0" peerDependencies: - storybook: ^8.6.15 + storybook: ^8.6.17 peerDependenciesMeta: typescript: optional: true - checksum: 10/8e8e816dbfa83e2fd56401a65a1551ecbaa658db54b2270e90560710c024878a09df48e1a4e114eacc75ff5c924cb7a277f4e3501ae4744c6a2742ab55135afe + checksum: 10/f37bba5c95daad1eca99387622112b6e85d34d7177eaf23c7fd6dd01bca1c0956799a28c510a96d267214e367a2583c3bb839a2053f4d84e0f84604178a07f4c languageName: node linkType: hard -"@storybook/components@npm:8.6.15": - version: 8.6.15 - resolution: "@storybook/components@npm:8.6.15" +"@storybook/components@npm:8.6.17": + version: 8.6.17 + resolution: "@storybook/components@npm:8.6.17" peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - checksum: 10/350075ffe67cfc307c0f8f9b6568c5f25e97a2ffb55d723b88c16a3f3ad6d687c312580c4b4d3cbf8ef245a30723797d89be20a44f6c500f79e7b173b0cb79d0 + checksum: 10/bd92739837e9c1ea0e32fb7d7bd270a3a7025722e0a95f5e7c4956b0d6b29a684f38196a9434bac1054ebb17a0eb79872bd1ed1a701ff6888911b57ba4f9a62f languageName: node linkType: hard @@ -11260,22 +11260,22 @@ __metadata: languageName: node linkType: hard -"@storybook/core-webpack@npm:8.6.15": - version: 8.6.15 - resolution: "@storybook/core-webpack@npm:8.6.15" +"@storybook/core-webpack@npm:8.6.17": + version: 8.6.17 + resolution: "@storybook/core-webpack@npm:8.6.17" dependencies: ts-dedent: "npm:^2.0.0" peerDependencies: - storybook: ^8.6.15 - checksum: 10/494110266d622be00a61cd160a4e799ed4c4c447ab04699d497de7c3d95ee800f212680c31722b8938819577853e095af545f7853e7ce4cf751d2a9e9b4c7f12 + storybook: ^8.6.17 + checksum: 10/29e64de67dac9fb48a3d53b65de627d09787df02dfee678c896d655fadf22ea5e0778fcdb2272e80c2f100e3e2a3d4a753739be34f00a564308b45c119e8269d languageName: node linkType: hard -"@storybook/core@npm:8.6.15": - version: 8.6.15 - resolution: "@storybook/core@npm:8.6.15" +"@storybook/core@npm:8.6.17": + version: 8.6.17 + resolution: "@storybook/core@npm:8.6.17" dependencies: - "@storybook/theming": "npm:8.6.15" + "@storybook/theming": "npm:8.6.17" better-opn: "npm:^3.0.2" browser-assert: "npm:^1.2.1" esbuild: "npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0" @@ -11291,18 +11291,18 @@ __metadata: peerDependenciesMeta: prettier: optional: true - checksum: 10/d268d6fa00c38b35e5c363ee33779c2e087ab8e4681e0e205baa2fdb2780ea9feda3c9f6db35d60092778878d2782b2093c744bdf1af173c5688c3e1e0e960ac + checksum: 10/2749dfed1219878f9959b9944533a0a428cae22482130a3d45ceffb2186d259670d79fc6ee4b3e334ba168f692308ae7b95d55fa2afba6ccbc6f6464a3cf0fd6 languageName: node linkType: hard -"@storybook/csf-plugin@npm:8.6.15": - version: 8.6.15 - resolution: "@storybook/csf-plugin@npm:8.6.15" +"@storybook/csf-plugin@npm:8.6.17": + version: 8.6.17 + resolution: "@storybook/csf-plugin@npm:8.6.17" dependencies: unplugin: "npm:^1.3.1" peerDependencies: - storybook: ^8.6.15 - checksum: 10/c544089d7a675d19e226e331a791db6c2e2cede893a2955578959086e7c35e846319a9080d91968ec81a2115e2c396fe3d76d2f50d74baca098dd5b03ad939b0 + storybook: ^8.6.17 + checksum: 10/ca5bd82ff5773c133a5b5e0710d0ffc1b6c72ab4677ec49999ad15fb08677227ab05ffae4d58be3817f9e2d61f33ee3c41e20660e4d0db14e1c7d43326ded92d languageName: node linkType: hard @@ -11332,24 +11332,24 @@ __metadata: languageName: node linkType: hard -"@storybook/instrumenter@npm:8.6.15": - version: 8.6.15 - resolution: "@storybook/instrumenter@npm:8.6.15" +"@storybook/instrumenter@npm:8.6.17": + version: 8.6.17 + resolution: "@storybook/instrumenter@npm:8.6.17" dependencies: "@storybook/global": "npm:^5.0.0" "@vitest/utils": "npm:^2.1.1" peerDependencies: - storybook: ^8.6.15 - checksum: 10/5f56da838ccd47b9a262e5aa54a5574985295994fb1b6fb165d4c3a66d96367588fa93c60fe55892ffa450ea3e41e0abcc313a12a873d5b4be7625a1c3b0d883 + storybook: ^8.6.17 + checksum: 10/ec4ae8e5f4b08931193722a86a07fae065c5db195d7eb9a766f301a6307ecbe8e6f7f104fcd1f03bc3a7283d379e7d960f621e331277fdc5444750ae02b3e186 languageName: node linkType: hard -"@storybook/manager-api@npm:8.6.15": - version: 8.6.15 - resolution: "@storybook/manager-api@npm:8.6.15" +"@storybook/manager-api@npm:8.6.17": + version: 8.6.17 + resolution: "@storybook/manager-api@npm:8.6.17" peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - checksum: 10/0b378fc657830c48b7c304ea915883161e8271d76b8f90b90e86e4260f6e513ae9f0f87eee6cad057a6c38e8a9faf7bac0ba169e586aabce2b4e48d36a0d27c3 + checksum: 10/40f34b34c8e4f9d5e5495c11a139ccd3c30135777d7d062b8d8a22e40e4ded9073844952919417a1d4b3895ebb4c67fb545aef933b78a4cc85240b027778f8f6 languageName: node linkType: hard @@ -11371,55 +11371,55 @@ __metadata: languageName: node linkType: hard -"@storybook/preact-webpack5@npm:^8.6.15": - version: 8.6.15 - resolution: "@storybook/preact-webpack5@npm:8.6.15" +"@storybook/preact-webpack5@npm:^8.6.17": + version: 8.6.17 + resolution: "@storybook/preact-webpack5@npm:8.6.17" dependencies: - "@storybook/builder-webpack5": "npm:8.6.15" - "@storybook/preact": "npm:8.6.15" - "@storybook/preset-preact-webpack": "npm:8.6.15" + "@storybook/builder-webpack5": "npm:8.6.17" + "@storybook/preact": "npm:8.6.17" + "@storybook/preset-preact-webpack": "npm:8.6.17" peerDependencies: preact: ">=10.0.0" - storybook: ^8.6.15 - checksum: 10/87ed12605a9b46b783fe3e331a09e3b2406aefc6bddaddebb1e1f0c83e807dabf731243d9733237f1f499b64f5eebe3c8b5c9d3dc72e580d9ddfd6a8bae73c62 + storybook: ^8.6.17 + checksum: 10/d628637d0142c09b243b5a06e2e02e635a68b2a8c88fe4a714b533b612fb19527b810b830648c21061ab66f867c00dca3061b39ee5905f3dd4ae1af493a600b0 languageName: node linkType: hard -"@storybook/preact@npm:8.6.15, @storybook/preact@npm:^8.6.15": - version: 8.6.15 - resolution: "@storybook/preact@npm:8.6.15" +"@storybook/preact@npm:8.6.17, @storybook/preact@npm:^8.6.17": + version: 8.6.17 + resolution: "@storybook/preact@npm:8.6.17" dependencies: - "@storybook/components": "npm:8.6.15" + "@storybook/components": "npm:8.6.17" "@storybook/global": "npm:^5.0.0" - "@storybook/manager-api": "npm:8.6.15" - "@storybook/preview-api": "npm:8.6.15" - "@storybook/theming": "npm:8.6.15" + "@storybook/manager-api": "npm:8.6.17" + "@storybook/preview-api": "npm:8.6.17" + "@storybook/theming": "npm:8.6.17" ts-dedent: "npm:^2.0.0" peerDependencies: preact: ^8.0.0||^10.0.0 - storybook: ^8.6.15 - checksum: 10/5b30ca8d43f0dd4ef643c03124c069c4e1e616e99b140a2ee941c4347398e556627e3e2745b41532882521838046946bd5cba5bc1f7fe534f721b8e14f16afc7 + storybook: ^8.6.17 + checksum: 10/8d2fdff72a90cce1dafc8f356df81e118a3c128c0969b17c93380c5b5601f81ce1ce1fb38bab314cbb42c21c30bfd3915b3ea20c884d7981f5448ef094d6052c languageName: node linkType: hard -"@storybook/preset-preact-webpack@npm:8.6.15": - version: 8.6.15 - resolution: "@storybook/preset-preact-webpack@npm:8.6.15" +"@storybook/preset-preact-webpack@npm:8.6.17": + version: 8.6.17 + resolution: "@storybook/preset-preact-webpack@npm:8.6.17" dependencies: - "@storybook/core-webpack": "npm:8.6.15" + "@storybook/core-webpack": "npm:8.6.17" peerDependencies: preact: ^8.0.0||^10.0.0 - storybook: ^8.6.15 - checksum: 10/2894515c50625c58e8a06dd8c93bcda4e2015016921d48dc336e1c362885efe2deccf804699b2a554c42258d1d3b026efca7872394c8990a399e27d95d7620a7 + storybook: ^8.6.17 + checksum: 10/ebed6cb1dd9cbd89dc3dae95ed5c8dac268b8d9b8bdb99866f3558cb29c08d336af9f8e058d0d775a5b738b5f200320f45a49e0d4384e03b9ecbbf3e9928ebd6 languageName: node linkType: hard -"@storybook/preset-react-webpack@npm:8.6.15": - version: 8.6.15 - resolution: "@storybook/preset-react-webpack@npm:8.6.15" +"@storybook/preset-react-webpack@npm:8.6.17": + version: 8.6.17 + resolution: "@storybook/preset-react-webpack@npm:8.6.17" dependencies: - "@storybook/core-webpack": "npm:8.6.15" - "@storybook/react": "npm:8.6.15" + "@storybook/core-webpack": "npm:8.6.17" + "@storybook/react": "npm:8.6.17" "@storybook/react-docgen-typescript-plugin": "npm:1.0.6--canary.9.0c3f3b7.0" "@types/semver": "npm:^7.3.4" find-up: "npm:^5.0.0" @@ -11432,20 +11432,20 @@ __metadata: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.6.15 + storybook: ^8.6.17 peerDependenciesMeta: typescript: optional: true - checksum: 10/e3c2bf792a3dc051f27f8723b1b2c9671ecda57f6f20950fe30d65c17ff3751c382ffcf606a2efd85d6b5266356e0b62b57e23560b44cc497d78335dd5ae6462 + checksum: 10/47449cad350dc9d8ef8957571c8a5df47ce83bfeedc08e895043d43702588eef0f6c1e1610efa39fea7e38677c817e4f8f63f70a9145130d015e75713c125b84 languageName: node linkType: hard -"@storybook/preview-api@npm:8.6.15, @storybook/preview-api@npm:^8.6.15": - version: 8.6.15 - resolution: "@storybook/preview-api@npm:8.6.15" +"@storybook/preview-api@npm:8.6.17, @storybook/preview-api@npm:^8.6.17": + version: 8.6.17 + resolution: "@storybook/preview-api@npm:8.6.17" peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - checksum: 10/70df6006ce7340371e207f7f077d52c8684743a64f172d54f3e99fb6d2e190f46a4321c40be7ea813b9bd407530f971372156b3dd6ba1f61681f7b3acc498010 + checksum: 10/cc5141088215f87a77a3b92aeecf08b921e64a18b7723839d85681e0145ac8089332395ef7d925a2663b2a335147ac65fe6dc5b27257402a0d9aabf72566aeba languageName: node linkType: hard @@ -11467,58 +11467,58 @@ __metadata: languageName: node linkType: hard -"@storybook/react-dom-shim@npm:8.6.15": - version: 8.6.15 - resolution: "@storybook/react-dom-shim@npm:8.6.15" +"@storybook/react-dom-shim@npm:8.6.17": + version: 8.6.17 + resolution: "@storybook/react-dom-shim@npm:8.6.17" peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.6.15 - checksum: 10/7625cfa2a385315851cd0aeb36f517e75679262c2942a5a53dd2909483b62602b9e5157ba51a8e464eb70112a4916278fba63b05178beef3d2ff493a464a2c64 + storybook: ^8.6.17 + checksum: 10/8cc54d9298bd3b649317b28bdf4a0d3b832350ca47f47a35f273e4ecbec3bca77607f42c223ab9ae24e242831983af2a02e4dbfd07a2f232c73a9ab87290bb9a languageName: node linkType: hard -"@storybook/react-webpack5@npm:^8.6.15": - version: 8.6.15 - resolution: "@storybook/react-webpack5@npm:8.6.15" +"@storybook/react-webpack5@npm:^8.6.17": + version: 8.6.17 + resolution: "@storybook/react-webpack5@npm:8.6.17" dependencies: - "@storybook/builder-webpack5": "npm:8.6.15" - "@storybook/preset-react-webpack": "npm:8.6.15" - "@storybook/react": "npm:8.6.15" + "@storybook/builder-webpack5": "npm:8.6.17" + "@storybook/preset-react-webpack": "npm:8.6.17" + "@storybook/react": "npm:8.6.17" peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.6.15 + storybook: ^8.6.17 typescript: ">= 4.2.x" peerDependenciesMeta: typescript: optional: true - checksum: 10/0468aa5ca0ed76170cbdef6b2a211625e64b74a8012aabeca03d38c8fa8f34013b2068f68dd173bb5db32aa9ccaa87c923344a506c561ad98d0afb0f88856b79 + checksum: 10/feefac4e221f0f59f00a2b5763dcf727cc7d2c9ab2e6a92498a4ba7685e22cd746615153b564ee47b77259b3883a43bfa6628ad4f2584c26c3bbd8e81a211c23 languageName: node linkType: hard -"@storybook/react@npm:8.6.15, @storybook/react@npm:^8.6.15": - version: 8.6.15 - resolution: "@storybook/react@npm:8.6.15" +"@storybook/react@npm:8.6.17, @storybook/react@npm:^8.6.17": + version: 8.6.17 + resolution: "@storybook/react@npm:8.6.17" dependencies: - "@storybook/components": "npm:8.6.15" + "@storybook/components": "npm:8.6.17" "@storybook/global": "npm:^5.0.0" - "@storybook/manager-api": "npm:8.6.15" - "@storybook/preview-api": "npm:8.6.15" - "@storybook/react-dom-shim": "npm:8.6.15" - "@storybook/theming": "npm:8.6.15" + "@storybook/manager-api": "npm:8.6.17" + "@storybook/preview-api": "npm:8.6.17" + "@storybook/react-dom-shim": "npm:8.6.17" + "@storybook/theming": "npm:8.6.17" peerDependencies: - "@storybook/test": 8.6.15 + "@storybook/test": 8.6.17 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta - storybook: ^8.6.15 + storybook: ^8.6.17 typescript: ">= 4.2.x" peerDependenciesMeta: "@storybook/test": optional: true typescript: optional: true - checksum: 10/a7e33bd68e25bf04fac77c9573c51d76d556c51a4329e84dae2a4e2afd39950e4d3ab9a49787ac465e9e81522b40ea12dc86cd0b60a2aab1ee2f88a753bc9b43 + checksum: 10/b60591a429ef638894869ae33f7d261ee72c13d7bc1ee1671b68e0e1b068c16be4273b3a79f7bbd5c445b8b46cc8d0a5930b23a520e52cfaeb2fbd8258aac6ef languageName: node linkType: hard @@ -11553,29 +11553,29 @@ __metadata: languageName: node linkType: hard -"@storybook/test@npm:8.6.15, @storybook/test@npm:^8.6.15": - version: 8.6.15 - resolution: "@storybook/test@npm:8.6.15" +"@storybook/test@npm:8.6.17, @storybook/test@npm:^8.6.17": + version: 8.6.17 + resolution: "@storybook/test@npm:8.6.17" dependencies: "@storybook/global": "npm:^5.0.0" - "@storybook/instrumenter": "npm:8.6.15" + "@storybook/instrumenter": "npm:8.6.17" "@testing-library/dom": "npm:10.4.0" "@testing-library/jest-dom": "npm:6.5.0" "@testing-library/user-event": "npm:14.5.2" "@vitest/expect": "npm:2.0.5" "@vitest/spy": "npm:2.0.5" peerDependencies: - storybook: ^8.6.15 - checksum: 10/5f54b9ef1910011813059708f6d2c32f4db5f4e719de9de2a7f2280efe7535c45c36668cf3e346b4788f4bbe6b71a3ab169ccdc17f0d8e634443d92849b46bf1 + storybook: ^8.6.17 + checksum: 10/23438b233b16472db4ab4f39344ae8b1f47cbad4973465ba7cd94bd2cfe09fe0c29bc4e9805fd2674deda46a43b5ffcb55c4aaed8ba02b4abd548f69ddf88f87 languageName: node linkType: hard -"@storybook/theming@npm:8.6.15, @storybook/theming@npm:^8.6.15": - version: 8.6.15 - resolution: "@storybook/theming@npm:8.6.15" +"@storybook/theming@npm:8.6.17, @storybook/theming@npm:^8.6.17": + version: 8.6.17 + resolution: "@storybook/theming@npm:8.6.17" peerDependencies: storybook: ^8.2.0 || ^8.3.0-0 || ^8.4.0-0 || ^8.5.0-0 || ^8.6.0-0 - checksum: 10/f02760831a13d7af9dbfeb6feea949f4c13c897861cbc75253a6776d133891567889c8d99c7e91a99124d0772c3bde1f978984c83b19117d1d7c908ed7eb8409 + checksum: 10/c2041070fba5eaabda3318cf259910af4e1e14a14a961345c2ec49322d95fb2f36cdf4d5a3e3d045f13751538fa8ac68b366053d37cd32de3ac80b786f2c97c5 languageName: node linkType: hard @@ -33877,11 +33877,11 @@ __metadata: languageName: node linkType: hard -"storybook@npm:^8.6.15": - version: 8.6.15 - resolution: "storybook@npm:8.6.15" +"storybook@npm:^8.6.17": + version: 8.6.17 + resolution: "storybook@npm:8.6.17" dependencies: - "@storybook/core": "npm:8.6.15" + "@storybook/core": "npm:8.6.17" peerDependencies: prettier: ^2 || ^3 peerDependenciesMeta: @@ -33891,7 +33891,7 @@ __metadata: getstorybook: ./bin/index.cjs sb: ./bin/index.cjs storybook: ./bin/index.cjs - checksum: 10/15762c79ec8444a46bc14cddfadbdd54dfd379828acd38555887a246c01e7c9ebb61e4eafafe04efb3ddf6278fb47035216e7f7d9f94fc205da148870173abdf + checksum: 10/b8a2fe00d3454e4cbeeae684a57c3c4cf8444e9780e5d8ab73f8f1a1df0260e746b0e3fff67458c039f0707f14e3f7870670a157834985b769166756f8a75bfc languageName: node linkType: hard From bab0fbfeffd5df5cb46381ff86bfb862f0ee0252 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 19:28:49 -0300 Subject: [PATCH 090/108] chore(deps): bump actions/download-artifact from 7 to 8 (#39123) Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-code-check.yml | 2 +- .github/workflows/ci-test-e2e.yml | 4 ++-- .github/workflows/ci-test-storybook.yml | 2 +- .github/workflows/ci-test-unit.yml | 2 +- .github/workflows/ci.yml | 14 +++++++------- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci-code-check.yml b/.github/workflows/ci-code-check.yml index 5555cead5e580..2b522a1c32565 100644 --- a/.github/workflows/ci-code-check.yml +++ b/.github/workflows/ci-code-check.yml @@ -44,7 +44,7 @@ jobs: - uses: rharkor/caching-for-turbo@v1.8 - name: Restore packages build - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: packages-build path: /tmp diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml index c3c3c9068de4b..c3bb2859ef2a5 100644 --- a/.github/workflows/ci-test-e2e.yml +++ b/.github/workflows/ci-test-e2e.yml @@ -126,7 +126,7 @@ jobs: - uses: rharkor/caching-for-turbo@v1.8 - name: Restore packages build - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: packages-build path: /tmp @@ -138,7 +138,7 @@ jobs: # Download Docker images from build artifacts - name: Download Docker images - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 if: github.event.pull_request.head.repo.full_name != github.repository && github.event_name != 'release' && github.ref != 'refs/heads/develop' with: pattern: ${{ inputs.release == 'ce' && 'docker-image-rocketchat-amd64-coverage' || 'docker-image-*-amd64-coverage' }} diff --git a/.github/workflows/ci-test-storybook.yml b/.github/workflows/ci-test-storybook.yml index 0606607121db3..f1442829c2438 100644 --- a/.github/workflows/ci-test-storybook.yml +++ b/.github/workflows/ci-test-storybook.yml @@ -38,7 +38,7 @@ jobs: - uses: rharkor/caching-for-turbo@v1.8 - name: Restore packages build - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: packages-build path: /tmp diff --git a/.github/workflows/ci-test-unit.yml b/.github/workflows/ci-test-unit.yml index b278eb9cd7a8a..3989da7c8a9df 100644 --- a/.github/workflows/ci-test-unit.yml +++ b/.github/workflows/ci-test-unit.yml @@ -42,7 +42,7 @@ jobs: - uses: rharkor/caching-for-turbo@v1.8 - name: Restore packages build - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: packages-build path: /tmp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a7eaf612bde45..0bd8fd4969e63 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -298,7 +298,7 @@ jobs: - uses: actions/checkout@v6 - name: Restore packages build - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: packages-build path: /tmp @@ -397,7 +397,7 @@ jobs: - name: Download manifests if: github.actor != 'dependabot[bot]' && (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: pattern: manifests-* path: /tmp/manifests @@ -603,7 +603,7 @@ jobs: - uses: rharkor/caching-for-turbo@v1.8 - name: Restore turbo build - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 continue-on-error: true with: name: turbo-build @@ -627,7 +627,7 @@ jobs: # Download Docker images from build artifacts - name: Download Docker images - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 if: github.event.pull_request.head.repo.full_name != github.repository && github.event_name != 'release' && github.ref != 'refs/heads/develop' with: pattern: 'docker-image-rocketchat-amd64-coverage' @@ -693,7 +693,7 @@ jobs: node-version: ${{ needs.release-versions.outputs.node-version }} - name: Restore coverage folder - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: pattern: coverage-* path: /tmp/coverage @@ -784,7 +784,7 @@ jobs: ref: ${{ github.ref }} - name: Restore build - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: name: build-production path: /tmp/build @@ -855,7 +855,7 @@ jobs: - name: Download manifests if: github.actor != 'dependabot[bot]' && (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'release' || github.ref == 'refs/heads/develop') - uses: actions/download-artifact@v7 + uses: actions/download-artifact@v8 with: pattern: manifests-* path: /tmp/manifests From 0f8317a05dee7a0abce134154457e02d71846401 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 27 Feb 2026 19:28:59 -0300 Subject: [PATCH 091/108] chore(deps): bump actions/setup-node from 6.1.0 to 6.2.0 (#38195) Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- .github/workflows/update-version-durability.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0bd8fd4969e63..f13c8c2c4c9a3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -688,7 +688,7 @@ jobs: - uses: actions/checkout@v6 - name: Use Node.js - uses: actions/setup-node@v6.1.0 + uses: actions/setup-node@v6.2.0 with: node-version: ${{ needs.release-versions.outputs.node-version }} diff --git a/.github/workflows/update-version-durability.yml b/.github/workflows/update-version-durability.yml index 00e1ee8125ad6..46f9fbb0c3b0e 100644 --- a/.github/workflows/update-version-durability.yml +++ b/.github/workflows/update-version-durability.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@v6 - name: Use Node.js - uses: actions/setup-node@v6.1.0 + uses: actions/setup-node@v6.2.0 with: node-version: 22.16.0 From dbe69aff8e001e6f3ee1553cab11ea55d3042e6d Mon Sep 17 00:00:00 2001 From: Tushar Grover <140243719+tushargr0ver@users.noreply.github.com> Date: Sat, 28 Feb 2026 03:01:04 +0530 Subject: [PATCH 092/108] fix(ux): Show full channel name on hover (#38320) Co-authored-by: Douglas Fabris <devfabris@gmail.com> --- apps/meteor/client/sidebar/Item/Condensed.tsx | 2 +- apps/meteor/client/sidebar/Item/Extended.tsx | 2 +- apps/meteor/client/sidebar/Item/Medium.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/meteor/client/sidebar/Item/Condensed.tsx b/apps/meteor/client/sidebar/Item/Condensed.tsx index 3c737cc30e7c9..1f9f522d2844e 100644 --- a/apps/meteor/client/sidebar/Item/Condensed.tsx +++ b/apps/meteor/client/sidebar/Item/Condensed.tsx @@ -24,7 +24,7 @@ const Condensed = ({ icon, title, avatar, actions, unread, menu, badges, ...prop const handlePointerEnter = () => setMenuVisibility(true); return ( - <SidebarV2Item {...props} onFocus={handleFocus} onPointerEnter={handlePointerEnter}> + <SidebarV2Item title={title} {...props} onFocus={handleFocus} onPointerEnter={handlePointerEnter}> {avatar && <SidebarV2ItemAvatarWrapper>{avatar}</SidebarV2ItemAvatarWrapper>} {icon} <SidebarV2ItemTitle unread={unread}>{title}</SidebarV2ItemTitle> diff --git a/apps/meteor/client/sidebar/Item/Extended.tsx b/apps/meteor/client/sidebar/Item/Extended.tsx index ce9faea597849..fde211d74f0d0 100644 --- a/apps/meteor/client/sidebar/Item/Extended.tsx +++ b/apps/meteor/client/sidebar/Item/Extended.tsx @@ -55,7 +55,7 @@ const Extended = ({ const handlePointerEnter = () => setMenuVisibility(true); return ( - <SidebarV2Item href={href} selected={selected} {...props} onFocus={handleFocus} onPointerEnter={handlePointerEnter}> + <SidebarV2Item title={title} href={href} selected={selected} {...props} onFocus={handleFocus} onPointerEnter={handlePointerEnter}> {avatar && <SidebarV2ItemAvatarWrapper>{avatar}</SidebarV2ItemAvatarWrapper>} <SidebarV2ItemCol> <SidebarV2ItemRow> diff --git a/apps/meteor/client/sidebar/Item/Medium.tsx b/apps/meteor/client/sidebar/Item/Medium.tsx index f26d64b983891..3492d4e55de34 100644 --- a/apps/meteor/client/sidebar/Item/Medium.tsx +++ b/apps/meteor/client/sidebar/Item/Medium.tsx @@ -23,7 +23,7 @@ const Medium = ({ icon, title, avatar, actions, badges, unread, menu, ...props } const handlePointerEnter = () => setMenuVisibility(true); return ( - <SidebarV2Item {...props} onFocus={handleFocus} onPointerEnter={handlePointerEnter}> + <SidebarV2Item title={title} {...props} onFocus={handleFocus} onPointerEnter={handlePointerEnter}> <SidebarV2ItemAvatarWrapper>{avatar}</SidebarV2ItemAvatarWrapper> {icon} <SidebarV2ItemTitle unread={unread}>{title}</SidebarV2ItemTitle> From a5aac0c160aaa85473bcdf79eb698bf06569200d Mon Sep 17 00:00:00 2001 From: Tasso Evangelista <tasso.evangelista@rocket.chat> Date: Fri, 27 Feb 2026 20:51:17 -0300 Subject: [PATCH 093/108] refactor(uikit-playground): Adapt to linting changes (#39162) --- apps/uikit-playground/.prettierrc | 5 - apps/uikit-playground/index.html | 22 +- apps/uikit-playground/package.json | 114 +-- apps/uikit-playground/src/App.tsx | 94 +- .../src/Components/CodeEditor/BlockEditor.tsx | 89 +- .../CodeEditor/Extensions/Extensions.ts | 22 +- .../CodeEditor/Extensions/HighlightStyle.ts | 14 +- .../CodeEditor/Extensions/basicSetup.ts | 78 +- .../CodeEditor/Extensions/payloadLinter.ts | 101 ++- .../Components/CodeEditor/Extensions/theme.ts | 50 +- .../CodeEditor/Parser/parsePayload.ts | 12 +- .../Components/CodeEditor/PreviewEditor.tsx | 31 +- .../src/Components/CodeEditor/index.tsx | 95 +-- .../ComponentSideBar/ScrollableSideBar.tsx | 29 +- .../Components/ComponentSideBar/SideBar.tsx | 56 +- .../Components/ComponentSideBar/SliderBtn.tsx | 185 ++-- .../CreateNewScreenContainer.tsx | 180 ++-- .../CreateNewScreen/ScreenThumbnail.tsx | 108 ++- .../Components/Draggable/DraggableList.tsx | 47 +- .../Draggable/DraggableListItem.tsx | 38 +- .../src/Components/DropDown/DropDown.tsx | 41 +- .../src/Components/DropDown/Items.tsx | 101 +-- .../src/Components/DropDown/ItemsIcon.tsx | 32 +- .../src/Components/DropDown/itemsStyle.ts | 78 +- .../src/Components/DropDown/types.ts | 14 +- .../FlowContainer/ConnectionLine.tsx | 49 +- .../ControlButtons/ControlButtons.tsx | 2 +- .../FlowContainer/ControlButtons/index.ts | 2 +- .../FlowContainer/FlowContainer.tsx | 184 ++-- .../UIKitWrapper/UIKitWrapper.tsx | 65 +- .../FlowContainer/UIKitWrapper/index.ts | 2 +- .../src/Components/FlowContainer/utils.ts | 44 +- .../HomeContainer/HomeContainer.tsx | 54 +- .../ProjectsList/ProjectsList.tsx | 51 +- .../ProjectsList/ProjectsThumbnail.tsx | 149 ++-- .../NavBar/BurgerIcon/BurgerIcon.tsx | 26 +- .../src/Components/NavBar/BurgerIcon/Line.tsx | 73 +- .../Components/NavBar/BurgerIcon/Wrapper.tsx | 34 +- .../src/Components/NavBar/Divider.tsx | 3 +- .../src/Components/NavBar/Logo.tsx | 18 +- .../src/Components/NavBar/NavBar.tsx | 185 ++-- .../src/Components/NavBar/RightNavBtn.tsx | 24 +- .../Components/PersistStore/PersistStore.tsx | 31 +- .../Components/Preview/Display/Display.tsx | 17 +- .../Preview/Display/Surface/BannerSurface.tsx | 4 +- .../Display/Surface/ContextualBarSurface.tsx | 110 ++- .../Display/Surface/MessageSurface.tsx | 78 +- .../Preview/Display/Surface/ModalSurface.tsx | 58 +- .../Preview/Display/Surface/Reorder.ts | 14 +- .../Preview/Display/Surface/Surface.tsx | 165 ++-- .../Preview/Display/Surface/SurfaceRender.tsx | 28 +- .../Preview/Display/Surface/constant.ts | 10 +- .../UiKitElementWrapper/DeleteElementBtn.tsx | 26 +- .../UiKitElementWrapper.scss | 7 +- .../UiKitElementWrapper.tsx | 4 +- .../Preview/Editor/ActionBlockEditor.tsx | 31 +- .../Preview/Editor/ActionPreviewEditor.tsx | 31 +- .../Components/Preview/Editor/EditorPanel.tsx | 150 ++-- .../Components/Preview/NavPanel/NavPanel.tsx | 87 +- .../src/Components/Preview/Preview.tsx | 53 +- .../SplitPlaneContainer.tsx | 68 +- .../src/Components/Preview/Wrapper.tsx | 61 +- .../PrototypeRender/PrototypeRender.tsx | 119 ++- .../PtototypeContainer/PrototypeContainer.tsx | 63 +- .../Components/PtototypeContainer/index.ts | 2 +- .../RenderPayload/RenderPayload.tsx | 46 +- .../src/Components/Routes/HomeLayout.tsx | 12 +- .../Routes/ProjectSpecificLayout.tsx | 30 +- .../src/Components/Routes/ProtectedLayout.tsx | 12 +- .../ScreenThumbnail/CreateNewScreenButton.tsx | 44 +- .../ScreenThumbnail/EditMenu/EditMenu.tsx | 147 ++-- .../ScreenThumbnail/EditMenu/index.ts | 2 +- .../EditableLabel/EditableLabel.tsx | 52 +- .../ScreenThumbnailWrapper.tsx | 48 +- .../Components/ScreenThumbnail/Thumbnail.tsx | 57 +- .../SurfaceSelect/SurfaceSelect.tsx | 33 +- .../src/Components/SurfaceSelect/options.ts | 8 +- .../Templates/Container/Container.tsx | 9 +- .../Templates/Container/Payload.tsx | 114 ++- .../Templates/Container/Section.tsx | 36 +- .../src/Components/Templates/Templates.tsx | 85 +- .../src/Components/ToggleTabs/index.tsx | 49 +- .../src/Components/navMenu/Menu/MenuItem.tsx | 57 +- .../src/Components/navMenu/Menu/Wrapper.tsx | 26 +- .../src/Components/navMenu/Menu/index.tsx | 143 ++-- .../src/Components/navMenu/NavMenu.tsx | 67 +- .../src/Context/action/actionPreviewAction.ts | 8 +- .../src/Context/action/activeProjectAction.ts | 10 +- .../src/Context/action/activeScreenAction.ts | 10 +- .../Context/action/createNewProjectAction.ts | 12 +- .../Context/action/createNewScreenAction.ts | 10 +- .../src/Context/action/deleteProjectAction.ts | 12 +- .../src/Context/action/deleteScreenAction.ts | 10 +- .../Context/action/duplicateProjectAction.ts | 15 +- .../Context/action/duplicateScreenAction.ts | 15 +- .../Context/action/editorTabsToggleAction.ts | 10 +- .../src/Context/action/isMobileAction.ts | 10 +- .../src/Context/action/isTabletAction.ts | 10 +- .../src/Context/action/navMenuToggleAction.ts | 10 +- .../action/openCreateNewScreenAction.ts | 10 +- .../Context/action/previewTabsToggleAction.ts | 10 +- .../src/Context/action/renameProjectAction.ts | 16 +- .../src/Context/action/renameScreenAction.ts | 16 +- .../src/Context/action/sidebarToggleAction.ts | 10 +- .../src/Context/action/surfaceAction.ts | 10 +- .../src/Context/action/tabsToggleAction.ts | 10 +- .../Context/action/templatesToggleAction.ts | 10 +- .../Context/action/updateFlowEdgesAction.ts | 15 +- .../action/updateNodesAndViewPortAction.ts | 19 +- .../src/Context/action/updatePayloadAction.ts | 22 +- .../src/Context/action/userAction.ts | 8 +- .../src/Context/createCtx.tsx | 28 +- apps/uikit-playground/src/Context/index.tsx | 6 +- .../src/Context/initialState.ts | 178 ++-- apps/uikit-playground/src/Context/reducer.ts | 597 +++++++------ .../src/Pages/FlowDiagram.tsx | 15 +- apps/uikit-playground/src/Pages/Home.tsx | 15 +- .../uikit-playground/src/Pages/Playground.tsx | 53 +- apps/uikit-playground/src/Pages/Prototype.tsx | 11 +- .../src/Pages/SignInSignUp.tsx | 112 ++- .../src/Payload/action/checkbox.ts | 72 +- .../src/Payload/action/radioButton.ts | 68 +- .../src/Payload/action/timePicker.ts | 34 +- .../src/Payload/action/toggleSwitch.ts | 72 +- .../src/Payload/actionBlock/BlocksTree.ts | 688 ++++++++------- .../src/Payload/actionBlock/action/button.ts | 272 +++--- .../Payload/actionBlock/action/datePicker.ts | 34 +- .../src/Payload/actionBlock/action/image.ts | 16 +- .../src/Payload/actionBlock/action/input.ts | 64 +- .../Payload/actionBlock/action/linearScale.ts | 28 +- .../src/Payload/actionBlock/action/menu.ts | 58 +- .../actionBlock/action/staticSelect.ts | 136 +-- .../src/Payload/actionBlock/context/index.ts | 96 +-- .../src/Payload/actionBlock/divider/index.ts | 6 +- .../src/Payload/actionBlock/image/index.ts | 30 +- .../src/Payload/actionBlock/infoCard/index.ts | 7 +- .../Payload/actionBlock/input/datePicker.ts | 40 +- .../src/Payload/actionBlock/input/input.ts | 84 +- .../Payload/actionBlock/input/linearScale.ts | 34 +- .../Payload/actionBlock/input/staticSelect.ts | 148 ++-- .../src/Payload/actionBlock/preview/index.ts | 216 ++--- .../src/Payload/actionBlock/section/button.ts | 266 +++--- .../Payload/actionBlock/section/datePicker.ts | 38 +- .../src/Payload/actionBlock/section/image.ts | 24 +- .../src/Payload/actionBlock/section/menu.ts | 110 +-- .../src/Payload/actionBlock/section/text.ts | 90 +- .../src/Payload/actionPreview/container.ts | 40 +- .../actionPreview/generateActionPreview.ts | 62 +- .../src/Payload/callout/index.ts | 64 +- apps/uikit-playground/src/Payload/index.ts | 2 - .../src/Payload/tabNavigation/disabled.ts | 74 +- .../src/Payload/tabNavigation/index.ts | 6 +- .../src/Payload/tabNavigation/plain.ts | 72 +- .../src/Payload/tabNavigation/selected.ts | 74 +- apps/uikit-playground/src/Routes/Routes.ts | 14 +- apps/uikit-playground/src/hooks/useAuth.tsx | 59 +- .../src/hooks/useCodeMirror.ts | 124 +-- .../src/hooks/useFormatCodeMirrorValue.ts | 43 +- .../src/hooks/useHorizontalScroll.ts | 37 +- .../src/hooks/useNodesAndEdges.ts | 123 ++- apps/uikit-playground/src/main.tsx | 18 +- apps/uikit-playground/src/module.d.ts | 8 +- .../src/utils/codePrettier.ts | 18 +- .../uikit-playground/src/utils/filterEdges.ts | 14 +- apps/uikit-playground/src/utils/formatDate.ts | 8 +- apps/uikit-playground/src/utils/getDate.ts | 6 +- .../src/utils/persistStore.ts | 4 +- apps/uikit-playground/src/utils/templates.ts | 804 +++++++++--------- apps/uikit-playground/tsconfig.json | 4 +- apps/uikit-playground/vite.config.ts | 24 +- eslint.config.mjs | 47 +- package.json | 1 - packages/eslint-config/index.js | 12 +- yarn.lock | 10 - 174 files changed, 5097 insertions(+), 5614 deletions(-) delete mode 100644 apps/uikit-playground/.prettierrc delete mode 100644 apps/uikit-playground/src/Payload/index.ts diff --git a/apps/uikit-playground/.prettierrc b/apps/uikit-playground/.prettierrc deleted file mode 100644 index e97ea1bfabbaf..0000000000000 --- a/apps/uikit-playground/.prettierrc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "tabWidth": 2, - "useTabs": false, - "singleQuote": true -} diff --git a/apps/uikit-playground/index.html b/apps/uikit-playground/index.html index 7b1100bf13878..751be9f3fedab 100644 --- a/apps/uikit-playground/index.html +++ b/apps/uikit-playground/index.html @@ -1,13 +1,13 @@ -<!DOCTYPE html> +<!doctype html> <html lang="en"> - <head> - <meta charset="UTF-8" /> - <link rel="icon" type="image/svg+xml" href="/vite.svg" /> - <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <title>UiKit-Playground - - -
- - + + + + + UiKit-Playground + + +
+ + diff --git a/apps/uikit-playground/package.json b/apps/uikit-playground/package.json index 88ee5ad0811e7..ba9a466f50a33 100644 --- a/apps/uikit-playground/package.json +++ b/apps/uikit-playground/package.json @@ -1,59 +1,59 @@ { - "name": "@rocket.chat/uikit-playground", - "version": "0.7.6", - "private": true, - "type": "module", - "scripts": { - ".:build-preview-move": "mkdir -p ../../.preview/ && cp -r ./dist ../../.preview/uikit-playground", - "build-preview": "tsc && vite build", - "dev": "vite", - "lint": "eslint .", - "preview": "vite preview", - "typecheck": "tsc --noEmit" - }, - "dependencies": { - "@codemirror/lang-javascript": "^6.2.4", - "@codemirror/lang-json": "^6.0.2", - "@hello-pangea/dnd": "^17.0.0", - "@lezer/highlight": "^1.2.3", - "@rocket.chat/core-typings": "workspace:^", - "@rocket.chat/css-in-js": "~0.31.25", - "@rocket.chat/fuselage": "^0.71.0", - "@rocket.chat/fuselage-hooks": "^0.39.0", - "@rocket.chat/fuselage-toastbar": "^0.35.2", - "@rocket.chat/fuselage-tokens": "~0.33.2", - "@rocket.chat/fuselage-ui-kit": "workspace:~", - "@rocket.chat/icons": "~0.46.0", - "@rocket.chat/logo": "^0.32.4", - "@rocket.chat/styled": "~0.32.0", - "@rocket.chat/ui-avatar": "workspace:^", - "@rocket.chat/ui-contexts": "workspace:~", - "codemirror": "^6.0.2", - "eslint4b-prebuilt": "^6.7.2", - "moment": "^2.30.1", - "prettier": "~3.3.3", - "rc-scrollbars": "^1.1.6", - "react": "~18.3.1", - "react-beautiful-dnd": "^13.1.1", - "react-dom": "~18.3.1", - "react-router-dom": "^6.30.3", - "react-split-pane": "^0.1.92", - "react-virtuoso": "^4.12.0", - "reactflow": "^11.11.4" - }, - "devDependencies": { - "@rocket.chat/emitter": "^0.32.0", - "@rocket.chat/tsconfig": "workspace:*", - "@types/lodash": "~4.17.23", - "@types/react": "~18.3.27", - "@types/react-beautiful-dnd": "^13.1.8", - "@types/react-dom": "~18.3.7", - "@vitejs/plugin-react": "~4.5.2", - "eslint": "~9.39.3", - "typescript": "~5.9.3", - "vite": "^6.2.4" - }, - "volta": { - "extends": "../../package.json" - } + "name": "@rocket.chat/uikit-playground", + "version": "0.7.6", + "private": true, + "type": "module", + "scripts": { + ".:build-preview-move": "mkdir -p ../../.preview/ && cp -r ./dist ../../.preview/uikit-playground", + "build-preview": "tsc && vite build", + "dev": "vite", + "lint": "eslint .", + "preview": "vite preview", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@codemirror/lang-javascript": "^6.2.4", + "@codemirror/lang-json": "^6.0.2", + "@hello-pangea/dnd": "^17.0.0", + "@lezer/highlight": "^1.2.3", + "@rocket.chat/core-typings": "workspace:^", + "@rocket.chat/css-in-js": "~0.31.25", + "@rocket.chat/fuselage": "^0.71.0", + "@rocket.chat/fuselage-hooks": "^0.39.0", + "@rocket.chat/fuselage-toastbar": "^0.35.2", + "@rocket.chat/fuselage-tokens": "~0.33.2", + "@rocket.chat/fuselage-ui-kit": "workspace:~", + "@rocket.chat/icons": "~0.46.0", + "@rocket.chat/logo": "^0.32.4", + "@rocket.chat/styled": "~0.32.0", + "@rocket.chat/ui-avatar": "workspace:^", + "@rocket.chat/ui-contexts": "workspace:~", + "codemirror": "^6.0.2", + "eslint4b-prebuilt": "^6.7.2", + "moment": "^2.30.1", + "prettier": "~3.3.3", + "rc-scrollbars": "^1.1.6", + "react": "~18.3.1", + "react-beautiful-dnd": "^13.1.1", + "react-dom": "~18.3.1", + "react-router-dom": "^6.30.3", + "react-split-pane": "^0.1.92", + "react-virtuoso": "^4.12.0", + "reactflow": "^11.11.4" + }, + "devDependencies": { + "@rocket.chat/emitter": "^0.32.0", + "@rocket.chat/tsconfig": "workspace:*", + "@types/lodash": "~4.17.23", + "@types/react": "~18.3.27", + "@types/react-beautiful-dnd": "^13.1.8", + "@types/react-dom": "~18.3.7", + "@vitejs/plugin-react": "~4.5.2", + "eslint": "~9.39.3", + "typescript": "~5.9.3", + "vite": "^6.2.4" + }, + "volta": { + "extends": "../../package.json" + } } diff --git a/apps/uikit-playground/src/App.tsx b/apps/uikit-playground/src/App.tsx index ed32f3c388fae..80655038904da 100644 --- a/apps/uikit-playground/src/App.tsx +++ b/apps/uikit-playground/src/App.tsx @@ -1,66 +1,58 @@ -import { useContext, useEffect } from 'react'; -import './App.css'; -import './_global.css'; -import './cssVariables.css'; +import { Box } from '@rocket.chat/fuselage'; +import { useMediaQueries } from '@rocket.chat/fuselage-hooks'; import { ToastBarProvider } from '@rocket.chat/fuselage-toastbar'; +import { useContext, useEffect } from 'react'; import { BrowserRouter, Route, Routes } from 'react-router-dom'; import { HomeLayout } from './Components/Routes/HomeLayout'; -import Playground from './Pages/Playground'; -import SignInToWorkspace from './Pages/SignInSignUp'; -import routes from './Routes/Routes'; -import Home from './Pages/Home'; +import { ProjectSpecificLayout } from './Components/Routes/ProjectSpecificLayout'; import { context, isMobileAction, isTabletAction } from './Context'; -import { useMediaQueries } from '@rocket.chat/fuselage-hooks'; -import { Box } from '@rocket.chat/fuselage'; import FlowDiagram from './Pages/FlowDiagram'; -import { ProjectSpecificLayout } from './Components/Routes/ProjectSpecificLayout'; +import Home from './Pages/Home'; +import Playground from './Pages/Playground'; import Prototype from './Pages/Prototype'; +import SignInToWorkspace from './Pages/SignInSignUp'; +import routes from './Routes/Routes'; + +import './App.css'; +import './_global.css'; +import './cssVariables.css'; function App() { - const { dispatch } = useContext(context); + const { dispatch } = useContext(context); - const [isMobile, isTablet] = useMediaQueries( - '(max-width: 630px)', - '(max-width: 1050px)' - ); + const [isMobile, isTablet] = useMediaQueries('(max-width: 630px)', '(max-width: 1050px)'); - useEffect(() => { - dispatch(isMobileAction(isMobile)); - }, [isMobile, dispatch]); + useEffect(() => { + dispatch(isMobileAction(isMobile)); + }, [isMobile, dispatch]); - useEffect(() => { - dispatch(isTabletAction(isTablet)); - }, [isTablet, dispatch]); - return ( - - - - - }> - } - /> - } - /> - - {/* }> */} - } /> - }> - } /> - } /> - } /> - - } /> - {/* */} - - - - - ); + useEffect(() => { + dispatch(isTabletAction(isTablet)); + }, [isTablet, dispatch]); + return ( + + + + + }> + } /> + } /> + + {/* }> */} + } /> + }> + } /> + } /> + } /> + + } /> + {/* */} + + + + + ); } export default App; diff --git a/apps/uikit-playground/src/Components/CodeEditor/BlockEditor.tsx b/apps/uikit-playground/src/Components/CodeEditor/BlockEditor.tsx index 15e3f7dcc8566..566fe8fc3bafd 100644 --- a/apps/uikit-playground/src/Components/CodeEditor/BlockEditor.tsx +++ b/apps/uikit-playground/src/Components/CodeEditor/BlockEditor.tsx @@ -5,64 +5,51 @@ import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; import { useEffect, useContext } from 'react'; import { updatePayloadAction, context } from '../../Context'; +import { type IPayload } from '../../Context/initialState'; import useCodeMirror from '../../hooks/useCodeMirror'; -import intendCode from '../../utils/intendCode'; -import { IPayload } from '../../Context/initialState'; import useFormatCodeMirrorValue from '../../hooks/useFormatCodeMirrorValue'; +import intendCode from '../../utils/intendCode'; type CodeMirrorProps = { - extensions?: Extension[]; + extensions?: Extension[]; }; const BlockEditor = ({ extensions }: CodeMirrorProps) => { - const { - state: { screens, activeScreen }, - dispatch, - } = useContext(context); - - const { editor, changes, setValue } = useCodeMirror( - extensions, - intendCode(screens[activeScreen]?.payload) - ); - const debounceValue = useDebouncedValue(changes, 1500); - - useFormatCodeMirrorValue( - ( - parsedCode: IPayload, - prettifiedCode: { formatted: string; cursorOffset: number } - ) => { - dispatch( - updatePayloadAction({ - blocks: parsedCode.blocks, - surface: parsedCode.surface, - }) - ); - setValue(prettifiedCode.formatted, { - cursor: prettifiedCode.cursorOffset, - }); - }, - debounceValue - ); - - useEffect(() => { - if (!screens[activeScreen]?.changedByEditor) { - setValue(intendCode(screens[activeScreen]?.payload), {}); - } - }, [ - screens[activeScreen]?.payload.blocks, - screens[activeScreen]?.payload.surface, - activeScreen, - ]); - - useEffect(() => { - setValue(intendCode(screens[activeScreen]?.payload), {}); - }, [activeScreen]); - - return ( - <> - - - ); + const { + state: { screens, activeScreen }, + dispatch, + } = useContext(context); + + const { editor, changes, setValue } = useCodeMirror(extensions, intendCode(screens[activeScreen]?.payload)); + const debounceValue = useDebouncedValue(changes, 1500); + + useFormatCodeMirrorValue((parsedCode: IPayload, prettifiedCode: { formatted: string; cursorOffset: number }) => { + dispatch( + updatePayloadAction({ + blocks: parsedCode.blocks, + surface: parsedCode.surface, + }), + ); + setValue(prettifiedCode.formatted, { + cursor: prettifiedCode.cursorOffset, + }); + }, debounceValue); + + useEffect(() => { + if (!screens[activeScreen]?.changedByEditor) { + setValue(intendCode(screens[activeScreen]?.payload), {}); + } + }, [screens[activeScreen]?.payload.blocks, screens[activeScreen]?.payload.surface, activeScreen]); + + useEffect(() => { + setValue(intendCode(screens[activeScreen]?.payload), {}); + }, [activeScreen]); + + return ( + <> + + + ); }; export default BlockEditor; diff --git a/apps/uikit-playground/src/Components/CodeEditor/Extensions/Extensions.ts b/apps/uikit-playground/src/Components/CodeEditor/Extensions/Extensions.ts index cb276ceaf2f96..709358dd6daa5 100644 --- a/apps/uikit-playground/src/Components/CodeEditor/Extensions/Extensions.ts +++ b/apps/uikit-playground/src/Components/CodeEditor/Extensions/Extensions.ts @@ -8,18 +8,18 @@ import jsonLinter from './jsonLinter'; import theme from './theme'; export const actionBlockExtensions = [ - highlightStyle, - json(), - jsonLinter, - basicSetup, - // payloadLinter, - ...theme, + highlightStyle, + json(), + jsonLinter, + basicSetup, + // payloadLinter, + ...theme, ]; export const actionPreviewExtensions = [ - EditorView.contentAttributes.of({ contenteditable: 'false' }), - highlightStyle, - json(), - basicSetup, - ...theme, + EditorView.contentAttributes.of({ contenteditable: 'false' }), + highlightStyle, + json(), + basicSetup, + ...theme, ]; diff --git a/apps/uikit-playground/src/Components/CodeEditor/Extensions/HighlightStyle.ts b/apps/uikit-playground/src/Components/CodeEditor/Extensions/HighlightStyle.ts index fc5aa5e9e9584..9ef0dc0d72b7f 100644 --- a/apps/uikit-playground/src/Components/CodeEditor/Extensions/HighlightStyle.ts +++ b/apps/uikit-playground/src/Components/CodeEditor/Extensions/HighlightStyle.ts @@ -2,14 +2,14 @@ import { HighlightStyle, syntaxHighlighting } from '@codemirror/language'; import { tags as t } from '@lezer/highlight'; const highLightStyle = () => { - const style = HighlightStyle.define([ - { tag: t.literal, color: 'var(--RCPG-primary-color)' }, - { tag: t.bool, color: 'var(--RCPG-tertary-color)' }, - { tag: t.number, color: 'var(--RCPG-secondary-color)' }, - { tag: t.null, color: 'var(--RCPG-tertary-color)' }, - ]); + const style = HighlightStyle.define([ + { tag: t.literal, color: 'var(--RCPG-primary-color)' }, + { tag: t.bool, color: 'var(--RCPG-tertary-color)' }, + { tag: t.number, color: 'var(--RCPG-secondary-color)' }, + { tag: t.null, color: 'var(--RCPG-tertary-color)' }, + ]); - return syntaxHighlighting(style); + return syntaxHighlighting(style); }; export default highLightStyle(); diff --git a/apps/uikit-playground/src/Components/CodeEditor/Extensions/basicSetup.ts b/apps/uikit-playground/src/Components/CodeEditor/Extensions/basicSetup.ts index e5973a7922170..b3923dd95c21b 100644 --- a/apps/uikit-playground/src/Components/CodeEditor/Extensions/basicSetup.ts +++ b/apps/uikit-playground/src/Components/CodeEditor/Extensions/basicSetup.ts @@ -1,59 +1,35 @@ -import { - completionKeymap, - closeBrackets, - closeBracketsKeymap, -} from '@codemirror/autocomplete'; -import { - defaultKeymap, - history, - historyKeymap, - indentWithTab, -} from '@codemirror/commands'; -import { - defaultHighlightStyle, - syntaxHighlighting, - indentOnInput, - bracketMatching, - foldGutter, - foldKeymap, -} from '@codemirror/language'; +import { completionKeymap, closeBrackets, closeBracketsKeymap } from '@codemirror/autocomplete'; +import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands'; +import { defaultHighlightStyle, syntaxHighlighting, indentOnInput, bracketMatching, foldGutter, foldKeymap } from '@codemirror/language'; import { lintKeymap } from '@codemirror/lint'; import { searchKeymap, highlightSelectionMatches } from '@codemirror/search'; import type { Extension } from '@codemirror/state'; -import { - keymap, - drawSelection, - dropCursor, - rectangularSelection, - crosshairCursor, - lineNumbers, - EditorView, -} from '@codemirror/view'; +import { keymap, drawSelection, dropCursor, rectangularSelection, crosshairCursor, lineNumbers, EditorView } from '@codemirror/view'; const basicSetup: Extension = (() => [ - lineNumbers(), - history(), - foldGutter(), - drawSelection(), - dropCursor(), - indentOnInput(), - EditorView.lineWrapping, - syntaxHighlighting(defaultHighlightStyle, { fallback: true }), - bracketMatching(), - closeBrackets(), - rectangularSelection(), - crosshairCursor(), - highlightSelectionMatches(), - keymap.of([ - ...closeBracketsKeymap, - ...defaultKeymap, - ...searchKeymap, - ...historyKeymap, - ...foldKeymap, - ...completionKeymap, - ...lintKeymap, - indentWithTab, - ]), + lineNumbers(), + history(), + foldGutter(), + drawSelection(), + dropCursor(), + indentOnInput(), + EditorView.lineWrapping, + syntaxHighlighting(defaultHighlightStyle, { fallback: true }), + bracketMatching(), + closeBrackets(), + rectangularSelection(), + crosshairCursor(), + highlightSelectionMatches(), + keymap.of([ + ...closeBracketsKeymap, + ...defaultKeymap, + ...searchKeymap, + ...historyKeymap, + ...foldKeymap, + ...completionKeymap, + ...lintKeymap, + indentWithTab, + ]), ])(); export default basicSetup; diff --git a/apps/uikit-playground/src/Components/CodeEditor/Extensions/payloadLinter.ts b/apps/uikit-playground/src/Components/CodeEditor/Extensions/payloadLinter.ts index 5eccf262f23fd..25af57c410eb5 100644 --- a/apps/uikit-playground/src/Components/CodeEditor/Extensions/payloadLinter.ts +++ b/apps/uikit-playground/src/Components/CodeEditor/Extensions/payloadLinter.ts @@ -6,60 +6,55 @@ import type { EditorView } from 'codemirror'; import parsePayload from '../Parser'; const payloadLinter = linter((view: EditorView) => { - const diagnostics: Diagnostic[] = []; - const tree = syntaxTree(view.state); - let head = tree.topNode.firstChild; - if (!head || !head.matchContext(['Script'])) { - diagnostics.push({ - from: 0, - to: 0, - message: 'Expecting a Script', - severity: 'error', - }); - return diagnostics; - } - head = head.firstChild; - if (!head || !head.matchContext(['ExpressionStatement'])) { - diagnostics.push({ - from: 0, - to: 0, - message: 'Expecting an expression statement', - severity: 'error', - }); - return diagnostics; - } - head = head.firstChild; - if (!head || !head.matchContext(['ArrayExpression'])) { - diagnostics.push({ - from: 0, - to: 0, - message: 'Expecting an array expression', - severity: 'error', - }); - return diagnostics; - } - // while (head.nextSibling && head.nextSibling.name !== ']') { - // } - do { - if ( - head.name !== '[' && - head.name !== ',' && - head.name !== ']' && - head.name !== 'ObjectExpression' - ) { - diagnostics.push({ - from: head.from, - to: head.to, - message: 'Expecting an Object expression', - severity: 'error', - }); - return diagnostics; - } - if (head.name === 'ObjectExpression') parsePayload(head, view); - head = head.nextSibling; - } while (head); + const diagnostics: Diagnostic[] = []; + const tree = syntaxTree(view.state); + let head = tree.topNode.firstChild; + if (!head?.matchContext(['Script'])) { + diagnostics.push({ + from: 0, + to: 0, + message: 'Expecting a Script', + severity: 'error', + }); + return diagnostics; + } + head = head.firstChild; + if (!head?.matchContext(['ExpressionStatement'])) { + diagnostics.push({ + from: 0, + to: 0, + message: 'Expecting an expression statement', + severity: 'error', + }); + return diagnostics; + } + head = head.firstChild; + if (!head?.matchContext(['ArrayExpression'])) { + diagnostics.push({ + from: 0, + to: 0, + message: 'Expecting an array expression', + severity: 'error', + }); + return diagnostics; + } + // while (head.nextSibling && head.nextSibling.name !== ']') { + // } + do { + if (head.name !== '[' && head.name !== ',' && head.name !== ']' && head.name !== 'ObjectExpression') { + diagnostics.push({ + from: head.from, + to: head.to, + message: 'Expecting an Object expression', + severity: 'error', + }); + return diagnostics; + } + if (head.name === 'ObjectExpression') parsePayload(head, view); + head = head.nextSibling; + } while (head); - return diagnostics; + return diagnostics; }); export default payloadLinter; diff --git a/apps/uikit-playground/src/Components/CodeEditor/Extensions/theme.ts b/apps/uikit-playground/src/Components/CodeEditor/Extensions/theme.ts index 71938add617fc..11caa7bb159ef 100644 --- a/apps/uikit-playground/src/Components/CodeEditor/Extensions/theme.ts +++ b/apps/uikit-playground/src/Components/CodeEditor/Extensions/theme.ts @@ -2,40 +2,40 @@ import type { Extension } from '@codemirror/state'; import { EditorView } from '@codemirror/view'; const gutters: Extension = EditorView.theme({ - '.cm-gutters': { - backgroundColor: 'transparent', - border: 'none', - userSelect: 'none', - minWidth: '32px', - display: 'flex', - justifyContent: 'flex-end', - }, + '.cm-gutters': { + backgroundColor: 'transparent', + border: 'none', + userSelect: 'none', + minWidth: '32px', + display: 'flex', + justifyContent: 'flex-end', + }, - '.cm-activeLineGutter': { - backgroundColor: 'transparent', - }, + '.cm-activeLineGutter': { + backgroundColor: 'transparent', + }, }); const selection: Extension = EditorView.theme({ - '.cm-selectionBackground': { - backgroundColor: 'var(--RCPG-secondary-color) !important', - opacity: 0.3, - }, + '.cm-selectionBackground': { + backgroundColor: 'var(--RCPG-secondary-color) !important', + opacity: 0.3, + }, - '.cm-selectionMatch': { - backgroundColor: '#74808930 !important', - }, + '.cm-selectionMatch': { + backgroundColor: '#74808930 !important', + }, - '.cm-matchingBracket': { - backgroundColor: 'transparent !important', - border: '1px solid #1d74f580', - }, + '.cm-matchingBracket': { + backgroundColor: 'transparent !important', + border: '1px solid #1d74f580', + }, }); const line: Extension = EditorView.theme({ - '.cm-activeLine': { - backgroundColor: 'transparent !important', - }, + '.cm-activeLine': { + backgroundColor: 'transparent !important', + }, }); export default [gutters, selection, line] as const; diff --git a/apps/uikit-playground/src/Components/CodeEditor/Parser/parsePayload.ts b/apps/uikit-playground/src/Components/CodeEditor/Parser/parsePayload.ts index 8a431bc05e158..4faeaf519ce03 100644 --- a/apps/uikit-playground/src/Components/CodeEditor/Parser/parsePayload.ts +++ b/apps/uikit-playground/src/Components/CodeEditor/Parser/parsePayload.ts @@ -1,15 +1,7 @@ -// import type { Diagnostic } from '@codemirror/lint'; import type { EditorView } from 'codemirror'; -const parsePayload = ( - head: { from: number; to: number }, - // Diagnostic: Diagnostic[], - view: EditorView -) => { - const payload = JSON.parse( - view.state.doc.toString().slice(head.from, head.to) - ); - payload && 1; +const parsePayload = (head: { from: number; to: number }, view: EditorView) => { + JSON.parse(view.state.doc.toString().slice(head.from, head.to)); }; export default parsePayload; diff --git a/apps/uikit-playground/src/Components/CodeEditor/PreviewEditor.tsx b/apps/uikit-playground/src/Components/CodeEditor/PreviewEditor.tsx index 0a0396167e3d7..8edf7b168a1c5 100644 --- a/apps/uikit-playground/src/Components/CodeEditor/PreviewEditor.tsx +++ b/apps/uikit-playground/src/Components/CodeEditor/PreviewEditor.tsx @@ -7,28 +7,25 @@ import useCodeMirror from '../../hooks/useCodeMirror'; import intendCode from '../../utils/intendCode'; type CodeMirrorProps = { - extensions?: Extension[]; + extensions?: Extension[]; }; const PreviewEditor = ({ extensions }: CodeMirrorProps) => { - const { - state: { screens, activeScreen }, - } = useContext(context); - const { editor, setValue } = useCodeMirror( - extensions, - intendCode(screens[activeScreen]?.actionPreview) - ); + const { + state: { screens, activeScreen }, + } = useContext(context); + const { editor, setValue } = useCodeMirror(extensions, intendCode(screens[activeScreen]?.actionPreview)); - useEffect(() => { - setValue(intendCode(screens[activeScreen]?.actionPreview), {}); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [screens[activeScreen]?.actionPreview]); + useEffect(() => { + setValue(intendCode(screens[activeScreen]?.actionPreview), {}); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [screens[activeScreen]?.actionPreview]); - return ( - <> - - - ); + return ( + <> + + + ); }; export default PreviewEditor; diff --git a/apps/uikit-playground/src/Components/CodeEditor/index.tsx b/apps/uikit-playground/src/Components/CodeEditor/index.tsx index 8a5b7e40848b8..4510b4aac74ab 100644 --- a/apps/uikit-playground/src/Components/CodeEditor/index.tsx +++ b/apps/uikit-playground/src/Components/CodeEditor/index.tsx @@ -5,67 +5,64 @@ import json5 from 'json5'; import { useEffect, useContext } from 'react'; import { updatePayloadAction, context } from '../../Context'; +import { type ILayoutBlock } from '../../Context/initialState'; import useCodeMirror from '../../hooks/useCodeMirror'; import codePrettier from '../../utils/codePrettier'; -import { ILayoutBlock } from '../../Context/initialState'; type CodeMirrorProps = { - extensions?: Extension[]; + extensions?: Extension[]; }; const CodeEditor = ({ extensions }: CodeMirrorProps) => { - const { - state: { screens, activeScreen }, - dispatch, - } = useContext(context); - const { editor, changes, setValue } = useCodeMirror( - extensions, - json5.stringify(screens[activeScreen].payload, undefined, 4), - ); - const debounceValue = useDebouncedValue(changes?.value, 1500); + const { + state: { screens, activeScreen }, + dispatch, + } = useContext(context); + const { editor, changes, setValue } = useCodeMirror(extensions, json5.stringify(screens[activeScreen].payload, undefined, 4)); + const debounceValue = useDebouncedValue(changes?.value, 1500); - useEffect(() => { - if (!changes?.isDispatch) { - try { - const parsedCode: ILayoutBlock[] = json5.parse(changes.value); - dispatch( - updatePayloadAction({ - blocks: parsedCode, - changedByEditor: false, - }), - ); + useEffect(() => { + if (!changes?.isDispatch) { + try { + const parsedCode: ILayoutBlock[] = json5.parse(changes.value); + dispatch( + updatePayloadAction({ + blocks: parsedCode, + changedByEditor: false, + }), + ); - dispatch(updatePayloadAction({ blocks: parsedCode })); - } catch (e) { - // do nothing - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [changes?.value]); + dispatch(updatePayloadAction({ blocks: parsedCode })); + } catch (e) { + // do nothing + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [changes?.value]); - useEffect(() => { - if (!changes?.isDispatch) { - codePrettier(changes.value, changes.cursor || 0).then((prettierCode) => { - setValue(prettierCode.formatted, { - cursor: prettierCode.cursorOffset, - }); - }); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [debounceValue]); + useEffect(() => { + if (!changes?.isDispatch) { + void codePrettier(changes.value, changes.cursor || 0).then((prettierCode) => { + setValue(prettierCode.formatted, { + cursor: prettierCode.cursorOffset, + }); + }); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [debounceValue]); - useEffect(() => { - if (!screens[activeScreen].changedByEditor) { - setValue(JSON.stringify(screens[activeScreen].payload, undefined, 4), {}); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [screens[activeScreen].payload]); + useEffect(() => { + if (!screens[activeScreen].changedByEditor) { + setValue(JSON.stringify(screens[activeScreen].payload, undefined, 4), {}); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [screens[activeScreen].payload]); - return ( - <> - - - ); + return ( + <> + + + ); }; export default CodeEditor; diff --git a/apps/uikit-playground/src/Components/ComponentSideBar/ScrollableSideBar.tsx b/apps/uikit-playground/src/Components/ComponentSideBar/ScrollableSideBar.tsx index 1d0a1bf318c0a..9dfb4681f0ec0 100644 --- a/apps/uikit-playground/src/Components/ComponentSideBar/ScrollableSideBar.tsx +++ b/apps/uikit-playground/src/Components/ComponentSideBar/ScrollableSideBar.tsx @@ -1,24 +1,23 @@ import { css } from '@rocket.chat/css-in-js'; import { Scrollable, Box } from '@rocket.chat/fuselage'; -import type { FC } from 'react'; import BlocksTree from '../../Payload/actionBlock/BlocksTree'; import DropDown from '../DropDown'; -const ScrollableSideBar: FC = () => ( - - - - - +const ScrollableSideBar = () => ( + + + + + ); export default ScrollableSideBar; diff --git a/apps/uikit-playground/src/Components/ComponentSideBar/SideBar.tsx b/apps/uikit-playground/src/Components/ComponentSideBar/SideBar.tsx index 8deae40dffa78..e180b5a6ec39c 100644 --- a/apps/uikit-playground/src/Components/ComponentSideBar/SideBar.tsx +++ b/apps/uikit-playground/src/Components/ComponentSideBar/SideBar.tsx @@ -1,45 +1,37 @@ import { css } from '@rocket.chat/css-in-js'; import { Box } from '@rocket.chat/fuselage'; -import type { FC } from 'react'; import { useEffect, useContext } from 'react'; -import { context, sidebarToggleAction } from '../../Context'; import ScrollableSideBar from './ScrollableSideBar'; import SliderBtn from './SliderBtn'; +import { context, sidebarToggleAction } from '../../Context'; -const SideBar: FC = () => { - const { state, dispatch } = useContext(context); +const SideBar = () => { + const { state, dispatch } = useContext(context); - useEffect(() => { - dispatch(sidebarToggleAction(false)); - }, [state?.isMobile, dispatch]); + useEffect(() => { + dispatch(sidebarToggleAction(false)); + }, [state?.isMobile, dispatch]); - const slide = state?.isMobile - ? css` - width: 100%; - user-select: none; - transform: translateX(${state?.sideBarToggle ? '0' : '-100%'}); - transition: var(--animation-default); - ` - : css` - width: var(--sidebar-width); - user-select: none; - transition: var(--animation-default); - `; + const slide = state?.isMobile + ? css` + width: 100%; + user-select: none; + transform: translateX(${state?.sideBarToggle ? '0' : '-100%'}); + transition: var(--animation-default); + ` + : css` + width: var(--sidebar-width); + user-select: none; + transition: var(--animation-default); + `; - return ( - - - - - ); + return ( + + + + + ); }; export default SideBar; diff --git a/apps/uikit-playground/src/Components/ComponentSideBar/SliderBtn.tsx b/apps/uikit-playground/src/Components/ComponentSideBar/SliderBtn.tsx index 6d1e7982bc0cc..f5d96319d69cc 100644 --- a/apps/uikit-playground/src/Components/ComponentSideBar/SliderBtn.tsx +++ b/apps/uikit-playground/src/Components/ComponentSideBar/SliderBtn.tsx @@ -1,115 +1,90 @@ import { css } from '@rocket.chat/css-in-js'; import { Box, Label } from '@rocket.chat/fuselage'; -import type { FC } from 'react'; -import { useContext } from 'react'; +import { useContext, useMemo } from 'react'; import { context, sidebarToggleAction } from '../../Context'; -const SliderBtn: FC = () => { - const { - state: { sideBarToggle, isMobile }, - dispatch, - } = useContext(context); - const slideBtnAnimation = sideBarToggle - ? css` - clip-path: polygon( - 10% 0, - 50% 40%, - 90% 0, - 100% 10%, - 60% 50%, - 100% 90%, - 90% 100%, - 50% 60%, - 10% 100%, - 0 90%, - 40% 50%, - 0 10% - ); - cursor: pointer; - transition: var(--animation-default); - ` - : css` - clip-path: polygon( - 32% 35%, - 32% 35%, - 79% 0, - 87% 10%, - 32% 50%, - 87% 90%, - 79% 100%, - 32% 64%, - 32% 65%, - 13% 50%, - 13% 50%, - 13% 50% - ); - transform: rotate(180deg); - transition: var(--animation-default); - `; +const SliderBtn = () => { + const { + state: { sideBarToggle, isMobile }, + dispatch, + } = useContext(context); + const slideBtnAnimation = sideBarToggle + ? css` + clip-path: polygon(10% 0, 50% 40%, 90% 0, 100% 10%, 60% 50%, 100% 90%, 90% 100%, 50% 60%, 10% 100%, 0 90%, 40% 50%, 0 10%); + cursor: pointer; + transition: var(--animation-default); + ` + : css` + clip-path: polygon(32% 35%, 32% 35%, 79% 0, 87% 10%, 32% 50%, 87% 90%, 79% 100%, 32% 64%, 32% 65%, 13% 50%, 13% 50%, 13% 50%); + transform: rotate(180deg); + transition: var(--animation-default); + `; - const toggleStyle = !isMobile - ? css` - left: 0px; - ` - : sideBarToggle - ? css` - right: 0; - transition: var(--animation-default); - ` - : css` - right: 0; - transform: translateX(100%); - cursor: pointer; - transition: var(--animation-default); - `; + const toggleStyle = useMemo(() => { + if (!isMobile) { + return css` + left: 0px; + `; + } - return ( - - !sideBarToggle && dispatch(sidebarToggleAction(!sideBarToggle)) - } - zIndex={1} - className={toggleStyle} - > - - {isMobile && ( - - sideBarToggle && dispatch(sidebarToggleAction(!sideBarToggle)) - } - className={css` - cursor: pointer; - `} - > - - - )} - - ); + if (sideBarToggle) { + return css` + right: 0; + transition: var(--animation-default); + `; + } + + return css` + right: 0; + transform: translateX(100%); + cursor: pointer; + transition: var(--animation-default); + `; + }, [isMobile, sideBarToggle]); + + return ( + !sideBarToggle && dispatch(sidebarToggleAction(!sideBarToggle))} + zIndex={1} + className={toggleStyle} + > + + {isMobile && ( + sideBarToggle && dispatch(sidebarToggleAction(!sideBarToggle))} + className={css` + cursor: pointer; + `} + > + + + )} + + ); }; export default SliderBtn; diff --git a/apps/uikit-playground/src/Components/CreateNewScreen/CreateNewScreenContainer.tsx b/apps/uikit-playground/src/Components/CreateNewScreen/CreateNewScreenContainer.tsx index e32ed198562ba..0220e1fb13060 100644 --- a/apps/uikit-playground/src/Components/CreateNewScreen/CreateNewScreenContainer.tsx +++ b/apps/uikit-playground/src/Components/CreateNewScreen/CreateNewScreenContainer.tsx @@ -3,107 +3,101 @@ import { Box, Button, Icon, Scrollable } from '@rocket.chat/fuselage'; import { useOutsideClick, useMergedRefs } from '@rocket.chat/fuselage-hooks'; import { useContext, useRef } from 'react'; +import ScreenThumbnail from './ScreenThumbnail'; import { context } from '../../Context'; -import { - openCreateNewScreenAction, - createNewScreenAction, -} from '../../Context/action'; +import { openCreateNewScreenAction, createNewScreenAction } from '../../Context/action'; import { useHorizontalScroll } from '../../hooks/useHorizontalScroll'; -import ScreenThumbnail from './ScreenThumbnail'; const CreateNewScreenContainer = () => { - const { - state: { projects, screens, activeProject, openCreateNewScreen }, - dispatch, - } = useContext(context); - const ref = useRef(null); + const { + state: { projects, screens, activeProject, openCreateNewScreen }, + dispatch, + } = useContext(context); + const ref = useRef(null); - const onClosehandler = () => { - dispatch(openCreateNewScreenAction(false)); - }; - useOutsideClick([ref], onClosehandler); + const onClosehandler = () => { + dispatch(openCreateNewScreenAction(false)); + }; + useOutsideClick([ref], onClosehandler); - const scrollRef = useHorizontalScroll(); + const scrollRef = useHorizontalScroll(); - const mergedRef = useMergedRefs(scrollRef, ref); - const createNewScreenhandler = () => { - dispatch(createNewScreenAction()); - }; + const mergedRef = useMergedRefs(scrollRef, ref); + const createNewScreenhandler = () => { + dispatch(createNewScreenAction()); + }; - return ( - - - - {openCreateNewScreen && ( - - {projects[activeProject]?.screens - .map((id) => screens[id]) - .map((screen, i) => ( - screens[id]) - .length <= 1 - } - /> - ))} - - - )} - - - ); + return ( + + + + {openCreateNewScreen && ( + + {projects[activeProject]?.screens + .map((id) => screens[id]) + .map((screen, i) => ( + screens[id]).length <= 1} + /> + ))} + + + )} + + + ); }; export default CreateNewScreenContainer; diff --git a/apps/uikit-playground/src/Components/CreateNewScreen/ScreenThumbnail.tsx b/apps/uikit-playground/src/Components/CreateNewScreen/ScreenThumbnail.tsx index d3720a07595ce..a8527efc3d8aa 100644 --- a/apps/uikit-playground/src/Components/CreateNewScreen/ScreenThumbnail.tsx +++ b/apps/uikit-playground/src/Components/CreateNewScreen/ScreenThumbnail.tsx @@ -3,75 +3,69 @@ import { useToastBarDispatch } from '@rocket.chat/fuselage-toastbar'; import type { ChangeEvent, MouseEvent } from 'react'; import { useContext, useState } from 'react'; -import ScreenThumbnailWrapper from '../ScreenThumbnail/ScreenThumbnailWrapper'; -import Thumbnail from '../ScreenThumbnail/Thumbnail'; import { context, renameScreenAction } from '../../Context'; import { activeScreenAction } from '../../Context/action/activeScreenAction'; import { deleteScreenAction } from '../../Context/action/deleteScreenAction'; import { duplicateScreenAction } from '../../Context/action/duplicateScreenAction'; +import { type ScreenType } from '../../Context/initialState'; import renderPayload from '../RenderPayload/RenderPayload'; -import { ScreenType } from '../../Context/initialState'; import EditMenu from '../ScreenThumbnail/EditMenu/EditMenu'; +import ScreenThumbnailWrapper from '../ScreenThumbnail/ScreenThumbnailWrapper'; +import Thumbnail from '../ScreenThumbnail/Thumbnail'; -const ScreenThumbnail = ({ - screen, - disableDelete, -}: { - screen: ScreenType; - disableDelete: boolean; -}) => { - const { dispatch } = useContext(context); - const [name, setName] = useState(screen?.name); - const toast = useToastBarDispatch(); +const ScreenThumbnail = ({ screen, disableDelete }: { screen: ScreenType; disableDelete: boolean }) => { + const { dispatch } = useContext(context); + const [name, setName] = useState(screen?.name); + const toast = useToastBarDispatch(); - const activateScreenHandler = (e: MouseEvent) => { - e.stopPropagation(); - dispatch(activeScreenAction(screen?.id)); - }; + const activateScreenHandler = (e: MouseEvent) => { + e.stopPropagation(); + dispatch(activeScreenAction(screen?.id)); + }; - const duplicateScreenHandler = () => { - dispatch(duplicateScreenAction({ id: screen?.id })); - }; + const duplicateScreenHandler = () => { + dispatch(duplicateScreenAction({ id: screen?.id })); + }; - const onChangeNameHandler = (e: ChangeEvent) => { - setName(e.currentTarget.value); - }; + const onChangeNameHandler = (e: ChangeEvent) => { + setName(e.currentTarget.value); + }; - const nameSaveHandler = () => { - if (!name.trim()) { - setName(screen.name); - return toast({ - type: 'error', - message: 'Cannot rename screen to empty name.', - }); - } - dispatch(renameScreenAction({ id: screen.id, name })); - }; + const nameSaveHandler = () => { + if (!name.trim()) { + setName(screen.name); + return toast({ + type: 'error', + message: 'Cannot rename screen to empty name.', + }); + } + dispatch(renameScreenAction({ id: screen.id, name })); + }; - const deleteScreenHandler = () => { - if (disableDelete) - return toast({ - type: 'info', - message: 'Cannot delete last screen.', - }); - dispatch(deleteScreenAction(screen?.id)); - }; - return ( - - - - - - - ); + const deleteScreenHandler = () => { + if (disableDelete) + return toast({ + type: 'info', + message: 'Cannot delete last screen.', + }); + dispatch(deleteScreenAction(screen?.id)); + }; + return ( + + + + + + + ); }; export default ScreenThumbnail; diff --git a/apps/uikit-playground/src/Components/Draggable/DraggableList.tsx b/apps/uikit-playground/src/Components/Draggable/DraggableList.tsx index db7b3968a3d91..7158b02ed6e3c 100644 --- a/apps/uikit-playground/src/Components/Draggable/DraggableList.tsx +++ b/apps/uikit-playground/src/Components/Draggable/DraggableList.tsx @@ -1,44 +1,35 @@ -import { memo } from 'react'; import type { OnDragEndResponder } from '@hello-pangea/dnd'; import { DragDropContext, Droppable } from '@hello-pangea/dnd'; +import { memo } from 'react'; import DraggableListItem from './DraggableListItem'; +import { type ILayoutBlock } from '../../Context/initialState'; import { SurfaceOptions } from '../Preview/Display/Surface/constant'; -import { ILayoutBlock } from '../../Context/initialState'; export type Block = { - id: string; - payload: ILayoutBlock; + id: string; + payload: ILayoutBlock; }; export type DraggableListProps = { - blocks: Block[]; - surface?: SurfaceOptions; - onDragEnd: OnDragEndResponder; + blocks: Block[]; + surface?: SurfaceOptions; + onDragEnd: OnDragEndResponder; }; const DraggableList = ({ blocks, surface, onDragEnd }: DraggableListProps) => ( - - - {(provided) => ( -
- {blocks.map((block, index) => ( - - ))} - {provided.placeholder} -
- )} -
-
+ + + {(provided) => ( +
+ {blocks.map((block, index) => ( + + ))} + {provided.placeholder} +
+ )} +
+
); export default memo(DraggableList); diff --git a/apps/uikit-playground/src/Components/Draggable/DraggableListItem.tsx b/apps/uikit-playground/src/Components/Draggable/DraggableListItem.tsx index 5c22a7f7627ea..b923566b70f64 100644 --- a/apps/uikit-playground/src/Components/Draggable/DraggableListItem.tsx +++ b/apps/uikit-playground/src/Components/Draggable/DraggableListItem.tsx @@ -1,35 +1,27 @@ import { Draggable } from '@hello-pangea/dnd'; +import type { Block } from './DraggableList'; import DeleteElementBtn from '../Preview/Display/UiKitElementWrapper/DeleteElementBtn'; import UiKitElementWrapper from '../Preview/Display/UiKitElementWrapper/UiKitElementWrapper'; import RenderPayload from '../RenderPayload/RenderPayload'; -import type { Block } from './DraggableList'; export type DraggableListItemProps = { - block: Block; - surface: number; - index: number; + block: Block; + surface: number; + index: number; }; -const DraggableListItem = ({ - block, - surface, - index, -}: DraggableListItemProps) => ( - - {(provided) => ( -
- - - - -
- )} -
+const DraggableListItem = ({ block, surface, index }: DraggableListItemProps) => ( + + {(provided) => ( +
+ + + + +
+ )} +
); export default DraggableListItem; diff --git a/apps/uikit-playground/src/Components/DropDown/DropDown.tsx b/apps/uikit-playground/src/Components/DropDown/DropDown.tsx index 8377a96c8a99a..921274c6eaa15 100644 --- a/apps/uikit-playground/src/Components/DropDown/DropDown.tsx +++ b/apps/uikit-playground/src/Components/DropDown/DropDown.tsx @@ -4,31 +4,28 @@ import { Fragment } from 'react'; import Items from './Items'; import type { Item, ItemBranch } from './types'; -interface DropDownProps { - readonly BlocksTree: Item; -} +type DropDownProps = { + readonly blocksTree: Item; +}; -const DropDown = ({ BlocksTree }: DropDownProps) => { - const layer = 1; +const DropDown = ({ blocksTree }: DropDownProps) => { + const layer = 1; - const recursiveComponentTree = (branch: ItemBranch, layer: number) => ( - - {branch.branches && - branch.branches.map((branch: ItemBranch, index: number) => ( - - {recursiveComponentTree(branch, layer + 1)} - - ))} - - ); + const recursiveComponentTree = (branch: ItemBranch, layer: number) => ( + + {branch.branches?.map((branch: ItemBranch, index: number) => ( + {recursiveComponentTree(branch, layer + 1)} + ))} + + ); - return ( - - {BlocksTree.map((branch: ItemBranch, i: number) => ( - {recursiveComponentTree(branch, layer)} - ))} - - ); + return ( + + {blocksTree.map((branch: ItemBranch, i: number) => ( + {recursiveComponentTree(branch, layer)} + ))} + + ); }; export default DropDown; diff --git a/apps/uikit-playground/src/Components/DropDown/Items.tsx b/apps/uikit-playground/src/Components/DropDown/Items.tsx index 18eb6a10dcc50..85bcd833007cd 100644 --- a/apps/uikit-playground/src/Components/DropDown/Items.tsx +++ b/apps/uikit-playground/src/Components/DropDown/Items.tsx @@ -2,68 +2,61 @@ import { css } from '@rocket.chat/css-in-js'; import { Box, Label, Chevron } from '@rocket.chat/fuselage'; import { useState, useContext } from 'react'; -import { context, updatePayloadAction } from '../../Context'; import ItemsIcon from './ItemsIcon'; import { itemStyle, labelStyle } from './itemsStyle'; import type { ItemProps } from './types'; +import { context, updatePayloadAction } from '../../Context'; import getUniqueId from '../../utils/getUniqueId'; const Items = ({ label, children, layer, payload }: ItemProps) => { - const [isOpen, toggleItemOpen] = useState(layer === 1); - const [hover, setHover] = useState(false); - const { state, dispatch } = useContext(context); + const [isOpen, toggleItemOpen] = useState(layer === 1); + const [hover, setHover] = useState(false); + const { state, dispatch } = useContext(context); - const itemClickHandler = () => { - toggleItemOpen(!isOpen); - payload && - dispatch( - updatePayloadAction({ - blocks: [ - ...state.screens[state.activeScreen].payload.blocks, - { actionId: getUniqueId(), ...payload[0] }, - ], - changedByEditor: false, - }) - ); - }; + const itemClickHandler = () => { + toggleItemOpen(!isOpen); + if (!payload) return; + dispatch( + updatePayloadAction({ + blocks: [...state.screens[state.activeScreen].payload.blocks, { actionId: getUniqueId(), ...payload[0] }], + changedByEditor: false, + }), + ); + }; - return ( - - setHover(true)} - onMouseLeave={() => setHover(false)} - onClick={itemClickHandler} - > - - {children && children.length > 0 && ( - - - - )} - - - - - - - {isOpen && children} - - ); + return ( + + setHover(true)} + onMouseLeave={() => setHover(false)} + onClick={itemClickHandler} + > + + {children && children.length > 0 && ( + + + + )} + + + + + + + {isOpen && children} + + ); }; export default Items; diff --git a/apps/uikit-playground/src/Components/DropDown/ItemsIcon.tsx b/apps/uikit-playground/src/Components/DropDown/ItemsIcon.tsx index fb74ae3971135..1e8ab7e34bddd 100644 --- a/apps/uikit-playground/src/Components/DropDown/ItemsIcon.tsx +++ b/apps/uikit-playground/src/Components/DropDown/ItemsIcon.tsx @@ -1,26 +1,16 @@ import { Icon } from '@rocket.chat/fuselage'; -const ItemsIcon = ({ - layer, - lastNode, - hover, -}: { - layer: number; - lastNode: boolean; - hover: boolean; -}) => { - const selectIcon = (layer: number, hover: boolean) => { - if (layer === 1) { - return ( - - ); - } - if (lastNode) { - return ; - } - return ; - }; - return <>{selectIcon(layer, hover)}; +const ItemsIcon = ({ layer, lastNode, hover }: { layer: number; lastNode: boolean; hover: boolean }) => { + const selectIcon = (layer: number, hover: boolean) => { + if (layer === 1) { + return ; + } + if (lastNode) { + return ; + } + return ; + }; + return <>{selectIcon(layer, hover)}; }; export default ItemsIcon; diff --git a/apps/uikit-playground/src/Components/DropDown/itemsStyle.ts b/apps/uikit-playground/src/Components/DropDown/itemsStyle.ts index d91dc5b80f9dd..4a50380f67b6c 100644 --- a/apps/uikit-playground/src/Components/DropDown/itemsStyle.ts +++ b/apps/uikit-playground/src/Components/DropDown/itemsStyle.ts @@ -1,47 +1,45 @@ import { css } from '@rocket.chat/css-in-js'; export const itemStyle = (layer: number, hover: boolean) => { - const style = css` - cursor: pointer !important; - padding-left: ${10 + (layer - 1) * 16}px !important; - background-color: ${hover - ? 'var(--RCPG-primary-color) !important' - : 'transparent !important'}; - `; - return style; + const style = css` + cursor: pointer !important; + padding-left: ${10 + (layer - 1) * 16}px !important; + background-color: ${hover ? 'var(--RCPG-primary-color) !important' : 'transparent !important'}; + `; + return style; }; export const labelStyle = (layer: number, hover: boolean) => { - let customStyle; - const basicStyle = css` - cursor: pointer !important; - padding-left: 4px !important; - `; - switch (layer) { - case 1: - customStyle = css` - font-weight: 700 !important; - font-size: 14px !important; - letter-spacing: 0.3px !important; - color: ${hover ? '#fff !important' : '#999 !important'}; - text-transform: uppercase !important; - `; - break; - case 2: - customStyle = css` - letter-spacing: 0.1px !important; - font-size: 12px !important; - color: ${hover ? '#fff !important' : '#555 !important'}; - text-transform: capitalize !important; - `; - break; - default: - customStyle = css` - font-size: 12px !important; - color: ${hover ? '#fff !important' : '#555 !important'}; - text-transform: capitalize !important; - `; - break; - } - return [customStyle, basicStyle]; + let customStyle; + const basicStyle = css` + cursor: pointer !important; + padding-left: 4px !important; + `; + switch (layer) { + case 1: + customStyle = css` + font-weight: 700 !important; + font-size: 14px !important; + letter-spacing: 0.3px !important; + color: ${hover ? '#fff !important' : '#999 !important'}; + text-transform: uppercase !important; + `; + break; + case 2: + customStyle = css` + letter-spacing: 0.1px !important; + font-size: 12px !important; + color: ${hover ? '#fff !important' : '#555 !important'}; + text-transform: capitalize !important; + `; + break; + default: + customStyle = css` + font-size: 12px !important; + color: ${hover ? '#fff !important' : '#555 !important'}; + text-transform: capitalize !important; + `; + break; + } + return [customStyle, basicStyle]; }; diff --git a/apps/uikit-playground/src/Components/DropDown/types.ts b/apps/uikit-playground/src/Components/DropDown/types.ts index fb50ee7f69598..47100c0e7ffc0 100644 --- a/apps/uikit-playground/src/Components/DropDown/types.ts +++ b/apps/uikit-playground/src/Components/DropDown/types.ts @@ -2,16 +2,16 @@ import type { LayoutBlock } from '@rocket.chat/ui-kit'; import type { JSX } from 'react'; export type ItemProps = { - label: string; - layer: number; - payload?: readonly LayoutBlock[]; - children?: ReadonlyArray; + label: string; + layer: number; + payload?: readonly LayoutBlock[]; + children?: ReadonlyArray; }; export type ItemBranch = { - label: string; - branches?: Item; - payload?: readonly LayoutBlock[]; + label: string; + branches?: Item; + payload?: readonly LayoutBlock[]; }; export type Item = ItemBranch[]; diff --git a/apps/uikit-playground/src/Components/FlowContainer/ConnectionLine.tsx b/apps/uikit-playground/src/Components/FlowContainer/ConnectionLine.tsx index 82fb979ff075b..d9e098e7add69 100644 --- a/apps/uikit-playground/src/Components/FlowContainer/ConnectionLine.tsx +++ b/apps/uikit-playground/src/Components/FlowContainer/ConnectionLine.tsx @@ -1,34 +1,27 @@ const ConnectionLine = ({ - fromX, - fromY, - toX, - toY, + fromX, + fromY, + toX, + toY, }: { - fromX: number; - fromY: number; - fromPosition: string; - toX: number; - toY: number; - toPosition: string; - connectionLineType: string; + fromX: number; + fromY: number; + fromPosition: string; + toX: number; + toY: number; + toPosition: string; + connectionLineType: string; }) => ( - - - - + + + + ); export default ConnectionLine; diff --git a/apps/uikit-playground/src/Components/FlowContainer/ControlButtons/ControlButtons.tsx b/apps/uikit-playground/src/Components/FlowContainer/ControlButtons/ControlButtons.tsx index ecea95fdd0678..d06bccff5c3f9 100644 --- a/apps/uikit-playground/src/Components/FlowContainer/ControlButtons/ControlButtons.tsx +++ b/apps/uikit-playground/src/Components/FlowContainer/ControlButtons/ControlButtons.tsx @@ -1,7 +1,7 @@ import { Controls } from 'reactflow'; const ControlButtons = () => { - return ; + return ; }; export default ControlButtons; diff --git a/apps/uikit-playground/src/Components/FlowContainer/ControlButtons/index.ts b/apps/uikit-playground/src/Components/FlowContainer/ControlButtons/index.ts index 63963693d46d8..9d122e2d002b2 100644 --- a/apps/uikit-playground/src/Components/FlowContainer/ControlButtons/index.ts +++ b/apps/uikit-playground/src/Components/FlowContainer/ControlButtons/index.ts @@ -1 +1 @@ -export {default} from './ControlButtons'; \ No newline at end of file +export { default } from './ControlButtons'; diff --git a/apps/uikit-playground/src/Components/FlowContainer/FlowContainer.tsx b/apps/uikit-playground/src/Components/FlowContainer/FlowContainer.tsx index 311ddaae795c7..52b3428450c28 100644 --- a/apps/uikit-playground/src/Components/FlowContainer/FlowContainer.tsx +++ b/apps/uikit-playground/src/Components/FlowContainer/FlowContainer.tsx @@ -1,117 +1,115 @@ import { useCallback, useContext, useMemo, useRef, useState } from 'react'; import ReactFlow, { - MiniMap, - Background, - addEdge, - updateEdge, - Node, - Viewport, - ReactFlowInstance, - useReactFlow, - Connection, - Edge, + MiniMap, + Background, + addEdge, + updateEdge, + type Node, + type Viewport, + type ReactFlowInstance, + useReactFlow, + type Connection, + type Edge, } from 'reactflow'; import 'reactflow/dist/style.css'; -import { context } from '../../Context'; import ConnectionLine from './ConnectionLine'; +import ControlButton from './ControlButtons'; import UIKitWrapper from './UIKitWrapper/UIKitWrapper'; import { FlowParams } from './utils'; -import ControlButton from './ControlButtons'; -import { useNodesAndEdges } from '../../hooks/useNodesAndEdges'; +import { context } from '../../Context'; import { updateNodesAndViewPortAction } from '../../Context/action/updateNodesAndViewPortAction'; +import { useNodesAndEdges } from '../../hooks/useNodesAndEdges'; const FlowContainer = () => { - const { dispatch } = useContext(context); + const { dispatch } = useContext(context); - const { nodes, edges, Viewport, onNodesChange, onEdgesChange, setEdges } = - useNodesAndEdges(); - const { setViewport } = useReactFlow(); + const { nodes, edges, Viewport, onNodesChange, onEdgesChange, setEdges } = useNodesAndEdges(); + const { setViewport } = useReactFlow(); - const nodeTypes = useMemo( - () => ({ - custom: UIKitWrapper, - }), - // used to rerender edge lines on reorder payload - // eslint-disable-next-line react-hooks/exhaustive-deps - [edges] - ); + const nodeTypes = useMemo( + () => ({ + custom: UIKitWrapper, + }), + // used to rerender edge lines on reorder payload + // eslint-disable-next-line react-hooks/exhaustive-deps + [edges], + ); - const [rfInstance, setRfInstance] = useState(); - const edgeUpdateSuccessful = useRef(true); + const [rfInstance, setRfInstance] = useState(); + const edgeUpdateSuccessful = useRef(true); - const onConnect = useCallback( - (connection: Connection) => { - if (connection.source === connection.target) return; - const newEdge = { - ...connection, - type: FlowParams.edgeType, - markerEnd: FlowParams.markerEnd, - style: FlowParams.style, - }; - setEdges((eds) => addEdge(newEdge, eds)); - }, - [setEdges] - ); + const onConnect = useCallback( + (connection: Connection) => { + if (connection.source === connection.target) return; + const newEdge = { + ...connection, + type: FlowParams.edgeType, + markerEnd: FlowParams.markerEnd, + style: FlowParams.style, + }; + setEdges((eds) => addEdge(newEdge, eds)); + }, + [setEdges], + ); - const onEdgeUpdateStart = useCallback(() => { - edgeUpdateSuccessful.current = false; - }, []); + const onEdgeUpdateStart = useCallback(() => { + edgeUpdateSuccessful.current = false; + }, []); - const onEdgeUpdate = useCallback( - (oldEdge: Edge, newConnection: Connection) => { - edgeUpdateSuccessful.current = true; - setEdges((els) => updateEdge(oldEdge, newConnection, els)); - }, - [setEdges] - ); + const onEdgeUpdate = useCallback( + (oldEdge: Edge, newConnection: Connection) => { + edgeUpdateSuccessful.current = true; + setEdges((els) => updateEdge(oldEdge, newConnection, els)); + }, + [setEdges], + ); - const onEdgeUpdateEnd = useCallback( - (_: MouseEvent | TouchEvent, edge: Edge) => { - if (!edgeUpdateSuccessful.current) { - setEdges((eds) => { - return eds.filter((e) => e.id !== edge.id); - }); - } - edgeUpdateSuccessful.current = true; - }, - [setEdges] - ); + const onEdgeUpdateEnd = useCallback( + (_: MouseEvent | TouchEvent, edge: Edge) => { + if (!edgeUpdateSuccessful.current) { + setEdges((eds) => { + return eds.filter((e) => e.id !== edge.id); + }); + } + edgeUpdateSuccessful.current = true; + }, + [setEdges], + ); - const onNodeDragStop = () => { - if (!rfInstance?.toObject()) return; - const { nodes, viewport }: { nodes: Node[]; viewport: Viewport } = - rfInstance.toObject(); - dispatch(updateNodesAndViewPortAction({ nodes, viewport })); - }; + const onNodeDragStop = () => { + if (!rfInstance?.toObject()) return; + const { nodes, viewport }: { nodes: Node[]; viewport: Viewport } = rfInstance.toObject(); + dispatch(updateNodesAndViewPortAction({ nodes, viewport })); + }; - const onInit = (instance: ReactFlowInstance) => { - setRfInstance(instance); - Viewport && setViewport(Viewport); - }; + const onInit = (instance: ReactFlowInstance) => { + setRfInstance(instance); + if (Viewport) setViewport(Viewport); + }; - return ( - - - - - - ); + return ( + + + + + + ); }; export default FlowContainer; diff --git a/apps/uikit-playground/src/Components/FlowContainer/UIKitWrapper/UIKitWrapper.tsx b/apps/uikit-playground/src/Components/FlowContainer/UIKitWrapper/UIKitWrapper.tsx index 79010f9442cc9..8c48b01528c50 100644 --- a/apps/uikit-playground/src/Components/FlowContainer/UIKitWrapper/UIKitWrapper.tsx +++ b/apps/uikit-playground/src/Components/FlowContainer/UIKitWrapper/UIKitWrapper.tsx @@ -3,51 +3,32 @@ import { useContext } from 'react'; import { Handle, Position } from 'reactflow'; import './UIKitWrapper.scss'; -import RenderPayload from '../../RenderPayload/RenderPayload'; -import SurfaceRender from '../../Preview/Display/Surface/SurfaceRender'; -import { idType } from '../../../Context/initialState'; import { context } from '../../../Context'; +import { type idType } from '../../../Context/initialState'; +import SurfaceRender from '../../Preview/Display/Surface/SurfaceRender'; +import RenderPayload from '../../RenderPayload/RenderPayload'; const UIKitWrapper = ({ id, data }: { id: string; data: idType }) => { - const { - state: { screens }, - } = useContext(context); - if (!screens[data]) return null; - const { blocks, surface } = screens[data].payload; - return ( - - - - {blocks.map((block, index) => ( - - - - - - - ))} - - - ); + const { + state: { screens }, + } = useContext(context); + if (!screens[data]) return null; + const { blocks, surface } = screens[data].payload; + return ( + + + + {blocks.map((block, index) => ( + + + + + + + ))} + + + ); }; export default UIKitWrapper; diff --git a/apps/uikit-playground/src/Components/FlowContainer/UIKitWrapper/index.ts b/apps/uikit-playground/src/Components/FlowContainer/UIKitWrapper/index.ts index 82f4f03d69d30..d6fc539add635 100644 --- a/apps/uikit-playground/src/Components/FlowContainer/UIKitWrapper/index.ts +++ b/apps/uikit-playground/src/Components/FlowContainer/UIKitWrapper/index.ts @@ -1 +1 @@ -export {default} from './UIKitWrapper'; \ No newline at end of file +export { default } from './UIKitWrapper'; diff --git a/apps/uikit-playground/src/Components/FlowContainer/utils.ts b/apps/uikit-playground/src/Components/FlowContainer/utils.ts index 2318fe1f1efcf..39200a5b3950d 100644 --- a/apps/uikit-playground/src/Components/FlowContainer/utils.ts +++ b/apps/uikit-playground/src/Components/FlowContainer/utils.ts @@ -3,32 +3,32 @@ import { MarkerType } from 'reactflow'; import type { ScreenType } from '../../Context/initialState'; export function createNodesAndEdges(screens: ScreenType[]) { - const center = { x: window.innerWidth / 2, y: window.innerHeight / 2 }; + const center = { x: window.innerWidth / 2, y: window.innerHeight / 2 }; - const nodes = screens.map((screen, i) => { - const degrees = i * (360 / 8); - const radians = degrees * (Math.PI / 180); - const x = 250 * Math.cos(radians) + center.x; - const y = 250 * Math.sin(radians) + center.y; + const nodes = screens.map((screen, i) => { + const degrees = i * (360 / 8); + const radians = degrees * (Math.PI / 180); + const x = 250 * Math.cos(radians) + center.x; + const y = 250 * Math.sin(radians) + center.y; - return { - id: screen.id, - type: 'custom', - position: { x, y }, - data: screen, - }; - }); + return { + id: screen.id, + type: 'custom', + position: { x, y }, + data: screen, + }; + }); - return { nodes }; + return { nodes }; } export const FlowParams = { - edgeType: 'smoothstep', - markerEnd: { - type: MarkerType.Arrow, - }, - style: { - strokeWidth: 2, - stroke: 'var(--RCPG-primary-color)', - }, + edgeType: 'smoothstep', + markerEnd: { + type: MarkerType.Arrow, + }, + style: { + strokeWidth: 2, + stroke: 'var(--RCPG-primary-color)', + }, }; diff --git a/apps/uikit-playground/src/Components/HomeContainer/HomeContainer.tsx b/apps/uikit-playground/src/Components/HomeContainer/HomeContainer.tsx index e6bf0e9274a76..1f05d9ba5148f 100644 --- a/apps/uikit-playground/src/Components/HomeContainer/HomeContainer.tsx +++ b/apps/uikit-playground/src/Components/HomeContainer/HomeContainer.tsx @@ -1,40 +1,32 @@ +import { css } from '@rocket.chat/css-in-js'; import { Box, Label } from '@rocket.chat/fuselage'; -import ProjectsList from './ProjectsList/ProjectsList'; import { useContext } from 'react'; + +import ProjectsList from './ProjectsList/ProjectsList'; import { context, createNewProjectAction } from '../../Context'; import CreateNewScreenButton from '../ScreenThumbnail/CreateNewScreenButton'; -import { css } from '@rocket.chat/css-in-js'; const HomeContainer = () => { - const { dispatch } = useContext(context); - return ( - - - - - dispatch(createNewProjectAction())} - /> - - - - - ); + const { dispatch } = useContext(context); + return ( + + + + + dispatch(createNewProjectAction())} /> + + + + + ); }; export default HomeContainer; diff --git a/apps/uikit-playground/src/Components/HomeContainer/ProjectsList/ProjectsList.tsx b/apps/uikit-playground/src/Components/HomeContainer/ProjectsList/ProjectsList.tsx index c767f406ad4eb..d53a080a17318 100644 --- a/apps/uikit-playground/src/Components/HomeContainer/ProjectsList/ProjectsList.tsx +++ b/apps/uikit-playground/src/Components/HomeContainer/ProjectsList/ProjectsList.tsx @@ -1,34 +1,35 @@ +import { css } from '@rocket.chat/css-in-js'; import { Box } from '@rocket.chat/fuselage'; -import { context } from '../../../Context'; import { useContext } from 'react'; + import ProjectsThumbnail from './ProjectsThumbnail'; -import { css } from '@rocket.chat/css-in-js'; +import { context } from '../../../Context'; const ProjectsList = () => { - const { - state: { screens, projects }, - } = useContext(context); + const { + state: { screens, projects }, + } = useContext(context); - return ( - - {Object.values(projects).map((project) => ( - - ))} - - ); + return ( + + {Object.values(projects).map((project) => ( + + ))} + + ); }; export default ProjectsList; diff --git a/apps/uikit-playground/src/Components/HomeContainer/ProjectsList/ProjectsThumbnail.tsx b/apps/uikit-playground/src/Components/HomeContainer/ProjectsList/ProjectsThumbnail.tsx index bcba0fca63065..f96bf299c55c8 100644 --- a/apps/uikit-playground/src/Components/HomeContainer/ProjectsList/ProjectsThumbnail.tsx +++ b/apps/uikit-playground/src/Components/HomeContainer/ProjectsList/ProjectsThumbnail.tsx @@ -1,99 +1,76 @@ +import { css } from '@rocket.chat/css-in-js'; import { Box } from '@rocket.chat/fuselage'; -import ScreenThumbnailWrapper from '../../ScreenThumbnail/ScreenThumbnailWrapper'; -import Thumbnail from '../../ScreenThumbnail/Thumbnail'; -import RenderPayload from '../../RenderPayload/RenderPayload'; -import { - activeProjectAction, - context, - renameProjectAction, -} from '../../../Context'; -import { ChangeEvent, useContext, useState } from 'react'; +import { useToastBarDispatch } from '@rocket.chat/fuselage-toastbar'; +import { type ChangeEvent, useContext, useState } from 'react'; import { useNavigate } from 'react-router-dom'; + +import { activeProjectAction, context, renameProjectAction } from '../../../Context'; +import { deleteProjectAction } from '../../../Context/action/deleteProjectAction'; +import { type ILayoutBlock } from '../../../Context/initialState'; +import routes from '../../../Routes/Routes'; import { formatDate } from '../../../utils/formatDate'; +import RenderPayload from '../../RenderPayload/RenderPayload'; import EditMenu from '../../ScreenThumbnail/EditMenu'; import EditableLabel from '../../ScreenThumbnail/EditableLabel/EditableLabel'; -import { css } from '@rocket.chat/css-in-js'; -import { deleteProjectAction } from '../../../Context/action/deleteProjectAction'; -import { useToastBarDispatch } from '@rocket.chat/fuselage-toastbar'; -import routes from '../../../Routes/Routes'; -import { ILayoutBlock } from '../../../Context/initialState'; +import ScreenThumbnailWrapper from '../../ScreenThumbnail/ScreenThumbnailWrapper'; +import Thumbnail from '../../ScreenThumbnail/Thumbnail'; -const ProjectsThumbnail = ({ - id, - name: _name, - date, - blocks, -}: { - id: string; - name: string; - date: string; - blocks: ILayoutBlock[]; -}) => { - const [name, setName] = useState(_name); - const navigate = useNavigate(); - const { dispatch } = useContext(context); - const toast = useToastBarDispatch(); - const activeProjectHandler = () => { - dispatch(activeProjectAction(id)); - navigate(`/${id}/${routes.project}`); - }; +const ProjectsThumbnail = ({ id, name: _name, date, blocks }: { id: string; name: string; date: string; blocks: ILayoutBlock[] }) => { + const [name, setName] = useState(_name); + const navigate = useNavigate(); + const { dispatch } = useContext(context); + const toast = useToastBarDispatch(); + const activeProjectHandler = () => { + dispatch(activeProjectAction(id)); + navigate(`/${id}/${routes.project}`); + }; - const deleteScreenHandler = () => { - dispatch(deleteProjectAction(id)); - }; + const deleteScreenHandler = () => { + dispatch(deleteProjectAction(id)); + }; - const onChangeNameHandler = (e: ChangeEvent) => { - setName(e.currentTarget.value); - }; + const onChangeNameHandler = (e: ChangeEvent) => { + setName(e.currentTarget.value); + }; - const nameSaveHandler = () => { - if (!name.trim()) { - setName(_name); - return toast({ - type: 'error', - message: 'Cannot rename project to empty name.', - }); - } - dispatch(renameProjectAction({ id, name })); - }; + const nameSaveHandler = () => { + if (!name.trim()) { + setName(_name); + return toast({ + type: 'error', + message: 'Cannot rename project to empty name.', + }); + } + dispatch(renameProjectAction({ id, name })); + }; - return ( - - - - - e.stopPropagation()}> - - - - {formatDate(date)} - - - ); + return ( + + + + } /> + e.stopPropagation()}> + + + + {formatDate(date)} + + + ); }; export default ProjectsThumbnail; diff --git a/apps/uikit-playground/src/Components/NavBar/BurgerIcon/BurgerIcon.tsx b/apps/uikit-playground/src/Components/NavBar/BurgerIcon/BurgerIcon.tsx index af02130ca4e00..03d895f216d71 100644 --- a/apps/uikit-playground/src/Components/NavBar/BurgerIcon/BurgerIcon.tsx +++ b/apps/uikit-playground/src/Components/NavBar/BurgerIcon/BurgerIcon.tsx @@ -2,24 +2,24 @@ import { usePrefersReducedMotion } from '@rocket.chat/fuselage-hooks'; import type { ReactElement, ReactNode } from 'react'; import { useContext } from 'react'; -import { context } from '../../../Context'; import Line from './Line'; import Wrapper from './Wrapper'; +import { context } from '../../../Context'; const BurgerIcon = ({ children }: { children?: ReactNode }): ReactElement => { - const isReducedMotionPreferred = usePrefersReducedMotion(); - const { - state: { navMenuToggle }, - } = useContext(context); + const isReducedMotionPreferred = usePrefersReducedMotion(); + const { + state: { navMenuToggle }, + } = useContext(context); - return ( - - - - - {children} - - ); + return ( + + + + + {children} + + ); }; export default BurgerIcon; diff --git a/apps/uikit-playground/src/Components/NavBar/BurgerIcon/Line.tsx b/apps/uikit-playground/src/Components/NavBar/BurgerIcon/Line.tsx index 64eb886b2967b..5e188bba2cbc4 100644 --- a/apps/uikit-playground/src/Components/NavBar/BurgerIcon/Line.tsx +++ b/apps/uikit-playground/src/Components/NavBar/BurgerIcon/Line.tsx @@ -2,51 +2,38 @@ import { css } from '@rocket.chat/css-in-js'; import { Box } from '@rocket.chat/fuselage'; import type { ReactElement } from 'react'; -const Line = ({ - animated, - moved, -}: { - animated: boolean; - moved?: boolean; -}): ReactElement => { - const animatedStyle = animated - ? css` - will-change: transform; - transition: transform 0.1s ease-out; - ` - : ''; +const Line = ({ animated, moved }: { animated: boolean; moved?: boolean }): ReactElement => { + const animatedStyle = animated + ? css` + will-change: transform; + transition: transform 0.1s ease-out; + ` + : ''; - const movedStyle = moved - ? css` - &:nth-child(1), - &:nth-child(3) { - transform-origin: 50%, 50%, 0; - } - &:nth-child(1) { - transform: translate(-25%, 3px) rotate(-45deg) scale(0.5, 1); - } - [dir='rtl'] &:nth-child(1) { - transform: translate(25%, 3px) rotate(45deg) scale(0.5, 1); - } - &:nth-child(3) { - transform: translate(-25%, -3px) rotate(45deg) scale(0.5, 1); - } - [dir='rtl'] &:nth-child(3) { - transform: translate(25%, -3px) rotate(-45deg) scale(0.5, 1); - } - ` - : ''; + const movedStyle = moved + ? css` + &:nth-child(1), + &:nth-child(3) { + transform-origin: 50%, 50%, 0; + } + &:nth-child(1) { + transform: translate(-25%, 3px) rotate(-45deg) scale(0.5, 1); + } + [dir='rtl'] &:nth-child(1) { + transform: translate(25%, 3px) rotate(45deg) scale(0.5, 1); + } + &:nth-child(3) { + transform: translate(-25%, -3px) rotate(45deg) scale(0.5, 1); + } + [dir='rtl'] &:nth-child(3) { + transform: translate(25%, -3px) rotate(-45deg) scale(0.5, 1); + } + ` + : ''; - return ( -