[NEW] Admin refactor Second phase (#17551)

* [NEW] Redesign Administration > Connectivity Services (#17525)

* [NEW] Admin Rewrite ->  Integrations Page (#17505)

* [NEW] Admin Rewrite -> OAuth Apps (#17646)

Co-authored-by: Guilherme Gazzo <guilhermegazzo@gmail.com>
Co-authored-by: Tasso Evangelista <tasso.evangelista@rocket.chat>
Co-authored-by: gabriellsh <40830821+gabriellsh@users.noreply.github.com>
pull/17663/head
Guilherme Gazzo 6 years ago committed by GitHub
parent 811688e119
commit 11a7ed87bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 16
      app/cloud/client/admin/callback.html
  2. 46
      app/cloud/client/admin/callback.js
  3. 145
      app/cloud/client/admin/cloud.html
  4. 233
      app/cloud/client/admin/cloud.js
  5. 26
      app/cloud/client/admin/cloudRegisterManually.css
  6. 36
      app/cloud/client/admin/cloudRegisterManually.html
  7. 106
      app/cloud/client/admin/cloudRegisterManually.js
  8. 2
      app/cloud/client/admin/index.js
  9. 33
      app/cloud/client/index.js
  10. 34
      app/integrations/client/getIntegration.js
  11. 3
      app/integrations/client/index.js
  12. 75
      app/integrations/client/route.js
  13. 3
      app/integrations/client/streamer.js
  14. 75
      app/integrations/client/stylesheets/integrations.css
  15. 10
      app/integrations/client/views/additional/zapier.html
  16. 11
      app/integrations/client/views/index.js
  17. 74
      app/integrations/client/views/integrations.html
  18. 65
      app/integrations/client/views/integrations.js
  19. 138
      app/integrations/client/views/integrationsIncoming.html
  20. 251
      app/integrations/client/views/integrationsIncoming.js
  21. 51
      app/integrations/client/views/integrationsNew.html
  22. 35
      app/integrations/client/views/integrationsNew.js
  23. 241
      app/integrations/client/views/integrationsOutgoing.html
  24. 353
      app/integrations/client/views/integrationsOutgoing.js
  25. 143
      app/integrations/client/views/integrationsOutgoingHistory.html
  26. 191
      app/integrations/client/views/integrationsOutgoingHistory.js
  27. 39
      app/integrations/client/views/messageExample.js
  28. 28
      app/oauth2-server-config/client/admin/route.js
  29. 4
      app/oauth2-server-config/client/admin/views/index.js
  30. 72
      app/oauth2-server-config/client/admin/views/oauthApp.html
  31. 120
      app/oauth2-server-config/client/admin/views/oauthApp.js
  32. 38
      app/oauth2-server-config/client/admin/views/oauthApps.html
  33. 33
      app/oauth2-server-config/client/admin/views/oauthApps.js
  34. 1
      app/oauth2-server-config/client/index.js
  35. 78
      app/ui/client/components/GenericTable.js
  36. 4
      app/utils/client/lib/handleError.js
  37. 146
      client/admin/cloud/CloudPage.js
  38. 17
      client/admin/cloud/CloudRoute.js
  39. 61
      client/admin/cloud/ConnectToCloudSection.js
  40. 183
      client/admin/cloud/ManualWorkspaceRegistrationModal.js
  41. 63
      client/admin/cloud/TroubleshootingSection.js
  42. 32
      client/admin/cloud/WhatIsItSection.js
  43. 108
      client/admin/cloud/WorkspaceLoginSection.js
  44. 128
      client/admin/cloud/WorkspaceRegistrationSection.js
  45. 3
      client/admin/cloud/constants.js
  46. 202
      client/admin/customSounds/EditSound.js
  47. 101
      client/admin/customSounds/NewSound.js
  48. 165
      client/admin/integrations/IncomingWebhookForm.js
  49. 56
      client/admin/integrations/IntegrationsPage.js
  50. 40
      client/admin/integrations/IntegrationsRoute.js
  51. 97
      client/admin/integrations/IntegrationsTable.js
  52. 264
      client/admin/integrations/OutgoiongWebhookForm.js
  53. 105
      client/admin/integrations/edit/EditIncomingWebhook.js
  54. 82
      client/admin/integrations/edit/EditIntegrationsPage.js
  55. 134
      client/admin/integrations/edit/EditOutgoingWebhook.js
  56. 268
      client/admin/integrations/edit/OutgoingWebhookHistoryPage.js
  57. 20
      client/admin/integrations/exampleIncomingData.js
  58. 9
      client/admin/integrations/new/NewBot.js
  59. 50
      client/admin/integrations/new/NewIncomingWebhook.js
  60. 58
      client/admin/integrations/new/NewIntegrationsPage.js
  61. 68
      client/admin/integrations/new/NewOutgoingWebhook.js
  62. 43
      client/admin/integrations/new/NewZapier.js
  63. 87
      client/admin/oauthApps/OAuthAddApp.js
  64. 39
      client/admin/oauthApps/OAuthAppsPage.js
  65. 15
      client/admin/oauthApps/OAuthAppsRoute.js
  66. 40
      client/admin/oauthApps/OAuthAppsTable.js
  67. 222
      client/admin/oauthApps/OAuthEditApp.js
  68. 8
      client/admin/rooms/EditRoom.js
  69. 194
      client/admin/rooms/edit/EditRoom.js
  70. 15
      client/admin/routes.js
  71. 7
      client/admin/sidebarItems.js
  72. 8
      client/admin/users/EditUser.js
  73. 4
      client/admin/users/InviteUsers.js
  74. 8
      client/admin/users/UserInfo.js
  75. 14
      client/components/setupWizard/StepHeader.js
  76. 28
      client/hooks/useForm.js
  77. 6
      client/hooks/useHilightCode.js
  78. 3
      client/importPackages.js
  79. 23
      package-lock.json
  80. 2
      package.json
  81. 7
      packages/rocketchat-i18n/i18n/ca.i18n.json
  82. 8
      packages/rocketchat-i18n/i18n/cs.i18n.json
  83. 5
      packages/rocketchat-i18n/i18n/da.i18n.json
  84. 5
      packages/rocketchat-i18n/i18n/de-IN.i18n.json
  85. 8
      packages/rocketchat-i18n/i18n/de.i18n.json
  86. 22
      packages/rocketchat-i18n/i18n/en.i18n.json
  87. 7
      packages/rocketchat-i18n/i18n/es.i18n.json
  88. 8
      packages/rocketchat-i18n/i18n/fa.i18n.json
  89. 4
      packages/rocketchat-i18n/i18n/fr.i18n.json
  90. 7
      packages/rocketchat-i18n/i18n/hr.i18n.json
  91. 7
      packages/rocketchat-i18n/i18n/hu.i18n.json
  92. 8
      packages/rocketchat-i18n/i18n/ja.i18n.json
  93. 8
      packages/rocketchat-i18n/i18n/km.i18n.json
  94. 7
      packages/rocketchat-i18n/i18n/ko.i18n.json
  95. 3
      packages/rocketchat-i18n/i18n/nl.i18n.json
  96. 8
      packages/rocketchat-i18n/i18n/pl.i18n.json
  97. 5
      packages/rocketchat-i18n/i18n/pt-BR.i18n.json
  98. 7
      packages/rocketchat-i18n/i18n/pt.i18n.json
  99. 8
      packages/rocketchat-i18n/i18n/ru.i18n.json
  100. 3
      packages/rocketchat-i18n/i18n/sv.i18n.json
  101. Some files were not shown because too many files have changed in this diff Show More

@ -1,16 +0,0 @@
<template name="cloudCallback">
<div class="main-content-flex">
<section class="page-container page-home page-static page-settings">
{{> header sectionName="Cloud_connect"}}
<div class="content">
{{#requiresPermission 'manage-cloud'}}
{{#if callbackError.error}}
<p>{{_ "Cloud_error_in_authenticating"}}</p>
<p>{{_ "Cloud_error_code"}} {{ callbackError.errorCode }}</p>
{{/if}}
{{/requiresPermission}}
</div>
</section>
</div>
</template>

@ -1,46 +0,0 @@
import './callback.html';
import { Meteor } from 'meteor/meteor';
import { ReactiveVar } from 'meteor/reactive-var';
import { Template } from 'meteor/templating';
import { Tracker } from 'meteor/tracker';
import { FlowRouter } from 'meteor/kadira:flow-router';
import queryString from 'query-string';
import { SideNav } from '../../../ui-utils/client';
Template.cloudCallback.onCreated(function() {
const instance = this;
instance.loading = new ReactiveVar(true);
instance.callbackError = new ReactiveVar({ error: false });
const params = queryString.parse(location.search);
if (params.error_code) {
instance.callbackError.set({ error: true, errorCode: params.error_code });
} else {
Meteor.call('cloud:finishOAuthAuthorization', params.code, params.state, (error) => {
if (error) {
console.warn('cloud:finishOAuthAuthorization', error);
return;
}
FlowRouter.go('/admin/cloud');
});
}
});
Template.cloudCallback.helpers({
callbackError() {
return Template.instance().callbackError.get();
},
});
Template.cloudCallback.onRendered(() => {
Tracker.afterFlush(() => {
SideNav.setFlex('adminFlex');
SideNav.openFlex();
});
});

@ -1,145 +0,0 @@
<template name="cloud">
<div class="main-content-flex">
<section class="page-container page-home page-static page-settings">
{{#header sectionName="Connectivity_Services" hideHelp=true fixedHeight=true fullpage=true}}
<div class="rc-header__section-button">
{{#unless info.workspaceRegistered}}
<button class="rc-button rc-button--small rc-button--primary rc-button--outline js-register">
{{_ "Cloud_Register_manually"}}
</button>
{{/unless}}
<a href="https://cloud.rocket.chat" class="rc-button rc-button--primary action cloud-console-btn" target="_blank">{{_ "Cloud_console"}}</a>
</div>
{{/header}}
<div class="content">
{{#requiresPermission 'manage-cloud'}}
<div class="section">
<div class="section-title">
<div class="section-title-text">
{{_ "Cloud_what_is_it"}}
</div>
</div>
<div class="section-content">
<p>{{_ "Cloud_what_is_it_description"}}</p>
</div>
<details>
<div class="section-content">
<p>{{_ "Cloud_what_is_it_services_like"}}</p>
<ul style="list-style-type:disc;margin-left:15px;">
<li>{{_ "Register_Server_Registered_Push_Notifications"}}</li>
<li>{{_ "Register_Server_Registered_Livechat"}}</li>
<li>{{_ "Register_Server_Registered_OAuth"}}</li>
<li>{{_ "Register_Server_Registered_Marketplace"}}</li>
</ul>
</div>
<div class="section-content">
{{_ "Cloud_what_is_it_additional"}}
</div>
</details>
</div>
<div class="section">
{{#if info.connectToCloud}}
{{#if info.workspaceRegistered}}
<div class="section-content border-component-color">
<p>{{_ "Cloud_workspace_connected"}}</p>
<div class="input-line double-col">
{{#if isLoggedIn}}
<label class="setting-label" title=""></label>
<div class="setting-field">
<button type="button" class="rc-button rc-button--primary action logout-btn" target="_blank">{{_ "Cloud_logout"}}</button>
</div>
{{else}}
<label class="setting-label" title=""></label>
<div class="setting-field">
<button type="button" class="rc-button rc-button--primary action login-btn" target="_blank">{{_ "Cloud_login_to_cloud"}}</button>
</div>
{{/if}}
</div>
</div>
<div class="section-content border-component-color">
<p>{{_ "Cloud_workspace_disconnect"}}</p>
<div class="input-line double-col">
<label class="setting-label" title=""></label>
<div class="setting-field">
<button type="button" class="rc-button rc-button--danger action disconnect-btn">{{_ "Disconnect"}}</button>
</div>
</div>
</div>
{{else}}
<div class="section-content border-component-color">
<div class="input-line double-col">
<label class="setting-label" title="cloudEmail">{{_ "Email"}}</label>
<div class="setting-field">
<input class="input-monitor rc-input__element" type="text" name="cloudEmail" value="{{ info.email }}">
<div class="settings-description secondary-font-color">{{_ "Cloud_address_to_send_registration_to"}}</div>
</div>
</div>
<div class="input-line double-col">
<label class="setting-label" title=""></label>
<div class="setting-field">
<button type="button" class="rc-button rc-button--primary action update-email-btn" style="float:left">{{_ "Cloud_update_email"}}</button>
<button type="button" class="rc-button rc-button--primary action resend-email-btn" style="float:left;margin-left:5px;">{{_ "Cloud_resend_email"}}</button>
</div>
</div>
<div class="input-line double-col">
<label class="setting-label" title="cloudToken">{{_ "Token"}}</label>
<div class="setting-field">
<input class="input-monitor rc-input__element" type="text" name="cloudToken" value="{{ info.token }}">
<div class="settings-description secondary-font-color">{{_ "Cloud_manually_input_token"}}</div>
</div>
</div>
<div class="input-line double-col">
<label class="setting-label" title=""></label>
<div class="setting-field">
<button type="button" class="rc-button rc-button--primary action connect-btn">{{_ "Connect"}}</button>
</div>
</div>
<p>{{_ "Cloud_connect_support"}}: <a href="mailto:support@rocket.chat?subject=[Self Hosted Registration]&body=WorkspaceId: {{ info.workspaceId }}%0D%0ADeployment Id: {{ info.uniqueId }}%0D%0AIssue: <please describe your issue here>">support@rocket.chat</a></p>
</div>
{{/if}}
{{else}}
<div class="section-title">
<div class="section-title-text">
{{_ "Cloud_registration_required"}}
</div>
</div>
<div class="section-content border-component-color">
<p>{{_ "Cloud_registration_required_description"}}</p>
<button type="button" class="rc-button rc-button--primary action register-btn">{{_ "Cloud_registration_requried_link_text"}}</button>
</div>
{{/if}}
</div>
{{#if info.connectToCloud}}
<div class="section">
<div class="section-title">
<div class="section-title-text">
{{_ "Cloud_troubleshooting"}}
</div>
</div>
<div class="section-content border-component-color">
<p>{{_ "Cloud_workspace_support"}}</p>
<div class="input-line double-col">
<label class="setting-label" title=""></label>
<div class="setting-field">
<button type="button" class="rc-button rc-button--danger action sync-btn">{{_ "Sync"}}</button>
</div>
</div>
</div>
<div class="section-content">
{{_ "Cloud_status_page_description"}}: <a href="https://status.rocket.chat" target="_blank">status.rocket.chat</a>
</div>
</div>
{{/if}}
{{/requiresPermission}}
</div>
</section>
</div>
</template>

@ -1,233 +0,0 @@
import './cloud.html';
import { Meteor } from 'meteor/meteor';
import { ReactiveVar } from 'meteor/reactive-var';
import { Template } from 'meteor/templating';
import { Tracker } from 'meteor/tracker';
import queryString from 'query-string';
import toastr from 'toastr';
import { t } from '../../../utils';
import { SideNav, modal } from '../../../ui-utils/client';
Template.cloud.onCreated(function() {
const instance = this;
instance.info = new ReactiveVar();
instance.loading = new ReactiveVar(true);
instance.isLoggedIn = new ReactiveVar(false);
instance.loadRegStatus = function _loadRegStatus() {
Meteor.call('cloud:checkRegisterStatus', (error, info) => {
if (error) {
console.warn('cloud:checkRegisterStatus', error);
return;
}
instance.info.set(info);
instance.loading.set(false);
});
};
instance.getLoggedIn = function _getLoggedIn() {
Meteor.call('cloud:checkUserLoggedIn', (error, result) => {
if (error) {
console.warn(error);
return;
}
instance.isLoggedIn.set(result);
});
};
instance.oauthAuthorize = function _oauthAuthorize() {
Meteor.call('cloud:getOAuthAuthorizationUrl', (error, url) => {
if (error) {
console.warn(error);
return;
}
window.location.href = url;
});
};
instance.logout = function _logout() {
Meteor.call('cloud:logout', (error) => {
if (error) {
console.warn(error);
return;
}
instance.getLoggedIn();
});
};
instance.connectWorkspace = function _connectWorkspace(token) {
Meteor.call('cloud:connectWorkspace', token, (error, success) => {
if (error) {
toastr.error(error);
instance.loadRegStatus();
return;
}
if (!success) {
toastr.error('An error occured connecting');
instance.loadRegStatus();
return;
}
toastr.success(t('Connected'));
instance.loadRegStatus();
});
};
instance.disconnectWorkspace = function _disconnectWorkspace() {
Meteor.call('cloud:disconnectWorkspace', (error, success) => {
if (error) {
toastr.error(error);
instance.loadRegStatus();
return;
}
if (!success) {
toastr.error('An error occured disconnecting');
instance.loadRegStatus();
return;
}
toastr.success(t('Disconnected'));
instance.loadRegStatus();
});
};
instance.syncWorkspace = function _syncWorkspace() {
Meteor.call('cloud:syncWorkspace', (error, success) => {
if (error) {
toastr.error(error);
instance.loadRegStatus();
return;
}
if (!success) {
toastr.error('An error occured syncing');
instance.loadRegStatus();
return;
}
toastr.success(t('Sync Complete'));
instance.loadRegStatus();
});
};
instance.registerWorkspace = function _registerWorkspace() {
Meteor.call('cloud:registerWorkspace', (error, success) => {
if (error) {
toastr.error(error);
instance.loadRegStatus();
return;
}
if (!success) {
toastr.error('An error occured');
instance.loadRegStatus();
return;
}
return instance.syncWorkspace();
});
};
const params = queryString.parse(location.search);
if (params.token) {
instance.connectWorkspace(params.token);
} else {
instance.loadRegStatus();
}
instance.getLoggedIn();
});
Template.cloud.helpers({
info() {
return Template.instance().info.get();
},
isLoggedIn() {
return Template.instance().isLoggedIn.get();
},
});
Template.cloud.events({
'click .js-register'() {
modal.open({
template: 'cloudRegisterManually',
showCancelButton: false,
showConfirmButton: false,
showFooter: false,
closeOnCancel: true,
html: true,
confirmOnEnter: false,
});
},
'click .update-email-btn'() {
const val = $('input[name=cloudEmail]').val();
Meteor.call('cloud:updateEmail', val, false, (error) => {
if (error) {
console.warn(error);
return;
}
toastr.success(t('Saved'));
});
},
'click .resend-email-btn'() {
const val = $('input[name=cloudEmail]').val();
Meteor.call('cloud:updateEmail', val, true, (error) => {
if (error) {
console.warn(error);
return;
}
toastr.success(t('Requested'));
});
},
'click .login-btn'(e, i) {
i.oauthAuthorize();
},
'click .logout-btn'(e, i) {
i.logout();
},
'click .connect-btn'(e, i) {
const token = $('input[name=cloudToken]').val();
i.connectWorkspace(token);
},
'click .register-btn'(e, i) {
i.registerWorkspace();
},
'click .disconnect-btn'(e, i) {
i.disconnectWorkspace();
},
'click .sync-btn'(e, i) {
i.syncWorkspace();
},
});
Template.cloud.onRendered(() => {
Tracker.afterFlush(() => {
SideNav.setFlex('adminFlex');
SideNav.openFlex();
});
});

@ -1,26 +0,0 @@
.rc-promtp {
display: flex;
min-height: 188px;
padding: 1rem;
border-radius: 2px;
background: #2f343d;
flex-flow: column wrap;
justify-content: space-between;
&--element,
&--element[disabled] {
flex: 1 1 auto;
resize: none;
color: #cbced1;
border: none;
background: none;
font-family: Menlo, Monaco, Consolas, Liberation Mono, Courier New, monospace;
font-size: 14px;
line-height: 20px;
}
}

@ -1,36 +0,0 @@
<template name="cloudRegisterManually">
{{> header sectionName="Cloud_Register_manually" hideHelp=true fullpage=true}}
{{# if copyStep }}
<form class="preferences-page__content">
<p class="rc-modal__description">{{_ "Cloud_register_offline_helper" }}</p>
<div class="rc-promtp">
<textarea class="rc-promtp--element" disabled>{{clientKey}}</textarea>
<button class="rc-button rc-button--primary js-copy" data-clipboard-text="{{clientKey}}">
{{>icon icon='copy'}} {{_ "Copy"}}
</button>
</div>
<p class="rc-modal__description js-cloud">{{#if cloudLink}} {{{cloudLink}}} {{else}} <a href="https://cloud.rocket.chat" rel="noopener noreferrer" class="cloud-console-btn" target="_blank"></a>{{/if}}</p>
</form>
<footer class="rc-modal__footer rc-modal__footer--empty">
<button class="rc-button rc-button--primary js-next">{{_ "Next"}}</button>
</footer>
{{else}}
<form class="preferences-page__content">
<p class="rc-modal__description">{{_ "Cloud_register_offline_finish_helper"}}</p>
<div class="rc-promtp">
<textarea class="js-cloud-key rc-promtp--element" placeholder="{{_ "Paste_here"}}" disabled={{isLoading}}></textarea>
</div>
</form>
<footer class="rc-modal__footer rc-modal__footer--empty">
<button class="rc-button rc-button--secondary js-back">{{_ "Back"}}</button>
<button class="rc-button rc-button--primary js-finish" disabled='{{disabled}}'>
{{#if isLoading}} {{> loading}} {{/if}}
<span style="{{#if isLoading}} visibility:hidden {{/if}}">{{_ "Finish Registration"}}</span>
</button>
</footer>
{{/if}}
</template>

@ -1,106 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { ReactiveDict } from 'meteor/reactive-dict';
import { ReactiveVar } from 'meteor/reactive-var';
import { Template } from 'meteor/templating';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import Clipboard from 'clipboard';
import toastr from 'toastr';
import { APIClient } from '../../../utils/client';
import { modal } from '../../../ui-utils/client';
import './cloudRegisterManually.html';
import './cloudRegisterManually.css';
const CLOUD_STEPS = {
COPY: 0,
PASTE: 1,
DONE: 2,
ERROR: 3,
};
Template.cloudRegisterManually.events({
'submit form'(e) {
e.preventDefault();
},
'input .js-cloud-key'(e, instance) {
instance.state.set('cloudKey', e.currentTarget.value);
},
'click .js-next'(event, instance) {
instance.state.set('step', CLOUD_STEPS.PASTE);
},
'click .js-back'(event, instance) {
instance.state.set('step', CLOUD_STEPS.COPY);
},
'click .js-finish'(event, instance) {
instance.state.set('loading', true);
APIClient
.post('v1/cloud.manualRegister', {}, { cloudBlob: instance.state.get('cloudKey') })
.then(() => modal.open({
type: 'success',
title: TAPi18n.__('Success'),
text: TAPi18n.__('Cloud_register_success'),
confirmButtonText: TAPi18n.__('Ok'),
closeOnConfirm: false,
showCancelButton: false,
}, () => window.location.reload()))
.catch(() => modal.open({
type: 'error',
title: TAPi18n.__('Error'),
text: TAPi18n.__('Cloud_register_error'),
}))
.then(() => instance.state.set('loading', false));
},
});
Template.cloudRegisterManually.helpers({
cloudLink() {
return Template.instance().cloudLink.get();
},
copyStep() {
return Template.instance().state.get('step') === CLOUD_STEPS.COPY;
},
clientKey() {
return Template.instance().state.get('clientKey');
},
isLoading() {
return Template.instance().state.get('loading');
},
step() {
return Template.instance().state.get('step');
},
disabled() {
const { state } = Template.instance();
const shouldDisable = state.get('cloudKey').trim().length === 0 || state.get('loading');
return shouldDisable && 'disabled';
},
});
Template.cloudRegisterManually.onRendered(function() {
const clipboard = new Clipboard('.js-copy');
clipboard.on('success', function() {
toastr.success(TAPi18n.__('Copied'));
});
const btn = this.find('.cloud-console-btn');
// After_copy_the_text_go_to_cloud
this.cloudLink.set(TAPi18n.__('Cloud_click_here').replace(/(\[(.*)\]\(\))/ig, (_, __, text) => btn.outerHTML.replace('</a>', `${ text }</a>`)));
});
Template.cloudRegisterManually.onCreated(function() {
this.cloudLink = new ReactiveVar();
this.state = new ReactiveDict({
step: CLOUD_STEPS.COPY,
loading: false,
clientKey: '',
cloudKey: '',
error: '',
});
Meteor.call('cloud:getWorkspaceRegisterData', (error, result) => {
this.state.set('clientKey', result);
});
});

@ -1,2 +0,0 @@
import './cloud';
import './callback';

@ -1,33 +0,0 @@
import './admin/callback';
import './admin/cloud';
import './admin/cloudRegisterManually';
import { BlazeLayout } from 'meteor/kadira:blaze-layout';
import { registerAdminRoute, registerAdminSidebarItem } from '../../../client/admin';
import { hasAtLeastOnePermission } from '../../authorization';
registerAdminRoute('/cloud', {
name: 'cloud',
async action() {
await import('./admin');
BlazeLayout.render('main', { center: 'cloud', old: true });
},
});
registerAdminRoute('/cloud/oauth-callback', {
name: 'cloud-oauth-callback',
async action() {
await import('./admin');
BlazeLayout.render('main', { center: 'cloudCallback', old: true });
},
});
registerAdminSidebarItem({
icon: 'cloud-plus',
href: 'cloud',
i18nLabel: 'Connectivity_Services',
permissionGranted() {
return hasAtLeastOnePermission(['manage-cloud']);
},
});

@ -1,34 +0,0 @@
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import { FlowRouter } from 'meteor/kadira:flow-router';
import toastr from 'toastr';
import { hasAllPermission } from '../../authorization/client';
import { APIClient } from '../../utils/client';
export async function getIntegration(integrationId, uid) {
if (!integrationId) {
return;
}
const reqParams = {
integrationId,
};
if (!hasAllPermission('manage-outgoing-integrations')) {
if (!hasAllPermission('manage-own-outgoing-integrations')) {
toastr.error(TAPi18n.__('No_integration_found'));
FlowRouter.go('admin-integrations');
return;
}
reqParams.createdBy = uid;
}
try {
const { integration } = await APIClient.v1.get('integrations.get', reqParams);
return integration;
} catch (e) {
toastr.error(TAPi18n.__('Error'));
console.error(e);
}
}

@ -1,3 +0,0 @@
import '../lib/rocketchat';
import './startup';
import './route';

@ -1,75 +0,0 @@
import { BlazeLayout } from 'meteor/kadira:blaze-layout';
import { registerAdminRoute } from '../../../client/admin';
import { t } from '../../utils';
const dynamic = () => import('./views');
registerAdminRoute('/integrations', {
name: 'admin-integrations',
async action() {
await dynamic();
return BlazeLayout.render('main', {
center: 'integrations',
pageTitle: t('Integrations'),
});
},
});
registerAdminRoute('/integrations/new', {
name: 'admin-integrations-new',
async action() {
await dynamic();
return BlazeLayout.render('main', {
center: 'integrationsNew',
pageTitle: t('Integration_New'),
});
},
});
registerAdminRoute('/integrations/incoming/:id?', {
name: 'admin-integrations-incoming',
async action(params) {
await dynamic();
return BlazeLayout.render('main', {
center: 'pageSettingsContainer',
pageTitle: t('Integration_Incoming_WebHook'),
pageTemplate: 'integrationsIncoming',
params,
});
},
});
registerAdminRoute('/integrations/outgoing/:id?', {
name: 'admin-integrations-outgoing',
async action(params) {
await dynamic();
return BlazeLayout.render('main', {
center: 'integrationsOutgoing',
pageTitle: t('Integration_Outgoing_WebHook'),
params,
});
},
});
registerAdminRoute('/integrations/outgoing/:id?/history', {
name: 'admin-integrations-outgoing-history',
async action(params) {
await dynamic();
return BlazeLayout.render('main', {
center: 'integrationsOutgoingHistory',
pageTitle: t('Integration_Outgoing_WebHook_History'),
params,
});
},
});
registerAdminRoute('/integrations/additional/zapier', {
name: 'admin-integrations-additional-zapier',
async action() {
await dynamic();
BlazeLayout.render('main', {
center: 'integrationsAdditionalZapier',
});
},
});

@ -1,3 +0,0 @@
import { Meteor } from 'meteor/meteor';
export const integrationHistoryStreamer = new Meteor.Streamer('integrationHistory');

@ -1,75 +0,0 @@
.admin-integrations-new-panel {
& .admin-integrations-new-item {
display: flex;
padding: 20px 10px;
cursor: pointer;
color: #444444;
border-bottom: 1px solid #dddddd;
align-items: center;
&:hover {
background-color: #fafafa;
}
& > i {
color: #aaaaaa;
font-size: 2rem;
}
& .admin-integrations-new-item-body {
display: flex;
flex-direction: column;
padding: 0 20px;
flex-grow: 1;
}
& .admin-integrations-new-item-title {
font-size: 1.4rem;
font-weight: 500;
line-height: 2.1rem;
}
& .admin-integrations-new-item-description {
color: #aaaaaa;
font-size: 1rem;
line-height: 1.5rem;
}
}
& > a:last-child > .admin-integrations-new-item {
border-bottom: none;
}
}
.message-example {
& li {
list-style: none;
}
}
.integrate-other-ways {
& p {
font-size: 1rem;
line-height: 1.5rem;
& a {
color: #175cc4 !important;
}
}
}
.content.zapier {
display: flex;
flex-direction: column;
align-items: center;
#zapier-goes-here {
width: 95%;
}
}

@ -1,10 +0,0 @@
<template name="integrationsAdditionalZapier">
<section class="page-container page-home page-static page-settings">
{{> header sectionName="Zapier" hideHelp=true fixedHeight=true}}
<a href="{{pathFor "admin-integrations"}}"><i class="icon-angle-left"></i> {{_ "Back_to_integrations"}}</a><br><br>
<div class="content zapier">
<div id="zapier-goes-here"></div>
</div>
</section>
<script src="https://zapier.com/apps/embed/widget.js?services=rocketchat&html_id=zapier-goes-here"></script>
</template>

@ -1,11 +0,0 @@
import './integrations.html';
import './integrations';
import './integrationsNew.html';
import './integrationsNew';
import './integrationsIncoming.html';
import './integrationsIncoming';
import './integrationsOutgoing.html';
import './integrationsOutgoing';
import './integrationsOutgoingHistory.html';
import './integrationsOutgoingHistory';
import './additional/zapier.html';

@ -1,74 +0,0 @@
<template name="integrations">
<section class="page-container page-home page-static page-settings">
{{#header sectionName=pageTitle buttons=true}}
{{#if hasPermission}}
<div class="rc-header__section-button">
<a href="{{pathFor "admin-integrations-additional-zapier"}}" class="rc-button rc-button--primary zapier-btn">Zapier</a>
<a href="{{pathFor "admin-integrations-new"}}" class="rc-button rc-button--primary">{{_ "New_integration"}}</a>
</div>
{{/if}}
{{/header}}
<div class="content">
{{#unless hasPermission}}
<p>{{_ "You_are_not_authorized_to_view_this_page"}}</p>
{{else}}
<div class="rocket-form">
<div class="section integrate-other-ways">
<h1>Zapier</h1>
<p>{{{_ "additional_integrations_Zapier"}}}</p>
<br>
<h1>{{_ "Bots"}}</h1>
<p>{{{_ "additional_integrations_Bots"}}}</p>
</div>
<div class="section">
<div class="admin-integrations-new-panel">
{{#each integrations}}
{{#if $eq type 'webhook-incoming'}}
<a href="{{pathFor "admin-integrations-incoming" id=_id}}">
<div class="admin-integrations-new-item">
<i class="icon-login"></i>
<div class="admin-integrations-new-item-body">
<div class="admin-integrations-new-item-title">
{{_ "Incoming_WebHook"}} {{#if name}}- {{name}}{{/if}}
</div>
<div class="admin-integrations-new-item-description">
{{{_ "Post_to_s_as_s" channel username}}}
</div>
<div class="admin-integrations-new-item-description">
{{{_ "Created_at_s_by_s" (dateFormated _createdAt) _createdBy.username}}}
</div>
</div>
<i class="icon-angle-right"></i>
</div>
</a>
{{/if}}
{{else}}
<h1>{{_ "There_are_no_integrations"}}</h1>
{{/each}}
{{#each integrations}}
{{#if $eq type 'webhook-outgoing'}}
<a href="{{pathFor "admin-integrations-outgoing" id=_id}}">
<div class="admin-integrations-new-item">
<i class="icon-login"></i>
<div class="admin-integrations-new-item-body">
<div class="admin-integrations-new-item-title">
{{_ "Outgoing_WebHook"}} {{#if name}}- {{name}}{{/if}}
</div>
<div class="admin-integrations-new-item-description">
{{{_ "Created_at_s_by_s_triggered_by_s" (dateFormated _createdAt) _createdBy.username (eventTypeI18n event)}}}
</div>
</div>
<i class="icon-angle-right"></i>
</div>
</a>
{{/if}}
{{/each}}
</div>
</div>
</div>
{{/unless}}
</div>
</section>
</template>

@ -1,65 +0,0 @@
import { Template } from 'meteor/templating';
import { ReactiveVar } from 'meteor/reactive-var';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import { Tracker } from 'meteor/tracker';
import moment from 'moment';
import _ from 'underscore';
import { hasAtLeastOnePermission } from '../../../authorization';
import { integrations } from '../../lib/rocketchat';
import { SideNav } from '../../../ui-utils/client';
import { APIClient } from '../../../utils/client';
const ITEMS_COUNT = 50;
Template.integrations.helpers({
hasPermission() {
return hasAtLeastOnePermission([
'manage-outgoing-integrations',
'manage-own-outgoing-integrations',
'manage-incoming-integrations',
'manage-own-incoming-integrations',
]);
},
integrations() {
return Template.instance().integrations.get();
},
dateFormated(date) {
return moment(date).format('L LT');
},
eventTypeI18n(event) {
return TAPi18n.__(integrations.outgoingEvents[event].label);
},
});
Template.integrations.onRendered(() => {
Tracker.afterFlush(() => {
SideNav.setFlex('adminFlex');
SideNav.openFlex();
});
});
Template.integrations.onCreated(async function() {
this.integrations = new ReactiveVar([]);
this.offset = new ReactiveVar(0);
this.total = new ReactiveVar(0);
this.autorun(async () => {
const offset = this.offset.get();
const { integrations, total } = await APIClient.v1.get(`integrations.list?sort={"type":1}&count=${ ITEMS_COUNT }&offset=${ offset }`);
this.total.set(total);
this.integrations.set(this.integrations.get().concat(integrations));
});
});
Template.integrations.events({
'scroll .content': _.throttle(function(e, instance) {
if (e.target.scrollTop >= (e.target.scrollHeight - e.target.clientHeight)) {
const integrations = instance.integrations.get();
if (instance.total.get() <= integrations.length) {
return;
}
return instance.offset.set(instance.offset.get() + ITEMS_COUNT);
}
}, 200),
});

@ -1,138 +0,0 @@
<template name="integrationsIncoming">
<section class="page-container page-home page-static page-settings">
{{#header sectionName=pageTitle buttons=true}}
<div class="rc-header__section-button">
<button class="rc-button rc-button--cancel delete" disabled="{{$not canDelete}}"><span>{{_ "Delete"}}</span></button>
<button class="rc-button rc-button--primary save" disabled="{{enabled}}"><span>{{_ "Save_changes"}}</span></button>
</div>
{{/header}}
<div class="content">
<a href="{{pathFor "admin-integrations"}}"><i class="icon-angle-left"></i> {{_ "Back_to_integrations"}}</a><br><br>
<div class="permissions-manager">
{{#if hasPermission}}
<div class="rocket-form">
<div class="section">
<div class="section-content">
<div class="input-line double-col">
<label>{{_ "Enabled"}}</label>
<div>
<label><input type="radio" name="enabled" value="1" checked="{{$eq data.enabled true}}" /> {{_ "True"}}</label>
<label><input type="radio" name="enabled" value="0" checked="{{$neq data.enabled true}}" /> {{_ "False"}}</label>
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Name"}} ({{_ "optional"}})</label>
<div>
<input type="text" class="rc-input__element" name="name" value="{{data.name}}" placeholder="{{_ 'Optional'}}" />
<div class="settings-description secondary-font-color">{{_ "You_should_name_it_to_easily_manage_your_integrations"}}</div>
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Post_to_Channel"}}</label>
<div>
<input type="text" class="rc-input__element" name="channel" value="{{data.channel}}" placeholder="{{_ 'User_or_channel_name'}}" />
<div class="settings-description secondary-font-color">{{_ "Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here"}}</div>
<div class="settings-description secondary-font-color">{{{_ "Start_with_s_for_user_or_s_for_channel_Eg_s_or_s" "@" "#" "@john" "#general"}}}</div>
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Post_as"}}</label>
<div>
<input type="text" class="rc-input__element" name="username" value="{{data.username}}" />
<div class="settings-description secondary-font-color">{{_ "Choose_the_username_that_this_integration_will_post_as"}}</div>
<div class="settings-description secondary-font-color">{{_ "Should_exists_a_user_with_this_username"}}</div>
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Alias"}} ({{_ "optional"}})</label>
<div>
<input type="text" class="rc-input__element" name="alias" value="{{data.alias}}" placeholder="{{_ 'Optional'}}" />
<div class="settings-description secondary-font-color">{{_ "Choose_the_alias_that_will_appear_before_the_username_in_messages"}}</div>
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Avatar_URL"}} ({{_ "optional"}})</label>
<div>
<input type="url" class="rc-input__element" name="avatar" value="{{data.avatar}}" placeholder="{{_ 'Optional'}}" />
<div class="settings-description secondary-font-color">{{_ "You_can_change_a_different_avatar_too"}}</div>
<div class="settings-description secondary-font-color">{{_ "Should_be_a_URL_of_an_image"}}</div>
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Emoji"}} ({{_ "optional"}})</label>
<div>
<input type="text" class="rc-input__element" name="emoji" value="{{data.emoji}}" placeholder="{{_ 'Optional'}}" />
<div class="settings-description secondary-font-color">{{_ "You_can_use_an_emoji_as_avatar"}}</div>
<div class="settings-description secondary-font-color">{{{_ "Example_s" ":ghost:"}}}</div>
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Script_Enabled"}}</label>
<div>
<label><input type="radio" name="scriptEnabled" value="1" checked="{{$eq data.scriptEnabled true}}" /> {{_ "True"}}</label>
<label><input type="radio" name="scriptEnabled" value="0" checked="{{$neq data.scriptEnabled true}}" /> {{_ "False"}}</label>
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Script"}}</label>
<div>
<div class="code-mirror-box">
<div class="title">
{{_ "Script"}}
</div>
{{> CodeMirror name="script" options=editorOptions code=data.script }}
<div class="buttons">
<button class="rc-button rc-button--primary button-fullscreen">{{_ "Full_Screen"}}</button>
<button class="rc-button rc-button--primary button-restore">{{_ "Exit_Full_Screen"}}</button>
</div>
</div>
{{#if data.hasScriptError }}
<div class="code-error-box">
<div class="title color-content-background-color background-error-color">
{{data.scriptError.name}}
</div>
<pre class="script-error background-transparent-lightest error-color error-border">{{data.scriptError.message}}</pre>
</div>
{{/if}}
</div>
</div>
{{#if data.token}}
<div class="input-line double-col">
<label>Webhook URL</label>
<div>
<input type="text" class="rc-input__element" name="webhookurl" value="{{data.url}}" readonly="readonly" />
<div class="settings-description secondary-font-color">{{_ "Send_your_JSON_payloads_to_this_URL"}}</div>
<div class="settings-description secondary-font-color"><button class="clipboard" data-clipboard-target="[name=webhookurl]">{{_ "COPY_TO_CLIPBOARD"}}</button></div>
</div>
</div>
<div class="input-line double-col">
<label>Token</label>
<div>
<input type="text" class="rc-input__element" name="completeToken" value="{{data.completeToken}}" readonly="readonly" />
<div class="settings-description secondary-font-color"><button class="clipboard" data-clipboard-target="[name=completeToken]">{{_ "COPY_TO_CLIPBOARD"}}</button></div>
</div>
</div>
{{/if}}
<div class="input-line double-col">
<label>{{_ "Example"}}</label>
<div>
<pre><code class="code-colors hljs json json-example">{{{exampleJson}}}</code></pre>
{{#if curl}}
<input type="text" class="rc-input__element" name="curl" value="{{curl}}" readonly="readonly" />
<div class="settings-description secondary-font-color"><button class="clipboard" data-clipboard-target="[name=curl]">{{_ "COPY_TO_CLIPBOARD"}}</button></div>
{{/if}}
</div>
</div>
<div class="input-line message-example">
{{> message msg=exampleMsg settings=exampleSettings u=exampleUser}}
</div>
</div>
</div>
</div>
{{else}}
{{_ "Not_authorized"}}
{{/if}}
</div>
</div>
</section>
</template>

@ -1,251 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { ReactiveVar } from 'meteor/reactive-var';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Template } from 'meteor/templating';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import { Tracker } from 'meteor/tracker';
import hljs from 'highlight.js';
import toastr from 'toastr';
import { exampleMsg, exampleSettings, exampleUser } from './messageExample';
import { hasAtLeastOnePermission } from '../../../authorization';
import { modal, SideNav } from '../../../ui-utils/client';
import { t, handleError } from '../../../utils';
import { getIntegration } from '../getIntegration';
Template.integrationsIncoming.onCreated(async function _incomingIntegrationsOnCreated() {
const params = Template.instance().data.params ? Template.instance().data.params() : undefined;
this.integration = new ReactiveVar({});
this.record = new ReactiveVar({
username: 'rocket.cat',
});
if (params && params.id) {
const integration = await getIntegration(params.id, Meteor.userId());
if (integration) {
this.integration.set(integration);
}
}
});
Template.integrationsIncoming.helpers({
exampleMsg,
exampleUser,
exampleSettings,
hasPermission() {
return hasAtLeastOnePermission([
'manage-incoming-integrations',
'manage-own-incoming-integrations',
]);
},
canDelete() {
return this.params && this.params() && typeof this.params().id !== 'undefined';
},
data() {
const data = Template.instance().integration.get();
if (data) {
const completeToken = `${ data._id }/${ data.token }`;
data.url = Meteor.absoluteUrl(`hooks/${ completeToken }`);
data.completeToken = completeToken;
data.hasScriptError = data.scriptEnabled && data.scriptError;
Template.instance().record.set(data);
return data;
}
return Template.instance().record.curValue;
},
exampleJson() {
const record = Template.instance().record.get();
const data = {
username: record.alias,
icon_emoji: record.emoji,
icon_url: record.avatar,
text: 'Example message',
attachments: [{
title: 'Rocket.Chat',
title_link: 'https://rocket.chat',
text: 'Rocket.Chat, the best open source chat',
image_url: '/images/integration-attachment-example.png',
color: '#764FA5',
}],
};
const invalidData = [null, ''];
Object.keys(data).forEach((key) => {
if (invalidData.includes(data[key])) {
delete data[key];
}
});
return hljs.highlight('json', JSON.stringify(data, null, 2)).value;
},
curl() {
const record = Template.instance().record.get();
if (!record.url) {
return;
}
const data = {
username: record.alias,
icon_emoji: record.emoji,
icon_url: record.avatar,
text: 'Example message',
attachments: [{
title: 'Rocket.Chat',
title_link: 'https://rocket.chat',
text: 'Rocket.Chat, the best open source chat',
image_url: '/images/integration-attachment-example.png',
color: '#764FA5',
}],
};
const invalidData = [null, ''];
Object.keys(data).forEach((key) => {
if (invalidData.includes(data[key])) {
delete data[key];
}
});
return `curl -X POST -H 'Content-Type: application/json' --data '${ JSON.stringify(data) }' ${ record.url }`;
},
editorOptions() {
return {
lineNumbers: true,
mode: 'javascript',
gutters: [
// 'CodeMirror-lint-markers'
'CodeMirror-linenumbers',
'CodeMirror-foldgutter',
],
// lint: true,
foldGutter: true,
// lineWrapping: true,
matchBrackets: true,
autoCloseBrackets: true,
matchTags: true,
showTrailingSpace: true,
highlightSelectionMatches: true,
};
},
});
Template.integrationsIncoming.events({
'blur input': (e, t) => {
const value = t.record.curValue || {};
value.name = $('[name=name]').val().trim();
value.alias = $('[name=alias]').val().trim();
value.emoji = $('[name=emoji]').val().trim();
value.avatar = $('[name=avatar]').val().trim();
value.channel = $('[name=channel]').val().trim();
value.username = $('[name=username]').val().trim();
t.record.set(value);
},
'click .rc-header__section-button > .delete': () => {
const params = Template.instance().data.params();
modal.open({
title: t('Are_you_sure'),
text: t('You_will_not_be_able_to_recover'),
type: 'warning',
showCancelButton: true,
confirmButtonColor: '#DD6B55',
confirmButtonText: t('Yes_delete_it'),
cancelButtonText: t('Cancel'),
closeOnConfirm: false,
html: false,
}, () => {
Meteor.call('deleteIncomingIntegration', params.id, (err) => {
if (err) {
return handleError(err);
}
modal.open({
title: t('Deleted'),
text: t('Your_entry_has_been_deleted'),
type: 'success',
timer: 1000,
showConfirmButton: false,
});
FlowRouter.go('admin-integrations');
});
});
},
'click .button-fullscreen': () => {
const codeMirrorBox = $('.code-mirror-box');
codeMirrorBox.addClass('code-mirror-box-fullscreen content-background-color');
codeMirrorBox.find('.CodeMirror')[0].CodeMirror.refresh();
},
'click .button-restore': () => {
const codeMirrorBox = $('.code-mirror-box');
codeMirrorBox.removeClass('code-mirror-box-fullscreen content-background-color');
codeMirrorBox.find('.CodeMirror')[0].CodeMirror.refresh();
},
'click .rc-header__section-button > .save': () => {
const enabled = $('[name=enabled]:checked').val().trim();
const name = $('[name=name]').val().trim();
const alias = $('[name=alias]').val().trim();
const emoji = $('[name=emoji]').val().trim();
const avatar = $('[name=avatar]').val().trim();
const channel = $('[name=channel]').val().trim();
const username = $('[name=username]').val().trim();
const scriptEnabled = $('[name=scriptEnabled]:checked').val().trim();
const script = $('[name=script]').val().trim();
if (channel === '') {
return toastr.error(TAPi18n.__('The_channel_name_is_required'));
}
if (username === '') {
return toastr.error(TAPi18n.__('The_username_is_required'));
}
const integration = {
enabled: enabled === '1',
channel,
username,
alias: alias !== '' ? alias : undefined,
emoji: emoji !== '' ? emoji : undefined,
avatar: avatar !== '' ? avatar : undefined,
name: name !== '' ? name : undefined,
script: script !== '' ? script : undefined,
scriptEnabled: scriptEnabled === '1',
};
const params = Template.instance().data.params ? Template.instance().data.params() : undefined;
if (params && params.id) {
Meteor.call('updateIncomingIntegration', params.id, integration, (err) => {
if (err) {
return handleError(err);
}
toastr.success(TAPi18n.__('Integration_updated'));
});
} else {
Meteor.call('addIncomingIntegration', integration, (err, data) => {
if (err) {
return handleError(err);
}
toastr.success(TAPi18n.__('Integration_added'));
FlowRouter.go('admin-integrations-incoming', { id: data._id });
});
}
},
});
Template.integrationsIncoming.onRendered(() => {
Tracker.afterFlush(() => {
SideNav.setFlex('adminFlex');
SideNav.openFlex();
});
});

@ -1,51 +0,0 @@
<template name="integrationsNew">
<section class="page-container page-home page-static page-settings">
{{> header sectionName=pageTitle}}
<div class="content">
<a href="{{pathFor "admin-integrations"}}"><i class="icon-angle-left"></i> {{_ "Back_to_integrations"}}</a><br><br>
{{#if hasPermission}}
<div class="rocket-form">
<div class="section">
<div class="admin-integrations-new-panel">
{{#if canAddIncomingIntegration}}
<a href="{{pathFor "admin-integrations-incoming"}}">
<div class="admin-integrations-new-item">
<i class="icon-login"></i>
<div class="admin-integrations-new-item-body">
<div class="admin-integrations-new-item-title">
{{_ "Incoming_WebHook"}}
</div>
<div class="admin-integrations-new-item-description">
{{_ "Send_data_into_RocketChat_in_realtime"}}
</div>
</div>
<i class="icon-angle-right"></i>
</div>
</a>
{{/if}}
{{#if canAddOutgoingIntegration}}
<a href="{{pathFor "admin-integrations-outgoing"}}">
<div class="admin-integrations-new-item">
<i class="icon-logout"></i>
<div class="admin-integrations-new-item-body">
<div class="admin-integrations-new-item-title">
{{_ "Outgoing_WebHook"}}
</div>
<div class="admin-integrations-new-item-description">
{{_ "Outgoing_WebHook_Description"}}
</div>
</div>
<i class="icon-angle-right"></i>
</div>
</a>
{{/if}}
</div>
</div>
</div>
{{else}}
{{_ "Not_authorized"}}
{{/if}}
</div>
</section>
</template>

@ -1,35 +0,0 @@
import { Template } from 'meteor/templating';
import { Tracker } from 'meteor/tracker';
import { hasAtLeastOnePermission } from '../../../authorization';
import { SideNav } from '../../../ui-utils/client';
Template.integrationsNew.helpers({
hasPermission() {
return hasAtLeastOnePermission([
'manage-outgoing-integrations',
'manage-own-outgoing-integrations',
'manage-incoming-integrations',
'manage-own-incoming-integrations',
]);
},
canAddIncomingIntegration() {
return hasAtLeastOnePermission([
'manage-incoming-integrations',
'manage-own-incoming-integrations',
]);
},
canAddOutgoingIntegration() {
return hasAtLeastOnePermission([
'manage-outgoing-integrations',
'manage-own-outgoing-integrations',
]);
},
});
Template.integrationsNew.onRendered(() => {
Tracker.afterFlush(() => {
SideNav.setFlex('adminFlex');
SideNav.openFlex();
});
});

@ -1,241 +0,0 @@
<template name="integrationsOutgoing">
<section class="page-container page-home page-static page-settings">
{{#header sectionName=pageTitle buttons=true}}
<div class="rc-header__section-button">
<button class="rc-button rc-button--secondary history" disabled="{{$not showHistoryButton}}">{{_ "History"}}</button>
<button class="rc-button rc-button--cancel delete" disabled="{{$not canDelete}}"><span>{{_ "Delete"}}</span></button>
<button class="rc-button rc-button--primary save" disabled="{{$not hasTypeSelected}}"><span>{{_ "Save_changes"}}</span></button>
</div>
{{/header}}
<div class="content">
<a href="{{pathFor "admin-integrations"}}"><i class="icon-angle-left"></i> {{_ "Back_to_integrations"}}</a><br><br>
{{#unless hasPermission}}
<p>{{_ "You_are_not_authorized_to_view_this_page"}}</p>
{{else}}
<div class="rocket-form">
<div class="section">
<div class="section-title">
<div class="section-title-text">{{_ "Webhook Details"}}</div>
<div class="section-title-right">
<button class="rc-button rc-button--primary collapse"><span>{{_ "Collapse"}}</span></button>
</div>
</div>
<div class="section-content">
<div class="input-line double-col">
<label>{{_ "Event_Trigger"}}</label>
<div>
<select name="event" class="required rc-input__element">
<option value=""></option>
{{#each eventTypes}}
<option value="{{value}}" selected="{{$eq data.event value}}" dir="auto">{{_ label}}</option>
{{/each}}
</select>
<div class="settings-description">{{_ "Event_Trigger_Description"}}</div>
</div>
</div>
{{#if hasTypeSelected}}
<div class="input-line double-col">
<label>{{_ "Enabled"}}</label>
<div>
<label><input type="radio" name="enabled" value="1" checked="{{$eq data.enabled true}}" /> {{_ "True"}}</label>
<label><input type="radio" name="enabled" value="0" checked="{{$neq data.enabled true}}" /> {{_ "False"}}</label>
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Name"}} ({{_ "optional"}})</label>
<div>
<input type="text" class="rc-input__element" name="name" value="{{data.name}}" placeholder="{{_ 'Optional'}}" />
<div class="settings-description">{{_ "You_should_name_it_to_easily_manage_your_integrations"}}</div>
</div>
</div>
{{#if shouldDisplayChannel}}
<div class="input-line double-col">
<label>{{_ "Channel"}}</label>
<div>
<input type="text" class="rc-input__element" name="channel" value="{{data.channel}}" placeholder="{{_ 'User_or_channel_name'}}" />
<div class="settings-description">{{_ "Channel_to_listen_on"}}</div>
<div class="settings-description">{{{_ "Start_with_s_for_user_or_s_for_channel_Eg_s_or_s" "@" "#" "@john" "#general"}}}</div>
<div class="settings-description">{{{_ "Integrations_for_all_channels"}}}</div>
</div>
</div>
{{/if}}
{{#if shouldDisplayTriggerWords}}
<div class="input-line double-col">
<label>{{_ "Trigger_Words"}} ({{_ "optional"}})</label>
<div>
<input type="text" class="rc-input__element" name="triggerWords" value="{{join data.triggerWords ','}}" />
<div class="settings-description">{{_ "When a line starts with one of these words, post to the URL(s) below"}}</div>
<div class="settings-description">{{_ "Separate multiple words with commas"}}</div>
</div>
</div>
{{/if}}
{{#if shouldDisplayTargetRoom}}
<div class="input-line double-col">
<label>{{_ "TargetRoom"}}</label>
<div>
<input type="text" class="rc-input__element" name="targetRoom" value="{{data.targetRoom}}" placeholder="{{_ 'User_or_channel_name'}}" />
<div class="settings-description">{{_ "TargetRoom_Description"}}</div>
<div class="settings-description">{{{_ "Start_with_s_for_user_or_s_for_channel_Eg_s_or_s" "@" "#" "@john" "#general"}}}</div>
</div>
</div>
{{/if}}
<div class="input-line double-col">
<label>{{_ "URLs"}}</label>
<div>
<textarea name="urls" class="rc-input__element" style="height: 100px;">{{join data.urls "\n"}}</textarea>
<div class="settings-description">{{_ "Enter as many URLs as you like, one per line, please"}}</div>
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Impersonate_user"}}</label>
<div>
<label><input type="radio" name="impersonateUser" value="1" checked="{{$eq data.impersonateUser true}}" /> {{_ "True"}}</label>
<label><input type="radio" name="impersonateUser" value="0" checked="{{$neq data.impersonateUser true}}" /> {{_ "False"}}</label>
<div class="settings-description">{{_ "Impersonate_user_description"}}</div>
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Post_as"}}</label>
<div>
<input type="text" class="rc-input__element" name="username" value="{{data.username}}" />
<div class="settings-description">{{_ "Choose_the_username_that_this_integration_will_post_as"}}</div>
<div class="settings-description">{{_ "Should_exists_a_user_with_this_username"}}</div>
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Alias"}} ({{_ "optional"}})</label>
<div>
<input type="text" class="rc-input__element" name="alias" value="{{data.alias}}" placeholder="{{_ 'Optional'}}" />
<div class="settings-description">{{_ "Choose_the_alias_that_will_appear_before_the_username_in_messages"}}</div>
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Avatar_URL"}} ({{_ "optional"}})</label>
<div>
<input type="url" class="rc-input__element" name="avatar" value="{{data.avatar}}" placeholder="{{_ 'Optional'}}" />
<div class="settings-description">{{_ "You_can_change_a_different_avatar_too"}}</div>
<div class="settings-description">{{_ "Should_be_a_URL_of_an_image"}}</div>
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Emoji"}} ({{_ "optional"}})</label>
<div>
<input type="text" class="rc-input__element" name="emoji" value="{{data.emoji}}" placeholder="{{_ 'Optional'}}" />
<div class="settings-description">{{_ "You_can_use_an_emoji_as_avatar"}}</div>
<div class="settings-description">{{{_ "Example_s" ":ghost:"}}}</div>
</div>
</div>
<div class="input-line double-col">
<label>Token ({{_ "optional"}})</label>
<div>
<input type="text" class="rc-input__element" name="token" value="{{data.token}}" />
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Script_Enabled"}}</label>
<div>
<label><input type="radio" name="scriptEnabled" value="1" checked="{{$eq data.scriptEnabled true}}" /> {{_ "True"}}</label>
<label><input type="radio" name="scriptEnabled" value="0" checked="{{$neq data.scriptEnabled true}}" /> {{_ "False"}}</label>
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Script"}}</label>
<div>
<div class="code-mirror-box">
<div class="title">
{{_ "Script"}}
</div>
{{> CodeMirror name="script" options=editorOptions code=data.script }}
<div class="buttons">
<button class="rc-button rc-button--primary button-fullscreen">{{_ "Full_Screen"}}</button>
<button class="rc-button rc-button--primary button-restore">{{_ "Exit_Full_Screen"}}</button>
</div>
</div>
{{#if data.hasScriptError }}
<div class="code-error-box">
<div class="title color-content-background-color background-error-color">
{{data.scriptError.name}}
</div>
<pre class="script-error background-transparent-lightest error-color error-border">{{data.scriptError.message}}</pre>
</div>
{{/if}}
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Responding"}}</label>
<div>
<div class="settings-description">{{{_ "If the handler wishes to post a response back into the channel, the following JSON should be returned as the body of the response:"}}}</div>
<pre><code class="code-colors hljs json json-example">{{{exampleJson}}}</code></pre>
<div class="settings-description">{{{_ "Empty bodies or bodies with an empty text property will simply be ignored. Non-200 responses will be retried a reasonable number of times. A response will be posted using the alias and avatar specified above. You can override these informations as in the example above."}}}</div>
</div>
</div>
<div class="input-line message-example">
{{> message msg=exampleMsg settings=exampleSettings u=exampleUser}}
</div>
{{/if}}
</div>
</div>
{{#if hasTypeSelected}}
<div class="section section-collapsed">
<div class="section-title">
<div class="section-title-text">{{_ "Integration_Advanced_Settings"}}</div>
<div class="section-title-right">
<button class="rc-button rc-button--primary expand"><span>{{_ "Expand"}}</span></button>
</div>
</div>
<div class="section-content">
<div class="input-line double-col">
<label>{{_ "Integration_Retry_Failed_Url_Calls"}}</label>
<div>
<label><input type="radio" name="retryFailedCalls" value="1" checked="{{$eq data.retryFailedCalls true}}" /> {{_ "True"}}</label>
<label><input type="radio" name="retryFailedCalls" value="0" checked="{{$neq data.retryFailedCalls true}}" /> {{_ "False"}}</label>
<div class="settings-description">{{_ "Integration_Retry_Failed_Url_Calls_Description"}}</div>
</div>
</div>
{{#if data.retryFailedCalls}}
<div class="input-line double-col">
<label>{{_ "Integration_Retry_Count"}}</label>
<div>
<input type="number" class="rc-input__element" name="retryCount" value="{{data.retryCount}}" />
<div class="settings-description">{{_ "Integration_Retry_Count_Description"}}</div>
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Integration_Retry_Delay"}}</label>
<div>
<select name="retryDelay" class="required">
<option value="powers-of-ten" selected="{{$eq data.retryDelay 'powers-of-ten'}}" dir="auto">powers-of-ten</option>
<option value="powers-of-two" selected="{{$eq data.retryDelay 'powers-of-two'}}" dir="auto">powers-of-two</option>
<option value="increments-of-two" selected="{{$eq data.retryDelay 'increments-of-two'}}" dir="auto">increments-of-two</option>
</select>
<div class="settings-description">{{{_ "Integration_Retry_Delay_Description"}}}</div>
</div>
</div>
{{/if}}
{{#if shouldDisplayTriggerWords}}
<div class="input-line double-col">
<label>{{_ "Integration_Word_Trigger_Placement"}}</label>
<div>
<label><input type="radio" name="triggerWordAnywhere" value="1" checked="{{$eq data.triggerWordAnywhere true}}" /> {{_ "True"}}</label>
<label><input type="radio" name="triggerWordAnywhere" value="0" checked="{{$neq data.triggerWordAnywhere true}}" /> {{_ "False"}}</label>
<div class="settings-description">{{_ "Integration_Word_Trigger_Placement_Description"}}</div>
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Integration_Run_When_Message_Is_Edited"}}</label>
<div>
<label><input type="radio" name="runOnEdits" value="1" checked="{{$eq data.runOnEdits true}}" /> {{_ "True"}}</label>
<label><input type="radio" name="runOnEdits" value="0" checked="{{$neq data.runOnEdits true}}" /> {{_ "False"}}</label>
<div class="settings-description">{{{_ "Integration_Run_When_Message_Is_Edited_Description"}}}</div>
</div>
</div>
{{/if}}
</div>
</div>
{{/if}}
</div>
{{/unless}}
</div>
</section>
</template>

@ -1,353 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { ReactiveVar } from 'meteor/reactive-var';
import { Random } from 'meteor/random';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Template } from 'meteor/templating';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import { Tracker } from 'meteor/tracker';
import hljs from 'highlight.js';
import toastr from 'toastr';
import { exampleMsg, exampleSettings, exampleUser } from './messageExample';
import { hasAtLeastOnePermission } from '../../../authorization';
import { modal, SideNav } from '../../../ui-utils';
import { t, handleError } from '../../../utils/client';
import { integrations } from '../../lib/rocketchat';
import { getIntegration } from '../getIntegration';
Template.integrationsOutgoing.onCreated(async function _integrationsOutgoingOnCreated() {
const params = Template.instance().data.params ? Template.instance().data.params() : undefined;
this.record = new ReactiveVar({
username: 'rocket.cat',
token: Random.id(24),
retryFailedCalls: true,
retryCount: 6,
retryDelay: 'powers-of-ten',
runOnEdits: true,
});
this.updateRecord = () => {
this.record.set({
enabled: $('[name=enabled]:checked').val().trim() === '1',
event: $('[name=event]').val().trim(),
name: $('[name=name]').val().trim(),
alias: $('[name=alias]').val().trim(),
emoji: $('[name=emoji]').val().trim(),
avatar: $('[name=avatar]').val().trim(),
channel: $('[name=channel]').val() ? $('[name=channel]').val().trim() : undefined,
username: $('[name=username]').val().trim(),
triggerWords: $('[name=triggerWords]').val() ? $('[name=triggerWords]').val().trim() : undefined,
urls: $('[name=urls]').val().trim(),
token: $('[name=token]').val().trim(),
scriptEnabled: $('[name=scriptEnabled]:checked').val().trim() === '1',
script: $('[name=script]').val().trim(),
targetRoom: $('[name=targetRoom]').val() ? $('[name=targetRoom]').val().trim() : undefined,
triggerWordAnywhere: $('[name=triggerWordAnywhere]:checked').val().trim() === '1',
retryFailedCalls: $('[name=retryFailedCalls]:checked').val().trim() === '1',
retryCount: $('[name=retryCount]').val() ? $('[name=retryCount]').val().trim() : 6,
retryDelay: $('[name=retryDelay]').val() ? $('[name=retryDelay]').val().trim() : 'powers-of-ten',
runOnEdits: $('[name=runOnEdits]:checked').val().trim() === '1',
});
};
const integration = await getIntegration(params.id, Meteor.userId());
if (params.id && !integration) {
toastr.error(TAPi18n.__('No_integration_found'));
FlowRouter.go('admin-integrations');
return;
}
integration.hasScriptError = integration.scriptEnabled && integration.scriptError;
this.record.set(integration);
});
Template.integrationsOutgoing.helpers({
exampleMsg,
exampleUser,
exampleSettings,
join(arr, sep) {
if (!arr || !arr.join) {
return arr;
}
return arr.join(sep);
},
showHistoryButton() {
return this.params && this.params() && typeof this.params().id !== 'undefined';
},
hasPermission() {
return hasAtLeastOnePermission([
'manage-outgoing-integrations',
'manage-own-outgoing-integrations',
]);
},
data() {
return Template.instance().record.get();
},
canDelete() {
return this.params && this.params() && typeof this.params().id !== 'undefined';
},
eventTypes() {
return Object.values(integrations.outgoingEvents);
},
hasTypeSelected() {
const record = Template.instance().record.get();
return typeof record.event === 'string' && record.event !== '';
},
shouldDisplayChannel() {
const record = Template.instance().record.get();
return typeof record.event === 'string' && integrations.outgoingEvents[record.event].use.channel;
},
shouldDisplayTriggerWords() {
const record = Template.instance().record.get();
return typeof record.event === 'string' && integrations.outgoingEvents[record.event].use.triggerWords;
},
shouldDisplayTargetRoom() {
const record = Template.instance().record.get();
return typeof record.event === 'string' && integrations.outgoingEvents[record.event].use.targetRoom;
},
exampleJson() {
const record = Template.instance().record.get();
const data = {
username: record.alias,
icon_emoji: record.emoji,
icon_url: record.avatar,
text: 'Response text',
attachments: [{
title: 'Rocket.Chat',
title_link: 'https://rocket.chat',
text: 'Rocket.Chat, the best open source chat',
image_url: '/images/integration-attachment-example.png',
color: '#764FA5',
}],
};
const invalidData = [null, ''];
Object.keys(data).forEach((key) => {
if (invalidData.includes(data[key])) {
delete data[key];
}
});
return hljs.highlight('json', JSON.stringify(data, null, 2)).value;
},
editorOptions() {
return {
lineNumbers: true,
mode: 'javascript',
gutters: [
// "CodeMirror-lint-markers",
'CodeMirror-linenumbers',
'CodeMirror-foldgutter',
],
// lint: true,
foldGutter: true,
// lineWrapping: true,
matchBrackets: true,
autoCloseBrackets: true,
matchTags: true,
showTrailingSpace: true,
highlightSelectionMatches: true,
};
},
});
Template.integrationsOutgoing.events({
'blur input': (e, t) => {
t.updateRecord();
},
'click input[type=radio]': (e, t) => {
t.updateRecord();
},
'change select[name=event]': (e, t) => {
const record = t.record.get();
record.event = $('[name=event]').val().trim();
t.record.set(record);
},
'click .rc-button.history': () => {
FlowRouter.go(`/admin/integrations/outgoing/${ FlowRouter.getParam('id') }/history`);
},
'click .expand': (e) => {
$(e.currentTarget).closest('.section').removeClass('section-collapsed');
$(e.currentTarget).closest('button').removeClass('expand').addClass('collapse').find('span').text(TAPi18n.__('Collapse'));
$('.CodeMirror').each((index, codeMirror) => codeMirror.CodeMirror.refresh());
},
'click .collapse': (e) => {
$(e.currentTarget).closest('.section').addClass('section-collapsed');
$(e.currentTarget).closest('button').addClass('expand').removeClass('collapse').find('span').text(TAPi18n.__('Expand'));
},
'click .rc-header__section-button > .delete': () => {
const params = Template.instance().data.params();
modal.open({
title: t('Are_you_sure'),
text: t('You_will_not_be_able_to_recover'),
type: 'warning',
showCancelButton: true,
confirmButtonColor: '#DD6B55',
confirmButtonText: t('Yes_delete_it'),
cancelButtonText: t('Cancel'),
closeOnConfirm: false,
html: false,
}, () => {
Meteor.call('deleteOutgoingIntegration', params.id, (err) => {
if (err) {
handleError(err);
} else {
modal.open({
title: t('Deleted'),
text: t('Your_entry_has_been_deleted'),
type: 'success',
timer: 1000,
showConfirmButton: false,
});
FlowRouter.go('admin-integrations');
}
});
});
},
'click .button-fullscreen': () => {
$('.code-mirror-box').addClass('code-mirror-box-fullscreen content-background-color');
$('.CodeMirror')[0].CodeMirror.refresh();
},
'click .button-restore': () => {
$('.code-mirror-box').removeClass('code-mirror-box-fullscreen content-background-color');
$('.CodeMirror')[0].CodeMirror.refresh();
},
'click .rc-header__section-button > .save': () => {
const event = $('[name=event]').val().trim();
const enabled = $('[name=enabled]:checked').val().trim();
const name = $('[name=name]').val().trim();
const impersonateUser = $('[name=impersonateUser]:checked').val().trim();
const alias = $('[name=alias]').val().trim();
const emoji = $('[name=emoji]').val().trim();
const avatar = $('[name=avatar]').val().trim();
const username = $('[name=username]').val().trim();
const token = $('[name=token]').val().trim();
const scriptEnabled = $('[name=scriptEnabled]:checked').val().trim();
const script = $('[name=script]').val().trim();
const retryFailedCalls = $('[name=retryFailedCalls]:checked').val().trim();
let urls = $('[name=urls]').val().trim();
if (username === '' && impersonateUser === '0') {
return toastr.error(TAPi18n.__('The_username_is_required'));
}
urls = urls.split('\n').filter((url) => url.trim() !== '');
if (urls.length === 0) {
return toastr.error(TAPi18n.__('You_should_inform_one_url_at_least'));
}
let triggerWords;
let triggerWordAnywhere;
let runOnEdits;
if (integrations.outgoingEvents[event].use.triggerWords) {
triggerWords = $('[name=triggerWords]').val().trim();
triggerWords = triggerWords.split(',').filter((word) => word.trim() !== '');
triggerWordAnywhere = $('[name=triggerWordAnywhere]:checked').val().trim();
runOnEdits = $('[name=runOnEdits]:checked').val().trim();
}
let channel;
if (integrations.outgoingEvents[event].use.channel) {
channel = $('[name=channel]').val().trim();
if (!channel || channel.trim() === '') {
return toastr.error(TAPi18n.__('error-the-field-is-required', { field: TAPi18n.__('Channel') }));
}
}
let targetRoom;
if (integrations.outgoingEvents[event].use.targetRoom) {
targetRoom = $('[name=targetRoom]').val().trim();
if (!targetRoom || targetRoom.trim() === '') {
return toastr.error(TAPi18n.__('error-the-field-is-required', { field: TAPi18n.__('TargetRoom') }));
}
}
let retryCount;
let retryDelay;
if (retryFailedCalls === '1') {
retryCount = parseInt($('[name=retryCount]').val().trim());
retryDelay = $('[name=retryDelay]').val().trim();
}
const integration = {
event: event !== '' ? event : undefined,
enabled: enabled === '1',
username,
channel: channel !== '' ? channel : undefined,
targetRoom: targetRoom !== '' ? targetRoom : undefined,
alias: alias !== '' ? alias : undefined,
emoji: emoji !== '' ? emoji : undefined,
avatar: avatar !== '' ? avatar : undefined,
name: name !== '' ? name : undefined,
triggerWords: triggerWords !== '' ? triggerWords : undefined,
urls: urls !== '' ? urls : undefined,
token: token !== '' ? token : undefined,
script: script !== '' ? script : undefined,
scriptEnabled: scriptEnabled === '1',
impersonateUser: impersonateUser === '1',
retryFailedCalls: retryFailedCalls === '1',
retryCount: retryCount || 6,
retryDelay: retryDelay || 'powers-of-ten',
triggerWordAnywhere: triggerWordAnywhere === '1',
runOnEdits: runOnEdits === '1',
};
const params = Template.instance().data.params ? Template.instance().data.params() : undefined;
if (params && params.id) {
Meteor.call('updateOutgoingIntegration', params.id, integration, (err) => {
if (err) {
return handleError(err);
}
toastr.success(TAPi18n.__('Integration_updated'));
});
} else {
Meteor.call('addOutgoingIntegration', integration, (err, data) => {
if (err) {
return handleError(err);
}
toastr.success(TAPi18n.__('Integration_added'));
FlowRouter.go('admin-integrations-outgoing', { id: data._id });
});
}
},
});
Template.integrationsOutgoing.onRendered(() => {
Tracker.afterFlush(() => {
SideNav.setFlex('adminFlex');
SideNav.openFlex();
});
});

@ -1,143 +0,0 @@
<template name="integrationsOutgoingHistory">
<section class="page-container page-home page-static page-settings">
{{#header sectionName=pageTitle buttons=true}}
<div class="rc-header__section-button">
<button class="rc-button rc-button--cancel clear-history">{{_ "clear_history"}}</button>
</div>
{{/header}}
<div class="content">
{{#unless hasPermission}}
<p>{{_ "You_are_not_authorized_to_view_this_page"}}</p>
{{else}}
<a href="{{pathFor "admin-integrations-outgoing" id=integrationId}}"><i class="icon-angle-left"></i> {{_ "Back_to_integration_detail"}}</a><br><br>
<div class="rocket-form">
{{#each history in histories}}
<div class="section section-collapsed">
<div class="section-title">
<div class="section-title-text">
<i class="{{iconClass history}}"></i>
{{formatDate history._createdAt}}
</div>
<div class="section-title-right">
<button class="rc-button rc-button--secondary replay" data-history-id="{{ history._id }}">Replay</button>
<button class="rc-button rc-button--primary expand">
<span>{{_ "Expand"}}</span>
</button>
</div>
</div>
<div class="section-content">
<div class="input-line double-col">
<label>{{_ "Status"}}</label>
<div>
<input class="input-monitor" type="text" disabled value="{{statusI18n history.error}}">
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Integration_Outgoing_WebHook_History_Time_Triggered"}}</label>
<div>
<input class="input-monitor" type="text" disabled value="{{formatDateDetail history._createdAt}}">
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Integration_Outgoing_WebHook_History_Time_Ended_Or_Error"}}</label>
<div>
<input class="input-monitor" type="text" disabled value="{{formatDateDetail history._updatedAt}}">
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Event_Trigger"}}</label>
<div>
<input class="input-monitor" type="text" disabled value="{{eventTypei18n history.event}}">
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Integration_Outgoing_WebHook_History_Trigger_Step"}}</label>
<div>
<input class="input-monitor" type="text" disabled value="{{history.step}}">
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Integration_Outgoing_WebHook_History_Data_Passed_To_Trigger"}}</label>
<div>
<pre><code class="code-colors hljs json">{{{ jsonStringify history.data }}}</code></pre>
</div>
</div>
{{#if hasProperty history 'prepareSentMessage'}}
<div class="input-line double-col">
<label>{{_ "Integration_Outgoing_WebHook_History_Messages_Sent_From_Prepare_Script"}}</label>
<div>
<pre><code class="code-colors hljs json">{{{ jsonStringify history.prepareSentMessage }}}</code></pre>
</div>
</div>
{{/if}}
{{#if hasProperty history 'processSentMessage'}}
<div class="input-line double-col">
<label>{{_ "Integration_Outgoing_WebHook_History_Messages_Sent_From_Process_Script"}}</label>
<div>
<pre><code class="code-colors hljs json">{{{ jsonStringify history.processSentMessage }}}</code></pre>
</div>
</div>
{{/if}}
{{#if hasProperty history 'url'}}
<div class="input-line double-col">
<label>{{_ "URL"}}</label>
<div>
<pre><code class="code-colors hljs json">{{ history.url }}</code></pre>
</div>
</div>
{{/if}}
{{#if hasProperty history 'httpCallData'}}
<div class="input-line double-col">
<label>{{_ "Integration_Outgoing_WebHook_History_Data_Passed_To_URL"}}</label>
<div>
<pre><code class="code-colors hljs json">{{{jsonStringify history.httpCallData }}}</code></pre>
</div>
</div>
{{/if}}
{{#if hasProperty history 'httpError'}}
<div class="input-line double-col">
<label>{{_ "Integration_Outgoing_WebHook_History_Http_Response_Error"}}</label>
<div>
<pre><code class="code-colors hljs json">{{{ jsonStringify history.httpError }}}</code></pre>
</div>
</div>
{{/if}}
{{#if hasProperty history 'httpResult'}}
<div class="input-line double-col">
<label>{{_ "Integration_Outgoing_WebHook_History_Http_Response"}}</label>
<div>
<pre><code class="code-colors hljs json">{{{ jsonStringify history.httpResult }}}</code></pre>
</div>
</div>
{{/if}}
{{#if hasProperty history 'errorStack'}}
<div class="input-line double-col">
<label>{{_ "Integration_Outgoing_WebHook_History_Error_Stacktrace"}}</label>
<div>
<pre><code class="code-colors hljs json">{{{ jsonStringify history.errorStack }}}</code></pre>
</div>
</div>
{{/if}}
</div>
</div>
{{else}}
<div class="section">
<div class="section-title">
<div class="section-title-text">
{{_ "Integration_Outgoing_WebHook_No_History"}}
</div>
</div>
</div>
{{/each}}
{{#if isLoading}}
<div class="load-more">
{{> loading}}
</div>
{{/if}}
</div>
{{/unless}}
</div>
</section>
</template>

@ -1,191 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { ReactiveVar } from 'meteor/reactive-var';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Template } from 'meteor/templating';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import { Tracker } from 'meteor/tracker';
import _ from 'underscore';
import hljs from 'highlight.js';
import moment from 'moment';
import toastr from 'toastr';
import { handleError } from '../../../utils';
import { hasAtLeastOnePermission } from '../../../authorization';
import { integrations } from '../../lib/rocketchat';
import { SideNav } from '../../../ui-utils/client';
import { APIClient } from '../../../utils/client';
import { getIntegration } from '../getIntegration';
import { integrationHistoryStreamer } from '../streamer';
const HISTORY_COUNT = 25;
Template.integrationsOutgoingHistory.onCreated(async function _integrationsOutgoingHistoryOnCreated() {
const params = Template.instance().data.params ? Template.instance().data.params() : undefined;
this.isLoading = new ReactiveVar(false);
this.history = new ReactiveVar([]);
this.offset = new ReactiveVar(0);
this.total = new ReactiveVar(0);
if (params && params.id) {
integrationHistoryStreamer.on(params.id, ({ type, id, diff, data }) => {
const histories = this.history.get();
if (type === 'inserted') {
this.history.set([{ ...data }].concat(histories));
return;
}
if (type === 'updated') {
const history = histories.find(({ _id }) => _id === id);
Object.assign(history, diff);
this.history.set(histories);
return;
}
if (type === 'removed') {
this.history.set([]);
}
});
const integration = await getIntegration(params.id, Meteor.userId());
if (!integration) {
toastr.error(TAPi18n.__('No_integration_found'));
return FlowRouter.go('admin-integrations');
}
this.autorun(async () => {
this.isLoading.set(true);
const { history, total } = await APIClient.v1.get(`integrations.history?id=${ integration._id }&count=${ HISTORY_COUNT }&offset=${ this.offset.get() }`);
this.history.set(this.history.get().concat(history));
this.total.set(total);
this.isLoading.set(false);
});
} else {
toastr.error(TAPi18n.__('No_integration_found'));
FlowRouter.go('admin-integrations');
}
});
Template.integrationsOutgoingHistory.helpers({
hasPermission() {
return hasAtLeastOnePermission(['manage-outgoing-integrations', 'manage-own-outgoing-integrations']);
},
isLoading() {
return Template.instance().isLoading.get();
},
histories() {
return Template.instance().history.get().sort((a, b) => {
if (+a._updatedAt < +b._updatedAt) {
return 1;
}
if (+a._updatedAt > +b._updatedAt) {
return -1;
}
return 0;
});
},
hasProperty(history, property) {
return typeof history[property] !== 'undefined' || history[property] != null;
},
iconClass(history) {
if (typeof history.error !== 'undefined' && history.error) {
return 'icon-cancel-circled error-color';
} if (history.finished) {
return 'icon-ok-circled success-color';
}
return 'icon-help-circled';
},
statusI18n(error) {
return typeof error !== 'undefined' && error ? TAPi18n.__('Failure') : TAPi18n.__('Success');
},
formatDate(date) {
return moment(date).format('L LTS');
},
formatDateDetail(date) {
return moment(date).format('L HH:mm:ss:SSSS');
},
eventTypei18n(event) {
return TAPi18n.__(integrations.outgoingEvents[event].label);
},
jsonStringify(data) {
if (!data) {
return '';
} if (typeof data === 'object') {
return hljs.highlight('json', JSON.stringify(data, null, 2)).value;
}
return hljs.highlight('json', data).value;
},
integrationId() {
return this.params && this.params() && this.params().id;
},
});
Template.integrationsOutgoingHistory.events({
'click .expand': (e) => {
$(e.currentTarget).closest('.section').removeClass('section-collapsed');
$(e.currentTarget).closest('button').removeClass('expand').addClass('collapse').find('span').text(TAPi18n.__('Collapse'));
$('.CodeMirror').each((index, codeMirror) => codeMirror.CodeMirror.refresh());
},
'click .collapse': (e) => {
$(e.currentTarget).closest('.section').addClass('section-collapsed');
$(e.currentTarget).closest('button').addClass('expand').removeClass('collapse').find('span').text(TAPi18n.__('Expand'));
},
'click .replay': (e, t) => {
if (!t || !t.data || !t.data.params || !t.data.params().id) {
return;
}
const historyId = $(e.currentTarget).attr('data-history-id');
Meteor.call('replayOutgoingIntegration', { integrationId: t.data.params().id, historyId }, (e) => {
if (e) {
handleError(e);
}
});
},
'click .clear-history': (e, t) => {
if (!t || !t.data || !t.data.params || !t.data.params().id) {
return;
}
Meteor.call('clearIntegrationHistory', t.data.params().id, (e) => {
if (e) {
handleError(e);
return;
}
toastr.success(TAPi18n.__('Integration_History_Cleared'));
t.history.set([]);
});
},
'scroll .content': _.throttle((e, instance) => {
const history = instance.history.get();
if ((e.target.scrollTop >= e.target.scrollHeight - e.target.clientHeight) && instance.total.get() > history.length) {
instance.offset.set(instance.offset.get() + HISTORY_COUNT);
}
}, 200),
});
Template.integrationsOutgoingHistory.onRendered(() => {
Tracker.afterFlush(() => {
SideNav.setFlex('adminFlex');
SideNav.openFlex();
});
});

@ -1,39 +0,0 @@
import { Template } from 'meteor/templating';
import { Random } from 'meteor/random';
export const exampleMsg = () => {
const record = Template.instance().record.get();
return {
_id: Random.id(),
alias: record.alias,
emoji: record.emoji,
avatar: record.avatar,
msg: 'Example message',
bot: {
i: Random.id(),
},
groupable: false,
attachments: [{
title: 'Rocket.Chat',
title_link: 'https://rocket.chat',
text: 'Rocket.Chat, the best open source chat',
image_url: '/images/integration-attachment-example.png',
color: '#764FA5',
}],
ts: new Date(),
u: {
_id: Random.id(),
username: record.username,
},
};
};
export const exampleUser = () => ({
u: {
_id: Random.id(),
},
});
export const exampleSettings = () => ({
settings: {},
});

@ -1,28 +0,0 @@
import { BlazeLayout } from 'meteor/kadira:blaze-layout';
import { registerAdminRoute } from '../../../../client/admin';
import { t } from '../../../utils';
registerAdminRoute('/oauth-apps', {
name: 'admin-oauth-apps',
async action() {
await import('./views');
return BlazeLayout.render('main', {
center: 'oauthApps',
pageTitle: t('OAuth_Applications'),
});
},
});
registerAdminRoute('/oauth-app/:id?', {
name: 'admin-oauth-app',
async action(params) {
await import('./views');
return BlazeLayout.render('main', {
center: 'pageSettingsContainer',
pageTitle: t('OAuth_Application'),
pageTemplate: 'oauthApp',
params,
});
},
});

@ -1,4 +0,0 @@
import './oauthApp.html';
import './oauthApp';
import './oauthApps.html';
import './oauthApps';

@ -1,72 +0,0 @@
<template name="oauthApp">
<div class="permissions-manager">
{{#if hasPermission}}
<a href="{{pathFor "admin-oauth-apps"}}"><i class="icon-angle-left"></i> {{_ "Back_to_applications"}}</a><br><br>
<div class="rocket-form">
<div class="section">
<div class="section-content">
<div class="input-line double-col">
<label>{{_ "Active"}}</label>
<div>
<label><input class="input-monitor" type="radio" name="active" value="1" checked="{{$eq data.active true}}" /> {{_ "True"}}</label>
<label><input class="input-monitor" type="radio" name="active" value="0" checked="{{$eq data.active false}}" /> {{_ "False"}}</label>
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Application_Name"}}</label>
<div>
<input type="text" class="rc-input__element" name="name" value="{{data.name}}" />
<div class="settings-description secondary-font-color">{{_ "Give_the_application_a_name_This_will_be_seen_by_your_users"}}</div>
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Redirect_URI"}}</label>
<div>
<textarea class="rc-input__element" name="redirectUri" rows="4" style="height: auto">{{data.redirectUri}}</textarea>
<div class="settings-description secondary-font-color">{{_ "After_OAuth2_authentication_users_will_be_redirected_to_this_URL"}}</div>
</div>
</div>
{{#if data.clientId}}
<div class="input-line double-col">
<label>{{_ "Client_ID"}}</label>
<div>
<input type="text" class="rc-input__element" name="clientId" value="{{data.clientId}}" readonly="readonly" />
<div class="settings-description secondary-font-color"><button class="clipboard" data-clipboard-target="[name=clientId]">{{_ "COPY_TO_CLIPBOARD"}}</button></div>
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Client_Secret"}}</label>
<div>
<input type="text" class="rc-input__element" name="clientSecret" value="{{data.clientSecret}}" readonly="readonly" />
<div class="settings-description secondary-font-color"><button class="clipboard" data-clipboard-target="[name=clientSecret]">{{_ "COPY_TO_CLIPBOARD"}}</button></div>
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Authorization_URL"}}</label>
<div>
<input type="text" class="rc-input__element" name="authorization_url" value="{{data.authorization_url}}" readonly="readonly" />
<div class="settings-description secondary-font-color"><button class="clipboard" data-clipboard-target="[name=authorization_url]">{{_ "COPY_TO_CLIPBOARD"}}</button></div>
</div>
</div>
<div class="input-line double-col">
<label>{{_ "Access_Token_URL"}}</label>
<div>
<input type="text" class="rc-input__element" name="access_token_url" value="{{data.access_token_url}}" readonly="readonly" />
<div class="settings-description secondary-font-color"><button class="clipboard" data-clipboard-target="[name=access_token_url]">{{_ "COPY_TO_CLIPBOARD"}}</button></div>
</div>
</div>
{{/if}}
</div>
</div>
<div class="submit">
{{#if data.clientId}}
<button class="rc-button rc-button--danger delete"><i class="icon-trash"></i><span>{{_ "Delete"}}</span></button>
{{/if}}
<button class="rc-button rc-button--primary save"><i class="icon-send"></i><span>{{_ "Save_changes"}}</span></button>
</div>
</div>
{{else}}
{{_ "Not_authorized"}}
{{/if}}
</div>
</template>

@ -1,120 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { ReactiveVar } from 'meteor/reactive-var';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Template } from 'meteor/templating';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import { Tracker } from 'meteor/tracker';
import toastr from 'toastr';
import { hasAllPermission } from '../../../../authorization';
import { modal, SideNav } from '../../../../ui-utils/client';
import { t, handleError } from '../../../../utils';
import { APIClient } from '../../../../utils/client';
Template.oauthApp.onCreated(async function() {
const params = this.data.params();
this.oauthApp = new ReactiveVar({});
this.record = new ReactiveVar({
active: true,
});
if (params && params.id) {
const { oauthApp } = await APIClient.v1.get(`oauth-apps.get?appId=${ params.id }`);
this.oauthApp.set(oauthApp);
}
});
Template.oauthApp.helpers({
hasPermission() {
return hasAllPermission('manage-oauth-apps');
},
data() {
const instance = Template.instance();
if (typeof instance.data.params === 'function') {
const params = instance.data.params();
if (params && params.id) {
const data = Template.instance().oauthApp.get();
if (data) {
data.authorization_url = Meteor.absoluteUrl('oauth/authorize');
data.access_token_url = Meteor.absoluteUrl('oauth/token');
if (Array.isArray(data.redirectUri)) {
data.redirectUri = data.redirectUri.join('\n');
}
Template.instance().record.set(data);
return data;
}
}
}
return Template.instance().record.curValue;
},
});
Template.oauthApp.events({
'click .submit > .delete'() {
const params = Template.instance().data.params();
modal.open({
title: t('Are_you_sure'),
text: t('You_will_not_be_able_to_recover'),
type: 'warning',
showCancelButton: true,
confirmButtonColor: '#DD6B55',
confirmButtonText: t('Yes_delete_it'),
cancelButtonText: t('Cancel'),
closeOnConfirm: false,
html: false,
}, function() {
Meteor.call('deleteOAuthApp', params.id, function() {
modal.open({
title: t('Deleted'),
text: t('Your_entry_has_been_deleted'),
type: 'success',
timer: 1000,
showConfirmButton: false,
});
FlowRouter.go('admin-oauth-apps');
});
});
},
'click .submit > .save'() {
const instance = Template.instance();
const name = $('[name=name]').val().trim();
const active = $('[name=active]:checked').val().trim() === '1';
const redirectUri = $('[name=redirectUri]').val().trim();
if (name === '') {
return toastr.error(TAPi18n.__('The_application_name_is_required'));
}
if (redirectUri === '') {
return toastr.error(TAPi18n.__('The_redirectUri_is_required'));
}
const app = {
name,
active,
redirectUri,
};
if (typeof instance.data.params === 'function') {
const params = instance.data.params();
if (params && params.id) {
return Meteor.call('updateOAuthApp', params.id, app, function(err) {
if (err != null) {
return handleError(err);
}
toastr.success(TAPi18n.__('Application_updated'));
});
}
}
Meteor.call('addOAuthApp', app, function(err, data) {
if (err != null) {
return handleError(err);
}
toastr.success(TAPi18n.__('Application_added'));
FlowRouter.go('admin-oauth-app', { id: data._id });
});
},
});
Template.oauthApp.onRendered(() => {
Tracker.afterFlush(() => {
SideNav.setFlex('adminFlex');
SideNav.openFlex();
});
});

@ -1,38 +0,0 @@
<template name="oauthApps">
<section class="permissions-manager page-container page-home page-static page-list">
{{# header sectionName=pageTitle}}
<div class="rc-header__section-button">
<a href="{{pathFor "admin-oauth-app"}}" class="rc-button rc-button--primary new-role">{{_ "New_Application"}}</a>
</div>
{{/header}}
<div class="content">
{{#if hasPermission}}
<div class="rocket-form">
<div class="section">
<div class="admin-integrations-new-panel">
{{#each applications}}
<a href="{{pathFor "admin-oauth-app" id=_id}}">
<div class="admin-integrations-new-item">
<div class="admin-integrations-new-item-body">
<div class="admin-integrations-new-item-title">
{{name}}
</div>
<div class="admin-integrations-new-item-description">
{{{_ "Created_at_s_by_s" (dateFormated _createdAt) _createdBy.username}}}
</div>
</div>
<i class="icon-angle-right"></i>
</div>
</a>
{{else}}
<h1>{{_ "There_are_no_applications"}}</h1>
{{/each}}
</div>
</div>
</div>
{{else}}
{{_ "Not_authorized"}}
{{/if}}
</div>
</section>
</template>

@ -1,33 +0,0 @@
import { Template } from 'meteor/templating';
import { Tracker } from 'meteor/tracker';
import { ReactiveVar } from 'meteor/reactive-var';
import moment from 'moment';
import { hasAllPermission } from '../../../../authorization';
import { SideNav } from '../../../../ui-utils/client';
import { APIClient } from '../../../../utils/client';
Template.oauthApps.onCreated(async function() {
this.oauthApps = new ReactiveVar([]);
const { oauthApps } = await APIClient.v1.get('oauth-apps.list');
this.oauthApps.set(oauthApps);
});
Template.oauthApps.helpers({
hasPermission() {
return hasAllPermission('manage-oauth-apps');
},
applications() {
return Template.instance().oauthApps.get();
},
dateFormated(date) {
return moment(date).format('L LT');
},
});
Template.oauthApps.onRendered(() => {
Tracker.afterFlush(() => {
SideNav.setFlex('adminFlex');
SideNav.openFlex();
});
});

@ -1,5 +1,4 @@
import './oauth/oauth2-client.html';
import './oauth/oauth2-client';
import './admin/startup';
import './admin/route';
import './oauth/stylesheets/oauth2.css';

@ -1,4 +1,4 @@
import React, { useMemo, useState, useEffect, useCallback } from 'react';
import React, { useMemo, useState, useEffect, useCallback, forwardRef } from 'react';
import { Box, Pagination, Skeleton, Table, Flex, Tile, Scrollable } from '@rocket.chat/fuselage';
import { useDebouncedValue } from '@rocket.chat/fuselage-hooks';
@ -35,15 +35,15 @@ const LoadingRow = ({ cols }) => <Table.Row>
</Table.Cell>)}
</Table.Row>;
export function GenericTable({
export const GenericTable = forwardRef(function GenericTable({
results,
total,
renderRow,
renderRow: RenderRow,
header,
setParams = () => { },
params: paramsDefault = '',
FilterComponent = () => null,
}) {
}, ref) {
const t = useTranslation();
const [filter, setFilter] = useState(paramsDefault);
@ -65,40 +65,38 @@ export function GenericTable({
const itemsPerPageLabel = useCallback(() => t('Items_per_page:'), []);
return <>
<>
<FilterComponent flexShrink={0} setFilter={setFilter}/>
{results && !results.length
? <Tile fontScale='p1' elevation='0' color='info' textAlign='center'>
{t('No_data_found')}
</Tile>
: <>
<Scrollable>
<Box mi='neg-x24' pi='x24' flexGrow={1}>
<Table fixed sticky>
{ header && <Table.Head>
<Table.Row>
{header}
</Table.Row>
</Table.Head> }
<Table.Body>
{results
? results.map(renderRow)
: <Loading/>}
</Table.Body>
</Table>
</Box>
</Scrollable>
<Pagination
current={current}
itemsPerPage={itemsPerPage}
itemsPerPageLabel={itemsPerPageLabel}
showingResultsLabel={showingResultsLabel}
count={total || 0}
onSetItemsPerPage={setItemsPerPage}
onSetCurrent={setCurrent}
/>
</>
}
</>
<FilterComponent setFilter={setFilter} />
{results && !results.length
? <Tile fontScale='p1' elevation='0' color='info' textAlign='center'>
{t('No_data_found')}
</Tile>
: <>
<Scrollable>
<Box mi='neg-x24' pi='x24' flexGrow={1} ref={ref}>
<Table fixed sticky>
{ header && <Table.Head>
<Table.Row>
{header}
</Table.Row>
</Table.Head> }
<Table.Body>
{results
? results.map((props, index) => <RenderRow key={props._id || index} { ...props }/>)
: <Loading/>}
</Table.Body>
</Table>
</Box>
</Scrollable>
<Pagination
current={current}
itemsPerPage={itemsPerPage}
itemsPerPageLabel={itemsPerPageLabel}
showingResultsLabel={showingResultsLabel}
count={total || 0}
onSetItemsPerPage={setItemsPerPage}
onSetCurrent={setCurrent}
/>
</>
}
</>;
}
});

@ -22,11 +22,11 @@ export const handleError = function(error, useToastr = true) {
}
const details = Object.entries(error.details || {})
.reduce((obj, [key, value]) => ({ ...obj, [key]: s.escapeHTML(value) }), {});
const message = TAPi18n.__(error.error, details);
const message = TAPi18n.__(error.error || error.message, details);
const title = details.errorTitle && TAPi18n.__(details.errorTitle);
return toastr.error(message, title);
}
return s.escapeHTML(TAPi18n.__(error.error, error.details));
return s.escapeHTML(TAPi18n.__(error.error || error.message, error.details));
};

@ -0,0 +1,146 @@
import { Box, Button, ButtonGroup, Margins } from '@rocket.chat/fuselage';
import { useMutableCallback, useSafely } from '@rocket.chat/fuselage-hooks';
import React, { useState, useEffect } from 'react';
import Page from '../../components/basic/Page';
import { useTranslation } from '../../contexts/TranslationContext';
import { useMethod } from '../../contexts/ServerContext';
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';
import { useQueryStringParameter, useRoute, useRouteParameter } from '../../contexts/RouterContext';
import WhatIsItSection from './WhatIsItSection';
import ConnectToCloudSection from './ConnectToCloudSection';
import TroubleshootingSection from './TroubleshootingSection';
import WorkspaceRegistrationSection from './WorkspaceRegistrationSection';
import WorkspaceLoginSection from './WorkspaceLoginSection';
import ManualWorkspaceRegistrationModal from './ManualWorkspaceRegistrationModal';
import { cloudConsoleUrl } from './constants';
function CloudPage() {
const t = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
const cloudRoute = useRoute('cloud');
const page = useRouteParameter('page');
const errorCode = useQueryStringParameter('error_code');
const code = useQueryStringParameter('code');
const state = useQueryStringParameter('state');
const token = useQueryStringParameter('token');
const finishOAuthAuthorization = useMethod('cloud:finishOAuthAuthorization');
const checkRegisterStatus = useMethod('cloud:checkRegisterStatus');
const connectWorkspace = useMethod('cloud:connectWorkspace');
useEffect(() => {
const acceptOAuthAuthorization = async () => {
if (page !== 'oauth-callback') {
return;
}
if (errorCode) {
dispatchToastMessage({
type: 'error',
title: t('Cloud_error_in_authenticating'),
message: t('Cloud_error_code', { errorCode }),
});
cloudRoute.push();
return;
}
try {
await finishOAuthAuthorization(code, state);
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
} finally {
cloudRoute.push();
}
};
acceptOAuthAuthorization();
}, [errorCode, code, state]);
const [registerStatus, setRegisterStatus] = useSafely(useState());
const [modal, setModal] = useState(null);
const fetchRegisterStatus = useMutableCallback(async () => {
try {
const registerStatus = await checkRegisterStatus();
setRegisterStatus(registerStatus);
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
}
});
useEffect(() => {
const acceptWorkspaceToken = async () => {
try {
if (token) {
const isConnected = await connectWorkspace(token);
if (!isConnected) {
throw Error(t('An error occured connecting'));
}
dispatchToastMessage({ type: 'success', message: t('Connected') });
}
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
} finally {
await fetchRegisterStatus();
}
};
acceptWorkspaceToken();
}, [token]);
const handleManualWorkspaceRegistrationButtonClick = () => {
const handleModalClose = () => {
setModal(null);
fetchRegisterStatus();
};
setModal(<ManualWorkspaceRegistrationModal onClose={handleModalClose} />);
};
const isConnectedToCloud = registerStatus?.connectToCloud;
const isWorkspaceRegistered = registerStatus?.workspaceRegistered;
return <Page>
<Page.Header title={t('Connectivity_Services')}>
<ButtonGroup>
{!isWorkspaceRegistered && <Button onClick={handleManualWorkspaceRegistrationButtonClick}>
{t('Cloud_Register_manually')}
</Button>}
<Button is='a' primary href={cloudConsoleUrl} target='_blank' rel='noopener noreferrer'>
{t('Cloud_console')}
</Button>
</ButtonGroup>
</Page.Header>
<Page.ScrollableContentWithShadow>
{modal}
<Box marginInline='auto' marginBlock='neg-x24' width='full' maxWidth='x580'>
<Margins block='x24'>
<WhatIsItSection />
{isConnectedToCloud && <>
{isWorkspaceRegistered
? <WorkspaceLoginSection onRegisterStatusChange={fetchRegisterStatus} />
: <WorkspaceRegistrationSection
email={registerStatus?.email}
token={registerStatus?.token}
workspaceId={registerStatus?.workspaceId}
uniqueId={registerStatus?.uniqueId}
onRegisterStatusChange={fetchRegisterStatus}
/>}
<TroubleshootingSection onRegisterStatusChange={fetchRegisterStatus} />
</>}
{!isConnectedToCloud && <ConnectToCloudSection onRegisterStatusChange={fetchRegisterStatus} />}
</Margins>
</Box>
</Page.ScrollableContentWithShadow>
</Page>;
}
export default CloudPage;

@ -0,0 +1,17 @@
import React from 'react';
import { usePermission } from '../../contexts/AuthorizationContext';
import NotAuthorizedPage from '../NotAuthorizedPage';
import CloudPage from './CloudPage';
function CloudRoute() {
const canManageCloud = usePermission('manage-cloud');
if (!canManageCloud) {
return <NotAuthorizedPage />;
}
return <CloudPage />;
}
export default CloudRoute;

@ -0,0 +1,61 @@
import { Box, Button, ButtonGroup, Throbber } from '@rocket.chat/fuselage';
import { useSafely } from '@rocket.chat/fuselage-hooks';
import React, { useState } from 'react';
import Subtitle from '../../components/basic/Subtitle';
import { useTranslation } from '../../contexts/TranslationContext';
import { useMethod } from '../../contexts/ServerContext';
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';
function ConnectToCloudSection({
onRegisterStatusChange,
...props
}) {
const t = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
const [isConnecting, setConnecting] = useSafely(useState(false));
const registerWorkspace = useMethod('cloud:registerWorkspace');
const syncWorkspace = useMethod('cloud:syncWorkspace');
const handleRegisterButtonClick = async () => {
setConnecting(true);
try {
const isRegistered = await registerWorkspace();
if (!isRegistered) {
throw Error(t('An error occured'));
}
// TODO: sync on register?
const isSynced = await syncWorkspace();
if (!isSynced) {
throw Error(t('An error occured syncing'));
}
dispatchToastMessage({ type: 'success', message: t('Sync Complete') });
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
} finally {
await (onRegisterStatusChange && onRegisterStatusChange());
setConnecting(false);
}
};
return <Box is='section' {...props}>
<Subtitle>{t('Cloud_registration_required')}</Subtitle>
<Box withRichContent>
<p>{t('Cloud_registration_required_description')}</p>
</Box>
<ButtonGroup>
<Button primary disabled={isConnecting} minHeight='x40' onClick={handleRegisterButtonClick}>
{isConnecting ? <Throbber is='span' inheritColor /> : t('Cloud_registration_required_link_text')}
</Button>
</ButtonGroup>
</Box>;
}
export default ConnectToCloudSection;

@ -0,0 +1,183 @@
import { Box, Button, ButtonGroup, Icon, Scrollable, Throbber } from '@rocket.chat/fuselage';
import Clipboard from 'clipboard';
import React, { useEffect, useState, useRef } from 'react';
import { Modal } from '../../components/basic/Modal';
import { useTranslation } from '../../contexts/TranslationContext';
import { useMethod, useEndpoint } from '../../contexts/ServerContext';
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';
import MarkdownText from '../../components/basic/MarkdownText';
import { cloudConsoleUrl } from './constants';
function CopyStep({ onNextButtonClick }) {
const t = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
const [clientKey, setClientKey] = useState('');
const getWorkspaceRegisterData = useMethod('cloud:getWorkspaceRegisterData');
useEffect(() => {
const loadWorkspaceRegisterData = async () => {
const clientKey = await getWorkspaceRegisterData();
setClientKey(clientKey);
};
loadWorkspaceRegisterData();
}, []);
const copyRef = useRef();
useEffect(function() {
const clipboard = new Clipboard(copyRef.current);
clipboard.on('success', () => {
dispatchToastMessage({ type: 'success', message: t('Copied') });
});
return () => {
clipboard.destroy();
};
}, []);
return <>
<Modal.Content>
<Box withRichContent>
<p>{t('Cloud_register_offline_helper')}</p>
</Box>
<Box
display='flex'
flexDirection='column'
alignItems='stretch'
padding='x16'
flexGrow={1}
backgroundColor='neutral-800'
>
<Scrollable vertical>
<Box
height='x108'
fontFamily='mono'
fontScale='p1'
color='alternative'
style={{ wordBreak: 'break-all' }}
>
{clientKey}
</Box>
</Scrollable>
<Button ref={copyRef} primary data-clipboard-text={clientKey}>
<Icon name='copy' /> {t('Copy')}
</Button>
</Box>
<Box withRichContent>
<p>
<MarkdownText>{t('Cloud_click_here', { cloudConsoleUrl })}</MarkdownText>
</p>
</Box>
</Modal.Content>
<Modal.Footer>
<ButtonGroup>
<Button primary onClick={onNextButtonClick}>{t('Next')}</Button>
</ButtonGroup>
</Modal.Footer>
</>;
}
function PasteStep({ onBackButtonClick, onFinish }) {
const t = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
const [isLoading, setLoading] = useState(false);
const [cloudKey, setCloudKey] = useState('');
const handleCloudKeyChange = (e) => {
setCloudKey(e.currentTarget.value);
};
const registerManually = useEndpoint('POST', 'cloud.manualRegister');
const handleFinishButtonClick = async () => {
setLoading(true);
try {
await registerManually({}, { cloudBlob: cloudKey });
dispatchToastMessage({ type: 'success', message: t('Cloud_register_success') });
} catch (error) {
dispatchToastMessage({ type: 'error', message: t('Cloud_register_error') });
} finally {
setLoading(false);
onFinish && onFinish();
}
};
return <>
<Modal.Content>
<Box withRichContent>
<p>{t('Cloud_register_offline_finish_helper')}</p>
</Box>
<Box
display='flex'
flexDirection='column'
alignItems='stretch'
padding='x16'
flexGrow={1}
backgroundColor='neutral-800'
>
<Scrollable vertical>
<Box
is='textarea'
height='x108'
fontFamily='mono'
fontScale='p1'
color='alternative'
style={{ wordBreak: 'break-all', resize: 'none' }}
placeholder={t('Paste_here')}
disabled={isLoading}
value={cloudKey}
autoComplete='off'
autoCorrect='off'
autoCapitalize='off'
spellCheck='false'
onChange={handleCloudKeyChange}
/>
</Scrollable>
</Box>
</Modal.Content>
<Modal.Footer>
<ButtonGroup>
<Button disabled={isLoading} onClick={onBackButtonClick}>{t('Back')}</Button>
<Button primary disabled={isLoading || !cloudKey.trim()} marginInlineStart='auto' onClick={handleFinishButtonClick}>
{isLoading ? <Throbber inheritColor /> : t('Finish Registration')}
</Button>
</ButtonGroup>
</Modal.Footer>
</>;
}
const Steps = {
COPY: 'copy',
PASTE: 'paste',
};
function ManualWorkspaceRegistrationModal({ onClose, props }) {
const t = useTranslation();
const [step, setStep] = useState(Steps.COPY);
const handleNextButtonClick = () => {
setStep(Steps.PASTE);
};
const handleBackButtonClick = () => {
setStep(Steps.COPY);
};
return <Modal {...props}>
<Modal.Header>
<Modal.Title>{t('Cloud_Register_manually')}</Modal.Title>
<Modal.Close onClick={onClose} />
</Modal.Header>
{(step === Steps.COPY && <CopyStep onNextButtonClick={handleNextButtonClick} />)
|| (step === Steps.PASTE && <PasteStep onBackButtonClick={handleBackButtonClick} onFinish={onClose} />)}
</Modal>;
}
export default ManualWorkspaceRegistrationModal;

@ -0,0 +1,63 @@
import { Box, Button, ButtonGroup, Throbber } from '@rocket.chat/fuselage';
import { useSafely } from '@rocket.chat/fuselage-hooks';
import React, { useState } from 'react';
import { useTranslation } from '../../contexts/TranslationContext';
import Subtitle from '../../components/basic/Subtitle';
import { useMethod } from '../../contexts/ServerContext';
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';
import { statusPageUrl } from './constants';
function TroubleshootingSection({
onRegisterStatusChange,
...props
}) {
const t = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
const [isSyncing, setSyncing] = useSafely(useState(false));
const syncWorkspace = useMethod('cloud:syncWorkspace');
const handleSyncButtonClick = async () => {
setSyncing(true);
try {
const isSynced = await syncWorkspace();
if (!isSynced) {
throw Error(t('An error occured syncing'));
}
dispatchToastMessage({ type: 'success', message: t('Sync Complete') });
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
} finally {
await (onRegisterStatusChange && onRegisterStatusChange());
setSyncing(false);
}
};
return <Box is='section' {...props}>
<Subtitle>{t('Cloud_troubleshooting')}</Subtitle>
<Box withRichContent>
<p>{t('Cloud_workspace_support')}</p>
</Box>
<ButtonGroup>
<Button disabled={isSyncing} minHeight='x40' onClick={handleSyncButtonClick}>
{isSyncing ? <Throbber is='span' inheritColor /> : t('Sync')}
</Button>
</ButtonGroup>
<Box withRichContent>
<p>
{t('Cloud_status_page_description')}:{' '}
<a href={statusPageUrl} target='_blank' rel='noopener noreferrer'>{statusPageUrl}</a>
</p>
</Box>
</Box>;
}
export default TroubleshootingSection;

@ -0,0 +1,32 @@
import { Box } from '@rocket.chat/fuselage';
import React from 'react';
import { useTranslation } from '../../contexts/TranslationContext';
import Subtitle from '../../components/basic/Subtitle';
function WhatIsItSection(props) {
const t = useTranslation();
return <Box is='section' {...props}>
<Subtitle>{t('Cloud_what_is_it')}</Subtitle>
<Box withRichContent>
<p>{t('Cloud_what_is_it_description')}</p>
<details>
<p>{t('Cloud_what_is_it_services_like')}</p>
<ul>
<li>{t('Register_Server_Registered_Push_Notifications')}</li>
<li>{t('Register_Server_Registered_Livechat')}</li>
<li>{t('Register_Server_Registered_OAuth')}</li>
<li>{t('Register_Server_Registered_Marketplace')}</li>
</ul>
<p>{t('Cloud_what_is_it_additional')}</p>
</details>
</Box>
</Box>;
}
export default WhatIsItSection;

@ -0,0 +1,108 @@
import { Box, Button, ButtonGroup } from '@rocket.chat/fuselage';
import { useSafely } from '@rocket.chat/fuselage-hooks';
import React, { useState, useEffect } from 'react';
import { useTranslation } from '../../contexts/TranslationContext';
import { useMethod } from '../../contexts/ServerContext';
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';
function WorkspaceLoginSection({
onRegisterStatusChange,
...props
}) {
const t = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
const checkUserLoggedIn = useMethod('cloud:checkUserLoggedIn');
const getOAuthAuthorizationUrl = useMethod('cloud:getOAuthAuthorizationUrl');
const logout = useMethod('cloud:logout');
const disconnectWorkspace = useMethod('cloud:disconnectWorkspace');
const [isLoggedIn, setLoggedIn] = useSafely(useState(false));
const [isLoading, setLoading] = useSafely(useState(true));
const handleLoginButtonClick = async () => {
setLoading(true);
try {
const url = await getOAuthAuthorizationUrl();
window.location.href = url;
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
} finally {
setLoading(false);
}
};
const handleLogoutButtonClick = async () => {
setLoading(true);
try {
await logout();
const isLoggedIn = await checkUserLoggedIn();
setLoggedIn(isLoggedIn);
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
} finally {
setLoading(false);
}
};
const handleDisconnectButtonClick = async () => {
setLoading(true);
try {
const success = await disconnectWorkspace();
if (!success) {
throw Error(t('An error occured disconnecting'));
}
dispatchToastMessage({ type: 'success', message: t('Disconnected') });
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
} finally {
await (onRegisterStatusChange && onRegisterStatusChange());
setLoading(false);
}
};
useEffect(() => {
const checkLoginState = async () => {
setLoading(true);
try {
const isLoggedIn = await checkUserLoggedIn();
setLoggedIn(isLoggedIn);
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
} finally {
setLoading(false);
}
};
checkLoginState();
}, []);
return <Box is='section' {...props}>
<Box withRichContent>
<p>{t('Cloud_workspace_connected')}</p>
</Box>
<ButtonGroup>
{isLoggedIn
? <Button primary danger disabled={isLoading} onClick={handleLogoutButtonClick}>{t('Cloud_logout')}</Button>
: <Button primary disabled={isLoading} onClick={handleLoginButtonClick}>{t('Cloud_login_to_cloud')}</Button>}
</ButtonGroup>
<Box withRichContent>
<p>{t('Cloud_workspace_disconnect')}</p>
</Box>
<ButtonGroup>
<Button primary danger disabled={isLoading} onClick={handleDisconnectButtonClick}>{t('Disconnect')}</Button>
</ButtonGroup>
</Box>;
}
export default WorkspaceLoginSection;

@ -0,0 +1,128 @@
import { Box, Button, ButtonGroup, EmailInput, Field, Margins, TextInput } from '@rocket.chat/fuselage';
import { useSafely, useUniqueId } from '@rocket.chat/fuselage-hooks';
import React, { useState, useMemo } from 'react';
import { useTranslation } from '../../contexts/TranslationContext';
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';
import { useMethod } from '../../contexts/ServerContext';
import { supportEmailAddress } from './constants';
function WorkspaceRegistrationSection({
email: initialEmail,
token: initialToken,
workspaceId,
uniqueId,
onRegisterStatusChange,
...props
}) {
const t = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
const updateEmail = useMethod('cloud:updateEmail');
const connectWorkspace = useMethod('cloud:connectWorkspace');
const [isProcessing, setProcessing] = useSafely(useState(false));
const [email, setEmail] = useState(initialEmail);
const [token, setToken] = useState(initialToken);
const supportMailtoUrl = useMemo(() => {
const subject = encodeURIComponent('Self Hosted Registration');
const body = encodeURIComponent([
`WorkspaceId: ${ workspaceId }`,
`Deployment Id: ${ uniqueId }`,
'Issue: <please describe your issue here>',
].join('\r\n'));
return `mailto:${ supportEmailAddress }?subject=${ subject }&body=${ body }`;
}, [workspaceId, uniqueId]);
const handleEmailChange = ({ currentTarget: { value } }) => {
setEmail(value);
};
const handleTokenChange = ({ currentTarget: { value } }) => {
setToken(value);
};
const handleUpdateEmailButtonClick = async () => {
setProcessing(true);
try {
await updateEmail(email, false);
dispatchToastMessage({ type: 'success', message: t('Saved') });
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
} finally {
setProcessing(false);
}
};
const handleResendEmailButtonClick = async () => {
setProcessing(true);
try {
await updateEmail(email, true);
dispatchToastMessage({ type: 'success', message: t('Requested') });
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
} finally {
setProcessing(false);
}
};
const handleConnectButtonClick = async () => {
setProcessing(true);
try {
const isConnected = await connectWorkspace(token);
if (!isConnected) {
throw Error(t('An error occured connecting'));
}
dispatchToastMessage({ type: 'success', message: t('Connected') });
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
} finally {
await (onRegisterStatusChange && onRegisterStatusChange());
setProcessing(false);
}
};
const emailInputId = useUniqueId();
const tokenInputId = useUniqueId();
return <Box marginBlock='neg-x24' {...props}>
<Margins block='x24'>
<Field>
<Field.Label htmlFor={emailInputId}>{t('Email')}</Field.Label>
<Field.Row>
<EmailInput id={emailInputId} disabled={isProcessing} value={email} onChange={handleEmailChange} />
</Field.Row>
<Field.Hint>{t('Cloud_address_to_send_registration_to')}</Field.Hint>
</Field>
<ButtonGroup>
<Button disabled={isProcessing} onClick={handleUpdateEmailButtonClick}>{t('Cloud_update_email')}</Button>
<Button disabled={isProcessing} onClick={handleResendEmailButtonClick}>{t('Cloud_resend_email')}</Button>
</ButtonGroup>
<Field>
<Field.Label htmlFor={tokenInputId}>{t('Token')}</Field.Label>
<Field.Row>
<TextInput id={tokenInputId} disabled={isProcessing} value={token} onChange={handleTokenChange} />
</Field.Row>
<Field.Hint>{t('Cloud_manually_input_token')}</Field.Hint>
</Field>
<ButtonGroup>
<Button primary disabled={isProcessing} onClick={handleConnectButtonClick}>{t('Connect')}</Button>
</ButtonGroup>
<Box withRichContent>
<p>{t('Cloud_connect_support')}: <a href={supportMailtoUrl} target='_blank' rel='noopener noreferrer'>{supportEmailAddress}</a></p>
</Box>
</Margins>
</Box>;
}
export default WorkspaceRegistrationSection;

@ -0,0 +1,3 @@
export const cloudConsoleUrl = 'https://cloud.rocket.chat';
export const supportEmailAddress = 'support@rocket.chat';
export const statusPageUrl = 'https://status.rocket.chat';

@ -0,0 +1,202 @@
import React, { useCallback, useState, useMemo, useEffect } from 'react';
import { Box, Button, ButtonGroup, Margins, TextInput, Field, Icon, Skeleton, Throbber, InputBox } from '@rocket.chat/fuselage';
import { useTranslation } from '../../contexts/TranslationContext';
import { useMethod } from '../../contexts/ServerContext';
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';
import { Modal } from '../../components/basic/Modal';
import { useFileInput } from '../../hooks/useFileInput';
import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../hooks/useEndpointDataExperimental';
import { validate, createSoundData } from './lib';
const DeleteWarningModal = ({ onDelete, onCancel, ...props }) => {
const t = useTranslation();
return <Modal {...props}>
<Modal.Header>
<Icon color='danger' name='modal-warning' size={20}/>
<Modal.Title>{t('Are_you_sure')}</Modal.Title>
<Modal.Close onClick={onCancel}/>
</Modal.Header>
<Modal.Content fontScale='p1'>
{t('Custom_Sound_Status_Delete_Warning')}
</Modal.Content>
<Modal.Footer>
<ButtonGroup align='end'>
<Button ghost onClick={onCancel}>{t('Cancel')}</Button>
<Button primary danger onClick={onDelete}>{t('Delete')}</Button>
</ButtonGroup>
</Modal.Footer>
</Modal>;
};
const SuccessModal = ({ onClose, ...props }) => {
const t = useTranslation();
return <Modal {...props}>
<Modal.Header>
<Icon color='success' name='checkmark-circled' size={20}/>
<Modal.Title>{t('Deleted')}</Modal.Title>
<Modal.Close onClick={onClose}/>
</Modal.Header>
<Modal.Content fontScale='p1'>
{t('Custom_Sound_Has_Been_Deleted')}
</Modal.Content>
<Modal.Footer>
<ButtonGroup align='end'>
<Button primary onClick={onClose}>{t('Ok')}</Button>
</ButtonGroup>
</Modal.Footer>
</Modal>;
};
export function EditSound({ _id, cache, ...props }) {
const t = useTranslation();
const query = useMemo(() => ({
query: JSON.stringify({ _id }),
}), [_id]);
const { data, state, error } = useEndpointDataExperimental('custom-sounds.list', query);
if (state === ENDPOINT_STATES.LOADING) {
return <Box pb='x20'>
<Skeleton mbs='x8'/>
<InputBox.Skeleton w='full'/>
<Skeleton mbs='x8'/>
<InputBox.Skeleton w='full'/>
<ButtonGroup stretch w='full' mbs='x8'>
<Button disabled><Throbber inheritColor/></Button>
<Button primary disabled><Throbber inheritColor/></Button>
</ButtonGroup>
<ButtonGroup stretch w='full' mbs='x8'>
<Button primary danger disabled><Throbber inheritColor/></Button>
</ButtonGroup>
</Box>;
}
if (error || !data || data.sounds.length < 1) {
return <Box fontScale='h1' pb='x20'>{t('Custom_User_Status_Error_Invalid_User_Status')}</Box>;
}
return <EditCustomSound data={data.sounds[0]} {...props}/>;
}
export function EditCustomSound({ close, onChange, data, ...props }) {
const t = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
const { _id, name: previousName } = data || {};
const previousSound = data || {};
const [name, setName] = useState('');
const [sound, setSound] = useState();
const [modal, setModal] = useState();
useEffect(() => {
setName(previousName || '');
setSound(previousSound || '');
}, [previousName, previousSound, _id]);
const deleteCustomSound = useMethod('deleteCustomSound');
const uploadCustomSound = useMethod('uploadCustomSound');
const insertOrUpdateSound = useMethod('insertOrUpdateSound');
const handleChangeFile = (soundFile) => {
setSound(soundFile);
};
const hasUnsavedChanges = useMemo(() => previousName !== name || previousSound !== sound, [name, sound]);
const saveAction = async (sound) => {
const soundData = createSoundData(sound, name, { previousName, previousSound, _id });
const validation = validate(soundData, sound);
if (validation.length === 0) {
let soundId;
try {
soundId = await insertOrUpdateSound(soundData);
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
}
soundData._id = soundId;
soundData.random = Math.round(Math.random() * 1000);
if (sound && sound !== previousSound) {
dispatchToastMessage({ type: 'success', message: t('Uploading_file') });
const reader = new FileReader();
reader.readAsBinaryString(sound);
reader.onloadend = () => {
try {
uploadCustomSound(reader.result, sound.type, soundData);
return dispatchToastMessage({ type: 'success', message: t('File_uploaded') });
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
}
};
}
}
validation.forEach((error) => dispatchToastMessage({ type: 'error', message: t('error-the-field-is-required', { field: t(error) }) }));
};
const handleSave = useCallback(async () => {
saveAction(sound);
onChange();
}, [name, _id, sound]);
const onDeleteConfirm = useCallback(async () => {
try {
await deleteCustomSound(_id);
setModal(() => <SuccessModal onClose={() => { setModal(undefined); close(); onChange(); }}/>);
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
onChange();
}
}, [_id]);
const openConfirmDelete = () => setModal(() => <DeleteWarningModal onDelete={onDeleteConfirm} onCancel={() => setModal(undefined)}/>);
const clickUpload = useFileInput(handleChangeFile, 'audio/mp3');
return <>
<Box display='flex' flexDirection='column' fontScale='p1' color='default' mbs='x20' {...props}>
<Margins block='x4'>
<Field>
<Field.Label>{t('Name')}</Field.Label>
<Field.Row>
<TextInput value={name} onChange={(e) => setName(e.currentTarget.value)} placeholder={t('Name')} />
</Field.Row>
</Field>
<Field>
<Field.Label alignSelf='stretch'>{t('Sound_File_mp3')}</Field.Label>
<Box display='flex' flexDirection='row' mbs='none'>
<Margins inline='x4'>
<Button square onClick={clickUpload}><Icon name='upload' size='x20'/></Button>
{(sound && sound.name) || 'none'}
</Margins>
</Box>
</Field>
<Field>
<Field.Row>
<ButtonGroup stretch w='full'>
<Button onClick={close}>{t('Cancel')}</Button>
<Button primary onClick={handleSave} disabled={!hasUnsavedChanges}>{t('Save')}</Button>
</ButtonGroup>
</Field.Row>
</Field>
<Field>
<Field.Row>
<ButtonGroup stretch w='full'>
<Button primary danger onClick={openConfirmDelete}><Icon name='trash' mie='x4'/>{t('Delete')}</Button>
</ButtonGroup>
</Field.Row>
</Field>
</Margins>
</Box>
{ modal }
</>;
}

@ -0,0 +1,101 @@
import React, { useState, useCallback } from 'react';
import { Field, TextInput, Box, Icon, Margins, Button, ButtonGroup } from '@rocket.chat/fuselage';
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';
import { useTranslation } from '../../contexts/TranslationContext';
import { useMethod } from '../../contexts/ServerContext';
import { useFileInput } from '../../hooks/useFileInput';
import { validate, createSoundData } from './lib';
export function NewSound({ goToNew, close, onChange, ...props }) {
const t = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
const [name, setName] = useState('');
const [sound, setSound] = useState();
const uploadCustomSound = useMethod('uploadCustomSound');
const insertOrUpdateSound = useMethod('insertOrUpdateSound');
const handleChangeFile = (soundFile) => {
setSound(soundFile);
};
const clickUpload = useFileInput(handleChangeFile, 'audio/mp3');
const saveAction = async (name, soundFile) => {
const soundData = createSoundData(soundFile, name);
const validation = validate(soundData, sound);
if (validation.length === 0) {
let soundId;
try {
soundId = await insertOrUpdateSound(soundData);
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
}
soundData._id = soundId;
soundData.random = Math.round(Math.random() * 1000);
if (soundId) {
dispatchToastMessage({ type: 'success', message: t('Uploading_file') });
const reader = new FileReader();
reader.readAsBinaryString(soundFile);
reader.onloadend = () => {
try {
uploadCustomSound(reader.result, soundFile.type, soundData);
dispatchToastMessage({ type: 'success', message: t('File_uploaded') });
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
}
};
}
return soundId;
}
validation.forEach((error) => { throw new Error({ type: 'error', message: t('error-the-field-is-required', { field: t(error) }) }); });
};
const handleSave = useCallback(async () => {
try {
const result = await saveAction(
name,
sound,
);
dispatchToastMessage({ type: 'success', message: t('Custom_Sound_Updated_Successfully') });
goToNew(result)();
onChange();
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
}
}, [name, sound]);
return <Box display='flex' flexDirection='column' fontScale='p1' color='default' mbs='x20' {...props}>
<Margins block='x4'>
<Field>
<Field.Label>{t('Name')}</Field.Label>
<Field.Row>
<TextInput value={name} onChange={(e) => setName(e.currentTarget.value)} placeholder={t('Name')} />
</Field.Row>
</Field>
<Field>
<Field.Label alignSelf='stretch'>{t('Sound_File_mp3')}</Field.Label>
<Box display='flex' flexDirection='row' mbs='none'>
<Margins inline='x4'>
<Button square onClick={clickUpload}><Icon name='upload' size='x20'/></Button>
{(sound && sound.name) || 'none'}
</Margins>
</Box>
</Field>
<Field>
<Field.Row>
<ButtonGroup stretch w='full'>
<Button mie='x4' onClick={close}>{t('Cancel')}</Button>
<Button primary onClick={handleSave} disabled={name === ''}>{t('Save')}</Button>
</ButtonGroup>
</Field.Row>
</Field>
</Margins>
</Box>;
}

@ -0,0 +1,165 @@
import React, { useMemo } from 'react';
import { Field, TextInput, Box, ToggleSwitch, Icon, TextAreaInput, FieldGroup, Margins } from '@rocket.chat/fuselage';
import { useTranslation } from '../../contexts/TranslationContext';
import { useAbsoluteUrl } from '../../contexts/ServerContext';
import { useHilightCode } from '../../hooks/useHilightCode';
import { useExampleData } from './exampleIncomingData';
import Page from '../../components/basic/Page';
export default function IncomingWebhookForm({ formValues, formHandlers, extraData = {}, append, ...props }) {
const t = useTranslation();
const hilightCode = useHilightCode();
const absoluteUrl = useAbsoluteUrl();
const {
enabled,
channel,
username,
name,
alias,
avatarUrl,
emoji,
scriptEnabled,
script,
} = formValues;
const {
handleEnabled,
handleChannel,
handleUsername,
handleName,
handleAlias,
handleAvatarUrl,
handleEmoji,
handleScriptEnabled,
handleScript,
} = formHandlers;
const url = absoluteUrl(`hooks/${ extraData._id }/${ extraData.token }`);
const [exampleData, curlData] = useExampleData({
aditionalFields: {
...alias && { alias },
...emoji && { emoji },
...avatarUrl && { avatar: avatarUrl },
},
url,
}, [alias, emoji, avatarUrl]);
const hilightedExampleJson = hilightCode('json', JSON.stringify(exampleData, null, 2));
return <Page.ScrollableContent pb='x24' mi='neg-x24' is='form' qa-admin-user-edit='form' { ...props }>
<Margins block='x16'>
<FieldGroup width='x600' alignSelf='center'>
{useMemo(() => <Field>
<Field.Label display='flex' justifyContent='space-between' w='full'>
{t('Enabled')}
<ToggleSwitch checked={enabled} onChange={handleEnabled} />
</Field.Label>
</Field>, [enabled, handleEnabled])}
{useMemo(() => <Field>
<Field.Label>{t('Name_optional')}</Field.Label>
<Field.Row>
<TextInput flexGrow={1} value={name} onChange={handleName}/>
</Field.Row>
<Field.Hint>{t('You_should_name_it_to_easily_manage_your_integrations')}</Field.Hint>
</Field>, [name, handleName])}
{useMemo(() => <Field>
<Field.Label>{t('Post_to_Channel')}</Field.Label>
<Field.Row>
<TextInput flexGrow={1} value={channel} onChange={handleChannel} addon={<Icon name='at' size='x20'/>}/>
</Field.Row>
<Field.Hint>{t('Messages_that_are_sent_to_the_Incoming_WebHook_will_be_posted_here')}</Field.Hint>
<Field.Hint dangerouslySetInnerHTML={{ __html: t('Start_with_s_for_user_or_s_for_channel_Eg_s_or_s', '@', '#', '@john', '#general') }} />
</Field>, [channel, handleChannel])}
{useMemo(() => <Field>
<Field.Label>{t('Post_as')}</Field.Label>
<Field.Row>
<TextInput flexGrow={1} value={username} onChange={handleUsername} addon={<Icon name='user' size='x20'/>}/>
</Field.Row>
<Field.Hint>{t('Choose_the_username_that_this_integration_will_post_as')}</Field.Hint>
<Field.Hint>{t('Should_exists_a_user_with_this_username')}</Field.Hint>
</Field>, [username, handleUsername])}
{useMemo(() => <Field>
<Field.Label>{`${ t('Alias') } (${ t('optional') })`}</Field.Label>
<Field.Row>
<TextInput flexGrow={1} value={alias} onChange={handleAlias} addon={<Icon name='edit' size='x20'/>}/>
</Field.Row>
<Field.Hint>{t('Choose_the_alias_that_will_appear_before_the_username_in_messages')}</Field.Hint>
</Field>, [alias, handleAlias])}
{useMemo(() => <Field>
<Field.Label>{`${ t('Avatar_URL') } (${ t('optional') })`}</Field.Label>
<Field.Row>
<TextInput flexGrow={1} value={avatarUrl} onChange={handleAvatarUrl} addon={<Icon name='user-rounded' size='x20' alignSelf='center'/>}/>
</Field.Row>
<Field.Hint>{t('You_can_change_a_different_avatar_too')}</Field.Hint>
<Field.Hint>{t('Should_be_a_URL_of_an_image')}</Field.Hint>
</Field>, [avatarUrl, handleAvatarUrl])}
{useMemo(() => <Field>
<Field.Label>{`${ t('Emoji') } (${ t('optional') })`}</Field.Label>
<Field.Row>
<TextInput flexGrow={1} value={emoji} onChange={handleEmoji} addon={<Icon name='emoji' size='x20' alignSelf='center'/>}/>
</Field.Row>
<Field.Hint>{t('You_can_use_an_emoji_as_avatar')}</Field.Hint>
<Field.Hint dangerouslySetInnerHTML={{ __html: t('Example_s', ':ghost:') }} />
</Field>, [emoji, handleEmoji])}
{useMemo(() => <Field>
<Field.Label display='flex' justifyContent='space-between' w='full'>
{t('Script_Enabled')}
<ToggleSwitch checked={scriptEnabled} onChange={handleScriptEnabled} />
</Field.Label>
</Field>, [scriptEnabled, handleScriptEnabled])}
{useMemo(() => <Field>
<Field.Label>{t('Script')}</Field.Label>
<Field.Row>
<TextAreaInput rows={10} flexGrow={1} value={script} onChange={handleScript} addon={<Icon name='code' size='x20' alignSelf='center'/>}/>
</Field.Row>
</Field>, [script, handleScript])}
{useMemo(() => !extraData._id && <><Field>
<Field.Label>{t('Webhook_URL')}</Field.Label>
<Field.Row>
<TextInput flexGrow={1} value={t('Will_be_available_here_after_saving')} addon={<Icon name='permalink' size='x20'/>} disabled/>
</Field.Row>
<Field.Hint>{t('Send_your_JSON_payloads_to_this_URL')}</Field.Hint>
</Field>
<Field>
<Field.Label>{t('Token')}</Field.Label>
<Field.Row>
<TextInput flexGrow={1} value={t('Will_be_available_here_after_saving')} addon={<Icon name='key' size='x20'/>} disabled/>
</Field.Row>
</Field></>, [extraData._id])}
{useMemo(() => extraData._id && <><Field>
<Field.Label>{t('Webhook_URL')}</Field.Label>
<Field.Row>
<TextInput flexGrow={1} value={url} addon={<Icon name='permalink' size='x20'/>}/>
</Field.Row>
<Field.Hint>{t('Send_your_JSON_payloads_to_this_URL')}</Field.Hint>
</Field>
<Field>
<Field.Label>{t('Token')}</Field.Label>
<Field.Row>
<TextInput flexGrow={1} value={`${ extraData._id }/${ extraData.token }`} addon={<Icon name='key' size='x20'/>}/>
</Field.Row>
</Field></>, [url, extraData._id, extraData.token])}
{useMemo(() => <Field>
<Field.Label>{t('Example_payload')}</Field.Label>
<Field.Row>
<Box fontScale='p1' withRichContent flexGrow={1}>
<pre><code dangerouslySetInnerHTML={{ __html: hilightedExampleJson }}></code></pre>
</Box>
</Field.Row>
</Field>, [hilightedExampleJson])}
{useMemo(() => extraData._id && <Field>
<Field.Label>{t('Curl')}</Field.Label>
<Field.Row>
<TextInput flexGrow={1} value={curlData} addon={<Icon name='code' size='x20'/>}/>
</Field.Row>
</Field>, [curlData])}
{ append }
</FieldGroup>
</Margins>
</Page.ScrollableContent>;
}

@ -0,0 +1,56 @@
import { Button, ButtonGroup, Icon, Tabs } from '@rocket.chat/fuselage';
import React, { useEffect, useCallback } from 'react';
import Page from '../../components/basic/Page';
import { useTranslation } from '../../contexts/TranslationContext';
import { useRoute, useRouteParameter } from '../../contexts/RouterContext';
import IntegrationsTable from './IntegrationsTable';
import NewZapier from './new/NewZapier';
import NewBot from './new/NewBot';
function IntegrationsPage() {
const t = useTranslation();
const router = useRoute('admin-integrations');
const handleNewButtonClick = useCallback(() => {
router.push({ context: 'new', type: 'incoming' });
}, []);
const context = useRouteParameter('context');
useEffect(() => {
if (!context) {
router.push({ context: 'webhook-incoming' });
}
}, [context]);
const showTable = !['zapier', 'bots'].includes(context);
const goToIncoming = useCallback(() => router.push({ context: 'webhook-incoming' }), []);
const goToOutgoing = useCallback(() => router.push({ context: 'webhook-outgoing' }), []);
const goToZapier = useCallback(() => router.push({ context: 'zapier' }), []);
const goToBots = useCallback(() => router.push({ context: 'bots' }), []);
return <Page flexDirection='column'>
<Page.Header title={t('Integrations')}>
<ButtonGroup>
<Button onClick={handleNewButtonClick}>
<Icon name='plus'/> {t('New')}
</Button>
</ButtonGroup>
</Page.Header>
<Tabs>
<Tabs.Item selected={context === 'webhook-incoming'} onClick={goToIncoming}>{t('Incoming')}</Tabs.Item>
<Tabs.Item selected={context === 'webhook-outgoing'} onClick={goToOutgoing}>{t('Outgoing')}</Tabs.Item>
<Tabs.Item selected={context === 'zapier'} onClick={goToZapier}>{t('Zapier')}</Tabs.Item>
<Tabs.Item selected={context === 'bots'} onClick={goToBots}>{t('Bots')}</Tabs.Item>
</Tabs>
<Page.Content>
{context === 'zapier' && <NewZapier />}
{context === 'bots' && <NewBot />}
{showTable && <IntegrationsTable type={context}/>}
</Page.Content>
</Page>;
}
export default IntegrationsPage;

@ -0,0 +1,40 @@
import React from 'react';
import { useAtLeastOnePermission } from '../../contexts/AuthorizationContext';
import { useRouteParameter } from '../../contexts/RouterContext';
import NotAuthorizedPage from '../NotAuthorizedPage';
import IntegrationsPage from './IntegrationsPage';
import NewIntegrationsPage from './new/NewIntegrationsPage';
import EditIntegrationsPage from './edit/EditIntegrationsPage';
import OutgoingWebhookHistoryPage from './edit/OutgoingWebhookHistoryPage';
function IntegrationsRoute() {
const canViewIntegrationsPage = useAtLeastOnePermission([
'manage-incoming-integrations',
'manage-outgoing-integrations',
'manage-own-incoming-integrations',
'manage-own-outgoing-integrations',
]);
const context = useRouteParameter('context');
if (!canViewIntegrationsPage) {
return <NotAuthorizedPage />;
}
if (context === 'new') {
return <NewIntegrationsPage />;
}
if (context === 'edit') {
return <EditIntegrationsPage />;
}
if (context === 'history') {
return <OutgoingWebhookHistoryPage />;
}
return <IntegrationsPage />;
}
export default IntegrationsRoute;

@ -0,0 +1,97 @@
import { Box, Table, TextInput, Icon } from '@rocket.chat/fuselage';
import { useDebouncedValue, useResizeObserver } from '@rocket.chat/fuselage-hooks';
import React, { useMemo, useCallback, useState, useEffect } from 'react';
import { GenericTable, Th } from '../../../app/ui/client/components/GenericTable';
import { useTranslation } from '../../contexts/TranslationContext';
import { useRoute } from '../../contexts/RouterContext';
import { useEndpointDataExperimental } from '../../hooks/useEndpointDataExperimental';
import { useFormatDateAndTime } from '../../hooks/useFormatDateAndTime';
const style = { whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden' };
const FilterByTypeAndText = React.memo(({ setFilter, ...props }) => {
const t = useTranslation();
const [text, setText] = useState('');
const handleChange = useCallback((event) => setText(event.currentTarget.value), []);
useEffect(() => {
setFilter({ text });
}, [text]);
return <Box mb='x16' is='form' display='flex' flexDirection='column' {...props}>
<TextInput placeholder={t('Search_Integrations')} addon={<Icon name='magnifier' size='x20'/>} onChange={handleChange} value={text} />
</Box>;
});
const useQuery = (params, sort) => useMemo(() => ({
query: JSON.stringify({ name: { $regex: params.text || '', $options: 'i' }, type: params.type }),
sort: JSON.stringify({ [sort[0]]: sort[1] === 'asc' ? 1 : -1 }),
...params.itemsPerPage && { count: params.itemsPerPage },
...params.current && { offset: params.current },
}), [JSON.stringify(params), JSON.stringify(sort)]);
const useResizeInlineBreakpoint = (sizes = [], debounceDelay = 0) => {
const { ref, borderBoxSize } = useResizeObserver({ debounceDelay });
const inlineSize = borderBoxSize ? borderBoxSize.inlineSize : 0;
sizes = useMemo(() => sizes.map((current) => (inlineSize ? inlineSize > current : true)), [inlineSize]);
return [ref, ...sizes];
};
export function IntegrationsTable({ type }) {
const t = useTranslation();
const formatDateAndTime = useFormatDateAndTime();
const [ref, isBig] = useResizeInlineBreakpoint([700], 200);
const [params, setParams] = useState({ text: '', current: 0, itemsPerPage: 25 });
const [sort, setSort] = useState(['name', 'asc']);
const debouncedText = useDebouncedValue(params.text, 500);
const debouncedSort = useDebouncedValue(sort, 500);
const query = useQuery({ ...params, text: debouncedText, type }, debouncedSort);
const { data } = useEndpointDataExperimental('integrations.list', query);
const router = useRoute('admin-integrations');
const onClick = (_id, type) => () => router.push({
context: 'edit',
type: type === 'webhook-incoming' ? 'incoming' : 'outgoing',
id: _id,
});
const onHeaderClick = useCallback((id) => {
const [sortBy, sortDirection] = sort;
if (sortBy === id) {
setSort([id, sortDirection === 'asc' ? 'desc' : 'asc']);
return;
}
setSort([id, 'asc']);
}, [sort]);
const header = useMemo(() => [
<Th key={'name'} direction={sort[1]} active={sort[0] === 'name'} onClick={onHeaderClick} sort='name' w={isBig ? 'x280' : 'x240'}>{t('Name')}</Th>,
<Th key={'channel'} direction={sort[1]} active={sort[0] === 'channel'} onClick={onHeaderClick} sort='channel'>{t('Post_to')}</Th>,
<Th key={'_createdBy'} direction={sort[1]} active={sort[0] === '_createdBy'} onClick={onHeaderClick} sort='_createdBy'>{t('Created_by')}</Th>,
isBig && <Th key={'_createdAt'} direction={sort[1]} active={sort[0] === '_createdAt'} onClick={onHeaderClick} sort='_createdAt'>{t('Created_at')}</Th>,
<Th key={'username'} direction={sort[1]} active={sort[0] === 'username'} onClick={onHeaderClick} sort='username'>{t('Post_as')}</Th>,
].filter(Boolean), [sort, isBig]);
const renderRow = useCallback(({ name, _id, type, username, _createdAt, _createdBy: { username: createdBy }, channel }) => {
const handler = useMemo(() => onClick(_id, type), []);
return <Table.Row key={_id} onKeyDown={handler} onClick={handler} tabIndex={0} role='link' action>
<Table.Cell style={style} color='default' fontScale='p2'>{name}</Table.Cell>
<Table.Cell style={style}>{channel.join(', ')}</Table.Cell>
<Table.Cell style={style}>{createdBy}</Table.Cell>
{isBig && <Table.Cell style={style}>{formatDateAndTime(_createdAt)}</Table.Cell>}
<Table.Cell style={style}>{username}</Table.Cell>
</Table.Row>;
}, []);
return <GenericTable ref={ref} FilterComponent={FilterByTypeAndText} header={header} renderRow={renderRow} results={data && data.integrations} total={data && data.total} setParams={setParams} params={params} />;
}
export default IntegrationsTable;

@ -0,0 +1,264 @@
import {
Field,
TextInput,
Box,
ToggleSwitch,
Icon,
TextAreaInput,
FieldGroup,
Margins,
Select,
Accordion,
} from '@rocket.chat/fuselage';
import React, { useMemo } from 'react';
import { useHilightCode } from '../../hooks/useHilightCode';
import { useExampleData } from './exampleIncomingData';
import { useTranslation } from '../../contexts/TranslationContext';
import Page from '../../components/basic/Page';
import { integrations as eventList } from '../../../app/integrations/lib/rocketchat';
export default function OutgoingWebhookForm({ formValues, formHandlers, append, ...props }) {
const t = useTranslation();
const {
enabled,
impersonateUser,
event,
urls,
triggerWords,
targetRoom,
channel,
username,
name,
alias,
avatar: avatarUrl,
emoji,
token,
scriptEnabled,
script,
retryFailedCalls,
retryCount,
retryDelay,
triggerWordAnywhere,
} = formValues;
const {
runOnEdits,
handleEvent,
handleEnabled,
handleName,
handleChannel,
handleTriggerWords,
handleTargetRoom,
handleUrls,
handleImpersonateUser,
handleUsername,
handleAlias,
handleAvatar,
handleEmoji,
handleToken,
handleScriptEnabled,
handleScript,
handleRetryFailedCalls,
handleRetryCount,
handleRetryDelay,
handleTriggerWordAnywhere,
handleRunOnEdits,
} = formHandlers;
const retryDelayOptions = useMemo(() => [
['powers-of-ten', t('powers-of-ten')],
['powers-of-two', t('powers-of-two')],
['increments-of-two', t('increments-of-two')],
], []);
const { outgoingEvents } = eventList;
const eventOptions = useMemo(() => Object.entries(outgoingEvents).map(([key, val]) => [key, t(val.label)]), []);
const hilightCode = useHilightCode();
const showChannel = useMemo(() => outgoingEvents[event].use.channel, [event]);
const showTriggerWords = useMemo(() => outgoingEvents[event].use.triggerWords, [event]);
const showTargetRoom = useMemo(() => outgoingEvents[event].use.targetRoom, [event]);
const [exampleData] = useExampleData({
aditionalFields: {
...alias && { alias },
...emoji && { emoji },
...avatarUrl && { avatar: avatarUrl },
},
url: null,
}, [alias, emoji, avatarUrl]);
const hilightedExampleJson = hilightCode('json', JSON.stringify(exampleData, null, 2));
return <Page.ScrollableContent pb='x24' mi='neg-x24' is='form' qa-admin-user-edit='form' { ...props }>
<Margins block='x16'>
<Accordion width='x600' alignSelf='center' >
<FieldGroup>
{ useMemo(() => <Field>
<Field.Label>{t('Event_Trigger')}</Field.Label>
<Field.Row>
<Select flexGrow={1} value={event} options={eventOptions} onChange={handleEvent}/>
</Field.Row>
<Field.Hint>{t('Event_Trigger_Description')}</Field.Hint>
</Field>, [event]) }
{ useMemo(() => <Field>
<Field.Label display='flex' justifyContent='space-between' w='full'>
{t('Enabled')}
<ToggleSwitch checked={enabled} onChange={handleEnabled} />
</Field.Label>
</Field>, [enabled]) }
{ useMemo(() => <Field>
<Field.Label>{t('Name_optional')}</Field.Label>
<Field.Row>
<TextInput flexGrow={1} value={name} onChange={handleName}/>
</Field.Row>
<Field.Hint>{t('You_should_name_it_to_easily_manage_your_integrations')}</Field.Hint>
</Field>, [name])}
{ useMemo(() => showChannel && <Field>
<Field.Label>{t('Channel')}</Field.Label>
<Field.Row>
<TextInput flexGrow={1} value={channel} onChange={handleChannel} addon={<Icon name='at' size='x20'/>}/>
</Field.Row>
<Field.Hint>{t('Channel_to_listen_on')}</Field.Hint>
<Field.Hint dangerouslySetInnerHTML={{ __html: t('Start_with_s_for_user_or_s_for_channel_Eg_s_or_s', '@', '#', '@john', '#general') }} />
<Field.Hint dangerouslySetInnerHTML={{ __html: t('Integrations_for_all_channels') }} />
</Field>, [showChannel, channel])}
{ useMemo(() => showTriggerWords && <Field>
<Field.Label>{t('Trigger_Words')}</Field.Label>
<Field.Row>
<TextInput flexGrow={1} value={triggerWords} onChange={handleTriggerWords}/>
</Field.Row>
<Field.Hint>{t('When_a_line_starts_with_one_of_there_words_post_to_the_URLs_below')}</Field.Hint>
<Field.Hint>{t('Separate_multiple_words_with_commas')}</Field.Hint>
</Field>, [triggerWords])}
{ useMemo(() => showTargetRoom && <Field>
<Field.Label>{t('TargetRoom')}</Field.Label>
<Field.Row>
<TextInput flexGrow={1} value={targetRoom} onChange={handleTargetRoom}/>
</Field.Row>
<Field.Hint>{t('TargetRoom_Description')}</Field.Hint>
<Field.Hint dangerouslySetInnerHTML={{ __html: t('Start_with_s_for_user_or_s_for_channel_Eg_s_or_s', '@', '#', '@john', '#general') }} />
</Field>, [showTargetRoom, targetRoom])}
{ useMemo(() => <Field>
<Field.Label>{t('URLs')}</Field.Label>
<Field.Row>
<TextAreaInput rows={10} flexGrow={1} value={urls} onChange={handleUrls} addon={<Icon name='permalink' size='x20'/>}/>
</Field.Row>
</Field>, [urls])}
{ useMemo(() => <Field>
<Field.Label display='flex' justifyContent='space-between' w='full'>
{t('Impersonate_user')}
<ToggleSwitch checked={impersonateUser} onChange={handleImpersonateUser} />
</Field.Label>
</Field>, [impersonateUser])}
{ useMemo(() => <Field>
<Field.Label>{t('Post_as')}</Field.Label>
<Field.Row>
<TextInput flexGrow={1} value={username} onChange={handleUsername} addon={<Icon name='user' size='x20'/>}/>
</Field.Row>
<Field.Hint>{t('Choose_the_username_that_this_integration_will_post_as')}</Field.Hint>
<Field.Hint>{t('Should_exists_a_user_with_this_username')}</Field.Hint>
</Field>, [username])}
{ useMemo(() => <Field>
<Field.Label>{`${ t('Alias') } (${ t('optional') })`}</Field.Label>
<Field.Row>
<TextInput flexGrow={1} value={alias} onChange={handleAlias} addon={<Icon name='edit' size='x20'/>}/>
</Field.Row>
<Field.Hint>{t('Choose_the_alias_that_will_appear_before_the_username_in_messages')}</Field.Hint>
</Field>, [alias])}
{ useMemo(() => <Field>
<Field.Label>{`${ t('Avatar_URL') } (${ t('optional') })`}</Field.Label>
<Field.Row>
<TextInput flexGrow={1} value={avatarUrl} onChange={handleAvatar} addon={<Icon name='user-rounded' size='x20' alignSelf='center'/>}/>
</Field.Row>
<Field.Hint>{t('You_can_change_a_different_avatar_too')}</Field.Hint>
<Field.Hint>{t('Should_be_a_URL_of_an_image')}</Field.Hint>
</Field>, [avatarUrl])}
{ useMemo(() => <Field>
<Field.Label>{`${ t('Emoji') } (${ t('optional') })`}</Field.Label>
<Field.Row>
<TextInput flexGrow={1} value={emoji} onChange={handleEmoji} addon={<Icon name='emoji' size='x20' alignSelf='center'/>}/>
</Field.Row>
<Field.Hint>{t('You_can_use_an_emoji_as_avatar')}</Field.Hint>
<Field.Hint dangerouslySetInnerHTML={{ __html: t('Example_s', ':ghost:') }} />
</Field>, [emoji])}
{ useMemo(() => <Field>
<Field.Label>{`${ t('Token') } (${ t('Optional') })`}</Field.Label>
<Field.Row>
<TextInput flexGrow={1} value={token} onChange={handleToken} addon={<Icon name='key' size='x20'/>}/>
</Field.Row>
</Field>, [token])}
{ useMemo(() => <Field>
<Field.Label display='flex' justifyContent='space-between' w='full'>
{t('Script_Enabled')}
<ToggleSwitch checked={scriptEnabled} onChange={handleScriptEnabled} />
</Field.Label>
</Field>, [scriptEnabled])}
{ useMemo(() => <Field>
<Field.Label>{t('Script')}</Field.Label>
<Field.Row>
<TextAreaInput rows={10} flexGrow={1} value={script} onChange={handleScript} addon={<Icon name='code' size='x20' alignSelf='center'/>}/>
</Field.Row>
</Field>, [script])}
{ useMemo(() => <Field>
<Field.Label>{t('Responding')}</Field.Label>
<Field.Hint>{t('Response_description_pre')}</Field.Hint>
<Field.Row>
<Box fontScale='p1' withRichContent flexGrow={1}>
<pre><code dangerouslySetInnerHTML={{ __html: hilightedExampleJson }}></code></pre>
</Box>
</Field.Row>
<Field.Hint>{t('Response_description_post')}</Field.Hint>
</Field>, [hilightedExampleJson])}
</FieldGroup>
<Accordion.Item title={t('Integration_Advanced_Settings')}>
<FieldGroup>
{ useMemo(() => <Field>
<Field.Label display='flex' justifyContent='space-between' w='full'>
{t('Integration_Retry_Failed_Url_Calls')}
<ToggleSwitch checked={retryFailedCalls} onChange={handleRetryFailedCalls} />
</Field.Label>
<Field.Hint>{t('Integration_Retry_Failed_Url_Calls_Description')}</Field.Hint>
</Field>, [retryFailedCalls])}
{ useMemo(() => <Field>
<Field.Label>{t('Retry_Count')}</Field.Label>
<Field.Row>
<TextInput flexGrow={1} value={retryCount} onChange={handleRetryCount}/>
</Field.Row>
<Field.Hint>{t('Integration_Retry_Count_Description')}</Field.Hint>
</Field>, [retryCount])}
{ useMemo(() => <Field>
<Field.Label>{t('Integration_Retry_Delay')}</Field.Label>
<Field.Row>
<Select flexGrow={1} value={retryDelay} options={retryDelayOptions} onChange={handleRetryDelay}/>
</Field.Row>
<Field.Hint dangerouslySetInnerHTML={{ __html: t('Integration_Retry_Delay_Description') }}/>
</Field>, [retryDelay])}
{ useMemo(() => event === 'sendMessage' && <FieldGroup>
<Field>
<Field.Label display='flex' justifyContent='space-between' w='full'>
{t('Integration_Word_Trigger_Placement')}
<ToggleSwitch checked={triggerWordAnywhere} onChange={handleTriggerWordAnywhere} />
</Field.Label>
<Field.Hint>{t('Integration_Word_Trigger_Placement_Description')}</Field.Hint>
</Field>
<Field>
<Field.Label display='flex' justifyContent='space-between' w='full'>
{t('Integration_Word_Trigger_Placement')}
<ToggleSwitch checked={runOnEdits} onChange={handleRunOnEdits} />
</Field.Label>
<Field.Hint>{t('Integration_Run_When_Message_Is_Edited_Description')}</Field.Hint>
</Field>
</FieldGroup>, [triggerWordAnywhere, runOnEdits])}
</FieldGroup>
</Accordion.Item>
{ append }
</Accordion>
</Margins>
</Page.ScrollableContent>;
}

@ -0,0 +1,105 @@
import React, { useMemo, useState } from 'react';
import { Field, Box, Headline, Skeleton, Margins, Button } from '@rocket.chat/fuselage';
import { SuccessModal, DeleteWarningModal } from './EditIntegrationsPage';
import { useTranslation } from '../../../contexts/TranslationContext';
import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../../hooks/useEndpointDataExperimental';
import { useMethod } from '../../../contexts/ServerContext';
import { useEndpointAction } from '../../../hooks/useEndpointAction';
import { useRoute } from '../../../contexts/RouterContext';
import { useToastMessageDispatch } from '../../../contexts/ToastMessagesContext';
import { useForm } from '../../../hooks/useForm';
import IncomingWebhookForm from '../IncomingWebhookForm';
export default function EditIncomingWebhookWithData({ integrationId, ...props }) {
const t = useTranslation();
const [cache, setCache] = useState();
const { data, state, error } = useEndpointDataExperimental('integrations.get', useMemo(() => ({ integrationId }), [integrationId, cache]));
const onChange = () => setCache(new Date());
if (state === ENDPOINT_STATES.LOADING) {
return <Box w='full' pb='x24' {...props}>
<Headline.Skeleton mbe='x4'/>
<Skeleton mbe='x8' />
<Headline.Skeleton mbe='x4'/>
<Skeleton mbe='x8'/>
<Headline.Skeleton mbe='x4'/>
<Skeleton mbe='x8'/>
</Box>;
}
if (error) {
return <Box mbs='x16' {...props}>{t('Oops_page_not_found')}</Box>;
}
return <EditIncomingWebhook data={data.integration} onChange={onChange} {...props}/>;
}
const getInitialValue = (data) => {
const initialValue = {
enabled: data.enabled,
channel: data.channel.join(', ') ?? '',
username: data.username ?? '',
name: data.name ?? '',
alias: data.alias ?? '',
avatarUrl: data.avatarUrl ?? '',
emoji: data.emoji ?? '',
scriptEnabled: data.scriptEnabled,
script: data.script,
};
return initialValue;
};
function EditIncomingWebhook({ data, onChange, ...props }) {
const t = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
const { values: formValues, handlers: formHandlers, reset } = useForm(getInitialValue(data));
const [modal, setModal] = useState();
const deleteQuery = useMemo(() => ({ type: 'webhook-incoming', integrationId: data._id }), [data._id]);
const deleteIntegration = useEndpointAction('POST', 'integrations.remove', deleteQuery);
const saveIntegration = useMethod('updateIncomingIntegration');
const router = useRoute('admin-integrations');
const handleDeleteIntegration = () => {
const closeModal = () => setModal();
const onDelete = async () => {
const result = await deleteIntegration();
if (result.success) { setModal(<SuccessModal onClose={() => { closeModal(); router.push({}); }}/>); }
};
setModal(<DeleteWarningModal onDelete={onDelete} onCancel={closeModal} />);
};
const handleSave = async () => {
try {
await saveIntegration(data._id, { ...formValues });
dispatchToastMessage({ type: 'success', message: t('Integration_updated') });
onChange();
} catch (e) {
dispatchToastMessage({ type: 'error', message: e });
}
};
const actionButtons = useMemo(() => <Field>
<Field.Row display='flex' flexDirection='column'>
<Box display='flex' flexDirection='row' justifyContent='space-between' w='full'>
<Margins inlineEnd='x4'>
<Button flexGrow={1} type='reset' onClick={reset}>{t('Reset')}</Button>
<Button mie='none' flexGrow={1} onClick={handleSave}>{t('Save')}</Button>
</Margins>
</Box>
<Button mbs='x4' primary danger w='full' onClick={handleDeleteIntegration} >{t('Delete')}</Button>
</Field.Row>
</Field>);
return <>
<IncomingWebhookForm formHandlers={formHandlers} formValues={formValues} extraData={{ _id: data._id, token: data.token }} append={actionButtons} {...props}/>
{ modal }
</>;
}

@ -0,0 +1,82 @@
import { Button, ButtonGroup, Icon } from '@rocket.chat/fuselage';
import React, { useCallback } from 'react';
import Page from '../../../components/basic/Page';
import EditIncomingWebhookWithData from './EditIncomingWebhook';
import EditOutgoingWebhookWithData from './EditOutgoingWebhook';
import { Modal } from '../../../components/basic/Modal';
import { useTranslation } from '../../../contexts/TranslationContext';
import { useRouteParameter, useRoute } from '../../../contexts/RouterContext';
export const DeleteWarningModal = ({ onDelete, onCancel, ...props }) => {
const t = useTranslation();
return <Modal {...props}>
<Modal.Header>
<Icon color='danger' name='modal-warning' size={20}/>
<Modal.Title>{t('Are_you_sure')}</Modal.Title>
<Modal.Close onClick={onCancel}/>
</Modal.Header>
<Modal.Content fontScale='p1'>
{t('Integration_Delete_Warning')}
</Modal.Content>
<Modal.Footer>
<ButtonGroup align='end'>
<Button ghost onClick={onCancel}>{t('Cancel')}</Button>
<Button primary danger onClick={onDelete}>{t('Delete')}</Button>
</ButtonGroup>
</Modal.Footer>
</Modal>;
};
export const SuccessModal = ({ onClose, ...props }) => {
const t = useTranslation();
return <Modal {...props}>
<Modal.Header>
<Icon color='success' name='checkmark-circled' size={20}/>
<Modal.Title>{t('Deleted')}</Modal.Title>
<Modal.Close onClick={onClose}/>
</Modal.Header>
<Modal.Content fontScale='p1'>
{t('Your_entry_has_been_deleted')}
</Modal.Content>
<Modal.Footer>
<ButtonGroup align='end'>
<Button primary onClick={onClose}>{t('Ok')}</Button>
</ButtonGroup>
</Modal.Footer>
</Modal>;
};
export default function NewIntegrationsPage({ ...props }) {
const t = useTranslation();
const router = useRoute('admin-integrations');
const type = useRouteParameter('type');
const integrationId = useRouteParameter('id');
const handleClickReturn = useCallback(() => {
router.push({ });
}, []);
const handleClickHistory = useCallback(() => {
router.push({ context: 'history', type: 'outgoing', id: integrationId });
}, [integrationId]);
return <Page flexDirection='column' {...props}>
<Page.Header title={type === 'incoming' ? t('Integration_Incoming_WebHook') : t('Integration_Outgoing_WebHook')} >
<ButtonGroup>
<Button onClick={handleClickReturn}>
<Icon name='back' size='x16'/> {t('Back')}
</Button>
{type === 'outgoing' && <Button onClick={handleClickHistory}>{t('History')}</Button>}
</ButtonGroup>
</Page.Header>
<Page.ScrollableContentWithShadow>
{
(type === 'outgoing' && <EditOutgoingWebhookWithData integrationId={integrationId} key='outgoing'/>)
|| (type === 'incoming' && <EditIncomingWebhookWithData integrationId={integrationId} key='incoming'/>)
}
</Page.ScrollableContentWithShadow>
</Page>;
}

@ -0,0 +1,134 @@
import React, { useMemo, useState } from 'react';
import {
Field,
Box,
Headline,
Skeleton,
Margins,
Button,
} from '@rocket.chat/fuselage';
import { SuccessModal, DeleteWarningModal } from './EditIntegrationsPage';
import { useTranslation } from '../../../contexts/TranslationContext';
import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../../hooks/useEndpointDataExperimental';
import { useEndpointAction } from '../../../hooks/useEndpointAction';
import { useRoute } from '../../../contexts/RouterContext';
import { useMethod } from '../../../contexts/ServerContext';
import { useToastMessageDispatch } from '../../../contexts/ToastMessagesContext';
import OutgoingWebhookForm from '../OutgoiongWebhookForm';
import { useForm } from '../../../hooks/useForm';
export default function EditOutgoingWebhookWithData({ integrationId, ...props }) {
const t = useTranslation();
const [cache, setCache] = useState();
const { data, state, error } = useEndpointDataExperimental('integrations.get', useMemo(() => ({ integrationId }), [integrationId, cache]));
const onChange = () => setCache(new Date());
if (state === ENDPOINT_STATES.LOADING) {
return <Box w='full' pb='x24' {...props}>
<Headline.Skeleton mbe='x4'/>
<Skeleton mbe='x8' />
<Headline.Skeleton mbe='x4'/>
<Skeleton mbe='x8'/>
<Headline.Skeleton mbe='x4'/>
<Skeleton mbe='x8'/>
</Box>;
}
if (error) {
return <Box mbs='x16' {...props}>{t('Oops_page_not_found')}</Box>;
}
return <EditOutgoingWebhook data={data.integration} onChange={onChange} {...props}/>;
}
const getInitialValue = (data) => {
const initialValue = {
enabled: data.enabled ?? true,
impersonateUser: data.impersonateUser,
event: data.event,
token: data.token,
urls: data.urls.join('\n') ?? '',
triggerWords: data.triggerWords?.join('; ') ?? '',
targetRoom: data.targetRoom ?? '',
channel: data.channel.join(', ') ?? '',
username: data.username ?? '',
name: data.name ?? '',
alias: data.alias ?? '',
avatarUrl: data.avatarUrl ?? '',
emoji: data.emoji ?? '',
scriptEnabled: data.scriptEnabled ?? false,
script: data.script ?? '',
retryFailedCalls: data.retryFailedCalls ?? true,
retryCount: data.retryCount ?? 5,
retryDelay: data.retryDelay ?? 'power-of-ten',
triggerrWordAnywhere: data.triggerrWordAnywhere ?? false,
runOnEdits: data.runOnEdits ?? true,
};
return initialValue;
};
function EditOutgoingWebhook({ data, onChange, setSaveAction, ...props }) {
const t = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
const { handlers: formHandlers, values: formValues, reset } = useForm(getInitialValue(data));
const [modal, setModal] = useState();
const saveIntegration = useMethod('updateOutgoingIntegration');
const router = useRoute('admin-integrations');
const deleteQuery = useMemo(() => ({ type: 'webhook-outgoing', integrationId: data._id }), [data._id]);
const deleteIntegration = useEndpointAction('POST', 'integrations.remove', deleteQuery);
const handleDeleteIntegration = () => {
const closeModal = () => setModal();
const onDelete = async () => {
const result = await deleteIntegration();
if (result.success) { setModal(<SuccessModal onClose={() => { closeModal(); router.push({}); }}/>); }
};
setModal(<DeleteWarningModal onDelete={onDelete} onCancel={closeModal} />);
};
const {
urls,
triggerWords,
} = formValues;
const handleSave = async () => {
try {
await saveIntegration(data._id, {
...formValues,
triggerWords: triggerWords.split(';'),
urls: urls.split('\n'),
});
dispatchToastMessage({ type: 'success', message: t('Integration_updated') });
onChange();
} catch (e) {
dispatchToastMessage({ type: 'error', message: e });
}
};
const actionButtons = useMemo(() => <Field>
<Field.Row display='flex' flexDirection='column'>
<Box display='flex' flexDirection='row' justifyContent='space-between' w='full'>
<Margins inlineEnd='x4'>
<Button flexGrow={1} type='reset' onClick={reset}>{t('Reset')}</Button>
<Button mie='none' flexGrow={1} onClick={handleSave}>{t('Save')}</Button>
</Margins>
</Box>
<Button mbs='x4' primary danger w='full' onClick={handleDeleteIntegration}>{t('Delete')}</Button>
</Field.Row>
</Field>);
return <>
<OutgoingWebhookForm formValues={formValues} formHandlers={formHandlers} append={actionButtons} {...props}/>
{ modal }
</>;
}

@ -0,0 +1,268 @@
import { Button, ButtonGroup, Icon, Headline, Skeleton, Box, Accordion, Field, FieldGroup, Pagination } from '@rocket.chat/fuselage';
import React, { useMemo, useCallback, useState, useEffect } from 'react';
import Page from '../../../components/basic/Page';
import { useTranslation } from '../../../contexts/TranslationContext';
import { useHilightCode } from '../../../hooks/useHilightCode';
import { integrations as eventList } from '../../../../app/integrations/lib/rocketchat';
import { useMethod } from '../../../contexts/ServerContext';
import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../../hooks/useEndpointDataExperimental';
import { useRoute, useRouteParameter } from '../../../contexts/RouterContext';
import { useFormatDateAndTime } from '../../../hooks/useFormatDateAndTime';
import { useToastMessageDispatch } from '../../../contexts/ToastMessagesContext';
function HistoryItem({ data, onChange, ...props }) {
const t = useTranslation();
const hilightCode = useHilightCode();
const replayOutgoingIntegration = useMethod('replayOutgoingIntegration');
const {
_id,
_createdAt,
_updatedAt,
httpResult,
event,
step,
httpCallData,
data: dataSentToTrigger,
prepareSentMessage,
processSentMessage,
url,
httpError,
errorStack,
error,
integration: { _id: integrationId },
} = data;
const handleClickReplay = useCallback((e) => {
e.stopPropagation();
replayOutgoingIntegration({ integrationId, historyId: _id });
onChange();
}, [_id]);
const formatDateAndTime = useFormatDateAndTime();
return <Accordion.Item
title={
<Box display='inline-flex' w='full' flexDirection='row' justifyContent='space-between'>
<Box display='flex' flexDirection='row' alignItems='center'>
<Icon name='info-circled' size='x16' mie='x4'/>{formatDateAndTime(_createdAt)}
</Box>
<Button ghost onClick={handleClickReplay}>{t('Replay')}</Button>
</Box>
}
{...props}
>
<FieldGroup>
<Field>
<Field.Label>{t('Status')}</Field.Label>
<Field.Row>
<Box withRichContent w='full'>
<code>{error ? t('Failure') : t('Success')}</code>
</Box>
</Field.Row>
</Field>
<Field>
<Field.Label>{t('Integration_Outgoing_WebHook_History_Time_Triggered')}</Field.Label>
<Field.Row>
<Box withRichContent w='full'>
<code>{_createdAt}</code>
</Box>
</Field.Row>
</Field>
<Field>
<Field.Label>{t('Integration_Outgoing_WebHook_History_Time_Ended_Or_Error')}</Field.Label>
<Field.Row>
<Box withRichContent w='full'>
<code>{_updatedAt}</code>
</Box>
</Field.Row>
</Field>
<Field>
<Field.Label>{t('Event_Trigger')}</Field.Label>
<Field.Row>
<Box withRichContent w='full'>
<code>{t(eventList.outgoingEvents[event].label)}</code>
</Box>
</Field.Row>
</Field>
<Field>
<Field.Label>{t('Integration_Outgoing_WebHook_History_Trigger_Step')}</Field.Label>
<Field.Row>
<Box withRichContent w='full'>
<code>{step}</code>
</Box>
</Field.Row>
</Field>
<Field>
<Field.Label>{t('Integration_Outgoing_WebHook_History_Data_Passed_To_Trigger')}</Field.Label>
<Field.Row>
<Box withRichContent w='full'>
<pre><code dangerouslySetInnerHTML={{ __html: hilightCode('json', JSON.stringify(dataSentToTrigger, null, 2)) }}></code></pre>
</Box>
</Field.Row>
</Field>
{prepareSentMessage && <Field>
<Field.Label>{t('Integration_Outgoing_WebHook_History_Messages_Sent_From_Prepare_Script')}</Field.Label>
<Field.Row>
<Box withRichContent w='full'>
<pre><code dangerouslySetInnerHTML={{ __html: hilightCode('json', JSON.stringify(prepareSentMessage, null, 2)) }}></code></pre>
</Box>
</Field.Row>
</Field>}
{processSentMessage && <Field>
<Field.Label>{t('Integration_Outgoing_WebHook_History_Messages_Sent_From_Process_Script')}</Field.Label>
<Field.Row>
<Box withRichContent w='full'>
<pre><code dangerouslySetInnerHTML={{ __html: hilightCode('json', JSON.stringify(processSentMessage, null, 2)) }}></code></pre>
</Box>
</Field.Row>
</Field>}
{url && <Field>
<Field.Label>{t('URL')}</Field.Label>
<Field.Row>
<Box withRichContent w='full'>
<code>{url}</code>
</Box>
</Field.Row>
</Field>}
{httpCallData && <Field>
<Field.Label>{t('Integration_Outgoing_WebHook_History_Data_Passed_To_URL')}</Field.Label>
<Field.Row>
<Box withRichContent w='full'>
<pre><code dangerouslySetInnerHTML={{ __html: hilightCode('json', JSON.stringify(httpCallData, null, 2)) }}></code></pre>
</Box>
</Field.Row>
</Field>}
{httpError && <Field>
<Field.Label>{t('Integration_Outgoing_WebHook_History_Http_Response_Error')}</Field.Label>
<Field.Row>
<Box withRichContent w='full'>
<pre><code dangerouslySetInnerHTML={{ __html: hilightCode('json', JSON.stringify(httpError, null, 2)) }}></code></pre>
</Box>
</Field.Row>
</Field>}
{httpResult && <Field>
<Field.Label>{t('Integration_Outgoing_WebHook_History_Http_Response')}</Field.Label>
<Field.Row>
<Box withRichContent w='full'>
<pre><code dangerouslySetInnerHTML={{ __html: hilightCode('json', httpResult) }}></code></pre>
</Box>
</Field.Row>
</Field>}
{errorStack && <Field>
<Field.Label>{t('Integration_Outgoing_WebHook_History_Error_Stacktrace')}</Field.Label>
<Field.Row>
<Box withRichContent w='full'>
<pre><code dangerouslySetInnerHTML={{ __html: hilightCode('json', JSON.stringify(errorStack, null, 2)) }}></code></pre>
</Box>
</Field.Row>
</Field>}
</FieldGroup>
</Accordion.Item>;
}
function HistoryContent({ data, state, onChange, ...props }) {
const t = useTranslation();
const [loadedData, setLoadedData] = useState();
useEffect(() => {
if (state === ENDPOINT_STATES.DONE) { setLoadedData(data); }
}, [state]);
if (!loadedData || state === ENDPOINT_STATES.LOADING) {
return <Box w='full' pb='x24' {...props}>
<Headline.Skeleton mbe='x4'/>
<Skeleton mbe='x8' />
<Headline.Skeleton mbe='x4'/>
<Skeleton mbe='x8'/>
<Headline.Skeleton mbe='x4'/>
<Skeleton mbe='x8'/>
</Box>;
}
if (loadedData.history.length < 1) {
return <Box mbs='x16' {...props}>{t('Integration_Outgoing_WebHook_No_History')}</Box>;
}
return <>
<Accordion w='full' maxWidth='x600' alignSelf='center' key='content'>
{loadedData.history.map((current) => <HistoryItem
data={current}
key={current._id}
onChange={onChange}
/>)}
</Accordion>
</>;
}
function OutgoingWebhookHistoryPage(props) {
const dispatchToastMessage = useToastMessageDispatch();
const t = useTranslation();
const [cache, setCache] = useState();
const [current, setCurrent] = useState();
const [itemsPerPage, setItemsPerPage] = useState();
const onChange = useCallback(() => {
setCache(new Date());
});
const router = useRoute('admin-integrations');
const clearHistory = useMethod('clearIntegrationHistory');
const handleClearHistory = async () => {
try {
await clearHistory();
dispatchToastMessage({ type: 'success', message: t('Integration_History_Cleared') });
onChange();
} catch (e) {
dispatchToastMessage({ type: 'error', message: e });
}
};
const handleClickReturn = () => {
router.push({ });
};
const id = useRouteParameter('id');
const query = useMemo(() => ({
id,
cout: itemsPerPage,
offset: current,
}), [id, itemsPerPage, current, cache]);
const { data, state } = useEndpointDataExperimental('integrations.history', query);
const showingResultsLabel = useCallback(({ count, current, itemsPerPage }) => t('Showing results %s - %s of %s', current + 1, Math.min(current + itemsPerPage, count), count), []);
return <Page flexDirection='column' {...props}>
<Page.Header title={t('Integration_Outgoing_WebHook_History')}>
<ButtonGroup>
<Button onClick={handleClickReturn}>
<Icon name='back' size='x16'/> {t('Back')}
</Button>
<Button primary danger onClick={handleClearHistory} disabled={!(data && data.history.length > 0)}>
<Icon name='trash'/> {t('clear_history')}
</Button>
</ButtonGroup>
</Page.Header>
<Page.ScrollableContentWithShadow>
<HistoryContent key='historyContent' data={data} state={state} onChange={onChange} />
<Pagination
current={current}
itemsPerPage={itemsPerPage}
itemsPerPageLabel={t('Items_per_page:')}
showingResultsLabel={showingResultsLabel}
count={(data && data.total) || 0}
onSetItemsPerPage={setItemsPerPage}
onSetCurrent={setCurrent}
/>
</Page.ScrollableContentWithShadow>
</Page>;
}
export default OutgoingWebhookHistoryPage;

@ -0,0 +1,20 @@
import { useMemo } from 'react';
export function useExampleData({ aditionalFields, url }, dep) {
const exampleData = {
...aditionalFields,
text: 'Example message',
attachments: [{
title: 'Rocket.Chat',
title_link: 'https://rocket.chat',
text: 'Rocket.Chat, the best open source chat',
image_url: '/images/integration-attachment-example.png',
color: '#764FA5',
}],
};
return useMemo(() => [
exampleData,
`curl -X POST -H 'Content-Type: application/json' --data '${ JSON.stringify(exampleData) }' ${ url }`,
], dep);
}

@ -0,0 +1,9 @@
import React from 'react';
import { Box } from '@rocket.chat/fuselage';
import { useTranslation } from '../../../contexts/TranslationContext';
export default function NewBot() {
const t = useTranslation();
return <Box pb='x20' fontScale='s1' key='bots' dangerouslySetInnerHTML={{ __html: t('additional_integrations_Bots') }}/>;
}

@ -0,0 +1,50 @@
import React, { useMemo } from 'react';
import { Field, Box, Margins, Button } from '@rocket.chat/fuselage';
import { useTranslation } from '../../../contexts/TranslationContext';
import { useRoute } from '../../../contexts/RouterContext';
import { useEndpointAction } from '../../../hooks/useEndpointAction';
import { useForm } from '../../../hooks/useForm';
import IncomingWebhookForm from '../IncomingWebhookForm';
const initialState = {
enabled: false,
channel: '',
username: '',
name: '',
alias: '',
avatarUrl: '',
emoji: '',
scriptEnabled: false,
script: '',
};
export default function NewIncomingWebhook(props) {
const t = useTranslation();
const router = useRoute('admin-integrations');
const { values: formValues, handlers: formHandlers, reset } = useForm(initialState);
const saveAction = useEndpointAction('POST', 'integrations.create', useMemo(() => ({ ...formValues, type: 'webhook-incoming' }), [JSON.stringify(formValues)]), t('Integration_added'));
const handleSave = async () => {
const result = await saveAction();
if (result.success) {
router.push({ context: 'edit', type: 'incoming', id: result.integration._id });
}
};
const actionButtons = useMemo(() => <Field>
<Field.Row>
<Box display='flex' flexDirection='row' justifyContent='space-between' w='full'>
<Margins inlineEnd='x4'>
<Button flexGrow={1} type='reset' onClick={reset}>{t('Reset')}</Button>
<Button mie='none' flexGrow={1} onClick={handleSave}>{t('Save')}</Button>
</Margins>
</Box>
</Field.Row>
</Field>);
return <IncomingWebhookForm formValues={formValues} formHandlers={formHandlers} append={actionButtons} {...props}/>;
}

@ -0,0 +1,58 @@
import { Tabs, Button, ButtonGroup, Icon } from '@rocket.chat/fuselage';
import React, { useCallback } from 'react';
import Page from '../../../components/basic/Page';
import NewIncomingWebhook from './NewIncomingWebhook';
import NewOutgoingWebhook from './NewOutgoingWebhook';
import { useTranslation } from '../../../contexts/TranslationContext';
import { useRouteParameter, useRoute } from '../../../contexts/RouterContext';
export default function NewIntegrationsPage({ ...props }) {
const t = useTranslation();
const router = useRoute('admin-integrations');
const handleClickTab = (type) => () => {
router.push({ context: 'new', type });
};
const handleClickReturn = useCallback(() => {
router.push({ });
}, []);
const tab = useRouteParameter('type');
const handleIncomingTab = useCallback(handleClickTab('incoming'), []);
const handleOutgoingTab = useCallback(handleClickTab('outgoing'), []);
return <Page flexDirection='column' {...props}>
<Page.Header title={t('Integrations')} >
<ButtonGroup>
<Button onClick={handleClickReturn}>
<Icon name='back' size='x16'/> {t('Back')}
</Button>
</ButtonGroup>
</Page.Header>
<Tabs>
<Tabs.Item
selected={tab === 'incoming'}
onClick={handleIncomingTab}
>
{t('Incoming')}
</Tabs.Item>
<Tabs.Item
selected={tab === 'outgoing'}
onClick={handleOutgoingTab}
>
{t('Outgoing')}
</Tabs.Item>
</Tabs>
<Page.ScrollableContentWithShadow>
{
(tab === 'incoming' && <NewIncomingWebhook key='incoming'/>)
|| (tab === 'outgoing' && <NewOutgoingWebhook key='outgoing'/>)
}
</Page.ScrollableContentWithShadow>
</Page>;
}

@ -0,0 +1,68 @@
import React, { useMemo, useCallback } from 'react';
import { Field, Button } from '@rocket.chat/fuselage';
import { useUniqueId } from '@rocket.chat/fuselage-hooks';
import { useTranslation } from '../../../contexts/TranslationContext';
import { useEndpointAction } from '../../../hooks/useEndpointAction';
import { useRoute } from '../../../contexts/RouterContext';
import { useForm } from '../../../hooks/useForm';
import OutgoingWebhookForm from '../OutgoiongWebhookForm';
const defaultData = {
type: 'webhook-outgoing',
enabled: true,
impersonateUser: false,
event: 'sendMessage',
urls: '',
triggerWords: '',
targetRoom: '',
channel: '',
username: '',
name: '',
alias: '',
avatar: '',
emoji: '',
scriptEnabled: false,
script: '',
retryFailedCalls: true,
retryCount: 6,
retryDelay: 'powers-of-ten',
triggerWordAnywhere: false,
runOnEdits: true,
};
export default function NewOutgoingWebhook({ data = defaultData, onChange, setSaveAction, ...props }) {
const t = useTranslation();
const router = useRoute('admin-integrations');
const { values: formValues, handlers: formHandlers } = useForm({ ...data, token: useUniqueId() });
const {
urls,
triggerWords,
} = formValues;
const query = useMemo(() => ({
...formValues,
urls: urls.split('\n'),
triggerWords: triggerWords.split(';'),
}), [JSON.stringify(formValues)]);
const saveIntegration = useEndpointAction('POST', 'integrations.create', query, t('Integration_added'));
const handleSave = useCallback(async () => {
const result = await saveIntegration();
if (result.success) {
router.push({ id: result.integration._id, context: 'edit', type: 'outgoing' });
}
}, [saveIntegration, router]);
const saveButton = useMemo(() => <Field>
<Field.Row>
<Button w='full' mie='none' flexGrow={1} onClick={handleSave}>{t('Save')}</Button>
</Field.Row>
</Field>);
return <OutgoingWebhookForm formValues={formValues} formHandlers={formHandlers} append={saveButton} {...props}/>;
}

@ -0,0 +1,43 @@
import React, { useEffect, useState } from 'react';
import { Box, Skeleton, Margins } from '@rocket.chat/fuselage';
import { useTranslation } from '../../../contexts/TranslationContext';
const blogSpotStyleScriptImport = (src) => new Promise((resolve) => {
const script = document.createElement('script');
script.type = 'text/javascript';
document.body.appendChild(script);
const resolveFunc = (event) => resolve(event.currentTarget);
script.onreadystatechange = resolveFunc;
script.onload = resolveFunc;
script.src = src;
});
export default function NewZapier({ ...props }) {
const t = useTranslation();
const [script, setScript] = useState();
useEffect(() => {
const importZapier = async () => {
const scriptEl = await blogSpotStyleScriptImport('https://zapier.com/apps/embed/widget.js?services=rocketchat&html_id=zapier-goes-here');
setScript(scriptEl);
};
if (!script) { importZapier(); }
return () => script && script.parentNode.removeChild(script);
}, [script]);
return <>
<Box pb='x20' fontScale='s1' dangerouslySetInnerHTML={{ __html: t('additional_integrations_Zapier') }}/>
{!script && <Box display='flex' flexDirection='column' alignItems='stretch' mbs={10}>
<Margins blockEnd={14}>
<Skeleton variant='rect' height={71}/>
<Skeleton variant='rect' height={71}/>
<Skeleton variant='rect' height={71}/>
<Skeleton variant='rect' height={71}/>
<Skeleton variant='rect' height={71}/>
</Margins>
</Box>}
<Box id='zapier-goes-here' {...props} />
</>;
}

@ -0,0 +1,87 @@
import React, { useCallback, useState } from 'react';
import {
Button,
ButtonGroup,
TextInput,
Field,
TextAreaInput,
ToggleSwitch,
FieldGroup,
} from '@rocket.chat/fuselage';
import { useTranslation } from '../../contexts/TranslationContext';
import { useMethod } from '../../contexts/ServerContext';
import { useRoute } from '../../contexts/RouterContext';
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';
import VerticalBar from '../../components/basic/VerticalBar';
export default function OAuthAddApp(props) {
const t = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
const [newData, setNewData] = useState({
name: '',
active: false,
redirectUri: '',
});
const saveApp = useMethod('addOAuthApp');
const router = useRoute('admin-oauth-apps');
const close = useCallback(() => router.push({}), [router]);
const handleSave = useCallback(async () => {
try {
await saveApp(
newData,
);
close();
dispatchToastMessage({ type: 'success', message: t('Application_added') });
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
}
}, [JSON.stringify(newData)]);
const handleChange = (field, getValue = (e) => e.currentTarget.value) => (e) => setNewData({ ...newData, [field]: getValue(e) });
const {
active,
name,
redirectUri,
} = newData;
return <VerticalBar.ScrollableContent w='full' {...props}>
<FieldGroup maxWidth='x600' alignSelf='center' w='full'>
<Field>
<Field.Label display='flex' justifyContent='space-between' w='full'>
{t('Active')}
<ToggleSwitch checked={active} onChange={handleChange('active', () => !active)}/>
</Field.Label>
</Field>
<Field>
<Field.Label>{t('Application_Name')}</Field.Label>
<Field.Row>
<TextInput value={name} onChange={handleChange('name')} />
</Field.Row>
<Field.Hint>{t('Give_the_application_a_name_This_will_be_seen_by_your_users')}</Field.Hint>
</Field>
<Field>
<Field.Label>{t('Redirect_URI')}</Field.Label>
<Field.Row>
<TextAreaInput rows={5} value={redirectUri} onChange={handleChange('redirectUri')}/>
</Field.Row>
<Field.Hint>{t('After_OAuth2_authentication_users_will_be_redirected_to_this_URL')}</Field.Hint>
</Field>
<Field>
<Field.Row>
<ButtonGroup stretch w='full'>
<Button onClick={close}>{t('Cancel')}</Button>
<Button primary onClick={handleSave} >{t('Save')}</Button>
</ButtonGroup>
</Field.Row>
</Field>
</FieldGroup>
</VerticalBar.ScrollableContent>;
}

@ -0,0 +1,39 @@
import React from 'react';
import { Button, Icon } from '@rocket.chat/fuselage';
import Page from '../../components/basic/Page';
// import VerticalBar from '../../components/basic/VerticalBar';
import { useTranslation } from '../../contexts/TranslationContext';
import { useRouteParameter, useRoute } from '../../contexts/RouterContext';
import OAuthAppsTable from './OAuthAppsTable';
import OAuthEditAppWithData from './OAuthEditApp';
import OAuthAddApp from './OAuthAddApp';
export function OAuthAppsPage() {
const t = useTranslation();
const router = useRoute('admin-oauth-apps');
const context = useRouteParameter('context');
const id = useRouteParameter('id');
return <Page flexDirection='row'>
<Page>
<Page.Header title={t('OAuth_Applications')}>
{context && <Button alignSelf='flex-end' onClick={() => router.push({})}>
<Icon name='back'/>{t('Back')}
</Button>}
{!context && <Button primary alignSelf='flex-end' onClick={() => router.push({ context: 'new' })}>
<Icon name='plus'/>{t('New_Application')}
</Button>}
</Page.Header>
<Page.Content>
{!context && <OAuthAppsTable />}
{context === 'edit' && <OAuthEditAppWithData _id={id}/>}
{context === 'new' && <OAuthAddApp />}
</Page.Content>
</Page>
</Page>;
}
export default OAuthAppsPage;

@ -0,0 +1,15 @@
import React from 'react';
import { usePermission } from '../../contexts/AuthorizationContext';
import NotAuthorizedPage from '../NotAuthorizedPage';
import OAuthAppsPage from './OAuthAppsPage';
export default function MailerRoute() {
const canAccessOAuthApps = usePermission('manage-oauth-apps');
if (!canAccessOAuthApps) {
return <NotAuthorizedPage/>;
}
return <OAuthAppsPage />;
}

@ -0,0 +1,40 @@
import { Table } from '@rocket.chat/fuselage';
import React, { useMemo, useCallback } from 'react';
import { GenericTable, Th } from '../../../app/ui/client/components/GenericTable';
import { useTranslation } from '../../contexts/TranslationContext';
import { useRoute } from '../../contexts/RouterContext';
import { useEndpointDataExperimental } from '../../hooks/useEndpointDataExperimental';
import { useFormatDateAndTime } from '../../hooks/useFormatDateAndTime';
export function OAuthAppsTable() {
const t = useTranslation();
const formatDateAndTime = useFormatDateAndTime();
const { data } = useEndpointDataExperimental('oauth-apps.list', useMemo(() => ({}), []));
const router = useRoute('admin-oauth-apps');
const onClick = (_id) => () => router.push({
context: 'edit',
id: _id,
});
const header = useMemo(() => [
<Th key={'name'}>{t('Name')}</Th>,
<Th key={'_createdBy'}>{t('Created_by')}</Th>,
<Th key={'_createdAt'}>{t('Created_at')}</Th>,
]);
const renderRow = useCallback(({ _id, name, _createdAt, _createdBy: { username: createdBy } }) =>
<Table.Row key={_id} onKeyDown={onClick(_id)} onClick={onClick(_id)} tabIndex={0} role='link' action qa-oauth-app-id={_id}>
<Table.Cell withTruncatedText color='default' fontScale='p2'>{name}</Table.Cell>
<Table.Cell withTruncatedText>{createdBy}</Table.Cell>
<Table.Cell withTruncatedText>{formatDateAndTime(_createdAt)}</Table.Cell>
</Table.Row>,
);
return <GenericTable header={header} renderRow={renderRow} results={data && data.oauthApps} />;
}
export default OAuthAppsTable;

@ -0,0 +1,222 @@
import React, { useCallback, useState, useMemo } from 'react';
import {
Box,
Button,
ButtonGroup,
TextInput,
Field,
Icon,
Skeleton,
Throbber,
InputBox,
TextAreaInput,
ToggleSwitch,
FieldGroup,
} from '@rocket.chat/fuselage';
import { useTranslation } from '../../contexts/TranslationContext';
import { useMethod, useAbsoluteUrl } from '../../contexts/ServerContext';
import { useRoute } from '../../contexts/RouterContext';
import { useToastMessageDispatch } from '../../contexts/ToastMessagesContext';
import { Modal } from '../../components/basic/Modal';
import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../hooks/useEndpointDataExperimental';
import VerticalBar from '../../components/basic/VerticalBar';
const DeleteWarningModal = ({ onDelete, onCancel, ...props }) => {
const t = useTranslation();
return <Modal {...props}>
<Modal.Header>
<Icon color='danger' name='modal-warning' size={20}/>
<Modal.Title>{t('Are_you_sure')}</Modal.Title>
<Modal.Close onClick={onCancel}/>
</Modal.Header>
<Modal.Content fontScale='p1'>
{t('Application_delete_warning')}
</Modal.Content>
<Modal.Footer>
<ButtonGroup align='end'>
<Button ghost onClick={onCancel}>{t('Cancel')}</Button>
<Button primary danger onClick={onDelete}>{t('Delete')}</Button>
</ButtonGroup>
</Modal.Footer>
</Modal>;
};
const SuccessModal = ({ onClose, ...props }) => {
const t = useTranslation();
return <Modal {...props}>
<Modal.Header>
<Icon color='success' name='checkmark-circled' size={20}/>
<Modal.Title>{t('Deleted')}</Modal.Title>
<Modal.Close onClick={onClose}/>
</Modal.Header>
<Modal.Content fontScale='p1'>
{t('Your_entry_has_been_deleted')}
</Modal.Content>
<Modal.Footer>
<ButtonGroup align='end'>
<Button primary onClick={onClose}>{t('Ok')}</Button>
</ButtonGroup>
</Modal.Footer>
</Modal>;
};
export default function EditOauthAppWithData({ _id, ...props }) {
const t = useTranslation();
const [cache, setCache] = useState();
const onChange = useCallback(() => {
setCache(new Date());
}, []);
const query = useMemo(() => ({
appId: _id,
}), [_id, cache]);
const { data, state, error } = useEndpointDataExperimental('oauth-apps.get', query);
if (state === ENDPOINT_STATES.LOADING) {
return <Box pb='x20' maxWidth='x600' w='full' alignSelf='center'>
<Skeleton mbs='x8'/>
<InputBox.Skeleton w='full'/>
<Skeleton mbs='x8'/>
<InputBox.Skeleton w='full'/>
<ButtonGroup stretch w='full' mbs='x8'>
<Button disabled><Throbber inheritColor/></Button>
<Button primary disabled><Throbber inheritColor/></Button>
</ButtonGroup>
<ButtonGroup stretch w='full' mbs='x8'>
<Button primary danger disabled><Throbber inheritColor/></Button>
</ButtonGroup>
</Box>;
}
if (error || !data || !_id) {
return <Box fontScale='h1' pb='x20'>{t('error-application-not-found')}</Box>;
}
return <EditOauthApp data={data.oauthApp} onChange={onChange} {...props}/>;
}
function EditOauthApp({ onChange, data, ...props }) {
const t = useTranslation();
const dispatchToastMessage = useToastMessageDispatch();
const [newData, setNewData] = useState({
name: data.name,
active: data.active,
redirectUri: Array.isArray(data.redirectUri) ? data.redirectUri.join('\n') : data.redirectUri,
});
const [modal, setModal] = useState();
const router = useRoute('admin-oauth-apps');
const close = useCallback(() => router.push({}), [router]);
const absoluteUrl = useAbsoluteUrl();
const authUrl = useMemo(() => absoluteUrl('oauth/authorize'));
const tokenUrl = useMemo(() => absoluteUrl('oauth/token'));
const saveApp = useMethod('updateOAuthApp');
const deleteApp = useMethod('deleteOAuthApp');
const handleSave = useCallback(async () => {
try {
await saveApp(
data._id,
newData,
);
dispatchToastMessage({ type: 'success', message: t('Application_updated') });
onChange();
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
}
}, [JSON.stringify(newData)]);
const onDeleteConfirm = useCallback(async () => {
try {
await deleteApp(data._id);
setModal(() => <SuccessModal onClose={() => { setModal(); close(); }}/>);
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
}
}, [data._id]);
const openConfirmDelete = () => setModal(() => <DeleteWarningModal onDelete={onDeleteConfirm} onCancel={() => setModal(undefined)}/>);
const handleChange = (field, getValue = (e) => e.currentTarget.value) => (e) => setNewData({ ...newData, [field]: getValue(e) });
const {
active,
name,
redirectUri,
} = newData;
return <>
<VerticalBar.ScrollableContent w='full' {...props}>
<FieldGroup maxWidth='x600' alignSelf='center' w='full'>
<Field>
<Field.Label display='flex' justifyContent='space-between' w='full'>
{t('Active')}
<ToggleSwitch checked={active} onChange={handleChange('active', () => !active)}/>
</Field.Label>
</Field>
<Field>
<Field.Label>{t('Application_Name')}</Field.Label>
<Field.Row>
<TextInput value={name} onChange={handleChange('name')} />
</Field.Row>
<Field.Hint>{t('Give_the_application_a_name_This_will_be_seen_by_your_users')}</Field.Hint>
</Field>
<Field>
<Field.Label>{t('Redirect_URI')}</Field.Label>
<Field.Row>
<TextAreaInput rows={5} value={redirectUri} onChange={handleChange('redirectUri')}/>
</Field.Row>
<Field.Hint>{t('After_OAuth2_authentication_users_will_be_redirected_to_this_URL')}</Field.Hint>
</Field>
<Field>
<Field.Label>{t('Client_ID')}</Field.Label>
<Field.Row>
<TextInput value={data.clientId} onChange={handleChange('clientId')} />
</Field.Row>
</Field>
<Field>
<Field.Label>{t('Client_Secret')}</Field.Label>
<Field.Row>
<TextInput value={data.clientSecret} />
</Field.Row>
</Field>
<Field>
<Field.Label>{t('Authorization_URL')}</Field.Label>
<Field.Row>
<TextInput value={authUrl} />
</Field.Row>
</Field>
<Field>
<Field.Label>{t('Access_Token_URL')}</Field.Label>
<Field.Row>
<TextInput value={tokenUrl} />
</Field.Row>
</Field>
<Field>
<Field.Row>
<ButtonGroup stretch w='full'>
<Button onClick={close}>{t('Cancel')}</Button>
<Button primary onClick={handleSave} >{t('Save')}</Button>
</ButtonGroup>
</Field.Row>
</Field>
<Field>
<Field.Row>
<ButtonGroup stretch w='full'>
<Button primary danger onClick={openConfirmDelete}><Icon name='trash' mie='x4'/>{t('Delete')}</Button>
</ButtonGroup>
</Field.Row>
</Field>
</FieldGroup>
</VerticalBar.ScrollableContent>
{ modal }
</>;
}

@ -1,5 +1,5 @@
import React, { useCallback, useState, useMemo } from 'react';
import { Box, Headline, Button, Margins, TextInput, Skeleton, Field, ToggleSwitch, Divider, Icon, Callout } from '@rocket.chat/fuselage';
import { Box, Button, Margins, TextInput, Skeleton, Field, ToggleSwitch, Divider, Icon, Callout } from '@rocket.chat/fuselage';
import { useTranslation } from '../../contexts/TranslationContext';
import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../hooks/useEndpointDataExperimental';
@ -22,11 +22,11 @@ function EditRoomWithData({ rid }) {
if (state === ENDPOINT_STATES.LOADING) {
return <Box w='full' pb='x24'>
<Headline.Skeleton mbe='x4'/>
<Skeleton mbe='x4'/>
<Skeleton mbe='x8' />
<Headline.Skeleton mbe='x4'/>
<Skeleton mbe='x4'/>
<Skeleton mbe='x8'/>
<Headline.Skeleton mbe='x4'/>
<Skeleton mbe='x4'/>
<Skeleton mbe='x8'/>
</Box>;
}

@ -0,0 +1,194 @@
import React, { useCallback, useState, useMemo } from 'react';
import { Box, Headline, Button, Margins, TextInput, Skeleton, Field, ToggleSwitch, Divider, Icon, Callout } from '@rocket.chat/fuselage';
import { useTranslation } from '../../../contexts/TranslationContext';
import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../../hooks/useEndpointDataExperimental';
import { roomTypes } from '../../../../app/utils/client';
import { useMethod } from '../../../contexts/ServerContext';
import { usePermission } from '../../../contexts/AuthorizationContext';
import NotAuthorizedPage from '../../NotAuthorizedPage';
import { useEndpointAction } from '../../../hooks/useEndpointAction';
import Page from '../../../components/basic/Page';
export function EditRoomContextBar({ rid }) {
const canViewRoomAdministration = usePermission('view-room-administration');
return canViewRoomAdministration ? <EditRoomWithData rid={rid}/> : <NotAuthorizedPage/>;
}
function EditRoomWithData({ rid }) {
const [cache, setState] = useState();
const { data = {}, state, error } = useEndpointDataExperimental('rooms.adminRooms.getRoom', useMemo(() => ({ rid }), [rid, cache]));
if (state === ENDPOINT_STATES.LOADING) {
return <Box w='full' pb='x24'>
<Headline.Skeleton mbe='x4'/>
<Skeleton mbe='x8' />
<Headline.Skeleton mbe='x4'/>
<Skeleton mbe='x8'/>
<Headline.Skeleton mbe='x4'/>
<Skeleton mbe='x8'/>
</Box>;
}
if (state === ENDPOINT_STATES.ERROR) {
return error.message;
}
return <EditRoom room={data} onChange={() => setState(new Date())}/>;
}
function EditRoom({ room, onChange }) {
const t = useTranslation();
const [deleted, setDeleted] = useState(false);
const [newData, setNewData] = useState({});
const [changeArchivation, setChangeArchivation] = useState(false);
const canDelete = usePermission(`delete-${ room.t }`);
const hasUnsavedChanges = useMemo(() => Object.values(newData).filter((current) => current === null).length < Object.keys(newData).length, [JSON.stringify(newData)]);
const saveQuery = useMemo(() => ({ rid: room._id, ...Object.fromEntries(Object.entries(newData).filter(([, value]) => value !== null)) }), [room._id, JSON.stringify(newData)]);
const archiveSelector = room.archived ? 'unarchive' : 'archive';
const archiveMessage = archiveSelector === 'archive' ? 'Room_has_been_archived' : 'Room_has_been_archived';
const archiveQuery = useMemo(() => ({ rid: room._id, action: room.archived ? 'unarchive' : 'archive' }), [room.rid, changeArchivation]);
const saveAction = useEndpointAction('POST', 'rooms.saveRoomSettings', saveQuery, t('Room_updated_successfully'));
const archiveAction = useEndpointAction('POST', 'rooms.changeArchivationState', archiveQuery, t(archiveMessage));
const updateType = (type) => () => (type === 'p' ? 'c' : 'p');
const areEqual = (a, b) => a === b || !(a || b);
const handleChange = (field, currentValue, getValue = (e) => e.currentTarget.value) => (e) => setNewData({ ...newData, [field]: areEqual(getValue(e), currentValue) ? null : getValue(e) });
const handleSave = async () => {
await Promise.all([hasUnsavedChanges && saveAction(), changeArchivation && archiveAction()].filter(Boolean));
onChange('update');
};
const deleteRoom = useMethod('eraseRoom');
const handleDelete = useCallback(async () => {
await deleteRoom(room._id);
setDeleted(true);
}, [room]);
const roomName = room.t === 'd' ? room.usernames.join(' x ') : roomTypes.getRoomName(room.t, { type: room.t, ...room });
const roomType = newData.roomType ?? room.t;
const readOnly = newData.readOnly ?? !!room.ro;
const isArchived = changeArchivation ? !room.archived : !!room.archived;
const isDefault = newData.default ?? !!room.default;
const isFavorite = newData.favorite ?? !!room.favorite;
const isFeatured = newData.featured ?? !!room.featured;
return <Page.ScrollableContent pb='x24' mi='neg-x24' is='form'>
<Margins blockEnd='x16'>
{deleted && <Callout type='danger' title={t('Room_has_been_deleted')}></Callout>}
<Field>
<Field.Label>{t('Name')}</Field.Label>
<Field.Row>
<TextInput disabled={deleted || room.t === 'd'} value={newData.roomName ?? roomName} onChange={handleChange('roomName', roomName)} flexGrow={1}/>
</Field.Row>
</Field>
{ room.t !== 'd' && <>
<Field>
<Field.Label>{t('Owner')}</Field.Label>
<Field.Row>
<Box fontScale='p1'>{room.u?.username}</Box>
</Field.Row>
</Field>
<Field>
<Field.Label>{t('Topic')}</Field.Label>
<Field.Row>
<TextInput disabled={deleted} value={(newData.roomTopic ?? room.topic) || ''} onChange={handleChange('roomTopic', room.topic)} flexGrow={1}/>
</Field.Row>
</Field>
<Field mbs='x20'>
<Field.Row>
<Box display='flex' flexDirection='row' alignItems='flex-start'>
<Box display='flex' flexDirection='column' alignItems='flex-end' flexGrow='1' color={roomType !== 'c' ? 'hint' : 'default'}>
<Box fontScale='s1'>{t('Public')}</Box>
<Box fontScale='p1' style={{ textAlign: 'end' }}>{t('All_users_in_the_channel_can_write_new_messages')}</Box>
</Box>
<Margins inline='x16'>
<ToggleSwitch disabled={deleted} checked={roomType === 'p'} onChange={handleChange('roomType', room.t, updateType(roomType))}/>
</Margins>
<Box display='flex' flexDirection='column' alignItems='flex-start' flexGrow='1' color={roomType !== 'p' ? 'hint' : 'default'}>
<Box fontScale='s1'>{t('Private')}</Box>
<Box fontScale='p1' style={{ textAlign: 'start' }}>{t('Just_invited_people_can_access_this_channel')}</Box>
</Box>
</Box>
</Field.Row>
<Divider />
</Field>
<Field mbs='x20'>
<Field.Row>
<Box display='flex' flexDirection='row' alignItems='flex-start'>
<Box display='flex' flexDirection='column' alignItems='flex-end' flexGrow='1' color={readOnly ? 'hint' : 'default'}>
<Box fontScale='s1'>{t('Collaborative')}</Box>
<Box fontScale='p1' style={{ textAlign: 'end' }}>{t('All_users_in_the_channel_can_write_new_messages')}</Box>
</Box>
<Margins inline='x16'>
<ToggleSwitch disabled={deleted} checked={readOnly} onChange={handleChange('readOnly', room.ro, () => !readOnly)}/>
</Margins>
<Box display='flex' flexDirection='column' alignItems='flex-start' flexGrow='1' color={!readOnly ? 'hint' : 'default'}>
<Box fontScale='s1'>{t('Read_only')}</Box>
<Box fontScale='p1' style={{ textAlign: 'start' }}>{t('Only_authorized_users_can_write_new_messages')}</Box>
</Box>
</Box>
</Field.Row>
<Divider />
</Field>
<Field>
<Field.Row>
<Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}>
<Field.Label>{t('Archived')}</Field.Label>
<ToggleSwitch disabled={deleted} checked={isArchived} onChange={() => setChangeArchivation(!changeArchivation)}/>
</Box>
</Field.Row>
</Field>
<Field>
<Field.Row>
<Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}>
<Field.Label>{t('Default')}</Field.Label>
<ToggleSwitch disabled={deleted} checked={isDefault} onChange={handleChange('default', room.default, () => !isDefault)}/>
</Box>
</Field.Row>
</Field>
<Field>
<Field.Row>
<Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}>
<Field.Label>{t('Favorite')}</Field.Label>
<ToggleSwitch disabled={deleted} checked={isFavorite} onChange={handleChange('favorite', room.default, () => !isFavorite)}/>
</Box>
</Field.Row>
</Field>
<Field>
<Field.Row>
<Box display='flex' flexDirection='row' justifyContent='space-between' flexGrow={1}>
<Field.Label>{t('Featured')}</Field.Label>
<ToggleSwitch disabled={deleted} checked={isFeatured} onChange={handleChange('featured', room.default, () => !isFeatured)}/>
</Box>
</Field.Row>
</Field>
<Field>
<Field.Row>
<Box display='flex' flexDirection='row' justifyContent='space-between' w='full'>
<Margins inlineEnd='x4'>
<Button disabled={deleted} flexGrow={1} type='reset' disabled={!hasUnsavedChanges && !changeArchivation} onClick={() => setNewData({})}>{t('Reset')}</Button>
<Button disabled={deleted} mie='none' flexGrow={1} disabled={!hasUnsavedChanges && !changeArchivation} onClick={handleSave}>{t('Save')}</Button>
</Margins>
</Box>
</Field.Row>
</Field>
</>}
<Field>
<Field.Row>
<Button primary danger disabled={deleted || !canDelete} onClick={handleDelete} display='flex' alignItems='center' justifyContent='center' flexGrow={1}><Icon name='trash' size='x16' />{t('Delete')}</Button>
</Field.Row>
</Field>
</Margins>
</Page.ScrollableContent>;
}

@ -72,6 +72,16 @@ registerAdminRoute('/mailer', {
lazyRouteComponent: () => import('./mailer/MailerRoute'),
});
registerAdminRoute('/oauth-apps/:context?/:id?', {
name: 'admin-oauth-apps',
lazyRouteComponent: () => import('./oauthApps/OAuthAppsRoute'),
});
registerAdminRoute('/integrations/:context?/:type?/:id?', {
name: 'admin-integrations',
lazyRouteComponent: () => import('./integrations/IntegrationsRoute'),
});
registerAdminRoute('/custom-user-status/:context?/:id?', {
name: 'custom-user-status',
lazyRouteComponent: () => import('./customUserStatus/CustomUserStatusRoute'),
@ -97,6 +107,11 @@ registerAdminRoute('/invites', {
lazyRouteComponent: () => import('./invites/InvitesRoute'),
});
registerAdminRoute('/cloud/:page?', {
name: 'cloud',
lazyRouteComponent: () => import('./cloud/CloudRoute'),
});
registerAdminRoute('/view-logs', {
name: 'admin-view-logs',
lazyRouteComponent: () => import('./viewLogs/ViewLogsRoute'),

@ -42,6 +42,13 @@ registerAdminSidebarItem({
permissionGranted: () => hasPermission('create-invite-links'),
});
registerAdminSidebarItem({
icon: 'cloud-plus',
href: 'cloud',
i18nLabel: 'Connectivity_Services',
permissionGranted: () => hasPermission('manage-cloud'),
});
registerAdminSidebarItem({
href: 'admin-view-logs',
i18nLabel: 'View_Logs',

@ -1,5 +1,5 @@
import React, { useMemo, useState } from 'react';
import { Field, TextInput, Box, Headline, Skeleton, ToggleSwitch, Icon, TextAreaInput, MultiSelectFiltered, Margins, Button } from '@rocket.chat/fuselage';
import { Field, TextInput, Box, Skeleton, ToggleSwitch, Icon, TextAreaInput, MultiSelectFiltered, Margins, Button } from '@rocket.chat/fuselage';
import { useTranslation } from '../../contexts/TranslationContext';
import { useEndpointData } from '../../hooks/useEndpointData';
@ -18,11 +18,11 @@ export function EditUserWithData({ userId, ...props }) {
if (state === ENDPOINT_STATES.LOADING) {
return <Box w='full' pb='x24' {...props}>
<Headline.Skeleton mbe='x4'/>
<Skeleton mbe='x4'/>
<Skeleton mbe='x8' />
<Headline.Skeleton mbe='x4'/>
<Skeleton mbe='x4'/>
<Skeleton mbe='x8'/>
<Headline.Skeleton mbe='x4'/>
<Skeleton mbe='x4'/>
<Skeleton mbe='x8'/>
</Box>;
}

@ -1,5 +1,5 @@
import React, { useCallback, useState } from 'react';
import { Box, Headline, Button, Icon, TextAreaInput } from '@rocket.chat/fuselage';
import { Box, Button, Icon, TextAreaInput } from '@rocket.chat/fuselage';
import { useTranslation } from '../../contexts/TranslationContext';
import { useMethod } from '../../contexts/ServerContext';
@ -24,7 +24,7 @@ export function InviteUsers({ data, ...props }) {
});
};
return <VerticalBar.ScrollableContent {...props}>
<Headline mbs='x8'>{t('Send_invitation_email')}</Headline>
<Box is='h2' fontScale='h1' mb='x8'>{t('Send_invitation_email')}</Box>
<Box fontScale='p1' mb='x8'>{t('Send_invitation_email_info')}</Box>
<TextAreaInput rows={5} flexGrow={0} onChange={(e) => setText(e.currentTarget.value)}/>
<Button primary onClick={handleClick} alignItems='stretch' mb='x8'>

@ -1,5 +1,5 @@
import React, { useMemo, useState, useEffect } from 'react';
import { Box, Avatar, Margins, Headline, Skeleton, Chip, Tag } from '@rocket.chat/fuselage';
import { Box, Avatar, Margins, Skeleton, Chip, Tag } from '@rocket.chat/fuselage';
import moment from 'moment';
import { useEndpointDataExperimental, ENDPOINT_STATES } from '../../hooks/useEndpointDataExperimental';
@ -38,11 +38,11 @@ export function UserInfoWithData({ userId, ...props }) {
if (state === ENDPOINT_STATES.LOADING) {
return <Box w='full' pb='x24'>
<Headline.Skeleton mbe='x4'/>
<Skeleton mbe='x4'/>
<Skeleton mbe='x8' />
<Headline.Skeleton mbe='x4'/>
<Skeleton mbe='x4'/>
<Skeleton mbe='x8'/>
<Headline.Skeleton mbe='x4'/>
<Skeleton mbe='x4'/>
<Skeleton mbe='x8'/>
</Box>;
}

@ -1,4 +1,4 @@
import { Box, Headline, Margins } from '@rocket.chat/fuselage';
import { Box } from '@rocket.chat/fuselage';
import React from 'react';
import { useTranslation } from '../../contexts/TranslationContext';
@ -6,12 +6,8 @@ import { useTranslation } from '../../contexts/TranslationContext';
export function StepHeader({ number, title }) {
const t = useTranslation();
return <Margins blockEnd='32'>
<Box is='header' className='SetupWizard__StepHeader'>
<Box is='p' fontScale='c1' color='hint'>
{t('Step')} {number}
</Box>
<Headline is='h2'>{title}</Headline>
</Box>
</Margins>;
return <Box is='header' marginBlockEnd='x32'>
<Box is='p' fontScale='c1' color='hint'>{t('Step')} {number}</Box>
<Box is='h2' fontScale='h1' color='default'>{title}</Box>
</Box>;
}

@ -0,0 +1,28 @@
import { useState, useCallback } from 'react';
const getValue = (e) => (e.currentTarget ? e.currentTarget.value : e);
const capitalize = (s) => {
if (typeof s !== 'string') { return ''; }
return s.charAt(0).toUpperCase() + s.slice(1);
};
export const useForm = (obj) => {
const resetCallbacks = [];
const ret = Object.keys(obj).sort().reduce((ret, key) => {
const value = obj[key];
const [data, setData] = useState(value);
ret.values = { ...ret.values, [key]: data };
ret.handlers = { ...ret.handlers, [`handle${ capitalize(key) }`]: useCallback(typeof value !== 'boolean' ? (e) => setData(getValue(e)) : () => setData(!data), [data]) };
resetCallbacks.push(() => setData(value));
return ret;
}, {});
ret.reset = () => {
resetCallbacks.forEach((reset) => reset());
};
return ret;
};

@ -0,0 +1,6 @@
import hljs from 'highlight.js';
import { useMemo } from 'react';
export function useHilightCode() {
return (language, text) => useMemo(() => hljs.highlight(language, text).value, [language, text]);
}

@ -7,7 +7,6 @@ import '../app/autotranslate/client';
import '../app/cas/client';
import '../app/channel-settings';
import '../app/channel-settings-mail-messages/client';
import '../app/cloud/client';
import '../app/colors/client';
import '../app/crowd/client';
import '../app/custom-oauth';
@ -31,7 +30,7 @@ import '../app/importer-hipchat/client';
import '../app/importer-hipchat-enterprise/client';
import '../app/importer-slack/client';
import '../app/importer-slack-users/client';
import '../app/integrations/client';
import '../app/integrations/client/startup';
import '../app/issuelinks/client';
import '../app/katex/client';
import '../app/ldap/client';

23
package-lock.json generated

@ -2810,11 +2810,6 @@
"uuid": "^3.2.1"
},
"dependencies": {
"adm-zip": {
"version": "0.4.14",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.14.tgz",
"integrity": "sha512-/9aQCnQHF+0IiCl0qhXoK7qs//SwYE7zX8lsr/DNk1BRAHYxeLZPL4pguwK29gUEqasYQjqPtEpDRSWEkdHn9g=="
},
"typescript": {
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.2.tgz",
@ -2879,9 +2874,9 @@
"integrity": "sha512-TVPEckSbzHr+Ix3h4OuEd5OyDVAVnviSQRqwYzAbfRvb4B9riZwqUlvdNKgRofy1og5ovRMZUL9CqduGbsdhsA=="
},
"@rocket.chat/fuselage-ui-kit": {
"version": "0.6.3-dev.35",
"resolved": "https://registry.npmjs.org/@rocket.chat/fuselage-ui-kit/-/fuselage-ui-kit-0.6.3-dev.35.tgz",
"integrity": "sha512-E/C7voGjzfnp8WGqflOau/BtXSQa7Zaf2zc4cOXeudlagPLsG3K0uSU99ZiazKXZOR01h3QZM3Ikw5mEXTkeww==",
"version": "0.6.3-dev.39",
"resolved": "https://registry.npmjs.org/@rocket.chat/fuselage-ui-kit/-/fuselage-ui-kit-0.6.3-dev.39.tgz",
"integrity": "sha512-ll9lYOM2mEAsKtHiPdxDwVO2YGkarZMCLl7UK6yT71BFVuDY7hJXm14+aj+JBTKc9B2J0CFIo+MfO1cDuqYc+A==",
"requires": {
"@rocket.chat/ui-kit": "^0.8.0"
},
@ -7788,9 +7783,9 @@
}
},
"caniuse-lite": {
"version": "1.0.30001051",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001051.tgz",
"integrity": "sha512-sw8UUnTlRevawTMZKN7vpfwSjCBVoiMPlYd8oT2VwNylyPCBdMAUmLGUApnYYTtIm5JXsQegUAY7GPHqgfDzjw==",
"version": "1.0.30001054",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001054.tgz",
"integrity": "sha512-jiKlTI6Ur8Kjfj8z0muGrV6FscpRvefcQVPSuMuXnvRCfExU7zlVLNjmOz1TnurWgUrAY7MMmjyy+uTgIl1XHw==",
"dev": true
},
"chalk": {
@ -7820,9 +7815,9 @@
"dev": true
},
"electron-to-chromium": {
"version": "1.3.428",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.428.tgz",
"integrity": "sha512-u3+5jEfgLKq/hGO96YfAoOAM1tgFnRDTCD5mLuev44tttcXix+INtVegAkmGzUcfDsnzkPt51XXurXZLLwXt0w==",
"version": "1.3.431",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.431.tgz",
"integrity": "sha512-2okqkXCIda7qDwjYhUFxPcQdZDIZZ/zBLDzVOif7WW/TSNfEhdT6SO07O1x/sFteEHX189Z//UwjbZKKCOn2Fg==",
"dev": true
},
"node-releases": {

@ -130,7 +130,7 @@
"@rocket.chat/fuselage": "^0.6.3-dev.39",
"@rocket.chat/fuselage-hooks": "^0.6.3-dev.35",
"@rocket.chat/fuselage-polyfills": "^0.6.3-dev.23",
"@rocket.chat/fuselage-ui-kit": "^0.6.3-dev.35",
"@rocket.chat/fuselage-ui-kit": "^0.6.3-dev.39",
"@rocket.chat/icons": "^0.6.3-dev.23",
"@rocket.chat/ui-kit": "^0.6.3-dev.23",
"@slack/client": "^4.8.0",

@ -634,7 +634,6 @@
"Closed_by_visitor": "Tancat pel visitant",
"Closing_chat": "Tancant xat",
"Cloud": "Cloud",
"Cloud_connect": "Connexió Rocket.Chat Cloud ",
"Cloud_connect_support": "Si encara no heu rebut cap correu electrònic de registre, assegureu-vos que el vostre correu electrònic s’ha actualitzat anteriorment. Si encara teniu problemes, podeu posar-vos en contacte amb l’assistència a",
"Cloud_console": "Cloud Console",
"Cloud_what_is_it": "Què és això?",
@ -652,9 +651,9 @@
"Cloud_manually_input_token": "Introduïu manualment la fitxa rebuda al correu electrònic de registre de Cloud.",
"Cloud_registration_required": "Cal registrar-se",
"Cloud_registration_required_description": "Sembla que durant la configuració no heu triat registrar l’espai de treball.",
"Cloud_registration_requried_link_text": "Feu clic aquí per registrar el vostre espai de treball.",
"Cloud_registration_required_link_text": "Feu clic aquí per registrar el vostre espai de treball.",
"Cloud_error_in_authenticating": "S'ha rebut un error en autenticar-se",
"Cloud_error_code": "Codi:",
"Cloud_error_code": "Codi: __errorCode__",
"Cloud_status_page_description": "Si un determinat servei Cloud té problemes, podeu comprovar els problemes coneguts a la nostra pàgina d’estat.",
"Cloud_troubleshooting": "Resolució de problemes",
"Collaborative": "Col·laboratiu",
@ -2996,4 +2995,4 @@
"Your_push_was_sent_to_s_devices": "La notificació push s'ha enviat a %s dispositius",
"Your_server_link": "Enllaç del servidor",
"Your_workspace_is_ready": "El vostre espai de treball està a punt per utilitzar 🎉"
}
}

@ -690,12 +690,10 @@
"Closing_chat": "Uzavření místnosti",
"Cloud": "Cloud",
"Cloud_Register_manually": "Registrovat ručně",
"Cloud_click_here": "Po zkopírování textu přejděte do cloudové konzole. [Klikněte zde]()",
"Cloud_register_offline_finish_helper": "Po dokončení procesu registrace v cloudové konzoli byste měli dostat nějaký text. Pro dokončení registrace jej vložte sem.",
"Cloud_register_offline_helper": "Pracovní prostory lze ručně zaregistrovat, pokud je k nim omezen síťový přístup. Zkopírujte níže uvedený text a dokončete proces pomocí naší cloudové konzole.",
"Cloud_register_success": "Váš pracovní prostor byl úspěšně zaregistrován!",
"Cloud_register_error": "Při zpracování vašeho požadavku došlo k chybě. Prosím zkuste to znovu později.",
"Cloud_connect": "Připojit k Rocket.Chat Cloud",
"Cloud_connect_support": "Pokud jste ještě neobdrželi registrační e-mail, ujistěte se, že váš e-mail je aktualizován výše. Pokud problémy přetrvávají, můžete kontaktovat podporu na adrese",
"Cloud_console": "Cloud přehled",
"Cloud_Info": "Informace o Cloudu",
@ -714,9 +712,9 @@
"Cloud_manually_input_token": "Ručně zadejte token přijatý z e-mailu pro registraci do Cloudu.",
"Cloud_registration_required": "Povinná registrace",
"Cloud_registration_required_description": "Vypadá to, že jste se nevybral/a zaregistraci svého pracovního prostoru během instalace .",
"Cloud_registration_requried_link_text": "Klikněte zde pro registraci svého pracovního prostoru.",
"Cloud_registration_required_link_text": "Klikněte zde pro registraci svého pracovního prostoru.",
"Cloud_error_in_authenticating": "Při ověřování došlo k chybě",
"Cloud_error_code": "Kód:",
"Cloud_error_code": "Kód: __errorCode__",
"Cloud_status_page_description": "Pokud má konkrétní cloudová služba problémy, můžete zkontrolovat známé problémy na naší stavové stránce na adrese",
"Cloud_Service_Agree_PrivacyTerms": "Souhlas s podmínkami soukromí služby cloud",
"Cloud_troubleshooting": "Odstraňování chyb",
@ -3694,4 +3692,4 @@
"Your_server_link": "Odkaz na Váš server",
"Your_temporary_password_is_password": "Vaše dočasné heslo je <strong>[password]</strong>.",
"Your_workspace_is_ready": "Váš prostředí je připraveno k použití 🎉"
}
}

@ -681,7 +681,6 @@
"Closed_by_visitor": "Lukket af besøgende",
"Closing_chat": "Lukning af chat",
"Cloud": "Cloud",
"Cloud_connect": "Rocket.Chat Cloud-forbindelse",
"Cloud_what_is_it": "Hvad er det her?",
"Cloud_what_is_it_description": "Rocket.Chat Cloud-forbindelse lader dig forbinde en Rocket.Chat-instans, som du selv er vært for, til vores sky. Hvis du gør det, kan du administrere dine licenser, regninger og support i Rocket.Chat Cloud.",
"Cloud_login_to_cloud": "Log ind i Rocket.Chat Cloud",
@ -690,9 +689,9 @@
"Cloud_manually_input_token": "Indtast manuelt den token, der fulgte med Cloud-tilmeldingen.",
"Cloud_registration_required": "Tilmelding påkrævet",
"Cloud_registration_required_description": "Under konfigurationen valgte du tilsyneladende ikke at tilmelde din instans.",
"Cloud_registration_requried_link_text": "Tryk her for at tilmelde din instans.",
"Cloud_registration_required_link_text": "Tryk her for at tilmelde din instans.",
"Cloud_error_in_authenticating": "Fejl i forbindelse med godkendelse",
"Cloud_error_code": "Kode:",
"Cloud_error_code": "Kode: __errorCode__",
"Collaborative": "Samarbejde",
"Collapse_Embedded_Media_By_Default": "Skjul embedded media som standard",
"color": "Farve",

@ -609,7 +609,6 @@
"Closed_by_visitor": "Durch Besucher geschlossen",
"Closing_chat": "Schließe Chat",
"Cloud": "Cloud",
"Cloud_connect": "Rocket.Chat-Cloud-Connector",
"Cloud_connect_support": "Wenn Du noch immer keine Registrierungs-E-Mail erhalten haben, überprüfe bitte die o. g. Adresse. Wenn es dann immer noch Probleme gibt, erreichst Du unseren Support unter",
"Cloud_console": "Cloud Console",
"Cloud_what_is_it": "Was ist das?",
@ -627,9 +626,9 @@
"Cloud_manually_input_token": "Gib das von der Cloud-Registrierungs-E-Mail erhaltene Token manuell ein.",
"Cloud_registration_required": "Registrierung erforderlich",
"Cloud_registration_required_description": "Sieht aus, als hättest Du Dich während des Setups nicht für die Registrierung Deines Arbeitsbereichs entschieden.",
"Cloud_registration_requried_link_text": "Klicke hier, um Deinen Arbeitsbereich zu registrieren.",
"Cloud_registration_required_link_text": "Klicke hier, um Deinen Arbeitsbereich zu registrieren.",
"Cloud_error_in_authenticating": "Fehler beim Authentifizieren",
"Cloud_error_code": "Code:",
"Cloud_error_code": "Code: __errorCode__",
"Cloud_status_page_description": "Wenn ein bestimmter Cloud-Dienste Probleme hat, lohnt sich ein Blick auf die Status-Seite der Cloud-Dienste unter",
"Cloud_troubleshooting": "Troubleshooting",
"Collaborative": "Kollaborativ",

@ -660,12 +660,10 @@
"Closing_chat": "Schließe Chat",
"Cloud": "Cloud",
"Cloud_Register_manually": "Manuell registrieren",
"Cloud_click_here": "Wechseln Sie nach dem Kopieren des Texts zur Cloud-Konsole. [Klicke hier]()",
"Cloud_register_offline_finish_helper": "Nach Abschluss des Registrierungsvorgangs in der Cloud-Konsole, sollte ein Text angezeigt werden. Bitte hier einfügen, um die Registrierung abzuschließen.",
"Cloud_register_offline_helper": "Arbeitsbereiche können manuell registriert werden, wenn ein Airgap besteht oder der Netzwerkzugriff eingeschränkt ist. Kopieren Sie den folgenden Text und rufen Sie unsere Cloud-Konsole auf, um den Vorgang abzuschließen.",
"Cloud_register_success": "Ihr Arbeitsbereich wurde erfolgreich registriert!",
"Cloud_register_error": "Beim Verarbeiten Ihrer Anfrage ist ein Fehler aufgetreten. Bitte versuchen Sie es später noch einmal.",
"Cloud_connect": "Rocket.Chat Cloud Connect",
"Cloud_connect_support": "Wenn Sie noch immer keine Registrierungs-E-Mail erhalten haben, überprüfen Sie bitte die o. g. Adresse. Wenn es dann immer noch Probleme gibt, erreichen Sie unseren Support unter",
"Cloud_console": "Cloud Console",
"Cloud_Info": "Cloud-Informationen",
@ -684,9 +682,9 @@
"Cloud_manually_input_token": "Geben Sie das von der Cloud-Registrierungs-E-Mail erhaltene Token manuell ein.",
"Cloud_registration_required": "Registrierung erforderlich",
"Cloud_registration_required_description": "Sieht aus, als hätten Sie sich während des Setups nicht für die Registrierung Ihres Arbeitsbereichs entschieden.",
"Cloud_registration_requried_link_text": "Klicken Sie hier, um Ihren Arbeitsbereich zu registrieren.",
"Cloud_registration_required_link_text": "Klicken Sie hier, um Ihren Arbeitsbereich zu registrieren.",
"Cloud_error_in_authenticating": "Fehler beim Authentifizieren",
"Cloud_error_code": "Code:",
"Cloud_error_code": "Code: __errorCode__",
"Cloud_status_page_description": "Wenn ein bestimmter Cloud-Dienste Probleme hat, lohnt sich ein Blick auf die Status-Seite der Cloud-Dienste unter",
"Cloud_Service_Agree_PrivacyTerms": "Cloud-Dienst-Datenschutzbestimmungen zustimmen",
"Cloud_troubleshooting": "Troubleshooting",
@ -3564,4 +3562,4 @@
"Your_server_link": "Ihre Serververbindung",
"Your_temporary_password_is_password": "Ihr temporäres Passwort lautet <strong>[password]</strong>.",
"Your_workspace_is_ready": "Ihr Arbeitsbereich ist einsatzbereit 🎉"
}
}

@ -388,6 +388,7 @@
"App_user_not_allowed_to_login": "App users are not allowed to log in directly.",
"Appearance": "Appearance",
"Application_added": "Application added",
"Application_delete_warning": "You will not be able to recover this Application!",
"Application_Name": "Application Name",
"Application_updated": "Application updated",
"Apply": "Apply",
@ -705,12 +706,11 @@
"Closing_chat": "Closing chat",
"Cloud": "Cloud",
"Cloud_Register_manually": "Register Manually",
"Cloud_click_here": "After copy the text, go to cloud console. [Click here]()",
"Cloud_click_here": "After copy the text, go to [cloud console (click here)](__cloudConsoleUrl__).",
"Cloud_register_offline_finish_helper": "After completing the registration process in the Cloud Console you should be presented with some text. Please paste it here to finish the registration.",
"Cloud_register_offline_helper": "Workspaces can be manually registered if airgapped or network access is restricted. Copy the text below and go to our Cloud Console to complete the process.",
"Cloud_register_success": "Your workspace has been successfully registered!",
"Cloud_register_error": "There has been an error trying to process your request. Please try again later.",
"Cloud_connect": "Rocket.Chat Cloud Connect",
"Cloud_connect_support": "If you still haven't received a registration email please make sure your email is updated above. If you still have issues you can contact support at",
"Cloud_console": "Cloud Console",
"Cloud_Info": "Cloud Info",
@ -729,9 +729,9 @@
"Cloud_manually_input_token": "Manually enter the token received from the Cloud Registration Email.",
"Cloud_registration_required": "Registration Required",
"Cloud_registration_required_description": "Looks like during setup you didn't chose to register your workspace.",
"Cloud_registration_requried_link_text": "Click here to register your workspace.",
"Cloud_registration_required_link_text": "Click here to register your workspace.",
"Cloud_error_in_authenticating": "Error received while authenticating",
"Cloud_error_code": "Code: ",
"Cloud_error_code": "Code: __errorCode__",
"Cloud_status_page_description": "If a particular Cloud Service is having issues you can check for known issues on our status page at",
"Cloud_Service_Agree_PrivacyTerms": "Cloud Service Agree PrivacyTerms",
"Cloud_troubleshooting": "Troubleshooting",
@ -1038,6 +1038,8 @@
"Create_unique_rules_for_this_channel": "Create unique rules for this channel",
"Created": "Created",
"Created_at": "Created at",
"Created_by": "Created by",
"Created_as": "Created as",
"Created_at_s_by_s": "Created at <strong>%s</strong> by <strong>%s</strong>",
"Created_at_s_by_s_triggered_by_s": "Created at <strong>%s</strong> by <strong>%s</strong> triggered by <strong>%s</strong>",
"CRM_Integration": "CRM Integration",
@ -1473,6 +1475,7 @@
"Everyone_can_access_this_channel": "Everyone can access this channel",
"Exact": "Exact",
"Example_s": "Example: <code class=\"inline\">%s</code>",
"Example_payload": "Example payload",
"Exclude_Botnames": "Exclude Bots",
"Exclude_Botnames_Description": "Do not propagate messages from bots whose name matches the regular expression above. If left empty, all messages from bots will be propagated.",
"Exclude_pinned": "Exclude pinned messages",
@ -1845,6 +1848,8 @@
"Integration_Incoming_WebHook": "Incoming WebHook Integration",
"Integration_New": "New Integration",
"Integration_Outgoing_WebHook": "Outgoing WebHook Integration",
"Integration_Delete_Warning": "Deleting an Integrations cannot be undone.",
"Webhook_Details": "WebHook Details",
"Integration_Outgoing_WebHook_History": "Outgoing WebHook Integration History",
"Integration_Outgoing_WebHook_History_Data_Passed_To_Trigger": "Data Passed to Integration",
"Integration_Outgoing_WebHook_History_Data_Passed_To_URL": "Data Passed to URL",
@ -2691,6 +2696,7 @@
"post-readonly": "Post ReadOnly",
"post-readonly_description": "Permission to post a message in a read-only channel",
"Post_as": "Post as",
"Post_to": "Post to",
"Post_to_Channel": "Post to Channel",
"Post_to_s_as_s": "Post to <strong>%s</strong> as <strong>%s</strong>",
"Preferences": "Preferences",
@ -2839,6 +2845,7 @@
"Removed_User": "Removed User",
"Replied_on": "Replied on",
"Reply": "Reply",
"Replay": "Replay",
"reply_counter": "__counter__ reply",
"reply_counter_plural": "__counter__ replies",
"Replies": "Replies",
@ -2864,6 +2871,9 @@
"Reset_password": "Reset password",
"Reset_section_settings": "Reset Section Settings",
"Reset_Connection": "Reset Connection",
"Responding": "Responding",
"Response_description_pre": "If the handler wishes to post a response back into the channel, the following JSON should be returned as the body of the response:",
"Response_description_post": "Empty bodies or bodies with an empty text property will simply be ignored. Non-200 responses will be retried a reasonable number of times. A response will be posted using the alias and avatar specified above. You can override these informations as in the example above.",
"Restart": "Restart",
"Restart_the_server": "Restart the server",
"Retail": "Retail",
@ -3009,6 +3019,7 @@
"Search_by_username": "Search by username",
"Search_Channels": "Search Channels",
"Search_current_provider_not_active": "Current Search Provider is not active",
"Search_Integrations": "Search Integrations",
"Search_message_search_failed": "Search request failed",
"Search_Messages": "Search Messages",
"Search_Page_Size": "Page Size",
@ -3062,6 +3073,7 @@
"Sending": "Sending...",
"Sent_an_attachment": "Sent an attachment",
"Sent_from": "Sent from",
"Separate_multiple_words_with_commas": "Separate multiple words with commas",
"Served_By": "Served By",
"Server_File_Path": "Server File Path",
"Server_Folder_Path": "Server Folder Path",
@ -3734,8 +3746,10 @@
"Welcome_to_the": "Welcome to the",
"Where_are_the_messages_being_sent?": "Where are the messages being sent?",
"When_is_the_chat_busier?": "When is the chat busier?",
"When_a_line_starts_with_one_of_there_words_post_to_the_URLs_below": "When a line starts with one of these words, post to the URL(s) below",
"Why_do_you_want_to_report_question_mark": "Why do you want to report?",
"will_be_able_to": "will be able to",
"Will_be_available_here_after_saving": "Will be available here after saving.",
"Worldwide": "Worldwide",
"Would_you_like_to_return_the_inquiry": "Would you like to return the inquiry?",
"Yes": "Yes",

@ -685,7 +685,6 @@
"Closed_by_visitor": "Cerrado por el visitante",
"Closing_chat": "Cerrando chat",
"Cloud_Register_manually": "Registrar manualmente",
"Cloud_click_here": "Después de copiar el texto, ve a la consola Cloud. [Haga clic aquí]()",
"Cloud_register_offline_finish_helper": "Después de completar el proceso de registro en la Cloud Console se le presentará un texto. Por favor, pégalo aquí para terminar el registro.",
"Cloud_register_offline_helper": "Los espacios de trabajo pueden registrarse manualmente si el acceso a la red está restringido. Copia el texto de abajo y ve a nuestra Cloud Console para completar el proceso.",
"Cloud_register_success": "¡Su espacio de trabajo ha sido registrado correctamente!",
@ -706,9 +705,9 @@
"Cloud_manually_input_token": "Introduzca manualmente el token recibido en el correo electrónico de registro en la nube.",
"Cloud_registration_required": "Se requiere registro",
"Cloud_registration_required_description": "Parece que durante la configuración no eligió registrar su espacio de trabajo.",
"Cloud_registration_requried_link_text": "Haga clic aquí para registrar su espacio de trabajo.",
"Cloud_registration_required_link_text": "Haga clic aquí para registrar su espacio de trabajo.",
"Cloud_error_in_authenticating": "Error durante la autentificación",
"Cloud_error_code": "Código:",
"Cloud_error_code": "Código: __errorCode__",
"Cloud_status_page_description": "Si está teniendo problemas con un servicio en particular de la nube, puede comprobar los problemas conocidos en nuestra página de estado en",
"Cloud_Service_Agree_PrivacyTerms": "Acuerdos y términos de privacidad del servicio en la nube",
"Cloud_troubleshooting": "Solución de problemas",
@ -3361,4 +3360,4 @@
"Your_push_was_sent_to_s_devices": "Su push fue enviado a los dispositivos %s",
"Your_server_link": "Su enlace de servidor",
"Your_workspace_is_ready": "Su espacio de trabajo está listo para usar 🎉"
}
}

@ -661,12 +661,10 @@
"Closing_chat": "در حال بستن چت",
"Cloud": "ابر",
"Cloud_Register_manually": "ثبت دستی",
"Cloud_click_here": "پس از کپی کردن متن ، به کنسول ابری بروید. [اینجا کلیک کنید]()",
"Cloud_register_offline_finish_helper": "پس از اتمام مراحل ثبت نام در بسترCloud ، باید متنی برای شما ارائه شود. لطفاً برای پایان ثبت نام آن را در اینجا جایگذاری کنید.",
"Cloud_register_offline_helper": "در صورت محدود بودن دسترسی به شبکه ، می توان مکان های کاری را به صورت دستی ثبت کرد. متن را کپی کنید و برای تکمیل مراحل به کنسول Cloud ما بروید.",
"Cloud_register_success": "فضای کاری شما با موفقیت ثبت شده است!",
"Cloud_register_error": "هنگام پردازش درخواست شما خطایی رخ داده است. لطفا بعدا دوباره امتحان کنید.",
"Cloud_connect": "اتصال Roket.Chat Cloud",
"Cloud_connect_support": "اگر هنوز ایمیل ثبت نام دریافت نکردید ، لطفاً اطمینان حاصل کنید که ایمیل شما در بالا به روز شده است. اگر هنوز مشکل دارید می توانید با پشتیبانی تماس بگیرید",
"Cloud_console": "کنسول Cloud",
"Cloud_Info": "اطلاعات Cloud",
@ -684,9 +682,9 @@
"Cloud_manually_input_token": "نشانه دریافت شده از ایمیل ثبت نام Cloud را به صورت دستی وارد کنید.",
"Cloud_registration_required": "نیاز به ثبت نام",
"Cloud_registration_required_description": "به نظر می رسد در هنگام راه اندازی شما تصمیم به ثبت نام فضای کاری خود ندارید.",
"Cloud_registration_requried_link_text": "برای ثبت فضای کاری خود اینجا را کلیک کنید.",
"Cloud_registration_required_link_text": "برای ثبت فضای کاری خود اینجا را کلیک کنید.",
"Cloud_error_in_authenticating": "هنگام تأیید اعتبار ، خطایی رخ داده",
"Cloud_error_code": "کد:",
"Cloud_error_code": "کد: __errorCode__",
"Cloud_status_page_description": "اگر سرویس ویژه Cloud مشکلی دارد ، می توانید موارد شناخته شده را در صفحه وضعیت ما در اینجا بررسی کنید",
"Cloud_Service_Agree_PrivacyTerms": "سرویس ابر با حریم خصوصی موافقت می کند",
"Cloud_troubleshooting": "عیب یابی",
@ -3057,4 +3055,4 @@
"Your_push_was_sent_to_s_devices": "فشار خود را به دستگاه %s را ارسال شد",
"Your_server_link": "لینک سرور شما",
"Your_workspace_is_ready": "فضای کاری شما آماده استفاده است"
}
}

@ -607,7 +607,7 @@
"Cloud_resend_email": "Envoyer à nouveau l'e-mail",
"Cloud_registration_required": "Inscription requise",
"Cloud_registration_required_description": "On dirait que, lors de l'installation, vous n'avez pas choisi d'enregistrer votre espace de travail.",
"Cloud_registration_requried_link_text": "Cliquez ici pour enregistrer votre espace de travail.",
"Cloud_registration_required_link_text": "Cliquez ici pour enregistrer votre espace de travail.",
"Cloud_troubleshooting": "Dépannage",
"Collapse_Embedded_Media_By_Default": "Réduire tous les médias intégrés par défaut",
"color": "Couleur",
@ -3094,4 +3094,4 @@
"Your_question": "Votre question",
"Your_server_link": "Le lien de votre serveur",
"Your_workspace_is_ready": "Votre espace de travail est prêt à l'emploi 🎉"
}
}

@ -600,7 +600,6 @@
"Closed_by_visitor": "Zatvorio posjetitelj",
"Closing_chat": "Zatvaranje chata",
"Cloud": "Cloud",
"Cloud_connect": "Rocket.Chat Cloud Connect",
"Cloud_what_is_it": "Što je ovo?",
"Cloud_what_is_it_description": "Rocket.Chat Cloud Connect omogućuje vam povezivanje vašeg Rocket.Chat radnog prostora koje ste sami ugradili u naš Cloud. Na taj način možete upravljati licencama, naplatom i podrškom u Rocket.Chat Cloudu.",
"Cloud_login_to_cloud": "Prijava u Rocket.Chat Cloud",
@ -609,9 +608,9 @@
"Cloud_manually_input_token": "Ručno unesite token primljen iz e-pošte za registraciju u Cloudu.",
"Cloud_registration_required": "Potrebna je registracija",
"Cloud_registration_required_description": "Izgleda da tijekom postavljanja niste odabrali registrirati svoj radni prostor.",
"Cloud_registration_requried_link_text": "Kliknite ovdje da biste registrirali svoj radni prostor.",
"Cloud_registration_required_link_text": "Kliknite ovdje da biste registrirali svoj radni prostor.",
"Cloud_error_in_authenticating": "Pri provjeri autentičnosti primljena je pogreška",
"Cloud_error_code": "Kod:",
"Cloud_error_code": "Kod: __errorCode__",
"Collaborative": "Kolaborativni",
"Collapse_Embedded_Media_By_Default": "Zadano sakrij sve ugrađene medije",
"color": "Boja",
@ -2979,4 +2978,4 @@
"Your_push_was_sent_to_s_devices": "Push obavijest je poslana %s uređaje",
"Your_server_link": "Veza poslužitelja",
"Your_workspace_is_ready": "Radni je prostor spreman za upotrebu 🎉"
}
}

@ -656,7 +656,6 @@
"Closed_by_visitor": "Látogató által bezárva",
"Closing_chat": "Beszélgetés bezáráse",
"Cloud": "Felhő",
"Cloud_connect": "Rocket.Chat Felhő kapcsolódás",
"Cloud_console": "Felhő konzol",
"Cloud_what_is_it": "Mi ez?",
"Cloud_what_is_it_services_like": "Szolgáltatások, mint:",
@ -670,9 +669,9 @@
"Cloud_manually_input_token": "Add meg kézzel a tokent, ami a Felhő regisztrációs e-mailben szerepel.",
"Cloud_registration_required": "Regisztráció szükséges",
"Cloud_registration_required_description": "Úgy tűnik, a beállítás során nem választottad a munkaterület regisztrációját.",
"Cloud_registration_requried_link_text": "Kattints ide a munkahely regisztrálásához.",
"Cloud_registration_required_link_text": "Kattints ide a munkahely regisztrálásához.",
"Cloud_error_in_authenticating": "Hiba a hitelesítés során:",
"Cloud_error_code": "Kód:",
"Cloud_error_code": "Kód: __errorCode__",
"Cloud_troubleshooting": "Hibakeresés",
"Collaborative": "Együttműködő",
"Collapse_Embedded_Media_By_Default": "Beágyazott média alapértelmezett összecsukása",
@ -3355,4 +3354,4 @@
"Your_question": "Kérdésed",
"Your_server_link": "A szerver linkje",
"Your_workspace_is_ready": "A munkaterület készen áll a 🎉 használatára"
}
}

@ -698,12 +698,10 @@
"Closing_chat": "閉じるチャット",
"Cloud": "クラウド",
"Cloud_Register_manually": "手動で登録する",
"Cloud_click_here": "テキストをコピーしたら、クラウドコンソールに移動します。 [ここをクリック]()",
"Cloud_register_offline_finish_helper": "Cloud Consoleで登録プロセスを完了すると、テキストが表示されます。ここに貼り付けて登録を完了してください。",
"Cloud_register_offline_helper": "エアギャップまたはネットワークアクセスが制限されている場合、ワークスペースを手動で登録できます。以下のテキストをコピーし、クラウドコンソールに移動してプロセスを完了します。",
"Cloud_register_success": "ワークスペースが正常に登録されました!",
"Cloud_register_error": "リクエストの処理中にエラーが発生しました。後でもう一度やり直してください。",
"Cloud_connect": "Rocket.Chat クラウド接続",
"Cloud_connect_support": "まだ登録メールが届いていない場合は、上記のメールアドレスを必ず更新してください。それでも問題が解決しない場合は、サポートに連絡することができます。",
"Cloud_console": "クラウドコンソール",
"Cloud_Info": "クラウド情報",
@ -722,9 +720,9 @@
"Cloud_manually_input_token": "クラウド登録メールに記載されたトークンを手動で入力してください。",
"Cloud_registration_required": "登録が必要です",
"Cloud_registration_required_description": "セットアップ中にワークスペースを登録しない選択をしたようです。",
"Cloud_registration_requried_link_text": "ワークスペースを登録するにはここをクリック。",
"Cloud_registration_required_link_text": "ワークスペースを登録するにはここをクリック。",
"Cloud_error_in_authenticating": "認証中にエラーが発生",
"Cloud_error_code": "コード:",
"Cloud_error_code": "コード: __errorCode__",
"Cloud_status_page_description": "特定のクラウドサービスに問題がある場合は、Googleのステータスページで既知の問題を確認できます。",
"Cloud_Service_Agree_PrivacyTerms": "クラウドサービスに同意するプライバシー規約",
"Cloud_troubleshooting": "トラブルシューティング",
@ -3761,4 +3759,4 @@
"Your_server_link": "サーバーのURLはこちら",
"Your_temporary_password_is_password": "一時的なパスワードは<strong>[password]</strong>です。",
"Your_workspace_is_ready": "ワークスペースの準備ができました🎉"
}
}

@ -654,9 +654,7 @@
"Closing_chat": "ករជជកបទ",
"Cloud": "ពពក",
"Cloud_Register_manually": "ចយដ",
"Cloud_click_here": "បនចមលងអតថបទសមចលទងសលកដ។ [ច]()",
"Cloud_register_success": "កនងករងររបសនកតវបនចយជគជយ!",
"Cloud_connect": "ករតភពពក Rocket.Chat ",
"Cloud_connect_support": "បរសនបនកននទនទទលអលចមបកដថលរបសនកតវបនធបចបននភពខងល។ បរសនបនកននបញនកអចទទងករគរ។",
"Cloud_console": "Cloud Console",
"Cloud_Info": "ពនពពក",
@ -675,9 +673,9 @@
"Cloud_manually_input_token": "បញលលខសមលបនទទលដយផលចពពក។",
"Cloud_registration_required": "តវក",
"Cloud_registration_required_description": "មលទចកងអងពលរបចលអនកមនបនជសដកនងធររបសនក។",
"Cloud_registration_requried_link_text": "សមចចតរងកនងធររបសនក។",
"Cloud_registration_required_link_text": "សមចចតរងកនងធររបសនក។",
"Cloud_error_in_authenticating": "កសបនទទលខណលផងផ",
"Cloud_error_code": "លខកដ:",
"Cloud_error_code": "លខកដ: __errorCode__",
"Cloud_status_page_description": "បរសនបកមមពពកជយមនបញនកអចពយមលបញលតវបនគរសនភពរបសង។",
"Cloud_troubleshooting": "កបញ",
"Collaborative": "សហករ",
@ -3174,4 +3172,4 @@
"Your_push_was_sent_to_s_devices": "ករជញរបសនកតវបនបញនទឧបករណ %s បន",
"Your_server_link": "តណភនមរបសនក",
"Your_workspace_is_ready": "កនងធររបសនករចរ🎉"
}
}

@ -599,15 +599,14 @@
"Closed_by_visitor": "방문자가 종료했습니다.",
"Closing_chat": "대화 종료 중..",
"Cloud": "클라우드",
"Cloud_connect": "Rocket.Chat 클라우드 접속",
"Cloud_console": "클라우드 콘솔",
"Cloud_what_is_it": "이게 무엇입니까?",
"Cloud_login_to_cloud": "Rocket.Chat Cloud에 로그인",
"Cloud_update_email": "이메일 업데이트",
"Cloud_resend_email": "이메일 재전송",
"Cloud_registration_required": "등록이 필요합니다.",
"Cloud_registration_requried_link_text": "작업 공간을 등록하려면 여기를 클릭하십시오.",
"Cloud_error_code": "코드:",
"Cloud_registration_required_link_text": "작업 공간을 등록하려면 여기를 클릭하십시오.",
"Cloud_error_code": "코드: __errorCode__",
"Collaborative": "협업",
"Collapse_Embedded_Media_By_Default": "기본적으로 포함된 미디어 접기",
"color": "색",
@ -3162,4 +3161,4 @@
"Your_question": "귀하의 질문",
"Your_server_link": "서버 링크",
"Your_workspace_is_ready": "Rocket.Chat을 사용할 준비가 되었습니다."
}
}

@ -609,7 +609,6 @@
"Closed_by_visitor": "Gesloten door bezoeker",
"Closing_chat": "closing-chat",
"Cloud": "Cloud",
"Cloud_connect": "Rocket.Chat Cloud Connect",
"Cloud_connect_support": "Als u nog steeds geen registratie-e-mail heeft ontvangen, zorg er dan voor dat uw e-mail hierboven is bijgewerkt. Als u nog steeds problemen ondervindt, kunt u contact opnemen met ondersteuning op",
"Cloud_console": "Cloud Console",
"Cloud_what_is_it": "Wat is dit?",
@ -3053,4 +3052,4 @@
"Your_push_was_sent_to_s_devices": "Je push werd verzonden naar %s apparaten",
"Your_server_link": "Uw serverlink",
"Your_workspace_is_ready": "Uw werkruimte is klaar voor gebruik 🎉"
}
}

@ -679,8 +679,6 @@
"Closed_by_visitor": "Zamknięte przez odwiedzającego",
"Closing_chat": "Zamykanie czatu",
"Cloud": "Chmura",
"Cloud_click_here": "Po skopiowaniu tekstu, przejdź do konsoli chmury. [Kliknij tutaj]()",
"Cloud_connect": "Połączenie z Chmurą Rocket.Chat",
"Cloud_console": "Konsola w chmurze",
"Cloud_Info": "Chmura",
"Cloud_what_is_it": "Co to jest?",
@ -697,9 +695,9 @@
"Cloud_manually_input_token": "Wpisz ręcznie swój token otrzymany z wiadomości rejestrującej w Chmurze.",
"Cloud_registration_required": "Rejestracja wymagana",
"Cloud_registration_required_description": "Wygląda na to że podczas instalacji nie wybrałeś opcji rejestracji przestrzeni roboczej.",
"Cloud_registration_requried_link_text": "Kliknij tutaj, aby zarejestrować swoją przestrzeń roboczą.",
"Cloud_registration_required_link_text": "Kliknij tutaj, aby zarejestrować swoją przestrzeń roboczą.",
"Cloud_error_in_authenticating": "Błąd podczas uwierzytelniania",
"Cloud_error_code": "Kod:",
"Cloud_error_code": "Kod: __errorCode__",
"Cloud_troubleshooting": "Rozwiązywanie problemów",
"Collaborative": "Współpracujący",
"Collapse_Embedded_Media_By_Default": "Zwiń media (obrazki itp.) domyślnie",
@ -3402,4 +3400,4 @@
"Your_question": "Twoje pytanie",
"Your_server_link": "Twój link do serwera",
"Your_workspace_is_ready": "Twój obszar roboczy jest gotowy do użycia 🎉"
}
}

@ -679,7 +679,6 @@
"Cloud_Register_manually": "Registre manualmente",
"Cloud_register_success": "A sua área de trabalho foi registrada com sucesso!",
"Cloud_register_error": "Houve um erro ao processar sua solicitação. Por favor tente novamente mais tarde. ",
"Cloud_connect": "Rocket.Chat Cloud Connect",
"Cloud_connect_support": "Se você ainda não recebeu um e-mail de registro, verifique se o seu e-mail está atualizado acima. Se você ainda tiver problemas, entre em contato com o suporte em",
"Cloud_what_is_it": "O que é isso?",
"Cloud_what_is_it_description": "O Rocket.Chat Cloud Connect permite que você conecte seu workspace Rocket.Chat auto-hospedado à nossa nuvem. Fazer isso permite que você gerencie suas licenças, Faturamento e Suporte no Rocket.Chat Cloud.",
@ -691,9 +690,9 @@
"Cloud_manually_input_token": "Insira manualmente o token recebido do e-mail de registro na nuvem.",
"Cloud_registration_required": "Registro requerido",
"Cloud_registration_required_description": "Parece que durante a configuração você não escolheu registrar seu workspace.",
"Cloud_registration_requried_link_text": "Clique aqui para registrar seu workspace.",
"Cloud_registration_required_link_text": "Clique aqui para registrar seu workspace.",
"Cloud_error_in_authenticating": "Erro recebido durante a autenticação",
"Cloud_error_code": "Código:",
"Cloud_error_code": "Código: __errorCode__",
"Cloud_troubleshooting": "Resolução de problemas",
"Collaborative": "Colaborativo",
"Collapse_Embedded_Media_By_Default": "Esconder mídia por padrão",

@ -637,7 +637,6 @@
"Closed_by_visitor": "Encerrado pelo convidado",
"Closing_chat": "A encerrar chat",
"Cloud": "Nuvem",
"Cloud_connect": "Rocket.Chat Cloud Connect",
"Cloud_connect_support": "Se ainda não recebeu um e-mail de registo, verifique se o seu e-mail está atualizado acima. Se ainda tiver problemas, entre em contacto com o suporte em",
"Cloud_console": "Cloud Console",
"Cloud_what_is_it": "O que é isto?",
@ -650,9 +649,9 @@
"Cloud_manually_input_token": "Insira manualmente o token recebido do e-mail de registro na nuvem.",
"Cloud_registration_required": "Registro requerido",
"Cloud_registration_required_description": "Parece que durante a configuração você não escolheu registrar seu workspace.",
"Cloud_registration_requried_link_text": "Clique aqui para registrar seu workspace.",
"Cloud_registration_required_link_text": "Clique aqui para registrar seu workspace.",
"Cloud_error_in_authenticating": "Erro recebido durante a autenticação",
"Cloud_error_code": "Código:",
"Cloud_error_code": "Código: __errorCode__",
"Collaborative": "Colaborativo",
"Collapse_Embedded_Media_By_Default": "Esconder Leitor por padrão",
"color": "Côr",
@ -3248,4 +3247,4 @@
"Your_question": "A sua pergunta",
"Your_server_link": "O link do seu servidor",
"Your_workspace_is_ready": "O seu espaço de trabalho está pronto a usar 🎉"
}
}

@ -662,12 +662,10 @@
"Closing_chat": "Закрыть чат",
"Cloud": "Облако",
"Cloud_Register_manually": "Зарегистрироваться вручную",
"Cloud_click_here": "После копирования текста перейдите на Cloud Console. [Щелкните здесь]()",
"Cloud_register_offline_finish_helper": "После завершения процесса регистрации в Cloud Console вам должен быть представлен некоторый текст. Пожалуйста, вставьте его здесь, чтобы закончить регистрацию.",
"Cloud_register_offline_helper": "Рабочие места могут быть зарегистрированы вручную, если доступ в сеть ограничен. Скопируйте текст ниже и перейдите в нашу Cloud Console, чтобы завершить процесс.",
"Cloud_register_success": "Ваше рабочее место успешно зарегистрировано!",
"Cloud_register_error": "Произошла ошибка при попытке обработать ваш запрос. Пожалуйста, попробуйте позже.",
"Cloud_connect": "Rocket.Chat Cloud Connect",
"Cloud_connect_support": "Если Вы все еще не получили регистрационное электронное письмо, пожалуйста, удостоверьтесь, что Ваша электронная почта обновлена выше. Если у Вас все еще есть проблемы, вы можете связаться с поддержкой по адресу",
"Cloud_console": "Облачная Консоль",
"Cloud_Info": "Информация об облаке",
@ -686,9 +684,9 @@
"Cloud_manually_input_token": "Вручную введите токен, полученный из электронного письма регистрации в облаке.",
"Cloud_registration_required": "Требуется регистрация",
"Cloud_registration_required_description": "Похоже, что во время установки вы не решили регистрировать свое рабочее пространство.",
"Cloud_registration_requried_link_text": "Нажмите здесь, чтобы зарегистрировать ваше рабочее пространство.",
"Cloud_registration_required_link_text": "Нажмите здесь, чтобы зарегистрировать ваше рабочее пространство.",
"Cloud_error_in_authenticating": "Ошибка при аутентификации",
"Cloud_error_code": "Код:",
"Cloud_error_code": "Код: __errorCode__",
"Cloud_status_page_description": "Если у конкретной облачной службы возникают проблемы, вы можете проверить ее на нашей странице состояния по адресу",
"Cloud_Service_Agree_PrivacyTerms": "Согласие с условиями конфиденциальности Cloud Service",
"Cloud_troubleshooting": "Исправление проблем",
@ -3554,4 +3552,4 @@
"Your_server_link": "Ссылка на ваш сервер",
"Your_temporary_password_is_password": "Ваш временный пароль <strong>[password]</strong>.",
"Your_workspace_is_ready": "Ваше рабочее пространство готово к работе 🎉"
}
}

@ -573,7 +573,6 @@
"Closed_by_visitor": "Stängd av besökare",
"Closing_chat": "Stänger chat",
"Cloud": "Cloud",
"Cloud_connect": "Rocket.Chat Cloud Connect",
"Collaborative": "Kollaborativ",
"Collapse_Embedded_Media_By_Default": "Kollapsa inbäddad media som standard",
"color": "Färg",
@ -2945,4 +2944,4 @@
"Your_push_was_sent_to_s_devices": "Din push skickades till %s enheter",
"Your_server_link": "Din serverlänk",
"Your_workspace_is_ready": "Din arbetsyta är redo att använda 🎉"
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save