diff --git a/.changeset/fifty-maps-deny.md b/.changeset/fifty-maps-deny.md new file mode 100644 index 0000000000000..a01c5e4c71212 --- /dev/null +++ b/.changeset/fifty-maps-deny.md @@ -0,0 +1,6 @@ +--- +"@rocket.chat/meteor": minor +"@rocket.chat/rest-typings": minor +--- + +Added `push.info` endpoint to enable users to retrieve info about the workspace's push gateway diff --git a/apps/meteor/app/api/server/v1/import.ts b/apps/meteor/app/api/server/v1/import.ts index d0fc8643e07cf..54dbce4d82d18 100644 --- a/apps/meteor/app/api/server/v1/import.ts +++ b/apps/meteor/app/api/server/v1/import.ts @@ -10,10 +10,13 @@ import { isDownloadPendingFilesParamsPOST, isDownloadPendingAvatarsParamsPOST, isGetCurrentImportOperationParamsGET, + isImportersListParamsGET, isImportAddUsersParamsPOST, } from '@rocket.chat/rest-typings'; import { Meteor } from 'meteor/meteor'; +import { PendingAvatarImporter } from '../../../importer-pending-avatars/server/PendingAvatarImporter'; +import { PendingFileImporter } from '../../../importer-pending-files/server/PendingFileImporter'; import { Importers } from '../../../importer/server'; import { executeUploadImportFile, @@ -136,9 +139,8 @@ API.v1.addRoute( } const operation = await Import.newOperation(this.userId, importer.name, importer.key); - - importer.instance = new importer.importer(importer, operation); // eslint-disable-line new-cap - const count = await importer.instance.prepareFileCount(); + const instance = new PendingFileImporter(importer, operation); + const count = await instance.prepareFileCount(); return API.v1.success({ count, @@ -162,8 +164,8 @@ API.v1.addRoute( } const operation = await Import.newOperation(this.userId, importer.name, importer.key); - importer.instance = new importer.importer(importer, operation); // eslint-disable-line new-cap - const count = await importer.instance.prepareFileCount(); + const instance = new PendingAvatarImporter(importer, operation); + const count = await instance.prepareFileCount(); return API.v1.success({ count, @@ -189,6 +191,22 @@ API.v1.addRoute( }, ); +API.v1.addRoute( + 'importers.list', + { + authRequired: true, + validateParams: isImportersListParamsGET, + permissionsRequired: ['run-import'], + }, + { + async get() { + const importers = Importers.getAllVisible().map(({ key, name }) => ({ key, name })); + + return API.v1.success(importers); + }, + }, +); + API.v1.addRoute( 'import.clear', { diff --git a/apps/meteor/app/api/server/v1/push.ts b/apps/meteor/app/api/server/v1/push.ts index b5fd93f94dda8..5910d47e4c76f 100644 --- a/apps/meteor/app/api/server/v1/push.ts +++ b/apps/meteor/app/api/server/v1/push.ts @@ -1,10 +1,11 @@ -import { Messages, AppsTokens, Users, Rooms } from '@rocket.chat/models'; +import { Messages, AppsTokens, Users, Rooms, Settings } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; import { Match, check } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; import { canAccessRoomAsync } from '../../../authorization/server/functions/canAccessRoom'; import PushNotification from '../../../push-notifications/server/lib/PushNotification'; +import { settings } from '../../../settings/server'; import { API } from '../api'; API.v1.addRoute( @@ -110,3 +111,18 @@ API.v1.addRoute( }, }, ); + +API.v1.addRoute( + 'push.info', + { authRequired: true }, + { + async get() { + const defaultGateway = (await Settings.findOneById('Push_gateway', { projection: { packageValue: 1 } }))?.packageValue; + const defaultPushGateway = settings.get('Push_gateway') === defaultGateway; + return API.v1.success({ + pushGatewayEnabled: settings.get('Push_enable'), + defaultPushGateway, + }); + }, + }, +); diff --git a/apps/meteor/app/autotranslate/server/autotranslate.ts b/apps/meteor/app/autotranslate/server/autotranslate.ts index 076c7c52818aa..6cac8028e1f24 100644 --- a/apps/meteor/app/autotranslate/server/autotranslate.ts +++ b/apps/meteor/app/autotranslate/server/autotranslate.ts @@ -1,3 +1,4 @@ +import { api } from '@rocket.chat/core-services'; import type { IMessage, IRoom, @@ -15,6 +16,7 @@ import _ from 'underscore'; import { callbacks } from '../../../lib/callbacks'; import { isTruthy } from '../../../lib/isTruthy'; +import { broadcastMessageSentEvent } from '../../../server/modules/watchers/lib/messages'; import { Markdown } from '../../markdown/server'; import { settings } from '../../settings/server'; @@ -305,6 +307,7 @@ export abstract class AutoTranslate { const translations = await this._translateMessage(targetMessage, targetLanguages); if (!_.isEmpty(translations)) { await Messages.addTranslations(message._id, translations, TranslationProviderRegistry[Provider] || ''); + this.notifyTranslatedMessage(message._id); } }); } @@ -320,6 +323,7 @@ export abstract class AutoTranslate { if (!_.isEmpty(translations)) { await Messages.addAttachmentTranslations(message._id, String(index), translations); + this.notifyTranslatedMessage(message._id); } } } @@ -328,6 +332,13 @@ export abstract class AutoTranslate { return Messages.findOneById(message._id); } + private notifyTranslatedMessage(messageId: string): void { + void broadcastMessageSentEvent({ + id: messageId, + broadcastCallback: (message) => api.broadcast('message.sent', message), + }); + } + /** * Returns metadata information about the service provider which is used by * the generic implementation diff --git a/apps/meteor/app/federation/server/endpoints/dispatch.js b/apps/meteor/app/federation/server/endpoints/dispatch.js index 59ffcb0f342fb..e54441a7aa9db 100644 --- a/apps/meteor/app/federation/server/endpoints/dispatch.js +++ b/apps/meteor/app/federation/server/endpoints/dispatch.js @@ -3,6 +3,7 @@ import { eventTypes } from '@rocket.chat/core-typings'; import { FederationServers, FederationRoomEvents, Rooms, Messages, Subscriptions, Users, ReadReceipts } from '@rocket.chat/models'; import EJSON from 'ejson'; +import { broadcastMessageSentEvent } from '../../../../server/modules/watchers/lib/messages'; import { API } from '../../../api/server'; import { FileUpload } from '../../../file-upload/server'; import { deleteRoom } from '../../../lib/server/functions/deleteRoom'; @@ -214,11 +215,13 @@ const eventHandlers = { // Check if message exists const persistedMessage = await Messages.findOne({ _id: message._id }); + let messageForNotification; if (persistedMessage) { // Update the federation if (!persistedMessage.federation) { await Messages.updateOne({ _id: persistedMessage._id }, { $set: { federation: message.federation } }); + messageForNotification = { ...persistedMessage, federation: message.federation }; } } else { // Load the room @@ -275,10 +278,18 @@ const eventHandlers = { // Notify users await notifyUsersOnMessage(denormalizedMessage, room); sendAllNotifications(denormalizedMessage, room); + messageForNotification = denormalizedMessage; } catch (err) { serverLogger.debug(`Error on creating message: ${message._id}`); } } + if (messageForNotification) { + void broadcastMessageSentEvent({ + id: messageForNotification._id, + data: messageForNotification, + broadcastCallback: (message) => api.broadcast('message.sent', message), + }); + } } return eventResult; @@ -305,6 +316,15 @@ const eventHandlers = { } else { // Update the message await Messages.updateOne({ _id: persistedMessage._id }, { $set: { msg: message.msg, federation: message.federation } }); + void broadcastMessageSentEvent({ + id: persistedMessage._id, + data: { + ...persistedMessage, + msg: message.msg, + federation: message.federation, + }, + broadcastCallback: (message) => api.broadcast('message.sent', message), + }); } } @@ -367,6 +387,17 @@ const eventHandlers = { // Update the property await Messages.updateOne({ _id: messageId }, { $set: { [`reactions.${reaction}`]: reactionObj } }); + void broadcastMessageSentEvent({ + id: persistedMessage._id, + data: { + ...persistedMessage, + reactions: { + ...persistedMessage.reactions, + [reaction]: reactionObj, + }, + }, + broadcastCallback: (message) => api.broadcast('message.sent', message), + }); } return eventResult; @@ -415,6 +446,17 @@ const eventHandlers = { // Otherwise, update the property await Messages.updateOne({ _id: messageId }, { $set: { [`reactions.${reaction}`]: reactionObj } }); } + void broadcastMessageSentEvent({ + id: persistedMessage._id, + data: { + ...persistedMessage, + reactions: { + ...persistedMessage.reactions, + [reaction]: reactionObj, + }, + }, + broadcastCallback: (message) => api.broadcast('message.sent', message), + }); } return eventResult; diff --git a/apps/meteor/app/importer-csv/client/adder.js b/apps/meteor/app/importer-csv/client/adder.js deleted file mode 100644 index 929a84640be9b..0000000000000 --- a/apps/meteor/app/importer-csv/client/adder.js +++ /dev/null @@ -1,4 +0,0 @@ -import { Importers } from '../../importer/client'; -import { CsvImporterInfo } from '../lib/info'; - -Importers.add(new CsvImporterInfo()); diff --git a/apps/meteor/app/importer-csv/client/index.ts b/apps/meteor/app/importer-csv/client/index.ts deleted file mode 100644 index 44a1b3bab84c5..0000000000000 --- a/apps/meteor/app/importer-csv/client/index.ts +++ /dev/null @@ -1 +0,0 @@ -import './adder'; diff --git a/apps/meteor/app/importer-csv/lib/info.js b/apps/meteor/app/importer-csv/lib/info.js deleted file mode 100644 index c4fccdf74991f..0000000000000 --- a/apps/meteor/app/importer-csv/lib/info.js +++ /dev/null @@ -1,12 +0,0 @@ -import { ImporterInfo } from '../../importer/lib/ImporterInfo'; - -export class CsvImporterInfo extends ImporterInfo { - constructor() { - super('csv', 'CSV', 'application/zip', [ - { - text: 'Importer_CSV_Information', - href: 'https://rocket.chat/docs/administrator-guides/import/csv/', - }, - ]); - } -} diff --git a/apps/meteor/app/importer-csv/server/importer.js b/apps/meteor/app/importer-csv/server/CsvImporter.ts similarity index 82% rename from apps/meteor/app/importer-csv/server/importer.js rename to apps/meteor/app/importer-csv/server/CsvImporter.ts index c2f20f75615d6..302aeb882ac59 100644 --- a/apps/meteor/app/importer-csv/server/importer.js +++ b/apps/meteor/app/importer-csv/server/CsvImporter.ts @@ -1,18 +1,23 @@ +import type { IImport } from '@rocket.chat/core-typings'; import { Settings, Users } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; +import { parse } from 'csv-parse/lib/sync'; -import { Base, ProgressStep, ImporterWebsocket } from '../../importer/server'; +import { Importer, ProgressStep, ImporterWebsocket } from '../../importer/server'; +import type { IConverterOptions } from '../../importer/server/classes/ImportDataConverter'; +import type { ImporterProgress } from '../../importer/server/classes/ImporterProgress'; +import type { ImporterInfo } from '../../importer/server/definitions/ImporterInfo'; -export class CsvImporter extends Base { - constructor(info, importRecord, converterOptions = {}) { - super(info, importRecord, converterOptions); +export class CsvImporter extends Importer { + private csvParser: (csv: string) => string[]; - const { parse } = require('csv-parse/lib/sync'); + constructor(info: ImporterInfo, importRecord: IImport, converterOptions: IConverterOptions = {}) { + super(info, importRecord, converterOptions); this.csvParser = parse; } - async prepareUsingLocalFile(fullFilePath) { + async prepareUsingLocalFile(fullFilePath: string): Promise { this.logger.debug('start preparing import operation'); await this.converter.clearImportData(); @@ -40,17 +45,21 @@ export class CsvImporter extends Base { let messagesCount = 0; let usersCount = 0; let channelsCount = 0; - const dmRooms = new Map(); - const roomIds = new Map(); - const usedUsernames = new Set(); - const availableUsernames = new Set(); - - const getRoomId = (roomName) => { - if (!roomIds.has(roomName)) { - roomIds.set(roomName, Random.id()); + const dmRooms = new Set(); + const roomIds = new Map(); + const usedUsernames = new Set(); + const availableUsernames = new Set(); + + const getRoomId = (roomName: string) => { + const roomId = roomIds.get(roomName); + + if (roomId === undefined) { + const fallbackRoomId = Random.id(); + roomIds.set(roomName, fallbackRoomId); + return fallbackRoomId; } - return roomIds.get(roomName); + return roomId; }; for await (const entry of zip.getEntries()) { @@ -149,7 +158,7 @@ export class CsvImporter extends Base { continue; } - let data; + let data: { username: string; ts: string; text: string; otherUsername?: string; isDirect?: true }[]; const msgGroupData = item[1].split('.')[0]; // messages let isDirect = false; @@ -173,6 +182,10 @@ export class CsvImporter extends Base { if (isDirect) { for await (const msg of data) { + if (!msg.otherUsername) { + continue; + } + const sourceId = [msg.username, msg.otherUsername].sort().join('/'); if (!dmRooms.has(sourceId)) { @@ -182,7 +195,7 @@ export class CsvImporter extends Base { t: 'd', }); - dmRooms.set(sourceId, true); + dmRooms.add(sourceId); } const newMessage = { @@ -217,8 +230,6 @@ export class CsvImporter extends Base { } await super.updateRecord({ 'count.messages': messagesCount, 'messagesstatus': null }); - increaseProgressCount(); - continue; } increaseProgressCount(); diff --git a/apps/meteor/app/importer-csv/server/index.ts b/apps/meteor/app/importer-csv/server/index.ts index 8ab31878fb7b5..2d913f740955c 100644 --- a/apps/meteor/app/importer-csv/server/index.ts +++ b/apps/meteor/app/importer-csv/server/index.ts @@ -1,5 +1,8 @@ import { Importers } from '../../importer/server'; -import { CsvImporterInfo } from '../lib/info'; -import { CsvImporter } from './importer'; +import { CsvImporter } from './CsvImporter'; -Importers.add(new CsvImporterInfo(), CsvImporter); +Importers.add({ + key: 'csv', + name: 'CSV', + importer: CsvImporter, +}); diff --git a/apps/meteor/app/importer-hipchat-enterprise/client/adder.ts b/apps/meteor/app/importer-hipchat-enterprise/client/adder.ts deleted file mode 100644 index 2c03c872c7eb9..0000000000000 --- a/apps/meteor/app/importer-hipchat-enterprise/client/adder.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Importers } from '../../importer/client'; -import { HipChatEnterpriseImporterInfo } from '../lib/info'; - -Importers.add(new HipChatEnterpriseImporterInfo()); diff --git a/apps/meteor/app/importer-hipchat-enterprise/client/index.ts b/apps/meteor/app/importer-hipchat-enterprise/client/index.ts deleted file mode 100644 index 44a1b3bab84c5..0000000000000 --- a/apps/meteor/app/importer-hipchat-enterprise/client/index.ts +++ /dev/null @@ -1 +0,0 @@ -import './adder'; diff --git a/apps/meteor/app/importer-hipchat-enterprise/lib/info.ts b/apps/meteor/app/importer-hipchat-enterprise/lib/info.ts deleted file mode 100644 index 71c1f17a72d70..0000000000000 --- a/apps/meteor/app/importer-hipchat-enterprise/lib/info.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ImporterInfo } from '../../importer/lib/ImporterInfo'; - -export class HipChatEnterpriseImporterInfo extends ImporterInfo { - constructor() { - super('hipchatenterprise', 'HipChat (tar.gz)', 'application/gzip', [ - { - text: 'Importer_HipChatEnterprise_Information', - href: 'https://rocket.chat/docs/administrator-guides/import/hipchat/enterprise/', - }, - ]); - } -} diff --git a/apps/meteor/app/importer-hipchat-enterprise/server/importer.js b/apps/meteor/app/importer-hipchat-enterprise/server/HipChatEnterpriseImporter.js similarity index 98% rename from apps/meteor/app/importer-hipchat-enterprise/server/importer.js rename to apps/meteor/app/importer-hipchat-enterprise/server/HipChatEnterpriseImporter.js index 18fefea074bf1..ac3d278d82abc 100644 --- a/apps/meteor/app/importer-hipchat-enterprise/server/importer.js +++ b/apps/meteor/app/importer-hipchat-enterprise/server/HipChatEnterpriseImporter.js @@ -5,9 +5,10 @@ import { Readable } from 'stream'; import { Settings } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; -import { Base, ProgressStep } from '../../importer/server'; +import { Importer, ProgressStep } from '../../importer/server'; -export class HipChatEnterpriseImporter extends Base { +/** @deprecated HipChat was discontinued at 2019-02-15 */ +export class HipChatEnterpriseImporter extends Importer { constructor(info, importRecord, converterOptions = {}) { super(info, importRecord, converterOptions); diff --git a/apps/meteor/app/importer-hipchat-enterprise/server/index.ts b/apps/meteor/app/importer-hipchat-enterprise/server/index.ts index 097a5071306da..e50a9b9c4bd39 100644 --- a/apps/meteor/app/importer-hipchat-enterprise/server/index.ts +++ b/apps/meteor/app/importer-hipchat-enterprise/server/index.ts @@ -1,5 +1,8 @@ import { Importers } from '../../importer/server'; -import { HipChatEnterpriseImporterInfo } from '../lib/info'; -import { HipChatEnterpriseImporter } from './importer'; +import { HipChatEnterpriseImporter } from './HipChatEnterpriseImporter'; -Importers.add(new HipChatEnterpriseImporterInfo(), HipChatEnterpriseImporter); +Importers.add({ + key: 'hipchatenterprise', + name: 'HipChat (tar.gz)', + importer: HipChatEnterpriseImporter, +}); diff --git a/apps/meteor/app/importer-pending-avatars/server/importer.js b/apps/meteor/app/importer-pending-avatars/server/PendingAvatarImporter.ts similarity index 70% rename from apps/meteor/app/importer-pending-avatars/server/importer.js rename to apps/meteor/app/importer-pending-avatars/server/PendingAvatarImporter.ts index a7517285762bb..0f6c8c7d41df6 100644 --- a/apps/meteor/app/importer-pending-avatars/server/importer.js +++ b/apps/meteor/app/importer-pending-avatars/server/PendingAvatarImporter.ts @@ -1,14 +1,15 @@ import { Users } from '@rocket.chat/models'; -import { Base, ProgressStep, Selection } from '../../importer/server'; +import { Importer, ProgressStep, Selection } from '../../importer/server'; +import type { ImporterProgress } from '../../importer/server/classes/ImporterProgress'; import { setAvatarFromServiceWithValidation } from '../../lib/server/functions/setUserAvatar'; -export class PendingAvatarImporter extends Base { +export class PendingAvatarImporter extends Importer { async prepareFileCount() { this.logger.debug('start preparing import operation'); await super.updateProgress(ProgressStep.PREPARING_STARTED); - const users = await Users.findAllUsersWithPendingAvatar(); + const users = Users.findAllUsersWithPendingAvatar(); const fileCount = await users.count(); if (fileCount === 0) { @@ -19,26 +20,26 @@ export class PendingAvatarImporter extends Base { await this.updateRecord({ 'count.messages': fileCount, 'messagesstatus': null }); await this.addCountToTotal(fileCount); - const fileData = new Selection(this.name, [], [], fileCount); + const fileData = new Selection(this.info.name, [], [], fileCount); await this.updateRecord({ fileData }); await super.updateProgress(ProgressStep.IMPORTING_FILES); setImmediate(() => { - this.startImport(fileData); + void this.startImport(fileData); }); return fileCount; } - async startImport() { - const pendingFileUserList = await Users.findAllUsersWithPendingAvatar(); + async startImport(importSelection: Selection): Promise { + const pendingFileUserList = Users.findAllUsersWithPendingAvatar(); try { for await (const user of pendingFileUserList) { try { const { _pendingAvatarUrl: url, name, _id } = user; try { - if (!url || !url.startsWith('http')) { + if (!url?.startsWith('http')) { continue; } @@ -57,9 +58,9 @@ export class PendingAvatarImporter extends Base { } } catch (error) { // If the cursor expired, restart the method - if (error && error.codeName === 'CursorNotFound') { + if (this.isCursorNotFoundError(error)) { this.logger.info('CursorNotFound'); - return this.startImport(); + return this.startImport(importSelection); } await super.updateProgress(ProgressStep.ERROR); diff --git a/apps/meteor/app/importer-pending-avatars/server/index.ts b/apps/meteor/app/importer-pending-avatars/server/index.ts index 9c6bc278abeee..b69c6de8e7457 100644 --- a/apps/meteor/app/importer-pending-avatars/server/index.ts +++ b/apps/meteor/app/importer-pending-avatars/server/index.ts @@ -1,5 +1,9 @@ import { Importers } from '../../importer/server'; -import { PendingAvatarImporter } from './importer'; -import { PendingAvatarImporterInfo } from './info'; +import { PendingAvatarImporter } from './PendingAvatarImporter'; -Importers.add(new PendingAvatarImporterInfo(), PendingAvatarImporter); +Importers.add({ + key: 'pending-avatars', + name: 'Pending Avatars', + visible: false, + importer: PendingAvatarImporter, +}); diff --git a/apps/meteor/app/importer-pending-avatars/server/info.js b/apps/meteor/app/importer-pending-avatars/server/info.js deleted file mode 100644 index 32e8886cb5383..0000000000000 --- a/apps/meteor/app/importer-pending-avatars/server/info.js +++ /dev/null @@ -1,7 +0,0 @@ -import { ImporterInfo } from '../../importer/lib/ImporterInfo'; - -export class PendingAvatarImporterInfo extends ImporterInfo { - constructor() { - super('pending-avatars', 'Pending Avatars', ''); - } -} diff --git a/apps/meteor/app/importer-pending-files/server/importer.js b/apps/meteor/app/importer-pending-files/server/PendingFileImporter.ts similarity index 60% rename from apps/meteor/app/importer-pending-files/server/importer.js rename to apps/meteor/app/importer-pending-files/server/PendingFileImporter.ts index 3d8d083c495b4..657a64002a5a9 100644 --- a/apps/meteor/app/importer-pending-files/server/importer.js +++ b/apps/meteor/app/importer-pending-files/server/PendingFileImporter.ts @@ -1,17 +1,19 @@ import http from 'http'; import https from 'https'; +import type { IImport, MessageAttachment, IUpload } from '@rocket.chat/core-typings'; import { Messages } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; import { FileUpload } from '../../file-upload/server'; -import { Base, ProgressStep, Selection } from '../../importer/server'; - -export class PendingFileImporter extends Base { - constructor(info, importRecord) { - super(info, importRecord); - this.userTags = []; - this.bots = {}; +import { Importer, ProgressStep, Selection } from '../../importer/server'; +import type { IConverterOptions } from '../../importer/server/classes/ImportDataConverter'; +import type { ImporterProgress } from '../../importer/server/classes/ImporterProgress'; +import type { ImporterInfo } from '../../importer/server/definitions/ImporterInfo'; + +export class PendingFileImporter extends Importer { + constructor(info: ImporterInfo, importRecord: IImport, converterOptions: IConverterOptions = {}) { + super(info, importRecord, converterOptions); } async prepareFileCount() { @@ -27,19 +29,19 @@ export class PendingFileImporter extends Base { await this.updateRecord({ 'count.messages': fileCount, 'messagesstatus': null }); await this.addCountToTotal(fileCount); - const fileData = new Selection(this.name, [], [], fileCount); + const fileData = new Selection(this.info.name, [], [], fileCount); await this.updateRecord({ fileData }); await super.updateProgress(ProgressStep.IMPORTING_FILES); setImmediate(() => { - this.startImport(fileData); + void this.startImport(fileData); }); return fileCount; } - async startImport() { - const downloadedFileIds = []; + async startImport(importSelection: Selection): Promise { + const downloadedFileIds: string[] = []; const maxFileCount = 10; const maxFileSize = 1024 * 1024 * 500; @@ -52,7 +54,7 @@ export class PendingFileImporter extends Base { return; } - return new Promise((resolve) => { + return new Promise((resolve) => { const handler = setInterval(() => { if (count + 1 >= maxFileCount) { return; @@ -68,15 +70,13 @@ export class PendingFileImporter extends Base { }); }; - const completeFile = async (details) => { + const completeFile = async (details: { size: number }) => { await this.addCountCompleted(1); count--; currentSize -= details.size; }; - const logError = (error) => { - this.logger.error(error); - }; + const logError = this.logger.error.bind(this.logger); try { const pendingFileMessageList = Messages.findAllImportedMessagesWithFilesToDownload(); @@ -86,16 +86,16 @@ export class PendingFileImporter extends Base { if (!_importFile || _importFile.downloaded || downloadedFileIds.includes(_importFile.id)) { await this.addCountCompleted(1); - return; + continue; } const url = _importFile.downloadUrl; - if (!url || !url.startsWith('http')) { + if (!url?.startsWith('http')) { await this.addCountCompleted(1); - return; + continue; } - const details = { + const details: { message_id: string; name: string; size: number; userId: string; rid: string; type?: string } = { message_id: `${message._id}-file-${_importFile.id}`, name: _importFile.name || Random.id(), size: _importFile.size || 0, @@ -105,7 +105,6 @@ export class PendingFileImporter extends Base { const requestModule = /https/i.test(url) ? https : http; const fileStore = FileUpload.getStore('Uploads'); - const reportProgress = this.reportProgress.bind(this); nextSize = details.size; await waitForFiles(); @@ -119,12 +118,12 @@ export class PendingFileImporter extends Base { details.type = contentType; } - const rawData = []; + const rawData: Uint8Array[] = []; res.on('data', (chunk) => { rawData.push(chunk); // Update progress more often on large files - reportProgress(); + this.reportProgress(); }); res.on('error', async (error) => { await completeFile(details); @@ -136,30 +135,8 @@ export class PendingFileImporter extends Base { // Bypass the fileStore filters const file = await fileStore._doInsert(details, Buffer.concat(rawData)); - const url = FileUpload.getPath(`${file._id}/${encodeURI(file.name)}`); - const attachment = { - title: file.name, - title_link: url, - }; - - if (/^image\/.+/.test(file.type)) { - attachment.image_url = url; - attachment.image_type = file.type; - attachment.image_size = file.size; - attachment.image_dimensions = file.identify != null ? file.identify.size : undefined; - } - - if (/^audio\/.+/.test(file.type)) { - attachment.audio_url = url; - attachment.audio_type = file.type; - attachment.audio_size = file.size; - } - - if (/^video\/.+/.test(file.type)) { - attachment.video_url = url; - attachment.video_type = file.type; - attachment.video_size = file.size; - } + const url = FileUpload.getPath(`${file._id}/${encodeURI(file.name || '')}`); + const attachment = this.getMessageAttachment(file, url); await Messages.setImportFileRocketChatAttachment(_importFile.id, url, attachment); await completeFile(details); @@ -175,8 +152,9 @@ export class PendingFileImporter extends Base { } } catch (error) { // If the cursor expired, restart the method - if (error && error.codeName === 'CursorNotFound') { - return this.startImport(); + if (this.isCursorNotFoundError(error)) { + this.logger.info('CursorNotFound'); + return this.startImport(importSelection); } await super.updateProgress(ProgressStep.ERROR); @@ -186,4 +164,44 @@ export class PendingFileImporter extends Base { await super.updateProgress(ProgressStep.DONE); return this.getProgress(); } + + getMessageAttachment(file: IUpload, url: string): MessageAttachment { + if (file.type) { + if (/^image\/.+/.test(file.type)) { + return { + title: file.name, + title_link: url, + image_url: url, + image_type: file.type, + image_size: file.size, + image_dimensions: file.identify ? file.identify.size : undefined, + }; + } + + if (/^audio\/.+/.test(file.type)) { + return { + title: file.name, + title_link: url, + audio_url: url, + audio_type: file.type, + audio_size: file.size, + }; + } + + if (/^video\/.+/.test(file.type)) { + return { + title: file.name, + title_link: url, + video_url: url, + video_type: file.type, + video_size: file.size, + }; + } + } + + return { + title: file.name, + title_link: url, + }; + } } diff --git a/apps/meteor/app/importer-pending-files/server/index.ts b/apps/meteor/app/importer-pending-files/server/index.ts index 2486e6b0f0d96..24961551cdb52 100644 --- a/apps/meteor/app/importer-pending-files/server/index.ts +++ b/apps/meteor/app/importer-pending-files/server/index.ts @@ -1,5 +1,9 @@ import { Importers } from '../../importer/server'; -import { PendingFileImporter } from './importer'; -import { PendingFileImporterInfo } from './info'; +import { PendingFileImporter } from './PendingFileImporter'; -Importers.add(new PendingFileImporterInfo(), PendingFileImporter); +Importers.add({ + key: 'pending-files', + name: 'Pending Files', + visible: false, + importer: PendingFileImporter, +}); diff --git a/apps/meteor/app/importer-pending-files/server/info.js b/apps/meteor/app/importer-pending-files/server/info.js deleted file mode 100644 index f12d01a526112..0000000000000 --- a/apps/meteor/app/importer-pending-files/server/info.js +++ /dev/null @@ -1,7 +0,0 @@ -import { ImporterInfo } from '../../importer/lib/ImporterInfo'; - -export class PendingFileImporterInfo extends ImporterInfo { - constructor() { - super('pending-files', 'Pending Files', ''); - } -} diff --git a/apps/meteor/app/importer-slack-users/client/adder.js b/apps/meteor/app/importer-slack-users/client/adder.js deleted file mode 100644 index 8c6b0276bfddd..0000000000000 --- a/apps/meteor/app/importer-slack-users/client/adder.js +++ /dev/null @@ -1,4 +0,0 @@ -import { Importers } from '../../importer/client'; -import { SlackUsersImporterInfo } from '../lib/info'; - -Importers.add(new SlackUsersImporterInfo()); diff --git a/apps/meteor/app/importer-slack-users/client/index.ts b/apps/meteor/app/importer-slack-users/client/index.ts deleted file mode 100644 index 44a1b3bab84c5..0000000000000 --- a/apps/meteor/app/importer-slack-users/client/index.ts +++ /dev/null @@ -1 +0,0 @@ -import './adder'; diff --git a/apps/meteor/app/importer-slack-users/lib/info.js b/apps/meteor/app/importer-slack-users/lib/info.js deleted file mode 100644 index 999c5e76cfc96..0000000000000 --- a/apps/meteor/app/importer-slack-users/lib/info.js +++ /dev/null @@ -1,12 +0,0 @@ -import { ImporterInfo } from '../../importer/lib/ImporterInfo'; - -export class SlackUsersImporterInfo extends ImporterInfo { - constructor() { - super('slack-users', 'Slack_Users', 'text/csv', [ - { - text: 'Importer_Slack_Users_CSV_Information', - href: 'https://rocket.chat/docs/administrator-guides/import/slack/users', - }, - ]); - } -} diff --git a/apps/meteor/app/importer-slack-users/server/importer.js b/apps/meteor/app/importer-slack-users/server/SlackUsersImporter.ts similarity index 67% rename from apps/meteor/app/importer-slack-users/server/importer.js rename to apps/meteor/app/importer-slack-users/server/SlackUsersImporter.ts index 177885b7833a0..2c26531bd5c41 100644 --- a/apps/meteor/app/importer-slack-users/server/importer.js +++ b/apps/meteor/app/importer-slack-users/server/SlackUsersImporter.ts @@ -1,20 +1,25 @@ import fs from 'fs'; +import type { IImport, IImportUser } from '@rocket.chat/core-typings'; import { Settings } from '@rocket.chat/models'; +import { parse } from 'csv-parse/lib/sync'; import { RocketChatFile } from '../../file/server'; -import { Base, ProgressStep } from '../../importer/server'; +import { Importer, ProgressStep } from '../../importer/server'; +import type { IConverterOptions } from '../../importer/server/classes/ImportDataConverter'; +import type { ImporterProgress } from '../../importer/server/classes/ImporterProgress'; +import type { ImporterInfo } from '../../importer/server/definitions/ImporterInfo'; -export class SlackUsersImporter extends Base { - constructor(info, importRecord, converterOptions = {}) { - super(info, importRecord, converterOptions); +export class SlackUsersImporter extends Importer { + private csvParser: (csv: string) => string[]; - const { parse } = require('csv-parse/lib/sync'); + constructor(info: ImporterInfo, importRecord: IImport, converterOptions: IConverterOptions = {}) { + super(info, importRecord, converterOptions); this.csvParser = parse; } - async prepareUsingLocalFile(fullFilePath) { + async prepareUsingLocalFile(fullFilePath: string): Promise { this.logger.debug('start preparing import operation'); await this.converter.clearImportData(); @@ -27,6 +32,12 @@ export class SlackUsersImporter extends Base { const data = buffer.toString('base64'); const dataURI = `data:${contentType};base64,${data}`; + return this.prepare(dataURI, fileName || ''); + } + + async prepare(dataURI: string, fileName: string): Promise { + this.logger.debug('start preparing import operation'); + await this.converter.clearImportData(); await this.updateRecord({ file: fileName }); await super.updateProgress(ProgressStep.PREPARING_USERS); @@ -49,7 +60,7 @@ export class SlackUsersImporter extends Base { const name = user[7] || user[8] || username; - const newUser = { + const newUser: IImportUser = { emails: [email], importIds: [email], username, diff --git a/apps/meteor/app/importer-slack-users/server/index.ts b/apps/meteor/app/importer-slack-users/server/index.ts index 6d23f6939b13f..ab99ede8f9121 100644 --- a/apps/meteor/app/importer-slack-users/server/index.ts +++ b/apps/meteor/app/importer-slack-users/server/index.ts @@ -1,5 +1,8 @@ import { Importers } from '../../importer/server'; -import { SlackUsersImporterInfo } from '../lib/info'; -import { SlackUsersImporter } from './importer'; +import { SlackUsersImporter } from './SlackUsersImporter'; -Importers.add(new SlackUsersImporterInfo(), SlackUsersImporter); +Importers.add({ + key: 'slack-users', + name: 'Slack_Users', + importer: SlackUsersImporter, +}); diff --git a/apps/meteor/app/importer-slack/client/adder.js b/apps/meteor/app/importer-slack/client/adder.js deleted file mode 100644 index 06e1941f38df0..0000000000000 --- a/apps/meteor/app/importer-slack/client/adder.js +++ /dev/null @@ -1,4 +0,0 @@ -import { Importers } from '../../importer/client'; -import { SlackImporterInfo } from '../lib/info'; - -Importers.add(new SlackImporterInfo()); diff --git a/apps/meteor/app/importer-slack/client/index.ts b/apps/meteor/app/importer-slack/client/index.ts deleted file mode 100644 index 44a1b3bab84c5..0000000000000 --- a/apps/meteor/app/importer-slack/client/index.ts +++ /dev/null @@ -1 +0,0 @@ -import './adder'; diff --git a/apps/meteor/app/importer-slack/lib/info.js b/apps/meteor/app/importer-slack/lib/info.js deleted file mode 100644 index bf431e8f7c582..0000000000000 --- a/apps/meteor/app/importer-slack/lib/info.js +++ /dev/null @@ -1,7 +0,0 @@ -import { ImporterInfo } from '../../importer/lib/ImporterInfo'; - -export class SlackImporterInfo extends ImporterInfo { - constructor() { - super('slack', 'Slack', 'application/zip'); - } -} diff --git a/apps/meteor/app/importer-slack/server/importer.js b/apps/meteor/app/importer-slack/server/SlackImporter.ts similarity index 74% rename from apps/meteor/app/importer-slack/server/importer.js rename to apps/meteor/app/importer-slack/server/SlackImporter.ts index a5b0da6e549d8..7d56c7784b76b 100644 --- a/apps/meteor/app/importer-slack/server/importer.js +++ b/apps/meteor/app/importer-slack/server/SlackImporter.ts @@ -1,26 +1,128 @@ +import type { IImportUser, IImportMessage, IImportPendingFile } from '@rocket.chat/core-typings'; import { Messages, Settings, ImportData } from '@rocket.chat/models'; -import _ from 'underscore'; +import type { IZipEntry } from 'adm-zip'; -import { Base, ProgressStep, ImporterWebsocket } from '../../importer/server'; +import { Importer, ProgressStep, ImporterWebsocket } from '../../importer/server'; +import type { ImporterProgress } from '../../importer/server/classes/ImporterProgress'; import { MentionsParser } from '../../mentions/lib/MentionsParser'; import { settings } from '../../settings/server'; import { getUserAvatarURL } from '../../utils/server/getUserAvatarURL'; -export class SlackImporter extends Base { - parseData(data) { - const dataString = data.toString(); - try { - this.logger.debug('parsing file contents'); - return JSON.parse(dataString); - } catch (e) { - this.logger.error(e); - return false; - } - } - - async prepareChannelsFile(entry) { +type SlackChannel = { + id: string; + name: string; + topic?: { + value: string; + creator: string; + last_set: number; + }; + members: string[]; + purpose?: { + value: string; + creator: string; + last_set: number; + }; + created: number; + creator: string | null; + is_general: boolean; + is_archived: boolean; +}; + +type SlackUser = { + id: string; + name: string; + profile: { + real_name: string; + email: string; + image_512: string; + image_original: string; + status_text: string; + title: string; + }; + tz_offset: number; + deleted: boolean; + is_bot: boolean; +}; + +type SlackFile = { + id: string; + url_private_download: string; + size: number; + name: string; + is_external: boolean; +}; + +type SlackMessage = { + id: string; + ts: string; + user: string; + reactions?: { + name: string; + users: string[]; + }[]; + type: 'message'; + subtype?: string; + files?: SlackFile[]; + text: string; + edited?: { + ts: string; + user: string; + }; + thread_ts?: string; + reply_users?: string[]; + reply_count?: number; + replies?: { + user: string; + }[]; + latest_reply: string; + icons?: { + emoji: string; + }; + attachments?: SlackAttachment[]; +} & ( + | { + subtype: 'channel_purpose' | 'group_purpose'; + purpose: string; + } + | { + subtype: 'channel_join' | 'group_join' | 'channel_leave' | 'group_leave'; + } + | { + subtype: 'channel_topic' | 'group_topic'; + topic: string; + } + | { + subtype: 'channel_name' | 'group_name'; + name: string; + } + | { + subtype: 'pinned_item'; + attachments: SlackAttachment[]; + } + | { + subtype: 'file_share'; + file: SlackFile; + } + | { + subtype: 'me_message'; + } +); + +type SlackAttachment = { + text: string; + title: string; + fallback: string; + author_subname: string; +}; + +export class SlackImporter extends Importer { + private _useUpsert = false; + + async prepareChannelsFile(entry: IZipEntry): Promise { await super.updateProgress(ProgressStep.PREPARING_CHANNELS); - const data = JSON.parse(entry.getData().toString()).filter((channel) => channel.creator != null); + const data = (JSON.parse(entry.getData().toString()) as SlackChannel[]).filter( + (channel): channel is SlackChannel & { creator: string } => 'creator' in channel && channel.creator != null, + ); this.logger.debug(`loaded ${data.length} channels.`); @@ -46,9 +148,11 @@ export class SlackImporter extends Base { return data.length; } - async prepareGroupsFile(entry) { + async prepareGroupsFile(entry: IZipEntry): Promise { await super.updateProgress(ProgressStep.PREPARING_CHANNELS); - const data = JSON.parse(entry.getData().toString()).filter((channel) => channel.creator != null); + const data = (JSON.parse(entry.getData().toString()) as SlackChannel[]).filter( + (channel): channel is SlackChannel & { creator: string } => 'creator' in channel && channel.creator != null, + ); this.logger.debug(`loaded ${data.length} groups.`); @@ -73,15 +177,17 @@ export class SlackImporter extends Base { return data.length; } - async prepareMpimpsFile(entry) { + async prepareMpimpsFile(entry: IZipEntry): Promise { await super.updateProgress(ProgressStep.PREPARING_CHANNELS); - const data = JSON.parse(entry.getData().toString()).filter((channel) => channel.creator != null); + const data = (JSON.parse(entry.getData().toString()) as SlackChannel[]).filter( + (channel): channel is SlackChannel & { creator: string } => 'creator' in channel && channel.creator != null, + ); this.logger.debug(`loaded ${data.length} mpims.`); await this.addCountToTotal(data.length); - const maxUsers = settings.get('DirectMesssage_maxUsers') || 1; + const maxUsers = settings.get('DirectMesssage_maxUsers') || 1; for await (const channel of data) { await this.converter.addChannel({ @@ -102,9 +208,9 @@ export class SlackImporter extends Base { return data.length; } - async prepareDMsFile(entry) { + async prepareDMsFile(entry: IZipEntry): Promise { await super.updateProgress(ProgressStep.PREPARING_CHANNELS); - const data = JSON.parse(entry.getData().toString()); + const data = JSON.parse(entry.getData().toString()) as SlackChannel[]; this.logger.debug(`loaded ${data.length} dms.`); @@ -121,9 +227,9 @@ export class SlackImporter extends Base { return data.length; } - async prepareUsersFile(entry) { + async prepareUsersFile(entry: IZipEntry): Promise { await super.updateProgress(ProgressStep.PREPARING_USERS); - const data = JSON.parse(entry.getData().toString()); + const data = JSON.parse(entry.getData().toString()) as SlackUser[]; this.logger.debug(`loaded ${data.length} users.`); @@ -132,7 +238,7 @@ export class SlackImporter extends Base { await this.addCountToTotal(data.length); for await (const user of data) { - const newUser = { + const newUser: IImportUser = { emails: [], importIds: [user.id], username: user.name, @@ -160,7 +266,7 @@ export class SlackImporter extends Base { return data.length; } - async prepareUsingLocalFile(fullFilePath) { + async prepareUsingLocalFile(fullFilePath: string): Promise { this.logger.debug('start preparing import operation'); await this.converter.clearImportData(); @@ -234,7 +340,7 @@ export class SlackImporter extends Base { await Settings.incrementValueById('Slack_Importer_Count', userCount); } - const missedTypes = {}; + const missedTypes: Record = {}; // If we have no slack message yet, then we can insert them instead of upserting this._useUpsert = !(await Messages.findOne({ _id: /slack\-.*/ })); @@ -262,7 +368,7 @@ export class SlackImporter extends Base { await super.updateProgress(ProgressStep.PREPARING_MESSAGES); } - const tempMessages = JSON.parse(entry.getData().toString()); + const tempMessages = JSON.parse(entry.getData().toString()) as SlackMessage[]; messagesCount += tempMessages.length; await this.updateRecord({ messagesstatus: `${channel}/${date}` }); await this.addCountToTotal(tempMessages.length); @@ -285,7 +391,7 @@ export class SlackImporter extends Base { increaseProgress(); } - if (!_.isEmpty(missedTypes)) { + if (Object.keys(missedTypes).length > 0) { this.logger.info('Missed import types:', missedTypes); } } catch (e) { @@ -299,7 +405,7 @@ export class SlackImporter extends Base { return this.progress; } - parseMentions(newMessage) { + parseMentions(newMessage: IImportMessage): void { const mentionsParser = new MentionsParser({ pattern: () => '[0-9a-zA-Z]+', useRealName: () => settings.get('UI_Use_Real_Name'), @@ -308,8 +414,8 @@ export class SlackImporter extends Base { const users = mentionsParser .getUserMentions(newMessage.msg) - .filter((u) => u) - .map((uid) => this._replaceSlackUserId(uid.slice(1, uid.length))); + .filter((u: string) => u) + .map((uid: string) => this._replaceSlackUserId(uid.slice(1, uid.length))); if (users.length) { if (!newMessage.mentions) { newMessage.mentions = []; @@ -319,8 +425,8 @@ export class SlackImporter extends Base { const channels = mentionsParser .getChannelMentions(newMessage.msg) - .filter((c) => c) - .map((name) => name.slice(1, name.length)); + .filter((c: string) => c) + .map((name: string) => name.slice(1, name.length)); if (channels.length) { if (!newMessage.channels) { newMessage.channels = []; @@ -329,8 +435,13 @@ export class SlackImporter extends Base { } } - async processMessageSubType(message, slackChannelId, newMessage, missedTypes) { - const ignoreTypes = { bot_add: true, file_comment: true, file_mention: true }; + async processMessageSubType( + message: SlackMessage, + slackChannelId: string, + newMessage: IImportMessage, + missedTypes: Record, + ): Promise { + const ignoreTypes: Record = { bot_add: true, file_comment: true, file_mention: true }; switch (message.subtype) { case 'channel_join': @@ -377,7 +488,7 @@ export class SlackImporter extends Base { case 'file_share': if (message.file?.url_private_download) { const fileId = this.makeSlackMessageId(slackChannelId, message.ts, 'share'); - const fileMessage = { + const fileMessage: IImportMessage = { _id: fileId, rid: newMessage.rid, ts: newMessage.ts, @@ -406,7 +517,7 @@ export class SlackImporter extends Base { return false; } - makeSlackMessageId(channelId, ts, fileIndex = undefined) { + makeSlackMessageId(channelId: string, ts: string, fileIndex?: string): string { const base = `slack-${channelId}-${ts.replace(/\./g, '-')}`; if (fileIndex) { @@ -416,12 +527,13 @@ export class SlackImporter extends Base { return base; } - async prepareMessageObject(message, missedTypes, slackChannelId) { + async prepareMessageObject(message: SlackMessage, missedTypes: Record, slackChannelId: string): Promise { const id = this.makeSlackMessageId(slackChannelId, message.ts); - const newMessage = { + const newMessage: IImportMessage = { _id: id, rid: slackChannelId, ts: new Date(parseInt(message.ts.split('.')[0]) * 1000), + msg: '', u: { _id: this._replaceSlackUserId(message.user), }, @@ -429,17 +541,13 @@ export class SlackImporter extends Base { // Process the reactions if (message.reactions && message.reactions.length > 0) { - newMessage.reactions = new Map(); - - message.reactions.forEach((reaction) => { + newMessage.reactions = message.reactions.reduce((newReactions, reaction) => { const name = `:${reaction.name}:`; - if (reaction.users && reaction.users.length) { - newMessage.reactions.set(name, { - name, - users: this._replaceSlackUserIds(reaction.users), - }); - } - }); + return { + ...newReactions, + ...(reaction.users?.length ? { name: { name, users: this._replaceSlackUserIds(reaction.users) } } : {}), + }; + }, {} as Required['reactions']); } if (message.type === 'message') { @@ -448,8 +556,8 @@ export class SlackImporter extends Base { const promises = message.files.map(async (file) => { fileIndex++; - const fileId = this.makeSlackMessageId(slackChannelId, message.ts, fileIndex); - const fileMessage = { + const fileId = this.makeSlackMessageId(slackChannelId, message.ts, String(fileIndex)); + const fileMessage: IImportMessage = { _id: fileId, rid: slackChannelId, ts: newMessage.ts, @@ -493,21 +601,21 @@ export class SlackImporter extends Base { if (message.thread_ts) { if (message.thread_ts === message.ts) { if (message.reply_users) { - const replies = new Set(); - message.reply_users.forEach((item) => { + const replies = new Set(); + message.reply_users.forEach((item: string) => { replies.add(this._replaceSlackUserId(item)); }); - if (replies.length) { + if (replies.size) { newMessage.replies = Array.from(replies); } } else if (message.replies) { - const replies = new Set(); - message.repĺies.forEach((item) => { + const replies = new Set(); + message.replies.forEach((item: { user: string }) => { replies.add(this._replaceSlackUserId(item.user)); }); - if (replies.length) { + if (replies.size) { newMessage.replies = Array.from(replies); } } else { @@ -532,7 +640,7 @@ export class SlackImporter extends Base { newMessage.attachments = this.convertMessageAttachments(message.attachments); } - if (message.icons && message.icons.emoji) { + if (message.icons?.emoji) { newMessage.emoji = message.icons.emoji; } @@ -542,7 +650,7 @@ export class SlackImporter extends Base { } } - _replaceSlackUserId(userId) { + _replaceSlackUserId(userId: string): string { if (userId === 'USLACKBOT') { return 'rocket.cat'; } @@ -550,14 +658,14 @@ export class SlackImporter extends Base { return userId; } - _replaceSlackUserIds(members) { + _replaceSlackUserIds(members: string[]) { if (!members?.length) { return []; } return members.map((userId) => this._replaceSlackUserId(userId)); } - convertSlackMessageToRocketChat(message) { + convertSlackMessageToRocketChat(message: string): string { if (message) { message = message.replace(//g, '@all'); message = message.replace(//g, '@all'); @@ -581,7 +689,7 @@ export class SlackImporter extends Base { return message; } - convertSlackFileToPendingFile(file) { + convertSlackFileToPendingFile(file: SlackFile): IImportPendingFile { return { downloadUrl: file.url_private_download, id: file.id, @@ -595,9 +703,9 @@ export class SlackImporter extends Base { }; } - convertMessageAttachments(attachments) { - if (!attachments || !attachments.length) { - return attachments; + convertMessageAttachments(attachments: SlackAttachment[]): IImportMessage['attachments'] { + if (!attachments?.length) { + return undefined; } return attachments.map((attachment) => ({ diff --git a/apps/meteor/app/importer-slack/server/index.ts b/apps/meteor/app/importer-slack/server/index.ts index 6442d31dfa428..b8040d77538a5 100644 --- a/apps/meteor/app/importer-slack/server/index.ts +++ b/apps/meteor/app/importer-slack/server/index.ts @@ -1,5 +1,8 @@ import { Importers } from '../../importer/server'; -import { SlackImporterInfo } from '../lib/info'; -import { SlackImporter } from './importer'; +import { SlackImporter } from './SlackImporter'; -Importers.add(new SlackImporterInfo(), SlackImporter); +Importers.add({ + key: 'slack', + name: 'Slack', + importer: SlackImporter, +}); diff --git a/apps/meteor/app/importer/client/index.ts b/apps/meteor/app/importer/client/index.ts deleted file mode 100644 index 7cd9fa3c3440e..0000000000000 --- a/apps/meteor/app/importer/client/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Importers } from '../lib/Importers'; diff --git a/apps/meteor/app/importer/lib/ImporterInfo.js b/apps/meteor/app/importer/lib/ImporterInfo.js deleted file mode 100644 index 627c1d786a283..0000000000000 --- a/apps/meteor/app/importer/lib/ImporterInfo.js +++ /dev/null @@ -1,19 +0,0 @@ -export class ImporterInfo { - /** - * Creates a new class which contains information about the importer. - * - * @param {string} key The unique key of this importer. - * @param {string} name The i18n name. - * @param {string} mimeType The type of file it expects. - * @param {{ href: string, text: string }[]} warnings An array of warning objects. `{ href, text }` - */ - constructor(key, name = '', mimeType = '', warnings = []) { - this.key = key; - this.name = name; - this.mimeType = mimeType; - this.warnings = warnings; - - this.importer = undefined; - this.instance = undefined; - } -} diff --git a/apps/meteor/app/importer/lib/ImporterProgressStep.ts b/apps/meteor/app/importer/lib/ImporterProgressStep.ts index da81c050967cb..1b5ffe53c93ff 100644 --- a/apps/meteor/app/importer/lib/ImporterProgressStep.ts +++ b/apps/meteor/app/importer/lib/ImporterProgressStep.ts @@ -1,3 +1,5 @@ +import type { IImportProgress } from '@rocket.chat/core-typings'; + /** The progress step that an importer is at. */ export const ProgressStep = Object.freeze({ NEW: 'importer_new', @@ -22,20 +24,20 @@ export const ProgressStep = Object.freeze({ DONE: 'importer_done', ERROR: 'importer_import_failed', CANCELLED: 'importer_import_cancelled', -}); +} satisfies Record); -export const ImportWaitingStates = [ProgressStep.NEW, ProgressStep.UPLOADING, ProgressStep.DOWNLOADING_FILE]; +export const ImportWaitingStates: IImportProgress['step'][] = [ProgressStep.NEW, ProgressStep.UPLOADING, ProgressStep.DOWNLOADING_FILE]; -export const ImportFileReadyStates = [ProgressStep.FILE_LOADED]; +export const ImportFileReadyStates: IImportProgress['step'][] = [ProgressStep.FILE_LOADED]; -export const ImportPreparingStartedStates = [ +export const ImportPreparingStartedStates: IImportProgress['step'][] = [ ProgressStep.PREPARING_STARTED, ProgressStep.PREPARING_USERS, ProgressStep.PREPARING_CHANNELS, ProgressStep.PREPARING_MESSAGES, ]; -export const ImportingStartedStates = [ +export const ImportingStartedStates: IImportProgress['step'][] = [ ProgressStep.IMPORTING_STARTED, ProgressStep.IMPORTING_USERS, ProgressStep.IMPORTING_CHANNELS, @@ -44,4 +46,4 @@ export const ImportingStartedStates = [ ProgressStep.FINISHING, ]; -export const ImportingErrorStates = [ProgressStep.ERROR, ProgressStep.CANCELLED]; +export const ImportingErrorStates: IImportProgress['step'][] = [ProgressStep.ERROR, ProgressStep.CANCELLED]; diff --git a/apps/meteor/app/importer/lib/Importers.js b/apps/meteor/app/importer/lib/Importers.js deleted file mode 100644 index 13acf2b616d87..0000000000000 --- a/apps/meteor/app/importer/lib/Importers.js +++ /dev/null @@ -1,47 +0,0 @@ -import { ImporterInfo } from './ImporterInfo'; - -/** Container class which holds all of the importer details. */ -class ImportersContainer { - constructor() { - this.importers = new Map(); - } - - /** - * Adds an importer to the import collection. Adding it more than once will - * overwrite the previous one. - * - * @param {ImporterInfo} info The information related to the importer. - * @param {*} [importer] The class for the importer, will be undefined on the client. - */ - add(info, importer) { - if (!(info instanceof ImporterInfo)) { - throw new Error('The importer must be a valid ImporterInfo instance.'); - } - - info.importer = importer; - - this.importers.set(info.key, info); - - return this.importers.get(info.key); - } - - /** - * Gets the importer information that is stored. - * - * @param {string} key The key of the importer. - */ - get(key) { - return this.importers.get(key); - } - - /** - * Gets all of the importers in array format. - * - * @returns {ImporterInfo[]} The array of importer information. - */ - getAll() { - return Array.from(this.importers.values()); - } -} - -export const Importers = new ImportersContainer(); diff --git a/apps/meteor/app/importer/server/classes/ImportDataConverter.ts b/apps/meteor/app/importer/server/classes/ImportDataConverter.ts index 1b596d625d9b4..fff8e8c9efd48 100644 --- a/apps/meteor/app/importer/server/classes/ImportDataConverter.ts +++ b/apps/meteor/app/importer/server/classes/ImportDataConverter.ts @@ -60,7 +60,6 @@ export type IConverterOptions = { flagEmailsAsVerified?: boolean; skipExistingUsers?: boolean; skipNewUsers?: boolean; - bindSkippedUsers?: boolean; skipUserCallbacks?: boolean; skipDefaultChannels?: boolean; @@ -100,7 +99,6 @@ export class ImportDataConverter { flagEmailsAsVerified: false, skipExistingUsers: false, skipNewUsers: false, - bindSkippedUsers: false, }; this._userCache = new Map(); this._userDisplayNameCache = new Map(); @@ -396,18 +394,6 @@ export class ImportDataConverter { } async findExistingUser(data: IImportUser): Promise { - // If we're gonna force-bind importIds, we search for them first to ensure they are unique - if (this._options.bindSkippedUsers) { - // #TODO: Use a single operation for multiple IDs - // (Currently there's no existing use case with multiple IDs being passed to this function) - for await (const importId of data.importIds) { - const importedUser = await Users.findOneByImportId(importId, {}); - if (importedUser) { - return importedUser; - } - } - } - if (data.emails.length) { const emailUser = await Users.findOneByEmailAddress(data.emails[0], {}); @@ -488,13 +474,6 @@ export class ImportDataConverter { const existingUser = await this.findExistingUser(data); if (existingUser && this._options.skipExistingUsers) { - if (this._options.bindSkippedUsers) { - const newImportIds = data.importIds.filter((importId) => !(existingUser as IUser).importIds?.includes(importId)); - if (newImportIds.length) { - await Users.addImportIds(existingUser._id, newImportIds); - } - } - await this.skipRecord(_id); skippedCount++; continue; diff --git a/apps/meteor/app/importer/server/classes/ImporterBase.js b/apps/meteor/app/importer/server/classes/Importer.ts similarity index 67% rename from apps/meteor/app/importer/server/classes/ImporterBase.js rename to apps/meteor/app/importer/server/classes/Importer.ts index 061644130a66e..92f506b379ad0 100644 --- a/apps/meteor/app/importer/server/classes/ImporterBase.js +++ b/apps/meteor/app/importer/server/classes/Importer.ts @@ -1,71 +1,87 @@ +import type { IImport, IImportRecord, IImportChannel, IImportUser, IImportProgress } from '@rocket.chat/core-typings'; import { Logger } from '@rocket.chat/logger'; import { Settings, ImportData, Imports } from '@rocket.chat/models'; import AdmZip from 'adm-zip'; +import type { MatchKeysAndValues, MongoServerError } from 'mongodb'; import { Selection, SelectionChannel, SelectionUser } from '..'; import { callbacks } from '../../../../lib/callbacks'; import { t } from '../../../utils/lib/i18n'; -import { ImporterInfo } from '../../lib/ImporterInfo'; import { ProgressStep, ImportPreparingStartedStates } from '../../lib/ImporterProgressStep'; +import type { ImporterInfo } from '../definitions/ImporterInfo'; import { ImportDataConverter } from './ImportDataConverter'; -import { Progress } from './ImporterProgress'; +import type { IConverterOptions } from './ImportDataConverter'; +import { ImporterProgress } from './ImporterProgress'; import { ImporterWebsocket } from './ImporterWebsocket'; +type OldSettings = { + allowedDomainList?: string | null; + allowUsernameChange?: boolean | null; + maxFileSize?: number | null; + mediaTypeWhiteList?: string | null; + mediaTypeBlackList?: string | null; +}; + /** * Base class for all of the importers. */ -export class Base { - constructor(info, importRecord, converterOptions = {}) { - if (!(info instanceof ImporterInfo)) { +export class Importer { + private _reportProgressHandler: ReturnType | undefined; + + protected AdmZip = AdmZip; + + protected converter: ImportDataConverter; + + protected info: ImporterInfo; + + protected logger: Logger; + + protected oldSettings: OldSettings; + + protected _lastProgressReportTotal = 0; + + public importRecord: IImport; + + public progress: ImporterProgress; + + constructor(info: ImporterInfo, importRecord: IImport, converterOptions: IConverterOptions = {}) { + if (!info.key || !info.importer) { throw new Error('Information passed in must be a valid ImporterInfo instance.'); } - this.AdmZip = AdmZip; this.converter = new ImportDataConverter(converterOptions); - this.startImport = this.startImport.bind(this); - this.getProgress = this.getProgress.bind(this); - this.updateProgress = this.updateProgress.bind(this); - this.addCountToTotal = this.addCountToTotal.bind(this); - this.addCountCompleted = this.addCountCompleted.bind(this); - this.updateRecord = this.updateRecord.bind(this); - this.info = info; this.logger = new Logger(`${this.info.name} Importer`); this.converter.setLogger(this.logger); this.importRecord = importRecord; - this.progress = new Progress(this.info.key, this.info.name); - this.users = {}; - this.channels = {}; - this.messages = {}; + this.progress = new ImporterProgress(this.info.key, this.info.name); this.oldSettings = {}; this.progress.step = this.importRecord.status; this._lastProgressReportTotal = 0; this.reloadCount(); + + this.logger.debug(`Constructed a new ${this.info.name} Importer.`); } /** * Registers the file name and content type on the import operation - * - * @param {string} fileName The name of the uploaded file. - * @param {string} contentType The sent file type. - * @returns {Progress} The progress record of the import. */ - async startFileUpload(fileName, contentType) { + async startFileUpload(fileName: string, contentType?: string): Promise { await this.updateProgress(ProgressStep.UPLOADING); - return this.updateRecord({ file: fileName, contentType }); + return this.updateRecord({ file: fileName, ...(contentType ? { contentType } : {}) }); } /** * Takes the uploaded file and extracts the users, channels, and messages from it. * - * @param {string} fullFilePath the full path of the uploaded file - * @returns {Progress} The progress record of the import. + * @param {string} _fullFilePath the full path of the uploaded file + * @returns {ImporterProgress} The progress record of the import. */ - async prepareUsingLocalFile() { + async prepareUsingLocalFile(_fullFilePath: string): Promise { return this.updateProgress(ProgressStep.PREPARING_STARTED); } @@ -76,9 +92,9 @@ export class Base { * The returned object should be the progress. * * @param {Selection} importSelection The selection data. - * @returns {Progress} The progress record of the import. + * @returns {ImporterProgress} The progress record of the import. */ - async startImport(importSelection, startedByUserId) { + async startImport(importSelection: Selection, startedByUserId: string): Promise { if (!(importSelection instanceof Selection)) { throw new Error(`Invalid Selection data provided to the ${this.info.name} importer.`); } else if (importSelection.users === undefined) { @@ -88,6 +104,9 @@ export class Base { `Channels in the selected data wasn't found, it must but at least an empty array for the ${this.info.name} importer.`, ); } + if (!startedByUserId) { + throw new Error('You must be logged in to do this.'); + } if (!startedByUserId) { throw new Error('You must be logged in to do this.'); @@ -97,7 +116,7 @@ export class Base { this.reloadCount(); const started = Date.now(); - const beforeImportFn = async ({ data, dataType: type }) => { + const beforeImportFn = async ({ data, dataType: type }: IImportRecord) => { if (this.importRecord.valid === false) { this.converter.aborted = true; throw new Error('The import operation is no longer valid.'); @@ -105,7 +124,13 @@ export class Base { switch (type) { case 'channel': { - const id = data.t === 'd' ? '__directMessages__' : data.importIds[0]; + if (!importSelection.channels) { + return true; + } + + const channelData = data as IImportChannel; + + const id = channelData.t === 'd' ? '__directMessages__' : channelData.importIds[0]; for (const channel of importSelection.channels) { if (channel.channel_id === id) { return channel.do_import; @@ -115,12 +140,14 @@ export class Base { return false; } case 'user': { - // #TODO: Replace this workaround in the final version of the API importer + // #TODO: Replace this workaround if (importSelection.users.length === 0 && this.info.key === 'api') { return true; } - const id = data.importIds[0]; + const userData = data as IImportUser; + + const id = userData.importIds[0]; for (const user of importSelection.users) { if (user.user_id === id) { return user.do_import; @@ -143,7 +170,7 @@ export class Base { } }; - const afterBatchFn = async (successCount, errorCount) => { + const afterBatchFn = async (successCount: number, errorCount: number) => { if (successCount) { await this.addCountCompleted(successCount); } @@ -200,10 +227,10 @@ export class Base { } async backupSettingValues() { - const allowUsernameChange = await Settings.findOneById('Accounts_AllowUsernameChange').value; - const maxFileSize = await Settings.findOneById('FileUpload_MaxFileSize').value; - const mediaTypeWhiteList = await Settings.findOneById('FileUpload_MediaTypeWhiteList').value; - const mediaTypeBlackList = await Settings.findOneById('FileUpload_MediaTypeBlackList').value; + const allowUsernameChange = (await Settings.findOneById('Accounts_AllowUsernameChange'))?.value as boolean | null; + const maxFileSize = (await Settings.findOneById('FileUpload_MaxFileSize'))?.value as number | null; + const mediaTypeWhiteList = (await Settings.findOneById('FileUpload_MediaTypeWhiteList'))?.value as string | null; + const mediaTypeBlackList = (await Settings.findOneById('FileUpload_MediaTypeBlackList'))?.value as string | null; this.oldSettings = { allowUsernameChange, @@ -213,19 +240,14 @@ export class Base { }; } - async applySettingValues(settingValues) { + async applySettingValues(settingValues: OldSettings) { await Settings.updateValueById('Accounts_AllowUsernameChange', settingValues.allowUsernameChange ?? true); await Settings.updateValueById('FileUpload_MaxFileSize', settingValues.maxFileSize ?? -1); await Settings.updateValueById('FileUpload_MediaTypeWhiteList', settingValues.mediaTypeWhiteList ?? '*'); await Settings.updateValueById('FileUpload_MediaTypeBlackList', settingValues.mediaTypeBlackList ?? ''); } - /** - * Gets the progress of this import. - * - * @returns {Progress} The progress record of the import. - */ - getProgress() { + getProgress(): ImporterProgress { return this.progress; } @@ -235,9 +257,9 @@ export class Base { * This way the importer can adjust user/room information at will. * * @param {ProgressStep} step The progress step which this import is currently at. - * @returns {Progress} The progress record of the import. + * @returns {ImporterProgress} The progress record of the import. */ - async updateProgress(step) { + async updateProgress(step: IImportProgress['step']): Promise { this.progress.step = step; this.logger.debug(`${this.info.name} is now at ${step}.`); @@ -252,12 +274,6 @@ export class Base { } reloadCount() { - if (!this.importRecord.count) { - this.progress.count.total = 0; - this.progress.count.completed = 0; - this.progress.count.error = 0; - } - this.progress.count.total = this.importRecord.count?.total || 0; this.progress.count.completed = this.importRecord.count?.completed || 0; this.progress.count.error = this.importRecord.count?.error || 0; @@ -267,9 +283,9 @@ export class Base { * Adds the passed in value to the total amount of items needed to complete. * * @param {number} count The amount to add to the total count of items. - * @returns {Progress} The progress record of the import. + * @returns {ImporterProgress} The progress record of the import. */ - async addCountToTotal(count) { + async addCountToTotal(count: number): Promise { this.progress.count.total += count; await this.updateRecord({ 'count.total': this.progress.count.total }); @@ -280,15 +296,15 @@ export class Base { * Adds the passed in value to the total amount of items completed. * * @param {number} count The amount to add to the total count of finished items. - * @returns {Progress} The progress record of the import. + * @returns {ImporterProgress} The progress record of the import. */ - async addCountCompleted(count) { + async addCountCompleted(count: number): Promise { this.progress.count.completed += count; return this.maybeUpdateRecord(); } - async addCountError(count) { + async addCountError(count: number): Promise { this.progress.count.error += count; return this.maybeUpdateRecord(); @@ -298,7 +314,11 @@ export class Base { // Only update the database every 500 messages (or 50 for users/channels) // Or the completed is greater than or equal to the total amount const count = this.progress.count.completed + this.progress.count.error; - const range = [ProgressStep.IMPORTING_USERS, ProgressStep.IMPORTING_CHANNELS].includes(this.progress.step) ? 50 : 500; + const range = ([ProgressStep.IMPORTING_USERS, ProgressStep.IMPORTING_CHANNELS] as IImportProgress['step'][]).includes( + this.progress.step, + ) + ? 50 + : 500; if (count % range === 0 || count >= this.progress.count.total || count - this._lastProgressReportTotal > range) { this._lastProgressReportTotal = this.progress.count.completed + this.progress.count.error; @@ -328,18 +348,20 @@ export class Base { /** * Updates the import record with the given fields being `set`. - * - * @param {any} fields The fields to set, it should be an object with key/values. - * @returns {Imports} The import record. */ - async updateRecord(fields) { + async updateRecord(fields: MatchKeysAndValues): Promise { + if (!this.importRecord) { + return this.importRecord; + } + await Imports.update({ _id: this.importRecord._id }, { $set: fields }); - this.importRecord = await Imports.findOne(this.importRecord._id); + // #TODO: Remove need for the typecast + this.importRecord = (await Imports.findOne(this.importRecord._id)) as IImport; return this.importRecord; } - async buildSelection() { + async buildSelection(): Promise { await this.updateProgress(ProgressStep.USER_SELECTION); const users = await ImportData.getAllUsersForSelection(); @@ -351,25 +373,23 @@ export class Base { new SelectionUser(u.data.importIds[0], u.data.username, u.data.emails[0], Boolean(u.data.deleted), u.data.type === 'bot', true), ); const selectionChannels = channels.map( - (c) => - new SelectionChannel( - c.data.importIds[0], - c.data.name, - Boolean(c.data.archived), - true, - c.data.t === 'p', - undefined, - c.data.t === 'd', - ), + (c) => new SelectionChannel(c.data.importIds[0], c.data.name, Boolean(c.data.archived), true, c.data.t === 'p', c.data.t === 'd'), ); const selectionMessages = await ImportData.countMessages(); if (hasDM) { - selectionChannels.push(new SelectionChannel('__directMessages__', t('Direct_Messages'), false, true, true, undefined, true)); + selectionChannels.push(new SelectionChannel('__directMessages__', t('Direct_Messages'), false, true, true, true)); } const results = new Selection(this.info.name, selectionUsers, selectionChannels, selectionMessages); return results; } + + /** + * Utility method to check if the passed in error is a `MongoServerError` with the `codeName` of `'CursorNotFound'`. + */ + protected isCursorNotFoundError(error: unknown): error is MongoServerError & { codeName: 'CursorNotFound' } { + return typeof error === 'object' && error !== null && 'codeName' in error && error.codeName === 'CursorNotFound'; + } } diff --git a/apps/meteor/app/importer/server/classes/ImporterProgress.js b/apps/meteor/app/importer/server/classes/ImporterProgress.js deleted file mode 100644 index 7bb49c50c4c2b..0000000000000 --- a/apps/meteor/app/importer/server/classes/ImporterProgress.js +++ /dev/null @@ -1,16 +0,0 @@ -import { ProgressStep } from '../../lib/ImporterProgressStep'; - -export class Progress { - /** - * Creates a new progress container for the importer. - * - * @param {string} key The unique key of the importer. - * @param {string} name The name of the importer. - */ - constructor(key, name) { - this.key = key; - this.name = name; - this.step = ProgressStep.NEW; - this.count = { completed: 0, total: 0 }; - } -} diff --git a/apps/meteor/app/importer/server/classes/ImporterProgress.ts b/apps/meteor/app/importer/server/classes/ImporterProgress.ts new file mode 100644 index 0000000000000..ff59a5eb20b14 --- /dev/null +++ b/apps/meteor/app/importer/server/classes/ImporterProgress.ts @@ -0,0 +1,30 @@ +import type { IImportProgress } from '@rocket.chat/core-typings'; + +import { ProgressStep } from '../../lib/ImporterProgressStep'; + +export class ImporterProgress implements IImportProgress { + public key: string; + + public name: string; + + public step: IImportProgress['step']; + + public count: { + completed: number; + total: number; + error: number; + }; + + /** + * Creates a new progress container for the importer. + * + * @param {string} key The unique key of the importer. + * @param {string} name The name of the importer. + */ + constructor(key: string, name: string) { + this.key = key; + this.name = name; + this.step = ProgressStep.NEW; + this.count = { completed: 0, total: 0, error: 0 }; + } +} diff --git a/apps/meteor/app/importer/server/classes/ImporterSelection.js b/apps/meteor/app/importer/server/classes/ImporterSelection.js deleted file mode 100644 index f3e055e9fb758..0000000000000 --- a/apps/meteor/app/importer/server/classes/ImporterSelection.js +++ /dev/null @@ -1,16 +0,0 @@ -export class Selection { - /** - * Constructs a new importer selection object. - * - * @param {string} name the name of the importer - * @param {SelectionUser[]} users the users which can be selected - * @param {SelectionChannel[]} channels the channels which can be selected - * @param {number} message_count the number of messages - */ - constructor(name, users, channels, message_count) { - this.name = name; - this.users = users; - this.channels = channels; - this.message_count = message_count; - } -} diff --git a/apps/meteor/app/importer/server/classes/ImporterSelection.ts b/apps/meteor/app/importer/server/classes/ImporterSelection.ts new file mode 100644 index 0000000000000..107dbbf9c8246 --- /dev/null +++ b/apps/meteor/app/importer/server/classes/ImporterSelection.ts @@ -0,0 +1,26 @@ +import type { IImporterSelection, IImporterSelectionChannel, IImporterSelectionUser } from '@rocket.chat/core-typings'; + +export class ImporterSelection implements IImporterSelection { + public name: string; + + public users: IImporterSelectionUser[]; + + public channels: IImporterSelectionChannel[]; + + public message_count: number; + + /** + * Constructs a new importer selection object. + * + * @param name the name of the importer + * @param users the users which can be selected + * @param channels the channels which can be selected + * @param messageCount the number of messages + */ + constructor(name: string, users: IImporterSelectionUser[], channels: IImporterSelectionChannel[], messageCount: number) { + this.name = name; + this.users = users; + this.channels = channels; + this.message_count = messageCount; + } +} diff --git a/apps/meteor/app/importer/server/classes/ImporterSelectionChannel.js b/apps/meteor/app/importer/server/classes/ImporterSelectionChannel.js deleted file mode 100644 index deb23849f1084..0000000000000 --- a/apps/meteor/app/importer/server/classes/ImporterSelectionChannel.js +++ /dev/null @@ -1,22 +0,0 @@ -export class SelectionChannel { - /** - * Constructs a new selection channel. - * - * @param {string} channel_id the unique identifier of the channel - * @param {string} name the name of the channel - * @param {boolean} is_archived whether the channel was archived or not - * @param {boolean} do_import whether we will be importing the channel or not - * @param {boolean} is_private whether the channel is private or public - * @param {int} creator the id of the channel owner - * @param {boolean} is_direct whether the channel represents direct messages - */ - constructor(channel_id, name, is_archived, do_import, is_private, creator, is_direct) { - this.channel_id = channel_id; - this.name = name; - this.is_archived = is_archived; - this.do_import = do_import; - this.is_private = is_private; - this.creator = creator; - this.is_direct = is_direct; - } -} diff --git a/apps/meteor/app/importer/server/classes/ImporterSelectionChannel.ts b/apps/meteor/app/importer/server/classes/ImporterSelectionChannel.ts new file mode 100644 index 0000000000000..359a18de5a6c1 --- /dev/null +++ b/apps/meteor/app/importer/server/classes/ImporterSelectionChannel.ts @@ -0,0 +1,34 @@ +import type { IImporterSelectionChannel } from '@rocket.chat/core-typings'; + +export class SelectionChannel implements IImporterSelectionChannel { + public channel_id: string; + + public name: string | undefined; + + public is_archived: boolean; + + public do_import: boolean; + + public is_private: boolean; + + public is_direct: boolean; + + public creator: undefined; + + /** + * Constructs a new selection channel. + * + * @param channelId the unique identifier of the channel + * @param name the name of the channel + * @param isArchived whether the channel was archived or not + * @param doImport whether we will be importing the channel or not + */ + constructor(channelId: string, name: string | undefined, isArchived: boolean, doImport: boolean, isPrivate: boolean, isDirect: boolean) { + this.channel_id = channelId; + this.name = name; + this.is_archived = isArchived; + this.do_import = doImport; + this.is_private = isPrivate; + this.is_direct = isDirect; + } +} diff --git a/apps/meteor/app/importer/server/classes/ImporterSelectionUser.js b/apps/meteor/app/importer/server/classes/ImporterSelectionUser.js deleted file mode 100644 index 7d243fde3dded..0000000000000 --- a/apps/meteor/app/importer/server/classes/ImporterSelectionUser.js +++ /dev/null @@ -1,22 +0,0 @@ -export class SelectionUser { - /** - * Constructs a new selection user. - * - * @param {string} user_id the unique user identifier - * @param {string} username the user's username - * @param {string} email the user's email - * @param {boolean} is_deleted whether the user was deleted or not - * @param {boolean} is_bot whether the user is a bot or not - * @param {boolean} do_import whether we are going to import this user or not - * @param {boolean} is_email_taken whether there's an existing user with the same email - */ - constructor(user_id, username, email, is_deleted, is_bot, do_import, is_email_taken = false) { - this.user_id = user_id; - this.username = username; - this.email = email; - this.is_deleted = is_deleted; - this.is_bot = is_bot; - this.do_import = do_import; - this.is_email_taken = is_email_taken; - } -} diff --git a/apps/meteor/app/importer/server/classes/ImporterSelectionUser.ts b/apps/meteor/app/importer/server/classes/ImporterSelectionUser.ts new file mode 100644 index 0000000000000..5dc9cb91b4c4c --- /dev/null +++ b/apps/meteor/app/importer/server/classes/ImporterSelectionUser.ts @@ -0,0 +1,46 @@ +import type { IImporterSelectionUser } from '@rocket.chat/core-typings'; + +export class SelectionUser implements IImporterSelectionUser { + public user_id: string; + + public username: string | undefined; + + public email: string; + + public is_deleted: boolean; + + public is_bot: boolean; + + public do_import: boolean; + + public is_email_taken: boolean; + + /** + * Constructs a new selection user. + * + * @param userId the unique user identifier + * @param username the user's username + * @param email the user's email + * @param isDeleted whether the user was deleted or not + * @param isBot whether the user is a bot or not + * @param doImport whether we are going to import this user or not + * @param isEmailTaken whether there's an existing user with the same email + */ + constructor( + userId: string, + username: string | undefined, + email: string, + isDeleted: boolean, + isBot: boolean, + doImport: boolean, + isEmailTaken = false, + ) { + this.user_id = userId; + this.username = username; + this.email = email; + this.is_deleted = isDeleted; + this.is_bot = isBot; + this.do_import = doImport; + this.is_email_taken = isEmailTaken; + } +} diff --git a/apps/meteor/app/importer/server/classes/ImporterWebsocket.js b/apps/meteor/app/importer/server/classes/ImporterWebsocket.ts similarity index 62% rename from apps/meteor/app/importer/server/classes/ImporterWebsocket.js rename to apps/meteor/app/importer/server/classes/ImporterWebsocket.ts index ba067fda38675..a08e62f8435c4 100644 --- a/apps/meteor/app/importer/server/classes/ImporterWebsocket.js +++ b/apps/meteor/app/importer/server/classes/ImporterWebsocket.ts @@ -1,6 +1,11 @@ +import type { IImportProgress } from '@rocket.chat/core-typings'; +import type { IStreamer } from 'meteor/rocketchat:streamer'; + import notifications from '../../../notifications/server/lib/Notifications'; class ImporterWebsocketDef { + private streamer: IStreamer<'importers'>; + constructor() { this.streamer = notifications.streamImporters; } @@ -10,7 +15,7 @@ class ImporterWebsocketDef { * * @param {Progress} progress The progress of the import. */ - progressUpdated(progress) { + progressUpdated(progress: { rate: number } | IImportProgress) { this.streamer.emit('progress', progress); } } diff --git a/apps/meteor/app/importer/server/classes/ImportersContainer.ts b/apps/meteor/app/importer/server/classes/ImportersContainer.ts new file mode 100644 index 0000000000000..97c98701e49e1 --- /dev/null +++ b/apps/meteor/app/importer/server/classes/ImportersContainer.ts @@ -0,0 +1,27 @@ +import type { ImporterInfo } from '../definitions/ImporterInfo'; + +/** Container class which holds all of the importer details. */ +export class ImportersContainer { + private importers: Map; + + constructor() { + this.importers = new Map(); + } + + add({ key, name, importer, visible }: Omit & { visible?: boolean }) { + this.importers.set(key, { + key, + name, + visible: visible !== false, + importer, + }); + } + + get(key: string): ImporterInfo | undefined { + return this.importers.get(key); + } + + getAllVisible(): ImporterInfo[] { + return Array.from(this.importers.values()).filter(({ visible }) => visible); + } +} diff --git a/apps/meteor/app/importer/server/definitions/ImporterInfo.ts b/apps/meteor/app/importer/server/definitions/ImporterInfo.ts new file mode 100644 index 0000000000000..4741b3b70f2d7 --- /dev/null +++ b/apps/meteor/app/importer/server/definitions/ImporterInfo.ts @@ -0,0 +1,10 @@ +import type { RocketchatI18nKeys } from '@rocket.chat/i18n'; + +import type { Importer } from '../classes/Importer'; + +export type ImporterInfo = { + key: string; + name: RocketchatI18nKeys; + visible: boolean; // Determines if this importer can be selected by the user in the New Import screen. + importer: typeof Importer; +}; diff --git a/apps/meteor/app/importer/server/index.ts b/apps/meteor/app/importer/server/index.ts index 8d7d872455c0c..02949733174c5 100644 --- a/apps/meteor/app/importer/server/index.ts +++ b/apps/meteor/app/importer/server/index.ts @@ -1,16 +1,21 @@ -import { ImporterInfo } from '../lib/ImporterInfo'; import { ProgressStep } from '../lib/ImporterProgressStep'; -import { Importers } from '../lib/Importers'; -import { Base } from './classes/ImporterBase'; -import { Selection } from './classes/ImporterSelection'; +import { Importer } from './classes/Importer'; +import { ImporterSelection } from './classes/ImporterSelection'; import { SelectionChannel } from './classes/ImporterSelectionChannel'; import { SelectionUser } from './classes/ImporterSelectionUser'; import { ImporterWebsocket } from './classes/ImporterWebsocket'; +import { ImportersContainer } from './classes/ImportersContainer'; import './methods'; import './startup/setImportsToInvalid'; import './startup/store'; -// Adding a link to the base class using the 'api' key. This won't be needed in the new importer structure implemented on the parallel PR -Importers.add(new ImporterInfo('api', 'API', ''), Base); +export { Importer, ImporterWebsocket, ProgressStep, ImporterSelection as Selection, SelectionChannel, SelectionUser }; -export { Base, Importers, ImporterWebsocket, ProgressStep, Selection, SelectionChannel, SelectionUser }; +export const Importers = new ImportersContainer(); + +Importers.add({ + key: 'api', + name: 'API', + visible: false, + importer: Importer, +}); diff --git a/apps/meteor/app/importer/server/methods/downloadPublicImportFile.ts b/apps/meteor/app/importer/server/methods/downloadPublicImportFile.ts index dc92901435ea7..81e06ec8eb0f2 100644 --- a/apps/meteor/app/importer/server/methods/downloadPublicImportFile.ts +++ b/apps/meteor/app/importer/server/methods/downloadPublicImportFile.ts @@ -35,14 +35,12 @@ export const executeDownloadPublicImportFile = async (userId: IUser['_id'], file ); } // Check if it's a valid url or path before creating a new import record - if (!isUrl) { - if (!fs.existsSync(fileUrl)) { - throw new Meteor.Error('error-import-file-missing', fileUrl, 'downloadPublicImportFile'); - } + if (!isUrl && !fs.existsSync(fileUrl)) { + throw new Meteor.Error('error-import-file-missing', fileUrl, 'downloadPublicImportFile'); } const operation = await Import.newOperation(userId, importer.name, importer.key); - importer.instance = new importer.importer(importer, operation); // eslint-disable-line new-cap + const instance = new importer.importer(importer, operation); // eslint-disable-line new-cap const oldFileName = fileUrl.substring(fileUrl.lastIndexOf('/') + 1).split('?')[0]; const date = new Date(); @@ -50,17 +48,17 @@ export const executeDownloadPublicImportFile = async (userId: IUser['_id'], file const newFileName = `${dateStr}_${userId}_${oldFileName}`; // Store the file name on the imports collection - await importer.instance.startFileUpload(newFileName); - await importer.instance.updateProgress(ProgressStep.DOWNLOADING_FILE); + await instance.startFileUpload(newFileName); + await instance.updateProgress(ProgressStep.DOWNLOADING_FILE); const writeStream = RocketChatImportFileInstance.createWriteStream(newFileName); writeStream.on('error', () => { - void importer.instance.updateProgress(ProgressStep.ERROR); + void instance.updateProgress(ProgressStep.ERROR); }); writeStream.on('end', () => { - void importer.instance.updateProgress(ProgressStep.FILE_LOADED); + void instance.updateProgress(ProgressStep.FILE_LOADED); }); if (isUrl) { @@ -68,8 +66,8 @@ export const executeDownloadPublicImportFile = async (userId: IUser['_id'], file } else { // If the url is actually a folder path on the current machine, skip moving it to the file store if (fs.statSync(fileUrl).isDirectory()) { - await importer.instance.updateRecord({ file: fileUrl }); - await importer.instance.updateProgress(ProgressStep.FILE_LOADED); + await instance.updateRecord({ file: fileUrl }); + await instance.updateProgress(ProgressStep.FILE_LOADED); return; } diff --git a/apps/meteor/app/importer/server/methods/getImportFileData.ts b/apps/meteor/app/importer/server/methods/getImportFileData.ts index cf81e3074da57..03f9a53abe6c2 100644 --- a/apps/meteor/app/importer/server/methods/getImportFileData.ts +++ b/apps/meteor/app/importer/server/methods/getImportFileData.ts @@ -1,7 +1,7 @@ import fs from 'fs'; import path from 'path'; -import type { IImportFileData } from '@rocket.chat/core-typings'; +import type { IImportProgress, IImporterSelection } from '@rocket.chat/core-typings'; import { Imports } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; @@ -11,7 +11,7 @@ import { hasPermissionAsync } from '../../../authorization/server/functions/hasP import { ProgressStep } from '../../lib/ImporterProgressStep'; import { RocketChatImportFileInstance } from '../startup/store'; -export const executeGetImportFileData = async (): Promise => { +export const executeGetImportFileData = async (): Promise => { const operation = await Imports.findLastImport(); if (!operation) { throw new Meteor.Error('error-operation-not-found', 'Import Operation Not Found', 'getImportFileData'); @@ -24,9 +24,9 @@ export const executeGetImportFileData = async (): Promise= 0) { - if (importer.instance.importRecord?.valid) { + if (waitingSteps.indexOf(instance.progress.step) >= 0) { + if (instance.importRecord?.valid) { return { waiting: true }; } throw new Meteor.Error('error-import-operation-invalid', 'Invalid Import Operation', 'getImportFileData'); } - const readySteps = [ProgressStep.USER_SELECTION, ProgressStep.DONE, ProgressStep.CANCELLED, ProgressStep.ERROR]; + const readySteps: IImportProgress['step'][] = [ + ProgressStep.USER_SELECTION, + ProgressStep.DONE, + ProgressStep.CANCELLED, + ProgressStep.ERROR, + ]; + + if (readySteps.indexOf(instance.progress.step) >= 0) { + return instance.buildSelection(); + } - if (readySteps.indexOf(importer.instance.progress.step) >= 0) { - return importer.instance.buildSelection(); + const fileName = instance.importRecord.file; + if (fileName) { + const fullFilePath = fs.existsSync(fileName) ? fileName : path.join(RocketChatImportFileInstance.absolutePath, fileName); + await instance.prepareUsingLocalFile(fullFilePath); } - const fileName = importer.instance.importRecord.file; - const fullFilePath = fs.existsSync(fileName) ? fileName : path.join(RocketChatImportFileInstance.absolutePath, fileName); - await importer.instance.prepareUsingLocalFile(fullFilePath); - return importer.instance.buildSelection(); + return instance.buildSelection(); }; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { - getImportFileData(): IImportFileData | { waiting: true }; + getImportFileData(): IImporterSelection | { waiting: true }; } } diff --git a/apps/meteor/app/importer/server/methods/getImportProgress.ts b/apps/meteor/app/importer/server/methods/getImportProgress.ts index cdce1da395d70..c03bc44b056d9 100644 --- a/apps/meteor/app/importer/server/methods/getImportProgress.ts +++ b/apps/meteor/app/importer/server/methods/getImportProgress.ts @@ -18,9 +18,9 @@ export const executeGetImportProgress = async (): Promise => { throw new Meteor.Error('error-importer-not-defined', `The importer (${importerKey}) has no import class defined.`, 'getImportProgress'); } - importer.instance = new importer.importer(importer, operation); // eslint-disable-line new-cap + const instance = new importer.importer(importer, operation); // eslint-disable-line new-cap - return importer.instance.getProgress(); + return instance.getProgress(); }; declare module '@rocket.chat/ui-contexts' { diff --git a/apps/meteor/app/importer/server/methods/startImport.ts b/apps/meteor/app/importer/server/methods/startImport.ts index fda135b4fba75..af91295ede292 100644 --- a/apps/meteor/app/importer/server/methods/startImport.ts +++ b/apps/meteor/app/importer/server/methods/startImport.ts @@ -19,25 +19,17 @@ export const executeStartImport = async ({ input }: StartImportParamsPOST, start throw new Meteor.Error('error-importer-not-defined', `The importer (${importerKey}) has no import class defined.`, 'startImport'); } - importer.instance = new importer.importer(importer, operation); // eslint-disable-line new-cap + const instance = new importer.importer(importer, operation); // eslint-disable-line new-cap const usersSelection = input.users.map( (user) => new SelectionUser(user.user_id, user.username, user.email, user.is_deleted, user.is_bot, user.do_import), ); const channelsSelection = input.channels.map( (channel) => - new SelectionChannel( - channel.channel_id, - channel.name, - channel.is_archived, - channel.do_import, - channel.is_private, - channel.creator, - channel.is_direct, - ), + new SelectionChannel(channel.channel_id, channel.name, channel.is_archived, channel.do_import, channel.is_private, channel.is_direct), ); const selection = new Selection(importer.name, usersSelection, channelsSelection, 0); - return importer.instance.startImport(selection, startedByUserId); + await instance.startImport(selection, startedByUserId); }; declare module '@rocket.chat/ui-contexts' { diff --git a/apps/meteor/app/importer/server/methods/uploadImportFile.ts b/apps/meteor/app/importer/server/methods/uploadImportFile.ts index 68955a102b948..d6ded455793b2 100644 --- a/apps/meteor/app/importer/server/methods/uploadImportFile.ts +++ b/apps/meteor/app/importer/server/methods/uploadImportFile.ts @@ -23,25 +23,36 @@ export const executeUploadImportFile = async ( const operation = await Import.newOperation(userId, importer.name, importer.key); - importer.instance = new importer.importer(importer, operation); // eslint-disable-line new-cap + const instance = new importer.importer(importer, operation); // eslint-disable-line new-cap const date = new Date(); const dateStr = `${date.getUTCFullYear()}${date.getUTCMonth()}${date.getUTCDate()}${date.getUTCHours()}${date.getUTCMinutes()}${date.getUTCSeconds()}`; const newFileName = `${dateStr}_${userId}_${fileName}`; // Store the file name and content type on the imports collection - await importer.instance.startFileUpload(newFileName, contentType); + await instance.startFileUpload(newFileName, contentType); // Save the file on the File Store const file = Buffer.from(binaryContent, 'base64'); const readStream = RocketChatFile.bufferToStream(file); const writeStream = RocketChatImportFileInstance.createWriteStream(newFileName, contentType); - writeStream.on('end', () => { - importer.instance.updateProgress(ProgressStep.FILE_LOADED); + await new Promise((resolve, reject) => { + try { + writeStream.on('end', () => { + resolve(); + }); + writeStream.on('error', (e: Error) => { + reject(e); + }); + + readStream.pipe(writeStream); + } catch (error) { + reject(error); + } }); - readStream.pipe(writeStream); + await instance.updateProgress(ProgressStep.FILE_LOADED); }; declare module '@rocket.chat/ui-contexts' { diff --git a/apps/meteor/app/livechat/imports/server/rest/dashboards.ts b/apps/meteor/app/livechat/imports/server/rest/dashboards.ts index 8dfdefc7e9177..eb23e85262fd1 100644 --- a/apps/meteor/app/livechat/imports/server/rest/dashboards.ts +++ b/apps/meteor/app/livechat/imports/server/rest/dashboards.ts @@ -29,19 +29,17 @@ API.v1.addRoute( if (isNaN(Date.parse(start))) { return API.v1.failure('The "start" query parameter must be a valid date.'); } - const startDate = new Date(start); if (isNaN(Date.parse(end))) { return API.v1.failure('The "end" query parameter must be a valid date.'); } - const endDate = new Date(end); const user = await Users.findOneById(this.userId, { projection: { utcOffset: 1, language: 1 } }); if (!user) { return API.v1.failure('User not found'); } - const totalizers = await getConversationsMetricsAsyncCached({ start: startDate, end: endDate, departmentId, user }); + const totalizers = await getConversationsMetricsAsyncCached({ start, end, departmentId, user }); return API.v1.success(totalizers); }, }, @@ -58,19 +56,17 @@ API.v1.addRoute( if (isNaN(Date.parse(start))) { return API.v1.failure('The "start" query parameter must be a valid date.'); } - const startDate = new Date(start); if (isNaN(Date.parse(end))) { return API.v1.failure('The "end" query parameter must be a valid date.'); } - const endDate = new Date(end); const user = await Users.findOneById(this.userId, { projection: { utcOffset: 1, language: 1 } }); if (!user) { return API.v1.failure('User not found'); } - const totalizers = await getAgentsProductivityMetricsAsyncCached({ start: startDate, end: endDate, departmentId, user }); + const totalizers = await getAgentsProductivityMetricsAsyncCached({ start, end, departmentId, user }); return API.v1.success(totalizers); }, }, @@ -111,19 +107,17 @@ API.v1.addRoute( if (isNaN(Date.parse(start))) { return API.v1.failure('The "start" query parameter must be a valid date.'); } - const startDate = new Date(start); if (isNaN(Date.parse(end))) { return API.v1.failure('The "end" query parameter must be a valid date.'); } - const endDate = new Date(end); const user = await Users.findOneById(this.userId, { projection: { utcOffset: 1, language: 1 } }); if (!user) { return API.v1.failure('User not found'); } - const totalizers = await getProductivityMetricsAsyncCached({ start: startDate, end: endDate, departmentId, user }); + const totalizers = await getProductivityMetricsAsyncCached({ start, end, departmentId, user }); return API.v1.success(totalizers); }, diff --git a/apps/meteor/app/livechat/server/index.ts b/apps/meteor/app/livechat/server/index.ts index b6f4e98af6dbb..06ad84ec49ac5 100644 --- a/apps/meteor/app/livechat/server/index.ts +++ b/apps/meteor/app/livechat/server/index.ts @@ -65,7 +65,6 @@ import './methods/sendTranscript'; import './methods/getFirstRoomMessage'; import './methods/getTagsList'; import './methods/getDepartmentForwardRestrictions'; -import './lib/Analytics'; import './lib/QueueManager'; import './lib/RoutingManager'; import './lib/routing/External'; diff --git a/apps/meteor/app/livechat/server/lib/Analytics.js b/apps/meteor/app/livechat/server/lib/Analytics.js deleted file mode 100644 index bff3dd1d25d77..0000000000000 --- a/apps/meteor/app/livechat/server/lib/Analytics.js +++ /dev/null @@ -1,901 +0,0 @@ -import { Logger } from '@rocket.chat/logger'; -import { LivechatRooms } from '@rocket.chat/models'; -import moment from 'moment-timezone'; - -import { callbacks } from '../../../../lib/callbacks'; -import { secondsToHHMMSS } from '../../../../lib/utils/secondsToHHMMSS'; -import { i18n } from '../../../../server/lib/i18n'; -import { getTimezone } from '../../../utils/server/lib/getTimezone'; - -const HOURS_IN_DAY = 24; -const logger = new Logger('OmnichannelAnalytics'); - -async function* dayIterator(from, to) { - const m = moment(from).startOf('day'); - while (m.diff(to, 'days') <= 0) { - yield moment(m); - m.add(1, 'days'); - } -} - -async function* weekIterator(from, to, customDay, timezone) { - const m = moment.tz(from, timezone).day(customDay); - while (m.diff(to, 'weeks') <= 0) { - yield moment(m); - m.add(1, 'weeks'); - } -} - -async function* hourIterator(day) { - const m = moment(day).startOf('day'); - let passedHours = 0; - while (passedHours < HOURS_IN_DAY) { - yield moment(m); - m.add(1, 'hours'); - passedHours++; - } -} - -const OverviewData = { - /** - * - * @param {Map} map - * - * @return {String} - */ - getKeyHavingMaxValue(map, def) { - let maxValue = 0; - let maxKey = def; // default - - map.forEach((value, key) => { - if (value > maxValue) { - maxValue = value; - maxKey = key; - } - }); - - return maxKey; - }, - - /** - * - * @param {Date} from - * @param {Date} to - * - * @returns {Array[Object]} - */ - async Conversations(from, to, departmentId, timezone, t = (v) => v, extraQuery) { - // TODO: most calls to db here can be done in one single call instead of one per day/hour - let totalConversations = 0; // Total conversations - let openConversations = 0; // open conversations - let totalMessages = 0; // total msgs - const totalMessagesOnWeekday = new Map(); // total messages on weekdays i.e Monday, Tuesday... - const totalMessagesInHour = new Map(); // total messages in hour 0, 1, ... 23 of weekday - const days = to.diff(from, 'days') + 1; // total days - - const summarize = - (m) => - ({ metrics, msgs, onHold = false }) => { - if (metrics && !metrics.chatDuration && !onHold) { - openConversations++; - } - totalMessages += msgs; - - const weekday = m.format('dddd'); // @string: Monday, Tuesday ... - totalMessagesOnWeekday.set(weekday, totalMessagesOnWeekday.has(weekday) ? totalMessagesOnWeekday.get(weekday) + msgs : msgs); - }; - - const m = moment.tz(from, timezone).startOf('day').utc(); - // eslint-disable-next-line no-unused-vars - for await (const _ of Array(days).fill(0)) { - const clonedDate = m.clone(); - const date = { - gte: clonedDate, - lt: m.add(1, 'days'), - }; - // eslint-disable-next-line no-await-in-loop - const result = await LivechatRooms.getAnalyticsBetweenDate(date, { departmentId }, extraQuery).toArray(); - totalConversations += result.length; - - result.forEach(summarize(clonedDate)); - } - - const busiestDay = this.getKeyHavingMaxValue(totalMessagesOnWeekday, '-'); // returns key with max value - - // TODO: this code assumes the busiest day is the same every week, which may not be true - // This means that for periods larger than 1 week, the busiest hour won't be the "busiest hour" - // on the period, but the busiest hour on the busiest day. (sorry for busiest excess) - // iterate through all busiestDay in given date-range and find busiest hour - for await (const m of weekIterator(from, to, timezone)) { - if (m < from) { - continue; - } - - for await (const h of hourIterator(m)) { - const date = { - gte: h.clone(), - lt: h.add(1, 'hours'), - }; - (await LivechatRooms.getAnalyticsBetweenDate(date, { departmentId }, extraQuery).toArray()).forEach(({ msgs }) => { - const dayHour = h.format('H'); // @int : 0, 1, ... 23 - totalMessagesInHour.set(dayHour, totalMessagesInHour.has(dayHour) ? totalMessagesInHour.get(dayHour) + msgs : msgs); - }); - } - } - - const utcBusiestHour = this.getKeyHavingMaxValue(totalMessagesInHour, -1); - const busiestHour = { - to: utcBusiestHour >= 0 ? moment.utc().set({ hour: utcBusiestHour }).tz(timezone).format('hA') : '-', - from: utcBusiestHour >= 0 ? moment.utc().set({ hour: utcBusiestHour }).subtract(1, 'hour').tz(timezone).format('hA') : '', - }; - const onHoldConversations = await LivechatRooms.getOnHoldConversationsBetweenDate(from, to, departmentId, extraQuery); - - return [ - { - title: 'Total_conversations', - value: totalConversations, - }, - { - title: 'Open_conversations', - value: openConversations, - }, - { - title: 'On_Hold_conversations', - value: onHoldConversations, - }, - { - title: 'Total_messages', - value: totalMessages, - }, - { - title: 'Busiest_day', - value: t(busiestDay), - }, - { - title: 'Conversations_per_day', - value: (totalConversations / days).toFixed(2), - }, - { - title: 'Busiest_time', - value: `${busiestHour.from}${busiestHour.to ? `- ${busiestHour.to}` : ''}`, - }, - ]; - }, - - /** - * - * @param {Date} from - * @param {Date} to - * - * @returns {Array[Object]} - */ - async Productivity(from, to, departmentId, extraQuery) { - let avgResponseTime = 0; - let firstResponseTime = 0; - let avgReactionTime = 0; - let count = 0; - - const date = { - gte: from, - lt: to.add(1, 'days'), - }; - - await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics }) => { - if (metrics && metrics.response && metrics.reaction) { - avgResponseTime += metrics.response.avg; - firstResponseTime += metrics.response.ft; - avgReactionTime += metrics.reaction.ft; - count++; - } - }); - - if (count) { - avgResponseTime /= count; - firstResponseTime /= count; - avgReactionTime /= count; - } - - const data = [ - { - title: 'Avg_response_time', - value: secondsToHHMMSS(avgResponseTime.toFixed(2)), - }, - { - title: 'Avg_first_response_time', - value: secondsToHHMMSS(firstResponseTime.toFixed(2)), - }, - { - title: 'Avg_reaction_time', - value: secondsToHHMMSS(avgReactionTime.toFixed(2)), - }, - ]; - - return data; - }, -}; - -const ChartData = { - /** - * - * @param {Object} date {gte: {Date}, lt: {Date}} - * - * @returns {Integer} - */ - Total_conversations(date, departmentId, extraQuery) { - return LivechatRooms.getTotalConversationsBetweenDate('l', date, { departmentId }, extraQuery); - }, - - async Avg_chat_duration(date, departmentId, extraQuery) { - let total = 0; - let count = 0; - - await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics }) => { - if (metrics && metrics.chatDuration) { - total += metrics.chatDuration; - count++; - } - }); - - const avgCD = count ? total / count : 0; - return Math.round(avgCD * 100) / 100; - }, - - async Total_messages(date, departmentId, extraQuery) { - let total = 0; - - // we don't want to count visitor messages - const extraFilter = { $lte: ['$token', null] }; - const allConversations = await LivechatRooms.getAnalyticsMetricsBetweenDateWithMessages( - 'l', - date, - { departmentId }, - extraFilter, - extraQuery, - ).toArray(); - allConversations.map(({ msgs }) => { - if (msgs) { - total += msgs; - } - return null; - }); - - return total; - }, - - /** - * - * @param {Object} date {gte: {Date}, lt: {Date}} - * - * @returns {Double} - */ - async Avg_first_response_time(date, departmentId, extraQuery) { - let frt = 0; - let count = 0; - await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics }) => { - if (metrics && metrics.response && metrics.response.ft) { - frt += metrics.response.ft; - count++; - } - }); - - const avgFrt = count ? frt / count : 0; - return Math.round(avgFrt * 100) / 100; - }, - - /** - * - * @param {Object} date {gte: {Date}, lt: {Date}} - * - * @returns {Double} - */ - async Best_first_response_time(date, departmentId, extraQuery) { - let maxFrt; - - await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics }) => { - if (metrics && metrics.response && metrics.response.ft) { - maxFrt = maxFrt ? Math.min(maxFrt, metrics.response.ft) : metrics.response.ft; - } - }); - - if (!maxFrt) { - maxFrt = 0; - } - - return Math.round(maxFrt * 100) / 100; - }, - - /** - * - * @param {Object} date {gte: {Date}, lt: {Date}} - * - * @returns {Double} - */ - async Avg_response_time(date, departmentId, extraQuery) { - let art = 0; - let count = 0; - await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics }) => { - if (metrics && metrics.response && metrics.response.avg) { - art += metrics.response.avg; - count++; - } - }); - - const avgArt = count ? art / count : 0; - - return Math.round(avgArt * 100) / 100; - }, - - /** - * - * @param {Object} date {gte: {Date}, lt: {Date}} - * - * @returns {Double} - */ - async Avg_reaction_time(date, departmentId, extraQuery) { - let arnt = 0; - let count = 0; - await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics }) => { - if (metrics && metrics.reaction && metrics.reaction.ft) { - arnt += metrics.reaction.ft; - count++; - } - }); - - const avgArnt = count ? arnt / count : 0; - - return Math.round(avgArnt * 100) / 100; - }, -}; - -const AgentOverviewData = { - /** - * do operation equivalent to map[key] += value - * - */ - updateMap(map, key, value) { - map.set(key, map.has(key) ? map.get(key) + value : value); - }, - - /** - * Sort array of objects by value property of object - * @param {Array(Object)} data - * @param {Boolean} [inv=false] reverse sort - */ - sortByValue(data, inv = false) { - data.sort((a, b) => { - // sort array - if (parseFloat(a.value) > parseFloat(b.value)) { - return inv ? -1 : 1; // if inv, reverse sort - } - if (parseFloat(a.value) < parseFloat(b.value)) { - return inv ? 1 : -1; - } - return 0; - }); - }, - - /** - * - * @param {Date} from - * @param {Date} to - * - * @returns {Array(Object), Array(Object)} - */ - async Total_conversations(from, to, departmentId, extraQuery) { - let total = 0; - const agentConversations = new Map(); // stores total conversations for each agent - const date = { - gte: from, - lt: to.add(1, 'days'), - }; - - const data = { - head: [ - { - name: 'Agent', - }, - { - name: '%_of_conversations', - }, - ], - data: [], - }; - - const allConversations = await LivechatRooms.getAnalyticsMetricsBetweenDateWithMessages( - 'l', - date, - { - departmentId, - }, - {}, - extraQuery, - ).toArray(); - allConversations.map((room) => { - if (room.servedBy) { - this.updateMap(agentConversations, room.servedBy.username, 1); - total++; - } - return null; - }); - - agentConversations.forEach((value, key) => { - // calculate percentage - const percentage = ((value / total) * 100).toFixed(2); - - data.data.push({ - name: key, - value: percentage, - }); - }); - - this.sortByValue(data.data, true); // reverse sort array - - data.data.forEach((value) => { - value.value = `${value.value}%`; - }); - - return data; - }, - - /** - * - * @param {Date} from - * @param {Date} to - * - * @returns {Array(Object), Array(Object)} - */ - async Avg_chat_duration(from, to, departmentId, extraQuery) { - const agentChatDurations = new Map(); // stores total conversations for each agent - const date = { - gte: from, - lt: to.add(1, 'days'), - }; - - const data = { - head: [ - { - name: 'Agent', - }, - { - name: 'Avg_chat_duration', - }, - ], - data: [], - }; - - await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics, servedBy }) => { - if (servedBy && metrics && metrics.chatDuration) { - if (agentChatDurations.has(servedBy.username)) { - agentChatDurations.set(servedBy.username, { - chatDuration: agentChatDurations.get(servedBy.username).chatDuration + metrics.chatDuration, - total: agentChatDurations.get(servedBy.username).total + 1, - }); - } else { - agentChatDurations.set(servedBy.username, { - chatDuration: metrics.chatDuration, - total: 1, - }); - } - } - }); - - agentChatDurations.forEach((obj, key) => { - // calculate percentage - const avg = (obj.chatDuration / obj.total).toFixed(2); - - data.data.push({ - name: key, - value: avg, - }); - }); - - this.sortByValue(data.data, true); // reverse sort array - - data.data.forEach((obj) => { - obj.value = secondsToHHMMSS(obj.value); - }); - - return data; - }, - - /** - * - * @param {Date} from - * @param {Date} to - * - * @returns {Array(Object), Array(Object)} - */ - async Total_messages(from, to, departmentId, extraQuery) { - const agentMessages = new Map(); // stores total conversations for each agent - const date = { - gte: from, - lt: to.add(1, 'days'), - }; - - const data = { - head: [ - { - name: 'Agent', - }, - { - name: 'Total_messages', - }, - ], - data: [], - }; - - // we don't want to count visitor messages - const extraFilter = { $lte: ['$token', null] }; - const allConversations = await LivechatRooms.getAnalyticsMetricsBetweenDateWithMessages( - 'l', - date, - { departmentId }, - extraFilter, - extraQuery, - ).toArray(); - allConversations.map(({ servedBy, msgs }) => { - if (servedBy) { - this.updateMap(agentMessages, servedBy.username, msgs); - } - return null; - }); - - agentMessages.forEach((value, key) => { - // calculate percentage - data.data.push({ - name: key, - value, - }); - }); - - this.sortByValue(data.data, true); // reverse sort array - - return data; - }, - - /** - * - * @param {Date} from - * @param {Date} to - * - * @returns {Array(Object), Array(Object)} - */ - async Avg_first_response_time(from, to, departmentId, extraQuery) { - const agentAvgRespTime = new Map(); // stores avg response time for each agent - const date = { - gte: from, - lt: to.add(1, 'days'), - }; - - const data = { - head: [ - { - name: 'Agent', - }, - { - name: 'Avg_first_response_time', - }, - ], - data: [], - }; - - await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics, servedBy }) => { - if (servedBy && metrics && metrics.response && metrics.response.ft) { - if (agentAvgRespTime.has(servedBy.username)) { - agentAvgRespTime.set(servedBy.username, { - frt: agentAvgRespTime.get(servedBy.username).frt + metrics.response.ft, - total: agentAvgRespTime.get(servedBy.username).total + 1, - }); - } else { - agentAvgRespTime.set(servedBy.username, { - frt: metrics.response.ft, - total: 1, - }); - } - } - }); - - agentAvgRespTime.forEach((obj, key) => { - // calculate avg - const avg = obj.frt / obj.total; - - data.data.push({ - name: key, - value: avg.toFixed(2), - }); - }); - - this.sortByValue(data.data, false); // sort array - - data.data.forEach((obj) => { - obj.value = secondsToHHMMSS(obj.value); - }); - - return data; - }, - - /** - * - * @param {Date} from - * @param {Date} to - * - * @returns {Array(Object), Array(Object)} - */ - async Best_first_response_time(from, to, departmentId, extraQuery) { - const agentFirstRespTime = new Map(); // stores avg response time for each agent - const date = { - gte: from, - lt: to.add(1, 'days'), - }; - - const data = { - head: [ - { - name: 'Agent', - }, - { - name: 'Best_first_response_time', - }, - ], - data: [], - }; - - await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics, servedBy }) => { - if (servedBy && metrics && metrics.response && metrics.response.ft) { - if (agentFirstRespTime.has(servedBy.username)) { - agentFirstRespTime.set(servedBy.username, Math.min(agentFirstRespTime.get(servedBy.username), metrics.response.ft)); - } else { - agentFirstRespTime.set(servedBy.username, metrics.response.ft); - } - } - }); - - agentFirstRespTime.forEach((value, key) => { - // calculate avg - data.data.push({ - name: key, - value: value.toFixed(2), - }); - }); - - this.sortByValue(data.data, false); // sort array - - data.data.forEach((obj) => { - obj.value = secondsToHHMMSS(obj.value); - }); - - return data; - }, - - /** - * - * @param {Date} from - * @param {Date} to - * - * @returns {Array(Object), Array(Object)} - */ - async Avg_response_time(from, to, departmentId, extraQuery) { - const agentAvgRespTime = new Map(); // stores avg response time for each agent - const date = { - gte: from, - lt: to.add(1, 'days'), - }; - - const data = { - head: [ - { - name: 'Agent', - }, - { - name: 'Avg_response_time', - }, - ], - data: [], - }; - - await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics, servedBy }) => { - if (servedBy && metrics && metrics.response && metrics.response.avg) { - if (agentAvgRespTime.has(servedBy.username)) { - agentAvgRespTime.set(servedBy.username, { - avg: agentAvgRespTime.get(servedBy.username).avg + metrics.response.avg, - total: agentAvgRespTime.get(servedBy.username).total + 1, - }); - } else { - agentAvgRespTime.set(servedBy.username, { - avg: metrics.response.avg, - total: 1, - }); - } - } - }); - - agentAvgRespTime.forEach((obj, key) => { - // calculate avg - const avg = obj.avg / obj.total; - - data.data.push({ - name: key, - value: avg.toFixed(2), - }); - }); - - this.sortByValue(data.data, false); // sort array - - data.data.forEach((obj) => { - obj.value = secondsToHHMMSS(obj.value); - }); - - return data; - }, - - /** - * - * @param {Date} from - * @param {Date} to - * - * @returns {Array(Object), Array(Object)} - */ - async Avg_reaction_time(from, to, departmentId, extraQuery) { - const agentAvgReactionTime = new Map(); // stores avg reaction time for each agent - const date = { - gte: from, - lt: to.add(1, 'days'), - }; - - const data = { - head: [ - { - name: 'Agent', - }, - { - name: 'Avg_reaction_time', - }, - ], - data: [], - }; - - await LivechatRooms.getAnalyticsMetricsBetweenDate('l', date, { departmentId }, extraQuery).forEach(({ metrics, servedBy }) => { - if (servedBy && metrics && metrics.reaction && metrics.reaction.ft) { - if (agentAvgReactionTime.has(servedBy.username)) { - agentAvgReactionTime.set(servedBy.username, { - frt: agentAvgReactionTime.get(servedBy.username).frt + metrics.reaction.ft, - total: agentAvgReactionTime.get(servedBy.username).total + 1, - }); - } else { - agentAvgReactionTime.set(servedBy.username, { - frt: metrics.reaction.ft, - total: 1, - }); - } - } - }); - - agentAvgReactionTime.forEach((obj, key) => { - // calculate avg - const avg = obj.frt / obj.total; - - data.data.push({ - name: key, - value: avg.toFixed(2), - }); - }); - - this.sortByValue(data.data, false); // sort array - - data.data.forEach((obj) => { - obj.value = secondsToHHMMSS(obj.value); - }); - - return data; - }, -}; - -export const Analytics = { - async getAgentOverviewData(options) { - const { departmentId, utcOffset, daterange: { from: fDate, to: tDate } = {}, chartOptions: { name } = {} } = options; - const timezone = getTimezone({ utcOffset }); - const from = moment.tz(fDate, 'YYYY-MM-DD', timezone).startOf('day').utc(); - const to = moment.tz(tDate, 'YYYY-MM-DD', timezone).endOf('day').utc(); - - if (!(moment(from).isValid() && moment(to).isValid())) { - logger.error('livechat:getAgentOverviewData => Invalid dates'); - return; - } - - if (!AgentOverviewData[name]) { - logger.error(`Method RocketChat.Livechat.Analytics.AgentOverviewData.${name} does NOT exist`); - return; - } - - const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); - return AgentOverviewData[name](from, to, departmentId, extraQuery); - }, - - async getAnalyticsChartData(options) { - const { - utcOffset, - departmentId, - daterange: { from: fDate, to: tDate } = {}, - chartOptions: { name: chartLabel }, - chartOptions: { name } = {}, - } = options; - - // Check if function exists, prevent server error in case property altered - if (!ChartData[name]) { - logger.error(`Method RocketChat.Livechat.Analytics.ChartData.${name} does NOT exist`); - return; - } - - const timezone = getTimezone({ utcOffset }); - const from = moment.tz(fDate, 'YYYY-MM-DD', timezone).startOf('day').utc(); - const to = moment.tz(tDate, 'YYYY-MM-DD', timezone).endOf('day').utc(); - const isSameDay = from.diff(to, 'days') === 0; - - if (!(moment(from).isValid() && moment(to).isValid())) { - logger.error('livechat:getAnalyticsChartData => Invalid dates'); - return; - } - - const data = { - chartLabel, - dataLabels: [], - dataPoints: [], - }; - - const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); - if (isSameDay) { - // data for single day - const m = moment(from); - for await (const currentHour of Array.from({ length: HOURS_IN_DAY }, (_, i) => i)) { - const hour = m.add(currentHour ? 1 : 0, 'hour').format('H'); - const label = { - from: moment.utc().set({ hour }).tz(timezone).format('hA'), - to: moment.utc().set({ hour }).add(1, 'hour').tz(timezone).format('hA'), - }; - data.dataLabels.push(`${label.from}-${label.to}`); - - const date = { - gte: m, - lt: moment(m).add(1, 'hours'), - }; - - data.dataPoints.push(await ChartData[name](date, departmentId, extraQuery)); - } - } else { - for await (const m of dayIterator(from, to)) { - data.dataLabels.push(m.format('M/D')); - - const date = { - gte: m, - lt: moment(m).add(1, 'days'), - }; - - data.dataPoints.push(await ChartData[name](date, departmentId, extraQuery)); - } - } - - return data; - }, - - async getAnalyticsOverviewData(options) { - const { departmentId, utcOffset = 0, language, daterange: { from: fDate, to: tDate } = {}, analyticsOptions: { name } = {} } = options; - const timezone = getTimezone({ utcOffset }); - const from = moment.tz(fDate, 'YYYY-MM-DD', timezone).startOf('day').utc(); - const to = moment.tz(tDate, 'YYYY-MM-DD', timezone).endOf('day').utc(); - - if (!(moment(from).isValid() && moment(to).isValid())) { - logger.error('livechat:getAnalyticsOverviewData => Invalid dates'); - return; - } - - if (!OverviewData[name]) { - logger.error(`Method RocketChat.Livechat.Analytics.OverviewData.${name} does NOT exist`); - return; - } - - const t = (s) => i18n.t(s, { lng: language }); - - const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); - return OverviewData[name](from, to, departmentId, timezone, t, extraQuery); - }, -}; diff --git a/apps/meteor/app/livechat/server/lib/AnalyticsTyped.ts b/apps/meteor/app/livechat/server/lib/AnalyticsTyped.ts index 1b0f7b4c29197..3b7c6a3051bf4 100644 --- a/apps/meteor/app/livechat/server/lib/AnalyticsTyped.ts +++ b/apps/meteor/app/livechat/server/lib/AnalyticsTyped.ts @@ -1,12 +1,14 @@ +import { OmnichannelAnalytics } from '@rocket.chat/core-services'; import mem from 'mem'; -import { Analytics } from './Analytics'; - -export const getAgentOverviewDataCached = mem(Analytics.getAgentOverviewData, { maxAge: 60000, cacheKey: JSON.stringify }); +export const getAgentOverviewDataCached = mem(OmnichannelAnalytics.getAgentOverviewData, { maxAge: 60000, cacheKey: JSON.stringify }); // Agent overview data on realtime is cached for 5 seconds // while the data on the overview page is cached for 1 minute -export const getAnalyticsOverviewDataCached = mem(Analytics.getAnalyticsOverviewData, { maxAge: 60000, cacheKey: JSON.stringify }); -export const getAnalyticsOverviewDataCachedForRealtime = mem(Analytics.getAnalyticsOverviewData, { +export const getAnalyticsOverviewDataCached = mem(OmnichannelAnalytics.getAnalyticsOverviewData, { + maxAge: 60000, + cacheKey: JSON.stringify, +}); +export const getAnalyticsOverviewDataCachedForRealtime = mem(OmnichannelAnalytics.getAnalyticsOverviewData, { maxAge: process.env.TEST_MODE === 'true' ? 1 : 5000, cacheKey: JSON.stringify, }); diff --git a/apps/meteor/app/livechat/server/lib/Livechat.js b/apps/meteor/app/livechat/server/lib/Livechat.js index 6ef74c33dfd44..a47d472f289b3 100644 --- a/apps/meteor/app/livechat/server/lib/Livechat.js +++ b/apps/meteor/app/livechat/server/lib/Livechat.js @@ -2,12 +2,8 @@ // Please add new methods to LivechatTyped.ts import { Logger } from '@rocket.chat/logger'; -import { Analytics } from './Analytics'; - const logger = new Logger('Livechat'); export const Livechat = { - Analytics, - logger, }; diff --git a/apps/meteor/app/livechat/server/lib/analytics/dashboards.ts b/apps/meteor/app/livechat/server/lib/analytics/dashboards.ts index 6788d01c44410..cacf2c19d5e99 100644 --- a/apps/meteor/app/livechat/server/lib/analytics/dashboards.ts +++ b/apps/meteor/app/livechat/server/lib/analytics/dashboards.ts @@ -1,3 +1,4 @@ +import { OmnichannelAnalytics } from '@rocket.chat/core-services'; import type { IUser } from '@rocket.chat/core-typings'; import { LivechatRooms, Users, LivechatVisitors, LivechatAgentActivity } from '@rocket.chat/models'; import mem from 'mem'; @@ -6,7 +7,6 @@ import moment from 'moment'; import { secondsToHHMMSS } from '../../../../../lib/utils/secondsToHHMMSS'; import { settings } from '../../../../settings/server'; import { getAnalyticsOverviewDataCachedForRealtime } from '../AnalyticsTyped'; -import { Livechat } from '../Livechat'; import { findPercentageOfAbandonedRoomsAsync, findAllAverageOfChatDurationTimeAsync, @@ -33,26 +33,27 @@ const getProductivityMetricsAsync = async ({ departmentId = undefined, user, }: { - start: Date; - end: Date; + start: string; + end: string; departmentId?: string; user: IUser; }) => { if (!start || !end) { throw new Error('"start" and "end" must be provided'); } - const totalizers = await Livechat.Analytics.getAnalyticsOverviewData({ - daterange: { - from: start, - to: end, - }, - analyticsOptions: { - name: 'Productivity', - }, - departmentId, - utcOffset: user?.utcOffset, - language: user?.language || settings.get('Language') || 'en', - }); + const totalizers = + (await OmnichannelAnalytics.getAnalyticsOverviewData({ + daterange: { + from: start, + to: end, + }, + analyticsOptions: { + name: 'Productivity', + }, + departmentId, + utcOffset: user?.utcOffset, + language: user?.language || settings.get('Language') || 'en', + })) || []; const averageWaitingTime = await findAllAverageWaitingTimeAsync({ start, end, @@ -78,8 +79,8 @@ const getAgentsProductivityMetricsAsync = async ({ departmentId = undefined, user, }: { - start: Date; - end: Date; + start: string; + end: string; departmentId?: string; user: IUser; }) => { @@ -98,18 +99,19 @@ const getAgentsProductivityMetricsAsync = async ({ end, departmentId, }); - const totalizers = await Livechat.Analytics.getAnalyticsOverviewData({ - daterange: { - from: start, - to: end, - }, - analyticsOptions: { - name: 'Conversations', - }, - departmentId, - utcOffset: user.utcOffset, - language: user.language || settings.get('Language') || 'en', - }); + const totalizers = + (await OmnichannelAnalytics.getAnalyticsOverviewData({ + daterange: { + from: start, + to: end, + }, + analyticsOptions: { + name: 'Conversations', + }, + departmentId, + utcOffset: user.utcOffset, + language: user.language || settings.get('Language') || 'en', + })) || []; const totalOfServiceTime = averageOfServiceTime.departments.length; @@ -223,30 +225,31 @@ const getConversationsMetricsAsync = async ({ departmentId, user, }: { - start: Date; - end: Date; + start: string; + end: string; departmentId?: string; user: IUser; }) => { if (!start || !end) { throw new Error('"start" and "end" must be provided'); } - const totalizers = await getAnalyticsOverviewDataCachedForRealtime({ - daterange: { - from: start, - to: end, - }, - analyticsOptions: { - name: 'Conversations', - }, - ...(departmentId && departmentId !== 'undefined' && { departmentId }), - utcOffset: user.utcOffset, - language: user.language || settings.get('Language') || 'en', - }); + const totalizers = + (await getAnalyticsOverviewDataCachedForRealtime({ + daterange: { + from: start, + to: end, + }, + analyticsOptions: { + name: 'Conversations', + }, + ...(departmentId && departmentId !== 'undefined' && { departmentId }), + utcOffset: user.utcOffset, + language: user.language || settings.get('Language') || 'en', + })) || []; const metrics = ['Total_conversations', 'Open_conversations', 'On_Hold_conversations', 'Total_messages']; const visitorsCount = await LivechatVisitors.getVisitorsBetweenDate({ - start, - end, + start: new Date(start), + end: new Date(end), department: departmentId, }).count(); return { diff --git a/apps/meteor/app/livechat/server/methods/getAgentOverviewData.ts b/apps/meteor/app/livechat/server/methods/getAgentOverviewData.ts index 9cd5de75a0f38..182cd238ec9e8 100644 --- a/apps/meteor/app/livechat/server/methods/getAgentOverviewData.ts +++ b/apps/meteor/app/livechat/server/methods/getAgentOverviewData.ts @@ -1,5 +1,7 @@ +import type { ConversationData } from '@rocket.chat/core-services'; +import { OmnichannelAnalytics } from '@rocket.chat/core-services'; import { Users } from '@rocket.chat/models'; -import type { ServerMethods, TranslationKey } from '@rocket.chat/ui-contexts'; +import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; @@ -9,10 +11,7 @@ import { Livechat } from '../lib/Livechat'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { - 'livechat:getAgentOverviewData'(options: { chartOptions: { name: string } }): { - head: { name: TranslationKey }[]; - data: { name: string; value: number | string }[]; - }; + 'livechat:getAgentOverviewData'(options: { chartOptions: { name: string } }): ConversationData | void; } } @@ -33,6 +32,6 @@ Meteor.methods({ } const user = await Users.findOneById(uid, { projection: { _id: 1, utcOffset: 1 } }); - return Livechat.Analytics.getAgentOverviewData({ ...options, utcOffset: user?.utcOffset || 0 }); + return OmnichannelAnalytics.getAgentOverviewData({ ...options, utcOffset: user?.utcOffset || 0 }); }, }); diff --git a/apps/meteor/app/livechat/server/methods/getAnalyticsChartData.ts b/apps/meteor/app/livechat/server/methods/getAnalyticsChartData.ts index 1250985cd2ef5..b5b1003969f8c 100644 --- a/apps/meteor/app/livechat/server/methods/getAnalyticsChartData.ts +++ b/apps/meteor/app/livechat/server/methods/getAnalyticsChartData.ts @@ -1,3 +1,5 @@ +import type { ChartDataResult } from '@rocket.chat/core-services'; +import { OmnichannelAnalytics } from '@rocket.chat/core-services'; import { Users } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; @@ -8,13 +10,7 @@ import { Livechat } from '../lib/Livechat'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { - 'livechat:getAnalyticsChartData'(options: { chartOptions: { name: string } }): - | { - chartLabel: string; - dataLabels: string[]; - dataPoints: number[]; - } - | undefined; + 'livechat:getAnalyticsChartData'(options: { chartOptions: { name: string } }): ChartDataResult | void; } } @@ -28,12 +24,17 @@ Meteor.methods({ } if (!options.chartOptions?.name) { - Livechat.logger.warn('Incorrect chart options'); + Livechat.logger.error('Incorrect chart options'); return; } const user = await Users.findOneById(userId, { projection: { _id: 1, utcOffset: 1 } }); - return Livechat.Analytics.getAnalyticsChartData({ ...options, utcOffset: user?.utcOffset }); + if (!user) { + Livechat.logger.error('User not found'); + return; + } + + return OmnichannelAnalytics.getAnalyticsChartData({ ...options, utcOffset: user?.utcOffset }); }, }); diff --git a/apps/meteor/app/livechat/server/methods/getAnalyticsOverviewData.ts b/apps/meteor/app/livechat/server/methods/getAnalyticsOverviewData.ts index 76b7f276d6719..81fe2658301ae 100644 --- a/apps/meteor/app/livechat/server/methods/getAnalyticsOverviewData.ts +++ b/apps/meteor/app/livechat/server/methods/getAnalyticsOverviewData.ts @@ -1,5 +1,7 @@ +import type { AnalyticsOverviewDataResult } from '@rocket.chat/core-services'; +import { OmnichannelAnalytics } from '@rocket.chat/core-services'; import { Users } from '@rocket.chat/models'; -import type { ServerMethods, TranslationKey } from '@rocket.chat/ui-contexts'; +import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; @@ -10,10 +12,7 @@ import { Livechat } from '../lib/Livechat'; declare module '@rocket.chat/ui-contexts' { // eslint-disable-next-line @typescript-eslint/naming-convention interface ServerMethods { - 'livechat:getAnalyticsOverviewData'(options: { analyticsOptions: { name: string } }): { - title: TranslationKey; - value: string; - }[]; + 'livechat:getAnalyticsOverviewData'(options: { analyticsOptions: { name: string } }): AnalyticsOverviewDataResult[] | void; } } @@ -35,7 +34,7 @@ Meteor.methods({ const user = await Users.findOneById(uid, { projection: { _id: 1, utcOffset: 1, language: 1 } }); const language = user?.language || settings.get('Language') || 'en'; - return Livechat.Analytics.getAnalyticsOverviewData({ + return OmnichannelAnalytics.getAnalyticsOverviewData({ ...options, utcOffset: user?.utcOffset || 0, language, diff --git a/apps/meteor/client/components/Contextualbar/ContextualbarScrollableContent.tsx b/apps/meteor/client/components/Contextualbar/ContextualbarScrollableContent.tsx index 6be178f86f77e..76069d748e921 100644 --- a/apps/meteor/client/components/Contextualbar/ContextualbarScrollableContent.tsx +++ b/apps/meteor/client/components/Contextualbar/ContextualbarScrollableContent.tsx @@ -2,14 +2,14 @@ import { Margins } from '@rocket.chat/fuselage'; import type { ComponentProps } from 'react'; import React, { forwardRef, memo } from 'react'; -import Page from '../Page'; +import { PageScrollableContent } from '../Page'; -const ContextualbarScrollableContent = forwardRef>( +const ContextualbarScrollableContent = forwardRef>( function ContextualbarScrollableContent({ children, ...props }, ref) { return ( - + {children} - + ); }, ); diff --git a/apps/meteor/client/components/Page/Page.stories.tsx b/apps/meteor/client/components/Page/Page.stories.tsx index a3e62140fc2d6..3171d707480a1 100644 --- a/apps/meteor/client/components/Page/Page.stories.tsx +++ b/apps/meteor/client/components/Page/Page.stories.tsx @@ -2,16 +2,16 @@ import { Box, Button, ButtonGroup } from '@rocket.chat/fuselage'; import type { ComponentMeta, ComponentStory } from '@storybook/react'; import React from 'react'; -import Page from '.'; +import { Page, PageContent, PageHeader, PageScrollableContent, PageScrollableContentWithShadow } from '.'; export default { title: 'Components/Page', component: Page, subcomponents: { - 'Page.Content': Page.Content, - 'Page.Header': Page.Header, - 'Page.ScrollableContent': Page.ScrollableContent, - 'Page.ScrollableContentWithShadow': Page.ScrollableContentWithShadow, + PageContent, + PageHeader, + PageScrollableContent, + PageScrollableContentWithShadow, }, parameters: { layout: 'fullscreen', @@ -29,45 +29,45 @@ const DummyContent = ({ rows = 10 }: { rows?: number }) => ( export const Example: ComponentStory = () => ( - - + + Say goodbye to inefficient email threads and managing multiple guest accounts. Enable teams to communicate safely with partners, vendors, and suppliers directly from Rocket.Chat regardless of which collaboration platform they use. - + ); export const WithButtonsAtTheHeader: ComponentStory = () => ( - + - - + + - + ); export const WithScrollableContent: ComponentStory = () => ( - - + + - + ); export const WithScrollableContentWithShadow: ComponentStory = () => ( - - + + - + ); diff --git a/apps/meteor/client/components/Page/PageHeader.tsx b/apps/meteor/client/components/Page/PageHeader.tsx index 25d20381e52e2..e955ac20a5282 100644 --- a/apps/meteor/client/components/Page/PageHeader.tsx +++ b/apps/meteor/client/components/Page/PageHeader.tsx @@ -1,4 +1,4 @@ -import { Box, IconButton } from '@rocket.chat/fuselage'; +import { Box, Button } from '@rocket.chat/fuselage'; import { HeaderToolbox, useDocumentTitle } from '@rocket.chat/ui-client'; import { useLayout, useTranslation } from '@rocket.chat/ui-contexts'; import type { FC, ComponentProps, ReactNode } from 'react'; @@ -35,10 +35,14 @@ const PageHeader: FC = ({ children = undefined, title, onClickB )} - {onClickBack && } {title} + {onClickBack && ( + + )} {children} diff --git a/apps/meteor/client/components/Page/index.ts b/apps/meteor/client/components/Page/index.ts index 9aad9332937ab..1525eb70186df 100644 --- a/apps/meteor/client/components/Page/index.ts +++ b/apps/meteor/client/components/Page/index.ts @@ -1,14 +1,6 @@ -import Page from './Page'; -import PageContent from './PageContent'; -import PageFooter from './PageFooter'; -import PageHeader from './PageHeader'; -import PageScrollableContent from './PageScrollableContent'; -import PageScrollableContentWithShadow from './PageScrollableContentWithShadow'; - -export default Object.assign(Page, { - Header: PageHeader, - Content: PageContent, - ScrollableContent: PageScrollableContent, - ScrollableContentWithShadow: PageScrollableContentWithShadow, - Footer: PageFooter, -}); +export { default as Page } from './Page'; +export { default as PageContent } from './PageContent'; +export { default as PageFooter } from './PageFooter'; +export { default as PageHeader } from './PageHeader'; +export { default as PageScrollableContent } from './PageScrollableContent'; +export { default as PageScrollableContentWithShadow } from './PageScrollableContentWithShadow'; diff --git a/apps/meteor/client/components/PageSkeleton.tsx b/apps/meteor/client/components/PageSkeleton.tsx index 2c3c588d6c4ca..b2413dd97ecc6 100644 --- a/apps/meteor/client/components/PageSkeleton.tsx +++ b/apps/meteor/client/components/PageSkeleton.tsx @@ -2,17 +2,16 @@ import { Box, Button, ButtonGroup, Skeleton } from '@rocket.chat/fuselage'; import type { ReactElement } from 'react'; import React from 'react'; -import Page from './Page'; +import { Page, PageHeader, PageContent } from './Page'; const PageSkeleton = (): ReactElement => ( - }> + }> - + ); }; diff --git a/apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewPage.tsx b/apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewPage.tsx index 29b2a796953e8..c271d0a2fee8e 100644 --- a/apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewPage.tsx +++ b/apps/meteor/client/views/account/featurePreview/AccountFeaturePreviewPage.tsx @@ -22,7 +22,7 @@ import type { ChangeEvent } from 'react'; import React, { useEffect, Fragment } from 'react'; import { useForm } from 'react-hook-form'; -import Page from '../../../components/Page'; +import { Page, PageHeader, PageScrollableContentWithShadow, PageFooter } from '../../../components/Page'; const AccountFeaturePreviewPage = () => { const t = useTranslation(); @@ -79,8 +79,8 @@ const AccountFeaturePreviewPage = () => { return ( - - + + {featuresPreview.length === 0 && ( @@ -124,15 +124,15 @@ const AccountFeaturePreviewPage = () => { )} - - + + - + ); }; diff --git a/apps/meteor/client/views/account/integrations/AccountIntegrationsPage.tsx b/apps/meteor/client/views/account/integrations/AccountIntegrationsPage.tsx index 54806d879aa11..312cc89b65a00 100644 --- a/apps/meteor/client/views/account/integrations/AccountIntegrationsPage.tsx +++ b/apps/meteor/client/views/account/integrations/AccountIntegrationsPage.tsx @@ -8,7 +8,7 @@ import React, { useMemo } from 'react'; import { useForm, Controller } from 'react-hook-form'; import { WebdavAccounts } from '../../../../app/models/client'; -import Page from '../../../components/Page'; +import { Page, PageHeader, PageScrollableContentWithShadow } from '../../../components/Page'; import { useReactiveValue } from '../../../hooks/useReactiveValue'; import { getWebdavServerName } from '../../../lib/getWebdavServerName'; @@ -34,8 +34,8 @@ const AccountIntegrationsPage = (): ReactElement => { return ( - - + + {t('WebDAV_Accounts')} @@ -60,7 +60,7 @@ const AccountIntegrationsPage = (): ReactElement => { - + ); }; diff --git a/apps/meteor/client/views/account/omnichannel/OmnichannelPreferencesPage.tsx b/apps/meteor/client/views/account/omnichannel/OmnichannelPreferencesPage.tsx index d448a180f834c..9abec01df1d05 100644 --- a/apps/meteor/client/views/account/omnichannel/OmnichannelPreferencesPage.tsx +++ b/apps/meteor/client/views/account/omnichannel/OmnichannelPreferencesPage.tsx @@ -4,7 +4,7 @@ import type { ReactElement } from 'react'; import React from 'react'; import { useForm, FormProvider } from 'react-hook-form'; -import Page from '../../../components/Page'; +import { Page, PageHeader, PageScrollableContentWithShadow, PageFooter } from '../../../components/Page'; import PreferencesConversationTranscript from './PreferencesConversationTranscript'; import { PreferencesGeneral } from './PreferencesGeneral'; @@ -45,8 +45,8 @@ const OmnichannelPreferencesPage = (): ReactElement => { return ( - - + + @@ -55,15 +55,15 @@ const OmnichannelPreferencesPage = (): ReactElement => { - - + + - + ); }; diff --git a/apps/meteor/client/views/account/preferences/AccountPreferencesPage.tsx b/apps/meteor/client/views/account/preferences/AccountPreferencesPage.tsx index e3b18cabe759b..77997422a21e0 100644 --- a/apps/meteor/client/views/account/preferences/AccountPreferencesPage.tsx +++ b/apps/meteor/client/views/account/preferences/AccountPreferencesPage.tsx @@ -6,7 +6,7 @@ import type { ReactElement } from 'react'; import React from 'react'; import { FormProvider, useForm } from 'react-hook-form'; -import Page from '../../../components/Page'; +import { Page, PageHeader, PageScrollableContentWithShadow, PageFooter } from '../../../components/Page'; import { getDirtyFields } from '../../../lib/getDirtyFields'; import PreferencesGlobalSection from './PreferencesGlobalSection'; import PreferencesHighlightsSection from './PreferencesHighlightsSection'; @@ -75,8 +75,8 @@ const AccountPreferencesPage = (): ReactElement => { return ( - - + + @@ -91,15 +91,15 @@ const AccountPreferencesPage = (): ReactElement => { - - + + - + ); }; diff --git a/apps/meteor/client/views/account/profile/AccountProfilePage.tsx b/apps/meteor/client/views/account/profile/AccountProfilePage.tsx index 95d7c9d6eb014..c6d675a203ac8 100644 --- a/apps/meteor/client/views/account/profile/AccountProfilePage.tsx +++ b/apps/meteor/client/views/account/profile/AccountProfilePage.tsx @@ -16,7 +16,7 @@ import React, { useState, useCallback } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import ConfirmOwnerChangeModal from '../../../components/ConfirmOwnerChangeModal'; -import Page from '../../../components/Page'; +import { Page, PageFooter, PageHeader, PageScrollableContentWithShadow } from '../../../components/Page'; import { useAllowPasswordChange } from '../security/useAllowPasswordChange'; import AccountProfileForm from './AccountProfileForm'; import ActionConfirmModal from './ActionConfirmModal'; @@ -113,8 +113,8 @@ const AccountProfilePage = (): ReactElement => { return ( - - + + @@ -130,8 +130,8 @@ const AccountProfilePage = (): ReactElement => { )} - - + + - + ); }; diff --git a/apps/meteor/client/views/account/security/AccountSecurityPage.tsx b/apps/meteor/client/views/account/security/AccountSecurityPage.tsx index 1c1023e0a7e3e..49ed9594c1106 100644 --- a/apps/meteor/client/views/account/security/AccountSecurityPage.tsx +++ b/apps/meteor/client/views/account/security/AccountSecurityPage.tsx @@ -5,7 +5,7 @@ import type { ReactElement } from 'react'; import React from 'react'; import { FormProvider, useForm } from 'react-hook-form'; -import Page from '../../../components/Page'; +import { Page, PageHeader, PageScrollableContentWithShadow, PageFooter } from '../../../components/Page'; import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; import ChangePassword from './ChangePassword'; import EndToEnd from './EndToEnd'; @@ -39,8 +39,8 @@ const AccountSecurityPage = (): ReactElement => { return ( - - + + @@ -68,15 +68,15 @@ const AccountSecurityPage = (): ReactElement => { )} - - + + - + ); }; diff --git a/apps/meteor/client/views/account/tokens/AccountTokensPage.tsx b/apps/meteor/client/views/account/tokens/AccountTokensPage.tsx index 5e7c46631cf82..9885b0bd3b862 100644 --- a/apps/meteor/client/views/account/tokens/AccountTokensPage.tsx +++ b/apps/meteor/client/views/account/tokens/AccountTokensPage.tsx @@ -2,7 +2,7 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; -import Page from '../../../components/Page'; +import { Page, PageHeader, PageContent } from '../../../components/Page'; import AccountTokensTable from './AccountTokensTable'; const AccountTokensPage = (): ReactElement => { @@ -10,10 +10,10 @@ const AccountTokensPage = (): ReactElement => { return ( - - + + - + ); }; diff --git a/apps/meteor/client/views/admin/customEmoji/CustomEmojiRoute.tsx b/apps/meteor/client/views/admin/customEmoji/CustomEmojiRoute.tsx index 26b74063e0252..695751246ea3c 100644 --- a/apps/meteor/client/views/admin/customEmoji/CustomEmojiRoute.tsx +++ b/apps/meteor/client/views/admin/customEmoji/CustomEmojiRoute.tsx @@ -4,7 +4,7 @@ import type { ReactElement } from 'react'; import React, { useCallback, useRef } from 'react'; import { Contextualbar, ContextualbarHeader, ContextualbarClose } from '../../../components/Contextualbar'; -import Page from '../../../components/Page'; +import { Page, PageHeader, PageContent } from '../../../components/Page'; import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; import AddCustomEmoji from './AddCustomEmoji'; import CustomEmoji from './CustomEmoji'; @@ -45,14 +45,14 @@ const CustomEmojiRoute = (): ReactElement => { return ( - + - - + + - + {context && ( diff --git a/apps/meteor/client/views/admin/customSounds/CustomSoundsPage.tsx b/apps/meteor/client/views/admin/customSounds/CustomSoundsPage.tsx index 82191963cbd28..339a8537ac390 100644 --- a/apps/meteor/client/views/admin/customSounds/CustomSoundsPage.tsx +++ b/apps/meteor/client/views/admin/customSounds/CustomSoundsPage.tsx @@ -3,7 +3,7 @@ import { useRoute, useRouteParameter, useTranslation } from '@rocket.chat/ui-con import React, { useCallback, useRef } from 'react'; import { ContextualbarTitle, Contextualbar, ContextualbarClose, ContextualbarHeader } from '../../../components/Contextualbar'; -import Page from '../../../components/Page'; +import { Page, PageHeader, PageContent } from '../../../components/Page'; import AddCustomSound from './AddCustomSound'; import CustomSoundsTable from './CustomSoundsTable'; import EditCustomSound from './EditCustomSound'; @@ -41,14 +41,14 @@ const CustomSoundsPage = () => { return ( - + - - + + - + {context && ( diff --git a/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusRoute.tsx b/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusRoute.tsx index 46be80496f014..28a02980242f5 100644 --- a/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusRoute.tsx +++ b/apps/meteor/client/views/admin/customUserStatus/CustomUserStatusRoute.tsx @@ -4,7 +4,7 @@ import type { ReactElement } from 'react'; import React, { useCallback, useRef, useEffect } from 'react'; import { Contextualbar, ContextualbarHeader, ContextualbarClose, ContextualbarTitle } from '../../../components/Contextualbar'; -import Page from '../../../components/Page'; +import { Page, PageHeader, PageContent } from '../../../components/Page'; import { useIsEnterprise } from '../../../hooks/useIsEnterprise'; import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; import CustomUserActiveConnections from './CustomUserActiveConnections'; @@ -57,16 +57,16 @@ const CustomUserStatusRoute = (): ReactElement => { return ( - + {!license?.isEnterprise && } - - + + - + {context && ( diff --git a/apps/meteor/client/views/admin/emailInbox/EmailInboxForm.tsx b/apps/meteor/client/views/admin/emailInbox/EmailInboxForm.tsx index d659e27e0186c..2dc1f372fc833 100644 --- a/apps/meteor/client/views/admin/emailInbox/EmailInboxForm.tsx +++ b/apps/meteor/client/views/admin/emailInbox/EmailInboxForm.tsx @@ -26,7 +26,7 @@ import { useForm, Controller } from 'react-hook-form'; import { validateEmail } from '../../../../lib/emailValidator'; import AutoCompleteDepartment from '../../../components/AutoCompleteDepartment'; import GenericModal from '../../../components/GenericModal'; -import Page from '../../../components/Page'; +import { PageScrollableContentWithShadow } from '../../../components/Page'; const EmailInboxForm = ({ inboxData }: { inboxData?: IEmailInboxPayload }): ReactElement => { const t = useTranslation(); @@ -167,7 +167,7 @@ const EmailInboxForm = ({ inboxData }: { inboxData?: IEmailInboxPayload }): Reac }); return ( - + @@ -378,7 +378,7 @@ const EmailInboxForm = ({ inboxData }: { inboxData?: IEmailInboxPayload }): Reac - + ); }; diff --git a/apps/meteor/client/views/admin/emailInbox/EmailInboxPage.tsx b/apps/meteor/client/views/admin/emailInbox/EmailInboxPage.tsx index f58f27a196da2..97078208b09c8 100644 --- a/apps/meteor/client/views/admin/emailInbox/EmailInboxPage.tsx +++ b/apps/meteor/client/views/admin/emailInbox/EmailInboxPage.tsx @@ -3,7 +3,7 @@ import { useRoute, useRouteParameter, useTranslation } from '@rocket.chat/ui-con import type { ReactElement } from 'react'; import React from 'react'; -import Page from '../../../components/Page'; +import { Page, PageHeader, PageContent } from '../../../components/Page'; import EmailInboxForm from './EmailInboxForm'; import EmailInboxFormWithData from './EmailInboxFormWithData'; import EmailInboxTable from './EmailInboxTable'; @@ -17,7 +17,7 @@ const EmailInboxPage = (): ReactElement => { return ( - + {context && ( )} - - + + {!context && } {context === 'new' && } {context === 'edit' && id && } - + ); diff --git a/apps/meteor/client/views/admin/federationDashboard/FederationDashboardPage.tsx b/apps/meteor/client/views/admin/federationDashboard/FederationDashboardPage.tsx index 940e404e23119..126a385e6cebf 100644 --- a/apps/meteor/client/views/admin/federationDashboard/FederationDashboardPage.tsx +++ b/apps/meteor/client/views/admin/federationDashboard/FederationDashboardPage.tsx @@ -3,7 +3,7 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; -import Page from '../../../components/Page'; +import { Page, PageHeader, PageScrollableContentWithShadow } from '../../../components/Page'; import OverviewSection from './OverviewSection'; import ServersSection from './ServersSection'; @@ -12,13 +12,13 @@ function FederationDashboardPage(): ReactElement { return ( - - + + - + ); } diff --git a/apps/meteor/client/views/admin/import/ChannelDescriptor.tsx b/apps/meteor/client/views/admin/import/ChannelDescriptor.tsx new file mode 100644 index 0000000000000..fdcfc0c7989e7 --- /dev/null +++ b/apps/meteor/client/views/admin/import/ChannelDescriptor.tsx @@ -0,0 +1,6 @@ +export type ChannelDescriptor = { + channel_id: string; + name: string; + is_archived: boolean; + do_import: boolean; +}; diff --git a/apps/meteor/client/views/admin/import/ImportHistoryPage.tsx b/apps/meteor/client/views/admin/import/ImportHistoryPage.tsx index daa3f3e392d85..42b8cee969f58 100644 --- a/apps/meteor/client/views/admin/import/ImportHistoryPage.tsx +++ b/apps/meteor/client/views/admin/import/ImportHistoryPage.tsx @@ -5,9 +5,11 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import React, { useMemo } from 'react'; import { ProgressStep } from '../../../../app/importer/lib/ImporterProgressStep'; -import Page from '../../../components/Page'; +import { Page, PageHeader, PageScrollableContentWithShadow } from '../../../components/Page'; import ImportOperationSummary from './ImportOperationSummary'; +import ImportOperationSummarySkeleton from './ImportOperationSummarySkeleton'; +// TODO: review inner logic function ImportHistoryPage() { const queryClient = useQueryClient(); const t = useTranslation(); @@ -95,7 +97,7 @@ function ImportHistoryPage() { return ( - + )} - - + + @@ -165,7 +167,7 @@ function ImportHistoryPage() { {isLoading && ( <> {Array.from({ length: 20 }, (_, i) => ( - + ))} )} @@ -185,7 +187,7 @@ function ImportHistoryPage() { )}
-
+
); } diff --git a/apps/meteor/client/views/admin/import/ImportOperationSummary.stories.tsx b/apps/meteor/client/views/admin/import/ImportOperationSummary.stories.tsx index 39972c78fa2da..1eec51aab3aae 100644 --- a/apps/meteor/client/views/admin/import/ImportOperationSummary.stories.tsx +++ b/apps/meteor/client/views/admin/import/ImportOperationSummary.stories.tsx @@ -3,12 +3,13 @@ import type { ComponentMeta, ComponentStory } from '@storybook/react'; import React from 'react'; import ImportOperationSummary from './ImportOperationSummary'; +import ImportOperationSummarySkeleton from './ImportOperationSummarySkeleton'; export default { title: 'Admin/Import/ImportOperationSummary', component: ImportOperationSummary, subcomponents: { - 'ImportOperationSummary.Skeleton': ImportOperationSummary.Skeleton, + ImportOperationSummarySkeleton, }, parameters: { layout: 'centered', @@ -24,4 +25,4 @@ export default { export const Default: ComponentStory = (args) => ; -export const Skeleton: ComponentStory = (args) => ; +export const Skeleton: ComponentStory = (args) => ; diff --git a/apps/meteor/client/views/admin/import/ImportOperationSummary.js b/apps/meteor/client/views/admin/import/ImportOperationSummary.tsx similarity index 79% rename from apps/meteor/client/views/admin/import/ImportOperationSummary.js rename to apps/meteor/client/views/admin/import/ImportOperationSummary.tsx index ddb21c01b07e9..dc8d6bfe0ec58 100644 --- a/apps/meteor/client/views/admin/import/ImportOperationSummary.js +++ b/apps/meteor/client/views/admin/import/ImportOperationSummary.tsx @@ -1,4 +1,6 @@ +import type { Serialized } from '@rocket.chat/core-typings'; import { TableRow, TableCell } from '@rocket.chat/fuselage'; +import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useRouter, useTranslation } from '@rocket.chat/ui-contexts'; import React, { useMemo } from 'react'; @@ -10,8 +12,24 @@ import { ProgressStep, } from '../../../../app/importer/lib/ImporterProgressStep'; import { useFormatDateAndTime } from '../../../hooks/useFormatDateAndTime'; -import ImportOperationSummarySkeleton from './ImportOperationSummarySkeleton'; +type ImportOperationSummaryProps = { + type: string; + _updatedAt: Serialized; + status: (typeof ProgressStep)[keyof typeof ProgressStep]; + file?: string; + user: string; + small?: boolean; + count?: { + users?: number; + channels?: number; + messages?: number; + total?: number; + }; + valid?: boolean; +}; + +// TODO: review inner logic function ImportOperationSummary({ type, _updatedAt, @@ -21,7 +39,7 @@ function ImportOperationSummary({ small, count: { users = 0, channels = 0, messages = 0, total = 0 } = {}, valid, -}) { +}: ImportOperationSummaryProps) { const t = useTranslation(); const formatDateAndTime = useFormatDateAndTime(); @@ -80,7 +98,7 @@ function ImportOperationSummary({ {formatDateAndTime(_updatedAt)} {!small && ( <> - {status && t(status.replace('importer_', 'importer_status_'))} + {status && t(status.replace('importer_', 'importer_status_') as TranslationKey)} {fileName} {users} {channels} @@ -92,6 +110,4 @@ function ImportOperationSummary({ ); } -export default Object.assign(ImportOperationSummary, { - Skeleton: ImportOperationSummarySkeleton, -}); +export default ImportOperationSummary; diff --git a/apps/meteor/client/views/admin/import/ImportOperationSummarySkeleton.js b/apps/meteor/client/views/admin/import/ImportOperationSummarySkeleton.tsx similarity index 79% rename from apps/meteor/client/views/admin/import/ImportOperationSummarySkeleton.js rename to apps/meteor/client/views/admin/import/ImportOperationSummarySkeleton.tsx index 81509516d234c..8c2a465cb58bc 100644 --- a/apps/meteor/client/views/admin/import/ImportOperationSummarySkeleton.js +++ b/apps/meteor/client/views/admin/import/ImportOperationSummarySkeleton.tsx @@ -1,7 +1,11 @@ import { Skeleton, TableRow, TableCell } from '@rocket.chat/fuselage'; import React from 'react'; -function ImportOperationSummarySkeleton({ small }) { +type ImportOperationSummarySkeletonProps = { + small?: boolean; +}; + +function ImportOperationSummarySkeleton({ small = false }: ImportOperationSummarySkeletonProps) { return ( diff --git a/apps/meteor/client/views/admin/import/ImportProgressPage.tsx b/apps/meteor/client/views/admin/import/ImportProgressPage.tsx index 2710721e2bcbe..a8e0835433980 100644 --- a/apps/meteor/client/views/admin/import/ImportProgressPage.tsx +++ b/apps/meteor/client/views/admin/import/ImportProgressPage.tsx @@ -7,9 +7,10 @@ import React, { useEffect } from 'react'; import { ImportingStartedStates } from '../../../../app/importer/lib/ImporterProgressStep'; import { numberFormat } from '../../../../lib/utils/stringUtils'; -import Page from '../../../components/Page'; +import { Page, PageHeader, PageScrollableContentWithShadow } from '../../../components/Page'; import { useErrorHandler } from './useErrorHandler'; +// TODO: review inner logic const ImportProgressPage = function ImportProgressPage() { const queryClient = useQueryClient(); const streamer = useStream('importers'); @@ -150,9 +151,8 @@ const ImportProgressPage = function ImportProgressPage() { return ( - - - + + {currentOperation.isLoading && } @@ -174,7 +174,7 @@ const ImportProgressPage = function ImportProgressPage() { )} - + ); }; diff --git a/apps/meteor/client/views/admin/import/ImportRoute.js b/apps/meteor/client/views/admin/import/ImportRoute.tsx similarity index 83% rename from apps/meteor/client/views/admin/import/ImportRoute.js rename to apps/meteor/client/views/admin/import/ImportRoute.tsx index a9904a178a3bd..96b5179b9ae0b 100644 --- a/apps/meteor/client/views/admin/import/ImportRoute.js +++ b/apps/meteor/client/views/admin/import/ImportRoute.tsx @@ -7,7 +7,11 @@ import ImportProgressPage from './ImportProgressPage'; import NewImportPage from './NewImportPage'; import PrepareImportPage from './PrepareImportPage'; -function ImportHistoryRoute({ page }) { +type ImportHistoryRouteProps = { + page: 'history' | 'new' | 'prepare' | 'progress'; +}; + +function ImportHistoryRoute({ page }: ImportHistoryRouteProps) { const canRunImport = usePermission('run-import'); if (!canRunImport) { diff --git a/apps/meteor/client/views/admin/import/NewImportPage.js b/apps/meteor/client/views/admin/import/NewImportPage.tsx similarity index 74% rename from apps/meteor/client/views/admin/import/NewImportPage.js rename to apps/meteor/client/views/admin/import/NewImportPage.tsx index eefc39b677d78..44771e44f6589 100644 --- a/apps/meteor/client/views/admin/import/NewImportPage.js +++ b/apps/meteor/client/views/admin/import/NewImportPage.tsx @@ -1,13 +1,16 @@ import { Box, Button, ButtonGroup, Callout, Chip, Field, Margins, Select, InputBox, TextInput, UrlInput } from '@rocket.chat/fuselage'; import { useUniqueId, useSafely } from '@rocket.chat/fuselage-hooks'; +import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useToastMessageDispatch, useRouter, useRouteParameter, useSetting, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; +import { useQuery } from '@tanstack/react-query'; +import type { ChangeEvent, DragEvent, FormEvent, Key, SyntheticEvent } from 'react'; import React, { useState, useMemo, useEffect } from 'react'; -import { Importers } from '../../../../app/importer/client/index'; -import Page from '../../../components/Page'; +import { Page, PageHeader, PageScrollableContentWithShadow } from '../../../components/Page'; import { useFormatMemorySize } from '../../../hooks/useFormatMemorySize'; import { useErrorHandler } from './useErrorHandler'; +// TODO: review inner logic function NewImportPage() { const t = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); @@ -15,10 +18,18 @@ function NewImportPage() { const [isLoading, setLoading] = useSafely(useState(false)); const [fileType, setFileType] = useSafely(useState('upload')); + + const listImportersEndpoint = useEndpoint('GET', '/v1/importers.list'); + const { data: importers, isLoading: isLoadingImporters } = useQuery(['importers'], async () => listImportersEndpoint(), { + refetchOnWindowFocus: false, + }); + + const options = useMemo(() => importers?.map(({ key, name }) => [key, t(name as TranslationKey)] as const) || [], [importers, t]); + const importerKey = useRouteParameter('importerKey'); - const importer = useMemo(() => Importers.get(importerKey), [importerKey]); + const importer = useMemo(() => (importers || []).find(({ key }) => key === importerKey), [importerKey, importers]); - const maxFileSize = useSetting('FileUpload_MaxFileSize'); + const maxFileSize = useSetting('FileUpload_MaxFileSize') ?? 0; const router = useRouter(); @@ -26,10 +37,10 @@ function NewImportPage() { const downloadPublicImportFile = useEndpoint('POST', '/v1/downloadPublicImportFile'); useEffect(() => { - if (importerKey && !importer) { + if (importerKey && !importer && !isLoadingImporters) { router.navigate('/admin/import/new', { replace: true }); } - }, [importer, importerKey, router]); + }, [importer, importerKey, router, isLoadingImporters]); const formatMemorySize = useFormatMemorySize(); @@ -37,37 +48,53 @@ function NewImportPage() { router.navigate('/admin/import'); }; - const handleImporterKeyChange = (importerKey) => { + const handleImporterKeyChange = (importerKey: Key) => { + if (typeof importerKey !== 'string') { + return; + } + router.navigate( - router.buildRoutePath({ - pattern: '/admin/import/new/:importerKey', + { + pattern: '/admin/import/new/:importerKey?', params: { importerKey }, - }), + }, { replace: true }, ); }; - const handleFileTypeChange = (fileType) => { + const handleFileTypeChange = (fileType: Key) => { + if (typeof fileType !== 'string') { + return; + } + setFileType(fileType); }; - const [files, setFiles] = useState([]); + const [files, setFiles] = useState([]); - const handleImportFileChange = async (event) => { - event = event.originalEvent || event; + const isDataTransferEvent = (event: T): event is T & DragEvent => + Boolean('dataTransfer' in event && (event as any).dataTransfer.files); + + const handleImportFileChange = async (event: ChangeEvent) => { let { files } = event.target; if (!files || files.length === 0) { - files = (event.dataTransfer != null ? event.dataTransfer.files : undefined) || []; + if (isDataTransferEvent(event)) { + files = event.dataTransfer.files; + } } - setFiles(Array.from(files)); + setFiles(Array.from(files ?? [])); }; - const handleFileUploadChipClick = (file) => () => { + const handleFileUploadChipClick = (file: File) => () => { setFiles((files) => files.filter((_file) => _file !== file)); }; const handleFileUploadImportButtonClick = async () => { + if (!importerKey) { + return; + } + setLoading(true); try { @@ -75,13 +102,14 @@ function NewImportPage() { Array.from( files, (file) => - new Promise((resolve) => { + new Promise((resolve) => { const reader = new FileReader(); reader.readAsDataURL(file); reader.onloadend = async () => { + const result = reader.result as string; try { await uploadImportFile({ - binaryContent: reader.result.split(';base64,')[1], + binaryContent: result.split(';base64,')[1], contentType: file.type, fileName: file.name, importerKey, @@ -104,11 +132,15 @@ function NewImportPage() { const [fileUrl, setFileUrl] = useSafely(useState('')); - const handleFileUrlChange = (event) => { + const handleFileUrlChange = (event: FormEvent) => { setFileUrl(event.currentTarget.value); }; const handleFileUrlImportButtonClick = async () => { + if (!importerKey) { + return; + } + setLoading(true); try { @@ -124,11 +156,15 @@ function NewImportPage() { const [filePath, setFilePath] = useSafely(useState('')); - const handleFilePathChange = (event) => { + const handleFilePathChange = (event: FormEvent) => { setFilePath(event.currentTarget.value); }; const handleFilePathImportButtonClick = async () => { + if (!importerKey) { + return; + } + setLoading(true); try { @@ -148,11 +184,12 @@ function NewImportPage() { const handleImportButtonClick = (fileType === 'upload' && handleFileUploadImportButtonClick) || (fileType === 'url' && handleFileUrlImportButtonClick) || - (fileType === 'path' && handleFilePathImportButtonClick); + (fileType === 'path' && handleFilePathImportButtonClick) || + undefined; return ( - + )} - - + + @@ -178,14 +215,14 @@ function NewImportPage() { disabled={isLoading} placeholder={t('Select_an_option')} onChange={handleImporterKeyChange} - options={Importers.getAll().map(({ key, name }) => [key, t(name)])} + options={options} /> {importer && ( - {importer.name === 'CSV' + {importer.key === 'csv' ? t('Importer_From_Description_CSV') - : t('Importer_From_Description', { from: t(importer.name) })} + : t('Importer_From_Description', { from: t(importer.name as TranslationKey) })} )} @@ -268,7 +305,7 @@ function NewImportPage() { )} - + ); } diff --git a/apps/meteor/client/views/admin/import/PrepareChannels.tsx b/apps/meteor/client/views/admin/import/PrepareChannels.tsx index e7b5f4a6a6a34..14d7cdc0614f8 100644 --- a/apps/meteor/client/views/admin/import/PrepareChannels.tsx +++ b/apps/meteor/client/views/admin/import/PrepareChannels.tsx @@ -3,12 +3,7 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import type { FC, Dispatch, SetStateAction, ChangeEvent } from 'react'; import React, { useState, useCallback } from 'react'; -type ChannelDescriptor = { - channel_id: string; - name: string; - is_archived: boolean; - do_import: boolean; -}; +import type { ChannelDescriptor } from './ChannelDescriptor'; type PrepareChannelsProps = { channelsCount: number; @@ -16,6 +11,7 @@ type PrepareChannelsProps = { setChannels: Dispatch>; }; +// TODO: review inner logic const PrepareChannels: FC = ({ channels, channelsCount, setChannels }) => { const t = useTranslation(); const [current, setCurrent] = useState(0); diff --git a/apps/meteor/client/views/admin/import/PrepareImportPage.js b/apps/meteor/client/views/admin/import/PrepareImportPage.tsx similarity index 73% rename from apps/meteor/client/views/admin/import/PrepareImportPage.js rename to apps/meteor/client/views/admin/import/PrepareImportPage.tsx index 3b86b3c06bf72..b6d378c60bd80 100644 --- a/apps/meteor/client/views/admin/import/PrepareImportPage.js +++ b/apps/meteor/client/views/admin/import/PrepareImportPage.tsx @@ -1,5 +1,7 @@ +import type { IImport, IImporterSelection, Serialized } from '@rocket.chat/core-typings'; import { Badge, Box, Button, ButtonGroup, Margins, ProgressBar, Throbber, Tabs } from '@rocket.chat/fuselage'; import { useDebouncedValue, useSafely } from '@rocket.chat/fuselage-hooks'; +import type { TranslationKey } from '@rocket.chat/ui-contexts'; import { useEndpoint, useTranslation, useStream, useRouter } from '@rocket.chat/ui-contexts'; import React, { useEffect, useState, useMemo } from 'react'; @@ -12,13 +14,15 @@ import { ImportingErrorStates, } from '../../../../app/importer/lib/ImporterProgressStep'; import { numberFormat } from '../../../../lib/utils/stringUtils'; -import Page from '../../../components/Page'; +import { Page, PageHeader, PageScrollableContentWithShadow } from '../../../components/Page'; +import type { ChannelDescriptor } from './ChannelDescriptor'; import PrepareChannels from './PrepareChannels'; import PrepareUsers from './PrepareUsers'; +import type { UserDescriptor } from './UserDescriptor'; import { useErrorHandler } from './useErrorHandler'; -const waitFor = (fn, predicate) => - new Promise((resolve, reject) => { +const waitFor = (fn: () => Promise, predicate: (arg: T) => arg is U) => + new Promise((resolve, reject) => { const callPromise = () => { fn().then((result) => { if (predicate(result)) { @@ -33,16 +37,17 @@ const waitFor = (fn, predicate) => callPromise(); }); +// TODO: review inner logic function PrepareImportPage() { const t = useTranslation(); const handleError = useErrorHandler(); const [isPreparing, setPreparing] = useSafely(useState(true)); - const [progressRate, setProgressRate] = useSafely(useState(null)); - const [status, setStatus] = useSafely(useState(null)); + const [progressRate, setProgressRate] = useSafely(useState(null)); + const [status, setStatus] = useSafely(useState(null)); const [messageCount, setMessageCount] = useSafely(useState(0)); - const [users, setUsers] = useState([]); - const [channels, setChannels] = useState([]); + const [users, setUsers] = useState([]); + const [channels, setChannels] = useState([]); const [isImporting, setImporting] = useSafely(useState(false)); const usersCount = useMemo(() => users.filter(({ do_import }) => do_import).length, [users]); @@ -58,8 +63,11 @@ function PrepareImportPage() { useEffect( () => - streamer('progress', ({ rate }) => { - setProgressRate(rate); + streamer('progress', (progress) => { + // Ignore any update without the rate since we're not showing any other info anyway + if ('rate' in progress) { + setProgressRate(progress.rate); + } }), [streamer, setProgressRate], ); @@ -67,7 +75,10 @@ function PrepareImportPage() { useEffect(() => { const loadImportFileData = async () => { try { - const data = await waitFor(getImportFileData, (data) => data && !data.waiting); + const data = await waitFor( + getImportFileData, + (data): data is IImporterSelection => data && (!('waiting' in data) || !data.waiting), + ); if (!data) { handleError(t('Importer_not_setup')); @@ -75,15 +86,9 @@ function PrepareImportPage() { return; } - if (data.step) { - handleError(t('Failed_To_Load_Import_Data')); - router.navigate('/admin/import'); - return; - } - setMessageCount(data.message_count); - setUsers(data.users.map((user) => ({ ...user, do_import: true }))); - setChannels(data.channels.map((channel) => ({ ...channel, do_import: true }))); + setUsers(data.users.map((user) => ({ ...user, username: user.username ?? '', do_import: true }))); + setChannels(data.channels.map((channel) => ({ ...channel, name: channel.name ?? '', do_import: true }))); setPreparing(false); setProgressRate(null); } catch (error) { @@ -96,7 +101,8 @@ function PrepareImportPage() { try { const { operation } = await waitFor( getCurrentImportOperation, - ({ operation }) => operation.valid && !ImportWaitingStates.includes(operation.status), + (data): data is Serialized<{ operation: IImport }> => + data.operation.valid && !ImportWaitingStates.includes(data.operation.status), ); if (!operation.valid) { @@ -149,7 +155,12 @@ function PrepareImportPage() { setImporting(true); try { - await startImport({ input: { users, channels } }); + await startImport({ + input: { + users: users.map((user) => ({ is_bot: false, is_email_taken: false, ...user })), + channels: channels.map((channel) => ({ is_private: false, is_direct: false, ...channel })), + }, + }); router.navigate('/admin/import/progress'); } catch (error) { handleError(error, t('Failed_To_Start_Import')); @@ -158,7 +169,7 @@ function PrepareImportPage() { }; const [tab, setTab] = useState('users'); - const handleTabClick = useMemo(() => (tab) => () => setTab(tab), []); + const handleTabClick = useMemo(() => (tab: string) => () => setTab(tab), []); const statusDebounced = useDebouncedValue(status, 100); @@ -169,7 +180,7 @@ function PrepareImportPage() { return ( - + - - - + + - {statusDebounced && t(statusDebounced.replace('importer_', 'importer_status_'))} + {statusDebounced && t(statusDebounced.replace('importer_', 'importer_status_') as TranslationKey)} {!isPreparing && ( @@ -204,7 +214,7 @@ function PrepareImportPage() { <> {progressRate ? ( - + {numberFormat(progressRate, 0)}% @@ -220,7 +230,7 @@ function PrepareImportPage() { )} - + ); } diff --git a/apps/meteor/client/views/admin/import/PrepareUsers.tsx b/apps/meteor/client/views/admin/import/PrepareUsers.tsx index 837b32c50e00e..c3000230a16f4 100644 --- a/apps/meteor/client/views/admin/import/PrepareUsers.tsx +++ b/apps/meteor/client/views/admin/import/PrepareUsers.tsx @@ -3,13 +3,7 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import type { FC, Dispatch, SetStateAction, ChangeEvent } from 'react'; import React, { useState, useCallback } from 'react'; -type UserDescriptor = { - user_id: string; - username: string; - email: string; - is_deleted: boolean; - do_import: boolean; -}; +import type { UserDescriptor } from './UserDescriptor'; type PrepareUsersProps = { usersCount: number; @@ -17,6 +11,7 @@ type PrepareUsersProps = { setUsers: Dispatch>; }; +// TODO: review inner logic const PrepareUsers: FC = ({ usersCount, users, setUsers }) => { const t = useTranslation(); const [current, setCurrent] = useState(0); diff --git a/apps/meteor/client/views/admin/import/UserDescriptor.tsx b/apps/meteor/client/views/admin/import/UserDescriptor.tsx new file mode 100644 index 0000000000000..b7a611fbd213c --- /dev/null +++ b/apps/meteor/client/views/admin/import/UserDescriptor.tsx @@ -0,0 +1,7 @@ +export type UserDescriptor = { + user_id: string; + username: string; + email: string; + is_deleted: boolean; + do_import: boolean; +}; diff --git a/apps/meteor/client/views/admin/import/useErrorHandler.js b/apps/meteor/client/views/admin/import/useErrorHandler.js deleted file mode 100644 index 0afa59f2cfbb8..0000000000000 --- a/apps/meteor/client/views/admin/import/useErrorHandler.js +++ /dev/null @@ -1,32 +0,0 @@ -import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import { useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; - -export const useErrorHandler = () => { - const t = useTranslation(); - const dispatchToastMessage = useToastMessageDispatch(); - - return useMutableCallback((error, defaultMessage) => { - console.error(error); - - if (typeof error === 'string') { - dispatchToastMessage({ type: 'error', message: error }); - return; - } - - const errorType = error?.xhr?.responseJSON?.errorType; - - if (typeof errorType === 'string' && t.has(errorType)) { - dispatchToastMessage({ type: 'error', message: t(errorType) }); - return; - } - - if (typeof errorType?.error === 'string' && t.has(errorType.error)) { - dispatchToastMessage({ type: 'error', message: t(errorType?.error) }); - return; - } - - if (defaultMessage) { - dispatchToastMessage({ type: 'error', message: defaultMessage }); - } - }); -}; diff --git a/apps/meteor/client/views/admin/import/useErrorHandler.ts b/apps/meteor/client/views/admin/import/useErrorHandler.ts new file mode 100644 index 0000000000000..124b7a6e195a4 --- /dev/null +++ b/apps/meteor/client/views/admin/import/useErrorHandler.ts @@ -0,0 +1,12 @@ +import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { useToastMessageDispatch } from '@rocket.chat/ui-contexts'; + +export const useErrorHandler = () => { + const dispatchToastMessage = useToastMessageDispatch(); + + return useMutableCallback((error: unknown, defaultMessage?: unknown) => { + console.error(error); + + dispatchToastMessage({ type: 'error', message: error ?? defaultMessage }); + }); +}; diff --git a/apps/meteor/client/views/admin/integrations/IntegrationsPage.tsx b/apps/meteor/client/views/admin/integrations/IntegrationsPage.tsx index dbf99f3b32812..a54c83a12429c 100644 --- a/apps/meteor/client/views/admin/integrations/IntegrationsPage.tsx +++ b/apps/meteor/client/views/admin/integrations/IntegrationsPage.tsx @@ -3,7 +3,7 @@ import { useRouteParameter, useRouter, useTranslation } from '@rocket.chat/ui-co import type { ReactElement } from 'react'; import React from 'react'; -import Page from '../../../components/Page'; +import { Page, PageHeader, PageContent } from '../../../components/Page'; import IntegrationsTable from './IntegrationsTable'; import NewBot from './NewBot'; import NewZapier from './NewZapier'; @@ -17,7 +17,7 @@ const IntegrationsPage = (): ReactElement => { return ( - + - + router.navigate('/admin/integrations')}> {t('All')} @@ -44,11 +44,11 @@ const IntegrationsPage = (): ReactElement => { {t('Bots')} - + {context === 'zapier' && } {context === 'bots' && } {showTable && } - + ); }; diff --git a/apps/meteor/client/views/admin/integrations/incoming/EditIncomingWebhook.tsx b/apps/meteor/client/views/admin/integrations/incoming/EditIncomingWebhook.tsx index 12ecb9be6a22e..d76f4fb65da9e 100644 --- a/apps/meteor/client/views/admin/integrations/incoming/EditIncomingWebhook.tsx +++ b/apps/meteor/client/views/admin/integrations/incoming/EditIncomingWebhook.tsx @@ -6,7 +6,7 @@ import React, { useCallback } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import GenericModal from '../../../../components/GenericModal'; -import Page from '../../../../components/Page'; +import { Page, PageHeader, PageScrollableContentWithShadow, PageFooter } from '../../../../components/Page'; import { useCreateIntegration } from '../hooks/useCreateIntegration'; import { useDeleteIntegration } from '../hooks/useDeleteIntegration'; import { useUpdateIntegration } from '../hooks/useUpdateIntegration'; @@ -77,7 +77,7 @@ const EditIncomingWebhook = ({ webhookData }: { webhookData?: Serialized - + - - + + @@ -129,7 +129,7 @@ const OutgoingWebhookHistoryPage = (props: ComponentProps) => { onSetItemsPerPage={setItemsPerPage} onSetCurrent={setCurrent} /> - +
); }; diff --git a/apps/meteor/client/views/admin/invites/InvitesPage.tsx b/apps/meteor/client/views/admin/invites/InvitesPage.tsx index 0f11ffc83e64a..d8b6444ffa8b7 100644 --- a/apps/meteor/client/views/admin/invites/InvitesPage.tsx +++ b/apps/meteor/client/views/admin/invites/InvitesPage.tsx @@ -14,7 +14,7 @@ import { GenericTableHeaderCell, GenericTableLoadingTable, } from '../../../components/GenericTable'; -import Page from '../../../components/Page'; +import { Page, PageHeader, PageContent } from '../../../components/Page'; import InviteRow from './InviteRow'; const InvitesPage = (): ReactElement => { @@ -88,8 +88,8 @@ const InvitesPage = (): ReactElement => { return ( - - + + <> {isLoading && ( @@ -121,7 +121,7 @@ const InvitesPage = (): ReactElement => { )} - + ); }; diff --git a/apps/meteor/client/views/admin/mailer/MailerPage.tsx b/apps/meteor/client/views/admin/mailer/MailerPage.tsx index e917315957bb4..474cf0be65742 100644 --- a/apps/meteor/client/views/admin/mailer/MailerPage.tsx +++ b/apps/meteor/client/views/admin/mailer/MailerPage.tsx @@ -20,7 +20,7 @@ import { Controller, useForm } from 'react-hook-form'; import { validateEmail } from '../../../../lib/emailValidator'; import { isJSON } from '../../../../lib/utils/isJSON'; -import Page from '../../../components/Page'; +import { Page, PageHeader, PageScrollableContentWithShadow, PageFooter } from '../../../components/Page'; export type SendEmailFormValue = { fromEmail: string; @@ -71,8 +71,8 @@ const MailerPage = () => { return ( - - + + @@ -178,15 +178,15 @@ const MailerPage = () => { - - + + - + ); }; diff --git a/apps/meteor/client/views/admin/moderation/ModerationConsolePage.tsx b/apps/meteor/client/views/admin/moderation/ModerationConsolePage.tsx index 005463f246ec4..6a32478df374f 100644 --- a/apps/meteor/client/views/admin/moderation/ModerationConsolePage.tsx +++ b/apps/meteor/client/views/admin/moderation/ModerationConsolePage.tsx @@ -2,7 +2,7 @@ import { useTranslation, useRouteParameter, useToastMessageDispatch } from '@roc import React from 'react'; import { Contextualbar } from '../../../components/Contextualbar'; -import Page from '../../../components/Page'; +import { Page, PageHeader, PageContent } from '../../../components/Page'; import { getPermaLink } from '../../../lib/getPermaLink'; import ModerationConsoleTable from './ModerationConsoleTable'; import UserMessages from './UserMessages'; @@ -26,10 +26,10 @@ const ModerationConsolePage = () => { return ( - - + + - + {context && {context === 'info' && id && }} diff --git a/apps/meteor/client/views/admin/oauthApps/OAuthAppsPage.tsx b/apps/meteor/client/views/admin/oauthApps/OAuthAppsPage.tsx index 0fde33b4a7b77..c5cd7f5fc96bc 100644 --- a/apps/meteor/client/views/admin/oauthApps/OAuthAppsPage.tsx +++ b/apps/meteor/client/views/admin/oauthApps/OAuthAppsPage.tsx @@ -3,7 +3,7 @@ import { useRouteParameter, useRoute, useTranslation } from '@rocket.chat/ui-con import type { ReactElement } from 'react'; import React from 'react'; -import Page from '../../../components/Page'; +import { Page, PageHeader, PageContent } from '../../../components/Page'; import EditOauthAppWithData from './EditOauthAppWithData'; import OAuthAddApp from './OAuthAddApp'; import OAuthAppsTable from './OAuthAppsTable'; @@ -19,7 +19,7 @@ const OAuthAppsPage = (): ReactElement => { return ( - + {context && ( )} - - + + {!context && } {id && context === 'edit' && } {context === 'new' && } - + ); diff --git a/apps/meteor/client/views/admin/permissions/PermissionsTable/PermissionsTable.tsx b/apps/meteor/client/views/admin/permissions/PermissionsTable/PermissionsTable.tsx index 4b956654cd5e1..1d688da630a95 100644 --- a/apps/meteor/client/views/admin/permissions/PermissionsTable/PermissionsTable.tsx +++ b/apps/meteor/client/views/admin/permissions/PermissionsTable/PermissionsTable.tsx @@ -7,7 +7,7 @@ import React, { useState } from 'react'; import GenericNoResults from '../../../../components/GenericNoResults'; import { GenericTable, GenericTableHeader, GenericTableHeaderCell, GenericTableBody } from '../../../../components/GenericTable'; import { usePagination } from '../../../../components/GenericTable/hooks/usePagination'; -import Page from '../../../../components/Page'; +import { Page, PageHeader, PageContent } from '../../../../components/Page'; import CustomRoleUpsellModal from '../CustomRoleUpsellModal'; import PermissionsContextBar from '../PermissionsContextBar'; import { usePermissionsAndRoles } from '../hooks/usePermissionsAndRoles'; @@ -58,11 +58,11 @@ const PermissionsTable = ({ isEnterprise }: { isEnterprise: boolean }): ReactEle return ( - + - + - + {permissions?.length === 0 && } @@ -120,7 +120,7 @@ const PermissionsTable = ({ isEnterprise }: { isEnterprise: boolean }): ReactEle )} - + diff --git a/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRolePage.tsx b/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRolePage.tsx index 73de11407686d..478a0b445e3e5 100644 --- a/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRolePage.tsx +++ b/apps/meteor/client/views/admin/permissions/UsersInRole/UsersInRolePage.tsx @@ -6,7 +6,7 @@ import type { ReactElement } from 'react'; import React, { useRef } from 'react'; import { useForm, Controller } from 'react-hook-form'; -import Page from '../../../../components/Page'; +import { Page, PageHeader, PageContent } from '../../../../components/Page'; import RoomAutoComplete from '../../../../components/RoomAutoComplete'; import UserAutoCompleteMultiple from '../../../../components/UserAutoCompleteMultiple'; import UsersInRoleTable from './UsersInRoleTable'; @@ -61,12 +61,12 @@ const UsersInRolePage = ({ role }: { role: IRole }): ReactElement => { return ( - + - - + + {role.scope !== 'Users' && ( @@ -108,7 +108,7 @@ const UsersInRolePage = ({ role }: { role: IRole }): ReactElement => { )} {role.scope !== 'Users' && !rid && {t('Select_a_room')}} - + ); }; diff --git a/apps/meteor/client/views/admin/rooms/RoomsPage.tsx b/apps/meteor/client/views/admin/rooms/RoomsPage.tsx index 9b51738ab7c72..e754fdce17e70 100644 --- a/apps/meteor/client/views/admin/rooms/RoomsPage.tsx +++ b/apps/meteor/client/views/admin/rooms/RoomsPage.tsx @@ -2,7 +2,7 @@ import { useRouteParameter, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useRef } from 'react'; -import Page from '../../../components/Page'; +import { Page, PageHeader, PageContent } from '../../../components/Page'; import EditRoomWithData from './EditRoomWithData'; import RoomsTable from './RoomsTable'; @@ -17,10 +17,10 @@ const RoomsPage = (): ReactElement => { return ( - - + + - + {context && } diff --git a/apps/meteor/client/views/admin/settings/GroupPage.tsx b/apps/meteor/client/views/admin/settings/GroupPage.tsx index 62b20b6c5048d..9e4ef1fa3ab85 100644 --- a/apps/meteor/client/views/admin/settings/GroupPage.tsx +++ b/apps/meteor/client/views/admin/settings/GroupPage.tsx @@ -14,7 +14,7 @@ import { import type { FC, ReactNode, FormEvent, MouseEvent } from 'react'; import React, { useMemo, memo } from 'react'; -import Page from '../../../components/Page'; +import { Page, PageHeader, PageScrollableContentWithShadow, PageFooter } from '../../../components/Page'; import type { EditableSetting } from '../EditableSettingsContext'; import { useEditableSettingsDispatch, useEditableSettings } from '../EditableSettingsContext'; import GroupPageSkeleton from './GroupPageSkeleton'; @@ -160,14 +160,14 @@ const GroupPage: FC = ({ return ( - + {headerButtons} - + {tabs} {isCustom ? ( children ) : ( - + {i18nDescription && isTranslationKey(i18nDescription) && t.has(i18nDescription) && ( @@ -177,9 +177,9 @@ const GroupPage: FC = ({ {children} - + )} - + {changedEditableSettings.length > 0 && ( @@ -48,9 +48,9 @@ const WorkspacePage = ({ )} - + - + {warningMultipleInstances && ( @@ -94,7 +94,7 @@ const WorkspacePage = ({ - + ); }; diff --git a/apps/meteor/client/views/admin/workspace/WorkspaceRoute.tsx b/apps/meteor/client/views/admin/workspace/WorkspaceRoute.tsx index bce054bf7f44b..ed10912965d36 100644 --- a/apps/meteor/client/views/admin/workspace/WorkspaceRoute.tsx +++ b/apps/meteor/client/views/admin/workspace/WorkspaceRoute.tsx @@ -3,7 +3,7 @@ import { usePermission, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { memo, useState } from 'react'; -import Page from '../../../components/Page'; +import { Page, PageHeader, PageScrollableContentWithShadow } from '../../../components/Page'; import PageSkeleton from '../../../components/PageSkeleton'; import { useWorkspaceInfo } from '../../../hooks/useWorkspaceInfo'; import { downloadJsonAs } from '../../../lib/download'; @@ -33,16 +33,16 @@ const WorkspaceRoute = (): ReactElement => { if (serverInfoQuery.isError || instancesQuery.isError || statisticsQuery.isError) { return ( - + - - + + {t('Error_loading_pages')} - + ); } diff --git a/apps/meteor/client/views/conference/ConferencePageError.tsx b/apps/meteor/client/views/conference/ConferencePageError.tsx index 7e60e3ae7c3eb..b2726b6ddaad7 100644 --- a/apps/meteor/client/views/conference/ConferencePageError.tsx +++ b/apps/meteor/client/views/conference/ConferencePageError.tsx @@ -2,7 +2,7 @@ import { States, StatesIcon, StatesTitle, StatesSubtitle, StatesActions, StatesA import { useTranslation, useUser, useRoute } from '@rocket.chat/ui-contexts'; import React from 'react'; -import Page from '../../components/Page'; +import { Page, PageHeader, PageContent } from '../../components/Page'; const ConferencePageError = () => { const t = useTranslation(); @@ -11,8 +11,8 @@ const ConferencePageError = () => { return ( - - + + {t('Call_not_found')} @@ -23,7 +23,7 @@ const ConferencePageError = () => { )} - + ); }; diff --git a/apps/meteor/client/views/directory/DirectoryPage.tsx b/apps/meteor/client/views/directory/DirectoryPage.tsx index d260971dde2ce..2edfe6e05e09e 100644 --- a/apps/meteor/client/views/directory/DirectoryPage.tsx +++ b/apps/meteor/client/views/directory/DirectoryPage.tsx @@ -3,7 +3,7 @@ import { useRouter, useRouteParameter, useSetting, useTranslation } from '@rocke import type { ReactElement } from 'react'; import React, { useEffect, useCallback } from 'react'; -import Page from '../../components/Page'; +import { Page, PageHeader, PageContent } from '../../components/Page'; import ChannelsTab from './tabs/channels/ChannelsTab'; import TeamsTab from './tabs/teams/TeamsTab'; import UsersTab from './tabs/users/UsersTab'; @@ -38,7 +38,7 @@ const DirectoryPage = (): ReactElement => { return ( - + {t('Channels')} @@ -55,12 +55,12 @@ const DirectoryPage = (): ReactElement => { )} - + {tab === 'channels' && } {tab === 'users' && } {tab === 'teams' && } {federationEnabled && tab === 'external' && } - + ); }; diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/AppDetailsPage.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/AppDetailsPage.tsx index 86bbd6ec9c96b..46803e1b6edc4 100644 --- a/apps/meteor/client/views/marketplace/AppDetailsPage/AppDetailsPage.tsx +++ b/apps/meteor/client/views/marketplace/AppDetailsPage/AppDetailsPage.tsx @@ -8,7 +8,7 @@ import React, { useState, useCallback, useRef } from 'react'; import type { ISettings } from '../../../../ee/client/apps/@types/IOrchestrator'; import { AppClientOrchestratorInstance } from '../../../../ee/client/apps/orchestrator'; -import Page from '../../../components/Page'; +import { Page, PageHeader, PageScrollableContentWithShadow } from '../../../components/Page'; import { handleAPIError } from '../helpers/handleAPIError'; import { useAppInfo } from '../hooks/useAppInfo'; import AppDetailsPageHeader from './AppDetailsPageHeader'; @@ -73,7 +73,7 @@ const AppDetailsPage = ({ id }: { id: App['id'] }): ReactElement => { return ( - + {installed && isAdminUser && ( )} - - + + {!appData && } {appData && ( @@ -117,7 +117,7 @@ const AppDetailsPage = ({ id }: { id: App['id'] }): ReactElement => { )} - + ); }; diff --git a/apps/meteor/client/views/marketplace/AppInstallPage.js b/apps/meteor/client/views/marketplace/AppInstallPage.js index f5ebb0f915aef..b72ed925a0662 100644 --- a/apps/meteor/client/views/marketplace/AppInstallPage.js +++ b/apps/meteor/client/views/marketplace/AppInstallPage.js @@ -13,7 +13,7 @@ import React, { useCallback, useState } from 'react'; import { useForm, Controller } from 'react-hook-form'; import { AppClientOrchestratorInstance } from '../../../ee/client/apps/orchestrator'; -import Page from '../../components/Page'; +import { Page, PageHeader, PageScrollableContent } from '../../components/Page'; import { useAppsReload } from '../../contexts/hooks/useAppsReload'; import { useExternalLink } from '../../hooks/useExternalLink'; import { useFileInput } from '../../hooks/useFileInput'; @@ -196,8 +196,8 @@ function AppInstallPage() { return ( - - + + {t('App_Url_to_Install_From')} @@ -242,7 +242,7 @@ function AppInstallPage() { - + ); } diff --git a/apps/meteor/client/views/marketplace/AppsPage/AppsPage.tsx b/apps/meteor/client/views/marketplace/AppsPage/AppsPage.tsx index f39bd8f5426b4..0c90ac238d243 100644 --- a/apps/meteor/client/views/marketplace/AppsPage/AppsPage.tsx +++ b/apps/meteor/client/views/marketplace/AppsPage/AppsPage.tsx @@ -2,7 +2,7 @@ import { useTranslation, useRouteParameter } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; -import Page from '../../../components/Page'; +import { Page, PageContent } from '../../../components/Page'; import MarketplaceHeader from '../components/MarketplaceHeader'; import AppsPageContent from './AppsPageContent'; @@ -16,9 +16,9 @@ const AppsPage = (): ReactElement => { return ( - + - + ); }; diff --git a/apps/meteor/client/views/marketplace/components/MarketplaceHeader.tsx b/apps/meteor/client/views/marketplace/components/MarketplaceHeader.tsx index 7696801c3124f..34a450868f9da 100644 --- a/apps/meteor/client/views/marketplace/components/MarketplaceHeader.tsx +++ b/apps/meteor/client/views/marketplace/components/MarketplaceHeader.tsx @@ -4,7 +4,7 @@ import type { ReactElement } from 'react'; import React, { useCallback } from 'react'; import { GenericResourceUsageSkeleton } from '../../../components/GenericResourceUsage'; -import Page from '../../../components/Page'; +import { PageHeader } from '../../../components/Page'; import UnlimitedAppsUpsellModal from '../UnlimitedAppsUpsellModal'; import { useAppsCountQuery } from '../hooks/useAppsCountQuery'; import EnabledAppsCount from './EnabledAppsCount'; @@ -26,7 +26,7 @@ const MarketplaceHeader = ({ title }: { title: string }): ReactElement | null => } return ( - + {result.isLoading && } {result.isSuccess && !result.data.hasUnlimitedApps && } @@ -41,7 +41,7 @@ const MarketplaceHeader = ({ title }: { title: string }): ReactElement | null => )} {isAdmin && context === 'private' && } - + ); }; diff --git a/apps/meteor/client/views/notAuthorized/NotAuthorizedPage.tsx b/apps/meteor/client/views/notAuthorized/NotAuthorizedPage.tsx index 7273c8def80dd..b5a156b56bf4a 100644 --- a/apps/meteor/client/views/notAuthorized/NotAuthorizedPage.tsx +++ b/apps/meteor/client/views/notAuthorized/NotAuthorizedPage.tsx @@ -3,18 +3,18 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; -import Page from '../../components/Page'; +import { Page, PageContent } from '../../components/Page'; const NotAuthorizedPage = (): ReactElement => { const t = useTranslation(); return ( - + {t('You_are_not_authorized_to_view_this_page')} - + ); }; diff --git a/apps/meteor/client/views/omnichannel/agents/AgentsPage.tsx b/apps/meteor/client/views/omnichannel/agents/AgentsPage.tsx index d276413b9861f..046b131ab3c17 100644 --- a/apps/meteor/client/views/omnichannel/agents/AgentsPage.tsx +++ b/apps/meteor/client/views/omnichannel/agents/AgentsPage.tsx @@ -2,7 +2,7 @@ import { usePermission, useRouteParameter, useTranslation } from '@rocket.chat/u import type { ReactElement } from 'react'; import React from 'react'; -import Page from '../../../components/Page'; +import { Page, PageHeader, PageContent } from '../../../components/Page'; import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; import AgentEditWithData from './AgentEditWithData'; import AgentInfo from './AgentInfo'; @@ -22,10 +22,10 @@ const AgentsPage = (): ReactElement => { return ( - - + + - + {id && context === 'edit' && } {id && context === 'info' && } diff --git a/apps/meteor/client/views/omnichannel/analytics/AnalyticsPage.tsx b/apps/meteor/client/views/omnichannel/analytics/AnalyticsPage.tsx index 4a7405b7b8ccf..7ce0b699e28c0 100644 --- a/apps/meteor/client/views/omnichannel/analytics/AnalyticsPage.tsx +++ b/apps/meteor/client/views/omnichannel/analytics/AnalyticsPage.tsx @@ -4,7 +4,7 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import React, { useMemo, useState, useEffect } from 'react'; import AutoCompleteDepartment from '../../../components/AutoCompleteDepartment'; -import Page from '../../../components/Page'; +import { Page, PageHeader, PageScrollableContentWithShadow } from '../../../components/Page'; import AgentOverview from './AgentOverview'; import DateRangePicker from './DateRangePicker'; import InterchangeableChart from './InterchangeableChart'; @@ -52,8 +52,8 @@ const AnalyticsPage = () => { return ( - - + + @@ -96,7 +96,7 @@ const AnalyticsPage = () => { - + ); }; diff --git a/apps/meteor/client/views/omnichannel/appearance/AppearancePage.tsx b/apps/meteor/client/views/omnichannel/appearance/AppearancePage.tsx index 92add7cd0272c..a2cfb7b8103b2 100644 --- a/apps/meteor/client/views/omnichannel/appearance/AppearancePage.tsx +++ b/apps/meteor/client/views/omnichannel/appearance/AppearancePage.tsx @@ -5,7 +5,7 @@ import { useToastMessageDispatch, useEndpoint, useTranslation } from '@rocket.ch import React from 'react'; import { useForm, FormProvider } from 'react-hook-form'; -import Page from '../../../components/Page'; +import { Page, PageHeader, PageScrollableContentWithShadow, PageFooter } from '../../../components/Page'; import AppearanceForm from './AppearanceForm'; type LivechatAppearanceSettings = { @@ -74,8 +74,8 @@ const AppearancePage = ({ settings }: { settings: Serialized[] }) => { return ( - - + +
@@ -83,15 +83,15 @@ const AppearancePage = ({ settings }: { settings: Serialized[] }) => {
-
- + + - +
); }; diff --git a/apps/meteor/client/views/omnichannel/appearance/AppearancePageContainer.tsx b/apps/meteor/client/views/omnichannel/appearance/AppearancePageContainer.tsx index 53eee2ccd0a8c..b28de74909818 100644 --- a/apps/meteor/client/views/omnichannel/appearance/AppearancePageContainer.tsx +++ b/apps/meteor/client/views/omnichannel/appearance/AppearancePageContainer.tsx @@ -3,7 +3,7 @@ import { usePermission, useTranslation } from '@rocket.chat/ui-contexts'; import type { FC } from 'react'; import React from 'react'; -import Page from '../../../components/Page'; +import { Page, PageHeader, PageScrollableContentWithShadow } from '../../../components/Page'; import PageSkeleton from '../../../components/PageSkeleton'; import { AsyncStatePhase } from '../../../hooks/useAsyncState'; import { useEndpointData } from '../../../hooks/useEndpointData'; @@ -28,10 +28,10 @@ const AppearancePageContainer: FC = () => { if (!data?.appearance || error) { return ( - - + + {t('Error')} - + ); } diff --git a/apps/meteor/client/views/omnichannel/businessHours/BusinessHoursPage.js b/apps/meteor/client/views/omnichannel/businessHours/BusinessHoursPage.js index 4a0c466378e60..2e738f75009db 100644 --- a/apps/meteor/client/views/omnichannel/businessHours/BusinessHoursPage.js +++ b/apps/meteor/client/views/omnichannel/businessHours/BusinessHoursPage.js @@ -3,7 +3,7 @@ import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useRoute, useTranslation } from '@rocket.chat/ui-contexts'; import React, { lazy, useMemo } from 'react'; -import Page from '../../../components/Page'; +import { Page, PageHeader, PageContent } from '../../../components/Page'; const BusinessHoursPage = () => { const t = useTranslation(); @@ -19,16 +19,16 @@ const BusinessHoursPage = () => { return ( - + - - + + - + ); }; diff --git a/apps/meteor/client/views/omnichannel/businessHours/EditBusinessHoursPage.js b/apps/meteor/client/views/omnichannel/businessHours/EditBusinessHoursPage.js index 1b45e62fbc83b..1b3b687b35677 100644 --- a/apps/meteor/client/views/omnichannel/businessHours/EditBusinessHoursPage.js +++ b/apps/meteor/client/views/omnichannel/businessHours/EditBusinessHoursPage.js @@ -3,7 +3,7 @@ import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useToastMessageDispatch, useRoute, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; import React, { useRef, useMemo, useState } from 'react'; -import Page from '../../../components/Page'; +import { Page, PageHeader, PageScrollableContentWithShadow } from '../../../components/Page'; import PageSkeleton from '../../../components/PageSkeleton'; import { AsyncStatePhase } from '../../../hooks/useAsyncState'; import { useEndpointData } from '../../../hooks/useEndpointData'; @@ -91,19 +91,19 @@ const EditBusinessHoursPage = ({ id, type }) => { if (state === AsyncStatePhase.REJECTED || (AsyncStatePhase.RESOLVED && !data.businessHour)) { return ( - + - - + + {t('Error')} - + ); } return ( - + {!isSingleBH && } {type === 'custom' && ( @@ -115,10 +115,10 @@ const EditBusinessHoursPage = ({ id, type }) => { {t('Save')} - - + + - + ); }; diff --git a/apps/meteor/client/views/omnichannel/businessHours/NewBusinessHoursPage.js b/apps/meteor/client/views/omnichannel/businessHours/NewBusinessHoursPage.js index ed31480de693a..fb8759ba8a3ce 100644 --- a/apps/meteor/client/views/omnichannel/businessHours/NewBusinessHoursPage.js +++ b/apps/meteor/client/views/omnichannel/businessHours/NewBusinessHoursPage.js @@ -3,7 +3,7 @@ import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useToastMessageDispatch, useRoute, useMethod, useTranslation } from '@rocket.chat/ui-contexts'; import React, { useRef, useState } from 'react'; -import Page from '../../../components/Page'; +import { Page, PageHeader, PageScrollableContentWithShadow } from '../../../components/Page'; import { DAYS_OF_WEEK } from './BusinessHoursForm'; import BusinessHoursFormContainer from './BusinessHoursFormContainer'; import { mapBusinessHoursForm } from './mapBusinessHoursForm'; @@ -79,17 +79,17 @@ const NewBusinessHoursPage = () => { return ( - + - - + + - + ); }; diff --git a/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsPage.tsx b/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsPage.tsx index 83e000f2f1b87..d01680eb32548 100644 --- a/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsPage.tsx +++ b/apps/meteor/client/views/omnichannel/currentChats/CurrentChatsPage.tsx @@ -22,7 +22,7 @@ import { } from '../../../components/GenericTable'; import { usePagination } from '../../../components/GenericTable/hooks/usePagination'; import { useSort } from '../../../components/GenericTable/hooks/useSort'; -import Page from '../../../components/Page'; +import { Page, PageHeader, PageContent } from '../../../components/Page'; import { useIsOverMacLimit } from '../../../hooks/omnichannel/useIsOverMacLimit'; import CustomFieldsList from './CustomFieldsList'; import FilterByText from './FilterByText'; @@ -295,8 +295,8 @@ const CurrentChatsPage = ({ id, onRowClick }: { id?: string; onRowClick: (_id: s return ( - - + + {((isSuccess && data?.rooms.length > 0) || queryHasChanged) && ( ['setFilter']} @@ -352,7 +352,7 @@ const CurrentChatsPage = ({ id, onRowClick }: { id?: string; onRowClick: (_id: s /> )} - + {id === 'custom-fields' && hasCustomFields && ( diff --git a/apps/meteor/client/views/omnichannel/customFields/CustomFieldsPage.tsx b/apps/meteor/client/views/omnichannel/customFields/CustomFieldsPage.tsx index 4ee8131592216..a46a20f286fc3 100644 --- a/apps/meteor/client/views/omnichannel/customFields/CustomFieldsPage.tsx +++ b/apps/meteor/client/views/omnichannel/customFields/CustomFieldsPage.tsx @@ -2,7 +2,7 @@ import { Button } from '@rocket.chat/fuselage'; import { useRouteParameter, useRouter, useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; -import Page from '../../../components/Page'; +import { Page, PageHeader, PageContent } from '../../../components/Page'; import CustomFieldsTable from './CustomFieldsTable'; import EditCustomFields from './EditCustomFields'; import EditCustomFieldsWithData from './EditCustomFieldsWithData'; @@ -17,14 +17,14 @@ const CustomFieldsPage = () => { return ( - + - - + + - + {context === 'edit' && id && } {context === 'new' && } diff --git a/apps/meteor/client/views/omnichannel/departments/DepartmentsPage.tsx b/apps/meteor/client/views/omnichannel/departments/DepartmentsPage.tsx index ba90e953b0d5b..0583f53a14ef4 100644 --- a/apps/meteor/client/views/omnichannel/departments/DepartmentsPage.tsx +++ b/apps/meteor/client/views/omnichannel/departments/DepartmentsPage.tsx @@ -3,7 +3,7 @@ import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import { useRoute, useTranslation, useRouteParameter } from '@rocket.chat/ui-contexts'; import React from 'react'; -import Page from '../../../components/Page'; +import { Page, PageHeader, PageContent } from '../../../components/Page'; import DepartmentsTableV2 from './DepartmentsTable'; import EditDepartmentWithData from './EditDepartmentWithData'; import NewDepartment from './NewDepartment'; @@ -38,9 +38,9 @@ const DepartmentsPage = () => { return ( - + - + handleTabClick(undefined)}> {t('All')} @@ -49,9 +49,9 @@ const DepartmentsPage = () => { {t('Archived')} - + - + ); diff --git a/apps/meteor/client/views/omnichannel/departments/EditDepartment.tsx b/apps/meteor/client/views/omnichannel/departments/EditDepartment.tsx index d7a6156361502..6bf9534fd6c33 100644 --- a/apps/meteor/client/views/omnichannel/departments/EditDepartment.tsx +++ b/apps/meteor/client/views/omnichannel/departments/EditDepartment.tsx @@ -24,7 +24,7 @@ import { Controller, useForm } from 'react-hook-form'; import { useHasLicenseModule } from '../../../../ee/client/hooks/useHasLicenseModule'; import { validateEmail } from '../../../../lib/emailValidator'; import AutoCompleteDepartment from '../../../components/AutoCompleteDepartment'; -import Page from '../../../components/Page'; +import { Page, PageHeader, PageScrollableContentWithShadow } from '../../../components/Page'; import { useRecordList } from '../../../hooks/lists/useRecordList'; import { useRoomsList } from '../../../hooks/useRoomsList'; import { AsyncStatePhase } from '../../../lib/asyncState'; @@ -211,7 +211,7 @@ function EditDepartment({ data, id, title, allowedToForwardData }: EditDepartmen return ( - + - - + + - + ); diff --git a/apps/meteor/client/views/omnichannel/directory/OmnichannelDirectoryPage.tsx b/apps/meteor/client/views/omnichannel/directory/OmnichannelDirectoryPage.tsx index 85a50e04f5a5a..02f967ca6ccc3 100644 --- a/apps/meteor/client/views/omnichannel/directory/OmnichannelDirectoryPage.tsx +++ b/apps/meteor/client/views/omnichannel/directory/OmnichannelDirectoryPage.tsx @@ -3,7 +3,7 @@ import { useRouteParameter, usePermission, useTranslation, useRouter } from '@ro import type { ReactElement } from 'react'; import React, { useEffect, useCallback } from 'react'; -import Page from '../../../components/Page'; +import { Page, PageHeader, PageContent } from '../../../components/Page'; import { queryClient } from '../../../lib/queryClient'; import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; import ContextualBar from './ContextualBar'; @@ -46,7 +46,7 @@ const OmnichannelDirectoryPage = (): ReactElement => { return ( - + {t('Contacts')} @@ -58,9 +58,9 @@ const OmnichannelDirectoryPage = (): ReactElement => { {t('Calls' as 'color')} - + {(page === 'contacts' && ) || (page === 'chats' && ) || (page === 'calls' && )} - + diff --git a/apps/meteor/client/views/omnichannel/installation/Installation.tsx b/apps/meteor/client/views/omnichannel/installation/Installation.tsx index be24f496e2961..a6433eed415ca 100644 --- a/apps/meteor/client/views/omnichannel/installation/Installation.tsx +++ b/apps/meteor/client/views/omnichannel/installation/Installation.tsx @@ -3,7 +3,7 @@ import { useSetting, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; -import Page from '../../../components/Page'; +import { Page, PageHeader, PageScrollableContentWithShadow } from '../../../components/Page'; import RawText from '../../../components/RawText'; import TextCopy from '../../../components/TextCopy'; import Wrapper from './Wrapper'; @@ -26,8 +26,8 @@ const Installation = (): ReactElement => { return ( - - + +

@@ -36,7 +36,7 @@ const Installation = (): ReactElement => {

-
+
); }; diff --git a/apps/meteor/client/views/omnichannel/managers/ManagersRoute.tsx b/apps/meteor/client/views/omnichannel/managers/ManagersRoute.tsx index a290be6975b4b..64aea5dc848ce 100644 --- a/apps/meteor/client/views/omnichannel/managers/ManagersRoute.tsx +++ b/apps/meteor/client/views/omnichannel/managers/ManagersRoute.tsx @@ -2,7 +2,7 @@ import { usePermission, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; -import Page from '../../../components/Page'; +import { Page, PageHeader, PageContent } from '../../../components/Page'; import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; import ManagersTable from './ManagersTable'; @@ -17,10 +17,10 @@ const ManagersRoute = (): ReactElement => { return ( - - + + - + ); diff --git a/apps/meteor/client/views/omnichannel/queueList/QueueListPage.tsx b/apps/meteor/client/views/omnichannel/queueList/QueueListPage.tsx index 315b7afe9a4d9..677b8a2019907 100644 --- a/apps/meteor/client/views/omnichannel/queueList/QueueListPage.tsx +++ b/apps/meteor/client/views/omnichannel/queueList/QueueListPage.tsx @@ -1,7 +1,7 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; -import Page from '../../../components/Page'; +import { Page, PageHeader, PageContent } from '../../../components/Page'; import QueueListTable from './QueueListTable'; const QueueListPage = () => { @@ -9,10 +9,10 @@ const QueueListPage = () => { return ( - - + + - + ); }; diff --git a/apps/meteor/client/views/omnichannel/realTimeMonitoring/RealTimeMonitoringPage.js b/apps/meteor/client/views/omnichannel/realTimeMonitoring/RealTimeMonitoringPage.js index d035e095ece37..e9e392ecc0f10 100644 --- a/apps/meteor/client/views/omnichannel/realTimeMonitoring/RealTimeMonitoringPage.js +++ b/apps/meteor/client/views/omnichannel/realTimeMonitoring/RealTimeMonitoringPage.js @@ -4,7 +4,7 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import React, { useRef, useState, useMemo, useEffect, Fragment } from 'react'; import AutoCompleteDepartment from '../../../components/AutoCompleteDepartment'; -import Page from '../../../components/Page'; +import { Page, PageHeader, PageScrollableContentWithShadow } from '../../../components/Page'; import { getDateRange } from '../../../lib/utils/getDateRange'; import Label from '../components/Label'; import AgentStatusChart from './charts/AgentStatusChart'; @@ -68,8 +68,8 @@ const RealTimeMonitoringPage = () => { return ( - - + + @@ -114,7 +114,7 @@ const RealTimeMonitoringPage = () => { - + ); }; diff --git a/apps/meteor/client/views/omnichannel/triggers/TriggersPage.tsx b/apps/meteor/client/views/omnichannel/triggers/TriggersPage.tsx index 79eea71b62328..cc7fb51fefbd6 100644 --- a/apps/meteor/client/views/omnichannel/triggers/TriggersPage.tsx +++ b/apps/meteor/client/views/omnichannel/triggers/TriggersPage.tsx @@ -2,7 +2,7 @@ import { Button } from '@rocket.chat/fuselage'; import { useRouteParameter, useRouter, useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; -import Page from '../../../components/Page'; +import { Page, PageHeader, PageContent } from '../../../components/Page'; import EditTrigger from './EditTrigger'; import EditTriggerWithData from './EditTriggerWithData'; import TriggersTable from './TriggersTable'; @@ -16,12 +16,12 @@ const TriggersPage = () => { return ( - + - - + + - + {context === 'edit' && id && } {context === 'new' && } diff --git a/apps/meteor/client/views/omnichannel/webhooks/WebhooksPage.tsx b/apps/meteor/client/views/omnichannel/webhooks/WebhooksPage.tsx index 5da7b233d9043..47e308cf37ea7 100644 --- a/apps/meteor/client/views/omnichannel/webhooks/WebhooksPage.tsx +++ b/apps/meteor/client/views/omnichannel/webhooks/WebhooksPage.tsx @@ -19,7 +19,7 @@ import { useMutation } from '@tanstack/react-query'; import React, { useMemo } from 'react'; import { Controller, useForm, useWatch } from 'react-hook-form'; -import Page from '../../../components/Page'; +import { Page, PageHeader, PageScrollableContentWithShadow } from '../../../components/Page'; type WebhooksPageProps = { settings: Record; @@ -149,7 +149,7 @@ const WebhooksPage = ({ settings }: WebhooksPageProps) => { return ( - + - - + +

{t('You_can_use_webhooks_to_easily_integrate_livechat_with_your_CRM')}

@@ -225,7 +225,7 @@ const WebhooksPage = ({ settings }: WebhooksPageProps) => { - + ); }; diff --git a/apps/meteor/client/views/omnichannel/webhooks/WebhooksPageContainer.tsx b/apps/meteor/client/views/omnichannel/webhooks/WebhooksPageContainer.tsx index 5442f3d27c903..0b59999b24b88 100644 --- a/apps/meteor/client/views/omnichannel/webhooks/WebhooksPageContainer.tsx +++ b/apps/meteor/client/views/omnichannel/webhooks/WebhooksPageContainer.tsx @@ -4,7 +4,7 @@ import { useEndpoint, usePermission, useTranslation } from '@rocket.chat/ui-cont import { useQuery } from '@tanstack/react-query'; import React from 'react'; -import Page from '../../../components/Page'; +import { Page, PageHeader, PageScrollableContentWithShadow } from '../../../components/Page'; import PageSkeleton from '../../../components/PageSkeleton'; import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; import WebhooksPage from './WebhooksPage'; @@ -38,10 +38,10 @@ const WebhooksPageContainer = () => { if (!data?.success || !data?.settings || isError) { return ( - - + + {t('Error')} - + ); } diff --git a/apps/meteor/ee/client/omnichannel/cannedResponses/CannedResponseEdit.tsx b/apps/meteor/ee/client/omnichannel/cannedResponses/CannedResponseEdit.tsx index 35fcee64cf137..0cdbcae396fc5 100644 --- a/apps/meteor/ee/client/omnichannel/cannedResponses/CannedResponseEdit.tsx +++ b/apps/meteor/ee/client/omnichannel/cannedResponses/CannedResponseEdit.tsx @@ -6,7 +6,7 @@ import { useQueryClient } from '@tanstack/react-query'; import React, { memo, useCallback } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; -import Page from '../../../../client/components/Page'; +import { Page, PageHeader, PageScrollableContentWithShadow, PageFooter } from '../../../../client/components/Page'; import CannedResponseForm from './components/cannedResponseForm'; import { useRemoveCannedResponse } from './useRemoveCannedResponse'; @@ -66,7 +66,7 @@ const CannedResponseEdit = ({ cannedResponseData }: CannedResponseEditProps) => return ( - + )} - - + + - - + + - + ); }; diff --git a/apps/meteor/ee/client/omnichannel/cannedResponses/CannedResponsesPage.tsx b/apps/meteor/ee/client/omnichannel/cannedResponses/CannedResponsesPage.tsx index 90556095cfbfa..9156cd7d678fc 100644 --- a/apps/meteor/ee/client/omnichannel/cannedResponses/CannedResponsesPage.tsx +++ b/apps/meteor/ee/client/omnichannel/cannedResponses/CannedResponsesPage.tsx @@ -2,7 +2,7 @@ import { Button, ButtonGroup } from '@rocket.chat/fuselage'; import { useRouteParameter, useRouter, useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; -import Page from '../../../../client/components/Page'; +import { Page, PageHeader, PageContent } from '../../../../client/components/Page'; import CannedResponseEdit from './CannedResponseEdit'; import CannedResponseEditWithData from './CannedResponseEditWithData'; import CannedResponsesTable from './CannedResponsesTable'; @@ -24,14 +24,14 @@ const CannedResponsesPage = () => { return ( - + - - + + - + ); }; diff --git a/apps/meteor/ee/client/omnichannel/monitors/MonitorsPage.tsx b/apps/meteor/ee/client/omnichannel/monitors/MonitorsPage.tsx index 6962338c24fd6..4574f74bb2b71 100644 --- a/apps/meteor/ee/client/omnichannel/monitors/MonitorsPage.tsx +++ b/apps/meteor/ee/client/omnichannel/monitors/MonitorsPage.tsx @@ -1,7 +1,7 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; -import Page from '../../../../client/components/Page'; +import { Page, PageHeader, PageContent } from '../../../../client/components/Page'; import MonitorsTable from './MonitorsTable'; const MonitorsPage = () => { @@ -10,10 +10,10 @@ const MonitorsPage = () => { return ( - - + + - + ); diff --git a/apps/meteor/ee/client/omnichannel/priorities/PrioritiesPage.tsx b/apps/meteor/ee/client/omnichannel/priorities/PrioritiesPage.tsx index 77ff92b561c97..1dd782b782bea 100644 --- a/apps/meteor/ee/client/omnichannel/priorities/PrioritiesPage.tsx +++ b/apps/meteor/ee/client/omnichannel/priorities/PrioritiesPage.tsx @@ -5,7 +5,7 @@ import { useQueryClient } from '@tanstack/react-query'; import type { ReactElement } from 'react'; import React, { useMemo, useState } from 'react'; -import Page from '../../../../client/components/Page'; +import { Page, PageHeader, PageContent } from '../../../../client/components/Page'; import { useOmnichannelPriorities } from '../hooks/useOmnichannelPriorities'; import { PrioritiesResetModal } from './PrioritiesResetModal'; import { PrioritiesTable } from './PrioritiesTable'; @@ -75,16 +75,16 @@ export const PrioritiesPage = ({ priorityId, context }: PrioritiesPageProps): Re return ( - + - - + + - + {context === 'edit' && ( diff --git a/apps/meteor/ee/client/omnichannel/reports/ReportsPage.tsx b/apps/meteor/ee/client/omnichannel/reports/ReportsPage.tsx index 147dff65ad974..399a4772baf53 100644 --- a/apps/meteor/ee/client/omnichannel/reports/ReportsPage.tsx +++ b/apps/meteor/ee/client/omnichannel/reports/ReportsPage.tsx @@ -2,7 +2,7 @@ import { Box } from '@rocket.chat/fuselage'; import { usePermission, useTranslation } from '@rocket.chat/ui-contexts'; import React from 'react'; -import Page from '../../../../client/components/Page'; +import { Page, PageHeader, PageScrollableContentWithShadow } from '../../../../client/components/Page'; import NotAuthorizedPage from '../../../../client/views/notAuthorized/NotAuthorizedPage'; import { useHasLicenseModule } from '../../hooks/useHasLicenseModule'; import { ResizeObserver } from './components/ResizeObserver'; @@ -20,11 +20,11 @@ const ReportsPage = () => { return ( - + {t('Omnichannel_Reports_Summary')} - + @@ -38,7 +38,7 @@ const ReportsPage = () => { - + ); }; diff --git a/apps/meteor/ee/client/omnichannel/slaPolicies/SlaPage.tsx b/apps/meteor/ee/client/omnichannel/slaPolicies/SlaPage.tsx index 55b4d93581c33..3ef38c5f59ebd 100644 --- a/apps/meteor/ee/client/omnichannel/slaPolicies/SlaPage.tsx +++ b/apps/meteor/ee/client/omnichannel/slaPolicies/SlaPage.tsx @@ -4,7 +4,7 @@ import { useRouteParameter, useRoute, useTranslation } from '@rocket.chat/ui-con import React, { useRef, useCallback } from 'react'; import { Contextualbar, ContextualbarTitle, ContextualbarHeader, ContextualbarClose } from '../../../../client/components/Contextualbar'; -import Page from '../../../../client/components/Page'; +import { Page, PageHeader, PageContent } from '../../../../client/components/Page'; import SlaEditWithData from './SlaEditWithData'; import SlaNew from './SlaNew'; import SlaTable from './SlaTable'; @@ -34,14 +34,14 @@ const SlaPage = () => { return ( - + - - + + - + {context && ( diff --git a/apps/meteor/ee/client/omnichannel/tags/TagsPage.tsx b/apps/meteor/ee/client/omnichannel/tags/TagsPage.tsx index c4af0a6299402..9f35789dda74c 100644 --- a/apps/meteor/ee/client/omnichannel/tags/TagsPage.tsx +++ b/apps/meteor/ee/client/omnichannel/tags/TagsPage.tsx @@ -2,7 +2,7 @@ import { Button, ButtonGroup } from '@rocket.chat/fuselage'; import { useRouter, useTranslation, useRouteParameter } from '@rocket.chat/ui-contexts'; import React from 'react'; -import Page from '../../../../client/components/Page'; +import { Page, PageHeader, PageContent } from '../../../../client/components/Page'; import TagEdit from './TagEdit'; import TagEditWithData from './TagEditWithData'; import TagsTable from './TagsTable'; @@ -16,14 +16,14 @@ const TagsPage = () => { return ( - + - - + + - + {context === 'edit' && id && } {context === 'new' && } diff --git a/apps/meteor/ee/client/omnichannel/units/UnitsPage.tsx b/apps/meteor/ee/client/omnichannel/units/UnitsPage.tsx index 10f1ee1c77536..9e5faae66cd2a 100644 --- a/apps/meteor/ee/client/omnichannel/units/UnitsPage.tsx +++ b/apps/meteor/ee/client/omnichannel/units/UnitsPage.tsx @@ -2,7 +2,7 @@ import { Button, ButtonGroup } from '@rocket.chat/fuselage'; import { useTranslation, useRouteParameter, useRouter } from '@rocket.chat/ui-contexts'; import React from 'react'; -import Page from '../../../../client/components/Page'; +import { Page, PageHeader, PageContent } from '../../../../client/components/Page'; import UnitEdit from './UnitEdit'; import UnitEditWithData from './UnitEditWithData'; import UnitsTable from './UnitsTable'; @@ -17,16 +17,16 @@ const UnitsPage = () => { return ( - + - - + + - + {context === 'edit' && id && } {context === 'new' && } diff --git a/apps/meteor/ee/client/views/account/deviceManagement/DeviceManagementAccountPage.tsx b/apps/meteor/ee/client/views/account/deviceManagement/DeviceManagementAccountPage.tsx index 21628845c1859..13722be69c694 100644 --- a/apps/meteor/ee/client/views/account/deviceManagement/DeviceManagementAccountPage.tsx +++ b/apps/meteor/ee/client/views/account/deviceManagement/DeviceManagementAccountPage.tsx @@ -2,7 +2,7 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React from 'react'; -import Page from '../../../../../client/components/Page'; +import { Page, PageHeader, PageContent } from '../../../../../client/components/Page'; import DeviceManagementAccountTable from './DeviceManagementAccountTable'; const DeviceManagementAccountPage = (): ReactElement => { @@ -10,10 +10,10 @@ const DeviceManagementAccountPage = (): ReactElement => { return ( - - + + - + ); }; diff --git a/apps/meteor/ee/client/views/admin/deviceManagement/DeviceManagementAdminPage.tsx b/apps/meteor/ee/client/views/admin/deviceManagement/DeviceManagementAdminPage.tsx index fc6b7059dd58e..9e9d11ddcde68 100644 --- a/apps/meteor/ee/client/views/admin/deviceManagement/DeviceManagementAdminPage.tsx +++ b/apps/meteor/ee/client/views/admin/deviceManagement/DeviceManagementAdminPage.tsx @@ -2,7 +2,7 @@ import { useRouteParameter, useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useRef } from 'react'; -import Page from '../../../../../client/components/Page'; +import { Page, PageHeader, PageContent } from '../../../../../client/components/Page'; import DeviceManagementAdminTable from './DeviceManagementAdminTable'; import DeviceManagementInfo from './DeviceManagementInfo'; @@ -16,10 +16,10 @@ const DeviceManagementAdminPage = (): ReactElement => { return ( - - + + - + {context === 'info' && deviceId && } diff --git a/apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardPage.tsx b/apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardPage.tsx index b138ca9a92cda..711798dc49072 100644 --- a/apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardPage.tsx +++ b/apps/meteor/ee/client/views/admin/engagementDashboard/EngagementDashboardPage.tsx @@ -3,7 +3,7 @@ import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ReactElement } from 'react'; import React, { useCallback, useMemo, useState } from 'react'; -import Page from '../../../../../client/components/Page'; +import { Page, PageHeader, PageScrollableContent } from '../../../../../client/components/Page'; import ChannelsTab from './channels/ChannelsTab'; import MessagesTab from './messages/MessagesTab'; import UsersTab from './users/UsersTab'; @@ -34,9 +34,9 @@ const EngagementDashboardPage = ({ tab = 'users', onSelectTab }: EngagementDashb return ( - +