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 @@
-
-
- {{> header sectionName="Import" fullpage="true"}}
-
- {{#unless hasPermission 'run-import'}}
-
{{_ "You_are_not_authorized_to_view_this_page"}}
- {{else}}
- {{#if isPreparing}}
- {{> loading}}
- {{else}}
-
-
-
- {{#if anySuccessfulSlackImports}}
-
- {{/if}}
-
-
- {{#if canShowCurrentOperation}}
-
-
{{_ "Current_Import_Operation"}}
-
-
-
- {{> importOperationSummary operation}}
-
- {{#if canContinueOperation}}
-
- {{else}}
- {{#if canCheckOperationProgress}}
-
- {{/if}}
- {{/if}}
-
- {{/if}}
-
- {{#if history}}
-
-
{{_ "Recent_Import_History"}}
-
-
- {{#each history}}
- {{#if isNotCurrentOperation}}
-
- {{> importOperationSummary .}}
-
- {{/if}}
- {{/each}}
- {{/if}}
- {{/if}}
-
- {{/unless}}
-
-
-
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 @@
-
-
- {{> header sectionName=pageTitle}}
-
-
{{_ "Back_to_imports"}}
-
- {{#unless hasPermission 'run-import'}}
-
{{_ "You_are_not_authorized_to_view_this_page"}}
- {{else}}
- {{#if isPreparing}}
- {{> loading}}
- {{else}}
-
-
-
-
- {{/if}}
-
- {{/unless}}
-
-
-
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 @@
-
-
- {{> header sectionName=pageTitle}}
-
- {{#unless hasPermission 'run-import'}}
-
{{_ "You_are_not_authorized_to_view_this_page"}}
- {{else}}
- {{#if isPreparing}}
- {{#if hasProgressRate}}
- {{ progressRate }}
- {{/if}}
-
- {{> loading}}
- {{else}}
-
{{_ "Back_to_imports"}}
-
-
-
{{_ "Actions"}}
-
-
-
-
-
-
-
{{_ "Messages"}}: {{message_count}}
-
-
- {{#if users.length}}
-
-
{{_ "Users"}}
-
-
-
-
-
-
-
-
-
-
- {{/if}}
-
- {{#if channels.length}}
-
-
{{_ "Channels"}}
-
-
-
-
-
-
-
-
-
-
- {{/if}}
- {{/if}}
- {{/unless}}
-
-
-
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 @@
-
- {{> loading}}
- {{step}}
- {{completed}} / {{total}}
- {{progressRate}}
-
- {{_ "You_can_close_this_window_now"}}
-
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) => ],
+};
+
+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 && {t('Importer_From_Description', { from: t(importer.name) })}}
+
+ {importer &&
+ {t('File_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",