From db919f9b23b81c13b653303faab5500cc8a2564e Mon Sep 17 00:00:00 2001 From: rocketchat-github-ci Date: Tue, 5 Sep 2023 20:13:30 +0000 Subject: [PATCH 01/13] Bump 6.3.4 --- .changeset/bump-patch-1693944810152.md | 5 +++++ yarn.lock | 16 ++++++++-------- 2 files changed, 13 insertions(+), 8 deletions(-) create mode 100644 .changeset/bump-patch-1693944810152.md diff --git a/.changeset/bump-patch-1693944810152.md b/.changeset/bump-patch-1693944810152.md new file mode 100644 index 000000000000..e1eaa7980afb --- /dev/null +++ b/.changeset/bump-patch-1693944810152.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/yarn.lock b/yarn.lock index 251222652634..95498d62f3b3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9898,9 +9898,9 @@ __metadata: "@rocket.chat/icons": "*" "@rocket.chat/prettier-config": "*" "@rocket.chat/styled": "*" - "@rocket.chat/ui-contexts": 1.0.2 + "@rocket.chat/ui-contexts": 1.0.3 "@rocket.chat/ui-kit": "*" - "@rocket.chat/ui-video-conf": 1.0.2 + "@rocket.chat/ui-video-conf": 1.0.3 "@tanstack/react-query": "*" react: "*" react-dom: "*" @@ -9982,14 +9982,14 @@ __metadata: ts-jest: ~29.0.5 typescript: ~5.1.3 peerDependencies: - "@rocket.chat/core-typings": 6.3.2 + "@rocket.chat/core-typings": 6.3.3 "@rocket.chat/css-in-js": "*" "@rocket.chat/fuselage": "*" "@rocket.chat/fuselage-tokens": "*" "@rocket.chat/message-parser": "*" "@rocket.chat/styled": "*" - "@rocket.chat/ui-client": 1.0.2 - "@rocket.chat/ui-contexts": 1.0.2 + "@rocket.chat/ui-client": 1.0.3 + "@rocket.chat/ui-contexts": 1.0.3 katex: "*" react: "*" languageName: unknown @@ -11048,7 +11048,7 @@ __metadata: "@rocket.chat/fuselage": "*" "@rocket.chat/fuselage-hooks": "*" "@rocket.chat/icons": "*" - "@rocket.chat/ui-contexts": 1.0.2 + "@rocket.chat/ui-contexts": 1.0.3 react: ~17.0.2 languageName: unknown linkType: soft @@ -11202,7 +11202,7 @@ __metadata: "@rocket.chat/fuselage-hooks": "*" "@rocket.chat/icons": "*" "@rocket.chat/styled": "*" - "@rocket.chat/ui-contexts": 1.0.2 + "@rocket.chat/ui-contexts": 1.0.3 react: ^17.0.2 react-dom: ^17.0.2 languageName: unknown @@ -11268,7 +11268,7 @@ __metadata: typescript: ~5.1.3 peerDependencies: "@rocket.chat/layout": "*" - "@rocket.chat/ui-contexts": 1.0.2 + "@rocket.chat/ui-contexts": 1.0.3 "@tanstack/react-query": "*" react: "*" react-hook-form: "*" From ebeb088441777a3c32bba0d4d50a048e3e9f5b8c Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Mon, 4 Sep 2023 13:33:57 -0600 Subject: [PATCH 02/13] fix: Prevent `RoomProvider.useEffect` from subscribing multiple times (#30273) --- .changeset/forty-hotels-pretend.md | 5 +++++ .../views/room/providers/RoomProvider.tsx | 22 ++++++++++--------- 2 files changed, 17 insertions(+), 10 deletions(-) create mode 100644 .changeset/forty-hotels-pretend.md diff --git a/.changeset/forty-hotels-pretend.md b/.changeset/forty-hotels-pretend.md new file mode 100644 index 000000000000..b23825d5a02a --- /dev/null +++ b/.changeset/forty-hotels-pretend.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +fix: Prevent `RoomProvider.useEffect` from subscribing to room-data stream multiple times diff --git a/apps/meteor/client/views/room/providers/RoomProvider.tsx b/apps/meteor/client/views/room/providers/RoomProvider.tsx index b2935f4702ea..20da5194895f 100644 --- a/apps/meteor/client/views/room/providers/RoomProvider.tsx +++ b/apps/meteor/client/views/room/providers/RoomProvider.tsx @@ -1,5 +1,4 @@ import type { IRoom } from '@rocket.chat/core-typings'; -import { isOmnichannelRoom } from '@rocket.chat/core-typings'; import { usePermission, useStream, useUserId, useRouter } from '@rocket.chat/ui-contexts'; import { useQueryClient } from '@tanstack/react-query'; import type { ReactNode, ContextType, ReactElement } from 'react'; @@ -36,17 +35,18 @@ const RoomProvider = ({ rid, children }: RoomProviderProps): ReactElement => { const queryClient = useQueryClient(); const userId = useUserId(); const isLivechatAdmin = usePermission('view-livechat-rooms'); + const { t: roomType } = room ?? {}; // TODO: move this to omnichannel context only useEffect(() => { - if (!room || !isOmnichannelRoom(room)) { + if (roomType !== 'l') { return; } return subscribeToRoom(rid, (room) => { queryClient.setQueryData(['rooms', rid], room); }); - }, [subscribeToRoom, rid, queryClient, room]); + }, [subscribeToRoom, rid, queryClient, roomType]); // TODO: the following effect is a workaround while we don't have a general and definitive solution for it const router = useRouter(); @@ -56,19 +56,21 @@ const RoomProvider = ({ rid, children }: RoomProviderProps): ReactElement => { } }, [isSuccess, room, router]); + const { _id: servedById } = room?.servedBy ?? {}; + // TODO: Review the necessity of this effect when we move away from cached collections useEffect(() => { - if (!room || !isOmnichannelRoom(room) || !room.servedBy) { + if (roomType !== 'l' || !servedById) { return; } - if (!isLivechatAdmin && room.servedBy._id !== userId) { - ChatRoom.remove(room._id); - queryClient.removeQueries(['rooms', room._id]); - queryClient.removeQueries(['rooms', { reference: room._id, type: 'l' }]); - queryClient.removeQueries(['/v1/rooms.info', room._id]); + if (!isLivechatAdmin && servedById !== userId) { + ChatRoom.remove(rid); + queryClient.removeQueries(['rooms', rid]); + queryClient.removeQueries(['rooms', { reference: rid, type: 'l' }]); + queryClient.removeQueries(['/v1/rooms.info', rid]); } - }, [isLivechatAdmin, queryClient, userId, room]); + }, [isLivechatAdmin, queryClient, userId, rid, roomType, servedById]); const subscriptionQuery = useReactiveQuery(['subscriptions', { rid }], () => ChatSubscription.findOne({ rid }) ?? null); From d418d0e752a69abecfe41ef84d1f7abf70a03d20 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Wed, 6 Sep 2023 12:45:10 -0300 Subject: [PATCH 03/13] fix: added debounce to units and custom fields search (#30291) --- .../views/omnichannel/customFields/CustomFieldsTable.tsx | 7 ++++--- apps/meteor/ee/client/omnichannel/units/UnitsTable.tsx | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/meteor/client/views/omnichannel/customFields/CustomFieldsTable.tsx b/apps/meteor/client/views/omnichannel/customFields/CustomFieldsTable.tsx index b92fad97633a..d9ef416226cc 100644 --- a/apps/meteor/client/views/omnichannel/customFields/CustomFieldsTable.tsx +++ b/apps/meteor/client/views/omnichannel/customFields/CustomFieldsTable.tsx @@ -1,5 +1,5 @@ import { Pagination } from '@rocket.chat/fuselage'; -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { useDebouncedValue, useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useRoute, useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import type { MutableRefObject } from 'react'; @@ -24,18 +24,19 @@ const CustomFieldsTable = ({ reload }: { reload: MutableRefObject<() => void> }) const t = useTranslation(); const [filter, setFilter] = useState(''); const departmentsRoute = useRoute('omnichannel-customfields'); + const debouncedFilter = useDebouncedValue(filter, 500); const { current, itemsPerPage, setItemsPerPage: onSetItemsPerPage, setCurrent: onSetCurrent, ...paginationProps } = usePagination(); const { sortBy, sortDirection, setSort } = useSort<'_id' | 'label' | 'scope' | 'visibility'>('_id'); const query = useMemo( () => ({ - text: filter, + text: debouncedFilter, sort: `{ "${sortBy}": ${sortDirection === 'asc' ? 1 : -1} }`, ...(itemsPerPage && { count: itemsPerPage }), ...(current && { offset: current }), }), - [filter, itemsPerPage, current, sortBy, sortDirection], + [debouncedFilter, itemsPerPage, current, sortBy, sortDirection], ); const getCustomFields = useEndpoint('GET', '/v1/livechat/custom-fields'); diff --git a/apps/meteor/ee/client/omnichannel/units/UnitsTable.tsx b/apps/meteor/ee/client/omnichannel/units/UnitsTable.tsx index a9ab094229f6..f85b73216612 100644 --- a/apps/meteor/ee/client/omnichannel/units/UnitsTable.tsx +++ b/apps/meteor/ee/client/omnichannel/units/UnitsTable.tsx @@ -1,5 +1,5 @@ import { Pagination } from '@rocket.chat/fuselage'; -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { useDebouncedValue, useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useEndpoint, useRoute, useTranslation } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import type { MutableRefObject } from 'react'; @@ -24,6 +24,7 @@ const UnitsTable = ({ reload }: { reload: MutableRefObject<() => void> }) => { const t = useTranslation(); const [filter, setFilter] = useState(''); const unitsRoute = useRoute('omnichannel-units'); + const debouncedFilter = useDebouncedValue(filter, 500); const { current, itemsPerPage, setItemsPerPage: onSetItemsPerPage, setCurrent: onSetCurrent, ...paginationProps } = usePagination(); const { sortBy, sortDirection, setSort } = useSort<'name' | 'visibility'>('name'); @@ -31,12 +32,12 @@ const UnitsTable = ({ reload }: { reload: MutableRefObject<() => void> }) => { const query = useMemo( () => ({ fields: JSON.stringify({ name: 1 }), - text: filter, + text: debouncedFilter, sort: JSON.stringify({ [sortBy]: sortDirection === 'asc' ? 1 : -1 }), ...(itemsPerPage && { count: itemsPerPage }), ...(current && { offset: current }), }), - [filter, itemsPerPage, current, sortBy, sortDirection], + [debouncedFilter, itemsPerPage, current, sortBy, sortDirection], ); const getUnits = useEndpoint('GET', '/v1/livechat/units'); From 69bb4771d76fdeaf95c439a90597223397d9e3eb Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Wed, 6 Sep 2023 15:02:39 -0300 Subject: [PATCH 04/13] chore: prevent unneeded calls to EE settings service (#30249) --- .../modules/listeners/listeners.module.ts | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/apps/meteor/server/modules/listeners/listeners.module.ts b/apps/meteor/server/modules/listeners/listeners.module.ts index 5c46343f67a4..856b91efce56 100644 --- a/apps/meteor/server/modules/listeners/listeners.module.ts +++ b/apps/meteor/server/modules/listeners/listeners.module.ts @@ -1,10 +1,11 @@ import type { ISetting as AppsSetting } from '@rocket.chat/apps-engine/definition/settings'; -import { UserStatus, isSettingColor } from '@rocket.chat/core-typings'; +import { UserStatus, isSettingColor, isSettingEnterprise } from '@rocket.chat/core-typings'; import type { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; -import type { IUser, IRoom, VideoConference, ISetting, IOmnichannelRoom } from '@rocket.chat/core-typings'; -import { parse } from '@rocket.chat/message-parser'; import type { IServiceClass } from '@rocket.chat/core-services'; import { EnterpriseSettings } from '@rocket.chat/core-services'; +import type { IUser, IRoom, VideoConference, ISetting, IOmnichannelRoom } from '@rocket.chat/core-typings'; +import { Logger } from '@rocket.chat/logger'; +import { parse } from '@rocket.chat/message-parser'; import type { NotificationsModule } from '../notifications/notifications.module'; import { settings } from '../../../app/settings/server/cached'; @@ -26,6 +27,8 @@ const minimongoChangeMap: Record = { export class ListenersModule { constructor(service: IServiceClass, notifications: NotificationsModule) { + const logger = new Logger('ListenersModule'); + service.onEvent('emoji.deleteCustom', (emoji) => { notifications.notifyLoggedInThisInstance('deleteEmojiCustom', { emojiData: emoji, @@ -247,11 +250,16 @@ export class ListenersModule { }); service.onEvent('watch.settings', async ({ clientAction, setting }): Promise => { - if (clientAction !== 'removed') { - // TODO check if setting is EE before calling this - const result = await EnterpriseSettings.changeSettingValue(setting); - if (result !== undefined && !(result instanceof Error)) { - setting.value = result; + // if a EE setting changed make sure we broadcast the correct value according to license + if (clientAction !== 'removed' && isSettingEnterprise(setting)) { + try { + const result = await EnterpriseSettings.changeSettingValue(setting); + if (result !== undefined && !(result instanceof Error)) { + setting.value = result; + } + } catch (err: unknown) { + logger.error({ msg: 'Error getting proper enterprise setting value. Returning `invalidValue` instead.', err }); + setting.value = setting.invalidValue; } } From 8ca4ac78cf5584bc02b6faa92781d1171ebcd01f Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Wed, 6 Sep 2023 16:56:47 -0300 Subject: [PATCH 05/13] chore: fix logger import --- apps/meteor/server/modules/listeners/listeners.module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/server/modules/listeners/listeners.module.ts b/apps/meteor/server/modules/listeners/listeners.module.ts index 856b91efce56..06e494fdb556 100644 --- a/apps/meteor/server/modules/listeners/listeners.module.ts +++ b/apps/meteor/server/modules/listeners/listeners.module.ts @@ -4,11 +4,11 @@ import type { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; import type { IServiceClass } from '@rocket.chat/core-services'; import { EnterpriseSettings } from '@rocket.chat/core-services'; import type { IUser, IRoom, VideoConference, ISetting, IOmnichannelRoom } from '@rocket.chat/core-typings'; -import { Logger } from '@rocket.chat/logger'; import { parse } from '@rocket.chat/message-parser'; import type { NotificationsModule } from '../notifications/notifications.module'; import { settings } from '../../../app/settings/server/cached'; +import { Logger } from '../../lib/logger/Logger'; const isMessageParserDisabled = process.env.DISABLE_MESSAGE_PARSER === 'true'; From 8a7d5d3898c40c221aad88c7df28753fe595dab1 Mon Sep 17 00:00:00 2001 From: Murtaza Patrawala <34130764+murtaza98@users.noreply.github.com> Date: Thu, 7 Sep 2023 22:41:37 +0400 Subject: [PATCH 06/13] fix: agent role being removed upon user deactivation (#30284) --- .changeset/seven-jobs-tickle.md | 6 + .../business-hour/AbstractBusinessHour.ts | 28 +---- .../livechat/server/business-hour/Single.ts | 34 +++++- .../livechat/server/hooks/afterUserActions.ts | 10 +- .../server/business-hour/Multiple.ts | 111 +++++++++++++++++- apps/meteor/server/models/raw/Users.js | 25 +++- .../model-typings/src/models/IUsersModel.ts | 1 + 7 files changed, 180 insertions(+), 35 deletions(-) create mode 100644 .changeset/seven-jobs-tickle.md diff --git a/.changeset/seven-jobs-tickle.md b/.changeset/seven-jobs-tickle.md new file mode 100644 index 000000000000..870bafbb7d9d --- /dev/null +++ b/.changeset/seven-jobs-tickle.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/model-typings": patch +--- + +fix: agent role being removed upon user deactivation diff --git a/apps/meteor/app/livechat/server/business-hour/AbstractBusinessHour.ts b/apps/meteor/app/livechat/server/business-hour/AbstractBusinessHour.ts index aeaf6bc1f287..7f9e8eaa404b 100644 --- a/apps/meteor/app/livechat/server/business-hour/AbstractBusinessHour.ts +++ b/apps/meteor/app/livechat/server/business-hour/AbstractBusinessHour.ts @@ -1,13 +1,10 @@ import moment from 'moment-timezone'; -import { ILivechatAgentStatus } from '@rocket.chat/core-typings'; -import type { ILivechatBusinessHour, ILivechatDepartment } from '@rocket.chat/core-typings'; +import type { ILivechatAgentStatus, ILivechatBusinessHour, ILivechatDepartment } from '@rocket.chat/core-typings'; import type { ILivechatBusinessHoursModel, IUsersModel } from '@rocket.chat/model-typings'; import { LivechatBusinessHours, Users } from '@rocket.chat/models'; import type { UpdateFilter } from 'mongodb'; import type { IWorkHoursCronJobsWrapper } from '../../../../server/models/raw/LivechatBusinessHours'; -import { businessHourLogger } from '../lib/logger'; -import { filterBusinessHoursThatMustBeOpened } from './Helper'; export interface IBusinessHourBehavior { findHoursToCreateJobs(): Promise; @@ -61,29 +58,6 @@ export abstract class AbstractBusinessHourBehavior { { livechatStatusSystemModified: true }, ); } - - async onNewAgentCreated(agentId: string): Promise { - businessHourLogger.debug(`Executing onNewAgentCreated for agentId: ${agentId}`); - - const defaultBusinessHour = await LivechatBusinessHours.findOneDefaultBusinessHour(); - if (!defaultBusinessHour) { - businessHourLogger.debug(`No default business hour found for agentId: ${agentId}`); - return; - } - - const businessHourToOpen = await filterBusinessHoursThatMustBeOpened([defaultBusinessHour]); - if (!businessHourToOpen.length) { - businessHourLogger.debug( - `No business hour to open found for agentId: ${agentId}. Default business hour is closed. Setting agentId: ${agentId} to status: ${ILivechatAgentStatus.NOT_AVAILABLE}`, - ); - await Users.setLivechatStatus(agentId, ILivechatAgentStatus.NOT_AVAILABLE); - return; - } - - await Users.addBusinessHourByAgentIds([agentId], defaultBusinessHour._id); - - businessHourLogger.debug(`Setting agentId: ${agentId} to status: ${ILivechatAgentStatus.AVAILABLE}`); - } } export abstract class AbstractBusinessHourType { diff --git a/apps/meteor/app/livechat/server/business-hour/Single.ts b/apps/meteor/app/livechat/server/business-hour/Single.ts index df9b3bce2a2e..cdb02d59d2b9 100644 --- a/apps/meteor/app/livechat/server/business-hour/Single.ts +++ b/apps/meteor/app/livechat/server/business-hour/Single.ts @@ -1,8 +1,9 @@ -import { LivechatBusinessHourTypes } from '@rocket.chat/core-typings'; +import { ILivechatAgentStatus, LivechatBusinessHourTypes } from '@rocket.chat/core-typings'; +import { LivechatBusinessHours, Users } from '@rocket.chat/models'; import type { IBusinessHourBehavior } from './AbstractBusinessHour'; import { AbstractBusinessHourBehavior } from './AbstractBusinessHour'; -import { openBusinessHourDefault } from './Helper'; +import { filterBusinessHoursThatMustBeOpened, openBusinessHourDefault } from './Helper'; import { businessHourLogger } from '../lib/logger'; export class SingleBusinessHourBehavior extends AbstractBusinessHourBehavior implements IBusinessHourBehavior { @@ -26,6 +27,35 @@ export class SingleBusinessHourBehavior extends AbstractBusinessHourBehavior imp return openBusinessHourDefault(); } + async onNewAgentCreated(agentId: string): Promise { + const defaultBusinessHour = await LivechatBusinessHours.findOneDefaultBusinessHour(); + if (!defaultBusinessHour) { + businessHourLogger.debug('No default business hour found for agentId', { + agentId, + }); + return; + } + + const businessHourToOpen = await filterBusinessHoursThatMustBeOpened([defaultBusinessHour]); + if (!businessHourToOpen.length) { + businessHourLogger.debug({ + msg: 'No business hours found. Moving agent to NOT_AVAILABLE status', + agentId, + newStatus: ILivechatAgentStatus.NOT_AVAILABLE, + }); + await Users.setLivechatStatus(agentId, ILivechatAgentStatus.NOT_AVAILABLE); + return; + } + + await Users.addBusinessHourByAgentIds([agentId], defaultBusinessHour._id); + + businessHourLogger.debug({ + msg: 'Business hours found. Moving agent to AVAILABLE status', + agentId, + newStatus: ILivechatAgentStatus.AVAILABLE, + }); + } + afterSaveBusinessHours(): Promise { return openBusinessHourDefault(); } diff --git a/apps/meteor/app/livechat/server/hooks/afterUserActions.ts b/apps/meteor/app/livechat/server/hooks/afterUserActions.ts index 50fe5846637b..30900481c4e2 100644 --- a/apps/meteor/app/livechat/server/hooks/afterUserActions.ts +++ b/apps/meteor/app/livechat/server/hooks/afterUserActions.ts @@ -1,4 +1,5 @@ -import type { IUser } from '@rocket.chat/core-typings'; +import { type IUser } from '@rocket.chat/core-typings'; +import { Users } from '@rocket.chat/models'; import { callbacks } from '../../../../lib/callbacks'; import { Livechat } from '../lib/Livechat'; @@ -33,8 +34,11 @@ const handleAgentCreated = async (user: IUser) => { const handleDeactivateUser = async (user: IUser) => { if (wasAgent(user)) { - callbackLogger.debug('Removing agent', user._id); - await Livechat.removeAgent(user.username); + callbackLogger.debug({ + msg: 'Removing agent extension & making agent unavailable', + userId: user._id, + }); + await Users.makeAgentUnavailableAndUnsetExtension(user._id); } }; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/business-hour/Multiple.ts b/apps/meteor/ee/app/livechat-enterprise/server/business-hour/Multiple.ts index 0fd1d3ee8284..a6bc2a456ad7 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/business-hour/Multiple.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/business-hour/Multiple.ts @@ -1,13 +1,18 @@ import moment from 'moment'; +import { LivechatBusinessHourTypes } from '@rocket.chat/core-typings'; import type { ILivechatDepartment, ILivechatBusinessHour } from '@rocket.chat/core-typings'; import { LivechatDepartment, LivechatDepartmentAgents, Users } from '@rocket.chat/models'; import type { IBusinessHourBehavior } from '../../../../../app/livechat/server/business-hour/AbstractBusinessHour'; import { AbstractBusinessHourBehavior } from '../../../../../app/livechat/server/business-hour/AbstractBusinessHour'; -import { filterBusinessHoursThatMustBeOpened } from '../../../../../app/livechat/server/business-hour/Helper'; +import { + filterBusinessHoursThatMustBeOpened, + filterBusinessHoursThatMustBeOpenedByDay, +} from '../../../../../app/livechat/server/business-hour/Helper'; +import { settings } from '../../../../../app/settings/server'; +import { isTruthy } from '../../../../../lib/isTruthy'; import { closeBusinessHour, openBusinessHour, removeBusinessHourByAgentIds } from './Helper'; import { bhLogger } from '../lib/logger'; -import { settings } from '../../../../../app/settings/server'; import { businessHourManager } from '../../../../../app/livechat/server/business-hour'; interface IBusinessHoursExtraProperties extends ILivechatBusinessHour { @@ -248,6 +253,108 @@ export class MultipleBusinessHoursBehavior extends AbstractBusinessHourBehavior return this.UsersRepository.isAgentWithinBusinessHours(agentId); } + async onNewAgentCreated(agentId: string): Promise { + bhLogger.debug({ + msg: 'Executing onNewAgentCreated for agent', + agentId, + }); + + await this.applyAnyOpenBusinessHourToAgent(agentId); + + await Users.updateLivechatStatusBasedOnBusinessHours([agentId]); + } + + private async applyAnyOpenBusinessHourToAgent(agentId: string): Promise { + const currentTime = moment().utc(); + const day = currentTime.format('dddd'); + const allActiveBusinessHoursForEntireWeek = await this.BusinessHourRepository.findActiveBusinessHours({ + projection: { + workHours: 1, + timezone: 1, + type: 1, + active: 1, + }, + }); + const openedBusinessHours = await filterBusinessHoursThatMustBeOpenedByDay(allActiveBusinessHoursForEntireWeek, day); + if (!openedBusinessHours.length) { + bhLogger.debug({ + msg: 'Business hour status check failed for agent. No opened business hour found for the current day', + agentId, + }); + return; + } + + const agentDepartments = await LivechatDepartmentAgents.find( + { departmentEnabled: true, agentId }, + { projection: { agentId: 1, departmentId: 1 } }, + ).toArray(); + + if (!agentDepartments.length) { + // check if default businessHour is active + const isDefaultBHActive = openedBusinessHours.find(({ type }) => type === LivechatBusinessHourTypes.DEFAULT); + if (isDefaultBHActive?._id) { + await Users.openAgentBusinessHoursByBusinessHourIdsAndAgentId([isDefaultBHActive._id], agentId); + + bhLogger.debug({ + msg: 'Business hour status check passed for agent. Found default business hour to be active', + agentId, + }); + return; + } + + bhLogger.debug({ + msg: 'Business hour status check failed for agent. Found default business hour to be inactive', + agentId, + }); + return; + } + + // check if any one these departments have a opened business hour linked to it + const departments = (await LivechatDepartment.findInIds( + agentDepartments.map(({ departmentId }) => departmentId), + { projection: { _id: 1, businessHourId: 1 } }, + ).toArray()) as Pick[]; + + const departmentsWithActiveBH = departments.filter( + ({ businessHourId }) => businessHourId && openedBusinessHours.findIndex(({ _id }) => _id === businessHourId) !== -1, + ); + + if (!departmentsWithActiveBH.length) { + // No opened business hour found for any of the departments connected to the agent + // check if this agent has any departments that is connected to any non-default business hour + // if no such departments found then check default BH and if it is active, then allow the agent to change service status + const hasAtLeastOneDepartmentWithNonDefaultBH = departments.some(({ businessHourId }) => { + // check if business hour is active + return businessHourId && allActiveBusinessHoursForEntireWeek.findIndex(({ _id }) => _id === businessHourId) !== -1; + }); + if (!hasAtLeastOneDepartmentWithNonDefaultBH) { + const isDefaultBHActive = openedBusinessHours.find(({ type }) => type === LivechatBusinessHourTypes.DEFAULT); + if (isDefaultBHActive?._id) { + await Users.openAgentBusinessHoursByBusinessHourIdsAndAgentId([isDefaultBHActive._id], agentId); + + bhLogger.debug({ + msg: 'Business hour status check passed for agentId. Found default business hour to be active and agent has no departments with non-default business hours', + agentId, + }); + return; + } + } + bhLogger.debug({ + msg: 'Business hour status check failed for agent. No opened business hour found for any of the departments connected to the agent', + agentId, + }); + return; + } + + const activeBusinessHoursForAgent = departmentsWithActiveBH.map(({ businessHourId }) => businessHourId).filter(isTruthy); + await Users.openAgentBusinessHoursByBusinessHourIdsAndAgentId(activeBusinessHoursForAgent, agentId); + + bhLogger.debug({ + msg: `Business hour status check passed for agent. Found opened business hour for departments connected to the agent`, + activeBusinessHoursForAgent, + }); + } + private async handleRemoveAgentsFromDepartments(department: Record, agentsIds: string[], options: any): Promise { const agentIdsWithoutDepartment: string[] = []; const agentIdsToRemoveCurrentBusinessHour: string[] = []; diff --git a/apps/meteor/server/models/raw/Users.js b/apps/meteor/server/models/raw/Users.js index 2719a5f32b9c..8c9bb726ef07 100644 --- a/apps/meteor/server/models/raw/Users.js +++ b/apps/meteor/server/models/raw/Users.js @@ -1,3 +1,4 @@ +import { ILivechatAgentStatus } from '@rocket.chat/core-typings'; import { escapeRegExp } from '@rocket.chat/string-helpers'; import { Subscriptions } from '@rocket.chat/models'; @@ -6,6 +7,8 @@ import { BaseRaw } from './BaseRaw'; const queryStatusAgentOnline = (extraFilters = {}, isLivechatEnabledWhenAgentIdle) => ({ statusLivechat: 'available', roles: 'livechat-agent', + // ignore deactivated users + active: true, ...(!isLivechatEnabledWhenAgentIdle && { $or: [ { @@ -887,7 +890,7 @@ export class UsersRaw extends BaseRaw { }, }; - return this.updateMany(query, update); + return this.updateOne(query, update); } addBusinessHourByAgentIds(agentIds = [], businessHourId) { @@ -985,6 +988,8 @@ export class UsersRaw extends BaseRaw { const query = { $or: [{ openBusinessHours: { $exists: false } }, { openBusinessHours: { $size: 0 } }], roles: 'livechat-agent', + // exclude deactivated users + active: true, // Avoid unnecessary updates statusLivechat: 'available', ...(Array.isArray(userIds) && userIds.length > 0 && { _id: { $in: userIds } }), @@ -1641,6 +1646,24 @@ export class UsersRaw extends BaseRaw { return this.updateOne(query, update); } + makeAgentUnavailableAndUnsetExtension(userId) { + const query = { + _id: userId, + roles: 'livechat-agent', + }; + + const update = { + $set: { + statusLivechat: ILivechatAgentStatus.NOT_AVAILABLE, + }, + $unset: { + extension: 1, + }, + }; + + return this.updateOne(query, update); + } + setLivechatData(userId, data = {}) { // TODO: Create class Agent const query = { diff --git a/packages/model-typings/src/models/IUsersModel.ts b/packages/model-typings/src/models/IUsersModel.ts index 9ee633a1cd80..393513dd78bc 100644 --- a/packages/model-typings/src/models/IUsersModel.ts +++ b/packages/model-typings/src/models/IUsersModel.ts @@ -258,6 +258,7 @@ export interface IUsersModel extends IBaseModel { getNextAgent(ignoreAgentId?: string, extraQuery?: Filter): Promise<{ agentId: string; username: string } | null>; getNextBotAgent(ignoreAgentId?: string): Promise<{ agentId: string; username: string } | null>; setLivechatStatus(userId: string, status: ILivechatAgentStatus): Promise; + makeAgentUnavailableAndUnsetExtension(userId: string): Promise; setLivechatData(userId: string, data?: Record): Promise; closeOffice(): Promise; openOffice(): Promise; From 16bfbb50bebafcd0f94d337000e640bbc0030d38 Mon Sep 17 00:00:00 2001 From: Aleksander Nicacio da Silva Date: Wed, 6 Sep 2023 18:37:45 -0300 Subject: [PATCH 07/13] fix: Selected options not being displayed for monitors and departments (#30292) Co-authored-by: Kevin Aleman <11577696+KevLehman@users.noreply.github.com> --- .../views/hooks/useDepartmentsByUnitsList.ts | 1 - .../client/views/hooks/useMonitorsList.ts | 12 ++++----- .../server/lib/Department.ts | 2 +- .../ee/client/omnichannel/units/UnitEdit.js | 25 +++++++++---------- 4 files changed, 19 insertions(+), 21 deletions(-) diff --git a/apps/meteor/client/views/hooks/useDepartmentsByUnitsList.ts b/apps/meteor/client/views/hooks/useDepartmentsByUnitsList.ts index ceb7dc933df5..6c402a199ded 100644 --- a/apps/meteor/client/views/hooks/useDepartmentsByUnitsList.ts +++ b/apps/meteor/client/views/hooks/useDepartmentsByUnitsList.ts @@ -45,7 +45,6 @@ export const useDepartmentsByUnitsList = ( name: department.archived ? `${name} [${t('Archived')}]` : name, label: name, value: _id, - ...(_updatedAt && { _updatedAt: new Date(_updatedAt) }), }; }), itemCount: total, diff --git a/apps/meteor/client/views/hooks/useMonitorsList.ts b/apps/meteor/client/views/hooks/useMonitorsList.ts index 4d9ccba5c883..512b49d49d8e 100644 --- a/apps/meteor/client/views/hooks/useMonitorsList.ts +++ b/apps/meteor/client/views/hooks/useMonitorsList.ts @@ -33,15 +33,15 @@ export const useMonitorsList = ( text: options.filter, offset: start, count: end + start, + sort: JSON.stringify({ username: 1 }), }); return { - items: monitors.map((members: any) => { - members._updatedAt = new Date(members._updatedAt); - members.label = members.username; - members.value = members._id; - return members; - }), + items: monitors.map((members: any) => ({ + ...members, + label: members.username, + value: members._id, + })), itemCount: total, }; }, diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/Department.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/Department.ts index a47515502840..01ce3b9470a8 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/Department.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/Department.ts @@ -24,7 +24,7 @@ export const findAllDepartmentsAvailable = async ( query = await callbacks.run('livechat.applyDepartmentRestrictions', query, { userId: uid }); } - const { cursor, totalCount } = LivechatDepartment.findPaginated(query, { limit: count, offset }); + const { cursor, totalCount } = LivechatDepartment.findPaginated(query, { limit: count, offset, sort: { name: 1 } }); const [departments, total] = await Promise.all([cursor.toArray(), totalCount]); diff --git a/apps/meteor/ee/client/omnichannel/units/UnitEdit.js b/apps/meteor/ee/client/omnichannel/units/UnitEdit.js index bf7b7db50e80..ef33d8da93e7 100644 --- a/apps/meteor/ee/client/omnichannel/units/UnitEdit.js +++ b/apps/meteor/ee/client/omnichannel/units/UnitEdit.js @@ -33,17 +33,6 @@ function UnitEdit({ title, data, unitId, isNew, unitMonitors, unitDepartments, r const { phase: departmentsPhase, items: departmentsItems, itemCount: departmentsTotal } = useRecordList(departmentsList); - const departmentsSortedByName = departmentsItems.sort((a, b) => { - if (a.name > b.name) { - return 1; - } - if (a.name < b.name) { - return -1; - } - - return 0; - }); - const unit = data || {}; const currUnitMonitors = useMemo( @@ -82,6 +71,16 @@ function UnitEdit({ title, data, unitId, isNew, unitMonitors, unitDepartments, r const { handleName, handleVisibility, handleDepartments, handleMonitors } = handlers; const { name, visibility, departments, monitors } = values; + const departmentsOptions = useMemo(() => { + const pending = departments.filter(({ value }) => !departmentsItems.find((dep) => dep.value === value)); + return [...departmentsItems, ...pending]; + }, [departments, departmentsItems]); + + const monitorsOptions = useMemo(() => { + const pending = monitors.filter(({ value }) => !monitorsItems.find((mon) => mon.value === value)); + return [...monitorsItems, ...pending]; + }, [monitors, monitorsItems]); + const nameError = useMemo(() => (!name || name.length === 0 ? t('The_field_is_required', t('name')) : undefined), [name, t]); const visibilityError = useMemo( () => (!visibility || visibility.length === 0 ? t('The_field_is_required', t('description')) : undefined), @@ -172,7 +171,7 @@ function UnitEdit({ title, data, unitId, isNew, unitMonitors, unitDepartments, r withTitle filter={departmentsFilter} setFilter={setDepartmentsFilter} - options={departmentsSortedByName} + options={departmentsOptions} value={departments} error={hasUnsavedChanges && departmentError} maxWidth='100%' @@ -194,7 +193,7 @@ function UnitEdit({ title, data, unitId, isNew, unitMonitors, unitDepartments, r withTitle filter={monitorsFilter} setFilter={setMonitorsFilter} - options={monitorsItems} + options={monitorsOptions} value={monitors} error={hasUnsavedChanges && unitMonitorsError} maxWidth='100%' From 759fe2472aadc4c06277cde9014870bf1347bdd4 Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Wed, 6 Sep 2023 09:59:09 -0600 Subject: [PATCH 08/13] chore: Increase cache time to 10s on `getUnits` function (#30285) --- .changeset/three-birds-tickle.md | 5 +++++ apps/meteor/ee/app/livechat-enterprise/server/lib/units.ts | 2 +- .../server/methods/getUnitsFromUserRoles.ts | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/three-birds-tickle.md diff --git a/.changeset/three-birds-tickle.md b/.changeset/three-birds-tickle.md new file mode 100644 index 000000000000..0ce911d9f6fa --- /dev/null +++ b/.changeset/three-birds-tickle.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +chore: Increase cache time from 5s to 10s on `getUnits` helpers. This should reduce the number of DB calls made by this method to fetch the unit limitations for a user. diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/units.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/units.ts index 4b370b6b7d8a..f22099a618db 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/units.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/units.ts @@ -8,7 +8,7 @@ async function hasUnits(): Promise { } // Units should't change really often, so we can cache the result -const memoizedHasUnits = mem(hasUnits, { maxAge: 5000 }); +const memoizedHasUnits = mem(hasUnits, { maxAge: 10000 }); export async function getUnitsFromUser(): Promise<{ [k: string]: any }[] | undefined> { if (!(await memoizedHasUnits())) { diff --git a/apps/meteor/ee/app/livechat-enterprise/server/methods/getUnitsFromUserRoles.ts b/apps/meteor/ee/app/livechat-enterprise/server/methods/getUnitsFromUserRoles.ts index a9d3c3556b97..0c2fdb7b806b 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/methods/getUnitsFromUserRoles.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/methods/getUnitsFromUserRoles.ts @@ -17,7 +17,7 @@ async function getUnitsFromUserRoles(user: string | null): Promise Date: Fri, 8 Sep 2023 18:15:00 -0300 Subject: [PATCH 09/13] regression: `AccountProfileForm` not reseting (#30332) 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> --- .../account/profile/AccountProfileForm.tsx | 168 +++++++++++------- 1 file changed, 100 insertions(+), 68 deletions(-) diff --git a/apps/meteor/client/views/account/profile/AccountProfileForm.tsx b/apps/meteor/client/views/account/profile/AccountProfileForm.tsx index cb65ce80317e..1a326db4544d 100644 --- a/apps/meteor/client/views/account/profile/AccountProfileForm.tsx +++ b/apps/meteor/client/views/account/profile/AccountProfileForm.tsx @@ -22,7 +22,6 @@ import UserAvatarEditor from '../../../components/avatar/UserAvatarEditor'; import { useUpdateAvatar } from '../../../hooks/useUpdateAvatar'; import { USER_STATUS_TEXT_MAX_LENGTH, BIO_TEXT_MAX_LENGTH } from '../../../lib/constants'; import type { AccountProfileFormValues } from './getProfileInitialValues'; -import { getProfileInitialValues } from './getProfileInitialValues'; import { useAccountProfileSettings } from './useAccountProfileSettings'; const AccountProfileForm = (props: AllHTMLAttributes): ReactElement => { @@ -46,10 +45,8 @@ const AccountProfileForm = (props: AllHTMLAttributes): ReactEle } = useAccountProfileSettings(); const { - register, control, watch, - reset, handleSubmit, formState: { errors }, } = useFormContext(); @@ -97,10 +94,10 @@ const AccountProfileForm = (props: AllHTMLAttributes): ReactEle try { await updateOwnBasicInfo( { - ...(allowRealNameChange ? { realname: name } : {}), - ...(allowEmailChange && user ? getUserEmailAddress(user) !== email && { email } : {}), - ...(canChangeUsername ? { username } : {}), - ...(allowUserStatusMessageChange ? { statusText } : {}), + realname: name, + ...(user ? getUserEmailAddress(user) !== email && { email } : {}), + username, + statusText, statusType, nickname, bio, @@ -110,7 +107,6 @@ const AccountProfileForm = (props: AllHTMLAttributes): ReactEle await updateAvatar(); dispatchToastMessage({ type: 'success', message: t('Profile_saved_successfully') }); - reset(getProfileInitialValues(user)); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); } @@ -147,16 +143,21 @@ const AccountProfileForm = (props: AllHTMLAttributes): ReactEle {t('Name')} - (requireName && name === '' ? t('error-the-field-is-required', { field: t('Name') }) : true), - })} - id={nameId} - error={errors.name?.message} - disabled={!allowRealNameChange} - aria-required='true' - aria-invalid={errors.username ? 'true' : 'false'} - aria-describedby={`${nameId}-error ${nameId}-hint`} + (requireName && name === '' ? t('error-the-field-is-required', { field: t('Name') }) : true) }} + render={({ field }) => ( + + )} /> {errors.name && ( @@ -171,17 +172,25 @@ const AccountProfileForm = (props: AllHTMLAttributes): ReactEle {t('Username')} - validateUsername(username), - })} - id={usernameId} - error={errors.username?.message} - addon={} - aria-required='true' - aria-invalid={errors.username ? 'true' : 'false'} - aria-describedby={`${usernameId}-error ${usernameId}-hint`} + }} + render={({ field }) => ( + } + aria-required='true' + aria-invalid={errors.username ? 'true' : 'false'} + aria-describedby={`${usernameId}-error ${usernameId}-hint`} + /> + )} /> {errors?.username && ( @@ -195,26 +204,31 @@ const AccountProfileForm = (props: AllHTMLAttributes): ReactEle {t('StatusMessage')} - ( - - )} + ( + ( + + )} + /> + } /> - } + )} /> {errors?.statusText && ( @@ -227,21 +241,34 @@ const AccountProfileForm = (props: AllHTMLAttributes): ReactEle {t('Nickname')} - } /> + ( + } /> + )} + /> {t('Bio')} - } - aria-invalid={errors.statusText ? 'true' : 'false'} - aria-describedby={`${bioId}-error`} + ( + } + aria-invalid={errors.statusText ? 'true' : 'false'} + aria-describedby={`${bioId}-error`} + /> + )} /> {errors?.bio && ( @@ -255,18 +282,23 @@ const AccountProfileForm = (props: AllHTMLAttributes): ReactEle {t('Email')} - (validateEmail(email) ? undefined : t('error-invalid-email-address')) }, - })} - flexGrow={1} - error={errors.email?.message} - addon={} - disabled={!allowEmailChange} - aria-required='true' - aria-invalid={errors.email ? 'true' : 'false'} - aria-describedby={`${emailId}-error ${emailId}-hint`} + (validateEmail(email) ? undefined : t('error-invalid-email-address')) } }} + render={({ field }) => ( + } + disabled={!allowEmailChange} + aria-required='true' + aria-invalid={errors.email ? 'true' : 'false'} + aria-describedby={`${emailId}-error ${emailId}-hint`} + /> + )} /> {!isUserVerified && ( - + )} ); diff --git a/apps/meteor/client/views/admin/info/LicenseCard.tsx b/apps/meteor/client/views/admin/info/LicenseCard.tsx index 8865ed990a4a..bccbddaa6db7 100644 --- a/apps/meteor/client/views/admin/info/LicenseCard.tsx +++ b/apps/meteor/client/views/admin/info/LicenseCard.tsx @@ -1,6 +1,6 @@ import { ButtonGroup, Button, Skeleton } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { Card } from '@rocket.chat/ui-client'; +import { Card, CardBody, CardCol, CardTitle, CardColSection, CardColTitle, CardFooter } from '@rocket.chat/ui-client'; import { useSetModal, useSetting, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; @@ -43,14 +43,14 @@ const LicenseCard = (): ReactElement => { return ( - {t('License')} - - - + {t('License')} + + + - - - {t('Features')} + + + {t('Features')} {isLoading ? ( <> @@ -67,10 +67,10 @@ const LicenseCard = (): ReactElement => { )} - - - - + + + + {isAirGapped ? ( )} - + ); }; diff --git a/apps/meteor/client/views/admin/info/UsageCard.tsx b/apps/meteor/client/views/admin/info/UsageCard.tsx index 793789c30a03..7a3b2123e5f2 100644 --- a/apps/meteor/client/views/admin/info/UsageCard.tsx +++ b/apps/meteor/client/views/admin/info/UsageCard.tsx @@ -1,7 +1,17 @@ import type { IStats } from '@rocket.chat/core-typings'; import { ButtonGroup, Button } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { TextSeparator, Card } from '@rocket.chat/ui-client'; +import { + Card, + CardBody, + CardCol, + CardTitle, + CardColSection, + CardColTitle, + CardFooter, + TextSeparator, + CardIcon, +} from '@rocket.chat/ui-client'; import { useRoute, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { memo } from 'react'; @@ -29,15 +39,15 @@ const UsageCard = ({ statistics, vertical }: UsageCardProps): ReactElement => { return ( - {t('Usage')} - - - - {t('Users')} + {t('Usage')} + + + + {t('Users')} - {t('Total')} + {t('Total')} } value={statistics.totalUsers} @@ -45,9 +55,9 @@ const UsageCard = ({ statistics, vertical }: UsageCardProps): ReactElement => { - + - {' '} + {' '} {t('Online')} } @@ -56,9 +66,9 @@ const UsageCard = ({ statistics, vertical }: UsageCardProps): ReactElement => { - + - {' '} + {' '} {t('Busy')} } @@ -67,9 +77,9 @@ const UsageCard = ({ statistics, vertical }: UsageCardProps): ReactElement => { - + - {' '} + {' '} {t('Away')} } @@ -78,34 +88,34 @@ const UsageCard = ({ statistics, vertical }: UsageCardProps): ReactElement => { - + - {' '} + {' '} {t('Offline')} } value={statistics.offlineUsers} /> - - - {t('Types_and_Distribution')} + + + {t('Types_and_Distribution')} - - - {t('Uploads')} + + + {t('Uploads')} - - - {t('Total_rooms')} + + + {t('Total_rooms')} - {t('Stats_Total_Rooms')} + {t('Stats_Total_Rooms')} } value={statistics.totalRooms} @@ -113,7 +123,7 @@ const UsageCard = ({ statistics, vertical }: UsageCardProps): ReactElement => { - {t('Stats_Total_Channels')} + {t('Stats_Total_Channels')} } value={statistics.totalChannels} @@ -121,7 +131,7 @@ const UsageCard = ({ statistics, vertical }: UsageCardProps): ReactElement => { - {t('Stats_Total_Private_Groups')} + {t('Stats_Total_Private_Groups')} } value={statistics.totalPrivateGroups} @@ -129,7 +139,7 @@ const UsageCard = ({ statistics, vertical }: UsageCardProps): ReactElement => { - {t('Stats_Total_Direct_Messages')} + {t('Stats_Total_Direct_Messages')} } value={statistics.totalDirect} @@ -137,7 +147,7 @@ const UsageCard = ({ statistics, vertical }: UsageCardProps): ReactElement => { - {t('Total_Discussions')} + {t('Total_Discussions')} } value={statistics.totalDiscussions} @@ -145,30 +155,30 @@ const UsageCard = ({ statistics, vertical }: UsageCardProps): ReactElement => { - {t('Stats_Total_Livechat_Rooms')} + {t('Stats_Total_Livechat_Rooms')} } value={statistics.totalLivechat} /> - - - {t('Total_messages')} + + + {t('Total_messages')} - - - - + + + + - + ); }; diff --git a/apps/meteor/client/views/admin/settings/SettingsGroupCard.tsx b/apps/meteor/client/views/admin/settings/SettingsGroupCard.tsx index 8bf75c3753d5..26331379a9fd 100644 --- a/apps/meteor/client/views/admin/settings/SettingsGroupCard.tsx +++ b/apps/meteor/client/views/admin/settings/SettingsGroupCard.tsx @@ -1,7 +1,7 @@ import type { ISetting } from '@rocket.chat/core-typings'; import { css } from '@rocket.chat/css-in-js'; import { Button, Box } from '@rocket.chat/fuselage'; -import { Card } from '@rocket.chat/ui-client'; +import { Card, CardBody, CardTitle, CardFooter } from '@rocket.chat/ui-client'; import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useRouter, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; @@ -28,13 +28,13 @@ const SettingsGroupCard = ({ id, title, description }: SettingsGroupCardProps): return ( - {t(title)} - + {t(title)} + {description && t.has(description) && } - - + + - + ); }; diff --git a/apps/meteor/client/views/home/cards/AddUsersCard.tsx b/apps/meteor/client/views/home/cards/AddUsersCard.tsx index fe36c6e6f165..551552a182d4 100644 --- a/apps/meteor/client/views/home/cards/AddUsersCard.tsx +++ b/apps/meteor/client/views/home/cards/AddUsersCard.tsx @@ -1,5 +1,5 @@ import { Button } from '@rocket.chat/fuselage'; -import { Card } from '@rocket.chat/ui-client'; +import { Card, CardBody, CardFooter, CardFooterWrapper, CardTitle } from '@rocket.chat/ui-client'; import { useTranslation, useRoute } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; @@ -14,15 +14,15 @@ const AddUsersCard = (): ReactElement => { return ( - {t('Add_users')} - {t('Invite_and_add_members_to_this_workspace_to_start_communicating')} - - + {t('Add_users')} + {t('Invite_and_add_members_to_this_workspace_to_start_communicating')} + + - - + + ); }; diff --git a/apps/meteor/client/views/home/cards/CreateChannelsCard.tsx b/apps/meteor/client/views/home/cards/CreateChannelsCard.tsx index f5421bd821e4..9e84a8ff373d 100644 --- a/apps/meteor/client/views/home/cards/CreateChannelsCard.tsx +++ b/apps/meteor/client/views/home/cards/CreateChannelsCard.tsx @@ -1,5 +1,5 @@ import { Button } from '@rocket.chat/fuselage'; -import { Card } from '@rocket.chat/ui-client'; +import { Card, CardBody, CardFooter, CardFooterWrapper, CardTitle } from '@rocket.chat/ui-client'; import { useTranslation, useSetModal } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; @@ -14,13 +14,13 @@ const CreateChannelsCard = (): ReactElement => { return ( - {t('Create_channels')} - {t('Create_a_public_channel_that_new_workspace_members_can_join')} - - + {t('Create_channels')} + {t('Create_a_public_channel_that_new_workspace_members_can_join')} + + - - + + ); }; diff --git a/apps/meteor/client/views/home/cards/CustomContentCard.tsx b/apps/meteor/client/views/home/cards/CustomContentCard.tsx index 118fa659e640..0f5c40ad87f9 100644 --- a/apps/meteor/client/views/home/cards/CustomContentCard.tsx +++ b/apps/meteor/client/views/home/cards/CustomContentCard.tsx @@ -1,5 +1,5 @@ import { Box, Button, Icon, Tag } from '@rocket.chat/fuselage'; -import { Card } from '@rocket.chat/ui-client'; +import { Card, CardFooter, CardFooterWrapper } from '@rocket.chat/ui-client'; import { useRole, useSettingSetValue, useSetting, useToastMessageDispatch, useTranslation, useRoute } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; @@ -63,8 +63,8 @@ const CustomContentCard = (): ReactElement | null => { {isCustomContentBodyEmpty ? t('Homepage_Custom_Content_Default_Message') : } - - + + @@ -86,8 +86,8 @@ const CustomContentCard = (): ReactElement | null => { > {!isCustomContentOnly ? t('Show_Only_This_Content') : t('Show_default_content')} - - + + ); } diff --git a/apps/meteor/client/views/home/cards/DesktopAppsCard.tsx b/apps/meteor/client/views/home/cards/DesktopAppsCard.tsx index b53d6603ccac..5a3c7834b3b4 100644 --- a/apps/meteor/client/views/home/cards/DesktopAppsCard.tsx +++ b/apps/meteor/client/views/home/cards/DesktopAppsCard.tsx @@ -1,5 +1,5 @@ import { Button } from '@rocket.chat/fuselage'; -import { Card } from '@rocket.chat/ui-client'; +import { Card, CardBody, CardFooter, CardFooterWrapper, CardTitle } from '@rocket.chat/ui-client'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; @@ -16,15 +16,15 @@ const DesktopAppsCard = (): ReactElement => { return ( - {t('Desktop_apps')} - {t('Install_rocket_chat_on_your_preferred_desktop_platform')} - - + {t('Desktop_apps')} + {t('Install_rocket_chat_on_your_preferred_desktop_platform')} + + - - + + ); }; diff --git a/apps/meteor/client/views/home/cards/DocumentationCard.tsx b/apps/meteor/client/views/home/cards/DocumentationCard.tsx index c833f7425a7b..2ae8c0618093 100644 --- a/apps/meteor/client/views/home/cards/DocumentationCard.tsx +++ b/apps/meteor/client/views/home/cards/DocumentationCard.tsx @@ -1,5 +1,5 @@ import { Button } from '@rocket.chat/fuselage'; -import { Card } from '@rocket.chat/ui-client'; +import { Card, CardBody, CardFooter, CardFooterWrapper, CardTitle } from '@rocket.chat/ui-client'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; @@ -14,13 +14,13 @@ const DocumentationCard = (): ReactElement => { return ( - {t('Documentation')} - {t('Learn_how_to_unlock_the_myriad_possibilities_of_rocket_chat')} - - + {t('Documentation')} + {t('Learn_how_to_unlock_the_myriad_possibilities_of_rocket_chat')} + + - - + + ); }; diff --git a/apps/meteor/client/views/home/cards/JoinRoomsCard.tsx b/apps/meteor/client/views/home/cards/JoinRoomsCard.tsx index 1a105ea8d58f..c4bfcc36d7e7 100644 --- a/apps/meteor/client/views/home/cards/JoinRoomsCard.tsx +++ b/apps/meteor/client/views/home/cards/JoinRoomsCard.tsx @@ -1,5 +1,5 @@ import { Button } from '@rocket.chat/fuselage'; -import { Card } from '@rocket.chat/ui-client'; +import { Card, CardBody, CardFooter, CardFooterWrapper, CardTitle } from '@rocket.chat/ui-client'; import { useTranslation, useRouter } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; @@ -14,13 +14,13 @@ const JoinRoomsCard = (): ReactElement => { return ( - {t('Join_rooms')} - {t('Discover_public_channels_and_teams_in_the_workspace_directory')} - - + {t('Join_rooms')} + {t('Discover_public_channels_and_teams_in_the_workspace_directory')} + + - - + + ); }; diff --git a/apps/meteor/client/views/home/cards/MobileAppsCard.tsx b/apps/meteor/client/views/home/cards/MobileAppsCard.tsx index e04097185f42..5527e8d3e0f5 100644 --- a/apps/meteor/client/views/home/cards/MobileAppsCard.tsx +++ b/apps/meteor/client/views/home/cards/MobileAppsCard.tsx @@ -1,5 +1,5 @@ import { Button } from '@rocket.chat/fuselage'; -import { Card } from '@rocket.chat/ui-client'; +import { Card, CardTitle, CardBody, CardFooterWrapper, CardFooter } from '@rocket.chat/ui-client'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; @@ -15,14 +15,14 @@ const MobileAppsCard = (): ReactElement => { return ( - {t('Mobile_apps')} - {t('Take_rocket_chat_with_you_with_mobile_applications')} - - + {t('Mobile_apps')} + {t('Take_rocket_chat_with_you_with_mobile_applications')} + + - - + + ); }; diff --git a/apps/meteor/ee/client/omnichannel/reports/components/ReportCard.tsx b/apps/meteor/ee/client/omnichannel/reports/components/ReportCard.tsx index 92a4d8d44199..d885242f7722 100644 --- a/apps/meteor/ee/client/omnichannel/reports/components/ReportCard.tsx +++ b/apps/meteor/ee/client/omnichannel/reports/components/ReportCard.tsx @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/naming-convention */ import { Box, Skeleton, States, StatesIcon, StatesSubtitle, StatesTitle } from '@rocket.chat/fuselage'; -import { Card } from '@rocket.chat/ui-client'; +import { Card, CardBody, CardCol, CardTitle } from '@rocket.chat/ui-client'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactNode, ComponentProps, ReactElement } from 'react'; import React from 'react'; @@ -61,7 +61,7 @@ export const ReportCard = ({ data-qa={id} aria-busy={isLoading} > - + @@ -76,9 +76,9 @@ export const ReportCard = ({ - - - + + + {isLoading && LoadingSkeleton} @@ -94,8 +94,8 @@ export const ReportCard = ({ {!isLoading && isDataFound && children} - - + + ); }; diff --git a/apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardCard.tsx b/apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardCard.tsx index 7c4965bce5d0..0fe32a90d825 100644 --- a/apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardCard.tsx +++ b/apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardCard.tsx @@ -1,5 +1,5 @@ import { Box } from '@rocket.chat/fuselage'; -import { Card } from '@rocket.chat/ui-client'; +import { Card, CardBody, CardCol, CardTitle } from '@rocket.chat/ui-client'; import type { ReactElement, ReactNode } from 'react'; import React from 'react'; @@ -13,14 +13,14 @@ type EngagementDashboardCardProps = { const EngagementDashboardCard = ({ children, title = undefined }: EngagementDashboardCardProps): ReactElement => ( - {title && {title}} - - + {title && {title}} + + {children} - - + + ); diff --git a/apps/meteor/ee/client/views/admin/info/SeatsCard.tsx b/apps/meteor/ee/client/views/admin/info/SeatsCard.tsx index ddcfe312122f..b595dd9c1fae 100644 --- a/apps/meteor/ee/client/views/admin/info/SeatsCard.tsx +++ b/apps/meteor/ee/client/views/admin/info/SeatsCard.tsx @@ -1,6 +1,6 @@ import { Box, Button, ButtonGroup, Skeleton } from '@rocket.chat/fuselage'; import colors from '@rocket.chat/fuselage-tokens/colors'; -import { Card } from '@rocket.chat/ui-client'; +import { Card, CardBody, CardCol, CardFooter, CardTitle } from '@rocket.chat/ui-client'; import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; @@ -27,9 +27,9 @@ const SeatsCard = ({ seatsCap }: SeatsCardProps): ReactElement => { return ( - {t('Seats_usage')} - - + {t('Seats_usage')} + + {!seatsCap ? ( @@ -43,15 +43,15 @@ const SeatsCard = ({ seatsCap }: SeatsCardProps): ReactElement => { /> )} - - - + + + - + ); }; diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 562908ff67a9..2e288c8dbd30 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -469,4 +469,4 @@ "installConfig": { "hoistingLimits": "workspaces" } -} \ No newline at end of file +} diff --git a/packages/ui-client/src/components/Card/Card.stories.tsx b/packages/ui-client/src/components/Card/Card.stories.tsx index 4697443e9259..56a2220219ed 100644 --- a/packages/ui-client/src/components/Card/Card.stories.tsx +++ b/packages/ui-client/src/components/Card/Card.stories.tsx @@ -1,7 +1,7 @@ import { Box, Button, ButtonGroup } from '@rocket.chat/fuselage'; import type { ComponentMeta, ComponentStory } from '@storybook/react'; -import Card from '.'; +import { Card, CardBody, CardCol, CardColSection, CardColTitle, CardDivider, CardFooter, CardIcon, CardTitle } from '.'; import TextSeparator from '../TextSeparator'; import { UserStatus } from '../UserStatus'; @@ -9,13 +9,13 @@ export default { title: 'Components/Card', component: Card, subcomponents: { - 'Card.Title': Card.Title, - 'Card.Body': Card.Body, - 'Card.Col': Card.Col, - 'Card.Col.Section': Card.Col.Section, - 'Card.Col.Title': Card.Col.Title, - 'Card.Footer': Card.Footer, - 'Card.Divider': Card.Divider, + CardTitle, + CardBody, + CardCol, + CardColSection, + CardColTitle, + CardFooter, + CardDivider, }, parameters: { layout: 'centered', @@ -25,14 +25,14 @@ export default { export const Example: ComponentStory = () => ( - Usage - - - Users + Usage + + + Users - Total + Total } value={123} @@ -40,9 +40,9 @@ export const Example: ComponentStory = () => ( - + - {' '} + {' '} Online } @@ -51,9 +51,9 @@ export const Example: ComponentStory = () => ( - + - {' '} + {' '} Busy } @@ -62,9 +62,9 @@ export const Example: ComponentStory = () => ( - + - {' '} + {' '} Away } @@ -73,111 +73,111 @@ export const Example: ComponentStory = () => ( - + - {' '} + {' '} Offline } value={123} /> - - - Types and Distribution + + + Types and Distribution - - - Uploads + + + Uploads - - + + ); export const Single: ComponentStory = () => ( - A card - - + A card + + - A Section + A Section
A bunch of stuff
A bunch of stuff
A bunch of stuff
A bunch of stuff
- Another Section + Another Section
A bunch of stuff
A bunch of stuff
A bunch of stuff
A bunch of stuff
-
-
- + + + - +
); export const Double: ComponentStory = () => ( - A card - - + A card + + - A Section + A Section
A bunch of stuff
A bunch of stuff
A bunch of stuff
A bunch of stuff
- Another Section + Another Section
A bunch of stuff
A bunch of stuff
A bunch of stuff
A bunch of stuff
-
- - + + + - A Section + A Section - A bunch of stuff + A bunch of stuff - A bunch of stuff + A bunch of stuff - A bunch of stuff + A bunch of stuff - A bunch of stuff + A bunch of stuff - Another Section + Another Section
A bunch of stuff
A bunch of stuff
A bunch of stuff
A bunch of stuff
-
-
- + + + - +
); diff --git a/packages/ui-client/src/components/Card/index.ts b/packages/ui-client/src/components/Card/index.ts index ade5391e61d6..76df65e5a809 100644 --- a/packages/ui-client/src/components/Card/index.ts +++ b/packages/ui-client/src/components/Card/index.ts @@ -1,30 +1,10 @@ -import Card from './Card'; -import CardBody from './CardBody'; -import CardCol from './CardCol'; -import CardColSection from './CardColSection'; -import CardColTitle from './CardColTitle'; -import CardDivider from './CardDivider'; -import CardFooter from './CardFooter'; -import CardFooterWrapper from './CardFooterWrapper'; -import CardIcon from './CardIcon'; -import CardTitle from './CardTitle'; - -export const DOUBLE_COLUMN_CARD_WIDTH = 552; - -/** - * @deprecated Avoid default usage, use named imports instead - */ -export default Object.assign(Card, { - Title: CardTitle, - Body: CardBody, - Col: Object.assign(CardCol, { - Title: CardColTitle, - Section: CardColSection, - }), - Footer: CardFooter, - FooterWrapper: CardFooterWrapper, - Divider: CardDivider, - Icon: CardIcon, -}); - -export { Card, CardBody, CardCol, CardColSection, CardColTitle, CardDivider, CardFooter, CardFooterWrapper, CardIcon, CardTitle }; +export { default as Card } from './Card'; +export { default as CardBody } from './CardBody'; +export { default as CardCol } from './CardCol'; +export { default as CardColSection } from './CardColSection'; +export { default as CardColTitle } from './CardColTitle'; +export { default as CardDivider } from './CardDivider'; +export { default as CardFooter } from './CardFooter'; +export { default as CardFooterWrapper } from './CardFooterWrapper'; +export { default as CardIcon } from './CardIcon'; +export { default as CardTitle } from './CardTitle'; diff --git a/packages/ui-client/src/components/index.ts b/packages/ui-client/src/components/index.ts index 6448f37b3e68..3de4bf411a5e 100644 --- a/packages/ui-client/src/components/index.ts +++ b/packages/ui-client/src/components/index.ts @@ -7,7 +7,7 @@ export * from '../hooks/useValidatePassword'; export { default as TextSeparator } from './TextSeparator'; export * from './TooltipComponent'; export * as UserStatus from './UserStatus'; -export { default as Card } from './Card'; +export * from './Card'; export * from './Header'; export * from './MultiSelectCustom/MultiSelectCustom'; export * from './FeaturePreview/FeaturePreview';