From 038431f1ce5306f97b802bd9c81eee3af217472c Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Mon, 20 Oct 2025 16:11:38 -0300 Subject: [PATCH 001/129] chore: add federation-sdk as dependency for meteor (#37267) --- apps/meteor/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 4334bb83b4a76..ed07f5eb0d9be 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -252,6 +252,7 @@ "@rocket.chat/emitter": "~0.31.25", "@rocket.chat/favicon": "workspace:^", "@rocket.chat/federation-matrix": "workspace:^", + "@rocket.chat/federation-sdk": "0.2.0", "@rocket.chat/freeswitch": "workspace:^", "@rocket.chat/fuselage": "~0.66.4", "@rocket.chat/fuselage-forms": "~0.1.0", From 08b2d4f154dcd68af1a579d8be492ae9eb668faf Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Mon, 20 Oct 2025 17:32:34 -0300 Subject: [PATCH 002/129] chore: new `secondary-danger` generic modal variant (#37223) Co-authored-by: Douglas Fabris <27704687+dougfabris@users.noreply.github.com> --- .../Modal/GenericModal/GenericModal.spec.tsx | 16 +++++ .../GenericModal/GenericModal.stories.tsx | 3 + .../Modal/GenericModal/GenericModal.tsx | 6 +- .../__snapshots__/GenericModal.spec.tsx.snap | 58 +++++++++++++++++++ 4 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 packages/ui-client/src/components/Modal/GenericModal/__snapshots__/GenericModal.spec.tsx.snap diff --git a/packages/ui-client/src/components/Modal/GenericModal/GenericModal.spec.tsx b/packages/ui-client/src/components/Modal/GenericModal/GenericModal.spec.tsx index 3d4b083e0c621..74e682b9988f1 100644 --- a/packages/ui-client/src/components/Modal/GenericModal/GenericModal.spec.tsx +++ b/packages/ui-client/src/components/Modal/GenericModal/GenericModal.spec.tsx @@ -1,6 +1,7 @@ import { useSetModal } from '@rocket.chat/ui-contexts'; import { act, screen, renderHook } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { axe } from 'jest-axe'; import type { ReactElement } from 'react'; import { Suspense } from 'react'; @@ -25,6 +26,21 @@ const renderModal = (modalElement: ReactElement) => { return { setModal }; }; +describe('renders', () => { + it('should render a modal without crashing', async () => { + renderModal(); + + expect(await screen.findByRole('dialog', { name: 'Modal' })).toMatchSnapshot(); + }); + + it('should have no accessibility violations when rendered', async () => { + renderModal(); + + const results = await axe(screen.getByRole('dialog', { name: 'Modal' })); + expect(results).toHaveNoViolations(); + }); +}); + describe('callbacks', () => { it('should call onClose callback when dismissed', async () => { const handleClose = jest.fn(); diff --git a/packages/ui-client/src/components/Modal/GenericModal/GenericModal.stories.tsx b/packages/ui-client/src/components/Modal/GenericModal/GenericModal.stories.tsx index 38bc99839f17b..663e9a3580266 100644 --- a/packages/ui-client/src/components/Modal/GenericModal/GenericModal.stories.tsx +++ b/packages/ui-client/src/components/Modal/GenericModal/GenericModal.stories.tsx @@ -46,6 +46,9 @@ Warning.args = { variant: 'warning' }; export const Success = Template.bind({}); Success.args = { variant: 'success' }; +export const DangerSecondary = Template.bind({}); +DangerSecondary.args = { variant: 'secondary-danger' }; + export const WithDontAskAgain: StoryFn = (args) => ; WithDontAskAgain.args = { dontAskAgain: { diff --git a/packages/ui-client/src/components/Modal/GenericModal/GenericModal.tsx b/packages/ui-client/src/components/Modal/GenericModal/GenericModal.tsx index 392f3ec8f02af..8fe78ae7fa788 100644 --- a/packages/ui-client/src/components/Modal/GenericModal/GenericModal.tsx +++ b/packages/ui-client/src/components/Modal/GenericModal/GenericModal.tsx @@ -22,7 +22,7 @@ import type { RequiredModalProps } from './withDoNotAskAgain'; import { withDoNotAskAgain } from './withDoNotAskAgain'; import { modalStore } from '../../../providers/ModalProvider/ModalStore'; -type VariantType = 'danger' | 'warning' | 'info' | 'success' | 'upsell'; +type VariantType = 'danger' | 'secondary-danger' | 'warning' | 'info' | 'success' | 'upsell'; type GenericModalProps = RequiredModalProps & { variant?: VariantType; @@ -50,6 +50,8 @@ const getButtonProps = (variant: VariantType): ComponentProps => switch (variant) { case 'danger': return { danger: true }; + case 'secondary-danger': + return { secondary: true, danger: true }; case 'warning': case 'upsell': return { primary: true }; @@ -59,7 +61,7 @@ const getButtonProps = (variant: VariantType): ComponentProps => }; const renderIcon = (icon: GenericModalProps['icon'], variant: VariantType): ReactNode => { - if (icon === null) { + if (icon === null || iconMap[variant] === undefined) { return null; } diff --git a/packages/ui-client/src/components/Modal/GenericModal/__snapshots__/GenericModal.spec.tsx.snap b/packages/ui-client/src/components/Modal/GenericModal/__snapshots__/GenericModal.spec.tsx.snap new file mode 100644 index 0000000000000..f73c133b38b63 --- /dev/null +++ b/packages/ui-client/src/components/Modal/GenericModal/__snapshots__/GenericModal.spec.tsx.snap @@ -0,0 +1,58 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`renders should render a modal without crashing 1`] = ` + +
+
+
+
+ +
+
+

+ Modal +

+
+
+
+
+
+
+ +
+`; From 7a7aad5cb1191c0eca61a22dadd620bd4f6c12a2 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Mon, 20 Oct 2025 20:19:09 -0300 Subject: [PATCH 003/129] fix(apps): prevent installation invalidation on app cron updates (#37152) --- .changeset/rotten-jars-occur.md | 6 + packages/apps-engine/src/server/AppManager.ts | 11 +- .../tests/server/AppManager.spec.ts | 142 +++++++++++++++++- .../server/managers/AppApiManager.spec.ts | 4 +- .../server/managers/AppSlashCommand.spec.ts | 3 +- .../managers/AppSlashCommandManager.spec.ts | 4 +- .../AppVideoConfProviderManager.spec.ts | 4 +- .../tests/test-data/storage/storage.ts | 41 +++-- .../apps-engine/tests/test-data/utilities.ts | 100 +++++++++++- 9 files changed, 286 insertions(+), 29 deletions(-) create mode 100644 .changeset/rotten-jars-occur.md diff --git a/.changeset/rotten-jars-occur.md b/.changeset/rotten-jars-occur.md new file mode 100644 index 0000000000000..e11d510d2614c --- /dev/null +++ b/.changeset/rotten-jars-occur.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/apps-engine': patch +'@rocket.chat/meteor': patch +--- + +Fixes a bug that would cause apps to go into `invalid_installation_disabled` in some cases diff --git a/packages/apps-engine/src/server/AppManager.ts b/packages/apps-engine/src/server/AppManager.ts index 1a0480c54c144..173dfc18fbd76 100644 --- a/packages/apps-engine/src/server/AppManager.ts +++ b/packages/apps-engine/src/server/AppManager.ts @@ -909,10 +909,15 @@ export class AppManager { } appStorageItem.marketplaceInfo[0].subscriptionInfo = appInfo.subscriptionInfo; + appStorageItem.signature = await this.getSignatureManager().signApp(appStorageItem); - return this.appMetadataStorage.updateMarketplaceInfo(appStorageItem._id, appStorageItem.marketplaceInfo); + return this.appMetadataStorage.updatePartialAndReturnDocument({ + _id: appStorageItem._id, + marketplaceInfo: appStorageItem.marketplaceInfo, + signature: appStorageItem.signature, + }); }), - ).catch(); + ).catch(() => {}); const queue = [] as Array>; @@ -933,7 +938,7 @@ export class AppManager { return; } - await this.purgeAppConfig(app); + await this.purgeAppConfig(app, { keepScheduledJobs: true }); return app.setStatus(AppStatus.INVALID_LICENSE_DISABLED); }) diff --git a/packages/apps-engine/tests/server/AppManager.spec.ts b/packages/apps-engine/tests/server/AppManager.spec.ts index 1f6ef3e4a18f2..b31900cb76301 100644 --- a/packages/apps-engine/tests/server/AppManager.spec.ts +++ b/packages/apps-engine/tests/server/AppManager.spec.ts @@ -1,4 +1,4 @@ -import { Expect, SetupFixture, Teardown, Test } from 'alsatian'; +import { AsyncTest, Expect, SetupFixture, SpyOn, Teardown, Test } from 'alsatian'; import { AppManager } from '../../src/server/AppManager'; import { AppBridges } from '../../src/server/bridges'; @@ -14,7 +14,7 @@ import { AppOutboundCommunicationProviderManager, } from '../../src/server/managers'; import type { AppLogStorage, AppMetadataStorage, AppSourceStorage } from '../../src/server/storage'; -import { SimpleClass, TestInfastructureSetup } from '../test-data/utilities'; +import { SimpleClass, TestData, TestInfastructureSetup } from '../test-data/utilities'; export class AppManagerTestFixture { private testingInfastructure: TestInfastructureSetup; @@ -121,4 +121,142 @@ export class AppManagerTestFixture { Expect(manager.getVideoConfProviderManager() instanceof AppVideoConfProviderManager).toBe(true); Expect(manager.getOutboundCommunicationProviderManager() instanceof AppOutboundCommunicationProviderManager).toBe(true); } + + @AsyncTest('Update Apps Marketplace Info - Apps without subscription info are skipped') + public async updateAppsMarketplaceInfoSkipsAppsWithoutSubscriptionInfo() { + const manager = new AppManager({ + metadataStorage: this.testingInfastructure.getAppStorage(), + logStorage: this.testingInfastructure.getLogStorage(), + bridges: this.testingInfastructure.getAppBridges(), + sourceStorage: this.testingInfastructure.getSourceStorage(), + }); + + const appsOverview = TestData.getAppsOverview(); + appsOverview[0].latest.subscriptionInfo = undefined; // No subscription info + + // Mock the apps Map to return our mock app + (manager as any).apps = new Map([['test-app', TestData.getMockApp(TestData.getAppStorageItem(), manager)]]); + + const updatePartialAndReturnDocumentSpy = SpyOn(manager.getStorage(), 'updatePartialAndReturnDocument'); + updatePartialAndReturnDocumentSpy.andReturn(Promise.resolve()); + + // Should not throw and complete successfully + await manager.updateAppsMarketplaceInfo(appsOverview); + + Expect(updatePartialAndReturnDocumentSpy).not.toHaveBeenCalled(); + } + + @AsyncTest('Update Apps Marketplace Info - Apps not found in manager are skipped') + public async updateAppsMarketplaceInfoSkipsAppsNotInManager() { + const manager = new AppManager({ + metadataStorage: this.testingInfastructure.getAppStorage(), + logStorage: this.testingInfastructure.getLogStorage(), + bridges: this.testingInfastructure.getAppBridges(), + sourceStorage: this.testingInfastructure.getSourceStorage(), + }); + + const appsOverview = TestData.getAppsOverview(); + appsOverview[0].latest.id = 'nonexistent-app'; // App not in manager + + // Mock the apps Map to return our mock app + (manager as any).apps = new Map([['test-app', TestData.getMockApp(TestData.getAppStorageItem(), manager)]]); + + const updatePartialAndReturnDocumentSpy = SpyOn(manager.getStorage(), 'updatePartialAndReturnDocument'); + updatePartialAndReturnDocumentSpy.andReturn(Promise.resolve()); + + // Should not throw and complete successfully + await manager.updateAppsMarketplaceInfo(appsOverview); + + Expect(updatePartialAndReturnDocumentSpy).not.toHaveBeenCalled(); + } + + @AsyncTest('Update Apps Marketplace Info - Apps with same license are skipped') + public async updateAppsMarketplaceInfoSkipsAppsWithSameLicense() { + const manager = new AppManager({ + metadataStorage: this.testingInfastructure.getAppStorage(), + logStorage: this.testingInfastructure.getLogStorage(), + bridges: this.testingInfastructure.getAppBridges(), + sourceStorage: this.testingInfastructure.getSourceStorage(), + }); + + const sameLicenseData = 'same-license-data'; + const existingSubscriptionInfo = TestData.getMarketplaceSubscriptionInfo({ + license: { license: sameLicenseData, version: 1, expireDate: new Date('2023-01-01') }, + }); + + const mockStorageItem = TestData.getAppStorageItem({ + marketplaceInfo: [TestData.getMarketplaceInfo({ subscriptionInfo: existingSubscriptionInfo })], + }); + + const mockApp = TestData.getMockApp(mockStorageItem, manager); + + // Mock the apps Map to return our mock app + (manager as any).apps = new Map([['test-app', mockApp]]); + + const appsOverview = TestData.getAppsOverview( + TestData.getMarketplaceSubscriptionInfo({ + license: { license: sameLicenseData, version: 1, expireDate: new Date('2023-01-01') }, + }), + ); + + const updatePartialAndReturnDocumentSpy = SpyOn(manager.getStorage(), 'updatePartialAndReturnDocument'); + updatePartialAndReturnDocumentSpy.andReturn(Promise.resolve()); + + // Should not throw and complete successfully + await manager.updateAppsMarketplaceInfo(appsOverview); + + // Verify the subscription info was not updated (should remain the same) + Expect(mockStorageItem.marketplaceInfo[0].subscriptionInfo.seats).toBe(10); // Original value + Expect(updatePartialAndReturnDocumentSpy).not.toHaveBeenCalled(); + } + + @AsyncTest('Update Apps Marketplace Info - Subscription info is updated and app is signed') + public async updateAppsMarketplaceInfoUpdatesSubscriptionAndSignsApp() { + const manager = new AppManager({ + metadataStorage: this.testingInfastructure.getAppStorage(), + logStorage: this.testingInfastructure.getLogStorage(), + bridges: this.testingInfastructure.getAppBridges(), + sourceStorage: this.testingInfastructure.getSourceStorage(), + }); + + const existingSubscriptionInfo = TestData.getMarketplaceSubscriptionInfo({ + license: { license: 'old-license-data', version: 1, expireDate: new Date('2023-01-01') }, + }); + + const newSubscriptionInfo = TestData.getMarketplaceSubscriptionInfo({ + seats: 20, + maxSeats: 200, + startDate: '2023-02-01', + periodEnd: '2024-01-31', + license: { license: 'new-license-data', version: 1, expireDate: new Date('2026-01-01') }, + }); + + const mockStorageItem = TestData.getAppStorageItem({ + marketplaceInfo: [TestData.getMarketplaceInfo({ subscriptionInfo: existingSubscriptionInfo })], + }); + + const mockApp = TestData.getMockApp(mockStorageItem, manager); + + // eslint-disable-next-line no-return-assign + SpyOn(manager.getSignatureManager(), 'signApp').andReturn(Promise.resolve('signed-app-data')); + SpyOn(mockApp, 'validateLicense').andReturn(Promise.resolve()); + + const updatePartialAndReturnDocumentSpy = SpyOn(manager.getStorage(), 'updatePartialAndReturnDocument'); + updatePartialAndReturnDocumentSpy.andReturn(Promise.resolve(mockStorageItem)); + + // Mock the apps Map and dependencies + (manager as any).apps = new Map([['test-app', mockApp]]); + + const appsOverview = TestData.getAppsOverview(newSubscriptionInfo); + + await manager.updateAppsMarketplaceInfo(appsOverview); + + const expectedStorageItem = mockApp.getStorageItem(); + + // Verify the subscription info was updated + Expect(expectedStorageItem.marketplaceInfo[0].subscriptionInfo.seats).toBe(20); + Expect(expectedStorageItem.marketplaceInfo[0].subscriptionInfo.license.license).toBe('new-license-data'); + Expect(expectedStorageItem.signature).toBe('signed-app-data'); + Expect(updatePartialAndReturnDocumentSpy).toHaveBeenCalled().exactly(1).times; + } } diff --git a/packages/apps-engine/tests/server/managers/AppApiManager.spec.ts b/packages/apps-engine/tests/server/managers/AppApiManager.spec.ts index ed683a9c6931a..7fb250b154e07 100644 --- a/packages/apps-engine/tests/server/managers/AppApiManager.spec.ts +++ b/packages/apps-engine/tests/server/managers/AppApiManager.spec.ts @@ -16,7 +16,7 @@ import type { import { AppAccessorManager, AppApiManager } from '../../../src/server/managers'; import { AppApi } from '../../../src/server/managers/AppApi'; import type { UIActionButtonManager } from '../../../src/server/managers/UIActionButtonManager'; -import type { AppLogStorage } from '../../../src/server/storage'; +import type { AppLogStorage, IAppStorageItem } from '../../../src/server/storage'; import { TestsAppBridges } from '../../test-data/bridges/appBridges'; import { TestsAppLogStorage } from '../../test-data/storage/logStorage'; import { TestData } from '../../test-data/utilities'; @@ -38,7 +38,7 @@ export class AppApiManagerTestFixture { public setupFixture() { this.mockBridges = new TestsAppBridges(); - this.mockApp = TestData.getMockApp({ id: 'testing', name: 'TestApp' }, this.mockManager); + this.mockApp = TestData.getMockApp({ info: { id: 'testing', name: 'TestApp' } } as IAppStorageItem, this.mockManager); const bri = this.mockBridges; const app = this.mockApp; diff --git a/packages/apps-engine/tests/server/managers/AppSlashCommand.spec.ts b/packages/apps-engine/tests/server/managers/AppSlashCommand.spec.ts index f6eda56c6fb02..7e4ed81caeeb8 100644 --- a/packages/apps-engine/tests/server/managers/AppSlashCommand.spec.ts +++ b/packages/apps-engine/tests/server/managers/AppSlashCommand.spec.ts @@ -1,5 +1,6 @@ import { Expect, SetupFixture, Test } from 'alsatian'; +import type { IAppStorageItem } from '../../../server/storage'; import type { ISlashCommand } from '../../../src/definition/slashcommands'; import type { AppManager } from '../../../src/server/AppManager'; import type { ProxiedApp } from '../../../src/server/ProxiedApp'; @@ -11,7 +12,7 @@ export class AppSlashCommandRegistrationTestFixture { @SetupFixture public setupFixture() { - this.mockApp = TestData.getMockApp({ id: 'test', name: 'TestApp' }, {} as AppManager); + this.mockApp = TestData.getMockApp({ info: { id: 'test', name: 'TestApp' } } as IAppStorageItem, {} as AppManager); } @Test() diff --git a/packages/apps-engine/tests/server/managers/AppSlashCommandManager.spec.ts b/packages/apps-engine/tests/server/managers/AppSlashCommandManager.spec.ts index 204e31e21fee1..f8e648625b765 100644 --- a/packages/apps-engine/tests/server/managers/AppSlashCommandManager.spec.ts +++ b/packages/apps-engine/tests/server/managers/AppSlashCommandManager.spec.ts @@ -17,7 +17,7 @@ import { AppAccessorManager, AppSlashCommandManager } from '../../../src/server/ import { AppSlashCommand } from '../../../src/server/managers/AppSlashCommand'; import type { UIActionButtonManager } from '../../../src/server/managers/UIActionButtonManager'; import { Room } from '../../../src/server/rooms/Room'; -import type { AppLogStorage } from '../../../src/server/storage'; +import type { AppLogStorage, IAppStorageItem } from '../../../src/server/storage'; import { TestsAppBridges } from '../../test-data/bridges/appBridges'; import { TestsAppLogStorage } from '../../test-data/storage/logStorage'; import { TestData } from '../../test-data/utilities'; @@ -39,7 +39,7 @@ export class AppSlashCommandManagerTestFixture { public setupFixture() { this.mockBridges = new TestsAppBridges(); - this.mockApp = TestData.getMockApp({ id: 'testing', name: 'TestApp' }, this.mockManager); + this.mockApp = TestData.getMockApp({ info: { id: 'testing', name: 'TestApp' } } as IAppStorageItem, this.mockManager); const bri = this.mockBridges; const app = this.mockApp; diff --git a/packages/apps-engine/tests/server/managers/AppVideoConfProviderManager.spec.ts b/packages/apps-engine/tests/server/managers/AppVideoConfProviderManager.spec.ts index 2ec03c61119a4..b78bca29888d8 100644 --- a/packages/apps-engine/tests/server/managers/AppVideoConfProviderManager.spec.ts +++ b/packages/apps-engine/tests/server/managers/AppVideoConfProviderManager.spec.ts @@ -8,7 +8,7 @@ import type { AppApiManager, AppExternalComponentManager, AppSchedulerManager, A import { AppAccessorManager, AppVideoConfProviderManager } from '../../../src/server/managers'; import { AppVideoConfProvider } from '../../../src/server/managers/AppVideoConfProvider'; import type { UIActionButtonManager } from '../../../src/server/managers/UIActionButtonManager'; -import type { AppLogStorage } from '../../../src/server/storage'; +import type { AppLogStorage, IAppStorageItem } from '../../../src/server/storage'; import { TestsAppBridges } from '../../test-data/bridges/appBridges'; import { TestsAppLogStorage } from '../../test-data/storage/logStorage'; import { TestData } from '../../test-data/utilities'; @@ -28,7 +28,7 @@ export class AppVideoConfProviderManagerTestFixture { public setupFixture() { this.mockBridges = new TestsAppBridges(); - this.mockApp = TestData.getMockApp({ id: 'testing', name: 'testing' }, this.mockManager); + this.mockApp = TestData.getMockApp({ info: { id: 'testing', name: 'testing' } } as IAppStorageItem, this.mockManager); const bri = this.mockBridges; const app = this.mockApp; diff --git a/packages/apps-engine/tests/test-data/storage/storage.ts b/packages/apps-engine/tests/test-data/storage/storage.ts index 8412e67c65e24..dbab953c6487e 100644 --- a/packages/apps-engine/tests/test-data/storage/storage.ts +++ b/packages/apps-engine/tests/test-data/storage/storage.ts @@ -1,3 +1,7 @@ +import type { AppStatus } from '../../../src/definition/AppStatus'; +import type { IAppInfo } from '../../../src/definition/metadata'; +import type { ISetting } from '../../../src/definition/settings'; +import type { IMarketplaceInfo } from '../../../src/server/marketplace'; import type { IAppStorageItem } from '../../../src/server/storage'; import { AppMetadataStorage } from '../../../src/server/storage'; @@ -82,20 +86,6 @@ export class TestsAppStorage extends AppMetadataStorage { }); } - public update(item: IAppStorageItem): Promise { - return new Promise((resolve, reject) => { - this.db.update({ id: item.id }, item, {}, (err, _numOfUpdated: number) => { - if (err) { - reject(err); - } else { - this.retrieveOne(item.id) - .then((updated: IAppStorageItem) => resolve(updated)) - .catch((err2: Error) => reject(err2)); - } - }); - }); - } - public remove(id: string): Promise<{ success: boolean }> { return new Promise((resolve, reject) => { this.db.remove({ id }, (err) => { @@ -107,4 +97,27 @@ export class TestsAppStorage extends AppMetadataStorage { }); }); } + + public updatePartialAndReturnDocument( + item: Partial, + options?: { unsetPermissionsGranted?: boolean }, + ): Promise { + throw new Error('Method not implemented.'); + } + + public updateStatus(_id: string, status: AppStatus): Promise { + throw new Error('Method not implemented.'); + } + + public updateSetting(_id: string, setting: ISetting): Promise { + throw new Error('Method not implemented.'); + } + + public updateAppInfo(_id: string, info: IAppInfo): Promise { + throw new Error('Method not implemented.'); + } + + public updateMarketplaceInfo(_id: string, marketplaceInfo: IMarketplaceInfo[]): Promise { + throw new Error('Method not implemented.'); + } } diff --git a/packages/apps-engine/tests/test-data/utilities.ts b/packages/apps-engine/tests/test-data/utilities.ts index 049d346ed25a4..9cd3135ed061c 100644 --- a/packages/apps-engine/tests/test-data/utilities.ts +++ b/packages/apps-engine/tests/test-data/utilities.ts @@ -51,8 +51,13 @@ import type { } from '../../src/server/managers'; import type { AppRuntimeManager } from '../../src/server/managers/AppRuntimeManager'; import type { UIActionButtonManager } from '../../src/server/managers/UIActionButtonManager'; +import type { IMarketplaceInfo, IMarketplaceSubscriptionInfo } from '../../src/server/marketplace'; +import { MarketplacePurchaseType } from '../../src/server/marketplace/MarketplacePurchaseType'; +import { MarketplaceSubscriptionStatus } from '../../src/server/marketplace/MarketplaceSubscriptionStatus'; +import { MarketplaceSubscriptionType } from '../../src/server/marketplace/MarketplaceSubscriptionType'; import type { IRuntimeController } from '../../src/server/runtime/IRuntimeController'; import type { AppLogStorage, AppMetadataStorage, AppSourceStorage, IAppStorageItem } from '../../src/server/storage'; +import { AppInstallationSource } from '../../src/server/storage/IAppStorageItem'; export class TestInfastructureSetup { private appStorage: TestsAppStorage; @@ -100,7 +105,9 @@ export class TestInfastructureSetup { return {} as AppExternalComponentManager; }, getOneById(appId: string): ProxiedApp { - return appId === 'failMePlease' ? undefined : TestData.getMockApp({ id: appId, name: 'testing' }, this); + return appId === 'failMePlease' + ? undefined + : TestData.getMockApp({ info: { id: appId, name: 'testing' } } as IAppStorageItem, this); }, getLogStorage(): AppLogStorage { return new TestsAppLogStorage(); @@ -601,13 +608,100 @@ export class TestData { return mock; } - public static getMockApp({ id, name }: { id: string; name: string }, manager: AppManager): ProxiedApp { + public static getMockApp(storageItem: Partial, manager: AppManager): ProxiedApp { + const { id, name } = storageItem.info || { id: 'test-app', name: 'Test App' }; + return new ProxiedApp( manager, - { id, status: AppStatus.AUTO_ENABLED, info: { id, name } } as IAppStorageItem, + { id, status: AppStatus.AUTO_ENABLED, info: { id, name }, ...storageItem } as IAppStorageItem, TestData.getMockRuntimeController(id), ); } + + public static getMarketplaceSubscriptionInfo(overrides: Partial = {}): IMarketplaceSubscriptionInfo { + return { + seats: 10, + maxSeats: 100, + startDate: '2023-01-01', + periodEnd: '2023-12-31', + isSubscripbedViaBundle: false, + typeOf: MarketplaceSubscriptionType.SubscriptionTypeApp, + status: MarketplaceSubscriptionStatus.PurchaseSubscriptionStatusActive, + license: { + license: 'encrypted-license-data', + version: 1, + expireDate: new Date('2023-01-01'), + }, + ...overrides, + }; + } + + public static getMarketplaceInfo(overrides: Partial = {}): IMarketplaceInfo { + return { + id: 'test-app', + name: 'Test App', + nameSlug: 'test-app', + version: '1.0.0', + description: 'Test app', + author: { name: 'Test Author', support: 'https://test.com', homepage: 'https://test.com' }, + permissions: [], + requiredApiVersion: '1.0.0', + classFile: 'main.js', + iconFile: 'icon.png', + implements: [], + categories: [], + status: 'active', + isVisible: true, + isPurchased: false, + isSubscribed: false, + isBundled: false, + createdDate: '2023-01-01', + modifiedDate: '2023-01-01', + price: 0, + purchaseType: MarketplacePurchaseType.PurchaseTypeSubscription, + subscriptionInfo: TestData.getMarketplaceSubscriptionInfo(), + ...overrides, + }; + } + + public static getAppStorageItem(overrides: Partial = {}): IAppStorageItem { + return { + id: 'test-app', + status: AppStatus.AUTO_ENABLED, + info: { + id: 'test-app', + name: 'Test App', + nameSlug: 'test-app', + version: '1.0.0', + description: 'Test app', + author: { name: 'Test Author', support: 'https://test.com', homepage: 'https://test.com' }, + permissions: [], + requiredApiVersion: '1.0.0', + classFile: 'main.js', + iconFile: 'icon.png', + implements: [], + }, + marketplaceInfo: [TestData.getMarketplaceInfo()], + createdAt: new Date(), + updatedAt: new Date(), + installationSource: AppInstallationSource.MARKETPLACE, + languageContent: {}, + settings: {}, + implemented: {}, + signature: 'default-signature', + ...overrides, + }; + } + + public static getAppsOverview(subscriptionInfo?: IMarketplaceSubscriptionInfo): Array<{ latest: IMarketplaceInfo }> { + return [ + { + latest: TestData.getMarketplaceInfo({ + subscriptionInfo: subscriptionInfo || TestData.getMarketplaceSubscriptionInfo(), + }), + }, + ]; + } } export class SimpleClass { From 0870d729214092d7be3dd74572cb0eb10598ccac Mon Sep 17 00:00:00 2001 From: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Date: Mon, 20 Oct 2025 23:27:56 -0300 Subject: [PATCH 004/129] feat: Replace old voice extension assingment flow (#37245) --- .changeset/shy-bats-worry.md | 5 + apps/meteor/app/api/server/v1/users.ts | 5 + .../server/functions/saveUser/saveNewUser.ts | 4 + .../lib/server/functions/saveUser/saveUser.ts | 10 + .../functions/saveUser/validateUserEditing.ts | 27 ++ .../components/UserInfo/UserInfo.stories.tsx | 5 + .../client/components/UserInfo/UserInfo.tsx | 9 + .../__snapshots__/UserInfo.spec.tsx.snap | 275 ++++++++++++++++++ .../views/admin/users/AdminUserForm.tsx | 22 +- .../admin/users/AdminUserInfoWithData.tsx | 2 + .../users/UsersPageHeaderContent.spec.tsx | 33 --- .../admin/users/UsersPageHeaderContent.tsx | 5 - .../users/UsersTable/UsersTable.spec.tsx | 137 +-------- .../admin/users/UsersTable/UsersTable.tsx | 2 +- .../admin/users/UsersTable/UsersTableRow.tsx | 11 - .../hooks => }/useVoipExtensionPermission.tsx | 0 .../users/voip/AssignExtensionButton.tsx | 24 -- .../users/voip/AssignExtensionModal.spec.tsx | 123 -------- .../admin/users/voip/AssignExtensionModal.tsx | 164 ----------- .../users/voip/RemoveExtensionModal.spec.tsx | 49 ---- .../admin/users/voip/RemoveExtensionModal.tsx | 98 ------- .../voip/hooks/useVoipExtensionAction.tsx | 36 --- .../UserInfo/UserInfoWithData.tsx | 2 + .../api-enterprise/server/voip-freeswitch.ts | 4 + apps/meteor/tests/data/users.helper.ts | 1 + apps/meteor/tests/end-to-end/api/users.ts | 203 +++++++++++++ .../src/v1/users/UserCreateParamsPOST.ts | 2 + .../src/v1/users/UsersUpdateParamsPOST.ts | 5 + 28 files changed, 582 insertions(+), 681 deletions(-) create mode 100644 .changeset/shy-bats-worry.md rename apps/meteor/client/views/admin/users/{voip/hooks => }/useVoipExtensionPermission.tsx (100%) delete mode 100644 apps/meteor/client/views/admin/users/voip/AssignExtensionButton.tsx delete mode 100644 apps/meteor/client/views/admin/users/voip/AssignExtensionModal.spec.tsx delete mode 100644 apps/meteor/client/views/admin/users/voip/AssignExtensionModal.tsx delete mode 100644 apps/meteor/client/views/admin/users/voip/RemoveExtensionModal.spec.tsx delete mode 100644 apps/meteor/client/views/admin/users/voip/RemoveExtensionModal.tsx delete mode 100644 apps/meteor/client/views/admin/users/voip/hooks/useVoipExtensionAction.tsx diff --git a/.changeset/shy-bats-worry.md b/.changeset/shy-bats-worry.md new file mode 100644 index 0000000000000..8615105495228 --- /dev/null +++ b/.changeset/shy-bats-worry.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": minor +--- + +Replaces old `Assign Extension` button and modal by introducing a proper input in the user edit form. diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts index b418d555761fd..048b289b3fcec 100644 --- a/apps/meteor/app/api/server/v1/users.ts +++ b/apps/meteor/app/api/server/v1/users.ts @@ -56,6 +56,7 @@ import { saveCustomFields } from '../../../lib/server/functions/saveCustomFields import { saveCustomFieldsWithoutValidation } from '../../../lib/server/functions/saveCustomFieldsWithoutValidation'; import { saveUser } from '../../../lib/server/functions/saveUser'; import { sendWelcomeEmail } from '../../../lib/server/functions/saveUser/sendUserEmail'; +import { canEditExtension } from '../../../lib/server/functions/saveUser/validateUserEditing'; import { setStatusText } from '../../../lib/server/functions/setStatusText'; import { setUserAvatar } from '../../../lib/server/functions/setUserAvatar'; import { setUsernameWithValidation } from '../../../lib/server/functions/setUsername'; @@ -325,6 +326,10 @@ API.v1.addRoute( validateCustomFields(this.bodyParams.customFields); } + if (this.bodyParams.freeSwitchExtension && !(await canEditExtension(this.userId, this.bodyParams.freeSwitchExtension))) { + return API.v1.failure('Setting user voice call extension is not allowed', 'error-action-not-allowed'); + } + const newUserId = await saveUser(this.userId, this.bodyParams); const userId = typeof newUserId !== 'string' ? this.userId : newUserId; diff --git a/apps/meteor/app/lib/server/functions/saveUser/saveNewUser.ts b/apps/meteor/app/lib/server/functions/saveUser/saveNewUser.ts index bb9a6a1cac672..508472110fc32 100644 --- a/apps/meteor/app/lib/server/functions/saveUser/saveNewUser.ts +++ b/apps/meteor/app/lib/server/functions/saveUser/saveNewUser.ts @@ -47,6 +47,10 @@ export const saveNewUser = async function (userData: SaveUserData, sendPassword: updater.set('emails.0.verified', userData.verified); } + if (typeof userData.freeSwitchExtension === 'string' && userData.freeSwitchExtension !== '') { + updater.set('freeSwitchExtension', userData.freeSwitchExtension); + } + handleBio(updater, userData.bio); handleNickname(updater, userData.nickname); diff --git a/apps/meteor/app/lib/server/functions/saveUser/saveUser.ts b/apps/meteor/app/lib/server/functions/saveUser/saveUser.ts index 13884b6e34b9b..e77e56cc84b9d 100644 --- a/apps/meteor/app/lib/server/functions/saveUser/saveUser.ts +++ b/apps/meteor/app/lib/server/functions/saveUser/saveUser.ts @@ -53,6 +53,8 @@ export type SaveUserData = { customFields?: Record; active?: boolean; + + freeSwitchExtension?: string; }; export type UpdateUserData = RequiredField; export const isUpdateUserData = (params: SaveUserData): params is UpdateUserData => '_id' in params && !!params._id; @@ -162,6 +164,14 @@ const _saveUser = (session?: ClientSession) => } } + if (typeof userData.freeSwitchExtension === 'string' && userData.freeSwitchExtension !== (oldUserData?.freeSwitchExtension ?? '')) { + if (userData.freeSwitchExtension.trim() === '') { + updater.unset('freeSwitchExtension'); + } else { + updater.set('freeSwitchExtension', userData.freeSwitchExtension); + } + } + if (typeof userData.verified === 'boolean') { if (oldUserData && 'emails' in oldUserData && oldUserData.emails?.some(({ address }) => address === userData.email)) { const index = oldUserData.emails.findIndex(({ address }) => address === userData.email); diff --git a/apps/meteor/app/lib/server/functions/saveUser/validateUserEditing.ts b/apps/meteor/app/lib/server/functions/saveUser/validateUserEditing.ts index c9f3ddbe296ac..2fd4a3f0e6f42 100644 --- a/apps/meteor/app/lib/server/functions/saveUser/validateUserEditing.ts +++ b/apps/meteor/app/lib/server/functions/saveUser/validateUserEditing.ts @@ -1,3 +1,4 @@ +/* eslint-disable complexity */ import { MeteorError } from '@rocket.chat/core-services'; import type { IUser } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; @@ -11,6 +12,22 @@ const isEditingUserRoles = (previousRoles: IUser['roles'], newRoles?: IUser['rol (newRoles.some((item) => !previousRoles.includes(item)) || previousRoles.some((item) => !newRoles.includes(item))); const isEditingField = (previousValue?: string, newValue?: string) => typeof newValue !== 'undefined' && newValue !== previousValue; +export const canEditExtension = async (userId: string, newExtension?: string) => { + if (!settings.get('VoIP_TeamCollab_Enabled')) { + return false; + } + + if (!(await hasPermissionAsync(userId, 'manage-voip-extensions'))) { + return false; + } + + if (newExtension && (await Users.findOneByFreeSwitchExtension(newExtension, { projection: { _id: 1 } }))) { + throw new MeteorError('error-extension-not-available', 'Extension is already assigned to another user'); + } + + return true; +}; + /** * Validate permissions to edit user fields * @@ -97,4 +114,14 @@ export async function validateUserEditing(userId: IUser['_id'], userData: Update action: 'Update_user', }); } + + if ( + isEditingField(user.freeSwitchExtension ?? '', userData.freeSwitchExtension) && + !(await canEditExtension(userId, userData.freeSwitchExtension)) + ) { + throw new MeteorError('error-action-not-allowed', 'Edit user voice call extension is not allowed', { + method: 'insertOrUpdateUser', + action: 'Update_user', + }); + } } diff --git a/apps/meteor/client/components/UserInfo/UserInfo.stories.tsx b/apps/meteor/client/components/UserInfo/UserInfo.stories.tsx index 9e76aa0064590..cf8aa0baad335 100644 --- a/apps/meteor/client/components/UserInfo/UserInfo.stories.tsx +++ b/apps/meteor/client/components/UserInfo/UserInfo.stories.tsx @@ -35,6 +35,11 @@ const Template: StoryFn = (args) => >; @@ -71,6 +72,7 @@ const UserInfo = ({ canViewAllInfo, actions, reason, + freeSwitchExtension, // @ts-expect-error - abacAttributes is not yet implemented in Users properties abacAttributes = null, ...props @@ -180,6 +182,13 @@ const UserInfo = ({ )} + {freeSwitchExtension && ( + + {t('Voice_call_extension')} + {freeSwitchExtension} + + )} + {abacAttributes?.length > 0 && ( {t('ABAC_Attributes')} 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 33c5d0d0d537b..3a519a834fa8f 100644 --- a/apps/meteor/client/components/UserInfo/__snapshots__/UserInfo.spec.tsx.snap +++ b/apps/meteor/client/components/UserInfo/__snapshots__/UserInfo.spec.tsx.snap @@ -572,3 +572,278 @@ exports[`renders WithABACAttributes without crashing 1`] = ` `; + +exports[`renders WithVoiceCallExtension without crashing 1`] = ` + +
+
+ +`; + +exports[`Story Tests renders Default without crashing 1`] = ` + +
+ +
+
+
+
+

+ Insert_timestamp +

+
+ +
+
+
+
+
+
+
+ + + + +
+
+
+
+ + + + +
+
+
+
+ + + + +
+
+
+
+ + + + +
+
+
+
+ + +
+ Mocked GazzodownText +
+
+
+
+
+
+
+ +
+
+
+ +`; diff --git a/apps/meteor/client/components/message/toolbar/items/actions/Timestamp/TimestampPicker/index.ts b/apps/meteor/client/components/message/toolbar/items/actions/Timestamp/TimestampPicker/index.ts new file mode 100644 index 0000000000000..a497f65b3bf6c --- /dev/null +++ b/apps/meteor/client/components/message/toolbar/items/actions/Timestamp/TimestampPicker/index.ts @@ -0,0 +1 @@ +export * from './TimestampPickerModal'; diff --git a/apps/meteor/client/lib/utils/timestamp/conversion.ts b/apps/meteor/client/lib/utils/timestamp/conversion.ts new file mode 100644 index 0000000000000..f556297269947 --- /dev/null +++ b/apps/meteor/client/lib/utils/timestamp/conversion.ts @@ -0,0 +1,30 @@ +import { UTCOffsets, type TimestampFormat, type TimezoneKey } from './types'; + +export const dateToISOString = (date: Date, timezone?: TimezoneKey): string => { + if (!date || isNaN(date.getTime())) { + return ''; + } + + if (timezone && timezone !== 'local') { + const offset = UTCOffsets[timezone]; + if (offset) { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + const ms = String(date.getMilliseconds()).padStart(3, '0'); + return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}.${ms}${offset}`; + } + } + + return date.toISOString().replace('Z', '+00:00'); +}; + +/** + * Generates a timestamp markup string in the format + */ +export const generateTimestampMarkup = (timestamp: string, format: TimestampFormat): string => { + return ``; +}; diff --git a/apps/meteor/client/lib/utils/timestamp/formats.ts b/apps/meteor/client/lib/utils/timestamp/formats.ts new file mode 100644 index 0000000000000..598efe2c264ed --- /dev/null +++ b/apps/meteor/client/lib/utils/timestamp/formats.ts @@ -0,0 +1,39 @@ +import type { TimestampFormat, ITimestampFormatConfig } from './types'; + +export const TIMESTAMP_FORMATS: Record = { + t: { + label: 'timestamps.shortTime', + format: 'p', + description: 'timestamps.shortTimeDescription', + }, + T: { + label: 'timestamps.longTime', + format: 'pp', + description: 'timestamps.longTimeDescription', + }, + d: { + label: 'timestamps.shortDate', + format: 'P', + description: 'timestamps.shortDateDescription', + }, + D: { + label: 'timestamps.longDate', + format: 'Pp', + description: 'timestamps.longDateDescription', + }, + f: { + label: 'timestamps.fullDateTime', + format: 'PPPppp', + description: 'timestamps.fullDateTimeDescription', + }, + F: { + label: 'timestamps.fullDateTimeLong', + format: 'PPPPpppp', + description: 'timestamps.fullDateTimeLongDescription', + }, + R: { + label: 'timestamps.relativeTime', + format: 'relative', + description: 'timestamps.relativeTimeDescription', + }, +}; diff --git a/apps/meteor/client/lib/utils/timestamp/types.ts b/apps/meteor/client/lib/utils/timestamp/types.ts new file mode 100644 index 0000000000000..c976265359602 --- /dev/null +++ b/apps/meteor/client/lib/utils/timestamp/types.ts @@ -0,0 +1,37 @@ +export type TimestampFormat = 't' | 'T' | 'd' | 'D' | 'f' | 'F' | 'R'; + +export interface ITimestampFormatConfig { + label: string; + format: string; + description: string; +} + +export enum UTCOffsets { + 'UTC-12' = '-12:00', + 'UTC-11' = '-11:00', + 'UTC-10' = '-10:00', + 'UTC-9' = '-09:00', + 'UTC-8' = '-08:00', + 'UTC-7' = '-07:00', + 'UTC-6' = '-06:00', + 'UTC-5' = '-05:00', + 'UTC-4' = '-04:00', + 'UTC-3' = '-03:00', + 'UTC-2' = '-02:00', + 'UTC-1' = '-01:00', + 'UTC' = '+00:00', + 'UTC+1' = '+01:00', + 'UTC+2' = '+02:00', + 'UTC+3' = '+03:00', + 'UTC+4' = '+04:00', + 'UTC+5' = '+05:00', + 'UTC+6' = '+06:00', + 'UTC+7' = '+07:00', + 'UTC+8' = '+08:00', + 'UTC+9' = '+09:00', + 'UTC+10' = '+10:00', + 'UTC+11' = '+11:00', + 'UTC+12' = '+12:00', +} + +export type TimezoneKey = keyof typeof UTCOffsets | 'local'; diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx index 35ab4110a9be2..ef4ce8c174141 100644 --- a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/MessageBoxActionsToolbar.tsx @@ -1,7 +1,6 @@ import type { IRoom, IMessage } from '@rocket.chat/core-typings'; import type { Icon } from '@rocket.chat/fuselage'; -import { GenericMenu } from '@rocket.chat/ui-client'; -import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; +import { GenericMenu, type GenericMenuItemProps } from '@rocket.chat/ui-client'; import { MessageComposerAction, MessageComposerActionsDivider } from '@rocket.chat/ui-composer'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useTranslation, useLayoutHiddenActions } from '@rocket.chat/ui-contexts'; @@ -12,6 +11,7 @@ import { useAudioMessageAction } from './hooks/useAudioMessageAction'; import { useCreateDiscussionAction } from './hooks/useCreateDiscussionAction'; import { useFileUploadAction } from './hooks/useFileUploadAction'; import { useShareLocationAction } from './hooks/useShareLocationAction'; +import { useTimestampAction } from './hooks/useTimestampAction'; import { useVideoMessageAction } from './hooks/useVideoMessageAction'; import { useWebdavActions } from './hooks/useWebdavActions'; import { messageBox } from '../../../../../../app/ui-utils/client'; @@ -61,6 +61,7 @@ const MessageBoxActionsToolbar = ({ const webdavActions = useWebdavActions(); const createDiscussionAction = useCreateDiscussionAction(room); const shareLocationAction = useShareLocationAction(room, tmid); + const timestampAction = useTimestampAction(chatContext.composer); const apps = useMessageboxAppsActionButtons(); const { composerToolbox: hiddenActions } = useLayoutHiddenActions(); @@ -71,15 +72,21 @@ const MessageBoxActionsToolbar = ({ ...(!isHidden(hiddenActions, fileUploadAction) && { fileUploadAction }), ...(!isHidden(hiddenActions, createDiscussionAction) && { createDiscussionAction }), ...(!isHidden(hiddenActions, shareLocationAction) && { shareLocationAction }), - ...(!hiddenActions.includes('webdav-add') && { webdavActions }), + ...(timestampAction && !isHidden(hiddenActions, timestampAction) && { timestampAction }), + ...(!hiddenActions.includes('webdav-add') && webdavActions && { webdavActions }), }; const featured = []; const createNew = []; const share = []; + const insert = []; createNew.push(allActions.createDiscussionAction); + if (allActions.timestampAction) { + insert.push(allActions.timestampAction); + } + if (variant === 'small') { featured.push(allActions.audioMessageAction, allActions.fileUploadAction); createNew.push(allActions.videoMessageAction); @@ -100,7 +107,6 @@ const MessageBoxActionsToolbar = ({ }), ...messageBox.actions.get(), }; - const messageBoxActions = Object.entries(groups).map(([name, group]) => { const items: GenericMenuItemProps[] = group .filter((item) => !hiddenActions.includes(item.id)) @@ -126,6 +132,7 @@ const MessageBoxActionsToolbar = ({ const createNewFiltered = createNew.filter(isTruthy); const shareFiltered = share.filter(isTruthy); + const insertFiltered = insert.filter(isTruthy); const renderAction = ({ id, icon, content, disabled, onClick }: GenericMenuItemProps) => { if (!icon) { @@ -144,7 +151,12 @@ const MessageBoxActionsToolbar = ({ data-qa-id='menu-more-actions' detached icon='plus' - sections={[{ title: t('Create_new'), items: createNewFiltered }, { title: t('Share'), items: shareFiltered }, ...messageBoxActions]} + sections={[ + { title: t('Create_new'), items: createNewFiltered }, + { title: t('Share'), items: shareFiltered }, + ...(insertFiltered.length > 0 ? [{ title: t('Insert'), items: insertFiltered }] : []), + ...messageBoxActions, + ]} title={t('More_actions')} /> diff --git a/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useTimestampAction.tsx b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useTimestampAction.tsx new file mode 100644 index 0000000000000..5a0a2eedc11b0 --- /dev/null +++ b/apps/meteor/client/views/room/composer/messageBox/MessageBoxActionsToolbar/hooks/useTimestampAction.tsx @@ -0,0 +1,32 @@ +import type { GenericMenuItemProps } from '@rocket.chat/ui-client'; +import { useFeaturePreview } from '@rocket.chat/ui-client'; +import { useSetModal } from '@rocket.chat/ui-contexts'; +import { useTranslation } from 'react-i18next'; + +import { TimestampPickerModal } from '../../../../../../components/message/toolbar/items/actions/Timestamp/TimestampPicker/TimestampPickerModal'; +import type { ComposerAPI } from '../../../../../../lib/chats/ChatAPI'; + +export const useTimestampAction = (composer: ComposerAPI | undefined): GenericMenuItemProps | undefined => { + const setModal = useSetModal(); + const { t } = useTranslation(); + const timestampFeatureEnabled = useFeaturePreview('enable-timestamp-message-parser'); + + if (!timestampFeatureEnabled) { + return; + } + + const handleClick = () => { + if (!composer) { + return; + } + + setModal( setModal(null)} composer={composer} />); + }; + + return { + id: 'timestamp', + icon: 'clock', + content: t('Timestamp'), + onClick: handleClick, + }; +}; diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json index 4c15739a7e3d1..dc60f672c4edc 100644 --- a/packages/i18n/src/locales/en.i18n.json +++ b/packages/i18n/src/locales/en.i18n.json @@ -7069,6 +7069,8 @@ "__roomName__was_added_to_favorites": "{{roomName}} was added to favorites", "__roomName__was_removed_from_favorites": "{{roomName}} was removed from favorites", "__unreadTitle__from__roomTitle__": "{{unreadTitle}} from {{roomTitle}}", + "Insert_timestamp": "Insert timestamp", + "Insert": "Insert", "__username__is_no_longer__role__defined_by__user_by_": "{{username}} is no longer {{role}} by {{user_by}}", "__username__was_set__role__by__user_by_": "{{username}} was set {{role}} by {{user_by}}", "__usernames__and__count__more_joined": "{{usernames}} and {{count}} more joined", @@ -7079,5 +7081,19 @@ "VERIFIED": "User is verified", "UNVERIFIED": "User is unverified", "UNABLE_TO_VERIFY": "Unable to verify user", - "Users_invited": "The users have been invited" + "Users_invited": "The users have been invited", + "timestamps.shortTime": "Short time", + "timestamps.longTime": "Long time", + "timestamps.shortDate": "Short date", + "timestamps.longDate": "Long date", + "timestamps.fullDateTime": "Full date and time", + "timestamps.fullDateTimeLong": "Full date and time (long)", + "timestamps.relativeTime": "Relative time", + "timestamps.shortTimeDescription": "12:00 AM", + "timestamps.longTimeDescription": "12:00:00 AM", + "timestamps.shortDateDescription": "12/31/2020", + "timestamps.longDateDescription": "12/31/2020, 12:00 AM", + "timestamps.fullDateTimeDescription": "December 31, 2020 12:00 AM", + "timestamps.fullDateTimeLongDescription": "Thursday, December 31, 2020 12:00:00 AM", + "timestamps.relativeTimeDescription": "1 year ago" } \ No newline at end of file From 88aa28eaaa87b8ed6b39980e238eb21376acbf3b Mon Sep 17 00:00:00 2001 From: Rodrigo Nascimento Date: Tue, 28 Oct 2025 19:34:13 -0300 Subject: [PATCH 017/129] chore: add support for MongoDB 8 (#37294) --- .github/workflows/ci-test-e2e.yml | 11 ++++++----- .github/workflows/ci.yml | 6 +++--- docker-compose-local.yml | 19 +++++++++++++------ 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci-test-e2e.yml b/.github/workflows/ci-test-e2e.yml index 84ebeca360068..839864ae1bdcd 100644 --- a/.github/workflows/ci-test-e2e.yml +++ b/.github/workflows/ci-test-e2e.yml @@ -26,7 +26,7 @@ on: transporter: type: string mongodb-version: - default: "['5.0', '7.0']" + default: "['5.0', '8.2']" required: false type: string release: @@ -77,8 +77,8 @@ jobs: test: runs-on: ubuntu-24.04 env: - RC_DOCKERFILE: ${{ inputs.rc-dockerfile }}.${{ (matrix.mongodb-version == '7.0' && 'debian' && false) || 'alpine' }} - RC_DOCKER_TAG: ${{ inputs.rc-docker-tag }}.${{ (matrix.mongodb-version == '7.0' && 'debian' && false) || 'alpine' }} + RC_DOCKERFILE: ${{ inputs.rc-dockerfile }}.${{ (matrix.mongodb-version == '8.2' && 'debian' && false) || 'alpine' }} + RC_DOCKER_TAG: ${{ inputs.rc-docker-tag }}.${{ (matrix.mongodb-version == '8.2' && 'debian' && false) || 'alpine' }} strategy: fail-fast: false @@ -86,7 +86,7 @@ jobs: mongodb-version: ${{ fromJSON(inputs.mongodb-version) }} shard: ${{ fromJSON(inputs.shard) }} - name: MongoDB ${{ matrix.mongodb-version }}${{ inputs.db-watcher-disabled == 'false' && ' [legacy watchers]' || '' }} (${{ matrix.shard }}/${{ inputs.total-shard }}) - ${{ (matrix.mongodb-version == '7.0' && 'Debian' && false) || 'Alpine (Official)' }} + name: MongoDB ${{ matrix.mongodb-version }}${{ inputs.db-watcher-disabled == 'false' && ' [legacy watchers]' || '' }} (${{ matrix.shard }}/${{ inputs.total-shard }}) - ${{ (matrix.mongodb-version == '8.2' && 'Debian' && false) || 'Alpine (Official)' }} steps: - name: Collect Workflow Telemetry @@ -116,7 +116,8 @@ jobs: - name: Launch MongoDB uses: supercharge/mongodb-github-action@1.12.0 with: - mongodb-version: ${{ matrix.mongodb-version }} + mongodb-image: mongodb/mongodb-community-server + mongodb-version: ${{ matrix.mongodb-version }}-ubi8 mongodb-replica-set: rs0 - uses: actions/checkout@v4 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cbc88a65a7461..8f603f7005633 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -130,7 +130,7 @@ jobs: fi; curl -H "Content-Type: application/json" -H "X-Update-Token: $UPDATE_TOKEN" -d \ - "{\"nodeVersion\": \"${{ needs.release-versions.outputs.node-version }}\", \"denoVersion\": \"${{ needs.release-versions.outputs.deno-version }}\", \"compatibleMongoVersions\": [\"5.0\", \"6.0\", \"7.0\"], \"commit\": \"$GITHUB_SHA\", \"tag\": \"$RC_VERSION\", \"branch\": \"$GIT_BRANCH\", \"artifactName\": \"$ARTIFACT_NAME\", \"releaseType\": \"draft\", \"draftAs\": \"$RC_RELEASE\"}" \ + "{\"nodeVersion\": \"${{ needs.release-versions.outputs.node-version }}\", \"denoVersion\": \"${{ needs.release-versions.outputs.deno-version }}\", \"compatibleMongoVersions\": [\"5\", \"6\", \"7\", \"8\"], \"commit\": \"$GITHUB_SHA\", \"tag\": \"$RC_VERSION\", \"branch\": \"$GIT_BRANCH\", \"artifactName\": \"$ARTIFACT_NAME\", \"releaseType\": \"draft\", \"draftAs\": \"$RC_RELEASE\"}" \ https://releases.rocket.chat/update packages-build: @@ -468,7 +468,7 @@ jobs: enterprise-license: ${{ needs.release-versions.outputs.enterprise-license }} shard: '[1, 2, 3, 4, 5]' total-shard: 5 - mongodb-version: "['7.0']" + mongodb-version: "['8.2']" node-version: ${{ needs.release-versions.outputs.node-version }} deno-version: ${{ needs.release-versions.outputs.deno-version }} lowercase-repo: ${{ needs.release-versions.outputs.lowercase-repo }} @@ -818,7 +818,7 @@ jobs: fi; curl -H "Content-Type: application/json" -H "X-Update-Token: $UPDATE_TOKEN" -d \ - "{\"nodeVersion\": \"${{ needs.release-versions.outputs.node-version }}\", \"denoVersion\": \"${{ needs.release-versions.outputs.deno-version }}\", \"compatibleMongoVersions\": [\"5.0\", \"6.0\", \"7.0\"], \"commit\": \"$GITHUB_SHA\", \"tag\": \"$RC_VERSION\", \"branch\": \"$GIT_BRANCH\", \"artifactName\": \"$ARTIFACT_NAME\", \"releaseType\": \"$RC_RELEASE\"}" \ + "{\"nodeVersion\": \"${{ needs.release-versions.outputs.node-version }}\", \"denoVersion\": \"${{ needs.release-versions.outputs.deno-version }}\", \"compatibleMongoVersions\": [\"5\", \"6\", \"7\", \"8\"], \"commit\": \"$GITHUB_SHA\", \"tag\": \"$RC_VERSION\", \"branch\": \"$GIT_BRANCH\", \"artifactName\": \"$ARTIFACT_NAME\", \"releaseType\": \"$RC_RELEASE\"}" \ https://releases.rocket.chat/update # Makes build fail if the release isn't there diff --git a/docker-compose-local.yml b/docker-compose-local.yml index 272f394637e64..917d9304370d8 100644 --- a/docker-compose-local.yml +++ b/docker-compose-local.yml @@ -146,19 +146,26 @@ services: - nats mongo: - image: docker.io/bitnamilegacy/mongodb:7.0.1 + image: mongodb/mongodb-community-server:8.2-ubi8 restart: on-failure ports: - 27017:27017 environment: - MONGODB_REPLICA_SET_MODE: primary MONGODB_REPLICA_SET_NAME: ${MONGODB_REPLICA_SET_NAME:-rs0} MONGODB_PORT_NUMBER: ${MONGODB_PORT_NUMBER:-27017} MONGODB_INITIAL_PRIMARY_HOST: ${MONGODB_INITIAL_PRIMARY_HOST:-mongo} - MONGODB_INITIAL_PRIMARY_PORT_NUMBER: ${MONGODB_INITIAL_PRIMARY_PORT_NUMBER:-27017} - MONGODB_ADVERTISED_HOSTNAME: ${MONGODB_ADVERTISED_HOSTNAME:-mongo} - MONGODB_ENABLE_JOURNAL: ${MONGODB_ENABLE_JOURNAL:-true} - ALLOW_EMPTY_PASSWORD: ${ALLOW_EMPTY_PASSWORD:-yes} + entrypoint: | + bash -c + "mongod --replSet $$MONGODB_REPLICA_SET_NAME --bind_ip_all & + sleep 2; + until mongosh --eval \"db.adminCommand('ping')\"; do + echo '=====> Waiting for Mongo...'; + sleep 1; + done; + echo \"=====> Initiating ReplSet $$MONGODB_REPLICA_SET_NAME at $$MONGODB_INITIAL_PRIMARY_HOST:$$MONGODB_PORT_NUMBER...\"; + mongosh --eval \"rs.initiate({_id: '$$MONGODB_REPLICA_SET_NAME', members: [{ _id: 0, host: '$$MONGODB_INITIAL_PRIMARY_HOST:$$MONGODB_PORT_NUMBER' }]})\"; + echo '=====> Initiating ReplSet done...'; + wait" nats: image: nats:2.6-alpine From eb631f67160e6fd413b6f13c57de3763753c665a Mon Sep 17 00:00:00 2001 From: Yash Rajpal <58601732+yash-rajpal@users.noreply.github.com> Date: Wed, 29 Oct 2025 16:45:49 +0530 Subject: [PATCH 018/129] fix: Email 2FA auto opt in (#37326) --- .changeset/lucky-bulldogs-divide.md | 5 + .../authentication/server/startup/index.js | 6 +- apps/meteor/tests/end-to-end/api/users.ts | 134 ++++++++++++++++++ 3 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 .changeset/lucky-bulldogs-divide.md diff --git a/.changeset/lucky-bulldogs-divide.md b/.changeset/lucky-bulldogs-divide.md new file mode 100644 index 0000000000000..7a28e7ccac816 --- /dev/null +++ b/.changeset/lucky-bulldogs-divide.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue related to creating new users, it should not auto opt in new users for email two factor authentication if any one of `Accounts_TwoFactorAuthentication_Enabled`, `Accounts_TwoFactorAuthentication_By_Email_Enabled` and `Accounts_TwoFactorAuthentication_By_Email_Auto_Opt_In` setting is disabled. diff --git a/apps/meteor/app/authentication/server/startup/index.js b/apps/meteor/app/authentication/server/startup/index.js index 345aa01e688ea..a56f237c6e527 100644 --- a/apps/meteor/app/authentication/server/startup/index.js +++ b/apps/meteor/app/authentication/server/startup/index.js @@ -307,7 +307,11 @@ Accounts.insertUserDoc = async function (options, user) { user.type = 'user'; } - if (settings.get('Accounts_TwoFactorAuthentication_By_Email_Auto_Opt_In')) { + if ( + settings.get('Accounts_TwoFactorAuthentication_Enabled') && + settings.get('Accounts_TwoFactorAuthentication_By_Email_Enabled') && + settings.get('Accounts_TwoFactorAuthentication_By_Email_Auto_Opt_In') + ) { user.services = user.services || {}; user.services.email2fa = { enabled: true, diff --git a/apps/meteor/tests/end-to-end/api/users.ts b/apps/meteor/tests/end-to-end/api/users.ts index aee958ce4f2cf..5e5790f97a832 100644 --- a/apps/meteor/tests/end-to-end/api/users.ts +++ b/apps/meteor/tests/end-to-end/api/users.ts @@ -693,6 +693,140 @@ describe('[Users]', () => { }); }); }); + + describe('default email2fa auto opt in configuration', () => { + let user: IUser; + + afterEach(async () => { + await deleteUser(user); + await updateSetting('Accounts_TwoFactorAuthentication_By_Email_Enabled', true); + await updateSetting('Accounts_TwoFactorAuthentication_By_Email_Auto_Opt_In', true); + await updateSetting('Accounts_TwoFactorAuthentication_Enabled', true); + }); + + const dummyUser = { + email: 'email2fa_auto_opt_in@rocket.chat', + name: 'email2fa_auto_opt_in', + username: 'email2fa_auto_opt_in', + password, + }; + + it('should auto opt in new users for email2fa ', async () => { + await request + .post(api('users.create')) + .set(credentials) + .send(dummyUser) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + user = res.body.user; + }); + + const newUserCredentials = await login(dummyUser.username, dummyUser.password); + + await request + .get(api('users.info')) + .set(newUserCredentials) + .query({ + username: dummyUser.username, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.nested.property('user.services.email2fa.enabled', true); + }); + }); + + it('should not auto opt in new users for email2fa if email2fa is disabled', async () => { + await updateSetting('Accounts_TwoFactorAuthentication_By_Email_Enabled', false); + await request + .post(api('users.create')) + .set(credentials) + .send(dummyUser) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + user = res.body.user; + }); + + const newUserCredentials = await login(dummyUser.username, dummyUser.password); + + await request + .get(api('users.info')) + .set(newUserCredentials) + .query({ + username: dummyUser.username, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.not.have.nested.property('user.services.email2fa.enabled'); + }); + }); + + it('should not auto opt in new users for email2fa if two factor authentication is disabled', async () => { + await updateSetting('Accounts_TwoFactorAuthentication_Enabled', false); + await request + .post(api('users.create')) + .set(credentials) + .send(dummyUser) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + user = res.body.user; + }); + + const newUserCredentials = await login(dummyUser.username, dummyUser.password); + + await request + .get(api('users.info')) + .set(newUserCredentials) + .query({ + username: dummyUser.username, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.not.have.nested.property('user.services.email2fa.enabled'); + }); + }); + + it('should not auto opt in new users for email2fa if email2fa is enabled but auto opt in is disabled', async () => { + await updateSetting('Accounts_TwoFactorAuthentication_By_Email_Auto_Opt_In', false); + + await request + .post(api('users.create')) + .set(credentials) + .send(dummyUser) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + user = res.body.user; + }); + + const newUserCredentials = await login(dummyUser.username, dummyUser.password); + + await request + .get(api('users.info')) + .set(newUserCredentials) + .query({ + username: dummyUser.username, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.not.have.nested.property('user.services.email2fa.enabled'); + }); + }); + }); }); describe('[/users.register]', () => { From 1acf6a3123c6d6973dbd78689ed1994fcc13731b Mon Sep 17 00:00:00 2001 From: Abhinav Kumar Date: Wed, 29 Oct 2025 20:45:29 +0530 Subject: [PATCH 019/129] fix: directory search does not return private channels (#37290) --- .changeset/old-cobras-serve.md | 5 +++ apps/meteor/app/api/server/v1/misc.ts | 3 +- apps/meteor/server/methods/browseChannels.ts | 10 +++--- .../tests/end-to-end/api/miscellaneous.ts | 34 +++++++++++++++++-- 4 files changed, 44 insertions(+), 8 deletions(-) create mode 100644 .changeset/old-cobras-serve.md diff --git a/.changeset/old-cobras-serve.md b/.changeset/old-cobras-serve.md new file mode 100644 index 0000000000000..6998eb7a2b6fe --- /dev/null +++ b/.changeset/old-cobras-serve.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue where private channels that a user belongs to were not shown in Directory search results. diff --git a/apps/meteor/app/api/server/v1/misc.ts b/apps/meteor/app/api/server/v1/misc.ts index 00c7262acdad9..ecdd8c58b052c 100644 --- a/apps/meteor/app/api/server/v1/misc.ts +++ b/apps/meteor/app/api/server/v1/misc.ts @@ -364,6 +364,7 @@ API.v1.addRoute( const sortBy = sort ? Object.keys(sort)[0] : undefined; const sortDirection = sort && Object.values(sort)[0] === 1 ? 'asc' : 'desc'; + const user = await Users.findOneById(this.userId, { projection: { __rooms: 1 } }); const result = await browseChannelsMethod( { ...filter, @@ -372,7 +373,7 @@ API.v1.addRoute( offset: Math.max(0, offset), limit: Math.max(0, count), }, - this.user, + user, ); if (!result) { diff --git a/apps/meteor/server/methods/browseChannels.ts b/apps/meteor/server/methods/browseChannels.ts index 2b22d2cd74ed1..965aa7669ea7a 100644 --- a/apps/meteor/server/methods/browseChannels.ts +++ b/apps/meteor/server/methods/browseChannels.ts @@ -1,5 +1,5 @@ import { Team } from '@rocket.chat/core-services'; -import type { IRoom, IUser } from '@rocket.chat/core-typings'; +import type { IUser, AtLeast } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ddp-client'; import { Rooms, Users, Subscriptions } from '@rocket.chat/models'; import { escapeRegExp } from '@rocket.chat/string-helpers'; @@ -45,7 +45,7 @@ const sortUsers = (field: string, direction: 'asc' | 'desc'): Record, canViewAnon: boolean, searchTerm: string, sort: Record, @@ -119,7 +119,7 @@ const getChannelsCountForTeam = mem((teamId) => Rooms.countByTeamId(teamId), { }); const getTeams = async ( - user: IUser, + user: AtLeast, searchTerm: string, sort: Record, pagination: { @@ -247,7 +247,7 @@ const findUsers = async ({ }; const getUsers = async ( - user: IUser | undefined, + user: AtLeast | undefined, text: string, workspace: string, sort: Record, @@ -299,7 +299,7 @@ export const browseChannelsMethod = async ( offset = 0, limit = 10, }: BrowseChannelsParams, - user: IUser | undefined | null, + user: AtLeast | undefined | null, ) => { const searchTerm = trim(escapeRegExp(text)); diff --git a/apps/meteor/tests/end-to-end/api/miscellaneous.ts b/apps/meteor/tests/end-to-end/api/miscellaneous.ts index ffa8f9d4586e1..de022f7a2f7dd 100644 --- a/apps/meteor/tests/end-to-end/api/miscellaneous.ts +++ b/apps/meteor/tests/end-to-end/api/miscellaneous.ts @@ -215,6 +215,7 @@ describe('miscellaneous', () => { describe('/directory', () => { let user: TestUser; let testChannel: IRoom; + let testGroup: IRoom; let normalUserCredentials: Credentials; const teamName = `new-team-name-${Date.now()}` as const; let teamCreated: ITeam; @@ -223,8 +224,11 @@ describe('miscellaneous', () => { await updatePermission('create-team', ['admin', 'user']); user = await createUser(); normalUserCredentials = await doLogin(user.username, password); - testChannel = (await createRoom({ name: `channel.test.${Date.now()}`, type: 'c' })).body.channel; - teamCreated = await createTeam(normalUserCredentials, teamName, TEAM_TYPE.PUBLIC); + [testChannel, testGroup, teamCreated] = await Promise.all([ + createRoom({ name: `channel.test.${Date.now()}`, type: 'c' }).then((res) => res.body.channel), + createRoom({ name: `group.test.${Date.now()}`, type: 'p' }).then((res) => res.body.group), + createTeam(normalUserCredentials, teamName, TEAM_TYPE.PUBLIC), + ]); }); after(async () => { @@ -232,6 +236,7 @@ describe('miscellaneous', () => { deleteTeam(normalUserCredentials, teamName), deleteUser(user), deleteRoom({ type: 'c', roomId: testChannel._id }), + deleteRoom({ type: 'p', roomId: testGroup._id }), updatePermission('create-team', ['admin', 'user']), ]); }); @@ -308,6 +313,31 @@ describe('miscellaneous', () => { }) .end(done); }); + + it('should return private group when search by channel and execute successfully', async () => { + await request + .get(api('directory')) + .set(credentials) + .query({ + text: testGroup.name, + type: 'channels', + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('offset'); + expect(res.body).to.have.property('total'); + expect(res.body).to.have.property('count'); + expect(res.body).to.have.property('result').and.to.be.an('array'); + expect(res.body.result[0]).to.have.property('_id', testGroup._id); + expect(res.body.result[0]).to.have.property('t', 'p'); + expect(res.body.result[0]).to.have.property('name'); + expect(res.body.result[0]).to.have.property('usersCount').and.to.be.an('number'); + expect(res.body.result[0]).to.have.property('ts'); + }); + }); + it('should return an array(result) when search by channel with sort params correctly and execute successfully', (done) => { void request .get(api('directory')) From 4be0c4b24d51c3bf6b5a35ba3881bb47fb9c6935 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 30 Oct 2025 11:57:55 -0300 Subject: [PATCH 020/129] chore: bump version to 7.13.0-develop --- 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 c348d21134a9a..7ce5e3d3c3b80 100644 --- a/apps/meteor/app/utils/rocketchat.info +++ b/apps/meteor/app/utils/rocketchat.info @@ -1,3 +1,3 @@ { - "version": "7.12.0-develop" + "version": "7.13.0-develop" } diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 5f19685a74194..130233166955b 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -1,7 +1,7 @@ { "name": "@rocket.chat/meteor", "description": "The Ultimate Open Source WebChat Platform", - "version": "7.12.0-develop", + "version": "7.13.0-develop", "private": true, "type": "commonjs", "author": { diff --git a/package.json b/package.json index fa2e640069817..88bcfb8eb3b53 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rocket.chat", - "version": "7.12.0-develop", + "version": "7.13.0-develop", "description": "Rocket.Chat Monorepo", "main": "index.js", "private": true, diff --git a/packages/core-typings/package.json b/packages/core-typings/package.json index d4006cdb2dc65..e6d26dc1b865a 100644 --- a/packages/core-typings/package.json +++ b/packages/core-typings/package.json @@ -2,7 +2,7 @@ "$schema": "https://json.schemastore.org/package", "name": "@rocket.chat/core-typings", "private": true, - "version": "7.12.0-develop", + "version": "7.13.0-develop", "devDependencies": { "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/eslint-config": "workspace:^", diff --git a/packages/rest-typings/package.json b/packages/rest-typings/package.json index 896451a6cd7dc..b1f72ffb211e5 100644 --- a/packages/rest-typings/package.json +++ b/packages/rest-typings/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/rest-typings", - "version": "7.12.0-develop", + "version": "7.13.0-develop", "devDependencies": { "@rocket.chat/apps-engine": "workspace:^", "@rocket.chat/eslint-config": "workspace:~", From 5c57e84aa584c1913efcdf1d9204b115539710e0 Mon Sep 17 00:00:00 2001 From: Guilherme Gazzo Date: Thu, 30 Oct 2025 16:57:16 +0100 Subject: [PATCH 021/129] chore: bump meteor 3.3.2 (#37336) --- apps/meteor/.meteor/packages | 14 +++--- apps/meteor/.meteor/release | 2 +- apps/meteor/.meteor/versions | 24 +++++----- apps/meteor/ee/server/services/package.json | 2 +- apps/meteor/package.json | 2 +- ee/apps/account-service/package.json | 2 +- ee/apps/authorization-service/package.json | 2 +- ee/apps/ddp-streamer/package.json | 2 +- ee/apps/omnichannel-transcript/package.json | 2 +- ee/apps/presence-service/package.json | 2 +- ee/apps/queue-worker/package.json | 2 +- ee/apps/stream-hub-service/package.json | 2 +- ee/packages/federation-matrix/package.json | 2 +- ee/packages/omni-core-ee/package.json | 2 +- ee/packages/omnichannel-services/package.json | 2 +- ee/packages/presence/package.json | 2 +- packages/agenda/package.json | 2 +- packages/core-services/package.json | 2 +- packages/core-typings/package.json | 2 +- packages/cron/package.json | 2 +- packages/instance-status/package.json | 2 +- packages/model-typings/package.json | 2 +- packages/mongo-adapter/package.json | 2 +- packages/omni-core/package.json | 2 +- packages/rest-typings/package.json | 2 +- packages/ui-contexts/package.json | 2 +- yarn.lock | 46 +++++++++---------- 27 files changed, 66 insertions(+), 66 deletions(-) diff --git a/apps/meteor/.meteor/packages b/apps/meteor/.meteor/packages index 7f6f660851cf2..0dce43f784ee4 100644 --- a/apps/meteor/.meteor/packages +++ b/apps/meteor/.meteor/packages @@ -9,13 +9,13 @@ rocketchat:livechat rocketchat:streamer rocketchat:version -accounts-base@3.1.1 +accounts-base@3.1.2 accounts-facebook@1.3.4 accounts-github@1.5.1 accounts-google@1.4.1 accounts-meteor-developer@1.5.1 accounts-oauth@1.4.6 -accounts-password@3.2.0 +accounts-password@3.2.1 accounts-twitter@1.5.2 google-oauth@1.4.5 @@ -31,12 +31,12 @@ meteor-base@1.5.2 ddp-common@1.4.4 webapp@2.0.7 -mongo@2.1.2 +mongo@2.1.4 reload@1.3.2 service-configuration@1.3.5 session@1.2.2 -shell-server@0.6.1 +shell-server@0.6.2 dispatch:run-as-user ostrio:cookies @@ -53,11 +53,11 @@ tracker@1.3.4 reactive-dict@1.3.2 reactive-var@1.0.13 -babel-compiler@7.12.0 +babel-compiler@7.12.2 standard-minifier-css@1.9.3 dynamic-import@0.7.4 -ecmascript@0.16.11 -typescript@5.6.4 +ecmascript@0.16.13 +typescript@5.6.6 autoupdate@2.0.1 diff --git a/apps/meteor/.meteor/release b/apps/meteor/.meteor/release index d515fb7f4946d..4876d6ff64c16 100644 --- a/apps/meteor/.meteor/release +++ b/apps/meteor/.meteor/release @@ -1 +1 @@ -METEOR@3.3 +METEOR@3.3.2 diff --git a/apps/meteor/.meteor/versions b/apps/meteor/.meteor/versions index 2c3b80cb7b7c1..04d79cb550212 100644 --- a/apps/meteor/.meteor/versions +++ b/apps/meteor/.meteor/versions @@ -1,19 +1,19 @@ -accounts-base@3.1.1 +accounts-base@3.1.2 accounts-facebook@1.3.4 accounts-github@1.5.1 accounts-google@1.4.1 accounts-meteor-developer@1.5.1 accounts-oauth@1.4.6 -accounts-password@3.2.0 +accounts-password@3.2.1 accounts-twitter@1.5.2 allow-deny@2.1.0 autoupdate@2.0.1 -babel-compiler@7.12.0 +babel-compiler@7.12.2 babel-runtime@1.5.2 base64@1.0.13 binary-heap@1.0.12 -boilerplate-generator@2.0.1 -callback-hook@1.6.0 +boilerplate-generator@2.0.2 +callback-hook@1.6.1 check@1.4.4 core-runtime@1.0.0 ddp@1.4.2 @@ -24,7 +24,7 @@ ddp-server@3.1.2 diff-sequence@1.1.3 dispatch:run-as-user@1.1.1 dynamic-import@0.7.4 -ecmascript@0.16.11 +ecmascript@0.16.13 ecmascript-runtime@0.8.3 ecmascript-runtime-client@0.12.3 ecmascript-runtime-server@0.11.1 @@ -48,15 +48,15 @@ meteor-base@1.5.2 meteor-developer-oauth@1.3.3 meteorhacks:inject-initial@1.0.5 minifier-css@2.0.1 -minimongo@2.0.2 -modern-browsers@0.2.2 +minimongo@2.0.4 +modern-browsers@0.2.3 modules@0.20.3 modules-runtime@0.13.2 -mongo@2.1.2 +mongo@2.1.4 mongo-decimal@0.2.0 mongo-dev-server@1.1.1 mongo-id@1.0.9 -npm-mongo@6.10.2 +npm-mongo@6.16.1 oauth@3.0.2 oauth1@1.5.2 oauth2@1.3.3 @@ -79,12 +79,12 @@ routepolicy@1.1.2 service-configuration@1.3.5 session@1.2.2 sha@1.0.10 -shell-server@0.6.1 +shell-server@0.6.2 socket-stream-client@0.6.1 standard-minifier-css@1.9.3 tracker@1.3.4 twitter-oauth@1.3.4 -typescript@5.6.4 +typescript@5.6.6 underscore@1.6.4 url@1.3.5 webapp@2.0.7 diff --git a/apps/meteor/ee/server/services/package.json b/apps/meteor/ee/server/services/package.json index 25d38ef433e96..f586940988705 100644 --- a/apps/meteor/ee/server/services/package.json +++ b/apps/meteor/ee/server/services/package.json @@ -39,7 +39,7 @@ "jaeger-client": "^3.19.0", "mem": "^8.1.1", "moleculer": "^0.14.35", - "mongodb": "6.10.0", + "mongodb": "6.16.0", "nats": "^2.28.2", "pino": "^8.21.0", "sodium-native": "^4.3.3", diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 130233166955b..c2d47cdc40001 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -402,7 +402,7 @@ "moment": "^2.30.1", "moment-timezone": "^0.5.48", "mongo-message-queue": "^1.1.0", - "mongodb": "6.10.0", + "mongodb": "6.16.0", "nats": "^2.28.2", "node-dogstatsd": "^0.0.7", "node-fetch": "2.7.0", diff --git a/ee/apps/account-service/package.json b/ee/apps/account-service/package.json index 3780a0ec5988c..2e3138f6912c2 100644 --- a/ee/apps/account-service/package.json +++ b/ee/apps/account-service/package.json @@ -33,7 +33,7 @@ "eventemitter3": "^5.0.1", "mem": "^8.1.1", "moleculer": "^0.14.35", - "mongodb": "6.10.0", + "mongodb": "6.16.0", "nats": "^2.28.2", "pino": "^8.21.0", "polka": "^0.5.2", diff --git a/ee/apps/authorization-service/package.json b/ee/apps/authorization-service/package.json index 1589198557a42..856f594b55781 100644 --- a/ee/apps/authorization-service/package.json +++ b/ee/apps/authorization-service/package.json @@ -31,7 +31,7 @@ "eventemitter3": "^5.0.1", "mem": "^8.1.1", "moleculer": "^0.14.35", - "mongodb": "6.10.0", + "mongodb": "6.16.0", "nats": "^2.28.2", "pino": "^8.21.0", "polka": "^0.5.2", diff --git a/ee/apps/ddp-streamer/package.json b/ee/apps/ddp-streamer/package.json index ed655e07bc1ad..b4922d23f8328 100644 --- a/ee/apps/ddp-streamer/package.json +++ b/ee/apps/ddp-streamer/package.json @@ -33,7 +33,7 @@ "jaeger-client": "^3.19.0", "mem": "^8.1.1", "moleculer": "^0.14.35", - "mongodb": "6.10.0", + "mongodb": "6.16.0", "nats": "^2.28.2", "pino": "^8.21.0", "polka": "^0.5.2", diff --git a/ee/apps/omnichannel-transcript/package.json b/ee/apps/omnichannel-transcript/package.json index 6552e38b97e73..b25a265d81ccb 100644 --- a/ee/apps/omnichannel-transcript/package.json +++ b/ee/apps/omnichannel-transcript/package.json @@ -39,7 +39,7 @@ "moleculer": "^0.14.35", "moment-timezone": "^0.5.48", "mongo-message-queue": "^1.1.0", - "mongodb": "6.10.0", + "mongodb": "6.16.0", "nats": "^2.28.2", "pino": "^8.21.0", "polka": "^0.5.2", diff --git a/ee/apps/presence-service/package.json b/ee/apps/presence-service/package.json index a25686bcb6d23..1731737f71c3f 100644 --- a/ee/apps/presence-service/package.json +++ b/ee/apps/presence-service/package.json @@ -31,7 +31,7 @@ "eventemitter3": "^5.0.1", "mem": "^8.1.1", "moleculer": "^0.14.35", - "mongodb": "6.10.0", + "mongodb": "6.16.0", "nats": "^2.28.2", "pino": "^8.21.0", "polka": "^0.5.2", diff --git a/ee/apps/queue-worker/package.json b/ee/apps/queue-worker/package.json index 8b73d1342527d..7c225bd6b1814 100644 --- a/ee/apps/queue-worker/package.json +++ b/ee/apps/queue-worker/package.json @@ -33,7 +33,7 @@ "moleculer": "^0.14.35", "moment-timezone": "^0.5.48", "mongo-message-queue": "^1.1.0", - "mongodb": "6.10.0", + "mongodb": "6.16.0", "nats": "^2.28.2", "pino": "^8.21.0", "polka": "^0.5.2", diff --git a/ee/apps/stream-hub-service/package.json b/ee/apps/stream-hub-service/package.json index 3e6a18114ab4b..894b5338dd085 100644 --- a/ee/apps/stream-hub-service/package.json +++ b/ee/apps/stream-hub-service/package.json @@ -30,7 +30,7 @@ "eventemitter3": "^5.0.1", "mem": "^8.1.1", "moleculer": "^0.14.35", - "mongodb": "6.10.0", + "mongodb": "6.16.0", "nats": "^2.28.2", "pino": "^8.21.0", "polka": "^0.5.2", diff --git a/ee/packages/federation-matrix/package.json b/ee/packages/federation-matrix/package.json index 3e90d6f4207c6..8b257b1c58f3b 100644 --- a/ee/packages/federation-matrix/package.json +++ b/ee/packages/federation-matrix/package.json @@ -46,7 +46,7 @@ "@rocket.chat/rest-typings": "workspace:^", "emojione": "^4.5.0", "marked": "^16.1.2", - "mongodb": "6.10.0", + "mongodb": "6.16.0", "pino": "^9.11.0", "reflect-metadata": "^0.2.2", "sanitize-html": "~2.17.0", diff --git a/ee/packages/omni-core-ee/package.json b/ee/packages/omni-core-ee/package.json index 8faf0c4b53664..207c68706ccdd 100644 --- a/ee/packages/omni-core-ee/package.json +++ b/ee/packages/omni-core-ee/package.json @@ -30,7 +30,7 @@ "@rocket.chat/models": "workspace:^", "@rocket.chat/omni-core": "workspace:^", "mem": "^8.1.1", - "mongodb": "6.10.0" + "mongodb": "6.16.0" }, "volta": { "extends": "../../../package.json" diff --git a/ee/packages/omnichannel-services/package.json b/ee/packages/omnichannel-services/package.json index 82ccbaec70cf5..5dac2198d1e3d 100644 --- a/ee/packages/omnichannel-services/package.json +++ b/ee/packages/omnichannel-services/package.json @@ -33,7 +33,7 @@ "mem": "^8.1.1", "moment-timezone": "^0.5.48", "mongo-message-queue": "^1.1.0", - "mongodb": "6.10.0", + "mongodb": "6.16.0", "pino": "^8.21.0" }, "scripts": { diff --git a/ee/packages/presence/package.json b/ee/packages/presence/package.json index d8f131ef2fcbd..ac335ea068e76 100644 --- a/ee/packages/presence/package.json +++ b/ee/packages/presence/package.json @@ -35,6 +35,6 @@ "@rocket.chat/core-services": "workspace:^", "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/models": "workspace:^", - "mongodb": "6.10.0" + "mongodb": "6.16.0" } } diff --git a/packages/agenda/package.json b/packages/agenda/package.json index 97a8844aaf6a1..fb3be8881dffd 100644 --- a/packages/agenda/package.json +++ b/packages/agenda/package.json @@ -9,7 +9,7 @@ "debug": "~4.3.7", "human-interval": "^2.0.1", "moment-timezone": "~0.5.48", - "mongodb": "6.10.0" + "mongodb": "6.16.0" }, "devDependencies": { "@types/debug": "^4.1.12", diff --git a/packages/core-services/package.json b/packages/core-services/package.json index baafbe4f42795..ed4610f14dd6b 100644 --- a/packages/core-services/package.json +++ b/packages/core-services/package.json @@ -14,7 +14,7 @@ "babel-jest": "~30.2.0", "eslint": "~8.45.0", "jest": "~30.2.0", - "mongodb": "6.10.0", + "mongodb": "6.16.0", "prettier": "~3.3.3", "typescript": "~5.9.3" }, diff --git a/packages/core-typings/package.json b/packages/core-typings/package.json index e6d26dc1b865a..f165120533de3 100644 --- a/packages/core-typings/package.json +++ b/packages/core-typings/package.json @@ -8,7 +8,7 @@ "@rocket.chat/eslint-config": "workspace:^", "@types/express": "^4.17.23", "eslint": "~8.45.0", - "mongodb": "6.10.0", + "mongodb": "6.16.0", "npm-run-all": "~4.1.5", "prettier": "~3.3.3", "rimraf": "^6.0.1", diff --git a/packages/cron/package.json b/packages/cron/package.json index 1dbf65c305e2c..18fa2a777327c 100644 --- a/packages/cron/package.json +++ b/packages/cron/package.json @@ -23,7 +23,7 @@ "@rocket.chat/core-typings": "workspace:^", "@rocket.chat/models": "workspace:^", "@rocket.chat/random": "workspace:^", - "mongodb": "6.10.0" + "mongodb": "6.16.0" }, "volta": { "extends": "../../package.json" diff --git a/packages/instance-status/package.json b/packages/instance-status/package.json index cc6963b29c974..0544a17de68bb 100644 --- a/packages/instance-status/package.json +++ b/packages/instance-status/package.json @@ -6,7 +6,7 @@ "@rocket.chat/eslint-config": "workspace:^", "@rocket.chat/tsconfig": "workspace:*", "eslint": "~8.45.0", - "mongodb": "6.10.0", + "mongodb": "6.16.0", "prettier": "~3.3.3", "typescript": "~5.9.3" }, diff --git a/packages/model-typings/package.json b/packages/model-typings/package.json index b743bc19177f3..918845caafe40 100644 --- a/packages/model-typings/package.json +++ b/packages/model-typings/package.json @@ -5,7 +5,7 @@ "devDependencies": { "@types/node-rsa": "^1.1.4", "eslint": "~8.45.0", - "mongodb": "6.10.0", + "mongodb": "6.16.0", "typescript": "~5.9.3" }, "scripts": { diff --git a/packages/mongo-adapter/package.json b/packages/mongo-adapter/package.json index 8c5edefd6ffa4..e8ee09a9101ce 100644 --- a/packages/mongo-adapter/package.json +++ b/packages/mongo-adapter/package.json @@ -19,7 +19,7 @@ "@rocket.chat/jest-presets": "workspace:~", "eslint": "~8.45.0", "jest": "~30.2.0", - "mongodb": "6.10.0", + "mongodb": "6.16.0", "typescript": "~5.9.3" }, "peerDependencies": { diff --git a/packages/omni-core/package.json b/packages/omni-core/package.json index e1ce395091b18..5d41443a3b43d 100644 --- a/packages/omni-core/package.json +++ b/packages/omni-core/package.json @@ -31,6 +31,6 @@ "dependencies": { "@rocket.chat/models": "workspace:^", "@rocket.chat/patch-injection": "workspace:^", - "mongodb": "6.10.0" + "mongodb": "6.16.0" } } diff --git a/packages/rest-typings/package.json b/packages/rest-typings/package.json index b1f72ffb211e5..77166faa02862 100644 --- a/packages/rest-typings/package.json +++ b/packages/rest-typings/package.json @@ -7,7 +7,7 @@ "@types/jest": "~30.0.0", "eslint": "~8.45.0", "jest": "~30.2.0", - "mongodb": "6.10.0", + "mongodb": "6.16.0", "typescript": "~5.9.3" }, "scripts": { diff --git a/packages/ui-contexts/package.json b/packages/ui-contexts/package.json index 13950fa150102..1fb920b467027 100644 --- a/packages/ui-contexts/package.json +++ b/packages/ui-contexts/package.json @@ -19,7 +19,7 @@ "eslint-plugin-react-hooks": "^5.0.0", "i18next": "~23.4.9", "jest": "~30.2.0", - "mongodb": "6.10.0", + "mongodb": "6.16.0", "react": "~18.3.1", "typescript": "~5.9.3" }, diff --git a/yarn.lock b/yarn.lock index ad901035c76fd..f05d70f69b326 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8016,7 +8016,7 @@ __metadata: eventemitter3: "npm:^5.0.1" mem: "npm:^8.1.1" moleculer: "npm:^0.14.35" - mongodb: "npm:6.10.0" + mongodb: "npm:6.16.0" nats: "npm:^2.28.2" pino: "npm:^8.21.0" polka: "npm:^0.5.2" @@ -8047,7 +8047,7 @@ __metadata: eslint: "npm:~8.45.0" human-interval: "npm:^2.0.1" moment-timezone: "npm:~0.5.48" - mongodb: "npm:6.10.0" + mongodb: "npm:6.16.0" typescript: "npm:~5.9.3" languageName: unknown linkType: soft @@ -8151,7 +8151,7 @@ __metadata: eventemitter3: "npm:^5.0.1" mem: "npm:^8.1.1" moleculer: "npm:^0.14.35" - mongodb: "npm:6.10.0" + mongodb: "npm:6.16.0" nats: "npm:^2.28.2" pino: "npm:^8.21.0" polka: "npm:^0.5.2" @@ -8214,7 +8214,7 @@ __metadata: babel-jest: "npm:~30.2.0" eslint: "npm:~8.45.0" jest: "npm:~30.2.0" - mongodb: "npm:6.10.0" + mongodb: "npm:6.16.0" prettier: "npm:~3.3.3" typescript: "npm:~5.9.3" languageName: unknown @@ -8231,7 +8231,7 @@ __metadata: "@rocket.chat/ui-kit": "workspace:~" "@types/express": "npm:^4.17.23" eslint: "npm:~8.45.0" - mongodb: "npm:6.10.0" + mongodb: "npm:6.16.0" npm-run-all: "npm:~4.1.5" prettier: "npm:~3.3.3" rimraf: "npm:^6.0.1" @@ -8251,7 +8251,7 @@ __metadata: "@rocket.chat/random": "workspace:^" "@rocket.chat/tsconfig": "workspace:*" eslint: "npm:~8.45.0" - mongodb: "npm:6.10.0" + mongodb: "npm:6.16.0" typescript: "npm:~5.9.3" languageName: unknown linkType: soft @@ -8333,7 +8333,7 @@ __metadata: jaeger-client: "npm:^3.19.0" mem: "npm:^8.1.1" moleculer: "npm:^0.14.35" - mongodb: "npm:6.10.0" + mongodb: "npm:6.16.0" nats: "npm:^2.28.2" pino: "npm:^8.21.0" pino-pretty: "npm:^7.6.1" @@ -8424,7 +8424,7 @@ __metadata: eslint: "npm:~8.45.0" jest: "npm:~30.2.0" marked: "npm:^16.1.2" - mongodb: "npm:6.10.0" + mongodb: "npm:6.16.0" pino: "npm:^9.11.0" pino-pretty: "npm:^7.6.1" reflect-metadata: "npm:^0.2.2" @@ -8774,7 +8774,7 @@ __metadata: "@rocket.chat/tracing": "workspace:^" "@rocket.chat/tsconfig": "workspace:*" eslint: "npm:~8.45.0" - mongodb: "npm:6.10.0" + mongodb: "npm:6.16.0" prettier: "npm:~3.3.3" typescript: "npm:~5.9.3" languageName: unknown @@ -9386,7 +9386,7 @@ __metadata: moment: "npm:^2.30.1" moment-timezone: "npm:^0.5.48" mongo-message-queue: "npm:^1.1.0" - mongodb: "npm:6.10.0" + mongodb: "npm:6.16.0" nats: "npm:^2.28.2" node-dogstatsd: "npm:^0.0.7" node-fetch: "npm:2.7.0" @@ -9520,7 +9520,7 @@ __metadata: "@rocket.chat/core-typings": "workspace:^" "@types/node-rsa": "npm:^1.1.4" eslint: "npm:~8.45.0" - mongodb: "npm:6.10.0" + mongodb: "npm:6.16.0" typescript: "npm:~5.9.3" languageName: unknown linkType: soft @@ -9554,7 +9554,7 @@ __metadata: "@rocket.chat/jest-presets": "workspace:~" eslint: "npm:~8.45.0" jest: "npm:~30.2.0" - mongodb: "npm:6.10.0" + mongodb: "npm:6.16.0" typescript: "npm:~5.9.3" peerDependencies: mongodb: 6.10.0 @@ -9605,7 +9605,7 @@ __metadata: eslint: "npm:~8.45.0" jest: "npm:~30.2.0" mem: "npm:^8.1.1" - mongodb: "npm:6.10.0" + mongodb: "npm:6.16.0" typescript: "npm:~5.9.3" languageName: unknown linkType: soft @@ -9623,7 +9623,7 @@ __metadata: "@types/jest": "npm:~30.0.0" eslint: "npm:~8.45.0" jest: "npm:~30.2.0" - mongodb: "npm:6.10.0" + mongodb: "npm:6.16.0" typescript: "npm:~5.9.3" languageName: unknown linkType: soft @@ -9659,7 +9659,7 @@ __metadata: mem: "npm:^8.1.1" moment-timezone: "npm:^0.5.48" mongo-message-queue: "npm:^1.1.0" - mongodb: "npm:6.10.0" + mongodb: "npm:6.16.0" pino: "npm:^8.21.0" typescript: "npm:~5.9.3" languageName: unknown @@ -9700,7 +9700,7 @@ __metadata: moleculer: "npm:^0.14.35" moment-timezone: "npm:^0.5.48" mongo-message-queue: "npm:^1.1.0" - mongodb: "npm:6.10.0" + mongodb: "npm:6.16.0" nats: "npm:^2.28.2" pino: "npm:^8.21.0" polka: "npm:^0.5.2" @@ -9848,7 +9848,7 @@ __metadata: eventemitter3: "npm:^5.0.1" mem: "npm:^8.1.1" moleculer: "npm:^0.14.35" - mongodb: "npm:6.10.0" + mongodb: "npm:6.16.0" nats: "npm:^2.28.2" pino: "npm:^8.21.0" polka: "npm:^0.5.2" @@ -9875,7 +9875,7 @@ __metadata: babel-jest: "npm:~30.2.0" eslint: "npm:~8.45.0" jest: "npm:~30.2.0" - mongodb: "npm:6.10.0" + mongodb: "npm:6.16.0" typescript: "npm:~5.9.3" languageName: unknown linkType: soft @@ -9916,7 +9916,7 @@ __metadata: moleculer: "npm:^0.14.35" moment-timezone: "npm:^0.5.48" mongo-message-queue: "npm:^1.1.0" - mongodb: "npm:6.10.0" + mongodb: "npm:6.16.0" nats: "npm:^2.28.2" pino: "npm:^8.21.0" polka: "npm:^0.5.2" @@ -9992,7 +9992,7 @@ __metadata: ajv-formats: "npm:^3.0.1" eslint: "npm:~8.45.0" jest: "npm:~30.2.0" - mongodb: "npm:6.10.0" + mongodb: "npm:6.16.0" typescript: "npm:~5.9.3" languageName: unknown linkType: soft @@ -10102,7 +10102,7 @@ __metadata: eventemitter3: "npm:^5.0.1" mem: "npm:^8.1.1" moleculer: "npm:^0.14.35" - mongodb: "npm:6.10.0" + mongodb: "npm:6.16.0" nats: "npm:^2.28.2" pino: "npm:^8.21.0" polka: "npm:^0.5.2" @@ -10327,7 +10327,7 @@ __metadata: eslint-plugin-react-hooks: "npm:^5.0.0" i18next: "npm:~23.4.9" jest: "npm:~30.2.0" - mongodb: "npm:6.10.0" + mongodb: "npm:6.16.0" react: "npm:~18.3.1" typescript: "npm:~5.9.3" peerDependencies: @@ -33862,7 +33862,7 @@ __metadata: jaeger-client: "npm:^3.19.0" mem: "npm:^8.1.1" moleculer: "npm:^0.14.35" - mongodb: "npm:6.10.0" + mongodb: "npm:6.16.0" nats: "npm:^2.28.2" npm-run-all: "npm:^4.1.5" pino: "npm:^8.21.0" From 65fbcbed9f64004b953dd9d4182b3fccb8147339 Mon Sep 17 00:00:00 2001 From: Jean Brito Date: Thu, 30 Oct 2025 15:59:28 -0300 Subject: [PATCH 022/129] fix: outlook notification showing event time in the wrong timezone (#37318) Co-authored-by: Pierre Lehnen <55164754+pierre-lehnen-rc@users.noreply.github.com> --- .changeset/proud-dryers-jump.md | 7 +++++++ .../loggedIn/useNotificationUserCalendar.ts | 18 +++++++++++++++++- .../meteor/server/services/calendar/service.ts | 1 + packages/core-typings/src/INotification.ts | 1 + 4 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 .changeset/proud-dryers-jump.md diff --git a/.changeset/proud-dryers-jump.md b/.changeset/proud-dryers-jump.md new file mode 100644 index 0000000000000..178e98db12d42 --- /dev/null +++ b/.changeset/proud-dryers-jump.md @@ -0,0 +1,7 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/core-typings": minor +--- + +Fixes the time display in calendar event notifications by converting the UTC time to the local time. + diff --git a/apps/meteor/client/views/root/hooks/loggedIn/useNotificationUserCalendar.ts b/apps/meteor/client/views/root/hooks/loggedIn/useNotificationUserCalendar.ts index 0785cba01c4c3..8bf576c35629a 100644 --- a/apps/meteor/client/views/root/hooks/loggedIn/useNotificationUserCalendar.ts +++ b/apps/meteor/client/views/root/hooks/loggedIn/useNotificationUserCalendar.ts @@ -15,8 +15,24 @@ export const useNotificationUserCalendar = (user: IUser) => { return; } + let body = notification.text; + if (notification.payload?.startTimeUtc) { + try { + const time = new Date(notification.payload.startTimeUtc); + const formattedTime = time.toLocaleTimeString(undefined, { + hour: 'numeric', + minute: 'numeric', + dayPeriod: 'narrow', + }); + body = formattedTime; + } catch (error) { + console.error('Failed to format calendar notification time:', error); + body = notification.text; + } + } + const n = new Notification(notification.title, { - body: notification.text, + body, tag: notification.payload._id, silent: true, requireInteraction, diff --git a/apps/meteor/server/services/calendar/service.ts b/apps/meteor/server/services/calendar/service.ts index 642f13f114c8f..b6b469ddd46fa 100644 --- a/apps/meteor/server/services/calendar/service.ts +++ b/apps/meteor/server/services/calendar/service.ts @@ -355,6 +355,7 @@ export class CalendarService extends ServiceClassInternal implements ICalendarSe text: event.startTime.toLocaleTimeString(undefined, { hour: 'numeric', minute: 'numeric', dayPeriod: 'narrow' }), payload: { _id: event._id, + startTimeUtc: event.startTime.toISOString(), }, }); } diff --git a/packages/core-typings/src/INotification.ts b/packages/core-typings/src/INotification.ts index c50dc3d3a4a22..1310d97ae4826 100644 --- a/packages/core-typings/src/INotification.ts +++ b/packages/core-typings/src/INotification.ts @@ -75,5 +75,6 @@ export interface ICalendarNotification { text: string; payload: { _id: ICalendarEvent['_id']; + startTimeUtc?: string; }; } From 79c67c7c1edf21617b467434df9eabd0160d94e2 Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Thu, 30 Oct 2025 22:34:47 -0300 Subject: [PATCH 023/129] feat: Disable confirm button while requesting delete message (#37276) --- .changeset/moody-spoons-press.md | 5 ++ .../lib/chats/flows/requestMessageDeletion.ts | 38 +++-------- .../DeleteMessageConfirmModal.tsx | 67 +++++++++++++++++++ .../modals/DeleteMessageConfirmModal/index.ts | 1 + apps/meteor/tests/e2e/message-actions.spec.ts | 7 +- .../page-objects/fragments/home-content.ts | 7 +- 6 files changed, 88 insertions(+), 37 deletions(-) create mode 100644 .changeset/moody-spoons-press.md create mode 100644 apps/meteor/client/views/room/modals/DeleteMessageConfirmModal/DeleteMessageConfirmModal.tsx create mode 100644 apps/meteor/client/views/room/modals/DeleteMessageConfirmModal/index.ts diff --git a/.changeset/moody-spoons-press.md b/.changeset/moody-spoons-press.md new file mode 100644 index 0000000000000..1d95ad29c88ce --- /dev/null +++ b/.changeset/moody-spoons-press.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': minor +--- + +Disables the delete message confirmation button to prevent the action from being triggered while the request is in progress \ No newline at end of file diff --git a/apps/meteor/client/lib/chats/flows/requestMessageDeletion.ts b/apps/meteor/client/lib/chats/flows/requestMessageDeletion.ts index 1cb73d222d9a2..4039b3ad8f6e3 100644 --- a/apps/meteor/client/lib/chats/flows/requestMessageDeletion.ts +++ b/apps/meteor/client/lib/chats/flows/requestMessageDeletion.ts @@ -1,7 +1,8 @@ import type { IMessage } from '@rocket.chat/core-typings'; -import { GenericModal, imperativeModal } from '@rocket.chat/ui-client'; +import { imperativeModal } from '@rocket.chat/ui-client'; import { t } from '../../../../app/utils/lib/i18n'; +import DeleteMessageConfirmModal from '../../../views/room/modals/DeleteMessageConfirmModal'; import { dispatchToastMessage } from '../../toast'; import type { ChatAPI } from '../ChatAPI'; @@ -15,28 +16,6 @@ export const requestMessageDeletion = async (chat: ChatAPI, message: IMessage): await new Promise((resolve, reject) => { const mid = chat.currentEditingMessage.getMID(); - const onConfirm = async (): Promise => { - try { - if (!(await chat.data.canDeleteMessage(message))) { - dispatchToastMessage({ type: 'error', message: t('Message_deleting_blocked') }); - return; - } - await chat.data.deleteMessage(message); - - imperativeModal.close(); - - if (mid === message._id) { - chat.currentEditingMessage.stop(); - } - chat.composer?.focus(); - - dispatchToastMessage({ type: 'success', message: t('Your_entry_has_been_deleted') }); - resolve(); - } catch (error) { - dispatchToastMessage({ type: 'error', message: error }); - reject(error); - } - }; const onCloseModal = async (): Promise => { imperativeModal.close(); @@ -50,14 +29,13 @@ export const requestMessageDeletion = async (chat: ChatAPI, message: IMessage): }; imperativeModal.open({ - component: GenericModal, + component: DeleteMessageConfirmModal, props: { - title: t('Are_you_sure'), - children: room ? t('The_message_is_a_discussion_you_will_not_be_able_to_recover') : t('You_will_not_be_able_to_recover'), - variant: 'danger', - confirmText: t('Yes_delete_it'), - onConfirm, - onClose: onCloseModal, + room, + chat, + resolve, + reject, + message, onCancel: onCloseModal, }, }); diff --git a/apps/meteor/client/views/room/modals/DeleteMessageConfirmModal/DeleteMessageConfirmModal.tsx b/apps/meteor/client/views/room/modals/DeleteMessageConfirmModal/DeleteMessageConfirmModal.tsx new file mode 100644 index 0000000000000..40c8486a03691 --- /dev/null +++ b/apps/meteor/client/views/room/modals/DeleteMessageConfirmModal/DeleteMessageConfirmModal.tsx @@ -0,0 +1,67 @@ +import type { IMessage, IRoom } from '@rocket.chat/core-typings'; +import { GenericModal } from '@rocket.chat/ui-client'; +import { useToastMessageDispatch } from '@rocket.chat/ui-contexts'; +import { useMutation } from '@tanstack/react-query'; +import { useTranslation } from 'react-i18next'; + +import type { ChatAPI } from '../../../../lib/chats/ChatAPI'; + +const DeleteMessageConfirmModal = ({ + room, + chat, + resolve, + reject, + onCancel, + message, +}: { + room?: IRoom; + chat: ChatAPI; + resolve: () => void; + reject: (reason?: any) => void; + message: IMessage; + onCancel: () => void; +}) => { + const { t } = useTranslation(); + const mid = chat.currentEditingMessage.getMID(); + const dispatchToastMessage = useToastMessageDispatch(); + + const deleteMessageMutation = useMutation({ + mutationFn: async () => { + if (!(await chat.data.canDeleteMessage(message))) { + throw new Error(t('Message_deleting_blocked')); + } + + await chat.data.deleteMessage(message); + }, + onSuccess: () => { + if (mid === message._id) { + chat.currentEditingMessage.stop(); + } + chat.composer?.focus(); + + dispatchToastMessage({ type: 'success', message: t('Your_entry_has_been_deleted') }); + resolve(); + }, + onError: (error) => { + dispatchToastMessage({ type: 'error', message: error }); + reject(error); + }, + onSettled: () => { + onCancel(); + }, + }); + + return ( + + ); +}; + +export default DeleteMessageConfirmModal; diff --git a/apps/meteor/client/views/room/modals/DeleteMessageConfirmModal/index.ts b/apps/meteor/client/views/room/modals/DeleteMessageConfirmModal/index.ts new file mode 100644 index 0000000000000..e4707a6f4d311 --- /dev/null +++ b/apps/meteor/client/views/room/modals/DeleteMessageConfirmModal/index.ts @@ -0,0 +1 @@ +export { default } from './DeleteMessageConfirmModal'; diff --git a/apps/meteor/tests/e2e/message-actions.spec.ts b/apps/meteor/tests/e2e/message-actions.spec.ts index 8c3f6079b6dd0..ae29a96d23dd3 100644 --- a/apps/meteor/tests/e2e/message-actions.spec.ts +++ b/apps/meteor/tests/e2e/message-actions.spec.ts @@ -116,11 +116,10 @@ test.describe.serial('message-actions', () => { await expect(poHomeChannel.content.lastUserMessageBody).toHaveText('this message was edited'); }); - test('expect message is deleted', async ({ page }) => { + test('should delete message ', async () => { await poHomeChannel.content.sendMessage('Message to delete'); - await poHomeChannel.content.openLastMessageMenu(); - await page.locator('role=menuitem[name="Delete"]').click(); - await page.locator('#modal-root .rcx-button-group--align-end .rcx-button--danger').click(); + await poHomeChannel.content.deleteLastMessage(); + await expect(poHomeChannel.content.lastUserMessage.locator('[data-qa-type="message-body"]:has-text("Message to delete")')).toHaveCount( 0, ); 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 f31a6c2619b1d..b0a474a00930c 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts @@ -438,9 +438,9 @@ export class HomeContent { } async openLastMessageMenu(): Promise { - await this.page.locator('[data-qa-type="message"]').last().hover(); - await this.page.locator('[data-qa-type="message"]').last().locator('role=button[name="More"]').waitFor(); - await this.page.locator('[data-qa-type="message"]').last().locator('role=button[name="More"]').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(); } get threadMessageList(): Locator { @@ -574,6 +574,7 @@ export class HomeContent { await this.openLastMessageMenu(); await this.btnOptionDeleteMessage.click(); await this.btnModalConfirmDelete.click(); + await expect(this.btnModalConfirmDelete).toBeDisabled(); } get btnClearSelection() { From bf64af2a643e510c1275caeafce2695db175f991 Mon Sep 17 00:00:00 2001 From: Yash Rajpal <58601732+yash-rajpal@users.noreply.github.com> Date: Fri, 31 Oct 2025 19:56:10 +0530 Subject: [PATCH 024/129] fix: Roles based mandatory two factor to check all cases (#37338) --- .changeset/tall-flies-jump.md | 5 +++ .../client/views/hooks/useRequire2faSetup.ts | 18 ++++++-- apps/meteor/tests/e2e/enforce-2FA.spec.ts | 41 +++++++++++++++++++ 3 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 .changeset/tall-flies-jump.md diff --git a/.changeset/tall-flies-jump.md b/.changeset/tall-flies-jump.md new file mode 100644 index 0000000000000..29586b0222ae9 --- /dev/null +++ b/.changeset/tall-flies-jump.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Improves mandatory role-based two-factor authentication setup to always verify available 2FA methods before enforcement. diff --git a/apps/meteor/client/views/hooks/useRequire2faSetup.ts b/apps/meteor/client/views/hooks/useRequire2faSetup.ts index a79994bf7b46a..f68952bbd9a91 100644 --- a/apps/meteor/client/views/hooks/useRequire2faSetup.ts +++ b/apps/meteor/client/views/hooks/useRequire2faSetup.ts @@ -5,14 +5,26 @@ import { Roles } from '../../stores'; export const useRequire2faSetup = () => { const user = useUser(); const tfaEnabled = useSetting('Accounts_TwoFactorAuthentication_Enabled', false); + const email2faEnabled = useSetting('Accounts_TwoFactorAuthentication_By_Email_Enabled', false); + const totp2faEnabled = useSetting('Accounts_TwoFactorAuthentication_By_TOTP_Enabled', false); + const is2FAEnabled = tfaEnabled && (email2faEnabled || totp2faEnabled); return Roles.use((state) => { - // User is already using 2fa - if (!user || user?.services?.totp?.enabled || user?.services?.email2fa?.enabled) { + if (!user || !is2FAEnabled) { return false; } const mandatoryRole = state.find((role) => !!role.mandatory2fa && user.roles?.includes(role._id)); - return mandatoryRole !== undefined && tfaEnabled; + + if (mandatoryRole === undefined) { + return false; + } + + const hasEmail2FA = !!user?.services?.email2fa?.enabled; + const hasTotp2FA = !!user?.services?.totp?.enabled; + + const hasAnyEnabled2FA = (email2faEnabled && hasEmail2FA) || (totp2faEnabled && hasTotp2FA); + + return !hasAnyEnabled2FA; }); }; diff --git a/apps/meteor/tests/e2e/enforce-2FA.spec.ts b/apps/meteor/tests/e2e/enforce-2FA.spec.ts index 188447df67365..ce2c74c65d54c 100644 --- a/apps/meteor/tests/e2e/enforce-2FA.spec.ts +++ b/apps/meteor/tests/e2e/enforce-2FA.spec.ts @@ -2,6 +2,7 @@ import { IS_EE } from './config/constants'; import { Users } from './fixtures/userStates'; import { HomeChannel, AccountProfile } from './page-objects'; import { createCustomRole, deleteCustomRole } from './utils/custom-role'; +import { setSettingValueById } from './utils/setSettingValueById'; import { test, expect } from './utils/test'; test.use({ storageState: Users.admin.state }); @@ -62,4 +63,44 @@ test.describe('enforce two factor authentication', () => { await expect(poHomeChannel.sidenav.sidebarHomeAction).toBeVisible(); await expect(poAccountProfile.securityHeader).not.toBeVisible(); }); + + test.describe('should still redirect to 2FA setup page when email 2FA is disabled', () => { + test.beforeAll(async ({ api }) => { + await setSettingValueById(api, 'Accounts_TwoFactorAuthentication_By_Email_Enabled', false); + }); + + test.afterAll(async ({ api }) => { + await setSettingValueById(api, 'Accounts_TwoFactorAuthentication_By_Email_Enabled', true); + }); + + test('should redirect to 2FA setup page and show totp 2FA setup', async ({ page }) => { + await page.goto('/home'); + await poAccountProfile.required2faModalSetUpButton.click(); + await expect(poHomeChannel.sidenav.sidebarHomeAction).not.toBeVisible(); + + await expect(poAccountProfile.securityHeader).toBeVisible(); + + await expect(poAccountProfile.security2FASection).toHaveAttribute('aria-expanded', 'true'); + await expect(poAccountProfile.totp2FASwitch).toBeVisible(); + await expect(poAccountProfile.email2FASwitch).not.toBeVisible(); + }); + }); + + test.describe('should not redirect to 2FA setup page when both email and totp 2FA are disabled', () => { + test.beforeAll(async ({ api }) => { + await setSettingValueById(api, 'Accounts_TwoFactorAuthentication_By_Email_Enabled', false); + await setSettingValueById(api, 'Accounts_TwoFactorAuthentication_By_TOTP_Enabled', false); + }); + + test.afterAll(async ({ api }) => { + await setSettingValueById(api, 'Accounts_TwoFactorAuthentication_By_Email_Enabled', true); + await setSettingValueById(api, 'Accounts_TwoFactorAuthentication_By_TOTP_Enabled', true); + }); + + test('should not redirect to 2FA setup page', async ({ page }) => { + await page.goto('/home'); + await expect(poHomeChannel.sidenav.sidebarHomeAction).toBeVisible(); + await expect(poAccountProfile.securityHeader).not.toBeVisible(); + }); + }); }); From 27980d49bfb4c5a17dac89f87080e8b398292524 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Fri, 31 Oct 2025 13:10:15 -0300 Subject: [PATCH 025/129] chore: Move `AppLayout` hooks (#37354) --- apps/meteor/client/views/root/AppLayout.tsx | 20 +++++++++---------- .../views/root/MainLayout/LoginPage.tsx | 2 +- .../{ => views/root}/hooks/useAnalytics.ts | 0 .../root}/hooks/useAnalyticsEventTracking.ts | 2 +- .../root}/hooks/useAutoupdate.spec.ts | 0 .../{ => views/root}/hooks/useAutoupdate.tsx | 2 +- .../views/root/hooks}/useCorsSSLConfig.ts | 0 .../views/root}/hooks/useEmojiOne.ts | 6 +++--- .../iframe => views/root/hooks}/useIframe.ts | 0 .../root/hooks}/useIframeLoginListener.ts | 0 .../root}/hooks/useLivechatEnterprise.ts | 10 +++++----- .../useLoadRoomForAllowedAnonymousRead.ts | 2 +- .../root/hooks}/useNotificationPermission.ts | 2 +- .../root/hooks}/useRedirectToSetupWizard.ts | 0 14 files changed, 23 insertions(+), 23 deletions(-) rename apps/meteor/client/{ => views/root}/hooks/useAnalytics.ts (100%) rename apps/meteor/client/{ => views/root}/hooks/useAnalyticsEventTracking.ts (99%) rename apps/meteor/client/{ => views/root}/hooks/useAutoupdate.spec.ts (100%) rename apps/meteor/client/{ => views/root}/hooks/useAutoupdate.tsx (90%) rename apps/meteor/{app/cors/client => client/views/root/hooks}/useCorsSSLConfig.ts (100%) rename apps/meteor/{app/emoji-emojione/client => client/views/root}/hooks/useEmojiOne.ts (88%) rename apps/meteor/client/{hooks/iframe => views/root/hooks}/useIframe.ts (100%) rename apps/meteor/client/{hooks/iframe => views/root/hooks}/useIframeLoginListener.ts (100%) rename apps/meteor/{app/livechat-enterprise => client/views/root}/hooks/useLivechatEnterprise.ts (54%) rename apps/meteor/client/{ => views/root}/hooks/useLoadRoomForAllowedAnonymousRead.ts (87%) rename apps/meteor/client/{hooks/notification => views/root/hooks}/useNotificationPermission.ts (89%) rename apps/meteor/client/{startup => views/root/hooks}/useRedirectToSetupWizard.ts (100%) diff --git a/apps/meteor/client/views/root/AppLayout.tsx b/apps/meteor/client/views/root/AppLayout.tsx index 198616b40c9ce..6daac59b4f863 100644 --- a/apps/meteor/client/views/root/AppLayout.tsx +++ b/apps/meteor/client/views/root/AppLayout.tsx @@ -11,26 +11,26 @@ import { useGitLabOAuth } from './hooks/customOAuth/useGitLabOAuth'; import { useNextcloudOAuth } from './hooks/customOAuth/useNextcloudOAuth'; import { useTokenpassOAuth } from './hooks/customOAuth/useTokenpassOAuth'; import { useWordPressOAuth } from './hooks/customOAuth/useWordPressOAuth'; +import { useAnalytics } from './hooks/useAnalytics'; +import { useAnalyticsEventTracking } from './hooks/useAnalyticsEventTracking'; +import { useAutoupdate } from './hooks/useAutoupdate'; import { useCodeHighlight } from './hooks/useCodeHighlight'; +import { useCorsSSLConfig } from './hooks/useCorsSSLConfig'; import { useDesktopFavicon } from './hooks/useDesktopFavicon'; import { useDesktopTitle } from './hooks/useDesktopTitle'; +import { useEmojiOne } from './hooks/useEmojiOne'; import { useEscapeKeyStroke } from './hooks/useEscapeKeyStroke'; import { useGoogleTagManager } from './hooks/useGoogleTagManager'; +import { useIframeLoginListener } from './hooks/useIframeLoginListener'; +import { useLivechatEnterprise } from './hooks/useLivechatEnterprise'; import { useLoadMissedMessages } from './hooks/useLoadMissedMessages'; +import { useLoadRoomForAllowedAnonymousRead } from './hooks/useLoadRoomForAllowedAnonymousRead'; import { useLoginViaQuery } from './hooks/useLoginViaQuery'; import { useMessageLinkClicks } from './hooks/useMessageLinkClicks'; +import { useNotificationPermission } from './hooks/useNotificationPermission'; +import { useRedirectToSetupWizard } from './hooks/useRedirectToSetupWizard'; import { useSettingsOnLoadSiteUrl } from './hooks/useSettingsOnLoadSiteUrl'; -import { useCorsSSLConfig } from '../../../app/cors/client/useCorsSSLConfig'; -import { useEmojiOne } from '../../../app/emoji-emojione/client/hooks/useEmojiOne'; -import { useLivechatEnterprise } from '../../../app/livechat-enterprise/hooks/useLivechatEnterprise'; -import { useIframeLoginListener } from '../../hooks/iframe/useIframeLoginListener'; -import { useNotificationPermission } from '../../hooks/notification/useNotificationPermission'; -import { useAnalytics } from '../../hooks/useAnalytics'; -import { useAnalyticsEventTracking } from '../../hooks/useAnalyticsEventTracking'; -import { useAutoupdate } from '../../hooks/useAutoupdate'; -import { useLoadRoomForAllowedAnonymousRead } from '../../hooks/useLoadRoomForAllowedAnonymousRead'; import { appLayout } from '../../lib/appLayout'; -import { useRedirectToSetupWizard } from '../../startup/useRedirectToSetupWizard'; const AppLayout = () => { useEffect(() => { diff --git a/apps/meteor/client/views/root/MainLayout/LoginPage.tsx b/apps/meteor/client/views/root/MainLayout/LoginPage.tsx index 65d0f3d048cf9..d270bcba6c465 100644 --- a/apps/meteor/client/views/root/MainLayout/LoginPage.tsx +++ b/apps/meteor/client/views/root/MainLayout/LoginPage.tsx @@ -6,7 +6,7 @@ import { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import LoggedOutBanner from '../../../components/deviceManagement/LoggedOutBanner'; -import { useIframe } from '../../../hooks/iframe/useIframe'; +import { useIframe } from '../hooks/useIframe'; const LoginPage = ({ defaultRoute, children }: { defaultRoute?: LoginRoutes; children?: ReactNode }): ReactElement => { const { t } = useTranslation(); diff --git a/apps/meteor/client/hooks/useAnalytics.ts b/apps/meteor/client/views/root/hooks/useAnalytics.ts similarity index 100% rename from apps/meteor/client/hooks/useAnalytics.ts rename to apps/meteor/client/views/root/hooks/useAnalytics.ts diff --git a/apps/meteor/client/hooks/useAnalyticsEventTracking.ts b/apps/meteor/client/views/root/hooks/useAnalyticsEventTracking.ts similarity index 99% rename from apps/meteor/client/hooks/useAnalyticsEventTracking.ts rename to apps/meteor/client/views/root/hooks/useAnalyticsEventTracking.ts index 9d1acf7b43182..dd443b97e750b 100644 --- a/apps/meteor/client/hooks/useAnalyticsEventTracking.ts +++ b/apps/meteor/client/views/root/hooks/useAnalyticsEventTracking.ts @@ -1,7 +1,7 @@ import { useRouter, useSetting, useUserId } from '@rocket.chat/ui-contexts'; import { useEffect } from 'react'; -import { callbacks } from '../../lib/callbacks'; +import { callbacks } from '../../../../lib/callbacks'; function trackEvent(category: string, action: string, label?: unknown) { const { _paq, ga } = window; diff --git a/apps/meteor/client/hooks/useAutoupdate.spec.ts b/apps/meteor/client/views/root/hooks/useAutoupdate.spec.ts similarity index 100% rename from apps/meteor/client/hooks/useAutoupdate.spec.ts rename to apps/meteor/client/views/root/hooks/useAutoupdate.spec.ts diff --git a/apps/meteor/client/hooks/useAutoupdate.tsx b/apps/meteor/client/views/root/hooks/useAutoupdate.tsx similarity index 90% rename from apps/meteor/client/hooks/useAutoupdate.tsx rename to apps/meteor/client/views/root/hooks/useAutoupdate.tsx index 6d73dee356089..28bd29ad88bf8 100644 --- a/apps/meteor/client/hooks/useAutoupdate.tsx +++ b/apps/meteor/client/views/root/hooks/useAutoupdate.tsx @@ -2,7 +2,7 @@ import { useToastMessageDispatch } from '@rocket.chat/ui-contexts'; import { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { AutoupdateToastMessage } from '../components/AutoupdateToastMessage'; +import { AutoupdateToastMessage } from '../../../components/AutoupdateToastMessage'; export const useAutoupdate = () => { const toast = useToastMessageDispatch(); diff --git a/apps/meteor/app/cors/client/useCorsSSLConfig.ts b/apps/meteor/client/views/root/hooks/useCorsSSLConfig.ts similarity index 100% rename from apps/meteor/app/cors/client/useCorsSSLConfig.ts rename to apps/meteor/client/views/root/hooks/useCorsSSLConfig.ts diff --git a/apps/meteor/app/emoji-emojione/client/hooks/useEmojiOne.ts b/apps/meteor/client/views/root/hooks/useEmojiOne.ts similarity index 88% rename from apps/meteor/app/emoji-emojione/client/hooks/useEmojiOne.ts rename to apps/meteor/client/views/root/hooks/useEmojiOne.ts index bf8919d200175..8c6d94b7ab753 100644 --- a/apps/meteor/app/emoji-emojione/client/hooks/useEmojiOne.ts +++ b/apps/meteor/client/views/root/hooks/useEmojiOne.ts @@ -1,9 +1,9 @@ import { useUserPreference } from '@rocket.chat/ui-contexts'; import { useEffect, useLayoutEffect } from 'react'; -import { emoji } from '../../../emoji/client'; -import { getEmojiConfig } from '../../lib/getEmojiConfig'; -import { isSetNotNull } from '../../lib/isSetNotNull'; +import { emoji } from '../../../../app/emoji/client'; +import { getEmojiConfig } from '../../../../app/emoji-emojione/lib/getEmojiConfig'; +import { isSetNotNull } from '../../../../app/emoji-emojione/lib/isSetNotNull'; const config = getEmojiConfig(); diff --git a/apps/meteor/client/hooks/iframe/useIframe.ts b/apps/meteor/client/views/root/hooks/useIframe.ts similarity index 100% rename from apps/meteor/client/hooks/iframe/useIframe.ts rename to apps/meteor/client/views/root/hooks/useIframe.ts diff --git a/apps/meteor/client/hooks/iframe/useIframeLoginListener.ts b/apps/meteor/client/views/root/hooks/useIframeLoginListener.ts similarity index 100% rename from apps/meteor/client/hooks/iframe/useIframeLoginListener.ts rename to apps/meteor/client/views/root/hooks/useIframeLoginListener.ts diff --git a/apps/meteor/app/livechat-enterprise/hooks/useLivechatEnterprise.ts b/apps/meteor/client/views/root/hooks/useLivechatEnterprise.ts similarity index 54% rename from apps/meteor/app/livechat-enterprise/hooks/useLivechatEnterprise.ts rename to apps/meteor/client/views/root/hooks/useLivechatEnterprise.ts index e9bb5a5f35f78..0e3763232d837 100644 --- a/apps/meteor/app/livechat-enterprise/hooks/useLivechatEnterprise.ts +++ b/apps/meteor/client/views/root/hooks/useLivechatEnterprise.ts @@ -1,11 +1,11 @@ import { useSetting } from '@rocket.chat/ui-contexts'; import { useEffect } from 'react'; -import { useHasLicenseModule } from '../../../client/hooks/useHasLicenseModule'; -import { businessHourManager } from '../../livechat/client/views/app/business-hours/BusinessHours'; -import type { IBusinessHourBehavior } from '../../livechat/client/views/app/business-hours/IBusinessHourBehavior'; -import { SingleBusinessHourBehavior } from '../../livechat/client/views/app/business-hours/Single'; -import { MultipleBusinessHoursBehavior } from '../client/views/business-hours/Multiple'; +import { businessHourManager } from '../../../../app/livechat/client/views/app/business-hours/BusinessHours'; +import type { IBusinessHourBehavior } from '../../../../app/livechat/client/views/app/business-hours/IBusinessHourBehavior'; +import { SingleBusinessHourBehavior } from '../../../../app/livechat/client/views/app/business-hours/Single'; +import { MultipleBusinessHoursBehavior } from '../../../../app/livechat-enterprise/client/views/business-hours/Multiple'; +import { useHasLicenseModule } from '../../../hooks/useHasLicenseModule'; const businessHours: Record = { multiple: new MultipleBusinessHoursBehavior(), diff --git a/apps/meteor/client/hooks/useLoadRoomForAllowedAnonymousRead.ts b/apps/meteor/client/views/root/hooks/useLoadRoomForAllowedAnonymousRead.ts similarity index 87% rename from apps/meteor/client/hooks/useLoadRoomForAllowedAnonymousRead.ts rename to apps/meteor/client/views/root/hooks/useLoadRoomForAllowedAnonymousRead.ts index 444721517775a..a66cbed2d38ca 100644 --- a/apps/meteor/client/hooks/useLoadRoomForAllowedAnonymousRead.ts +++ b/apps/meteor/client/views/root/hooks/useLoadRoomForAllowedAnonymousRead.ts @@ -1,7 +1,7 @@ import { useSetting, useUserId } from '@rocket.chat/ui-contexts'; import { useEffect } from 'react'; -import { RoomsCachedStore, SubscriptionsCachedStore } from '../cachedStores'; +import { RoomsCachedStore, SubscriptionsCachedStore } from '../../../cachedStores'; export const useLoadRoomForAllowedAnonymousRead = () => { const userId = useUserId(); diff --git a/apps/meteor/client/hooks/notification/useNotificationPermission.ts b/apps/meteor/client/views/root/hooks/useNotificationPermission.ts similarity index 89% rename from apps/meteor/client/hooks/notification/useNotificationPermission.ts rename to apps/meteor/client/views/root/hooks/useNotificationPermission.ts index abd6c95be9f71..e5e3faeb8ee71 100644 --- a/apps/meteor/client/hooks/notification/useNotificationPermission.ts +++ b/apps/meteor/client/views/root/hooks/useNotificationPermission.ts @@ -1,6 +1,6 @@ import { useEffectEvent } from '@rocket.chat/fuselage-hooks'; -import { notificationManager } from '../../lib/notificationManager'; +import { notificationManager } from '../../../lib/notificationManager'; export const useNotificationPermission = () => { const requestPermission = useEffectEvent(async () => { diff --git a/apps/meteor/client/startup/useRedirectToSetupWizard.ts b/apps/meteor/client/views/root/hooks/useRedirectToSetupWizard.ts similarity index 100% rename from apps/meteor/client/startup/useRedirectToSetupWizard.ts rename to apps/meteor/client/views/root/hooks/useRedirectToSetupWizard.ts From 94349918a7a56b2a713793ff4f21a7c63a6a4f63 Mon Sep 17 00:00:00 2001 From: Tasso Evangelista Date: Fri, 31 Oct 2025 14:15:04 -0300 Subject: [PATCH 026/129] chore: Move Meteor specific startup code (#37199) --- .../meteor/client/lib/customOAuth/CustomOAuth.ts | 2 +- apps/meteor/client/main.ts | 6 +++--- .../client/meteor/{overrides => }/login/cas.ts | 4 ++-- .../client/meteor/{overrides => }/login/crowd.ts | 2 +- .../meteor/{overrides => }/login/facebook.ts | 4 ++-- .../meteor/{overrides => }/login/github.ts | 4 ++-- .../meteor/{overrides => }/login/google.ts | 4 ++-- apps/meteor/client/meteor/login/index.ts | 11 +++++++++++ .../client/meteor/{overrides => }/login/ldap.ts | 2 +- .../login/meteorDeveloperAccount.ts | 4 ++-- .../client/meteor/{overrides => }/login/oauth.ts | 6 +++--- .../meteor/{overrides => }/login/password.ts | 2 +- .../client/meteor/{overrides => }/login/saml.ts | 6 +++--- .../meteor/{overrides => }/login/twitter.ts | 4 ++-- .../overrides}/desktopInjection.ts | 4 ++-- apps/meteor/client/meteor/overrides/index.ts | 16 +++------------- .../client/{ => meteor}/startup/absoluteUrl.ts | 2 +- .../client/{ => meteor}/startup/accounts.ts | 12 ++++++------ apps/meteor/client/meteor/startup/index.ts | 2 ++ apps/meteor/client/startup/index.ts | 1 - 20 files changed, 50 insertions(+), 48 deletions(-) rename apps/meteor/client/meteor/{overrides => }/login/cas.ts (83%) rename apps/meteor/client/meteor/{overrides => }/login/crowd.ts (96%) rename apps/meteor/client/meteor/{overrides => }/login/facebook.ts (93%) rename apps/meteor/client/meteor/{overrides => }/login/github.ts (90%) rename apps/meteor/client/meteor/{overrides => }/login/google.ts (95%) create mode 100644 apps/meteor/client/meteor/login/index.ts rename apps/meteor/client/meteor/{overrides => }/login/ldap.ts (96%) rename apps/meteor/client/meteor/{overrides => }/login/meteorDeveloperAccount.ts (91%) rename apps/meteor/client/meteor/{overrides => }/login/oauth.ts (94%) rename apps/meteor/client/meteor/{overrides => }/login/password.ts (93%) rename apps/meteor/client/meteor/{overrides => }/login/saml.ts (95%) rename apps/meteor/client/meteor/{overrides => }/login/twitter.ts (91%) rename apps/meteor/client/{startup => meteor/overrides}/desktopInjection.ts (89%) rename apps/meteor/client/{ => meteor}/startup/absoluteUrl.ts (67%) rename apps/meteor/client/{ => meteor}/startup/accounts.ts (84%) create mode 100644 apps/meteor/client/meteor/startup/index.ts diff --git a/apps/meteor/client/lib/customOAuth/CustomOAuth.ts b/apps/meteor/client/lib/customOAuth/CustomOAuth.ts index 7a0c1c3548d38..8ac31adc0a5ce 100644 --- a/apps/meteor/client/lib/customOAuth/CustomOAuth.ts +++ b/apps/meteor/client/lib/customOAuth/CustomOAuth.ts @@ -7,7 +7,7 @@ import { OAuth } from 'meteor/oauth'; import { isURL } from '../../../lib/utils/isURL'; import type { IOAuthProvider } from '../../definitions/IOAuthProvider'; -import { createOAuthTotpLoginMethod } from '../../meteor/overrides/login/oauth'; +import { createOAuthTotpLoginMethod } from '../../meteor/login/oauth'; import { overrideLoginMethod, type LoginCallback } from '../2fa/overrideLoginMethod'; import { loginServices } from '../loginServices'; import { CustomOAuthError } from './CustomOAuthError'; diff --git a/apps/meteor/client/main.ts b/apps/meteor/client/main.ts index 7edd934d5264d..f6b040937176e 100644 --- a/apps/meteor/client/main.ts +++ b/apps/meteor/client/main.ts @@ -1,9 +1,9 @@ +import './meteor/overrides'; +import './meteor/startup'; import './serviceWorker'; -import './startup/accounts'; -import './startup/desktopInjection'; import('@rocket.chat/fuselage-polyfills') - .then(() => import('./meteor/overrides')) + .then(() => import('./meteor/login')) .then(() => import('./ecdh')) .then(() => import('./importPackages')) .then(() => import('./startup')) diff --git a/apps/meteor/client/meteor/overrides/login/cas.ts b/apps/meteor/client/meteor/login/cas.ts similarity index 83% rename from apps/meteor/client/meteor/overrides/login/cas.ts rename to apps/meteor/client/meteor/login/cas.ts index 9b753d5042943..93a9f1d5b2365 100644 --- a/apps/meteor/client/meteor/overrides/login/cas.ts +++ b/apps/meteor/client/meteor/login/cas.ts @@ -1,7 +1,7 @@ import { Random } from '@rocket.chat/random'; import { Meteor } from 'meteor/meteor'; -import { callLoginMethod } from '../../../lib/2fa/overrideLoginMethod'; +import { callLoginMethod } from '../../lib/2fa/overrideLoginMethod'; declare module 'meteor/meteor' { // eslint-disable-next-line @typescript-eslint/no-namespace @@ -12,7 +12,7 @@ declare module 'meteor/meteor' { Meteor.loginWithCas = (_, callback) => { const credentialToken = Random.id(); - import('../../../lib/openCASLoginPopup') + import('../../lib/openCASLoginPopup') .then(({ openCASLoginPopup }) => openCASLoginPopup(credentialToken)) .then(() => callLoginMethod({ methodArguments: [{ cas: { credentialToken } }] })) .then(() => callback?.()) diff --git a/apps/meteor/client/meteor/overrides/login/crowd.ts b/apps/meteor/client/meteor/login/crowd.ts similarity index 96% rename from apps/meteor/client/meteor/overrides/login/crowd.ts rename to apps/meteor/client/meteor/login/crowd.ts index a07a149c09be0..9b1d4b83d4025 100644 --- a/apps/meteor/client/meteor/overrides/login/crowd.ts +++ b/apps/meteor/client/meteor/login/crowd.ts @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor'; -import { callLoginMethod, handleLogin, type LoginCallback } from '../../../lib/2fa/overrideLoginMethod'; +import { callLoginMethod, handleLogin, type LoginCallback } from '../../lib/2fa/overrideLoginMethod'; declare module 'meteor/meteor' { // eslint-disable-next-line @typescript-eslint/no-namespace diff --git a/apps/meteor/client/meteor/overrides/login/facebook.ts b/apps/meteor/client/meteor/login/facebook.ts similarity index 93% rename from apps/meteor/client/meteor/overrides/login/facebook.ts rename to apps/meteor/client/meteor/login/facebook.ts index 8fdeac364ff79..f13b22daec289 100644 --- a/apps/meteor/client/meteor/overrides/login/facebook.ts +++ b/apps/meteor/client/meteor/login/facebook.ts @@ -7,8 +7,8 @@ import { Meteor } from 'meteor/meteor'; import { OAuth } from 'meteor/oauth'; import { createOAuthTotpLoginMethod } from './oauth'; -import { overrideLoginMethod } from '../../../lib/2fa/overrideLoginMethod'; -import { wrapRequestCredentialFn } from '../../../lib/wrapRequestCredentialFn'; +import { overrideLoginMethod } from '../../lib/2fa/overrideLoginMethod'; +import { wrapRequestCredentialFn } from '../../lib/wrapRequestCredentialFn'; const { loginWithFacebook } = Meteor; const loginWithFacebookAndTOTP = createOAuthTotpLoginMethod(Facebook); diff --git a/apps/meteor/client/meteor/overrides/login/github.ts b/apps/meteor/client/meteor/login/github.ts similarity index 90% rename from apps/meteor/client/meteor/overrides/login/github.ts rename to apps/meteor/client/meteor/login/github.ts index 2402ed858a959..98c8fb8fea76d 100644 --- a/apps/meteor/client/meteor/overrides/login/github.ts +++ b/apps/meteor/client/meteor/login/github.ts @@ -7,8 +7,8 @@ import { Meteor } from 'meteor/meteor'; import { OAuth } from 'meteor/oauth'; import { createOAuthTotpLoginMethod } from './oauth'; -import { overrideLoginMethod } from '../../../lib/2fa/overrideLoginMethod'; -import { wrapRequestCredentialFn } from '../../../lib/wrapRequestCredentialFn'; +import { overrideLoginMethod } from '../../lib/2fa/overrideLoginMethod'; +import { wrapRequestCredentialFn } from '../../lib/wrapRequestCredentialFn'; const { loginWithGithub } = Meteor; const loginWithGithubAndTOTP = createOAuthTotpLoginMethod(Github); diff --git a/apps/meteor/client/meteor/overrides/login/google.ts b/apps/meteor/client/meteor/login/google.ts similarity index 95% rename from apps/meteor/client/meteor/overrides/login/google.ts rename to apps/meteor/client/meteor/login/google.ts index 01d7be4143b2d..9eaf46ac40b44 100644 --- a/apps/meteor/client/meteor/overrides/login/google.ts +++ b/apps/meteor/client/meteor/login/google.ts @@ -7,8 +7,8 @@ import { Meteor } from 'meteor/meteor'; import { OAuth } from 'meteor/oauth'; import { createOAuthTotpLoginMethod } from './oauth'; -import { overrideLoginMethod, type LoginCallback } from '../../../lib/2fa/overrideLoginMethod'; -import { wrapRequestCredentialFn } from '../../../lib/wrapRequestCredentialFn'; +import { overrideLoginMethod, type LoginCallback } from '../../lib/2fa/overrideLoginMethod'; +import { wrapRequestCredentialFn } from '../../lib/wrapRequestCredentialFn'; declare module 'meteor/meteor' { // eslint-disable-next-line @typescript-eslint/no-namespace diff --git a/apps/meteor/client/meteor/login/index.ts b/apps/meteor/client/meteor/login/index.ts new file mode 100644 index 0000000000000..cef3570085f43 --- /dev/null +++ b/apps/meteor/client/meteor/login/index.ts @@ -0,0 +1,11 @@ +import './cas'; +import './crowd'; +import './facebook'; +import './github'; +import './google'; +import './ldap'; +import './meteorDeveloperAccount'; +import './oauth'; +import './password'; +import './saml'; +import './twitter'; diff --git a/apps/meteor/client/meteor/overrides/login/ldap.ts b/apps/meteor/client/meteor/login/ldap.ts similarity index 96% rename from apps/meteor/client/meteor/overrides/login/ldap.ts rename to apps/meteor/client/meteor/login/ldap.ts index 23238126ea5ea..77a16ce3675d4 100644 --- a/apps/meteor/client/meteor/overrides/login/ldap.ts +++ b/apps/meteor/client/meteor/login/ldap.ts @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor'; -import { callLoginMethod, handleLogin, type LoginCallback } from '../../../lib/2fa/overrideLoginMethod'; +import { callLoginMethod, handleLogin, type LoginCallback } from '../../lib/2fa/overrideLoginMethod'; declare module 'meteor/meteor' { // eslint-disable-next-line @typescript-eslint/no-namespace diff --git a/apps/meteor/client/meteor/overrides/login/meteorDeveloperAccount.ts b/apps/meteor/client/meteor/login/meteorDeveloperAccount.ts similarity index 91% rename from apps/meteor/client/meteor/overrides/login/meteorDeveloperAccount.ts rename to apps/meteor/client/meteor/login/meteorDeveloperAccount.ts index 561d778ee52f3..1b499a00b7dec 100644 --- a/apps/meteor/client/meteor/overrides/login/meteorDeveloperAccount.ts +++ b/apps/meteor/client/meteor/login/meteorDeveloperAccount.ts @@ -5,8 +5,8 @@ import { MeteorDeveloperAccounts } from 'meteor/meteor-developer-oauth'; import { OAuth } from 'meteor/oauth'; import { createOAuthTotpLoginMethod } from './oauth'; -import { overrideLoginMethod } from '../../../lib/2fa/overrideLoginMethod'; -import { wrapRequestCredentialFn } from '../../../lib/wrapRequestCredentialFn'; +import { overrideLoginMethod } from '../../lib/2fa/overrideLoginMethod'; +import { wrapRequestCredentialFn } from '../../lib/wrapRequestCredentialFn'; const { loginWithMeteorDeveloperAccount } = Meteor; const loginWithMeteorDeveloperAccountAndTOTP = createOAuthTotpLoginMethod(MeteorDeveloperAccounts); diff --git a/apps/meteor/client/meteor/overrides/login/oauth.ts b/apps/meteor/client/meteor/login/oauth.ts similarity index 94% rename from apps/meteor/client/meteor/overrides/login/oauth.ts rename to apps/meteor/client/meteor/login/oauth.ts index cc31be9fb677d..a3f9d72c9cbf8 100644 --- a/apps/meteor/client/meteor/overrides/login/oauth.ts +++ b/apps/meteor/client/meteor/login/oauth.ts @@ -2,8 +2,8 @@ import { Accounts } from 'meteor/accounts-base'; import { Meteor } from 'meteor/meteor'; import { OAuth } from 'meteor/oauth'; -import type { IOAuthProvider } from '../../../definitions/IOAuthProvider'; -import type { LoginCallback } from '../../../lib/2fa/overrideLoginMethod'; +import type { IOAuthProvider } from '../../definitions/IOAuthProvider'; +import type { LoginCallback } from '../../lib/2fa/overrideLoginMethod'; const isLoginCancelledError = (error: unknown): error is Meteor.Error => error instanceof Meteor.Error && error.error === Accounts.LoginCancelledError.numericError; @@ -113,7 +113,7 @@ Accounts.onPageLoadLogin(async (loginAttempt: any) => { const { credentialToken, credentialSecret } = oAuthArgs.oauth; const cb = loginAttempt.userCallback; - const { process2faReturn } = await import('../../../lib/2fa/process2faReturn'); + const { process2faReturn } = await import('../../lib/2fa/process2faReturn'); await process2faReturn({ error: loginAttempt.error, diff --git a/apps/meteor/client/meteor/overrides/login/password.ts b/apps/meteor/client/meteor/login/password.ts similarity index 93% rename from apps/meteor/client/meteor/overrides/login/password.ts rename to apps/meteor/client/meteor/login/password.ts index 7074290b1f2c5..f1c6e32f2282b 100644 --- a/apps/meteor/client/meteor/overrides/login/password.ts +++ b/apps/meteor/client/meteor/login/password.ts @@ -1,7 +1,7 @@ import { Accounts } from 'meteor/accounts-base'; import { Meteor } from 'meteor/meteor'; -import { overrideLoginMethod, type LoginCallback } from '../../../lib/2fa/overrideLoginMethod'; +import { overrideLoginMethod, type LoginCallback } from '../../lib/2fa/overrideLoginMethod'; declare module 'meteor/meteor' { // eslint-disable-next-line @typescript-eslint/no-namespace diff --git a/apps/meteor/client/meteor/overrides/login/saml.ts b/apps/meteor/client/meteor/login/saml.ts similarity index 95% rename from apps/meteor/client/meteor/overrides/login/saml.ts rename to apps/meteor/client/meteor/login/saml.ts index 052fa42033594..0561d6db13ad8 100644 --- a/apps/meteor/client/meteor/overrides/login/saml.ts +++ b/apps/meteor/client/meteor/login/saml.ts @@ -2,8 +2,8 @@ import { Random } from '@rocket.chat/random'; import { Accounts } from 'meteor/accounts-base'; import { Meteor } from 'meteor/meteor'; -import { type LoginCallback, callLoginMethod, handleLogin } from '../../../lib/2fa/overrideLoginMethod'; -import { settings } from '../../../lib/settings'; +import { type LoginCallback, callLoginMethod, handleLogin } from '../../lib/2fa/overrideLoginMethod'; +import { settings } from '../../lib/settings'; declare module 'meteor/meteor' { // eslint-disable-next-line @typescript-eslint/no-namespace @@ -61,7 +61,7 @@ Meteor.logout = async function (...args) { if (provider && settings.peek('SAML_Custom_Default_idp_slo_redirect_url')) { console.info('SAML session terminated via SLO'); - const { sdk } = await import('../../../../app/utils/client/lib/SDKClient'); + const { sdk } = await import('../../../app/utils/client/lib/SDKClient'); sdk .call('samlLogout', provider) .then((result) => { diff --git a/apps/meteor/client/meteor/overrides/login/twitter.ts b/apps/meteor/client/meteor/login/twitter.ts similarity index 91% rename from apps/meteor/client/meteor/overrides/login/twitter.ts rename to apps/meteor/client/meteor/login/twitter.ts index f20de544b89b4..dc72000923d63 100644 --- a/apps/meteor/client/meteor/overrides/login/twitter.ts +++ b/apps/meteor/client/meteor/login/twitter.ts @@ -7,8 +7,8 @@ import { OAuth } from 'meteor/oauth'; import { Twitter } from 'meteor/twitter-oauth'; import { createOAuthTotpLoginMethod } from './oauth'; -import { overrideLoginMethod } from '../../../lib/2fa/overrideLoginMethod'; -import { wrapRequestCredentialFn } from '../../../lib/wrapRequestCredentialFn'; +import { overrideLoginMethod } from '../../lib/2fa/overrideLoginMethod'; +import { wrapRequestCredentialFn } from '../../lib/wrapRequestCredentialFn'; const { loginWithTwitter } = Meteor; const loginWithTwitterAndTOTP = createOAuthTotpLoginMethod(Twitter); diff --git a/apps/meteor/client/startup/desktopInjection.ts b/apps/meteor/client/meteor/overrides/desktopInjection.ts similarity index 89% rename from apps/meteor/client/startup/desktopInjection.ts rename to apps/meteor/client/meteor/overrides/desktopInjection.ts index f3d20b525e92f..9f835424c7644 100644 --- a/apps/meteor/client/startup/desktopInjection.ts +++ b/apps/meteor/client/meteor/overrides/desktopInjection.ts @@ -1,5 +1,5 @@ -import { watch } from '../meteor/watch'; -import { PublicSettings } from '../stores'; +import { PublicSettings } from '../../stores'; +import { watch } from '../watch'; if (window.RocketChatDesktop) { // backport of rocketchat:user-presence for the desktop app diff --git a/apps/meteor/client/meteor/overrides/index.ts b/apps/meteor/client/meteor/overrides/index.ts index 83ff25324adcf..f3370db0592f5 100644 --- a/apps/meteor/client/meteor/overrides/index.ts +++ b/apps/meteor/client/meteor/overrides/index.ts @@ -1,17 +1,7 @@ import './ddpOverREST'; -import './totpOnCall'; +import './desktopInjection'; import './oauthRedirectUri'; +import './settings'; +import './totpOnCall'; import './unstoreLoginToken'; import './userAndUsers'; -import './login/cas'; -import './login/crowd'; -import './login/facebook'; -import './login/github'; -import './login/google'; -import './login/ldap'; -import './login/meteorDeveloperAccount'; -import './login/oauth'; -import './login/password'; -import './login/saml'; -import './login/twitter'; -import './settings'; diff --git a/apps/meteor/client/startup/absoluteUrl.ts b/apps/meteor/client/meteor/startup/absoluteUrl.ts similarity index 67% rename from apps/meteor/client/startup/absoluteUrl.ts rename to apps/meteor/client/meteor/startup/absoluteUrl.ts index 04a698b6cc848..8e2578cad8b7b 100644 --- a/apps/meteor/client/startup/absoluteUrl.ts +++ b/apps/meteor/client/meteor/startup/absoluteUrl.ts @@ -1,5 +1,5 @@ import { Meteor } from 'meteor/meteor'; -import { baseURI } from '../lib/baseURI'; +import { baseURI } from '../../lib/baseURI'; Meteor.absoluteUrl.defaultOptions.rootUrl = baseURI; diff --git a/apps/meteor/client/startup/accounts.ts b/apps/meteor/client/meteor/startup/accounts.ts similarity index 84% rename from apps/meteor/client/startup/accounts.ts rename to apps/meteor/client/meteor/startup/accounts.ts index d61734864823d..3fbb07686fcd9 100644 --- a/apps/meteor/client/startup/accounts.ts +++ b/apps/meteor/client/meteor/startup/accounts.ts @@ -1,12 +1,12 @@ import { Accounts } from 'meteor/accounts-base'; import { Meteor } from 'meteor/meteor'; -import { sdk } from '../../app/utils/client/lib/SDKClient'; -import { t } from '../../app/utils/lib/i18n'; -import { PublicSettingsCachedStore, SubscriptionsCachedStore } from '../cachedStores'; -import { dispatchToastMessage } from '../lib/toast'; -import { userIdStore } from '../lib/user'; -import { useUserDataSyncReady } from '../lib/userData'; +import { sdk } from '../../../app/utils/client/lib/SDKClient'; +import { t } from '../../../app/utils/lib/i18n'; +import { PublicSettingsCachedStore, SubscriptionsCachedStore } from '../../cachedStores'; +import { dispatchToastMessage } from '../../lib/toast'; +import { userIdStore } from '../../lib/user'; +import { useUserDataSyncReady } from '../../lib/userData'; const whenMainReady = (): Promise => { const isMainReady = (): boolean => { diff --git a/apps/meteor/client/meteor/startup/index.ts b/apps/meteor/client/meteor/startup/index.ts new file mode 100644 index 0000000000000..286913b22c6a4 --- /dev/null +++ b/apps/meteor/client/meteor/startup/index.ts @@ -0,0 +1,2 @@ +import './absoluteUrl'; +import './accounts'; diff --git a/apps/meteor/client/startup/index.ts b/apps/meteor/client/startup/index.ts index 9f5e4eb11b4cd..689d5cbe33aa0 100644 --- a/apps/meteor/client/startup/index.ts +++ b/apps/meteor/client/startup/index.ts @@ -1,5 +1,4 @@ import '../lib/rooms/roomTypes'; -import './absoluteUrl'; import './appRoot'; import './audit'; import './callbacks'; From 99255057f27b9d854a89da815681a03d0776f846 Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Mon, 3 Nov 2025 12:27:44 -0300 Subject: [PATCH 027/129] fix(Enhanced navigation): Change breakpoint for collapsing sidebar (#37350) --- .changeset/hot-cups-smash.md | 6 ++++++ apps/meteor/client/NavBarV2/NavBarPagesSection.tsx | 4 ++-- .../client/components/Page/PageHeaderNoShadow.tsx | 8 +++----- apps/meteor/client/providers/LayoutProvider.tsx | 6 +++--- apps/meteor/client/sidebarv2/SidebarRegion.tsx | 13 ++++++++----- apps/meteor/tests/e2e/feature-preview.spec.ts | 6 +++--- packages/ui-contexts/src/LayoutContext.ts | 2 ++ 7 files changed, 27 insertions(+), 18 deletions(-) create mode 100644 .changeset/hot-cups-smash.md diff --git a/.changeset/hot-cups-smash.md b/.changeset/hot-cups-smash.md new file mode 100644 index 0000000000000..3fead344b5deb --- /dev/null +++ b/.changeset/hot-cups-smash.md @@ -0,0 +1,6 @@ +--- +'@rocket.chat/ui-contexts': patch +'@rocket.chat/meteor': patch +--- + +Fixes the sidebar collapse breakpoint in enhanced navigation diff --git a/apps/meteor/client/NavBarV2/NavBarPagesSection.tsx b/apps/meteor/client/NavBarV2/NavBarPagesSection.tsx index f3f949753a903..df157817f5189 100644 --- a/apps/meteor/client/NavBarV2/NavBarPagesSection.tsx +++ b/apps/meteor/client/NavBarV2/NavBarPagesSection.tsx @@ -5,11 +5,11 @@ import NavBarPagesGroup from './NavBarPagesGroup'; import { SidebarTogglerV2 } from '../components/SidebarTogglerV2'; const NavBarPagesSection = () => { - const { isTablet } = useLayout(); + const { sidebar } = useLayout(); return ( - {isTablet && ( + {sidebar.shouldToggle && ( <> diff --git a/apps/meteor/client/components/Page/PageHeaderNoShadow.tsx b/apps/meteor/client/components/Page/PageHeaderNoShadow.tsx index 84e4e9b07a7a2..15f6f001dec5c 100644 --- a/apps/meteor/client/components/Page/PageHeaderNoShadow.tsx +++ b/apps/meteor/client/components/Page/PageHeaderNoShadow.tsx @@ -15,8 +15,7 @@ type PageHeaderProps = { const PageHeaderNoShadow = ({ children = undefined, title, onClickBack, ...props }: PageHeaderProps) => { const { t } = useTranslation(); - - const { isMobile, isTablet, isEmbedded } = useLayout(); + const { sidebar, isEmbedded } = useLayout(); useDocumentTitle(typeof title === 'string' ? title : undefined); @@ -34,21 +33,20 @@ const PageHeaderNoShadow = ({ children = undefined, title, onClickBack, ...props > - {isMobile ? ( + {sidebar.shouldToggle ? ( ) : null} - {isTablet && isEmbedded ? ( + {sidebar.shouldToggle && isEmbedded ? ( ) : null} - {onClickBack && } {title} diff --git a/apps/meteor/client/providers/LayoutProvider.tsx b/apps/meteor/client/providers/LayoutProvider.tsx index fbfdbbf3cf62e..2ef162b26c3c9 100644 --- a/apps/meteor/client/providers/LayoutProvider.tsx +++ b/apps/meteor/client/providers/LayoutProvider.tsx @@ -23,7 +23,6 @@ const LayoutProvider = ({ children }: LayoutProviderProps) => { const [navBarSearchExpanded, setNavBarSearchExpanded] = useState(false); const breakpoints = useBreakpoints(); // ["xs", "sm", "md", "lg", "xl", xxl"] const [hiddenActions, setHiddenActions] = useState(hiddenActionsDefaultValue); - const enhancedNavigationEnabled = useFeaturePreview('newNavigation'); const secondSidebarEnabled = useFeaturePreview('secondarySidebar'); const router = useRouter(); @@ -33,7 +32,7 @@ const LayoutProvider = ({ children }: LayoutProviderProps) => { const isMobile = !breakpoints.includes('md'); const isTablet = !breakpoints.includes('lg'); - const shouldToggle = enhancedNavigationEnabled ? isTablet || isMobile : isMobile; + const shouldToggle = secondSidebarEnabled ? isTablet || isMobile : isMobile; const shouldDisplaySidePanel = !isTablet || displaySidePanel; const defaultSidebarWidth = secondSidebarEnabled ? '220px' : '240px'; @@ -71,6 +70,7 @@ const LayoutProvider = ({ children }: LayoutProviderProps) => { overlayed, setOverlayed, isCollapsed, + shouldToggle, toggle: shouldToggle ? () => setIsCollapsed((isCollapsed) => !isCollapsed) : () => undefined, collapse: () => setIsCollapsed(true), expand: () => setIsCollapsed(false), @@ -82,7 +82,7 @@ const LayoutProvider = ({ children }: LayoutProviderProps) => { openSidePanel: () => setDisplaySidePanel(true), }, size: { - sidebar: isTablet ? '280px' : defaultSidebarWidth, + sidebar: shouldToggle ? '280px' : defaultSidebarWidth, // eslint-disable-next-line no-nested-ternary contextualBar: breakpoints.includes('sm') ? (breakpoints.includes('xl') ? '38%' : '380px') : '100%', }, diff --git a/apps/meteor/client/sidebarv2/SidebarRegion.tsx b/apps/meteor/client/sidebarv2/SidebarRegion.tsx index 83eb4e750c6da..1541c900d7a5d 100644 --- a/apps/meteor/client/sidebarv2/SidebarRegion.tsx +++ b/apps/meteor/client/sidebarv2/SidebarRegion.tsx @@ -7,7 +7,7 @@ import { FocusScope } from 'react-aria'; import Sidebar from './Sidebar'; const SidebarRegion = () => { - const { isTablet, sidebar } = useLayout(); + const { sidebar } = useLayout(); const sidebarMobileClass = css` position: absolute; @@ -93,13 +93,16 @@ const SidebarRegion = () => { - {isTablet && ( + {sidebar.shouldToggle && ( sidebar.toggle()} /> )} diff --git a/apps/meteor/tests/e2e/feature-preview.spec.ts b/apps/meteor/tests/e2e/feature-preview.spec.ts index 5691a840cbcdc..cf0962e54586d 100644 --- a/apps/meteor/tests/e2e/feature-preview.spec.ts +++ b/apps/meteor/tests/e2e/feature-preview.spec.ts @@ -122,15 +122,15 @@ test.describe.serial('feature preview', () => { await expect(poHomeChannel.navbar.btnDirectory).toBeVisible(); }); - await test.step('should display home and directory inside a menu and sidebar toggler in tablet view', async () => { + await test.step('should display home and directory inside a menu', async () => { await page.setViewportSize({ width: 1023, height: 767 }); await expect(poHomeChannel.navbar.btnMenuPages).toBeVisible(); - await expect(poHomeChannel.navbar.btnSidebarToggler()).toBeVisible(); }); - await test.step('should display voice and omnichannel items inside a menu in mobile view', async () => { + await test.step('should display voice and omnichannel items inside a menu and sidebar toggler in mobile view', async () => { await page.setViewportSize({ width: 767, height: 510 }); await expect(poHomeChannel.navbar.btnVoiceAndOmnichannel).toBeVisible(); + await expect(poHomeChannel.navbar.btnSidebarToggler()).toBeVisible(); }); await test.step('should hide everything else when navbar search is focused in mobile view', async () => { diff --git a/packages/ui-contexts/src/LayoutContext.ts b/packages/ui-contexts/src/LayoutContext.ts index 106c35488bfca..6fe5cb10b4c2c 100644 --- a/packages/ui-contexts/src/LayoutContext.ts +++ b/packages/ui-contexts/src/LayoutContext.ts @@ -15,6 +15,7 @@ export type LayoutContextValue = { overlayed: boolean; setOverlayed: (value: boolean) => void; isCollapsed: boolean; + shouldToggle: boolean; toggle: () => void; collapse: () => void; expand: () => void; @@ -56,6 +57,7 @@ export const LayoutContext = createContext({ overlayed: false, setOverlayed: () => undefined, isCollapsed: false, + shouldToggle: false, toggle: () => undefined, collapse: () => undefined, expand: () => undefined, From e14f95585f7f1dbd0004306299fe1ada04402cec Mon Sep 17 00:00:00 2001 From: Abhinav Kumar Date: Mon, 3 Nov 2025 23:27:35 +0530 Subject: [PATCH 028/129] chore: optimize getRoomsWithSingleOwner to stop cursor iteration after assigning new owner (#37292) --- .../meteor/app/lib/server/functions/getRoomsWithSingleOwner.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/meteor/app/lib/server/functions/getRoomsWithSingleOwner.ts b/apps/meteor/app/lib/server/functions/getRoomsWithSingleOwner.ts index 596bd301d99cb..db0bd4a894f4e 100644 --- a/apps/meteor/app/lib/server/functions/getRoomsWithSingleOwner.ts +++ b/apps/meteor/app/lib/server/functions/getRoomsWithSingleOwner.ts @@ -51,7 +51,7 @@ export async function getSubscribedRoomsForUserWithDetails( u: { _id: uid }, } of subscribersCursor) { // If we already changed the owner or this subscription is for the user we are removing, then don't try to give it ownership - if (roomData.shouldChangeOwner || uid === userId) { + if (uid === userId) { continue; } const newOwner = await Users.findOneActiveById(uid, { projection: { _id: 1 } }); @@ -61,6 +61,7 @@ export async function getSubscribedRoomsForUserWithDetails( roomData.newOwner = uid; roomData.shouldChangeOwner = true; + break; } // If there's no subscriber available to be the new owner and it's not a public room, we can remove it. From 2772d866828eb354330c1b16c9850f52c44f3527 Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Mon, 3 Nov 2025 16:23:55 -0300 Subject: [PATCH 029/129] test: Refactor admin locators (#37341) --- apps/meteor/tests/e2e/admin-room.spec.ts | 30 +- .../e2e/admin-users-custom-fields.spec.ts | 50 ++-- .../e2e/admin-users-role-management.spec.ts | 30 +- .../e2e/admin-users-status-management.spec.ts | 24 +- apps/meteor/tests/e2e/admin-users.spec.ts | 32 +- .../tests/e2e/administration-settings.spec.ts | 20 +- apps/meteor/tests/e2e/administration.spec.ts | 273 +++++++++--------- apps/meteor/tests/e2e/email-inboxes.spec.ts | 17 +- apps/meteor/tests/e2e/embedded-layout.spec.ts | 2 +- apps/meteor/tests/e2e/feature-preview.spec.ts | 20 +- apps/meteor/tests/e2e/imports.spec.ts | 44 +-- .../e2e/page-objects/admin-email-inboxes.ts | 20 +- .../tests/e2e/page-objects/admin-imports.ts | 30 ++ .../tests/e2e/page-objects/admin-info.ts | 3 + .../e2e/page-objects/admin-integrations.ts | 43 +++ .../tests/e2e/page-objects/admin-roles.ts | 42 +++ .../tests/e2e/page-objects/admin-rooms.ts | 29 ++ .../tests/e2e/page-objects/admin-settings.ts | 45 +++ .../page-objects/admin-third-party-login.ts | 47 +++ .../tests/e2e/page-objects/admin-users.ts | 67 +++++ apps/meteor/tests/e2e/page-objects/admin.ts | 236 +-------------- .../fragments/admin-flextab-users.ts | 129 --------- .../page-objects/fragments/admin-flextab.ts | 11 - .../fragments/edit-room-flextab.ts | 53 ++++ .../fragments/edit-user-flextab.ts | 57 ++++ .../e2e/page-objects/fragments/flextab.ts | 24 ++ .../tests/e2e/page-objects/fragments/index.ts | 2 + .../tests/e2e/page-objects/fragments/menu.ts | 21 ++ .../tests/e2e/page-objects/fragments/modal.ts | 15 + .../e2e/page-objects/fragments/sidebar.ts | 47 ++- .../fragments/user-info-flextab.ts | 25 ++ .../tests/e2e/page-objects/home-channel.ts | 6 +- apps/meteor/tests/e2e/page-objects/index.ts | 8 + apps/meteor/tests/e2e/page-objects/utils.ts | 4 - apps/meteor/tests/e2e/settings-assets.spec.ts | 26 +- apps/meteor/tests/e2e/settings-int.spec.ts | 12 +- 36 files changed, 858 insertions(+), 686 deletions(-) create mode 100644 apps/meteor/tests/e2e/page-objects/admin-imports.ts create mode 100644 apps/meteor/tests/e2e/page-objects/admin-info.ts create mode 100644 apps/meteor/tests/e2e/page-objects/admin-integrations.ts create mode 100644 apps/meteor/tests/e2e/page-objects/admin-roles.ts create mode 100644 apps/meteor/tests/e2e/page-objects/admin-rooms.ts create mode 100644 apps/meteor/tests/e2e/page-objects/admin-settings.ts create mode 100644 apps/meteor/tests/e2e/page-objects/admin-third-party-login.ts create mode 100644 apps/meteor/tests/e2e/page-objects/admin-users.ts delete mode 100644 apps/meteor/tests/e2e/page-objects/fragments/admin-flextab-users.ts delete mode 100644 apps/meteor/tests/e2e/page-objects/fragments/admin-flextab.ts create mode 100644 apps/meteor/tests/e2e/page-objects/fragments/edit-room-flextab.ts create mode 100644 apps/meteor/tests/e2e/page-objects/fragments/edit-user-flextab.ts create mode 100644 apps/meteor/tests/e2e/page-objects/fragments/flextab.ts create mode 100644 apps/meteor/tests/e2e/page-objects/fragments/menu.ts create mode 100644 apps/meteor/tests/e2e/page-objects/fragments/user-info-flextab.ts diff --git a/apps/meteor/tests/e2e/admin-room.spec.ts b/apps/meteor/tests/e2e/admin-room.spec.ts index ff560e9013ed3..35c4d0a9fad4e 100644 --- a/apps/meteor/tests/e2e/admin-room.spec.ts +++ b/apps/meteor/tests/e2e/admin-room.spec.ts @@ -1,7 +1,7 @@ import { faker } from '@faker-js/faker'; import { Users } from './fixtures/userStates'; -import { Admin, AdminSectionsHref } from './page-objects'; +import { AdminRooms, AdminSectionsHref } from './page-objects'; import { createTargetChannel, createTargetPrivateChannel } from './utils'; import { expect, test } from './utils/test'; @@ -10,10 +10,10 @@ test.use({ storageState: Users.admin.state }); test.describe.serial('admin-rooms', () => { let channel: string; let privateRoom: string; - let admin: Admin; + let adminRooms: AdminRooms; test.beforeEach(async ({ page }) => { - admin = new Admin(page); + adminRooms = new AdminRooms(page); await page.goto('/admin/rooms'); }); @@ -26,13 +26,13 @@ test.describe.serial('admin-rooms', () => { }); test('should filter room by name', async ({ page }) => { - await admin.inputSearchRooms.fill(channel); + await adminRooms.inputSearchRooms.fill(channel); await expect(page.locator(`[qa-room-name="${channel}"]`)).toBeVisible(); }); test('should filter rooms by type', async ({ page }) => { - const dropdown = await admin.dropdownFilterRoomType(); + const dropdown = await adminRooms.dropdownFilterRoomType(); await dropdown.click(); const privateOption = page.locator('text=Private channels'); @@ -40,16 +40,16 @@ test.describe.serial('admin-rooms', () => { await privateOption.waitFor(); await privateOption.click(); - const selectedDropdown = await admin.dropdownFilterRoomType('Rooms (1)'); + const selectedDropdown = await adminRooms.dropdownFilterRoomType('Rooms (1)'); await expect(selectedDropdown).toBeVisible(); await expect(page.locator('text=Private Channel').first()).toBeVisible(); }); test('should filter rooms by type and name', async ({ page }) => { - await admin.inputSearchRooms.fill(privateRoom); + await adminRooms.inputSearchRooms.fill(privateRoom); - const dropdown = await admin.dropdownFilterRoomType(); + const dropdown = await adminRooms.dropdownFilterRoomType(); await dropdown.click(); await page.locator('text=Private channels').click(); @@ -60,9 +60,9 @@ test.describe.serial('admin-rooms', () => { test('should be empty in case of the search does not find any room', async ({ page }) => { const nonExistingChannel = faker.string.alpha(10); - await admin.inputSearchRooms.fill(nonExistingChannel); + await adminRooms.inputSearchRooms.fill(nonExistingChannel); - const dropdown = await admin.dropdownFilterRoomType(); + const dropdown = await adminRooms.dropdownFilterRoomType(); await dropdown.click(); await page.locator('text=Private channels').click(); @@ -71,19 +71,19 @@ test.describe.serial('admin-rooms', () => { }); test('should filter rooms by type and name and clean the filter after changing section', async ({ page }) => { - await admin.inputSearchRooms.fill(privateRoom); - const dropdown = await admin.dropdownFilterRoomType(); + await adminRooms.inputSearchRooms.fill(privateRoom); + const dropdown = await adminRooms.dropdownFilterRoomType(); await dropdown.click(); await page.locator('text=Private channels').click(); - const workspaceButton = await admin.adminSectionButton(AdminSectionsHref.Workspace); + const workspaceButton = await adminRooms.adminSectionButton(AdminSectionsHref.Workspace); await workspaceButton.click(); - const roomsButton = await admin.adminSectionButton(AdminSectionsHref.Rooms); + const roomsButton = await adminRooms.adminSectionButton(AdminSectionsHref.Rooms); await roomsButton.click(); - const selectDropdown = await admin.dropdownFilterRoomType('All rooms'); + const selectDropdown = await adminRooms.dropdownFilterRoomType('All rooms'); await expect(selectDropdown).toBeVisible(); }); }); diff --git a/apps/meteor/tests/e2e/admin-users-custom-fields.spec.ts b/apps/meteor/tests/e2e/admin-users-custom-fields.spec.ts index 58f669d71e6cd..b9cec4d555215 100644 --- a/apps/meteor/tests/e2e/admin-users-custom-fields.spec.ts +++ b/apps/meteor/tests/e2e/admin-users-custom-fields.spec.ts @@ -1,5 +1,5 @@ import { Users } from './fixtures/userStates'; -import { HomeChannel, Admin } from './page-objects'; +import { AdminUsers, HomeChannel } from './page-objects'; import { test, expect } from './utils/test'; import { createTestUser, type ITestUser } from './utils/user-helpers'; @@ -10,7 +10,7 @@ const adminCustomFieldUpdated1 = 'updated_admin1'; test.describe('Admin users custom fields', () => { let poHomeChannel: HomeChannel; - let poAdmin: Admin; + let poAdmin: AdminUsers; let addTestUser: ITestUser; let updateTestUser: ITestUser; @@ -55,7 +55,7 @@ test.describe('Admin users custom fields', () => { test.beforeEach(async ({ page }) => { poHomeChannel = new HomeChannel(page); - poAdmin = new Admin(page); + poAdmin = new AdminUsers(page); await page.goto('/admin/users'); }); @@ -68,26 +68,26 @@ test.describe('Admin users custom fields', () => { }); await test.step('should navigate to edit user form', async () => { - await poAdmin.btnEdit.click(); + await poAdmin.userInfo.btnEdit.click(); }); await test.step('should fill custom fields for user', async () => { - await poAdmin.tabs.users.getCustomField('customFieldText1').fill(adminCustomFieldValue1); - await poAdmin.tabs.users.getCustomField('customFieldText2').fill(adminCustomFieldValue2); + await poAdmin.editUser.getCustomField('customFieldText1').fill(adminCustomFieldValue1); + await poAdmin.editUser.getCustomField('customFieldText2').fill(adminCustomFieldValue2); }); await test.step('should save user custom fields', async () => { - await poAdmin.tabs.users.btnSaveUser.click(); + await poAdmin.editUser.btnSaveUser.click(); await poHomeChannel.dismissToast(); + await poAdmin.editUser.close(); }); await test.step('should verify custom fields were saved', async () => { - await poAdmin.tabs.users.btnContextualbarClose.click(); await poAdmin.getUserRowByUsername(addTestUser.data.username).click(); - await poAdmin.btnEdit.click(); + await poAdmin.userInfo.btnEdit.click(); - await expect(poAdmin.tabs.users.getCustomField('customFieldText1')).toHaveValue(adminCustomFieldValue1); - await expect(poAdmin.tabs.users.getCustomField('customFieldText2')).toHaveValue(adminCustomFieldValue2); + await expect(poAdmin.editUser.getCustomField('customFieldText1')).toHaveValue(adminCustomFieldValue1); + await expect(poAdmin.editUser.getCustomField('customFieldText2')).toHaveValue(adminCustomFieldValue2); }); }); @@ -100,29 +100,29 @@ test.describe('Admin users custom fields', () => { }); await test.step('should navigate to edit user form', async () => { - await poAdmin.btnEdit.click(); + await poAdmin.userInfo.btnEdit.click(); }); await test.step('should verify existing values and update one custom field', async () => { - await poAdmin.tabs.users.inputName.waitFor(); + await poAdmin.editUser.inputName.waitFor(); - await expect(poAdmin.tabs.users.getCustomField('customFieldText1')).toHaveValue(customFieldInitial1); - await expect(poAdmin.tabs.users.getCustomField('customFieldText2')).toHaveValue(adminCustomFieldValue2); + await expect(poAdmin.editUser.getCustomField('customFieldText1')).toHaveValue(customFieldInitial1); + await expect(poAdmin.editUser.getCustomField('customFieldText2')).toHaveValue(adminCustomFieldValue2); - await poAdmin.tabs.users.getCustomField('customFieldText1').clear(); - await poAdmin.tabs.users.getCustomField('customFieldText1').fill(adminCustomFieldUpdated1); + await poAdmin.editUser.getCustomField('customFieldText1').clear(); + await poAdmin.editUser.getCustomField('customFieldText1').fill(adminCustomFieldUpdated1); }); await test.step('should save and verify partial update', async () => { - await poAdmin.tabs.users.btnSaveUser.click(); + await poAdmin.editUser.btnSaveUser.click(); await poHomeChannel.dismissToast(); - await poAdmin.tabs.users.btnContextualbarClose.click(); + await poAdmin.editUser.close(); await poAdmin.getUserRowByUsername(updateTestUser.data.username).click(); - await poAdmin.btnEdit.click(); + await poAdmin.userInfo.btnEdit.click(); - await expect(poAdmin.tabs.users.getCustomField('customFieldText1')).toHaveValue(adminCustomFieldUpdated1); - await expect(poAdmin.tabs.users.getCustomField('customFieldText2')).toHaveValue(adminCustomFieldValue2); + await expect(poAdmin.editUser.getCustomField('customFieldText1')).toHaveValue(adminCustomFieldUpdated1); + await expect(poAdmin.editUser.getCustomField('customFieldText2')).toHaveValue(adminCustomFieldValue2); }); }); @@ -151,12 +151,12 @@ test.describe('Admin users custom fields', () => { }); await test.step('should navigate to edit user form', async () => { - await poAdmin.btnEdit.click(); + await poAdmin.userInfo.btnEdit.click(); }); await test.step('should verify custom field is not rendered', async () => { - await expect(poAdmin.tabs.users.getCustomField('customFieldText1')).not.toBeVisible(); - await expect(poAdmin.tabs.users.getCustomField('customFieldText2')).toBeVisible(); + await expect(poAdmin.editUser.getCustomField('customFieldText1')).not.toBeVisible(); + await expect(poAdmin.editUser.getCustomField('customFieldText2')).toBeVisible(); }); }); }); diff --git a/apps/meteor/tests/e2e/admin-users-role-management.spec.ts b/apps/meteor/tests/e2e/admin-users-role-management.spec.ts index 1b1f531fee90a..10757f3b2a60f 100644 --- a/apps/meteor/tests/e2e/admin-users-role-management.spec.ts +++ b/apps/meteor/tests/e2e/admin-users-role-management.spec.ts @@ -1,5 +1,5 @@ import { Users } from './fixtures/userStates'; -import { Admin } from './page-objects'; +import { AdminUsers } from './page-objects'; import { ToastBar } from './page-objects/toastBar'; import { test, expect } from './utils/test'; import type { ITestUser } from './utils/user-helpers'; @@ -7,7 +7,7 @@ import { createTestUser } from './utils/user-helpers'; let userWithoutAdminAccess: ITestUser; let userWithAdminAccess: ITestUser; -let admin: Admin; +let admin: AdminUsers; let poToastBar: ToastBar; test.describe('Admin > Users Role Management', () => { @@ -25,51 +25,51 @@ test.describe('Admin > Users Role Management', () => { test.use({ storageState: Users.admin.state }); test.beforeEach('Go to /admin/users', async ({ page }) => { - admin = new Admin(page); + admin = new AdminUsers(page); poToastBar = new ToastBar(page); await page.goto('/admin/users'); }); test('Make a newly created user as admin', async () => { - await admin.tabs.users.inputSearch.fill(userWithoutAdminAccess.data.username); + await admin.inputSearchUsers.fill(userWithoutAdminAccess.data.username); await test.step('should be visible in the All tab', async () => { - await admin.tabs.users.tabAll.click(); + await admin.getTabByName().click(); await expect(admin.getUserRowByUsername(userWithoutAdminAccess.data.username)).toBeVisible(); }); await test.step('make a user admin', async () => { - await admin.tabs.users.openUserActionMenu(userWithoutAdminAccess.data.username); - await admin.tabs.users.menuItemMakeAdmin.click(); + await admin.openUserActionMenu(userWithoutAdminAccess.data.username); + await admin.menuItemMakeAdmin.click(); await expect(poToastBar.alert).toBeVisible(); await expect(poToastBar.alert).toHaveText('User is now an admin'); }); await test.step('verify user is admin', async () => { await admin.getUserRowByUsername(userWithoutAdminAccess.data.username).click(); - await expect(admin.tabs.users.userInfoDialog).toBeVisible(); - await expect(admin.tabs.users.userInfoDialog).toContainText('Admin'); + await expect(admin.userInfo.root).toBeVisible(); + await expect(admin.userInfo.root).toContainText('Admin'); }); }); test('Remove role as admin', async () => { - await admin.tabs.users.inputSearch.fill(userWithAdminAccess.data.username); + await admin.inputSearchUsers.fill(userWithAdminAccess.data.username); await test.step('User should be visible in the All tab', async () => { - await admin.tabs.users.tabAll.click(); + await admin.getTabByName().click(); await expect(admin.getUserRowByUsername(userWithAdminAccess.data.username)).toBeVisible(); }); await test.step('remove admin role', async () => { - await admin.tabs.users.openUserActionMenu(userWithAdminAccess.data.username); - await admin.tabs.users.menuItemRemoveAdmin.click(); + await admin.openUserActionMenu(userWithAdminAccess.data.username); + await admin.menuItemRemoveAdmin.click(); await expect(poToastBar.alert).toBeVisible(); await expect(poToastBar.alert).toHaveText('User is no longer an admin'); }); await test.step('verify user role as admin is removed', async () => { await admin.getUserRowByUsername(userWithAdminAccess.data.username).click(); - await expect(admin.tabs.users.userInfoDialog).toBeVisible(); - await expect(admin.tabs.users.userInfoDialog).not.toHaveText('Admin'); + await expect(admin.userInfo.root).toBeVisible(); + await expect(admin.userInfo.root).not.toHaveText('Admin'); }); }); }); diff --git a/apps/meteor/tests/e2e/admin-users-status-management.spec.ts b/apps/meteor/tests/e2e/admin-users-status-management.spec.ts index 4d3ff07ab6160..f920bf9da29b9 100644 --- a/apps/meteor/tests/e2e/admin-users-status-management.spec.ts +++ b/apps/meteor/tests/e2e/admin-users-status-management.spec.ts @@ -2,13 +2,13 @@ import type { BrowserContext, Page } from '@playwright/test'; import { DEFAULT_USER_CREDENTIALS } from './config/constants'; import { Users } from './fixtures/userStates'; -import { Admin, Registration, Utils } from './page-objects'; +import { AdminUsers, Registration, Utils } from './page-objects'; import { test, expect } from './utils/test'; import type { ITestUser } from './utils/user-helpers'; import { createTestUser } from './utils/user-helpers'; let user: ITestUser; -let admin: Admin; +let adminUsers: AdminUsers; test.describe.serial('Admin > Users', () => { test.beforeAll('Create a new user', async ({ api }) => { @@ -53,31 +53,31 @@ test.describe.serial('Admin > Users', () => { test.use({ storageState: Users.admin.state }); test.beforeEach('Go to /admin/users', async ({ page }) => { - admin = new Admin(page); + adminUsers = new AdminUsers(page); await page.goto('/admin/users'); }); test('After the first login, the user gets listed under the Active tab', async () => { - await admin.tabs.users.inputSearch.fill(user.data.username); + await adminUsers.inputSearchUsers.fill(user.data.username); await test.step('should be visible in the All tab', async () => { - await admin.tabs.users.tabActive.click(); - await expect(admin.getUserRowByUsername(user.data.username)).toBeVisible(); + await adminUsers.getTabByName('Active').click(); + await expect(adminUsers.getUserRowByUsername(user.data.username)).toBeVisible(); }); await test.step('should not be visible in the Pending tab', async () => { - await admin.tabs.users.tabPending.click(); - await expect(admin.getUserRowByUsername(user.data.username)).not.toBeVisible(); + await adminUsers.getTabByName('Pending').click(); + await expect(adminUsers.getUserRowByUsername(user.data.username)).not.toBeVisible(); }); await test.step('should be visible in the Active tab', async () => { - await admin.tabs.users.tabActive.click(); - await expect(admin.getUserRowByUsername(user.data.username)).toBeVisible(); + await adminUsers.getTabByName('Active').click(); + await expect(adminUsers.getUserRowByUsername(user.data.username)).toBeVisible(); }); await test.step('should not be visible in the Deactivated tab', async () => { - await admin.tabs.users.tabDeactivated.click(); - await expect(admin.getUserRowByUsername(user.data.username)).not.toBeVisible(); + await adminUsers.getTabByName('Deactivated').click(); + await expect(adminUsers.getUserRowByUsername(user.data.username)).not.toBeVisible(); }); }); }); diff --git a/apps/meteor/tests/e2e/admin-users.spec.ts b/apps/meteor/tests/e2e/admin-users.spec.ts index 7dc02d4416b38..95e7eb5ca05ee 100644 --- a/apps/meteor/tests/e2e/admin-users.spec.ts +++ b/apps/meteor/tests/e2e/admin-users.spec.ts @@ -1,11 +1,11 @@ import { Users } from './fixtures/userStates'; -import { Admin } from './page-objects'; +import { AdminUsers } from './page-objects'; import { test, expect } from './utils/test'; import type { ITestUser } from './utils/user-helpers'; import { createTestUser } from './utils/user-helpers'; let user: ITestUser; -let admin: Admin; +let admin: AdminUsers; test.use({ storageState: Users.admin.state }); @@ -19,47 +19,47 @@ test.describe('Admin > Users', () => { }); test.beforeEach('Go to /admin/users', async ({ page }) => { - admin = new Admin(page); + admin = new AdminUsers(page); await page.goto('/admin/users'); }); test('New user shows in correct tabs when deactivated', async () => { - await admin.tabs.users.inputSearch.fill(user.data.username); + await admin.inputSearchUsers.fill(user.data.username); await test.step('should be visible in the All tab', async () => { - await admin.tabs.users.tabAll.click(); + await admin.getTabByName().click(); await expect(admin.getUserRowByUsername(user.data.username)).toBeVisible(); }); await test.step('should be visible in the Pending tab', async () => { - await admin.tabs.users.tabPending.click(); + await admin.getTabByName('Pending').click(); await expect(admin.getUserRowByUsername(user.data.username)).toBeVisible(); }); await test.step('should not be visible in the Active tab', async () => { - await admin.tabs.users.tabActive.click(); + await admin.getTabByName('Active').click(); await expect(admin.getUserRowByUsername(user.data.username)).not.toBeVisible(); }); await test.step('should not be visible in the Deactivated tab', async () => { - await admin.tabs.users.tabDeactivated.click(); + await admin.getTabByName('Deactivated').click(); await expect(admin.getUserRowByUsername(user.data.username)).not.toBeVisible(); }); await test.step('should move from Pending to Deactivated tab', async () => { - await admin.tabs.users.tabPending.click(); - await admin.tabs.users.btnMoreActionsMenu.click(); - await admin.tabs.users.menuItemDeactivated.click(); + await admin.getTabByName('Pending').click(); + await admin.btnMoreActionsMenu.click(); + await admin.menuItemDeactivated.click(); await expect(admin.getUserRowByUsername(user.data.username)).not.toBeVisible(); - await admin.tabs.users.tabDeactivated.click(); + await admin.getTabByName('Deactivated').click(); await expect(admin.getUserRowByUsername(user.data.username)).toBeVisible(); }); await test.step('should move from Deactivated to Pending tab', async () => { - await admin.tabs.users.tabDeactivated.click(); - await admin.tabs.users.btnMoreActionsMenu.click(); - await admin.tabs.users.menuItemActivate.click(); + await admin.getTabByName('Deactivated').click(); + await admin.btnMoreActionsMenu.click(); + await admin.menuItemActivate.click(); await expect(admin.getUserRowByUsername(user.data.username)).not.toBeVisible(); - await admin.tabs.users.tabPending.click(); + await admin.getTabByName('Pending').click(); await expect(admin.getUserRowByUsername(user.data.username)).toBeVisible(); }); }); diff --git a/apps/meteor/tests/e2e/administration-settings.spec.ts b/apps/meteor/tests/e2e/administration-settings.spec.ts index 24ffa09d34c1c..cb918540a5f35 100644 --- a/apps/meteor/tests/e2e/administration-settings.spec.ts +++ b/apps/meteor/tests/e2e/administration-settings.spec.ts @@ -1,15 +1,15 @@ import { Users } from './fixtures/userStates'; -import { Admin } from './page-objects'; +import { AdminSettings } from './page-objects'; import { getSettingValueById, setSettingValueById } from './utils'; import { test, expect } from './utils/test'; test.use({ storageState: Users.admin.state }); test.describe.parallel('administration-settings', () => { - let poAdmin: Admin; + let poAdminSettings: AdminSettings; test.beforeEach(async ({ page }) => { - poAdmin = new Admin(page); + poAdminSettings = new AdminSettings(page); }); test.describe('General', () => { @@ -24,14 +24,14 @@ test.describe.parallel('administration-settings', () => { }); test('should be able to reset a setting after a change', async () => { - await poAdmin.inputSiteURL.fill('any_text'); - await poAdmin.btnResetSiteURL.click(); + await poAdminSettings.inputSiteURL.fill('any_text'); + await poAdminSettings.btnResetSiteURL.click(); - await expect(poAdmin.inputSiteURL).toHaveValue(inputSiteURLSetting); + await expect(poAdminSettings.inputSiteURL).toHaveValue(inputSiteURLSetting); }); test('should be able to go back to the settings page', async ({ page }) => { - await poAdmin.btnBack.click(); + await poAdminSettings.btnBack.click(); await expect(page).toHaveURL('/admin/settings'); }); @@ -45,7 +45,7 @@ test.describe.parallel('administration-settings', () => { test.afterAll(async ({ api }) => setSettingValueById(api, 'theme-custom-css', '')); test('should display the code mirror correctly', async ({ page, api }) => { - await poAdmin.getAccordionBtnByName('Custom CSS').click(); + await poAdminSettings.getAccordionBtnByName('Custom CSS').click(); await test.step('should render only one code mirror element', async () => { const codeMirrorParent = page.getByRole('code'); @@ -53,9 +53,9 @@ test.describe.parallel('administration-settings', () => { }); await test.step('should display full screen properly', async () => { - await poAdmin.btnFullScreen.click(); + await poAdminSettings.btnFullScreen.click(); await expect(page.getByRole('code')).toHaveCSS('width', '920px'); - await poAdmin.btnExitFullScreen.click(); + await poAdminSettings.btnExitFullScreen.click(); }); await test.step('should reflect updated value when valueProp changes after server update', async () => { diff --git a/apps/meteor/tests/e2e/administration.spec.ts b/apps/meteor/tests/e2e/administration.spec.ts index c0aa19e79a2d7..cc81a32d09736 100644 --- a/apps/meteor/tests/e2e/administration.spec.ts +++ b/apps/meteor/tests/e2e/administration.spec.ts @@ -3,19 +3,17 @@ import type { IUser } from '@rocket.chat/apps-engine/definition/users'; import { IS_EE } from './config/constants'; import { Users } from './fixtures/userStates'; -import { Admin, Utils } from './page-objects'; +import { Utils, AdminUsers, AdminRoles, AdminRooms, AdminThirdPartyLogin, AdminIntegrations } from './page-objects'; import { createTargetChannel, setSettingValueById } from './utils'; import { test, expect } from './utils/test'; test.use({ storageState: Users.admin.state }); test.describe.parallel('administration', () => { - let poAdmin: Admin; let poUtils: Utils; let targetChannel: string; test.beforeEach(async ({ page }) => { - poAdmin = new Admin(page); poUtils = new Utils(page); }); @@ -64,53 +62,56 @@ test.describe.parallel('administration', () => { }); test.describe('Users', () => { + let poAdminUsers: AdminUsers; test.beforeEach(async ({ page }) => { + poAdminUsers = new AdminUsers(page); + await page.goto('/admin/users'); }); test('expect find "user1" user', async ({ page }) => { - await poAdmin.inputSearchUsers.type('user1'); + await poAdminUsers.inputSearchUsers.fill('user1'); await expect(page.locator('table tr[qa-user-id="user1"]')).toBeVisible(); }); test('expect create a user', async () => { - await poAdmin.tabs.users.btnNewUser.click(); - await poAdmin.tabs.users.inputEmail.type(faker.internet.email()); - await poAdmin.tabs.users.inputName.type(faker.person.firstName()); - await poAdmin.tabs.users.inputUserName.type(faker.internet.userName()); - await poAdmin.tabs.users.inputSetManually.click(); - await poAdmin.tabs.users.inputPassword.type('any_password'); - await poAdmin.tabs.users.inputConfirmPassword.type('any_password'); - await expect(poAdmin.tabs.users.userRole).toBeVisible(); - await poAdmin.tabs.users.btnSave.click(); + await poAdminUsers.btnNewUser.click(); + await poAdminUsers.editUser.inputEmail.fill(faker.internet.email()); + await poAdminUsers.editUser.inputName.fill(faker.person.firstName()); + await poAdminUsers.editUser.inputUserName.fill(faker.internet.userName()); + await poAdminUsers.editUser.inputSetManually.click(); + await poAdminUsers.editUser.inputPassword.fill('any_password'); + await poAdminUsers.editUser.inputConfirmPassword.fill('any_password'); + await expect(poAdminUsers.editUser.userRole).toBeVisible(); + await poAdminUsers.editUser.btnAddUser.click(); }); test('expect SMTP setup warning and routing to email settings', async ({ page }) => { - await poAdmin.tabs.users.btnInvite.click(); - await poAdmin.tabs.users.setupSmtpLink.click(); + await poAdminUsers.btnInvite.click(); + await poAdminUsers.editUser.setupSmtpLink.click(); await expect(page).toHaveURL('/admin/settings/Email'); }); test('expect to show join default channels option only when creating new users, not when editing users', async () => { const username = faker.internet.userName(); - await poAdmin.tabs.users.btnNewUser.click(); - await poAdmin.tabs.users.inputName.type(faker.person.firstName()); - await poAdmin.tabs.users.inputUserName.type(username); - await poAdmin.tabs.users.inputEmail.type(faker.internet.email()); - await poAdmin.tabs.users.inputSetManually.click(); - await poAdmin.tabs.users.inputPassword.type('any_password'); - await poAdmin.tabs.users.inputConfirmPassword.type('any_password'); - await expect(poAdmin.tabs.users.userRole).toBeVisible(); - await expect(poAdmin.tabs.users.joinDefaultChannels).toBeVisible(); - await poAdmin.tabs.users.btnSave.click(); + await poAdminUsers.btnNewUser.click(); + await poAdminUsers.editUser.inputName.type(faker.person.firstName()); + await poAdminUsers.editUser.inputUserName.type(username); + await poAdminUsers.editUser.inputEmail.type(faker.internet.email()); + await poAdminUsers.editUser.inputSetManually.click(); + await poAdminUsers.editUser.inputPassword.type('any_password'); + await poAdminUsers.editUser.inputConfirmPassword.type('any_password'); + await expect(poAdminUsers.editUser.userRole).toBeVisible(); + await expect(poAdminUsers.editUser.joinDefaultChannels).toBeVisible(); + await poAdminUsers.editUser.btnAddUser.click(); - await poAdmin.inputSearchUsers.fill(username); - await poAdmin.getUserRow(username).click(); - await poAdmin.btnEdit.click(); - await expect(poAdmin.tabs.users.inputUserName).toHaveValue(username); - await expect(poAdmin.tabs.users.joinDefaultChannels).not.toBeVisible(); + await poAdminUsers.inputSearchUsers.fill(username); + await poAdminUsers.getUserRowByUsername(username).click(); + await poAdminUsers.userInfo.btnEdit.click(); + await expect(poAdminUsers.editUser.inputUserName).toHaveValue(username); + await expect(poAdminUsers.editUser.joinDefaultChannels).not.toBeVisible(); }); test.describe('Delete user', () => { @@ -154,15 +155,10 @@ test.describe.parallel('administration', () => { }); test('expect to show owner change modal, when deleting last owner of any room', async ({ page }) => { - await poAdmin.inputSearchUsers.type(ownerUser.username); - await poAdmin.getUserRow(ownerUser.username).click(); - await poAdmin.tabs.users.btnMoreActions.click(); - await poAdmin.tabs.users.btnDeleteUser.click(); - + await poAdminUsers.deleteUser(ownerUser.username); await expect(page.getByRole('dialog', { name: 'Are you sure?' })).toBeVisible(); await page.getByRole('dialog').getByRole('button', { name: 'Delete' }).click(); - await expect(page.getByRole('dialog', { name: 'Are you sure?' })).toContainText( `A new owner will be assigned automatically to the ${nonEmptyChannelName} room.`, ); @@ -172,120 +168,119 @@ test.describe.parallel('administration', () => { await expect(page.getByRole('dialog').getByRole('button', { name: 'Delete' })).toBeVisible(); await page.getByRole('dialog').getByRole('button', { name: 'Delete' }).click(); - await expect(poUtils.toastBarSuccess).toBeVisible(); await expect(page.getByRole('heading', { name: 'No users' })).toBeVisible(); }); test('expect to delete user', async ({ page }) => { - await poAdmin.inputSearchUsers.type(user.username); - await poAdmin.getUserRow(user.username).click(); - await poAdmin.tabs.users.btnMoreActions.click(); - await poAdmin.tabs.users.btnDeleteUser.click(); - + await poAdminUsers.deleteUser(user.username); await expect(page.getByRole('dialog', { name: 'Are you sure?' })).toBeVisible(); await page.getByRole('dialog').getByRole('button', { name: 'Delete' }).click(); - await expect(poUtils.toastBarSuccess).toBeVisible(); await expect(page.getByRole('heading', { name: 'No users' })).toBeVisible(); }); }); }); + // TODO: Move this suit to admin-rooms.spec.ts test.describe('Rooms', () => { + let poAdminRooms: AdminRooms; test.beforeAll(async ({ api }) => { targetChannel = await createTargetChannel(api); }); test.beforeEach(async ({ page }) => { + poAdminRooms = new AdminRooms(page); await page.goto('/admin/rooms'); }); test('should find "general" channel', async ({ page }) => { - await poAdmin.inputSearchRooms.type('general'); + await poAdminRooms.inputSearchRooms.type('general'); await page.waitForSelector('[qa-room-id="GENERAL"]'); }); test('should edit target channel name', async () => { - await poAdmin.inputSearchRooms.fill(targetChannel); - await poAdmin.getRoomRow(targetChannel).click(); - await poAdmin.roomNameInput.fill(`${targetChannel}-edited`); - await poAdmin.btnSave.click(); + await poAdminRooms.inputSearchRooms.fill(targetChannel); + await poAdminRooms.getRoomRow(targetChannel).click(); + await poAdminRooms.editRoom.roomNameInput.fill(`${targetChannel}-edited`); + await poAdminRooms.editRoom.btnSave.click(); - await expect(poAdmin.getRoomRow(targetChannel)).toContainText(`${targetChannel}-edited`); + await expect(poAdminRooms.getRoomRow(targetChannel)).toContainText(`${targetChannel}-edited`); targetChannel = `${targetChannel}-edited`; }); test('should edit target channel type', async () => { - await poAdmin.inputSearchRooms.type(targetChannel); - await poAdmin.getRoomRow(targetChannel).click(); - await poAdmin.privateLabel.click(); - await poAdmin.btnSave.click(); - await expect(poAdmin.getRoomRow(targetChannel)).toContainText('Private Channel'); + await poAdminRooms.inputSearchRooms.type(targetChannel); + await poAdminRooms.getRoomRow(targetChannel).click(); + await poAdminRooms.editRoom.privateLabel.click(); + await poAdminRooms.editRoom.btnSave.click(); + await expect(poAdminRooms.getRoomRow(targetChannel)).toContainText('Private Channel'); }); test('should archive target channel', async () => { - await poAdmin.inputSearchRooms.type(targetChannel); - await poAdmin.getRoomRow(targetChannel).click(); - await poAdmin.archivedLabel.click(); - await poAdmin.btnSave.click(); + await poAdminRooms.inputSearchRooms.type(targetChannel); + await poAdminRooms.getRoomRow(targetChannel).click(); + await poAdminRooms.editRoom.archivedLabel.click(); + await poAdminRooms.editRoom.btnSave.click(); - await poAdmin.getRoomRow(targetChannel).click(); - await expect(poAdmin.archivedInput).toBeChecked(); + await poAdminRooms.getRoomRow(targetChannel).click(); + await expect(poAdminRooms.editRoom.archivedInput).toBeChecked(); }); test.describe.serial('Default rooms', () => { test('expect target channel to be default', async () => { - await poAdmin.inputSearchRooms.type(targetChannel); - await poAdmin.getRoomRow(targetChannel).click(); - await poAdmin.defaultLabel.click(); + await poAdminRooms.inputSearchRooms.type(targetChannel); + await poAdminRooms.getRoomRow(targetChannel).click(); + await poAdminRooms.editRoom.defaultLabel.click(); await test.step('should close contextualbar after saving', async () => { - await poAdmin.btnSave.click(); - await expect(poAdmin.page).toHaveURL(new RegExp('/admin/rooms$')); + await poAdminRooms.editRoom.btnSave.click(); + await poAdminRooms.editRoom.waitForDismissal(); }); - await poAdmin.getRoomRow(targetChannel).click(); - await expect(poAdmin.defaultInput).toBeChecked(); + await poAdminRooms.getRoomRow(targetChannel).click(); + await expect(poAdminRooms.editRoom.defaultInput).toBeChecked(); }); test('should mark target default channel as "favorite by default"', async () => { - await poAdmin.inputSearchRooms.type(targetChannel); - await poAdmin.getRoomRow(targetChannel).click(); - await poAdmin.favoriteLabel.click(); - await poAdmin.btnSave.click(); - await expect(poAdmin.btnSave).not.toBeVisible(); - - await poAdmin.getRoomRow(targetChannel).click(); - await expect(poAdmin.favoriteInput).toBeChecked(); + await poAdminRooms.inputSearchRooms.fill(targetChannel); + await poAdminRooms.getRoomRow(targetChannel).click(); + await poAdminRooms.editRoom.favoriteLabel.click(); + await poAdminRooms.editRoom.btnSave.click(); + await expect(poAdminRooms.editRoom.btnSave).not.toBeVisible(); + + await poAdminRooms.getRoomRow(targetChannel).click(); + await expect(poAdminRooms.editRoom.favoriteInput).toBeChecked(); }); test('should see favorite switch disabled when default is not true', async () => { - await poAdmin.inputSearchRooms.type(targetChannel); - await poAdmin.getRoomRow(targetChannel).click(); - await poAdmin.defaultLabel.click(); + await poAdminRooms.inputSearchRooms.fill(targetChannel); + await poAdminRooms.getRoomRow(targetChannel).click(); + await poAdminRooms.editRoom.defaultLabel.click(); - await expect(poAdmin.favoriteInput).toBeDisabled(); + await expect(poAdminRooms.editRoom.favoriteInput).toBeDisabled(); }); test('should see favorite switch enabled when default is true', async () => { - await poAdmin.inputSearchRooms.type(targetChannel); - await poAdmin.getRoomRow(targetChannel).click(); + await poAdminRooms.inputSearchRooms.type(targetChannel); + await poAdminRooms.getRoomRow(targetChannel).click(); - await expect(poAdmin.favoriteInput).toBeEnabled(); + await expect(poAdminRooms.editRoom.favoriteInput).toBeEnabled(); }); }); }); test.describe('Permissions', () => { + let poAdminRoles: AdminRoles; test.beforeEach(async ({ page }) => { + poAdminRoles = new AdminRoles(page); await page.goto('/admin/permissions'); }); test('expect open upsell modal if not enterprise', async ({ page }) => { test.skip(IS_EE); - await poAdmin.btnCreateRole.click(); + await poAdminRoles.btnCreateRole.click(); await expect(page.getByRole('dialog', { name: 'Custom roles' })).toBeVisible(); }); @@ -301,45 +296,43 @@ test.describe.parallel('administration', () => { }); test('admin should be able to get the owners of a room that wasnt created by him', async ({ page }) => { - await poAdmin.openRoleByName('Owner').click(); - await poAdmin.btnUsersInRole.click(); - await poAdmin.inputRoom.fill(channelName); + await poAdminRoles.openRoleByName('Owner').click(); + await poAdminRoles.btnUsersInRole.click(); + await poAdminRoles.inputRoom.fill(channelName); await page.getByRole('option', { name: channelName }).click(); - await expect(poAdmin.getUserRowByUsername('user1')).toBeVisible(); + await expect(poAdminRoles.getUserInRoleRowByUsername('user1')).toBeVisible(); }); test('should add user1 as moderator of target channel', async ({ page }) => { - await poAdmin.openRoleByName('Moderator').click(); - await poAdmin.btnUsersInRole.click(); + await poAdminRoles.openRoleByName('Moderator').click(); + await poAdminRoles.btnUsersInRole.click(); - await poAdmin.inputRoom.fill(channelName); + await poAdminRoles.inputRoom.fill(channelName); await page.getByRole('option', { name: channelName }).click(); - await poAdmin.inputUsers.fill('user1'); + await poAdminRoles.inputUsers.fill('user1'); await page.getByRole('option', { name: 'user1' }).click(); - await poAdmin.btnAdd.click(); + await poAdminRoles.btnAdd.click(); - await expect(poAdmin.getUserRowByUsername('user1')).toBeVisible(); + await expect(poAdminRoles.getUserInRoleRowByUsername('user1')).toBeVisible(); }); test('should remove user1 as moderator of target channel', async ({ page }) => { - await poAdmin.openRoleByName('Moderator').click(); - await poAdmin.btnUsersInRole.click(); + await poAdminRoles.openRoleByName('Moderator').click(); + await poAdminRoles.btnUsersInRole.click(); - await poAdmin.inputRoom.fill(channelName); + await poAdminRoles.inputRoom.fill(channelName); await page.getByRole('option', { name: channelName }).click(); - await poAdmin.getUserRowByUsername('user1').getByRole('button', { name: 'Remove' }).click(); - await poUtils.btnModalConfirmDelete.click(); - + await poAdminRoles.removeUserFromRoleByUsername('user1'); await expect(page.locator('h3 >> text="No results found"')).toBeVisible(); }); test('should back to the permissions page', async ({ page }) => { - await poAdmin.openRoleByName('Moderator').click(); - await poAdmin.btnUsersInRole.click(); - await poAdmin.btnBack.click(); + await poAdminRoles.openRoleByName('Moderator').click(); + await poAdminRoles.btnUsersInRole.click(); + await poAdminRoles.btnBack.click(); await expect(page.locator('h1 >> text="Permissions"')).toBeVisible(); }); @@ -358,11 +351,13 @@ test.describe.parallel('administration', () => { }); test.describe.serial('Third party login', () => { + let poAdminThirdPartyLogin: AdminThirdPartyLogin; const appName = faker.string.uuid(); const appRedirectURI = faker.internet.url(); test.beforeEach(async ({ page }) => { await page.goto('/admin/third-party-login'); + poAdminThirdPartyLogin = new AdminThirdPartyLogin(page); }); test('should show Third-party login page', async ({ page }) => { @@ -372,52 +367,52 @@ test.describe.parallel('administration', () => { }); test('should not be able to create a new application without application name', async ({ page }) => { - await poAdmin.btnNewApplication.click(); - await poAdmin.inputRedirectURI.fill(appRedirectURI); - await poAdmin.btnSave.click(); + await poAdminThirdPartyLogin.btnNewApplication.click(); + await poAdminThirdPartyLogin.inputRedirectURI.fill(appRedirectURI); + await poAdminThirdPartyLogin.btnSave.click(); await expect(page.getByText('Name required')).toBeVisible(); }); test('should not be able to create a new application without redirect URI', async ({ page }) => { - await poAdmin.btnNewApplication.click(); - await poAdmin.inputApplicationName.fill(appName); - await poAdmin.btnSave.click(); + await poAdminThirdPartyLogin.btnNewApplication.click(); + await poAdminThirdPartyLogin.inputApplicationName.fill(appName); + await poAdminThirdPartyLogin.btnSave.click(); await expect(page.getByText('Redirect URI required')).toBeVisible(); }); test('should be able to create a new application', async ({ page }) => { - await poAdmin.btnNewApplication.click(); - await poAdmin.inputApplicationName.fill(appName); - await poAdmin.inputRedirectURI.fill(appRedirectURI); - await poAdmin.btnSave.click(); + await poAdminThirdPartyLogin.btnNewApplication.click(); + await poAdminThirdPartyLogin.inputApplicationName.fill(appName); + await poAdminThirdPartyLogin.inputRedirectURI.fill(appRedirectURI); + await poAdminThirdPartyLogin.btnSave.click(); - await expect(poAdmin.getThirdPartyAppByName(appName)).toBeVisible(); + await expect(poAdminThirdPartyLogin.getThirdPartyAppByName(appName)).toBeVisible(); await expect(page.getByText('Application added')).toBeVisible(); }); test('should be able see aplication fields', async () => { - await poAdmin.getThirdPartyAppByName(appName).click(); - await expect(poAdmin.inputApplicationName).toBeVisible(); - await expect(poAdmin.inputRedirectURI).toBeVisible(); - await expect(poAdmin.inputClientId).toBeVisible(); - await expect(poAdmin.inputClientSecret).toBeVisible(); - await expect(poAdmin.inputAuthUrl).toBeVisible(); - await expect(poAdmin.inputTokenUrl).toBeVisible(); + await poAdminThirdPartyLogin.getThirdPartyAppByName(appName).click(); + await expect(poAdminThirdPartyLogin.inputApplicationName).toBeVisible(); + await expect(poAdminThirdPartyLogin.inputRedirectURI).toBeVisible(); + await expect(poAdminThirdPartyLogin.inputClientId).toBeVisible(); + await expect(poAdminThirdPartyLogin.inputClientSecret).toBeVisible(); + await expect(poAdminThirdPartyLogin.inputAuthUrl).toBeVisible(); + await expect(poAdminThirdPartyLogin.inputTokenUrl).toBeVisible(); }); test('should be able to delete an application', async ({ page }) => { - await poAdmin.getThirdPartyAppByName(appName).click(); - await poAdmin.btnDelete.click(); - await poUtils.btnModalConfirmDelete.click(); + await poAdminThirdPartyLogin.deleteThirdPartyAppByName(appName); + await expect(poAdminThirdPartyLogin.getThirdPartyAppByName(appName)).not.toBeVisible(); await expect(page.getByText('Your entry has been deleted.')).toBeVisible(); - await expect(poAdmin.getIntegrationByName(appName)).not.toBeVisible(); }); }); test.describe('Integrations', () => { + let poAdminIntegrations: AdminIntegrations; + const messageCodeHighlightDefault = 'javascript,css,markdown,dockerfile,json,go,rust,clean,bash,plaintext,powershell,scss,shell,yaml,vim'; const incomingIntegrationName = faker.string.uuid(); @@ -427,6 +422,7 @@ test.describe.parallel('administration', () => { }); test.beforeEach(async ({ page }) => { + poAdminIntegrations = new AdminIntegrations(page); await page.goto('/admin/integrations'); }); @@ -435,31 +431,28 @@ test.describe.parallel('administration', () => { }); test('should display the example payload correctly', async () => { - await poAdmin.btnNew.click(); - await poAdmin.btnInstructions.click(); + await poAdminIntegrations.btnNew.click(); + await poAdminIntegrations.btnInstructions.click(); - await expect(poAdmin.codeExamplePayload('Loading')).not.toBeVisible(); + await expect(poAdminIntegrations.codeExamplePayload('Loading')).not.toBeVisible(); }); test('should be able to create new incoming integration', async () => { - await poAdmin.btnNew.click(); - await poAdmin.inputName.fill(incomingIntegrationName); - await poAdmin.inputPostToChannel.fill('#general'); - await poAdmin.inputPostAs.fill(Users.admin.data.username); - await poAdmin.btnSave.click(); + await poAdminIntegrations.btnNew.click(); + await poAdminIntegrations.inputName.fill(incomingIntegrationName); + await poAdminIntegrations.inputPostToChannel.fill('#general'); + await poAdminIntegrations.inputPostAs.fill(Users.admin.data.username); + await poAdminIntegrations.btnSave.click(); - await expect(poAdmin.inputWebhookUrl).not.toHaveValue('Will be available here after saving.'); + await expect(poAdminIntegrations.inputWebhookUrl).not.toHaveValue('Will be available here after saving.'); - await poAdmin.btnBack.click(); - await expect(poAdmin.getIntegrationByName(incomingIntegrationName)).toBeVisible(); + await poAdminIntegrations.btnBack.click(); + await expect(poAdminIntegrations.getIntegrationByName(incomingIntegrationName)).toBeVisible(); }); test('should be able to delete an incoming integration', async () => { - await poAdmin.getIntegrationByName(incomingIntegrationName).click(); - await poAdmin.btnDelete.click(); - await poUtils.btnModalConfirmDelete.click(); - - await expect(poAdmin.getIntegrationByName(incomingIntegrationName)).not.toBeVisible(); + await poAdminIntegrations.deleteIntegrationByName(incomingIntegrationName); + await expect(poAdminIntegrations.getIntegrationByName(incomingIntegrationName)).not.toBeVisible(); }); }); }); diff --git a/apps/meteor/tests/e2e/email-inboxes.spec.ts b/apps/meteor/tests/e2e/email-inboxes.spec.ts index 74a910b5b1e3f..384532390e609 100644 --- a/apps/meteor/tests/e2e/email-inboxes.spec.ts +++ b/apps/meteor/tests/e2e/email-inboxes.spec.ts @@ -1,20 +1,18 @@ import { faker } from '@faker-js/faker'; import { Users } from './fixtures/userStates'; -import { AdminEmailInboxes, Utils } from './page-objects'; +import { AdminEmailInboxes } from './page-objects'; import { test, expect } from './utils/test'; test.use({ storageState: Users.admin.state }); test.describe.serial('email-inboxes', () => { let poAdminEmailInboxes: AdminEmailInboxes; - let poUtils: Utils; const email = faker.internet.email(); test.beforeEach(async ({ page }) => { poAdminEmailInboxes = new AdminEmailInboxes(page); - poUtils = new Utils(page); await page.goto('/admin/email-inboxes'); }); @@ -42,10 +40,13 @@ test.describe.serial('email-inboxes', () => { await expect(poAdminEmailInboxes.itemRow(name)).toBeVisible(); }); - test('expect delete an email inbox', async () => { - await poAdminEmailInboxes.itemRow(email).click(); - await poAdminEmailInboxes.btnDelete.click(); - await poUtils.btnModalConfirmDelete.click(); - await expect(poUtils.toastBarSuccess).toBeVisible(); + test('expect delete an email inbox', async ({ page }) => { + await poAdminEmailInboxes.deleteEmailInboxByName(email); + // await poAdminEmailInboxes.itemRow(email).click(); + // await poAdminEmailInboxes.btnDelete.click(); + // await poUtils.btnModalConfirmDelete.click(); + // await expect(poUtils.toastBarSuccess).toBeVisible(); + + await expect(page.locator('text=No results found')).toBeVisible(); }); }); diff --git a/apps/meteor/tests/e2e/embedded-layout.spec.ts b/apps/meteor/tests/e2e/embedded-layout.spec.ts index 28f8c09c4f618..2d35ecf53dc0e 100644 --- a/apps/meteor/tests/e2e/embedded-layout.spec.ts +++ b/apps/meteor/tests/e2e/embedded-layout.spec.ts @@ -37,7 +37,7 @@ test.describe('embedded-layout', () => { await page.goto(embeddedLayoutURL(page.url())); await expect(poHomeChannel.roomHeaderToolbar).not.toBeVisible(); - await expect(poHomeChannel.sidebar.sidebar).not.toBeVisible(); + await poHomeChannel.sidebar.waitForDismissal(); }); test.describe('should show room header toolbar when show top navbar setting is enabled', () => { diff --git a/apps/meteor/tests/e2e/feature-preview.spec.ts b/apps/meteor/tests/e2e/feature-preview.spec.ts index cf0962e54586d..7610ed01c7da2 100644 --- a/apps/meteor/tests/e2e/feature-preview.spec.ts +++ b/apps/meteor/tests/e2e/feature-preview.spec.ts @@ -2,7 +2,7 @@ import { faker } from '@faker-js/faker'; import type { Page } from '@playwright/test'; import { Users } from './fixtures/userStates'; -import { AccountProfile, Admin, HomeChannel } from './page-objects'; +import { AccountProfile, AdminInfo, HomeChannel } from './page-objects'; import { createTargetChannel, createTargetTeam, @@ -666,9 +666,9 @@ test.describe.serial('feature preview', () => { await poHomeChannel.navbar.openAdminPanel(); await expect(page).toHaveURL(/\/admin/); - const adminPage = new Admin(page); + const adminPage = new AdminInfo(page); - await adminPage.btnClose.click(); + await adminPage.sidebar.btnClose.click(); await expect(page).toHaveURL(/\/home/); await expect(poHomeChannel.sidepanel.unreadCheckbox).toBeChecked(); @@ -683,7 +683,7 @@ test.describe.serial('feature preview', () => { test('should show button to toggle sidebar/sidepanel', async ({ page }) => { await page.goto('/home'); - await expect(poHomeChannel.sidebar.sidebar).not.toBeVisible(); + await poHomeChannel.sidebar.waitForDismissal(); await expect(poHomeChannel.sidepanel.sidepanel).not.toBeVisible(); await expect(poHomeChannel.navbar.btnSidebarToggler()).toBeVisible(); }); @@ -692,11 +692,11 @@ test.describe.serial('feature preview', () => { await page.goto('/home'); await poHomeChannel.navbar.btnSidebarToggler().click(); - await expect(poHomeChannel.sidebar.sidebar).not.toBeVisible(); + await poHomeChannel.sidebar.waitForDismissal(); await expect(poHomeChannel.sidepanel.sidepanel).toBeVisible(); await poHomeChannel.navbar.btnSidebarToggler(true).click(); - await expect(poHomeChannel.sidebar.sidebar).not.toBeVisible(); + await poHomeChannel.sidebar.waitForDismissal(); await expect(poHomeChannel.sidepanel.sidepanel).not.toBeVisible(); }); @@ -708,7 +708,7 @@ test.describe.serial('feature preview', () => { await poHomeChannel.sidepanel.sidepanelBackButton.click(); - await expect(poHomeChannel.sidebar.sidebar).toBeVisible(); + await poHomeChannel.sidebar.waitForDisplay(); await expect(poHomeChannel.sidepanel.sidepanel).not.toBeVisible(); await poHomeChannel.sidebar.favoritesTeamCollabFilter.click(); @@ -726,7 +726,7 @@ test.describe.serial('feature preview', () => { await page.click('main', { force: true }); await expect(poHomeChannel.sidepanel.sidepanel).not.toBeVisible(); - await expect(poHomeChannel.sidebar.sidebar).not.toBeVisible(); + await poHomeChannel.sidebar.waitForDismissal(); }); test('should close nav region when opening a room', async ({ page }) => { @@ -740,7 +740,7 @@ test.describe.serial('feature preview', () => { await expect(poHomeChannel.content.channelHeader).toContainText(targetChannel); await expect(poHomeChannel.sidepanel.sidepanel).not.toBeVisible(); - await expect(poHomeChannel.sidebar.sidebar).not.toBeVisible(); + await poHomeChannel.sidebar.waitForDismissal(); }); }); }); @@ -877,7 +877,7 @@ test.describe.serial('feature preview', () => { await poHomeChannel.sidenav.waitForHome(); - await expect(poHomeChannel.sidebar.sidebar).not.toBeVisible(); + await poHomeChannel.sidebar.waitForDismissal(); await expect(poHomeChannel.sidepanel.sidepanel).not.toBeVisible(); await poHomeChannel.navbar.btnSidebarToggler().click(); await expect(poHomeChannel.sidepanel.sidepanel).toBeVisible(); diff --git a/apps/meteor/tests/e2e/imports.spec.ts b/apps/meteor/tests/e2e/imports.spec.ts index ed76d866c7284..0c9a4e3dd63e6 100644 --- a/apps/meteor/tests/e2e/imports.spec.ts +++ b/apps/meteor/tests/e2e/imports.spec.ts @@ -4,7 +4,7 @@ import * as path from 'path'; import { parse } from 'csv-parse'; import { Users } from './fixtures/userStates'; -import { Admin } from './page-objects'; +import { AdminImports, AdminRooms } from './page-objects'; import { test, expect } from './utils/test'; test.use({ storageState: Users.admin.state }); @@ -87,39 +87,39 @@ test.describe.serial('imports', () => { }); test('expect import users data from slack', async ({ page }) => { - const poAdmin: Admin = new Admin(page); + const poAdminImports = new AdminImports(page); await page.goto('/admin/import'); - await poAdmin.btnImportNewFile.click(); + await poAdminImports.btnImportNewFile.click(); - await (await poAdmin.getOptionFileType("Slack's Users CSV")).click(); + await (await poAdminImports.getOptionFileType("Slack's Users CSV")).click(); - await poAdmin.inputFile.setInputFiles(slackCsvDir); - await poAdmin.btnImport.click(); + await poAdminImports.inputFile.setInputFiles(slackCsvDir); + await poAdminImports.btnImport.click(); - await poAdmin.btnStartImport.click(); + await poAdminImports.btnStartImport.click(); - await expect(poAdmin.importStatusTableFirstRowCell).toBeVisible({ + await expect(poAdminImports.importStatusTableFirstRowCell).toBeVisible({ timeout: 30_000, }); }); test('expect import users data from zipped CSV files', async ({ page }) => { - const poAdmin: Admin = new Admin(page); + const poAdminImports = new AdminImports(page); await page.goto('/admin/import'); - await poAdmin.btnImportNewFile.click(); + await poAdminImports.btnImportNewFile.click(); - await (await poAdmin.getOptionFileType('CSV')).click(); + await (await poAdminImports.getOptionFileType('CSV')).click(); - await poAdmin.inputFile.setInputFiles(zipCsvImportDir); - await poAdmin.btnImport.click(); + await poAdminImports.inputFile.setInputFiles(zipCsvImportDir); + await poAdminImports.btnImport.click(); - await poAdmin.findFileCheckboxByUsername('billy.billy').click(); + await poAdminImports.findFileCheckboxByUsername('billy.billy').click(); - await poAdmin.btnStartImport.click(); + await poAdminImports.btnStartImport.click(); - await expect(poAdmin.importStatusTableFirstRowCell).toBeVisible({ + await expect(poAdminImports.importStatusTableFirstRowCell).toBeVisible({ timeout: 30_000, }); }); @@ -137,7 +137,7 @@ test.describe.serial('imports', () => { }); test('expect all imported rooms to be actually listed as rooms with correct members count', async ({ page }) => { - const poAdmin: Admin = new Admin(page); + const poAdmin: AdminRooms = new AdminRooms(page); await page.goto('/admin/rooms'); for await (const room of importedRooms) { @@ -149,7 +149,7 @@ test.describe.serial('imports', () => { }); test('expect all imported rooms to have correct room type and owner', async ({ page }) => { - const poAdmin: Admin = new Admin(page); + const poAdmin = new AdminRooms(page); await page.goto('/admin/rooms'); for await (const room of importedRooms) { @@ -157,14 +157,14 @@ test.describe.serial('imports', () => { await poAdmin.getRoomRow(room.name).click(); room.visibility === 'private' - ? await expect(poAdmin.privateInput).toBeChecked() - : await expect(poAdmin.privateInput).not.toBeChecked(); - await expect(poAdmin.roomOwnerInput).toHaveValue(room.ownerUsername); + ? await expect(poAdmin.editRoom.privateInput).toBeChecked() + : await expect(poAdmin.editRoom.privateInput).not.toBeChecked(); + await expect(poAdmin.editRoom.roomOwnerInput).toHaveValue(room.ownerUsername); } }); test('expect imported DM to be actually listed as a room with correct members and messages count', async ({ page }) => { - const poAdmin: Admin = new Admin(page); + const poAdmin = new AdminRooms(page); await page.goto('/admin/rooms'); for await (const user of csvImportedUsernames) { diff --git a/apps/meteor/tests/e2e/page-objects/admin-email-inboxes.ts b/apps/meteor/tests/e2e/page-objects/admin-email-inboxes.ts index f1ebc95be0b6a..2cf146f0fcf47 100644 --- a/apps/meteor/tests/e2e/page-objects/admin-email-inboxes.ts +++ b/apps/meteor/tests/e2e/page-objects/admin-email-inboxes.ts @@ -1,10 +1,10 @@ import type { Locator, Page } from '@playwright/test'; -export class AdminEmailInboxes { - private readonly page: Page; +import { Admin } from './admin'; +export class AdminEmailInboxes extends Admin { constructor(page: Page) { - this.page = page; + super(page); } get btnNewEmailInbox(): Locator { @@ -53,15 +53,13 @@ export class AdminEmailInboxes { return this.page.locator('label >> text="Connect with SSL/TLS"').last(); } - get btnSave(): Locator { - return this.page.locator('button >> text=Save'); - } - - get btnDelete(): Locator { - return this.page.locator('button >> text=Delete'); - } - itemRow(name: string): Locator { return this.page.locator(`td >> text="${name}"`); } + + async deleteEmailInboxByName(name: string): Promise { + await this.itemRow(name).click(); + await this.btnDelete.click(); + await this.deleteModal.confirmDelete(); + } } diff --git a/apps/meteor/tests/e2e/page-objects/admin-imports.ts b/apps/meteor/tests/e2e/page-objects/admin-imports.ts new file mode 100644 index 0000000000000..aa6380bce9694 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/admin-imports.ts @@ -0,0 +1,30 @@ +import type { Locator } from '@playwright/test'; + +import { Admin } from './admin'; + +export class AdminImports extends Admin { + get btnImportNewFile(): Locator { + return this.page.locator('.rcx-button--primary.rcx-button >> text="Import New File"'); + } + + async getOptionFileType(option: string): Promise { + await this.page.locator('.rcx-select').click(); + return this.page.locator(`.rcx-option__content >> text="${option}"`); + } + + get inputFile(): Locator { + return this.page.locator('input[type=file]'); + } + + get btnImport(): Locator { + return this.page.locator('.rcx-button--primary.rcx-button >> text="Import"'); + } + + get btnStartImport(): Locator { + return this.page.locator('.rcx-button--primary.rcx-button >> text="Start Importing"'); + } + + get importStatusTableFirstRowCell(): Locator { + return this.page.locator('[data-qa-id="ImportTable"] tbody tr:first-child td >> text="Completed successfully"'); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/admin-info.ts b/apps/meteor/tests/e2e/page-objects/admin-info.ts new file mode 100644 index 0000000000000..8668df589e7fc --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/admin-info.ts @@ -0,0 +1,3 @@ +import { Admin } from './admin'; + +export class AdminInfo extends Admin {} diff --git a/apps/meteor/tests/e2e/page-objects/admin-integrations.ts b/apps/meteor/tests/e2e/page-objects/admin-integrations.ts new file mode 100644 index 0000000000000..0013265cf642f --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/admin-integrations.ts @@ -0,0 +1,43 @@ +import type { Locator, Page } from '@playwright/test'; + +import { Admin } from './admin'; + +export class AdminIntegrations extends Admin { + constructor(page: Page) { + super(page); + } + + get btnInstructions(): Locator { + return this.page.getByRole('button', { name: 'Instructions', exact: true }); + } + + codeExamplePayload(text: string): Locator { + return this.page.locator('code', { hasText: text }); + } + + get inputName(): Locator { + return this.page.getByRole('textbox', { name: 'Name' }); + } + + get inputPostToChannel(): Locator { + return this.page.getByRole('textbox', { name: 'Post to Channel' }); + } + + get inputPostAs(): Locator { + return this.page.getByRole('textbox', { name: 'Post as' }); + } + + getIntegrationByName(name: string): Locator { + return this.page.getByRole('table', { name: 'Integrations table' }).locator('tr', { hasText: name }); + } + + get inputWebhookUrl(): Locator { + return this.page.getByRole('textbox', { name: 'Webhook URL' }); + } + + async deleteIntegrationByName(name: string) { + await this.getIntegrationByName(name).click(); + await this.btnDelete.click(); + await this.deleteModal.confirmDelete(); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/admin-roles.ts b/apps/meteor/tests/e2e/page-objects/admin-roles.ts new file mode 100644 index 0000000000000..95c30dc2996c0 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/admin-roles.ts @@ -0,0 +1,42 @@ +import type { Locator, Page } from '@playwright/test'; + +import { Admin } from './admin'; + +export class AdminRoles extends Admin { + constructor(page: Page) { + super(page); + } + + get btnCreateRole(): Locator { + return this.page.locator('button[name="New role"]'); + } + + openRoleByName(name: string): Locator { + return this.page.getByRole('table').getByRole('button', { name }); + } + + get btnUsersInRole(): Locator { + return this.page.getByRole('dialog').getByRole('button', { name: 'Users in role', exact: true }); + } + + get tableUsersInRole(): Locator { + return this.page.getByRole('table'); + } + + get inputRoom(): Locator { + return this.page.locator('input[placeholder="Room"]'); + } + + get inputUsers(): Locator { + return this.page.locator('input[placeholder="Users"]'); + } + + getUserInRoleRowByUsername(username: string): Locator { + return this.tableUsersInRole.locator('tr', { hasText: username }); + } + + removeUserFromRoleByUsername = async (username: string): Promise => { + await this.getUserInRoleRowByUsername(username).getByRole('button', { name: 'Remove' }).click(); + await this.deleteModal.confirmDelete(); + }; +} diff --git a/apps/meteor/tests/e2e/page-objects/admin-rooms.ts b/apps/meteor/tests/e2e/page-objects/admin-rooms.ts new file mode 100644 index 0000000000000..b1f688529d347 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/admin-rooms.ts @@ -0,0 +1,29 @@ +import type { Locator, Page } from '@playwright/test'; + +import { Admin } from './admin'; +import { EditRoomFlexTab } from './fragments/edit-room-flextab'; + +export class AdminRooms extends Admin { + readonly editRoom: EditRoomFlexTab; + + constructor(page: Page) { + super(page); + this.editRoom = new EditRoomFlexTab(page); + } + + get inputSearchRooms(): Locator { + return this.page.getByPlaceholder('Search rooms'); + } + + getRoomRow(name?: string): Locator { + return this.page.getByRole('link', { name }); + } + + get btnEdit(): Locator { + return this.page.getByRole('button', { name: 'Edit' }); + } + + dropdownFilterRoomType(text = 'All rooms'): Locator { + return this.page.getByRole('button', { name: text }); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/admin-settings.ts b/apps/meteor/tests/e2e/page-objects/admin-settings.ts new file mode 100644 index 0000000000000..2c6e3e6d2f765 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/admin-settings.ts @@ -0,0 +1,45 @@ +import type { Locator, Page } from '@playwright/test'; + +import { Admin } from './admin'; + +export class AdminSettings extends Admin { + constructor(page: Page) { + super(page); + } + + get inputSearchSettings(): Locator { + return this.page.locator('input[type=search]'); + } + + get inputSiteURL(): Locator { + return this.page.locator('[data-qa-setting-id="Site_Url"]'); + } + + get btnResetSiteURL(): Locator { + return this.page.locator('//label[@title="Site_Url"]//following-sibling::button'); + } + + get btnAssetsSettings(): Locator { + return this.page.locator('[data-qa-id="Assets"] >> role=link[name="Open"]'); + } + + get btnDeleteAssetsLogo(): Locator { + return this.page.locator('//label[@title="Assets_logo"]/following-sibling::span >> role=button[name="Delete"]'); + } + + get inputAssetsLogo(): Locator { + return this.page.locator('//label[@title="Assets_logo"]/following-sibling::span >> input[type="file"]'); + } + + get btnFullScreen(): Locator { + return this.page.getByRole('button', { name: 'Full Screen', exact: true }); + } + + get btnExitFullScreen(): Locator { + return this.page.getByRole('button', { name: 'Exit Full Screen', exact: true }); + } + + get btnSaveChanges(): Locator { + return this.page.getByRole('button', { name: 'Save changes' }); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/admin-third-party-login.ts b/apps/meteor/tests/e2e/page-objects/admin-third-party-login.ts new file mode 100644 index 0000000000000..a7e35d51f8744 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/admin-third-party-login.ts @@ -0,0 +1,47 @@ +import type { Locator, Page } from '@playwright/test'; + +import { Admin } from './admin'; + +export class AdminThirdPartyLogin extends Admin { + constructor(page: Page) { + super(page); + } + + get btnNewApplication(): Locator { + return this.page.getByRole('button', { name: 'New Application', exact: true }); + } + + get inputRedirectURI(): Locator { + return this.page.getByRole('textbox', { name: 'Redirect URI' }); + } + + get inputApplicationName(): Locator { + return this.page.getByRole('textbox', { name: 'Application Name' }); + } + + get inputClientId(): Locator { + return this.page.getByRole('textbox', { name: 'Client ID' }); + } + + get inputClientSecret(): Locator { + return this.page.getByRole('textbox', { name: 'Client Secret' }); + } + + get inputAuthUrl(): Locator { + return this.page.getByRole('textbox', { name: 'Authorization URL' }); + } + + get inputTokenUrl(): Locator { + return this.page.getByRole('textbox', { name: 'Access Token URL' }); + } + + getThirdPartyAppByName(name: string): Locator { + return this.page.getByRole('table', { name: 'Third-party applications table' }).locator('tr', { hasText: name }); + } + + async deleteThirdPartyAppByName(name: string) { + await this.getThirdPartyAppByName(name).click(); + await this.btnDelete.click(); + await this.deleteModal.confirmDelete(); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/admin-users.ts b/apps/meteor/tests/e2e/page-objects/admin-users.ts new file mode 100644 index 0000000000000..52eda08bc9bf4 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/admin-users.ts @@ -0,0 +1,67 @@ +import type { Locator, Page } from '@playwright/test'; + +import { Admin } from './admin'; +import { UserInfoFlexTab, EditUserFlexTab } from './fragments'; + +export class AdminUsers extends Admin { + readonly editUser: EditUserFlexTab; + + readonly userInfo: UserInfoFlexTab; + + constructor(page: Page) { + super(page); + this.editUser = new EditUserFlexTab(page); + this.userInfo = new UserInfoFlexTab(page); + } + + get btnNewUser(): Locator { + return this.page.locator('role=button[name="New user"]'); + } + + get btnInvite(): Locator { + return this.page.locator('role=button[name="Invite"]'); + } + + get inputSearchUsers(): Locator { + return this.page.getByRole('textbox', { name: 'Search Users' }); + } + + get btnMoreActionsMenu(): Locator { + return this.page.getByRole('button', { name: 'More actions' }); + } + + get menuItemDeactivated(): Locator { + return this.page.getByRole('menuitem', { name: 'Deactivate' }); + } + + get menuItemActivate(): Locator { + return this.page.getByRole('menuitem', { name: 'Activate' }); + } + + get menuItemMakeAdmin(): Locator { + return this.page.getByRole('menuitem', { name: 'Make Admin' }); + } + + get menuItemRemoveAdmin(): Locator { + return this.page.getByRole('menuitem', { name: 'Remove Admin' }); + } + + getUserRowByUsername(username: string): Locator { + return this.page.locator('tr', { hasText: username }); + } + + getTabByName(name: 'All' | 'Pending' | 'Active' | 'Deactivated' = 'All'): Locator { + return this.page.getByRole('tab', { name }); + } + + async openUserActionMenu(username: string): Promise { + await this.getUserRowByUsername(username).getByRole('button', { name: 'More actions' }).click(); + } + + async deleteUser(username: string): Promise { + await this.inputSearchUsers.fill(username); + await this.getUserRowByUsername(username).click(); + await this.userInfo.btnMoreActions.click(); + await this.userInfo.menuItemDeleteUser.click(); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/admin.ts b/apps/meteor/tests/e2e/page-objects/admin.ts index ac2b8a6c99cfd..b1457d60b9e70 100644 --- a/apps/meteor/tests/e2e/page-objects/admin.ts +++ b/apps/meteor/tests/e2e/page-objects/admin.ts @@ -1,6 +1,7 @@ import type { Locator, Page } from '@playwright/test'; -import { AdminFlextab } from './fragments/admin-flextab'; +import { AdminSidebar } from './fragments'; +import { ConfirmDeleteModal } from './fragments/modal'; export enum AdminSectionsHref { Workspace = '/admin/info', @@ -23,249 +24,40 @@ export enum AdminSectionsHref { Emoji = '/admin/emoji', Settings = '/admin/settings', } -export class Admin { - public readonly page: Page; +export abstract class Admin { + readonly sidebar: AdminSidebar; - readonly tabs: AdminFlextab; + readonly deleteModal: ConfirmDeleteModal; - constructor(page: Page) { - this.page = page; - this.tabs = new AdminFlextab(page); - } - - get inputSearchRooms(): Locator { - return this.page.locator('input[placeholder ="Search rooms"]'); - } - - getRoomRow(name?: string): Locator { - return this.page.locator('[role="link"]', { hasText: name }); - } - - getUserRow(username?: string): Locator { - return this.page.locator('[role="link"]', { hasText: username }); - } - - get btnSave(): Locator { - return this.page.locator('button >> text="Save"'); - } - - get btnSaveSettings(): Locator { - return this.page.getByRole('button', { name: 'Save changes' }); - } - - get btnEdit(): Locator { - return this.page.locator('button >> text="Edit"'); - } - - get privateLabel(): Locator { - return this.page.locator(`label >> text=Private`); - } - - get privateInput(): Locator { - return this.page.locator('input[name="roomType"]'); - } - - get roomNameInput(): Locator { - return this.page.locator('input[name="roomName"]'); - } - - get roomOwnerInput(): Locator { - return this.page.locator('input[name="roomOwner"]'); - } - - get archivedLabel(): Locator { - return this.page.locator('label >> text=Archived'); - } - - get archivedInput(): Locator { - return this.page.locator('input[name="archived"]'); - } - - get favoriteLabel(): Locator { - return this.page.locator('label >> text=Favorite'); - } - - get favoriteInput(): Locator { - return this.page.locator('input[name="favorite"]'); - } - - get defaultLabel(): Locator { - return this.page.locator('label >> text=Default'); - } - - get defaultInput(): Locator { - return this.page.locator('input[name="isDefault"]'); - } - - get inputSearchUsers(): Locator { - return this.page.locator('input[placeholder="Search Users"]'); - } - - get inputSearchSettings(): Locator { - return this.page.locator('input[type=search]'); - } - - get inputSiteURL(): Locator { - return this.page.locator('[data-qa-setting-id="Site_Url"]'); - } - - get btnResetSiteURL(): Locator { - return this.page.locator('//label[@title="Site_Url"]//following-sibling::button'); - } - - get btnImportNewFile(): Locator { - return this.page.locator('.rcx-button--primary.rcx-button >> text="Import New File"'); - } - - async getOptionFileType(option: string): Promise { - await this.page.locator('.rcx-select').click(); - return this.page.locator(`.rcx-option__content >> text="${option}"`); - } - - get inputFile(): Locator { - return this.page.locator('input[type=file]'); - } - - get btnImport(): Locator { - return this.page.locator('.rcx-button--primary.rcx-button >> text="Import"'); - } - - get btnStartImport(): Locator { - return this.page.locator('.rcx-button--primary.rcx-button >> text="Start Importing"'); - } - - get importStatusTableFirstRowCell(): Locator { - return this.page.locator('[data-qa-id="ImportTable"] tbody tr:first-child td >> text="Completed successfully"'); - } - - get btnAssetsSettings(): Locator { - return this.page.locator('[data-qa-id="Assets"] >> role=link[name="Open"]'); - } - - get btnDeleteAssetsLogo(): Locator { - return this.page.locator('//label[@title="Assets_logo"]/following-sibling::span >> role=button[name="Delete"]'); - } - - get inputAssetsLogo(): Locator { - return this.page.locator('//label[@title="Assets_logo"]/following-sibling::span >> input[type="file"]'); - } - - get btnCreateRole(): Locator { - return this.page.locator('button[name="New role"]'); - } - - openRoleByName(name: string): Locator { - return this.page.getByRole('table').getByRole('button', { name }); - } - - get btnUsersInRole(): Locator { - return this.page.getByRole('dialog').getByRole('button', { name: 'Users in role', exact: true }); - } - - get inputRoom(): Locator { - return this.page.locator('input[placeholder="Room"]'); - } - - get inputUsers(): Locator { - return this.page.locator('input[placeholder="Users"]'); + constructor(protected page: Page) { + this.sidebar = new AdminSidebar(page); + this.deleteModal = new ConfirmDeleteModal(page.getByRole('dialog')); } get btnAdd(): Locator { return this.page.getByRole('button', { name: 'Add', exact: true }); } - getUserRowByUsername(username: string): Locator { - return this.page.locator('tr', { hasText: username }); - } - get btnBack(): Locator { return this.page.getByRole('button', { name: 'Back', exact: true }); } - get btnNew(): Locator { - return this.page.getByRole('button', { name: 'New', exact: true }); + get btnSave(): Locator { + return this.page.getByRole('button', { name: 'Save', exact: true }); } - get btnNewApplication(): Locator { - return this.page.getByRole('button', { name: 'New Application', exact: true }); + get btnNew(): Locator { + return this.page.getByRole('button', { name: 'New', exact: true }); } get btnDelete(): Locator { return this.page.getByRole('button', { name: 'Delete', exact: true }); } - get btnInstructions(): Locator { - return this.page.getByRole('button', { name: 'Instructions', exact: true }); - } - - get inputName(): Locator { - return this.page.getByRole('textbox', { name: 'Name' }); - } - - get inputApplicationName(): Locator { - return this.page.getByRole('textbox', { name: 'Application Name' }); - } - - get inputClientId(): Locator { - return this.page.getByRole('textbox', { name: 'Client ID' }); - } - - get inputClientSecret(): Locator { - return this.page.getByRole('textbox', { name: 'Client Secret' }); - } - - get inputAuthUrl(): Locator { - return this.page.getByRole('textbox', { name: 'Authorization URL' }); - } - - get inputTokenUrl(): Locator { - return this.page.getByRole('textbox', { name: 'Access Token URL' }); - } - - get inputPostToChannel(): Locator { - return this.page.getByRole('textbox', { name: 'Post to Channel' }); - } - - get inputPostAs(): Locator { - return this.page.getByRole('textbox', { name: 'Post as' }); - } - - get inputRedirectURI(): Locator { - return this.page.getByRole('textbox', { name: 'Redirect URI' }); - } - - codeExamplePayload(text: string): Locator { - return this.page.locator('code', { hasText: text }); - } - - getIntegrationByName(name: string): Locator { - return this.page.getByRole('table', { name: 'Integrations table' }).locator('tr', { hasText: name }); - } - - getThirdPartyAppByName(name: string): Locator { - return this.page.getByRole('table', { name: 'Third-party applications table' }).locator('tr', { hasText: name }); - } - - get inputWebhookUrl(): Locator { - return this.page.getByRole('textbox', { name: 'Webhook URL' }); - } - getAccordionBtnByName(name: string): Locator { return this.page.getByRole('button', { name, exact: true }); } - get btnFullScreen(): Locator { - return this.page.getByRole('button', { name: 'Full Screen', exact: true }); - } - - get btnExitFullScreen(): Locator { - return this.page.getByRole('button', { name: 'Exit Full Screen', exact: true }); - } - - async dropdownFilterRoomType(text = 'All rooms'): Promise { - return this.page.locator(`div[role="button"]:has-text("${text}")`); - } - async adminSectionButton(href: AdminSectionsHref): Promise { return this.page.locator(`a[href="${href}"]`); } @@ -277,8 +69,4 @@ export class Admin { findFileCheckboxByUsername(username: string) { return this.findFileRowByUsername(username).locator('label', { has: this.page.getByRole('checkbox') }); } - - get btnClose(): Locator { - return this.page.locator('role=navigation >> role=button[name=Close]'); - } } diff --git a/apps/meteor/tests/e2e/page-objects/fragments/admin-flextab-users.ts b/apps/meteor/tests/e2e/page-objects/fragments/admin-flextab-users.ts deleted file mode 100644 index fa9bab2a74230..0000000000000 --- a/apps/meteor/tests/e2e/page-objects/fragments/admin-flextab-users.ts +++ /dev/null @@ -1,129 +0,0 @@ -import type { Locator, Page } from '@playwright/test'; - -export class AdminFlextabUsers { - private readonly page: Page; - - constructor(page: Page) { - this.page = page; - } - - get btnNewUser(): Locator { - return this.page.locator('role=button[name="New user"]'); - } - - get btnSave(): Locator { - return this.page.locator('role=button[name="Add user"]'); - } - - get btnSaveUser(): Locator { - return this.page.locator('role=button[name="Save user"]'); - } - - get btnMoreActions(): Locator { - return this.page.locator('role=button[name="More"]'); - } - - get btnDeleteUser(): Locator { - return this.page.locator('role=menuitem[name="Delete"]'); - } - - get btnInvite(): Locator { - return this.page.locator('role=button[name="Invite"]'); - } - - get inputName(): Locator { - return this.page.locator('//label[text()="Name"]/following-sibling::span//input'); - } - - get inputUserName(): Locator { - return this.page.locator('//label[text()="Username"]/following-sibling::span//input'); - } - - get inputEmail(): Locator { - return this.page.locator('//label[text()="Email"]/following-sibling::span//input').first(); - } - - get inputSetManually(): Locator { - return this.page.locator('//label[text()="Set manually"]'); - } - - get inputPassword(): Locator { - return this.page.locator('input[placeholder="Password"]'); - } - - get inputConfirmPassword(): Locator { - return this.page.locator('input[placeholder="Confirm password"]'); - } - - get joinDefaultChannels(): Locator { - return this.page.locator('//label[text()="Join default channels"]'); - } - - get userRole(): Locator { - return this.page.locator('button[role="option"]:has-text("user")'); - } - - get setupSmtpLink(): Locator { - return this.page.locator('role=link[name="Set up SMTP"]'); - } - - get btnContextualbarClose(): Locator { - return this.page.locator('button[data-qa="ContextualbarActionClose"]'); - } - - get btnMoreActionsMenu(): Locator { - return this.page.getByRole('button', { name: 'More actions' }); - } - - get menuItemDeactivated(): Locator { - return this.page.getByRole('menuitem', { name: 'Deactivate' }); - } - - get menuItemActivate(): Locator { - return this.page.getByRole('menuitem', { name: 'Activate' }); - } - - get tabActive(): Locator { - return this.page.getByRole('tab', { name: 'Active' }); - } - - get tabDeactivated(): Locator { - return this.page.getByRole('tab', { name: 'Deactivated' }); - } - - get tabPending(): Locator { - return this.page.getByRole('tab', { name: 'Pending' }); - } - - get tabAll(): Locator { - return this.page.getByRole('tab', { name: 'All' }); - } - - get inputSearch(): Locator { - return this.page.getByRole('textbox', { name: 'Search Users' }); - } - - get menuItemMakeAdmin(): Locator { - return this.page.getByRole('menuitem', { name: 'Make Admin' }); - } - - get menuItemRemoveAdmin(): Locator { - return this.page.getByRole('menuitem', { name: 'Remove Admin' }); - } - - get userInfoDialog(): Locator { - return this.page.getByRole('dialog'); - } - - getUserRowByUsername(username: string): Locator { - return this.page.getByRole('link', { name: username }); - } - - async openUserActionMenu(username: string): Promise { - await this.getUserRowByUsername(username).getByRole('button', { name: 'More actions' }).click(); - } - - getCustomField(fieldName: string): Locator { - return this.page.getByRole('textbox', { name: fieldName }); - } -} diff --git a/apps/meteor/tests/e2e/page-objects/fragments/admin-flextab.ts b/apps/meteor/tests/e2e/page-objects/fragments/admin-flextab.ts deleted file mode 100644 index df85e91ac6491..0000000000000 --- a/apps/meteor/tests/e2e/page-objects/fragments/admin-flextab.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { Page } from '@playwright/test'; - -import { AdminFlextabUsers } from './admin-flextab-users'; - -export class AdminFlextab { - readonly users: AdminFlextabUsers; - - constructor(page: Page) { - this.users = new AdminFlextabUsers(page); - } -} 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 new file mode 100644 index 0000000000000..1b5e48fc168b8 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/fragments/edit-room-flextab.ts @@ -0,0 +1,53 @@ +import type { Locator, Page } from '@playwright/test'; + +import { FlexTab } from './flextab'; + +export class EditRoomFlexTab extends FlexTab { + constructor(page: Page) { + super(page.getByRole('dialog', { name: 'Room Information' })); + } + + get btnSave(): Locator { + return this.root.locator('button >> text="Save"'); + } + + get roomNameInput(): Locator { + return this.root.locator('input[name="roomName"]'); + } + + get privateLabel(): Locator { + return this.root.locator(`label >> text=Private`); + } + + get privateInput(): Locator { + return this.root.locator('input[name="roomType"]'); + } + + get roomOwnerInput(): Locator { + return this.root.locator('input[name="roomOwner"]'); + } + + get archivedLabel(): Locator { + return this.root.locator('label >> text=Archived'); + } + + get archivedInput(): Locator { + return this.root.locator('input[name="archived"]'); + } + + get favoriteLabel(): Locator { + return this.root.locator('label >> text=Favorite'); + } + + get favoriteInput(): Locator { + return this.root.locator('input[name="favorite"]'); + } + + get defaultLabel(): Locator { + return this.root.locator('label >> text=Default'); + } + + get defaultInput(): Locator { + return this.root.locator('input[name="isDefault"]'); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/fragments/edit-user-flextab.ts b/apps/meteor/tests/e2e/page-objects/fragments/edit-user-flextab.ts new file mode 100644 index 0000000000000..796660efc25d1 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/fragments/edit-user-flextab.ts @@ -0,0 +1,57 @@ +import type { Locator, Page } from '@playwright/test'; + +import { FlexTab } from './flextab'; + +export class EditUserFlexTab extends FlexTab { + constructor(page: Page) { + super(page.getByRole('dialog')); + } + + get btnAddUser(): Locator { + return this.root.locator('role=button[name="Add user"]'); + } + + get btnSaveUser(): Locator { + return this.root.locator('role=button[name="Save user"]'); + } + + get inputName(): Locator { + return this.root.getByRole('textbox', { name: 'Name', exact: true }); + } + + get inputUserName(): Locator { + return this.root.getByRole('textbox', { name: 'Username', exact: true }); + } + + get inputEmail(): Locator { + return this.root.getByRole('textbox', { name: 'Email', exact: true }); + } + + get inputSetManually(): Locator { + return this.root.locator('//label[text()="Set manually"]'); + } + + get inputPassword(): Locator { + return this.root.getByPlaceholder('Password', { exact: true }); + } + + get inputConfirmPassword(): Locator { + return this.root.getByPlaceholder('Confirm password', { exact: true }); + } + + get joinDefaultChannels(): Locator { + return this.root.locator('//label[text()="Join default channels"]'); + } + + get userRole(): Locator { + return this.root.locator('button[role="option"]:has-text("user")'); + } + + get setupSmtpLink(): Locator { + return this.root.getByRole('link', { name: 'Set up SMTP' }); + } + + getCustomField(fieldName: string): Locator { + return this.root.getByRole('textbox', { name: fieldName, exact: true }); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/fragments/flextab.ts b/apps/meteor/tests/e2e/page-objects/fragments/flextab.ts new file mode 100644 index 0000000000000..770cf5997b440 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/fragments/flextab.ts @@ -0,0 +1,24 @@ +import type { Locator } from '@playwright/test'; + +import { expect } from '../../utils/test'; + +export abstract class FlexTab { + constructor(public root: Locator) {} + + waitForDisplay() { + return expect(this.root).toBeVisible(); + } + + waitForDismissal() { + return expect(this.root).not.toBeVisible(); + } + + private get btnClose() { + return this.root.getByRole('button', { name: 'Close' }); + } + + async close() { + await this.btnClose.click(); + await this.waitForDismissal(); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/fragments/index.ts b/apps/meteor/tests/e2e/page-objects/fragments/index.ts index 5541c078b7e31..62943469c40b9 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/index.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/index.ts @@ -1,3 +1,5 @@ +export * from './edit-user-flextab'; +export * from './user-info-flextab'; export * from './home-content'; export * from './home-omnichannel-content'; export * from './home-flextab'; diff --git a/apps/meteor/tests/e2e/page-objects/fragments/menu.ts b/apps/meteor/tests/e2e/page-objects/fragments/menu.ts new file mode 100644 index 0000000000000..814b476ad4be0 --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/fragments/menu.ts @@ -0,0 +1,21 @@ +import type { Locator, Page } from '@playwright/test'; + +import { expect } from '../../utils/test'; + +export abstract class Menu { + constructor(public root: Locator) {} + + waitForDisplay() { + return expect(this.root).toBeVisible(); + } + + waitForDismissal() { + return expect(this.root).not.toBeVisible(); + } +} + +export class MenuMore extends Menu { + constructor(page: Page) { + super(page.getByRole('menu', { name: 'More' })); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/fragments/modal.ts b/apps/meteor/tests/e2e/page-objects/fragments/modal.ts index 445871c18239b..ea45ba53e2366 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/modal.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/modal.ts @@ -31,3 +31,18 @@ export abstract class Modal { await this.waitForDismissal(); } } + +export class ConfirmDeleteModal extends Modal { + constructor(root: Locator) { + super(root); + } + + private btnDelete() { + return this.root.getByRole('button', { name: 'Delete' }); + } + + async confirmDelete() { + await this.btnDelete().click(); + await this.waitForDismissal(); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/fragments/sidebar.ts b/apps/meteor/tests/e2e/page-objects/fragments/sidebar.ts index 58b444c855a04..3de345574d442 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/sidebar.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/sidebar.ts @@ -1,23 +1,30 @@ import type { Locator, Page } from '@playwright/test'; -export class Sidebar { - private readonly page: Page; +import { expect } from '../../utils/test'; - constructor(page: Page) { - this.page = page; +export abstract class Sidebar { + constructor(protected root: Locator) {} + + waitForDismissal() { + return expect(this.root).not.toBeVisible(); } - // New navigation locators - get sidebar(): Locator { - return this.page.getByRole('navigation', { name: 'Sidebar' }); + waitForDisplay() { + return expect(this.root).toBeVisible(); + } +} + +export class RoomSidebar extends Sidebar { + constructor(protected page: Page) { + super(page.getByRole('navigation', { name: 'Sidebar' })); } get teamCollabFilters(): Locator { - return this.sidebar.getByRole('tablist', { name: 'Team collaboration filters' }); + return this.root.getByRole('tablist', { name: 'Team collaboration filters' }); } get omnichannelFilters(): Locator { - return this.sidebar.getByRole('tablist', { name: 'Omnichannel filters' }); + return this.root.getByRole('tablist', { name: 'Omnichannel filters' }); } get allTeamCollabFilter(): Locator { @@ -34,13 +41,13 @@ export class Sidebar { // TODO: fix this filter, workaround due to virtuoso get topChannelList(): Locator { - return this.sidebar.getByTestId('virtuoso-top-item-list'); + return this.root.getByTestId('virtuoso-top-item-list'); } get channelsList(): Locator { // TODO: fix this filter, workaround due to virtuoso // return this.sidebar.getByRole('list', { name: 'Channels' }).filter({ has: this.page.getByRole('listitem') }); - return this.sidebar.getByTestId('virtuoso-item-list'); + return this.root.getByTestId('virtuoso-item-list'); } getSearchRoomByName(name: string) { @@ -52,7 +59,7 @@ export class Sidebar { } get teamsCollapser(): Locator { - return this.sidebar.getByRole('region', { name: 'Collapse Teams' }).first(); + return this.root.getByRole('region', { name: 'Collapse Teams' }).first(); } get channelsCollapser(): Locator { @@ -86,3 +93,19 @@ export class Sidebar { return item.getByRole('status', { name: 'unread' }); } } + +export class AdminSidebar extends Sidebar { + constructor(page: Page) { + // TODO: This locator should be more specific + super(page.getByRole('navigation')); + } + + get btnClose(): Locator { + return this.root.getByRole('button', { name: 'Close' }); + } + + async close(): Promise { + await this.btnClose.click(); + await this.waitForDismissal(); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/fragments/user-info-flextab.ts b/apps/meteor/tests/e2e/page-objects/fragments/user-info-flextab.ts new file mode 100644 index 0000000000000..249fb15c35eae --- /dev/null +++ b/apps/meteor/tests/e2e/page-objects/fragments/user-info-flextab.ts @@ -0,0 +1,25 @@ +import type { Locator, Page } from '@playwright/test'; + +import { FlexTab } from './flextab'; +import { MenuMore } from './menu'; + +export class UserInfoFlexTab extends FlexTab { + readonly menu: MenuMore; + + constructor(page: Page) { + super(page.getByRole('dialog', { name: 'User Info' })); + this.menu = new MenuMore(page); + } + + get btnEdit(): Locator { + return this.root.getByRole('button', { name: 'Edit' }); + } + + get btnMoreActions(): Locator { + return this.root.getByRole('button', { name: 'More' }); + } + + get menuItemDeleteUser(): Locator { + return this.menu.root.getByRole('menuitem', { name: 'Delete' }); + } +} diff --git a/apps/meteor/tests/e2e/page-objects/home-channel.ts b/apps/meteor/tests/e2e/page-objects/home-channel.ts index e5c44558f3137..010c6752a9567 100644 --- a/apps/meteor/tests/e2e/page-objects/home-channel.ts +++ b/apps/meteor/tests/e2e/page-objects/home-channel.ts @@ -1,6 +1,6 @@ import type { Locator, Page } from '@playwright/test'; -import { HomeContent, HomeSidenav, HomeFlextab, Navbar, Sidebar, Sidepanel } from './fragments'; +import { HomeContent, HomeSidenav, HomeFlextab, Navbar, Sidepanel, RoomSidebar } from './fragments'; import { RoomToolbar } from './fragments/toolbar'; export class HomeChannel { @@ -10,7 +10,7 @@ export class HomeChannel { readonly sidenav: HomeSidenav; - readonly sidebar: Sidebar; + readonly sidebar: RoomSidebar; readonly sidepanel: Sidepanel; @@ -24,7 +24,7 @@ export class HomeChannel { this.page = page; this.content = new HomeContent(page); this.sidenav = new HomeSidenav(page); - this.sidebar = new Sidebar(page); + this.sidebar = new RoomSidebar(page); this.sidepanel = new Sidepanel(page); this.navbar = new Navbar(page); this.tabs = new HomeFlextab(page); diff --git a/apps/meteor/tests/e2e/page-objects/index.ts b/apps/meteor/tests/e2e/page-objects/index.ts index b4f2213e94339..2b888b6f61b88 100644 --- a/apps/meteor/tests/e2e/page-objects/index.ts +++ b/apps/meteor/tests/e2e/page-objects/index.ts @@ -1,6 +1,14 @@ export * from './account-profile'; export * from './admin-email-inboxes'; +export * from './admin-rooms'; +export * from './admin-users'; +export * from './admin-settings'; +export * from './admin-info'; +export * from './admin-integrations'; +export * from './admin-roles'; +export * from './admin-third-party-login'; export * from './admin-emojis'; +export * from './admin-imports'; export * from './admin'; export * from './auth'; export * from './home-channel'; diff --git a/apps/meteor/tests/e2e/page-objects/utils.ts b/apps/meteor/tests/e2e/page-objects/utils.ts index d7e82d806c679..b969a673e57df 100644 --- a/apps/meteor/tests/e2e/page-objects/utils.ts +++ b/apps/meteor/tests/e2e/page-objects/utils.ts @@ -15,10 +15,6 @@ export class Utils { return this.page.locator('.rcx-toastbar.rcx-toastbar--success'); } - get btnModalConfirmDelete() { - return this.page.locator('.rcx-modal >> button >> text="Delete"'); - } - getAlertByText(text: string): Locator { return this.page.locator('[role="alert"]', { hasText: text, diff --git a/apps/meteor/tests/e2e/settings-assets.spec.ts b/apps/meteor/tests/e2e/settings-assets.spec.ts index e6df85663f261..bdc265b63dc3d 100644 --- a/apps/meteor/tests/e2e/settings-assets.spec.ts +++ b/apps/meteor/tests/e2e/settings-assets.spec.ts @@ -1,29 +1,29 @@ import { Users } from './fixtures/userStates'; -import { Admin } from './page-objects'; +import { AdminSettings } from './page-objects'; import { test, expect } from './utils/test'; test.use({ storageState: Users.admin.state }); test.describe.serial('settings-assets', () => { - let poAdmin: Admin; + let poAdminSettings: AdminSettings; test.beforeEach(async ({ page }) => { - poAdmin = new Admin(page); + poAdminSettings = new AdminSettings(page); await page.goto('/admin/settings'); - await poAdmin.btnAssetsSettings.click(); - + await poAdminSettings.btnAssetsSettings.click(); + // FIXME: This is not good practice. We should look for a better way to ensure the page is loaded await expect(page.locator('[data-qa-type="PageHeader-title"]')).toHaveText('Assets'); }); test('expect upload and delete logo asset and label should be visible', async ({ page }) => { await expect(page.locator('[title="Assets_logo"]')).toHaveText('logo (svg, png, jpg)'); - await poAdmin.inputAssetsLogo.setInputFiles('./tests/e2e/fixtures/files/test-image.jpeg'); + await poAdminSettings.inputAssetsLogo.setInputFiles('./tests/e2e/fixtures/files/test-image.jpeg'); await expect(page.locator('role=img[name="Asset preview"]')).toBeVisible(); - await poAdmin.btnDeleteAssetsLogo.click(); + await poAdminSettings.btnDeleteAssetsLogo.click(); await expect(page.locator('role=img[name="Asset preview"]')).not.toBeVisible(); }); @@ -31,33 +31,33 @@ test.describe.serial('settings-assets', () => { test('expect upload and delete logo asset for dark theme and label should be visible', async ({ page }) => { await expect(page.locator('[title="Assets_logo_dark"]')).toHaveText('logo - dark theme (svg, png, jpg)'); - await poAdmin.inputAssetsLogo.setInputFiles('./tests/e2e/fixtures/files/test-image.jpeg'); + await poAdminSettings.inputAssetsLogo.setInputFiles('./tests/e2e/fixtures/files/test-image.jpeg'); await expect(page.locator('role=img[name="Asset preview"]')).toBeVisible(); - await poAdmin.btnDeleteAssetsLogo.click(); + await poAdminSettings.btnDeleteAssetsLogo.click(); await expect(page.locator('role=img[name="Asset preview"]')).not.toBeVisible(); }); test('expect upload and delete background asset and label should be visible', async ({ page }) => { await expect(page.locator('[title="Assets_background"]')).toHaveText('login background (svg, png, jpg)'); - await poAdmin.inputAssetsLogo.setInputFiles('./tests/e2e/fixtures/files/test-image.jpeg'); + await poAdminSettings.inputAssetsLogo.setInputFiles('./tests/e2e/fixtures/files/test-image.jpeg'); await expect(page.locator('role=img[name="Asset preview"]')).toBeVisible(); - await poAdmin.btnDeleteAssetsLogo.click(); + await poAdminSettings.btnDeleteAssetsLogo.click(); await expect(page.locator('role=img[name="Asset preview"]')).not.toBeVisible(); }); test('expect upload and delete background asset for dark theme and label should be visible', async ({ page }) => { await expect(page.locator('[title="Assets_background_dark"]')).toHaveText('login background - dark theme (svg, png, jpg)'); - await poAdmin.inputAssetsLogo.setInputFiles('./tests/e2e/fixtures/files/test-image.jpeg'); + await poAdminSettings.inputAssetsLogo.setInputFiles('./tests/e2e/fixtures/files/test-image.jpeg'); await expect(page.locator('role=img[name="Asset preview"]')).toBeVisible(); - await poAdmin.btnDeleteAssetsLogo.click(); + await poAdminSettings.btnDeleteAssetsLogo.click(); await expect(page.locator('role=img[name="Asset preview"]')).not.toBeVisible(); }); diff --git a/apps/meteor/tests/e2e/settings-int.spec.ts b/apps/meteor/tests/e2e/settings-int.spec.ts index 2403c61853fc2..5fdd0edae7db0 100644 --- a/apps/meteor/tests/e2e/settings-int.spec.ts +++ b/apps/meteor/tests/e2e/settings-int.spec.ts @@ -1,24 +1,26 @@ import { Users } from './fixtures/userStates'; -import { Admin } from './page-objects'; +import { AdminSettings } from './page-objects'; import { test, expect } from './utils/test'; test.use({ storageState: Users.admin.state }); test.describe.serial('settings-int', () => { - let poAdmin: Admin; + let poAdminSettings: AdminSettings; test.beforeEach(async ({ page }) => { - poAdmin = new Admin(page); + poAdminSettings = new AdminSettings(page); + const pageTitle = page.locator('[data-qa-type="PageHeader-title"]'); await page.goto('/admin/settings/Message'); - await expect(page.locator('[data-qa-type="PageHeader-title"]')).toHaveText('Message'); + await pageTitle.waitFor(); + await expect(pageTitle).toHaveText('Message'); }); test('expect not being able to set int value as empty string', async ({ page }) => { await page.locator('#Message_AllowEditing_BlockEditInMinutes').fill(''); await page.locator('#Message_AllowEditing_BlockEditInMinutes').blur(); - await poAdmin.btnSaveSettings.click(); + await poAdminSettings.btnSaveChanges.click(); await expect(page.locator('.rcx-toastbar.rcx-toastbar--error')).toBeVisible(); }); From 8e99ec6ce16656d4a43e7ea79e2b6e0d77ee15fc Mon Sep 17 00:00:00 2001 From: Martin Schoeler Date: Mon, 3 Nov 2025 18:59:43 -0300 Subject: [PATCH 030/129] chore: disable invite link when in ABAC enabled room (#37349) --- .../RoomMembers/RoomMembers.spec.tsx | 23 + .../RoomMembers/RoomMembers.stories.tsx | 24 + .../contextualBar/RoomMembers/RoomMembers.tsx | 13 +- .../RoomMembers/RoomMembersItem.tsx | 3 +- .../RoomMembers/RoomMembersWithData.tsx | 3 +- .../__snapshots__/RoomMembers.spec.tsx.snap | 480 ++++++++++++++++++ 6 files changed, 541 insertions(+), 5 deletions(-) create mode 100644 apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembers.spec.tsx create mode 100644 apps/meteor/client/views/room/contextualBar/RoomMembers/__snapshots__/RoomMembers.spec.tsx.snap diff --git a/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembers.spec.tsx b/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembers.spec.tsx new file mode 100644 index 0000000000000..07816200d9275 --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembers.spec.tsx @@ -0,0 +1,23 @@ +import { composeStories } from '@storybook/react'; +import { render } from '@testing-library/react'; +import { axe } from 'jest-axe'; + +import * as stories from './RoomMembers.stories'; + +jest.mock('../../hooks/useUserInfoActions', () => ({ + useUserInfoActions: jest.fn(), +})); + +const testCases = Object.values(composeStories(stories)).map((Story) => [Story.storyName || 'Story', Story]); +test.each(testCases)(`renders %s without crashing`, async (_storyname, Story) => { + const { baseElement } = render(); + expect(baseElement).toMatchSnapshot(); +}); + +test.each(testCases)('%s should have no a11y violations', async (_storyname, Story) => { + const { container } = render(); + + // Disable 'nested-interactive' rule because our `Select` component is still not a11y compliant + const results = await axe(container, { rules: { 'nested-interactive': { enabled: false } } }); + expect(results).toHaveNoViolations(); +}); diff --git a/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembers.stories.tsx b/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembers.stories.tsx index e2a169dfa795a..d7aed0515c6de 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembers.stories.tsx +++ b/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembers.stories.tsx @@ -47,3 +47,27 @@ Loading.args = { loadMoreItems: action('loadMoreItems'), reload: action('reload'), }; + +export const WithABACRoom = Template.bind({}); +WithABACRoom.args = { + loading: false, + members: [ + { + _id: 'rocket.cat', + username: 'rocket.cat', + status: UserStatus.ONLINE, + name: 'Rocket.Cat', + }, + ], + text: 'filter', + type: 'online', + setText: action('Lorem Ipsum'), + setType: action('online'), + total: 123, + loadMoreItems: action('loadMoreItems'), + rid: '!roomId', + isTeam: false, + isDirect: false, + reload: action('reload'), + isABACRoom: true, +}; diff --git a/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembers.tsx b/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembers.tsx index 93f0d4eb07218..89dec4efc0a54 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembers.tsx +++ b/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembers.tsx @@ -44,13 +44,14 @@ type RoomMembersProps = { loadMoreItems: () => void; renderRow?: ElementType>; reload: () => void; + isABACRoom?: boolean; }; const RoomMembers = ({ loading, members = [], text, - type, + type = 'online', setText, setType, onClickClose, @@ -65,6 +66,7 @@ const RoomMembers = ({ isTeam, isDirect, reload, + isABACRoom = false, }: RoomMembersProps): ReactElement => { const t = useTranslation(); const inputRef = useAutoFocus(true); @@ -199,7 +201,14 @@ const RoomMembers = ({ {onClickInvite && ( - )} diff --git a/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembersItem.tsx b/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembersItem.tsx index 5cca6da9f5a44..a9f2f02991fc6 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembersItem.tsx +++ b/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembersItem.tsx @@ -39,7 +39,6 @@ const RoomMembersItem = ({ useRealName, }: RoomMembersItemProps): ReactElement => { const [showButton, setShowButton] = useState(); - const isReduceMotionEnabled = usePrefersReducedMotion(); const handleMenuEvent = { [isReduceMotionEnabled ? 'onMouseEnter' : 'onTransitionEnd']: setShowButton, @@ -62,7 +61,7 @@ const RoomMembersItem = ({ {showButton ? ( ) : ( - + )} diff --git a/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembersWithData.tsx b/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembersWithData.tsx index 2f2dd1a59975f..1abda763611f8 100644 --- a/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembersWithData.tsx +++ b/apps/meteor/client/views/room/contextualBar/RoomMembers/RoomMembersWithData.tsx @@ -34,7 +34,6 @@ const RoomMembersWithData = ({ rid }: { rid: IRoom['_id'] }): ReactElement => { const isDirect = room && isDirectMessageRoom(room); const hasPermissionToCreateInviteLinks = usePermission('create-invite-links', rid); const isFederated = room && isRoomFederated(room); - // we are dropping the non native federation for now const isFederationBlocked = room && !isRoomNativeFederated(room); @@ -118,6 +117,8 @@ const RoomMembersWithData = ({ rid }: { rid: IRoom['_id'] }): ReactElement => { reload={refetch} onClickInvite={canCreateInviteLinks && canAddUsers ? openInvite : undefined} onClickAdd={canAddUsers ? openAddUser : undefined} + // @ts-expect-error to be implemented in ABAC Feature branch + isABACRoom={Boolean(room?.abacAttributes)} /> ); }; diff --git a/apps/meteor/client/views/room/contextualBar/RoomMembers/__snapshots__/RoomMembers.spec.tsx.snap b/apps/meteor/client/views/room/contextualBar/RoomMembers/__snapshots__/RoomMembers.spec.tsx.snap new file mode 100644 index 0000000000000..d5d6ce44d8d6e --- /dev/null +++ b/apps/meteor/client/views/room/contextualBar/RoomMembers/__snapshots__/RoomMembers.spec.tsx.snap @@ -0,0 +1,480 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`renders Default without crashing 1`] = ` + +
+
+