From 813cdfbe4530c98b08ab110bf260b92babe40bd6 Mon Sep 17 00:00:00 2001 From: Filipe Marins Date: Tue, 14 Feb 2023 16:20:08 -0300 Subject: [PATCH] [NEW] [EE] PDF Chat transcript for Omnichannel conversations (#27572) Co-authored-by: Kevin Aleman Co-authored-by: Murtaza Patrawala <34130764+murtaza98@users.noreply.github.com> Co-authored-by: murtaza98 Co-authored-by: Diego Sampaio --- .hygen.js | 5 + _templates/service/new/package.json.ejs.t | 5 +- _templates/service/new/service.ejs.t | 2 +- .../app/apps/server/bridges/livechat.ts | 5 +- .../app/file-upload/server/lib/FileUpload.js | 5 + .../closeOmnichannelConversations.ts | 10 +- .../server/functions/setUserActiveStatus.ts | 9 +- .../meteor/app/lib/server/startup/settings.ts | 6 + .../meteor/app/livechat/server/api/v1/room.ts | 121 +- .../livechat/server/hooks/beforeCloseRoom.js | 46 - .../server/hooks/processRoomAbandonment.js | 10 +- .../hooks/sendEmailTranscriptOnClose.ts | 71 + .../app/livechat/server/hooks/sendToCRM.js | 9 +- .../server/hooks/sendTranscriptOnClose.js | 20 - apps/meteor/app/livechat/server/index.js | 4 +- .../app/livechat/server/lib/Livechat.js | 113 +- .../livechat/server/lib/LivechatTyped.d.ts | 31 + .../app/livechat/server/lib/LivechatTyped.ts | 182 ++ .../app/livechat/server/lib/QueueManager.js | 3 +- .../livechat/server/lib/stream/agentStatus.ts | 2 +- .../livechat/server/methods/closeByVisitor.js | 23 - .../app/livechat/server/methods/closeRoom.js | 43 - .../app/livechat/server/methods/closeRoom.ts | 129 + .../server/methods/discardTranscript.js | 32 - .../server/methods/discardTranscript.ts | 36 + .../server/methods/removeAllClosedRooms.js | 26 - .../server/methods/removeAllClosedRooms.ts | 30 + .../methods/{removeRoom.js => removeRoom.ts} | 9 +- .../server/methods/requestTranscript.js | 24 - .../server/methods/requestTranscript.ts | 29 + .../roomAccessValidator.compatibility.js | 10 + .../models/server/models/LivechatInquiry.ts | 9 +- .../app/models/server/models/LivechatRooms.js | 62 +- .../Omnichannel/modals/CloseChatModal.tsx | 127 +- .../Omnichannel/modals/CloseChatModalData.tsx | 9 +- .../Omnichannel/modals/TranscriptModal.tsx | 7 +- .../file/GenericFileAttachment.tsx | 6 +- .../OmnichannelPreferencesPage.tsx | 68 + .../PreferencesConversationTranscript.tsx | 64 + apps/meteor/client/views/account/routes.tsx | 5 + .../client/views/account/sidebarItems.ts | 8 +- .../Omnichannel/OmnichannelRoomHeader.tsx | 20 +- .../Omnichannel/QuickActions/QuickActions.tsx | 9 +- .../QuickActions/ToolBoxActionOptions.tsx | 49 + .../QuickActions/hooks/useQuickActions.tsx | 79 +- .../views/room/Header/ToolBox/ToolBox.tsx | 2 +- .../room/lib/QuickActions/defaultActions.ts | 15 +- .../views/room/lib/QuickActions/index.tsx | 12 + .../views/room/providers/RoomProvider.tsx | 6 +- .../ee/app/license/server/getStatistics.ts | 22 +- .../livechat-enterprise/server/api/index.ts | 1 + .../server/api/transcript.ts | 37 + .../server/hooks/{index.js => index.ts} | 1 + .../server/hooks/onAgentAssignmentFailed.ts | 6 +- .../server/hooks/onCloseLivechat.js | 8 +- .../server/hooks/scheduleAutoTransfer.ts | 20 +- .../server/hooks/sendPdfTranscriptOnClose.ts | 42 + .../server/lib/AutoCloseOnHoldScheduler.ts | 17 +- .../server/lib/QueueInactivityMonitor.ts | 25 +- .../server/lib/VisitorInactivityMonitor.js | 4 +- .../livechat-enterprise/server/permissions.ts | 2 + .../livechat-enterprise/server/settings.ts | 6 + .../server/services/voipService.ts | 6 +- .../ee/client/omnichannel/tags/TagsRoute.js | 9 +- apps/meteor/ee/server/NetworkBroker.ts | 5 +- .../ee/server/lib/registerServiceModels.ts | 6 + .../ee/server/models/raw/LivechatRooms.ts | 13 +- .../ee/server/services/docker-compose.yml | 22 + apps/meteor/lib/callbacks.ts | 5 +- apps/meteor/package.json | 6 + .../rocketchat-i18n/i18n/en.i18n.json | 31 +- .../server/methods/saveUserPreferences.js | 2 + .../server/models/raw/LivechatInquiry.ts | 6 +- .../meteor/server/models/raw/LivechatRooms.js | 107 + apps/meteor/server/models/raw/Messages.ts | 25 + apps/meteor/server/models/raw/VoipRoom.ts | 4 +- .../server/services/messages/service.ts | 13 + .../services/omnichannel-voip/service.ts | 10 +- apps/meteor/server/services/room/service.ts | 13 + .../server/services/settings/service.ts | 12 + apps/meteor/server/services/startup.ts | 14 + .../server/services/translation/service.ts | 35 + apps/meteor/server/services/upload/service.ts | 24 +- apps/meteor/tests/data/livechat/inboxes.ts | 1 - .../e2e/omnichannel-send-transcript.spec.ts | 11 +- .../page-objects/fragments/home-content.ts | 8 + .../tests/end-to-end/api/00-miscellaneous.js | 4 +- .../tests/end-to-end/api/livechat/00-rooms.ts | 104 +- .../end-to-end/api/livechat/11-email-inbox.ts | 80 +- .../modals/TranscriptModal.spec.tsx | 58 + docker-compose-ci.yml | 32 + docker-compose-local.yml | 171 ++ ee/apps/omnichannel-transcript/.eslintrc.json | 4 + ee/apps/omnichannel-transcript/Dockerfile | 49 + ee/apps/omnichannel-transcript/package.json | 57 + ee/apps/omnichannel-transcript/src/service.ts | 41 + ee/apps/omnichannel-transcript/tsconfig.json | 11 + ee/apps/presence-service/package.json | 5 +- ee/apps/queue-worker/.eslintrc.json | 4 + ee/apps/queue-worker/Dockerfile | 49 + ee/apps/queue-worker/package.json | 54 + ee/apps/queue-worker/src/service.ts | 41 + ee/apps/queue-worker/tsconfig.json | 11 + .../omnichannel-services/.eslintrc.json | 4 + ee/packages/omnichannel-services/package.json | 46 + .../src/OmnichannelTranscript.ts | 382 +++ .../omnichannel-services/src/QueueWorker.ts | 165 ++ .../src/externals/mongo-message-queue.d.ts | 34 + ee/packages/omnichannel-services/src/index.ts | 2 + .../omnichannel-services/tsconfig.json | 9 + ee/packages/pdf-worker/.eslintrc.json | 4 + ee/packages/pdf-worker/.storybook/main.js | 6 + ee/packages/pdf-worker/.storybook/preview.js | 25 + ee/packages/pdf-worker/README.md | 56 + ee/packages/pdf-worker/jest.config.js | 17 + ee/packages/pdf-worker/package.json | 51 + ee/packages/pdf-worker/src/index.ts | 42 + .../pdf-worker/src/public/fira-code700.ttf | Bin 0 -> 61584 bytes .../pdf-worker/src/public/inter400-italic.ttf | Bin 0 -> 75540 bytes .../pdf-worker/src/public/inter400.ttf | Bin 0 -> 46500 bytes .../pdf-worker/src/public/inter500-italic.ttf | Bin 0 -> 81472 bytes .../pdf-worker/src/public/inter500.ttf | Bin 0 -> 46760 bytes .../pdf-worker/src/public/inter700-italic.ttf | Bin 0 -> 78608 bytes .../pdf-worker/src/public/inter700.ttf | Bin 0 -> 46920 bytes .../src/strategies/ChatTranscript.spec.ts | 60 + .../src/strategies/ChatTranscript.ts | 84 + .../ChatTranscript/ChatTranscript.fixtures.ts | 242 ++ .../ChatTranscript/ChatTranscript.stories.tsx | 39 + .../ChatTranscript/components/Divider.tsx | 28 + .../ChatTranscript/components/Files.spec.tsx | 33 + .../ChatTranscript/components/Files.tsx | 46 + .../ChatTranscript/components/Header.tsx | 67 + .../components/MessageHeader.tsx | 27 + .../components/MessageList.spec.tsx | 40 + .../ChatTranscript/components/MessageList.tsx | 33 + .../src/templates/ChatTranscript/index.tsx | 89 + .../markup/blocks/BigEmojiBlock.tsx | 18 + .../markup/blocks/CodeBlock.tsx | 21 + .../markup/blocks/HeadingBlock.tsx | 18 + .../markup/blocks/OrderedListBlock.tsx | 33 + .../markup/blocks/ParagraphBlock.tsx | 16 + .../markup/blocks/UnorderedListBlock.tsx | 33 + .../markup/elements/BoldSpan.tsx | 41 + .../markup/elements/CodeSpan.tsx | 31 + .../markup/elements/EmojiSpan.tsx | 9 + .../markup/elements/InlineElements.tsx | 57 + .../markup/elements/ItalicSpan.tsx | 41 + .../markup/elements/LinkSpan.tsx | 43 + .../markup/elements/StrikeSpan.tsx | 41 + .../templates/ChatTranscript/markup/index.tsx | 42 + ee/packages/pdf-worker/src/types/Data.ts | 5 + ee/packages/pdf-worker/src/types/IStrategy.ts | 6 + .../pdf-worker/src/types/emoji-toolkit.ts | 3 + ee/packages/pdf-worker/tsconfig.json | 10 + packages/core-services/src/index.ts | 16 + packages/core-services/src/lib/Api.ts | 4 +- packages/core-services/src/types/IBroker.ts | 2 +- .../src/types/IMessageService.ts | 5 + .../types/IOmnichannelTranscriptService.ts | 6 + .../src/types/IQueueWorkerService.ts | 12 + .../core-services/src/types/IRoomService.ts | 1 + .../src/types/ISettingsService.ts | 3 + .../src/types/ITranslationService.ts | 7 + .../core-services/src/types/IUploadService.ts | 5 +- packages/core-typings/src/IRoom.ts | 39 +- .../src/surfaces/createSurfaceRenderer.tsx | 1 + packages/livechat/src/lib/room.js | 2 + .../src/models/ILivechatInquiryModel.ts | 3 +- .../src/models/ILivechatRoomsModel.ts | 14 +- .../src/models/IMessagesModel.ts | 7 +- .../src/models/IVoipRoomModel.ts | 4 +- packages/rest-typings/src/v1/omnichannel.ts | 73 + .../v1/users/UsersSetPreferenceParamsPOST.ts | 10 + packages/rest-typings/src/v1/voip.ts | 2 +- packages/tools/.eslintrc.json | 4 + packages/tools/package.json | 27 + packages/tools/src/index.ts | 1 + packages/tools/src/timezone.ts | 14 + packages/tools/tsconfig.json | 9 + .../Header/ToolBox/ToolBoxAction.tsx | 37 +- yarn.lock | 2640 ++++++++++++++++- 181 files changed, 7315 insertions(+), 693 deletions(-) create mode 100644 .hygen.js delete mode 100644 apps/meteor/app/livechat/server/hooks/beforeCloseRoom.js create mode 100644 apps/meteor/app/livechat/server/hooks/sendEmailTranscriptOnClose.ts delete mode 100644 apps/meteor/app/livechat/server/hooks/sendTranscriptOnClose.js create mode 100644 apps/meteor/app/livechat/server/lib/LivechatTyped.d.ts create mode 100644 apps/meteor/app/livechat/server/lib/LivechatTyped.ts delete mode 100644 apps/meteor/app/livechat/server/methods/closeByVisitor.js delete mode 100644 apps/meteor/app/livechat/server/methods/closeRoom.js create mode 100644 apps/meteor/app/livechat/server/methods/closeRoom.ts delete mode 100644 apps/meteor/app/livechat/server/methods/discardTranscript.js create mode 100644 apps/meteor/app/livechat/server/methods/discardTranscript.ts delete mode 100644 apps/meteor/app/livechat/server/methods/removeAllClosedRooms.js create mode 100644 apps/meteor/app/livechat/server/methods/removeAllClosedRooms.ts rename apps/meteor/app/livechat/server/methods/{removeRoom.js => removeRoom.ts} (72%) delete mode 100644 apps/meteor/app/livechat/server/methods/requestTranscript.js create mode 100644 apps/meteor/app/livechat/server/methods/requestTranscript.ts create mode 100644 apps/meteor/client/views/account/omnichannel/OmnichannelPreferencesPage.tsx create mode 100644 apps/meteor/client/views/account/omnichannel/PreferencesConversationTranscript.tsx create mode 100644 apps/meteor/client/views/room/Header/Omnichannel/QuickActions/ToolBoxActionOptions.tsx create mode 100644 apps/meteor/ee/app/livechat-enterprise/server/api/transcript.ts rename apps/meteor/ee/app/livechat-enterprise/server/hooks/{index.js => index.ts} (95%) create mode 100644 apps/meteor/ee/app/livechat-enterprise/server/hooks/sendPdfTranscriptOnClose.ts create mode 100644 apps/meteor/server/services/messages/service.ts create mode 100644 apps/meteor/server/services/settings/service.ts create mode 100644 apps/meteor/server/services/translation/service.ts create mode 100644 apps/meteor/tests/unit/client/components/Omnichannel/modals/TranscriptModal.spec.tsx create mode 100644 docker-compose-local.yml create mode 100644 ee/apps/omnichannel-transcript/.eslintrc.json create mode 100644 ee/apps/omnichannel-transcript/Dockerfile create mode 100644 ee/apps/omnichannel-transcript/package.json create mode 100644 ee/apps/omnichannel-transcript/src/service.ts create mode 100644 ee/apps/omnichannel-transcript/tsconfig.json create mode 100644 ee/apps/queue-worker/.eslintrc.json create mode 100644 ee/apps/queue-worker/Dockerfile create mode 100644 ee/apps/queue-worker/package.json create mode 100644 ee/apps/queue-worker/src/service.ts create mode 100644 ee/apps/queue-worker/tsconfig.json create mode 100644 ee/packages/omnichannel-services/.eslintrc.json create mode 100644 ee/packages/omnichannel-services/package.json create mode 100644 ee/packages/omnichannel-services/src/OmnichannelTranscript.ts create mode 100644 ee/packages/omnichannel-services/src/QueueWorker.ts create mode 100644 ee/packages/omnichannel-services/src/externals/mongo-message-queue.d.ts create mode 100644 ee/packages/omnichannel-services/src/index.ts create mode 100644 ee/packages/omnichannel-services/tsconfig.json create mode 100644 ee/packages/pdf-worker/.eslintrc.json create mode 100644 ee/packages/pdf-worker/.storybook/main.js create mode 100644 ee/packages/pdf-worker/.storybook/preview.js create mode 100644 ee/packages/pdf-worker/README.md create mode 100644 ee/packages/pdf-worker/jest.config.js create mode 100644 ee/packages/pdf-worker/package.json create mode 100644 ee/packages/pdf-worker/src/index.ts create mode 100644 ee/packages/pdf-worker/src/public/fira-code700.ttf create mode 100644 ee/packages/pdf-worker/src/public/inter400-italic.ttf create mode 100644 ee/packages/pdf-worker/src/public/inter400.ttf create mode 100644 ee/packages/pdf-worker/src/public/inter500-italic.ttf create mode 100644 ee/packages/pdf-worker/src/public/inter500.ttf create mode 100644 ee/packages/pdf-worker/src/public/inter700-italic.ttf create mode 100644 ee/packages/pdf-worker/src/public/inter700.ttf create mode 100644 ee/packages/pdf-worker/src/strategies/ChatTranscript.spec.ts create mode 100644 ee/packages/pdf-worker/src/strategies/ChatTranscript.ts create mode 100644 ee/packages/pdf-worker/src/templates/ChatTranscript/ChatTranscript.fixtures.ts create mode 100644 ee/packages/pdf-worker/src/templates/ChatTranscript/ChatTranscript.stories.tsx create mode 100644 ee/packages/pdf-worker/src/templates/ChatTranscript/components/Divider.tsx create mode 100644 ee/packages/pdf-worker/src/templates/ChatTranscript/components/Files.spec.tsx create mode 100644 ee/packages/pdf-worker/src/templates/ChatTranscript/components/Files.tsx create mode 100644 ee/packages/pdf-worker/src/templates/ChatTranscript/components/Header.tsx create mode 100644 ee/packages/pdf-worker/src/templates/ChatTranscript/components/MessageHeader.tsx create mode 100644 ee/packages/pdf-worker/src/templates/ChatTranscript/components/MessageList.spec.tsx create mode 100644 ee/packages/pdf-worker/src/templates/ChatTranscript/components/MessageList.tsx create mode 100644 ee/packages/pdf-worker/src/templates/ChatTranscript/index.tsx create mode 100644 ee/packages/pdf-worker/src/templates/ChatTranscript/markup/blocks/BigEmojiBlock.tsx create mode 100644 ee/packages/pdf-worker/src/templates/ChatTranscript/markup/blocks/CodeBlock.tsx create mode 100644 ee/packages/pdf-worker/src/templates/ChatTranscript/markup/blocks/HeadingBlock.tsx create mode 100644 ee/packages/pdf-worker/src/templates/ChatTranscript/markup/blocks/OrderedListBlock.tsx create mode 100644 ee/packages/pdf-worker/src/templates/ChatTranscript/markup/blocks/ParagraphBlock.tsx create mode 100644 ee/packages/pdf-worker/src/templates/ChatTranscript/markup/blocks/UnorderedListBlock.tsx create mode 100644 ee/packages/pdf-worker/src/templates/ChatTranscript/markup/elements/BoldSpan.tsx create mode 100644 ee/packages/pdf-worker/src/templates/ChatTranscript/markup/elements/CodeSpan.tsx create mode 100644 ee/packages/pdf-worker/src/templates/ChatTranscript/markup/elements/EmojiSpan.tsx create mode 100644 ee/packages/pdf-worker/src/templates/ChatTranscript/markup/elements/InlineElements.tsx create mode 100644 ee/packages/pdf-worker/src/templates/ChatTranscript/markup/elements/ItalicSpan.tsx create mode 100644 ee/packages/pdf-worker/src/templates/ChatTranscript/markup/elements/LinkSpan.tsx create mode 100644 ee/packages/pdf-worker/src/templates/ChatTranscript/markup/elements/StrikeSpan.tsx create mode 100644 ee/packages/pdf-worker/src/templates/ChatTranscript/markup/index.tsx create mode 100644 ee/packages/pdf-worker/src/types/Data.ts create mode 100644 ee/packages/pdf-worker/src/types/IStrategy.ts create mode 100644 ee/packages/pdf-worker/src/types/emoji-toolkit.ts create mode 100644 ee/packages/pdf-worker/tsconfig.json create mode 100644 packages/core-services/src/types/IMessageService.ts create mode 100644 packages/core-services/src/types/IOmnichannelTranscriptService.ts create mode 100644 packages/core-services/src/types/IQueueWorkerService.ts create mode 100644 packages/core-services/src/types/ISettingsService.ts create mode 100644 packages/core-services/src/types/ITranslationService.ts create mode 100644 packages/tools/.eslintrc.json create mode 100644 packages/tools/package.json create mode 100644 packages/tools/src/index.ts create mode 100644 packages/tools/src/timezone.ts create mode 100644 packages/tools/tsconfig.json diff --git a/.hygen.js b/.hygen.js new file mode 100644 index 00000000000..6a43ecb7808 --- /dev/null +++ b/.hygen.js @@ -0,0 +1,5 @@ +module.exports = { + helpers: { + random: () => Math.floor(3000 + (5000 - 3000) * Math.random()), + }, +}; diff --git a/_templates/service/new/package.json.ejs.t b/_templates/service/new/package.json.ejs.t index d1b5753dc41..d3040640a68 100644 --- a/_templates/service/new/package.json.ejs.t +++ b/_templates/service/new/package.json.ejs.t @@ -47,6 +47,9 @@ to: ee/apps/<%= name %>/package.json "main": "./dist/ee/apps/<%= name %>/src/service.js", "files": [ "/dist" - ] + ], + "volta": { + "node": "14.19.3" + } } diff --git a/_templates/service/new/service.ejs.t b/_templates/service/new/service.ejs.t index 5e8c7b769da..54080d94cf0 100644 --- a/_templates/service/new/service.ejs.t +++ b/_templates/service/new/service.ejs.t @@ -9,7 +9,7 @@ import { broker } from '../../../../apps/meteor/ee/server/startup/broker'; import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; -const PORT = process.env.PORT || 3034; +const PORT = process.env.PORT || <%= h.random() %>; (async () => { const db = await getConnection(); diff --git a/apps/meteor/app/apps/server/bridges/livechat.ts b/apps/meteor/app/apps/server/bridges/livechat.ts index 73a391c7126..ba84a1f77e6 100644 --- a/apps/meteor/app/apps/server/bridges/livechat.ts +++ b/apps/meteor/app/apps/server/bridges/livechat.ts @@ -17,6 +17,7 @@ import { getRoom } from '../../../livechat/server/api/lib/livechat'; import { Livechat } from '../../../livechat/server/lib/Livechat'; import { Users, LivechatDepartment, LivechatRooms } from '../../../models/server'; import type { AppServerOrchestrator } from '../orchestrator'; +import { Livechat as LivechatTyped } from '../../../livechat/server/lib/LivechatTyped'; export class AppLivechatBridge extends LivechatBridge { // eslint-disable-next-line no-empty-function @@ -122,7 +123,9 @@ export class AppLivechatBridge extends LivechatBridge { ...(visitor && { visitor }), }; - return Livechat.closeRoom(closeData); + await LivechatTyped.closeRoom(closeData); + + return true; } protected async findRooms(visitor: IVisitor, departmentId: string | null, appId: string): Promise> { diff --git a/apps/meteor/app/file-upload/server/lib/FileUpload.js b/apps/meteor/app/file-upload/server/lib/FileUpload.js index ffd263db6f9..afcec09bc24 100644 --- a/apps/meteor/app/file-upload/server/lib/FileUpload.js +++ b/apps/meteor/app/file-upload/server/lib/FileUpload.js @@ -706,6 +706,11 @@ export class FileUploadClass { streamOrBuffer = Promise.await(streamToBuffer(streamOrBuffer)); } + if (streamOrBuffer instanceof Uint8Array) { + // Services compat :) + streamOrBuffer = Buffer.from(streamOrBuffer); + } + // Check if the fileData matches store filter const filter = this.store.getFilter(); if (filter && filter.check) { diff --git a/apps/meteor/app/lib/server/functions/closeOmnichannelConversations.ts b/apps/meteor/app/lib/server/functions/closeOmnichannelConversations.ts index 2c891c78d95..598bffad188 100644 --- a/apps/meteor/app/lib/server/functions/closeOmnichannelConversations.ts +++ b/apps/meteor/app/lib/server/functions/closeOmnichannelConversations.ts @@ -3,18 +3,22 @@ import type { IUser } from '@rocket.chat/core-typings'; import { LivechatRooms } from '../../../models/server'; import { settings } from '../../../settings/server'; -import { Livechat } from '../../../livechat/server/lib/Livechat'; +import { Livechat } from '../../../livechat/server/lib/LivechatTyped'; type SubscribedRooms = { rid: string; t: string; }; -export const closeOmnichannelConversations = (user: IUser, subscribedRooms: SubscribedRooms[]): void => { +export const closeOmnichannelConversations = async (user: IUser, subscribedRooms: SubscribedRooms[]): Promise => { const roomsInfo = LivechatRooms.findByIds(subscribedRooms.map(({ rid }) => rid)); const language = settings.get('Language') || 'en'; const comment = TAPi18n.__('Agent_deactivated', { lng: language }); + + const promises: Promise[] = []; roomsInfo.forEach((room: any) => { - Livechat.closeRoom({ user, visitor: {}, room, comment }); + promises.push(Livechat.closeRoom({ user, room, comment })); }); + + await Promise.all(promises); }; diff --git a/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts b/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts index 998074d89e6..729a9b74ab1 100644 --- a/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts +++ b/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts @@ -64,8 +64,13 @@ export function setUserActiveStatus(userId: string, active: boolean, confirmReli throw new Meteor.Error('user-last-owner', '', rooms); } - closeOmnichannelConversations(user, livechatSubscribedRooms); - Promise.await(relinquishRoomOwnerships(user, chatSubscribedRooms, false)); + Promise.await( + // We don't want one killing the other :) + Promise.allSettled([ + closeOmnichannelConversations(user, livechatSubscribedRooms), + relinquishRoomOwnerships(user, chatSubscribedRooms, false), + ]), + ); } if (active && !user.active) { diff --git a/apps/meteor/app/lib/server/startup/settings.ts b/apps/meteor/app/lib/server/startup/settings.ts index 9d371b73d0b..b6c36e32303 100644 --- a/apps/meteor/app/lib/server/startup/settings.ts +++ b/apps/meteor/app/lib/server/startup/settings.ts @@ -524,6 +524,12 @@ settingsRegistry.addGroup('Accounts', function () { public: true, i18nLabel: 'Notifications_Sound_Volume', }); + + this.add('Accounts_Default_User_Preferences_omnichannelTranscriptEmail', false, { + type: 'boolean', + public: true, + i18nLabel: 'Omnichannel_transcript_email', + }); }); this.section('Avatar', function () { diff --git a/apps/meteor/app/livechat/server/api/v1/room.ts b/apps/meteor/app/livechat/server/api/v1/room.ts index d001f81df12..8b1925e6fc3 100644 --- a/apps/meteor/app/livechat/server/api/v1/room.ts +++ b/apps/meteor/app/livechat/server/api/v1/room.ts @@ -2,9 +2,9 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; import { Random } from 'meteor/random'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import type { ILivechatAgent, IOmnichannelRoom } from '@rocket.chat/core-typings'; +import type { ILivechatAgent, IOmnichannelRoom, IUser } from '@rocket.chat/core-typings'; import { isOmnichannelRoom, OmnichannelSourceType } from '@rocket.chat/core-typings'; -import { LivechatVisitors, Users } from '@rocket.chat/models'; +import { LivechatVisitors, Users, LivechatRooms as LivechatRoomsRaw, Subscriptions } from '@rocket.chat/models'; import { isLiveChatRoomForwardProps, isPOSTLivechatRoomCloseParams, @@ -13,6 +13,7 @@ import { isLiveChatRoomJoinProps, isPUTLivechatRoomVisitorParams, isLiveChatRoomSaveInfoProps, + isPOSTLivechatRoomCloseByUserParams, } from '@rocket.chat/rest-typings'; import { settings as rcSettings } from '../../../../settings/server'; @@ -20,13 +21,16 @@ import { Messages, LivechatRooms } from '../../../../models/server'; import { API } from '../../../../api/server'; import { findGuest, findRoom, getRoom, settings, findAgent, onCheckRoomParams } from '../lib/livechat'; import { Livechat } from '../../lib/Livechat'; +import { Livechat as LivechatTyped } from '../../lib/LivechatTyped'; import { normalizeTransferredByData } from '../../lib/Helper'; import { findVisitorInfo } from '../lib/visitors'; import { canAccessRoom, hasPermission } from '../../../../authorization/server'; +import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission'; import { addUserToRoom } from '../../../../lib/server/functions'; import { apiDeprecationLogger } from '../../../../lib/server/lib/deprecationWarningLogger'; import { deprecationWarning } from '../../../../api/server/helpers/deprecationWarning'; import { callbacks } from '../../../../../lib/callbacks'; +import type { CloseRoomParams } from '../../lib/LivechatTyped.d'; const isAgentWithInfo = (agentObj: ILivechatAgent | { hiddenInfo: true }): agentObj is ILivechatAgent => !('hiddenInfo' in agentObj); @@ -86,6 +90,8 @@ API.v1.addRoute('livechat/room', { }, }); +// Note: use this route if a visitor is closing a room +// If a RC user(like eg agent) is closing a room, use the `livechat/room.closeByUser` route API.v1.addRoute( 'livechat/room.close', { validateParams: isPOSTLivechatRoomCloseParams }, @@ -110,16 +116,121 @@ API.v1.addRoute( const language = rcSettings.get('Language') || 'en'; const comment = TAPi18n.__('Closed_by_visitor', { lng: language }); - // @ts-expect-error -- typings on closeRoom are wrong - if (!Livechat.closeRoom({ visitor, room, comment })) { - return API.v1.failure(); + const options: CloseRoomParams['options'] = {}; + if (room.servedBy) { + const servingAgent: Pick | null = + await Users.findOneById(room.servedBy._id, { + projection: { + name: 1, + username: 1, + utcOffset: 1, + settings: 1, + language: 1, + }, + }); + + if (servingAgent?.settings?.preferences?.omnichannelTranscriptPDF) { + options.pdfTranscript = { + requestedBy: servingAgent._id, + }; + } + + // We'll send the transcript by email only if the setting is disabled (that means, we're not asking the user if he wants to receive the transcript by email) + // And the agent has the preference enabled to send the transcript by email and the visitor has an email address + // When Livechat_enable_transcript is enabled, the email will be sent via livechat/transcript route + if ( + !rcSettings.get('Livechat_enable_transcript') && + servingAgent?.settings?.preferences?.omnichannelTranscriptEmail && + visitor.visitorEmails?.length && + visitor.visitorEmails?.[0]?.address + ) { + const visitorEmail = visitor.visitorEmails?.[0]?.address; + + const language = servingAgent.language || rcSettings.get('Language') || 'en'; + const t = (s: string): string => TAPi18n.__(s, { lng: language }); + const subject = t('Transcript_of_your_livechat_conversation'); + + options.emailTranscript = { + sendToVisitor: true, + requestData: { + email: visitorEmail, + requestedAt: new Date(), + requestedBy: servingAgent, + subject, + }, + }; + } } + await LivechatTyped.closeRoom({ visitor, room, comment, options }); + return API.v1.success({ rid, comment }); }, }, ); +API.v1.addRoute( + 'livechat/room.closeByUser', + { + validateParams: isPOSTLivechatRoomCloseByUserParams, + authRequired: true, + permissionsRequired: ['close-livechat-room'], + }, + { + async post() { + const { rid, comment, tags, generateTranscriptPdf, transcriptEmail } = this.bodyParams; + + const room = await LivechatRoomsRaw.findOneById(rid); + if (!room || !isOmnichannelRoom(room)) { + throw new Error('error-invalid-room'); + } + + if (!room.open) { + throw new Error('error-room-already-closed'); + } + + const subscription = await Subscriptions.findOneByRoomIdAndUserId(rid, this.userId, { projection: { _id: 1 } }); + if (!subscription && !(await hasPermissionAsync(this.userId, 'close-others-livechat-room'))) { + throw new Error('error-not-authorized'); + } + + const options: CloseRoomParams['options'] = { + clientAction: true, + tags, + ...(generateTranscriptPdf && { pdfTranscript: { requestedBy: this.userId } }), + ...(transcriptEmail && { + ...(transcriptEmail.sendToVisitor + ? { + emailTranscript: { + sendToVisitor: true, + requestData: { + email: transcriptEmail.requestData.email, + subject: transcriptEmail.requestData.subject, + requestedAt: new Date(), + requestedBy: this.user, + }, + }, + } + : { + emailTranscript: { + sendToVisitor: false, + }, + }), + }), + }; + + await LivechatTyped.closeRoom({ + room, + user: this.user, + options, + comment, + }); + + return API.v1.success(); + }, + }, +); + API.v1.addRoute( 'livechat/room.transfer', { validateParams: isPOSTLivechatRoomTransferParams }, diff --git a/apps/meteor/app/livechat/server/hooks/beforeCloseRoom.js b/apps/meteor/app/livechat/server/hooks/beforeCloseRoom.js deleted file mode 100644 index 641b2bbb7b9..00000000000 --- a/apps/meteor/app/livechat/server/hooks/beforeCloseRoom.js +++ /dev/null @@ -1,46 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { callbacks } from '../../../../lib/callbacks'; -import { LivechatDepartment } from '../../../models/server'; - -const concatUnique = (...arrays) => [...new Set([].concat(...arrays.filter(Array.isArray)))]; - -const normalizeParams = (params, tags = []) => Object.assign(params, { extraData: { tags } }); - -callbacks.add( - 'livechat.beforeCloseRoom', - (originalParams = {}) => { - const { room, options } = originalParams; - const { departmentId, tags: optionsTags } = room; - const { clientAction, tags: oldRoomTags } = options; - const roomTags = concatUnique(oldRoomTags, optionsTags); - - if (!departmentId) { - return normalizeParams({ ...originalParams }, roomTags); - } - - const department = LivechatDepartment.findOneById(departmentId); - if (!department) { - return normalizeParams({ ...originalParams }, roomTags); - } - - const { requestTagBeforeClosingChat, chatClosingTags } = department; - const extraRoomTags = concatUnique(roomTags, chatClosingTags); - - if (!requestTagBeforeClosingChat) { - return normalizeParams({ ...originalParams }, extraRoomTags); - } - - const checkRoomTags = !clientAction || (roomTags && roomTags.length > 0); - const checkDepartmentTags = chatClosingTags && chatClosingTags.length > 0; - if (!checkRoomTags || !checkDepartmentTags) { - throw new Meteor.Error('error-tags-must-be-assigned-before-closing-chat', 'Tag(s) must be assigned before closing the chat', { - method: 'livechat.beforeCloseRoom', - }); - } - - return normalizeParams({ ...originalParams }, extraRoomTags); - }, - callbacks.priority.HIGH, - 'livechat-before-close-Room', -); diff --git a/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.js b/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.js index e3453f6e145..4cffd99123f 100644 --- a/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.js +++ b/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.js @@ -58,20 +58,22 @@ const getSecondsSinceLastAgentResponse = async (room, agentLastMessage) => { callbacks.add( 'livechat.closeRoom', - (room) => { + (params) => { + const { room } = params; + const closedByAgent = room.closer !== 'visitor'; const wasTheLastMessageSentByAgent = room.lastMessage && !room.lastMessage.token; if (!closedByAgent || !wasTheLastMessageSentByAgent) { - return; + return params; } const agentLastMessage = Messages.findAgentLastMessageByVisitorLastMessageTs(room._id, room.v.lastMessageTs); if (!agentLastMessage) { - return; + return params; } const secondsSinceLastAgentResponse = Promise.await(getSecondsSinceLastAgentResponse(room, agentLastMessage)); LivechatRooms.setVisitorInactivityInSecondsById(room._id, secondsSinceLastAgentResponse); - return room; + return params; }, callbacks.priority.HIGH, 'process-room-abandonment', diff --git a/apps/meteor/app/livechat/server/hooks/sendEmailTranscriptOnClose.ts b/apps/meteor/app/livechat/server/hooks/sendEmailTranscriptOnClose.ts new file mode 100644 index 00000000000..a65995dc8a6 --- /dev/null +++ b/apps/meteor/app/livechat/server/hooks/sendEmailTranscriptOnClose.ts @@ -0,0 +1,71 @@ +import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; +import { isOmnichannelRoom } from '@rocket.chat/core-typings'; +import { LivechatRooms } from '@rocket.chat/models'; + +import { callbacks } from '../../../../lib/callbacks'; +import { Livechat } from '../lib/Livechat'; +import type { CloseRoomParams } from '../lib/LivechatTyped.d'; + +type LivechatCloseCallbackParams = { + room: IOmnichannelRoom; + options: CloseRoomParams['options']; +}; + +const sendEmailTranscriptOnClose = async (params: LivechatCloseCallbackParams): Promise => { + const { room, options } = params; + + if (!isOmnichannelRoom(room)) { + return params; + } + + const { _id: rid, v: { token } = {} } = room; + if (!token) { + return params; + } + + const transcriptData = resolveTranscriptData(room, options); + if (!transcriptData) { + return params; + } + + const { email, subject, requestedBy: user } = transcriptData; + + await Promise.all([ + Livechat.sendTranscript({ token, rid, email, subject, user }), + LivechatRooms.unsetEmailTranscriptRequestedByRoomId(rid), + ]); + + delete room.transcriptRequest; + + return { + room, + options, + }; +}; + +const resolveTranscriptData = ( + room: IOmnichannelRoom, + options: LivechatCloseCallbackParams['options'] = {}, +): IOmnichannelRoom['transcriptRequest'] | undefined => { + const { transcriptRequest: roomTranscriptRequest } = room; + + const { emailTranscript: optionsTranscriptRequest } = options; + + // Note: options.emailTranscript will override the room.transcriptRequest check + // If options.emailTranscript is not set, then the room.transcriptRequest will be checked + if (optionsTranscriptRequest === undefined) { + return roomTranscriptRequest; + } + + if (!optionsTranscriptRequest.sendToVisitor) { + return undefined; + } + return optionsTranscriptRequest.requestData; +}; + +callbacks.add( + 'livechat.closeRoom', + (params: LivechatCloseCallbackParams) => Promise.await(sendEmailTranscriptOnClose(params)), + callbacks.priority.HIGH, + 'livechat-send-email-transcript-on-close-room', +); diff --git a/apps/meteor/app/livechat/server/hooks/sendToCRM.js b/apps/meteor/app/livechat/server/hooks/sendToCRM.js index 1ce109eaf75..8eb491b5df5 100644 --- a/apps/meteor/app/livechat/server/hooks/sendToCRM.js +++ b/apps/meteor/app/livechat/server/hooks/sendToCRM.js @@ -106,12 +106,15 @@ function sendToCRM(type, room, includeMessages = true) { callbacks.add( 'livechat.closeRoom', - (room) => { + (params) => { + const { room } = params; if (!settings.get('Livechat_webhook_on_close')) { - return room; + return params; } - return sendToCRM('LivechatSession', room); + sendToCRM('LivechatSession', room); + + return params; }, callbacks.priority.MEDIUM, 'livechat-send-crm-close-room', diff --git a/apps/meteor/app/livechat/server/hooks/sendTranscriptOnClose.js b/apps/meteor/app/livechat/server/hooks/sendTranscriptOnClose.js deleted file mode 100644 index 597d656b2f0..00000000000 --- a/apps/meteor/app/livechat/server/hooks/sendTranscriptOnClose.js +++ /dev/null @@ -1,20 +0,0 @@ -import { callbacks } from '../../../../lib/callbacks'; -import { Livechat } from '../lib/Livechat'; -import { LivechatRooms } from '../../../models/server'; - -const sendTranscriptOnClose = (room) => { - const { _id: rid, transcriptRequest, v: { token } = {} } = room; - if (!transcriptRequest || !token) { - return room; - } - - const { email, subject, requestedBy: user } = transcriptRequest; - // TODO: refactor this to use normal await - Promise.await(Livechat.sendTranscript({ token, rid, email, subject, user })); - - LivechatRooms.removeTranscriptRequestByRoomId(rid); - - return LivechatRooms.findOneById(rid); -}; - -callbacks.add('livechat.closeRoom', sendTranscriptOnClose, callbacks.priority.HIGH, 'livechat-send-transcript-on-close-room'); diff --git a/apps/meteor/app/livechat/server/index.js b/apps/meteor/app/livechat/server/index.js index c64dd5b181f..7be7be57b2f 100644 --- a/apps/meteor/app/livechat/server/index.js +++ b/apps/meteor/app/livechat/server/index.js @@ -2,7 +2,6 @@ import './livechat'; import './config'; import './startup'; import '../lib/messageTypes'; -import './hooks/beforeCloseRoom'; import './hooks/beforeDelegateAgent'; import './hooks/leadCapture'; import './hooks/markRoomResponded'; @@ -13,14 +12,13 @@ import './hooks/sendToCRM'; import './hooks/processRoomAbandonment'; import './hooks/saveLastVisitorMessageTs'; import './hooks/markRoomNotResponded'; -import './hooks/sendTranscriptOnClose'; +import './hooks/sendEmailTranscriptOnClose'; import './hooks/saveContactLastChat'; import './hooks/saveLastMessageToInquiry'; import './hooks/afterUserActions'; import './methods/addAgent'; import './methods/addManager'; import './methods/changeLivechatStatus'; -import './methods/closeByVisitor'; import './methods/closeRoom'; import './methods/discardTranscript'; import './methods/getCustomFields'; diff --git a/apps/meteor/app/livechat/server/lib/Livechat.js b/apps/meteor/app/livechat/server/lib/Livechat.js index 3e628793cf2..a90e199a2bb 100644 --- a/apps/meteor/app/livechat/server/lib/Livechat.js +++ b/apps/meteor/app/livechat/server/lib/Livechat.js @@ -1,3 +1,6 @@ +// Note: Please don't add any new methods to this file, since its still in js and we are migrating to ts +// Please add new methods to LivechatTyped.ts + import dns from 'dns'; import { Meteor } from 'meteor/meteor'; @@ -14,6 +17,10 @@ import { LivechatVisitors, LivechatCustomField, Settings, + LivechatRooms as LivechatRoomsRaw, + LivechatInquiry as LivechatInquiryRaw, + Subscriptions as SubscriptionsRaw, + Messages as MessagesRaw, LivechatDepartment as LivechatDepartmentRaw, } from '@rocket.chat/models'; import { VideoConf, api } from '@rocket.chat/core-services'; @@ -46,6 +53,7 @@ import { Apps, AppEvents } from '../../../apps/server'; import { businessHourManager } from '../business-hour'; import { addUserRoles } from '../../../../server/lib/roles/addUserRoles'; import { removeUserFromRoles } from '../../../../server/lib/roles/removeUserFromRoles'; +import { Livechat as LivechatTyped } from './LivechatTyped'; const logger = new Logger('Livechat'); @@ -427,76 +435,7 @@ export const Livechat = { return ret; }, - closeRoom({ user, visitor, room, comment, options = {} }) { - Livechat.logger.debug(`Attempting to close room ${room._id}`); - if (!room || room.t !== 'l' || !room.open) { - return false; - } - - const params = callbacks.run('livechat.beforeCloseRoom', { room, options }); - const { extraData } = params; - - const now = new Date(); - const { _id: rid, servedBy, transcriptRequest } = room; - const serviceTimeDuration = servedBy && (now.getTime() - servedBy.ts) / 1000; - - const closeData = { - closedAt: now, - chatDuration: (now.getTime() - room.ts) / 1000, - ...(serviceTimeDuration && { serviceTimeDuration }), - ...extraData, - }; - Livechat.logger.debug(`Room ${room._id} was closed at ${closeData.closedAt} (duration ${closeData.chatDuration})`); - - if (user) { - Livechat.logger.debug(`Closing by user ${user._id}`); - closeData.closer = 'user'; - closeData.closedBy = { - _id: user._id, - username: user.username, - }; - } else if (visitor) { - Livechat.logger.debug(`Closing by visitor ${visitor._id}`); - closeData.closer = 'visitor'; - closeData.closedBy = { - _id: visitor._id, - username: visitor.username, - }; - } - - LivechatRooms.closeByRoomId(rid, closeData); - LivechatInquiry.removeByRoomId(rid); - Subscriptions.removeByRoomId(rid); - - const message = { - t: 'livechat-close', - msg: comment, - groupable: false, - transcriptRequested: !!transcriptRequest, - }; - - // Retreive the closed room - room = LivechatRooms.findOneByIdOrName(rid); - - Livechat.logger.debug(`Sending closing message to room ${room._id}`); - sendMessage(user || visitor, message, room); - - Messages.createCommandWithRoomIdAndUser('promptTranscript', rid, closeData.closedBy); - - Meteor.defer(() => { - /** - * @deprecated the `AppEvents.ILivechatRoomClosedHandler` event will be removed - * in the next major version of the Apps-Engine - */ - Apps.getBridges().getListenerBridge().livechatEvent(AppEvents.ILivechatRoomClosedHandler, room); - Apps.getBridges().getListenerBridge().livechatEvent(AppEvents.IPostLivechatRoomClosed, room); - }); - callbacks.runAsync('livechat.closeRoom', room); - - return true; - }, - - removeRoom(rid) { + async removeRoom(rid) { Livechat.logger.debug(`Deleting room ${rid}`); check(rid, String); const room = LivechatRooms.findOneById(rid); @@ -506,10 +445,21 @@ export const Livechat = { }); } - Messages.removeByRoomId(rid); - Subscriptions.removeByRoomId(rid); - LivechatInquiry.removeByRoomId(rid); - return LivechatRooms.removeById(rid); + const result = await Promise.allSettled([ + MessagesRaw.removeByRoomId(rid), + SubscriptionsRaw.removeByRoomId(rid), + LivechatInquiryRaw.removeByRoomId(rid), + LivechatRoomsRaw.removeById(rid), + ]); + + const errors = result.filter((r) => r.status === 'rejected').map((r) => r.reason); + if (errors.length > 0) { + this.logger.error(`Error removing room ${rid}: ${errors.join(', ')}`); + throw new Meteor.Error('error-removing-room', 'Error removing room', { + method: 'livechat:removeRoom', + errors, + }); + } }, async setCustomFields({ token, key, value, overwrite } = {}) { @@ -635,12 +585,17 @@ export const Livechat = { } }, - closeOpenChats(userId, comment) { + async closeOpenChats(userId, comment) { Livechat.logger.debug(`Closing open chats for user ${userId}`); const user = Users.findOneById(userId); - LivechatRooms.findOpenByAgent(userId).forEach((room) => { - this.closeRoom({ user, room, comment }); + + const openChats = LivechatRooms.findOpenByAgent(userId); + const promises = []; + openChats.forEach((room) => { + promises.push(LivechatTyped.closeRoom({ user, room, comment })); }); + + await Promise.all(promises); }, forwardOpenChats(userId) { @@ -1249,7 +1204,7 @@ export const Livechat = { }).fetch(); }, - requestTranscript({ rid, email, subject, user }) { + async requestTranscript({ rid, email, subject, user }) { check(rid, String); check(email, String); check(subject, String); @@ -1286,7 +1241,7 @@ export const Livechat = { subject, }; - LivechatRooms.requestTranscriptByRoomId(rid, transcriptRequest); + await LivechatRoomsRaw.setEmailTranscriptRequestedByRoomId(rid, transcriptRequest); return true; }, diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.d.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.d.ts new file mode 100644 index 00000000000..ab71ccaf627 --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.d.ts @@ -0,0 +1,31 @@ +import type { IOmnichannelRoom, IUser, ILivechatVisitor } from '@rocket.chat/core-typings'; + +type GenericCloseRoomParams = { + room: IOmnichannelRoom; + comment?: string; + options?: { + clientAction?: boolean; + tags?: string[]; + emailTranscript?: + | { + sendToVisitor: false; + } + | { + sendToVisitor: true; + requestData: NonNullable; + }; + pdfTranscript?: { + requestedBy: string; + }; + }; +}; + +export type CloseRoomParamsByUser = { + user: IUser; +} & GenericCloseRoomParams; + +export type CloseRoomParamsByVisitor = { + visitor: ILivechatVisitor; +} & GenericCloseRoomParams; + +export type CloseRoomParams = CloseRoomParamsByUser | CloseRoomParamsByVisitor; diff --git a/apps/meteor/app/livechat/server/lib/LivechatTyped.ts b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts new file mode 100644 index 00000000000..10d6f582259 --- /dev/null +++ b/apps/meteor/app/livechat/server/lib/LivechatTyped.ts @@ -0,0 +1,182 @@ +// Goal is to have a typed version of apps/meteor/app/livechat/server/lib/Livechat.js +// This is a work in progress, and is not yet complete +// But it is a start. + +// Important note: Try to not use the original Livechat.js file, but use this one instead. +// If possible, move methods from Livechat.js to this file. +// This is because we want to slowly convert the code to typescript, and this is a good way to do it. +import type { IOmnichannelRoom, IOmnichannelRoomClosingInfo } from '@rocket.chat/core-typings'; +import { isOmnichannelRoom } from '@rocket.chat/core-typings'; +import { LivechatDepartment, LivechatInquiry, LivechatRooms, Subscriptions } from '@rocket.chat/models'; + +import { callbacks } from '../../../../lib/callbacks'; +import { Logger } from '../../../logger/server'; +import type { CloseRoomParams, CloseRoomParamsByUser, CloseRoomParamsByVisitor } from './LivechatTyped.d'; +import { sendMessage } from '../../../lib/server/functions/sendMessage'; +import { Apps, AppEvents } from '../../../apps/server'; +import { Messages as LegacyMessage } from '../../../models/server'; + +class LivechatClass { + logger: Logger; + + constructor() { + this.logger = new Logger('Livechat'); + } + + async closeRoom(params: CloseRoomParams): Promise { + const { comment } = params; + let { room } = params; + + this.logger.debug(`Attempting to close room ${room._id}`); + if (!room || !isOmnichannelRoom(room) || !room.open) { + this.logger.debug(`Room ${room._id} is not open`); + return; + } + + const { updatedOptions: options } = await this.resolveChatTags(room, params.options); + this.logger.debug(`Resolved chat tags for room ${room._id}`); + + const now = new Date(); + const { _id: rid, servedBy, transcriptRequest } = room; + const serviceTimeDuration = servedBy && (now.getTime() - new Date(servedBy.ts).getTime()) / 1000; + + const closeData: IOmnichannelRoomClosingInfo = { + closedAt: now, + chatDuration: (now.getTime() - new Date(room.ts).getTime()) / 1000, + ...(serviceTimeDuration && { serviceTimeDuration }), + ...options, + }; + this.logger.debug(`Room ${room._id} was closed at ${closeData.closedAt} (duration ${closeData.chatDuration})`); + + const isRoomClosedByUserParams = (params: CloseRoomParams): params is CloseRoomParamsByUser => + (params as CloseRoomParamsByUser).user !== undefined; + const isRoomClosedByVisitorParams = (params: CloseRoomParams): params is CloseRoomParamsByVisitor => + (params as CloseRoomParamsByVisitor).visitor !== undefined; + + let chatCloser: any; + if (isRoomClosedByUserParams(params)) { + const { user } = params; + this.logger.debug(`Closing by user ${user._id}`); + closeData.closer = 'user'; + closeData.closedBy = { + _id: user._id, + username: user.username, + }; + chatCloser = user; + } else if (isRoomClosedByVisitorParams(params)) { + const { visitor } = params; + this.logger.debug(`Closing by visitor ${params.visitor._id}`); + closeData.closer = 'visitor'; + closeData.closedBy = { + _id: visitor._id, + username: visitor.username, + }; + chatCloser = visitor; + } else { + throw new Error('Error: Please provide details of the user or visitor who closed the room'); + } + + this.logger.debug(`Updating DB for room ${room._id} with close data`); + + await Promise.all([ + LivechatRooms.closeRoomById(rid, closeData), + LivechatInquiry.removeByRoomId(rid), + Subscriptions.removeByRoomId(rid), + ]); + + this.logger.debug(`DB updated for room ${room._id}`); + + const message = { + t: 'livechat-close', + msg: comment, + groupable: false, + transcriptRequested: !!transcriptRequest, + }; + + // Retrieve the closed room + room = (await LivechatRooms.findOneById(rid)) as IOmnichannelRoom; + + this.logger.debug(`Sending closing message to room ${room._id}`); + sendMessage(chatCloser, message, room); + + LegacyMessage.createCommandWithRoomIdAndUser('promptTranscript', rid, closeData.closedBy); + + this.logger.debug(`Running callbacks for room ${room._id}`); + + Meteor.defer(() => { + /** + * @deprecated the `AppEvents.ILivechatRoomClosedHandler` event will be removed + * in the next major version of the Apps-Engine + */ + Apps.getBridges()?.getListenerBridge().livechatEvent(AppEvents.ILivechatRoomClosedHandler, room); + Apps.getBridges()?.getListenerBridge().livechatEvent(AppEvents.IPostLivechatRoomClosed, room); + }); + callbacks.runAsync('livechat.closeRoom', { + room, + options, + }); + + this.logger.debug(`Room ${room._id} was closed`); + } + + private async resolveChatTags( + room: IOmnichannelRoom, + options: CloseRoomParams['options'] = {}, + ): Promise<{ updatedOptions: CloseRoomParams['options'] }> { + this.logger.debug(`Resolving chat tags for room ${room._id}`); + + const concatUnique = (...arrays: (string[] | undefined)[]): string[] => [ + ...new Set(([] as string[]).concat(...arrays.filter((a): a is string[] => !!a))), + ]; + + const { departmentId, tags: optionsTags } = room; + const { clientAction, tags: oldRoomTags } = options; + const roomTags = concatUnique(oldRoomTags, optionsTags); + + if (!departmentId) { + return { + updatedOptions: { + ...options, + ...(roomTags.length && { tags: roomTags }), + }, + }; + } + + const department = await LivechatDepartment.findOneById(departmentId); + if (!department) { + return { + updatedOptions: { + ...options, + ...(roomTags.length && { tags: roomTags }), + }, + }; + } + + const { requestTagBeforeClosingChat, chatClosingTags } = department; + const extraRoomTags = concatUnique(roomTags, chatClosingTags); + + if (!requestTagBeforeClosingChat) { + return { + updatedOptions: { + ...options, + ...(extraRoomTags.length && { tags: extraRoomTags }), + }, + }; + } + + const checkRoomTags = !clientAction || (roomTags && roomTags.length > 0); + const checkDepartmentTags = chatClosingTags && chatClosingTags.length > 0; + if (!checkRoomTags || !checkDepartmentTags) { + throw new Error('error-tags-must-be-assigned-before-closing-chat'); + } + + return { + updatedOptions: { + ...options, + ...(extraRoomTags.length && { tags: extraRoomTags }), + }, + }; + } +} + +export const Livechat = new LivechatClass(); diff --git a/apps/meteor/app/livechat/server/lib/QueueManager.js b/apps/meteor/app/livechat/server/lib/QueueManager.js index 5ffea4cb788..3c42be858f2 100644 --- a/apps/meteor/app/livechat/server/lib/QueueManager.js +++ b/apps/meteor/app/livechat/server/lib/QueueManager.js @@ -1,5 +1,6 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; +import { LivechatInquiry as LivechatInquiryRaw } from '@rocket.chat/models'; import { LivechatRooms, LivechatInquiry, Users } from '../../../models/server'; import { checkServiceStatus, createLivechatRoom, createLivechatInquiry } from './Helper'; @@ -98,7 +99,7 @@ export const QueueManager = { const oldInquiry = LivechatInquiry.findOneByRoomId(rid); if (oldInquiry) { logger.debug(`Removing old inquiry (${oldInquiry._id}) for room ${rid}`); - LivechatInquiry.removeByRoomId(rid); + await LivechatInquiryRaw.removeByRoomId(rid); } const guest = { diff --git a/apps/meteor/app/livechat/server/lib/stream/agentStatus.ts b/apps/meteor/app/livechat/server/lib/stream/agentStatus.ts index 34c470c03f4..049c8d2813b 100644 --- a/apps/meteor/app/livechat/server/lib/stream/agentStatus.ts +++ b/apps/meteor/app/livechat/server/lib/stream/agentStatus.ts @@ -69,7 +69,7 @@ export const onlineAgents = { try { if (action === 'close') { - return Livechat.closeOpenChats(userId, comment); + return Promise.await(Livechat.closeOpenChats(userId, comment)); } if (action === 'forward') { diff --git a/apps/meteor/app/livechat/server/methods/closeByVisitor.js b/apps/meteor/app/livechat/server/methods/closeByVisitor.js deleted file mode 100644 index 525656fc77d..00000000000 --- a/apps/meteor/app/livechat/server/methods/closeByVisitor.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import { LivechatVisitors } from '@rocket.chat/models'; - -import { settings } from '../../../settings/server'; -import { LivechatRooms } from '../../../models/server'; -import { Livechat } from '../lib/Livechat'; -import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; - -Meteor.methods({ - async 'livechat:closeByVisitor'({ roomId, token }) { - methodDeprecationLogger.warn('livechat:closeByVisitor will be deprecated in future versions of Rocket.Chat'); - const visitor = await LivechatVisitors.getVisitorByToken(token); - - const language = (visitor && visitor.language) || settings.get('Language') || 'en'; - - return Livechat.closeRoom({ - visitor, - room: LivechatRooms.findOneOpenByRoomIdAndVisitorToken(roomId, token), - comment: TAPi18n.__('Closed_by_visitor', { lng: language }), - }); - }, -}); diff --git a/apps/meteor/app/livechat/server/methods/closeRoom.js b/apps/meteor/app/livechat/server/methods/closeRoom.js deleted file mode 100644 index 9eeccfac638..00000000000 --- a/apps/meteor/app/livechat/server/methods/closeRoom.js +++ /dev/null @@ -1,43 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { hasPermission } from '../../../authorization'; -import { Subscriptions, LivechatRooms } from '../../../models/server'; -import { Livechat } from '../lib/Livechat'; - -Meteor.methods({ - 'livechat:closeRoom'(roomId, comment, options = {}) { - const userId = Meteor.userId(); - if (!userId || !hasPermission(userId, 'close-livechat-room')) { - throw new Meteor.Error('error-not-authorized', 'Not authorized', { - method: 'livechat:closeRoom', - }); - } - - const room = LivechatRooms.findOneById(roomId); - if (!room || room.t !== 'l') { - throw new Meteor.Error('error-invalid-room', 'Invalid room', { - method: 'livechat:closeRoom', - }); - } - - if (!room.open) { - throw new Meteor.Error('room-closed', 'Room closed', { method: 'livechat:closeRoom' }); - } - - const user = Meteor.user(); - - const subscription = Subscriptions.findOneByRoomIdAndUserId(roomId, user._id, { _id: 1 }); - if (!subscription && !hasPermission(userId, 'close-others-livechat-room')) { - throw new Meteor.Error('error-not-authorized', 'Not authorized', { - method: 'livechat:closeRoom', - }); - } - - return Livechat.closeRoom({ - user, - room: LivechatRooms.findOneById(roomId), - comment, - options, - }); - }, -}); diff --git a/apps/meteor/app/livechat/server/methods/closeRoom.ts b/apps/meteor/app/livechat/server/methods/closeRoom.ts new file mode 100644 index 00000000000..f1fa700f0dd --- /dev/null +++ b/apps/meteor/app/livechat/server/methods/closeRoom.ts @@ -0,0 +1,129 @@ +import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; +import { Meteor } from 'meteor/meteor'; +import { Users, LivechatRooms, Subscriptions as SubscriptionRaw } from '@rocket.chat/models'; + +import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { Livechat } from '../lib/LivechatTyped'; +import { methodDeprecationLogger } from '../../../lib/server/lib/deprecationWarningLogger'; + +type CloseRoomOptions = { + clientAction?: boolean; + tags?: string[]; + emailTranscript?: + | { + sendToVisitor: false; + } + | { + sendToVisitor: true; + requestData: Pick, 'email' | 'subject'>; + }; + generateTranscriptPdf?: boolean; +}; + +type LivechatCloseRoomOptions = Omit & { + emailTranscript?: + | { + sendToVisitor: false; + } + | { + sendToVisitor: true; + requestData: NonNullable; + }; + pdfTranscript?: { + requestedBy: string; + }; +}; + +Meteor.methods({ + async 'livechat:closeRoom'(roomId: string, comment?: string, options?: CloseRoomOptions) { + methodDeprecationLogger.warn( + 'livechat:closeRoom is deprecated and will be removed in next major version. Use /api/v1/livechat/room.closeByUser API instead.', + ); + + const userId = Meteor.userId(); + if (!userId || !(await hasPermissionAsync(userId, 'close-livechat-room'))) { + throw new Meteor.Error('error-not-authorized', 'Not authorized', { + method: 'livechat:closeRoom', + }); + } + + const room = await LivechatRooms.findOneById(roomId); + if (!room || room.t !== 'l') { + throw new Meteor.Error('error-invalid-room', 'Invalid room', { + method: 'livechat:closeRoom', + }); + } + + if (!room.open) { + throw new Meteor.Error('room-closed', 'Room closed', { method: 'livechat:closeRoom' }); + } + + const user = await Users.findOneById(userId); + if (!user) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'livechat:closeRoom', + }); + } + + const subscription = await SubscriptionRaw.findOneByRoomIdAndUserId(roomId, user._id, { + projection: { + _id: 1, + }, + }); + if (!subscription && !(await hasPermissionAsync(userId, 'close-others-livechat-room'))) { + throw new Meteor.Error('error-not-authorized', 'Not authorized', { + method: 'livechat:closeRoom', + }); + } + + await Livechat.closeRoom({ + user, + room, + comment, + options: resolveOptions(user, options), + }); + }, +}); + +const resolveOptions = ( + user: NonNullable['requestedBy'], + options?: CloseRoomOptions, +): LivechatCloseRoomOptions | undefined => { + if (!options) { + return undefined; + } + + const resolvedOptions: LivechatCloseRoomOptions = { + clientAction: options.clientAction, + tags: options.tags, + }; + + if (options.generateTranscriptPdf) { + resolvedOptions.pdfTranscript = { + requestedBy: user._id, + }; + } + + if (!options?.emailTranscript) { + return resolvedOptions; + } + if (options?.emailTranscript.sendToVisitor === false) { + return { + ...resolvedOptions, + emailTranscript: { + sendToVisitor: false, + }, + }; + } + return { + ...resolvedOptions, + emailTranscript: { + sendToVisitor: true, + requestData: { + ...options.emailTranscript.requestData, + requestedBy: user, + requestedAt: new Date(), + }, + }, + }; +}; diff --git a/apps/meteor/app/livechat/server/methods/discardTranscript.js b/apps/meteor/app/livechat/server/methods/discardTranscript.js deleted file mode 100644 index 099c8ad002d..00000000000 --- a/apps/meteor/app/livechat/server/methods/discardTranscript.js +++ /dev/null @@ -1,32 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; - -import { hasPermission } from '../../../authorization'; -import { LivechatRooms } from '../../../models/server'; - -Meteor.methods({ - 'livechat:discardTranscript'(rid) { - check(rid, String); - - if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'send-omnichannel-chat-transcript')) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { - method: 'livechat:requestTranscript', - }); - } - - const room = LivechatRooms.findOneById(rid); - if (!room || !room.open) { - throw new Meteor.Error('error-invalid-room', 'Invalid room', { - method: 'livechat:requestTranscript', - }); - } - - if (!room.transcriptRequest) { - throw new Meteor.Error('error-transcript-not-requested', 'No transcript requested for this chat', { - method: 'livechat:requestTranscript', - }); - } - - return LivechatRooms.removeTranscriptRequestByRoomId(rid); - }, -}); diff --git a/apps/meteor/app/livechat/server/methods/discardTranscript.ts b/apps/meteor/app/livechat/server/methods/discardTranscript.ts new file mode 100644 index 00000000000..c9336b64a2e --- /dev/null +++ b/apps/meteor/app/livechat/server/methods/discardTranscript.ts @@ -0,0 +1,36 @@ +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; +import { LivechatRooms } from '@rocket.chat/models'; + +import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; + +Meteor.methods({ + async 'livechat:discardTranscript'(rid: string) { + check(rid, String); + + const user = Meteor.userId(); + + if (!user || !(await hasPermissionAsync(user, 'send-omnichannel-chat-transcript'))) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { + method: 'livechat:requestTranscript', + }); + } + + const room = await LivechatRooms.findOneById(rid); + if (!room || !room.open) { + throw new Meteor.Error('error-invalid-room', 'Invalid room', { + method: 'livechat:discardTranscript', + }); + } + + if (!room.transcriptRequest) { + throw new Meteor.Error('error-transcript-not-requested', 'No transcript requested for this chat', { + method: 'livechat:discardTranscript', + }); + } + + await LivechatRooms.unsetEmailTranscriptRequestedByRoomId(rid); + + return true; + }, +}); diff --git a/apps/meteor/app/livechat/server/methods/removeAllClosedRooms.js b/apps/meteor/app/livechat/server/methods/removeAllClosedRooms.js deleted file mode 100644 index 58888051fa8..00000000000 --- a/apps/meteor/app/livechat/server/methods/removeAllClosedRooms.js +++ /dev/null @@ -1,26 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { hasPermission } from '../../../authorization'; -import { LivechatRooms } from '../../../models/server'; -import { Livechat } from '../lib/Livechat'; - -Meteor.methods({ - 'livechat:removeAllClosedRooms'(departmentIds) { - if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'remove-closed-livechat-rooms')) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { - method: 'livechat:removeAllClosedRoom', - }); - } - - let count = 0; - // These are not debug logs since we want to know when the action is performed - Livechat.logger.info(`User ${Meteor.userId()} is removing all closed rooms`); - LivechatRooms.findClosedRooms(departmentIds).forEach(({ _id }) => { - Livechat.removeRoom(_id); - count++; - }); - - Livechat.logger.info(`User ${Meteor.userId()} removed ${count} closed rooms`); - return count; - }, -}); diff --git a/apps/meteor/app/livechat/server/methods/removeAllClosedRooms.ts b/apps/meteor/app/livechat/server/methods/removeAllClosedRooms.ts new file mode 100644 index 00000000000..ffc711d0716 --- /dev/null +++ b/apps/meteor/app/livechat/server/methods/removeAllClosedRooms.ts @@ -0,0 +1,30 @@ +import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; +import { Meteor } from 'meteor/meteor'; + +import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { LivechatRooms } from '../../../models/server'; +import { Livechat } from '../lib/Livechat'; + +Meteor.methods({ + async 'livechat:removeAllClosedRooms'(departmentIds: string[]) { + const user = Meteor.userId(); + + if (!user || !(await hasPermissionAsync(user, 'remove-closed-livechat-rooms'))) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { + method: 'livechat:removeAllClosedRoom', + }); + } + + // These are not debug logs since we want to know when the action is performed + Livechat.logger.info(`User ${Meteor.userId()} is removing all closed rooms`); + + const promises: Promise[] = []; + LivechatRooms.findClosedRooms(departmentIds).forEach(({ _id }: IOmnichannelRoom) => { + promises.push(Livechat.removeRoom(_id)); + }); + await Promise.all(promises); + + Livechat.logger.info(`User ${Meteor.userId()} removed ${promises.length} closed rooms`); + return promises.length; + }, +}); diff --git a/apps/meteor/app/livechat/server/methods/removeRoom.js b/apps/meteor/app/livechat/server/methods/removeRoom.ts similarity index 72% rename from apps/meteor/app/livechat/server/methods/removeRoom.js rename to apps/meteor/app/livechat/server/methods/removeRoom.ts index a39a0abb4f0..9b207c682d8 100644 --- a/apps/meteor/app/livechat/server/methods/removeRoom.js +++ b/apps/meteor/app/livechat/server/methods/removeRoom.ts @@ -1,12 +1,13 @@ import { Meteor } from 'meteor/meteor'; -import { hasPermission } from '../../../authorization'; +import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { LivechatRooms } from '../../../models/server'; import { Livechat } from '../lib/Livechat'; Meteor.methods({ - 'livechat:removeRoom'(rid) { - if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'remove-closed-livechat-rooms')) { + async 'livechat:removeRoom'(rid: string) { + const user = Meteor.userId(); + if (!user || !(await hasPermissionAsync(user, 'remove-closed-livechat-rooms'))) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:removeRoom' }); } @@ -30,6 +31,6 @@ Meteor.methods({ }); } - return Livechat.removeRoom(rid); + await Livechat.removeRoom(rid); }, }); diff --git a/apps/meteor/app/livechat/server/methods/requestTranscript.js b/apps/meteor/app/livechat/server/methods/requestTranscript.js deleted file mode 100644 index 8e3424c8d00..00000000000 --- a/apps/meteor/app/livechat/server/methods/requestTranscript.js +++ /dev/null @@ -1,24 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import { check } from 'meteor/check'; - -import { hasPermission } from '../../../authorization'; -import { Users } from '../../../models/server'; -import { Livechat } from '../lib/Livechat'; - -Meteor.methods({ - 'livechat:requestTranscript'(rid, email, subject) { - check(rid, String); - check(email, String); - - if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'send-omnichannel-chat-transcript')) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { - method: 'livechat:requestTranscript', - }); - } - - const user = Users.findOneById(Meteor.userId(), { - fields: { _id: 1, username: 1, name: 1, utcOffset: 1 }, - }); - return Livechat.requestTranscript({ rid, email, subject, user }); - }, -}); diff --git a/apps/meteor/app/livechat/server/methods/requestTranscript.ts b/apps/meteor/app/livechat/server/methods/requestTranscript.ts new file mode 100644 index 00000000000..f10c6ce27d1 --- /dev/null +++ b/apps/meteor/app/livechat/server/methods/requestTranscript.ts @@ -0,0 +1,29 @@ +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; + +import { Users } from '../../../models/server'; +import { Livechat } from '../lib/Livechat'; +import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; + +Meteor.methods({ + async 'livechat:requestTranscript'(rid: string, email: string, subject: string) { + check(rid, String); + check(email, String); + + const userId = Meteor.userId(); + + if (!userId || !(await hasPermissionAsync(userId, 'send-omnichannel-chat-transcript'))) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { + method: 'livechat:requestTranscript', + }); + } + + const user = Users.findOneById(userId, { + fields: { _id: 1, username: 1, name: 1, utcOffset: 1 }, + }); + + await Livechat.requestTranscript({ rid, email, subject, user }); + + return true; + }, +}); diff --git a/apps/meteor/app/livechat/server/roomAccessValidator.compatibility.js b/apps/meteor/app/livechat/server/roomAccessValidator.compatibility.js index fe6e1d4a636..93bc0915fa5 100644 --- a/apps/meteor/app/livechat/server/roomAccessValidator.compatibility.js +++ b/apps/meteor/app/livechat/server/roomAccessValidator.compatibility.js @@ -70,4 +70,14 @@ export const validators = [ } return hasPermission(user._id, 'view-livechat-room-closed-same-department'); }, + function (room, user) { + // Check if user is rocket.cat + if (!user?._id) { + return false; + } + + // This opens the ability for rocketcat to upload files to a livechat room without being included in it :) + // Worst case, someone manages to log in as rocketcat lol + return user._id === 'rocket.cat'; + }, ]; diff --git a/apps/meteor/app/models/server/models/LivechatInquiry.ts b/apps/meteor/app/models/server/models/LivechatInquiry.ts index 901b93e4cc8..5ece5ab5040 100644 --- a/apps/meteor/app/models/server/models/LivechatInquiry.ts +++ b/apps/meteor/app/models/server/models/LivechatInquiry.ts @@ -1,5 +1,5 @@ import type { ILivechatInquiryRecord } from '@rocket.chat/core-typings'; -import type { FindOptions, FindCursor, UpdateResult, DeleteResult } from 'mongodb'; +import type { FindOptions, FindCursor, UpdateResult } from 'mongodb'; import { readSecondaryPreferred } from '../../../../server/database/readSecondaryPreferred'; import { Base } from './_Base'; @@ -259,13 +259,6 @@ export class LivechatInquiry extends Base { ); } - /* - * remove the inquiry by roomId - */ - removeByRoomId(rid: string): DeleteResult { - return this.remove({ rid }); - } - removeByVisitorToken(token: string): void { const query = { 'v.token': token, diff --git a/apps/meteor/app/models/server/models/LivechatRooms.js b/apps/meteor/app/models/server/models/LivechatRooms.js index 98c21251910..bd9f47eeb09 100644 --- a/apps/meteor/app/models/server/models/LivechatRooms.js +++ b/apps/meteor/app/models/server/models/LivechatRooms.js @@ -35,6 +35,8 @@ export class LivechatRooms extends Base { }, ); this.tryEnsureIndex({ 'livechatData.$**': 1 }); + this.tryEnsureIndex({ pdfTranscriptRequested: 1 }, { sparse: true }); + this.tryEnsureIndex({ pdfFileId: 1 }, { sparse: true }); } findOneByIdOrName(_idOrName, options) { @@ -649,66 +651,6 @@ export class LivechatRooms extends Base { ); } - closeByRoomId(roomId, closeInfo) { - const { closer, closedBy, closedAt, chatDuration, serviceTimeDuration, ...extraData } = closeInfo; - - return this.update( - { - _id: roomId, - t: 'l', - }, - { - $set: { - closer, - closedBy, - closedAt, - 'metrics.chatDuration': chatDuration, - 'metrics.serviceTimeDuration': serviceTimeDuration, - 'v.status': 'offline', - ...extraData, - }, - $unset: { - open: 1, - }, - }, - ); - } - - requestTranscriptByRoomId(roomId, transcriptInfo = {}) { - const { requestedAt, requestedBy, email, subject } = transcriptInfo; - - return this.update( - { - _id: roomId, - t: 'l', - }, - { - $set: { - transcriptRequest: { - requestedAt, - requestedBy, - email, - subject, - }, - }, - }, - ); - } - - removeTranscriptRequestByRoomId(roomId) { - return this.update( - { - _id: roomId, - t: 'l', - }, - { - $unset: { - transcriptRequest: 1, - }, - }, - ); - } - findOpenByAgent(userId) { const query = { 't': 'l', diff --git a/apps/meteor/client/components/Omnichannel/modals/CloseChatModal.tsx b/apps/meteor/client/components/Omnichannel/modals/CloseChatModal.tsx index b6bdc36e86c..9ebe109a9b8 100644 --- a/apps/meteor/client/components/Omnichannel/modals/CloseChatModal.tsx +++ b/apps/meteor/client/components/Omnichannel/modals/CloseChatModal.tsx @@ -1,21 +1,30 @@ import type { ILivechatDepartment } from '@rocket.chat/core-typings'; -import { Field, Button, TextInput, Modal, Box } from '@rocket.chat/fuselage'; -import { useSetting, useTranslation } from '@rocket.chat/ui-contexts'; +import { Field, Button, TextInput, Modal, Box, CheckBox, Divider, EmailInput } from '@rocket.chat/fuselage'; +import { usePermission, useSetting, useTranslation, useUserPreference } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useCallback, useState, useEffect, useMemo } from 'react'; import { useForm } from 'react-hook-form'; +import { useHasLicenseModule } from '../../../../ee/client/hooks/useHasLicenseModule'; +import { dispatchToastMessage } from '../../../lib/toast'; import GenericModal from '../../GenericModal'; import Tags from '../Tags'; const CloseChatModal = ({ department, + visitorEmail, onCancel, onConfirm, }: { department?: ILivechatDepartment | null; + visitorEmail?: string; onCancel: () => void; - onConfirm: (comment?: string, tags?: string[]) => Promise; + onConfirm: ( + comment?: string, + tags?: string[], + preferences?: { omnichannelTranscriptPDF: boolean; omnichannelTranscriptEmail: boolean }, + requestData?: { email: string; subject: string }, + ) => Promise; }): ReactElement => { const t = useTranslation(); @@ -34,33 +43,57 @@ const CloseChatModal = ({ const tags = watch('tags'); const comment = watch('comment'); + const transcriptEmail = watch('transcriptEmail'); + const subject = watch('subject'); + + const userTranscriptEmail = useUserPreference('omnichannelTranscriptEmail') ?? false; + const userTranscriptPDF = useUserPreference('omnichannelTranscriptPDF') ?? false; + const hasLicense = useHasLicenseModule('livechat-enterprise'); + const transcriptPDFPermission = usePermission('request-pdf-transcript'); + const transcriptEmailPermission = usePermission('send-omnichannel-chat-transcript'); + + const canSendTranscriptEmail = transcriptEmailPermission && visitorEmail; + const canSendTranscriptPDF = transcriptPDFPermission && hasLicense; + const canSendTranscript = canSendTranscriptEmail || canSendTranscriptPDF; const handleTags = (value: string[]): void => { setValue('tags', value); }; const onSubmit = useCallback( - ({ comment, tags }): void => { + ({ comment, tags, transcriptPDF, transcriptEmail, subject }): void => { + const preferences = { + omnichannelTranscriptPDF: !!transcriptPDF, + omnichannelTranscriptEmail: !!transcriptEmail, + }; + const requestData = transcriptEmail && visitorEmail ? { email: visitorEmail, subject } : undefined; + if (!comment && commentRequired) { setError('comment', { type: 'custom', message: t('The_field_is_required', t('Comment')) }); } + if (transcriptEmail && !subject) { + setError('subject', { type: 'custom', message: t('The_field_is_required', t('Subject')) }); + } + if (!tags?.length && tagRequired) { setError('tags', { type: 'custom', message: t('error-tags-must-be-assigned-before-closing-chat') }); } if (!errors.comment || errors.tags) { - onConfirm(comment, tags); + onConfirm(comment, tags, preferences, requestData); } }, - [commentRequired, tagRequired, errors, setError, t, onConfirm], + [commentRequired, tagRequired, visitorEmail, errors, setError, t, onConfirm], ); const cannotSubmit = useMemo(() => { const cannotSendTag = (tagRequired && !tags?.length) || errors.tags; const cannotSendComment = (commentRequired && !comment) || errors.comment; - return Boolean(cannotSendTag || cannotSendComment); - }, [comment, commentRequired, errors, tagRequired, tags]); + const cannotSendTranscriptEmail = transcriptEmail && (!visitorEmail || !subject); + + return Boolean(cannotSendTag || cannotSendComment || cannotSendTranscriptEmail); + }, [comment, commentRequired, errors, tagRequired, tags, transcriptEmail, visitorEmail, subject]); useEffect(() => { if (department?.requestTagBeforeClosingChat) { @@ -80,11 +113,21 @@ const CloseChatModal = ({ } }, [register, tagRequired]); - return commentRequired || tagRequired ? ( + useEffect(() => { + if (transcriptEmail) { + if (!visitorEmail) { + dispatchToastMessage({ type: 'error', message: t('Customer_without_registered_email') }); + return; + } + setValue('subject', subject || t('Transcript_of_your_livechat_conversation')); + } + }, [transcriptEmail, setValue, visitorEmail, subject, t]); + + return commentRequired || tagRequired || canSendTranscript ? ( - {t('Closing_chat')} + {t('Wrap_up_conversation')} @@ -110,6 +153,70 @@ const CloseChatModal = ({ {errors.tags?.message} + {canSendTranscript && ( + <> + + + {t('Chat_transcript')} + + {canSendTranscriptPDF && ( + + + + + {t('Omnichannel_transcript_pdf')} + + + + )} + {canSendTranscriptEmail && ( + <> + + + + + {t('Omnichannel_transcript_email')} + + + + {transcriptEmail && ( + <> + + {t('Contact_email')} + + + + + + {t('Subject')} + + + + {errors.subject?.message} + + + )} + + )} + + + {canSendTranscriptPDF && canSendTranscriptEmail + ? t('These_options_affect_this_conversation_only_To_set_default_selections_go_to_My_Account_Omnichannel') + : t('This_option_affect_this_conversation_only_To_set_default_selection_go_to_My_Account_Omnichannel')} + + + + )} diff --git a/apps/meteor/client/components/Omnichannel/modals/CloseChatModalData.tsx b/apps/meteor/client/components/Omnichannel/modals/CloseChatModalData.tsx index eae95e47cd4..eadaa353c80 100644 --- a/apps/meteor/client/components/Omnichannel/modals/CloseChatModalData.tsx +++ b/apps/meteor/client/components/Omnichannel/modals/CloseChatModalData.tsx @@ -9,12 +9,18 @@ import CloseChatModal from './CloseChatModal'; const CloseChatModalData = ({ departmentId, + visitorEmail, onCancel, onConfirm, }: { departmentId: ILivechatDepartment['_id']; onCancel: () => void; - onConfirm: (comment?: string, tags?: string[]) => Promise; + visitorEmail?: string; + onConfirm: ( + comment?: string, + tags?: string[], + preferences?: { omnichannelTranscriptPDF: boolean; omnichannelTranscriptEmail: boolean }, + ) => Promise; }): ReactElement => { const { value: data, phase: state } = useEndpointData('/v1/livechat/department/:_id', { keys: { _id: departmentId } }); @@ -31,6 +37,7 @@ const CloseChatModalData = ({ = ({ - {roomOpen && transcriptRequest ? ( + {roomOpen && transcriptRequest && ( - ) : ( + )} + {roomOpen && !transcriptRequest && ( diff --git a/apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx b/apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx index 8bfc8e11f52..e5e599798bf 100644 --- a/apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx +++ b/apps/meteor/client/components/message/content/attachments/file/GenericFileAttachment.tsx @@ -35,7 +35,11 @@ export const GenericFileAttachment: FC = ({ } > - + {title} {size && ( diff --git a/apps/meteor/client/views/account/omnichannel/OmnichannelPreferencesPage.tsx b/apps/meteor/client/views/account/omnichannel/OmnichannelPreferencesPage.tsx new file mode 100644 index 00000000000..e7941359472 --- /dev/null +++ b/apps/meteor/client/views/account/omnichannel/OmnichannelPreferencesPage.tsx @@ -0,0 +1,68 @@ +import { ButtonGroup, Button, Box, Accordion } from '@rocket.chat/fuselage'; +import { useToastMessageDispatch, useTranslation, useEndpoint, useUserPreference } from '@rocket.chat/ui-contexts'; +import type { ReactElement } from 'react'; +import React from 'react'; +import type { UseFormRegister } from 'react-hook-form'; +import { useForm } from 'react-hook-form'; + +import Page from '../../../components/Page'; +import PreferencesConversationTranscript from './PreferencesConversationTranscript'; + +type CurrentData = { + omnichannelTranscriptPDF: boolean; + omnichannelTranscriptEmail: boolean; +}; + +export type FormSectionProps = { + register: UseFormRegister; +}; + +const OmnichannelPreferencesPage = (): ReactElement => { + const t = useTranslation(); + const dispatchToastMessage = useToastMessageDispatch(); + + const omnichannelTranscriptPDF = useUserPreference('omnichannelTranscriptPDF') ?? false; + const omnichannelTranscriptEmail = useUserPreference('omnichannelTranscriptEmail') ?? false; + + const { + handleSubmit, + register, + formState: { isDirty }, + reset, + } = useForm({ + defaultValues: { omnichannelTranscriptPDF, omnichannelTranscriptEmail }, + }); + + const saveFn = useEndpoint('POST', '/v1/users.setPreferences'); + + const handleSave = async (data: CurrentData) => { + try { + await saveFn({ data }); + reset(data); + dispatchToastMessage({ type: 'success', message: t('Preferences_saved') }); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error }); + } + }; + + return ( + + + + + + + + + + + + + + + ); +}; + +export default OmnichannelPreferencesPage; diff --git a/apps/meteor/client/views/account/omnichannel/PreferencesConversationTranscript.tsx b/apps/meteor/client/views/account/omnichannel/PreferencesConversationTranscript.tsx new file mode 100644 index 00000000000..5c201971b94 --- /dev/null +++ b/apps/meteor/client/views/account/omnichannel/PreferencesConversationTranscript.tsx @@ -0,0 +1,64 @@ +import { Accordion, Box, Field, FieldGroup, Tag, ToggleSwitch } from '@rocket.chat/fuselage'; +import { useTranslation, usePermission } from '@rocket.chat/ui-contexts'; +import type { ReactElement } from 'react'; +import React from 'react'; + +import { useHasLicenseModule } from '../../../../ee/client/hooks/useHasLicenseModule'; +import type { FormSectionProps } from './OmnichannelPreferencesPage'; + +const PreferencesConversationTranscript = ({ register }: FormSectionProps): ReactElement | null => { + const t = useTranslation(); + + const hasLicense = useHasLicenseModule('livechat-enterprise'); + const canSendTranscriptPDF = usePermission('request-pdf-transcript'); + const canSendTranscriptEmail = usePermission('send-omnichannel-chat-transcript'); + const cantSendTranscriptPDF = !canSendTranscriptPDF || !hasLicense; + + return ( + + + + + + + {t('Omnichannel_transcript_pdf')} + + {!hasLicense && {t('Enterprise')}} + {!canSendTranscriptPDF && hasLicense && {t('No_permission')}} + + + + + + + + + {t('Accounts_Default_User_Preferences_omnichannelTranscriptPDF_Description')} + + + + + + + {t('Omnichannel_transcript_email')} + {!canSendTranscriptEmail && ( + + {t('No_permission')} + + )} + + + + + + + + {t('Accounts_Default_User_Preferences_omnichannelTranscriptEmail_Description')} + + + + + ); +}; + +export default PreferencesConversationTranscript; diff --git a/apps/meteor/client/views/account/routes.tsx b/apps/meteor/client/views/account/routes.tsx index ab55dbd95c6..17bfc152528 100644 --- a/apps/meteor/client/views/account/routes.tsx +++ b/apps/meteor/client/views/account/routes.tsx @@ -32,3 +32,8 @@ registerAccountRoute('/tokens', { name: 'tokens', component: lazy(() => import('./tokens/AccountTokensRoute')), }); + +registerAccountRoute('/omnichannel', { + name: 'omnichannel', + component: lazy(() => import('./omnichannel/OmnichannelPreferencesPage')), +}); diff --git a/apps/meteor/client/views/account/sidebarItems.ts b/apps/meteor/client/views/account/sidebarItems.ts index 77ed0fa0126..3f36d5a7136 100644 --- a/apps/meteor/client/views/account/sidebarItems.ts +++ b/apps/meteor/client/views/account/sidebarItems.ts @@ -1,4 +1,4 @@ -import { hasPermission } from '../../../app/authorization/client'; +import { hasPermission, hasAtLeastOnePermission } from '../../../app/authorization/client'; import { settings } from '../../../app/settings/client'; import { createSidebarItems } from '../../lib/createSidebarItems'; @@ -37,4 +37,10 @@ export const { icon: 'key', permissionGranted: (): boolean => hasPermission('create-personal-access-tokens'), }, + { + href: 'omnichannel', + i18nLabel: 'Omnichannel', + icon: 'headset', + permissionGranted: (): boolean => hasAtLeastOnePermission(['send-omnichannel-chat-transcript', 'request-pdf-transcript']), + }, ]); diff --git a/apps/meteor/client/views/room/Header/Omnichannel/OmnichannelRoomHeader.tsx b/apps/meteor/client/views/room/Header/Omnichannel/OmnichannelRoomHeader.tsx index bae7ac0c39a..94168360ed2 100644 --- a/apps/meteor/client/views/room/Header/Omnichannel/OmnichannelRoomHeader.tsx +++ b/apps/meteor/client/views/room/Header/Omnichannel/OmnichannelRoomHeader.tsx @@ -10,7 +10,6 @@ import type { ToolboxActionConfig } from '../../lib/Toolbox'; import RoomHeader from '../RoomHeader'; import { BackButton } from './BackButton'; import QuickActions from './QuickActions'; -import { useQuickActions } from './QuickActions/hooks/useQuickActions'; type OmnichannelRoomHeaderProps = { slots: { @@ -31,7 +30,6 @@ const OmnichannelRoomHeader: FC = ({ slots: parentSl const [name] = useCurrentRoute(); const { isMobile } = useLayout(); const room = useOmnichannelRoom(); - const { visibleActions, getAction } = useQuickActions(room); const toolbox = useToolboxContext(); const slots = useMemo( @@ -43,7 +41,7 @@ const OmnichannelRoomHeader: FC = ({ slots: parentSl {} ), - ...(!isMobile && { insideContent: }), + posContent: , }), [isMobile, name, parentSlot, room], ); @@ -52,21 +50,9 @@ const OmnichannelRoomHeader: FC = ({ slots: parentSl value={useMemo( () => ({ ...toolbox, - actions: new Map([ - ...(isMobile - ? (visibleActions.map((action) => [ - action.id, - { - ...action, - action: (): unknown => getAction(action.id), - order: (action.order || 0) - 10, - }, - ]) as [string, ToolboxActionConfig][]) - : []), - ...(Array.from(toolbox.actions.entries()) as [string, ToolboxActionConfig][]), - ]), + actions: new Map([...(Array.from(toolbox.actions.entries()) as [string, ToolboxActionConfig][])]), }), - [toolbox, isMobile, visibleActions, getAction], + [toolbox], )} > diff --git a/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/QuickActions.tsx b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/QuickActions.tsx index 9c2aa80b927..d480c1bc0ce 100644 --- a/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/QuickActions.tsx +++ b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/QuickActions.tsx @@ -5,6 +5,7 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import type { FC, ComponentProps } from 'react'; import React, { memo } from 'react'; +import ToolBoxActionOptions from './ToolBoxActionOptions'; import { useQuickActions } from './hooks/useQuickActions'; type QuickActionsProps = { @@ -18,7 +19,7 @@ const QuickActions: FC = ({ room, className }) => { return ( - {visibleActions.map(({ id, color, icon, title, action = actionDefault }, index) => { + {visibleActions.map(({ id, color, icon, title, action = actionDefault, options }, index) => { const props = { id, icon, @@ -29,10 +30,16 @@ const QuickActions: FC = ({ room, className }) => { primary: false, action, key: id, + room, }; + if (options) { + return ; + } + return ; })} + {visibleActions.length > 0 && } ); }; diff --git a/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/ToolBoxActionOptions.tsx b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/ToolBoxActionOptions.tsx new file mode 100644 index 00000000000..e3a063b4606 --- /dev/null +++ b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/ToolBoxActionOptions.tsx @@ -0,0 +1,49 @@ +import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; +import { Box, Dropdown, Option } from '@rocket.chat/fuselage'; +import { Header } from '@rocket.chat/ui-client'; +import { useTranslation } from '@rocket.chat/ui-contexts'; +import type { FC } from 'react'; +import React, { memo, useRef } from 'react'; + +import { useDropdownVisibility } from '../../../../../sidebar/header/hooks/useDropdownVisibility'; +import type { QuickActionsActionOptions } from '../../../lib/QuickActions'; + +type ToolBoxActionOptionsProps = { + options: QuickActionsActionOptions; + action: (id: string) => void; + room: IOmnichannelRoom; +}; + +const ToolBoxActionOptions: FC = ({ options, room, action, ...props }) => { + const t = useTranslation(); + const reference = useRef(null); + const target = useRef(null); + const { isVisible, toggle } = useDropdownVisibility({ reference, target }); + + const handleClick = (id: string) => (): void => { + toggle(); + action(id); + }; + + return ( + <> + toggle()} secondary={isVisible} {...props} /> + {isVisible && ( + + {options.map(({ id, label, validate }) => { + const { value: valid = true, tooltip } = validate?.(room) || {}; + return ( + + ); + })} + + )} + + ); +}; + +export default memo(ToolBoxActionOptions); diff --git a/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx index e6927dfffaa..6a62666da23 100644 --- a/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx +++ b/apps/meteor/client/views/room/Header/Omnichannel/QuickActions/hooks/useQuickActions.tsx @@ -17,6 +17,7 @@ import React, { useCallback, useState, useEffect } from 'react'; import { RoomManager } from '../../../../../../../app/ui-utils/client'; import PlaceChatOnHoldModal from '../../../../../../../ee/app/livechat-enterprise/client/components/modals/PlaceChatOnHoldModal'; +import { useHasLicenseModule } from '../../../../../../../ee/client/hooks/useHasLicenseModule'; import CloseChatModal from '../../../../../../components/Omnichannel/modals/CloseChatModal'; import CloseChatModalData from '../../../../../../components/Omnichannel/modals/CloseChatModalData'; import ForwardChatModal from '../../../../../../components/Omnichannel/modals/ForwardChatModal'; @@ -84,7 +85,7 @@ export const useQuickActions = ( closeModal(); dispatchToastMessage({ type: 'success', - message: t('Livechat_transcript_has_been_requested'), + message: t('Livechat_email_transcript_has_been_requested'), }); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); @@ -93,6 +94,20 @@ export const useQuickActions = ( [closeModal, dispatchToastMessage, requestTranscript, rid, t], ); + const sendTranscriptPDF = useEndpoint('POST', '/v1/omnichannel/:rid/request-transcript', { rid }); + + const handleSendTranscriptPDF = useCallback(async () => { + try { + await sendTranscriptPDF(); + dispatchToastMessage({ + type: 'success', + message: t('Livechat_transcript_has_been_requested'), + }); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error }); + } + }, [dispatchToastMessage, sendTranscriptPDF, t]); + const sendTranscript = useMethod('livechat:sendTranscript'); const handleSendTranscript = useCallback( @@ -164,19 +179,39 @@ export const useQuickActions = ( [closeModal, dispatchToastMessage, forwardChat, room.t, rid, homeRoute, t], ); - const closeChat = useMethod('livechat:closeRoom'); + const closeChat = useEndpoint('POST', '/v1/livechat/room.closeByUser'); const handleClose = useCallback( - async (comment?: string, tags?: string[]) => { + async ( + comment?: string, + tags?: string[], + preferences?: { omnichannelTranscriptPDF: boolean; omnichannelTranscriptEmail: boolean }, + requestData?: { email: string; subject: string }, + ) => { try { - await closeChat(rid, comment, { clientAction: true, tags }); + await closeChat({ + rid, + ...(comment && { comment }), + ...(tags && { tags }), + ...(preferences?.omnichannelTranscriptPDF && { generateTranscriptPdf: true }), + ...(preferences?.omnichannelTranscriptEmail && requestData + ? { + transcriptEmail: { + sendToVisitor: preferences?.omnichannelTranscriptEmail, + requestData, + }, + } + : { transcriptEmail: { sendToVisitor: false } }), + }); + homeRoute.push(); + RoomManager.close(room.t + rid); closeModal(); dispatchToastMessage({ type: 'success', message: t('Chat_closed_successfully') }); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); } }, - [closeChat, closeModal, dispatchToastMessage, rid, t], + [closeChat, closeModal, dispatchToastMessage, homeRoute, room.t, rid, t], ); const returnChatToQueueMutation = useReturnChatToQueueMutation({ @@ -204,7 +239,7 @@ export const useQuickActions = ( }, }); - const openModal = useMutableCallback(async (id: string) => { + const handleAction = useMutableCallback(async (id: string) => { switch (id) { case QuickActionsEnum.MoveQueue: setModal( @@ -216,7 +251,10 @@ export const useQuickActions = ( />, ); break; - case QuickActionsEnum.Transcript: + case QuickActionsEnum.TranscriptPDF: + handleSendTranscriptPDF(); + break; + case QuickActionsEnum.TranscriptEmail: const visitorEmail = await getVisitorEmail(); if (!visitorEmail) { @@ -239,11 +277,12 @@ export const useQuickActions = ( setModal(); break; case QuickActionsEnum.CloseChat: + const email = await getVisitorEmail(); setModal( room.departmentId ? ( - + ) : ( - + ), ); break; @@ -273,7 +312,9 @@ export const useQuickActions = ( const roomOpen = room?.open && (room.u?._id === uid || hasManagerRole) && room?.lastMessage?.t !== 'livechat-close'; const canMoveQueue = !!omnichannelRouteConfig?.returnQueue && room?.u !== undefined; const canForwardGuest = usePermission('transfer-livechat-guest'); - const canSendTranscript = usePermission('send-omnichannel-chat-transcript'); + const canSendTranscriptEmail = usePermission('send-omnichannel-chat-transcript'); + const hasLicense = useHasLicenseModule('livechat-enterprise'); + const canSendTranscriptPDF = usePermission('request-pdf-transcript'); const canCloseRoom = usePermission('close-livechat-room'); const canCloseOthersRoom = usePermission('close-others-livechat-room'); const canPlaceChatOnHold = Boolean(!room.onHold && room.u && !(room as any).lastMessage?.token && manualOnHoldAllowed); @@ -285,7 +326,11 @@ export const useQuickActions = ( case QuickActionsEnum.ChatForward: return !!roomOpen && canForwardGuest; case QuickActionsEnum.Transcript: - return canSendTranscript; + return canSendTranscriptEmail || (hasLicense && canSendTranscriptPDF); + case QuickActionsEnum.TranscriptEmail: + return canSendTranscriptEmail; + case QuickActionsEnum.TranscriptPDF: + return hasLicense && canSendTranscriptPDF; case QuickActionsEnum.CloseChat: return !!roomOpen && (canCloseRoom || canCloseOthersRoom); case QuickActionsEnum.OnHoldChat: @@ -296,14 +341,20 @@ export const useQuickActions = ( return false; }; - const visibleActions = actions.filter(({ id }) => hasPermissionButtons(id)); + const visibleActions = actions.filter((action) => { + const { options, id } = action; + if (options) { + action.options = options.filter(({ id }) => hasPermissionButtons(id)); + } + return hasPermissionButtons(id); + }); const actionDefault = useMutableCallback((actionId) => { - openModal(actionId); + handleAction(actionId); }); const getAction = useMutableCallback((id) => { - openModal(id); + handleAction(id); }); return { visibleActions, actionDefault, getAction }; diff --git a/apps/meteor/client/views/room/Header/ToolBox/ToolBox.tsx b/apps/meteor/client/views/room/Header/ToolBox/ToolBox.tsx index 1beb5293088..c9a654f63d6 100644 --- a/apps/meteor/client/views/room/Header/ToolBox/ToolBox.tsx +++ b/apps/meteor/client/views/room/Header/ToolBox/ToolBox.tsx @@ -117,7 +117,7 @@ const ToolBox = ({ className }: ToolBoxProps): ReactElement => { } return ; })} - {filteredActions.length > 6 && ( + {(filteredActions.length > 6 || isMobile) && ( ({ + tooltip: 'Export_enabled_at_the_end_of_the_conversation', + value: !room?.open, + }), + }, + ], }); addAction(QuickActionsEnum.CloseChat, { groups: ['live'], id: QuickActionsEnum.CloseChat, - title: 'Close', + title: 'End_conversation', icon: 'balloon-close-top-right', order: 5, color: 'danger', diff --git a/apps/meteor/client/views/room/lib/QuickActions/index.tsx b/apps/meteor/client/views/room/lib/QuickActions/index.tsx index ba84a221e64..ddc7e760eb1 100644 --- a/apps/meteor/client/views/room/lib/QuickActions/index.tsx +++ b/apps/meteor/client/views/room/lib/QuickActions/index.tsx @@ -1,5 +1,6 @@ import type { IRoom } from '@rocket.chat/core-typings'; import type { Box, Option } from '@rocket.chat/fuselage'; +import type { TranslationKey } from '@rocket.chat/ui-contexts'; import type { ComponentProps, ReactNode } from 'react'; import type { ToolboxActionConfig } from '../Toolbox'; @@ -20,9 +21,18 @@ type OptionRendererProps = ComponentProps; export type OptionRenderer = (props: OptionRendererProps) => ReactNode; +export type QuickActionsActionOptions = Array<{ + id: string; + label: TranslationKey; + enabled?: boolean; + validate?: (room: IRoom) => { value: boolean; tooltip: TranslationKey }; +}>; + export type QuickActionsActionConfig = ToolboxActionConfig & { + action?: (id?: QuickActionsActionConfig['id']) => void; groups: Array<'live'>; color?: string; + options?: QuickActionsActionOptions; }; export type QuickActionsAction = QuickActionsHook | QuickActionsActionConfig; @@ -37,6 +47,8 @@ export enum QuickActionsEnum { MoveQueue = 'rocket-move-to-queue', ChatForward = 'rocket-chat-forward', Transcript = 'rocket-transcript', + TranscriptEmail = 'rocket-transcript-email', + TranscriptPDF = 'rocket-transcript-pdf', CloseChat = 'rocket-close-chat', OnHoldChat = 'rocket-on-hold-chat', } diff --git a/apps/meteor/client/views/room/providers/RoomProvider.tsx b/apps/meteor/client/views/room/providers/RoomProvider.tsx index 202e513165b..6e018276df9 100644 --- a/apps/meteor/client/views/room/providers/RoomProvider.tsx +++ b/apps/meteor/client/views/room/providers/RoomProvider.tsx @@ -25,12 +25,12 @@ type RoomProviderProps = { const RoomProvider = ({ rid, children }: RoomProviderProps): ReactElement => { useRoomRolesManagement(rid); - const roomQuery = useReactiveQuery(['rooms', rid], ({ rooms }) => rooms.findOne({ _id: rid })); + const roomQuery = useReactiveQuery(['rooms', rid], ({ rooms }) => rooms.findOne({ _id: rid }) ?? null); // TODO: the following effect is a workaround while we don't have a general and definitive solution for it const homeRoute = useRoute('home'); useEffect(() => { - if (roomQuery.isSuccess && !roomQuery.data) { + if (roomQuery.isSuccess && roomQuery.data === undefined) { homeRoute.push(); } }, [roomQuery.isSuccess, roomQuery.data, homeRoute]); @@ -103,7 +103,7 @@ const RoomProvider = ({ rid, children }: RoomProviderProps): ReactElement => { const api = useMemo(() => ({}), []); if (!pseudoRoom) { - return roomQuery.isSuccess ? : ; + return roomQuery.isSuccess && roomQuery.data === undefined ? : ; } return ( diff --git a/apps/meteor/ee/app/license/server/getStatistics.ts b/apps/meteor/ee/app/license/server/getStatistics.ts index e7c17b1f1f6..2d9c9fa185f 100644 --- a/apps/meteor/ee/app/license/server/getStatistics.ts +++ b/apps/meteor/ee/app/license/server/getStatistics.ts @@ -1,6 +1,6 @@ import { log } from 'console'; -import { CannedResponse, LivechatPriority, LivechatTag, LivechatUnit } from '@rocket.chat/models'; +import { CannedResponse, LivechatPriority, LivechatRooms, LivechatTag, LivechatUnit } from '@rocket.chat/models'; import { Analytics } from '@rocket.chat/core-services'; import { getModules, getTags, hasLicense } from './license'; @@ -18,6 +18,8 @@ type EEOnlyStats = { cannedResponses: number; priorities: number; businessUnits: number; + omnichannelPdfTranscriptRequested: number; + omnichannelPdfTranscriptSucceeded: number; }; export async function getStatistics(): Promise { @@ -81,6 +83,24 @@ async function getEEStatistics(): Promise { }), ); + // Number of PDF transcript requested + statsPms.push( + LivechatRooms.find({ pdfTranscriptRequested: { $exists: true } }) + .count() + .then((count) => { + statistics.omnichannelPdfTranscriptRequested = count; + }), + ); + + // Number of PDF transcript that succeeded + statsPms.push( + LivechatRooms.find({ pdfFileId: { $exists: true } }) + .count() + .then((count) => { + statistics.omnichannelPdfTranscriptSucceeded = count; + }), + ); + await Promise.all(statsPms).catch(log); return statistics as EEOnlyStats; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/index.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/index.ts index 1906206a69b..823efcae994 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/api/index.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/index.ts @@ -7,3 +7,4 @@ import './tags'; import './units'; import './business-hours'; import './rooms'; +import './transcript'; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/api/transcript.ts b/apps/meteor/ee/app/livechat-enterprise/server/api/transcript.ts new file mode 100644 index 00000000000..90510b18d35 --- /dev/null +++ b/apps/meteor/ee/app/livechat-enterprise/server/api/transcript.ts @@ -0,0 +1,37 @@ +import { LivechatRooms } from '@rocket.chat/models'; +import { OmnichannelTranscript } from '@rocket.chat/core-services'; + +import { API } from '../../../../../app/api/server'; +import { canAccessRoomAsync } from '../../../../../app/authorization/server/functions/canAccessRoom'; + +API.v1.addRoute( + 'omnichannel/:rid/request-transcript', + { authRequired: true, permissionsRequired: ['request-pdf-transcript'] }, + { + async post() { + const room = await LivechatRooms.findOneById(this.urlParams.rid); + if (!room) { + throw new Error('error-invalid-room'); + } + + if (!(await canAccessRoomAsync(room, { _id: this.userId }))) { + throw new Error('error-not-allowed'); + } + + // Flow is as follows: + // 1. Call OmnichannelTranscript.requestTranscript() + // 2. OmnichannelTranscript.requestTranscript() calls QueueWorker.queueWork() + // 3. QueueWorker.queueWork() eventually calls OmnichannelTranscript.workOnPdf() + // 4. OmnichannelTranscript.workOnPdf() calls OmnichannelTranscript.pdfComplete() when processing ends + // 5. OmnichannelTranscript.pdfComplete() sends the messages to the user, and updates the room with the flags + await OmnichannelTranscript.requestTranscript({ + details: { + userId: this.userId, + rid: this.urlParams.rid, + }, + }); + + return API.v1.success(); + }, + }, +); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/index.js b/apps/meteor/ee/app/livechat-enterprise/server/hooks/index.ts similarity index 95% rename from apps/meteor/ee/app/livechat-enterprise/server/hooks/index.js rename to apps/meteor/ee/app/livechat-enterprise/server/hooks/index.ts index a5d2e309227..ee43f31a81f 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/index.js +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/index.ts @@ -23,3 +23,4 @@ import './applyDepartmentRestrictions'; import './afterForwardChatToAgent'; import './applySimultaneousChatsRestrictions'; import './afterInquiryQueued'; +import './sendPdfTranscriptOnClose'; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/onAgentAssignmentFailed.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/onAgentAssignmentFailed.ts index 846f721e4c4..00c5d1bbfab 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/onAgentAssignmentFailed.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/onAgentAssignmentFailed.ts @@ -1,5 +1,7 @@ +import { Subscriptions } from '@rocket.chat/models'; + import { callbacks } from '../../../../../lib/callbacks'; -import { LivechatInquiry, Subscriptions, LivechatRooms } from '../../../../../app/models/server'; +import { LivechatInquiry, LivechatRooms } from '../../../../../app/models/server'; import { queueInquiry } from '../../../../../app/livechat/server/lib/QueueManager'; import { settings } from '../../../../../app/settings/server'; import { cbLogger } from '../lib/logger'; @@ -29,7 +31,7 @@ const handleOnAgentAssignmentFailed = async ({ const { _id: inquiryId } = inquiry; LivechatInquiry.queueInquiryAndRemoveDefaultAgent(inquiryId); LivechatRooms.removeAgentByRoomId(roomId); - Subscriptions.removeByRoomId(roomId); + await Subscriptions.removeByRoomId(roomId); dispatchAgentDelegated(roomId, null); const newInquiry = LivechatInquiry.findOneById(inquiryId); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.js b/apps/meteor/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.js index 46ad95fd701..88a91e7a242 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.js +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.js @@ -3,17 +3,19 @@ import { settings } from '../../../../../app/settings/server'; import { debouncedDispatchWaitingQueueStatus } from '../lib/Helper'; import { LivechatEnterprise } from '../lib/LivechatEnterprise'; -const onCloseLivechat = (room) => { +const onCloseLivechat = (params) => { + const { room } = params; + Promise.await(LivechatEnterprise.releaseOnHoldChat(room)); if (!settings.get('Livechat_waiting_queue')) { - return room; + return params; } const { departmentId } = room || {}; debouncedDispatchWaitingQueueStatus(departmentId); - return room; + return params; }; callbacks.add('livechat.closeRoom', onCloseLivechat, callbacks.priority.HIGH, 'livechat-waiting-queue-monitor-close-room'); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/scheduleAutoTransfer.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/scheduleAutoTransfer.ts index e3c7377c3e6..b243f7f55e7 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/hooks/scheduleAutoTransfer.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/scheduleAutoTransfer.ts @@ -1,10 +1,16 @@ -import type { IMessage, IRoom } from '@rocket.chat/core-typings'; +import type { IMessage, IOmnichannelRoom } from '@rocket.chat/core-typings'; import { AutoTransferChatScheduler } from '../lib/AutoTransferChatScheduler'; import { callbacks } from '../../../../../lib/callbacks'; import { settings } from '../../../../../app/settings/server'; import { LivechatRooms } from '../../../../../app/models/server'; import { cbLogger } from '../lib/logger'; +import type { CloseRoomParams } from '../../../../../app/livechat/server/lib/LivechatTyped.d'; + +type LivechatCloseCallbackParams = { + room: IOmnichannelRoom; + options: CloseRoomParams['options']; +}; let autoTransferTimeout = 0; @@ -56,23 +62,25 @@ const handleAfterSaveMessage = (message: any = {}, room: any = {}): IMessage => return message; }; -const handleAfterCloseRoom = (room: any = {}): IRoom => { +const handleAfterCloseRoom = (params: LivechatCloseCallbackParams): LivechatCloseCallbackParams => { + const { room } = params; + const { _id: rid, autoTransferredAt, autoTransferOngoing } = room; if (!autoTransferTimeout || autoTransferTimeout <= 0) { - return room; + return params; } if (autoTransferredAt) { - return room; + return params; } if (!autoTransferOngoing) { - return room; + return params; } Promise.await(AutoTransferChatScheduler.unscheduleRoom(rid)); - return room; + return params; }; settings.watch('Livechat_auto_transfer_chat_timeout', function (value) { diff --git a/apps/meteor/ee/app/livechat-enterprise/server/hooks/sendPdfTranscriptOnClose.ts b/apps/meteor/ee/app/livechat-enterprise/server/hooks/sendPdfTranscriptOnClose.ts new file mode 100644 index 00000000000..fdcf0408bef --- /dev/null +++ b/apps/meteor/ee/app/livechat-enterprise/server/hooks/sendPdfTranscriptOnClose.ts @@ -0,0 +1,42 @@ +import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; +import { isOmnichannelRoom } from '@rocket.chat/core-typings'; +import { OmnichannelTranscript } from '@rocket.chat/core-services'; + +import { callbacks } from '../../../../../lib/callbacks'; +import type { CloseRoomParams } from '../../../../../app/livechat/server/lib/LivechatTyped.d'; + +type LivechatCloseCallbackParams = { + room: IOmnichannelRoom; + options: CloseRoomParams['options']; +}; + +const sendPdfTranscriptOnClose = async (params: LivechatCloseCallbackParams): Promise => { + const { room, options } = params; + + if (!isOmnichannelRoom(room)) { + return params; + } + + const { pdfTranscript } = options || {}; + if (!pdfTranscript) { + return params; + } + + const { requestedBy } = pdfTranscript; + + await OmnichannelTranscript.requestTranscript({ + details: { + userId: requestedBy, + rid: room._id, + }, + }); + + return params; +}; + +callbacks.add( + 'livechat.closeRoom', + (params: LivechatCloseCallbackParams) => Promise.await(sendPdfTranscriptOnClose(params)), + callbacks.priority.HIGH, + 'livechat-send-pdf-transcript-on-close-room', +); diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoCloseOnHoldScheduler.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoCloseOnHoldScheduler.ts index e0b71d2d800..a41ba8fa84a 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoCloseOnHoldScheduler.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/AutoCloseOnHoldScheduler.ts @@ -5,7 +5,7 @@ import moment from 'moment'; import { LivechatRooms, Users } from '@rocket.chat/models'; import type { IUser } from '@rocket.chat/core-typings'; -import { Livechat } from '../../../../../app/livechat/server'; +import { Livechat } from '../../../../../app/livechat/server/lib/LivechatTyped'; const SCHEDULER_NAME = 'omnichannel_auto_close_on_hold_scheduler'; @@ -49,15 +49,20 @@ class AutoCloseOnHoldSchedulerClass { private async executeJob({ attrs: { data } }: any = {}): Promise { const { roomId, comment } = data; + const [room, user] = await Promise.all([LivechatRooms.findOneById(roomId), this.getSchedulerUser()]); + if (!room || !user) { + throw new Error( + `Unable to process AutoCloseOnHoldScheduler job because room or user not found for roomId: ${roomId} and userId: rocket.cat`, + ); + } + const payload = { - user: await this.getSchedulerUser(), - room: await LivechatRooms.findOneById(roomId), + room, + user, comment, - options: {}, - visitor: undefined, }; - Livechat.closeRoom(payload); + await Livechat.closeRoom(payload); } private async getSchedulerUser(): Promise { diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/QueueInactivityMonitor.ts b/apps/meteor/ee/app/livechat-enterprise/server/lib/QueueInactivityMonitor.ts index f9962cdaaff..bcc2a73aebf 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/QueueInactivityMonitor.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/QueueInactivityMonitor.ts @@ -4,11 +4,12 @@ import { MongoInternals } from 'meteor/mongo'; import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import type { IUser, IOmnichannelRoom } from '@rocket.chat/core-typings'; +import { LivechatRooms as LivechatRoomsRaw, LivechatInquiry as LivechatInquiryRaw } from '@rocket.chat/models'; import { settings } from '../../../../../app/settings/server'; import { Logger } from '../../../../../app/logger/server'; -import { LivechatRooms, Users, LivechatInquiry } from '../../../../../app/models/server'; -import { Livechat } from '../../../../../app/livechat/server/lib/Livechat'; +import { Users } from '../../../../../app/models/server'; +import { Livechat } from '../../../../../app/livechat/server/lib/LivechatTyped'; const SCHEDULER_NAME = 'omnichannel_queue_inactivity_monitor'; @@ -92,29 +93,33 @@ export class OmnichannelQueueInactivityMonitorClass { await this.scheduler.cancel({ name }); } - closeRoomAction(room: IOmnichannelRoom): void { + closeRoomAction(room: IOmnichannelRoom): Promise { const comment = this.message; - Livechat.closeRoom({ + return Livechat.closeRoom({ comment, room, user: this.user, - visitor: null, }); } closeRoom({ attrs: { data } }: any = {}): void { const { inquiryId } = data; - const inquiry = LivechatInquiry.findOneById(inquiryId); - + const inquiry = Promise.await(LivechatInquiryRaw.findOneById(inquiryId)); this.logger.debug(`Processing inquiry item ${inquiryId}`); if (!inquiry || inquiry.status !== 'queued') { this.logger.debug(`Skipping inquiry ${inquiryId}. Invalid or not queued anymore`); return; } - this.closeRoomAction(LivechatRooms.findOneById(inquiry.rid)); - Promise.await(this.stopInquiry(inquiryId)); - this.logger.debug(`Running succesful. Closed inquiry ${inquiry._id} because of inactivity`); + const room = Promise.await(LivechatRoomsRaw.findOneById(inquiry.rid)); + if (!room) { + this.logger.error(`Error: unable to find room ${inquiry.rid} for inquiry ${inquiryId} to close in queue inactivity monitor`); + return; + } + + Promise.await(Promise.all([this.closeRoomAction(room), this.stopInquiry(inquiryId)])); + + this.logger.debug(`Running successful. Closed inquiry ${inquiry._id} because of inactivity`); } } diff --git a/apps/meteor/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.js b/apps/meteor/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.js index eea2ba2fc68..2ff7eb7c506 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.js +++ b/apps/meteor/ee/app/livechat-enterprise/server/lib/VisitorInactivityMonitor.js @@ -4,7 +4,7 @@ import { Meteor } from 'meteor/meteor'; import { LivechatVisitors, LivechatRooms, LivechatDepartment, Users } from '@rocket.chat/models'; import { settings } from '../../../../../app/settings/server'; -import { Livechat } from '../../../../../app/livechat/server/lib/Livechat'; +import { Livechat } from '../../../../../app/livechat/server/lib/LivechatTyped'; import { LivechatEnterprise } from './LivechatEnterprise'; import { logger } from './logger'; @@ -72,7 +72,7 @@ export class VisitorInactivityMonitor { if (room.departmentId) { comment = (await this._getDepartmentAbandonedCustomMessage(room.departmentId)) || comment; } - Livechat.closeRoom({ + await Livechat.closeRoom({ comment, room, user: this.user, diff --git a/apps/meteor/ee/app/livechat-enterprise/server/permissions.ts b/apps/meteor/ee/app/livechat-enterprise/server/permissions.ts index 95e07f94018..42d09d85015 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/permissions.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/permissions.ts @@ -6,6 +6,7 @@ export const createPermissions = async (): Promise => { const livechatMonitorRole = 'livechat-monitor'; const livechatManagerRole = 'livechat-manager'; const adminRole = 'admin'; + const livechatAgentRole = 'livechat-agent'; const monitorRole = await Roles.findOneById(livechatMonitorRole, { projection: { _id: 1 } }); if (!monitorRole) { @@ -22,5 +23,6 @@ export const createPermissions = async (): Promise => { Permissions.create('manage-livechat-canned-responses', [adminRole, livechatManagerRole, livechatMonitorRole]), Permissions.create('spy-voip-calls', [adminRole, livechatManagerRole, livechatMonitorRole]), Permissions.create('outbound-voip-calls', [adminRole, livechatManagerRole]), + Permissions.create('request-pdf-transcript', [adminRole, livechatManagerRole, livechatMonitorRole, livechatAgentRole]), ]); }; diff --git a/apps/meteor/ee/app/livechat-enterprise/server/settings.ts b/apps/meteor/ee/app/livechat-enterprise/server/settings.ts index a217874a553..efce3d51978 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/settings.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/settings.ts @@ -207,6 +207,12 @@ export const createSettings = async (): Promise => { enableQuery: omnichannelEnabledQuery, }); + settingsRegistry.add('Accounts_Default_User_Preferences_omnichannelTranscriptPDF', false, { + type: 'boolean', + public: true, + i18nLabel: 'Omnichannel_transcript_pdf', + }); + await Settings.addOptionValueById('Livechat_Routing_Method', { key: 'Load_Balancing', i18nLabel: 'Load_Balancing', diff --git a/apps/meteor/ee/app/voip-enterprise/server/services/voipService.ts b/apps/meteor/ee/app/voip-enterprise/server/services/voipService.ts index a9cd20fc8ea..94c9786c2db 100644 --- a/apps/meteor/ee/app/voip-enterprise/server/services/voipService.ts +++ b/apps/meteor/ee/app/voip-enterprise/server/services/voipService.ts @@ -1,4 +1,4 @@ -import type { ILivechatAgent, ILivechatVisitor, IRoomClosingInfo, IUser, IVoipRoom } from '@rocket.chat/core-typings'; +import type { ILivechatAgent, ILivechatVisitor, IVoipRoomClosingInfo, IUser, IVoipRoom } from '@rocket.chat/core-typings'; import type { IOmniRoomClosingMessage } from '../../../../../server/services/omnichannel-voip/internalTypes'; import { OmnichannelVoipService } from '../../../../../server/services/omnichannel-voip/service'; @@ -14,12 +14,12 @@ overwriteClassOnLicense('voip-enterprise', OmnichannelVoipService, { sysMessageId?: 'voip-call-wrapup' | 'voip-call-ended-unexpectedly', options?: { comment?: string | null; tags?: string[] | null }, ) => Promise, - closeInfo: IRoomClosingInfo, + closeInfo: IVoipRoomClosingInfo, closeSystemMsgData: IOmniRoomClosingMessage, room: IVoipRoom, sysMessageId: 'voip-call-wrapup' | 'voip-call-ended-unexpectedly', options?: { comment?: string; tags?: string[] }, - ): { closeInfo: IRoomClosingInfo; closeSystemMsgData: IOmniRoomClosingMessage } { + ): { closeInfo: IVoipRoomClosingInfo; closeSystemMsgData: IOmniRoomClosingMessage } { const { comment, tags } = options || {}; if (comment) { closeSystemMsgData.msg = comment; diff --git a/apps/meteor/ee/client/omnichannel/tags/TagsRoute.js b/apps/meteor/ee/client/omnichannel/tags/TagsRoute.js index 89b3d0097c9..43dbea252a0 100644 --- a/apps/meteor/ee/client/omnichannel/tags/TagsRoute.js +++ b/apps/meteor/ee/client/omnichannel/tags/TagsRoute.js @@ -95,12 +95,11 @@ function TagsRoute() { [reload, onRowClick], ); - if (context === 'new') { - return ; - } - if (context === 'edit') { - return ; + return ; + } + if (context === 'new') { + return ; } if (!canViewTags) { diff --git a/apps/meteor/ee/server/NetworkBroker.ts b/apps/meteor/ee/server/NetworkBroker.ts index 1f8b50e4ab3..b77c269dec7 100644 --- a/apps/meteor/ee/server/NetworkBroker.ts +++ b/apps/meteor/ee/server/NetworkBroker.ts @@ -78,7 +78,7 @@ export class NetworkBroker implements IBroker { this.broker.destroyService(name); } - createService(instance: IServiceClass): void { + createService(instance: IServiceClass, serviceDependencies?: string[]): void { const methods = ( instance.constructor?.name === 'Object' ? Object.getOwnPropertyNames(instance) @@ -97,7 +97,8 @@ export class NetworkBroker implements IBroker { return; } - const dependencies = name !== 'license' ? { dependencies: ['license'] } : {}; + // Allow services to depend on other services too + const dependencies = name !== 'license' ? { dependencies: ['license', ...(serviceDependencies || [])] } : {}; const service: ServiceSchema = { name, diff --git a/apps/meteor/ee/server/lib/registerServiceModels.ts b/apps/meteor/ee/server/lib/registerServiceModels.ts index cb4ef969603..14397d0fc43 100644 --- a/apps/meteor/ee/server/lib/registerServiceModels.ts +++ b/apps/meteor/ee/server/lib/registerServiceModels.ts @@ -26,6 +26,9 @@ import { IntegrationHistoryRaw } from '../../../server/models/raw/IntegrationHis import { IntegrationsRaw } from '../../../server/models/raw/Integrations'; import { EmailInboxRaw } from '../../../server/models/raw/EmailInbox'; import { PbxEventsRaw } from '../../../server/models/raw/PbxEvents'; +import { LivechatRoomsRaw } from '../../../server/models/raw/LivechatRooms'; +import { UploadsRaw } from '../../../server/models/raw/Uploads'; +import { LivechatVisitorsRaw } from '../../../server/models/raw/LivechatVisitors'; // TODO add trash param to appropiate model instances export function registerServiceModels(db: Db, trash?: Collection>): void { @@ -55,4 +58,7 @@ export function registerServiceModels(db: Db, trash?: Collection new IntegrationsRaw(db)); registerModel('IEmailInboxModel', () => new EmailInboxRaw(db)); registerModel('IPbxEventsModel', () => new PbxEventsRaw(db)); + registerModel('ILivechatRoomsModel', () => new LivechatRoomsRaw(db)); + registerModel('IUploadsModel', () => new UploadsRaw(db)); + registerModel('ILivechatVisitorsModel', () => new LivechatVisitorsRaw(db)); } diff --git a/apps/meteor/ee/server/models/raw/LivechatRooms.ts b/apps/meteor/ee/server/models/raw/LivechatRooms.ts index f0513cb51a0..de7e2b12466 100644 --- a/apps/meteor/ee/server/models/raw/LivechatRooms.ts +++ b/apps/meteor/ee/server/models/raw/LivechatRooms.ts @@ -202,11 +202,18 @@ export class LivechatRoomsRawEE extends LivechatRoomsRaw implements ILivechatRoo return super.update(restrictedQuery, ...restArgs); } - updateOne(...args: Parameters) { - const [query, ...restArgs] = args; + updateOne(...args: Parameters & { bypassUnits?: boolean }) { + const [query, update, opts, extraOpts] = args; + if (extraOpts?.bypassUnits) { + // When calling updateOne from a service, we cannot call the meteor code inside the query restrictions + // So the solution now is to pass a bypassUnits flag to the updateOne method which prevents checking + // units restrictions on the query, but just for the query the service is actually using + // We need to find a way of remove the meteor dependency when fetching units, and then, we can remove this flag + return super.updateOne(query, update, opts); + } const restrictedQuery = addQueryRestrictionsToRoomsModel(query); queriesLogger.debug({ msg: 'LivechatRoomsRawEE.updateOne', query: restrictedQuery }); - return super.updateOne(restrictedQuery, ...restArgs); + return super.updateOne(restrictedQuery, update, opts); } updateMany(...args: Parameters) { diff --git a/apps/meteor/ee/server/services/docker-compose.yml b/apps/meteor/ee/server/services/docker-compose.yml index 5eb010f85f1..60eb6d27df4 100644 --- a/apps/meteor/ee/server/services/docker-compose.yml +++ b/apps/meteor/ee/server/services/docker-compose.yml @@ -100,6 +100,28 @@ services: depends_on: - nats + queue-worker-service: + container_name: queue-worker-service + build: + context: . + args: + SERVICE: queue-worker + image: rocketchat/queue-worker-service:latest + env_file: .config/services/service.env + depends_on: + - nats + + omnichannel-transcript-service: + container_name: omnichannel-transcript-service + build: + context: . + args: + SERVICE: omnichannel-transcript + image: rocketchat/omnichannel-transcript-service:latest + env_file: .config/services/service.env + depends_on: + - nats + nats: image: nats ports: diff --git a/apps/meteor/lib/callbacks.ts b/apps/meteor/lib/callbacks.ts index 8c1df9b3548..749d6d17193 100644 --- a/apps/meteor/lib/callbacks.ts +++ b/apps/meteor/lib/callbacks.ts @@ -15,6 +15,7 @@ import type { ParsedUrl, OEmbedMeta, OEmbedUrlContent, + IOmnichannelRoom, } from '@rocket.chat/core-typings'; import type { Logger } from '../app/logger/server'; @@ -22,6 +23,7 @@ import type { IBusinessHourBehavior } from '../app/livechat/server/business-hour import { getRandomId } from './random'; import type { ILoginAttempt } from '../app/authentication/server/ILoginAttempt'; import { compareByRanking } from './utils/comparisons'; +import type { CloseRoomParams } from '../app/livechat/server/lib/LivechatTyped.d'; enum CallbackPriority { HIGH = -1000, @@ -49,7 +51,7 @@ type EventLikeCallbackSignatures = { 'afterSaveMessage': (message: IMessage, room: IRoom, uid?: string) => void; 'livechat.removeAgentDepartment': (params: { departmentId: ILivechatDepartmentRecord['_id']; agentsId: ILivechatAgent['_id'][] }) => void; 'livechat.saveAgentDepartment': (params: { departmentId: ILivechatDepartmentRecord['_id']; agentsId: ILivechatAgent['_id'][] }) => void; - 'livechat.closeRoom': (room: IRoom) => void; + 'livechat.closeRoom': (params: { room: IOmnichannelRoom; options: CloseRoomParams['options'] }) => void; 'livechat.saveRoom': (room: IRoom) => void; 'livechat:afterReturnRoomAsInquiry': (params: { room: IRoom }) => void; 'livechat.setUserStatusLivechat': (params: { userId: IUser['_id']; status: OmnichannelAgentStatus }) => void; @@ -112,7 +114,6 @@ type ChainedCallbackSignatures = { agentsId: ILivechatAgent['_id'][]; }; 'livechat.applySimultaneousChatRestrictions': (_: undefined, params: { departmentId?: ILivechatDepartmentRecord['_id'] }) => undefined; - 'livechat.beforeCloseRoom': (params: { room: IRoom; options: unknown }) => { room: IRoom; options: unknown }; 'livechat.beforeDelegateAgent': (agent: ILivechatAgent, params: { department?: ILivechatDepartmentRecord }) => ILivechatAgent | null; 'livechat.applyDepartmentRestrictions': ( query: FilterOperators, diff --git a/apps/meteor/package.json b/apps/meteor/package.json index e2109059ba9..5310ada0344 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -206,6 +206,7 @@ "@nivo/line": "0.79.1", "@nivo/pie": "0.79.1", "@react-aria/color": "^3.0.0-beta.15", + "@react-pdf/renderer": "^3.1.3", "@rocket.chat/agenda": "workspace:^", "@rocket.chat/api-client": "workspace:^", "@rocket.chat/apps-engine": "1.36.0", @@ -233,11 +234,14 @@ "@rocket.chat/model-typings": "workspace:^", "@rocket.chat/models": "workspace:^", "@rocket.chat/mp3-encoder": "0.24.0", + "@rocket.chat/omnichannel-services": "workspace:^", "@rocket.chat/onboarding-ui": "next", + "@rocket.chat/pdf-worker": "workspace:^", "@rocket.chat/poplib": "workspace:^", "@rocket.chat/presence": "workspace:^", "@rocket.chat/rest-typings": "workspace:^", "@rocket.chat/string-helpers": "next", + "@rocket.chat/tools": "workspace:^", "@rocket.chat/ui-client": "workspace:^", "@rocket.chat/ui-composer": "workspace:^", "@rocket.chat/ui-contexts": "workspace:^", @@ -287,6 +291,7 @@ "dompurify": "^2.3.8", "ejson": "^2.2.2", "emailreplyparser": "^0.0.5", + "emoji-toolkit": "^7.0.0", "emojione": "^4.5.0", "eslint-plugin-anti-trojan-source": "^1.1.0", "eventemitter3": "^4.0.7", @@ -336,6 +341,7 @@ "moleculer": "^0.14.21", "moment": "^2.29.4", "moment-timezone": "^0.5.34", + "mongo-message-queue": "^1.0.0", "mongodb": "^4.12.1", "mongodb-memory-server": "^7.6.3", "nats": "^2.6.1", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 95576e93499..78777bd8b62 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -944,6 +944,9 @@ "Closed_automatically_because_chat_was_onhold_for_seconds": "Closed automatically because chat was On Hold for __onHoldTime__ seconds", "Closed_automatically_chat_queued_too_long": "Closed automatically by the system (queue maximum time exceeded)", "Closed_by_visitor": "Closed by visitor", + "Wrap_up_conversation": "Wrap up conversation", + "These_options_affect_this_conversation_only_To_set_default_selections_go_to_My_Account_Omnichannel": "These options affect this conversation only. To set default selections, go to My Account > Omnichannel.", + "This_option_affect_this_conversation_only_To_set_default_selection_go_to_My_Account_Omnichannel": "This option affect this conversation only. To set default selection, go to My Account > Omnichannel.", "Closing_chat": "Closing chat", "Closing_chat_message": "Closing chat message", "Cloud": "Cloud", @@ -1754,6 +1757,7 @@ "End": "End", "End_suspicious_sessions": "End any suspicious sessions", "End_call": "End call", + "End_conversation": "End conversation", "Expand_view": "Expand view", "Explore_marketplace": "Explore Marketplace", "Explore_the_marketplace_to_find_awesome_apps": "Explore the Marketplace to find awesome apps for Rocket.Chat", @@ -1924,6 +1928,7 @@ "error-role-name-required": "Role name is required", "error-room-does-not-exist": "This room does not exist", "error-role-already-present": "A role with this name already exists", + "error-room-already-closed": "Room is already closed", "error-room-is-not-closed": "Room is not closed", "error-room-onHold": "Error! Room is On Hold", "error-selected-agent-room-agent-are-same": "The selected agent and the room agent are the same", @@ -2920,7 +2925,8 @@ "Livechat_title": "Livechat Title", "Livechat_title_color": "Livechat Title Background Color", "Livechat_transcript_already_requested_warning": "The transcript of this chat has already been requested and will be sent as soon as the conversation ends.", - "Livechat_transcript_has_been_requested": "The chat transcript has been requested.", + "Livechat_transcript_has_been_requested": "Export requested. It may take a few seconds.", + "Livechat_email_transcript_has_been_requested": "The transcript has been requested. It may take a few seconds.", "Livechat_transcript_request_has_been_canceled": "The chat transcription request has been canceled.", "Livechat_transcript_sent": "Omnichannel transcript sent", "Livechat_transfer_return_to_the_queue": "__from__ returned the chat to the queue", @@ -3649,6 +3655,8 @@ "Paid_Apps": "Paid Apps", "Payload": "Payload", "PDF": "PDF", + "pdf_success_message": "PDF Transcript successfully generated", + "pdf_error_message": "Error generating PDF Transcript", "Peer_Password": "Peer Password", "People": "People", "Permalink": "Permalink", @@ -3936,6 +3944,8 @@ "Request_comment_when_closing_conversation": "Request comment when closing conversation", "Request_comment_when_closing_conversation_description": "If enabled, the agent will need to set a comment before the conversation is closed.", "Request_tag_before_closing_chat": "Request tag(s) before closing conversation", + "request-pdf-transcript": "Request PDF Transcript", + "request-pdf-transcript_description": "Permission to request a PDF transcript for a given Omnichannel room", "Requested_At": "Requested At", "Requested_By": "Requested By", "Require": "Require", @@ -4268,6 +4278,8 @@ "Send_Test_Email": "Send test email", "Send_via_email": "Send via email", "Send_via_Email_as_attachment": "Send via Email as attachment", + "Export_as_PDF": "Export as PDF", + "Export_enabled_at_the_end_of_the_conversation": "Export enabled at the end of the conversation", "Send_Visitor_navigation_history_as_a_message": "Send Visitor Navigation History as a Message", "Send_visitor_navigation_history_on_request": "Send Visitor Navigation History on Request", "Send_welcome_email": "Send welcome email", @@ -5501,6 +5513,23 @@ "Theme_match_system": "Match system", "Join_your_team": "Join your team", "Create_an_account": "Create an account", + "Chat_transcript": "Chat transcript", + "Conversational_transcript": "Conversational transcript", + "Send_conversation_transcript_via_email": "Send conversation transcript via email", + "Always_send_the_transcript_to_contacts_at_the_end_of_the_conversations": "Always send the transcript to contacts at the end of the conversations.", + "Export_conversation_transcript_as_PDF": "Export conversation transcript as PDF", + "Omnichannel_transcript_email": "Send chat transcript via email.", + "Accounts_Default_User_Preferences_omnichannelTranscriptEmail_Description": "Always send the transcript to contacts at the end of the conversations.", + "Omnichannel_transcript_pdf": "Export chat transcript as PDF.", + "Accounts_Default_User_Preferences_omnichannelTranscriptPDF_Description": "Always export the transcript as PDF at the end of conversations.", + "Contact_email": "Contact email", + "Customer": "Customer", + "Time": "Time", + "Omnichannel_Agent": "Omnichannel Agent", + "This_attachment_is_not_supported": "This attachment is not supported", + "Send_transcript": "Send transcript", + "Undo_request": "Undo request", + "No_permission": "No permission", "Community_cap_description": "Community workspaces have a cap of 200 concurrent connections, although you can have more connections active, once you hit that limit you won't be able to see users' status. This doesn't affect their ability to send & receive messages.", "Enterprise_cap_description": "Enterprise workspaces have no cap on the presence service.", "Service_status": "Service status", diff --git a/apps/meteor/server/methods/saveUserPreferences.js b/apps/meteor/server/methods/saveUserPreferences.js index 842c4ccd163..194f58139ed 100644 --- a/apps/meteor/server/methods/saveUserPreferences.js +++ b/apps/meteor/server/methods/saveUserPreferences.js @@ -35,6 +35,8 @@ Meteor.methods({ sidebarDisplayAvatar: Match.Optional(Boolean), sidebarGroupByType: Match.Optional(Boolean), muteFocusedConversations: Match.Optional(Boolean), + omnichannelTranscriptEmail: Match.Optional(Boolean), + omnichannelTranscriptPDF: Match.Optional(Boolean), }; check(settings, Match.ObjectIncluding(keys)); const user = Meteor.user(); diff --git a/apps/meteor/server/models/raw/LivechatInquiry.ts b/apps/meteor/server/models/raw/LivechatInquiry.ts index db70a888b38..0df53468d2b 100644 --- a/apps/meteor/server/models/raw/LivechatInquiry.ts +++ b/apps/meteor/server/models/raw/LivechatInquiry.ts @@ -1,5 +1,5 @@ import type { ILivechatInquiryModel } from '@rocket.chat/model-typings'; -import type { Collection, Db, Document, FindOptions, DistinctOptions, UpdateResult } from 'mongodb'; +import type { Collection, Db, Document, FindOptions, DistinctOptions, UpdateResult, DeleteResult } from 'mongodb'; import type { ILivechatInquiryRecord, IMessage, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; import { LivechatInquiryStatus } from '@rocket.chat/core-typings'; @@ -91,4 +91,8 @@ export class LivechatInquiryRaw extends BaseRaw implemen { $unset: { locked: 1, lockedAt: 1 } }, ); } + + async removeByRoomId(rid: string): Promise { + return this.deleteOne({ rid }); + } } diff --git a/apps/meteor/server/models/raw/LivechatRooms.js b/apps/meteor/server/models/raw/LivechatRooms.js index 542f05ddbb0..b6e39293b20 100644 --- a/apps/meteor/server/models/raw/LivechatRooms.js +++ b/apps/meteor/server/models/raw/LivechatRooms.js @@ -1289,6 +1289,113 @@ export class LivechatRoomsRaw extends BaseRaw { ]); } + // These 3 methods shouldn't be here :( but current EE model has a meteor dependency + // And refactoring it could take time + setTranscriptRequestedPdfById(rid) { + return this.updateOne( + { + _id: rid, + }, + { + $set: { pdfTranscriptRequested: true }, + }, + {}, + { + bypassUnits: true, + }, + ); + } + + unsetTranscriptRequestedPdfById(rid) { + return this.updateOne( + { + _id: rid, + }, + { + $unset: { pdfTranscriptRequested: 1 }, + }, + {}, + { + bypassUnits: true, + }, + ); + } + + setPdfTranscriptFileIdById(rid, fileId) { + return this.updateOne( + { + _id: rid, + }, + { + $set: { pdfTranscriptFileId: fileId }, + }, + {}, + { + bypassUnits: true, + }, + ); + } + + setEmailTranscriptRequestedByRoomId(roomId, transcriptInfo) { + const { requestedAt, requestedBy, email, subject } = transcriptInfo; + + return this.updateOne( + { + _id: roomId, + t: 'l', + }, + { + $set: { + transcriptRequest: { + requestedAt, + requestedBy, + email, + subject, + }, + }, + }, + ); + } + + unsetEmailTranscriptRequestedByRoomId(roomId) { + return this.updateOne( + { + _id: roomId, + t: 'l', + }, + { + $unset: { + transcriptRequest: 1, + }, + }, + ); + } + + closeRoomById(roomId, closeInfo) { + const { closer, closedBy, closedAt, chatDuration, serviceTimeDuration, tags } = closeInfo; + + return this.updateOne( + { + _id: roomId, + t: 'l', + }, + { + $set: { + closedAt, + 'metrics.chatDuration': chatDuration, + 'metrics.serviceTimeDuration': serviceTimeDuration, + 'v.status': 'offline', + ...(closer && { closer }), + ...(closedBy && { closedBy }), + ...(tags && { tags }), + }, + $unset: { + open: 1, + }, + }, + ); + } + bulkRemoveDepartmentAndUnitsFromRooms(departmentId) { return this.updateMany({ departmentId }, { $unset: { departmentId: 1, departmentAncestors: 1 } }); } diff --git a/apps/meteor/server/models/raw/Messages.ts b/apps/meteor/server/models/raw/Messages.ts index f0dcc66ab7e..d1e15fc96cb 100644 --- a/apps/meteor/server/models/raw/Messages.ts +++ b/apps/meteor/server/models/raw/Messages.ts @@ -11,6 +11,7 @@ import type { Filter, FindOptions, IndexDescription, + DeleteResult, } from 'mongodb'; import { escapeRegExp } from '@rocket.chat/string-helpers'; @@ -222,6 +223,26 @@ export class MessagesRaw extends BaseRaw implements IMessagesModel { ); } + findLivechatMessages(rid: IRoom['_id'], options?: FindOptions): FindCursor { + return this.find( + { + rid, + $or: [{ t: { $exists: false } }, { t: 'livechat-close' }], + }, + options, + ); + } + + findLivechatMessagesWithoutClosing(rid: IRoom['_id'], options?: FindOptions): FindCursor { + return this.find( + { + rid, + t: { $exists: false }, + }, + options, + ); + } + async setBlocksById(_id: string, blocks: Required['blocks']): Promise { await this.updateOne( { _id }, @@ -404,4 +425,8 @@ export class MessagesRaw extends BaseRaw implements IMessagesModel { .toArray() )[0] as IMessage; } + + removeByRoomId(roomId: string): Promise { + return this.deleteMany({ rid: roomId }); + } } diff --git a/apps/meteor/server/models/raw/VoipRoom.ts b/apps/meteor/server/models/raw/VoipRoom.ts index 71711c608df..2aa92317659 100644 --- a/apps/meteor/server/models/raw/VoipRoom.ts +++ b/apps/meteor/server/models/raw/VoipRoom.ts @@ -1,4 +1,4 @@ -import type { IRoomClosingInfo, IVoipRoom, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; +import type { IVoipRoomClosingInfo, IVoipRoom, RocketChatRecordDeleted } from '@rocket.chat/core-typings'; import type { FindPaginated, IVoipRoomModel } from '@rocket.chat/model-typings'; import type { Collection, FindCursor, Db, Filter, FindOptions, UpdateResult, Document } from 'mongodb'; import { escapeRegExp } from '@rocket.chat/string-helpers'; @@ -81,7 +81,7 @@ export class VoipRoomRaw extends BaseRaw implements IVoipRoomModel { return this.findOne(query, options); } - closeByRoomId(roomId: IVoipRoom['_id'], closeInfo: IRoomClosingInfo): Promise { + closeByRoomId(roomId: IVoipRoom['_id'], closeInfo: IVoipRoomClosingInfo): Promise { const { closer, closedBy, closedAt, callDuration, serviceTimeDuration, ...extraData } = closeInfo; return this.updateOne( diff --git a/apps/meteor/server/services/messages/service.ts b/apps/meteor/server/services/messages/service.ts new file mode 100644 index 00000000000..b16078274c2 --- /dev/null +++ b/apps/meteor/server/services/messages/service.ts @@ -0,0 +1,13 @@ +import type { IMessage } from '@rocket.chat/core-typings'; +import type { IMessageService } from '@rocket.chat/core-services'; +import { ServiceClassInternal } from '@rocket.chat/core-services'; + +import { executeSendMessage } from '../../../app/lib/server/methods/sendMessage'; + +export class MessageService extends ServiceClassInternal implements IMessageService { + protected name = 'message'; + + async sendMessage({ fromId, rid, msg }: { fromId: string; rid: string; msg: string }): Promise { + return executeSendMessage(fromId, { rid, msg }); + } +} diff --git a/apps/meteor/server/services/omnichannel-voip/service.ts b/apps/meteor/server/services/omnichannel-voip/service.ts index 1d4b58a33fd..b62b722ab91 100644 --- a/apps/meteor/server/services/omnichannel-voip/service.ts +++ b/apps/meteor/server/services/omnichannel-voip/service.ts @@ -9,7 +9,7 @@ import type { ILivechatAgent, ILivechatVisitor, IVoipRoom, - IRoomClosingInfo, + IVoipRoomClosingInfo, } from '@rocket.chat/core-typings'; import { isILivechatVisitor, OmnichannelSourceType, isVoipRoom, VoipClientEvents } from '@rocket.chat/core-typings'; import type { PaginatedResult } from '@rocket.chat/rest-typings'; @@ -308,12 +308,12 @@ export class OmnichannelVoipService extends ServiceClassInternal implements IOmn } getRoomClosingData( - closeInfo: IRoomClosingInfo, + closeInfo: IVoipRoomClosingInfo, closeSystemMsgData: IOmniRoomClosingMessage, _room: IVoipRoom, _sysMessageId: 'voip-call-wrapup' | 'voip-call-ended-unexpectedly', _options?: { comment?: string; tags?: string[] }, - ): { closeInfo: IRoomClosingInfo; closeSystemMsgData: IOmniRoomClosingMessage } { + ): { closeInfo: IVoipRoomClosingInfo; closeSystemMsgData: IOmniRoomClosingMessage } { return { closeInfo, closeSystemMsgData }; } @@ -322,11 +322,11 @@ export class OmnichannelVoipService extends ServiceClassInternal implements IOmn room: IVoipRoom, sysMessageId: 'voip-call-wrapup' | 'voip-call-ended-unexpectedly', _options?: { comment?: string; tags?: string[] }, - ): Promise<{ closeInfo: IRoomClosingInfo; closeSystemMsgData: IOmniRoomClosingMessage }> { + ): Promise<{ closeInfo: IVoipRoomClosingInfo; closeSystemMsgData: IOmniRoomClosingMessage }> { const now = new Date(); const closer = isILivechatVisitor(closerParam) ? 'visitor' : 'user'; - const closeData: IRoomClosingInfo = { + const closeData: IVoipRoomClosingInfo = { closedAt: now, callDuration: now.getTime() - room.ts.getTime(), closer, diff --git a/apps/meteor/server/services/room/service.ts b/apps/meteor/server/services/room/service.ts index 4bf24606be4..bd6ba099a96 100644 --- a/apps/meteor/server/services/room/service.ts +++ b/apps/meteor/server/services/room/service.ts @@ -4,6 +4,7 @@ import { ServiceClassInternal, Authorization } from '@rocket.chat/core-services' import type { ICreateRoomParams, IRoomService } from '@rocket.chat/core-services'; import { createRoom } from '../../../app/lib/server/functions/createRoom'; // TODO remove this import +import { createDirectMessage } from '../../methods/createDirectMessage'; export class RoomService extends ServiceClassInternal implements IRoomService { protected name = 'room'; @@ -27,6 +28,18 @@ export class RoomService extends ServiceClassInternal implements IRoomService { return createRoom(type, name, user.username, members, readOnly, extraData, options) as unknown as IRoom; } + async createDirectMessage({ to, from }: { to: string; from: string }): Promise<{ rid: string }> { + const [toUser, fromUser] = await Promise.all([ + Users.findOneById(to, { projection: { username: 1 } }), + Users.findOneById(from, { projection: { _id: 1 } }), + ]); + + if (!toUser || !fromUser) { + throw new Error('error-invalid-user'); + } + return createDirectMessage([toUser.username], fromUser._id); + } + async addMember(uid: string, rid: string): Promise { const hasPermission = await Authorization.hasPermission(uid, 'add-user-to-joined-room', rid); if (!hasPermission) { diff --git a/apps/meteor/server/services/settings/service.ts b/apps/meteor/server/services/settings/service.ts new file mode 100644 index 00000000000..5fe5adf026b --- /dev/null +++ b/apps/meteor/server/services/settings/service.ts @@ -0,0 +1,12 @@ +import type { ISettingsService } from '@rocket.chat/core-services'; +import { ServiceClassInternal } from '@rocket.chat/core-services'; + +import { settings } from '../../../app/settings/server'; + +export class SettingsService extends ServiceClassInternal implements ISettingsService { + protected name = 'settings'; + + async get(settingId: string): Promise { + return settings.get(settingId); + } +} diff --git a/apps/meteor/server/services/startup.ts b/apps/meteor/server/services/startup.ts index 79be55d3a1e..6ebb335f2f4 100644 --- a/apps/meteor/server/services/startup.ts +++ b/apps/meteor/server/services/startup.ts @@ -1,5 +1,6 @@ import { MongoInternals } from 'meteor/mongo'; import { api } from '@rocket.chat/core-services'; +import { OmnichannelTranscript, QueueWorker } from '@rocket.chat/omnichannel-services'; import { AnalyticsService } from './analytics/service'; import { AppsEngineService } from './apps-engine/service'; @@ -22,6 +23,10 @@ import { PushService } from './push/service'; import { DeviceManagementService } from './device-management/service'; import { FederationService } from './federation/service'; import { UploadService } from './upload/service'; +import { MessageService } from './messages/service'; +import { TranslationService } from './translation/service'; +import { SettingsService } from './settings/service'; +import { Logger } from '../lib/logger/Logger'; const { db } = MongoInternals.defaultRemoteCollectionDriver().mongo; @@ -45,6 +50,9 @@ api.registerService(new DeviceManagementService()); api.registerService(new VideoConfService()); api.registerService(new FederationService()); api.registerService(new UploadService()); +api.registerService(new MessageService()); +api.registerService(new TranslationService()); +api.registerService(new SettingsService()); // if the process is running in micro services mode we don't need to register services that will run separately if (!isRunningMs()) { @@ -55,5 +63,11 @@ if (!isRunningMs()) { api.registerService(new Presence()); api.registerService(new Authorization()); + + // Run EE services defined outside of the main repo + // Otherwise, monolith would ignore them :( + // Always register the service and manage licensing inside the service (tbd) + api.registerService(new QueueWorker(db, Logger)); + api.registerService(new OmnichannelTranscript(Logger)); })(); } diff --git a/apps/meteor/server/services/translation/service.ts b/apps/meteor/server/services/translation/service.ts new file mode 100644 index 00000000000..5bc77d8ba40 --- /dev/null +++ b/apps/meteor/server/services/translation/service.ts @@ -0,0 +1,35 @@ +import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; +import { Settings } from '@rocket.chat/models'; +import type { IUser } from '@rocket.chat/core-typings'; +import mem from 'mem'; +import { ServiceClassInternal } from '@rocket.chat/core-services'; +import type { ITranslationService } from '@rocket.chat/core-services'; + +export class TranslationService extends ServiceClassInternal implements ITranslationService { + protected name = 'translation'; + + // Cache the server language for 1 hour + private getServerLanguageCached = mem(this.getServerLanguage.bind(this), { maxAge: 1000 * 60 * 60 }); + + private async getServerLanguage(): Promise { + return ((await Settings.findOneById('Language'))?.value as string) || 'en'; + } + + // Use translateText when you already know the language, or want to translate to a predefined language + translateText(text: string, targetLanguage: string): Promise { + return Promise.resolve(TAPi18n.__(text, { lng: targetLanguage })); + } + + // Use translate when you want to translate to the user's language, or server's as a fallback + async translate(text: string, user: IUser): Promise { + const language = user.language || (await this.getServerLanguageCached()); + + return this.translateText(text, language); + } + + async translateToServerLanguage(text: string): Promise { + const language = await this.getServerLanguageCached(); + + return this.translateText(text, language); + } +} diff --git a/apps/meteor/server/services/upload/service.ts b/apps/meteor/server/services/upload/service.ts index 8166012c17e..0c04c860e5c 100644 --- a/apps/meteor/server/services/upload/service.ts +++ b/apps/meteor/server/services/upload/service.ts @@ -8,9 +8,11 @@ import { FileUpload } from '../../../app/file-upload/server'; export class UploadService extends ServiceClassInternal implements IUploadService { protected name = 'upload'; - async uploadFile({ buffer, details }: IUploadFileParams): Promise { - const fileStore = FileUpload.getStore('Uploads'); - return fileStore.insert(details, buffer); + async uploadFile({ buffer, details, userId }: IUploadFileParams): Promise { + return Meteor.runAsUser(userId, () => { + const fileStore = FileUpload.getStore('Uploads'); + return fileStore.insert(details, buffer); + }); } async sendFileMessage({ roomId, file, userId, message }: ISendFileMessageParams): Promise { @@ -21,13 +23,15 @@ export class UploadService extends ServiceClassInternal implements IUploadServic return Meteor.call('sendFileLivechatMessage', roomId, visitorToken, file, message); } - async getFileBuffer({ file }: { userId: string; file: IUpload }): Promise { - return new Promise((resolve, reject) => { - FileUpload.getBuffer(file, (err: Error, buffer: Buffer) => { - if (err) { - return reject(err); - } - return resolve(buffer); + async getFileBuffer({ userId, file }: { userId: string; file: IUpload }): Promise { + return Meteor.runAsUser(userId, () => { + return new Promise((resolve, reject) => { + FileUpload.getBuffer(file, (err: Error, buffer: Buffer) => { + if (err) { + return reject(err); + } + return resolve(buffer); + }); }); }); } diff --git a/apps/meteor/tests/data/livechat/inboxes.ts b/apps/meteor/tests/data/livechat/inboxes.ts index 8f2fef1c9f4..ccc7c742da1 100644 --- a/apps/meteor/tests/data/livechat/inboxes.ts +++ b/apps/meteor/tests/data/livechat/inboxes.ts @@ -11,7 +11,6 @@ export const createEmailInbox = async (): Promise<{ _id: string }> => { email: `test${new Date().getTime()}@test.com`, description: 'test', senderInfo: 'test', - department: 'test', smtp: { server: 'smtp.example.com', port: 587, diff --git a/apps/meteor/tests/e2e/omnichannel-send-transcript.spec.ts b/apps/meteor/tests/e2e/omnichannel-send-transcript.spec.ts index b6d06784b22..e2847ef8753 100644 --- a/apps/meteor/tests/e2e/omnichannel-send-transcript.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel-send-transcript.spec.ts @@ -3,6 +3,7 @@ import type { Browser, Page } from '@playwright/test'; import { test, expect } from './utils/test'; import { OmnichannelLiveChat, HomeChannel } from './page-objects'; +import { IS_EE } from './config/constants'; const createAuxContext = async (browser: Browser, storageState: string): Promise<{ page: Page; poHomeChannel: HomeChannel }> => { const page = await browser.newPage({ storageState }); @@ -52,10 +53,18 @@ test.describe('omnichannel-transcript', () => { await agent.poHomeChannel.sidenav.openChat(newUser.name); }); - await test.step('Expect to be able to create transcript', async () => { + await test.step('Expect to be able to send transcript to email', async () => { await agent.poHomeChannel.content.btnSendTranscript.click(); + await agent.poHomeChannel.content.btnSendTranscriptToEmail.click(); await agent.poHomeChannel.content.btnModalConfirm.click(); await expect(agent.poHomeChannel.toastSuccess).toBeVisible(); }); + + await test.step('Expect to be not able send transcript as PDF', async () => { + test.skip(!IS_EE, 'Enterprise Only'); + await agent.poHomeChannel.content.btnSendTranscript.click(); + await agent.poHomeChannel.content.btnSendTranscriptAsPDF.hover(); + await expect(agent.poHomeChannel.content.btnSendTranscriptAsPDF).toHaveAttribute('aria-disabled', 'true'); + }); }); }); 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 8d0dfc290d5..72b81586d37 100644 --- a/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts +++ b/apps/meteor/tests/e2e/page-objects/fragments/home-content.ts @@ -130,6 +130,14 @@ export class HomeContent { return this.page.locator('[data-qa-id="ToolBoxAction-mail-arrow-top-right"]'); } + get btnSendTranscriptToEmail(): Locator { + return this.page.locator('li.rcx-option', { hasText: 'Send via email' }); + } + + get btnSendTranscriptAsPDF(): Locator { + return this.page.locator('li.rcx-option', { hasText: 'Export as PDF' }); + } + get btnCannedResponses(): Locator { return this.page.locator('[data-qa-id="ToolBoxAction-canned-response"]'); } diff --git a/apps/meteor/tests/end-to-end/api/00-miscellaneous.js b/apps/meteor/tests/end-to-end/api/00-miscellaneous.js index df30f2b71cd..8a956508149 100644 --- a/apps/meteor/tests/end-to-end/api/00-miscellaneous.js +++ b/apps/meteor/tests/end-to-end/api/00-miscellaneous.js @@ -151,6 +151,8 @@ describe('miscellaneous', function () { 'emailNotificationMode', 'unreadAlert', 'notificationsSoundVolume', + 'omnichannelTranscriptEmail', + IS_EE ? 'omnichannelTranscriptPDF' : false, 'desktopNotifications', 'pushNotifications', 'enableAutoAway', @@ -169,7 +171,7 @@ describe('miscellaneous', function () { 'sidebarDisplayAvatar', 'sidebarGroupByType', 'muteFocusedConversations', - ]; + ].filter((p) => Boolean(p)); expect(res.body).to.have.property('success', true); expect(res.body).to.have.property('_id', credentials['X-User-Id']); diff --git a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts index 51300079aa1..e34e1ab1312 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/00-rooms.ts @@ -17,6 +17,8 @@ import { getLivechatRoomInfo, sendMessage, closeRoom, + takeInquiry, + fetchInquiry, } from '../../../data/livechat/rooms'; import { updatePermission, updateSetting } from '../../../data/permissions.helper'; import { createUser, login } from '../../../data/users.helper.js'; @@ -1447,8 +1449,106 @@ describe('LIVECHAT - rooms', function () { }); }); - // TODO: Implement proper department data cleanup after each test to run in CE - (IS_EE ? describe : describe.skip)('it should mark room as unread when a new message arrives and the config is activated', () => { + describe('livechat/room.closeByUser', () => { + it('should fail if user is not logged in', async () => { + await request.post(api('livechat/room.closeByUser')).expect(401); + }); + it('should fail if not all required params are passed (rid)', async () => { + await request.post(api('livechat/room.closeByUser')).set(credentials).expect(400); + }); + it('should fail if user doesnt have close-livechat-room permission', async () => { + await updatePermission('close-livechat-room', []); + await request.post(api('livechat/room.closeByUser')).set(credentials).send({ rid: 'invalid-room-id' }).expect(403); + }); + it('should fail if room is not found', async () => { + await updatePermission('close-livechat-room', ['admin']); + await request.post(api('livechat/room.closeByUser')).set(credentials).send({ rid: 'invalid-room-id' }).expect(400); + }); + it('should fail if room is closed', async () => { + const visitor = await createVisitor(); + const { _id } = await createLivechatRoom(visitor.token); + await closeRoom(_id); + await request.post(api('livechat/room.closeByUser')).set(credentials).send({ rid: _id }).expect(400); + }); + it('should fail if user is not serving and doesnt have close-others-livechat-room permission', async () => { + await updatePermission('close-others-livechat-room', []); + const visitor = await createVisitor(); + const { _id } = await createLivechatRoom(visitor.token); + await request.post(api('livechat/room.closeByUser')).set(credentials).send({ rid: _id }).expect(400); + }); + it('should close room if user has permission', async () => { + await updatePermission('close-others-livechat-room', ['admin']); + const visitor = await createVisitor(); + const { _id } = await createLivechatRoom(visitor.token); + await request.post(api('livechat/room.closeByUser')).set(credentials).send({ rid: _id }).expect(200); + }); + }); + + (IS_EE ? describe : describe.skip)('omnichannel/:rid/request-transcript', () => { + before(async () => { + await updateSetting('Livechat_Routing_Method', 'Manual_Selection'); + // Wait for one sec to be sure routing stops + await sleep(1000); + }); + + it('should fail if user is not logged in', async () => { + await request.post(api('omnichannel/rid/request-transcript')).expect(401); + }); + it('should fail if :rid doesnt exists', async () => { + await request.post(api('omnichannel/rid/request-transcript')).set(credentials).expect(400); + }); + it('should fail if user doesnt have request-pdf-transcript permission', async () => { + await updatePermission('request-pdf-transcript', []); + const visitor = await createVisitor(); + const { _id } = await createLivechatRoom(visitor.token); + await request + .post(api(`omnichannel/${_id}/request-transcript`)) + .set(credentials) + .expect(403); + }); + // Increasing a bit the timeout since service calls + other calls are a bit slow on pipe + it('should fail if room is not closed', async () => { + await updatePermission('request-pdf-transcript', ['admin', 'livechat-agent', 'livechat-manager']); + const visitor = await createVisitor(); + const { _id } = await createLivechatRoom(visitor.token); + await request + .post(api(`omnichannel/${_id}/request-transcript`)) + .set(credentials) + .expect(400); + }); + it('should fail if no one is serving the room', async () => { + const visitor = await createVisitor(); + const { _id } = await createLivechatRoom(visitor.token); + await closeRoom(_id); + await request + .post(api(`omnichannel/${_id}/request-transcript`)) + .set(credentials) + .expect(400); + }); + let roomId: string; + it('should request a pdf transcript when all conditions are met', async () => { + await createAgent(); + const visitor = await createVisitor(); + const { _id } = await createLivechatRoom(visitor.token); + const inq = await fetchInquiry(_id); + roomId = _id; + await takeInquiry(inq._id); + await closeRoom(_id); + + await request + .post(api(`omnichannel/${_id}/request-transcript`)) + .set(credentials) + .expect(200); + }); + it('should return immediately if transcript was already requested', async () => { + await request + .post(api(`omnichannel/${roomId}/request-transcript`)) + .set(credentials) + .expect(200); + }); + }); + + describe('it should mark room as unread when a new message arrives and the config is activated', () => { let room: IOmnichannelRoom; let visitor: ILivechatVisitor; let totalMessagesSent = 0; diff --git a/apps/meteor/tests/end-to-end/api/livechat/11-email-inbox.ts b/apps/meteor/tests/end-to-end/api/livechat/11-email-inbox.ts index 4e81a159b96..357d83d60bb 100644 --- a/apps/meteor/tests/end-to-end/api/livechat/11-email-inbox.ts +++ b/apps/meteor/tests/end-to-end/api/livechat/11-email-inbox.ts @@ -3,7 +3,6 @@ import { expect } from 'chai'; import type { Response } from 'supertest'; import { getCredentials, api, request, credentials } from '../../../data/api-data'; -import { createDepartment } from '../../../data/livechat/rooms'; import { createEmailInbox } from '../../../data/livechat/inboxes'; import { updatePermission } from '../../../data/permissions.helper'; @@ -11,47 +10,43 @@ describe('Email inbox', () => { before((done) => getCredentials(done)); let testInbox = ''; before((done) => { - createDepartment() - .then((dept) => - request - .post(api('email-inbox')) - .set(credentials) - .send({ - active: true, - name: 'test-email-inbox##', - email: 'test-email@example.com', - description: 'test email inbox', - senderInfo: 'test email inbox', - department: dept.name, - smtp: { - server: 'smtp.example.com', - port: 587, - username: 'example@example.com', - password: 'not-a-real-password', - secure: true, - }, - imap: { - server: 'imap.example.com', - port: 993, - username: 'example@example.com', - password: 'not-a-real-password', - secure: true, - maxRetries: 10, - }, - }) - .expect('Content-Type', 'application/json') - .expect(200) - .expect((res: Response) => { - expect(res.body).to.have.property('success'); - if (res.body.success === true) { - testInbox = res.body._id; - } else { - expect(res.body).to.have.property('error'); - expect(res.body.error.includes('E11000')).to.be.eq(true); - } - }), - ) - .finally(done); + request + .post(api('email-inbox')) + .set(credentials) + .send({ + active: true, + name: 'test-email-inbox##', + email: 'test-email@example.com', + description: 'test email inbox', + senderInfo: 'test email inbox', + smtp: { + server: 'smtp.example.com', + port: 587, + username: 'example@example.com', + password: 'not-a-real-password', + secure: true, + }, + imap: { + server: 'imap.example.com', + port: 993, + username: 'example@example.com', + password: 'not-a-real-password', + secure: true, + maxRetries: 10, + }, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res: Response) => { + expect(res.body).to.have.property('success'); + if (res.body.success === true) { + testInbox = res.body._id; + } else { + expect(res.body).to.have.property('error'); + expect(res.body.error.includes('E11000')).to.be.eq(true); + } + }) + .end(done); }); after((done) => { if (testInbox) { @@ -97,7 +92,6 @@ describe('Email inbox', () => { email: `test${new Date().getTime()}@test.com`, description: 'Updated test description', senderInfo: 'test', - department: 'test', smtp: { server: 'smtp.example.com', port: 587, diff --git a/apps/meteor/tests/unit/client/components/Omnichannel/modals/TranscriptModal.spec.tsx b/apps/meteor/tests/unit/client/components/Omnichannel/modals/TranscriptModal.spec.tsx new file mode 100644 index 00000000000..2a06fc8a332 --- /dev/null +++ b/apps/meteor/tests/unit/client/components/Omnichannel/modals/TranscriptModal.spec.tsx @@ -0,0 +1,58 @@ +import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import { expect, spy } from 'chai'; + +import TranscriptModal from '../../../../../../client/components/Omnichannel/modals/TranscriptModal'; + +const room = { + open: true, + v: { token: '1234567890' }, + transcriptRequest: { + email: 'example@example.com', + subject: 'Transcript of livechat conversation', + }, +} as IOmnichannelRoom; + +const defaultProps = { + room, + email: 'test@example.com', + onRequest: () => null, + onSend: () => null, + onCancel: () => null, + onDiscard: () => null, +}; + +describe('components/Omnichannel/TranscriptModal', () => { + it('should show Undo request button when roomOpen is true and transcriptRequest exist', () => { + const onDiscardMock = spy(); + const { getByText } = render(); + const undoRequestButton = getByText('Undo_request'); + + fireEvent.click(undoRequestButton); + + expect(onDiscardMock).to.have.been.called(); + }); + + it('should show Request button when roomOpen is true and transcriptRequest not exist', () => { + const onRequestMock = spy(); + const { getByText } = render( + , + ); + const requestButton = getByText('Request'); + + fireEvent.click(requestButton); + + expect(onRequestMock).to.have.been.called(); + }); + + it('should show Send button when roomOpen is false', () => { + const onSendMock = spy(); + const { getByText } = render(); + const requestButton = getByText('Send'); + + fireEvent.click(requestButton); + + expect(onSendMock).to.have.been.called(); + }); +}); diff --git a/docker-compose-ci.yml b/docker-compose-ci.yml index 0f6a009a2e3..160cd869056 100644 --- a/docker-compose-ci.yml +++ b/docker-compose-ci.yml @@ -110,6 +110,38 @@ services: - 'host.docker.internal:host-gateway' depends_on: - nats + + queue-worker-service: + platform: linux/amd64 + build: + dockerfile: ee/apps/queue-worker/Dockerfile + args: + SERVICE: queue-worker + image: ghcr.io/${LOWERCASE_REPOSITORY}/queue-worker-service:${DOCKER_TAG} + environment: + - MONGO_URL=${MONGO_URL} + - 'TRANSPORTER=${TRANSPORTER}' + - MOLECULER_LOG_LEVEL=info + extra_hosts: + - 'host.docker.internal:host-gateway' + depends_on: + - nats + + omnichannel-transcript-service: + platform: linux/amd64 + build: + dockerfile: ee/apps/omnichannel-transcript/Dockerfile + args: + SERVICE: omnichannel-transcript + image: ghcr.io/${LOWERCASE_REPOSITORY}/omnichannel-transcript-service:${DOCKER_TAG} + environment: + - MONGO_URL=${MONGO_URL} + - 'TRANSPORTER=${TRANSPORTER}' + - MOLECULER_LOG_LEVEL=info + extra_hosts: + - 'host.docker.internal:host-gateway' + depends_on: + - nats nats: image: nats:2.6-alpine diff --git a/docker-compose-local.yml b/docker-compose-local.yml new file mode 100644 index 00000000000..16c64187461 --- /dev/null +++ b/docker-compose-local.yml @@ -0,0 +1,171 @@ +version: '3.8' + +services: + rocketchat: + platform: linux/amd64 + build: + dockerfile: ${RC_DOCKERFILE} + context: /tmp/build + image: ghcr.io/${LOWERCASE_REPOSITORY}/rocket.chat:${RC_DOCKER_TAG} + environment: + - TEST_MODE=true + - EXIT_UNHANDLEDPROMISEREJECTION=true + - 'MONGO_URL=${MONGO_URL}' + - 'MONGO_OPLOG_URL=${MONGO_OPLOG_URL}' + - 'TRANSPORTER=${TRANSPORTER}' + - MOLECULER_LOG_LEVEL=info + - 'ROCKETCHAT_LICENSE=${ENTERPRISE_LICENSE}' + - OVERWRITE_SETTING_Log_Level=2 + extra_hosts: + - 'host.docker.internal:host-gateway' + depends_on: + - traefik + - mongo + labels: + traefik.enable: true + traefik.http.services.rocketchat.loadbalancer.server.port: 3000 + traefik.http.routers.rocketchat.service: rocketchat + traefik.http.routers.rocketchat.rule: PathPrefix(`/`) + + authorization-service: + platform: linux/amd64 + build: + dockerfile: ee/apps/authorization-service/Dockerfile + args: + SERVICE: authorization-service + image: ghcr.io/${LOWERCASE_REPOSITORY}/authorization-service:${DOCKER_TAG} + environment: + - 'MONGO_URL=${MONGO_URL}' + - 'TRANSPORTER=${TRANSPORTER}' + - MOLECULER_LOG_LEVEL=info + extra_hosts: + - 'host.docker.internal:host-gateway' + depends_on: + - nats + + account-service: + platform: linux/amd64 + build: + dockerfile: ee/apps/account-service/Dockerfile + args: + SERVICE: account-service + image: ghcr.io/${LOWERCASE_REPOSITORY}/account-service:${DOCKER_TAG} + environment: + - MONGO_URL=${MONGO_URL} + - 'TRANSPORTER=${TRANSPORTER}' + - MOLECULER_LOG_LEVEL=info + extra_hosts: + - 'host.docker.internal:host-gateway' + depends_on: + - nats + + presence-service: + platform: linux/amd64 + build: + dockerfile: ee/apps/presence-service/Dockerfile + args: + SERVICE: presence-service + image: ghcr.io/${LOWERCASE_REPOSITORY}/presence-service:${DOCKER_TAG} + environment: + - MONGO_URL=${MONGO_URL} + - 'TRANSPORTER=${TRANSPORTER}' + - MOLECULER_LOG_LEVEL=info + extra_hosts: + - 'host.docker.internal:host-gateway' + depends_on: + - nats + + ddp-streamer-service: + platform: linux/amd64 + build: + dockerfile: ee/apps/ddp-streamer/Dockerfile + args: + SERVICE: ddp-streamer + image: ghcr.io/${LOWERCASE_REPOSITORY}/ddp-streamer-service:${DOCKER_TAG} + environment: + - MONGO_URL=${MONGO_URL} + - 'TRANSPORTER=${TRANSPORTER}' + - MOLECULER_LOG_LEVEL=info + extra_hosts: + - 'host.docker.internal:host-gateway' + depends_on: + - nats + - traefik + labels: + traefik.enable: true + traefik.http.services.ddp-streamer-service.loadbalancer.server.port: 3000 + traefik.http.routers.ddp-streamer-service.service: ddp-streamer-service + traefik.http.routers.ddp-streamer-service.rule: PathPrefix(`/websocket`) || PathPrefix(`/sockjs`) + + stream-hub-service: + platform: linux/amd64 + build: + dockerfile: ee/apps/stream-hub-service/Dockerfile + args: + SERVICE: stream-hub-service + image: ghcr.io/${LOWERCASE_REPOSITORY}/stream-hub-service:${DOCKER_TAG} + environment: + - MONGO_URL=${MONGO_URL} + - 'TRANSPORTER=${TRANSPORTER}' + - MOLECULER_LOG_LEVEL=info + extra_hosts: + - 'host.docker.internal:host-gateway' + depends_on: + - nats + + queue-worker-service: + platform: linux/amd64 + build: + dockerfile: ee/apps/queue-worker/Dockerfile + args: + SERVICE: queue-worker + image: ghcr.io/${LOWERCASE_REPOSITORY}/queue-worker-service:${DOCKER_TAG} + environment: + - MONGO_URL=${MONGO_URL} + - 'TRANSPORTER=${TRANSPORTER}' + - MOLECULER_LOG_LEVEL=info + extra_hosts: + - 'host.docker.internal:host-gateway' + depends_on: + - nats + + omnichannel-transcript-service: + platform: linux/amd64 + build: + dockerfile: ee/apps/omnichannel-transcript/Dockerfile + args: + SERVICE: omnichannel-transcript + image: ghcr.io/${LOWERCASE_REPOSITORY}/omnichannel-transcript-service:${DOCKER_TAG} + environment: + - MONGO_URL=${MONGO_URL} + - 'TRANSPORTER=${TRANSPORTER}' + - MOLECULER_LOG_LEVEL=info + extra_hosts: + - 'host.docker.internal:host-gateway' + depends_on: + - nats + + mongo: + image: docker.io/bitnami/mongodb:4.4 + restart: on-failure + environment: + MONGODB_REPLICA_SET_MODE: primary + MONGODB_REPLICA_SET_NAME: ${MONGODB_REPLICA_SET_NAME:-rs0} + MONGODB_PORT_NUMBER: ${MONGODB_PORT_NUMBER:-27017} + MONGODB_INITIAL_PRIMARY_HOST: ${MONGODB_INITIAL_PRIMARY_HOST:-mongo} + MONGODB_INITIAL_PRIMARY_PORT_NUMBER: ${MONGODB_INITIAL_PRIMARY_PORT_NUMBER:-27017} + MONGODB_ADVERTISED_HOSTNAME: ${MONGODB_ADVERTISED_HOSTNAME:-mongo} + MONGODB_ENABLE_JOURNAL: ${MONGODB_ENABLE_JOURNAL:-true} + ALLOW_EMPTY_PASSWORD: ${ALLOW_EMPTY_PASSWORD:-yes} + + nats: + image: nats:2.6-alpine + + traefik: + image: traefik:v2.8 + command: + - --providers.docker=true + ports: + - 3000:80 + volumes: + - /var/run/docker.sock:/var/run/docker.sock diff --git a/ee/apps/omnichannel-transcript/.eslintrc.json b/ee/apps/omnichannel-transcript/.eslintrc.json new file mode 100644 index 00000000000..a83aeda48e6 --- /dev/null +++ b/ee/apps/omnichannel-transcript/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "extends": ["@rocket.chat/eslint-config"], + "ignorePatterns": ["**/dist"] +} diff --git a/ee/apps/omnichannel-transcript/Dockerfile b/ee/apps/omnichannel-transcript/Dockerfile new file mode 100644 index 00000000000..4afb81c910b --- /dev/null +++ b/ee/apps/omnichannel-transcript/Dockerfile @@ -0,0 +1,49 @@ +FROM node:14.21.2-alpine + +ARG SERVICE + +WORKDIR /app + +COPY ./packages/core-services/package.json packages/core-services/package.json +COPY ./packages/core-services/dist packages/core-services/dist + +COPY ./packages/core-typings/package.json packages/core-typings/package.json +COPY ./packages/core-typings/dist packages/core-typings/dist + +COPY ./packages/rest-typings/package.json packages/rest-typings/package.json +COPY ./packages/rest-typings/dist packages/rest-typings/dist + +COPY ./packages/model-typings/package.json packages/model-typings/package.json +COPY ./packages/model-typings/dist packages/model-typings/dist + +COPY ./packages/models/package.json packages/models/package.json +COPY ./packages/models/dist packages/models/dist + +COPY ./ee/packages/omnichannel-services/package.json ee/packages/omnichannel-services/package.json +COPY ./ee/packages/omnichannel-services/dist ee/packages/omnichannel-services/dist + +COPY ./ee/packages/pdf-worker/package.json ee/packages/pdf-worker/package.json +COPY ./ee/packages/pdf-worker/dist ee/packages/pdf-worker/dist + +COPY ./packages/tools/package.json packages/tools/package.json +COPY ./packages/tools/dist packages/tools/dist + +COPY ./ee/apps/${SERVICE}/dist . + +COPY ./package.json . +COPY ./yarn.lock . +COPY ./.yarnrc.yml . +COPY ./.yarn/plugins .yarn/plugins +COPY ./.yarn/releases .yarn/releases +COPY ./ee/apps/${SERVICE}/package.json ee/apps/${SERVICE}/package.json + +ENV NODE_ENV=production \ + PORT=3000 + +WORKDIR /app/ee/apps/${SERVICE} + +RUN yarn workspaces focus --production + +EXPOSE 3000 9458 + +CMD ["node", "src/service.js"] diff --git a/ee/apps/omnichannel-transcript/package.json b/ee/apps/omnichannel-transcript/package.json new file mode 100644 index 00000000000..db1895f2dd2 --- /dev/null +++ b/ee/apps/omnichannel-transcript/package.json @@ -0,0 +1,57 @@ +{ + "name": "@rocket.chat/omnichannel-transcript", + "private": true, + "version": "0.1.0", + "description": "Rocket.Chat service", + "scripts": { + "build": "tsc -p tsconfig.json", + "ms": "TRANSPORTER=${TRANSPORTER:-TCP} MONGO_URL=${MONGO_URL:-mongodb://localhost:3001/meteor} ts-node --files src/service.ts", + "test": "echo \"Error: no test specified\" && exit 1", + "lint": "eslint src", + "typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json" + }, + "keywords": [ + "rocketchat" + ], + "author": "Rocket.Chat", + "dependencies": { + "@react-pdf/renderer": "^3.1.3", + "@rocket.chat/core-services": "workspace:^", + "@rocket.chat/core-typings": "workspace:^", + "@rocket.chat/emitter": "0.31.22", + "@rocket.chat/model-typings": "workspace:^", + "@rocket.chat/models": "workspace:^", + "@rocket.chat/omnichannel-services": "workspace:^", + "@rocket.chat/pdf-worker": "workspace:^", + "@rocket.chat/tools": "workspace:^", + "@types/node": "^14.18.21", + "ejson": "^2.2.2", + "emoji-toolkit": "^7.0.0", + "eventemitter3": "^4.0.7", + "fibers": "^5.0.3", + "mem": "^8.1.1", + "moleculer": "^0.14.21", + "moment-timezone": "^0.5.34", + "mongo-message-queue": "^1.0.0", + "mongodb": "^4.12.1", + "nats": "^2.4.0", + "pino": "^8.4.2", + "polka": "^0.5.2" + }, + "devDependencies": { + "@rocket.chat/eslint-config": "workspace:^", + "@rocket.chat/ui-contexts": "workspace:^", + "@types/eslint": "^8.4.10", + "@types/polka": "^0.5.4", + "eslint": "^8.29.0", + "ts-node": "^10.9.1", + "typescript": "~4.5.5" + }, + "main": "./dist/ee/apps/omnichannel-transcript/src/service.js", + "files": [ + "/dist" + ], + "volta": { + "extends": "../../../package.json" + } +} diff --git a/ee/apps/omnichannel-transcript/src/service.ts b/ee/apps/omnichannel-transcript/src/service.ts new file mode 100644 index 00000000000..b6edec15b9c --- /dev/null +++ b/ee/apps/omnichannel-transcript/src/service.ts @@ -0,0 +1,41 @@ +import type { Document } from 'mongodb'; +import polka from 'polka'; +import { api } from '@rocket.chat/core-services'; + +import { broker } from '../../../../apps/meteor/ee/server/startup/broker'; +import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; +import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; +import { Logger } from '../../../../apps/meteor/server/lib/logger/Logger'; + +const PORT = process.env.PORT || 3036; + +(async () => { + const db = await getConnection(); + + const trash = await getCollection(Collections.Trash); + + registerServiceModels(db, trash); + + api.setBroker(broker); + + // need to import service after models are registered + const { OmnichannelTranscript } = await import('@rocket.chat/omnichannel-services'); + + api.registerService(new OmnichannelTranscript(Logger), ['queue-worker']); + + await api.start(); + + polka() + .get('/health', async function (_req, res) { + try { + await api.nodeList(); + res.end('ok'); + } catch (err) { + console.error('Service not healthy', err); + + res.writeHead(500); + res.end('not healthy'); + } + }) + .listen(PORT); +})(); diff --git a/ee/apps/omnichannel-transcript/tsconfig.json b/ee/apps/omnichannel-transcript/tsconfig.json new file mode 100644 index 00000000000..58ffe2a53b0 --- /dev/null +++ b/ee/apps/omnichannel-transcript/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../../tsconfig.base.server.json", + "compilerOptions": { + "allowJs": true, + "strictPropertyInitialization": false, + "outDir": "./dist", + }, + "files": ["./src/service.ts"], + "include": ["../../../apps/meteor/definition/externals/meteor"], + "exclude": ["./dist"] +} diff --git a/ee/apps/presence-service/package.json b/ee/apps/presence-service/package.json index bebe0155a38..fa60fc1c8e3 100644 --- a/ee/apps/presence-service/package.json +++ b/ee/apps/presence-service/package.json @@ -43,5 +43,8 @@ "main": "./dist/ee/apps/presence-service/src/service.js", "files": [ "/dist" - ] + ], + "volta": { + "extends": "../../../package.json" + } } diff --git a/ee/apps/queue-worker/.eslintrc.json b/ee/apps/queue-worker/.eslintrc.json new file mode 100644 index 00000000000..a83aeda48e6 --- /dev/null +++ b/ee/apps/queue-worker/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "extends": ["@rocket.chat/eslint-config"], + "ignorePatterns": ["**/dist"] +} diff --git a/ee/apps/queue-worker/Dockerfile b/ee/apps/queue-worker/Dockerfile new file mode 100644 index 00000000000..4afb81c910b --- /dev/null +++ b/ee/apps/queue-worker/Dockerfile @@ -0,0 +1,49 @@ +FROM node:14.21.2-alpine + +ARG SERVICE + +WORKDIR /app + +COPY ./packages/core-services/package.json packages/core-services/package.json +COPY ./packages/core-services/dist packages/core-services/dist + +COPY ./packages/core-typings/package.json packages/core-typings/package.json +COPY ./packages/core-typings/dist packages/core-typings/dist + +COPY ./packages/rest-typings/package.json packages/rest-typings/package.json +COPY ./packages/rest-typings/dist packages/rest-typings/dist + +COPY ./packages/model-typings/package.json packages/model-typings/package.json +COPY ./packages/model-typings/dist packages/model-typings/dist + +COPY ./packages/models/package.json packages/models/package.json +COPY ./packages/models/dist packages/models/dist + +COPY ./ee/packages/omnichannel-services/package.json ee/packages/omnichannel-services/package.json +COPY ./ee/packages/omnichannel-services/dist ee/packages/omnichannel-services/dist + +COPY ./ee/packages/pdf-worker/package.json ee/packages/pdf-worker/package.json +COPY ./ee/packages/pdf-worker/dist ee/packages/pdf-worker/dist + +COPY ./packages/tools/package.json packages/tools/package.json +COPY ./packages/tools/dist packages/tools/dist + +COPY ./ee/apps/${SERVICE}/dist . + +COPY ./package.json . +COPY ./yarn.lock . +COPY ./.yarnrc.yml . +COPY ./.yarn/plugins .yarn/plugins +COPY ./.yarn/releases .yarn/releases +COPY ./ee/apps/${SERVICE}/package.json ee/apps/${SERVICE}/package.json + +ENV NODE_ENV=production \ + PORT=3000 + +WORKDIR /app/ee/apps/${SERVICE} + +RUN yarn workspaces focus --production + +EXPOSE 3000 9458 + +CMD ["node", "src/service.js"] diff --git a/ee/apps/queue-worker/package.json b/ee/apps/queue-worker/package.json new file mode 100644 index 00000000000..bf66f95098f --- /dev/null +++ b/ee/apps/queue-worker/package.json @@ -0,0 +1,54 @@ +{ + "name": "@rocket.chat/queue-worker", + "private": true, + "version": "0.1.0", + "description": "Rocket.Chat service", + "scripts": { + "build": "tsc -p tsconfig.json", + "ms": "TRANSPORTER=${TRANSPORTER:-TCP} MONGO_URL=${MONGO_URL:-mongodb://localhost:3001/meteor} ts-node --files src/service.ts", + "test": "echo \"Error: no test specified\" && exit 1", + "lint": "eslint src", + "typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json" + }, + "keywords": [ + "rocketchat" + ], + "author": "Rocket.Chat", + "dependencies": { + "@react-pdf/renderer": "^3.1.3", + "@rocket.chat/core-services": "workspace:^", + "@rocket.chat/core-typings": "workspace:^", + "@rocket.chat/emitter": "0.31.22", + "@rocket.chat/model-typings": "workspace:^", + "@rocket.chat/models": "workspace:^", + "@rocket.chat/omnichannel-services": "workspace:^", + "@types/node": "^14.18.21", + "ejson": "^2.2.2", + "emoji-toolkit": "^7.0.0", + "eventemitter3": "^4.0.7", + "fibers": "^5.0.3", + "mem": "^8.1.1", + "moleculer": "^0.14.21", + "moment-timezone": "^0.5.34", + "mongo-message-queue": "^1.0.0", + "mongodb": "^4.12.1", + "nats": "^2.4.0", + "pino": "^8.4.2", + "polka": "^0.5.2" + }, + "devDependencies": { + "@rocket.chat/eslint-config": "workspace:^", + "@types/eslint": "^8.4.10", + "@types/polka": "^0.5.4", + "eslint": "^8.29.0", + "ts-node": "^10.9.1", + "typescript": "~4.5.5" + }, + "main": "./dist/ee/apps/queue-worker/src/service.js", + "files": [ + "/dist" + ], + "volta": { + "extends": "../../../package.json" + } +} diff --git a/ee/apps/queue-worker/src/service.ts b/ee/apps/queue-worker/src/service.ts new file mode 100644 index 00000000000..9445c7b3cdc --- /dev/null +++ b/ee/apps/queue-worker/src/service.ts @@ -0,0 +1,41 @@ +import type { Document } from 'mongodb'; +import polka from 'polka'; +import { api } from '@rocket.chat/core-services'; + +import { broker } from '../../../../apps/meteor/ee/server/startup/broker'; +import { Collections, getCollection, getConnection } from '../../../../apps/meteor/ee/server/services/mongo'; +import { registerServiceModels } from '../../../../apps/meteor/ee/server/lib/registerServiceModels'; +import { Logger } from '../../../../apps/meteor/server/lib/logger/Logger'; + +const PORT = process.env.PORT || 3038; + +(async () => { + const db = await getConnection(); + + const trash = await getCollection(Collections.Trash); + + registerServiceModels(db, trash); + + api.setBroker(broker); + + // need to import service after models are registeredpackagfe + const { QueueWorker } = await import('@rocket.chat/omnichannel-services'); + + api.registerService(new QueueWorker(db, Logger)); + + await api.start(); + + polka() + .get('/health', async function (_req, res) { + try { + await api.nodeList(); + res.end('ok'); + } catch (err) { + console.error('Service not healthy', err); + + res.writeHead(500); + res.end('not healthy'); + } + }) + .listen(PORT); +})(); diff --git a/ee/apps/queue-worker/tsconfig.json b/ee/apps/queue-worker/tsconfig.json new file mode 100644 index 00000000000..58ffe2a53b0 --- /dev/null +++ b/ee/apps/queue-worker/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../../tsconfig.base.server.json", + "compilerOptions": { + "allowJs": true, + "strictPropertyInitialization": false, + "outDir": "./dist", + }, + "files": ["./src/service.ts"], + "include": ["../../../apps/meteor/definition/externals/meteor"], + "exclude": ["./dist"] +} diff --git a/ee/packages/omnichannel-services/.eslintrc.json b/ee/packages/omnichannel-services/.eslintrc.json new file mode 100644 index 00000000000..a83aeda48e6 --- /dev/null +++ b/ee/packages/omnichannel-services/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "extends": ["@rocket.chat/eslint-config"], + "ignorePatterns": ["**/dist"] +} diff --git a/ee/packages/omnichannel-services/package.json b/ee/packages/omnichannel-services/package.json new file mode 100644 index 00000000000..a48c222aa27 --- /dev/null +++ b/ee/packages/omnichannel-services/package.json @@ -0,0 +1,46 @@ +{ + "name": "@rocket.chat/omnichannel-services", + "version": "0.0.1", + "private": true, + "devDependencies": { + "@rocket.chat/eslint-config": "workspace:^", + "@types/jest": "^27.4.1", + "eslint": "^8.12.0", + "jest": "^27.5.1", + "ts-jest": "^27.1.4", + "typescript": "~4.6.4" + }, + "dependencies": { + "@rocket.chat/core-services": "workspace:^", + "@rocket.chat/core-typings": "workspace:^", + "@rocket.chat/emitter": "0.31.22", + "@rocket.chat/model-typings": "workspace:^", + "@rocket.chat/models": "workspace:^", + "@rocket.chat/pdf-worker": "workspace:^", + "@rocket.chat/rest-typings": "workspace:^", + "@rocket.chat/string-helpers": "0.31.22", + "@rocket.chat/tools": "workspace:^", + "@types/node": "^14.18.21", + "ejson": "^2.2.2", + "emoji-toolkit": "^7.0.0", + "eventemitter3": "^4.0.7", + "fibers": "^5.0.3", + "mem": "^8.1.1", + "moment-timezone": "^0.5.34", + "mongo-message-queue": "^1.0.0", + "mongodb": "^4.12.1", + "pino": "^8.4.2" + }, + "scripts": { + "lint": "eslint --ext .js,.jsx,.ts,.tsx .", + "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "jest": "jest", + "build": "rm -rf dist && tsc -p tsconfig.json", + "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput" + }, + "main": "./dist/ee/packages/omnichannel-services/src/index.js", + "typings": "./dist/ee/packages/omnichannel-services/src/index.d.ts", + "files": [ + "/dist" + ] +} diff --git a/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts b/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts new file mode 100644 index 00000000000..eea88ade501 --- /dev/null +++ b/ee/packages/omnichannel-services/src/OmnichannelTranscript.ts @@ -0,0 +1,382 @@ +import { LivechatRooms, Messages, Uploads, Users, LivechatVisitors } from '@rocket.chat/models'; +import { PdfWorker } from '@rocket.chat/pdf-worker'; +import type { Templates } from '@rocket.chat/pdf-worker'; +import type { IMessage, IUser, IRoom, IUpload, ILivechatVisitor, ILivechatAgent } from '@rocket.chat/core-typings'; +import { isFileAttachment, isFileImageAttachment } from '@rocket.chat/core-typings'; +import { + ServiceClass, + Upload as uploadService, + Message as messageService, + Room as roomService, + QueueWorker as queueService, + Translation as translationService, + Settings as settingsService, + License as licenseService, +} from '@rocket.chat/core-services'; +import type { IOmnichannelTranscriptService } from '@rocket.chat/core-services'; +import { guessTimezone, guessTimezoneFromOffset } from '@rocket.chat/tools'; + +import type { Logger } from '../../../../apps/meteor/server/lib/logger/Logger'; + +const isPromiseRejectedResult = (result: any): result is PromiseRejectedResult => result.status === 'rejected'; + +type WorkDetails = { + rid: IRoom['_id']; + userId: IUser['_id']; +}; + +type WorkDetailsWithSource = WorkDetails & { + from: string; +}; + +type MessageWithFiles = Pick & { + files: ({ name?: string; buffer: Buffer | null; extension?: string } | undefined)[]; +}; + +type WorkerData = { + siteName: string; + visitor: ILivechatVisitor | null; + agent: ILivechatAgent | undefined; + closedAt?: Date; + messages: MessageWithFiles[]; + timezone: string; + dateFormat: string; + timeAndDateFormat: string; + translations: { key: string; value: string }[]; +}; + +export class OmnichannelTranscript extends ServiceClass implements IOmnichannelTranscriptService { + protected name = 'omnichannel-transcript'; + + private worker: PdfWorker; + + private log: Logger; + + maxNumberOfConcurrentJobs = 25; + + currentJobNumber = 0; + + shouldWork = false; + + constructor(loggerClass: typeof Logger) { + super(); + this.worker = new PdfWorker('chat-transcript'); + // eslint-disable-next-line new-cap + this.log = new loggerClass('OmnichannelTranscript'); + + this.onEvent('license.module', ({ module, valid }) => { + if (module === 'scalability') { + this.shouldWork = valid; + } + }); + } + + async started(): Promise { + try { + this.shouldWork = await licenseService.hasLicense('scalability'); + } catch (e: unknown) { + // ignore + } + } + + async getTimezone(user?: { utcOffset?: string | number }): Promise { + const reportingTimezone = await settingsService.get('Default_Timezone_For_Reporting'); + + switch (reportingTimezone) { + case 'custom': + return settingsService.get('Default_Custom_Timezone'); + case 'user': + if (user?.utcOffset) { + return guessTimezoneFromOffset(user.utcOffset); + } + return guessTimezone(); + default: + return guessTimezone(); + } + } + + private getMessagesFromRoom({ rid }: { rid: string }): Promise { + // Closing message should not appear :) + return Messages.findLivechatMessagesWithoutClosing(rid, { + sort: { ts: 1 }, + projection: { _id: 1, msg: 1, u: 1, t: 1, ts: 1, attachments: 1, files: 1, md: 1 }, + }).toArray(); + } + + async requestTranscript({ details }: { details: WorkDetails }): Promise { + if (!this.shouldWork) { + this.log.info(`Not requesting transcript for room ${details.rid} because scalability module is not enabled`); + return; + } + this.log.info(`Requesting transcript for room ${details.rid} by user ${details.userId}`); + const room = await LivechatRooms.findOneById(details.rid); + if (!room) { + throw new Error('room-not-found'); + } + + if (room.open) { + throw new Error('room-still-open'); + } + + if (!room.servedBy || !room.v) { + throw new Error('improper-room-state'); + } + + // Don't request a transcript if there's already one requested :) + if (room.pdfTranscriptRequested) { + // TODO: use logger + this.log.info(`Transcript already requested for room ${details.rid}`); + return; + } + + await LivechatRooms.setTranscriptRequestedPdfById(details.rid); + + // Even when processing is done "in-house", we still need to queue the work + // to avoid blocking the request + this.log.info(`Queuing work for room ${details.rid}`); + await queueService.queueWork('work', `${this.name}.workOnPdf`, { + template: 'omnichannel-transcript', + details: { userId: details.userId, rid: details.rid, from: this.name }, + }); + } + + private async getFiles(userId: string, messages: IMessage[]): Promise { + const messagesWithFiles: MessageWithFiles[] = []; + for await (const message of messages) { + if (!message.attachments || !message.attachments.length) { + // If there's no attachment and no message, what was sent? lol + messagesWithFiles.push({ _id: message._id, files: [], ts: message.ts, u: message.u, msg: message.msg, md: message.md }); + continue; + } + + const files = []; + + for await (const attachment of message.attachments) { + if (isFileAttachment(attachment) && attachment.type !== 'file') { + this.log.error(`Invalid attachment type ${attachment.type} for file ${attachment.title} in room ${message.rid}!`); + // ignore other types of attachments + continue; + } + if (isFileAttachment(attachment) && isFileImageAttachment(attachment) && !this.worker.isMimeTypeValid(attachment.image_type)) { + this.log.error(`Invalid mime type ${attachment.image_type} for file ${attachment.title} in room ${message.rid}!`); + // ignore invalid mime types + files.push({ name: attachment.title, buffer: null }); + continue; + } + let file = message.files?.map((v) => ({ _id: v._id, name: v.name })).find((file) => file.name === attachment.title); + if (!file) { + this.log.debug(`File ${attachment.title} not found in room ${message.rid}!`); + // For some reason, when an image is uploaded from clipboard, it doesn't have a file :( + // So, we'll try to get the FILE_ID from the `title_link` prop which has the format `/file-upload/FILE_ID/FILE_NAME` using a regex + const fileId = attachment.title_link?.match(/\/file-upload\/(.*)\/.*/)?.[1]; + if (!fileId) { + this.log.error(`File ${attachment.title} not found in room ${message.rid}!`); + // ignore attachments without file + files.push({ name: attachment.title, buffer: null }); + continue; + } + file = { _id: fileId, name: attachment.title || 'upload' }; + } + + if (!file) { + this.log.error(`File ${attachment.title} not found in room ${message.rid}!`); + // ignore attachments without file + files.push({ name: attachment.title, buffer: null }); + continue; + } + + const uploadedFile = await Uploads.findOneById(file._id); + if (!uploadedFile) { + this.log.error(`Uploaded file ${file._id} not found in room ${message.rid}!`); + // ignore attachments without file + files.push({ name: file.name, buffer: null }); + continue; + } + + const fileBuffer = await uploadService.getFileBuffer({ userId, file: uploadedFile }); + files.push({ name: file.name, buffer: fileBuffer, extension: uploadedFile.extension }); + } + + // When you send a file message, the things you type in the modal are not "msg", they're in "description" of the attachment + // So, we'll fetch the the msg, if empty, go for the first description on an attachment, if empty, empty string + const msg = message.msg || message.attachments.find((attachment) => attachment.description)?.description || ''; + // Remove nulls from final array + messagesWithFiles.push({ _id: message._id, msg, u: message.u, files: files.filter(Boolean), ts: message.ts }); + } + + return messagesWithFiles; + } + + private async getTranslations(): Promise> { + const keys: string[] = [ + 'Agent', + 'Date', + 'Customer', + 'Omnichannel_Agent', + 'Time', + 'Chat_transcript', + 'This_attachment_is_not_supported', + ]; + + return Promise.all( + keys.map(async (key) => { + return { + key, + value: await translationService.translateToServerLanguage(key), + }; + }), + ); + } + + async workOnPdf({ template, details }: { template: Templates; details: WorkDetailsWithSource }): Promise { + if (!this.shouldWork) { + this.log.info(`Processing transcript for room ${details.rid} by user ${details.userId} - Stopped (no scalability license found)`); + return; + } + this.log.info(`Processing transcript for room ${details.rid} by user ${details.userId} - Received from queue`); + if (this.maxNumberOfConcurrentJobs <= this.currentJobNumber) { + this.log.error(`Processing transcript for room ${details.rid} by user ${details.userId} - Too many concurrent jobs, queuing again`); + throw new Error('retry'); + } + this.currentJobNumber++; + try { + const room = await LivechatRooms.findOneById(details.rid); + if (!room) { + throw new Error('room-not-found'); + } + const messages = await this.getMessagesFromRoom({ rid: room._id }); + + const visitor = + room.v && (await LivechatVisitors.findOneById(room.v._id, { projection: { _id: 1, name: 1, username: 1, visitorEmails: 1 } })); + const agent = + room.servedBy && (await Users.findOneAgentById(room.servedBy._id, { projection: { _id: 1, name: 1, username: 1, utcOffset: 1 } })); + + const messagesFiles = await this.getFiles(details.userId, messages); + + const [siteName, dateFormat, timeAndDateFormat, timezone, translations] = await Promise.all([ + settingsService.get('Site_Name'), + settingsService.get('Message_DateFormat'), + settingsService.get('Message_TimeAndDateFormat'), + this.getTimezone(agent), + this.getTranslations(), + ]); + const data = { + visitor, + agent, + closedAt: room.closedAt, + siteName, + messages: messagesFiles, + dateFormat, + timeAndDateFormat, + timezone, + translations, + }; + + await this.doRender({ template, data, details }); + } catch (error) { + await this.pdfFailed({ details, e: error as Error }); + } finally { + this.currentJobNumber--; + } + } + + async doRender({ template, data, details }: { template: Templates; data: WorkerData; details: WorkDetailsWithSource }): Promise { + const buf: Uint8Array[] = []; + let outBuff = Buffer.alloc(0); + const transcriptText = await translationService.translateToServerLanguage('Transcript'); + + const stream = await this.worker.renderToStream({ template, data }); + stream.on('data', (chunk) => { + buf.push(chunk); + }); + stream.on('end', () => { + outBuff = Buffer.concat(buf); + + return uploadService + .uploadFile({ + userId: details.userId, + buffer: outBuff, + details: { + // transcript_{company-name)_{date}_{hour}.pdf + name: `${transcriptText}_${data.siteName}_${new Intl.DateTimeFormat('en-US').format(new Date())}_${ + data.visitor?.name || data.visitor?.username || 'Visitor' + }.pdf`, + type: 'application/pdf', + rid: details.rid, + // Rocket.cat is the goat + userId: 'rocket.cat', + size: outBuff.length, + }, + }) + .then((file) => this.pdfComplete({ details, file })) + .catch((e) => this.pdfFailed({ details, e })); + }); + } + + private async pdfFailed({ details, e }: { details: WorkDetailsWithSource; e: Error }): Promise { + this.log.error(`Transcript for room ${details.rid} by user ${details.userId} - Failed: ${e.message}`); + const room = await LivechatRooms.findOneById(details.rid); + if (!room) { + return; + } + const user = await Users.findOneById(details.userId); + if (!user) { + return; + } + + // Remove `transcriptRequestedPdf` from room to allow another request + await LivechatRooms.unsetTranscriptRequestedPdfById(details.rid); + + const { rid } = await roomService.createDirectMessage({ to: details.userId, from: 'rocket.cat' }); + this.log.info(`Transcript for room ${details.rid} by user ${details.userId} - Sending error message to user`); + await messageService.sendMessage({ + fromId: 'rocket.cat', + rid, + msg: `${await translationService.translate('pdf_error_message', user)}: ${e.message}`, + }); + } + + private async pdfComplete({ details, file }: { details: WorkDetailsWithSource; file: IUpload }): Promise { + this.log.info(`Transcript for room ${details.rid} by user ${details.userId} - Complete`); + const user = await Users.findOneById(details.userId); + if (!user) { + return; + } + // Send the file to the livechat room where this was requested, to keep it in context + try { + const [, { rid }] = await Promise.all([ + LivechatRooms.setPdfTranscriptFileIdById(details.rid, file._id), + roomService.createDirectMessage({ to: details.userId, from: 'rocket.cat' }), + ]); + + this.log.info(`Transcript for room ${details.rid} by user ${details.userId} - Sending success message to user`); + const result = await Promise.allSettled([ + uploadService.sendFileMessage({ + roomId: details.rid, + userId: 'rocket.cat', + file, + message: { + // Translate from service + msg: await translationService.translateToServerLanguage('pdf_success_message'), + }, + }), + // Send the file to the user who requested it, so they can download it + uploadService.sendFileMessage({ + roomId: rid, + userId: 'rocket.cat', + file, + message: { + // Translate from service + msg: await translationService.translate('pdf_success_message', user), + }, + }), + ]); + const e = result.find((r) => isPromiseRejectedResult(r)); + if (e && isPromiseRejectedResult(e)) { + throw e.reason; + } + } catch (err) { + this.log.error({ msg: `Transcript for room ${details.rid} by user ${details.userId} - Failed to send message`, err }); + } + } +} diff --git a/ee/packages/omnichannel-services/src/QueueWorker.ts b/ee/packages/omnichannel-services/src/QueueWorker.ts new file mode 100644 index 00000000000..89c66fbc33a --- /dev/null +++ b/ee/packages/omnichannel-services/src/QueueWorker.ts @@ -0,0 +1,165 @@ +import type { Db } from 'mongodb'; +import type { Actions, ValidResult, Work } from 'mongo-message-queue'; +import MessageQueue from 'mongo-message-queue'; +import { ServiceClass, api, License } from '@rocket.chat/core-services'; +import type { IQueueWorkerService, HealthAggResult } from '@rocket.chat/core-services'; + +import type { Logger } from '../../../../apps/meteor/server/lib/logger/Logger'; + +export class QueueWorker extends ServiceClass implements IQueueWorkerService { + protected name = 'queue-worker'; + + protected retryCount = 5; + + // Default delay is 5 seconds + protected retryDelay = 5000; + + protected queue: MessageQueue; + + private logger: Logger; + + private shouldWork = true; + + constructor(private readonly db: Db, loggerClass: typeof Logger) { + super(); + + // eslint-disable-next-line new-cap + this.logger = new loggerClass('QueueWorker'); + this.queue = new MessageQueue(); + + this.onEvent('license.module', ({ module, valid }) => { + if (module === 'scalability') { + this.shouldWork = valid; + } + }); + } + + async started(): Promise { + try { + this.shouldWork = await License.hasLicense('scalability'); + } catch (e: unknown) { + // ignore + } + } + + isServiceNotFoundMessage(message: string): boolean { + return message.includes('is not found'); + } + + isServiceRetryError(message: string): boolean { + return message.includes('retry'); + } + + async created(): Promise { + this.logger.info('Starting queue worker'); + this.queue.databasePromise = () => { + return Promise.resolve(this.db); + }; + + try { + await this.registerWorkers(); + await this.createIndexes(); + } catch (e) { + this.logger.fatal(e, 'Fatal error occurred when registering workers'); + process.exit(1); + } + } + + async createIndexes(): Promise { + this.logger.info('Creating indexes for queue worker'); + + // Library doesnt create indexes by itself, for some reason + // This should create the indexes we need and improve queue perf on reading + await this.db.collection(this.queue.collectionName).createIndex({ type: 1 }); + await this.db.collection(this.queue.collectionName).createIndex({ rejectedTime: 1 }, { sparse: true }); + await this.db.collection(this.queue.collectionName).createIndex({ nextReceivableTime: 1 }, { sparse: true }); + await this.db.collection(this.queue.collectionName).createIndex({ receivedTime: 1 }, { sparse: true }); + } + + async stopped(): Promise { + this.logger.info('Stopping queue worker'); + this.queue.stopPolling(); + } + + private isRetryableError(error: string): boolean { + // Let's retry on 2 circumstances: (for now) + // 1. When the error is "service not found" -> this means the service is not yet registered + // 2. When the error is "retry" -> this means the service is registered, but is not willing to process it right now, maybe due to load + return this.isServiceNotFoundMessage(error) || this.isServiceRetryError(error); + } + + private async workerCallback(queueItem: Work<{ to: string; data: any }>): Promise { + this.logger.info(`Processing queue item ${queueItem._id} for work`); + this.logger.info(`Queue item is trying to call ${queueItem.message.to}`); + try { + await api.waitAndCall(queueItem.message.to, [queueItem.message]); + this.logger.info(`Queue item ${queueItem._id} completed`); + return 'Completed' as const; + } catch (err: unknown) { + const e = err as Error; + this.logger.error(`Queue item ${queueItem._id} errored: ${e.message}`); + queueItem.releasedReason = e.message; + // Let's only retry for X times when the error is "service not found" + // For any other error, we'll just reject the item + if ((queueItem.retryCount || 0) < this.retryCount && this.isRetryableError(e.message)) { + this.logger.info(`Queue item ${queueItem._id} will be retried in 10 seconds`); + queueItem.nextReceivableTime = new Date(Date.now() + this.retryDelay); + return 'Retry' as const; + } + this.logger.info(`Queue item ${queueItem._id} will be rejected`); + return 'Rejected' as const; + } + } + + // Registers the actual workers, the actions lib will try to fetch elements to work on + private async registerWorkers(): Promise { + this.logger.info('Registering workers of type "work"'); + this.queue.registerWorker('work', this.workerCallback.bind(this)); + + this.logger.info('Registering workers of type "workComplete"'); + this.queue.registerWorker('workComplete', this.workerCallback.bind(this)); + } + + private matchServiceCall(service: string): boolean { + const [namespace, action] = service.split('.'); + if (!namespace || !action) { + return false; + } + return true; + } + + // Queues an action of type "X" to be processed by the workers + // Action receives a record of unknown data that will be passed to the actual service + // `to` is a service name that will be called, including namespace + action + // This is a "generic" job that allows you to call any service + async queueWork>(queue: Actions, to: string, data: T): Promise { + if (!this.shouldWork) { + this.logger.info('Queue worker is disabled, not queueing work'); + return; + } + + this.logger.info(`Queueing work for ${to}`); + if (!this.matchServiceCall(to)) { + // We don't want to queue calls to invalid service names + throw new Error(`Invalid service name ${to}`); + } + + await this.queue.enqueue(queue, { ...data, to }); + } + + async queueInfo(): Promise { + return this.db + .collection(this.queue.collectionName) + .aggregate([ + { + $addFields: { + status: { $cond: [{ $ifNull: ['$rejectionReason', false] }, 'Rejected', 'In progress'] }, + }, + }, + { $group: { _id: { type: '$type', status: '$status' }, elements: { $push: '$$ROOT' }, total: { $sum: 1 } } }, + // Project from each group the type, status and total of elements + { $project: { _id: 0, type: '$_id.type', status: '$_id.status', total: 1 } }, + ]) + .toArray(); + } +} diff --git a/ee/packages/omnichannel-services/src/externals/mongo-message-queue.d.ts b/ee/packages/omnichannel-services/src/externals/mongo-message-queue.d.ts new file mode 100644 index 00000000000..ea7b6f4ed97 --- /dev/null +++ b/ee/packages/omnichannel-services/src/externals/mongo-message-queue.d.ts @@ -0,0 +1,34 @@ +declare module 'mongo-message-queue' { + import type { Db } from 'mongodb'; + + export type Work = { + _id: string; + dateCreated: Date; + type: string; + message: T; + priority: number; + receivedTime: Date; + releasedReason?: string; + retryCount?: number; + nextReceivableTime?: Date; + rejectionReason?: string; + }; + + export type ValidResult = 'Completed' | 'Rejected' | 'Retry'; + + export type Actions = 'work' | 'workComplete'; + + export default class MessageQueue { + collectionName: string; + + databasePromise: () => Promise; + + registerWorker(type: Actions, worker: (queueItem: Work) => Promise): void; + + enqueue(type: Actions, message: T, options?: { nextReceivableTime: Date; priority: number }): Promise; + + enqueueAndProcess(type: Actions, message: T, options?: { nextReceivableTime: Date; priority: number }): Promise; + + stopPolling(): void; + } +} diff --git a/ee/packages/omnichannel-services/src/index.ts b/ee/packages/omnichannel-services/src/index.ts new file mode 100644 index 00000000000..4ddfee08c5a --- /dev/null +++ b/ee/packages/omnichannel-services/src/index.ts @@ -0,0 +1,2 @@ +export { OmnichannelTranscript } from './OmnichannelTranscript'; +export { QueueWorker } from './QueueWorker'; diff --git a/ee/packages/omnichannel-services/tsconfig.json b/ee/packages/omnichannel-services/tsconfig.json new file mode 100644 index 00000000000..230b31ccf29 --- /dev/null +++ b/ee/packages/omnichannel-services/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../tsconfig.base.server.json", + "compilerOptions": { + "outDir": "./dist", + "declaration": true, + }, + "include": ["./src/**/*", "../../../apps/meteor/definition/externals/meteor"], + "exclude": ["./dist"] +} diff --git a/ee/packages/pdf-worker/.eslintrc.json b/ee/packages/pdf-worker/.eslintrc.json new file mode 100644 index 00000000000..a83aeda48e6 --- /dev/null +++ b/ee/packages/pdf-worker/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "extends": ["@rocket.chat/eslint-config"], + "ignorePatterns": ["**/dist"] +} diff --git a/ee/packages/pdf-worker/.storybook/main.js b/ee/packages/pdf-worker/.storybook/main.js new file mode 100644 index 00000000000..a4d2430303f --- /dev/null +++ b/ee/packages/pdf-worker/.storybook/main.js @@ -0,0 +1,6 @@ +module.exports = { + stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], + addons: ['@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-interactions'], + framework: '@storybook/react', + staticDirs: ['../src/public'], +}; diff --git a/ee/packages/pdf-worker/.storybook/preview.js b/ee/packages/pdf-worker/.storybook/preview.js new file mode 100644 index 00000000000..abd704f7951 --- /dev/null +++ b/ee/packages/pdf-worker/.storybook/preview.js @@ -0,0 +1,25 @@ +import '../../../apps/meteor/app/theme/client/main.css'; +import 'highlight.js/styles/github.css'; + +export const parameters = { + actions: { argTypesRegex: "^on[A-Z].*" }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/, + }, + }, +} + +export const decorators = [ + (Story) => ( +
+ + +
+ ) +]; diff --git a/ee/packages/pdf-worker/README.md b/ee/packages/pdf-worker/README.md new file mode 100644 index 00000000000..aa81fd1b499 --- /dev/null +++ b/ee/packages/pdf-worker/README.md @@ -0,0 +1,56 @@ +# @rocket.chat/pdf-worker + +This package is a PDF worker for Rocket.Chat. It allows for the rendering of PDFs within the Rocket.Chat application. `@react-pdf/renderer` is used as the PDF renderer, as it provides a React-based solution for rendering PDFs, making it easy to integrate into the existing React codebase of Rocket.Chat. + +## Installation + +To install this package, you can use yarn: + +``` +yarn add @rocket.chat/pdf-worker + +yarn install +``` + +## Usage + +To use this package, you will need to import it in your project and use the provided PDF renderer. + +``` +import { PdfWorker } from '@rocket.chat/pdf-worker'; + +const PdfWorker = new PdfWorker(); +PdfWorker.render('template-mode'); +``` + +## Development + +If you wish to contribute to the development of this package, you can clone the repository and run the following commands: + +``` +yarn dev +``` + +This will start a development server and allow you to make changes to the code. + +## Testing + +You can run the tests for this package with the following command: + +``` +yarn test +``` + +## Storybook + +You can also run Storybook to see the components in action and debug during development: + +``` +yarn storybook +``` + +This will start a development server and allow you to see the different components and their states. It also provides a visual representation of the components and how they will look in the final application, making it easy to debug and develop the templates. + +## Additional Note + +Please refer to the [official documentation](https://docs.rocket.chat/) of @rocket.chat/pdf-worker for more information about this package. diff --git a/ee/packages/pdf-worker/jest.config.js b/ee/packages/pdf-worker/jest.config.js new file mode 100644 index 00000000000..b8dc79efdfc --- /dev/null +++ b/ee/packages/pdf-worker/jest.config.js @@ -0,0 +1,17 @@ +module.exports = { + preset: 'ts-jest', + errorOnDeprecated: true, + testEnvironment: 'jsdom', + modulePathIgnorePatterns: ['/dist/'], + globals: { + 'ts-jest': { + tsconfig: { + noUnusedLocals: false, + noUnusedParameters: false, + }, + }, + }, + moduleNameMapper: { + '\\.css$': 'identity-obj-proxy', + }, +}; diff --git a/ee/packages/pdf-worker/package.json b/ee/packages/pdf-worker/package.json new file mode 100644 index 00000000000..ba37b6e78e3 --- /dev/null +++ b/ee/packages/pdf-worker/package.json @@ -0,0 +1,51 @@ +{ + "name": "@rocket.chat/pdf-worker", + "version": "0.0.1", + "private": true, + "devDependencies": { + "@storybook/addon-actions": "~6.5.14", + "@storybook/addon-docs": "~6.5.14", + "@storybook/addon-essentials": "~6.5.14", + "@storybook/addon-interactions": "~6.5.14", + "@storybook/addon-links": "~6.5.14", + "@storybook/builder-webpack4": "~6.5.14", + "@storybook/manager-webpack4": "~6.5.14", + "@storybook/react": "~6.5.14", + "@storybook/testing-library": "~0.0.13", + "@testing-library/jest-dom": "^5.16.5", + "@testing-library/react": "~13.4.0", + "@types/emojione": "^2.2.6", + "@types/jest": "^27.4.1", + "@types/react-dom": "^18", + "@types/testing-library__jest-dom": "^5", + "eslint": "^8.12.0", + "jest": "^27.5.1", + "react-dom": "^18.2.0", + "ts-jest": "^27.1.4", + "typescript": "~4.5.5" + }, + "scripts": { + "lint": "eslint --ext .js,.jsx,.ts,.tsx .", + "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "jest": "jest", + "build": "rm -rf dist && tsc -p tsconfig.json && cp -r src/public dist/public", + "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput", + "storybook": "start-storybook -p 6006" + }, + "main": "./dist/index.js", + "typings": "./dist/index.d.ts", + "files": [ + "/dist" + ], + "dependencies": { + "@react-pdf/renderer": "^3.1.3", + "@rocket.chat/core-typings": "workspace:^", + "@rocket.chat/fuselage-tokens": "next", + "@types/react": "^18.0.26", + "emoji-assets": "^7.0.1", + "emoji-toolkit": "^7.0.0", + "moment": "^2.29.4", + "moment-timezone": "^0.5.34", + "react": "^18.2.0" + } +} diff --git a/ee/packages/pdf-worker/src/index.ts b/ee/packages/pdf-worker/src/index.ts new file mode 100644 index 00000000000..e40f73355bb --- /dev/null +++ b/ee/packages/pdf-worker/src/index.ts @@ -0,0 +1,42 @@ +import { ChatTranscript } from './strategies/ChatTranscript'; +import type { IStrategy } from './types/IStrategy'; + +export type Templates = 'chat-transcript'; + +export class PdfWorker { + protected validMimeTypes = ['image/jpeg', 'image/png', 'image/jpg']; + + mode: Templates; + + worker: IStrategy; + + constructor(mode: Templates) { + if (!mode) { + throw new Error('Invalid mode'); + } + + this.mode = mode; + this.worker = this.getWorkerClass(); + } + + getWorkerClass(): IStrategy { + switch (this.mode) { + case 'chat-transcript': + return new ChatTranscript(); + default: + throw new Error('Invalid mode'); + } + } + + isMimeTypeValid(mimeType?: string): boolean { + if (!mimeType) { + return false; + } + return this.validMimeTypes.includes(mimeType?.toLowerCase()); + } + + async renderToStream({ data }: { template: Templates; data: Record }): Promise { + const parsedData = this.worker.parseTemplateData(data); + return this.worker.renderTemplate(parsedData); + } +} diff --git a/ee/packages/pdf-worker/src/public/fira-code700.ttf b/ee/packages/pdf-worker/src/public/fira-code700.ttf new file mode 100644 index 0000000000000000000000000000000000000000..fe43df2996d2c0e1015c227abe7d3de2a28e16cf GIT binary patch literal 61584 zcmc${2YejG^#{JQyLTs@+TE%5JDqyh$*OL0le^q)$u{o2<8EUc(`~t8z}OUnDW-

( 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 iGM-nAz76d%V$6n*6`NbCV{87FiwN=krf`bI(2N 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 literal 0 HcmV?d00001 diff --git a/ee/packages/pdf-worker/src/public/inter500-italic.ttf b/ee/packages/pdf-worker/src/public/inter500-italic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..a08e95b8f6ed58b1af08a4c81d2e7b581d1418e0 GIT binary patch literal 81472 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 D=6x(| literal 0 HcmV?d00001 diff --git a/ee/packages/pdf-worker/src/public/inter500.ttf b/ee/packages/pdf-worker/src/public/inter500.ttf new file mode 100644 index 0000000000000000000000000000000000000000..a5e8c6e4360443d05497315431369033b7d0434c GIT binary patch literal 46760 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 E%@4 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 literal 0 HcmV?d00001 diff --git a/ee/packages/pdf-worker/src/public/inter700-italic.ttf b/ee/packages/pdf-worker/src/public/inter700-italic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..a095e5285609d5626421e804f0f7bc4f5c64aba4 GIT binary patch literal 78608 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 V55>V}u;D#!fM2ok`R{Sh`yb7_(~JNB literal 0 HcmV?d00001 diff --git a/ee/packages/pdf-worker/src/strategies/ChatTranscript.spec.ts b/ee/packages/pdf-worker/src/strategies/ChatTranscript.spec.ts new file mode 100644 index 00000000000..96e7a3e139a --- /dev/null +++ b/ee/packages/pdf-worker/src/strategies/ChatTranscript.spec.ts @@ -0,0 +1,60 @@ +import moment from 'moment'; + +import '@testing-library/jest-dom'; +import { ChatTranscript } from './ChatTranscript'; +import { invalidData, validData, newDayData, sameDayData, translationsData } from '../templates/ChatTranscript/ChatTranscript.fixtures'; + +jest.mock('../templates/ChatTranscript', () => { + return { + exportTranscript: jest.fn(() => Promise.resolve()), + }; +}); + +describe('Strategies/ChatTranscript', () => { + let chatTranscript: ChatTranscript; + + beforeEach(() => { + chatTranscript = new ChatTranscript(); + }); + + it('should throws an error if data is not valid', () => { + expect(() => { + chatTranscript.renderTemplate(invalidData); + }).toThrow('Invalid data'); + }); + + it('should creates a divider for a first message', () => { + const result = chatTranscript.parseTemplateData(validData); + expect(result.messages[0]).toHaveProperty('divider'); + }); + + it('should creates a divider if message is from a new day', () => { + const result = chatTranscript.parseTemplateData(newDayData); + expect(result.messages[0]).toHaveProperty('divider'); + expect(result.messages[1]).toHaveProperty('divider', moment(newDayData.messages[1].ts).format(newDayData.dateFormat)); + }); + + it('should not create a divider if message is from the same day', () => { + const result = chatTranscript.parseTemplateData(sameDayData); + expect(result.messages[0]).toHaveProperty('divider'); + expect(result.messages[1]).not.toHaveProperty('divider'); + }); + + it('should returns the correct translation value for a given key', () => { + const data = { ...validData, translations: translationsData.translations }; + const result = chatTranscript.parseTemplateData(data); + expect(result.t('transcript')).toEqual('Transcript'); + expect(result.t('visitor')).toEqual('Visitor'); + expect(result.t('agent')).toEqual('Agent'); + expect(result.t('date')).toEqual('Date'); + expect(result.t('time')).toEqual('Time'); + }); + + it('should throws an error if translation not found', () => { + const data = { ...validData, translations: translationsData.translations }; + const result = chatTranscript.parseTemplateData(data); + expect(() => { + result.t('invalidKey'); + }).toThrow('Translation not found for key: invalidKey'); + }); +}); diff --git a/ee/packages/pdf-worker/src/strategies/ChatTranscript.ts b/ee/packages/pdf-worker/src/strategies/ChatTranscript.ts new file mode 100644 index 00000000000..3aea23e60c0 --- /dev/null +++ b/ee/packages/pdf-worker/src/strategies/ChatTranscript.ts @@ -0,0 +1,84 @@ +import moment from 'moment-timezone'; + +import type { Data } from '../types/Data'; +import type { IStrategy } from '../types/IStrategy'; +import exportChatTranscript from '../templates/ChatTranscript'; +import type { ChatTranscriptData, PDFMessage } from '../templates/ChatTranscript'; + +export class ChatTranscript implements IStrategy { + private isNewDay(current: PDFMessage, previous: PDFMessage | undefined, timezone: string): boolean { + return !previous || !moment(current.ts).tz(timezone).isSame(previous.ts, 'day'); + } + + private parserMessages(messages: PDFMessage[], dateFormat: string, timeAndDateFormat: string, timezone: string): PDFMessage[] { + return messages.map((message, index, arr) => { + const previousMessage = arr[index - 1]; + const { ts, ...rest } = message; + const formattedTs = moment(ts).tz(timezone).format(timeAndDateFormat); + const isDivider = this.isNewDay(message, previousMessage, timezone); + + if (isDivider) { + return { + ...rest, + ts: formattedTs, + divider: moment(ts).tz(timezone).format(dateFormat), + }; + } + + return { + ...rest, + ts: formattedTs, + }; + }); + } + + private getTranslations(translations: Record[]): (key: string) => unknown { + return (key: string) => { + const translation = translations.find((t) => t.key === key); + if (!translation) { + throw new Error(`Translation not found for key: ${key}`); + } + return translation.value; + }; + } + + private isChatTranscriptData = (data: any): data is ChatTranscriptData => { + return ( + 'header' in data && + 'messages' in data && + 't' in data && + 'agent' in data.header && + 'visitor' in data.header && + 'siteName' in data.header && + 'date' in data.header && + 'time' in data.header + ); + }; + + renderTemplate(data: Data): Promise { + if (!this.isChatTranscriptData(data)) { + throw new Error('Invalid data'); + } + return exportChatTranscript(data); + } + + parseTemplateData(data: Record): Data { + return { + header: { + visitor: data.visitor, + agent: data.agent, + siteName: data.siteName, + date: `${moment(data.closedAt as Date) + .tz(data.timezone as string) + .format(String(data.dateFormat))}`, + time: `${moment(data.closedAt as Date) + .tz(data.timezone as string) + .format('H:mm:ss')} ${data.timezone}`, + }, + messages: Array.isArray(data.messages) + ? this.parserMessages(data.messages, data.dateFormat as string, data.timeAndDateFormat as string, data.timezone as string) + : [], + t: this.getTranslations(data.translations as Record[]), + }; + } +} diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/ChatTranscript.fixtures.ts b/ee/packages/pdf-worker/src/templates/ChatTranscript/ChatTranscript.fixtures.ts new file mode 100644 index 00000000000..1ffa48c47c8 --- /dev/null +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/ChatTranscript.fixtures.ts @@ -0,0 +1,242 @@ +import type { Data } from '../../types/Data'; + +const base64Image = + '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'; + +export const validData = { + visitor: { name: 'John Doe', username: 'john.doe' }, + agent: { name: 'Jane Smith', username: 'jane.smith' }, + siteName: 'example.com', + closedAt: '2022-11-21T00:00:00.000Z', + dateFormat: 'MMM D, YYYY', + timeAndDateFormat: 'MMM D, YYYY H:mm:ss', + timezone: 'UTC', + messages: [ + { ts: '2022-11-21T16:00:00.000Z', text: 'Hello, how can I help you today?' }, + { ts: '2022-11-21T16:00:00.000Z', text: 'I am having trouble with my account.' }, + ], + translations: [ + { key: 'transcript', value: 'Transcript' }, + { key: 'visitor', value: 'Visitor' }, + { key: 'agent', value: 'Agent' }, + { key: 'siteName', value: 'Site Name' }, + { key: 'date', value: 'Date' }, + { key: 'time', value: 'Time' }, + ], +}; + +export const invalidData = {} as Data; + +export const newDayData = { + closedAt: '2022-11-21T00:00:00.000Z', + dateFormat: 'MMM D, YYYY', + timeAndDateFormat: 'MMM D, YYYY H:mm:ss', + messages: [ + { ts: '2022-11-21T16:00:00.000Z', text: 'Hello' }, + { ts: '2022-11-22T16:00:00.000Z', text: 'How are you' }, + ], +}; + +export const sameDayData = { + closedAt: '2022-11-21T00:00:00.000Z', + dateFormat: 'MMM D, YYYY', + timeAndDateFormat: 'MMM D, YYYY H:mm:ss', + messages: [ + { ts: '2022-11-21T16:00:00.000Z', text: 'Hello' }, + { ts: '2022-11-21T16:00:00.000Z', text: 'How are you' }, + ], +}; + +export const translationsData = { + translations: [ + { key: 'transcript', value: 'Transcript' }, + { key: 'visitor', value: 'Visitor' }, + { key: 'agent', value: 'Agent' }, + { key: 'date', value: 'Date' }, + { key: 'time', value: 'Time' }, + ], +}; + +export const validFile = { name: 'screenshot.png', buffer: Buffer.from([1, 2, 3]) }; + +export const invalidFile = { name: 'audio.mp3', buffer: null }; + +export const validMessage = { + msg: 'Hello, how can I help you today?', + ts: '2022-11-21T16:00:00.000Z', + u: { + _id: '123', + name: 'Juanito De Ponce', + username: 'juanito.ponce', + }, +}; + +export const exampleData = { + agent: { + name: 'Juanito De Ponce', + username: 'juanito.ponce', + }, + visitor: { + name: 'Christian Castro', + username: 'christian.castro', + }, + siteName: 'Rocket.Chat', + closedAt: '2022-11-21T00:00:00.000Z', + dateFormat: 'MMM D, YYYY', + timeAndDateFormat: 'MMM D, YYYY H:mm:ss', + timezone: 'Etc/GMT+1', + translations: [ + { key: 'Chat_transcript', value: 'Chat transcript' }, + { key: 'Agent', value: 'Agent' }, + { key: 'Date', value: 'Date' }, + { key: 'Customer', value: 'Customer' }, + { key: 'Time', value: 'Time' }, + { key: 'This_attachment_is_not_supported', value: 'This attachment is not supported' }, + ], + messages: [ + { + msg: 'Hello, how can I help you today?', + ts: '2022-11-21T16:00:00.000Z', + u: { + _id: '123', + name: 'Juanito De Ponce', + username: 'juanito.ponce', + }, + }, + { + msg: 'I am having trouble with my account.', + ts: '2022-11-21T16:00:00.000Z', + u: { + _id: '321', + name: 'Christian Castro', + username: 'cristiano.castro', + }, + md: [ + { + type: 'UNORDERED_LIST', + value: [ + { type: 'LIST_ITEM', value: [{ type: 'PLAIN_TEXT', value: 'I am having trouble with my account;' }] }, + { + type: 'LIST_ITEM', + value: [ + { type: 'PLAIN_TEXT', value: 'I am having trouble with my password. ' }, + { type: 'EMOJI', value: undefined, unicode: '🙂' }, + ], + }, + ], + }, + ], + }, + { + msg: 'Can you please provide your account email?', + ts: '2022-11-21T16:00:00.000Z', + u: { + _id: '123', + name: 'Juanito De Ponce', + username: 'juanito.ponce', + }, + }, + { + msg: 'myemail@example.com', + ts: '2022-11-21T16:00:00.000Z', + u: { + _id: '321', + name: 'Christian Castro', + username: 'cristiano.castro', + }, + files: [{ name: 'screenshot.png', buffer: Buffer.from(base64Image, 'base64') }], + }, + { + msg: 'Thank you, I am checking on that for you now.', + ts: '2022-11-21T16:00:00.000Z', + u: { + _id: '123', + name: 'Juanito De Ponce', + username: 'juanito.ponce', + }, + files: [invalidFile], + }, + { + ts: '2022-11-21T16:00:00.000Z', + u: { + _id: '123', + name: 'Juanito De Ponce', + username: 'juanito.ponce', + }, + md: [ + { + type: 'PARAGRAPH', + value: [ + { type: 'PLAIN_TEXT', value: 'I have fixed the issue, is there anything else I can help you with? ' }, + { type: 'EMOJI', value: { type: 'PLAIN_TEXT', value: 'smile' } }, + ], + }, + ], + }, + { + msg: 'No, that is all. Thank you for your help.', + ts: '2022-11-21T16:00:00.000Z', + u: { + _id: '321', + name: 'Christian Castro', + username: 'cristiano.castro', + }, + md: [ + { + type: 'PARAGRAPH', + value: [ + { type: 'PLAIN_TEXT', value: 'No, that is all. Thank you for your help. ' }, + { type: 'INLINE_CODE', value: { type: 'PLAIN_TEXT', value: 'Inline code' } }, + ], + }, + ], + }, + { + msg: 'You are welcome. Have a great day!', + ts: '2022-11-22T16:00:00.000Z', + u: { + _id: '123', + name: 'Juanito De Ponce', + username: 'juanito.ponce', + }, + md: [ + { + type: 'CODE', + value: [ + { type: 'CODE_INLINE', value: { type: 'PLAIN_TEXT', value: 'Multi line code' } }, + { type: 'CODE_INLINE', value: { type: 'PLAIN_TEXT', value: '' } }, + { type: 'CODE_INLINE', value: { type: 'PLAIN_TEXT', value: '--rcx-color-button-background-success-focus: #general;' } }, + { type: 'CODE_INLINE', value: { type: 'PLAIN_TEXT', value: '--rcx-color-button-background-success-keyfocus: #1D7256;' } }, + ], + }, + ], + }, + { + msg: 'You are welcome. Have a great day!', + ts: '2022-11-22T16:00:00.000Z', + u: { + _id: '123', + name: 'Juanito De Ponce', + username: 'juanito.ponce', + }, + }, + { + msg: 'Do you have any question?', + ts: '2022-11-22T16:00:00.000Z', + u: { + _id: '123', + name: 'Juanito De Ponce', + username: 'juanito.ponce', + }, + }, + { + msg: 'No, I am good. Thanks!', + ts: '2022-11-22T16:00:00.000Z', + u: { + _id: '321', + name: 'Christian Castro', + username: 'cristiano.castro', + }, + }, + ], +}; diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/ChatTranscript.stories.tsx b/ee/packages/pdf-worker/src/templates/ChatTranscript/ChatTranscript.stories.tsx new file mode 100644 index 00000000000..95bc1537c7d --- /dev/null +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/ChatTranscript.stories.tsx @@ -0,0 +1,39 @@ +import { Font, PDFViewer } from '@react-pdf/renderer'; +import type { ComponentMeta, ComponentStory } from '@storybook/react'; + +import { exampleData } from './ChatTranscript.fixtures'; +import { ChatTranscript } from '../../strategies/ChatTranscript'; +import type { ChatTranscriptData } from '.'; +import { ChatTranscriptPDF } from '.'; + +Font.register({ + family: 'Inter', + fonts: [ + { src: '/inter400.ttf' }, + { src: '/inter400-italic.ttf', fontStyle: 'italic' }, + { src: '/inter500.ttf', fontWeight: 500 }, + { src: '/inter500-italic.ttf', fontWeight: 500, fontStyle: 'italic' }, + { src: '/inter700.ttf', fontWeight: 700 }, + { src: '/inter700-italic.ttf', fontWeight: 700, fontStyle: 'italic' }, + ], +}); + +Font.register({ + family: 'FiraCode', + fonts: [{ src: '/fira-code700.ttf', fontWeight: 700 }], +}); + +Font.registerHyphenationCallback((word) => [word]); + +export default { + title: 'ChatTranscriptPDFTemplate', + component: ChatTranscriptPDF, +} as ComponentMeta; + +const data = new ChatTranscript().parseTemplateData(exampleData) as unknown as ChatTranscriptData; + +export const ChatTranscriptPDFTemplate: ComponentStory = () => ( + + + +); diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/components/Divider.tsx b/ee/packages/pdf-worker/src/templates/ChatTranscript/components/Divider.tsx new file mode 100644 index 00000000000..a8e1a11491a --- /dev/null +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/components/Divider.tsx @@ -0,0 +1,28 @@ +import { Text, View, StyleSheet } from '@react-pdf/renderer'; +import colors from '@rocket.chat/fuselage-tokens/colors.json'; + +const styles = StyleSheet.create({ + wrapper: { + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 12, + fontSize: 12, + fontWeight: 700, + }, + line: { + flex: 1, + height: 2, + backgroundColor: colors.n200, + }, + text: { + paddingHorizontal: 8, + }, +}); + +export const Divider = ({ divider }: { divider: string }) => ( + + + {divider} + + +); diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/components/Files.spec.tsx b/ee/packages/pdf-worker/src/templates/ChatTranscript/components/Files.spec.tsx new file mode 100644 index 00000000000..ca45996ee9f --- /dev/null +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/components/Files.spec.tsx @@ -0,0 +1,33 @@ +import { render } from '@testing-library/react'; +import type { ReactNode } from 'react'; + +import '@testing-library/jest-dom'; +import { Files } from './Files'; +import { invalidFile, validFile } from '../ChatTranscript.fixtures'; + +jest.mock('@react-pdf/renderer', () => ({ + StyleSheet: { create: () => ({ style: '' }) }, + Image: () => , + Text: ({ children }: { children: ReactNode }) =>

, + View: ({ children }: { children: ReactNode }) =>
{children}
, +})); + +describe('components/Files', () => { + it('should renders file name and invalid message when buffer is null', () => { + const { getByText } = render(); + const invalidText = getByText('invalid'); + const fileName = getByText(invalidFile.name); + + expect(invalidText).toBeInTheDocument(); + expect(fileName).toBeInTheDocument(); + }); + + it('should renders file name and image when buffer is not null', () => { + const { getByRole, getByText } = render(); + const image = getByRole('img'); + const fileName = getByText(validFile.name); + + expect(image).toBeInTheDocument(); + expect(fileName).toBeInTheDocument(); + }); +}); diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/components/Files.tsx b/ee/packages/pdf-worker/src/templates/ChatTranscript/components/Files.tsx new file mode 100644 index 00000000000..6c46437b3a9 --- /dev/null +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/components/Files.tsx @@ -0,0 +1,46 @@ +import { View, StyleSheet, Text, Image } from '@react-pdf/renderer'; +import { fontScales } from '@rocket.chat/fuselage-tokens/typography.json'; +import colors from '@rocket.chat/fuselage-tokens/colors.json'; + +import type { PDFFile } from '..'; + +const styles = StyleSheet.create({ + file: { + color: colors.n700, + marginTop: 4, + flexDirection: 'column', + fontSize: fontScales.c1.fontSize, + }, + image: { + width: 400, + maxHeight: 240, + objectFit: 'contain', + objectPosition: '0', + }, + invalidMessage: { + backgroundColor: colors.n100, + textAlign: 'center', + borderColor: colors.n250, + borderWidth: 1, + paddingVertical: 8, + marginTop: 4, + }, +}); + +export const Files = ({ files, invalidMessage }: { files: PDFFile[]; invalidMessage: string }) => ( + + {files?.map((file, index) => ( + + {file.name} + {file.buffer ? ( + // Cache = false is required to avoid a bug in react-pdf + // Which causes the image to be duplicated when using buffers because of bad caching + // https://github.com/diegomura/react-pdf/issues/1805 + + ) : ( + {invalidMessage} + )} + + ))} + +); diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/components/Header.tsx b/ee/packages/pdf-worker/src/templates/ChatTranscript/components/Header.tsx new file mode 100644 index 00000000000..f9f33d677b4 --- /dev/null +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/components/Header.tsx @@ -0,0 +1,67 @@ +import { Text, View, StyleSheet } from '@react-pdf/renderer'; +import colors from '@rocket.chat/fuselage-tokens/colors.json'; +import { fontScales } from '@rocket.chat/fuselage-tokens/typography.json'; + +const styles = StyleSheet.create({ + header: { + padding: 32, + borderRadius: 4, + backgroundColor: colors.n100, + color: colors.n900, + marginBottom: 16, + }, + headerText: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + borderBottomColor: colors.n200, + borderBottomWidth: 2, + marginBottom: 16, + paddingBottom: 16, + }, + pagination: { + fontSize: fontScales.c1.fontSize, + color: colors.n700, + }, + title: { + fontSize: fontScales.h4.fontSize, + fontWeight: fontScales.h4.fontWeight, + }, + subtitle: { + fontSize: fontScales.p2m.fontSize, + fontWeight: fontScales.p2m.fontWeight, + }, + container: { + fontSize: fontScales.c1.fontSize, + flexDirection: 'row', + flexWrap: 'wrap', + }, + item: { + paddingTop: 8, + paddingRight: 10, + flexGrow: 1, + flexShrink: 1, + width: '50%', + }, + key: { + fontWeight: fontScales.c2.fontWeight, + }, +}); + +export const Header = ({ title, subtitle, values }: { title: string; subtitle: string; values: { key: string; value: string }[] }) => ( + + + {title} + `${pageNumber}/${totalPages}`} /> + + {subtitle} + + {values.map((value, index) => ( + + {value.key}: + {value.value} + + ))} + + +); diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/components/MessageHeader.tsx b/ee/packages/pdf-worker/src/templates/ChatTranscript/components/MessageHeader.tsx new file mode 100644 index 00000000000..9914dabc1f9 --- /dev/null +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/components/MessageHeader.tsx @@ -0,0 +1,27 @@ +import { Text, View, StyleSheet } from '@react-pdf/renderer'; +import colors from '@rocket.chat/fuselage-tokens/colors.json'; +import { fontScales } from '@rocket.chat/fuselage-tokens/typography.json'; + +const styles = StyleSheet.create({ + header: { + flexDirection: 'row', + alignItems: 'center', + }, + name: { + fontSize: fontScales.p2b.fontSize, + fontWeight: fontScales.p2b.fontWeight, + color: colors.n900, + }, + time: { + fontSize: fontScales.c1.fontSize, + color: colors.n700, + marginLeft: 4, + }, +}); + +export const MessageHeader = ({ name, time }: { name: string; time: string }) => ( + + {name} + {time} + +); diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/components/MessageList.spec.tsx b/ee/packages/pdf-worker/src/templates/ChatTranscript/components/MessageList.spec.tsx new file mode 100644 index 00000000000..faf0eb515e4 --- /dev/null +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/components/MessageList.spec.tsx @@ -0,0 +1,40 @@ +import { render } from '@testing-library/react'; + +import '@testing-library/jest-dom'; +import { MessageList } from './MessageList'; +import { invalidFile, validFile, validMessage } from '../ChatTranscript.fixtures'; + +jest.mock('@react-pdf/renderer', () => ({ + Image: () =>
Image
, + StyleSheet: { create: () => ({ style: '' }) }, + Text: ({ children }: { children: string }) =>
{children}
, + View: ({ children }: { children: string }) =>
{children}
, + Files: ({ children }: { children: string }) =>
{children}
, +})); + +describe('components/MessageList', () => { + it('should render correctly', () => { + const { getByText } = render(); + expect(getByText(validMessage.msg)).toBeInTheDocument(); + }); + + it('should render divider', () => { + const { getByText } = render(); + expect(getByText(validMessage.msg)).toBeInTheDocument(); + expect(getByText('divider')).toBeInTheDocument(); + }); + + it('should render file', () => { + const { getByText } = render(); + expect(getByText(validMessage.msg)).toBeInTheDocument(); + expect(getByText(validFile.name)).toBeInTheDocument(); + }); + + it('should render invalid file message', () => { + const { getByText } = render( + , + ); + expect(getByText(validMessage.msg)).toBeInTheDocument(); + expect(getByText('invalid message')).toBeInTheDocument(); + }); +}); diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/components/MessageList.tsx b/ee/packages/pdf-worker/src/templates/ChatTranscript/components/MessageList.tsx new file mode 100644 index 00000000000..8c42d2245c0 --- /dev/null +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/components/MessageList.tsx @@ -0,0 +1,33 @@ +import { Text, View, StyleSheet } from '@react-pdf/renderer'; +import { fontScales } from '@rocket.chat/fuselage-tokens/typography.json'; + +import { Divider } from './Divider'; +import { MessageHeader } from './MessageHeader'; +import { Files } from './Files'; +import type { ChatTranscriptData } from '..'; +import { Markup } from '../markup'; + +const styles = StyleSheet.create({ + wrapper: { + marginBottom: 16, + paddingHorizontal: 32, + }, + message: { + marginTop: 1, + fontSize: fontScales.p2.fontSize, + textAlign: 'justify', + }, +}); + +export const MessageList = ({ messages, invalidFileMessage }: { messages: ChatTranscriptData['messages']; invalidFileMessage: string }) => ( + + {messages.map((message, index) => ( + + {message.divider && } + + {message.md ? : {message.msg}} + {message.files && } + + ))} + +); diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/index.tsx b/ee/packages/pdf-worker/src/templates/ChatTranscript/index.tsx new file mode 100644 index 00000000000..f7b8cfc0fe0 --- /dev/null +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/index.tsx @@ -0,0 +1,89 @@ +import * as path from 'path'; + +import ReactPDF, { Font, Document, Page, StyleSheet } from '@react-pdf/renderer'; +import type { ILivechatAgent, ILivechatVisitor, IMessage, Serialized } from '@rocket.chat/core-typings'; + +import { Header } from './components/Header'; +import { MessageList } from './components/MessageList'; + +const FONT_PATH = path.resolve(__dirname, '../../public'); + +export type PDFFile = { name?: string; buffer: Buffer | null; extension?: 'png' | 'jpg' }; + +export type PDFMessage = Serialized, 'files'>> & { + files?: PDFFile[]; +} & { divider?: string }; + +export type ChatTranscriptData = { + header: { + agent: Pick; + visitor: Pick; + siteName: string; + date: string; + time: string; + }; + messages: PDFMessage[]; + t: (key: string) => string; +}; + +const styles = StyleSheet.create({ + page: { + fontFamily: 'Inter', + lineHeight: 1.25, + }, + wrapper: { + paddingHorizontal: 32, + }, + message: { + wordWrap: 'break-word', + fontSize: 12, + marginBottom: 20, + textAlign: 'justify', + }, +}); + +export const ChatTranscriptPDF = ({ header, messages, t }: ChatTranscriptData) => { + const agentValue = header.agent?.name || header.agent?.username || t('Omnichannel_Agent'); + const customerValue = header.visitor?.name || header.visitor?.username; + const dateValue = header.date; + const timeValue = header.time; + + return ( + + +
+ + + + ); +}; + +export default async (data: ChatTranscriptData): Promise => { + Font.register({ + family: 'Inter', + fonts: [ + { src: `${FONT_PATH}/inter400.ttf` }, + { src: `${FONT_PATH}/inter400-italic.ttf`, fontStyle: 'italic' }, + { src: `${FONT_PATH}/inter500.ttf`, fontWeight: 500 }, + { src: `${FONT_PATH}/inter500-italic.ttf`, fontWeight: 500, fontStyle: 'italic' }, + { src: `${FONT_PATH}/inter700.ttf`, fontWeight: 700 }, + { src: `${FONT_PATH}/inter700-italic.ttf`, fontWeight: 700, fontStyle: 'italic' }, + ], + }); + Font.register({ + family: 'FiraCode', + fonts: [{ src: `${FONT_PATH}/fira-code700.ttf`, fontWeight: 700 }], + }); + Font.registerHyphenationCallback((word) => [word]); + + return ReactPDF.renderToStream(); +}; diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/blocks/BigEmojiBlock.tsx b/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/blocks/BigEmojiBlock.tsx new file mode 100644 index 00000000000..83e8a52cbf8 --- /dev/null +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/blocks/BigEmojiBlock.tsx @@ -0,0 +1,18 @@ +import { Text } from '@react-pdf/renderer'; +import type * as MessageParser from '@rocket.chat/message-parser'; +import emojione from 'emoji-toolkit'; +import type { ReactElement } from 'react'; + +type BigEmojiBlockProps = { + emoji: MessageParser.Emoji[]; +}; + +const BigEmojiBlock = ({ emoji }: BigEmojiBlockProps): ReactElement => ( + + {emoji.map((emoji, index) => ( + {emoji.value ? `:${emoji.value?.value}:` : emojione.toShort(emoji.unicode)} + ))} + +); + +export default BigEmojiBlock; diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/blocks/CodeBlock.tsx b/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/blocks/CodeBlock.tsx new file mode 100644 index 00000000000..2d11b3c89a7 --- /dev/null +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/blocks/CodeBlock.tsx @@ -0,0 +1,21 @@ +import { Text, View } from '@react-pdf/renderer'; +import type * as MessageParser from '@rocket.chat/message-parser'; +import type { ReactElement } from 'react'; + +import { codeStyles } from '../elements/CodeSpan'; + +type CodeBlockProps = { + lines: MessageParser.CodeLine[]; +}; + +const CodeBlock = ({ lines }: CodeBlockProps): ReactElement => ( + + {lines.map((line, index) => ( + + {line.value?.value || ' '} + + ))} + +); + +export default CodeBlock; diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/blocks/HeadingBlock.tsx b/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/blocks/HeadingBlock.tsx new file mode 100644 index 00000000000..17b53896065 --- /dev/null +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/blocks/HeadingBlock.tsx @@ -0,0 +1,18 @@ +import type * as MessageParser from '@rocket.chat/message-parser'; +import { Text, View } from '@react-pdf/renderer'; +import { fontScales } from '@rocket.chat/fuselage-tokens/typography.json'; + +type HeadingBlockProps = { + items?: MessageParser.Plain[]; + level?: 1 | 2 | 3 | 4; +}; + +const Header = ({ items = [], level = 1 }: HeadingBlockProps) => ( + + {items.map((block, index) => ( + {block.value} + ))} + +); + +export default Header; diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/blocks/OrderedListBlock.tsx b/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/blocks/OrderedListBlock.tsx new file mode 100644 index 00000000000..c394e0a72eb --- /dev/null +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/blocks/OrderedListBlock.tsx @@ -0,0 +1,33 @@ +import { StyleSheet, Text, View } from '@react-pdf/renderer'; +import type * as MessageParser from '@rocket.chat/message-parser'; + +import InlineElements from '../elements/InlineElements'; + +const styles = StyleSheet.create({ + wrapper: { + marginTop: 4, + }, + list: { + flexDirection: 'row', + }, + number: { + fontWeight: 700, + marginHorizontal: 4, + }, +}); + +type OrderedListBlockProps = { + items: MessageParser.ListItem[]; +}; + +const OrderedListBlock = ({ items }: OrderedListBlockProps) => ( + + {items.map(({ value, number }, index) => ( + + {number}. + + ))} + +); + +export default OrderedListBlock; diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/blocks/ParagraphBlock.tsx b/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/blocks/ParagraphBlock.tsx new file mode 100644 index 00000000000..c8aa293835b --- /dev/null +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/blocks/ParagraphBlock.tsx @@ -0,0 +1,16 @@ +import { View } from '@react-pdf/renderer'; +import type * as MessageParser from '@rocket.chat/message-parser'; + +import InlineElements from '../elements/InlineElements'; + +type ParagraphBlockProps = { + items: MessageParser.Inlines[]; +}; + +const ParagraphBlock = ({ items }: ParagraphBlockProps) => ( + + + +); + +export default ParagraphBlock; diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/blocks/UnorderedListBlock.tsx b/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/blocks/UnorderedListBlock.tsx new file mode 100644 index 00000000000..dbedb8a0b5e --- /dev/null +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/blocks/UnorderedListBlock.tsx @@ -0,0 +1,33 @@ +import { StyleSheet, Text, View } from '@react-pdf/renderer'; +import type * as MessageParser from '@rocket.chat/message-parser'; + +import InlineElements from '../elements/InlineElements'; + +const styles = StyleSheet.create({ + wrapper: { + marginTop: 4, + }, + list: { + display: 'flex', + flexDirection: 'row', + }, + bullet: { + marginHorizontal: 4, + }, +}); + +type UnorderedListBlockProps = { + items: MessageParser.ListItem[]; +}; +const UnorderedListBlock = ({ items }: UnorderedListBlockProps) => ( + + {items.map(({ value }, index) => ( + + • + + + ))} + +); + +export default UnorderedListBlock; diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/elements/BoldSpan.tsx b/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/elements/BoldSpan.tsx new file mode 100644 index 00000000000..362fe269a65 --- /dev/null +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/elements/BoldSpan.tsx @@ -0,0 +1,41 @@ +import { StyleSheet, View, Text } from '@react-pdf/renderer'; +import type * as MessageParser from '@rocket.chat/message-parser'; + +import ItalicSpan from './ItalicSpan'; +import LinkSpan from './LinkSpan'; +import StrikeSpan from './StrikeSpan'; + +const styles = StyleSheet.create({ + bold: { + fontWeight: 700, + }, +}); + +type BoldSpanProps = { + children: (MessageParser.Link | MessageParser.MarkupExcluding)[]; +}; + +const BoldSpan = ({ children }: BoldSpanProps) => ( + + {children.map((child, index) => { + switch (child.type) { + case 'LINK': + return ; + + case 'PLAIN_TEXT': + return {child.value}; + + case 'STRIKE': + return ; + + case 'ITALIC': + return ; + + default: + return null; + } + })} + +); + +export default BoldSpan; diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/elements/CodeSpan.tsx b/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/elements/CodeSpan.tsx new file mode 100644 index 00000000000..c7f19a3e518 --- /dev/null +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/elements/CodeSpan.tsx @@ -0,0 +1,31 @@ +import { StyleSheet, View, Text } from '@react-pdf/renderer'; +import colors from '@rocket.chat/fuselage-tokens/colors.json'; + +export const codeStyles = StyleSheet.create({ + wrapper: { + backgroundColor: colors.n250, + borderColor: colors.n100, + borderWidth: 1, + borderRadius: 4, + paddingHorizontal: 3, + paddingVertical: 1, + }, + code: { + fontSize: 13, + color: colors.n800, + fontWeight: 700, + fontFamily: 'FiraCode', + }, +}); + +type CodeSpanProps = { + code: string; +}; + +const CodeSpan = ({ code }: CodeSpanProps) => ( + + {code} + +); + +export default CodeSpan; diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/elements/EmojiSpan.tsx b/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/elements/EmojiSpan.tsx new file mode 100644 index 00000000000..8adaeab5192 --- /dev/null +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/elements/EmojiSpan.tsx @@ -0,0 +1,9 @@ +import { Text } from '@react-pdf/renderer'; +import type * as MessageParser from '@rocket.chat/message-parser'; +import emojione from 'emoji-toolkit'; + +type EmojiSpanProps = MessageParser.Emoji; + +const EmojiSpan = (emoji: EmojiSpanProps) => {emoji.value ? `:${emoji.value?.value}:` : emojione.toShort(emoji.unicode)}; + +export default EmojiSpan; diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/elements/InlineElements.tsx b/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/elements/InlineElements.tsx new file mode 100644 index 00000000000..cb847d992fc --- /dev/null +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/elements/InlineElements.tsx @@ -0,0 +1,57 @@ +import { StyleSheet, Text, View } from '@react-pdf/renderer'; +import type * as MessageParser from '@rocket.chat/message-parser'; + +import BoldSpan from './BoldSpan'; +import ItalicSpan from './ItalicSpan'; +import LinkSpan from './LinkSpan'; +import StrikeSpan from './StrikeSpan'; +import EmojiSpan from './EmojiSpan'; +import CodeSpan from './CodeSpan'; + +type InlineElementsProps = { + children: MessageParser.Inlines[]; +}; + +const styles = StyleSheet.create({ + inline: { + flexDirection: 'row', + }, +}); + +const InlineElements = ({ children }: InlineElementsProps) => ( + + {children.map((child, index) => { + switch (child.type) { + case 'BOLD': + return ; + + case 'STRIKE': + return ; + + case 'ITALIC': + return ; + + case 'LINK': + return ; + + case 'PLAIN_TEXT': + return {child.value}; + + case 'EMOJI': + return ; + + case 'INLINE_CODE': + return ; + + case 'MENTION_USER': + case 'MENTION_CHANNEL': + return {child.value?.value}; + + default: + return null; + } + })} + +); + +export default InlineElements; diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/elements/ItalicSpan.tsx b/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/elements/ItalicSpan.tsx new file mode 100644 index 00000000000..d970cde92f4 --- /dev/null +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/elements/ItalicSpan.tsx @@ -0,0 +1,41 @@ +import { StyleSheet, Text, View } from '@react-pdf/renderer'; +import type * as MessageParser from '@rocket.chat/message-parser'; + +import BoldSpan from './BoldSpan'; +import LinkSpan from './LinkSpan'; +import StrikeSpan from './StrikeSpan'; + +const styles = StyleSheet.create({ + italic: { + fontStyle: 'italic', + }, +}); + +type ItalicSpanProps = { + children: (MessageParser.Link | MessageParser.MarkupExcluding)[]; +}; + +const ItalicSpan = ({ children }: ItalicSpanProps) => ( + + {children.map((child, index) => { + switch (child.type) { + case 'LINK': + return ; + + case 'PLAIN_TEXT': + return {child.value}; + + case 'STRIKE': + return ; + + case 'BOLD': + return ; + + default: + return null; + } + })} + +); + +export default ItalicSpan; diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/elements/LinkSpan.tsx b/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/elements/LinkSpan.tsx new file mode 100644 index 00000000000..396345c1b08 --- /dev/null +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/elements/LinkSpan.tsx @@ -0,0 +1,43 @@ +import { View, Text } from '@react-pdf/renderer'; +import type * as MessageParser from '@rocket.chat/message-parser'; +import type { ReactElement } from 'react'; +import { useMemo } from 'react'; + +import BoldSpan from './BoldSpan'; +import ItalicSpan from './ItalicSpan'; +import StrikeSpan from './StrikeSpan'; + +type LinkSpanProps = { + label: MessageParser.Markup | MessageParser.Markup[]; +}; + +const LinkSpan = ({ label }: LinkSpanProps): ReactElement => { + const children = useMemo(() => { + const labelArray = Array.isArray(label) ? label : [label]; + + const labelElements = labelArray.map((child, index) => { + switch (child.type) { + case 'PLAIN_TEXT': + return {child.value}; + + case 'STRIKE': + return ; + + case 'ITALIC': + return ; + + case 'BOLD': + return ; + + default: + return null; + } + }); + + return labelElements; + }, [label]); + + return {children}; +}; + +export default LinkSpan; diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/elements/StrikeSpan.tsx b/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/elements/StrikeSpan.tsx new file mode 100644 index 00000000000..0ce411d03dc --- /dev/null +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/elements/StrikeSpan.tsx @@ -0,0 +1,41 @@ +import { StyleSheet, View, Text } from '@react-pdf/renderer'; +import type * as MessageParser from '@rocket.chat/message-parser'; + +import ItalicSpan from './ItalicSpan'; +import LinkSpan from './LinkSpan'; +import BoldSpan from './BoldSpan'; + +const styles = StyleSheet.create({ + strike: { + textDecoration: 'line-through', + }, +}); + +type StrikeSpanProps = { + children: (MessageParser.Link | MessageParser.MarkupExcluding)[]; +}; + +const StrikeSpan = ({ children }: StrikeSpanProps) => ( + + {children.map((child, index) => { + switch (child.type) { + case 'LINK': + return ; + + case 'PLAIN_TEXT': + return {child.value}; + + case 'BOLD': + return ; + + case 'ITALIC': + return ; + + default: + return null; + } + })} + +); + +export default StrikeSpan; diff --git a/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/index.tsx b/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/index.tsx new file mode 100644 index 00000000000..55148ece1d1 --- /dev/null +++ b/ee/packages/pdf-worker/src/templates/ChatTranscript/markup/index.tsx @@ -0,0 +1,42 @@ +import type * as MessageParser from '@rocket.chat/message-parser'; +import { View } from '@react-pdf/renderer'; + +import HeadingBlock from './blocks/HeadingBlock'; +import OrderedListBlock from './blocks/OrderedListBlock'; +import ParagraphBlock from './blocks/ParagraphBlock'; +import UnorderedListBlock from './blocks/UnorderedListBlock'; +import BigEmojiBlock from './blocks/BigEmojiBlock'; +import CodeBlock from './blocks/CodeBlock'; + +type MarkupProps = { + tokens: MessageParser.Root; +}; + +export const Markup = ({ tokens }: MarkupProps) => ( + + {tokens.map((child, index) => { + switch (child.type) { + case 'PARAGRAPH': + return ; + + case 'HEADING': + return ; + + case 'UNORDERED_LIST': + return ; + + case 'ORDERED_LIST': + return ; + + case 'BIG_EMOJI': + return ; + + case 'CODE': + return ; + + default: + return null; + } + })} + +); diff --git a/ee/packages/pdf-worker/src/types/Data.ts b/ee/packages/pdf-worker/src/types/Data.ts new file mode 100644 index 00000000000..d3d2b7320b1 --- /dev/null +++ b/ee/packages/pdf-worker/src/types/Data.ts @@ -0,0 +1,5 @@ +export type Data = { + header: Record; + messages: unknown[]; + t: (key: string) => unknown; +}; diff --git a/ee/packages/pdf-worker/src/types/IStrategy.ts b/ee/packages/pdf-worker/src/types/IStrategy.ts new file mode 100644 index 00000000000..f72aee70ec8 --- /dev/null +++ b/ee/packages/pdf-worker/src/types/IStrategy.ts @@ -0,0 +1,6 @@ +import type { Data } from './Data'; + +export interface IStrategy { + renderTemplate(data: Data): Promise; + parseTemplateData(data: Record): Data; +} diff --git a/ee/packages/pdf-worker/src/types/emoji-toolkit.ts b/ee/packages/pdf-worker/src/types/emoji-toolkit.ts new file mode 100644 index 00000000000..bde67b445f4 --- /dev/null +++ b/ee/packages/pdf-worker/src/types/emoji-toolkit.ts @@ -0,0 +1,3 @@ +declare module 'emoji-toolkit' { + export function toShort(input: string): string; +} diff --git a/ee/packages/pdf-worker/tsconfig.json b/ee/packages/pdf-worker/tsconfig.json new file mode 100644 index 00000000000..64ec0d793b2 --- /dev/null +++ b/ee/packages/pdf-worker/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.base.server.json", + "compilerOptions": { + "declaration": true, + "rootDir": "./src", + "outDir": "./dist", + "jsx": "react-jsx", + }, + "include": ["./src/**/*"] +} diff --git a/packages/core-services/src/index.ts b/packages/core-services/src/index.ts index 20b6761377b..e6b4c82aa27 100644 --- a/packages/core-services/src/index.ts +++ b/packages/core-services/src/index.ts @@ -35,6 +35,11 @@ import type { IDeviceManagementService } from './types/IDeviceManagementService' import type { IPushService } from './types/IPushService'; import type { IOmnichannelService } from './types/IOmnichannelService'; import type { ITelemetryEvent, TelemetryMap, TelemetryEvents } from './types/ITelemetryEvent'; +import type { IOmnichannelTranscriptService } from './types/IOmnichannelTranscriptService'; +import type { IQueueWorkerService, HealthAggResult } from './types/IQueueWorkerService'; +import type { ITranslationService } from './types/ITranslationService'; +import type { IMessageService } from './types/IMessageService'; +import type { ISettingsService } from './types/ISettingsService'; export { asyncLocalStorage } from './lib/asyncLocalStorage'; export { MeteorError, isMeteorError } from './MeteorError'; @@ -98,6 +103,12 @@ export { ISendFileMessageParams, IUploadFileParams, IUploadService, + IOmnichannelTranscriptService, + IQueueWorkerService, + HealthAggResult, + ITranslationService, + IMessageService, + ISettingsService, }; // TODO think in a way to not have to pass the service name to proxify here as well @@ -120,6 +131,11 @@ export const SAUMonitor = proxifyWithWait('sau-monitor'); export const DeviceManagement = proxifyWithWait('device-management'); export const VideoConf = proxifyWithWait('video-conference'); export const Upload = proxifyWithWait('upload'); +export const QueueWorker = proxifyWithWait('queue-worker'); +export const OmnichannelTranscript = proxifyWithWait('omnichannel-transcript'); +export const Message = proxifyWithWait('message'); +export const Translation = proxifyWithWait('translation'); +export const Settings = proxifyWithWait('settings'); // Calls without wait. Means that the service is optional and the result may be an error // of service/method not available diff --git a/packages/core-services/src/lib/Api.ts b/packages/core-services/src/lib/Api.ts index 9d9eb3e416f..cfe8a1ea7af 100644 --- a/packages/core-services/src/lib/Api.ts +++ b/packages/core-services/src/lib/Api.ts @@ -27,13 +27,13 @@ export class Api implements IApiService { this.services.delete(instance); } - registerService(instance: IServiceClass): void { + registerService(instance: IServiceClass, serviceDependencies?: string[]): void { this.services.add(instance); instance.setApi(this); if (this.broker) { - this.broker.createService(instance); + this.broker.createService(instance, serviceDependencies); } } diff --git a/packages/core-services/src/types/IBroker.ts b/packages/core-services/src/types/IBroker.ts index 9dc0676f1dc..8739d94a570 100644 --- a/packages/core-services/src/types/IBroker.ts +++ b/packages/core-services/src/types/IBroker.ts @@ -48,7 +48,7 @@ export interface IServiceMetrics { export interface IBroker { metrics?: IServiceMetrics; destroyService(service: IServiceClass): void; - createService(service: IServiceClass): void; + createService(service: IServiceClass, serviceDependencies?: string[]): void; call(method: string, data: any): Promise; waitAndCall(method: string, data: any): Promise; broadcastToServices( diff --git a/packages/core-services/src/types/IMessageService.ts b/packages/core-services/src/types/IMessageService.ts new file mode 100644 index 00000000000..541c77dacbe --- /dev/null +++ b/packages/core-services/src/types/IMessageService.ts @@ -0,0 +1,5 @@ +import type { IMessage } from '@rocket.chat/core-typings'; + +export interface IMessageService { + sendMessage({ fromId, rid, msg }: { fromId: string; rid: string; msg: string }): Promise; +} diff --git a/packages/core-services/src/types/IOmnichannelTranscriptService.ts b/packages/core-services/src/types/IOmnichannelTranscriptService.ts new file mode 100644 index 00000000000..90cc4799bc9 --- /dev/null +++ b/packages/core-services/src/types/IOmnichannelTranscriptService.ts @@ -0,0 +1,6 @@ +import type { IUser, IRoom } from '@rocket.chat/core-typings'; + +export interface IOmnichannelTranscriptService { + requestTranscript({ details }: { details: { userId: IUser['_id']; rid: IRoom['_id'] } }): Promise; + workOnPdf({ template, details }: { template: string; details: any }): Promise; +} diff --git a/packages/core-services/src/types/IQueueWorkerService.ts b/packages/core-services/src/types/IQueueWorkerService.ts new file mode 100644 index 00000000000..b9ad075d413 --- /dev/null +++ b/packages/core-services/src/types/IQueueWorkerService.ts @@ -0,0 +1,12 @@ +export type HealthAggResult = { + total: number; + type: string; + status: 'Rejected' | 'In progress'; +}; + +type Actions = 'work' | 'workComplete'; + +export interface IQueueWorkerService { + queueWork>(queue: Actions, to: string, data: T): Promise; + queueInfo(): Promise; +} diff --git a/packages/core-services/src/types/IRoomService.ts b/packages/core-services/src/types/IRoomService.ts index 0e8d23d885a..f849713df17 100644 --- a/packages/core-services/src/types/IRoomService.ts +++ b/packages/core-services/src/types/IRoomService.ts @@ -28,4 +28,5 @@ export interface ICreateRoomParams { export interface IRoomService { addMember(uid: string, rid: string): Promise; create(uid: string, params: ICreateRoomParams): Promise; + createDirectMessage(data: { to: string; from: string }): Promise<{ rid: string }>; } diff --git a/packages/core-services/src/types/ISettingsService.ts b/packages/core-services/src/types/ISettingsService.ts new file mode 100644 index 00000000000..6de32083d08 --- /dev/null +++ b/packages/core-services/src/types/ISettingsService.ts @@ -0,0 +1,3 @@ +export interface ISettingsService { + get(settingId: string): Promise; +} diff --git a/packages/core-services/src/types/ITranslationService.ts b/packages/core-services/src/types/ITranslationService.ts new file mode 100644 index 00000000000..f4a034fe704 --- /dev/null +++ b/packages/core-services/src/types/ITranslationService.ts @@ -0,0 +1,7 @@ +import type { IUser } from '@rocket.chat/core-typings'; + +export interface ITranslationService { + translateText(text: string, targetLanguage: string): Promise; + translate(text: string, user: IUser): Promise; + translateToServerLanguage(text: string): Promise; +} diff --git a/packages/core-services/src/types/IUploadService.ts b/packages/core-services/src/types/IUploadService.ts index 641189355bb..c816612c034 100644 --- a/packages/core-services/src/types/IUploadService.ts +++ b/packages/core-services/src/types/IUploadService.ts @@ -10,18 +10,19 @@ export interface ISendFileMessageParams { roomId: string; userId: string; file: IUpload; - message?: IMessage; + message?: Partial; } export interface ISendFileLivechatMessageParams { roomId: string; visitorToken: string; file: IUpload; - message?: IMessage; + message?: Partial; } export interface IUploadService { uploadFile(params: IUploadFileParams): Promise; sendFileMessage(params: ISendFileMessageParams): Promise; sendFileLivechatMessage(params: ISendFileLivechatMessageParams): Promise; + getFileBuffer({ file }: { userId: string; file: IUpload }): Promise; } diff --git a/packages/core-typings/src/IRoom.ts b/packages/core-typings/src/IRoom.ts index 3250254a094..af80f5f8b32 100644 --- a/packages/core-typings/src/IRoom.ts +++ b/packages/core-typings/src/IRoom.ts @@ -9,10 +9,10 @@ type CallStatus = 'ringing' | 'ended' | 'declined' | 'ongoing'; export type RoomID = string; export type ChannelName = string; interface IRequestTranscript { - email: string; + email: string; // the email address to send the transcript to + subject: string; // the subject of the email requestedAt: Date; - requestedBy: IUser; - subject: string; + requestedBy: Pick; } export interface IRoom extends IRocketChatRecord { @@ -155,7 +155,10 @@ export interface IOmnichannelGenericRoom extends Omit & +export type IVoipRoomClosingInfo = Pick & Pick & { serviceTimeDuration?: number; }; +export type IOmnichannelRoomClosingInfo = Pick & { + serviceTimeDuration?: number; + chatDuration: number; +}; + export const isOmnichannelRoom = (room: IRoom): room is IOmnichannelRoom & IRoom => room.t === 'l'; export const isVoipRoom = (room: IRoom): room is IVoipRoom & IRoom => room.t === 'v'; diff --git a/packages/fuselage-ui-kit/src/surfaces/createSurfaceRenderer.tsx b/packages/fuselage-ui-kit/src/surfaces/createSurfaceRenderer.tsx index 329664e1f2d..2f6115f8ca6 100644 --- a/packages/fuselage-ui-kit/src/surfaces/createSurfaceRenderer.tsx +++ b/packages/fuselage-ui-kit/src/surfaces/createSurfaceRenderer.tsx @@ -4,6 +4,7 @@ import type { ComponentType, ReactElement } from 'react'; export const createSurfaceRenderer = < S extends UiKit.SurfaceRenderer >( + // eslint-disable-next-line @typescript-eslint/naming-convention SurfaceComponent: ComponentType, surfaceRenderer: S ) => diff --git a/packages/livechat/src/lib/room.js b/packages/livechat/src/lib/room.js index 3cfd8918234..58c2f02e73d 100644 --- a/packages/livechat/src/lib/room.js +++ b/packages/livechat/src/lib/room.js @@ -17,6 +17,8 @@ import { handleTranscript } from './transcript'; const commands = new Commands(); export const closeChat = async ({ transcriptRequested } = {}) => { + Livechat.unsubscribeAll(); + if (!transcriptRequested) { await handleTranscript(); } diff --git a/packages/model-typings/src/models/ILivechatInquiryModel.ts b/packages/model-typings/src/models/ILivechatInquiryModel.ts index 650b20c2f54..77155fb608b 100644 --- a/packages/model-typings/src/models/ILivechatInquiryModel.ts +++ b/packages/model-typings/src/models/ILivechatInquiryModel.ts @@ -1,4 +1,4 @@ -import type { FindOptions, DistinctOptions, Document, UpdateResult } from 'mongodb'; +import type { FindOptions, DistinctOptions, Document, UpdateResult, DeleteResult } from 'mongodb'; import type { IMessage, ILivechatInquiryRecord, LivechatInquiryStatus } from '@rocket.chat/core-typings'; import type { IBaseModel } from './IBaseModel'; @@ -15,4 +15,5 @@ export interface ILivechatInquiryModel extends IBaseModel; unlock(inquiryId: string): Promise; unlockAll(): Promise; + removeByRoomId(rid: string): Promise; } diff --git a/packages/model-typings/src/models/ILivechatRoomsModel.ts b/packages/model-typings/src/models/ILivechatRoomsModel.ts index 4e7f36b3004..9b14789d907 100644 --- a/packages/model-typings/src/models/ILivechatRoomsModel.ts +++ b/packages/model-typings/src/models/ILivechatRoomsModel.ts @@ -1,4 +1,4 @@ -import type { IOmnichannelRoom } from '@rocket.chat/core-typings'; +import type { IOmnichannelRoom, IOmnichannelRoomClosingInfo } from '@rocket.chat/core-typings'; import type { FindCursor, UpdateResult, AggregationCursor, Document } from 'mongodb'; import type { FindPaginated } from '..'; @@ -110,5 +110,17 @@ export interface ILivechatRoomsModel extends IBaseModel { findAvailableSources(): AggregationCursor; + setTranscriptRequestedPdfById(rid: string): Promise; + unsetTranscriptRequestedPdfById(rid: string): Promise; + setPdfTranscriptFileIdById(rid: string, fileId: string): Promise; + + setEmailTranscriptRequestedByRoomId( + rid: string, + transcriptInfo: NonNullable, + ): Promise; + unsetEmailTranscriptRequestedByRoomId(rid: string): Promise; + + closeRoomById(roomId: string, closeInfo: IOmnichannelRoomClosingInfo): Promise; + bulkRemoveDepartmentAndUnitsFromRooms(departmentId: string): Promise; } diff --git a/packages/model-typings/src/models/IMessagesModel.ts b/packages/model-typings/src/models/IMessagesModel.ts index d80f3757a3f..fbcdfcc4655 100644 --- a/packages/model-typings/src/models/IMessagesModel.ts +++ b/packages/model-typings/src/models/IMessagesModel.ts @@ -1,5 +1,5 @@ import type { IMessage, IRoom, IUser, ILivechatDepartment } from '@rocket.chat/core-typings'; -import type { AggregationCursor, CountDocumentsOptions, FindCursor, FindOptions, AggregateOptions } from 'mongodb'; +import type { AggregationCursor, CountDocumentsOptions, FindCursor, FindOptions, AggregateOptions, DeleteResult } from 'mongodb'; import type { FindPaginated, IBaseModel } from './IBaseModel'; @@ -33,7 +33,8 @@ export interface IMessagesModel extends IBaseModel { getTotalOfMessagesSentByDate(params: { start: Date; end: Date; options?: any }): Promise; findLivechatClosedMessages(rid: IRoom['_id'], searchTerm?: string, options?: FindOptions): FindPaginated>; - + findLivechatMessages(rid: IRoom['_id'], options?: FindOptions): FindCursor; + findLivechatMessagesWithoutClosing(rid: IRoom['_id'], options?: FindOptions): FindCursor; countRoomsWithStarredMessages(options: AggregateOptions): Promise; countRoomsWithPinnedMessages(options: AggregateOptions): Promise; @@ -61,4 +62,6 @@ export interface IMessagesModel extends IBaseModel { findOneByFederationId(federationEventId: string): Promise; setFederationEventIdById(_id: string, federationEventId: string): Promise; + + removeByRoomId(roomId: IRoom['_id']): Promise; } diff --git a/packages/model-typings/src/models/IVoipRoomModel.ts b/packages/model-typings/src/models/IVoipRoomModel.ts index a8f34a102a2..b45311c4d9b 100644 --- a/packages/model-typings/src/models/IVoipRoomModel.ts +++ b/packages/model-typings/src/models/IVoipRoomModel.ts @@ -1,5 +1,5 @@ import type { FindOptions, UpdateResult, Document, FindCursor } from 'mongodb'; -import type { IVoipRoom, IRoomClosingInfo } from '@rocket.chat/core-typings'; +import type { IVoipRoom, IVoipRoomClosingInfo } from '@rocket.chat/core-typings'; import type { FindPaginated, IBaseModel } from './IBaseModel'; @@ -13,7 +13,7 @@ export interface IVoipRoomModel extends IBaseModel { findOneByVisitorToken(visitorToken: string, options?: FindOptions): Promise; findOneByIdAndVisitorToken(_id: IVoipRoom['_id'], visitorToken: string, options?: FindOptions): Promise; - closeByRoomId(roomId: IVoipRoom['_id'], closeInfo: IRoomClosingInfo): Promise; + closeByRoomId(roomId: IVoipRoom['_id'], closeInfo: IVoipRoomClosingInfo): Promise; findRoomsWithCriteria({ agents, diff --git a/packages/rest-typings/src/v1/omnichannel.ts b/packages/rest-typings/src/v1/omnichannel.ts index db29a915bc9..2f589b90638 100644 --- a/packages/rest-typings/src/v1/omnichannel.ts +++ b/packages/rest-typings/src/v1/omnichannel.ts @@ -2021,6 +2021,73 @@ const POSTLivechatRoomCloseParamsSchema = { export const isPOSTLivechatRoomCloseParams = ajv.compile(POSTLivechatRoomCloseParamsSchema); +type POSTLivechatRoomCloseByUserParams = { + rid: string; + comment?: string; + tags?: string[]; + generateTranscriptPdf?: boolean; + transcriptEmail?: + | { + // Note: if sendToVisitor is false, then any previously requested transcripts (like via livechat:requestTranscript) will be also cancelled + sendToVisitor: false; + } + | { + sendToVisitor: true; + requestData: Pick, 'email' | 'subject'>; + }; +}; + +const POSTLivechatRoomCloseByUserParamsSchema = { + type: 'object', + properties: { + rid: { + type: 'string', + }, + comment: { + type: 'string', + nullable: true, + }, + tags: { + type: 'array', + items: { + type: 'string', + }, + nullable: true, + }, + generateTranscriptPdf: { + type: 'boolean', + nullable: true, + }, + transcriptEmail: { + type: 'object', + properties: { + sendToVisitor: { + type: 'boolean', + }, + requestData: { + type: 'object', + properties: { + email: { + type: 'string', + }, + subject: { + type: 'string', + }, + }, + required: ['email', 'subject'], + additionalProperties: false, + }, + }, + required: ['sendToVisitor'], + additionalProperties: false, + }, + }, + required: ['rid'], + additionalProperties: false, +}; + +export const isPOSTLivechatRoomCloseByUserParams = ajv.compile(POSTLivechatRoomCloseByUserParamsSchema); + type POSTLivechatRoomTransferParams = { token: string; rid: string; @@ -2983,6 +3050,9 @@ export type OmnichannelEndpoints = { '/v1/livechat/room.close': { POST: (params: POSTLivechatRoomCloseParams) => { rid: string; comment: string }; }; + '/v1/livechat/room.closeByUser': { + POST: (params: POSTLivechatRoomCloseByUserParams) => void; + }; '/v1/livechat/room.transfer': { POST: (params: POSTLivechatRoomTransferParams) => Deprecated<{ room: IOmnichannelRoom }>; }; @@ -3197,4 +3267,7 @@ export type OmnichannelEndpoints = { }[]; }>; }; + '/v1/omnichannel/:rid/request-transcript': { + POST: () => void; + }; }; diff --git a/packages/rest-typings/src/v1/users/UsersSetPreferenceParamsPOST.ts b/packages/rest-typings/src/v1/users/UsersSetPreferenceParamsPOST.ts index a53ade45b89..d3082d5c678 100644 --- a/packages/rest-typings/src/v1/users/UsersSetPreferenceParamsPOST.ts +++ b/packages/rest-typings/src/v1/users/UsersSetPreferenceParamsPOST.ts @@ -40,6 +40,8 @@ export type UsersSetPreferencesParamsPOST = { dontAskAgainList?: Array<{ action: string; label: string }>; receiveLoginDetectionEmail?: boolean; idleTimeLimit?: number; + omnichannelTranscriptEmail?: boolean; + omnichannelTranscriptPDF?: boolean; }; }; @@ -196,6 +198,14 @@ const UsersSetPreferencesParamsPostSchema = { type: 'number', nullable: true, }, + omnichannelTranscriptEmail: { + type: 'boolean', + nullable: true, + }, + omnichannelTranscriptPDF: { + type: 'boolean', + nullable: true, + }, }, required: [], additionalProperties: false, diff --git a/packages/rest-typings/src/v1/voip.ts b/packages/rest-typings/src/v1/voip.ts index 2e95d15b2d5..5f10b82633c 100644 --- a/packages/rest-typings/src/v1/voip.ts +++ b/packages/rest-typings/src/v1/voip.ts @@ -23,7 +23,7 @@ const ajv = new Ajv({ /** *************************************************/ type CustomSoundsList = PaginatedRequest<{ query: string }>; -const CustomSoundsListSchema: JSONSchemaType = { +const CustomSoundsListSchema = { type: 'object', properties: { count: { diff --git a/packages/tools/.eslintrc.json b/packages/tools/.eslintrc.json new file mode 100644 index 00000000000..a83aeda48e6 --- /dev/null +++ b/packages/tools/.eslintrc.json @@ -0,0 +1,4 @@ +{ + "extends": ["@rocket.chat/eslint-config"], + "ignorePatterns": ["**/dist"] +} diff --git a/packages/tools/package.json b/packages/tools/package.json new file mode 100644 index 00000000000..124268b1997 --- /dev/null +++ b/packages/tools/package.json @@ -0,0 +1,27 @@ +{ + "name": "@rocket.chat/tools", + "version": "0.0.1", + "private": true, + "devDependencies": { + "@types/jest": "^27.4.1", + "eslint": "^8.12.0", + "jest": "^27.5.1", + "ts-jest": "^27.1.4", + "typescript": "~4.5.5" + }, + "scripts": { + "lint": "eslint --ext .js,.jsx,.ts,.tsx .", + "lint:fix": "eslint --ext .js,.jsx,.ts,.tsx . --fix", + "jest": "jest", + "build": "rm -rf dist && tsc -p tsconfig.json", + "dev": "tsc -p tsconfig.json --watch --preserveWatchOutput" + }, + "main": "./dist/index.js", + "typings": "./dist/index.d.ts", + "files": [ + "/dist" + ], + "dependencies": { + "moment-timezone": "^0.5.40" + } +} diff --git a/packages/tools/src/index.ts b/packages/tools/src/index.ts new file mode 100644 index 00000000000..f3107465cd9 --- /dev/null +++ b/packages/tools/src/index.ts @@ -0,0 +1 @@ +export * from './timezone'; diff --git a/packages/tools/src/timezone.ts b/packages/tools/src/timezone.ts new file mode 100644 index 00000000000..2201dc63914 --- /dev/null +++ b/packages/tools/src/timezone.ts @@ -0,0 +1,14 @@ +import moment from 'moment-timezone'; + +const padOffset = (offset: string | number): string => { + const numberOffset = Number(offset); + const absOffset = Math.abs(numberOffset); + const isNegative = !(numberOffset === absOffset); + + return `${isNegative ? '-' : '+'}${absOffset < 10 ? `0${absOffset}` : absOffset}:00`; +}; + +export const guessTimezoneFromOffset = (offset: string | number): string => + moment.tz.names().find((tz) => padOffset(offset) === moment.tz(tz).format('Z').toString()) || moment.tz.guess(); + +export const guessTimezone = (): string => moment.tz.guess(); diff --git a/packages/tools/tsconfig.json b/packages/tools/tsconfig.json new file mode 100644 index 00000000000..52e9dd8c497 --- /dev/null +++ b/packages/tools/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.server.json", + "compilerOptions": { + "declaration": true, + "rootDir": "./src", + "outDir": "./dist" + }, + "include": ["./src/**/*"] +} diff --git a/packages/ui-client/src/components/Header/ToolBox/ToolBoxAction.tsx b/packages/ui-client/src/components/Header/ToolBox/ToolBoxAction.tsx index 26a10a389ca..8bf83cb9a9a 100644 --- a/packages/ui-client/src/components/Header/ToolBox/ToolBoxAction.tsx +++ b/packages/ui-client/src/components/Header/ToolBox/ToolBoxAction.tsx @@ -1,20 +1,27 @@ import { IconButton } from '@rocket.chat/fuselage'; import type { FC } from 'react'; +import { forwardRef } from 'react'; -const ToolBoxAction: FC = ({ id, icon, color, action, className, index, title, 'data-tooltip': tooltip, ...props }) => ( - action(id)} - data-toolbox={index} - key={id} - icon={icon} - position='relative' - tiny - overflow='visible' - color={!!color && color} - {...{ ...props, ...(tooltip ? { 'data-tooltip': tooltip, 'title': '' } : { title }) }} - /> -); +const ToolBoxAction: FC = forwardRef(function ToolBoxAction( + { id, icon, color, action, className, index, title, 'data-tooltip': tooltip, ...props }, + ref, +) { + return ( + action(id)} + data-toolbox={index} + key={id} + icon={icon} + position='relative' + tiny + overflow='visible' + ref={ref} + color={!!color && color} + {...{ ...props, ...(tooltip ? { 'data-tooltip': tooltip, 'title': '' } : { title }) }} + /> + ); +}); export default ToolBoxAction; diff --git a/yarn.lock b/yarn.lock index 6a88d39dd9e..9903d3171a0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -949,7 +949,30 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.1.0, @babel/core@npm:^7.11.6, @babel/core@npm:^7.12.10, @babel/core@npm:^7.12.3, @babel/core@npm:^7.20.5, @babel/core@npm:^7.7.2, @babel/core@npm:^7.7.5, @babel/core@npm:^7.8.0, @babel/core@npm:~7.20.5": +"@babel/core@npm:^7.1.0, @babel/core@npm:^7.11.6, @babel/core@npm:^7.12.10, @babel/core@npm:^7.12.3, @babel/core@npm:^7.7.2, @babel/core@npm:^7.7.5, @babel/core@npm:^7.8.0": + version: 7.20.2 + resolution: "@babel/core@npm:7.20.2" + dependencies: + "@ampproject/remapping": ^2.1.0 + "@babel/code-frame": ^7.18.6 + "@babel/generator": ^7.20.2 + "@babel/helper-compilation-targets": ^7.20.0 + "@babel/helper-module-transforms": ^7.20.2 + "@babel/helpers": ^7.20.1 + "@babel/parser": ^7.20.2 + "@babel/template": ^7.18.10 + "@babel/traverse": ^7.20.1 + "@babel/types": ^7.20.2 + convert-source-map: ^1.7.0 + debug: ^4.1.0 + gensync: ^1.0.0-beta.2 + json5: ^2.2.1 + semver: ^6.3.0 + checksum: 98faaaef26103a276a30a141b951a93bc8418d100d1f668bf7a69d12f3e25df57958e8b6b9100d95663f720db62da85ade736f6629a5ebb1e640251a1b43c0e4 + languageName: node + linkType: hard + +"@babel/core@npm:^7.20.5, @babel/core@npm:~7.20.5": version: 7.20.5 resolution: "@babel/core@npm:7.20.5" dependencies: @@ -986,7 +1009,18 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.12.11, @babel/generator@npm:^7.12.5, @babel/generator@npm:^7.20.5, @babel/generator@npm:^7.7.2": +"@babel/generator@npm:^7.12.11, @babel/generator@npm:^7.12.5, @babel/generator@npm:^7.20.1, @babel/generator@npm:^7.20.2, @babel/generator@npm:^7.7.2": + version: 7.20.4 + resolution: "@babel/generator@npm:7.20.4" + dependencies: + "@babel/types": ^7.20.2 + "@jridgewell/gen-mapping": ^0.3.2 + jsesc: ^2.5.1 + checksum: 967b59f18e5ce999e5a741825bcecb2be4bbfc1824a92c21b47d0b5694e0eb09314a70f8b9142e9591c149c7fb83d51f73ae8fbd96d30a42666425889e51ceb1 + languageName: node + linkType: hard + +"@babel/generator@npm:^7.20.5": version: 7.20.5 resolution: "@babel/generator@npm:7.20.5" dependencies: @@ -1272,7 +1306,18 @@ __metadata: languageName: node linkType: hard -"@babel/helpers@npm:^7.12.5, @babel/helpers@npm:^7.20.5": +"@babel/helpers@npm:^7.12.5, @babel/helpers@npm:^7.20.1": + version: 7.20.1 + resolution: "@babel/helpers@npm:7.20.1" + dependencies: + "@babel/template": ^7.18.10 + "@babel/traverse": ^7.20.1 + "@babel/types": ^7.20.0 + checksum: be35f78666bdab895775ed94dbeb098f7b4fa08ce4cfb0c3a9e69b7220cce56960dcdc2b14f5df9d3b80388d4bf7df155c97f6cf6768c0138f4e6931d0f44955 + languageName: node + linkType: hard + +"@babel/helpers@npm:^7.20.5": version: 7.20.6 resolution: "@babel/helpers@npm:7.20.6" dependencies: @@ -1294,7 +1339,16 @@ __metadata: languageName: node linkType: hard -"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.12.11, @babel/parser@npm:^7.12.7, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.18.10, @babel/parser@npm:^7.20.5": +"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.12.11, @babel/parser@npm:^7.12.7, @babel/parser@npm:^7.14.7, @babel/parser@npm:^7.18.10, @babel/parser@npm:^7.20.1, @babel/parser@npm:^7.20.2": + version: 7.20.3 + resolution: "@babel/parser@npm:7.20.3" + bin: + parser: ./bin/babel-parser.js + checksum: 33bcdb45de65a3cf27ed376cb34f32be3c3485a10e3252f8d0126f6a034efc3145c0d219e57fcd5a8956361552008bc30b9bae4a723823fb3633027071be8a45 + languageName: node + linkType: hard + +"@babel/parser@npm:^7.20.5": version: 7.20.5 resolution: "@babel/parser@npm:7.20.5" bin: @@ -2427,7 +2481,25 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.14.5, @babel/runtime@npm:^7.17.2, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.20.6, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.0, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.2, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": +"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.0, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.14.5, @babel/runtime@npm:^7.17.2, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.0, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.2, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": + version: 7.19.0 + resolution: "@babel/runtime@npm:7.19.0" + dependencies: + regenerator-runtime: ^0.13.4 + checksum: fa69c351bb05e1db3ceb9a02fdcf620c234180af68cdda02152d3561015f6d55277265d3109815992f96d910f3db709458cae4f8df1c3def66f32e0867d82294 + languageName: node + linkType: hard + +"@babel/runtime@npm:^7.16.4": + version: 7.20.7 + resolution: "@babel/runtime@npm:7.20.7" + dependencies: + regenerator-runtime: ^0.13.11 + checksum: 4629ce5c46f06cca9cfb9b7fc00d48003335a809888e2b91ec2069a2dcfbfef738480cff32ba81e0b7c290f8918e5c22ddcf2b710001464ee84ba62c7e32a3a3 + languageName: node + linkType: hard + +"@babel/runtime@npm:^7.20.6": version: 7.20.6 resolution: "@babel/runtime@npm:7.20.6" dependencies: @@ -2456,7 +2528,25 @@ __metadata: languageName: node linkType: hard -"@babel/traverse@npm:^7.1.6, @babel/traverse@npm:^7.12.11, @babel/traverse@npm:^7.12.9, @babel/traverse@npm:^7.13.0, @babel/traverse@npm:^7.19.0, @babel/traverse@npm:^7.19.1, @babel/traverse@npm:^7.20.1, @babel/traverse@npm:^7.20.5, @babel/traverse@npm:^7.7.2": +"@babel/traverse@npm:^7.1.6, @babel/traverse@npm:^7.12.11, @babel/traverse@npm:^7.12.9, @babel/traverse@npm:^7.13.0, @babel/traverse@npm:^7.19.0, @babel/traverse@npm:^7.19.1, @babel/traverse@npm:^7.20.1, @babel/traverse@npm:^7.7.2": + version: 7.20.1 + resolution: "@babel/traverse@npm:7.20.1" + dependencies: + "@babel/code-frame": ^7.18.6 + "@babel/generator": ^7.20.1 + "@babel/helper-environment-visitor": ^7.18.9 + "@babel/helper-function-name": ^7.19.0 + "@babel/helper-hoist-variables": ^7.18.6 + "@babel/helper-split-export-declaration": ^7.18.6 + "@babel/parser": ^7.20.1 + "@babel/types": ^7.20.0 + debug: ^4.1.0 + globals: ^11.1.0 + checksum: 6696176d574b7ff93466848010bc7e94b250169379ec2a84f1b10da46a7cc2018ea5e3a520c3078487db51e3a4afab9ecff48f25d1dbad8c1319362f4148fb4b + languageName: node + linkType: hard + +"@babel/traverse@npm:^7.20.5": version: 7.20.5 resolution: "@babel/traverse@npm:7.20.5" dependencies: @@ -2474,7 +2564,18 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.12.11, @babel/types@npm:^7.12.7, @babel/types@npm:^7.18.10, @babel/types@npm:^7.18.6, @babel/types@npm:^7.18.9, @babel/types@npm:^7.19.0, @babel/types@npm:^7.2.0, @babel/types@npm:^7.20.2, @babel/types@npm:^7.20.5, @babel/types@npm:^7.3.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.12.11, @babel/types@npm:^7.12.7, @babel/types@npm:^7.18.10, @babel/types@npm:^7.18.6, @babel/types@npm:^7.18.9, @babel/types@npm:^7.19.0, @babel/types@npm:^7.2.0, @babel/types@npm:^7.20.0, @babel/types@npm:^7.20.2, @babel/types@npm:^7.3.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": + version: 7.20.2 + resolution: "@babel/types@npm:7.20.2" + dependencies: + "@babel/helper-string-parser": ^7.19.4 + "@babel/helper-validator-identifier": ^7.19.1 + to-fast-properties: ^2.0.0 + checksum: 57e76e5f21876135f481bfd4010c87f2d38196bb0a2bc60a28d6e55e3afa90cdd9accf164e4cb71bdfb620517fa0a0cb5600cdce36c21d59fdaccfbb899c024c + languageName: node + linkType: hard + +"@babel/types@npm:^7.20.5": version: 7.20.5 resolution: "@babel/types@npm:7.20.5" dependencies: @@ -2852,6 +2953,23 @@ __metadata: languageName: node linkType: hard +"@eslint/eslintrc@npm:^1.4.1": + version: 1.4.1 + resolution: "@eslint/eslintrc@npm:1.4.1" + dependencies: + ajv: ^6.12.4 + debug: ^4.3.2 + espree: ^9.4.0 + globals: ^13.19.0 + ignore: ^5.2.0 + import-fresh: ^3.2.1 + js-yaml: ^4.1.0 + minimatch: ^3.1.2 + strip-json-comments: ^3.1.1 + checksum: cd3e5a8683db604739938b1c1c8b77927dc04fce3e28e0c88e7f2cd4900b89466baf83dfbad76b2b9e4d2746abdd00dd3f9da544d3e311633d8693f327d04cd7 + languageName: node + linkType: hard + "@faker-js/faker@npm:^6.3.1": version: 6.3.1 resolution: "@faker-js/faker@npm:6.3.1" @@ -2981,6 +3099,17 @@ __metadata: languageName: node linkType: hard +"@humanwhocodes/config-array@npm:^0.11.8": + version: 0.11.8 + resolution: "@humanwhocodes/config-array@npm:0.11.8" + dependencies: + "@humanwhocodes/object-schema": ^1.2.1 + debug: ^4.1.1 + minimatch: ^3.0.5 + checksum: 0fd6b3c54f1674ce0a224df09b9c2f9846d20b9e54fabae1281ecfc04f2e6ad69bf19e1d6af6a28f88e8aa3990168b6cb9e1ef755868c3256a630605ec2cb1d3 + languageName: node + linkType: hard + "@humanwhocodes/config-array@npm:^0.5.0": version: 0.5.0 resolution: "@humanwhocodes/config-array@npm:0.5.0" @@ -5206,6 +5335,177 @@ __metadata: languageName: node linkType: hard +"@react-pdf/fns@npm:2.0.0": + version: 2.0.0 + resolution: "@react-pdf/fns@npm:2.0.0" + checksum: b6366d5313c361e9237caafdd54cf68a8b82515e4833c8e6e65626816989e494096611da0614a740dffb606e9d7025ee04bcaa97c6549abc36d485aa0d6cd243 + languageName: node + linkType: hard + +"@react-pdf/font@npm:^2.3.1": + version: 2.3.1 + resolution: "@react-pdf/font@npm:2.3.1" + dependencies: + "@babel/runtime": ^7.16.4 + "@react-pdf/types": ^2.0.9 + cross-fetch: ^3.1.5 + fontkit: ^2.0.2 + is-url: ^1.2.4 + checksum: f3b9db3cb09f038941bc926b5167020f3a3981ebac9ebf0c49a5fd4cefacd0402827f43959b206efd13e758827dcc219d38781216e7f9dd991ee0b7808951699 + languageName: node + linkType: hard + +"@react-pdf/image@npm:^2.2.0": + version: 2.2.0 + resolution: "@react-pdf/image@npm:2.2.0" + dependencies: + "@babel/runtime": ^7.16.4 + "@react-pdf/png-js": ^2.2.0 + cross-fetch: ^3.1.5 + checksum: 071bac3fb34cb4ce8a41e58ae14afd6427885ae327840b5666c2ad6c293a1a4524bcb9bf5c72dfdee44de432cc31928a7a9cc67d6da77ffb5b8c6aa3121d5953 + languageName: node + linkType: hard + +"@react-pdf/layout@npm:^3.3.0": + version: 3.3.0 + resolution: "@react-pdf/layout@npm:3.3.0" + dependencies: + "@babel/runtime": ^7.16.4 + "@react-pdf/fns": 2.0.0 + "@react-pdf/image": ^2.2.0 + "@react-pdf/pdfkit": ^3.0.0 + "@react-pdf/primitives": ^3.0.0 + "@react-pdf/stylesheet": ^4.1.0 + "@react-pdf/textkit": ^4.1.0 + "@react-pdf/types": ^2.2.0 + "@react-pdf/yoga": ^4.0.0 + cross-fetch: ^3.1.5 + emoji-regex: ^8.0.0 + queue: ^6.0.1 + checksum: 0ecadf50b0ebdbcbe74b48bcfdbbe0fa4528bb42b0a47f36491a330d45860c8a64e67eb6fff81b18985855da29215928f8b3a36dcdc347cd93b60252ae11c516 + languageName: node + linkType: hard + +"@react-pdf/pdfkit@npm:^3.0.0, @react-pdf/pdfkit@npm:^3.0.1": + version: 3.0.1 + resolution: "@react-pdf/pdfkit@npm:3.0.1" + dependencies: + "@babel/runtime": ^7.16.4 + "@react-pdf/png-js": ^2.2.0 + browserify-zlib: ^0.2.0 + crypto-js: ^4.0.0 + fontkit: ^2.0.2 + vite-compatible-readable-stream: ^3.6.1 + checksum: d2c5a157ada00a5936b82fb559cd65edf141426edf0a5688a62f1ba3685f0800c8b35564cce461a9eacbb86d801e1966548f094d4075d24139a4f9b09f2d7b1b + languageName: node + linkType: hard + +"@react-pdf/png-js@npm:^2.2.0": + version: 2.2.0 + resolution: "@react-pdf/png-js@npm:2.2.0" + dependencies: + browserify-zlib: ^0.2.0 + checksum: 0f1571f43dcffa93c219ede411210c83f20c81a01f0a0cb39e713c9f066fa4b7b9781602240676e937e89c0218f2940f9acb98f06165e30dc7012897d1ff8d07 + languageName: node + linkType: hard + +"@react-pdf/primitives@npm:^3.0.0": + version: 3.0.1 + resolution: "@react-pdf/primitives@npm:3.0.1" + checksum: c4bea5c6c61a2bbfdc462385def8ec9a36b60320b2008643f694de4e587dd05058ff78c98aa8a5d66bd472bbe09c93a477b15fefd64db58f6e64c122adabbdf1 + languageName: node + linkType: hard + +"@react-pdf/render@npm:^3.2.1": + version: 3.2.1 + resolution: "@react-pdf/render@npm:3.2.1" + dependencies: + "@babel/runtime": ^7.16.4 + "@react-pdf/fns": 2.0.0 + "@react-pdf/primitives": ^3.0.0 + "@react-pdf/textkit": ^4.1.0 + "@react-pdf/types": ^2.1.0 + abs-svg-path: ^0.1.1 + color-string: ^1.5.3 + normalize-svg-path: ^1.1.0 + parse-svg-path: ^0.1.2 + svg-arc-to-cubic-bezier: ^3.2.0 + checksum: ac076ceca86f436b7dff66af2f852f9d400fe39d451a09c990eb9d9705547403d0e915bc3cc9bc0dfe813c0afdd5908f73724288bdc05e0ec70c1c9707dd79b4 + languageName: node + linkType: hard + +"@react-pdf/renderer@npm:^3.1.3": + version: 3.1.3 + resolution: "@react-pdf/renderer@npm:3.1.3" + dependencies: + "@babel/runtime": ^7.16.4 + "@react-pdf/font": ^2.3.1 + "@react-pdf/layout": ^3.3.0 + "@react-pdf/pdfkit": ^3.0.1 + "@react-pdf/primitives": ^3.0.0 + "@react-pdf/render": ^3.2.1 + "@react-pdf/types": ^2.2.0 + loose-envify: ^1.1.0 + object-assign: ^4.1.1 + prop-types: ^15.6.2 + queue: ^6.0.1 + scheduler: ^0.17.0 + peerDependencies: + react: ^16.8.6 || ^17.0.0 || ^18.0.0 + checksum: e10aafe3760a53a24c032fcd499c532797886031ccab3851bc48b179cb456197b497fc8008dee736c164f352c7948ed1f17be738257af2332ab154214aa446b9 + languageName: node + linkType: hard + +"@react-pdf/stylesheet@npm:^4.1.0": + version: 4.1.0 + resolution: "@react-pdf/stylesheet@npm:4.1.0" + dependencies: + "@babel/runtime": ^7.16.4 + "@react-pdf/fns": 2.0.0 + "@react-pdf/types": ^2.2.0 + color-string: ^1.5.3 + hsl-to-hex: ^1.0.0 + media-engine: ^1.0.3 + postcss-value-parser: ^4.1.0 + checksum: 9a58e56430752aecf80ca1fb2ccd3525afaad288ace2117981abdb367b42a79fe130f1c7cb2e765fa7d47df072d82e029849ee9b989475b4d4430d21fa9da7a4 + languageName: node + linkType: hard + +"@react-pdf/textkit@npm:^4.1.0": + version: 4.1.0 + resolution: "@react-pdf/textkit@npm:4.1.0" + dependencies: + "@babel/runtime": ^7.16.4 + "@react-pdf/fns": 2.0.0 + hyphen: ^1.6.4 + unicode-properties: ^1.4.1 + checksum: f4eb696fb6028820f17fb186be40159e47951bab30762d12b592fbcdae40955b22a7aa442928c01d04d9f8c72fed68ccfea54b113bbd74e5441c4731c2d34e5f + languageName: node + linkType: hard + +"@react-pdf/types@npm:^2.0.9, @react-pdf/types@npm:^2.1.0": + version: 2.1.1 + resolution: "@react-pdf/types@npm:2.1.1" + checksum: 63c52f7d552f0c373575fc8322a56cdc4f370872ce51da63ca9436fa2ee99cefe1f8d536ca062e70fdfb03114fef728cfb176404f7c7e06ad6e1a34b11671079 + languageName: node + linkType: hard + +"@react-pdf/types@npm:^2.2.0": + version: 2.2.0 + resolution: "@react-pdf/types@npm:2.2.0" + checksum: c0d5dcac934e3c6f7edbaac995445223cd083bcb6985269322114b86d735918fa1e20f7e022dc44418ed29565f45dc4e84d0c56d8db9d37058070e08c454bf72 + languageName: node + linkType: hard + +"@react-pdf/yoga@npm:^4.0.0": + version: 4.0.1 + resolution: "@react-pdf/yoga@npm:4.0.1" + dependencies: + "@babel/runtime": ^7.16.4 + checksum: 74180c52dfa1b2748bebd0d6c4e285fa47e05a700b19ab1aabd91adca5b08e45db61316d7a9fe818f7c8e6e2528eedeaaffd27b8d0576ecf932db8566e518443 + languageName: node + linkType: hard + "@react-spectrum/dnd@npm:3.0.0-alpha.6": version: 3.0.0-alpha.6 resolution: "@react-spectrum/dnd@npm:3.0.0-alpha.6" @@ -6230,7 +6530,7 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/apps-engine@npm:1.36.0, @rocket.chat/apps-engine@npm:^1.32.0": +"@rocket.chat/apps-engine@npm:1.36.0": version: 1.36.0 resolution: "@rocket.chat/apps-engine@npm:1.36.0" dependencies: @@ -6245,6 +6545,21 @@ __metadata: languageName: node linkType: hard +"@rocket.chat/apps-engine@npm:^1.32.0": + version: 1.35.0 + resolution: "@rocket.chat/apps-engine@npm:1.35.0" + dependencies: + adm-zip: ^0.5.9 + cryptiles: ^4.1.3 + lodash.clonedeep: ^4.5.0 + semver: ^5.7.1 + stack-trace: 0.0.10 + uuid: ^3.4.0 + vm2: ^3.9.11 + checksum: 36c1aeeca2163a0bcde0de8f28eceedccb2cb0a55f014487c4924b72ba00e0969d9a28797a70aa40f8a6e3856b541608a0af251292b077e173c9e4fd9d0010f1 + languageName: node + linkType: hard + "@rocket.chat/apps-engine@npm:~1.30.0": version: 1.30.0 resolution: "@rocket.chat/apps-engine@npm:1.30.0" @@ -6339,7 +6654,20 @@ __metadata: languageName: unknown linkType: soft -"@rocket.chat/css-in-js@npm:next, @rocket.chat/css-in-js@npm:~0.31.23-dev.50": +"@rocket.chat/css-in-js@npm:next, @rocket.chat/css-in-js@npm:~0.31.23-dev.9": + version: 0.31.23-dev.9 + resolution: "@rocket.chat/css-in-js@npm:0.31.23-dev.9" + dependencies: + "@emotion/hash": ^0.9.0 + "@rocket.chat/css-supports": ~0.31.23-dev.9 + "@rocket.chat/memo": ~0.31.23-dev.9 + "@rocket.chat/stylis-logical-props-middleware": ~0.31.23-dev.9 + stylis: ~4.1.3 + checksum: 83249a59344ba14f10cefeca6d956bd8c4770450db38ad12682680244d5da5f602431439856b6c407b6448f8b8d26f2e08135ccc63c9c7d1f34d446e640f39af + languageName: node + linkType: hard + +"@rocket.chat/css-in-js@npm:~0.31.23-dev.50": version: 0.31.23-dev.50 resolution: "@rocket.chat/css-in-js@npm:0.31.23-dev.50" dependencies: @@ -6361,6 +6689,15 @@ __metadata: languageName: node linkType: hard +"@rocket.chat/css-supports@npm:~0.31.23-dev.9": + version: 0.31.23-dev.9 + resolution: "@rocket.chat/css-supports@npm:0.31.23-dev.9" + dependencies: + "@rocket.chat/memo": ~0.31.23-dev.9 + checksum: 7fd894043af62f57c1baf4afcc4a13945ed5c5db273a0375eb8b441e8f0cdf1b07c217f17ba730030c0ffa12d5c6e3bab8d1f316a6d13ee8b0b9563a83081db3 + languageName: node + linkType: hard + "@rocket.chat/ddp-streamer@workspace:ee/apps/ddp-streamer": version: 0.0.0-use.local resolution: "@rocket.chat/ddp-streamer@workspace:ee/apps/ddp-streamer" @@ -6404,6 +6741,13 @@ __metadata: languageName: unknown linkType: soft +"@rocket.chat/emitter@npm:0.31.22": + version: 0.31.22 + resolution: "@rocket.chat/emitter@npm:0.31.22" + checksum: 0c14b4fffb3a89189b4f03a43be49c77f37889738fc31a0495c0591c8df232e456c2657365bed06d1396b97540c6a80a185009b9d3124c1f33c8dfac2a44d9d3 + languageName: node + linkType: hard + "@rocket.chat/emitter@npm:next": version: 0.31.23-dev.50 resolution: "@rocket.chat/emitter@npm:0.31.23-dev.50" @@ -6814,7 +7158,14 @@ __metadata: languageName: node linkType: hard -"@rocket.chat/memo@npm:next, @rocket.chat/memo@npm:~0.31.23-dev.50": +"@rocket.chat/memo@npm:next, @rocket.chat/memo@npm:~0.31.23-dev.9": + version: 0.31.23-dev.9 + resolution: "@rocket.chat/memo@npm:0.31.23-dev.9" + checksum: 4fc8f0ab10838f52890acb6fc43165fc2297dd381892181c2368886b4c12b1c5cc96db8ea11eb66c97460ba37d218975ae59b0bca763e11448bd04b8aa3c9e22 + languageName: node + linkType: hard + +"@rocket.chat/memo@npm:~0.31.23-dev.50": version: 0.31.23-dev.50 resolution: "@rocket.chat/memo@npm:0.31.23-dev.50" checksum: 1da7eca7c167c0a4784ea4a701c962109f4f1a69e6d4469faff3f947f3bd5a2e2359c3dfe50404c1338a577d24c33e80b8a3cf63140038e7734ebd741d16ce64 @@ -6853,6 +7204,7 @@ __metadata: "@nivo/pie": 0.79.1 "@playwright/test": ^1.22.2 "@react-aria/color": ^3.0.0-beta.15 + "@react-pdf/renderer": ^3.1.3 "@rocket.chat/agenda": "workspace:^" "@rocket.chat/api-client": "workspace:^" "@rocket.chat/apps-engine": 1.36.0 @@ -6882,11 +7234,14 @@ __metadata: "@rocket.chat/model-typings": "workspace:^" "@rocket.chat/models": "workspace:^" "@rocket.chat/mp3-encoder": 0.24.0 + "@rocket.chat/omnichannel-services": "workspace:^" "@rocket.chat/onboarding-ui": next + "@rocket.chat/pdf-worker": "workspace:^" "@rocket.chat/poplib": "workspace:^" "@rocket.chat/presence": "workspace:^" "@rocket.chat/rest-typings": "workspace:^" "@rocket.chat/string-helpers": next + "@rocket.chat/tools": "workspace:^" "@rocket.chat/ui-client": "workspace:^" "@rocket.chat/ui-composer": "workspace:^" "@rocket.chat/ui-contexts": "workspace:^" @@ -7022,6 +7377,7 @@ __metadata: dompurify: ^2.3.8 ejson: ^2.2.2 emailreplyparser: ^0.0.5 + emoji-toolkit: ^7.0.0 emojione: ^4.5.0 emojione-assets: ^4.5.0 eslint: ^8.29.0 @@ -7084,6 +7440,7 @@ __metadata: moleculer: ^0.14.21 moment: ^2.29.4 moment-timezone: ^0.5.34 + mongo-message-queue: ^1.0.0 mongodb: ^4.12.1 mongodb-memory-server: ^7.6.3 nats: ^2.6.1 @@ -7206,6 +7563,74 @@ __metadata: languageName: node linkType: hard +"@rocket.chat/omnichannel-services@workspace:^, @rocket.chat/omnichannel-services@workspace:ee/packages/omnichannel-services": + version: 0.0.0-use.local + resolution: "@rocket.chat/omnichannel-services@workspace:ee/packages/omnichannel-services" + dependencies: + "@rocket.chat/core-services": "workspace:^" + "@rocket.chat/core-typings": "workspace:^" + "@rocket.chat/emitter": 0.31.22 + "@rocket.chat/eslint-config": "workspace:^" + "@rocket.chat/model-typings": "workspace:^" + "@rocket.chat/models": "workspace:^" + "@rocket.chat/pdf-worker": "workspace:^" + "@rocket.chat/rest-typings": "workspace:^" + "@rocket.chat/string-helpers": 0.31.22 + "@rocket.chat/tools": "workspace:^" + "@types/jest": ^27.4.1 + "@types/node": ^14.18.21 + ejson: ^2.2.2 + emoji-toolkit: ^7.0.0 + eslint: ^8.12.0 + eventemitter3: ^4.0.7 + fibers: ^5.0.3 + jest: ^27.5.1 + mem: ^8.1.1 + moment-timezone: ^0.5.34 + mongo-message-queue: ^1.0.0 + mongodb: ^4.12.1 + pino: ^8.4.2 + ts-jest: ^27.1.4 + typescript: ~4.6.4 + languageName: unknown + linkType: soft + +"@rocket.chat/omnichannel-transcript@workspace:ee/apps/omnichannel-transcript": + version: 0.0.0-use.local + resolution: "@rocket.chat/omnichannel-transcript@workspace:ee/apps/omnichannel-transcript" + dependencies: + "@react-pdf/renderer": ^3.1.3 + "@rocket.chat/core-services": "workspace:^" + "@rocket.chat/core-typings": "workspace:^" + "@rocket.chat/emitter": 0.31.22 + "@rocket.chat/eslint-config": "workspace:^" + "@rocket.chat/model-typings": "workspace:^" + "@rocket.chat/models": "workspace:^" + "@rocket.chat/omnichannel-services": "workspace:^" + "@rocket.chat/pdf-worker": "workspace:^" + "@rocket.chat/tools": "workspace:^" + "@rocket.chat/ui-contexts": "workspace:^" + "@types/eslint": ^8.4.10 + "@types/node": ^14.18.21 + "@types/polka": ^0.5.4 + ejson: ^2.2.2 + emoji-toolkit: ^7.0.0 + eslint: ^8.29.0 + eventemitter3: ^4.0.7 + fibers: ^5.0.3 + mem: ^8.1.1 + moleculer: ^0.14.21 + moment-timezone: ^0.5.34 + mongo-message-queue: ^1.0.0 + mongodb: ^4.12.1 + nats: ^2.4.0 + pino: ^8.4.2 + polka: ^0.5.2 + ts-node: ^10.9.1 + typescript: ~4.5.5 + languageName: unknown + linkType: soft + "@rocket.chat/onboarding-ui@npm:next": version: 0.32.0-dev.275 resolution: "@rocket.chat/onboarding-ui@npm:0.32.0-dev.275" @@ -7228,6 +7653,42 @@ __metadata: languageName: node linkType: hard +"@rocket.chat/pdf-worker@workspace:^, @rocket.chat/pdf-worker@workspace:ee/packages/pdf-worker": + version: 0.0.0-use.local + resolution: "@rocket.chat/pdf-worker@workspace:ee/packages/pdf-worker" + dependencies: + "@react-pdf/renderer": ^3.1.3 + "@rocket.chat/core-typings": "workspace:^" + "@rocket.chat/fuselage-tokens": next + "@storybook/addon-actions": ~6.5.14 + "@storybook/addon-docs": ~6.5.14 + "@storybook/addon-essentials": ~6.5.14 + "@storybook/addon-interactions": ~6.5.14 + "@storybook/addon-links": ~6.5.14 + "@storybook/builder-webpack4": ~6.5.14 + "@storybook/manager-webpack4": ~6.5.14 + "@storybook/react": ~6.5.14 + "@storybook/testing-library": ~0.0.13 + "@testing-library/jest-dom": ^5.16.5 + "@testing-library/react": ~13.4.0 + "@types/emojione": ^2.2.6 + "@types/jest": ^27.4.1 + "@types/react": ^18.0.26 + "@types/react-dom": ^18 + "@types/testing-library__jest-dom": ^5 + emoji-assets: ^7.0.1 + emoji-toolkit: ^7.0.0 + eslint: ^8.12.0 + jest: ^27.5.1 + moment: ^2.29.4 + moment-timezone: ^0.5.34 + react: ^18.2.0 + react-dom: ^18.2.0 + ts-jest: ^27.1.4 + typescript: ~4.5.5 + languageName: unknown + linkType: soft + "@rocket.chat/poplib@workspace:^, @rocket.chat/poplib@workspace:packages/node-poplib": version: 0.0.0-use.local resolution: "@rocket.chat/poplib@workspace:packages/node-poplib" @@ -7300,6 +7761,39 @@ __metadata: languageName: node linkType: hard +"@rocket.chat/queue-worker@workspace:ee/apps/queue-worker": + version: 0.0.0-use.local + resolution: "@rocket.chat/queue-worker@workspace:ee/apps/queue-worker" + dependencies: + "@react-pdf/renderer": ^3.1.3 + "@rocket.chat/core-services": "workspace:^" + "@rocket.chat/core-typings": "workspace:^" + "@rocket.chat/emitter": 0.31.22 + "@rocket.chat/eslint-config": "workspace:^" + "@rocket.chat/model-typings": "workspace:^" + "@rocket.chat/models": "workspace:^" + "@rocket.chat/omnichannel-services": "workspace:^" + "@types/eslint": ^8.4.10 + "@types/node": ^14.18.21 + "@types/polka": ^0.5.4 + ejson: ^2.2.2 + emoji-toolkit: ^7.0.0 + eslint: ^8.29.0 + eventemitter3: ^4.0.7 + fibers: ^5.0.3 + mem: ^8.1.1 + moleculer: ^0.14.21 + moment-timezone: ^0.5.34 + mongo-message-queue: ^1.0.0 + mongodb: ^4.12.1 + nats: ^2.4.0 + pino: ^8.4.2 + polka: ^0.5.2 + ts-node: ^10.9.1 + typescript: ~4.5.5 + languageName: unknown + linkType: soft + "@rocket.chat/rest-typings@workspace:^, @rocket.chat/rest-typings@workspace:packages/rest-typings": version: 0.0.0-use.local resolution: "@rocket.chat/rest-typings@workspace:packages/rest-typings" @@ -7363,6 +7857,15 @@ __metadata: languageName: unknown linkType: soft +"@rocket.chat/string-helpers@npm:0.31.22": + version: 0.31.22 + resolution: "@rocket.chat/string-helpers@npm:0.31.22" + dependencies: + tslib: ^2.3.1 + checksum: 5c574c9b8d0b72b44e48ff91dfa83c3648d1fbc2757fc5959ea8a18fafa5959a843e89551aa3e2b7a612b6e1d929f60b5dd8dc0f522841640b7ddd0b90c31faa + languageName: node + linkType: hard + "@rocket.chat/string-helpers@npm:next": version: 0.31.23-dev.50 resolution: "@rocket.chat/string-helpers@npm:0.31.23-dev.50" @@ -7372,7 +7875,17 @@ __metadata: languageName: node linkType: hard -"@rocket.chat/styled@npm:next, @rocket.chat/styled@npm:~0.31.23-dev.50": +"@rocket.chat/styled@npm:next": + version: 0.31.23-dev.9 + resolution: "@rocket.chat/styled@npm:0.31.23-dev.9" + dependencies: + "@rocket.chat/css-in-js": ~0.31.23-dev.9 + tslib: ^2.3.1 + checksum: 7c6d571a95a4bfef5cd0bdb57a668764f25906dd268585e18e55e9a70e4c495f7f44a04d12dd3d69646782fdac5806a13fa4faf5a66bde253a3480f77e4b4760 + languageName: node + linkType: hard + +"@rocket.chat/styled@npm:~0.31.23-dev.50": version: 0.31.23-dev.50 resolution: "@rocket.chat/styled@npm:0.31.23-dev.50" dependencies: @@ -7394,6 +7907,31 @@ __metadata: languageName: node linkType: hard +"@rocket.chat/stylis-logical-props-middleware@npm:~0.31.23-dev.9": + version: 0.31.23-dev.9 + resolution: "@rocket.chat/stylis-logical-props-middleware@npm:0.31.23-dev.9" + dependencies: + "@rocket.chat/css-supports": ~0.31.23-dev.9 + tslib: ^2.3.1 + peerDependencies: + stylis: 4.0.10 + checksum: 5b41ad83a055fe2d5dbd08b047b49bdc06aa542abe9c450b5e5c4c08e564d899130758c31e3d580408e31c0586103560235b5e6d59107ceca06acd41af5e0088 + languageName: node + linkType: hard + +"@rocket.chat/tools@workspace:^, @rocket.chat/tools@workspace:packages/tools": + version: 0.0.0-use.local + resolution: "@rocket.chat/tools@workspace:packages/tools" + dependencies: + "@types/jest": ^27.4.1 + eslint: ^8.12.0 + jest: ^27.5.1 + moment-timezone: ^0.5.40 + ts-jest: ^27.1.4 + typescript: ~4.5.5 + languageName: unknown + linkType: soft + "@rocket.chat/ui-client@workspace:^, @rocket.chat/ui-client@workspace:packages/ui-client": version: 0.0.0-use.local resolution: "@rocket.chat/ui-client@workspace:packages/ui-client" @@ -7803,6 +8341,41 @@ __metadata: languageName: node linkType: hard +"@storybook/addon-actions@npm:6.5.16, @storybook/addon-actions@npm:~6.5.14": + version: 6.5.16 + resolution: "@storybook/addon-actions@npm:6.5.16" + dependencies: + "@storybook/addons": 6.5.16 + "@storybook/api": 6.5.16 + "@storybook/client-logger": 6.5.16 + "@storybook/components": 6.5.16 + "@storybook/core-events": 6.5.16 + "@storybook/csf": 0.0.2--canary.4566f4d.1 + "@storybook/theming": 6.5.16 + core-js: ^3.8.2 + fast-deep-equal: ^3.1.3 + global: ^4.4.0 + lodash: ^4.17.21 + polished: ^4.2.2 + prop-types: ^15.7.2 + react-inspector: ^5.1.0 + regenerator-runtime: ^0.13.7 + telejson: ^6.0.8 + ts-dedent: ^2.0.0 + util-deprecate: ^1.0.2 + uuid-browser: ^3.1.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + checksum: d506a932f38412fc234cd58b5f2c8a0bfb8f3820b0ce8042234e9bf4bd277a2befc2d8458d061405ee72722206756375f471a22c37ea32f384259fcbb1a2b6a5 + languageName: node + linkType: hard + "@storybook/addon-backgrounds@npm:6.5.15, @storybook/addon-backgrounds@npm:~6.5.15": version: 6.5.15 resolution: "@storybook/addon-backgrounds@npm:6.5.15" @@ -7832,6 +8405,35 @@ __metadata: languageName: node linkType: hard +"@storybook/addon-backgrounds@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/addon-backgrounds@npm:6.5.16" + dependencies: + "@storybook/addons": 6.5.16 + "@storybook/api": 6.5.16 + "@storybook/client-logger": 6.5.16 + "@storybook/components": 6.5.16 + "@storybook/core-events": 6.5.16 + "@storybook/csf": 0.0.2--canary.4566f4d.1 + "@storybook/theming": 6.5.16 + core-js: ^3.8.2 + global: ^4.4.0 + memoizerific: ^1.11.3 + regenerator-runtime: ^0.13.7 + ts-dedent: ^2.0.0 + util-deprecate: ^1.0.2 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + checksum: d10f0a6b5bf8f9974d3be08f1c30023f3148a0121456bf6296dbf70678f2591440e6fb5fd0643bc937a822c49284d81afeeed66f1b3de775d24c1149f402824b + languageName: node + linkType: hard + "@storybook/addon-controls@npm:6.5.15": version: 6.5.15 resolution: "@storybook/addon-controls@npm:6.5.15" @@ -7860,6 +8462,34 @@ __metadata: languageName: node linkType: hard +"@storybook/addon-controls@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/addon-controls@npm:6.5.16" + dependencies: + "@storybook/addons": 6.5.16 + "@storybook/api": 6.5.16 + "@storybook/client-logger": 6.5.16 + "@storybook/components": 6.5.16 + "@storybook/core-common": 6.5.16 + "@storybook/csf": 0.0.2--canary.4566f4d.1 + "@storybook/node-logger": 6.5.16 + "@storybook/store": 6.5.16 + "@storybook/theming": 6.5.16 + core-js: ^3.8.2 + lodash: ^4.17.21 + ts-dedent: ^2.0.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + checksum: a9f1f577e5d991ae271c9823662adf65952554303094a2e0127bfe9d48e2415796628dadc3cfbc767600e21588336bfd9cb43da59fe76507b2186f6a61da34b8 + languageName: node + linkType: hard + "@storybook/addon-docs@npm:6.5.15, @storybook/addon-docs@npm:~6.5.15": version: 6.5.15 resolution: "@storybook/addon-docs@npm:6.5.15" @@ -7907,6 +8537,113 @@ __metadata: languageName: node linkType: hard +"@storybook/addon-docs@npm:6.5.16, @storybook/addon-docs@npm:~6.5.14": + version: 6.5.16 + resolution: "@storybook/addon-docs@npm:6.5.16" + dependencies: + "@babel/plugin-transform-react-jsx": ^7.12.12 + "@babel/preset-env": ^7.12.11 + "@jest/transform": ^26.6.2 + "@mdx-js/react": ^1.6.22 + "@storybook/addons": 6.5.16 + "@storybook/api": 6.5.16 + "@storybook/components": 6.5.16 + "@storybook/core-common": 6.5.16 + "@storybook/core-events": 6.5.16 + "@storybook/csf": 0.0.2--canary.4566f4d.1 + "@storybook/docs-tools": 6.5.16 + "@storybook/mdx1-csf": ^0.0.1 + "@storybook/node-logger": 6.5.16 + "@storybook/postinstall": 6.5.16 + "@storybook/preview-web": 6.5.16 + "@storybook/source-loader": 6.5.16 + "@storybook/store": 6.5.16 + "@storybook/theming": 6.5.16 + babel-loader: ^8.0.0 + core-js: ^3.8.2 + fast-deep-equal: ^3.1.3 + global: ^4.4.0 + lodash: ^4.17.21 + regenerator-runtime: ^0.13.7 + remark-external-links: ^8.0.0 + remark-slug: ^6.0.0 + ts-dedent: ^2.0.0 + util-deprecate: ^1.0.2 + peerDependencies: + "@storybook/mdx2-csf": ^0.0.3 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@storybook/mdx2-csf": + optional: true + react: + optional: true + react-dom: + optional: true + checksum: 3203abc3af20bd8d22bda78c3c98b57f1c46ef29fe1942def0de687ddf08769592ec99d978048ed0aca82c13017b758392f644aaba40a0c0b68d2c61a9e5957d + languageName: node + linkType: hard + +"@storybook/addon-essentials@npm:~6.5.14": + version: 6.5.16 + resolution: "@storybook/addon-essentials@npm:6.5.16" + dependencies: + "@storybook/addon-actions": 6.5.16 + "@storybook/addon-backgrounds": 6.5.16 + "@storybook/addon-controls": 6.5.16 + "@storybook/addon-docs": 6.5.16 + "@storybook/addon-measure": 6.5.16 + "@storybook/addon-outline": 6.5.16 + "@storybook/addon-toolbars": 6.5.16 + "@storybook/addon-viewport": 6.5.16 + "@storybook/addons": 6.5.16 + "@storybook/api": 6.5.16 + "@storybook/core-common": 6.5.16 + "@storybook/node-logger": 6.5.16 + core-js: ^3.8.2 + regenerator-runtime: ^0.13.7 + ts-dedent: ^2.0.0 + peerDependencies: + "@babel/core": ^7.9.6 + peerDependenciesMeta: + "@storybook/angular": + optional: true + "@storybook/builder-manager4": + optional: true + "@storybook/builder-manager5": + optional: true + "@storybook/builder-webpack4": + optional: true + "@storybook/builder-webpack5": + optional: true + "@storybook/html": + optional: true + "@storybook/vue": + optional: true + "@storybook/vue3": + optional: true + "@storybook/web-components": + optional: true + lit: + optional: true + lit-html: + optional: true + react: + optional: true + react-dom: + optional: true + svelte: + optional: true + sveltedoc-parser: + optional: true + vue: + optional: true + webpack: + optional: true + checksum: f82a02d00f02c642dae01b2c6c32d48dc4647fe4adbf17d55bb517812d9e483a773084c1c5ceda39d7db5fdaebcaca324a28bb465e35fb524667ef2f5382b1d6 + languageName: node + linkType: hard + "@storybook/addon-essentials@npm:~6.5.15": version: 6.5.15 resolution: "@storybook/addon-essentials@npm:6.5.15" @@ -7967,6 +8704,37 @@ __metadata: languageName: node linkType: hard +"@storybook/addon-interactions@npm:~6.5.14": + version: 6.5.16 + resolution: "@storybook/addon-interactions@npm:6.5.16" + dependencies: + "@devtools-ds/object-inspector": ^1.1.2 + "@storybook/addons": 6.5.16 + "@storybook/api": 6.5.16 + "@storybook/client-logger": 6.5.16 + "@storybook/components": 6.5.16 + "@storybook/core-common": 6.5.16 + "@storybook/core-events": 6.5.16 + "@storybook/csf": 0.0.2--canary.4566f4d.1 + "@storybook/instrumenter": 6.5.16 + "@storybook/theming": 6.5.16 + core-js: ^3.8.2 + global: ^4.4.0 + jest-mock: ^27.0.6 + polished: ^4.2.2 + ts-dedent: ^2.2.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + checksum: cba31aa22e684c5551b9a7af95d949aa80286179324f1ef2a42e9f8be78109c140d730244bce1236af7dc157ba241bf567c3767ca99564162307ec377dffec48 + languageName: node + linkType: hard + "@storybook/addon-interactions@npm:~6.5.15": version: 6.5.15 resolution: "@storybook/addon-interactions@npm:6.5.15" @@ -8030,6 +8798,34 @@ __metadata: languageName: node linkType: hard +"@storybook/addon-links@npm:~6.5.14": + version: 6.5.16 + resolution: "@storybook/addon-links@npm:6.5.16" + dependencies: + "@storybook/addons": 6.5.16 + "@storybook/client-logger": 6.5.16 + "@storybook/core-events": 6.5.16 + "@storybook/csf": 0.0.2--canary.4566f4d.1 + "@storybook/router": 6.5.16 + "@types/qs": ^6.9.5 + core-js: ^3.8.2 + global: ^4.4.0 + prop-types: ^15.7.2 + qs: ^6.10.0 + regenerator-runtime: ^0.13.7 + ts-dedent: ^2.0.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + checksum: 40fa5fcd98df3be50b3587efda79ddf0156eb0078dd0afec43e81e961475bc8583feec1314baabe59fe2dc8e5b9b4bb4a738435172c208f828d1538cd59882fe + languageName: node + linkType: hard + "@storybook/addon-links@npm:~6.5.15": version: 6.5.15 resolution: "@storybook/addon-links@npm:6.5.15" @@ -8082,6 +8878,30 @@ __metadata: languageName: node linkType: hard +"@storybook/addon-measure@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/addon-measure@npm:6.5.16" + dependencies: + "@storybook/addons": 6.5.16 + "@storybook/api": 6.5.16 + "@storybook/client-logger": 6.5.16 + "@storybook/components": 6.5.16 + "@storybook/core-events": 6.5.16 + "@storybook/csf": 0.0.2--canary.4566f4d.1 + core-js: ^3.8.2 + global: ^4.4.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + checksum: 52fc33249679bb19fdd4e7285436b925832f3d18c223c495cea2b90aa68f08bc626199064eead88ea339ce7e7fa73940daf220e4408ccd4dfd3841288dc645e4 + languageName: node + linkType: hard + "@storybook/addon-outline@npm:6.5.15": version: 6.5.15 resolution: "@storybook/addon-outline@npm:6.5.15" @@ -8108,6 +8928,32 @@ __metadata: languageName: node linkType: hard +"@storybook/addon-outline@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/addon-outline@npm:6.5.16" + dependencies: + "@storybook/addons": 6.5.16 + "@storybook/api": 6.5.16 + "@storybook/client-logger": 6.5.16 + "@storybook/components": 6.5.16 + "@storybook/core-events": 6.5.16 + "@storybook/csf": 0.0.2--canary.4566f4d.1 + core-js: ^3.8.2 + global: ^4.4.0 + regenerator-runtime: ^0.13.7 + ts-dedent: ^2.0.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + checksum: cb838ecbbdb446552aab891e5fadef6663acf4b16b2bdc18b9a86c01866ccefff0129d9fb7d801604c43946fff5afdcb2c11a1a7813319948a08351c9f35bf46 + languageName: node + linkType: hard + "@storybook/addon-postcss@npm:~2.0.0": version: 2.0.0 resolution: "@storybook/addon-postcss@npm:2.0.0" @@ -8144,6 +8990,29 @@ __metadata: languageName: node linkType: hard +"@storybook/addon-toolbars@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/addon-toolbars@npm:6.5.16" + dependencies: + "@storybook/addons": 6.5.16 + "@storybook/api": 6.5.16 + "@storybook/client-logger": 6.5.16 + "@storybook/components": 6.5.16 + "@storybook/theming": 6.5.16 + core-js: ^3.8.2 + regenerator-runtime: ^0.13.7 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + checksum: 7a30259bef831769db3e8d76ad439cc5deec919abf47b27a9d0143a581434748d2c8868fbbf8b9cce2910fd61f2200415b6ab5bc0dfab02436fbea2c312da770 + languageName: node + linkType: hard + "@storybook/addon-viewport@npm:6.5.15, @storybook/addon-viewport@npm:~6.5.15": version: 6.5.15 resolution: "@storybook/addon-viewport@npm:6.5.15" @@ -8171,6 +9040,55 @@ __metadata: languageName: node linkType: hard +"@storybook/addon-viewport@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/addon-viewport@npm:6.5.16" + dependencies: + "@storybook/addons": 6.5.16 + "@storybook/api": 6.5.16 + "@storybook/client-logger": 6.5.16 + "@storybook/components": 6.5.16 + "@storybook/core-events": 6.5.16 + "@storybook/theming": 6.5.16 + core-js: ^3.8.2 + global: ^4.4.0 + memoizerific: ^1.11.3 + prop-types: ^15.7.2 + regenerator-runtime: ^0.13.7 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + checksum: 4b1de32b85b305c22b976bae040c360063d6152c5077930953cc9cb565735a516c1d239b0670f9a8218264aabff9e8d6c4336fdb70698765009791f24c0fc867 + languageName: node + linkType: hard + +"@storybook/addons@npm:6.5.14": + version: 6.5.14 + resolution: "@storybook/addons@npm:6.5.14" + dependencies: + "@storybook/api": 6.5.14 + "@storybook/channels": 6.5.14 + "@storybook/client-logger": 6.5.14 + "@storybook/core-events": 6.5.14 + "@storybook/csf": 0.0.2--canary.4566f4d.1 + "@storybook/router": 6.5.14 + "@storybook/theming": 6.5.14 + "@types/webpack-env": ^1.16.0 + core-js: ^3.8.2 + global: ^4.4.0 + regenerator-runtime: ^0.13.7 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 99d06641bab06a3cc2821f309589d062c0efd8707b451ae24017449034da408bfddce3beda1ccdedadf59669d7d13348bee127f6fd4fc057200c84ff43288312 + languageName: node + linkType: hard + "@storybook/addons@npm:6.5.15, @storybook/addons@npm:~6.5.15": version: 6.5.15 resolution: "@storybook/addons@npm:6.5.15" @@ -8193,6 +9111,56 @@ __metadata: languageName: node linkType: hard +"@storybook/addons@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/addons@npm:6.5.16" + dependencies: + "@storybook/api": 6.5.16 + "@storybook/channels": 6.5.16 + "@storybook/client-logger": 6.5.16 + "@storybook/core-events": 6.5.16 + "@storybook/csf": 0.0.2--canary.4566f4d.1 + "@storybook/router": 6.5.16 + "@storybook/theming": 6.5.16 + "@types/webpack-env": ^1.16.0 + core-js: ^3.8.2 + global: ^4.4.0 + regenerator-runtime: ^0.13.7 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 0463150e4cf7bd2b2aaafdbaadfb4420e4e0a31eb651cfc1a2d7f4b4974caf67878712602474585dfa18f583000608598045594909959d2e9e2ec32ba004392d + languageName: node + linkType: hard + +"@storybook/api@npm:6.5.14": + version: 6.5.14 + resolution: "@storybook/api@npm:6.5.14" + dependencies: + "@storybook/channels": 6.5.14 + "@storybook/client-logger": 6.5.14 + "@storybook/core-events": 6.5.14 + "@storybook/csf": 0.0.2--canary.4566f4d.1 + "@storybook/router": 6.5.14 + "@storybook/semver": ^7.3.2 + "@storybook/theming": 6.5.14 + core-js: ^3.8.2 + fast-deep-equal: ^3.1.3 + global: ^4.4.0 + lodash: ^4.17.21 + memoizerific: ^1.11.3 + regenerator-runtime: ^0.13.7 + store2: ^2.12.0 + telejson: ^6.0.8 + ts-dedent: ^2.0.0 + util-deprecate: ^1.0.2 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 0d421c3211a49cb8910dea647b898edd60af79755108ed321626a8fc134713dd1b018c830f15c2fc6c863f0528b571c2e2b34bb79df3c2f43497f5ab36fa9bbf + languageName: node + linkType: hard + "@storybook/api@npm:6.5.15": version: 6.5.15 resolution: "@storybook/api@npm:6.5.15" @@ -8221,6 +9189,34 @@ __metadata: languageName: node linkType: hard +"@storybook/api@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/api@npm:6.5.16" + dependencies: + "@storybook/channels": 6.5.16 + "@storybook/client-logger": 6.5.16 + "@storybook/core-events": 6.5.16 + "@storybook/csf": 0.0.2--canary.4566f4d.1 + "@storybook/router": 6.5.16 + "@storybook/semver": ^7.3.2 + "@storybook/theming": 6.5.16 + core-js: ^3.8.2 + fast-deep-equal: ^3.1.3 + global: ^4.4.0 + lodash: ^4.17.21 + memoizerific: ^1.11.3 + regenerator-runtime: ^0.13.7 + store2: ^2.12.0 + telejson: ^6.0.8 + ts-dedent: ^2.0.0 + util-deprecate: ^1.0.2 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: c873189ac1e501825d647903baa125899c492cee962cb86ebb7455110bd09194eeb6943f5c58a1f808ce4ee2e20e305f5604a4e60b07003c82a6fc6ceaee5ea9 + languageName: node + linkType: hard + "@storybook/builder-webpack4@npm:6.5.15, @storybook/builder-webpack4@npm:~6.5.15": version: 6.5.15 resolution: "@storybook/builder-webpack4@npm:6.5.15" @@ -8282,6 +9278,67 @@ __metadata: languageName: node linkType: hard +"@storybook/builder-webpack4@npm:6.5.16, @storybook/builder-webpack4@npm:~6.5.14": + version: 6.5.16 + resolution: "@storybook/builder-webpack4@npm:6.5.16" + dependencies: + "@babel/core": ^7.12.10 + "@storybook/addons": 6.5.16 + "@storybook/api": 6.5.16 + "@storybook/channel-postmessage": 6.5.16 + "@storybook/channels": 6.5.16 + "@storybook/client-api": 6.5.16 + "@storybook/client-logger": 6.5.16 + "@storybook/components": 6.5.16 + "@storybook/core-common": 6.5.16 + "@storybook/core-events": 6.5.16 + "@storybook/node-logger": 6.5.16 + "@storybook/preview-web": 6.5.16 + "@storybook/router": 6.5.16 + "@storybook/semver": ^7.3.2 + "@storybook/store": 6.5.16 + "@storybook/theming": 6.5.16 + "@storybook/ui": 6.5.16 + "@types/node": ^14.0.10 || ^16.0.0 + "@types/webpack": ^4.41.26 + autoprefixer: ^9.8.6 + babel-loader: ^8.0.0 + case-sensitive-paths-webpack-plugin: ^2.3.0 + core-js: ^3.8.2 + css-loader: ^3.6.0 + file-loader: ^6.2.0 + find-up: ^5.0.0 + fork-ts-checker-webpack-plugin: ^4.1.6 + glob: ^7.1.6 + glob-promise: ^3.4.0 + global: ^4.4.0 + html-webpack-plugin: ^4.0.0 + pnp-webpack-plugin: 1.6.4 + postcss: ^7.0.36 + postcss-flexbugs-fixes: ^4.2.1 + postcss-loader: ^4.2.0 + raw-loader: ^4.0.2 + stable: ^0.1.8 + style-loader: ^1.3.0 + terser-webpack-plugin: ^4.2.3 + ts-dedent: ^2.0.0 + url-loader: ^4.1.1 + util-deprecate: ^1.0.2 + webpack: 4 + webpack-dev-middleware: ^3.7.3 + webpack-filter-warnings-plugin: ^1.2.1 + webpack-hot-middleware: ^2.25.1 + webpack-virtual-modules: ^0.2.2 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 5e9137c390db00b4e166df3ca730eb1748f6bac92c841f3f75c37ad5277d6f5565f899de3bb0357fc51ce6821c8a8a8adba724e3dd7a3d1cc80816e09e5b7128 + languageName: node + linkType: hard + "@storybook/builder-webpack5@npm:~6.5.15": version: 6.5.15 resolution: "@storybook/builder-webpack5@npm:6.5.15" @@ -8349,6 +9406,21 @@ __metadata: languageName: node linkType: hard +"@storybook/channel-postmessage@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/channel-postmessage@npm:6.5.16" + dependencies: + "@storybook/channels": 6.5.16 + "@storybook/client-logger": 6.5.16 + "@storybook/core-events": 6.5.16 + core-js: ^3.8.2 + global: ^4.4.0 + qs: ^6.10.0 + telejson: ^6.0.8 + checksum: d3560d81dbf4710cc23b227c12be328d87e627581afcb5fec959f1e795fb2b5824db2a7f03a4ddcd185ec9a37a7025415d8bb43b7a245f2466395908eb3e9bc3 + languageName: node + linkType: hard + "@storybook/channel-websocket@npm:6.5.15": version: 6.5.15 resolution: "@storybook/channel-websocket@npm:6.5.15" @@ -8362,6 +9434,30 @@ __metadata: languageName: node linkType: hard +"@storybook/channel-websocket@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/channel-websocket@npm:6.5.16" + dependencies: + "@storybook/channels": 6.5.16 + "@storybook/client-logger": 6.5.16 + core-js: ^3.8.2 + global: ^4.4.0 + telejson: ^6.0.8 + checksum: 355c85f22d7cc65764871852debe347c43c3fe92d6a0caa64aecbe2dce78d4bf73b98e997099f9e4e7c204ad5821b979939b0700e446fa26478c1e1ba48e7380 + languageName: node + linkType: hard + +"@storybook/channels@npm:6.5.14": + version: 6.5.14 + resolution: "@storybook/channels@npm:6.5.14" + dependencies: + core-js: ^3.8.2 + ts-dedent: ^2.0.0 + util-deprecate: ^1.0.2 + checksum: ff1ee3fea3c7b8591280ba7eabe13c999fc3e12a483ff2c0467cc9cca027662cbbc4676438da567865919157521df8a9a50bd20b35daed6896f39a3a7251a1e5 + languageName: node + linkType: hard + "@storybook/channels@npm:6.5.15": version: 6.5.15 resolution: "@storybook/channels@npm:6.5.15" @@ -8373,6 +9469,17 @@ __metadata: languageName: node linkType: hard +"@storybook/channels@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/channels@npm:6.5.16" + dependencies: + core-js: ^3.8.2 + ts-dedent: ^2.0.0 + util-deprecate: ^1.0.2 + checksum: 3d7f7bc19ed7b250976e00e02ab544408806b439106bed18a5db9815612f6c5df9bdf7c1a97b5a40ba3194184ebe7e4c75e2bca5496025d6b26afefa95cfccbd + languageName: node + linkType: hard + "@storybook/client-api@npm:6.5.15": version: 6.5.15 resolution: "@storybook/client-api@npm:6.5.15" @@ -8404,7 +9511,48 @@ __metadata: languageName: node linkType: hard -"@storybook/client-logger@npm:6.5.15, @storybook/client-logger@npm:^6.4.0": +"@storybook/client-api@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/client-api@npm:6.5.16" + dependencies: + "@storybook/addons": 6.5.16 + "@storybook/channel-postmessage": 6.5.16 + "@storybook/channels": 6.5.16 + "@storybook/client-logger": 6.5.16 + "@storybook/core-events": 6.5.16 + "@storybook/csf": 0.0.2--canary.4566f4d.1 + "@storybook/store": 6.5.16 + "@types/qs": ^6.9.5 + "@types/webpack-env": ^1.16.0 + core-js: ^3.8.2 + fast-deep-equal: ^3.1.3 + global: ^4.4.0 + lodash: ^4.17.21 + memoizerific: ^1.11.3 + qs: ^6.10.0 + regenerator-runtime: ^0.13.7 + store2: ^2.12.0 + synchronous-promise: ^2.0.15 + ts-dedent: ^2.0.0 + util-deprecate: ^1.0.2 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: a62276fa67d2c3cc766ea9145d3798c0c8ef3f9de9fb18e7c43d67e39226f47a2546c4319ccc6075545df65dc4fc65bdb97e904062daf426be6534767eacada6 + languageName: node + linkType: hard + +"@storybook/client-logger@npm:6.5.14": + version: 6.5.14 + resolution: "@storybook/client-logger@npm:6.5.14" + dependencies: + core-js: ^3.8.2 + global: ^4.4.0 + checksum: 29cc0b58db7a8dc90484320c86b386975580c0e534791b29f6a8c00ce5b156f2bff9513994202f9f9ef99787e8d793988048ae88d2780ba151c6782f3bbf97ff + languageName: node + linkType: hard + +"@storybook/client-logger@npm:6.5.15": version: 6.5.15 resolution: "@storybook/client-logger@npm:6.5.15" dependencies: @@ -8414,6 +9562,26 @@ __metadata: languageName: node linkType: hard +"@storybook/client-logger@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/client-logger@npm:6.5.16" + dependencies: + core-js: ^3.8.2 + global: ^4.4.0 + checksum: 0a86959b1bacb1b893e282173b48afe9c857b8cdc67a47ad87a7f11ba7dbc15ebc4f0d05c07dffb988e0cd3e1de0f09f300ee06c66afe4c50e9be83aaed75971 + languageName: node + linkType: hard + +"@storybook/client-logger@npm:^6.4.0": + version: 6.5.12 + resolution: "@storybook/client-logger@npm:6.5.12" + dependencies: + core-js: ^3.8.2 + global: ^4.4.0 + checksum: bd11bc25115f9b4a965e378d7dac28f9152038173ab5debb1e116a7aba69c814752d2c8aa4092dd1fc3f60cd99d4896c9e74d5e6f3c85768e7633adaf5bd2bf2 + languageName: node + linkType: hard + "@storybook/components@npm:6.5.15": version: 6.5.15 resolution: "@storybook/components@npm:6.5.15" @@ -8433,6 +9601,25 @@ __metadata: languageName: node linkType: hard +"@storybook/components@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/components@npm:6.5.16" + dependencies: + "@storybook/client-logger": 6.5.16 + "@storybook/csf": 0.0.2--canary.4566f4d.1 + "@storybook/theming": 6.5.16 + core-js: ^3.8.2 + memoizerific: ^1.11.3 + qs: ^6.10.0 + regenerator-runtime: ^0.13.7 + util-deprecate: ^1.0.2 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 1caf822bf1293ca043822f1c77f05c0f01631e8a61adad6bc4651ba9be78c8f4822ba0905e39c8feaa3fb44ae10422e9ccd3004348b18531fb82c54cfcea4fa9 + languageName: node + linkType: hard + "@storybook/core-client@npm:6.5.15": version: 6.5.15 resolution: "@storybook/core-client@npm:6.5.15" @@ -8468,6 +9655,41 @@ __metadata: languageName: node linkType: hard +"@storybook/core-client@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/core-client@npm:6.5.16" + dependencies: + "@storybook/addons": 6.5.16 + "@storybook/channel-postmessage": 6.5.16 + "@storybook/channel-websocket": 6.5.16 + "@storybook/client-api": 6.5.16 + "@storybook/client-logger": 6.5.16 + "@storybook/core-events": 6.5.16 + "@storybook/csf": 0.0.2--canary.4566f4d.1 + "@storybook/preview-web": 6.5.16 + "@storybook/store": 6.5.16 + "@storybook/ui": 6.5.16 + airbnb-js-shims: ^2.2.1 + ansi-to-html: ^0.6.11 + core-js: ^3.8.2 + global: ^4.4.0 + lodash: ^4.17.21 + qs: ^6.10.0 + regenerator-runtime: ^0.13.7 + ts-dedent: ^2.0.0 + unfetch: ^4.2.0 + util-deprecate: ^1.0.2 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + webpack: "*" + peerDependenciesMeta: + typescript: + optional: true + checksum: 467710777ddd740c431cf65035ecc489daae2fc5f4844a40b7339b806535e239140f40442a0e1d89356e107169c39d9e84d726c01982ed4609c043b6861e0778 + languageName: node + linkType: hard + "@storybook/core-common@npm:6.5.15": version: 6.5.15 resolution: "@storybook/core-common@npm:6.5.15" @@ -8532,6 +9754,79 @@ __metadata: languageName: node linkType: hard +"@storybook/core-common@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/core-common@npm:6.5.16" + dependencies: + "@babel/core": ^7.12.10 + "@babel/plugin-proposal-class-properties": ^7.12.1 + "@babel/plugin-proposal-decorators": ^7.12.12 + "@babel/plugin-proposal-export-default-from": ^7.12.1 + "@babel/plugin-proposal-nullish-coalescing-operator": ^7.12.1 + "@babel/plugin-proposal-object-rest-spread": ^7.12.1 + "@babel/plugin-proposal-optional-chaining": ^7.12.7 + "@babel/plugin-proposal-private-methods": ^7.12.1 + "@babel/plugin-proposal-private-property-in-object": ^7.12.1 + "@babel/plugin-syntax-dynamic-import": ^7.8.3 + "@babel/plugin-transform-arrow-functions": ^7.12.1 + "@babel/plugin-transform-block-scoping": ^7.12.12 + "@babel/plugin-transform-classes": ^7.12.1 + "@babel/plugin-transform-destructuring": ^7.12.1 + "@babel/plugin-transform-for-of": ^7.12.1 + "@babel/plugin-transform-parameters": ^7.12.1 + "@babel/plugin-transform-shorthand-properties": ^7.12.1 + "@babel/plugin-transform-spread": ^7.12.1 + "@babel/preset-env": ^7.12.11 + "@babel/preset-react": ^7.12.10 + "@babel/preset-typescript": ^7.12.7 + "@babel/register": ^7.12.1 + "@storybook/node-logger": 6.5.16 + "@storybook/semver": ^7.3.2 + "@types/node": ^14.0.10 || ^16.0.0 + "@types/pretty-hrtime": ^1.0.0 + babel-loader: ^8.0.0 + babel-plugin-macros: ^3.0.1 + babel-plugin-polyfill-corejs3: ^0.1.0 + chalk: ^4.1.0 + core-js: ^3.8.2 + express: ^4.17.1 + file-system-cache: ^1.0.5 + find-up: ^5.0.0 + fork-ts-checker-webpack-plugin: ^6.0.4 + fs-extra: ^9.0.1 + glob: ^7.1.6 + handlebars: ^4.7.7 + interpret: ^2.2.0 + json5: ^2.2.3 + lazy-universal-dotenv: ^3.0.1 + picomatch: ^2.3.0 + pkg-dir: ^5.0.0 + pretty-hrtime: ^1.0.3 + resolve-from: ^5.0.0 + slash: ^3.0.0 + telejson: ^6.0.8 + ts-dedent: ^2.0.0 + util-deprecate: ^1.0.2 + webpack: 4 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 886a701876599939950c3c98e306b373cd026c7b995ca08d88475b3f35624a53763459d6b202728ec703e99126813a254b956c2d0fe7e85f99dcb5765a999b19 + languageName: node + linkType: hard + +"@storybook/core-events@npm:6.5.14": + version: 6.5.14 + resolution: "@storybook/core-events@npm:6.5.14" + dependencies: + core-js: ^3.8.2 + checksum: 6787925c520a6ee5aee748d4b7e2ec599c5ee16a87dbb62a94eeec88003ef42683d8e7ac8b98b49ea2a33205e0648805410c4759d16a997ba2f4215f6c8784ce + languageName: node + linkType: hard + "@storybook/core-events@npm:6.5.15": version: 6.5.15 resolution: "@storybook/core-events@npm:6.5.15" @@ -8541,6 +9836,15 @@ __metadata: languageName: node linkType: hard +"@storybook/core-events@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/core-events@npm:6.5.16" + dependencies: + core-js: ^3.8.2 + checksum: 1844bdabfb7828af7ddd54129fbb321bf65d8b65459eaac99c8f3f94c7c2f0ee000468362758076444083f863a3bc835ecd1e4f2128524eb5c00c8a576473bc9 + languageName: node + linkType: hard + "@storybook/core-server@npm:6.5.15": version: 6.5.15 resolution: "@storybook/core-server@npm:6.5.15" @@ -8604,6 +9908,69 @@ __metadata: languageName: node linkType: hard +"@storybook/core-server@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/core-server@npm:6.5.16" + dependencies: + "@discoveryjs/json-ext": ^0.5.3 + "@storybook/builder-webpack4": 6.5.16 + "@storybook/core-client": 6.5.16 + "@storybook/core-common": 6.5.16 + "@storybook/core-events": 6.5.16 + "@storybook/csf": 0.0.2--canary.4566f4d.1 + "@storybook/csf-tools": 6.5.16 + "@storybook/manager-webpack4": 6.5.16 + "@storybook/node-logger": 6.5.16 + "@storybook/semver": ^7.3.2 + "@storybook/store": 6.5.16 + "@storybook/telemetry": 6.5.16 + "@types/node": ^14.0.10 || ^16.0.0 + "@types/node-fetch": ^2.5.7 + "@types/pretty-hrtime": ^1.0.0 + "@types/webpack": ^4.41.26 + better-opn: ^2.1.1 + boxen: ^5.1.2 + chalk: ^4.1.0 + cli-table3: ^0.6.1 + commander: ^6.2.1 + compression: ^1.7.4 + core-js: ^3.8.2 + cpy: ^8.1.2 + detect-port: ^1.3.0 + express: ^4.17.1 + fs-extra: ^9.0.1 + global: ^4.4.0 + globby: ^11.0.2 + ip: ^2.0.0 + lodash: ^4.17.21 + node-fetch: ^2.6.7 + open: ^8.4.0 + pretty-hrtime: ^1.0.3 + prompts: ^2.4.0 + regenerator-runtime: ^0.13.7 + serve-favicon: ^2.5.0 + slash: ^3.0.0 + telejson: ^6.0.8 + ts-dedent: ^2.0.0 + util-deprecate: ^1.0.2 + watchpack: ^2.2.0 + webpack: 4 + ws: ^8.2.3 + x-default-browser: ^0.4.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + "@storybook/builder-webpack5": + optional: true + "@storybook/manager-webpack5": + optional: true + typescript: + optional: true + checksum: 2027adba39b2e0a5c3664241f48ec256a92866755aace96f3b8e2064b50237bbcd4e814bc58a1084006baae41c48d7d0eccefc9867d84e17d68d7f969e65f149 + languageName: node + linkType: hard + "@storybook/core@npm:6.5.15": version: 6.5.15 resolution: "@storybook/core@npm:6.5.15" @@ -8625,6 +9992,27 @@ __metadata: languageName: node linkType: hard +"@storybook/core@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/core@npm:6.5.16" + dependencies: + "@storybook/core-client": 6.5.16 + "@storybook/core-server": 6.5.16 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + webpack: "*" + peerDependenciesMeta: + "@storybook/builder-webpack5": + optional: true + "@storybook/manager-webpack5": + optional: true + typescript: + optional: true + checksum: f1732338741692007230a351419ef3aa4e387810d7d0c0e6ffb1159e1de4d757199f2b543cf4f6413fc40acda514b908d2fd9b3e0d56e3f6cec1e3a82c2fcc10 + languageName: node + linkType: hard + "@storybook/csf-tools@npm:6.5.15": version: 6.5.15 resolution: "@storybook/csf-tools@npm:6.5.15" @@ -8652,6 +10040,33 @@ __metadata: languageName: node linkType: hard +"@storybook/csf-tools@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/csf-tools@npm:6.5.16" + dependencies: + "@babel/core": ^7.12.10 + "@babel/generator": ^7.12.11 + "@babel/parser": ^7.12.11 + "@babel/plugin-transform-react-jsx": ^7.12.12 + "@babel/preset-env": ^7.12.11 + "@babel/traverse": ^7.12.11 + "@babel/types": ^7.12.11 + "@storybook/csf": 0.0.2--canary.4566f4d.1 + "@storybook/mdx1-csf": ^0.0.1 + core-js: ^3.8.2 + fs-extra: ^9.0.1 + global: ^4.4.0 + regenerator-runtime: ^0.13.7 + ts-dedent: ^2.0.0 + peerDependencies: + "@storybook/mdx2-csf": ^0.0.3 + peerDependenciesMeta: + "@storybook/mdx2-csf": + optional: true + checksum: ee71a47d90186c35fc1dbcb6ece2888ff4d730bde823bb1bd242d802b74045b482d2c469f3a91687b691b6f828ce449b182896d1912033846b9746457ee960ba + languageName: node + linkType: hard + "@storybook/csf@npm:0.0.2--canary.4566f4d.1": version: 0.0.2--canary.4566f4d.1 resolution: "@storybook/csf@npm:0.0.2--canary.4566f4d.1" @@ -8685,7 +10100,22 @@ __metadata: languageName: node linkType: hard -"@storybook/instrumenter@npm:6.5.15, @storybook/instrumenter@npm:^6.4.0": +"@storybook/docs-tools@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/docs-tools@npm:6.5.16" + dependencies: + "@babel/core": ^7.12.10 + "@storybook/csf": 0.0.2--canary.4566f4d.1 + "@storybook/store": 6.5.16 + core-js: ^3.8.2 + doctrine: ^3.0.0 + lodash: ^4.17.21 + regenerator-runtime: ^0.13.7 + checksum: 6351c5b1cbe5820f0f0dfcc3e4e7da8cca3c8d73a06c5803e65cb86e9e81ccbae53cec8e1b579af0ac9a5bbb6d4b6ac03ffe26af2220dc5dfe8f065067f0e2d7 + languageName: node + linkType: hard + +"@storybook/instrumenter@npm:6.5.15": version: 6.5.15 resolution: "@storybook/instrumenter@npm:6.5.15" dependencies: @@ -8698,6 +10128,32 @@ __metadata: languageName: node linkType: hard +"@storybook/instrumenter@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/instrumenter@npm:6.5.16" + dependencies: + "@storybook/addons": 6.5.16 + "@storybook/client-logger": 6.5.16 + "@storybook/core-events": 6.5.16 + core-js: ^3.8.2 + global: ^4.4.0 + checksum: f22bb4adfa848121d897a6a21e12bfe32d0e809be3480c99f681f2b6a6630b0cb93a63a4a1abea3a0e35411c4959f36fd9160e7e540cc219d45d35dce7746db6 + languageName: node + linkType: hard + +"@storybook/instrumenter@npm:^6.4.0": + version: 6.5.14 + resolution: "@storybook/instrumenter@npm:6.5.14" + dependencies: + "@storybook/addons": 6.5.14 + "@storybook/client-logger": 6.5.14 + "@storybook/core-events": 6.5.14 + core-js: ^3.8.2 + global: ^4.4.0 + checksum: 99d480968012e59ead965034a153e19e0958622b917ca063e2f929b31782fb27db401f2cc272cc76143777785fa48e39a1f8a24d1a815a864164b3b5ce11d847 + languageName: node + linkType: hard + "@storybook/manager-webpack4@npm:6.5.15, @storybook/manager-webpack4@npm:~6.5.15": version: 6.5.15 resolution: "@storybook/manager-webpack4@npm:6.5.15" @@ -8747,6 +10203,104 @@ __metadata: languageName: node linkType: hard +"@storybook/manager-webpack4@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/manager-webpack4@npm:6.5.16" + dependencies: + "@babel/core": ^7.12.10 + "@babel/plugin-transform-template-literals": ^7.12.1 + "@babel/preset-react": ^7.12.10 + "@storybook/addons": 6.5.16 + "@storybook/core-client": 6.5.16 + "@storybook/core-common": 6.5.16 + "@storybook/node-logger": 6.5.16 + "@storybook/theming": 6.5.16 + "@storybook/ui": 6.5.16 + "@types/node": ^14.0.10 || ^16.0.0 + "@types/webpack": ^4.41.26 + babel-loader: ^8.0.0 + case-sensitive-paths-webpack-plugin: ^2.3.0 + chalk: ^4.1.0 + core-js: ^3.8.2 + css-loader: ^3.6.0 + express: ^4.17.1 + file-loader: ^6.2.0 + find-up: ^5.0.0 + fs-extra: ^9.0.1 + html-webpack-plugin: ^4.0.0 + node-fetch: ^2.6.7 + pnp-webpack-plugin: 1.6.4 + read-pkg-up: ^7.0.1 + regenerator-runtime: ^0.13.7 + resolve-from: ^5.0.0 + style-loader: ^1.3.0 + telejson: ^6.0.8 + terser-webpack-plugin: ^4.2.3 + ts-dedent: ^2.0.0 + url-loader: ^4.1.1 + util-deprecate: ^1.0.2 + webpack: 4 + webpack-dev-middleware: ^3.7.3 + webpack-virtual-modules: ^0.2.2 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 873c871c822ecde30fbd95e9517549a18c5bb2de46d6160d6dcd7c1b5635fda2073b5bc4bd4d87e72de6e8df8bccf39b81f062e07cd7a23ffb4b43293e488fbb + languageName: node + linkType: hard + +"@storybook/manager-webpack4@npm:~6.5.14": + version: 6.5.14 + resolution: "@storybook/manager-webpack4@npm:6.5.14" + dependencies: + "@babel/core": ^7.12.10 + "@babel/plugin-transform-template-literals": ^7.12.1 + "@babel/preset-react": ^7.12.10 + "@storybook/addons": 6.5.15 + "@storybook/core-client": 6.5.15 + "@storybook/core-common": 6.5.15 + "@storybook/node-logger": 6.5.15 + "@storybook/theming": 6.5.15 + "@storybook/ui": 6.5.15 + "@types/node": ^14.0.10 || ^16.0.0 + "@types/webpack": ^4.41.26 + babel-loader: ^8.0.0 + case-sensitive-paths-webpack-plugin: ^2.3.0 + chalk: ^4.1.0 + core-js: ^3.8.2 + css-loader: ^3.6.0 + express: ^4.17.1 + file-loader: ^6.2.0 + find-up: ^5.0.0 + fs-extra: ^9.0.1 + html-webpack-plugin: ^4.0.0 + node-fetch: ^2.6.7 + pnp-webpack-plugin: 1.6.4 + read-pkg-up: ^7.0.1 + regenerator-runtime: ^0.13.7 + resolve-from: ^5.0.0 + style-loader: ^1.3.0 + telejson: ^6.0.8 + terser-webpack-plugin: ^4.2.3 + ts-dedent: ^2.0.0 + url-loader: ^4.1.1 + util-deprecate: ^1.0.2 + webpack: 4 + webpack-dev-middleware: ^3.7.3 + webpack-virtual-modules: ^0.2.2 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: c8547b64f80c87dd8590a3e7b784b6f72952607514f9e42858f89efce22ea077404033b25674aecc3874e61264ce74c38220b6130aecbce5361d182b018f7fc7 + languageName: node + linkType: hard + "@storybook/manager-webpack5@npm:~6.5.15": version: 6.5.15 resolution: "@storybook/manager-webpack5@npm:6.5.15" @@ -8812,7 +10366,7 @@ __metadata: languageName: node linkType: hard -"@storybook/node-logger@npm:6.5.15, @storybook/node-logger@npm:^6.1.14": +"@storybook/node-logger@npm:6.5.15": version: 6.5.15 resolution: "@storybook/node-logger@npm:6.5.15" dependencies: @@ -8825,6 +10379,32 @@ __metadata: languageName: node linkType: hard +"@storybook/node-logger@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/node-logger@npm:6.5.16" + dependencies: + "@types/npmlog": ^4.1.2 + chalk: ^4.1.0 + core-js: ^3.8.2 + npmlog: ^5.0.1 + pretty-hrtime: ^1.0.3 + checksum: 4ae47c03b6cec6b820e0e482e6f6675bf745fca5c124eb919240c0339b9f4a1b110c8fde7c5ddbc1748d3992773c61d37ba1f5c489b42279cf03517d4e1d51c5 + languageName: node + linkType: hard + +"@storybook/node-logger@npm:^6.1.14": + version: 6.5.14 + resolution: "@storybook/node-logger@npm:6.5.14" + dependencies: + "@types/npmlog": ^4.1.2 + chalk: ^4.1.0 + core-js: ^3.8.2 + npmlog: ^5.0.1 + pretty-hrtime: ^1.0.3 + checksum: d36d17816b20bf8409504f73b8fe7da2c1aa6b6f37c3cc910f544f4e3d9dfe8cfe5ab66930977413ab12a067f94477d5a1e3da8fa55c07af5dbc88160845ee47 + languageName: node + linkType: hard + "@storybook/postinstall@npm:6.5.15": version: 6.5.15 resolution: "@storybook/postinstall@npm:6.5.15" @@ -8834,6 +10414,15 @@ __metadata: languageName: node linkType: hard +"@storybook/postinstall@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/postinstall@npm:6.5.16" + dependencies: + core-js: ^3.8.2 + checksum: 023a19a0681675ce51f4acebf068f372e8657520680c67171c0a1b458f6009d1e444daa5680eeae7efb1088df184fbee61008548a73131d976201961dad65266 + languageName: node + linkType: hard + "@storybook/preview-web@npm:6.5.15": version: 6.5.15 resolution: "@storybook/preview-web@npm:6.5.15" @@ -8861,6 +10450,33 @@ __metadata: languageName: node linkType: hard +"@storybook/preview-web@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/preview-web@npm:6.5.16" + dependencies: + "@storybook/addons": 6.5.16 + "@storybook/channel-postmessage": 6.5.16 + "@storybook/client-logger": 6.5.16 + "@storybook/core-events": 6.5.16 + "@storybook/csf": 0.0.2--canary.4566f4d.1 + "@storybook/store": 6.5.16 + ansi-to-html: ^0.6.11 + core-js: ^3.8.2 + global: ^4.4.0 + lodash: ^4.17.21 + qs: ^6.10.0 + regenerator-runtime: ^0.13.7 + synchronous-promise: ^2.0.15 + ts-dedent: ^2.0.0 + unfetch: ^4.2.0 + util-deprecate: ^1.0.2 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 6161c96e9ee459ef93c3d972374ce339ae57d0c5fa25730007484e4824f79a34814110431db97031107558e5ce41259710f8a54564e8975db0215b78c5572a1b + languageName: node + linkType: hard + "@storybook/react-docgen-typescript-plugin@npm:1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0": version: 1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0 resolution: "@storybook/react-docgen-typescript-plugin@npm:1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0" @@ -8879,6 +10495,71 @@ __metadata: languageName: node linkType: hard +"@storybook/react@npm:~6.5.14": + version: 6.5.16 + resolution: "@storybook/react@npm:6.5.16" + dependencies: + "@babel/preset-flow": ^7.12.1 + "@babel/preset-react": ^7.12.10 + "@pmmmwh/react-refresh-webpack-plugin": ^0.5.3 + "@storybook/addons": 6.5.16 + "@storybook/client-logger": 6.5.16 + "@storybook/core": 6.5.16 + "@storybook/core-common": 6.5.16 + "@storybook/csf": 0.0.2--canary.4566f4d.1 + "@storybook/docs-tools": 6.5.16 + "@storybook/node-logger": 6.5.16 + "@storybook/react-docgen-typescript-plugin": 1.0.2-canary.6.9d540b91e815f8fc2f8829189deb00553559ff63.0 + "@storybook/semver": ^7.3.2 + "@storybook/store": 6.5.16 + "@types/estree": ^0.0.51 + "@types/node": ^14.14.20 || ^16.0.0 + "@types/webpack-env": ^1.16.0 + acorn: ^7.4.1 + acorn-jsx: ^5.3.1 + acorn-walk: ^7.2.0 + babel-plugin-add-react-displayname: ^0.0.5 + babel-plugin-react-docgen: ^4.2.1 + core-js: ^3.8.2 + escodegen: ^2.0.0 + fs-extra: ^9.0.1 + global: ^4.4.0 + html-tags: ^3.1.0 + lodash: ^4.17.21 + prop-types: ^15.7.2 + react-element-to-jsx-string: ^14.3.4 + react-refresh: ^0.11.0 + read-pkg-up: ^7.0.1 + regenerator-runtime: ^0.13.7 + ts-dedent: ^2.0.0 + util-deprecate: ^1.0.2 + webpack: ">=4.43.0 <6.0.0" + peerDependencies: + "@babel/core": ^7.11.5 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + require-from-string: ^2.0.2 + peerDependenciesMeta: + "@babel/core": + optional: true + "@storybook/builder-webpack4": + optional: true + "@storybook/builder-webpack5": + optional: true + "@storybook/manager-webpack4": + optional: true + "@storybook/manager-webpack5": + optional: true + typescript: + optional: true + bin: + build-storybook: bin/build.js + start-storybook: bin/index.js + storybook-server: bin/index.js + checksum: c5396e748ef13acdb2590dc15ff0b3d95d3599abd0c372786d707164d3f71e46836240195dcd6f4bce6f90d2792602f6d31373fc87e069ef3c73a63d1e9a1289 + languageName: node + linkType: hard + "@storybook/react@npm:~6.5.15": version: 6.5.15 resolution: "@storybook/react@npm:6.5.15" @@ -8944,6 +10625,22 @@ __metadata: languageName: node linkType: hard +"@storybook/router@npm:6.5.14": + version: 6.5.14 + resolution: "@storybook/router@npm:6.5.14" + dependencies: + "@storybook/client-logger": 6.5.14 + core-js: ^3.8.2 + memoizerific: ^1.11.3 + qs: ^6.10.0 + regenerator-runtime: ^0.13.7 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: ec2550568c02f45de5307e77928eaeb39413049944e994adbc397d9c7e083ac7e110886e40517ddae40e3879c172f458167682f1d73d0bb150bc93ab9dd61514 + languageName: node + linkType: hard + "@storybook/router@npm:6.5.15": version: 6.5.15 resolution: "@storybook/router@npm:6.5.15" @@ -8960,6 +10657,22 @@ __metadata: languageName: node linkType: hard +"@storybook/router@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/router@npm:6.5.16" + dependencies: + "@storybook/client-logger": 6.5.16 + core-js: ^3.8.2 + memoizerific: ^1.11.3 + qs: ^6.10.0 + regenerator-runtime: ^0.13.7 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 2812b93997026b1d85f02072d04f18e98e24de288efb73402f8d15ececd390e13dc620ef011268e09986c629f497ffa03230c2431e89b4e37c01b70761be2c6d + languageName: node + linkType: hard + "@storybook/semver@npm:^7.3.2": version: 7.3.2 resolution: "@storybook/semver@npm:7.3.2" @@ -8993,6 +10706,27 @@ __metadata: languageName: node linkType: hard +"@storybook/source-loader@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/source-loader@npm:6.5.16" + dependencies: + "@storybook/addons": 6.5.16 + "@storybook/client-logger": 6.5.16 + "@storybook/csf": 0.0.2--canary.4566f4d.1 + core-js: ^3.8.2 + estraverse: ^5.2.0 + global: ^4.4.0 + loader-utils: ^2.0.4 + lodash: ^4.17.21 + prettier: ">=2.2.1 <=2.3.0" + regenerator-runtime: ^0.13.7 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: a299acdd6f36add3222ef294e1118b7b1f38c2cd2b4648ebf9e1803a3ccf532c147dbe643a527915b570eb3ce36c4a17ca2b3566fa58a2a0a7821f0849ec3e07 + languageName: node + linkType: hard + "@storybook/store@npm:6.5.15": version: 6.5.15 resolution: "@storybook/store@npm:6.5.15" @@ -9019,6 +10753,32 @@ __metadata: languageName: node linkType: hard +"@storybook/store@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/store@npm:6.5.16" + dependencies: + "@storybook/addons": 6.5.16 + "@storybook/client-logger": 6.5.16 + "@storybook/core-events": 6.5.16 + "@storybook/csf": 0.0.2--canary.4566f4d.1 + core-js: ^3.8.2 + fast-deep-equal: ^3.1.3 + global: ^4.4.0 + lodash: ^4.17.21 + memoizerific: ^1.11.3 + regenerator-runtime: ^0.13.7 + slash: ^3.0.0 + stable: ^0.1.8 + synchronous-promise: ^2.0.15 + ts-dedent: ^2.0.0 + util-deprecate: ^1.0.2 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: f438fb020af240e23348742b2936a326bef1f7ffd489fe9f39cfd516310ab592a11609205fdacd11090b0c0b6bc72c75dff986085a6a97acc5efa64829a49309 + languageName: node + linkType: hard + "@storybook/telemetry@npm:6.5.15": version: 6.5.15 resolution: "@storybook/telemetry@npm:6.5.15" @@ -9039,6 +10799,26 @@ __metadata: languageName: node linkType: hard +"@storybook/telemetry@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/telemetry@npm:6.5.16" + dependencies: + "@storybook/client-logger": 6.5.16 + "@storybook/core-common": 6.5.16 + chalk: ^4.1.0 + core-js: ^3.8.2 + detect-package-manager: ^2.0.1 + fetch-retry: ^5.0.2 + fs-extra: ^9.0.1 + global: ^4.4.0 + isomorphic-unfetch: ^3.1.0 + nanoid: ^3.3.1 + read-pkg-up: ^7.0.1 + regenerator-runtime: ^0.13.7 + checksum: 21eef590b04db8ee85b0b1d875d8646e26492b3e90538a248314f92d6ab0642ec65db09c5d2bc0d7f547f0fa6b83ca4442bdc115b400861360e02d8cf179497e + languageName: node + linkType: hard + "@storybook/testing-library@npm:0.0.13, @storybook/testing-library@npm:~0.0.13": version: 0.0.13 resolution: "@storybook/testing-library@npm:0.0.13" @@ -9052,6 +10832,21 @@ __metadata: languageName: node linkType: hard +"@storybook/theming@npm:6.5.14": + version: 6.5.14 + resolution: "@storybook/theming@npm:6.5.14" + dependencies: + "@storybook/client-logger": 6.5.14 + core-js: ^3.8.2 + memoizerific: ^1.11.3 + regenerator-runtime: ^0.13.7 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: d139325dd51e8dfa58458a5c033104123b019fc02ddc899898e02de2b5d1358fd318b5def7ef82e6138420f9198e90d50b0fdfbea926987ac6852fc3a2e77c6d + languageName: node + linkType: hard + "@storybook/theming@npm:6.5.15, @storybook/theming@npm:~6.5.15": version: 6.5.15 resolution: "@storybook/theming@npm:6.5.15" @@ -9067,6 +10862,21 @@ __metadata: languageName: node linkType: hard +"@storybook/theming@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/theming@npm:6.5.16" + dependencies: + "@storybook/client-logger": 6.5.16 + core-js: ^3.8.2 + memoizerific: ^1.11.3 + regenerator-runtime: ^0.13.7 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: 349affa5c5208240291a5d24c73d852e220bfaf36b8fda70564aec1cac6070248ce7566ccb755c55a6ce0844ab2bbfd55881f6f788240b38cb407714e393c6f3 + languageName: node + linkType: hard + "@storybook/ui@npm:6.5.15": version: 6.5.15 resolution: "@storybook/ui@npm:6.5.15" @@ -9092,90 +10902,115 @@ __metadata: languageName: node linkType: hard -"@swc/core-darwin-arm64@npm:1.3.24": - version: 1.3.24 - resolution: "@swc/core-darwin-arm64@npm:1.3.24" +"@storybook/ui@npm:6.5.16": + version: 6.5.16 + resolution: "@storybook/ui@npm:6.5.16" + dependencies: + "@storybook/addons": 6.5.16 + "@storybook/api": 6.5.16 + "@storybook/channels": 6.5.16 + "@storybook/client-logger": 6.5.16 + "@storybook/components": 6.5.16 + "@storybook/core-events": 6.5.16 + "@storybook/router": 6.5.16 + "@storybook/semver": ^7.3.2 + "@storybook/theming": 6.5.16 + core-js: ^3.8.2 + memoizerific: ^1.11.3 + qs: ^6.10.0 + regenerator-runtime: ^0.13.7 + resolve-from: ^5.0.0 + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + checksum: bfebcf4d56dc5fd6024eaa08fe50aecc3c348670b7c0ec6b467680d64d525421580b9c98839bcaf1e2a9e69b78478a21c9943a9a392b49a0405b4784038b2eba + languageName: node + linkType: hard + +"@swc/core-darwin-arm64@npm:1.3.25": + version: 1.3.25 + resolution: "@swc/core-darwin-arm64@npm:1.3.25" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@swc/core-darwin-x64@npm:1.3.24": - version: 1.3.24 - resolution: "@swc/core-darwin-x64@npm:1.3.24" +"@swc/core-darwin-x64@npm:1.3.25": + version: 1.3.25 + resolution: "@swc/core-darwin-x64@npm:1.3.25" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@swc/core-linux-arm-gnueabihf@npm:1.3.24": - version: 1.3.24 - resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.24" +"@swc/core-linux-arm-gnueabihf@npm:1.3.25": + version: 1.3.25 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.25" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@swc/core-linux-arm64-gnu@npm:1.3.24": - version: 1.3.24 - resolution: "@swc/core-linux-arm64-gnu@npm:1.3.24" +"@swc/core-linux-arm64-gnu@npm:1.3.25": + version: 1.3.25 + resolution: "@swc/core-linux-arm64-gnu@npm:1.3.25" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-arm64-musl@npm:1.3.24": - version: 1.3.24 - resolution: "@swc/core-linux-arm64-musl@npm:1.3.24" +"@swc/core-linux-arm64-musl@npm:1.3.25": + version: 1.3.25 + resolution: "@swc/core-linux-arm64-musl@npm:1.3.25" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@swc/core-linux-x64-gnu@npm:1.3.24": - version: 1.3.24 - resolution: "@swc/core-linux-x64-gnu@npm:1.3.24" +"@swc/core-linux-x64-gnu@npm:1.3.25": + version: 1.3.25 + resolution: "@swc/core-linux-x64-gnu@npm:1.3.25" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-x64-musl@npm:1.3.24": - version: 1.3.24 - resolution: "@swc/core-linux-x64-musl@npm:1.3.24" +"@swc/core-linux-x64-musl@npm:1.3.25": + version: 1.3.25 + resolution: "@swc/core-linux-x64-musl@npm:1.3.25" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@swc/core-win32-arm64-msvc@npm:1.3.24": - version: 1.3.24 - resolution: "@swc/core-win32-arm64-msvc@npm:1.3.24" +"@swc/core-win32-arm64-msvc@npm:1.3.25": + version: 1.3.25 + resolution: "@swc/core-win32-arm64-msvc@npm:1.3.25" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@swc/core-win32-ia32-msvc@npm:1.3.24": - version: 1.3.24 - resolution: "@swc/core-win32-ia32-msvc@npm:1.3.24" +"@swc/core-win32-ia32-msvc@npm:1.3.25": + version: 1.3.25 + resolution: "@swc/core-win32-ia32-msvc@npm:1.3.25" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@swc/core-win32-x64-msvc@npm:1.3.24": - version: 1.3.24 - resolution: "@swc/core-win32-x64-msvc@npm:1.3.24" +"@swc/core-win32-x64-msvc@npm:1.3.25": + version: 1.3.25 + resolution: "@swc/core-win32-x64-msvc@npm:1.3.25" conditions: os=win32 & cpu=x64 languageName: node linkType: hard "@swc/core@npm:^1.3.24": - version: 1.3.24 - resolution: "@swc/core@npm:1.3.24" - dependencies: - "@swc/core-darwin-arm64": 1.3.24 - "@swc/core-darwin-x64": 1.3.24 - "@swc/core-linux-arm-gnueabihf": 1.3.24 - "@swc/core-linux-arm64-gnu": 1.3.24 - "@swc/core-linux-arm64-musl": 1.3.24 - "@swc/core-linux-x64-gnu": 1.3.24 - "@swc/core-linux-x64-musl": 1.3.24 - "@swc/core-win32-arm64-msvc": 1.3.24 - "@swc/core-win32-ia32-msvc": 1.3.24 - "@swc/core-win32-x64-msvc": 1.3.24 + version: 1.3.25 + resolution: "@swc/core@npm:1.3.25" + dependencies: + "@swc/core-darwin-arm64": 1.3.25 + "@swc/core-darwin-x64": 1.3.25 + "@swc/core-linux-arm-gnueabihf": 1.3.25 + "@swc/core-linux-arm64-gnu": 1.3.25 + "@swc/core-linux-arm64-musl": 1.3.25 + "@swc/core-linux-x64-gnu": 1.3.25 + "@swc/core-linux-x64-musl": 1.3.25 + "@swc/core-win32-arm64-msvc": 1.3.25 + "@swc/core-win32-ia32-msvc": 1.3.25 + "@swc/core-win32-x64-msvc": 1.3.25 dependenciesMeta: "@swc/core-darwin-arm64": optional: true @@ -9197,9 +11032,16 @@ __metadata: optional: true "@swc/core-win32-x64-msvc": optional: true - bin: - swcx: run_swcx.js - checksum: a27b842be129b83c116f804e63deaa51dbd5d9b77d6260888d549f6408df1dd05aeef20046ceacc9fd7458e6afda6723545249bd77f77086b98bd9bf84738c19 + checksum: de45a7dd871cc9497ad998d6a320d3c95cb9c74fdcb70590ff1f631e75001820d021bbfd5c463e9172afcb5ee47bffaa8fb893230e1329538c9f7afbd5ed45cf + languageName: node + linkType: hard + +"@swc/helpers@npm:^0.4.2": + version: 0.4.14 + resolution: "@swc/helpers@npm:0.4.14" + dependencies: + tslib: ^2.4.0 + checksum: 273fd3f3fc461a92f3790cc551ea054745c6d6959afbe1232e6d7aa1c722bbc114d308aab96bef5c78fc0303c85c7b472ef00e2253251cc89737f3b1af56e5a5 languageName: node linkType: hard @@ -9220,17 +11062,17 @@ __metadata: linkType: hard "@tanstack/react-query-devtools@npm:^4.19.1": - version: 4.19.1 - resolution: "@tanstack/react-query-devtools@npm:4.19.1" + version: 4.20.4 + resolution: "@tanstack/react-query-devtools@npm:4.20.4" dependencies: "@tanstack/match-sorter-utils": ^8.7.0 superjson: ^1.10.0 use-sync-external-store: ^1.2.0 peerDependencies: - "@tanstack/react-query": 4.19.1 + "@tanstack/react-query": 4.20.4 react: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: 0c40e7b55d06600ff8a2448f8679e52657f22f05ae397ccee83842ee9ce6dcd681f41bd33606adc6590535bb868561dce3da631233c339701fffa602ec1e381c + checksum: 77f55b3f7794ee95284969f4f5dfc8f81a856f31d9fa76343059e9580cb6099841cc63a540c6cdcc841be2c2249affdf49e1ad247f2c150472f856f2ecda9056 languageName: node linkType: hard @@ -9336,6 +11178,20 @@ __metadata: languageName: node linkType: hard +"@testing-library/react@npm:~13.4.0": + version: 13.4.0 + resolution: "@testing-library/react@npm:13.4.0" + dependencies: + "@babel/runtime": ^7.12.5 + "@testing-library/dom": ^8.5.0 + "@types/react-dom": ^18.0.0 + peerDependencies: + react: ^18.0.0 + react-dom: ^18.0.0 + checksum: 51ec548c1fdb1271089a5c63e0908f0166f2c7fcd9cacd3108ebbe0ce64cb4351812d885892020dc37608418cfb15698514856502b3cab0e5cc58d6cc1bd4a3e + languageName: node + linkType: hard + "@testing-library/user-event@npm:^13.2.1, @testing-library/user-event@npm:~13.5.0": version: 13.5.0 resolution: "@testing-library/user-event@npm:13.5.0" @@ -9421,7 +11277,20 @@ __metadata: languageName: node linkType: hard -"@types/babel__core@npm:^7.0.0, @types/babel__core@npm:^7.1.14, @types/babel__core@npm:^7.1.20": +"@types/babel__core@npm:^7.0.0, @types/babel__core@npm:^7.1.14": + version: 7.1.19 + resolution: "@types/babel__core@npm:7.1.19" + dependencies: + "@babel/parser": ^7.1.0 + "@babel/types": ^7.0.0 + "@types/babel__generator": "*" + "@types/babel__template": "*" + "@types/babel__traverse": "*" + checksum: 8c9fa87a1c2224cbec251683a58bebb0d74c497118034166aaa0491a4e2627998a6621fc71f8a60ffd27d9c0c52097defedf7637adc6618d0331c15adb302338 + languageName: node + linkType: hard + +"@types/babel__core@npm:^7.1.20": version: 7.1.20 resolution: "@types/babel__core@npm:7.1.20" dependencies: @@ -9664,7 +11533,17 @@ __metadata: languageName: node linkType: hard -"@types/eslint@npm:*, @types/eslint@npm:^8.4.10": +"@types/eslint@npm:*": + version: 8.4.5 + resolution: "@types/eslint@npm:8.4.5" + dependencies: + "@types/estree": "*" + "@types/json-schema": "*" + checksum: 428b0c971a50adb0d08621e76f21b284580a0052a31341a0e6d553f72b54cd0142d549aa1497c7e3bc56e9f6bcc27286e66e0216e1ba76d1a5ecd2279c40bc8c + languageName: node + linkType: hard + +"@types/eslint@npm:^8.4.10": version: 8.4.10 resolution: "@types/eslint@npm:8.4.10" dependencies: @@ -10306,6 +12185,15 @@ __metadata: languageName: node linkType: hard +"@types/react-dom@npm:^18": + version: 18.0.10 + resolution: "@types/react-dom@npm:18.0.10" + dependencies: + "@types/react": "*" + checksum: ff8282d5005a0b1cd95fb65bf79d3d8485e4cfe2aaf052129033a178684b940014a3f4536bc20d573f8a01cf4c6f4770c74988cef7c2b5cac3041d9f172647e3 + languageName: node + linkType: hard + "@types/react-dom@npm:^18.0.0": version: 18.0.6 resolution: "@types/react-dom@npm:18.0.6" @@ -10326,6 +12214,17 @@ __metadata: languageName: node linkType: hard +"@types/react@npm:^18.0.26": + version: 18.0.26 + resolution: "@types/react@npm:18.0.26" + dependencies: + "@types/prop-types": "*" + "@types/scheduler": "*" + csstype: ^3.0.2 + checksum: b62f0ea3cdfa68e106391728325057ad36f1bde7ba2dfae029472c47e01e482bc77c6ba4f1dad59f3f04ee81cb597618ff7c30a33c157c0a20462b6dd6aa2d4d + languageName: node + linkType: hard + "@types/responselike@npm:^1.0.0": version: 1.0.0 resolution: "@types/responselike@npm:1.0.0" @@ -11309,6 +13208,13 @@ __metadata: languageName: node linkType: hard +"abs-svg-path@npm:^0.1.1": + version: 0.1.1 + resolution: "abs-svg-path@npm:0.1.1" + checksum: af1a167c09e8bdb76c80adca7333f3d828e5b50e37b9702aa03675e271919e7b1eeaa35cce939970ecba14769953b7465ea34c2129ab683ddff9d973a07f164f + languageName: node + linkType: hard + "abstract-logging@npm:^2.0.0": version: 2.0.1 resolution: "abstract-logging@npm:2.0.1" @@ -11973,7 +13879,20 @@ __metadata: languageName: node linkType: hard -"array-includes@npm:^3.0.3, array-includes@npm:^3.1.4, array-includes@npm:^3.1.6": +"array-includes@npm:^3.0.3, array-includes@npm:^3.1.4": + version: 3.1.5 + resolution: "array-includes@npm:3.1.5" + dependencies: + call-bind: ^1.0.2 + define-properties: ^1.1.4 + es-abstract: ^1.19.5 + get-intrinsic: ^1.1.1 + is-string: ^1.0.7 + checksum: f6f24d834179604656b7bec3e047251d5cc87e9e87fab7c175c61af48e80e75acd296017abcde21fb52292ab6a2a449ab2ee37213ee48c8709f004d75983f9c5 + languageName: node + linkType: hard + +"array-includes@npm:^3.1.6": version: 3.1.6 resolution: "array-includes@npm:3.1.6" dependencies: @@ -12034,7 +13953,19 @@ __metadata: languageName: node linkType: hard -"array.prototype.flatmap@npm:^1.2.1, array.prototype.flatmap@npm:^1.3.1": +"array.prototype.flatmap@npm:^1.2.1": + version: 1.3.0 + resolution: "array.prototype.flatmap@npm:1.3.0" + dependencies: + call-bind: ^1.0.2 + define-properties: ^1.1.3 + es-abstract: ^1.19.2 + es-shim-unscopables: ^1.0.0 + checksum: 818538f39409c4045d874be85df0dbd195e1446b14d22f95bdcfefea44ae77db44e42dcd89a559254ec5a7c8b338cfc986cc6d641e3472f9a5326b21eb2976a2 + languageName: node + linkType: hard + +"array.prototype.flatmap@npm:^1.3.1": version: 1.3.1 resolution: "array.prototype.flatmap@npm:1.3.1" dependencies: @@ -12443,9 +14374,9 @@ __metadata: languageName: node linkType: hard -"babel-loader@npm:^8.0.0, babel-loader@npm:^8.3.0": - version: 8.3.0 - resolution: "babel-loader@npm:8.3.0" +"babel-loader@npm:^8.0.0, babel-loader@npm:~8.2.5": + version: 8.2.5 + resolution: "babel-loader@npm:8.2.5" dependencies: find-cache-dir: ^3.3.1 loader-utils: ^2.0.0 @@ -12454,13 +14385,13 @@ __metadata: peerDependencies: "@babel/core": ^7.0.0 webpack: ">=2" - checksum: d48bcf9e030e598656ad3ff5fb85967db2eaaf38af5b4a4b99d25618a2057f9f100e6b231af2a46c1913206db506115ca7a8cbdf52c9c73d767070dae4352ab5 + checksum: a6605557885eabbc3250412405f2c63ca87287a95a439c643fdb47d5ea3d5326f72e43ab97be070316998cb685d5dfbc70927ce1abe8be7a6a4f5919287773fb languageName: node linkType: hard -"babel-loader@npm:~8.2.5": - version: 8.2.5 - resolution: "babel-loader@npm:8.2.5" +"babel-loader@npm:^8.3.0": + version: 8.3.0 + resolution: "babel-loader@npm:8.3.0" dependencies: find-cache-dir: ^3.3.1 loader-utils: ^2.0.0 @@ -12469,7 +14400,7 @@ __metadata: peerDependencies: "@babel/core": ^7.0.0 webpack: ">=2" - checksum: a6605557885eabbc3250412405f2c63ca87287a95a439c643fdb47d5ea3d5326f72e43ab97be070316998cb685d5dfbc70927ce1abe8be7a6a4f5919287773fb + checksum: d48bcf9e030e598656ad3ff5fb85967db2eaaf38af5b4a4b99d25618a2057f9f100e6b231af2a46c1913206db506115ca7a8cbdf52c9c73d767070dae4352ab5 languageName: node linkType: hard @@ -12803,7 +14734,7 @@ __metadata: languageName: node linkType: hard -"base64-js@npm:^1.0.2, base64-js@npm:^1.3.0, base64-js@npm:^1.3.1": +"base64-js@npm:^1.0.2, base64-js@npm:^1.1.2, base64-js@npm:^1.3.0, base64-js@npm:^1.3.1": version: 1.5.1 resolution: "base64-js@npm:1.5.1" checksum: 669632eb3745404c2f822a18fc3a0122d2f9a7a13f7fb8b5823ee19d1d2ff9ee5b52c53367176ea4ad093c332fd5ab4bd0ebae5a8e27917a4105a4cfc86b1005 @@ -13239,6 +15170,15 @@ __metadata: languageName: node linkType: hard +"brotli@npm:^1.3.2": + version: 1.3.3 + resolution: "brotli@npm:1.3.3" + dependencies: + base64-js: ^1.1.2 + checksum: 2c97329f4ccb8e4332cedd2f63b85c2e15ffb305b1cbf046df86201434caf93cb7992ca73c0f7053b6a1417f595069ec7783c26e01510cefc10035a0f466e594 + languageName: node + linkType: hard + "browser-assert@npm:^1.2.1": version: 1.2.1 resolution: "browser-assert@npm:1.2.1" @@ -13381,12 +15321,12 @@ __metadata: languageName: node linkType: hard -"bson@npm:*, bson@npm:^4.6.4, bson@npm:^4.7.0": - version: 4.7.0 - resolution: "bson@npm:4.7.0" +"bson@npm:*, bson@npm:^4.6.3, bson@npm:^4.6.4": + version: 4.6.4 + resolution: "bson@npm:4.6.4" dependencies: buffer: ^5.6.0 - checksum: 83e7b64afdad5a505073a7e6206e7b345f59e7888fbcb1948fba72b6101a1baf58b7499314f8e24b650567665f7973eda048aabbb1ddcfbadfba7d6c6b0f5e83 + checksum: f56375865c8fc048179075296019a0d2e058edbbb6692e54e2751da738840968de678a48a2276faf2ec8f8b36c5c26f14670ab4d414fe68f0169215efe15d570 languageName: node linkType: hard @@ -13397,6 +15337,15 @@ __metadata: languageName: node linkType: hard +"bson@npm:^4.7.0": + version: 4.7.0 + resolution: "bson@npm:4.7.0" + dependencies: + buffer: ^5.6.0 + checksum: 83e7b64afdad5a505073a7e6206e7b345f59e7888fbcb1948fba72b6101a1baf58b7499314f8e24b650567665f7973eda048aabbb1ddcfbadfba7d6c6b0f5e83 + languageName: node + linkType: hard + "buffer-alloc-unsafe@npm:^1.1.0": version: 1.1.0 resolution: "buffer-alloc-unsafe@npm:1.1.0" @@ -14461,6 +16410,13 @@ __metadata: languageName: node linkType: hard +"clone@npm:^2.1.2": + version: 2.1.2 + resolution: "clone@npm:2.1.2" + checksum: aaf106e9bc025b21333e2f4c12da539b568db4925c0501a1bf4070836c9e848c892fa22c35548ce0d1132b08bbbfa17a00144fe58fccdab6fa900fec4250f67d + languageName: node + linkType: hard + "clsx@npm:1.1.0": version: 1.1.0 resolution: "clsx@npm:1.1.0" @@ -14570,6 +16526,16 @@ __metadata: languageName: node linkType: hard +"color-string@npm:^1.5.3": + version: 1.9.1 + resolution: "color-string@npm:1.9.1" + dependencies: + color-name: ^1.0.0 + simple-swizzle: ^0.2.2 + checksum: c13fe7cff7885f603f49105827d621ce87f4571d78ba28ef4a3f1a104304748f620615e6bf065ecd2145d0d9dad83a3553f52bb25ede7239d18e9f81622f1cc5 + languageName: node + linkType: hard + "color-string@npm:^1.6.0, color-string@npm:^1.9.0": version: 1.9.0 resolution: "color-string@npm:1.9.0" @@ -15246,7 +17212,7 @@ __metadata: languageName: node linkType: hard -"cross-fetch@npm:3.1.5": +"cross-fetch@npm:3.1.5, cross-fetch@npm:^3.1.5": version: 3.1.5 resolution: "cross-fetch@npm:3.1.5" dependencies: @@ -15325,7 +17291,7 @@ __metadata: languageName: node linkType: hard -"crypto-js@npm:^4.1.1": +"crypto-js@npm:^4.0.0, crypto-js@npm:^4.1.1": version: 4.1.1 resolution: "crypto-js@npm:4.1.1" checksum: b3747c12ee3a7632fab3b3e171ea50f78b182545f0714f6d3e7e2858385f0f4101a15f2517e033802ce9d12ba50a391575ff4638c9de3dd9b2c4bc47768d5425 @@ -16273,6 +18239,13 @@ __metadata: languageName: node linkType: hard +"denque@npm:^2.0.1": + version: 2.0.1 + resolution: "denque@npm:2.0.1" + checksum: ec398d1e3c6c8d4f5213dcf9ad74d7faa3b461e29a0019c9742b49a97ac5e16aa7134db45fa9d841e318e7722dd1ba670a474fde9a5b0d870b3a5fc6fe914c30 + languageName: node + linkType: hard + "depd@npm:2.0.0, depd@npm:~2.0.0": version: 2.0.0 resolution: "depd@npm:2.0.0" @@ -16398,6 +18371,13 @@ __metadata: languageName: node linkType: hard +"dfa@npm:^1.2.0": + version: 1.2.0 + resolution: "dfa@npm:1.2.0" + checksum: 83b954b856a4a0c4282550a35532ac66dfc6362a08500a4b09c0d7a306c6813cbf50cc18d81bf8997d98559fc2675df89f6ece255d92517cc46f6bf8ef5ff727 + languageName: node + linkType: hard + "diff-sequences@npm:^27.5.1": version: 27.5.1 resolution: "diff-sequences@npm:27.5.1" @@ -16909,6 +18889,13 @@ __metadata: languageName: node linkType: hard +"emoji-assets@npm:^7.0.1": + version: 7.0.1 + resolution: "emoji-assets@npm:7.0.1" + checksum: b8a5239a3c429b7b5deb00411ed065c6d96bc2612b887ada513457ae639ea953ebc6e3af50d7666a29be2e53bbcde189092166cba1a502297467ea8473db3106 + languageName: node + linkType: hard + "emoji-mart@npm:^3.0.1": version: 3.0.1 resolution: "emoji-mart@npm:3.0.1" @@ -16942,6 +18929,13 @@ __metadata: languageName: node linkType: hard +"emoji-toolkit@npm:^7.0.0": + version: 7.0.0 + resolution: "emoji-toolkit@npm:7.0.0" + checksum: 0e1ad04dbbbd1ab6d0c735ed2ab24deb8ba2b9d9901367c5a7b12cf7c1f35803cd6082af26e96f7a2bd7371d8af7493aa24664e54df365729fdf6785f28860e3 + languageName: node + linkType: hard + "emojione-assets@npm:^4.5.0": version: 4.5.0 resolution: "emojione-assets@npm:4.5.0" @@ -17133,7 +19127,38 @@ __metadata: languageName: node linkType: hard -"es-abstract@npm:^1.17.2, es-abstract@npm:^1.18.5, es-abstract@npm:^1.19.0, es-abstract@npm:^1.19.1, es-abstract@npm:^1.19.5, es-abstract@npm:^1.20.4": +"es-abstract@npm:^1.17.2, es-abstract@npm:^1.18.5, es-abstract@npm:^1.19.0, es-abstract@npm:^1.19.1, es-abstract@npm:^1.19.2, es-abstract@npm:^1.19.5": + version: 1.20.1 + resolution: "es-abstract@npm:1.20.1" + dependencies: + call-bind: ^1.0.2 + es-to-primitive: ^1.2.1 + function-bind: ^1.1.1 + function.prototype.name: ^1.1.5 + get-intrinsic: ^1.1.1 + get-symbol-description: ^1.0.0 + has: ^1.0.3 + has-property-descriptors: ^1.0.0 + has-symbols: ^1.0.3 + internal-slot: ^1.0.3 + is-callable: ^1.2.4 + is-negative-zero: ^2.0.2 + is-regex: ^1.1.4 + is-shared-array-buffer: ^1.0.2 + is-string: ^1.0.7 + is-weakref: ^1.0.2 + object-inspect: ^1.12.0 + object-keys: ^1.1.1 + object.assign: ^4.1.2 + regexp.prototype.flags: ^1.4.3 + string.prototype.trimend: ^1.0.5 + string.prototype.trimstart: ^1.0.5 + unbox-primitive: ^1.0.2 + checksum: 28da27ae0ed9c76df7ee8ef5c278df79dcfdb554415faf7068bb7c58f8ba8e2a16bfb59e586844be6429ab4c302ca7748979d48442224cb1140b051866d74b7f + languageName: node + linkType: hard + +"es-abstract@npm:^1.20.4": version: 1.20.4 resolution: "es-abstract@npm:1.20.4" dependencies: @@ -17611,6 +19636,55 @@ __metadata: languageName: node linkType: hard +"eslint@npm:^8.12.0": + version: 8.31.0 + resolution: "eslint@npm:8.31.0" + dependencies: + "@eslint/eslintrc": ^1.4.1 + "@humanwhocodes/config-array": ^0.11.8 + "@humanwhocodes/module-importer": ^1.0.1 + "@nodelib/fs.walk": ^1.2.8 + ajv: ^6.10.0 + chalk: ^4.0.0 + cross-spawn: ^7.0.2 + debug: ^4.3.2 + doctrine: ^3.0.0 + escape-string-regexp: ^4.0.0 + eslint-scope: ^7.1.1 + eslint-utils: ^3.0.0 + eslint-visitor-keys: ^3.3.0 + espree: ^9.4.0 + esquery: ^1.4.0 + esutils: ^2.0.2 + fast-deep-equal: ^3.1.3 + file-entry-cache: ^6.0.1 + find-up: ^5.0.0 + glob-parent: ^6.0.2 + globals: ^13.19.0 + grapheme-splitter: ^1.0.4 + ignore: ^5.2.0 + import-fresh: ^3.0.0 + imurmurhash: ^0.1.4 + is-glob: ^4.0.0 + is-path-inside: ^3.0.3 + js-sdsl: ^4.1.4 + js-yaml: ^4.1.0 + json-stable-stringify-without-jsonify: ^1.0.1 + levn: ^0.4.1 + lodash.merge: ^4.6.2 + minimatch: ^3.1.2 + natural-compare: ^1.4.0 + optionator: ^0.9.1 + regexpp: ^3.2.0 + strip-ansi: ^6.0.1 + strip-json-comments: ^3.1.0 + text-table: ^0.2.0 + bin: + eslint: bin/eslint.js + checksum: 5e5688bb864edc6b12d165849994812eefa67fb3fc44bb26f53659b63edcd8bcc68389d27cc6cc9e5b79ee22f24b6f311fa3ed047bddcafdec7d84c1b5561e4f + languageName: node + linkType: hard + "eslint@npm:^8.29.0, eslint@npm:~8.29.0": version: 8.29.0 resolution: "eslint@npm:8.29.0" @@ -18809,6 +20883,23 @@ __metadata: languageName: node linkType: hard +"fontkit@npm:^2.0.2": + version: 2.0.2 + resolution: "fontkit@npm:2.0.2" + dependencies: + "@swc/helpers": ^0.4.2 + brotli: ^1.3.2 + clone: ^2.1.2 + dfa: ^1.2.0 + fast-deep-equal: ^3.1.3 + restructure: ^3.0.0 + tiny-inflate: ^1.0.3 + unicode-properties: ^1.4.0 + unicode-trie: ^2.0.0 + checksum: ef5841a46cb46af56496646136a4e61f014360caacb8d69e3a4abfda9d89d66d2a68b0855494c070c4e0675fb57724c9db37154913413609ebb065d35b23838e + languageName: node + linkType: hard + "for-in@npm:^1.0.2": version: 1.0.2 resolution: "for-in@npm:1.0.2" @@ -19317,7 +21408,18 @@ __metadata: languageName: node linkType: hard -"get-intrinsic@npm:^1.0.2, get-intrinsic@npm:^1.1.0, get-intrinsic@npm:^1.1.1, get-intrinsic@npm:^1.1.3": +"get-intrinsic@npm:^1.0.2, get-intrinsic@npm:^1.1.0, get-intrinsic@npm:^1.1.1": + version: 1.1.1 + resolution: "get-intrinsic@npm:1.1.1" + dependencies: + function-bind: ^1.1.1 + has: ^1.0.3 + has-symbols: ^1.0.1 + checksum: a9fe2ca8fa3f07f9b0d30fb202bcd01f3d9b9b6b732452e79c48e79f7d6d8d003af3f9e38514250e3553fdc83c61650851cb6870832ac89deaaceb08e3721a17 + languageName: node + linkType: hard + +"get-intrinsic@npm:^1.1.3": version: 1.1.3 resolution: "get-intrinsic@npm:1.1.3" dependencies: @@ -19684,6 +21786,15 @@ __metadata: languageName: node linkType: hard +"globals@npm:^13.19.0": + version: 13.19.0 + resolution: "globals@npm:13.19.0" + dependencies: + type-fest: ^0.20.2 + checksum: a000dbd00bcf28f0941d8a29c3522b1c3b8e4bfe4e60e262c477a550c3cbbe8dbe2925a6905f037acd40f9a93c039242e1f7079c76b0fd184bc41dcc3b5c8e2e + languageName: node + linkType: hard + "globalthis@npm:^1.0.0": version: 1.0.2 resolution: "globalthis@npm:1.0.2" @@ -20435,6 +22546,22 @@ __metadata: languageName: node linkType: hard +"hsl-to-hex@npm:^1.0.0": + version: 1.0.0 + resolution: "hsl-to-hex@npm:1.0.0" + dependencies: + hsl-to-rgb-for-reals: ^1.1.0 + checksum: e748cea0d9cdf444727bd3fc3f62515d0c5806ad4b52850730d365e54f6f0ae1e41e2076ab17de8523ab5ebdd30c62323f26b2cdd383529755ad27d1a33965b8 + languageName: node + linkType: hard + +"hsl-to-rgb-for-reals@npm:^1.1.0": + version: 1.1.1 + resolution: "hsl-to-rgb-for-reals@npm:1.1.1" + checksum: b31452617e6c399509c5b8016999d659f9e347e71290da287bf0f536da031609d51b240535dc8eb3dbd7770a7b367d9896ab6a13794db7d16e4cf86e363e156f + languageName: node + linkType: hard + "hsla-regex@npm:^1.0.0": version: 1.0.0 resolution: "hsla-regex@npm:1.0.0" @@ -20837,6 +22964,13 @@ __metadata: languageName: node linkType: hard +"hyphen@npm:^1.6.4": + version: 1.6.4 + resolution: "hyphen@npm:1.6.4" + checksum: 3ee1a69004f95717ffd7f97ab2b20ca09ba6b16714bdc9fbdd5aca6bfeec14dbb2a3cd6d6286c77d54770f7c0fa99b0bd66cf3d1681b0f12f549cfc843379fda + languageName: node + linkType: hard + "i18next-http-backend@npm:^1.4.1": version: 1.4.1 resolution: "i18next-http-backend@npm:1.4.1" @@ -21590,7 +23724,14 @@ __metadata: languageName: node linkType: hard -"is-callable@npm:^1.1.4, is-callable@npm:^1.2.7": +"is-callable@npm:^1.1.4, is-callable@npm:^1.2.4": + version: 1.2.4 + resolution: "is-callable@npm:1.2.4" + checksum: 1a28d57dc435797dae04b173b65d6d1e77d4f16276e9eff973f994eadcfdc30a017e6a597f092752a083c1103cceb56c91e3dadc6692fedb9898dfaba701575f + languageName: node + linkType: hard + +"is-callable@npm:^1.2.7": version: 1.2.7 resolution: "is-callable@npm:1.2.7" checksum: 61fd57d03b0d984e2ed3720fb1c7a897827ea174bd44402878e059542ea8c4aeedee0ea0985998aa5cc2736b2fa6e271c08587addb5b3959ac52cf665173d1ac @@ -22206,7 +24347,7 @@ __metadata: languageName: node linkType: hard -"is-url@npm:^1.1.0": +"is-url@npm:^1.1.0, is-url@npm:^1.2.4": version: 1.2.4 resolution: "is-url@npm:1.2.4" checksum: 100e74b3b1feab87a43ef7653736e88d997eb7bd32e71fd3ebc413e58c1cbe56269699c776aaea84244b0567f2a7d68dfaa512a062293ed2f9fdecb394148432 @@ -23763,6 +25904,15 @@ __metadata: languageName: node linkType: hard +"json5@npm:^2.2.3": + version: 2.2.3 + resolution: "json5@npm:2.2.3" + bin: + json5: lib/cli.js + checksum: 2a7436a93393830bce797d4626275152e37e877b265e94ca69c99e3d20c2b9dab021279146a39cdb700e71b2dd32a4cebd1514cd57cee102b1af906ce5040349 + languageName: node + linkType: hard + "jsonfile@npm:^2.1.0": version: 2.4.0 resolution: "jsonfile@npm:2.4.0" @@ -24375,7 +26525,18 @@ __metadata: languageName: node linkType: hard -"loader-utils@npm:^2.0.0, loader-utils@npm:^2.0.4": +"loader-utils@npm:^2.0.0": + version: 2.0.2 + resolution: "loader-utils@npm:2.0.2" + dependencies: + big.js: ^5.2.2 + emojis-list: ^3.0.0 + json5: ^2.1.2 + checksum: 9078d1ed47cadc57f4c6ddbdb2add324ee7da544cea41de3b7f1128e8108fcd41cd3443a85b7ee8d7d8ac439148aa221922774efe4cf87506d4fb054d5889303 + languageName: node + linkType: hard + +"loader-utils@npm:^2.0.4": version: 2.0.4 resolution: "loader-utils@npm:2.0.4" dependencies: @@ -24587,7 +26748,7 @@ __metadata: languageName: node linkType: hard -"lodash@npm:4, lodash@npm:^4.17.11, lodash@npm:^4.17.12, lodash@npm:^4.17.14, lodash@npm:^4.17.15, lodash@npm:^4.17.19, lodash@npm:^4.17.20, lodash@npm:^4.17.21, lodash@npm:^4.17.4, lodash@npm:^4.7.0": +"lodash@npm:4, lodash@npm:>=3.8.0, lodash@npm:^4.17.11, lodash@npm:^4.17.12, lodash@npm:^4.17.14, lodash@npm:^4.17.15, lodash@npm:^4.17.19, lodash@npm:^4.17.20, lodash@npm:^4.17.21, lodash@npm:^4.17.4, lodash@npm:^4.7.0": version: 4.17.21 resolution: "lodash@npm:4.17.21" checksum: eb835a2e51d381e561e508ce932ea50a8e5a68f4ebdd771ea240d3048244a8d13658acbd502cd4829768c56f2e16bdd4340b9ea141297d472517b83868e677f7 @@ -25129,6 +27290,13 @@ __metadata: languageName: node linkType: hard +"media-engine@npm:^1.0.3": + version: 1.0.3 + resolution: "media-engine@npm:1.0.3" + checksum: 3c2834077e7223d95cc137e4d13f777750887748a04b3aa29d1abfb05b35ad483ea074b7798827aedc29a6a6f6da299c5822d1ce99414d6c9c81b4299dbbd85a + languageName: node + linkType: hard + "media-typer@npm:0.3.0": version: 0.3.0 resolution: "media-typer@npm:0.3.0" @@ -25966,6 +28134,15 @@ __metadata: languageName: node linkType: hard +"moment-timezone@npm:^0.5.40": + version: 0.5.40 + resolution: "moment-timezone@npm:0.5.40" + dependencies: + moment: ">= 2.9.0" + checksum: 6f6be5412b37fd937bb143efe74bf65b2c3f115fd967a6dc13b717a126ed6dd198bff6db6e179d69a089e20ac03ce7622c6b5598dd585005195554487a91b528 + languageName: node + linkType: hard + "moment@npm:>= 2.9.0, moment@npm:^2.10.2, moment@npm:^2.29.1, moment@npm:^2.29.4": version: 2.29.4 resolution: "moment@npm:2.29.4" @@ -25973,6 +28150,27 @@ __metadata: languageName: node linkType: hard +"mongo-message-queue@npm:^1.0.0": + version: 1.0.0 + resolution: "mongo-message-queue@npm:1.0.0" + dependencies: + lodash: ">=3.8.0" + peerDependencies: + mongodb: ">= 4.0.0 < 5" + checksum: 960652db224899cdbb614309a6b03643e7df6df429101f807c5562cecc54f45af3f5ccb6ec69a058dcb2a9b9fcbd428b487009147b2e03556957bbd391d6f70c + languageName: node + linkType: hard + +"mongodb-connection-string-url@npm:^2.5.2": + version: 2.5.2 + resolution: "mongodb-connection-string-url@npm:2.5.2" + dependencies: + "@types/whatwg-url": ^8.2.1 + whatwg-url: ^11.0.0 + checksum: bd13af7d62d33e2d6e5217692961e34b2dafbbba5f41d361417257592754df3e925efc00fa8a5e038624e284245dab39e913d5f06ff962feede86d5f58fc5827 + languageName: node + linkType: hard + "mongodb-connection-string-url@npm:^2.5.4": version: 2.6.0 resolution: "mongodb-connection-string-url@npm:2.6.0" @@ -26049,7 +28247,7 @@ __metadata: languageName: node linkType: hard -"mongodb@npm:^4.12.1, mongodb@npm:^4.3.1": +"mongodb@npm:^4.12.1": version: 4.12.1 resolution: "mongodb@npm:4.12.1" dependencies: @@ -26067,6 +28265,22 @@ __metadata: languageName: node linkType: hard +"mongodb@npm:^4.3.1": + version: 4.7.0 + resolution: "mongodb@npm:4.7.0" + dependencies: + bson: ^4.6.3 + denque: ^2.0.1 + mongodb-connection-string-url: ^2.5.2 + saslprep: ^1.0.3 + socks: ^2.6.2 + dependenciesMeta: + saslprep: + optional: true + checksum: 6c8ddf1d14a4392d83702aa532e5a1e6deaa501cfd66e352a1226358422716e340465ca4b0f2c5d8f660bf15c06456059953fb99482f714c1dab408ecfe3aeea + languageName: node + linkType: hard + "moo@npm:^0.5.0, moo@npm:^0.5.1": version: 0.5.1 resolution: "moo@npm:0.5.1" @@ -26686,6 +28900,15 @@ __metadata: languageName: node linkType: hard +"normalize-svg-path@npm:^1.1.0": + version: 1.1.0 + resolution: "normalize-svg-path@npm:1.1.0" + dependencies: + svg-arc-to-cubic-bezier: ^3.0.0 + checksum: 106e108b2f99e9e222a1c6edfc859523c6c3c2b0a6ba64743ed08af120b23b9bc2c16682bc2ae043a24c011c34c8252376c68525cf11735c6f110b571740eb2e + languageName: node + linkType: hard + "normalize-url@npm:1.9.1": version: 1.9.1 resolution: "normalize-url@npm:1.9.1" @@ -26975,7 +29198,14 @@ __metadata: languageName: node linkType: hard -"object-inspect@npm:^1.12.0, object-inspect@npm:^1.12.2, object-inspect@npm:^1.9.0": +"object-inspect@npm:^1.12.0, object-inspect@npm:^1.9.0": + version: 1.12.0 + resolution: "object-inspect@npm:1.12.0" + checksum: 2b36d4001a9c921c6b342e2965734519c9c58c355822243c3207fbf0aac271f8d44d30d2d570d450b2cc6f0f00b72bcdba515c37827d2560e5f22b1899a31cf4 + languageName: node + linkType: hard + +"object-inspect@npm:^1.12.2": version: 1.12.2 resolution: "object-inspect@npm:1.12.2" checksum: a534fc1b8534284ed71f25ce3a496013b7ea030f3d1b77118f6b7b1713829262be9e6243acbcb3ef8c626e2b64186112cb7f6db74e37b2789b9c789ca23048b2 @@ -27015,7 +29245,19 @@ __metadata: languageName: node linkType: hard -"object.assign@npm:^4.1.2, object.assign@npm:^4.1.4": +"object.assign@npm:^4.1.2": + version: 4.1.2 + resolution: "object.assign@npm:4.1.2" + dependencies: + call-bind: ^1.0.0 + define-properties: ^1.1.3 + has-symbols: ^1.0.1 + object-keys: ^1.1.1 + checksum: d621d832ed7b16ac74027adb87196804a500d80d9aca536fccb7ba48d33a7e9306a75f94c1d29cbfa324bc091bfc530bc24789568efdaee6a47fcfa298993814 + languageName: node + linkType: hard + +"object.assign@npm:^4.1.4": version: 4.1.4 resolution: "object.assign@npm:4.1.4" dependencies: @@ -27027,7 +29269,18 @@ __metadata: languageName: node linkType: hard -"object.entries@npm:^1.1.0, object.entries@npm:^1.1.6": +"object.entries@npm:^1.1.0": + version: 1.1.5 + resolution: "object.entries@npm:1.1.5" + dependencies: + call-bind: ^1.0.2 + define-properties: ^1.1.3 + es-abstract: ^1.19.1 + checksum: d658696f74fd222060d8428d2a9fda2ce736b700cb06f6bdf4a16a1892d145afb746f453502b2fa55d1dca8ead6f14ddbcf66c545df45adadea757a6c4cd86c7 + languageName: node + linkType: hard + +"object.entries@npm:^1.1.6": version: 1.1.6 resolution: "object.entries@npm:1.1.6" dependencies: @@ -27038,7 +29291,18 @@ __metadata: languageName: node linkType: hard -"object.fromentries@npm:^2.0.0 || ^1.0.0, object.fromentries@npm:^2.0.6": +"object.fromentries@npm:^2.0.0 || ^1.0.0": + version: 2.0.5 + resolution: "object.fromentries@npm:2.0.5" + dependencies: + call-bind: ^1.0.2 + define-properties: ^1.1.3 + es-abstract: ^1.19.1 + checksum: 61a0b565ded97b76df9e30b569729866e1824cce902f98e90bb106e84f378aea20163366f66dc75c9000e2aad2ed0caf65c6f530cb2abc4c0c0f6c982102db4b + languageName: node + linkType: hard + +"object.fromentries@npm:^2.0.6": version: 2.0.6 resolution: "object.fromentries@npm:2.0.6" dependencies: @@ -27079,7 +29343,18 @@ __metadata: languageName: node linkType: hard -"object.values@npm:^1.1.0, object.values@npm:^1.1.5, object.values@npm:^1.1.6": +"object.values@npm:^1.1.0, object.values@npm:^1.1.5": + version: 1.1.5 + resolution: "object.values@npm:1.1.5" + dependencies: + call-bind: ^1.0.2 + define-properties: ^1.1.3 + es-abstract: ^1.19.1 + checksum: 0f17e99741ebfbd0fa55ce942f6184743d3070c61bd39221afc929c8422c4907618c8da694c6915bc04a83ab3224260c779ba37fc07bb668bdc5f33b66a902a4 + languageName: node + linkType: hard + +"object.values@npm:^1.1.6": version: 1.1.6 resolution: "object.values@npm:1.1.6" dependencies: @@ -27797,6 +30072,13 @@ __metadata: languageName: node linkType: hard +"parse-svg-path@npm:^0.1.2": + version: 0.1.2 + resolution: "parse-svg-path@npm:0.1.2" + checksum: bba7d4b4207fcc9eaf553b0d34db96ea8a1173635bc94528b5b66e1581902d4792d8d6229103764f01af4d839274234e97a4fa1c6f0fe7dcce195383848cec56 + languageName: node + linkType: hard + "parse5-htmlparser2-tree-adapter@npm:^6.0.1": version: 6.0.1 resolution: "parse5-htmlparser2-tree-adapter@npm:6.0.1" @@ -29954,7 +32236,7 @@ __metadata: languageName: node linkType: hard -"queue@npm:6.0.2": +"queue@npm:6.0.2, queue@npm:^6.0.1": version: 6.0.2 resolution: "queue@npm:6.0.2" dependencies: @@ -30281,6 +32563,18 @@ __metadata: languageName: node linkType: hard +"react-dom@npm:^18.2.0": + version: 18.2.0 + resolution: "react-dom@npm:18.2.0" + dependencies: + loose-envify: ^1.1.0 + scheduler: ^0.23.0 + peerDependencies: + react: ^18.2.0 + checksum: 7d323310bea3a91be2965f9468d552f201b1c27891e45ddc2d6b8f717680c95a75ae0bc1e3f5cf41472446a2589a75aed4483aee8169287909fcd59ad149e8cc + languageName: node + linkType: hard + "react-element-to-jsx-string@npm:^14.3.4": version: 14.3.4 resolution: "react-element-to-jsx-string@npm:14.3.4" @@ -30497,6 +32791,15 @@ __metadata: languageName: node linkType: hard +"react@npm:^18.2.0": + version: 18.2.0 + resolution: "react@npm:18.2.0" + dependencies: + loose-envify: ^1.1.0 + checksum: 88e38092da8839b830cda6feef2e8505dec8ace60579e46aa5490fc3dc9bba0bd50336507dc166f43e3afc1c42939c09fe33b25fae889d6f402721dcd78fca1b + languageName: node + linkType: hard + "react@npm:~17.0.2": version: 17.0.2 resolution: "react@npm:17.0.2" @@ -30813,13 +33116,20 @@ __metadata: languageName: node linkType: hard -"regenerator-runtime@npm:^0.13.11, regenerator-runtime@npm:^0.13.2, regenerator-runtime@npm:^0.13.7": +"regenerator-runtime@npm:^0.13.11": version: 0.13.11 resolution: "regenerator-runtime@npm:0.13.11" checksum: 27481628d22a1c4e3ff551096a683b424242a216fee44685467307f14d58020af1e19660bf2e26064de946bad7eff28950eae9f8209d55723e2d9351e632bbb4 languageName: node linkType: hard +"regenerator-runtime@npm:^0.13.2, regenerator-runtime@npm:^0.13.4, regenerator-runtime@npm:^0.13.7": + version: 0.13.9 + resolution: "regenerator-runtime@npm:0.13.9" + checksum: 65ed455fe5afd799e2897baf691ca21c2772e1a969d19bb0c4695757c2d96249eb74ee3553ea34a91062b2a676beedf630b4c1551cc6299afb937be1426ec55e + languageName: node + linkType: hard + "regenerator-transform@npm:^0.15.0": version: 0.15.0 resolution: "regenerator-transform@npm:0.15.0" @@ -30839,7 +33149,7 @@ __metadata: languageName: node linkType: hard -"regexp.prototype.flags@npm:^1.2.0, regexp.prototype.flags@npm:^1.4.3": +"regexp.prototype.flags@npm:^1.2.0, regexp.prototype.flags@npm:^1.4.1, regexp.prototype.flags@npm:^1.4.3": version: 1.4.3 resolution: "regexp.prototype.flags@npm:1.4.3" dependencies: @@ -31342,6 +33652,13 @@ __metadata: languageName: node linkType: hard +"restructure@npm:^3.0.0": + version: 3.0.0 + resolution: "restructure@npm:3.0.0" + checksum: 4525b5414ec0f2dc4ad66b5fbcebbc2f49e7ad778c30ce45b8b8f776af67e2c0752eb309748d7a597add6fc064e688df2662c834ffeaaea580af6d43087dc7d3 + languageName: node + linkType: hard + "ret@npm:~0.1.10": version: 0.1.15 resolution: "ret@npm:0.1.15" @@ -31726,6 +34043,16 @@ __metadata: languageName: node linkType: hard +"scheduler@npm:^0.17.0": + version: 0.17.0 + resolution: "scheduler@npm:0.17.0" + dependencies: + loose-envify: ^1.1.0 + object-assign: ^4.1.1 + checksum: 18d1e66cad3d26e3becd99b006d0744cda3556dbb356fc5b30df6d5499c85a308d18ee55353e01595f7c047b526564603ea80ef3d927a325faedc53ede03680c + languageName: node + linkType: hard + "scheduler@npm:^0.20.2": version: 0.20.2 resolution: "scheduler@npm:0.20.2" @@ -31736,6 +34063,15 @@ __metadata: languageName: node linkType: hard +"scheduler@npm:^0.23.0": + version: 0.23.0 + resolution: "scheduler@npm:0.23.0" + dependencies: + loose-envify: ^1.1.0 + checksum: d79192eeaa12abef860c195ea45d37cbf2bbf5f66e3c4dcd16f54a7da53b17788a70d109ee3d3dde1a0fd50e6a8fc171f4300356c5aee4fc0171de526bf35f8a + languageName: node + linkType: hard + "schema-utils@npm:2.7.0": version: 2.7.0 resolution: "schema-utils@npm:2.7.0" @@ -32436,7 +34772,17 @@ __metadata: languageName: node linkType: hard -"socks@npm:^2.3.3, socks@npm:^2.6.1, socks@npm:^2.7.1": +"socks@npm:^2.3.3, socks@npm:^2.6.1, socks@npm:^2.6.2": + version: 2.6.2 + resolution: "socks@npm:2.6.2" + dependencies: + ip: ^1.1.5 + smart-buffer: ^4.2.0 + checksum: dd9194293059d737759d5c69273850ad4149f448426249325c4bea0e340d1cf3d266c3b022694b0dcf5d31f759de23657244c481fc1e8322add80b7985c36b5e + languageName: node + linkType: hard + +"socks@npm:^2.7.1": version: 2.7.1 resolution: "socks@npm:2.7.1" dependencies: @@ -33184,7 +35530,23 @@ __metadata: languageName: node linkType: hard -"string.prototype.matchall@npm:^4.0.0 || ^3.0.1, string.prototype.matchall@npm:^4.0.8": +"string.prototype.matchall@npm:^4.0.0 || ^3.0.1": + version: 4.0.7 + resolution: "string.prototype.matchall@npm:4.0.7" + dependencies: + call-bind: ^1.0.2 + define-properties: ^1.1.3 + es-abstract: ^1.19.1 + get-intrinsic: ^1.1.1 + has-symbols: ^1.0.3 + internal-slot: ^1.0.3 + regexp.prototype.flags: ^1.4.1 + side-channel: ^1.0.4 + checksum: fc09f3ccbfb325de0472bcc87a6be0598a7499e0b4a31db5789676155b15754a4cc4bb83924f15fc9ed48934dac7366ee52c8b9bd160bed6fd072c93b489e75c + languageName: node + linkType: hard + +"string.prototype.matchall@npm:^4.0.8": version: 4.0.8 resolution: "string.prototype.matchall@npm:4.0.8" dependencies: @@ -33576,11 +35938,11 @@ __metadata: linkType: hard "superjson@npm:^1.10.0": - version: 1.12.0 - resolution: "superjson@npm:1.12.0" + version: 1.12.1 + resolution: "superjson@npm:1.12.1" dependencies: copy-anything: ^3.0.2 - checksum: 57b4a2086ab613a88672f617fa0840af0ba25f253f7b778a8859aa1846b01600f55dba31663fad3439fcead874614ae32a8cf72117c32428e9487eca676964ca + checksum: d69badde9892bd2acfb6a2c37f233b7018b64fe88357568a7ab480547d6755ac1ed33a8d438d7ca856178312f5367574de180774d558bc0b75d1439efdfc53ba languageName: node linkType: hard @@ -33661,6 +36023,13 @@ __metadata: languageName: node linkType: hard +"svg-arc-to-cubic-bezier@npm:^3.0.0, svg-arc-to-cubic-bezier@npm:^3.2.0": + version: 3.2.0 + resolution: "svg-arc-to-cubic-bezier@npm:3.2.0" + checksum: 55bf17756d558b9c0daddf636a6c9f2fe01fd5ac412229dfa2d4b29740226a82c980bcd3b5eb09ce311cbea282106c7549d97f8c8dba3a5a7b75f786bcb5e155 + languageName: node + linkType: hard + "svg-loader@npm:^0.0.2": version: 0.0.2 resolution: "svg-loader@npm:0.0.2" @@ -34138,6 +36507,13 @@ __metadata: languageName: node linkType: hard +"tiny-inflate@npm:^1.0.0, tiny-inflate@npm:^1.0.3": + version: 1.0.3 + resolution: "tiny-inflate@npm:1.0.3" + checksum: 4086a1f8938dafa4a20c63b099aeb47bf8fef5aca991bf4ea4b35dd2684fa52363b2c19b3e76660311e7613cb7c4f063bc48751b9bdf9555e498d997c30bc2d6 + languageName: node + linkType: hard + "tiny-invariant@npm:^1.2.0": version: 1.2.0 resolution: "tiny-invariant@npm:1.2.0" @@ -34548,6 +36924,13 @@ __metadata: languageName: node linkType: hard +"tslib@npm:^2.4.0": + version: 2.4.1 + resolution: "tslib@npm:2.4.1" + checksum: 19480d6e0313292bd6505d4efe096a6b31c70e21cf08b5febf4da62e95c265c8f571f7b36fcc3d1a17e068032f59c269fab3459d6cd3ed6949eafecf64315fca + languageName: node + linkType: hard + "tslib@npm:~2.3.1": version: 2.3.1 resolution: "tslib@npm:2.3.1" @@ -34906,6 +37289,16 @@ __metadata: languageName: node linkType: hard +"typescript@npm:~4.5.5": + version: 4.5.5 + resolution: "typescript@npm:4.5.5" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 506f4c919dc8aeaafa92068c997f1d213b9df4d9756d0fae1a1e7ab66b585ab3498050e236113a1c9e57ee08c21ec6814ca7a7f61378c058d79af50a4b1f5a5e + languageName: node + linkType: hard + "typescript@npm:~4.6.4": version: 4.6.4 resolution: "typescript@npm:4.6.4" @@ -34916,6 +37309,16 @@ __metadata: languageName: node linkType: hard +"typescript@patch:typescript@~4.5.5#~builtin": + version: 4.5.5 + resolution: "typescript@patch:typescript@npm%3A4.5.5#~builtin::version=4.5.5&hash=f456af" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 858c61fa63f7274ca4aaaffeced854d550bf416cff6e558c4884041b3311fb662f476f167cf5c9f8680c607239797e26a2ee0bcc6467fbc05bfcb218e1c6c671 + languageName: node + linkType: hard + "typescript@patch:typescript@~4.6.4#~builtin": version: 4.6.4 resolution: "typescript@patch:typescript@npm%3A4.6.4#~builtin::version=4.6.4&hash=f456af" @@ -35050,6 +37453,16 @@ __metadata: languageName: node linkType: hard +"unicode-properties@npm:^1.4.0, unicode-properties@npm:^1.4.1": + version: 1.4.1 + resolution: "unicode-properties@npm:1.4.1" + dependencies: + base64-js: ^1.3.0 + unicode-trie: ^2.0.0 + checksum: 337fba8a3c4707692d662fafbea60718ca9d8dfd2147cb2642bc4a1b5ad11136d848fa9c92818a35f59e6c866674ec7fd140e3e25412aea8fb8817f1b32fc3fe + languageName: node + linkType: hard + "unicode-property-aliases-ecmascript@npm:^2.0.0": version: 2.0.0 resolution: "unicode-property-aliases-ecmascript@npm:2.0.0" @@ -35057,6 +37470,16 @@ __metadata: languageName: node linkType: hard +"unicode-trie@npm:^2.0.0": + version: 2.0.0 + resolution: "unicode-trie@npm:2.0.0" + dependencies: + pako: ^0.2.5 + tiny-inflate: ^1.0.0 + checksum: 19e637ce20953ec1fbfa9087abef4746a50352679b833be27924e4ba7ad753cc4073b74263747ccfccb5e38b30b17468cbb96f361eb49903ff8602396280b5a4 + languageName: node + linkType: hard + "unified@npm:9.2.0": version: 9.2.0 resolution: "unified@npm:9.2.0" @@ -35778,6 +38201,17 @@ __metadata: languageName: node linkType: hard +"vite-compatible-readable-stream@npm:^3.6.1": + version: 3.6.1 + resolution: "vite-compatible-readable-stream@npm:3.6.1" + dependencies: + inherits: ^2.0.3 + string_decoder: ^1.1.1 + util-deprecate: ^1.0.1 + checksum: 7fd50738616a7bd012fb936b7036877940a0a83078fbe2584726fa9d1a5d15c934a5883e12e16213d6be54996b4ad7b6368d2897f9867a6c1110d03eacd91302 + languageName: node + linkType: hard + "vizion@npm:~2.2.1": version: 2.2.1 resolution: "vizion@npm:2.2.1"