From 5b3ff91d1bb09f78ff19b92a4997d09b30d2597f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?=
<60678893+juliajforesti@users.noreply.github.com>
Date: Thu, 19 Oct 2023 16:31:40 -0300
Subject: [PATCH 01/22] feat: move a11y features to CE (#30676)
---
.../providers/UserProvider/UserProvider.tsx | 9 --
.../accessibility/AccessibilityPage.tsx | 80 ++++--------------
.../accessibility/HighContrastUpsellModal.tsx | 41 ---------
.../MentionsWithSymbolUpsellModal.tsx | 40 ---------
.../views/account/accessibility/themeItems.ts | 10 ++-
.../rocketchat-i18n/i18n/en.i18n.json | 7 --
.../images/high-contrast-upsell-modal.png | Bin 13392 -> 0 bytes
.../public/images/mentions-upsell-modal.png | Bin 9723 -> 0 bytes
8 files changed, 24 insertions(+), 163 deletions(-)
delete mode 100644 apps/meteor/client/views/account/accessibility/HighContrastUpsellModal.tsx
delete mode 100644 apps/meteor/client/views/account/accessibility/MentionsWithSymbolUpsellModal.tsx
delete mode 100644 apps/meteor/public/images/high-contrast-upsell-modal.png
delete mode 100644 apps/meteor/public/images/mentions-upsell-modal.png
diff --git a/apps/meteor/client/providers/UserProvider/UserProvider.tsx b/apps/meteor/client/providers/UserProvider/UserProvider.tsx
index 432a197671f3..09f631ffa6a6 100644
--- a/apps/meteor/client/providers/UserProvider/UserProvider.tsx
+++ b/apps/meteor/client/providers/UserProvider/UserProvider.tsx
@@ -10,7 +10,6 @@ import { Subscriptions, ChatRoom } from '../../../app/models/client';
import { getUserPreference } from '../../../app/utils/client';
import { sdk } from '../../../app/utils/client/lib/SDKClient';
import { afterLogoutCleanUpCallback } from '../../../lib/callbacks/afterLogoutCleanUpCallback';
-import { useIsEnterprise } from '../../hooks/useIsEnterprise';
import { useReactiveValue } from '../../hooks/useReactiveValue';
import { createReactiveSubscriptionFactory } from '../../lib/createReactiveSubscriptionFactory';
import { useCreateFontStyleElement } from '../../views/account/accessibility/hooks/useCreateFontStyleElement';
@@ -180,14 +179,6 @@ const UserProvider = ({ children }: UserProviderProps): ReactElement => {
}
}, [preferedLanguage, setPreferedLanguage, setUserLanguage, user?.language, userLanguage, userId, setUserPreferences]);
- const { data: license } = useIsEnterprise({ enabled: !!userId });
-
- useEffect(() => {
- if (!license?.isEnterprise && user?.settings?.preferences?.themeAppearence === 'high-contrast') {
- setUserPreferences({ data: { themeAppearence: 'light' } });
- }
- }, [license?.isEnterprise, setUserPreferences, user?.settings?.preferences?.themeAppearence]);
-
return ;
};
diff --git a/apps/meteor/client/views/account/accessibility/AccessibilityPage.tsx b/apps/meteor/client/views/account/accessibility/AccessibilityPage.tsx
index 657548d5a1b9..c8179f08bef2 100644
--- a/apps/meteor/client/views/account/accessibility/AccessibilityPage.tsx
+++ b/apps/meteor/client/views/account/accessibility/AccessibilityPage.tsx
@@ -1,7 +1,6 @@
import { css } from '@rocket.chat/css-in-js';
import type { SelectOption } from '@rocket.chat/fuselage';
import {
- Icon,
FieldDescription,
Accordion,
Box,
@@ -14,20 +13,16 @@ import {
FieldRow,
RadioButton,
Select,
- Tag,
ToggleSwitch,
} from '@rocket.chat/fuselage';
-import { useLocalStorage, useUniqueId } from '@rocket.chat/fuselage-hooks';
-import { useSetModal, useTranslation, useToastMessageDispatch, useEndpoint, useSetting } from '@rocket.chat/ui-contexts';
+import { useUniqueId } from '@rocket.chat/fuselage-hooks';
+import { useTranslation, useToastMessageDispatch, useEndpoint, useSetting } from '@rocket.chat/ui-contexts';
import { useMutation } from '@tanstack/react-query';
import React, { useMemo } from 'react';
import { Controller, useForm } from 'react-hook-form';
import Page from '../../../components/Page';
-import { useIsEnterprise } from '../../../hooks/useIsEnterprise';
import { getDirtyFields } from '../../../lib/getDirtyFields';
-import HighContrastUpsellModal from './HighContrastUpsellModal';
-import MentionsWithSymbolUpsellModal from './MentionsWithSymbolUpsellModal';
import { fontSizes } from './fontSizes';
import type { AccessibilityPreferencesData } from './hooks/useAcessibilityPreferencesValues';
import { useAccessiblityPreferencesValues } from './hooks/useAcessibilityPreferencesValues';
@@ -36,14 +31,9 @@ import { themeItems as themes } from './themeItems';
const AccessibilityPage = () => {
const t = useTranslation();
- const setModal = useSetModal();
const dispatchToastMessage = useToastMessageDispatch();
const preferencesValues = useAccessiblityPreferencesValues();
- const { data: license } = useIsEnterprise();
- const isEnterprise = license?.isEnterprise;
- const { themeAppearence } = preferencesValues;
- const [, setPrevTheme] = useLocalStorage('prevTheme', themeAppearence);
const createFontStyleElement = useCreateFontStyleElement();
const displayRolesEnabled = useSetting('UI_DisplayRoles');
@@ -82,7 +72,6 @@ const AccessibilityPage = () => {
onError: (error) => dispatchToastMessage({ type: 'error', message: error }),
onSettled: (_data, _error, { data: { fontSize } }) => {
reset(currentData);
- dirtyFields.themeAppearence && setPrevTheme(themeAppearence);
dirtyFields.fontSize && fontSize && createFontStyleElement(fontSize);
},
});
@@ -102,45 +91,25 @@ const AccessibilityPage = () => {
- {themes.map(({ id, title, description, ...item }, index) => {
- const showCommunityUpsellTriggers = 'isEEOnly' in item && item.isEEOnly && !isEnterprise;
-
+ {themes.map(({ id, title, description }, index) => {
return (
- {t.has(title) ? t(title) : title}
- {showCommunityUpsellTriggers && (
-
-
-
- {t('Enterprise')}
-
-
- )}
+ {t(title)}
{
- if (showCommunityUpsellTriggers) {
- return (
- setModal( setModal(null)} />)}
- checked={false}
- />
- );
- }
- return onChange(id)} checked={value === id} />;
- }}
+ render={({ field: { onChange, value, ref } }) => (
+ onChange(id)} checked={value === id} />
+ )}
/>
- {t.has(description) ? t(description) : description}
+ {t(description)}
);
@@ -165,32 +134,15 @@ const AccessibilityPage = () => {
-
- {t('Mentions_with_@_symbol')}
- {!isEnterprise && (
-
-
-
- {t('Enterprise')}
-
-
- )}
-
+ {t('Mentions_with_@_symbol')}
- {isEnterprise ? (
- (
-
- )}
- />
- ) : (
- setModal( setModal(null)} />)}
- checked={false}
- />
- )}
+ (
+
+ )}
+ />
void }) => {
- const t = useTranslation();
-
- const isAdmin = useRole('admin');
- const { handleGoFullyFeatured, handleTalkToSales } = useUpsellActions();
-
- if (!isAdmin) {
- return (
-
- );
- }
- return (
-
- );
-};
-export default HighContrastUpsellModal;
diff --git a/apps/meteor/client/views/account/accessibility/MentionsWithSymbolUpsellModal.tsx b/apps/meteor/client/views/account/accessibility/MentionsWithSymbolUpsellModal.tsx
deleted file mode 100644
index b92ca74d0f6e..000000000000
--- a/apps/meteor/client/views/account/accessibility/MentionsWithSymbolUpsellModal.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import { useRole, useTranslation } from '@rocket.chat/ui-contexts';
-import React from 'react';
-
-import GenericUpsellModal from '../../../components/GenericUpsellModal';
-import { useUpsellActions } from '../../../components/GenericUpsellModal/hooks';
-
-const MentionsWithSymbolUpsellModal = ({ onClose }: { onClose: () => void }) => {
- const t = useTranslation();
-
- const isAdmin = useRole('admin');
- const { handleGoFullyFeatured, handleTalkToSales } = useUpsellActions();
-
- if (!isAdmin) {
- return (
-
- );
- }
- return (
-
- );
-};
-export default MentionsWithSymbolUpsellModal;
diff --git a/apps/meteor/client/views/account/accessibility/themeItems.ts b/apps/meteor/client/views/account/accessibility/themeItems.ts
index 62bf3830d952..f16d9128503d 100644
--- a/apps/meteor/client/views/account/accessibility/themeItems.ts
+++ b/apps/meteor/client/views/account/accessibility/themeItems.ts
@@ -1,4 +1,11 @@
-export const themeItems = [
+import type { TranslationKey } from '@rocket.chat/ui-contexts';
+
+type ThemeItem = {
+ id: string;
+ title: TranslationKey;
+ description: TranslationKey;
+};
+export const themeItems: ThemeItem[] = [
{
id: 'light',
title: 'Theme_light',
@@ -10,7 +17,6 @@ export const themeItems = [
description: 'Theme_dark_description',
},
{
- isEEOnly: true,
id: 'high-contrast',
title: 'Theme_high_contrast',
description: 'Theme_high_contrast_description',
diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
index 7ef10e0988b0..46805a1f0e3e 100644
--- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
+++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
@@ -1100,7 +1100,6 @@
"Condition": "Condition",
"Commit_details": "Commit Details",
"Completed": "Completed",
- "Compliant_use_of_color": "Compliant use of color",
"Computer": "Computer",
"Conference_call_apps": "Conference call apps",
"Conference_call_has_ended": "_Call has ended._",
@@ -1865,7 +1864,6 @@
"EmojiCustomFilesystem_Description": "Specify how emojis are stored.",
"Empty_no_agent_selected": "Empty, no agent selected",
"Empty_title": "Empty title",
- "Empower_access_move_beyond_color": "Empower access, move beyond color",
"Enable": "Enable",
"Enable_Auto_Away": "Enable Auto Away",
"Enable_CSP": "Enable Content-Security-Policy",
@@ -3364,7 +3362,6 @@
"Mentions_only": "Mentions only",
"Mentions_with_@_symbol": "Mentions with @ symbol",
"Mentions_with_@_symbol_description": "Mentions notify and highlight messages for groups or specific users, facilitating targeted communication.\n\nThe screen reader functionality is optimized when the \"@\" symbol is employed in the mention feature. This ensures that users relying on screen readers can easily interpret and engage with these mentions.",
- "Mentions_with_symbol_upsell_description": "Unlock the full potential of a barrier-free business with our premium accessibility feature.\n\nSay goodbye to color-related compliance challenges all while aligning with WCAG (Web Content Accessibility Guidelines) and BITV (Barrierefreie Informationstechnik-Verordnung) standards.\n\nThe use of the @ symbol makes it easier for screen readers to navigate and interact with your content, ensuring the best experience for all users.",
"Merge_Channels": "Merge Channels",
"message": "message",
"Message": "Message",
@@ -5976,10 +5973,6 @@
"Theme_high_contrast": "High contrast",
"Theme_high_contrast_description": "Maximum tonal differentiation with bold colors and sharp contrasts provide enhanced accessibility.",
"Highlighted_chosen_word": "Highlighted chosen word",
- "High_contrast_upsell_title": "Enable high contrast theme",
- "High_contrast_upsell_subtitle": "Enhance your team’s reading experience",
- "High_contrast_upsell_description": "Especially designed for individuals with visual impairments or conditions such as color vision deficiency, low vision, or sensitivity to low contrast.\n\nThis theme increases contrast between text and background elements, making content more distinguishable and easier to read.",
- "High_contrast_upsell_annotation": "Talk to your workspace admin about enabling the high contrast theme for everyone.",
"Join_your_team": "Join your team",
"Create_a_password": "Create a password",
"Create_an_account": "Create an account",
diff --git a/apps/meteor/public/images/high-contrast-upsell-modal.png b/apps/meteor/public/images/high-contrast-upsell-modal.png
deleted file mode 100644
index b761a1b0b76c81cda36007039682c3a5493b8448..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 13392
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
ZrjobB@s5`tfeS!L%JLd=)iU6){{_?5;TZq`
diff --git a/apps/meteor/public/images/mentions-upsell-modal.png b/apps/meteor/public/images/mentions-upsell-modal.png
deleted file mode 100644
index a71b3b837d9a77866e4b2c490543b1f608050871..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 9723
zcmc(Fc{r5q+y5;>mZSyQpA^bk*|Me*(Gaq)W#6(6S!R$2l|3>s_O-0p34=tq$MgP<-@oti{xQcK_cix*-`90spYuFF=jWVf_Y8G8+4$K20N}i%
zcgq9-jyM7U1Hj5mA2}V;&rAP0=BH;B2mtJ-4nGV)b`BqXkRi}S_XbciEVx9!U~hXO4jsqYOB=VW0t-1b`VwIP&9Wkm
z%S^AGP3b+eEqM~V{c#WCuPxYjvUE>@Kk4Hc6z1-yZ5Q+FMxKBzz>{F35YtNd-$D;hi1&pQ|AHTI+9tL&7=})cecN(UUGV&us(F*(O7r@4tjs?8^!(X
z80jx0Cog@Cp94SOZ&Y!dPCkg7x-m(kV&e~7zt>R9-ukadCi(PZ4UP7(Z8&dt4I`Z^XK=COacXFGS}
z?8DQ)xA5OH<#}NZe$|1O43aqnLLcq_xi>*bVE@N^FNIcM<$Ilur=3lr&3bRskHH!l
z*|o{{pJfdpbYm&;Zh!ySb9B~6_aCe9MztwzeJoAWK8s#^Mmno$U;Kc+rF;V~X(?WN
zk=xow0DzDvt_J{7dE8M9^b2o4hHuVSxM8hIY$own+LfMo{hD1AMQ`N<7K?rfjG-?&
zaJ6APR$aLj7_83H|DI!X+ZO&o@B8}>s}H()0-E>*hWVYdg4v?2m&Yg#$BWE7JT%dGe-V$Ypv=81CCi@hOim=_B?>jZ
zZsxJXLJAF&^DpF31bI#T04stFQrZJ>{hl7L3T-V=jfMd(&=-%8WhJlgO3C=n6DaP>
zU?G+2h_k;)@6M%9L}X?9&U7_2il-;cZV(9PVG}`TP9^pgn^DIa3707j*p_V%P&u!|
zBSS-3d)?{0(ozx$(Q;SH9+KyEP(1qZ?URLkas-5kWavyuXna3T>YIDz{?eR}d0_R6
zj`dOvW?QAzaNpW*D+?Q(9j*8_&;?>JCD{$09wYpu2ZLDWhy9Rd+e_Q(n
zo_VNO9Y5(-$sYR1kKvWg)t9Z;@)KK?;?r7VLmT2!)RiNKhZsLCUsX}~?HS&F#iC8W
z&@T%IFC~oGS-qb44hl1pmFa;0p8)u(od2U2mjJ*qLB-Hnb*kFR9xe(#T{0x5cIY^7
z^-eH7RQuyRw+};Y)!h3n3noVZ=)QE7r7YmD-{v{XKu<+)pR+GpUtyyMKTDU>lOl+F
zjE9l=>Jj<>4g-~ol6qx@bgM3=u~$l-{2dt*0S@|v3miw5%Zo%0A7;J@A${Qe_pDpS
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
From 79f6388678aa1241628656792e8a03565107a037 Mon Sep 17 00:00:00 2001
From: Diego Sampaio
Date: Thu, 19 Oct 2023 16:57:17 -0300
Subject: [PATCH 02/22] test: add cleanup to users tests (#30654)
---
apps/meteor/tests/data/api-data.js | 9 +-
apps/meteor/tests/data/custom-fields.js | 18 +-
apps/meteor/tests/data/users.helper.js | 17 +-
apps/meteor/tests/end-to-end/api/01-users.js | 466 ++++++++++--------
.../tests/end-to-end/api/10-subscriptions.js | 12 +-
5 files changed, 275 insertions(+), 247 deletions(-)
diff --git a/apps/meteor/tests/data/api-data.js b/apps/meteor/tests/data/api-data.js
index d08e4cc50c54..b311af16e764 100644
--- a/apps/meteor/tests/data/api-data.js
+++ b/apps/meteor/tests/data/api-data.js
@@ -13,10 +13,10 @@ export function wait(cb, time) {
return () => setTimeout(cb, time);
}
-export const apiUsername = `api${username}`;
-export const apiEmail = `api${email}`;
-export const apiPublicChannelName = `api${publicChannelName}`;
-export const apiPrivateChannelName = `api${privateChannelName}`;
+export const apiUsername = `api${username}-${Date.now()}`;
+export const apiEmail = `api${email}-${Date.now()}`;
+export const apiPublicChannelName = `api${publicChannelName}-${Date.now()}`;
+export const apiPrivateChannelName = `api${privateChannelName}-${Date.now()}`;
export const apiRoleNameUsers = `api${roleNameUsers}`;
export const apiRoleNameSubscriptions = `api${roleNameSubscriptions}`;
@@ -25,7 +25,6 @@ export const apiRoleScopeSubscriptions = `${roleScopeSubscriptions}`;
export const apiRoleDescription = `api${roleDescription}`;
export const reservedWords = ['admin', 'administrator', 'system', 'user'];
-export const targetUser = {};
export const channel = {};
export const group = {};
export const message = {};
diff --git a/apps/meteor/tests/data/custom-fields.js b/apps/meteor/tests/data/custom-fields.js
index 2509dddf5d84..e2e175429b4c 100644
--- a/apps/meteor/tests/data/custom-fields.js
+++ b/apps/meteor/tests/data/custom-fields.js
@@ -1,4 +1,4 @@
-import { getCredentials, request, api, credentials } from './api-data.js';
+import { credentials, request, api } from './api-data.js';
export const customFieldText = {
type: 'text',
@@ -7,18 +7,12 @@ export const customFieldText = {
maxLength: 10,
};
-export function setCustomFields(customFields, done) {
- getCredentials((error) => {
- if (error) {
- return done(error);
- }
+export function setCustomFields(customFields) {
+ const stringified = customFields ? JSON.stringify(customFields) : '';
- const stringified = customFields ? JSON.stringify(customFields) : '';
-
- request.post(api('settings/Accounts_CustomFields')).set(credentials).send({ value: stringified }).expect(200).end(done);
- });
+ return request.post(api('settings/Accounts_CustomFields')).set(credentials).send({ value: stringified }).expect(200);
}
-export function clearCustomFields(done = () => {}) {
- setCustomFields(null, done);
+export function clearCustomFields() {
+ return setCustomFields(null);
}
diff --git a/apps/meteor/tests/data/users.helper.js b/apps/meteor/tests/data/users.helper.js
index 92425902cb5b..82ab8446547d 100644
--- a/apps/meteor/tests/data/users.helper.js
+++ b/apps/meteor/tests/data/users.helper.js
@@ -33,16 +33,13 @@ export const login = (username, password) =>
});
});
-export const deleteUser = (user) =>
- new Promise((resolve) => {
- request
- .post(api('users.delete'))
- .set(credentials)
- .send({
- userId: user._id,
- })
- .end(resolve);
- });
+export const deleteUser = async (user) =>
+ request
+ .post(api('users.delete'))
+ .set(credentials)
+ .send({
+ userId: user._id,
+ });
export const getUserByUsername = (username) =>
new Promise((resolve) => {
diff --git a/apps/meteor/tests/end-to-end/api/01-users.js b/apps/meteor/tests/end-to-end/api/01-users.js
index b8343dc015da..eaafc97527a3 100644
--- a/apps/meteor/tests/end-to-end/api/01-users.js
+++ b/apps/meteor/tests/end-to-end/api/01-users.js
@@ -5,23 +5,12 @@ import { expect } from 'chai';
import { after, afterEach, before, beforeEach, describe, it } from 'mocha';
import { sleep } from '../../../lib/utils/sleep';
-import {
- getCredentials,
- api,
- request,
- credentials,
- apiEmail,
- apiUsername,
- targetUser,
- log,
- wait,
- reservedWords,
-} from '../../data/api-data.js';
+import { getCredentials, api, request, credentials, apiEmail, apiUsername, log, wait, reservedWords } from '../../data/api-data.js';
import { MAX_BIO_LENGTH, MAX_NICKNAME_LENGTH } from '../../data/constants.ts';
import { customFieldText, clearCustomFields, setCustomFields } from '../../data/custom-fields.js';
import { imgURL } from '../../data/interactions';
import { updatePermission, updateSetting } from '../../data/permissions.helper';
-import { createRoom } from '../../data/rooms.helper';
+import { createRoom, deleteRoom } from '../../data/rooms.helper';
import { adminEmail, preferences, password, adminUsername } from '../../data/user';
import { createUser, login, deleteUser, getUserStatus, getUserByUsername } from '../../data/users.helper.js';
@@ -39,11 +28,48 @@ async function joinChannel(userCredentials, roomId) {
});
}
+const targetUser = {};
+
describe('[Users]', function () {
this.retries(0);
before((done) => getCredentials(done));
+ before('should create a new user', async () => {
+ await request
+ .post(api('users.create'))
+ .set(credentials)
+ .send({
+ email: apiEmail,
+ name: apiUsername,
+ username: apiUsername,
+ password,
+ active: true,
+ roles: ['user'],
+ joinDefaultChannels: true,
+ verified: true,
+ })
+ .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.username', apiUsername);
+ expect(res.body).to.have.nested.property('user.emails[0].address', apiEmail);
+ expect(res.body).to.have.nested.property('user.active', true);
+ expect(res.body).to.have.nested.property('user.name', apiUsername);
+ expect(res.body).to.not.have.nested.property('user.e2e');
+
+ expect(res.body).to.not.have.nested.property('user.customFields');
+
+ targetUser._id = res.body.user._id;
+ targetUser.username = res.body.user.username;
+ });
+ });
+
+ after(async () => {
+ await deleteUser(targetUser);
+ });
+
it('enabling E2E in server and generating keys to user...', async () => {
await updateSetting('E2E_Enable', true);
await request
@@ -71,145 +97,101 @@ describe('[Users]', function () {
});
describe('[/users.create]', () => {
- before((done) => clearCustomFields(done));
- after((done) => clearCustomFields(done));
+ before(async () => clearCustomFields());
+ after(async () => clearCustomFields());
+
+ it('should create a new user with custom fields', async () => {
+ await setCustomFields({ customFieldText });
+
+ const username = `customField_${apiUsername}`;
+ const email = `customField_${apiEmail}`;
+ const customFields = { customFieldText: 'success' };
+
+ let user;
- it('should create a new user', async () => {
await request
.post(api('users.create'))
.set(credentials)
.send({
- email: apiEmail,
- name: apiUsername,
- username: apiUsername,
+ email,
+ name: username,
+ username,
password,
active: true,
roles: ['user'],
joinDefaultChannels: true,
verified: true,
+ customFields,
})
.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.username', apiUsername);
- expect(res.body).to.have.nested.property('user.emails[0].address', apiEmail);
+ expect(res.body).to.have.nested.property('user.username', username);
+ expect(res.body).to.have.nested.property('user.emails[0].address', email);
expect(res.body).to.have.nested.property('user.active', true);
- expect(res.body).to.have.nested.property('user.name', apiUsername);
+ expect(res.body).to.have.nested.property('user.name', username);
+ expect(res.body).to.have.nested.property('user.customFields.customFieldText', 'success');
expect(res.body).to.not.have.nested.property('user.e2e');
- expect(res.body).to.not.have.nested.property('user.customFields');
-
- targetUser._id = res.body.user._id;
- targetUser.username = res.body.user.username;
+ user = res.body.user;
});
- await request
- .post(api('login'))
- .send({
- user: apiUsername,
- password,
- })
- .expect('Content-Type', 'application/json')
- .expect(200);
+ await deleteUser(user);
});
- it('should create a new user with custom fields', (done) => {
- setCustomFields({ customFieldText }, (error) => {
- if (error) {
- return done(error);
- }
-
- const username = `customField_${apiUsername}`;
- const email = `customField_${apiEmail}`;
- const customFields = { customFieldText: 'success' };
-
+ function failCreateUser(name) {
+ it(`should not create a new user if username is the reserved word ${name}`, (done) => {
request
.post(api('users.create'))
.set(credentials)
.send({
- email,
- name: username,
- username,
+ email: `create_user_fail_${apiEmail}`,
+ name: `create_user_fail_${apiUsername}`,
+ username: name,
password,
active: true,
roles: ['user'],
joinDefaultChannels: true,
verified: true,
- customFields,
})
.expect('Content-Type', 'application/json')
- .expect(200)
+ .expect(400)
.expect((res) => {
- expect(res.body).to.have.property('success', true);
- expect(res.body).to.have.nested.property('user.username', username);
- expect(res.body).to.have.nested.property('user.emails[0].address', email);
- expect(res.body).to.have.nested.property('user.active', true);
- expect(res.body).to.have.nested.property('user.name', username);
- expect(res.body).to.have.nested.property('user.customFields.customFieldText', 'success');
- expect(res.body).to.not.have.nested.property('user.e2e');
+ expect(res.body).to.have.property('success', false);
+ expect(res.body).to.have.property('error', `${name} is blocked and can't be used! [error-blocked-username]`);
})
.end(done);
});
- });
+ }
- function failCreateUser(name) {
- it(`should not create a new user if username is the reserved word ${name}`, (done) => {
- request
+ function failUserWithCustomField(field) {
+ it(`should not create a user if a custom field ${field.reason}`, async () => {
+ await setCustomFields({ customFieldText });
+
+ const customFields = {};
+ customFields[field.name] = field.value;
+
+ await request
.post(api('users.create'))
.set(credentials)
.send({
- email: `create_user_fail_${apiEmail}`,
- name: `create_user_fail_${apiUsername}`,
- username: name,
+ email: `customField_fail_${apiEmail}`,
+ name: `customField_fail_${apiUsername}`,
+ username: `customField_fail_${apiUsername}`,
password,
active: true,
roles: ['user'],
joinDefaultChannels: true,
verified: true,
+ customFields,
})
.expect('Content-Type', 'application/json')
.expect(400)
.expect((res) => {
expect(res.body).to.have.property('success', false);
- expect(res.body).to.have.property('error', `${name} is blocked and can't be used! [error-blocked-username]`);
- })
- .end(done);
- });
- }
-
- function failUserWithCustomField(field) {
- it(`should not create a user if a custom field ${field.reason}`, (done) => {
- setCustomFields({ customFieldText }, (error) => {
- if (error) {
- return done(error);
- }
-
- const customFields = {};
- customFields[field.name] = field.value;
-
- request
- .post(api('users.create'))
- .set(credentials)
- .send({
- email: `customField_fail_${apiEmail}`,
- name: `customField_fail_${apiUsername}`,
- username: `customField_fail_${apiUsername}`,
- password,
- active: true,
- roles: ['user'],
- joinDefaultChannels: true,
- verified: true,
- customFields,
- })
- .expect('Content-Type', 'application/json')
- .expect(400)
- .expect((res) => {
- expect(res.body).to.have.property('success', false);
- expect(res.body).to.have.property('errorType', 'error-user-registration-custom-field');
- })
- .end(done);
- });
+ expect(res.body).to.have.property('errorType', 'error-user-registration-custom-field');
+ });
});
}
@@ -226,12 +208,16 @@ describe('[Users]', function () {
});
describe('users default roles configuration', () => {
+ const users = [];
+
before(async () => {
await updateSetting('Accounts_Registration_Users_Default_Roles', 'user,admin');
});
after(async () => {
await updateSetting('Accounts_Registration_Users_Default_Roles', 'user');
+
+ await Promise.all(users.map((user) => deleteUser(user)));
});
it('should create a new user with default roles', (done) => {
@@ -256,6 +242,8 @@ describe('[Users]', function () {
expect(res.body).to.have.nested.property('user.active', true);
expect(res.body).to.have.nested.property('user.name', username);
expect(res.body.user.roles).to.have.members(['user', 'admin']);
+
+ users.push(res.body.user);
})
.end(done);
});
@@ -283,6 +271,8 @@ describe('[Users]', function () {
expect(res.body).to.have.nested.property('user.active', true);
expect(res.body).to.have.nested.property('user.name', username);
expect(res.body.user.roles).to.have.members(['guest']);
+
+ users.push(res.body.user);
})
.end(done);
});
@@ -292,6 +282,10 @@ describe('[Users]', function () {
describe('[/users.register]', () => {
const email = `email@email${Date.now()}.com`;
const username = `myusername${Date.now()}`;
+ let user;
+
+ after(async () => deleteUser(user));
+
it('should register new user', (done) => {
request
.post(api('users.register'))
@@ -308,6 +302,7 @@ describe('[Users]', function () {
expect(res.body).to.have.nested.property('user.username', username);
expect(res.body).to.have.nested.property('user.active', true);
expect(res.body).to.have.nested.property('user.name', 'name');
+ user = res.body.user;
})
.end(done);
});
@@ -331,9 +326,11 @@ describe('[Users]', function () {
});
describe('[/users.info]', () => {
- after(() => {
- updatePermission('view-other-user-channels', ['admin']);
- updatePermission('view-full-other-user-info', ['admin']);
+ after(async () => {
+ await Promise.all([
+ updatePermission('view-other-user-channels', ['admin']),
+ updatePermission('view-full-other-user-info', ['admin']),
+ ]);
});
it('should return an error when the user does not exist', (done) => {
@@ -476,26 +473,30 @@ describe('[Users]', function () {
});
it('should correctly route users that have `ufs` in their username', async () => {
+ const ufsUsername = `ufs-${Date.now()}`;
+ let user;
+
await request
.post(api('users.create'))
.set(credentials)
.send({
- email: 'me@email.com',
+ email: `me-${Date.now()}@email.com`,
name: 'testuser',
- username: 'ufs',
+ username: ufsUsername,
password: '1234',
})
.expect('Content-Type', 'application/json')
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
+ user = res.body.user;
});
await request
.get(api('users.info'))
.set(credentials)
.query({
- username: 'ufs',
+ username: ufsUsername,
})
.expect('Content-Type', 'application/json')
.expect(200)
@@ -503,9 +504,11 @@ describe('[Users]', function () {
expect(res.body).to.have.property('success', true);
expect(res.body.user).to.have.property('type', 'user');
expect(res.body.user).to.have.property('name', 'testuser');
- expect(res.body.user).to.have.property('username', 'ufs');
+ expect(res.body.user).to.have.property('username', ufsUsername);
expect(res.body.user).to.have.property('active', true);
});
+
+ await deleteUser(user);
});
});
describe('[/users.getPresence]', () => {
@@ -549,10 +552,10 @@ describe('[Users]', function () {
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('full', true);
- expect(res.body)
- .to.have.property('users')
- .to.have.property('0')
- .to.deep.have.all.keys('_id', 'avatarETag', 'username', 'name', 'status', 'utcOffset');
+
+ const user = res.body.users.find((user) => user.username === 'rocket.cat');
+
+ expect(user).to.have.all.keys('_id', 'avatarETag', 'username', 'name', 'status', 'utcOffset');
})
.end(done);
});
@@ -583,10 +586,10 @@ describe('[Users]', function () {
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.property('full', true);
- expect(res.body)
- .to.have.property('users')
- .to.have.property('0')
- .to.deep.have.all.keys('_id', 'avatarETag', 'username', 'name', 'status', 'utcOffset');
+
+ const user = res.body.users.find((user) => user.username === 'rocket.cat');
+
+ expect(user).to.have.all.keys('_id', 'avatarETag', 'username', 'name', 'status', 'utcOffset');
})
.end(done);
});
@@ -599,68 +602,59 @@ describe('[Users]', function () {
let user2;
let user2Credentials;
- before((done) => {
- const createDeactivatedUser = async () => {
- const username = `deactivated_${Date.now()}${apiUsername}`;
- const email = `deactivated_+${Date.now()}${apiEmail}`;
-
- const userData = {
- email,
- name: username,
- username,
- password,
- active: false,
- };
-
- deactivatedUser = await createUser(userData);
-
- expect(deactivatedUser).to.not.be.null;
- expect(deactivatedUser).to.have.nested.property('username', username);
- expect(deactivatedUser).to.have.nested.property('emails[0].address', email);
- expect(deactivatedUser).to.have.nested.property('active', false);
- expect(deactivatedUser).to.have.nested.property('name', username);
- expect(deactivatedUser).to.not.have.nested.property('e2e');
+ before(async () => {
+ const username = `deactivated_${Date.now()}${apiUsername}`;
+ const email = `deactivated_+${Date.now()}${apiEmail}`;
+
+ const userData = {
+ email,
+ name: username,
+ username,
+ password,
+ active: false,
};
- createDeactivatedUser().then(done);
- });
- before((done) =>
- setCustomFields({ customFieldText }, async (error) => {
- if (error) {
- return done(error);
- }
-
- const username = `customField_${Date.now()}${apiUsername}`;
- const email = `customField_+${Date.now()}${apiEmail}`;
- const customFields = { customFieldText: 'success' };
+ deactivatedUser = await createUser(userData);
- const userData = {
- email,
- name: username,
- username,
- password,
- active: true,
- roles: ['user'],
- joinDefaultChannels: true,
- verified: true,
- customFields,
- };
+ expect(deactivatedUser).to.not.be.null;
+ expect(deactivatedUser).to.have.nested.property('username', username);
+ expect(deactivatedUser).to.have.nested.property('emails[0].address', email);
+ expect(deactivatedUser).to.have.nested.property('active', false);
+ expect(deactivatedUser).to.have.nested.property('name', username);
+ expect(deactivatedUser).to.not.have.nested.property('e2e');
+ });
- user = await createUser(userData);
+ before(async () => {
+ await setCustomFields({ customFieldText });
+
+ const username = `customField_${Date.now()}${apiUsername}`;
+ const email = `customField_+${Date.now()}${apiEmail}`;
+ const customFields = { customFieldText: 'success' };
+
+ const userData = {
+ email,
+ name: username,
+ username,
+ password,
+ active: true,
+ roles: ['user'],
+ joinDefaultChannels: true,
+ verified: true,
+ customFields,
+ };
- expect(user).to.not.be.null;
- expect(user).to.have.nested.property('username', username);
- expect(user).to.have.nested.property('emails[0].address', email);
- expect(user).to.have.nested.property('active', true);
- expect(user).to.have.nested.property('name', username);
- expect(user).to.have.nested.property('customFields.customFieldText', 'success');
- expect(user).to.not.have.nested.property('e2e');
+ user = await createUser(userData);
- return done();
- }),
- );
+ expect(user).to.not.be.null;
+ expect(user).to.have.nested.property('username', username);
+ expect(user).to.have.nested.property('emails[0].address', email);
+ expect(user).to.have.nested.property('active', true);
+ expect(user).to.have.nested.property('name', username);
+ expect(user).to.have.nested.property('customFields.customFieldText', 'success');
+ expect(user).to.not.have.nested.property('e2e');
+ });
- after((done) => clearCustomFields(done));
+ after(async () => clearCustomFields());
before(async () => {
user2 = await createUser({ joinDefaultChannels: false });
@@ -668,6 +662,8 @@ describe('[Users]', function () {
});
after(async () => {
+ await deleteUser(deactivatedUser);
+ await deleteUser(user);
await deleteUser(user2);
user2 = undefined;
@@ -1281,26 +1277,23 @@ describe('[Users]', function () {
});
});
- it('should update the user name when the required permission is applied', (done) => {
- updatePermission('edit-other-user-info', ['admin']).then(() => {
- updateSetting('Accounts_AllowUsernameChange', false).then(() => {
- request
- .post(api('users.update'))
- .set(credentials)
- .send({
- userId: targetUser._id,
- data: {
- username: 'fake.name',
- },
- })
- .expect('Content-Type', 'application/json')
- .expect(200)
- .expect((res) => {
- expect(res.body).to.have.property('success', true);
- })
- .end(done);
+ it('should update the user name when the required permission is applied', async () => {
+ await Promise.all([updatePermission('edit-other-user-info', ['admin']), updateSetting('Accounts_AllowUsernameChange', false)]);
+
+ await request
+ .post(api('users.update'))
+ .set(credentials)
+ .send({
+ userId: targetUser._id,
+ data: {
+ username: `fake.name.${Date.now()}`,
+ },
+ })
+ .expect('Content-Type', 'application/json')
+ .expect(200)
+ .expect((res) => {
+ expect(res.body).to.have.property('success', true);
});
- });
});
it('should return an error when trying update user real name and it is not allowed', (done) => {
@@ -2297,6 +2290,8 @@ describe('[Users]', function () {
.end(done);
});
+ after(async () => deleteUser(targetUser));
+
it('should return an username suggestion', (done) => {
request
.get(api('users.getUsernameSuggestion'))
@@ -2326,7 +2321,7 @@ describe('[Users]', function () {
const testUsername = `test-username-123456-${+new Date()}`;
let targetUser;
let userCredentials;
- it('register a new user...', (done) => {
+ before((done) => {
request
.post(api('users.register'))
.set(credentials)
@@ -2343,7 +2338,7 @@ describe('[Users]', function () {
})
.end(done);
});
- it('Login...', (done) => {
+ before((done) => {
request
.post(api('login'))
.send({
@@ -2360,6 +2355,8 @@ describe('[Users]', function () {
.end(done);
});
+ after(async () => deleteUser(targetUser));
+
it('should return true if the username is the same user username set', (done) => {
request
.get(api('users.checkUsernameAvailability'))
@@ -2410,7 +2407,7 @@ describe('[Users]', function () {
const testUsername = `testuser${+new Date()}`;
let targetUser;
let userCredentials;
- it('register a new user...', (done) => {
+ before((done) => {
request
.post(api('users.register'))
.set(credentials)
@@ -2427,7 +2424,7 @@ describe('[Users]', function () {
})
.end(done);
});
- it('Login...', (done) => {
+ before((done) => {
request
.post(api('login'))
.send({
@@ -2444,6 +2441,8 @@ describe('[Users]', function () {
.end(done);
});
+ after(async () => deleteUser(targetUser));
+
it('Enable "Accounts_AllowDeleteOwnAccount" setting...', (done) => {
request
.post('/api/v1/settings/Accounts_AllowDeleteOwnAccount')
@@ -2472,23 +2471,22 @@ describe('[Users]', function () {
.end(done);
});
- it('should delete user own account when the SHA256 hash is in upper case', (done) => {
- createUser().then((user) => {
- login(user.username, password).then((createdUserCredentials) => {
- request
- .post(api('users.deleteOwnAccount'))
- .set(createdUserCredentials)
- .send({
- password: crypto.createHash('sha256').update(password, 'utf8').digest('hex').toUpperCase(),
- })
- .expect('Content-Type', 'application/json')
- .expect(200)
- .expect((res) => {
- expect(res.body).to.have.property('success', true);
- })
- .end(done);
+ it('should delete user own account when the SHA256 hash is in upper case', async () => {
+ const user = await createUser();
+ const createdUserCredentials = await login(user.username, password);
+ await request
+ .post(api('users.deleteOwnAccount'))
+ .set(createdUserCredentials)
+ .send({
+ password: crypto.createHash('sha256').update(password, 'utf8').digest('hex').toUpperCase(),
+ })
+ .expect('Content-Type', 'application/json')
+ .expect(200)
+ .expect((res) => {
+ expect(res.body).to.have.property('success', true);
});
- });
+
+ await deleteUser(user);
});
it('should return an error when trying to delete user own account if user is the last room owner', async () => {
@@ -2542,6 +2540,9 @@ describe('[Users]', function () {
expect(res.body).to.have.property('error', '[user-last-owner]');
expect(res.body).to.have.property('errorType', 'user-last-owner');
});
+
+ await deleteRoom({ type: 'c', roomId: room._id });
+ await deleteUser(user);
});
it('should delete user own account if the user is the last room owner and `confirmRelinquish` is set to `true`', async () => {
@@ -2594,6 +2595,8 @@ describe('[Users]', function () {
.expect((res) => {
expect(res.body).to.have.property('success', true);
});
+ await deleteRoom({ type: 'c', roomId: room._id });
+ await deleteUser(user);
});
it('should assign a new owner to the room if the last room owner is deleted', async () => {
@@ -2661,6 +2664,8 @@ describe('[Users]', function () {
expect(res.body.roles[0].roles).to.eql(['owner']);
expect(res.body.roles[0].u).to.have.property('_id', credentials['X-User-Id']);
});
+ await deleteRoom({ type: 'c', roomId: room._id });
+ await deleteUser(user);
});
});
@@ -2763,6 +2768,8 @@ describe('[Users]', function () {
expect(res.body).to.have.property('error', '[user-last-owner]');
expect(res.body).to.have.property('errorType', 'user-last-owner');
});
+
+ await deleteRoom({ type: 'c', roomId: room._id });
});
it('should delete user account if the user is the last room owner and `confirmRelinquish` is set to `true`', async () => {
@@ -2813,6 +2820,8 @@ describe('[Users]', function () {
.expect((res) => {
expect(res.body).to.have.property('success', true);
});
+
+ await deleteRoom({ type: 'c', roomId: room._id });
});
it('should delete user account when logged user has "delete-user" permission', async () => {
@@ -2893,6 +2902,8 @@ describe('[Users]', function () {
expect(res.body.roles[0].roles).to.eql(['owner']);
expect(res.body.roles[0].u).to.have.property('_id', credentials['X-User-Id']);
});
+
+ await deleteRoom({ type: 'c', roomId: room._id });
});
});
@@ -3241,6 +3252,8 @@ describe('[Users]', function () {
expect(res.body).to.have.property('error', '[user-last-owner]');
expect(res.body).to.have.property('errorType', 'user-last-owner');
});
+
+ await deleteRoom({ type: 'c', roomId: room._id });
});
it('should set other user status to inactive if the user is the last owner of a room and `confirmRelinquish` is set to `true`', async () => {
@@ -3305,6 +3318,8 @@ describe('[Users]', function () {
.expect((res) => {
expect(res.body).to.have.property('success', true);
});
+
+ await deleteRoom({ type: 'c', roomId: room._id });
});
it('should set other user as room owner if the last owner of a room is deactivated and `confirmRelinquish` is set to `true`', async () => {
@@ -3396,6 +3411,8 @@ describe('[Users]', function () {
expect(res.body.roles[1].roles).to.eql(['owner']);
expect(res.body.roles[1].u).to.have.property('_id', credentials['X-User-Id']);
});
+
+ await deleteRoom({ type: 'c', roomId: room._id });
});
it('should return an error when trying to set other user active status and has not the necessary permission(edit-other-user-active-status)', (done) => {
@@ -3464,6 +3481,8 @@ describe('[Users]', function () {
expect(user).to.have.property('roles');
expect(user.roles).to.be.an('array').of.length(2);
expect(user.roles).to.include('user', 'livechat-agent');
+
+ await deleteUser(testUser);
});
});
@@ -3516,6 +3535,10 @@ describe('[Users]', function () {
.end(done);
});
+ after(async () => {
+ await deleteUser(testUser);
+ });
+
it('should fail to deactivate if user doesnt have edit-other-user-active-status permission', (done) => {
updatePermission('edit-other-user-active-status', []).then(() => {
request
@@ -3563,7 +3586,7 @@ describe('[Users]', function () {
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
- expect(res.body).to.have.property('count', 2);
+ expect(res.body).to.have.property('count', 1);
})
.end(done);
});
@@ -3690,7 +3713,7 @@ describe('[Users]', function () {
updatePermission('view-outside-room', ['admin', 'owner', 'moderator', 'user']);
});
- describe('[without permission]', () => {
+ describe('[without permission]', function () {
let user;
let userCredentials;
let user2;
@@ -3711,6 +3734,12 @@ describe('[Users]', function () {
roomId = await createChannel(userCredentials, `channel.autocomplete.${Date.now()}`);
});
+ after(async () => {
+ await deleteRoom({ type: 'c', roomId });
+ await deleteUser(user);
+ await deleteUser(user2);
+ });
+
it('should return an empty list when the user does not have any subscription', (done) => {
request
.get(api('users.autocomplete?selector={}'))
@@ -4126,6 +4155,10 @@ describe('[Users]', function () {
.then(() => done());
});
+ after(async () => {
+ await deleteUser(testUser);
+ });
+
it('should list both channels', (done) => {
request
.get(api('users.listTeams'))
@@ -4151,14 +4184,19 @@ describe('[Users]', function () {
describe('[/users.logout]', () => {
let user;
let otherUser;
+ let userCredentials;
+
before(async () => {
user = await createUser();
otherUser = await createUser();
});
+ before(async () => {
+ userCredentials = await login(user.username, password);
+ });
+
after(async () => {
await deleteUser(user);
await deleteUser(otherUser);
- user = undefined;
});
it('should throw unauthorized error to user w/o "logout-other-user" permission', (done) => {
@@ -4187,7 +4225,7 @@ describe('[Users]', function () {
it('should logout the requester', (done) => {
updatePermission('logout-other-user', []).then(() => {
- request.post(api('users.logout')).set(credentials).expect('Content-Type', 'application/json').expect(200).end(done);
+ request.post(api('users.logout')).set(userCredentials).expect('Content-Type', 'application/json').expect(200).end(done);
});
});
});
diff --git a/apps/meteor/tests/end-to-end/api/10-subscriptions.js b/apps/meteor/tests/end-to-end/api/10-subscriptions.js
index f547895eb8a4..531291a99216 100644
--- a/apps/meteor/tests/end-to-end/api/10-subscriptions.js
+++ b/apps/meteor/tests/end-to-end/api/10-subscriptions.js
@@ -236,7 +236,8 @@ describe('[Subscriptions]', function () {
before(async () => {
user = await createUser({ username: 'testthread123', password: 'testthread123' });
threadUserCredentials = await login('testthread123', 'testthread123');
- request
+
+ const res = await request
.post(api('chat.sendMessage'))
.set(threadUserCredentials)
.send({
@@ -244,14 +245,13 @@ describe('[Subscriptions]', function () {
rid: testChannel._id,
msg: 'Starting a Thread',
},
- })
- .end((_, res) => {
- threadId = res.body.message._id;
});
+
+ threadId = res.body.message._id;
});
- after((done) => {
- deleteUser(user).then(done);
+ after(async () => {
+ await deleteUser(user);
});
it('should mark threads as read', async () => {
From 93a0859e87f0a115152e79e224fb42c10748c5fe Mon Sep 17 00:00:00 2001
From: Yash Rajpal <58601732+yash-rajpal@users.noreply.github.com>
Date: Fri, 20 Oct 2023 02:23:53 +0530
Subject: [PATCH 03/22] fix: Unnecessary username validation on account profile
form (#30677)
---
.changeset/empty-files-know.md | 5 +++++
.../client/views/account/profile/AccountProfileForm.tsx | 5 +++++
2 files changed, 10 insertions(+)
create mode 100644 .changeset/empty-files-know.md
diff --git a/.changeset/empty-files-know.md b/.changeset/empty-files-know.md
new file mode 100644
index 000000000000..5e6fb8f751b2
--- /dev/null
+++ b/.changeset/empty-files-know.md
@@ -0,0 +1,5 @@
+---
+'@rocket.chat/meteor': patch
+---
+
+Fix unnecessary username validation on accounts profile form
diff --git a/apps/meteor/client/views/account/profile/AccountProfileForm.tsx b/apps/meteor/client/views/account/profile/AccountProfileForm.tsx
index ed97b95caae8..65b3a0967d49 100644
--- a/apps/meteor/client/views/account/profile/AccountProfileForm.tsx
+++ b/apps/meteor/client/views/account/profile/AccountProfileForm.tsx
@@ -66,6 +66,7 @@ const AccountProfileForm = (props: AllHTMLAttributes): ReactEle
const { email, avatar, username } = watch();
const previousEmail = user ? getUserEmailAddress(user) : '';
+ const previousUsername = user?.username || '';
const isUserVerified = user?.emails?.[0]?.verified ?? false;
const mutateConfirmationEmail = useMutation({
@@ -87,6 +88,10 @@ const AccountProfileForm = (props: AllHTMLAttributes): ReactEle
return;
}
+ if (username === previousUsername) {
+ return;
+ }
+
if (!namesRegex.test(username)) {
return t('error-invalid-username');
}
From b85df55030f9aaf351d15fe66ee0ec008bfc9691 Mon Sep 17 00:00:00 2001
From: csuadev <72958726+csuadev@users.noreply.github.com>
Date: Thu, 19 Oct 2023 16:46:06 -0500
Subject: [PATCH 04/22] fix: UI issue on marketplace filters (#30660)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: Júlia Jaeger Foresti <60678893+juliajforesti@users.noreply.github.com>
---
.changeset/cyan-mangos-do.md | 5 +++
.../CategoryFilter/CategoryDropDownAnchor.tsx | 41 ++++++++++++-------
.../RadioDropDown/RadioDownAnchor.tsx | 22 ++++++----
3 files changed, 44 insertions(+), 24 deletions(-)
create mode 100644 .changeset/cyan-mangos-do.md
diff --git a/.changeset/cyan-mangos-do.md b/.changeset/cyan-mangos-do.md
new file mode 100644
index 000000000000..e188686c82d5
--- /dev/null
+++ b/.changeset/cyan-mangos-do.md
@@ -0,0 +1,5 @@
+---
+"@rocket.chat/meteor": patch
+---
+
+fix: UI issue on marketplace filters
diff --git a/apps/meteor/client/views/marketplace/components/CategoryFilter/CategoryDropDownAnchor.tsx b/apps/meteor/client/views/marketplace/components/CategoryFilter/CategoryDropDownAnchor.tsx
index 91e66683e66f..b3e43fda942f 100644
--- a/apps/meteor/client/views/marketplace/components/CategoryFilter/CategoryDropDownAnchor.tsx
+++ b/apps/meteor/client/views/marketplace/components/CategoryFilter/CategoryDropDownAnchor.tsx
@@ -1,4 +1,6 @@
-import { Box, Button, Icon } from '@rocket.chat/fuselage';
+import type { Button } from '@rocket.chat/fuselage';
+import { Box, Icon } from '@rocket.chat/fuselage';
+import colorTokens from '@rocket.chat/fuselage-tokens/colors.json';
import { useTranslation } from '@rocket.chat/ui-contexts';
import type { ComponentProps, MouseEventHandler } from 'react';
import React, { forwardRef } from 'react';
@@ -15,33 +17,42 @@ const CategoryDropDownAnchor = forwardRef
{selectedCategoriesCount > 0 && (
{selectedCategoriesCount}
diff --git a/apps/meteor/client/views/marketplace/components/RadioDropDown/RadioDownAnchor.tsx b/apps/meteor/client/views/marketplace/components/RadioDropDown/RadioDownAnchor.tsx
index 36e4ff55657f..f480b2a60280 100644
--- a/apps/meteor/client/views/marketplace/components/RadioDropDown/RadioDownAnchor.tsx
+++ b/apps/meteor/client/views/marketplace/components/RadioDropDown/RadioDownAnchor.tsx
@@ -1,4 +1,5 @@
-import { Box, Button, Icon } from '@rocket.chat/fuselage';
+import type { Button } from '@rocket.chat/fuselage';
+import { Box, Icon } from '@rocket.chat/fuselage';
import type { ComponentProps } from 'react';
import React, { forwardRef } from 'react';
@@ -14,22 +15,25 @@ const RadioDownAnchor = forwardRef(functi
return (
{selected}
From b9a3381d9394d39bb22629c9fc951c2407e00db8 Mon Sep 17 00:00:00 2001
From: Kevin Aleman
Date: Thu, 19 Oct 2023 16:47:32 -0600
Subject: [PATCH 05/22] test: `ShouldPreventAction` (#30690)
---
ee/packages/license/src/license.spec.ts | 24 ++++++++++++++++++++++++
1 file changed, 24 insertions(+)
diff --git a/ee/packages/license/src/license.spec.ts b/ee/packages/license/src/license.spec.ts
index 989be7b69ae1..b637ee33ddfd 100644
--- a/ee/packages/license/src/license.spec.ts
+++ b/ee/packages/license/src/license.spec.ts
@@ -41,6 +41,30 @@ it('should prevent if the counter is equal or over the limit', async () => {
await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(true);
});
+it.skip('should not prevent an action if another limit is over the limit', async () => {
+ const licenseManager = await getReadyLicenseManager();
+
+ const license = await new MockedLicenseBuilder()
+ .withLimits('activeUsers', [
+ {
+ max: 10,
+ behavior: 'prevent_action',
+ },
+ ])
+ .withLimits('monthlyActiveContacts', [
+ {
+ max: 10,
+ behavior: 'prevent_action',
+ },
+ ]);
+
+ await expect(licenseManager.setLicense(await license.sign())).resolves.toBe(true);
+
+ licenseManager.setLicenseLimitCounter('activeUsers', () => 11);
+ licenseManager.setLicenseLimitCounter('monthlyActiveContacts', () => 2);
+ await expect(licenseManager.shouldPreventAction('monthlyActiveContacts')).resolves.toBe(false);
+});
+
describe('Validate License Limits', () => {
describe('prevent_action behavior', () => {
describe('during the licensing apply', () => {
From 53cf1f5940c76e6d4df132e3ca8e7118206d1ea5 Mon Sep 17 00:00:00 2001
From: Kevin Aleman
Date: Thu, 19 Oct 2023 19:25:00 -0600
Subject: [PATCH 06/22] refactor: Move functions out of `livechat.js` (#30664)
---
.../app/apps/server/bridges/livechat.ts | 2 +-
.../app/livechat/imports/server/rest/sms.js | 2 +-
.../app/livechat/server/api/v1/message.ts | 16 +-
.../app/livechat/server/api/v1/visitor.ts | 2 +-
apps/meteor/app/livechat/server/lib/Helper.ts | 6 +-
.../app/livechat/server/lib/Livechat.js | 191 +---------------
.../app/livechat/server/lib/LivechatTyped.ts | 207 +++++++++++++++++-
.../server/methods/returnAsInquiry.ts | 4 +-
.../server/methods/sendMessageLivechat.ts | 28 +--
apps/meteor/app/livechat/server/startup.ts | 10 +-
.../server/lib/AutoTransferChatScheduler.ts | 4 +-
.../EmailInbox/EmailInbox_Incoming.ts | 3 +-
12 files changed, 238 insertions(+), 237 deletions(-)
diff --git a/apps/meteor/app/apps/server/bridges/livechat.ts b/apps/meteor/app/apps/server/bridges/livechat.ts
index 5b6c76257667..70802f280095 100644
--- a/apps/meteor/app/apps/server/bridges/livechat.ts
+++ b/apps/meteor/app/apps/server/bridges/livechat.ts
@@ -44,7 +44,7 @@ export class AppLivechatBridge extends LivechatBridge {
throw new Error('Invalid token for livechat message');
}
- const msg = await Livechat.sendMessage({
+ const msg = await LivechatTyped.sendMessage({
guest: this.orch.getConverters()?.get('visitors').convertAppVisitor(message.visitor),
message: await this.orch.getConverters()?.get('messages').convertAppMessage(message),
agent: undefined,
diff --git a/apps/meteor/app/livechat/imports/server/rest/sms.js b/apps/meteor/app/livechat/imports/server/rest/sms.js
index 7ecb3b3fc100..9d2bee133784 100644
--- a/apps/meteor/app/livechat/imports/server/rest/sms.js
+++ b/apps/meteor/app/livechat/imports/server/rest/sms.js
@@ -182,7 +182,7 @@ API.v1.addRoute('livechat/sms-incoming/:service', {
};
try {
- const msg = SMSService.response.call(this, await Livechat.sendMessage(sendMessage));
+ const msg = SMSService.response.call(this, await LivechatTyped.sendMessage(sendMessage));
setImmediate(async () => {
if (sms.extra) {
if (sms.extra.fromCountry) {
diff --git a/apps/meteor/app/livechat/server/api/v1/message.ts b/apps/meteor/app/livechat/server/api/v1/message.ts
index 0d5a22b90d89..1dcf54e403a6 100644
--- a/apps/meteor/app/livechat/server/api/v1/message.ts
+++ b/apps/meteor/app/livechat/server/api/v1/message.ts
@@ -17,7 +17,6 @@ import { isWidget } from '../../../../api/server/helpers/isWidget';
import { loadMessageHistory } from '../../../../lib/server/functions/loadMessageHistory';
import { settings } from '../../../../settings/server';
import { normalizeMessageFileUpload } from '../../../../utils/server/functions/normalizeMessageFileUpload';
-import { Livechat } from '../../lib/Livechat';
import { Livechat as LivechatTyped } from '../../lib/LivechatTyped';
import { findGuest, findRoom, normalizeHttpHeaderData } from '../lib/livechat';
@@ -67,7 +66,7 @@ API.v1.addRoute(
},
};
- const result = await Livechat.sendMessage(sendMessage);
+ const result = await LivechatTyped.sendMessage(sendMessage);
if (result) {
const message = await Messages.findOneById(_id);
if (!message) {
@@ -176,7 +175,7 @@ API.v1.addRoute(
throw new Error('invalid-message');
}
- const result = await Livechat.deleteMessage({ guest, message });
+ const result = await LivechatTyped.deleteMessage({ guest, message });
if (result) {
return API.v1.success({
message: {
@@ -272,10 +271,15 @@ API.v1.addRoute(
visitor = await LivechatVisitors.findOneEnabledById(visitorId);
}
+ const guest = visitor;
+ if (!guest) {
+ throw new Error('error-invalid-token');
+ }
+
const sentMessages = await Promise.all(
this.bodyParams.messages.map(async (message: { msg: string }): Promise<{ username: string; msg: string; ts: number }> => {
const sendMessage = {
- guest: visitor,
+ guest,
message: {
_id: Random.id(),
rid,
@@ -288,8 +292,8 @@ API.v1.addRoute(
},
},
};
- // @ts-expect-error -- Typings on sendMessage are wrong
- const sentMessage = await Livechat.sendMessage(sendMessage);
+
+ const sentMessage = await LivechatTyped.sendMessage(sendMessage);
return {
username: sentMessage.u.username,
msg: sentMessage.msg,
diff --git a/apps/meteor/app/livechat/server/api/v1/visitor.ts b/apps/meteor/app/livechat/server/api/v1/visitor.ts
index 84f7b96e155d..6488d34eab7a 100644
--- a/apps/meteor/app/livechat/server/api/v1/visitor.ts
+++ b/apps/meteor/app/livechat/server/api/v1/visitor.ts
@@ -121,7 +121,7 @@ API.v1.addRoute('livechat/visitor/:token', {
}
const { _id } = visitor;
- const result = await Livechat.removeGuest(_id);
+ const result = await LivechatTyped.removeGuest(_id);
if (!result.modifiedCount) {
throw new Meteor.Error('error-removing-visitor', 'An error ocurred while deleting visitor');
}
diff --git a/apps/meteor/app/livechat/server/lib/Helper.ts b/apps/meteor/app/livechat/server/lib/Helper.ts
index 63cbbd6998ef..4acbdf5090ad 100644
--- a/apps/meteor/app/livechat/server/lib/Helper.ts
+++ b/apps/meteor/app/livechat/server/lib/Helper.ts
@@ -437,7 +437,7 @@ export const forwardRoomToAgent = async (room: IOmnichannelRoom, transferData: T
return false;
}
- await Livechat.saveTransferHistory(room, transferData);
+ await LivechatTyped.saveTransferHistory(room, transferData);
const { servedBy } = roomTaken;
if (servedBy) {
@@ -537,7 +537,7 @@ export const forwardRoomToDepartment = async (room: IOmnichannelRoom, guest: ILi
logger.debug(
`Routing algorithm doesn't support auto assignment (using ${RoutingManager.methodName}). Chat will be on department queue`,
);
- await Livechat.saveTransferHistory(room, transferData);
+ await LivechatTyped.saveTransferHistory(room, transferData);
return RoutingManager.unassignAgent(inquiry, departmentId);
}
@@ -573,7 +573,7 @@ export const forwardRoomToDepartment = async (room: IOmnichannelRoom, guest: ILi
}
}
- await Livechat.saveTransferHistory(room, transferData);
+ await LivechatTyped.saveTransferHistory(room, transferData);
if (oldServedBy) {
// if chat is queued then we don't ignore the new servedBy agent bcs at this
// point the chat is not assigned to him/her and it is still in the queue
diff --git a/apps/meteor/app/livechat/server/lib/Livechat.js b/apps/meteor/app/livechat/server/lib/Livechat.js
index 837a8eb7309b..b208c9fb5e85 100644
--- a/apps/meteor/app/livechat/server/lib/Livechat.js
+++ b/apps/meteor/app/livechat/server/lib/Livechat.js
@@ -8,11 +8,9 @@ import {
LivechatRooms,
LivechatInquiry,
Subscriptions,
- Messages,
LivechatDepartment as LivechatDepartmentRaw,
Rooms,
Users,
- ReadReceipts,
} from '@rocket.chat/models';
import { Match, check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
@@ -25,15 +23,11 @@ import { i18n } from '../../../../server/lib/i18n';
import { addUserRolesAsync } from '../../../../server/lib/roles/addUserRoles';
import { removeUserFromRolesAsync } from '../../../../server/lib/roles/removeUserFromRoles';
import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
-import { FileUpload } from '../../../file-upload/server';
-import { deleteMessage } from '../../../lib/server/functions/deleteMessage';
-import { sendMessage } from '../../../lib/server/functions/sendMessage';
import * as Mailer from '../../../mailer/server/api';
import { settings } from '../../../settings/server';
import { businessHourManager } from '../business-hour';
import { Analytics } from './Analytics';
-import { normalizeTransferredByData, parseAgentCustomFields, updateDepartmentAgents } from './Helper';
-import { Livechat as LivechatTyped } from './LivechatTyped';
+import { parseAgentCustomFields, updateDepartmentAgents } from './Helper';
import { RoutingManager } from './RoutingManager';
const logger = new Logger('Livechat');
@@ -43,41 +37,6 @@ export const Livechat = {
logger,
- async sendMessage({ guest, message, roomInfo, agent }) {
- const { room, newRoom } = await LivechatTyped.getRoom(guest, message, roomInfo, agent);
- if (guest.name) {
- message.alias = guest.name;
- }
- return Object.assign(await sendMessage(guest, message, room), {
- newRoom,
- showConnecting: this.showConnecting(),
- });
- },
-
- async deleteMessage({ guest, message }) {
- Livechat.logger.debug(`Attempting to delete a message by visitor ${guest._id}`);
- check(message, Match.ObjectIncluding({ _id: String }));
-
- const msg = await Messages.findOneById(message._id);
- if (!msg || !msg._id) {
- return;
- }
-
- const deleteAllowed = settings.get('Message_AllowDeleting');
- const editOwn = msg.u && msg.u._id === guest._id;
-
- if (!deleteAllowed || !editOwn) {
- Livechat.logger.debug('Cannot delete message: not allowed');
- throw new Meteor.Error('error-action-not-allowed', 'Message deleting not allowed', {
- method: 'livechatDeleteMessage',
- });
- }
-
- await deleteMessage(message, guest);
-
- return true;
- },
-
async saveGuest(guestData, userId) {
const { _id, name, email, phone, livechatData = {} } = guestData;
Livechat.logger.debug(`Saving data for visitor ${_id}`);
@@ -234,111 +193,6 @@ export const Livechat = {
return Message.saveSystemMessage('livechat_navigation_history', roomId, `${pageTitle} - ${pageUrl}`, user, extraData);
},
- async saveTransferHistory(room, transferData) {
- Livechat.logger.debug(`Saving transfer history for room ${room._id}`);
- const { departmentId: previousDepartment } = room;
- const { department: nextDepartment, transferredBy, transferredTo, scope, comment } = transferData;
-
- check(
- transferredBy,
- Match.ObjectIncluding({
- _id: String,
- username: String,
- name: Match.Maybe(String),
- type: String,
- }),
- );
-
- const { _id, username } = transferredBy;
- const scopeData = scope || (nextDepartment ? 'department' : 'agent');
- Livechat.logger.debug(`Storing new chat transfer of ${room._id} [Transfered by: ${_id} to ${scopeData}]`);
-
- const transfer = {
- transferData: {
- transferredBy,
- ts: new Date(),
- scope: scopeData,
- comment,
- ...(previousDepartment && { previousDepartment }),
- ...(nextDepartment && { nextDepartment }),
- ...(transferredTo && { transferredTo }),
- },
- };
-
- const type = 'livechat_transfer_history';
- const transferMessage = {
- t: type,
- rid: room._id,
- ts: new Date(),
- msg: '',
- u: {
- _id,
- username,
- },
- groupable: false,
- };
-
- Object.assign(transferMessage, transfer);
-
- await sendMessage(transferredBy, transferMessage, room);
- },
-
- async returnRoomAsInquiry(rid, departmentId, overrideTransferData = {}) {
- Livechat.logger.debug(`Transfering room ${rid} to ${departmentId ? 'department' : ''} queue`);
- const room = await LivechatRooms.findOneById(rid);
- if (!room) {
- throw new Meteor.Error('error-invalid-room', 'Invalid room', {
- method: 'livechat:returnRoomAsInquiry',
- });
- }
-
- if (!room.open) {
- throw new Meteor.Error('room-closed', 'Room closed', {
- method: 'livechat:returnRoomAsInquiry',
- });
- }
-
- if (room.onHold) {
- throw new Meteor.Error('error-room-onHold', 'Room On Hold', {
- method: 'livechat:returnRoomAsInquiry',
- });
- }
-
- if (!room.servedBy) {
- return false;
- }
-
- const user = await Users.findOneById(room.servedBy._id);
- if (!user || !user._id) {
- throw new Meteor.Error('error-invalid-user', 'Invalid user', {
- method: 'livechat:returnRoomAsInquiry',
- });
- }
-
- // find inquiry corresponding to room
- const inquiry = await LivechatInquiry.findOne({ rid });
- if (!inquiry) {
- return false;
- }
-
- const transferredBy = normalizeTransferredByData(user, room);
- Livechat.logger.debug(`Transfering room ${room._id} by user ${transferredBy._id}`);
- const transferData = { roomId: rid, scope: 'queue', departmentId, transferredBy, ...overrideTransferData };
- try {
- await this.saveTransferHistory(room, transferData);
- await RoutingManager.unassignAgent(inquiry, departmentId);
- } catch (e) {
- this.logger.error(e);
- throw new Meteor.Error('error-returning-inquiry', 'Error returning inquiry to the queue', {
- method: 'livechat:returnRoomAsInquiry',
- });
- }
-
- callbacks.runAsync('livechat:afterReturnRoomAsInquiry', { room });
-
- return true;
- },
-
async getLivechatRoomGuestInfo(room) {
const visitor = await LivechatVisitors.findOneEnabledById(room.v._id);
const agent = await Users.findOneById(room.servedBy && room.servedBy._id);
@@ -481,55 +335,12 @@ export const Livechat = {
return removeUserFromRolesAsync(user._id, ['livechat-manager']);
},
- async removeGuest(_id) {
- const guest = await LivechatVisitors.findOneEnabledById(_id, { projection: { _id: 1, token: 1 } });
- if (!guest) {
- throw new Meteor.Error('error-invalid-guest', 'Invalid guest', {
- method: 'livechat:removeGuest',
- });
- }
-
- await this.cleanGuestHistory(guest);
- return LivechatVisitors.disableById(_id);
- },
-
async setUserStatusLivechat(userId, status) {
const user = await Users.setLivechatStatus(userId, status);
callbacks.runAsync('livechat.setUserStatusLivechat', { userId, status });
return user;
},
- async setUserStatusLivechatIf(userId, status, condition, fields) {
- const user = await Users.setLivechatStatusIf(userId, status, condition, fields);
- callbacks.runAsync('livechat.setUserStatusLivechat', { userId, status });
- return user;
- },
-
- async cleanGuestHistory(guest) {
- const { token } = guest;
-
- // This shouldn't be possible, but just in case
- if (!token) {
- throw new Error('error-invalid-guest');
- }
-
- const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {});
- const cursor = LivechatRooms.findByVisitorToken(token, extraQuery);
- for await (const room of cursor) {
- await Promise.all([
- FileUpload.removeFilesByRoomId(room._id),
- Messages.removeByRoomId(room._id),
- ReadReceipts.removeByRoomId(room._id),
- ]);
- }
-
- await Promise.all([
- Subscriptions.removeByVisitorToken(token),
- LivechatRooms.removeByVisitorToken(token),
- LivechatInquiry.removeByVisitorToken(token),
- ]);
- },
-
async saveDepartmentAgents(_id, departmentAgents) {
check(_id, String);
check(departmentAgents, {
diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts
index 293b15e8d63c..32cb5c83acd9 100644
--- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts
+++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts
@@ -15,6 +15,9 @@ import type {
ILivechatDepartment,
AtLeast,
TransferData,
+ MessageAttachment,
+ IMessageInbox,
+ ILivechatAgentStatus,
} from '@rocket.chat/core-typings';
import { UserStatus, isOmnichannelRoom } from '@rocket.chat/core-typings';
import { Logger, type MainLogger } from '@rocket.chat/logger';
@@ -34,13 +37,15 @@ import {
import { Random } from '@rocket.chat/random';
import { serverFetch as fetch } from '@rocket.chat/server-fetch';
import moment from 'moment-timezone';
-import type { FindCursor, UpdateFilter } from 'mongodb';
+import type { Filter, FindCursor, UpdateFilter } from 'mongodb';
import { Apps, AppEvents } from '../../../../ee/server/apps';
import { callbacks } from '../../../../lib/callbacks';
import { i18n } from '../../../../server/lib/i18n';
import { canAccessRoomAsync } from '../../../authorization/server';
import { hasRoleAsync } from '../../../authorization/server/functions/hasRole';
+import { FileUpload } from '../../../file-upload/server';
+import { deleteMessage } from '../../../lib/server/functions/deleteMessage';
import { sendMessage } from '../../../lib/server/functions/sendMessage';
import { updateMessage } from '../../../lib/server/functions/updateMessage';
import * as Mailer from '../../../mailer/server/api';
@@ -89,6 +94,40 @@ type OfflineMessageData = {
host?: string;
};
+export interface ILivechatMessage {
+ token: string;
+ _id: string;
+ rid: string;
+ msg: string;
+ file?: {
+ _id: string;
+ name?: string;
+ type?: string;
+ size?: number;
+ description?: string;
+ identify?: { size: { width: number; height: number } };
+ format?: string;
+ };
+ files?: {
+ _id: string;
+ name?: string;
+ type?: string;
+ size?: number;
+ description?: string;
+ identify?: { size: { width: number; height: number } };
+ format?: string;
+ }[];
+ attachments?: MessageAttachment[];
+ alias?: string;
+ groupable?: boolean;
+ blocks?: IMessage['blocks'];
+ email?: IMessageInbox['email'];
+}
+
+type AKeyOf = {
+ [K in keyof T]?: T[K];
+};
+
const dnsResolveMx = util.promisify(dns.resolveMx);
class LivechatClass {
@@ -1123,6 +1162,172 @@ class LivechatClass {
void callbacks.run('livechat.offlineMessage', data);
});
}
+
+ async sendMessage({
+ guest,
+ message,
+ roomInfo,
+ agent,
+ }: {
+ guest: ILivechatVisitor;
+ message: ILivechatMessage;
+ roomInfo: {
+ source?: IOmnichannelRoom['source'];
+ [key: string]: unknown;
+ };
+ agent?: SelectedAgent;
+ }) {
+ const { room, newRoom } = await this.getRoom(guest, message, roomInfo, agent);
+ if (guest.name) {
+ message.alias = guest.name;
+ }
+ return Object.assign(await sendMessage(guest, message, room), {
+ newRoom,
+ showConnecting: this.showConnecting(),
+ });
+ }
+
+ async removeGuest(_id: string) {
+ const guest = await LivechatVisitors.findOneEnabledById(_id, { projection: { _id: 1, token: 1 } });
+ if (!guest) {
+ throw new Error('error-invalid-guest');
+ }
+
+ await this.cleanGuestHistory(guest);
+ return LivechatVisitors.disableById(_id);
+ }
+
+ async cleanGuestHistory(guest: ILivechatVisitor) {
+ const { token } = guest;
+
+ // This shouldn't be possible, but just in case
+ if (!token) {
+ throw new Error('error-invalid-guest');
+ }
+
+ const cursor = LivechatRooms.findByVisitorToken(token);
+ for await (const room of cursor) {
+ await Promise.all([
+ FileUpload.removeFilesByRoomId(room._id),
+ Messages.removeByRoomId(room._id),
+ ReadReceipts.removeByRoomId(room._id),
+ ]);
+ }
+
+ await Promise.all([
+ Subscriptions.removeByVisitorToken(token),
+ LivechatRooms.removeByVisitorToken(token),
+ LivechatInquiry.removeByVisitorToken(token),
+ ]);
+ }
+
+ async deleteMessage({ guest, message }: { guest: ILivechatVisitor; message: IMessage }) {
+ const deleteAllowed = settings.get('Message_AllowDeleting');
+ const editOwn = message.u && message.u._id === guest._id;
+
+ if (!deleteAllowed || !editOwn) {
+ throw new Error('error-action-not-allowed');
+ }
+
+ await deleteMessage(message, guest as unknown as IUser);
+
+ return true;
+ }
+
+ async setUserStatusLivechatIf(userId: string, status: ILivechatAgentStatus, condition?: Filter, fields?: AKeyOf) {
+ const user = await Users.setLivechatStatusIf(userId, status, condition, fields);
+ callbacks.runAsync('livechat.setUserStatusLivechat', { userId, status });
+ return user;
+ }
+
+ async returnRoomAsInquiry(room: IOmnichannelRoom, departmentId?: string, overrideTransferData: any = {}) {
+ this.logger.debug({ msg: `Transfering room to ${departmentId ? 'department' : ''} queue`, room });
+ if (!room.open) {
+ throw new Meteor.Error('room-closed');
+ }
+
+ if (room.onHold) {
+ throw new Meteor.Error('error-room-onHold');
+ }
+
+ if (!room.servedBy) {
+ return false;
+ }
+
+ const user = await Users.findOneById(room.servedBy._id);
+ if (!user?._id) {
+ throw new Meteor.Error('error-invalid-user');
+ }
+
+ // find inquiry corresponding to room
+ const inquiry = await LivechatInquiry.findOne({ rid: room._id });
+ if (!inquiry) {
+ return false;
+ }
+
+ const transferredBy = normalizeTransferredByData(user, room);
+ this.logger.debug(`Transfering room ${room._id} by user ${transferredBy._id}`);
+ const transferData = { roomId: room._id, scope: 'queue', departmentId, transferredBy, ...overrideTransferData };
+ try {
+ await this.saveTransferHistory(room, transferData);
+ await RoutingManager.unassignAgent(inquiry, departmentId);
+ } catch (e) {
+ this.logger.error(e);
+ throw new Meteor.Error('error-returning-inquiry');
+ }
+
+ callbacks.runAsync('livechat:afterReturnRoomAsInquiry', { room });
+
+ return true;
+ }
+
+ async saveTransferHistory(room: IOmnichannelRoom, transferData: TransferData) {
+ const { departmentId: previousDepartment } = room;
+ const { department: nextDepartment, transferredBy, transferredTo, scope, comment } = transferData;
+
+ check(
+ transferredBy,
+ Match.ObjectIncluding({
+ _id: String,
+ username: String,
+ name: Match.Maybe(String),
+ type: String,
+ }),
+ );
+
+ const { _id, username } = transferredBy;
+ const scopeData = scope || (nextDepartment ? 'department' : 'agent');
+ this.logger.info(`Storing new chat transfer of ${room._id} [Transfered by: ${_id} to ${scopeData}]`);
+
+ const transfer = {
+ transferData: {
+ transferredBy,
+ ts: new Date(),
+ scope: scopeData,
+ comment,
+ ...(previousDepartment && { previousDepartment }),
+ ...(nextDepartment && { nextDepartment }),
+ ...(transferredTo && { transferredTo }),
+ },
+ };
+
+ const type = 'livechat_transfer_history';
+ const transferMessage = {
+ t: type,
+ rid: room._id,
+ ts: new Date(),
+ msg: '',
+ u: {
+ _id,
+ username,
+ },
+ groupable: false,
+ };
+
+ Object.assign(transferMessage, transfer);
+
+ await sendMessage(transferredBy, transferMessage, room);
+ }
}
export const Livechat = new LivechatClass();
diff --git a/apps/meteor/app/livechat/server/methods/returnAsInquiry.ts b/apps/meteor/app/livechat/server/methods/returnAsInquiry.ts
index 57a2b0afa3d5..0c12d0df5275 100644
--- a/apps/meteor/app/livechat/server/methods/returnAsInquiry.ts
+++ b/apps/meteor/app/livechat/server/methods/returnAsInquiry.ts
@@ -4,7 +4,7 @@ import type { ServerMethods } from '@rocket.chat/ui-contexts';
import { Meteor } from 'meteor/meteor';
import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
-import { Livechat } from '../lib/Livechat';
+import { Livechat } from '../lib/LivechatTyped';
declare module '@rocket.chat/ui-contexts' {
// eslint-disable-next-line @typescript-eslint/naming-convention
@@ -33,6 +33,6 @@ Meteor.methods({
throw new Meteor.Error('room-closed', 'Room closed', { method: 'livechat:returnAsInquiry' });
}
- return Livechat.returnRoomAsInquiry(rid, departmentId);
+ return Livechat.returnRoomAsInquiry(room, departmentId);
},
});
diff --git a/apps/meteor/app/livechat/server/methods/sendMessageLivechat.ts b/apps/meteor/app/livechat/server/methods/sendMessageLivechat.ts
index c7d412ea4a06..516a9bc5081f 100644
--- a/apps/meteor/app/livechat/server/methods/sendMessageLivechat.ts
+++ b/apps/meteor/app/livechat/server/methods/sendMessageLivechat.ts
@@ -1,36 +1,12 @@
import { OmnichannelSourceType } from '@rocket.chat/core-typings';
-import type { MessageAttachment } from '@rocket.chat/core-typings';
import { LivechatVisitors } from '@rocket.chat/models';
import type { ServerMethods } from '@rocket.chat/ui-contexts';
import { Match, check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
import { settings } from '../../../settings/server';
-import { Livechat } from '../lib/Livechat';
-
-interface ILivechatMessage {
- token: string;
- _id: string;
- rid: string;
- msg: string;
- file?: {
- _id: string;
- name?: string;
- type?: string;
- size?: number;
- description?: string;
- identify?: { size: { width: number; height: number } };
- };
- files?: {
- _id: string;
- name?: string;
- type?: string;
- size?: number;
- description?: string;
- identify?: { size: { width: number; height: number } };
- }[];
- attachments?: MessageAttachment[];
-}
+import { Livechat } from '../lib/LivechatTyped';
+import type { ILivechatMessage } from '../lib/LivechatTyped';
interface ILivechatMessageAgent {
agentId: string;
diff --git a/apps/meteor/app/livechat/server/startup.ts b/apps/meteor/app/livechat/server/startup.ts
index f9fce509e39a..c8487f742b3a 100644
--- a/apps/meteor/app/livechat/server/startup.ts
+++ b/apps/meteor/app/livechat/server/startup.ts
@@ -1,5 +1,5 @@
import type { IUser } from '@rocket.chat/core-typings';
-import { isOmnichannelRoom } from '@rocket.chat/core-typings';
+import { ILivechatAgentStatus, isOmnichannelRoom } from '@rocket.chat/core-typings';
import { LivechatRooms } from '@rocket.chat/models';
import { Accounts } from 'meteor/accounts-base';
import { Meteor } from 'meteor/meteor';
@@ -13,6 +13,7 @@ import { settings } from '../../settings/server';
import { businessHourManager } from './business-hour';
import { createDefaultBusinessHourIfNotExists } from './business-hour/Helper';
import { Livechat } from './lib/Livechat';
+import { Livechat as LivechatTyped } from './lib/LivechatTyped';
import { RoutingManager } from './lib/RoutingManager';
import { LivechatAgentActivityMonitor } from './statistics/LivechatAgentActivityMonitor';
import './roomAccessValidator.internalService';
@@ -79,6 +80,11 @@ Meteor.startup(async () => {
({ user }: { user: IUser }) =>
user?.roles?.includes('livechat-agent') &&
!user?.roles?.includes('bot') &&
- void Livechat.setUserStatusLivechatIf(user._id, 'not-available', {}, { livechatStatusSystemModified: true }).catch(),
+ void LivechatTyped.setUserStatusLivechatIf(
+ user._id,
+ ILivechatAgentStatus.NOT_AVAILABLE,
+ {},
+ { livechatStatusSystemModified: true },
+ ).catch(),
);
});
diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts
index 9d4590836ac9..68044a550277 100644
--- a/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts
+++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoTransferChatScheduler.ts
@@ -5,8 +5,8 @@ import { LivechatRooms, Users } from '@rocket.chat/models';
import { Meteor } from 'meteor/meteor';
import { MongoInternals } from 'meteor/mongo';
-import { Livechat } from '../../../../../app/livechat/server';
import { forwardRoomToAgent } from '../../../../../app/livechat/server/lib/Helper';
+import { Livechat as LivechatTyped } from '../../../../../app/livechat/server/lib/LivechatTyped';
import { RoutingManager } from '../../../../../app/livechat/server/lib/RoutingManager';
import { settings } from '../../../../../app/settings/server';
import { schedulerLogger } from './logger';
@@ -90,7 +90,7 @@ class AutoTransferChatSchedulerClass {
if (!RoutingManager.getConfig()?.autoAssignAgent) {
this.logger.debug(`Auto-assign agent is disabled, returning room ${roomId} as inquiry`);
- await Livechat.returnRoomAsInquiry(room._id, departmentId, {
+ await LivechatTyped.returnRoomAsInquiry(room, departmentId, {
scope: 'autoTransferUnansweredChatsToQueue',
comment: timeoutDuration,
transferredBy: await this.getSchedulerUser(),
diff --git a/apps/meteor/server/features/EmailInbox/EmailInbox_Incoming.ts b/apps/meteor/server/features/EmailInbox/EmailInbox_Incoming.ts
index 939d91661650..ebdd9cdcac01 100644
--- a/apps/meteor/server/features/EmailInbox/EmailInbox_Incoming.ts
+++ b/apps/meteor/server/features/EmailInbox/EmailInbox_Incoming.ts
@@ -12,7 +12,6 @@ import type { ParsedMail, Attachment } from 'mailparser';
import stripHtml from 'string-strip-html';
import { FileUpload } from '../../../app/file-upload/server';
-import { Livechat } from '../../../app/livechat/server/lib/Livechat';
import { Livechat as LivechatTyped } from '../../../app/livechat/server/lib/LivechatTyped';
import { QueueManager } from '../../../app/livechat/server/lib/QueueManager';
import { settings } from '../../../app/settings/server';
@@ -148,7 +147,7 @@ export async function onEmailReceived(email: ParsedMail, inbox: string, departme
const rid = room?._id ?? Random.id();
const msgId = Random.id();
- Livechat.sendMessage({
+ LivechatTyped.sendMessage({
guest,
message: {
_id: msgId,
From a3b3dea4816c6a93829d5be3e56c9b68fbd9ad48 Mon Sep 17 00:00:00 2001
From: Guilherme Gazzo
Date: Thu, 19 Oct 2023 18:27:43 -0700
Subject: [PATCH 07/22] regression: validateLicenseLimits not using the
expected limit (#30693)
---
ee/packages/license/src/license.spec.ts | 3 ++-
ee/packages/license/src/license.ts | 1 +
2 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/ee/packages/license/src/license.spec.ts b/ee/packages/license/src/license.spec.ts
index b637ee33ddfd..c605d6467118 100644
--- a/ee/packages/license/src/license.spec.ts
+++ b/ee/packages/license/src/license.spec.ts
@@ -41,7 +41,7 @@ it('should prevent if the counter is equal or over the limit', async () => {
await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(true);
});
-it.skip('should not prevent an action if another limit is over the limit', async () => {
+it('should not prevent an action if another limit is over the limit', async () => {
const licenseManager = await getReadyLicenseManager();
const license = await new MockedLicenseBuilder()
@@ -63,6 +63,7 @@ it.skip('should not prevent an action if another limit is over the limit', async
licenseManager.setLicenseLimitCounter('activeUsers', () => 11);
licenseManager.setLicenseLimitCounter('monthlyActiveContacts', () => 2);
await expect(licenseManager.shouldPreventAction('monthlyActiveContacts')).resolves.toBe(false);
+ await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(true);
});
describe('Validate License Limits', () => {
diff --git a/ee/packages/license/src/license.ts b/ee/packages/license/src/license.ts
index 14dceedd735a..8212a4a0da27 100644
--- a/ee/packages/license/src/license.ts
+++ b/ee/packages/license/src/license.ts
@@ -268,6 +268,7 @@ export class LicenseManager extends Emitter {
...(extraCount && { behaviors: ['prevent_action'] }),
isNewLicense: false,
suppressLog: !!suppressLog,
+ limits: [action],
context: {
[action]: {
extraCount,
From c29f5ff4172845d8c8880fd2e93fb5a16e7c8337 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?=
<60678893+juliajforesti@users.noreply.github.com>
Date: Thu, 19 Oct 2023 23:01:44 -0300
Subject: [PATCH 08/22] chore: bump fuselage packages (#30696)
---
.../admin/info/UsagePieGraph.stories.tsx | 10 +--
.../views/room/Header/icons/Encrypted.tsx | 2 +-
.../users/ActiveUsersSection.tsx | 6 +-
.../users/UsersByTimeOfTheDaySection.tsx | 14 ++--
.../ee/client/views/admin/info/SeatsCard.tsx | 2 +-
apps/meteor/package.json | 6 +-
ee/packages/pdf-worker/package.json | 2 +-
ee/packages/ui-theming/package.json | 2 +-
packages/fuselage-ui-kit/package.json | 2 +-
packages/gazzodown/package.json | 4 +-
packages/livechat/package.json | 4 +-
packages/ui-client/package.json | 2 +-
packages/ui-composer/package.json | 2 +-
packages/ui-video-conf/package.json | 2 +-
packages/uikit-playground/package.json | 6 +-
yarn.lock | 65 +++++++++----------
16 files changed, 62 insertions(+), 69 deletions(-)
diff --git a/apps/meteor/client/views/admin/info/UsagePieGraph.stories.tsx b/apps/meteor/client/views/admin/info/UsagePieGraph.stories.tsx
index 12f211d47c5e..d76731f0d2c4 100644
--- a/apps/meteor/client/views/admin/info/UsagePieGraph.stories.tsx
+++ b/apps/meteor/client/views/admin/info/UsagePieGraph.stories.tsx
@@ -38,27 +38,27 @@ export const Animated: Story, 'size' |
{
total: 100,
used: 0,
- color: colorTokens.s500,
+ color: colorTokens.g500,
},
{
total: 100,
used: 25,
- color: colorTokens.p500,
+ color: colorTokens.b500,
},
{
total: 100,
used: 50,
- color: colorTokens.w500,
+ color: colorTokens.y500,
},
{
total: 100,
used: 75,
- color: colorTokens['s1-500'],
+ color: colorTokens.o500,
},
{
total: 100,
used: 100,
- color: colorTokens.d500,
+ color: colorTokens.r500,
},
]);
diff --git a/apps/meteor/client/views/room/Header/icons/Encrypted.tsx b/apps/meteor/client/views/room/Header/icons/Encrypted.tsx
index dbfda21f5b7a..bd380c5d8af2 100644
--- a/apps/meteor/client/views/room/Header/icons/Encrypted.tsx
+++ b/apps/meteor/client/views/room/Header/icons/Encrypted.tsx
@@ -29,7 +29,7 @@ const Encrypted = ({ room }: { room: IRoom }) => {
});
});
return e2eEnabled && room?.encrypted ? (
-
+
) : null;
};
diff --git a/apps/meteor/ee/client/views/admin/engagementDashboard/users/ActiveUsersSection.tsx b/apps/meteor/ee/client/views/admin/engagementDashboard/users/ActiveUsersSection.tsx
index e067f777090f..eb504033e1e6 100644
--- a/apps/meteor/ee/client/views/admin/engagementDashboard/users/ActiveUsersSection.tsx
+++ b/apps/meteor/ee/client/views/admin/engagementDashboard/users/ActiveUsersSection.tsx
@@ -127,7 +127,7 @@ const ActiveUsersSection = ({ timezone }: ActiveUsersSectionProps): ReactElement
variation: diffDailyActiveUsers ?? 0,
description: (
<>
- {t('Daily_Active_Users')}
+ {t('Daily_Active_Users')}
>
),
},
@@ -136,7 +136,7 @@ const ActiveUsersSection = ({ timezone }: ActiveUsersSectionProps): ReactElement
variation: diffWeeklyActiveUsers ?? 0,
description: (
<>
- {t('Weekly_Active_Users')}
+ {t('Weekly_Active_Users')}
>
),
},
@@ -203,7 +203,7 @@ const ActiveUsersSection = ({ timezone }: ActiveUsersSectionProps): ReactElement
right: 0,
left: 40,
}}
- colors={[colors.p200, colors.p300, colors.p500]}
+ colors={[colors.b200, colors.b300, colors.b500]}
axisLeft={{
// TODO: Get it from theme
tickSize: 0,
diff --git a/apps/meteor/ee/client/views/admin/engagementDashboard/users/UsersByTimeOfTheDaySection.tsx b/apps/meteor/ee/client/views/admin/engagementDashboard/users/UsersByTimeOfTheDaySection.tsx
index d8f13bb891a3..fa5664ebca27 100644
--- a/apps/meteor/ee/client/views/admin/engagementDashboard/users/UsersByTimeOfTheDaySection.tsx
+++ b/apps/meteor/ee/client/views/admin/engagementDashboard/users/UsersByTimeOfTheDaySection.tsx
@@ -119,13 +119,13 @@ const UsersByTimeOfTheDaySection = ({ timezone }: UsersByTimeOfTheDaySectionProp
type: 'quantize',
colors: [
// TODO: Get it from theme
- colors.p100,
- colors.p200,
- colors.p300,
- colors.p400,
- colors.p500,
- colors.p600,
- colors.p700,
+ colors.b100,
+ colors.b200,
+ colors.b300,
+ colors.b400,
+ colors.b500,
+ colors.b600,
+ colors.b700,
],
}}
emptyColor='transparent'
diff --git a/apps/meteor/ee/client/views/admin/info/SeatsCard.tsx b/apps/meteor/ee/client/views/admin/info/SeatsCard.tsx
index b595dd9c1fae..804893ae8458 100644
--- a/apps/meteor/ee/client/views/admin/info/SeatsCard.tsx
+++ b/apps/meteor/ee/client/views/admin/info/SeatsCard.tsx
@@ -23,7 +23,7 @@ const SeatsCard = ({ seatsCap }: SeatsCardProps): ReactElement => {
const isNearLimit = seatsCap && seatsCap.activeUsers / seatsCap.maxActiveUsers >= 0.8;
- const color = isNearLimit ? colors.d500 : undefined;
+ const color = isNearLimit ? colors.r500 : undefined;
return (
diff --git a/apps/meteor/package.json b/apps/meteor/package.json
index 3ee3366f47dd..18e4725ffc40 100644
--- a/apps/meteor/package.json
+++ b/apps/meteor/package.json
@@ -236,11 +236,11 @@
"@rocket.chat/favicon": "workspace:^",
"@rocket.chat/forked-matrix-appservice-bridge": "^4.0.1",
"@rocket.chat/forked-matrix-bot-sdk": "^0.6.0-beta.2",
- "@rocket.chat/fuselage": "^0.34.0",
+ "@rocket.chat/fuselage": "^0.35.0",
"@rocket.chat/fuselage-hooks": "^0.32.1",
"@rocket.chat/fuselage-polyfills": "next",
"@rocket.chat/fuselage-toastbar": "next",
- "@rocket.chat/fuselage-tokens": "next",
+ "@rocket.chat/fuselage-tokens": "^0.32.0",
"@rocket.chat/fuselage-ui-kit": "workspace:^",
"@rocket.chat/gazzodown": "workspace:^",
"@rocket.chat/i18n": "workspace:^",
@@ -251,7 +251,7 @@
"@rocket.chat/license": "workspace:^",
"@rocket.chat/log-format": "workspace:^",
"@rocket.chat/logger": "workspace:^",
- "@rocket.chat/logo": "^0.31.27",
+ "@rocket.chat/logo": "^0.31.28",
"@rocket.chat/memo": "next",
"@rocket.chat/message-parser": "next",
"@rocket.chat/model-typings": "workspace:^",
diff --git a/ee/packages/pdf-worker/package.json b/ee/packages/pdf-worker/package.json
index 9081c64fba34..f4bd7c5b44f6 100644
--- a/ee/packages/pdf-worker/package.json
+++ b/ee/packages/pdf-worker/package.json
@@ -34,7 +34,7 @@
"dependencies": {
"@react-pdf/renderer": "^3.1.12",
"@rocket.chat/core-typings": "workspace:^",
- "@rocket.chat/fuselage-tokens": "next",
+ "@rocket.chat/fuselage-tokens": "^0.32.0",
"@types/react": "~17.0.62",
"emoji-assets": "^7.0.1",
"emoji-toolkit": "^7.0.1",
diff --git a/ee/packages/ui-theming/package.json b/ee/packages/ui-theming/package.json
index 11aa5fd57ff8..52b8062f332d 100644
--- a/ee/packages/ui-theming/package.json
+++ b/ee/packages/ui-theming/package.json
@@ -4,7 +4,7 @@
"private": true,
"devDependencies": {
"@rocket.chat/css-in-js": "next",
- "@rocket.chat/fuselage": "^0.34.0",
+ "@rocket.chat/fuselage": "^0.35.0",
"@rocket.chat/fuselage-hooks": "^0.32.1",
"@rocket.chat/icons": "^0.32.0",
"@rocket.chat/ui-contexts": "workspace:~",
diff --git a/packages/fuselage-ui-kit/package.json b/packages/fuselage-ui-kit/package.json
index 4555216c2d44..a32d2456752b 100644
--- a/packages/fuselage-ui-kit/package.json
+++ b/packages/fuselage-ui-kit/package.json
@@ -56,7 +56,7 @@
"devDependencies": {
"@rocket.chat/apps-engine": "1.41.0-alpha.290",
"@rocket.chat/eslint-config": "workspace:^",
- "@rocket.chat/fuselage": "^0.34.0",
+ "@rocket.chat/fuselage": "^0.35.0",
"@rocket.chat/fuselage-hooks": "^0.32.1",
"@rocket.chat/fuselage-polyfills": "next",
"@rocket.chat/icons": "^0.32.0",
diff --git a/packages/gazzodown/package.json b/packages/gazzodown/package.json
index e891e5677c75..311950f2222d 100644
--- a/packages/gazzodown/package.json
+++ b/packages/gazzodown/package.json
@@ -6,8 +6,8 @@
"@babel/core": "~7.22.9",
"@rocket.chat/core-typings": "workspace:^",
"@rocket.chat/css-in-js": "next",
- "@rocket.chat/fuselage": "^0.34.0",
- "@rocket.chat/fuselage-tokens": "next",
+ "@rocket.chat/fuselage": "^0.35.0",
+ "@rocket.chat/fuselage-tokens": "^0.32.0",
"@rocket.chat/message-parser": "next",
"@rocket.chat/styled": "next",
"@rocket.chat/ui-client": "workspace:^",
diff --git a/packages/livechat/package.json b/packages/livechat/package.json
index 756248c1df0c..bb92a0716669 100644
--- a/packages/livechat/package.json
+++ b/packages/livechat/package.json
@@ -30,8 +30,8 @@
"@rocket.chat/core-typings": "workspace:^",
"@rocket.chat/ddp-client": "workspace:^",
"@rocket.chat/eslint-config": "workspace:^",
- "@rocket.chat/fuselage-tokens": "next",
- "@rocket.chat/logo": "^0.31.27",
+ "@rocket.chat/fuselage-tokens": "^0.32.0",
+ "@rocket.chat/logo": "^0.31.28",
"@storybook/addon-essentials": "~6.5.16",
"@storybook/addon-postcss": "~2.0.0",
"@storybook/preact": "~6.5.16",
diff --git a/packages/ui-client/package.json b/packages/ui-client/package.json
index f8ef2d3a1e93..7edd02cc6e56 100644
--- a/packages/ui-client/package.json
+++ b/packages/ui-client/package.json
@@ -5,7 +5,7 @@
"devDependencies": {
"@babel/core": "~7.22.9",
"@rocket.chat/css-in-js": "next",
- "@rocket.chat/fuselage": "^0.34.0",
+ "@rocket.chat/fuselage": "^0.35.0",
"@rocket.chat/fuselage-hooks": "^0.32.1",
"@rocket.chat/icons": "^0.32.0",
"@rocket.chat/mock-providers": "workspace:^",
diff --git a/packages/ui-composer/package.json b/packages/ui-composer/package.json
index b5c804b4a2ad..b13328bd001b 100644
--- a/packages/ui-composer/package.json
+++ b/packages/ui-composer/package.json
@@ -5,7 +5,7 @@
"devDependencies": {
"@babel/core": "~7.22.9",
"@rocket.chat/eslint-config": "workspace:^",
- "@rocket.chat/fuselage": "^0.34.0",
+ "@rocket.chat/fuselage": "^0.35.0",
"@rocket.chat/icons": "^0.32.0",
"@storybook/addon-actions": "~6.5.16",
"@storybook/addon-docs": "~6.5.16",
diff --git a/packages/ui-video-conf/package.json b/packages/ui-video-conf/package.json
index 7ca7b1d86140..2e880b3db8bb 100644
--- a/packages/ui-video-conf/package.json
+++ b/packages/ui-video-conf/package.json
@@ -6,7 +6,7 @@
"@babel/core": "~7.22.9",
"@rocket.chat/css-in-js": "next",
"@rocket.chat/eslint-config": "workspace:^",
- "@rocket.chat/fuselage": "^0.34.0",
+ "@rocket.chat/fuselage": "^0.35.0",
"@rocket.chat/fuselage-hooks": "^0.32.1",
"@rocket.chat/icons": "^0.32.0",
"@rocket.chat/styled": "next",
diff --git a/packages/uikit-playground/package.json b/packages/uikit-playground/package.json
index 5a8b1276defb..d9abdf001162 100644
--- a/packages/uikit-playground/package.json
+++ b/packages/uikit-playground/package.json
@@ -15,13 +15,13 @@
"@codemirror/tooltip": "^0.19.16",
"@lezer/highlight": "^1.1.6",
"@rocket.chat/css-in-js": "next",
- "@rocket.chat/fuselage": "^0.34.0",
+ "@rocket.chat/fuselage": "^0.35.0",
"@rocket.chat/fuselage-hooks": "^0.32.1",
"@rocket.chat/fuselage-polyfills": "next",
- "@rocket.chat/fuselage-tokens": "next",
+ "@rocket.chat/fuselage-tokens": "^0.32.0",
"@rocket.chat/fuselage-ui-kit": "workspace:~",
"@rocket.chat/icons": "^0.32.0",
- "@rocket.chat/logo": "^0.31.27",
+ "@rocket.chat/logo": "^0.31.28",
"@rocket.chat/styled": "next",
"@rocket.chat/ui-contexts": "workspace:~",
"codemirror": "^6.0.1",
diff --git a/yarn.lock b/yarn.lock
index 4b4fd7f27bc9..dc01a3898eba 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8219,17 +8219,10 @@ __metadata:
languageName: node
linkType: hard
-"@rocket.chat/fuselage-tokens@npm:^0.31.25":
- version: 0.31.25
- resolution: "@rocket.chat/fuselage-tokens@npm:0.31.25"
- checksum: d05460f2f7b7f01b1498aab6fb7d932b7d752d55ce5a6bad6e7a42f2c1f056164ff8caa7dd8ec11bc0f4441a83d8aad0b8aab5e02c03f3452c4583d159b1a2f7
- languageName: node
- linkType: hard
-
-"@rocket.chat/fuselage-tokens@npm:next":
- version: 0.32.0-dev.379
- resolution: "@rocket.chat/fuselage-tokens@npm:0.32.0-dev.379"
- checksum: c5cf40295c4ae1a5918651b9e156629d6400d5823b8cf5f81a14c66da986a9302d79392b45e991c2fc37aad9633f3d8e2f7f29c68969592340b05968265244e6
+"@rocket.chat/fuselage-tokens@npm:^0.32.0":
+ version: 0.32.0
+ resolution: "@rocket.chat/fuselage-tokens@npm:0.32.0"
+ checksum: 8da7836877ba93462f90d13de6d3d3add8b2758b58c7988e14a8f0deffd1ceef0547f26d4c60a7ddc881e21e3327b5a04cbf17336e5ca8ab9c19789d8e6af3c0
languageName: node
linkType: hard
@@ -8239,7 +8232,7 @@ __metadata:
dependencies:
"@rocket.chat/apps-engine": 1.41.0-alpha.290
"@rocket.chat/eslint-config": "workspace:^"
- "@rocket.chat/fuselage": ^0.34.0
+ "@rocket.chat/fuselage": ^0.35.0
"@rocket.chat/fuselage-hooks": ^0.32.1
"@rocket.chat/fuselage-polyfills": next
"@rocket.chat/gazzodown": "workspace:^"
@@ -8288,13 +8281,13 @@ __metadata:
languageName: unknown
linkType: soft
-"@rocket.chat/fuselage@npm:^0.34.0":
- version: 0.34.0
- resolution: "@rocket.chat/fuselage@npm:0.34.0"
+"@rocket.chat/fuselage@npm:^0.35.0":
+ version: 0.35.0
+ resolution: "@rocket.chat/fuselage@npm:0.35.0"
dependencies:
"@rocket.chat/css-in-js": ^0.31.25
"@rocket.chat/css-supports": ^0.31.25
- "@rocket.chat/fuselage-tokens": ^0.31.25
+ "@rocket.chat/fuselage-tokens": ^0.32.0
"@rocket.chat/memo": ^0.31.25
"@rocket.chat/styled": ^0.31.25
invariant: ^2.2.4
@@ -8308,7 +8301,7 @@ __metadata:
react: ^17.0.2
react-dom: ^17.0.2
react-virtuoso: 1.2.4
- checksum: 72cd1dd7ef13cc3b69fadac5c064a45cd2b65b8a221cde2e8149fa873ac6de89648c677caedb10979e5cf08d39b79f1d7a30caa6378bdeeb873414c7fbac5e6e
+ checksum: 46deea587a1ab4c80a25f4e93882905e2f24778c0e612b7cdd18bfb0c72b2c079d4eee6fe7ad4c52a62354197ebed0a62eaf939b5714859b7086c923668f3f05
languageName: node
linkType: hard
@@ -8319,8 +8312,8 @@ __metadata:
"@babel/core": ~7.22.9
"@rocket.chat/core-typings": "workspace:^"
"@rocket.chat/css-in-js": next
- "@rocket.chat/fuselage": ^0.34.0
- "@rocket.chat/fuselage-tokens": next
+ "@rocket.chat/fuselage": ^0.35.0
+ "@rocket.chat/fuselage-tokens": ^0.32.0
"@rocket.chat/message-parser": next
"@rocket.chat/styled": next
"@rocket.chat/ui-client": "workspace:^"
@@ -8468,9 +8461,9 @@ __metadata:
"@rocket.chat/core-typings": "workspace:^"
"@rocket.chat/ddp-client": "workspace:^"
"@rocket.chat/eslint-config": "workspace:^"
- "@rocket.chat/fuselage-tokens": next
+ "@rocket.chat/fuselage-tokens": ^0.32.0
"@rocket.chat/gazzodown": "workspace:^"
- "@rocket.chat/logo": ^0.31.27
+ "@rocket.chat/logo": ^0.31.28
"@rocket.chat/message-parser": next
"@rocket.chat/random": "workspace:~"
"@rocket.chat/sdk": ^1.0.0-alpha.42
@@ -8581,16 +8574,16 @@ __metadata:
languageName: unknown
linkType: soft
-"@rocket.chat/logo@npm:^0.31.27":
- version: 0.31.27
- resolution: "@rocket.chat/logo@npm:0.31.27"
+"@rocket.chat/logo@npm:^0.31.28":
+ version: 0.31.28
+ resolution: "@rocket.chat/logo@npm:0.31.28"
dependencies:
"@rocket.chat/fuselage-hooks": ^0.32.1
"@rocket.chat/styled": ^0.31.25
peerDependencies:
react: 17.0.2
react-dom: 17.0.2
- checksum: acc56410813a0d4f634f9e847bc4b49275c26aff4e2f285720818cb012a2ad42554982fcc4078c485222a9c9a78244d1a4b16b60588b5c50441b8928c3957efb
+ checksum: 2ba185326fadb0d1ccf7d2767435204dd3cd857400d18e59eb8a07055ac0183c6e780d0e8e45436410c551aef516ecea7491a5c87b59406252b2be4694034af8
languageName: node
linkType: hard
@@ -8665,11 +8658,11 @@ __metadata:
"@rocket.chat/favicon": "workspace:^"
"@rocket.chat/forked-matrix-appservice-bridge": ^4.0.1
"@rocket.chat/forked-matrix-bot-sdk": ^0.6.0-beta.2
- "@rocket.chat/fuselage": ^0.34.0
+ "@rocket.chat/fuselage": ^0.35.0
"@rocket.chat/fuselage-hooks": ^0.32.1
"@rocket.chat/fuselage-polyfills": next
"@rocket.chat/fuselage-toastbar": next
- "@rocket.chat/fuselage-tokens": next
+ "@rocket.chat/fuselage-tokens": ^0.32.0
"@rocket.chat/fuselage-ui-kit": "workspace:^"
"@rocket.chat/gazzodown": "workspace:^"
"@rocket.chat/i18n": "workspace:^"
@@ -8681,7 +8674,7 @@ __metadata:
"@rocket.chat/livechat": "workspace:^"
"@rocket.chat/log-format": "workspace:^"
"@rocket.chat/logger": "workspace:^"
- "@rocket.chat/logo": ^0.31.27
+ "@rocket.chat/logo": ^0.31.28
"@rocket.chat/memo": next
"@rocket.chat/message-parser": next
"@rocket.chat/mock-providers": "workspace:^"
@@ -9175,7 +9168,7 @@ __metadata:
dependencies:
"@react-pdf/renderer": ^3.1.12
"@rocket.chat/core-typings": "workspace:^"
- "@rocket.chat/fuselage-tokens": next
+ "@rocket.chat/fuselage-tokens": ^0.32.0
"@storybook/addon-essentials": ~6.5.16
"@storybook/react": ~6.5.16
"@testing-library/jest-dom": ^5.16.5
@@ -9528,7 +9521,7 @@ __metadata:
dependencies:
"@babel/core": ~7.22.9
"@rocket.chat/css-in-js": next
- "@rocket.chat/fuselage": ^0.34.0
+ "@rocket.chat/fuselage": ^0.35.0
"@rocket.chat/fuselage-hooks": ^0.32.1
"@rocket.chat/icons": ^0.32.0
"@rocket.chat/mock-providers": "workspace:^"
@@ -9579,7 +9572,7 @@ __metadata:
dependencies:
"@babel/core": ~7.22.9
"@rocket.chat/eslint-config": "workspace:^"
- "@rocket.chat/fuselage": ^0.34.0
+ "@rocket.chat/fuselage": ^0.35.0
"@rocket.chat/icons": ^0.32.0
"@storybook/addon-actions": ~6.5.16
"@storybook/addon-docs": ~6.5.16
@@ -9650,7 +9643,7 @@ __metadata:
resolution: "@rocket.chat/ui-theming@workspace:ee/packages/ui-theming"
dependencies:
"@rocket.chat/css-in-js": next
- "@rocket.chat/fuselage": ^0.34.0
+ "@rocket.chat/fuselage": ^0.35.0
"@rocket.chat/fuselage-hooks": ^0.32.1
"@rocket.chat/icons": ^0.32.0
"@rocket.chat/ui-contexts": "workspace:~"
@@ -9693,7 +9686,7 @@ __metadata:
"@rocket.chat/css-in-js": next
"@rocket.chat/emitter": next
"@rocket.chat/eslint-config": "workspace:^"
- "@rocket.chat/fuselage": ^0.34.0
+ "@rocket.chat/fuselage": ^0.35.0
"@rocket.chat/fuselage-hooks": ^0.32.1
"@rocket.chat/icons": ^0.32.0
"@rocket.chat/styled": next
@@ -9736,13 +9729,13 @@ __metadata:
"@codemirror/tooltip": ^0.19.16
"@lezer/highlight": ^1.1.6
"@rocket.chat/css-in-js": next
- "@rocket.chat/fuselage": ^0.34.0
+ "@rocket.chat/fuselage": ^0.35.0
"@rocket.chat/fuselage-hooks": ^0.32.1
"@rocket.chat/fuselage-polyfills": next
- "@rocket.chat/fuselage-tokens": next
+ "@rocket.chat/fuselage-tokens": ^0.32.0
"@rocket.chat/fuselage-ui-kit": "workspace:~"
"@rocket.chat/icons": ^0.32.0
- "@rocket.chat/logo": ^0.31.27
+ "@rocket.chat/logo": ^0.31.28
"@rocket.chat/styled": next
"@rocket.chat/ui-contexts": "workspace:~"
"@types/react": ~17.0.62
From febc7165dc62a37599acde295cd1e61d8f9c8654 Mon Sep 17 00:00:00 2001
From: Guilherme Gazzo
Date: Fri, 20 Oct 2023 05:59:05 -0700
Subject: [PATCH 09/22] test: License v3 - add test cases for empty limitations
(#30695)
---
ee/packages/license/src/license.spec.ts | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/ee/packages/license/src/license.spec.ts b/ee/packages/license/src/license.spec.ts
index c605d6467118..73cf13d75035 100644
--- a/ee/packages/license/src/license.spec.ts
+++ b/ee/packages/license/src/license.spec.ts
@@ -22,6 +22,19 @@ it('should not prevent if the counter is under the limit', async () => {
await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(false);
});
+it('should not prevent actions if there is no limit set in the license', async () => {
+ const licenseManager = await getReadyLicenseManager();
+
+ const license = await new MockedLicenseBuilder();
+
+ await expect(licenseManager.setLicense(await license.sign())).resolves.toBe(true);
+
+ licenseManager.setLicenseLimitCounter('activeUsers', () => 5);
+ licenseManager.setLicenseLimitCounter('monthlyActiveContacts', () => 5);
+ await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(false);
+ await expect(licenseManager.shouldPreventAction('monthlyActiveContacts')).resolves.toBe(false);
+});
+
it('should prevent if the counter is equal or over the limit', async () => {
const licenseManager = await getReadyLicenseManager();
From 832df7f4cd1fd67e5552cf38af272d27c99820dc Mon Sep 17 00:00:00 2001
From: Guilherme Gazzo
Date: Fri, 20 Oct 2023 06:22:34 -0700
Subject: [PATCH 10/22] feat: License v3 store prevent action results and just
fire it if changes (#30692)
---
ee/packages/license/__tests__/emitter.spec.ts | 176 ++++++++++++++++++
.../license/src/definition/LicenseBehavior.ts | 8 +-
.../license/src/definition/LicenseInfo.ts | 1 +
ee/packages/license/src/definition/events.ts | 6 +
ee/packages/license/src/events/emitter.ts | 15 +-
ee/packages/license/src/events/listeners.ts | 8 +
ee/packages/license/src/index.ts | 3 +
ee/packages/license/src/license.ts | 38 +++-
8 files changed, 249 insertions(+), 6 deletions(-)
diff --git a/ee/packages/license/__tests__/emitter.spec.ts b/ee/packages/license/__tests__/emitter.spec.ts
index 6147d12623bc..5682715d6d2b 100644
--- a/ee/packages/license/__tests__/emitter.spec.ts
+++ b/ee/packages/license/__tests__/emitter.spec.ts
@@ -116,4 +116,180 @@ describe('Event License behaviors', () => {
await expect(fn).toBeCalledWith(undefined);
});
});
+
+ /**
+ * this is only called when the prevent_action behavior is triggered for the first time
+ * it will not be called again until the behavior is toggled
+ */
+ describe('Toggled behaviors', () => {
+ it('should emit `behaviorToggled:prevent_action` event when the limit is reached once but `behavior:prevent_action` twice', async () => {
+ const licenseManager = await getReadyLicenseManager();
+ const fn = jest.fn();
+ const toggleFn = jest.fn();
+
+ licenseManager.onBehaviorTriggered('prevent_action', fn);
+
+ licenseManager.onBehaviorToggled('prevent_action', toggleFn);
+
+ const license = await new MockedLicenseBuilder().withLimits('activeUsers', [
+ {
+ max: 10,
+ behavior: 'prevent_action',
+ },
+ ]);
+
+ await expect(licenseManager.setLicense(await license.sign())).resolves.toBe(true);
+
+ licenseManager.setLicenseLimitCounter('activeUsers', () => 10);
+
+ await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(true);
+ await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(true);
+
+ await expect(fn).toBeCalledTimes(2);
+ await expect(toggleFn).toBeCalledTimes(1);
+
+ await expect(fn).toBeCalledWith({
+ reason: 'limit',
+ limit: 'activeUsers',
+ });
+ });
+
+ it('should emit `behaviorToggled:allow_action` event when the limit is not reached once but `behavior:allow_action` twice', async () => {
+ const licenseManager = await getReadyLicenseManager();
+ const fn = jest.fn();
+ const toggleFn = jest.fn();
+
+ licenseManager.onBehaviorTriggered('allow_action', fn);
+
+ licenseManager.onBehaviorToggled('allow_action', toggleFn);
+
+ const license = await new MockedLicenseBuilder().withLimits('activeUsers', [
+ {
+ max: 10,
+ behavior: 'prevent_action',
+ },
+ ]);
+
+ await expect(licenseManager.setLicense(await license.sign())).resolves.toBe(true);
+
+ licenseManager.setLicenseLimitCounter('activeUsers', () => 9);
+
+ await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(false);
+ await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(false);
+
+ await expect(fn).toBeCalledTimes(2);
+ await expect(toggleFn).toBeCalledTimes(1);
+
+ await expect(fn).toBeCalledWith({
+ reason: 'limit',
+ limit: 'activeUsers',
+ });
+ });
+
+ it('should emit `behaviorToggled:prevent_action` and `behaviorToggled:allow_action` events when the shouldPreventAction function changes the result', async () => {
+ const licenseManager = await getReadyLicenseManager();
+ const preventFn = jest.fn();
+ const preventToggleFn = jest.fn();
+ const allowFn = jest.fn();
+ const allowToggleFn = jest.fn();
+
+ licenseManager.onBehaviorTriggered('prevent_action', preventFn);
+ licenseManager.onBehaviorToggled('prevent_action', preventToggleFn);
+ licenseManager.onBehaviorTriggered('allow_action', allowFn);
+ licenseManager.onBehaviorToggled('allow_action', allowToggleFn);
+
+ const license = await new MockedLicenseBuilder().withLimits('activeUsers', [
+ {
+ max: 10,
+ behavior: 'prevent_action',
+ },
+ ]);
+
+ await expect(licenseManager.setLicense(await license.sign())).resolves.toBe(true);
+
+ licenseManager.setLicenseLimitCounter('activeUsers', () => 5);
+
+ await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(false);
+ expect(preventFn).toBeCalledTimes(0);
+ expect(preventToggleFn).toBeCalledTimes(0);
+ expect(allowFn).toBeCalledTimes(1);
+ expect(allowToggleFn).toBeCalledTimes(1);
+
+ preventFn.mockClear();
+ preventToggleFn.mockClear();
+ allowFn.mockClear();
+ allowToggleFn.mockClear();
+ await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(false);
+ expect(preventFn).toBeCalledTimes(0);
+ expect(preventToggleFn).toBeCalledTimes(0);
+ expect(allowFn).toBeCalledTimes(1);
+ expect(allowToggleFn).toBeCalledTimes(0);
+
+ licenseManager.setLicenseLimitCounter('activeUsers', () => 10);
+
+ preventFn.mockClear();
+ preventToggleFn.mockClear();
+ allowFn.mockClear();
+ allowToggleFn.mockClear();
+ await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(true);
+ expect(preventFn).toBeCalledTimes(1);
+ expect(preventToggleFn).toBeCalledTimes(1);
+ expect(allowFn).toBeCalledTimes(0);
+ expect(allowToggleFn).toBeCalledTimes(0);
+
+ preventFn.mockClear();
+ preventToggleFn.mockClear();
+ allowFn.mockClear();
+ allowToggleFn.mockClear();
+ await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(true);
+ expect(preventFn).toBeCalledTimes(1);
+ expect(preventToggleFn).toBeCalledTimes(0);
+ expect(allowFn).toBeCalledTimes(0);
+ expect(allowToggleFn).toBeCalledTimes(0);
+
+ licenseManager.setLicenseLimitCounter('activeUsers', () => 5);
+
+ preventFn.mockClear();
+ preventToggleFn.mockClear();
+ allowFn.mockClear();
+ allowToggleFn.mockClear();
+ await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(false);
+ expect(preventFn).toBeCalledTimes(0);
+ expect(preventToggleFn).toBeCalledTimes(0);
+ expect(allowFn).toBeCalledTimes(1);
+ expect(allowToggleFn).toBeCalledTimes(1);
+ });
+ });
+
+ describe('Allow actions', () => {
+ it('should emit `behavior:allow_action` event when the limit is not reached', async () => {
+ const licenseManager = await getReadyLicenseManager();
+ const fn = jest.fn();
+ const preventFn = jest.fn();
+
+ licenseManager.onBehaviorTriggered('allow_action', fn);
+ licenseManager.onBehaviorTriggered('prevent_action', preventFn);
+
+ const license = await new MockedLicenseBuilder().withLimits('activeUsers', [
+ {
+ max: 10,
+ behavior: 'prevent_action',
+ },
+ ]);
+
+ await expect(licenseManager.setLicense(await license.sign())).resolves.toBe(true);
+
+ licenseManager.setLicenseLimitCounter('activeUsers', () => 9);
+
+ await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(false);
+
+ await expect(fn).toBeCalledTimes(1);
+ await expect(preventFn).toBeCalledTimes(0);
+
+ await expect(fn).toBeCalledWith({
+ reason: 'limit',
+ limit: 'activeUsers',
+ });
+ });
+ });
});
diff --git a/ee/packages/license/src/definition/LicenseBehavior.ts b/ee/packages/license/src/definition/LicenseBehavior.ts
index 8b5af5f3c481..ac2249233ab5 100644
--- a/ee/packages/license/src/definition/LicenseBehavior.ts
+++ b/ee/packages/license/src/definition/LicenseBehavior.ts
@@ -1,7 +1,13 @@
import type { LicenseLimitKind } from './ILicenseV3';
import type { LicenseModule } from './LicenseModule';
-export type LicenseBehavior = 'invalidate_license' | 'start_fair_policy' | 'prevent_action' | 'prevent_installation' | 'disable_modules';
+export type LicenseBehavior =
+ | 'invalidate_license'
+ | 'start_fair_policy'
+ | 'prevent_action'
+ | 'allow_action'
+ | 'prevent_installation'
+ | 'disable_modules';
export type BehaviorWithContext =
| {
diff --git a/ee/packages/license/src/definition/LicenseInfo.ts b/ee/packages/license/src/definition/LicenseInfo.ts
index 4c4e34d30528..019d1b9e1ca0 100644
--- a/ee/packages/license/src/definition/LicenseInfo.ts
+++ b/ee/packages/license/src/definition/LicenseInfo.ts
@@ -5,6 +5,7 @@ import type { LicenseModule } from './LicenseModule';
export type LicenseInfo = {
license?: ILicenseV3;
activeModules: LicenseModule[];
+ preventedActions: Record;
limits: Record;
tags: ILicenseTag[];
trial: boolean;
diff --git a/ee/packages/license/src/definition/events.ts b/ee/packages/license/src/definition/events.ts
index 53f3afe846db..ad1114738cce 100644
--- a/ee/packages/license/src/definition/events.ts
+++ b/ee/packages/license/src/definition/events.ts
@@ -4,9 +4,15 @@ import type { LicenseModule } from './LicenseModule';
type ModuleValidation = Record<`${'invalid' | 'valid'}:${LicenseModule}`, undefined>;
type BehaviorTriggered = Record<`behavior:${LicenseBehavior}`, { reason: BehaviorWithContext['reason']; limit?: LicenseLimitKind }>;
+type BehaviorTriggeredToggled = Record<
+ `behaviorToggled:${LicenseBehavior}`,
+ { reason: BehaviorWithContext['reason']; limit?: LicenseLimitKind }
+>;
+
type LimitReached = Record<`limitReached:${LicenseLimitKind}`, undefined>;
export type LicenseEvents = ModuleValidation &
+ BehaviorTriggeredToggled &
BehaviorTriggered &
LimitReached & {
validate: undefined;
diff --git a/ee/packages/license/src/events/emitter.ts b/ee/packages/license/src/events/emitter.ts
index 9256bcafe5f7..51f3282a9742 100644
--- a/ee/packages/license/src/events/emitter.ts
+++ b/ee/packages/license/src/events/emitter.ts
@@ -33,7 +33,7 @@ export function behaviorTriggered(this: LicenseManager, options: BehaviorWithCon
logger.error({ msg: 'Error running behavior triggered event', error });
}
- if (behavior !== 'prevent_action') {
+ if (!['prevent_action'].includes(behavior)) {
return;
}
@@ -48,6 +48,19 @@ export function behaviorTriggered(this: LicenseManager, options: BehaviorWithCon
}
}
+export function behaviorTriggeredToggled(this: LicenseManager, options: BehaviorWithContext) {
+ const { behavior, reason, modules: _, ...rest } = options;
+
+ try {
+ this.emit(`behaviorToggled:${behavior}`, {
+ reason,
+ ...rest,
+ });
+ } catch (error) {
+ logger.error({ msg: 'Error running behavior triggered event', error });
+ }
+}
+
export function licenseValidated(this: LicenseManager) {
try {
this.emit('validate');
diff --git a/ee/packages/license/src/events/listeners.ts b/ee/packages/license/src/events/listeners.ts
index ecabecb28c0f..6c80867b7ac8 100644
--- a/ee/packages/license/src/events/listeners.ts
+++ b/ee/packages/license/src/events/listeners.ts
@@ -79,6 +79,14 @@ export function onBehaviorTriggered(
this.on(`behavior:${behavior}`, cb);
}
+export function onBehaviorToggled(
+ this: LicenseManager,
+ behavior: Exclude,
+ cb: (data: { reason: BehaviorWithContext['reason']; limit?: LicenseLimitKind }) => void,
+) {
+ this.on(`behaviorToggled:${behavior}`, cb);
+}
+
export function onLimitReached(this: LicenseManager, limitKind: LicenseLimitKind, cb: () => void) {
this.on(`limitReached:${limitKind}`, cb);
}
diff --git a/ee/packages/license/src/index.ts b/ee/packages/license/src/index.ts
index 9707a41d96ab..92b30c4af40d 100644
--- a/ee/packages/license/src/index.ts
+++ b/ee/packages/license/src/index.ts
@@ -4,6 +4,7 @@ import type { LimitContext } from './definition/LimitContext';
import { getAppsConfig, getMaxActiveUsers, getUnmodifiedLicenseAndModules } from './deprecated';
import { onLicense } from './events/deprecated';
import {
+ onBehaviorToggled,
onBehaviorTriggered,
onInvalidFeature,
onInvalidateLicense,
@@ -97,6 +98,8 @@ export class LicenseImp extends LicenseManager implements License {
onBehaviorTriggered = onBehaviorTriggered;
+ onBehaviorToggled = onBehaviorToggled;
+
// Deprecated:
onLicense = onLicense;
diff --git a/ee/packages/license/src/license.ts b/ee/packages/license/src/license.ts
index 8212a4a0da27..5987065bd697 100644
--- a/ee/packages/license/src/license.ts
+++ b/ee/packages/license/src/license.ts
@@ -12,7 +12,7 @@ import type { LicenseEvents } from './definition/events';
import { DuplicatedLicenseError } from './errors/DuplicatedLicenseError';
import { InvalidLicenseError } from './errors/InvalidLicenseError';
import { NotReadyForValidation } from './errors/NotReadyForValidation';
-import { behaviorTriggered, licenseInvalidated, licenseValidated } from './events/emitter';
+import { behaviorTriggered, behaviorTriggeredToggled, licenseInvalidated, licenseValidated } from './events/emitter';
import { logger } from './logger';
import { getModules, invalidateAll, replaceModules } from './modules';
import { applyPendingLicense, clearPendingLicense, hasPendingLicense, isPendingLicense, setPendingLicense } from './pendingLicense';
@@ -49,6 +49,8 @@ export class LicenseManager extends Emitter {
private _lockedLicense: string | undefined;
+ public shouldPreventActionResults = new Map();
+
constructor() {
super();
@@ -106,6 +108,8 @@ export class LicenseManager extends Emitter {
this._unmodifiedLicense = undefined;
this._valid = false;
this._lockedLicense = undefined;
+
+ this.shouldPreventActionResults.clear();
clearPendingLicense.call(this);
}
@@ -243,6 +247,12 @@ export class LicenseManager extends Emitter {
}
}
+ private triggerBehaviorEventsToggled(validationResult: BehaviorWithContext[]): void {
+ for (const { ...options } of validationResult) {
+ behaviorTriggeredToggled.call(this, { ...options });
+ }
+ }
+
public hasValidLicense(): boolean {
return Boolean(this.getLicense());
}
@@ -279,18 +289,37 @@ export class LicenseManager extends Emitter {
const validationResult = await runValidation.call(this, license, options);
+ const shouldPreventAction = isBehaviorsInResult(validationResult, ['prevent_action']);
+
// extra values should not call events since they are not actually reaching the limit just checking if they would
if (extraCount) {
- return isBehaviorsInResult(validationResult, ['prevent_action']);
+ return shouldPreventAction;
}
if (isBehaviorsInResult(validationResult, ['invalidate_license', 'disable_modules', 'start_fair_policy'])) {
await this.revalidateLicense();
}
- this.triggerBehaviorEvents(filterBehaviorsResult(validationResult, ['prevent_action']));
+ const eventsToEmit = shouldPreventAction
+ ? filterBehaviorsResult(validationResult, ['prevent_action'])
+ : [
+ {
+ behavior: 'allow_action',
+ modules: [],
+ reason: 'limit',
+ limit: action,
+ } as BehaviorWithContext,
+ ];
+
+ if (this.shouldPreventActionResults.get(action) !== shouldPreventAction) {
+ this.shouldPreventActionResults.set(action, shouldPreventAction);
+
+ this.triggerBehaviorEventsToggled(eventsToEmit);
+ }
+
+ this.triggerBehaviorEvents(eventsToEmit);
- return isBehaviorsInResult(validationResult, ['prevent_action']);
+ return shouldPreventAction;
}
public async getInfo({
@@ -331,6 +360,7 @@ export class LicenseManager extends Emitter {
return {
license: (includeLicense && license) || undefined,
activeModules,
+ preventedActions: Object.fromEntries(this.shouldPreventActionResults.entries()) as Record,
limits: limits as Record,
tags: license?.information.tags || [],
trial: Boolean(license?.information.trial),
From 94c6e897264ba54ae52a0f187b6d6e05f643d977 Mon Sep 17 00:00:00 2001
From: Guilherme Gazzo
Date: Fri, 20 Oct 2023 06:31:56 -0700
Subject: [PATCH 11/22] chore: replace endpoint to licenses.info (#30697)
---
apps/meteor/client/hooks/useLicense.ts | 6 +-
.../hooks/useAdministrationItems.spec.tsx | 47 +++++++-------
.../client/views/admin/info/LicenseCard.tsx | 63 +++++++++++--------
.../client/views/hooks/useUpgradeTabParams.ts | 10 +--
apps/meteor/ee/server/api/licenses.ts | 12 ++--
.../tests/end-to-end/api/20-licenses.js | 12 ++--
packages/rest-typings/src/v1/licenses.ts | 2 +-
7 files changed, 80 insertions(+), 72 deletions(-)
diff --git a/apps/meteor/client/hooks/useLicense.ts b/apps/meteor/client/hooks/useLicense.ts
index 0f568d9bd5cc..1549b431eeb7 100644
--- a/apps/meteor/client/hooks/useLicense.ts
+++ b/apps/meteor/client/hooks/useLicense.ts
@@ -3,8 +3,8 @@ import { useEndpoint, usePermission } from '@rocket.chat/ui-contexts';
import type { UseQueryResult } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
-export const useLicense = (): UseQueryResult> => {
- const getLicenses = useEndpoint('GET', '/v1/licenses.get');
+export const useLicense = (): UseQueryResult> => {
+ const getLicenses = useEndpoint('GET', '/v1/licenses.info');
const canViewLicense = usePermission('view-privileged-setting');
return useQuery(
@@ -13,7 +13,7 @@ export const useLicense = (): UseQueryResult {
const { result, waitFor } = renderHook(() => useAdministrationItems(), {
wrapper: mockAppRoot()
- .withEndpoint('GET', '/v1/licenses.get', () => ({
- licenses: [
- {
- modules: ['testModule'],
- meta: { trial: false },
- } as any,
- ],
+ .withEndpoint('GET', '/v1/licenses.info', () => ({
+ license: {
+ // @ts-expect-error this is a mock
+ license: { activeModules: ['testModule'] },
+ trial: false,
+ },
}))
.withEndpoint('GET', '/v1/cloud.registrationStatus', () => ({
registrationStatus: {
@@ -25,19 +24,21 @@ it('should not show upgrade item if has license and not have trial', async () =>
});
await waitFor(() => !!(result.all.length > 1));
-
expect(result.current.length).toEqual(1);
+
+ expect(result.current[0]).toEqual(
+ expect.objectContaining({
+ id: 'workspace',
+ }),
+ );
});
it('should return an upgrade item if not have license or if have a trial', async () => {
const { result, waitFor } = renderHook(() => useAdministrationItems(), {
wrapper: mockAppRoot()
- .withEndpoint('GET', '/v1/licenses.get', () => ({
- licenses: [
- {
- modules: [],
- } as any,
- ],
+ .withEndpoint('GET', '/v1/licenses.info', () => ({
+ // @ts-expect-error this is a mock
+ license: {},
}))
.withEndpoint('GET', '/v1/cloud.registrationStatus', () => ({
registrationStatus: {
@@ -62,12 +63,9 @@ it('should return an upgrade item if not have license or if have a trial', async
it('should return omnichannel item if has `view-livechat-manager` permission ', async () => {
const { result, waitFor } = renderHook(() => useAdministrationItems(), {
wrapper: mockAppRoot()
- .withEndpoint('GET', '/v1/licenses.get', () => ({
- licenses: [
- {
- modules: [],
- } as any,
- ],
+ .withEndpoint('GET', '/v1/licenses.info', () => ({
+ // @ts-expect-error this is a mock
+ license: {},
}))
.withEndpoint('GET', '/v1/cloud.registrationStatus', () => ({
registrationStatus: {
@@ -90,12 +88,9 @@ it('should return omnichannel item if has `view-livechat-manager` permission ',
it('should show administration item if has at least one admin permission', async () => {
const { result, waitFor } = renderHook(() => useAdministrationItems(), {
wrapper: mockAppRoot()
- .withEndpoint('GET', '/v1/licenses.get', () => ({
- licenses: [
- {
- modules: [],
- } as any,
- ],
+ .withEndpoint('GET', '/v1/licenses.info', () => ({
+ // @ts-expect-error this is a mock
+ license: {},
}))
.withEndpoint('GET', '/v1/cloud.registrationStatus', () => ({
registrationStatus: {
diff --git a/apps/meteor/client/views/admin/info/LicenseCard.tsx b/apps/meteor/client/views/admin/info/LicenseCard.tsx
index bccbddaa6db7..8aab636f4720 100644
--- a/apps/meteor/client/views/admin/info/LicenseCard.tsx
+++ b/apps/meteor/client/views/admin/info/LicenseCard.tsx
@@ -19,15 +19,7 @@ const LicenseCard = (): ReactElement => {
const isAirGapped = true;
- const { data, isError, isLoading } = useLicense();
-
- const { modules = [] } = isLoading || isError || !data?.licenses?.length ? {} : data?.licenses[0];
-
- const hasEngagement = modules.includes('engagement-dashboard');
- const hasOmnichannel = modules.includes('livechat-enterprise');
- const hasAuditing = modules.includes('auditing');
- const hasCannedResponses = modules.includes('canned-responses');
- const hasReadReceipts = modules.includes('message-read-receipt');
+ const request = useLicense();
const handleApplyLicense = useMutableCallback(() =>
setModal(
@@ -41,6 +33,37 @@ const LicenseCard = (): ReactElement => {
),
);
+ if (request.isLoading || request.isError) {
+ return (
+
+ {t('License')}
+
+
+
+
+
+
+ {t('Features')}
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ const { activeModules } = request.data.license;
+
+ const hasEngagement = activeModules.includes('engagement-dashboard');
+ const hasOmnichannel = activeModules.includes('livechat-enterprise');
+ const hasAuditing = activeModules.includes('auditing');
+ const hasCannedResponses = activeModules.includes('canned-responses');
+ const hasReadReceipts = activeModules.includes('message-read-receipt');
+
return (
{t('License')}
@@ -51,22 +74,12 @@ const LicenseCard = (): ReactElement => {
{t('Features')}
- {isLoading ? (
- <>
-
-
-
-
- >
- ) : (
- <>
-
-
-
-
-
- >
- )}
+
+
+
+
+
+
diff --git a/apps/meteor/client/views/hooks/useUpgradeTabParams.ts b/apps/meteor/client/views/hooks/useUpgradeTabParams.ts
index 65dd4cb1e396..1d152b08d5b9 100644
--- a/apps/meteor/client/views/hooks/useUpgradeTabParams.ts
+++ b/apps/meteor/client/views/hooks/useUpgradeTabParams.ts
@@ -1,4 +1,3 @@
-import type { ILicenseV2, ILicenseV3 } from '@rocket.chat/license';
import { useSetting } from '@rocket.chat/ui-contexts';
import { format } from 'date-fns';
@@ -14,14 +13,11 @@ export const useUpgradeTabParams = (): { tabType: UpgradeTabVariant | false; tri
const { data: registrationStatusData, isSuccess: isSuccessRegistrationStatus } = useRegistrationStatus();
const registered = registrationStatusData?.registrationStatus?.workspaceRegistered ?? false;
- const hasValidLicense = licensesData?.licenses.some((license) => license.modules.length > 0) ?? false;
+ const hasValidLicense = Boolean(licensesData?.license?.license ?? false);
const hadExpiredTrials = cloudWorkspaceHadTrial ?? false;
- const licenses = (licensesData?.licenses || []) as (Partial & { modules: string[] })[];
-
- const trialLicense = licenses.find(({ meta, information }) => information?.trial ?? meta?.trial);
- const isTrial = Boolean(trialLicense);
- const trialEndDateStr = trialLicense?.information?.visualExpiration || trialLicense?.meta?.trialEnd || trialLicense?.cloudMeta?.trialEnd;
+ const isTrial = Boolean(licensesData?.license?.trial);
+ const trialEndDateStr = licensesData?.license?.license?.information?.visualExpiration;
const trialEndDate = trialEndDateStr ? format(new Date(trialEndDateStr), 'yyyy-MM-dd') : undefined;
const upgradeTabType = getUpgradeTabType({
diff --git a/apps/meteor/ee/server/api/licenses.ts b/apps/meteor/ee/server/api/licenses.ts
index b7ac3ba81e9c..a2e7a75b072a 100644
--- a/apps/meteor/ee/server/api/licenses.ts
+++ b/apps/meteor/ee/server/api/licenses.ts
@@ -5,12 +5,15 @@ import { check } from 'meteor/check';
import { API } from '../../../app/api/server/api';
import { hasPermissionAsync } from '../../../app/authorization/server/functions/hasPermission';
+import { apiDeprecationLogger } from '../../../app/lib/server/lib/deprecationWarningLogger';
API.v1.addRoute(
'licenses.get',
{ authRequired: true },
{
async get() {
+ apiDeprecationLogger.endpoint(this.request.route, '7.0.0', this.response, ' Use licenses.info instead.');
+
if (!(await hasPermissionAsync(this.userId, 'view-privileged-setting'))) {
return API.v1.unauthorized();
}
@@ -31,9 +34,9 @@ API.v1.addRoute(
const unrestrictedAccess = await hasPermissionAsync(this.userId, 'view-privileged-setting');
const loadCurrentValues = unrestrictedAccess && Boolean(this.queryParams.loadValues);
- const data = await License.getInfo({ limits: unrestrictedAccess, license: unrestrictedAccess, currentValues: loadCurrentValues });
+ const license = await License.getInfo({ limits: unrestrictedAccess, license: unrestrictedAccess, currentValues: loadCurrentValues });
- return API.v1.success({ data });
+ return API.v1.success({ license });
},
},
);
@@ -81,8 +84,9 @@ API.v1.addRoute(
{ authOrAnonRequired: true },
{
get() {
- const isEnterpriseEdtion = License.hasValidLicense();
- return API.v1.success({ isEnterprise: isEnterpriseEdtion });
+ apiDeprecationLogger.endpoint(this.request.route, '7.0.0', this.response, ' Use licenses.info instead.');
+ const isEnterpriseEdition = License.hasValidLicense();
+ return API.v1.success({ isEnterprise: isEnterpriseEdition });
},
},
);
diff --git a/apps/meteor/tests/end-to-end/api/20-licenses.js b/apps/meteor/tests/end-to-end/api/20-licenses.js
index 302011addef9..9088e4e9e1d9 100644
--- a/apps/meteor/tests/end-to-end/api/20-licenses.js
+++ b/apps/meteor/tests/end-to-end/api/20-licenses.js
@@ -126,9 +126,9 @@ describe('licenses', function () {
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
- expect(res.body).to.have.property('data').and.to.be.an('object');
- expect(res.body.data).to.not.have.property('license');
- expect(res.body.data).to.have.property('tags').and.to.be.an('array');
+ expect(res.body).to.have.property('license').and.to.be.an('object');
+ expect(res.body.license).to.not.have.property('license');
+ expect(res.body.license).to.have.property('tags').and.to.be.an('array');
})
.end(done);
});
@@ -140,11 +140,11 @@ describe('licenses', function () {
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
- expect(res.body).to.have.property('data').and.to.be.an('object');
+ expect(res.body).to.have.property('license').and.to.be.an('object');
if (process.env.IS_EE) {
- expect(res.body.data).to.have.property('license').and.to.be.an('object');
+ expect(res.body.license).to.have.property('license').and.to.be.an('object');
}
- expect(res.body.data).to.have.property('tags').and.to.be.an('array');
+ expect(res.body.license).to.have.property('tags').and.to.be.an('array');
})
.end(done);
diff --git a/packages/rest-typings/src/v1/licenses.ts b/packages/rest-typings/src/v1/licenses.ts
index d229ca49f1fc..4eb1ac196840 100644
--- a/packages/rest-typings/src/v1/licenses.ts
+++ b/packages/rest-typings/src/v1/licenses.ts
@@ -45,7 +45,7 @@ export type LicensesEndpoints = {
};
'/v1/licenses.info': {
GET: (params: licensesInfoProps) => {
- data: LicenseInfo;
+ license: LicenseInfo;
};
};
'/v1/licenses.add': {
From daa28f02a404599243a734c7cf8a1d2a0ff6c96b Mon Sep 17 00:00:00 2001
From: Diego Sampaio
Date: Fri, 20 Oct 2023 14:31:54 -0300
Subject: [PATCH 12/22] test: fix API test flakiness (#30657)
---
.../rocketchat-mongo-config/server/index.js | 19 +++++++++----------
.../server/lib/dataExport/uploadZipFile.ts | 6 ++----
2 files changed, 11 insertions(+), 14 deletions(-)
diff --git a/apps/meteor/packages/rocketchat-mongo-config/server/index.js b/apps/meteor/packages/rocketchat-mongo-config/server/index.js
index 80e664a9c209..65464a31095c 100644
--- a/apps/meteor/packages/rocketchat-mongo-config/server/index.js
+++ b/apps/meteor/packages/rocketchat-mongo-config/server/index.js
@@ -34,20 +34,19 @@ if (Object.keys(mongoConnectionOptions).length > 0) {
process.env.HTTP_FORWARDED_COUNT = process.env.HTTP_FORWARDED_COUNT || '1';
-// Send emails to a "fake" stream instead of print them in console in case MAIL_URL or SMTP is not configured
-if (process.env.NODE_ENV !== 'development') {
- const { sendAsync } = Email;
+// Just print to logs if in TEST_MODE due to a bug in Meteor 2.5: TypeError: Cannot read property '_syncSendMail' of null
+if (process.env.TEST_MODE === 'true') {
+ Email.sendAsync = function _sendAsync(options) {
+ console.log('Email.sendAsync', options);
+ };
+} else if (process.env.NODE_ENV !== 'development') {
+ // Send emails to a "fake" stream instead of print them in console in case MAIL_URL or SMTP is not configured
const stream = new PassThrough();
stream.on('data', () => {});
stream.on('end', () => {});
- Email.sendAsync = function _sendAsync(options) {
- return sendAsync.call(this, { stream, ...options });
- };
-}
-// Just print to logs if in TEST_MODE due to a bug in Meteor 2.5: TypeError: Cannot read property '_syncSendMail' of null
-if (process.env.TEST_MODE === 'true') {
+ const { sendAsync } = Email;
Email.sendAsync = function _sendAsync(options) {
- console.log('Email.sendAsync', options);
+ return sendAsync.call(this, { stream, ...options });
};
}
diff --git a/apps/meteor/server/lib/dataExport/uploadZipFile.ts b/apps/meteor/server/lib/dataExport/uploadZipFile.ts
index e6a76472db7f..5fe9ea2d57dd 100644
--- a/apps/meteor/server/lib/dataExport/uploadZipFile.ts
+++ b/apps/meteor/server/lib/dataExport/uploadZipFile.ts
@@ -1,5 +1,5 @@
import { createReadStream } from 'fs';
-import { open, stat } from 'fs/promises';
+import { stat } from 'fs/promises';
import type { IUser } from '@rocket.chat/core-typings';
import { Users } from '@rocket.chat/models';
@@ -28,9 +28,7 @@ export const uploadZipFile = async (filePath: string, userId: IUser['_id'], expo
name: newFileName,
};
- const { fd } = await open(filePath);
-
- const stream = createReadStream('', { fd }); // @todo once upgrades to Node.js v16.x, use createReadStream from fs.promises.open
+ const stream = createReadStream(filePath);
const userDataStore = FileUpload.getStore('UserDataFiles');
From 4fb5523406fec1dcb4509bbb7a115d49c0ca6cc4 Mon Sep 17 00:00:00 2001
From: Guilherme Gazzo
Date: Fri, 20 Oct 2023 11:19:07 -0700
Subject: [PATCH 13/22] feat: License v3 sync event (#30703)
---
apps/meteor/client/hooks/useLicense.ts | 20 +++++-
.../license/server/license.internalService.ts | 4 ++
apps/meteor/ee/app/license/server/startup.ts | 22 +++++++
.../modules/listeners/listeners.module.ts | 3 +
ee/packages/ddp-client/src/types/streams.ts | 2 +
ee/packages/license/__tests__/emitter.spec.ts | 64 +++++++++++++++++++
.../definition/LicenseValidationOptions.ts | 1 +
ee/packages/license/src/definition/events.ts | 1 +
ee/packages/license/src/events/listeners.ts | 7 ++
ee/packages/license/src/index.ts | 3 +
ee/packages/license/src/license.ts | 54 +++++++++++++++-
packages/core-services/src/Events.ts | 4 ++
12 files changed, 180 insertions(+), 5 deletions(-)
diff --git a/apps/meteor/client/hooks/useLicense.ts b/apps/meteor/client/hooks/useLicense.ts
index 1549b431eeb7..8ba594c5b5d3 100644
--- a/apps/meteor/client/hooks/useLicense.ts
+++ b/apps/meteor/client/hooks/useLicense.ts
@@ -1,12 +1,28 @@
+import { useDebouncedCallback } from '@rocket.chat/fuselage-hooks';
import type { OperationResult } from '@rocket.chat/rest-typings';
-import { useEndpoint, usePermission } from '@rocket.chat/ui-contexts';
+import { useEndpoint, usePermission, useSingleStream } from '@rocket.chat/ui-contexts';
import type { UseQueryResult } from '@tanstack/react-query';
-import { useQuery } from '@tanstack/react-query';
+import { useQuery, useQueryClient } from '@tanstack/react-query';
+import { useEffect } from 'react';
export const useLicense = (): UseQueryResult> => {
const getLicenses = useEndpoint('GET', '/v1/licenses.info');
const canViewLicense = usePermission('view-privileged-setting');
+ const queryClient = useQueryClient();
+
+ const invalidate = useDebouncedCallback(
+ () => {
+ queryClient.invalidateQueries(['licenses', 'getLicenses']);
+ },
+ 5000,
+ [],
+ );
+
+ const notify = useSingleStream('notify-all');
+
+ useEffect(() => notify('license', () => invalidate()), [notify, invalidate]);
+
return useQuery(
['licenses', 'getLicenses'],
() => {
diff --git a/apps/meteor/ee/app/license/server/license.internalService.ts b/apps/meteor/ee/app/license/server/license.internalService.ts
index 9036a9b1848c..6c179bf9797e 100644
--- a/apps/meteor/ee/app/license/server/license.internalService.ts
+++ b/apps/meteor/ee/app/license/server/license.internalService.ts
@@ -23,6 +23,10 @@ export class LicenseService extends ServiceClassInternal implements ILicense {
License.onModule((licenseModule) => {
void api.broadcast('license.module', licenseModule);
});
+
+ this.onEvent('license.actions', (preventedActions) => License.syncShouldPreventActionResults(preventedActions));
+
+ this.onEvent('license.sync', () => License.sync());
}
async started(): Promise {
diff --git a/apps/meteor/ee/app/license/server/startup.ts b/apps/meteor/ee/app/license/server/startup.ts
index 8cce4d3d1410..fc6b693e0441 100644
--- a/apps/meteor/ee/app/license/server/startup.ts
+++ b/apps/meteor/ee/app/license/server/startup.ts
@@ -1,3 +1,5 @@
+import { api } from '@rocket.chat/core-services';
+import type { LicenseLimitKind } from '@rocket.chat/license';
import { License } from '@rocket.chat/license';
import { Subscriptions, Users, Settings } from '@rocket.chat/models';
import { wrapExceptions } from '@rocket.chat/tools';
@@ -93,6 +95,26 @@ settings.onReady(async () => {
License.onBehaviorTriggered('start_fair_policy', async (context) => syncByTrigger(`start_fair_policy_${context.limit}`));
License.onBehaviorTriggered('disable_modules', async (context) => syncByTrigger(`disable_modules_${context.limit}`));
+
+ License.onChange(() => api.broadcast('license.sync'));
+
+ License.onBehaviorToggled('prevent_action', (context) => {
+ if (!context.limit) {
+ return;
+ }
+ void api.broadcast('license.actions', {
+ [context.limit]: true,
+ } as Record, boolean>);
+ });
+
+ License.onBehaviorToggled('allow_action', (context) => {
+ if (!context.limit) {
+ return;
+ }
+ void api.broadcast('license.actions', {
+ [context.limit]: false,
+ } as Record, boolean>);
+ });
});
License.setLicenseLimitCounter('activeUsers', () => Users.getActiveLocalUserCount());
diff --git a/apps/meteor/server/modules/listeners/listeners.module.ts b/apps/meteor/server/modules/listeners/listeners.module.ts
index f21081e43d0a..c580b47d7c6e 100644
--- a/apps/meteor/server/modules/listeners/listeners.module.ts
+++ b/apps/meteor/server/modules/listeners/listeners.module.ts
@@ -29,6 +29,9 @@ export class ListenersModule {
constructor(service: IServiceClass, notifications: NotificationsModule) {
const logger = new Logger('ListenersModule');
+ service.onEvent('license.sync', () => notifications.notifyAllInThisInstance('license'));
+ service.onEvent('license.actions', () => notifications.notifyAllInThisInstance('license'));
+
service.onEvent('emoji.deleteCustom', (emoji) => {
notifications.notifyLoggedInThisInstance('deleteEmojiCustom', {
emojiData: emoji,
diff --git a/ee/packages/ddp-client/src/types/streams.ts b/ee/packages/ddp-client/src/types/streams.ts
index 9010517faf7a..abadd53c1851 100644
--- a/ee/packages/ddp-client/src/types/streams.ts
+++ b/ee/packages/ddp-client/src/types/streams.ts
@@ -25,6 +25,7 @@ import type {
IBanner,
UiKit,
} from '@rocket.chat/core-typings';
+import type { LicenseLimitKind } from '@rocket.chat/license';
type ClientAction = 'inserted' | 'updated' | 'removed' | 'changed';
@@ -69,6 +70,7 @@ export interface StreamerEvents {
{ key: 'public-settings-changed'; args: ['inserted' | 'updated' | 'removed' | 'changed', ISetting] },
{ key: 'deleteCustomSound'; args: [{ soundData: ICustomSound }] },
{ key: 'updateCustomSound'; args: [{ soundData: ICustomSound }] },
+ { key: 'license'; args: [{ preventedActions: Record }] | [] },
];
'notify-user': [
diff --git a/ee/packages/license/__tests__/emitter.spec.ts b/ee/packages/license/__tests__/emitter.spec.ts
index 5682715d6d2b..ce949365e8a6 100644
--- a/ee/packages/license/__tests__/emitter.spec.ts
+++ b/ee/packages/license/__tests__/emitter.spec.ts
@@ -117,6 +117,70 @@ describe('Event License behaviors', () => {
});
});
+ /**
+ * This event is used to sync multiple instances of license manager
+ * The sync event is triggered when the license is changed, but if the validation is running due to a previous change, no sync should be triggered, avoiding multiple/loops syncs
+ */
+ describe('sync event', () => {
+ it('should emit `sync` event when the license is changed', async () => {
+ const licenseManager = await getReadyLicenseManager();
+ const fn = jest.fn();
+
+ licenseManager.onChange(fn);
+
+ const license = await new MockedLicenseBuilder().withLimits('activeUsers', [
+ {
+ max: 10,
+ behavior: 'prevent_action',
+ },
+ {
+ max: 20,
+ behavior: 'invalidate_license',
+ },
+ ]);
+
+ await expect(licenseManager.setLicense(await license.sign())).resolves.toBe(true);
+
+ licenseManager.setLicenseLimitCounter('activeUsers', () => 21);
+
+ await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(true);
+
+ await expect(fn).toBeCalledTimes(1);
+ });
+
+ it('should not emit `sync` event when the license validation was triggered by a the sync method', async () => {
+ const licenseManager = await getReadyLicenseManager();
+ const fn = jest.fn();
+
+ licenseManager.onChange(fn);
+
+ const license = await new MockedLicenseBuilder().withLimits('activeUsers', [
+ {
+ max: 10,
+ behavior: 'prevent_action',
+ },
+ {
+ max: 20,
+ behavior: 'invalidate_license',
+ },
+ ]);
+
+ await expect(licenseManager.setLicense(await license.sign())).resolves.toBe(true);
+
+ licenseManager.setLicenseLimitCounter('activeUsers', () => 21);
+
+ await expect(licenseManager.shouldPreventAction('activeUsers')).resolves.toBe(true);
+
+ await expect(fn).toBeCalledTimes(1);
+
+ fn.mockClear();
+
+ await expect(licenseManager.sync()).resolves.toBe(undefined);
+
+ await expect(fn).toBeCalledTimes(0);
+ });
+ });
+
/**
* this is only called when the prevent_action behavior is triggered for the first time
* it will not be called again until the behavior is toggled
diff --git a/ee/packages/license/src/definition/LicenseValidationOptions.ts b/ee/packages/license/src/definition/LicenseValidationOptions.ts
index 6aa1e4213c62..9357b021878b 100644
--- a/ee/packages/license/src/definition/LicenseValidationOptions.ts
+++ b/ee/packages/license/src/definition/LicenseValidationOptions.ts
@@ -8,4 +8,5 @@ export type LicenseValidationOptions = {
suppressLog?: boolean;
isNewLicense?: boolean;
context?: Partial<{ [K in LicenseLimitKind]: Partial> }>;
+ triggerSync?: boolean;
};
diff --git a/ee/packages/license/src/definition/events.ts b/ee/packages/license/src/definition/events.ts
index ad1114738cce..b9d211da9b7a 100644
--- a/ee/packages/license/src/definition/events.ts
+++ b/ee/packages/license/src/definition/events.ts
@@ -18,4 +18,5 @@ export type LicenseEvents = ModuleValidation &
validate: undefined;
invalidate: undefined;
module: { module: LicenseModule; valid: boolean };
+ sync: undefined;
};
diff --git a/ee/packages/license/src/events/listeners.ts b/ee/packages/license/src/events/listeners.ts
index 6c80867b7ac8..f8c291edc4be 100644
--- a/ee/packages/license/src/events/listeners.ts
+++ b/ee/packages/license/src/events/listeners.ts
@@ -4,6 +4,13 @@ import type { LicenseModule } from '../definition/LicenseModule';
import type { LicenseManager } from '../license';
import { hasModule } from '../modules';
+/**
+ * Invoked when the license changes some internal state. it's called to sync the license with other instances.
+ */
+export function onChange(this: LicenseManager, cb: () => void) {
+ this.on('sync', cb);
+}
+
export function onValidFeature(this: LicenseManager, feature: LicenseModule, cb: () => void) {
this.on(`valid:${feature}`, cb);
diff --git a/ee/packages/license/src/index.ts b/ee/packages/license/src/index.ts
index 92b30c4af40d..e590ce7722b2 100644
--- a/ee/packages/license/src/index.ts
+++ b/ee/packages/license/src/index.ts
@@ -10,6 +10,7 @@ import {
onInvalidateLicense,
onLimitReached,
onModule,
+ onChange,
onToggledFeature,
onValidFeature,
onValidateLicense,
@@ -82,6 +83,8 @@ export class LicenseImp extends LicenseManager implements License {
return this.shouldPreventAction(action, 0, context);
}
+ onChange = onChange;
+
onValidFeature = onValidFeature;
onInvalidFeature = onInvalidFeature;
diff --git a/ee/packages/license/src/license.ts b/ee/packages/license/src/license.ts
index 5987065bd697..e8be1df85eac 100644
--- a/ee/packages/license/src/license.ts
+++ b/ee/packages/license/src/license.ts
@@ -95,7 +95,27 @@ export class LicenseManager extends Emitter {
}
try {
- await this.validateLicense({ ...options, isNewLicense: false });
+ await this.validateLicense({ ...options, isNewLicense: false, triggerSync: true });
+ } catch (e) {
+ if (e instanceof InvalidLicenseError) {
+ this.invalidateLicense();
+ this.emit('sync');
+ }
+ }
+ }
+
+ /**
+ * The sync method should be called when a license from a different instance is has changed, so the local instance
+ * needs to be updated. This method will validate the license and update the local instance if the license is valid, but will not trigger the onSync event.
+ */
+
+ public async sync(options: Omit = {}): Promise {
+ if (!this.hasValidLicense()) {
+ return;
+ }
+
+ try {
+ await this.validateLicense({ ...options, isNewLicense: false, triggerSync: false });
} catch (e) {
if (e instanceof InvalidLicenseError) {
this.invalidateLicense();
@@ -152,7 +172,11 @@ export class LicenseManager extends Emitter {
return Boolean(this._lockedLicense && this._lockedLicense === encryptedLicense);
}
- private async validateLicense(options: LicenseValidationOptions = {}): Promise {
+ private async validateLicense(
+ options: LicenseValidationOptions = {
+ triggerSync: true,
+ },
+ ): Promise {
if (!this._license) {
throw new InvalidLicenseError();
}
@@ -195,6 +219,16 @@ export class LicenseManager extends Emitter {
}
licenseValidated.call(this);
+
+ // If something changed in the license and the sync option is enabled, trigger a sync
+ if (
+ ((!options.isNewLicense &&
+ filterBehaviorsResult(validationResult, ['invalidate_license', 'start_fair_policy', 'prevent_installation'])) ||
+ modulesChanged) &&
+ options.triggerSync
+ ) {
+ this.emit('sync');
+ }
}
public async setLicense(encryptedLicense: string, isNewLicense = true): Promise {
@@ -263,6 +297,20 @@ export class LicenseManager extends Emitter {
}
}
+ public syncShouldPreventActionResults(actions: Record): void {
+ for (const [action, shouldPreventAction] of Object.entries(actions)) {
+ this.shouldPreventActionResults.set(action as LicenseLimitKind, shouldPreventAction);
+ }
+ }
+
+ public get shouldPreventActionResultsMap(): {
+ [key in LicenseLimitKind]: boolean;
+ } {
+ return Object.fromEntries(this.shouldPreventActionResults.entries()) as {
+ [key in LicenseLimitKind]: boolean;
+ };
+ }
+
public async shouldPreventAction(
action: T,
extraCount = 0,
@@ -360,7 +408,7 @@ export class LicenseManager extends Emitter {
return {
license: (includeLicense && license) || undefined,
activeModules,
- preventedActions: Object.fromEntries(this.shouldPreventActionResults.entries()) as Record,
+ preventedActions: this.shouldPreventActionResultsMap,
limits: limits as Record,
tags: license?.information.tags || [],
trial: Boolean(license?.information.trial),
diff --git a/packages/core-services/src/Events.ts b/packages/core-services/src/Events.ts
index 2aa39588d2f0..c5f36d921655 100644
--- a/packages/core-services/src/Events.ts
+++ b/packages/core-services/src/Events.ts
@@ -34,6 +34,7 @@ import type {
ILivechatVisitor,
UiKit,
} from '@rocket.chat/core-typings';
+import type { LicenseLimitKind } from '@rocket.chat/license';
import type { AutoUpdateRecord } from './types/IMeteor';
@@ -55,6 +56,9 @@ export type EventSignatures = {
'emoji.deleteCustom'(emoji: IEmoji): void;
'emoji.updateCustom'(emoji: IEmoji): void;
'license.module'(data: { module: string; valid: boolean }): void;
+ 'license.sync'(): void;
+ 'license.actions'(actions: Record, boolean>): void;
+
'livechat-inquiry-queue-observer'(data: { action: string; inquiry: IInquiry }): void;
'message'(data: { action: string; message: IMessage }): void;
'meteor.clientVersionUpdated'(data: AutoUpdateRecord): void;
From bba3c9da6a2a0498bdc2d9c2c182fe6e287b3e9e Mon Sep 17 00:00:00 2001
From: Kevin Aleman
Date: Fri, 20 Oct 2023 13:16:02 -0600
Subject: [PATCH 14/22] fix: Omnichannel webhook is not retrying requests
(#30687)
---
.changeset/hip-pans-argue.md | 5 +++++
.../app/livechat/server/lib/LivechatTyped.ts | 20 ++++++++++++-------
2 files changed, 18 insertions(+), 7 deletions(-)
create mode 100644 .changeset/hip-pans-argue.md
diff --git a/.changeset/hip-pans-argue.md b/.changeset/hip-pans-argue.md
new file mode 100644
index 000000000000..af8050383467
--- /dev/null
+++ b/.changeset/hip-pans-argue.md
@@ -0,0 +1,5 @@
+---
+"@rocket.chat/meteor": patch
+---
+
+fix: Omnichannel webhook is not retrying requests
diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts
index 32cb5c83acd9..0d25616bda60 100644
--- a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts
+++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts
@@ -796,12 +796,15 @@ class LivechatClass {
attempts = 10,
) {
if (!attempts) {
+ Livechat.logger.error({ msg: 'Omnichannel webhook call failed. Max attempts reached' });
return;
}
const timeout = settings.get('Livechat_http_timeout');
const secretToken = settings.get('Livechat_secret_token');
+ const webhookUrl = settings.get('Livechat_webhookUrl');
try {
- const result = await fetch(settings.get('Livechat_webhookUrl'), {
+ Livechat.webhookLogger.debug({ msg: 'Sending webhook request', postData });
+ const result = await fetch(webhookUrl, {
method: 'POST',
headers: {
...(secretToken && { 'X-RocketChat-Livechat-Token': secretToken }),
@@ -812,17 +815,20 @@ class LivechatClass {
if (result.status === 200) {
metrics.totalLivechatWebhooksSuccess.inc();
- } else {
- metrics.totalLivechatWebhooksFailures.inc();
+ return result;
}
- return result;
+
+ metrics.totalLivechatWebhooksFailures.inc();
+ throw new Error(await result.text());
} catch (err) {
- Livechat.webhookLogger.error({ msg: `Response error on ${11 - attempts} try ->`, err });
+ const retryAfter = timeout * 4;
+ Livechat.webhookLogger.error({ msg: `Error response on ${11 - attempts} try ->`, err });
// try 10 times after 20 seconds each
- attempts - 1 && Livechat.webhookLogger.warn(`Will try again in ${(timeout / 1000) * 4} seconds ...`);
+ attempts - 1 &&
+ Livechat.webhookLogger.warn({ msg: `Webhook call failed. Retrying`, newAttemptAfterSeconds: retryAfter / 1000, webhookUrl });
setTimeout(async () => {
await Livechat.sendRequest(postData, attempts - 1);
- }, timeout * 4);
+ }, retryAfter);
}
}
From b98b99e7a29a2673bbdd9cbeb8f448e3c053072d Mon Sep 17 00:00:00 2001
From: Guilherme Gazzo
Date: Fri, 20 Oct 2023 21:04:37 -0300
Subject: [PATCH 15/22] feat: make sure every preventActions contains a value
(#30708)
---
ee/packages/license/src/license.ts | 44 ++++++++++++++++++++++++++----
1 file changed, 38 insertions(+), 6 deletions(-)
diff --git a/ee/packages/license/src/license.ts b/ee/packages/license/src/license.ts
index e8be1df85eac..fb290d541cfb 100644
--- a/ee/packages/license/src/license.ts
+++ b/ee/packages/license/src/license.ts
@@ -27,6 +27,7 @@ import { isBehaviorsInResult } from './validation/isBehaviorsInResult';
import { isReadyForValidation } from './validation/isReadyForValidation';
import { runValidation } from './validation/runValidation';
import { validateFormat } from './validation/validateFormat';
+import { validateLicenseLimits } from './validation/validateLicenseLimits';
const globalLimitKinds: LicenseLimitKind[] = ['activeUsers', 'guestUsers', 'privateApps', 'marketplaceApps', 'monthlyActiveContacts'];
@@ -303,12 +304,43 @@ export class LicenseManager extends Emitter {
}
}
- public get shouldPreventActionResultsMap(): {
+ public async shouldPreventActionResultsMap(): Promise<{
[key in LicenseLimitKind]: boolean;
- } {
- return Object.fromEntries(this.shouldPreventActionResults.entries()) as {
- [key in LicenseLimitKind]: boolean;
- };
+ }> {
+ const keys: LicenseLimitKind[] = [
+ 'activeUsers',
+ 'guestUsers',
+ 'roomsPerGuest',
+ 'privateApps',
+ 'marketplaceApps',
+ 'monthlyActiveContacts',
+ ];
+
+ const items = await Promise.all(
+ keys.map(async (limit) => {
+ const cached = this.shouldPreventActionResults.get(limit as LicenseLimitKind);
+
+ if (cached !== undefined) {
+ return [limit as LicenseLimitKind, cached];
+ }
+
+ const fresh = this._license
+ ? isBehaviorsInResult(
+ await validateLicenseLimits.call(this, this._license, {
+ behaviors: ['prevent_action'],
+ limits: [limit],
+ }),
+ ['prevent_action'],
+ )
+ : false;
+
+ this.shouldPreventActionResults.set(limit as LicenseLimitKind, fresh);
+
+ return [limit as LicenseLimitKind, fresh];
+ }),
+ );
+
+ return Object.fromEntries(items);
}
public async shouldPreventAction(
@@ -408,7 +440,7 @@ export class LicenseManager extends Emitter {
return {
license: (includeLicense && license) || undefined,
activeModules,
- preventedActions: this.shouldPreventActionResultsMap,
+ preventedActions: await this.shouldPreventActionResultsMap(),
limits: limits as Record,
tags: license?.information.tags || [],
trial: Boolean(license?.information.trial),
From 47303b5232393a9e8b0e5b6130f0746788955b12 Mon Sep 17 00:00:00 2001
From: Allan RIbeiro <35040806+AllanPazRibeiro@users.noreply.github.com>
Date: Mon, 23 Oct 2023 09:36:14 -0300
Subject: [PATCH 16/22] chore: just some portuguese translations (#30647)
---
.changeset/lazy-shoes-teach.md | 5 +++++
apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json | 8 +++++++-
2 files changed, 12 insertions(+), 1 deletion(-)
create mode 100644 .changeset/lazy-shoes-teach.md
diff --git a/.changeset/lazy-shoes-teach.md b/.changeset/lazy-shoes-teach.md
new file mode 100644
index 000000000000..7737f39cd671
--- /dev/null
+++ b/.changeset/lazy-shoes-teach.md
@@ -0,0 +1,5 @@
+---
+'@rocket.chat/meteor': minor
+---
+
+chore: adding some portugueses translations to the app details page
diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json
index 8da04e7c4e67..9812ddc46db3 100644
--- a/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json
+++ b/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json
@@ -2833,6 +2833,7 @@
"Mark_read": "Marcar como Lido",
"Mark_unread": "Marcar como não lido",
"Marketplace": "Marketplace",
+ "Marketplace_app_last_updated": "Ultima atualização {{lastUpdated}}",
"Marketplace_view_marketplace": "Ver Marketplace",
"Marketplace_error": "Não é possível conectar à internet, ou seu espaço de trabalho pode ser uma instalação offline.",
"MAU_value": "MAU {{value}}",
@@ -3522,6 +3523,7 @@
"Regular_Expressions": "Expressões regulares",
"Reject_call": "Rejeitar chamada",
"Release": "Versão",
+ "Releases": "Versões",
"Religious": "Religioso",
"Reload": "Recarregar",
"Reload_page": "Recarregar página",
@@ -3575,6 +3577,10 @@
"Request_comment_when_closing_conversation": "Solicitar comentário ao encerrar a conversa",
"Request_comment_when_closing_conversation_description": "Se ativado, o agente precisará informar um comentário antes que a conversa seja encerrada.",
"Request_tag_before_closing_chat": "Solicitar tag(s) antes de encerrar a conversa",
+ "request": "solicitar",
+ "requests": "solicitações",
+ "Requests": "Solicitações",
+ "Requested": "Solicitado",
"Requested_At": "Solicitado em",
"Requested_By": "Solicitado por",
"Require": "Exigir",
@@ -4972,4 +4978,4 @@
"RegisterWorkspace_Features_Omnichannel_Title": "Omnichannel",
"RegisterWorkspace_Setup_Label": "E-mail da conta da nuvem",
"cloud.RegisterWorkspace_Setup_Terms_Privacy": "Eu concordo com os <1>Termos e condições1> e a <3>Política de privacidade3>"
-}
\ No newline at end of file
+}
From c4325e82a734c624650fcaedbff8d02058e6da25 Mon Sep 17 00:00:00 2001
From: Marcos Spessatto Defendi
Date: Mon, 23 Oct 2023 09:40:37 -0300
Subject: [PATCH 17/22] feat: add statsToken to statistics and send it to the
sync process (#30181)
---
.../server/functions/buildRegistrationData.ts | 2 ++
.../app/statistics/server/lib/statistics.ts | 4 +++-
apps/meteor/server/cron/statistics.ts | 18 +++++++++++++-----
packages/core-typings/src/IStats.ts | 1 +
4 files changed, 19 insertions(+), 6 deletions(-)
diff --git a/apps/meteor/app/cloud/server/functions/buildRegistrationData.ts b/apps/meteor/app/cloud/server/functions/buildRegistrationData.ts
index c2bd91e82dd8..ea94db8d17a1 100644
--- a/apps/meteor/app/cloud/server/functions/buildRegistrationData.ts
+++ b/apps/meteor/app/cloud/server/functions/buildRegistrationData.ts
@@ -38,6 +38,7 @@ export type WorkspaceRegistrationData = {
MAC: number;
// activeContactsBillingMonth: number;
// activeContactsYesterday: number;
+ statsToken?: string;
};
export async function buildWorkspaceRegistrationData(contactEmail: T): Promise> {
@@ -92,5 +93,6 @@ export async function buildWorkspaceRegistrationData {
const rcStatistics = await statistics.get();
rcStatistics.createdAt = new Date();
- await Statistics.insertOne(rcStatistics);
+ const { insertedId } = await Statistics.insertOne(rcStatistics);
+ rcStatistics._id = insertedId;
+
return rcStatistics;
},
};
diff --git a/apps/meteor/server/cron/statistics.ts b/apps/meteor/server/cron/statistics.ts
index 7e7dea6adbc7..44dcb554824c 100644
--- a/apps/meteor/server/cron/statistics.ts
+++ b/apps/meteor/server/cron/statistics.ts
@@ -1,5 +1,6 @@
import { cronJobs } from '@rocket.chat/cron';
import type { Logger } from '@rocket.chat/logger';
+import { Statistics } from '@rocket.chat/models';
import { serverFetch as fetch } from '@rocket.chat/server-fetch';
import { Meteor } from 'meteor/meteor';
@@ -8,9 +9,7 @@ import { settings } from '../../app/settings/server';
import { statistics } from '../../app/statistics/server';
async function generateStatistics(logger: Logger): Promise {
- const cronStatistics: Record = await statistics.save();
-
- cronStatistics.host = Meteor.absoluteUrl();
+ const cronStatistics = await statistics.save();
if (!settings.get('Statistics_reporting')) {
return;
@@ -20,11 +19,20 @@ async function generateStatistics(logger: Logger): Promise {
const token = await getWorkspaceAccessToken();
const headers = { ...(token && { Authorization: `Bearer ${token}` }) };
- await fetch('https://collector.rocket.chat/', {
+ const response = await fetch('https://collector.rocket.chat/', {
method: 'POST',
- body: cronStatistics,
+ body: {
+ ...cronStatistics,
+ host: Meteor.absoluteUrl(),
+ },
headers,
});
+
+ const { statsToken } = await response.json();
+
+ if (statsToken != null) {
+ await Statistics.updateOne({ _id: cronStatistics._id }, { $set: { statsToken } });
+ }
} catch (error) {
/* error*/
logger.warn('Failed to send usage report');
diff --git a/packages/core-typings/src/IStats.ts b/packages/core-typings/src/IStats.ts
index 7fd5cd8218bc..0df389f2dd86 100644
--- a/packages/core-typings/src/IStats.ts
+++ b/packages/core-typings/src/IStats.ts
@@ -227,4 +227,5 @@ export interface IStats {
webRTCEnabled: boolean;
webRTCEnabledForOmnichannel: boolean;
omnichannelWebRTCCalls: number;
+ statsToken?: string;
}
From a82d8c2bb0a2b5f19f69e31af593fdb02df4ca77 Mon Sep 17 00:00:00 2001
From: Kevin Aleman
Date: Mon, 23 Oct 2023 07:19:10 -0600
Subject: [PATCH 18/22] feat: Add pagination & tooltips to agent's dropdown on
forwarding modal (#30629)
---
.changeset/rotten-dryers-allow.md | 5 +++
.../app/livechat/imports/server/rest/users.ts | 13 ++++---
.../app/livechat/server/api/lib/users.ts | 39 +++++++++++++++++--
.../client/components/AutoCompleteAgent.tsx | 30 +++++++-------
.../Omnichannel/hooks/useAgentsList.ts | 17 +++++---
.../Omnichannel/modals/ForwardChatModal.tsx | 35 ++++-------------
.../rocketchat-i18n/i18n/en.i18n.json | 1 +
.../rocketchat-i18n/i18n/pt-BR.i18n.json | 1 +
...channel-transfer-to-another-agents.spec.ts | 4 +-
.../page-objects/fragments/home-content.ts | 14 ++-----
.../end-to-end/api/livechat/01-agents.ts | 20 ++++++++++
packages/rest-typings/src/v1/omnichannel.ts | 24 +++++++++++-
12 files changed, 132 insertions(+), 71 deletions(-)
create mode 100644 .changeset/rotten-dryers-allow.md
diff --git a/.changeset/rotten-dryers-allow.md b/.changeset/rotten-dryers-allow.md
new file mode 100644
index 000000000000..154dea572780
--- /dev/null
+++ b/.changeset/rotten-dryers-allow.md
@@ -0,0 +1,5 @@
+---
+"@rocket.chat/meteor": patch
+---
+
+Add pagination & tooltips to agent's dropdown on forwarding modal
diff --git a/apps/meteor/app/livechat/imports/server/rest/users.ts b/apps/meteor/app/livechat/imports/server/rest/users.ts
index 680bdc7e80d2..196970583249 100644
--- a/apps/meteor/app/livechat/imports/server/rest/users.ts
+++ b/apps/meteor/app/livechat/imports/server/rest/users.ts
@@ -9,16 +9,15 @@ import { hasAtLeastOnePermissionAsync } from '../../../../authorization/server/f
import { findAgents, findManagers } from '../../../server/api/lib/users';
import { Livechat } from '../../../server/lib/Livechat';
+const emptyStringArray: string[] = [];
+
API.v1.addRoute(
'livechat/users/:type',
{
authRequired: true,
permissionsRequired: {
- GET: {
- permissions: ['manage-livechat-agents'],
- operation: 'hasAll',
- },
- POST: { permissions: ['view-livechat-manager'], operation: 'hasAll' },
+ 'POST': ['view-livechat-manager'],
+ '*': emptyStringArray,
},
validateParams: {
GET: isLivechatUsersManagerGETProps,
@@ -39,9 +38,13 @@ API.v1.addRoute(
return API.v1.unauthorized();
}
+ const { onlyAvailable, excludeId, showIdleAgents } = this.queryParams;
return API.v1.success(
await findAgents({
text,
+ onlyAvailable,
+ excludeId,
+ showIdleAgents,
pagination: {
offset,
count,
diff --git a/apps/meteor/app/livechat/server/api/lib/users.ts b/apps/meteor/app/livechat/server/api/lib/users.ts
index 948cde0f8bfd..49ac5682a6c0 100644
--- a/apps/meteor/app/livechat/server/api/lib/users.ts
+++ b/apps/meteor/app/livechat/server/api/lib/users.ts
@@ -1,6 +1,7 @@
import type { ILivechatAgent, IRole } from '@rocket.chat/core-typings';
import { Users } from '@rocket.chat/models';
import { escapeRegExp } from '@rocket.chat/string-helpers';
+import type { FilterOperators } from 'mongodb';
/**
* @param {IRole['_id']} role the role id
@@ -10,18 +11,39 @@ import { escapeRegExp } from '@rocket.chat/string-helpers';
async function findUsers({
role,
text,
+ onlyAvailable = false,
+ excludeId,
+ showIdleAgents = true,
pagination: { offset, count, sort },
}: {
role: IRole['_id'];
text?: string;
+ onlyAvailable?: boolean;
+ excludeId?: string;
+ showIdleAgents?: boolean;
pagination: { offset: number; count: number; sort: any };
}): Promise<{ users: ILivechatAgent[]; count: number; offset: number; total: number }> {
- const query = {};
+ const query: FilterOperators = {};
+ const orConditions: FilterOperators['$or'] = [];
if (text) {
const filterReg = new RegExp(escapeRegExp(text), 'i');
- Object.assign(query, {
- $or: [{ username: filterReg }, { name: filterReg }, { 'emails.address': filterReg }],
- });
+ orConditions.push({ $or: [{ username: filterReg }, { name: filterReg }, { 'emails.address': filterReg }] });
+ }
+
+ if (onlyAvailable) {
+ query.statusLivechat = 'available';
+ }
+
+ if (excludeId) {
+ query._id = { $ne: excludeId };
+ }
+
+ if (!showIdleAgents) {
+ orConditions.push({ $or: [{ status: { $exists: true, $ne: 'offline' }, roles: { $ne: 'bot' } }, { roles: 'bot' }] });
+ }
+
+ if (orConditions.length) {
+ query.$and = orConditions;
}
const [
@@ -52,14 +74,23 @@ async function findUsers({
}
export async function findAgents({
text,
+ onlyAvailable = false,
+ excludeId,
+ showIdleAgents = true,
pagination: { offset, count, sort },
}: {
text?: string;
+ onlyAvailable: boolean;
+ excludeId?: string;
+ showIdleAgents?: boolean;
pagination: { offset: number; count: number; sort: any };
}): Promise> {
return findUsers({
role: 'livechat-agent',
text,
+ onlyAvailable,
+ excludeId,
+ showIdleAgents,
pagination: {
offset,
count,
diff --git a/apps/meteor/client/components/AutoCompleteAgent.tsx b/apps/meteor/client/components/AutoCompleteAgent.tsx
index b4e287bcc4ae..f2cbebe46920 100644
--- a/apps/meteor/client/components/AutoCompleteAgent.tsx
+++ b/apps/meteor/client/components/AutoCompleteAgent.tsx
@@ -11,18 +11,26 @@ type AutoCompleteAgentProps = {
value: string;
error?: string;
placeholder?: string;
- onChange: (value: string) => void;
haveAll?: boolean;
haveNoAgentsSelectedOption?: boolean;
+ excludeId?: string;
+ showIdleAgents?: boolean;
+ onlyAvailable?: boolean;
+ withTitle?: boolean;
+ onChange: (value: string) => void;
};
const AutoCompleteAgent = ({
value,
error,
placeholder,
- onChange,
haveAll = false,
haveNoAgentsSelectedOption = false,
+ excludeId,
+ showIdleAgents = false,
+ onlyAvailable = false,
+ withTitle = false,
+ onChange,
}: AutoCompleteAgentProps): ReactElement => {
const [agentsFilter, setAgentsFilter] = useState('');
@@ -30,26 +38,16 @@ const AutoCompleteAgent = ({
const { itemsList: AgentsList, loadMoreItems: loadMoreAgents } = useAgentsList(
useMemo(
- () => ({ text: debouncedAgentsFilter, haveAll, haveNoAgentsSelectedOption }),
- [debouncedAgentsFilter, haveAll, haveNoAgentsSelectedOption],
+ () => ({ text: debouncedAgentsFilter, onlyAvailable, haveAll, haveNoAgentsSelectedOption, excludeId, showIdleAgents }),
+ [debouncedAgentsFilter, excludeId, haveAll, haveNoAgentsSelectedOption, onlyAvailable, showIdleAgents],
),
);
const { phase: agentsPhase, itemCount: agentsTotal, items: agentsItems } = useRecordList(AgentsList);
- const sortedByName = agentsItems.sort((a, b) => {
- if (a.label > b.label) {
- return 1;
- }
- if (a.label < b.label) {
- return -1;
- }
-
- return 0;
- });
-
return (
void}
- options={sortedByName}
+ options={agentsItems}
data-qa='autocomplete-agent'
endReached={
agentsPhase === AsyncStatePhase.LOADING ? (): void => undefined : (start): void => loadMoreAgents(start, Math.min(50, agentsTotal))
diff --git a/apps/meteor/client/components/Omnichannel/hooks/useAgentsList.ts b/apps/meteor/client/components/Omnichannel/hooks/useAgentsList.ts
index b854866184f7..e2f6f80f2355 100644
--- a/apps/meteor/client/components/Omnichannel/hooks/useAgentsList.ts
+++ b/apps/meteor/client/components/Omnichannel/hooks/useAgentsList.ts
@@ -9,6 +9,9 @@ type AgentsListOptions = {
text: string;
haveAll: boolean;
haveNoAgentsSelectedOption: boolean;
+ excludeId?: string;
+ showIdleAgents?: boolean;
+ onlyAvailable?: boolean;
};
type AgentOption = { value: string; label: string; _updatedAt: Date; _id: string };
@@ -26,6 +29,7 @@ export const useAgentsList = (
const reload = useCallback(() => setItemsList(new RecordList()), []);
const getAgents = useEndpoint('GET', '/v1/livechat/users/agent');
+ const { text, onlyAvailable = false, showIdleAgents = false, excludeId, haveAll, haveNoAgentsSelectedOption } = options;
useComponentDidUpdate(() => {
options && reload();
@@ -34,7 +38,10 @@ export const useAgentsList = (
const fetchData = useCallback(
async (start, end) => {
const { users: agents, total } = await getAgents({
- ...(options.text && { text: options.text }),
+ ...(text && { text }),
+ ...(excludeId && { excludeId }),
+ showIdleAgents,
+ onlyAvailable,
offset: start,
count: end + start,
sort: `{ "name": 1 }`,
@@ -43,14 +50,14 @@ export const useAgentsList = (
const items = agents.map((agent) => {
const agentOption = {
_updatedAt: new Date(agent._updatedAt),
- label: agent.username || agent._id,
+ label: `${agent.name || agent._id} (@${agent.username})`,
value: agent._id,
_id: agent._id,
};
return agentOption;
});
- options.haveAll &&
+ haveAll &&
items.unshift({
label: t('All'),
value: 'all',
@@ -58,7 +65,7 @@ export const useAgentsList = (
_id: 'all',
});
- options.haveNoAgentsSelectedOption &&
+ haveNoAgentsSelectedOption &&
items.unshift({
label: t('Empty_no_agent_selected'),
value: 'no-agent-selected',
@@ -71,7 +78,7 @@ export const useAgentsList = (
itemCount: total + 1,
};
},
- [getAgents, options.haveAll, options.haveNoAgentsSelectedOption, options.text, t],
+ [excludeId, getAgents, haveAll, haveNoAgentsSelectedOption, onlyAvailable, showIdleAgents, t, text],
);
const { loadMoreItems, initialItemCount } = useScrollableRecordList(itemsList, fetchData, 25);
diff --git a/apps/meteor/client/components/Omnichannel/modals/ForwardChatModal.tsx b/apps/meteor/client/components/Omnichannel/modals/ForwardChatModal.tsx
index bdbde6b05acd..a4d095fdb90d 100644
--- a/apps/meteor/client/components/Omnichannel/modals/ForwardChatModal.tsx
+++ b/apps/meteor/client/components/Omnichannel/modals/ForwardChatModal.tsx
@@ -19,7 +19,7 @@ import { useForm } from 'react-hook-form';
import { useRecordList } from '../../../hooks/lists/useRecordList';
import { AsyncStatePhase } from '../../../hooks/useAsyncState';
-import UserAutoComplete from '../../UserAutoComplete';
+import AutoCompleteAgent from '../../AutoCompleteAgent';
import { useDepartmentsList } from '../hooks/useDepartmentsList';
const ForwardChatModal = ({
@@ -53,28 +53,6 @@ const ForwardChatModal = ({
);
const { phase: departmentsPhase, items: departments, itemCount: departmentsTotal } = useRecordList(departmentsList);
- const _id = { $ne: room.servedBy?._id };
- const conditions = {
- _id,
- ...(!idleAgentsAllowedForForwarding && {
- $or: [
- {
- status: {
- $exists: true,
- $ne: 'offline',
- },
- roles: {
- $ne: 'bot',
- },
- },
- {
- roles: 'bot',
- },
- ],
- }),
- statusLivechat: 'available',
- };
-
const endReached = useCallback(
(start) => {
if (departmentsPhase !== AsyncStatePhase.LOADING) {
@@ -134,13 +112,16 @@ const ForwardChatModal = ({
{t('Forward_to_user')}
- {
setValue('username', value);
}}
- value={getValues().username}
/>
diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
index 46805a1f0e3e..d260037d2aab 100644
--- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
+++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
@@ -5453,6 +5453,7 @@
"Username_title": "Register username",
"Username_has_been_updated": "Username has been updated",
"Username_wants_to_start_otr_Do_you_want_to_accept": "{{username}} wants to start OTR. Do you want to accept?",
+ "Username_name_email": "Username, name or e-mail",
"Users": "Users",
"Users must use Two Factor Authentication": "Users must use Two Factor Authentication",
"Users_added": "The users have been added",
diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json
index 9812ddc46db3..b31c69d477e0 100644
--- a/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json
+++ b/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json
@@ -4581,6 +4581,7 @@
"Username_Placeholder": "Digite os nomes de usuário...",
"Username_title": "Cadastre um nome de usuário",
"Username_wants_to_start_otr_Do_you_want_to_accept": "{{username}} quer começar OTR. Você aceita?",
+ "Username_name_email": "Nome de usuário, nome ou e-mail",
"Users": "Usuários",
"Users must use Two Factor Authentication": "Os usuários devem usar autenticação de dois fatores",
"Users_added": "Os usuários foram adicionados",
diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-transfer-to-another-agents.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-transfer-to-another-agents.spec.ts
index 3c74065a9a84..dc31f54be934 100644
--- a/apps/meteor/tests/e2e/omnichannel/omnichannel-transfer-to-another-agents.spec.ts
+++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-transfer-to-another-agents.spec.ts
@@ -65,6 +65,7 @@ test.describe('omnichannel-transfer-to-another-agent', () => {
await agent2.poHomeOmnichannel.sidenav.switchStatus('offline');
await agent1.poHomeOmnichannel.content.btnForwardChat.click();
+ await agent1.poHomeOmnichannel.content.inputModalAgentUserName.click();
await agent1.poHomeOmnichannel.content.inputModalAgentUserName.type('user2');
await expect(agent1.page.locator('text=Empty')).toBeVisible();
@@ -76,8 +77,9 @@ test.describe('omnichannel-transfer-to-another-agent', () => {
await agent1.poHomeOmnichannel.sidenav.getSidebarItemByName(newVisitor.name).click();
await agent1.poHomeOmnichannel.content.btnForwardChat.click();
+ await agent1.poHomeOmnichannel.content.inputModalAgentUserName.click();
await agent1.poHomeOmnichannel.content.inputModalAgentUserName.type('user2');
- await agent1.page.locator('.rcx-option .rcx-option__wrapper >> text="user2"').click();
+ await agent1.page.locator('.rcx-option .rcx-option__wrapper >> text="user2 (@user2)"').click();
await agent1.poHomeOmnichannel.content.inputModalAgentForwardComment.type('any_comment');
await agent1.poHomeOmnichannel.content.btnModalConfirm.click();
await expect(agent1.poHomeOmnichannel.toastSuccess).toBeVisible();
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 cb8c8b089095..2ba8cd6428d9 100644
--- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts
+++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts
@@ -197,7 +197,7 @@ export class HomeContent {
}
get inputModalAgentUserName(): Locator {
- return this.page.locator('#modal-root input:nth-child(1)');
+ return this.page.locator('#modal-root input[placeholder="Username, name or e-mail"]');
}
get inputModalAgentForwardComment(): Locator {
@@ -237,16 +237,8 @@ export class HomeContent {
async openLastThreadMessageMenu(): Promise {
await this.page.locator('//main//aside >> [data-qa-type="message"]').last().hover();
- await this.page
- .locator('//main//aside >> [data-qa-type="message"]')
- .last()
- .locator('role=button[name="More"]')
- .waitFor();
- await this.page
- .locator('//main//aside >> [data-qa-type="message"]')
- .last()
- .locator('role=button[name="More"]')
- .click();
+ await this.page.locator('//main//aside >> [data-qa-type="message"]').last().locator('role=button[name="More"]').waitFor();
+ await this.page.locator('//main//aside >> [data-qa-type="message"]').last().locator('role=button[name="More"]').click();
}
async toggleAlsoSendThreadToChannel(isChecked: boolean): Promise {
diff --git a/apps/meteor/tests/end-to-end/api/livechat/01-agents.ts b/apps/meteor/tests/end-to-end/api/livechat/01-agents.ts
index 83efe2c96aa2..6f9120ea9094 100644
--- a/apps/meteor/tests/end-to-end/api/livechat/01-agents.ts
+++ b/apps/meteor/tests/end-to-end/api/livechat/01-agents.ts
@@ -94,6 +94,26 @@ describe('LIVECHAT - Agents', function () {
expect(agentRecentlyCreated?._id).to.be.equal(agent._id);
});
});
+ it('should return an array of available agents', async () => {
+ await updatePermission('edit-omnichannel-contact', ['admin']);
+ await updatePermission('transfer-livechat-guest', ['admin']);
+ await updatePermission('manage-livechat-agents', ['admin']);
+
+ await request
+ .get(api('livechat/users/agent'))
+ .set(credentials)
+ .expect('Content-Type', 'application/json')
+ .query({ onlyAvailable: true })
+ .expect(200)
+ .expect((res: Response) => {
+ expect(res.body).to.have.property('success', true);
+ expect(res.body.users).to.be.an('array');
+ expect(res.body).to.have.property('offset');
+ expect(res.body).to.have.property('total');
+ expect(res.body).to.have.property('count');
+ expect(res.body.users.every((u: { statusLivechat: string }) => u.statusLivechat === 'available')).to.be.true;
+ });
+ });
it('should return an array of managers', async () => {
await updatePermission('view-livechat-manager', ['admin']);
await updatePermission('manage-livechat-agents', ['admin']);
diff --git a/packages/rest-typings/src/v1/omnichannel.ts b/packages/rest-typings/src/v1/omnichannel.ts
index bebea2856861..3baeae111202 100644
--- a/packages/rest-typings/src/v1/omnichannel.ts
+++ b/packages/rest-typings/src/v1/omnichannel.ts
@@ -749,7 +749,13 @@ const LivechatDepartmentsByUnitIdSchema = {
export const isLivechatDepartmentsByUnitIdProps = ajv.compile(LivechatDepartmentsByUnitIdSchema);
-type LivechatUsersManagerGETProps = PaginatedRequest<{ text?: string; fields?: string }>;
+type LivechatUsersManagerGETProps = PaginatedRequest<{
+ text?: string;
+ fields?: string;
+ onlyAvailable?: boolean;
+ excludeId?: string;
+ showIdleAgents?: boolean;
+}>;
const LivechatUsersManagerGETSchema = {
type: 'object',
@@ -758,6 +764,18 @@ const LivechatUsersManagerGETSchema = {
type: 'string',
nullable: true,
},
+ onlyAvailable: {
+ type: 'string',
+ nullable: true,
+ },
+ excludeId: {
+ type: 'string',
+ nullable: true,
+ },
+ showIdleAgents: {
+ type: 'boolean',
+ nullable: true,
+ },
count: {
type: 'number',
nullable: true,
@@ -3386,7 +3404,9 @@ export type OmnichannelEndpoints = {
};
'/v1/livechat/users/agent': {
- GET: (params: PaginatedRequest<{ text?: string }>) => PaginatedResult<{
+ GET: (
+ params: PaginatedRequest<{ text?: string; onlyAvailable?: boolean; excludeId?: string; showIdleAgents?: boolean }>,
+ ) => PaginatedResult<{
users: (ILivechatAgent & { departments: string[] })[];
}>;
POST: (params: LivechatUsersManagerPOSTProps) => { success: boolean };
From d26a61c388dcc8dcaa717f2b2a92ad89bfcf4720 Mon Sep 17 00:00:00 2001
From: Kevin Aleman
Date: Mon, 23 Oct 2023 09:26:04 -0600
Subject: [PATCH 19/22] fix: `nextAgent` widget call sending an array of chars
instead of `department` query (#30689)
---
packages/livechat/src/lib/triggers.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/livechat/src/lib/triggers.js b/packages/livechat/src/lib/triggers.js
index 4cf01090587c..efa9e9bdc65d 100644
--- a/packages/livechat/src/lib/triggers.js
+++ b/packages/livechat/src/lib/triggers.js
@@ -34,7 +34,7 @@ const getAgent = (triggerAction) => {
let agent;
try {
- agent = await Livechat.nextAgent(department);
+ agent = await Livechat.nextAgent({ department });
} catch (error) {
return reject(error);
}
From 18ed36cdd1ae417ffe8549b969e51e836ab8941c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=BAlia=20Jaeger=20Foresti?=
<60678893+juliajforesti@users.noreply.github.com>
Date: Mon, 23 Oct 2023 13:53:42 -0300
Subject: [PATCH 20/22] fix: custom-css injection (#30716)
---
.changeset/cuddly-ties-run.md | 5 +++++
apps/meteor/app/ui-master/server/index.js | 2 --
2 files changed, 5 insertions(+), 2 deletions(-)
create mode 100644 .changeset/cuddly-ties-run.md
diff --git a/.changeset/cuddly-ties-run.md b/.changeset/cuddly-ties-run.md
new file mode 100644
index 000000000000..cb3873899841
--- /dev/null
+++ b/.changeset/cuddly-ties-run.md
@@ -0,0 +1,5 @@
+---
+'@rocket.chat/meteor': patch
+---
+
+fix: custom-css injection
diff --git a/apps/meteor/app/ui-master/server/index.js b/apps/meteor/app/ui-master/server/index.js
index f9e335451d74..34a9618d2db6 100644
--- a/apps/meteor/app/ui-master/server/index.js
+++ b/apps/meteor/app/ui-master/server/index.js
@@ -120,8 +120,6 @@ Meteor.startup(() => {
})(__meteor_runtime_config__.ROOT_URL_PATH_PREFIX);
injectIntoHead('base', ``);
-
- injectIntoHead('css-theme', '');
});
const renderDynamicCssList = withDebouncing({ wait: 500 })(async () => {
From dfe4b39b89ff97ff1cf557dc0f8e821d610ac9c7 Mon Sep 17 00:00:00 2001
From: Guilherme Jun Grillo <48109548+guijun13@users.noreply.github.com>
Date: Mon, 23 Oct 2023 15:40:54 -0300
Subject: [PATCH 21/22] i18n: Add accessibility and appearance finnish
translation (#30718)
---
apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json
index 79a27d83cbe6..f482e42419a4 100644
--- a/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json
+++ b/apps/meteor/packages/rocketchat-i18n/i18n/fi.i18n.json
@@ -30,6 +30,7 @@
"A_secure_and_highly_private_self-managed_solution_for_conference_calls": "Suojattu ja vahvasti yksityinen itsepalveluratkaisu neuvottelupuheluille.",
"A_workspace_admin_needs_to_install_and_configure_a_conference_call_app": "Työtilan järjestelmänvalvojan on asennettava ja määritettävä neuvottelupuhelusovellus.",
"An_app_needs_to_be_installed_and_configured": "Sovellus on asennettavaa ja määritettävä.",
+ "Accessibility_and_Appearance": "Helppokäyttöisyys ja ulkoasu",
"Accept_Call": "Hyväksy puhelu",
"Accept": "Hyväksy",
"Accept_incoming_livechat_requests_even_if_there_are_no_online_agents": "Hyväksy saapuvat monikanavapyynnöt, vaikka agentteja ei ole paikalla",
@@ -5749,4 +5750,4 @@
"Uninstall_grandfathered_app": "Poistetaanko {{appName}}?",
"App_will_lose_grandfathered_status": "**Tämä {{context}}sovellus menettää aikaisemmin käytetössä olleen sovelluksen tilansa.** \n \nYhteisöversion työtiloissa voi olla käytössä enintään {{limit}} {{context}} sovellusta. aikaisemmin Aikaisemmin käytössä olleet sovellukset lasketaan mukaan rajoitukseen, mutta rajoitusta ei sovelleta niihin.",
"Theme_Appearence": "Teeman ulkoasu"
-}
\ No newline at end of file
+}
From 72653e678047edc45ba46f01eb228676dc281a28 Mon Sep 17 00:00:00 2001
From: Aleksander Nicacio da Silva
Date: Mon, 23 Oct 2023 15:55:29 -0300
Subject: [PATCH 22/22] chore: simplified useLicense return value (#30719)
---
apps/meteor/client/hooks/useLicense.ts | 26 +++++++------------
.../client/views/admin/info/LicenseCard.tsx | 2 +-
.../client/views/hooks/useUpgradeTabParams.ts | 6 ++---
3 files changed, 14 insertions(+), 20 deletions(-)
diff --git a/apps/meteor/client/hooks/useLicense.ts b/apps/meteor/client/hooks/useLicense.ts
index 8ba594c5b5d3..ec07e1702bed 100644
--- a/apps/meteor/client/hooks/useLicense.ts
+++ b/apps/meteor/client/hooks/useLicense.ts
@@ -1,13 +1,15 @@
+import type { Serialized } from '@rocket.chat/core-typings';
import { useDebouncedCallback } from '@rocket.chat/fuselage-hooks';
import type { OperationResult } from '@rocket.chat/rest-typings';
-import { useEndpoint, usePermission, useSingleStream } from '@rocket.chat/ui-contexts';
+import { useEndpoint, useSingleStream } from '@rocket.chat/ui-contexts';
import type { UseQueryResult } from '@tanstack/react-query';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useEffect } from 'react';
-export const useLicense = (): UseQueryResult> => {
+type LicenseDataType = Awaited>['license'];
+
+export const useLicense = (): UseQueryResult> => {
const getLicenses = useEndpoint('GET', '/v1/licenses.info');
- const canViewLicense = usePermission('view-privileged-setting');
const queryClient = useQueryClient();
@@ -23,17 +25,9 @@ export const useLicense = (): UseQueryResult notify('license', () => invalidate()), [notify, invalidate]);
- return useQuery(
- ['licenses', 'getLicenses'],
- () => {
- if (!canViewLicense) {
- throw new Error('unauthorized api call');
- }
- return getLicenses({});
- },
- {
- staleTime: Infinity,
- keepPreviousData: true,
- },
- );
+ return useQuery(['licenses', 'getLicenses'], () => getLicenses({}), {
+ staleTime: Infinity,
+ keepPreviousData: true,
+ select: (data) => data.license,
+ });
};
diff --git a/apps/meteor/client/views/admin/info/LicenseCard.tsx b/apps/meteor/client/views/admin/info/LicenseCard.tsx
index 8aab636f4720..195121932e80 100644
--- a/apps/meteor/client/views/admin/info/LicenseCard.tsx
+++ b/apps/meteor/client/views/admin/info/LicenseCard.tsx
@@ -56,7 +56,7 @@ const LicenseCard = (): ReactElement => {
);
}
- const { activeModules } = request.data.license;
+ const { activeModules } = request.data;
const hasEngagement = activeModules.includes('engagement-dashboard');
const hasOmnichannel = activeModules.includes('livechat-enterprise');
diff --git a/apps/meteor/client/views/hooks/useUpgradeTabParams.ts b/apps/meteor/client/views/hooks/useUpgradeTabParams.ts
index 1d152b08d5b9..abfa9da251dc 100644
--- a/apps/meteor/client/views/hooks/useUpgradeTabParams.ts
+++ b/apps/meteor/client/views/hooks/useUpgradeTabParams.ts
@@ -13,11 +13,11 @@ export const useUpgradeTabParams = (): { tabType: UpgradeTabVariant | false; tri
const { data: registrationStatusData, isSuccess: isSuccessRegistrationStatus } = useRegistrationStatus();
const registered = registrationStatusData?.registrationStatus?.workspaceRegistered ?? false;
- const hasValidLicense = Boolean(licensesData?.license?.license ?? false);
+ const hasValidLicense = Boolean(licensesData?.license ?? false);
const hadExpiredTrials = cloudWorkspaceHadTrial ?? false;
- const isTrial = Boolean(licensesData?.license?.trial);
- const trialEndDateStr = licensesData?.license?.license?.information?.visualExpiration;
+ const isTrial = Boolean(licensesData?.trial);
+ const trialEndDateStr = licensesData?.license?.information?.visualExpiration;
const trialEndDate = trialEndDateStr ? format(new Date(trialEndDateStr), 'yyyy-MM-dd') : undefined;
const upgradeTabType = getUpgradeTabType({