diff --git a/.storybook/.babelrc b/.storybook/.babelrc index 826e22a858c5..97ab12b7f950 100644 --- a/.storybook/.babelrc +++ b/.storybook/.babelrc @@ -13,6 +13,7 @@ "@babel/preset-flow" ], "plugins": [ - "@babel/plugin-proposal-class-properties" + "@babel/plugin-proposal-class-properties", + "@babel/plugin-proposal-optional-chaining" ] } diff --git a/.storybook/config.js b/.storybook/config.js index 420f7df4a6dc..c7fb95b21240 100644 --- a/.storybook/config.js +++ b/.storybook/config.js @@ -1,16 +1,12 @@ import { withKnobs } from '@storybook/addon-knobs'; -import { MINIMAL_VIEWPORTS, INITIAL_VIEWPORTS } from '@storybook/addon-viewport/dist/defaults'; +import { MINIMAL_VIEWPORTS } from '@storybook/addon-viewport/dist/defaults'; import { addDecorator, addParameters, configure } from '@storybook/react'; import { rocketChatDecorator } from './mocks/decorators'; addParameters({ viewport: { - viewports: { - ...MINIMAL_VIEWPORTS, - ...INITIAL_VIEWPORTS, - }, - defaultViewport: 'responsive', + viewports: MINIMAL_VIEWPORTS, }, }); diff --git a/app/apps/server/bridges/rooms.js b/app/apps/server/bridges/rooms.js index d8bc4e76d4ac..a91d8b2105ad 100644 --- a/app/apps/server/bridges/rooms.js +++ b/app/apps/server/bridges/rooms.js @@ -1,8 +1,8 @@ -import { Meteor } from 'meteor/meteor'; import { RoomType } from '@rocket.chat/apps-engine/definition/rooms'; +import { Meteor } from 'meteor/meteor'; -import { Rooms, Subscriptions, Users } from '../../../models/server'; import { addUserToRoom } from '../../../lib/server/functions/addUserToRoom'; +import { Rooms, Subscriptions, Users } from '../../../models/server'; export class AppRoomBridge { constructor(orch) { @@ -120,4 +120,35 @@ export class AppRoomBridge { addUserToRoom(rm._id, member); } } + + async createDiscussion(room, parentMessage = null, reply = '', members = [], appId) { + this.orch.debugLog(`The App ${ appId } is creating a new discussion.`, room); + + const rcRoom = this.orch.getConverters().get('rooms').convertAppRoom(room); + + let rcMessage; + if (parentMessage) { + rcMessage = this.orch.getConverters().get('messages').convertAppMessage(parentMessage); + } + + if (!rcRoom.prid || !Rooms.findOneById(rcRoom.prid)) { + throw new Error('There must be a parent room to create a discussion.'); + } + + const discussion = { + prid: rcRoom.prid, + t_name: rcRoom.fname, + pmid: rcMessage ? rcMessage._id : undefined, + reply: reply && reply.trim() !== '' ? reply : undefined, + users: members.length > 0 ? members : [], + }; + + let rid; + Meteor.runAsUser(room.creator.id, () => { + const info = Meteor.call('createDiscussion', discussion); + rid = info.rid; + }); + + return rid; + } } diff --git a/app/apps/server/converters/rooms.js b/app/apps/server/converters/rooms.js index a511f471d256..8c8aedbe1448 100644 --- a/app/apps/server/converters/rooms.js +++ b/app/apps/server/converters/rooms.js @@ -91,6 +91,7 @@ export class AppRoomsConverter { closedAt: room.closedAt, lm: room.lastModifiedAt, customFields: room.customFields, + prid: typeof room.parentRoom === 'undefined' ? undefined : room.parentRoom.id, }; return Object.assign(newRoom, room._unmappedProperties_); @@ -195,7 +196,17 @@ export class AppRoomsConverter { return this.orch.getConverters().get('users').convertById(responseBy._id); }, + parentRoom: (room) => { + const { prid } = room; + if (!prid) { + return undefined; + } + + delete room.prid; + + return this.orch.getConverters().get('rooms').convertById(prid); + }, }; return transformMappedData(room, map); diff --git a/app/importer/client/admin/adminImport.html b/app/importer/client/admin/adminImport.html deleted file mode 100644 index 87fc49ab06d6..000000000000 --- a/app/importer/client/admin/adminImport.html +++ /dev/null @@ -1,55 +0,0 @@ - diff --git a/app/importer/client/admin/adminImport.js b/app/importer/client/admin/adminImport.js deleted file mode 100644 index ddbf8fc0f8d6..000000000000 --- a/app/importer/client/admin/adminImport.js +++ /dev/null @@ -1,136 +0,0 @@ -import { Tracker } from 'meteor/tracker'; -import { FlowRouter } from 'meteor/kadira:flow-router'; -import { Template } from 'meteor/templating'; -import { ReactiveVar } from 'meteor/reactive-var'; -import toastr from 'toastr'; - -import { t, APIClient } from '../../../utils'; -import { SideNav } from '../../../ui-utils/client'; -import { ImportWaitingStates, ImportFileReadyStates, ImportPreparingStartedStates, ImportingStartedStates, ProgressStep } from '../../lib/ImporterProgressStep'; - -import './adminImport.html'; -import './importOperationSummary.js'; - -Template.adminImport.helpers({ - isPreparing() { - return Template.instance().preparing.get(); - }, - history() { - return Template.instance().history.get(); - }, - - operation() { - return Template.instance().operation.get(); - }, - - isNotCurrentOperation() { - const operation = Template.instance().operation.get(); - if (!operation) { - return true; - } - - return operation._id !== this._id || !operation.valid; - }, - - canShowCurrentOperation() { - const operation = Template.instance().operation.get(); - return operation && operation.valid; - }, - - canContinueOperation() { - const operation = Template.instance().operation.get(); - if (!operation || !operation.valid) { - return false; - } - - const possibleStatus = [ProgressStep.USER_SELECTION].concat(ImportWaitingStates).concat(ImportFileReadyStates).concat(ImportPreparingStartedStates); - return possibleStatus.includes(operation.status); - }, - - canCheckOperationProgress() { - const operation = Template.instance().operation.get(); - if (!operation || !operation.valid) { - return false; - } - - return ImportingStartedStates.includes(operation.status); - }, - - anySuccessfulSlackImports() { - const history = Template.instance().history.get(); - if (!history) { - return false; - } - - for (const op of history) { - if (op.importerKey === 'slack' && op.status === ProgressStep.DONE) { - return true; - } - } - - return false; - }, -}); - -Template.adminImport.events({ - 'click .new-import-btn'() { - FlowRouter.go('/admin/import/new'); - }, - 'click .download-slack-files-btn'(event, template) { - template.preparing.set(true); - APIClient.post('v1/downloadPendingFiles').then((data) => { - template.preparing.set(false); - if (data.count) { - toastr.success(t('File_Downloads_Started')); - FlowRouter.go('/admin/import/progress'); - } else { - toastr.success(t('No_files_left_to_download')); - } - }).catch((error) => { - template.preparing.set(false); - if (error) { - console.error(error); - toastr.error(t('Failed_To_Download_Files')); - } - }); - }, - 'click .prepare-btn'() { - FlowRouter.go('/admin/import/prepare'); - }, - 'click .progress-btn'() { - FlowRouter.go('/admin/import/progress'); - }, -}); - -Template.adminImport.onCreated(function() { - const instance = this; - this.preparing = new ReactiveVar(true); - this.history = new ReactiveVar([]); - this.operation = new ReactiveVar(false); - - APIClient.get('v1/getCurrentImportOperation').then((data) => { - instance.operation.set(data.operation); - - APIClient.get('v1/getLatestImportOperations').then((data) => { - instance.history.set(data); - instance.preparing.set(false); - }).catch((error) => { - if (error) { - toastr.error(t('Failed_To_Load_Import_History')); - instance.preparing.set(false); - } - }); - }).catch((error) => { - if (error) { - toastr.error(t('Failed_To_Load_Import_Operation')); - instance.preparing.set(false); - } - }); -}); - -Template.adminImport.onRendered(() => { - Tracker.afterFlush(() => { - SideNav.setFlex('adminFlex'); - SideNav.openFlex(); - }); -}); diff --git a/app/importer/client/admin/adminImportNew.html b/app/importer/client/admin/adminImportNew.html deleted file mode 100644 index 17e9230b5cb2..000000000000 --- a/app/importer/client/admin/adminImportNew.html +++ /dev/null @@ -1,83 +0,0 @@ - diff --git a/app/importer/client/admin/adminImportNew.js b/app/importer/client/admin/adminImportNew.js deleted file mode 100644 index 2d643312dc1c..000000000000 --- a/app/importer/client/admin/adminImportNew.js +++ /dev/null @@ -1,158 +0,0 @@ -import { Tracker } from 'meteor/tracker'; -import { FlowRouter } from 'meteor/kadira:flow-router'; -import { Template } from 'meteor/templating'; -import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import { ReactiveVar } from 'meteor/reactive-var'; -import toastr from 'toastr'; - -import { t, APIClient } from '../../../utils'; -import { SideNav } from '../../../ui-utils/client'; -import { settings } from '../../../settings'; -import { showImporterException } from '../functions/showImporterException'; - -import { Importers } from '..'; - -import './adminImportNew.html'; - -Template.adminImportNew.helpers({ - isPreparing() { - return Template.instance().preparing.get(); - }, - importers() { - return Importers.getAll(); - }, - pageTitle() { - const importerKey = Template.instance().importType.get(); - if (!importerKey) { - return t('Import_New_File'); - } - - const importer = Importers.get(importerKey); - if (!importer) { - return t('Import_New_File'); - } - - return TAPi18n.__('Importer_From_Description', { from: t(importer.name) }); - }, - importType() { - return Template.instance().importType.get(); - }, - fileType() { - return Template.instance().fileType.get(); - }, - isImporterSelected() { - return Template.instance().importType.get(); - }, - isFileTypeSelected() { - return Template.instance().fileType.get(); - }, - isUpload() { - return Template.instance().fileType.get() === 'upload'; - }, - isPublicURL() { - return Template.instance().fileType.get() === 'url'; - }, - isServerFile() { - return Template.instance().fileType.get() === 'path'; - }, - fileSizeLimitMessage() { - const maxFileSize = settings.get('FileUpload_MaxFileSize'); - let message; - - if (maxFileSize > 0) { - const sizeInKb = maxFileSize / 1024; - const sizeInMb = sizeInKb / 1024; - - let fileSizeMessage; - if (sizeInMb > 0) { - fileSizeMessage = TAPi18n.__('FileSize_MB', { fileSize: sizeInMb.toFixed(2) }); - } else if (sizeInKb > 0) { - fileSizeMessage = TAPi18n.__('FileSize_KB', { fileSize: sizeInKb.toFixed(2) }); - } else { - fileSizeMessage = TAPi18n.__('FileSize_Bytes', { fileSize: maxFileSize.toFixed(0) }); - } - - message = TAPi18n.__('Importer_Upload_FileSize_Message', { maxFileSize: fileSizeMessage }); - } else { - message = TAPi18n.__('Importer_Upload_Unlimited_FileSize'); - } - - return message; - }, -}); - -Template.adminImportNew.events({ - 'change .file-type'(event, template) { - template.fileType.set($('select[name=file-type]').val()); - }, - 'change .import-type'(event, template) { - template.importType.set($('select[name=import-type]').val()); - }, - - 'change .import-file-input'(event, template) { - const importType = template.importType.get(); - - const e = event.originalEvent || event; - let { files } = e.target; - if (!files || (files.length === 0)) { - files = (e.dataTransfer != null ? e.dataTransfer.files : undefined) || []; - } - - Array.from(files).forEach((file) => { - template.preparing.set(true); - - const reader = new FileReader(); - - reader.readAsDataURL(file); - reader.onloadend = () => { - APIClient.post('v1/uploadImportFile', { - binaryContent: reader.result.split(';base64,')[1], - contentType: file.type, - fileName: file.name, - importerKey: importType, - }).then(() => { - toastr.success(t('File_uploaded_successfully')); - FlowRouter.go('/admin/import/prepare'); - }).catch((error) => { - if (error) { - showImporterException(error); - template.preparing.set(false); - } - }); - }; - }); - }, - - 'click .import-btn'(event, template) { - const importType = template.importType.get(); - const fileUrl = $('.import-file-url').val(); - - template.preparing.set(true); - - APIClient.post('v1/downloadPublicImportFile', { - fileUrl, - importerKey: importType, - }).then(() => { - toastr.success(t('Import_requested_successfully')); - FlowRouter.go('/admin/import/prepare'); - }).catch((error) => { - if (error) { - showImporterException(error); - template.preparing.set(false); - } - }); - }, -}); - -Template.adminImportNew.onCreated(function() { - this.preparing = new ReactiveVar(false); - this.importType = new ReactiveVar(''); - this.fileType = new ReactiveVar('upload'); -}); - -Template.adminImportNew.onRendered(() => { - Tracker.afterFlush(() => { - SideNav.setFlex('adminFlex'); - SideNav.openFlex(); - }); -}); diff --git a/app/importer/client/admin/adminImportPrepare.html b/app/importer/client/admin/adminImportPrepare.html deleted file mode 100644 index ccb7c60d9401..000000000000 --- a/app/importer/client/admin/adminImportPrepare.html +++ /dev/null @@ -1,81 +0,0 @@ - diff --git a/app/importer/client/admin/adminImportPrepare.js b/app/importer/client/admin/adminImportPrepare.js deleted file mode 100644 index a91e8ea9d55b..000000000000 --- a/app/importer/client/admin/adminImportPrepare.js +++ /dev/null @@ -1,231 +0,0 @@ -import { Tracker } from 'meteor/tracker'; -import { FlowRouter } from 'meteor/kadira:flow-router'; -import { Template } from 'meteor/templating'; -import { ReactiveVar } from 'meteor/reactive-var'; -import toastr from 'toastr'; - -import { t, APIClient } from '../../../utils'; -import { SideNav } from '../../../ui-utils/client'; -import { ProgressStep, ImportWaitingStates, ImportFileReadyStates, ImportPreparingStartedStates, ImportingStartedStates, ImportingErrorStates } from '../../lib/ImporterProgressStep'; -import { showImporterException } from '../functions/showImporterException'; - -import { ImporterWebsocketReceiver } from '..'; - -import './adminImportPrepare.html'; - -Template.adminImportPrepare.helpers({ - isPreparing() { - return Template.instance().preparing.get(); - }, - hasProgressRate() { - return Template.instance().progressRate.get() !== false; - }, - progressRate() { - const rate = Template.instance().progressRate.get(); - if (rate) { - return `${ rate }%`; - } - - return ''; - }, - pageTitle() { - return t('Importing_Data'); - }, - users() { - return Template.instance().users.get(); - }, - channels() { - return Template.instance().channels.get(); - }, - message_count() { - return Template.instance().message_count.get(); - }, -}); - -Template.adminImportPrepare.events({ - 'click .button.start'(event, template) { - const btn = this; - $(btn).prop('disabled', true); - for (const user of Array.from(template.users.get())) { - user.do_import = $(`[name='${ user.user_id }']`).is(':checked'); - } - - for (const channel of Array.from(template.channels.get())) { - channel.do_import = $(`[name='${ channel.channel_id }']`).is(':checked'); - } - - APIClient.post('v1/startImport', { input: { users: template.users.get(), channels: template.channels.get() } }).then(() => { - template.users.set([]); - template.channels.set([]); - return FlowRouter.go('/admin/import/progress'); - }).catch((error) => { - if (error) { - showImporterException(error, 'Failed_To_Start_Import'); - return FlowRouter.go('/admin/import'); - } - }); - }, - - 'click .button.uncheck-deleted-users'(event, template) { - Array.from(template.users.get()).filter((user) => user.is_deleted).map((user) => { - const box = $(`[name=${ user.user_id }]`); - return box && box.length && box[0].checked && box.click(); - }); - }, - - 'click .button.check-all-users'(event, template) { - Array.from(template.users.get()).forEach((user) => { - const box = $(`[name=${ user.user_id }]`); - return box && box.length && !box[0].checked && box.click(); - }); - }, - - 'click .button.uncheck-all-users'(event, template) { - Array.from(template.users.get()).forEach((user) => { - const box = $(`[name=${ user.user_id }]`); - return box && box.length && box[0].checked && box.click(); - }); - }, - - 'click .button.uncheck-archived-channels'(event, template) { - Array.from(template.channels.get()).filter((channel) => channel.is_archived).map((channel) => { - const box = $(`[name=${ channel.channel_id }]`); - return box && box.length && box[0].checked && box.click(); - }); - }, - - 'click .button.check-all-channels'(event, template) { - Array.from(template.channels.get()).forEach((channel) => { - const box = $(`[name=${ channel.channel_id }]`); - return box && box.length && !box[0].checked && box.click(); - }); - }, - - 'click .button.uncheck-all-channels'(event, template) { - Array.from(template.channels.get()).forEach((channel) => { - const box = $(`[name=${ channel.channel_id }]`); - return box && box.length && box[0].checked && box.click(); - }); - }, - -}); - -function getImportFileData(template) { - APIClient.get('v1/getImportFileData').then((data) => { - if (!data) { - console.warn('The importer is not set up correctly, as it did not return any data.'); - toastr.error(t('Importer_not_setup')); - return FlowRouter.go('/admin/import'); - } - - if (data.waiting) { - setTimeout(() => { - getImportFileData(template); - }, 1000); - return; - } - - if (data.step) { - console.warn('Invalid file, contains `data.step`.', data); - toastr.error(t('Failed_To_Load_Import_Data')); - return FlowRouter.go('/admin/import'); - } - - template.users.set(data.users); - template.channels.set(data.channels); - template.message_count.set(data.message_count); - template.preparing.set(false); - template.progressRate.set(false); - }).catch((error) => { - if (error) { - showImporterException(error, 'Failed_To_Load_Import_Data'); - return FlowRouter.go('/admin/import'); - } - }); -} - -function loadOperation(template) { - APIClient.get('v1/getCurrentImportOperation').then((data) => { - const { operation } = data; - - if (!operation.valid) { - return FlowRouter.go('/admin/import/new'); - } - - // If the import has already started, move to the progress screen - if (ImportingStartedStates.includes(operation.status)) { - return FlowRouter.go('/admin/import/progress'); - } - - // The getImportFileData method can handle it if the state is: - // 1) ready to select the users, - // 2) preparing - // 3) ready to be prepared - - if (operation.status === ProgressStep.USER_SELECTION || ImportPreparingStartedStates.includes(operation.status) || ImportFileReadyStates.includes(operation.status)) { - if (!template.callbackRegistered) { - ImporterWebsocketReceiver.registerCallback(template.progressUpdated); - template.callbackRegistered = true; - } - - getImportFileData(template); - return template.preparing.set(true); - } - - // We're still waiting for a file... This shouldn't take long - if (ImportWaitingStates.includes(operation.status)) { - setTimeout(() => { - loadOperation(template); - }, 1000); - - return template.preparing.set(true); - } - - if (ImportingErrorStates.includes(operation.status)) { - toastr.error(t('Import_Operation_Failed')); - return FlowRouter.go('/admin/import'); - } - - if (operation.status === ProgressStep.DONE) { - return FlowRouter.go('/admin/import'); - } - - toastr.error(t('Unknown_Import_State')); - return FlowRouter.go('/admin/import'); - }).catch((error) => { - if (error) { - toastr.error(t('Failed_To_Load_Import_Data')); - return FlowRouter.go('/admin/import'); - } - }); -} - -Template.adminImportPrepare.onCreated(function() { - this.preparing = new ReactiveVar(true); - this.progressRate = new ReactiveVar(false); - this.callbackRegistered = false; - this.users = new ReactiveVar([]); - this.channels = new ReactiveVar([]); - this.message_count = new ReactiveVar(0); - - this.progressUpdated = (progress) => { - if ('rate' in progress) { - const { rate } = progress; - this.progressRate.set(rate); - } - }; - - loadOperation(this); -}); - -Template.adminImportPrepare.onRendered(() => { - Tracker.afterFlush(() => { - SideNav.setFlex('adminFlex'); - SideNav.openFlex(); - }); -}); - -Template.adminImportPrepare.onDestroyed(function() { - this.callbackRegistered = false; - ImporterWebsocketReceiver.unregisterCallback(this.progressUpdated); -}); diff --git a/app/importer/client/admin/adminImportProgress.html b/app/importer/client/admin/adminImportProgress.html deleted file mode 100644 index 05337b38b150..000000000000 --- a/app/importer/client/admin/adminImportProgress.html +++ /dev/null @@ -1,8 +0,0 @@ - diff --git a/app/importer/client/admin/adminImportProgress.js b/app/importer/client/admin/adminImportProgress.js deleted file mode 100644 index cf59ab3c79cc..000000000000 --- a/app/importer/client/admin/adminImportProgress.js +++ /dev/null @@ -1,137 +0,0 @@ -import { ReactiveVar } from 'meteor/reactive-var'; -import { FlowRouter } from 'meteor/kadira:flow-router'; -import { Template } from 'meteor/templating'; -import toastr from 'toastr'; - -import { t, handleError, APIClient } from '../../../utils'; -import { ProgressStep, ImportingStartedStates } from '../../lib/ImporterProgressStep'; - -import { ImporterWebsocketReceiver } from '..'; - -import './adminImportProgress.html'; - -Template.adminImportProgress.helpers({ - step() { - return Template.instance().step.get(); - }, - completed() { - return Template.instance().completed.get(); - }, - total() { - return Template.instance().total.get(); - }, - progressRate() { - try { - const instance = Template.instance(); - const completed = instance.completed.get(); - const total = instance.total.get(); - - const rate = Math.floor(completed * 10000 / total) / 100; - - if (isNaN(rate)) { - return ''; - } - - return `${ rate }%`; - } catch { - return ''; - } - }, -}); - -Template.adminImportProgress.onCreated(function() { - const template = this; - this.operation = new ReactiveVar(false); - this.step = new ReactiveVar(t('Loading...')); - this.completed = new ReactiveVar(0); - this.total = new ReactiveVar(0); - - let importerKey = false; - - function _updateProgress(progress) { - switch (progress.step) { - case ProgressStep.DONE: - toastr.success(t(progress.step[0].toUpperCase() + progress.step.slice(1))); - return FlowRouter.go('/admin/import'); - case ProgressStep.ERROR: - case ProgressStep.CANCELLED: - toastr.error(t(progress.step[0].toUpperCase() + progress.step.slice(1))); - return FlowRouter.go('/admin/import'); - default: - template.step.set(t(progress.step[0].toUpperCase() + progress.step.slice(1))); - if (progress.count.completed) { - template.completed.set(progress.count.completed); - } - if (progress.count.total) { - template.total.set(progress.count.total); - } - break; - } - } - - this.progressUpdated = function _progressUpdated(progress) { - if (progress.key.toLowerCase() !== importerKey) { - return; - } - - _updateProgress(progress); - }; - - APIClient.get('v1/getCurrentImportOperation').then((data) => { - const { operation } = data; - - if (!operation.valid) { - return FlowRouter.go('/admin/import'); - } - - // If the import has not started, move to the prepare screen - if (!ImportingStartedStates.includes(operation.status)) { - return FlowRouter.go('/admin/import/prepare'); - } - - importerKey = operation.importerKey; - template.operation.set(operation); - if (operation.count) { - if (operation.count.total) { - template.total.set(operation.count.total); - } - if (operation.count.completed) { - template.completed.set(operation.count.completed); - } - } - - APIClient.get('v1/getImportProgress').then((progress) => { - if (!progress) { - toastr.warning(t('Importer_not_in_progress')); - return FlowRouter.go('/admin/import/prepare'); - } - - const whereTo = _updateProgress(progress); - - if (!whereTo) { - ImporterWebsocketReceiver.registerCallback(template.progressUpdated); - } - }).catch((error) => { - console.warn('Error on getting the import progress:', error); - - if (error) { - handleError(error); - } else { - toastr.error(t('Failed_To_Load_Import_Data')); - } - - return FlowRouter.go('/admin/import'); - }); - }).catch((error) => { - if (error) { - handleError(error); - } else { - toastr.error(t('Failed_To_Load_Import_Data')); - } - return FlowRouter.go('/admin/import'); - }); -}); - -Template.adminImportProgress.onDestroyed(function() { - ImporterWebsocketReceiver.unregisterCallback(this.progressUpdated); -}); diff --git a/app/importer/client/admin/importOperationSummary.js b/app/importer/client/admin/importOperationSummary.js deleted file mode 100644 index 0fb5293a044a..000000000000 --- a/app/importer/client/admin/importOperationSummary.js +++ /dev/null @@ -1,136 +0,0 @@ -import { Template } from 'meteor/templating'; - -import { t } from '../../../utils'; - -import './importOperationSummary.html'; - -Template.importOperationSummary.helpers({ - lastUpdated() { - if (!this._updatedAt) { - return ''; - } - - const date = new Date(this._updatedAt); - return date.toLocaleString(); - }, - - status() { - if (!this.status) { - return ''; - } - - return t(this.status.replace('importer_', 'importer_status_')); - }, - - fileName() { - const fileName = this.file; - if (!fileName) { - return ''; - } - - // If the userid is inside the filename, remove it and anything before it - const idx = fileName.indexOf(`_${ this.user }_`); - if (idx >= 0) { - return fileName.substring(idx + this.user.length + 2); - } - - return fileName; - }, - - hasCounters() { - return Boolean(this.count); - }, - - userCount() { - if (this.count && this.count.users) { - return this.count.users; - } - - return 0; - }, - - channelCount() { - if (this.count && this.count.channels) { - return this.count.channels; - } - - return 0; - }, - - messageCount() { - if (this.count && this.count.messages) { - return this.count.messages; - } - - return 0; - }, - - totalCount() { - if (this.count && this.count.total) { - return this.count.total; - } - - return 0; - }, - - hasErrors() { - if (!this.fileData) { - return false; - } - - if (this.fileData.users) { - for (const user of this.fileData.users) { - if (user.is_email_taken) { - return true; - } - if (user.error) { - return true; - } - } - } - - if (this.errors && this.errors.length > 0) { - return true; - } - - return false; - }, - - formatedError() { - if (!this.error) { - return ''; - } - - if (typeof this.error === 'string') { - return this.error; - } - - if (typeof this.error === 'object') { - if (this.error.message) { - return this.error.message; - } - if (this.error.error && typeof this.error.error === 'string') { - return this.error.error; - } - - try { - const json = JSON.stringify(this.error); - console.log(json); - return json; - } catch (e) { - return t('Error'); - } - } - - return this.error.toString(); - }, - - messageTime() { - if (!this.msg || !this.msg.ts) { - return ''; - } - - const date = new Date(this.msg.ts); - return date.toLocaleString(); - }, -}); diff --git a/app/importer/client/components/ImportHistoryPage.js b/app/importer/client/components/ImportHistoryPage.js new file mode 100644 index 000000000000..4788ebe89b3d --- /dev/null +++ b/app/importer/client/components/ImportHistoryPage.js @@ -0,0 +1,120 @@ +import { Button, ButtonGroup, Table } from '@rocket.chat/fuselage'; +import React, { useState, useEffect, useMemo } from 'react'; + +import { Page } from '../../../../client/components/basic/Page'; +import { useTranslation } from '../../../../client/contexts/TranslationContext'; +import { useToastMessageDispatch } from '../../../../client/contexts/ToastMessagesContext'; +import { useRoute } from '../../../../client/contexts/RouterContext'; +import { useEndpoint } from '../../../../client/contexts/ServerContext'; +import { ProgressStep } from '../../lib/ImporterProgressStep'; +import ImportOperationSummary from './ImportOperationSummary'; +import { useSafely } from '../../../../client/hooks/useSafely'; + +function ImportHistoryPage() { + const t = useTranslation(); + const dispatchToastMessage = useToastMessageDispatch(); + + const [isLoading, setLoading] = useSafely(useState(true)); + const [currentOperation, setCurrentOperation] = useSafely(useState()); + const [latestOperations, setLatestOperations] = useSafely(useState([])); + + const getCurrentImportOperation = useEndpoint('GET', 'getCurrentImportOperation'); + const getLatestImportOperations = useEndpoint('GET', 'getLatestImportOperations'); + const downloadPendingFiles = useEndpoint('POST', 'downloadPendingFiles'); + + const newImportRoute = useRoute('admin-import-new'); + const importProgressRoute = useRoute('admin-import-progress'); + + useEffect(() => { + const loadData = async () => { + setLoading(true); + + try { + const { operation } = await getCurrentImportOperation(); + setCurrentOperation(operation); + } catch (error) { + dispatchToastMessage({ type: 'error', message: t('Failed_To_Load_Import_Operation') }); + } + + try { + const operations = await getLatestImportOperations(); + setLatestOperations(operations); + } catch (error) { + dispatchToastMessage({ type: 'error', message: t('Failed_To_Load_Import_History') }); + } + + setLoading(false); + }; + + loadData(); + }, []); + + const hasAnySuccessfulSlackImport = useMemo(() => + latestOperations?.some(({ importerKey, status }) => importerKey === 'slack' && status === ProgressStep.DONE), [latestOperations]); + + const handleNewImportClick = () => { + newImportRoute.push(); + }; + + const handleDownloadPendingFilesClick = async () => { + try { + setLoading(true); + const { count } = await downloadPendingFiles(); + + if (count) { + dispatchToastMessage({ type: 'info', message: t('No_files_left_to_download') }); + setLoading(false); + return; + } + + dispatchToastMessage({ type: 'info', message: t('File_Downloads_Started') }); + importProgressRoute.push(); + } catch (error) { + console.error(error); + dispatchToastMessage({ type: 'error', message: t('Failed_To_Download_Files') }); + setLoading(false); + } + }; + + return + + + + {hasAnySuccessfulSlackImport + && } + + + + + + + {t('Import_Type')} + {t('Last_Updated')} + {t('Last_Status')} + {t('File')} + {t('Counters')} + + + {t('Users')} + {t('Channels')} + {t('Messages')} + {t('Total')} + + + + {isLoading + ? Array.from({ length: 20 }, (_, i) => ) + : <> + {currentOperation?.valid && } + {latestOperations + ?.filter(({ _id }) => currentOperation?._id !== _id || !currentOperation?.valid) + // Forcing valid=false as the current API only accept preparation/progress over currentOperation + ?.map((operation) => )} + } + +
+
+
; +} + +export default ImportHistoryPage; diff --git a/app/importer/client/components/ImportHistoryPage.stories.js b/app/importer/client/components/ImportHistoryPage.stories.js new file mode 100644 index 000000000000..dc5ba06b244a --- /dev/null +++ b/app/importer/client/components/ImportHistoryPage.stories.js @@ -0,0 +1,10 @@ +import React from 'react'; + +import ImportHistoryPage from './ImportHistoryPage'; + +export default { + title: 'admin/import/ImportHistoryPage', + component: ImportHistoryPage, +}; + +export const _default = () => ; diff --git a/app/importer/client/components/ImportOperationSummary.js b/app/importer/client/components/ImportOperationSummary.js new file mode 100644 index 000000000000..c1e8463ce12f --- /dev/null +++ b/app/importer/client/components/ImportOperationSummary.js @@ -0,0 +1,112 @@ +import { Skeleton, Table } from '@rocket.chat/fuselage'; +import React, { useMemo } from 'react'; + +import { useTranslation } from '../../../../client/contexts/TranslationContext'; +import { useRoute } from '../../../../client/contexts/RouterContext'; +import { useFormatDateAndTime } from '../../../ui/client/views/app/components/hooks'; +import { + ImportWaitingStates, + ImportFileReadyStates, + ImportPreparingStartedStates, + ImportingStartedStates, + ProgressStep, +} from '../../lib/ImporterProgressStep'; + +function ImportOperationSummary({ + type, + _updatedAt, + status, + file, + user, + count: { + users = 0, + channels = 0, + messages = 0, + total = 0, + } = { + users: null, + channels: null, + messages: null, + total: null, + }, + valid, +}) { + const t = useTranslation(); + const formatDateAndTime = useFormatDateAndTime(); + + const fileName = useMemo(() => { + if (!file) { + return ''; + } + + const fileName = file; + + const userPattern = `_${ user }_`; + const idx = fileName.indexOf(userPattern); + if (idx >= 0) { + return fileName.slice(idx + userPattern.length); + } + + return fileName; + }, [file, user]); + + const canContinue = useMemo(() => valid && [ + ProgressStep.USER_SELECTION, + ...ImportWaitingStates, + ...ImportFileReadyStates, + ...ImportPreparingStartedStates, + ].includes(status), [valid, status]); + + const canCheckProgress = useMemo(() => valid && ImportingStartedStates.includes(status), [valid, status]); + + const prepareImportRoute = useRoute('admin-import-prepare'); + const importProgressRoute = useRoute('admin-import-progress'); + + const handleClick = () => { + if (canContinue) { + prepareImportRoute.push(); + return; + } + + if (canCheckProgress) { + importProgressRoute.push(); + } + }; + + const hasAction = canContinue || canCheckProgress; + + const props = hasAction ? { + tabIndex: 0, + role: 'link', + action: true, + onClick: handleClick, + } : {}; + + return + {type} + {formatDateAndTime(_updatedAt)} + {status && t(status.replace('importer_', 'importer_status_'))} + {fileName} + {users} + {channels} + {messages} + {total} + ; +} + +function ImportOperationSummarySkeleton() { + return + + + + + + + + + ; +} + +ImportOperationSummary.Skeleton = ImportOperationSummarySkeleton; + +export default ImportOperationSummary; diff --git a/app/importer/client/components/ImportOperationSummary.stories.js b/app/importer/client/components/ImportOperationSummary.stories.js new file mode 100644 index 000000000000..ceab4c81dacd --- /dev/null +++ b/app/importer/client/components/ImportOperationSummary.stories.js @@ -0,0 +1,18 @@ +import { Table } from '@rocket.chat/fuselage'; +import React from 'react'; + +import ImportOperationSummary from './ImportOperationSummary'; + +export default { + title: 'admin/import/ImportOperationSummary', + component: ImportOperationSummary, + decorators: [(fn) => + + {fn()} + +
], +}; + +export const _default = () => ; + +export const skeleton = () => ; diff --git a/app/importer/client/components/ImportProgressPage.js b/app/importer/client/components/ImportProgressPage.js new file mode 100644 index 000000000000..ec8416cea6b9 --- /dev/null +++ b/app/importer/client/components/ImportProgressPage.js @@ -0,0 +1,138 @@ +import { Box, Margins, Throbber } from '@rocket.chat/fuselage'; +import React, { useEffect, useState, useMemo } from 'react'; +import s from 'underscore.string'; + +import { useTranslation } from '../../../../client/contexts/TranslationContext'; +import { ProgressStep, ImportingStartedStates } from '../../lib/ImporterProgressStep'; +import { useToastMessageDispatch } from '../../../../client/contexts/ToastMessagesContext'; +import { ImporterWebsocketReceiver } from '../ImporterWebsocketReceiver'; +import { useSafely } from '../../../../client/hooks/useSafely'; +import { useEndpoint } from '../../../../client/contexts/ServerContext'; +import { useRoute } from '../../../../client/contexts/RouterContext'; +import { Page } from '../../../../client/components/basic/Page'; + +function ImportProgressPage() { + const t = useTranslation(); + const dispatchToastMessage = useToastMessageDispatch(); + + const [importerKey, setImporterKey] = useSafely(useState(null)); + const [step, setStep] = useSafely(useState('Loading...')); + const [completed, setCompleted] = useSafely(useState(0)); + const [total, setTotal] = useSafely(useState(0)); + + const getCurrentImportOperation = useEndpoint('GET', 'getCurrentImportOperation'); + const getImportProgress = useEndpoint('GET', 'getImportProgress'); + + const importHistoryRoute = useRoute('admin-import'); + const prepareImportRoute = useRoute('admin-import-prepare'); + + useEffect(() => { + const loadCurrentOperation = async () => { + try { + const { operation } = await getCurrentImportOperation(); + + if (!operation.valid) { + importHistoryRoute.push(); + return; + } + + if (!ImportingStartedStates.includes(operation.status)) { + prepareImportRoute.push(); + return; + } + + setImporterKey(operation.importerKey); + setCompleted(operation.count.completed); + setTotal(operation.count.total); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error || t('Failed_To_Load_Import_Data') }); + importHistoryRoute.push(); + } + }; + + loadCurrentOperation(); + }, []); + + useEffect(() => { + if (!importerKey) { + return; + } + + const handleProgressUpdated = ({ key, step, count: { completed = 0, total = 0 } = {} }) => { + if (key.toLowerCase() !== importerKey) { + return; + } + + switch (step) { + case ProgressStep.DONE: + dispatchToastMessage({ type: 'success', message: t(step[0].toUpperCase() + step.slice(1)) }); + importHistoryRoute.push(); + return; + + case ProgressStep.ERROR: + case ProgressStep.CANCELLED: + dispatchToastMessage({ type: 'error', message: t(step[0].toUpperCase() + step.slice(1)) }); + importHistoryRoute.push(); + return; + + default: + setStep(step); + setCompleted(completed); + setTotal(total); + break; + } + }; + + const loadImportProgress = async () => { + try { + const progress = await getImportProgress(); + + if (!progress) { + dispatchToastMessage({ type: 'warning', message: t('Importer_not_in_progress') }); + prepareImportRoute.push(); + return; + } + + ImporterWebsocketReceiver.registerCallback(handleProgressUpdated); + handleProgressUpdated(progress); + } catch (error) { + dispatchToastMessage({ type: 'error', message: error || t('Failed_To_Load_Import_Data') }); + importHistoryRoute.push(); + } + }; + + loadImportProgress(); + + return () => { + ImporterWebsocketReceiver.unregisterCallback(handleProgressUpdated); + }; + }, [importerKey]); + + const progressRate = useMemo(() => { + if (total === 0) { + return null; + } + + return completed / total * 100; + }); + + return + + + + + + {t(step[0].toUpperCase() + step.slice(1))} + {progressRate + ? + + {completed}/{total} ({s.numberFormat(progressRate, 0) }%) + + : } + + + + ; +} + +export default ImportProgressPage; diff --git a/app/importer/client/components/ImportRoute.js b/app/importer/client/components/ImportRoute.js new file mode 100644 index 000000000000..d44d70efa403 --- /dev/null +++ b/app/importer/client/components/ImportRoute.js @@ -0,0 +1,36 @@ +import React from 'react'; + +import { usePermission } from '../../../../client/contexts/AuthorizationContext'; +import NotAuthorizedPage from '../../../ui-admin/client/components/NotAuthorizedPage'; +import ImportHistoryPage from './ImportHistoryPage'; +import NewImportPage from './NewImportPage'; +import PrepareImportPage from './PrepareImportPage'; +import ImportProgressPage from './ImportProgressPage'; + +function ImportHistoryRoute({ page }) { + const canRunImport = usePermission('run-import'); + + if (!canRunImport) { + return ; + } + + if (page === 'history') { + return ; + } + + if (page === 'new') { + return ; + } + + if (page === 'prepare') { + return ; + } + + if (page === 'progress') { + return ; + } + + return null; +} + +export default ImportHistoryRoute; diff --git a/app/importer/client/components/NewImportPage.js b/app/importer/client/components/NewImportPage.js new file mode 100644 index 000000000000..242a44c4bfe1 --- /dev/null +++ b/app/importer/client/components/NewImportPage.js @@ -0,0 +1,247 @@ +import { + Box, + Button, + ButtonGroup, + Callout, + Chip, + Field, + Icon, + Margins, + Select, + InputBox, + TextInput, + Throbber, + UrlInput, +} from '@rocket.chat/fuselage'; +import { useUniqueId } from '@rocket.chat/fuselage-hooks'; +import React, { useState, useMemo, useEffect } from 'react'; + +import { Page } from '../../../../client/components/basic/Page'; +import { useTranslation } from '../../../../client/contexts/TranslationContext'; +import { useSetting } from '../../../../client/contexts/SettingsContext'; +import { useToastMessageDispatch } from '../../../../client/contexts/ToastMessagesContext'; +import { useRoute, useRouteParameter } from '../../../../client/contexts/RouterContext'; +import { showImporterException } from '../functions/showImporterException'; +import { useEndpoint } from '../../../../client/contexts/ServerContext'; +import { Importers } from '../index'; +import { useSafely } from '../../../../client/hooks/useSafely'; +import { useFormatMemorySize } from '../../../ui/client/views/app/components/hooks'; + +function NewImportPage() { + const t = useTranslation(); + const dispatchToastMessage = useToastMessageDispatch(); + + const [isLoading, setLoading] = useSafely(useState(false)); + const [fileType, setFileType] = useSafely(useState('upload')); + const importerKey = useRouteParameter('importerKey'); + const importer = useMemo(() => Importers.get(importerKey), [importerKey]); + + const maxFileSize = useSetting('FileUpload_MaxFileSize'); + + const importHistoryRoute = useRoute('admin-import'); + const newImportRoute = useRoute('admin-import-new'); + const prepareImportRoute = useRoute('admin-import-prepare'); + + const uploadImportFile = useEndpoint('POST', 'uploadImportFile'); + const downloadPublicImportFile = useEndpoint('POST', 'downloadPublicImportFile'); + + useEffect(() => { + if (importerKey && !importer) { + newImportRoute.replace(); + } + }, [importerKey, !importer]); + + const formatMemorySize = useFormatMemorySize(); + + const handleBackToImportsButtonClick = () => { + importHistoryRoute.push(); + }; + + const handleImporterKeyChange = (importerKey) => { + newImportRoute.replace({ importerKey }); + }; + + const handleFileTypeChange = (fileType) => { + setFileType(fileType); + }; + + const [files, setFiles] = useState([]); + + const handleImportFileChange = async (event) => { + event = event.originalEvent || event; + + let { files } = event.target; + if (!files || (files.length === 0)) { + files = (event.dataTransfer != null ? event.dataTransfer.files : undefined) || []; + } + + setFiles(Array.from(files)); + }; + + const handleFileUploadChipClick = (file) => () => { + setFiles((files) => files.filter((_file) => _file !== file)); + }; + + const handleFileUploadImportButtonClick = async () => { + setLoading(true); + + try { + await Promise.all( + Array.from(files, (file) => new Promise((resolve) => { + const reader = new FileReader(); + reader.readAsDataURL(file); + reader.onloadend = async () => { + try { + await uploadImportFile({ + binaryContent: reader.result.split(';base64,')[1], + contentType: file.type, + fileName: file.name, + importerKey, + }); + dispatchToastMessage({ type: 'success', message: t('File_uploaded_successfully') }); + } catch (error) { + showImporterException(error); + } finally { + resolve(); + } + }; + reader.onerror = () => resolve(); + })), + ); + prepareImportRoute.push(); + } finally { + setLoading(false); + } + }; + + const [fileUrl, setFileUrl] = useSafely(useState('')); + + const handleFileUrlChange = (event) => { + setFileUrl(event.currentTarget.value); + }; + + const handleFileUrlImportButtonClick = async () => { + setLoading(true); + + try { + await downloadPublicImportFile({ importerKey, fileUrl }); + dispatchToastMessage({ type: 'success', message: t('Import_requested_successfully') }); + prepareImportRoute.push(); + } catch (error) { + showImporterException(error); + } finally { + setLoading(false); + } + }; + + const [filePath, setFilePath] = useSafely(useState('')); + + const handleFilePathChange = (event) => { + setFilePath(event.currentTarget.value); + }; + + const handleFilePathImportButtonClick = async () => { + setLoading(true); + + try { + await downloadPublicImportFile({ importerKey, fileUrl: filePath }); + dispatchToastMessage({ type: 'success', message: t('Import_requested_successfully') }); + prepareImportRoute.push(); + } catch (error) { + showImporterException(error); + } finally { + setLoading(false); + } + }; + + const importerKeySelectId = useUniqueId(); + const fileTypeSelectId = useUniqueId(); + const fileSourceInputId = useUniqueId(); + const handleImportButtonClick = (fileType === 'upload' && handleFileUploadImportButtonClick) + || (fileType === 'url' && handleFileUrlImportButtonClick) + || (fileType === 'path' && handleFilePathImportButtonClick); + + return + + + + {importer && } + + + + + + + {t('Import_Type')} + + + + } + {importer && <> + {fileType === 'upload' && <> + {maxFileSize > 0 + ? + {t('Importer_Upload_FileSize_Message', { maxFileSize: formatMemorySize(maxFileSize) })} + + : + {t('Importer_Upload_Unlimited_FileSize')} + } + + {t('Importer_Source_File')} + + + + {files?.length > 0 && + {files.map((file, i) => {file.name})} + } + + } + {fileType === 'url' && + {t('File_URL')} + + + + } + {fileType === 'path' && + {t('File_Path')} + + + + } + } + + + + ; +} + +export default NewImportPage; diff --git a/app/importer/client/components/NewImportPage.stories.js b/app/importer/client/components/NewImportPage.stories.js new file mode 100644 index 000000000000..029039865d07 --- /dev/null +++ b/app/importer/client/components/NewImportPage.stories.js @@ -0,0 +1,10 @@ +import React from 'react'; + +import NewImportPage from './NewImportPage'; + +export default { + title: 'admin/import/NewImportPage', + component: NewImportPage, +}; + +export const _default = () => ; diff --git a/app/importer/client/components/PrepareImportPage.js b/app/importer/client/components/PrepareImportPage.js new file mode 100644 index 000000000000..f8c45cd4ea4a --- /dev/null +++ b/app/importer/client/components/PrepareImportPage.js @@ -0,0 +1,307 @@ +import { + Badge, + Box, + Button, + ButtonGroup, + CheckBox, + Icon, + Margins, + Table, + Tag, + Throbber, +} from '@rocket.chat/fuselage'; +import React, { useEffect, useState, useMemo } from 'react'; +import s from 'underscore.string'; + +import { Page } from '../../../../client/components/basic/Page'; +import { useTranslation } from '../../../../client/contexts/TranslationContext'; +import { useToastMessageDispatch } from '../../../../client/contexts/ToastMessagesContext'; +import { + ProgressStep, + ImportWaitingStates, + ImportFileReadyStates, + ImportPreparingStartedStates, + ImportingStartedStates, + ImportingErrorStates, +} from '../../lib/ImporterProgressStep'; +import { ImporterWebsocketReceiver } from '../ImporterWebsocketReceiver'; +import { showImporterException } from '../functions/showImporterException'; +import { useRoute } from '../../../../client/contexts/RouterContext'; +import { useSafely } from '../../../../client/hooks/useSafely'; +import { useEndpoint } from '../../../../client/contexts/ServerContext'; + +const waitFor = (fn, predicate) => new Promise((resolve, reject) => { + const callPromise = () => { + fn().then((result) => { + if (predicate(result)) { + resolve(result); + return; + } + + setTimeout(callPromise, 1000); + }, reject); + }; + + callPromise(); +}); + +function PrepareImportPage() { + const t = useTranslation(); + const dispatchToastMessage = useToastMessageDispatch(); + + const [isPreparing, setPreparing] = useSafely(useState(true)); + const [progressRate, setProgressRate] = useSafely(useState(null)); + const [status, setStatus] = useSafely(useState(null)); + const [messageCount, setMessageCount] = useSafely(useState(0)); + const [users, setUsers] = useSafely(useState([])); + const [channels, setChannels] = useSafely(useState([])); + const [isImporting, setImporting] = useSafely(useState(false)); + + const usersCount = useMemo(() => users.filter(({ do_import }) => do_import).length, [users]); + const channelsCount = useMemo(() => channels.filter(({ do_import }) => do_import).length, [channels]); + + const importHistoryRoute = useRoute('admin-import'); + const newImportRoute = useRoute('admin-import-new'); + const importProgressRoute = useRoute('admin-import-progress'); + + const getImportFileData = useEndpoint('GET', 'getImportFileData'); + const getCurrentImportOperation = useEndpoint('GET', 'getCurrentImportOperation'); + const startImport = useEndpoint('POST', 'startImport'); + + useEffect(() => { + const handleProgressUpdated = ({ rate }) => { + setProgressRate(rate); + }; + + ImporterWebsocketReceiver.registerCallback(handleProgressUpdated); + + return () => { + ImporterWebsocketReceiver.unregisterCallback(handleProgressUpdated); + }; + }, []); + + useEffect(() => { + const loadImportFileData = async () => { + try { + const data = await waitFor(getImportFileData, (data) => data && !data.waiting); + + if (!data) { + dispatchToastMessage({ type: 'error', message: t('Importer_not_setup') }); + importHistoryRoute.push(); + return; + } + + if (data.step) { + dispatchToastMessage({ type: 'error', message: t('Failed_To_Load_Import_Data') }); + importHistoryRoute.push(); + return; + } + + setMessageCount(data.message_count); + setUsers(data.users.map((user) => ({ ...user, do_import: true }))); + setChannels(data.channels.map((channel) => ({ ...channel, do_import: true }))); + setPreparing(false); + setProgressRate(null); + } catch (error) { + showImporterException(error, 'Failed_To_Load_Import_Data'); + importHistoryRoute.push(); + } + }; + + const loadCurrentOperation = async () => { + try { + const { operation } = await waitFor(getCurrentImportOperation, ({ operation }) => + operation.valid && !ImportWaitingStates.includes(operation.status)); + + if (!operation.valid) { + newImportRoute.push(); + return; + } + + if (ImportingStartedStates.includes(operation.status)) { + importProgressRoute.push(); + return; + } + + if (operation.status === ProgressStep.USER_SELECTION + || ImportPreparingStartedStates.includes(operation.status) + || ImportFileReadyStates.includes(operation.status)) { + setStatus(operation.status); + loadImportFileData(); + return; + } + + if (ImportingErrorStates.includes(operation.status)) { + dispatchToastMessage({ type: 'error', message: t('Import_Operation_Failed') }); + importHistoryRoute.push(); + return; + } + + if (operation.status === ProgressStep.DONE) { + importHistoryRoute.push(); + return; + } + + dispatchToastMessage({ type: 'error', message: t('Unknown_Import_State') }); + importHistoryRoute.push(); + } catch (error) { + dispatchToastMessage({ type: 'error', message: t('Failed_To_Load_Import_Data') }); + importHistoryRoute.push(); + } + }; + + loadCurrentOperation(); + }, []); + + const handleBackToImportsButtonClick = () => { + importHistoryRoute.push(); + }; + + const handleStartButtonClick = async () => { + setImporting(true); + + try { + await startImport({ input: { users, channels } }); + importProgressRoute.push(); + } catch (error) { + showImporterException(error, 'Failed_To_Start_Import'); + importHistoryRoute.push(); + } + }; + + return + + + + + + + + + + + {isPreparing && <> + {progressRate + ? + + {s.numberFormat(progressRate, 0) }% + + : } + } + + {!isPreparing && <> + {status && t(status.replace('importer_', 'importer_status_'))} + + {t('Messages')} {messageCount} + + {t('Users')} {usersCount} + + {users.length && + + + + 0} + indeterminate={usersCount > 0 && usersCount !== users.length} + onChange={() => { + setUsers((users) => { + const hasCheckedDeletedUsers = users.some(({ is_deleted, do_import }) => is_deleted && do_import); + const isChecking = usersCount === 0; + + if (isChecking) { + return users.map((user) => ({ ...user, do_import: true })); + } + + if (hasCheckedDeletedUsers) { + return users.map((user) => (user.is_deleted ? { ...user, do_import: false } : user)); + } + + return users.map((user) => ({ ...user, do_import: false })); + }); + }} + /> + + {t('Username')} + {t('Email')} + + + + + {users.map((user) => + + { + const { checked } = event.currentTarget; + setUsers((users) => + users.map((_user) => (_user === user ? { ..._user, do_import: checked } : _user))); + }} + /> + + {user.username} + {user.email} + {user.is_deleted && {t('Deleted')}} + )} + +
} + + {t('Channels')} {channelsCount} + + {channels.length && + + + + 0} + indeterminate={channelsCount > 0 && channelsCount !== channels.length} + onChange={() => { + setChannels((channels) => { + const hasCheckedArchivedChannels = channels.some(({ is_archived, do_import }) => is_archived && do_import); + const isChecking = channelsCount === 0; + + if (isChecking) { + return channels.map((channel) => ({ ...channel, do_import: true })); + } + + if (hasCheckedArchivedChannels) { + return channels.map((channel) => (channel.is_deleted ? { ...channel, do_import: false } : channel)); + } + + return channels.map((channel) => ({ ...channel, do_import: false })); + }); + }} + /> + + {t('Name')} + + + + + {channels.map((channel) => + + { + const { checked } = event.currentTarget; + setChannels((channels) => + channels.map((_channel) => (_channel === channel ? { ..._channel, do_import: checked } : _channel))); + }} + /> + + {channel.name} + {channel.is_archived && {t('Importer_Archived')}} + )} + +
} + } +
+
+
+
; +} + +export default PrepareImportPage; diff --git a/app/importer/client/index.js b/app/importer/client/index.js index 0b5b15ef294b..54b80e9bea16 100644 --- a/app/importer/client/index.js +++ b/app/importer/client/index.js @@ -1,46 +1,6 @@ -import { BlazeLayout } from 'meteor/kadira:blaze-layout'; +import './routes'; -import { registerAdminRoute } from '../../ui-admin/client'; -import { Importers } from '../lib/Importers'; -import { ImporterInfo } from '../lib/ImporterInfo'; -import { ProgressStep } from '../lib/ImporterProgressStep'; -import { ImporterWebsocketReceiver } from './ImporterWebsocketReceiver'; - -registerAdminRoute('/import', { - name: 'admin-import', - async action() { - await import('./admin/adminImport'); - BlazeLayout.render('main', { center: 'adminImport' }); - }, -}); - -registerAdminRoute('/import/new', { - name: 'admin-import-new', - async action() { - await import('./admin/adminImportNew'); - BlazeLayout.render('main', { center: 'adminImportNew' }); - }, -}); - -registerAdminRoute('/import/prepare', { - name: 'admin-import-prepare', - async action() { - await import('./admin/adminImportPrepare'); - BlazeLayout.render('main', { center: 'adminImportPrepare' }); - }, -}); - -registerAdminRoute('/import/progress', { - name: 'admin-import-progress', - async action() { - await import('./admin/adminImportProgress'); - BlazeLayout.render('main', { center: 'adminImportProgress' }); - }, -}); - -export { - Importers, - ImporterInfo, - ImporterWebsocketReceiver, - ProgressStep, -}; +export { Importers } from '../lib/Importers'; +export { ImporterInfo } from '../lib/ImporterInfo'; +export { ProgressStep } from '../lib/ImporterProgressStep'; +export { ImporterWebsocketReceiver } from './ImporterWebsocketReceiver'; diff --git a/app/importer/client/routes.js b/app/importer/client/routes.js new file mode 100644 index 000000000000..875a6b2fa3d8 --- /dev/null +++ b/app/importer/client/routes.js @@ -0,0 +1,25 @@ +import { registerAdminRoute } from '../../ui-admin/client'; + +registerAdminRoute('/import', { + name: 'admin-import', + lazyRouteComponent: () => import('./components/ImportRoute'), + props: { page: 'history' }, +}); + +registerAdminRoute('/import/new/:importerKey?', { + name: 'admin-import-new', + lazyRouteComponent: () => import('./components/ImportRoute'), + props: { page: 'new' }, +}); + +registerAdminRoute('/import/prepare', { + name: 'admin-import-prepare', + lazyRouteComponent: () => import('./components/ImportRoute'), + props: { page: 'prepare' }, +}); + +registerAdminRoute('/import/progress', { + name: 'admin-import-progress', + lazyRouteComponent: () => import('./components/ImportRoute'), + props: { page: 'progress' }, +}); diff --git a/app/livechat/client/views/app/livechatCurrentChats.css b/app/livechat/client/views/app/livechatCurrentChats.css new file mode 100644 index 000000000000..7345337d5979 --- /dev/null +++ b/app/livechat/client/views/app/livechatCurrentChats.css @@ -0,0 +1,25 @@ +.rc-table-content { + & .js-sort { + cursor: pointer; + + &.is-sorting .table-fake-th .rc-icon { + opacity: 1; + } + } + + & .table-fake-th { + color: #444444; + + &:hover .rc-icon { + opacity: 1; + } + + & .rc-icon { + transition: opacity 0.3s; + + opacity: 0; + + font-size: 1rem; + } + } +} diff --git a/app/livechat/client/views/app/livechatCurrentChats.html b/app/livechat/client/views/app/livechatCurrentChats.html index c45ad535ab1b..df4720ba4234 100644 --- a/app/livechat/client/views/app/livechatCurrentChats.html +++ b/app/livechat/client/views/app/livechatCurrentChats.html @@ -127,24 +127,24 @@ {{#table fixed='true' onScroll=onTableScroll onResize=onTableResize onSort=onTableSort}} - -
{{_ "Name"}}
+ +
{{_ "Name"}}{{> icon icon=(sortIcon 'fname')}}
- -
{{_ "Department"}}
+ +
{{_ "Department"}}{{> icon icon=(sortIcon 'departmentId')}}
- -
{{_ "Served_By"}}
+ +
{{_ "Served_By"}}{{> icon icon=(sortIcon 'servedBy.username')}}
- -
{{_ "Started_At"}}
+ +
{{_ "Started_At"}}{{> icon icon=(sortIcon 'ts')}}
- -
{{_ "Last_Message_At"}}
+ +
{{_ "Last_Message_At"}}{{> icon icon=(sortIcon 'lm')}}
- -
{{_ "Status"}}
+ +
{{_ "Status"}}{{> icon icon=(sortIcon 'open')}}
 
diff --git a/app/livechat/client/views/app/livechatCurrentChats.js b/app/livechat/client/views/app/livechatCurrentChats.js index d5e017721360..b8493dfeab53 100644 --- a/app/livechat/client/views/app/livechatCurrentChats.js +++ b/app/livechat/client/views/app/livechatCurrentChats.js @@ -1,6 +1,7 @@ import 'moment-timezone'; import _ from 'underscore'; import moment from 'moment'; +import './livechatCurrentChats.css'; import { ReactiveVar } from 'meteor/reactive-var'; import { FlowRouter } from 'meteor/kadira:flow-router'; import { Template } from 'meteor/templating'; @@ -102,6 +103,25 @@ Template.livechatCurrentChats.helpers({ onSelectDepartments() { return Template.instance().onSelectDepartments; }, + onTableSort() { + const { sortDirection, sortBy } = Template.instance(); + return function(type) { + if (sortBy.get() === type) { + return sortDirection.set(sortDirection.get() === 'asc' ? 'desc' : 'asc'); + } + sortBy.set(type); + sortDirection.set('asc'); + }; + }, + sortBy(key) { + return Template.instance().sortBy.get() === key; + }, + sortIcon(key) { + const { sortDirection, sortBy } = Template.instance(); + return key === sortBy.get() && sortDirection.get() === 'asc' + ? 'sort-up' + : 'sort-down'; + }, }); Template.livechatCurrentChats.events({ @@ -371,6 +391,8 @@ Template.livechatCurrentChats.onCreated(async function() { this.customFields = new ReactiveVar([]); this.tagFilters = new ReactiveVar([]); this.selectedDepartments = new ReactiveVar([]); + this.sortBy = new ReactiveVar('ts'); + this.sortDirection = new ReactiveVar('desc'); this.onSelectDepartments = ({ item: department }) => { department.text = department.name; @@ -387,9 +409,9 @@ Template.livechatCurrentChats.onCreated(async function() { return acc; }, ''); - const mountUrlWithParams = (filter, offset) => { + const mountUrlWithParams = (filter, offset, sort) => { const { status, agents, department, from, to, tags, customFields, name: roomName } = filter; - let url = `livechat/rooms?count=${ ROOMS_COUNT }&offset=${ offset }&sort={"ts": -1}`; + let url = `livechat/rooms?count=${ ROOMS_COUNT }&offset=${ offset }&sort=${ JSON.stringify(sort) }`; const dateRange = {}; if (status) { url += `&open=${ status === 'opened' }`; @@ -452,9 +474,9 @@ Template.livechatCurrentChats.onCreated(async function() { this.filter.set({}); }; - this.loadRooms = async (filter, offset) => { + this.loadRooms = async (filter, offset, sort) => { this.isLoading.set(true); - const { rooms, total } = await APIClient.v1.get(mountUrlWithParams(filter, offset)); + const { rooms, total } = await APIClient.v1.get(mountUrlWithParams(filter, offset, sort)); this.total.set(total); if (offset === 0) { this.livechatRooms.set(rooms); @@ -475,7 +497,8 @@ Template.livechatCurrentChats.onCreated(async function() { this.autorun(async () => { const filter = this.filter.get(); const offset = this.offset.get(); - this.loadRooms(filter, offset); + const { sortDirection, sortBy } = Template.instance(); + this.loadRooms(filter, offset, { [sortBy.get()]: sortDirection.get() === 'asc' ? 1 : -1 }); }); Meteor.call('livechat:getCustomFields', (err, customFields) => { diff --git a/app/ui-admin/client/components/settings/NotAuthorizedPage.js b/app/ui-admin/client/components/NotAuthorizedPage.js similarity index 56% rename from app/ui-admin/client/components/settings/NotAuthorizedPage.js rename to app/ui-admin/client/components/NotAuthorizedPage.js index 3d42b11d7104..e7606e0fbdff 100644 --- a/app/ui-admin/client/components/settings/NotAuthorizedPage.js +++ b/app/ui-admin/client/components/NotAuthorizedPage.js @@ -1,10 +1,10 @@ import { Box } from '@rocket.chat/fuselage'; import React from 'react'; -import { useTranslation } from '../../../../../client/contexts/TranslationContext'; -import { Page } from '../../../../../client/components/basic/Page'; +import { useTranslation } from '../../../../client/contexts/TranslationContext'; +import { Page } from '../../../../client/components/basic/Page'; -export function NotAuthorizedPage() { +function NotAuthorizedPage() { const t = useTranslation(); return @@ -13,3 +13,5 @@ export function NotAuthorizedPage() { ; } + +export default NotAuthorizedPage; diff --git a/app/ui-admin/client/components/settings/NotAuthorizedPage.stories.js b/app/ui-admin/client/components/NotAuthorizedPage.stories.js similarity index 57% rename from app/ui-admin/client/components/settings/NotAuthorizedPage.stories.js rename to app/ui-admin/client/components/NotAuthorizedPage.stories.js index df29af858e14..02cc7071cdd3 100644 --- a/app/ui-admin/client/components/settings/NotAuthorizedPage.stories.js +++ b/app/ui-admin/client/components/NotAuthorizedPage.stories.js @@ -1,9 +1,9 @@ import React from 'react'; -import { NotAuthorizedPage } from './NotAuthorizedPage'; +import NotAuthorizedPage from './NotAuthorizedPage'; export default { - title: 'admin/settings/NotAuthorizedPage', + title: 'admin/NotAuthorizedPage', component: NotAuthorizedPage, }; diff --git a/app/ui-admin/client/components/info/RuntimeEnvironmentSection.js b/app/ui-admin/client/components/info/RuntimeEnvironmentSection.js index 4a416f57872a..11d9b53eba36 100644 --- a/app/ui-admin/client/components/info/RuntimeEnvironmentSection.js +++ b/app/ui-admin/client/components/info/RuntimeEnvironmentSection.js @@ -2,12 +2,14 @@ import { Skeleton, Subtitle } from '@rocket.chat/fuselage'; import React from 'react'; import { useTranslation } from '../../../../../client/contexts/TranslationContext'; +import { useFormatMemorySize } from '../../../../ui/client/views/app/components/hooks'; import { DescriptionList } from './DescriptionList'; -import { formatMemorySize, formatHumanReadableTime, formatCPULoad } from './formatters'; +import { formatHumanReadableTime, formatCPULoad } from './formatters'; export function RuntimeEnvironmentSection({ statistics, isLoading }) { const s = (fn) => (isLoading ? : fn()); const t = useTranslation(); + const formatMemorySize = useFormatMemorySize(); return <> {t('Runtime_Environment')} diff --git a/app/ui-admin/client/components/info/UsageSection.js b/app/ui-admin/client/components/info/UsageSection.js index 7ea4e60377af..8983e3a54150 100644 --- a/app/ui-admin/client/components/info/UsageSection.js +++ b/app/ui-admin/client/components/info/UsageSection.js @@ -2,11 +2,12 @@ import { Subtitle, Skeleton } from '@rocket.chat/fuselage'; import React from 'react'; import { useTranslation } from '../../../../../client/contexts/TranslationContext'; +import { useFormatMemorySize } from '../../../../ui/client/views/app/components/hooks'; import { DescriptionList } from './DescriptionList'; -import { formatMemorySize } from './formatters'; export function UsageSection({ statistics, isLoading }) { const s = (fn) => (isLoading ? : fn()); + const formatMemorySize = useFormatMemorySize(); const t = useTranslation(); return <> diff --git a/app/ui-admin/client/components/info/formatters.js b/app/ui-admin/client/components/info/formatters.js index db8c147d2841..ed1b906e453e 100644 --- a/app/ui-admin/client/components/info/formatters.js +++ b/app/ui-admin/client/components/info/formatters.js @@ -3,27 +3,6 @@ import s from 'underscore.string'; export const formatNumber = (number) => s.numberFormat(number, 2); -export const formatMemorySize = (memorySize) => { - if (typeof memorySize !== 'number') { - return null; - } - - const units = ['bytes', 'kB', 'MB', 'GB']; - - let order; - for (order = 0; order < units.length - 1; ++order) { - const upperLimit = Math.pow(1024, order + 1); - - if (memorySize < upperLimit) { - break; - } - } - - const divider = Math.pow(1024, order); - const decimalDigits = order === 0 ? 0 : 2; - return `${ s.numberFormat(memorySize / divider, decimalDigits) } ${ units[order] }`; -}; - export const formatDate = (date) => { if (!date) { return null; diff --git a/app/ui-admin/client/components/mailer/MailerRoute.js b/app/ui-admin/client/components/mailer/MailerRoute.js index 366ed51aa98f..a1ebfdf8f4cf 100644 --- a/app/ui-admin/client/components/mailer/MailerRoute.js +++ b/app/ui-admin/client/components/mailer/MailerRoute.js @@ -5,7 +5,7 @@ import { usePermission } from '../../../../../client/contexts/AuthorizationConte import { useMethod } from '../../../../../client/contexts/ServerContext'; import { useTranslation } from '../../../../../client/contexts/TranslationContext'; import { Mailer } from './Mailer'; -import { NotAuthorizedPage } from '../settings/NotAuthorizedPage'; +import NotAuthorizedPage from '../NotAuthorizedPage'; const useSendMail = () => { @@ -33,5 +33,9 @@ export default function MailerRoute(props) { const canAccessMailer = usePermission('access-mailer'); const sendMail = useSendMail(); - return canAccessMailer ? : ; + if (!canAccessMailer) { + return ; + } + + return ; } diff --git a/app/ui-admin/client/components/settings/SettingsRoute.js b/app/ui-admin/client/components/settings/SettingsRoute.js index 48535f2e88dd..4d10c0b0cb05 100644 --- a/app/ui-admin/client/components/settings/SettingsRoute.js +++ b/app/ui-admin/client/components/settings/SettingsRoute.js @@ -3,7 +3,7 @@ import React from 'react'; import { useAtLeastOnePermission } from '../../../../../client/contexts/AuthorizationContext'; import { useRouteParameter } from '../../../../../client/contexts/RouterContext'; import { GroupSelector } from './GroupSelector'; -import { NotAuthorizedPage } from './NotAuthorizedPage'; +import NotAuthorizedPage from '../NotAuthorizedPage'; import { SettingsState } from './SettingsState'; export function SettingsRoute() { diff --git a/app/ui/client/views/app/components/hooks.js b/app/ui/client/views/app/components/hooks.js index 10bc3369cce4..50c3c2bb37a8 100644 --- a/app/ui/client/views/app/components/hooks.js +++ b/app/ui/client/views/app/components/hooks.js @@ -1,5 +1,6 @@ import { useState, useEffect, useMemo, useCallback } from 'react'; import moment from 'moment'; +import s from 'underscore.string'; import { useUserPreference } from '../../../../../../client/contexts/UserContext'; import { useSetting } from '../../../../../../client/contexts/SettingsContext'; @@ -82,3 +83,24 @@ export function useFormatDate() { const format = useSetting('Message_DateFormat'); return useCallback((time) => moment(time).format(format), [format]); } + +export const useFormatMemorySize = () => useCallback((memorySize) => { + if (typeof memorySize !== 'number') { + return null; + } + + const units = ['bytes', 'kB', 'MB', 'GB']; + + let order; + for (order = 0; order < units.length - 1; ++order) { + const upperLimit = Math.pow(1024, order + 1); + + if (memorySize < upperLimit) { + break; + } + } + + const divider = Math.pow(1024, order); + const decimalDigits = order === 0 ? 0 : 2; + return `${ s.numberFormat(memorySize / divider, decimalDigits) } ${ units[order] }`; +}, []); diff --git a/client/contexts/RouterContext.js b/client/contexts/RouterContext.js index 56655f86a4f3..fda98b78b5ca 100644 --- a/client/contexts/RouterContext.js +++ b/client/contexts/RouterContext.js @@ -3,17 +3,17 @@ import { useSubscription } from 'use-subscription'; export const RouterContext = createContext({ getRoutePath: () => {}, - subscribeToRoutePath: () => {}, + subscribeToRoutePath: () => () => {}, getRouteUrl: () => {}, - subscribeToRouteUrl: () => {}, + subscribeToRouteUrl: () => () => {}, pushRoute: () => {}, replaceRoute: () => {}, getRouteParameter: () => {}, - subscribeToRouteParameter: () => {}, + subscribeToRouteParameter: () => () => {}, getQueryStringParameter: () => {}, - subscribeToQueryStringParameter: () => {}, + subscribeToQueryStringParameter: () => () => {}, getCurrentRoute: () => {}, - subscribeToCurrentRoute: () => {}, + subscribeToCurrentRoute: () => () => {}, }); export const useRoute = (name) => { diff --git a/client/hooks/useSafely.js b/client/hooks/useSafely.js new file mode 100644 index 000000000000..90e13a666ed6 --- /dev/null +++ b/client/hooks/useSafely.js @@ -0,0 +1,24 @@ +import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { useEffect, useRef } from 'react'; + +export const useSafely = ([state, updater]) => { + const mountedRef = useRef(); + + useEffect(() => { + mountedRef.current = true; + + return () => { + mountedRef.current = false; + }; + }); + + const safeUpdater = useMutableCallback((...args) => { + if (!mountedRef.current) { + return; + } + + updater(...args); + }); + + return [state, safeUpdater]; +}; diff --git a/package-lock.json b/package-lock.json index 2ace3c2d2702..ef558ddb576c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1164,6 +1164,24 @@ "@babel/plugin-syntax-optional-catch-binding": "^7.2.0" } }, + "@babel/plugin-proposal-optional-chaining": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.9.0.tgz", + "integrity": "sha512-NDn5tu3tcv4W30jNhmc2hyD5c56G6cXx4TesJubhxrJeCvuuMpttxr0OnNCqbZGhFjLrg+NIhxxC+BK5F6yS3w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.0" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", + "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", + "dev": true + } + } + }, "@babel/plugin-proposal-unicode-property-regex": { "version": "7.6.2", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.6.2.tgz", @@ -1283,6 +1301,23 @@ "@babel/helper-plugin-utils": "^7.0.0" } }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz", + "integrity": "sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ==", + "dev": true + } + } + }, "@babel/plugin-transform-arrow-functions": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.2.0.tgz", @@ -2849,9 +2884,9 @@ } }, "@rocket.chat/apps-engine": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@rocket.chat/apps-engine/-/apps-engine-1.13.0.tgz", - "integrity": "sha512-gU72qk3xhk5UYGmgsp4VLnOOYeAcsc4O8K2rYiLfs1EpBA1tNY+/YtR6Crl5usdU7sRSHUq4Y86pAchepT6aJQ==", + "version": "1.14.0-beta.3119", + "resolved": "https://registry.npmjs.org/@rocket.chat/apps-engine/-/apps-engine-1.14.0-beta.3119.tgz", + "integrity": "sha512-SoQicHOGkQD6wwcnMzc1qETbNoMwQ2ei5j5krzh0/dachCbHG+C1rBO8yYbK2TQwuSjxR0wLM20ZbM57iHaYKw==", "requires": { "adm-zip": "^0.4.9", "cryptiles": "^4.1.3", @@ -6780,16 +6815,8 @@ "integrity": "sha1-WYc/Nej89sc2HBAjkmHXbhU0i7I=" }, "adm-zip": { - "version": "github:RocketChat/adm-zip#34ac787ce7f45c6cea99df049deb17d0c476a3b5", - "from": "github:RocketChat/adm-zip" - }, - "agent-base": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz", - "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==", - "requires": { - "es6-promisify": "^5.0.0" - } + "version": "0.4.12", + "resolved": "github:RocketChat/adm-zip#34ac787ce7f45c6cea99df049deb17d0c476a3b5" }, "aggregate-error": { "version": "3.0.1", @@ -16115,15 +16142,6 @@ "ms": "^2.1.1" } }, - "https-proxy-agent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz", - "integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==", - "requires": { - "agent-base": "5", - "debug": "4" - } - }, "is-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", @@ -16951,6 +16969,31 @@ "semver": "^5.5.0" }, "dependencies": { + "agent-base": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", + "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "requires": { + "es6-promisify": "^5.0.0" + } + }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "https-proxy-agent": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", + "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", + "requires": { + "agent-base": "^4.3.0", + "debug": "^3.1.0" + } + }, "lru-cache": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", @@ -16959,6 +17002,11 @@ "pseudomap": "^1.0.2", "yallist": "^2.1.2" } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" } } }, @@ -17626,26 +17674,31 @@ "dev": true }, "https-proxy-agent": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz", - "integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz", + "integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==", "requires": { - "agent-base": "^4.1.0", - "debug": "^3.1.0" + "agent-base": "5", + "debug": "4" }, "dependencies": { + "agent-base": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz", + "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==" + }, "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "requires": { "ms": "^2.1.1" } }, "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" } } }, @@ -29037,6 +29090,38 @@ "https-proxy-agent": "^2.2.1", "node-fetch": "^2.2.0", "uuid": "^3.3.2" + }, + "dependencies": { + "agent-base": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", + "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "requires": { + "es6-promisify": "^5.0.0" + } + }, + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "requires": { + "ms": "^2.1.1" + } + }, + "https-proxy-agent": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", + "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", + "requires": { + "agent-base": "^4.3.0", + "debug": "^3.1.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } } }, "telejson": { diff --git a/package.json b/package.json index 83e34c5f96ed..56e3a8874222 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ }, "devDependencies": { "@babel/core": "^7.6.2", + "@babel/plugin-proposal-optional-chaining": "^7.9.0", "@babel/preset-env": "^7.6.2", "@babel/preset-react": "^7.0.0", "@octokit/rest": "^16.1.0", @@ -128,7 +129,7 @@ "@nivo/heatmap": "^0.61.0", "@nivo/line": "^0.61.1", "@nivo/pie": "^0.61.1", - "@rocket.chat/apps-engine": "^1.13.0", + "@rocket.chat/apps-engine": "^1.14.0-beta.3119", "@rocket.chat/fuselage": "^0.7.1", "@rocket.chat/fuselage-hooks": "^0.7.1", "@rocket.chat/fuselage-polyfills": "^0.7.1",