# Conflicts: # .docker/Dockerfile.rhel # .github/history.json # HISTORY.md # app/lib/server/startup/settings.js # app/utils/rocketchat.info # package-lock.json # package.jsonpull/17762/head
commit
abd5ebf0aa
@ -1,42 +1,253 @@ |
||||
# Contributing to Rocket.Chat |
||||
|
||||
:+1::tada: First off, thanks for taking the time to contribute! :tada::+1: |
||||
**First off, thanks for taking the time to contribute! :tada::+1:** |
||||
|
||||
The following is a set of guidelines for contributing to Rocket.Chat and its packages, which are hosted in the [Rocket.Chat Organization](https://github.com/RocketChat) on GitHub. |
||||
> There are many ways to contribute to Rocket.Chat even if you're not technical or a developer: |
||||
> |
||||
> * Email us at marketing@rocket.chat to tell us how much you love the project |
||||
> * Write about us in your blogs |
||||
> * Fix some small typos in our [documentation](https://docs.rocket.chat/contributing) |
||||
> * Become our [GitHub sponsor](https://github.com/sponsors/RocketChat) |
||||
> * Tell others about us and help us spread the word |
||||
> |
||||
> Every bit of contribution is appreciated 🙂 thank you! |
||||
|
||||
The following is a set of guidelines for contributing to Rocket.Chat, which are hosted in the [Rocket.Chat Organization](https://github.com/RocketChat) on GitHub. |
||||
|
||||
__Note:__ If there's a feature you'd like, there's a bug you'd like to fix, or you'd just like to get involved please raise an issue and start a conversation. We'll help as much as we can so you can get contributing - although we may not always be able to respond right away :) |
||||
|
||||
## ECMAScript 2015 vs CoffeeScript |
||||
## Setup |
||||
|
||||
Your development workstation needs to have at least 8GB or RAM to be able to build the Rocket.Chat's source code. |
||||
|
||||
Rocket.Chat runs on top of [Meteor](https://www.meteor.com/). To run it on development mode you need to [install Meteor](https://www.meteor.com/install) and clone/download the Rocket.Chat's code, then just open the code folder and run: |
||||
```shell |
||||
meteor npm install && meteor |
||||
``` |
||||
It should build and run the application and database for you, now you can access the UI on (http://localhost:3000) |
||||
|
||||
It's not necessary to install Nodejs or NPM, every time you need to use them you can run `meteor node` or `meteor npm`. |
||||
|
||||
It's important to always run the NPM commands using `meteor npm` to ensure that you are installing the modules using the right Nodejs version. |
||||
|
||||
## Coding |
||||
|
||||
We provide a [.editorconfig](../.editorconfig) file that will help you to keep some standards in place. |
||||
|
||||
### ECMAScript vs TypeScript |
||||
|
||||
We are currently adopting TypeScript as the default language on our projects, the current codebase will be migrated incrementally from JavaScript to TypeScript. |
||||
|
||||
While we still have a lot of JavaScript files you should not create new ones. As much as possible new code contributions should be in **TypeScript**. |
||||
|
||||
While we still have a lot of CoffeeScript files you should not create new ones. New code contributions should be in **ECMAScript 2015**. |
||||
### Blaze vs React |
||||
|
||||
## Coding standards |
||||
We are currently adopting React over Blaze as our UI engine, the current codebase is under migration and will continue. You will still find Blaze templates in our code. Code changes or contributions may need to be made in Blaze while we continue to evolve our components library. |
||||
|
||||
Most of the coding standards are covered by `.editorconfig` and `.eslintrc.js`. |
||||
[Fuselage](https://github.com/RocketChat/Rocket.Chat.Fuselage) is our component library based on React, check it out when contributing to the Rocket.Chat UI and feel free to contribute new components or fixes. |
||||
|
||||
### Standards |
||||
|
||||
Most of the coding standards are covered by ESLint configured at [.eslintrc](../.eslintrc), and most of them came from our own [ESLint Config Package](https://github.com/RocketChat/eslint-config-rocketchat). |
||||
|
||||
Things not covered by `eslint`: |
||||
|
||||
* `exports`/`module.exports` should be at the end of the file |
||||
* Longer, descriptive variable names are preferred, e.g. `error` vs `err` |
||||
* Prefer longer/descriptive variable names, e.g. `error` vs `err`, unless dealing with common record properties already shortened, e.g. `rid` and `uid` |
||||
* Use return early pattern. [See more](https://blog.timoxley.com/post/47041269194/avoid-else-return-early) |
||||
* Prefer `Promise` over `callbacks` |
||||
* Prefer `await` over `then/catch` |
||||
* Don't create queries outside models, the query description should be inside the model class. |
||||
* Don't hardcode fields inside models. Same method can be used for different purposes, using different fields. |
||||
* Prefer create REST endpoints over Meteor methods |
||||
* Prefer call REST endpoints over Meteor methods when both are available |
||||
* v1 REST endpoints should follow the following pattern: `/api/v1/dashed-namespace.camelCaseAction` |
||||
* Prefer TypeScript over JavaScript. Check [ECMAScript vs TypeScript](#ecmascript-vs-typescript) |
||||
|
||||
We acknowledge all the code does not meet these standards but we are working to change this over time. |
||||
#### Blaze |
||||
* Import the HTML file from it's sibling JS/TS file |
||||
|
||||
### Syntax check |
||||
|
||||
Before submitting a PR you should get no errors on `eslint`. |
||||
|
||||
To check your files, first install `eslint`: |
||||
To check your files run: |
||||
|
||||
```shell |
||||
meteor npm run lint |
||||
``` |
||||
|
||||
## Tests |
||||
|
||||
There are 2 types of tests we run on Rocket.Chat, **Unit** tests and **End to End** tests. The major difference is that End to End tests require a Rocket.Chat instance running to execute the API and UI checks. |
||||
|
||||
### End to End Tests |
||||
|
||||
First you need to run a Rocket.Chat server on **Test Mode** and on a **Empty Database**: |
||||
```shell |
||||
# Running with a local mongodb database |
||||
MONGO_URL=mongodb://localhost/empty MONGO_OPLOG_URL=mongodb://localhost/local TEST_MODE=true meteor |
||||
``` |
||||
```shell |
||||
# Running with a local mongodb database but cleaning it before |
||||
mongo --eval "db.dropDatabase()" empty && MONGO_URL=mongodb://localhost/empty MONGO_OPLOG_URL=mongodb://localhost/local TEST_MODE=true meteor |
||||
``` |
||||
|
||||
Now you can run the tests: |
||||
```shell |
||||
meteor npm test |
||||
``` |
||||
|
||||
### Unit Tests |
||||
|
||||
Unit tests are simpler to setup and run. They do not require a working Rocket.Chat instance. |
||||
```shell |
||||
meteor npm run testunit |
||||
``` |
||||
|
||||
It's possible to run on watch mode as well: |
||||
```shell |
||||
meteor npm run testunit-watch |
||||
``` |
||||
|
||||
<!-- ### Storybook --> |
||||
|
||||
## Before Push your code |
||||
|
||||
It's important to run the lint and tests before push your code or submit a Pull Request, otherwise your contribution may fail quickly on the CI. Reviewers are forced to demand fixes and the review of your contribution will be further delayed. |
||||
|
||||
Rocket.Chat uses [husky](https://www.npmjs.com/package/husky) to run the **lint** and **unit tests** before proceed to the code push process, so you may notice a delay when pushing your code to your repository. |
||||
|
||||
## Choosing a good PR title |
||||
|
||||
It is very important to note that we use PR titles when creating our change log. Keep this in mind when you title your PR. Make sure the title makes sense to a person reading a releases' change log! |
||||
|
||||
Keep your PR's title as short and concise as possible, use PR's description section, which you can find in the PR's template, to provide more details into the changelog. |
||||
|
||||
Good titles require thinking from a user's point of view. Don't get technical and talk code or architecture. What is the actual user-facing feature or the bug fixed? For example: |
||||
|
||||
``` |
||||
[NEW] Allow search permissions and settings by name instead of only ID |
||||
``` |
||||
|
||||
Even it's being something new in the code the users already expect the filter to filter by what they see (translations), a better one would be: |
||||
|
||||
``` |
||||
[FIX] Permissions' search doesn't filter base on presented translation, only on internal ids |
||||
``` |
||||
|
||||
## Choosing the right PR tag |
||||
|
||||
You can use several tags do describe your PR, i.e.: `[FIX]`, `[NEW]`, etc. You can use the descriptions below to better understand the meaning of each one, and decide which one you should use: |
||||
|
||||
### `[NEW]` |
||||
|
||||
#### When |
||||
- When adding a new feature that is important to the end user |
||||
|
||||
#### How |
||||
|
||||
Do not start repeating the section (`Add ...` or `New ...`) |
||||
Always describe what's being fixed, improved or added and not *how* it was fixed, improved or added. |
||||
|
||||
Exemple of **bad** PR titles: |
||||
|
||||
``` |
||||
[NEW] Add ability to set tags in the Omnichannel room closing dialog |
||||
[NEW] Adds ability for Rocket.Chat Apps to create discussions |
||||
[NEW] Add MMS support to Voxtelesys |
||||
[NEW] Add Color variable to left sidebar |
||||
``` |
||||
|
||||
Exemple of **good** PR titles: |
||||
|
||||
``` |
||||
npm install -g eslint |
||||
[NEW] Ability to set tags in the Omnichannel room closing dialog |
||||
[NEW] Ability for Rocket.Chat Apps to create discussions |
||||
[NEW] MMS support to Voxtelesys |
||||
[NEW] Color variable to left sidebar |
||||
``` |
||||
|
||||
Then run: |
||||
### `[FIX]` |
||||
|
||||
#### When |
||||
- When fixing something not working or behaving wrong from the end user perspective |
||||
|
||||
#### How |
||||
|
||||
Always describe what's being fixed and not *how* it was fixed. |
||||
|
||||
Exemple of a **bad** PR title: |
||||
|
||||
``` |
||||
eslint . |
||||
[FIX] Add Content-Type for public files with JWT |
||||
``` |
||||
|
||||
# Contributor License Agreement |
||||
Exemple of a **good** PR title: |
||||
|
||||
``` |
||||
[FIX] Missing Content-Type header for public files with JWT |
||||
``` |
||||
|
||||
### `[IMPROVE]` |
||||
|
||||
#### When |
||||
- When a change enhances a not buggy behavior. When in doubt if it's a Improve or Fix prefer to use as fix. |
||||
|
||||
#### How |
||||
Always describe what's being improved and not *how* it was improved. |
||||
|
||||
Exemple of **good** PR title: |
||||
|
||||
``` |
||||
[IMPROVE] Displays Nothing found on admin sidebar when search returns nothing |
||||
``` |
||||
|
||||
### `[BREAK]` |
||||
|
||||
#### When |
||||
- When the changes affect a working feature |
||||
|
||||
##### Back-End |
||||
- When the API contract (data structure and endpoints) are limited, expanded as required or removed |
||||
- When the business logic (permissions and roles) are limited, expanded (without migration) or removed |
||||
|
||||
##### Front-End |
||||
- When the change limits (format, size, etc) or removes the ability of read or change the data (when the limitation was not caused by the back-end) |
||||
|
||||
### Second tag e.g. `[NEW][ENTERPRISE]` |
||||
|
||||
Use a second tag to group entries on the change log, we currently use it only for the Enterprise items but we are going to expand it's usage soon, please do not use it until we create a patter for it. |
||||
|
||||
### Minor Changes |
||||
|
||||
For those PRs that aren't important for the end user, we are working on a better pattern, but for now please use the same tags, use them without the brackets and in camel case: |
||||
|
||||
``` |
||||
Fix: Missing Content-Type header for public files with JWT |
||||
``` |
||||
|
||||
All those PRs will be grouped under the `Minor changes` section which is collapsed, so users can expand it to check for those minor things but they are not visible directly on changelog. |
||||
|
||||
## Security Best Practices |
||||
|
||||
- Never expose unnecessary data to the APIs' responses |
||||
- Always check for permissions or create new ones when you must expose sensitive data |
||||
- Never provide new APIs without rate limiters |
||||
- Always escape the user's input when rendering data |
||||
- Always limit the user's input size on server side |
||||
- Always execute the validations on the server side even when executing on the client side as well |
||||
|
||||
## Performance Best Practices |
||||
|
||||
- Prefer inform the fields you want, and only the necessary ones, when querying data from database over query the full documents |
||||
- Limit the number of returned records to a reasonable value |
||||
- Check if the query is using indexes, it it's not create new indexes |
||||
- Prefer queues over long executions |
||||
- Create new metrics to mesure things whenever possible |
||||
- Cache data and returns whenever possible |
||||
|
||||
## Contributor License Agreement |
||||
|
||||
To have your contribution accepted you must sign our [Contributor License Agreement](https://cla-assistant.io/RocketChat/Rocket.Chat). In case you submit a Pull Request before sign the CLA GitHub will alert you with a new comment asking you to sign and will block the Pull Request from be merged by us. |
||||
|
||||
Please review and sign our CLA at https://cla-assistant.io/RocketChat/Rocket.Chat |
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,2 @@ |
||||
{{> versions}} |
||||
{{> _prs}} |
||||
@ -0,0 +1,13 @@ |
||||
{{#if (or release.node_version release.npm_version release.mongo_versions)}} |
||||
|
||||
### Engine versions |
||||
{{#if release.node_version}} |
||||
- Node: `{{ release.node_version }}` |
||||
{{/if}} |
||||
{{#if release.npm_version}} |
||||
- NPM: `{{ release.npm_version }}` |
||||
{{/if}} |
||||
{{#if release.mongo_versions}} |
||||
- MongoDB: `{{ join release.mongo_versions ', ' }}` |
||||
{{/if}} |
||||
{{/if}} |
||||
@ -1 +1 @@ |
||||
METEOR@1.9.2 |
||||
METEOR@1.10.2 |
||||
|
||||
@ -1,19 +0,0 @@ |
||||
{ |
||||
"presets": [ |
||||
[ |
||||
"@babel/preset-env", |
||||
{ |
||||
"shippedProposals": true, |
||||
"useBuiltIns": "usage", |
||||
"corejs": "3", |
||||
"modules": "commonjs" |
||||
} |
||||
], |
||||
"@babel/preset-react", |
||||
"@babel/preset-flow" |
||||
], |
||||
"plugins": [ |
||||
"@babel/plugin-proposal-class-properties", |
||||
"@babel/plugin-proposal-optional-chaining" |
||||
] |
||||
} |
||||
@ -1,4 +0,0 @@ |
||||
import '@storybook/addon-actions/register'; |
||||
import '@storybook/addon-knobs/register'; |
||||
import '@storybook/addon-links/register'; |
||||
import '@storybook/addon-viewport/register'; |
||||
@ -0,0 +1,20 @@ |
||||
module.exports = { |
||||
presets: [ |
||||
[ |
||||
'@babel/preset-env', |
||||
{ |
||||
shippedProposals: true, |
||||
useBuiltIns: 'usage', |
||||
corejs: '3', |
||||
modules: 'commonjs', |
||||
}, |
||||
], |
||||
'@babel/preset-react', |
||||
'@babel/preset-flow', |
||||
], |
||||
plugins: [ |
||||
'@babel/plugin-proposal-class-properties', |
||||
'@babel/plugin-proposal-optional-chaining', |
||||
'@babel/plugin-proposal-nullish-coalescing-operator', |
||||
], |
||||
}; |
||||
@ -1,20 +0,0 @@ |
||||
import { withKnobs } from '@storybook/addon-knobs'; |
||||
import { MINIMAL_VIEWPORTS } from '@storybook/addon-viewport/dist/defaults'; |
||||
import { addDecorator, addParameters, configure } from '@storybook/react'; |
||||
|
||||
import { rocketChatDecorator } from './mocks/decorators'; |
||||
|
||||
addParameters({ |
||||
viewport: { |
||||
viewports: MINIMAL_VIEWPORTS, |
||||
}, |
||||
}); |
||||
|
||||
addDecorator(rocketChatDecorator); |
||||
addDecorator(withKnobs); |
||||
|
||||
configure([ |
||||
require.context('../app', true, /\.stories\.js$/), |
||||
require.context('../client', true, /\.stories\.js$/), |
||||
require.context('../ee/app', true, /\.stories\.js$/), |
||||
], module); |
||||
@ -0,0 +1,11 @@ |
||||
module.exports = { |
||||
stories: [ |
||||
'../app/**/*.stories.js', |
||||
'../client/**/*.stories.js', |
||||
'../ee/app/**/*.stories.js', |
||||
], |
||||
addons: [ |
||||
'@storybook/addon-actions', |
||||
'@storybook/addon-knobs', |
||||
], |
||||
}; |
||||
@ -0,0 +1,7 @@ |
||||
import { withKnobs } from '@storybook/addon-knobs'; |
||||
import { addDecorator } from '@storybook/react'; |
||||
|
||||
import { rocketChatDecorator } from './mocks/decorators'; |
||||
|
||||
addDecorator(rocketChatDecorator); |
||||
addDecorator(withKnobs); |
||||
@ -1 +0,0 @@ |
||||
export * from './server/index'; |
||||
@ -1,27 +1,58 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
|
||||
import { handleError } from '../../../utils'; |
||||
import { actionLinks } from '../../both/lib/actionLinks'; |
||||
// Action Links Handler. This method will be called off the client.
|
||||
import { handleError } from '../../../utils/client'; |
||||
import { Messages, Subscriptions } from '../../../models/client'; |
||||
|
||||
actionLinks.run = (name, messageId, instance) => { |
||||
const message = actionLinks.getMessage(name, messageId); |
||||
// Action Links namespace creation.
|
||||
export const actionLinks = { |
||||
actions: {}, |
||||
register(name, funct) { |
||||
actionLinks.actions[name] = funct; |
||||
}, |
||||
getMessage(name, messageId) { |
||||
const userId = Meteor.userId(); |
||||
if (!userId) { |
||||
throw new Meteor.Error('error-invalid-user', 'Invalid user', { function: 'actionLinks.getMessage' }); |
||||
} |
||||
|
||||
const message = Messages.findOne({ _id: messageId }); |
||||
if (!message) { |
||||
throw new Meteor.Error('error-invalid-message', 'Invalid message', { function: 'actionLinks.getMessage' }); |
||||
} |
||||
|
||||
const subscription = Subscriptions.findOne({ |
||||
rid: message.rid, |
||||
'u._id': userId, |
||||
}); |
||||
if (!subscription) { |
||||
throw new Meteor.Error('error-not-allowed', 'Not allowed', { function: 'actionLinks.getMessage' }); |
||||
} |
||||
|
||||
if (!message.actionLinks || !message.actionLinks[name]) { |
||||
throw new Meteor.Error('error-invalid-actionlink', 'Invalid action link', { function: 'actionLinks.getMessage' }); |
||||
} |
||||
|
||||
const actionLink = message.actionLinks[name]; |
||||
return message; |
||||
}, |
||||
run(name, messageId, instance) { |
||||
const message = actionLinks.getMessage(name, messageId); |
||||
|
||||
let ranClient = false; |
||||
const actionLink = message.actionLinks[name]; |
||||
|
||||
if (actionLinks && actionLinks.actions && actionLinks.actions[actionLink.method_id]) { |
||||
// run just on client side
|
||||
actionLinks.actions[actionLink.method_id](message, actionLink.params, instance); |
||||
let ranClient = false; |
||||
|
||||
ranClient = true; |
||||
} |
||||
if (actionLinks && actionLinks.actions && actionLinks.actions[actionLink.method_id]) { |
||||
// run just on client side
|
||||
actionLinks.actions[actionLink.method_id](message, actionLink.params, instance); |
||||
|
||||
// and run on server side
|
||||
Meteor.call('actionLinkHandler', name, messageId, (err) => { |
||||
if (err && !ranClient) { |
||||
handleError(err); |
||||
ranClient = true; |
||||
} |
||||
}); |
||||
|
||||
// and run on server side
|
||||
Meteor.call('actionLinkHandler', name, messageId, (err) => { |
||||
if (err && !ranClient) { |
||||
handleError(err); |
||||
} |
||||
}); |
||||
}, |
||||
}; |
||||
|
||||
@ -1,8 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
|
||||
if (Meteor.isClient) { |
||||
module.exports = require('./client/index.js'); |
||||
} |
||||
if (Meteor.isServer) { |
||||
module.exports = require('./server/index.js'); |
||||
} |
||||
@ -1,6 +1,6 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
|
||||
import { Messages, Subscriptions } from '../../../models'; |
||||
import { Messages, Subscriptions } from '../../../models/server'; |
||||
|
||||
// Action Links namespace creation.
|
||||
export const actionLinks = { |
||||
@ -1 +0,0 @@ |
||||
export * from './server/index'; |
||||
@ -1,3 +1,3 @@ |
||||
import './cron'; |
||||
|
||||
export { Apps } from './orchestrator'; |
||||
export { Apps, AppEvents } from './orchestrator'; |
||||
|
||||
@ -1 +0,0 @@ |
||||
export * from './server/index'; |
||||
@ -1 +0,0 @@ |
||||
export { default } from './server/bigbluebutton-api'; |
||||
@ -0,0 +1 @@ |
||||
export { default } from './bigbluebutton-api'; |
||||
@ -1 +0,0 @@ |
||||
import './server/index'; |
||||
@ -0,0 +1 @@ |
||||
export const baseUrl = (process.env.CHATPAL_URL || 'https://api.chatpal.io/v1').replace(/\/?$/, '/'); |
||||
@ -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 '../../ui-admin/client'; |
||||
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,111 +0,0 @@ |
||||
.sound-info { |
||||
& .icon-play-circled { |
||||
cursor: pointer; |
||||
} |
||||
} |
||||
|
||||
.sound-view { |
||||
z-index: 15; |
||||
|
||||
overflow-x: hidden; |
||||
overflow-y: auto; |
||||
|
||||
& .thumb { |
||||
width: 100%; |
||||
height: 350px; |
||||
padding: 20px; |
||||
} |
||||
|
||||
& nav { |
||||
padding: 0 20px; |
||||
} |
||||
|
||||
& .info { |
||||
padding: 0 20px; |
||||
|
||||
white-space: normal; |
||||
|
||||
& h3 { |
||||
overflow: hidden; |
||||
|
||||
width: 100%; |
||||
margin: 8px 0; |
||||
|
||||
user-select: text; |
||||
white-space: nowrap; |
||||
text-overflow: ellipsis; |
||||
|
||||
font-size: 24px; |
||||
line-height: 27px; |
||||
|
||||
& i::after { |
||||
display: inline-block; |
||||
|
||||
width: 8px; |
||||
height: 8px; |
||||
|
||||
content: " "; |
||||
vertical-align: middle; |
||||
|
||||
border-radius: 4px; |
||||
} |
||||
} |
||||
|
||||
& p { |
||||
-webkit-user-select: text; |
||||
-moz-user-select: text; |
||||
-ms-user-select: text; |
||||
user-select: text; |
||||
|
||||
font-size: 12px; |
||||
font-weight: 300; |
||||
line-height: 18px; |
||||
} |
||||
} |
||||
|
||||
& .edit-form { |
||||
padding: 20px 20px 0; |
||||
|
||||
white-space: normal; |
||||
|
||||
& h3 { |
||||
margin-bottom: 8px; |
||||
|
||||
font-size: 24px; |
||||
line-height: 22px; |
||||
} |
||||
|
||||
& p { |
||||
font-size: 12px; |
||||
font-weight: 300; |
||||
line-height: 18px; |
||||
} |
||||
|
||||
& > .input-line { |
||||
margin-top: 20px; |
||||
} |
||||
|
||||
& nav { |
||||
padding: 0; |
||||
|
||||
&.buttons { |
||||
margin-top: 2em; |
||||
} |
||||
} |
||||
|
||||
& .form-divisor { |
||||
height: 9px; |
||||
margin: 2em 0; |
||||
|
||||
text-align: center; |
||||
|
||||
& > span { |
||||
padding: 0 1em; |
||||
} |
||||
} |
||||
} |
||||
|
||||
& .room-info-content > div { |
||||
margin: 0 0 20px; |
||||
} |
||||
} |
||||
@ -1,7 +0,0 @@ |
||||
<template name="adminSoundEdit"> |
||||
<div class="content"> |
||||
<div class="sound-view"> |
||||
{{> soundEdit .}} |
||||
</div> |
||||
</div> |
||||
</template> |
||||
@ -1,7 +0,0 @@ |
||||
<template name="adminSoundInfo"> |
||||
<div class="content"> |
||||
<div class="sound-view"> |
||||
{{> soundInfo .}} |
||||
</div> |
||||
</div> |
||||
</template> |
||||
@ -1,78 +0,0 @@ |
||||
<template name="adminSounds"> |
||||
<div class="main-content-flex"> |
||||
<section class="page-container page-list flex-tab-main-content"> |
||||
{{> header sectionName="Custom_Sounds"}} |
||||
<div class="content"> |
||||
{{#requiresPermission 'manage-sounds'}} |
||||
<form class="search-form" role="form"> |
||||
<div class="rc-input__wrapper"> |
||||
<div class="rc-input__icon"> |
||||
{{#if isLoading}} |
||||
{{> loading }} |
||||
{{else}} |
||||
{{> icon block="rc-input__icon-svg" icon="magnifier" }} |
||||
{{/if}} |
||||
</div> |
||||
<input id="sound-filter" type="text" class="rc-input__element" |
||||
placeholder="{{_ "Search"}}" autofocus dir="auto"> |
||||
</div> |
||||
</form> |
||||
<div class="results"> |
||||
{{{_ "Showing_results" customsounds.length}}} |
||||
</div> |
||||
{{#table fixed='true' onItemClick=onTableItemClick onScroll=onTableScroll onResize=onTableResize}} |
||||
<thead> |
||||
<tr> |
||||
<th width="95%"> |
||||
<div class="table-fake-th">{{_ "Name"}}</div> |
||||
</th> |
||||
<th width="5%"> |
||||
<div class="table-fake-th">{{_ "Action"}}</div> |
||||
</th> |
||||
</tr> |
||||
</thead> |
||||
<tbody> |
||||
{{#each customsounds}} |
||||
<tr> |
||||
<td width="80%"> |
||||
<div class="rc-table-wrapper"> |
||||
<div class="rc-table-info"> |
||||
<span class="rc-table-title"> |
||||
{{name}} |
||||
</span> |
||||
</div> |
||||
</div> |
||||
</td> |
||||
<td width="20%"> |
||||
<div class="rc-table-wrapper"> |
||||
{{#if isPlaying _id}} |
||||
{{>icon _id=_id icon="pause" block="icon-pause-circled"}} |
||||
{{else}} |
||||
{{>icon _id=_id icon="play" block="icon-play-circled"}} |
||||
{{/if}} |
||||
{{>icon _id=_id icon="ban" block="icon-reset-circled"}} |
||||
</div> |
||||
</td> |
||||
</tr> |
||||
{{else}} |
||||
{{# with searchText}} |
||||
<tr class="table-no-click"> |
||||
<td>{{_ "No_results_found_for"}} {{.}}</td> |
||||
</tr> |
||||
{{/with}} |
||||
{{/each}} |
||||
{{#if isLoading}} |
||||
<tr class="table-no-click"> |
||||
<td class="table-loading-td">{{> loading}}</td> |
||||
</tr> |
||||
{{/if}} |
||||
</tbody> |
||||
{{/table}} |
||||
{{/requiresPermission}} |
||||
</div> |
||||
</section> |
||||
{{#with flexData}} |
||||
{{> flexTabBar}} |
||||
{{/with}} |
||||
</div> |
||||
</template> |
||||
@ -1,176 +0,0 @@ |
||||
import { ReactiveVar } from 'meteor/reactive-var'; |
||||
import { Tracker } from 'meteor/tracker'; |
||||
import { FlowRouter } from 'meteor/kadira:flow-router'; |
||||
import { Template } from 'meteor/templating'; |
||||
import _ from 'underscore'; |
||||
|
||||
import { RocketChatTabBar, SideNav, TabBar } from '../../../ui-utils'; |
||||
import { CustomSounds } from '../lib/CustomSounds'; |
||||
import { APIClient } from '../../../utils/client'; |
||||
|
||||
const LIST_SIZE = 50; |
||||
const DEBOUNCE_TIME_TO_SEARCH_IN_MS = 500; |
||||
|
||||
Template.adminSounds.helpers({ |
||||
searchText() { |
||||
const instance = Template.instance(); |
||||
return instance.filter && instance.filter.get(); |
||||
}, |
||||
isPlaying(_id) { |
||||
return Template.instance().isPlayingId.get() === _id; |
||||
}, |
||||
customsounds() { |
||||
return Template.instance().sounds.get(); |
||||
}, |
||||
isLoading() { |
||||
return Template.instance().isLoading.get(); |
||||
}, |
||||
flexData() { |
||||
return { |
||||
tabBar: Template.instance().tabBar, |
||||
data: Template.instance().tabBarData.get(), |
||||
}; |
||||
}, |
||||
|
||||
onTableScroll() { |
||||
const instance = Template.instance(); |
||||
return function(currentTarget) { |
||||
if (currentTarget.offsetHeight + currentTarget.scrollTop < currentTarget.scrollHeight - 100) { |
||||
return; |
||||
} |
||||
const sounds = instance.sounds.get(); |
||||
if (instance.total.get() > sounds.length) { |
||||
instance.offset.set(instance.offset.get() + LIST_SIZE); |
||||
} |
||||
}; |
||||
}, |
||||
onTableItemClick() { |
||||
const instance = Template.instance(); |
||||
return function(item) { |
||||
instance.tabBarData.set({ |
||||
sound: instance.sounds.get().find((sound) => sound._id === item._id), |
||||
onSuccess: instance.onSuccessCallback, |
||||
}); |
||||
instance.tabBar.showGroup('custom-sounds-selected'); |
||||
instance.tabBar.open('admin-sound-info'); |
||||
}; |
||||
}, |
||||
}); |
||||
|
||||
Template.adminSounds.onCreated(function() { |
||||
const instance = this; |
||||
this.sounds = new ReactiveVar([]); |
||||
this.offset = new ReactiveVar(0); |
||||
this.total = new ReactiveVar(0); |
||||
this.query = new ReactiveVar({}); |
||||
this.isLoading = new ReactiveVar(false); |
||||
this.filter = new ReactiveVar(''); |
||||
this.isPlayingId = new ReactiveVar(''); |
||||
|
||||
this.tabBar = new RocketChatTabBar(); |
||||
this.tabBar.showGroup(FlowRouter.current().route.name); |
||||
this.tabBarData = new ReactiveVar(); |
||||
|
||||
TabBar.addButton({ |
||||
groups: ['custom-sounds', 'custom-sounds-selected'], |
||||
id: 'add-sound', |
||||
i18nTitle: 'Custom_Sound_Add', |
||||
icon: 'plus', |
||||
template: 'adminSoundEdit', |
||||
order: 1, |
||||
}); |
||||
|
||||
TabBar.addButton({ |
||||
groups: ['custom-sounds-selected'], |
||||
id: 'admin-sound-info', |
||||
i18nTitle: 'Custom_Sound_Info', |
||||
icon: 'customize', |
||||
template: 'adminSoundInfo', |
||||
order: 2, |
||||
}); |
||||
|
||||
this.onSuccessCallback = () => { |
||||
this.offset.set(0); |
||||
return this.loadSounds(this.query.get(), this.offset.get()); |
||||
}; |
||||
|
||||
this.tabBarData.set({ |
||||
onSuccess: instance.onSuccessCallback, |
||||
}); |
||||
|
||||
this.loadSounds = _.debounce(async (query, offset) => { |
||||
this.isLoading.set(true); |
||||
const { sounds, total } = await APIClient.v1.get(`custom-sounds.list?count=${ LIST_SIZE }&offset=${ offset }&query=${ JSON.stringify(query) }`); |
||||
this.total.set(total); |
||||
if (offset === 0) { |
||||
this.sounds.set(sounds); |
||||
} else { |
||||
this.sounds.set(this.sounds.get().concat(sounds)); |
||||
} |
||||
this.isLoading.set(false); |
||||
}, DEBOUNCE_TIME_TO_SEARCH_IN_MS); |
||||
|
||||
this.autorun(() => { |
||||
const filter = this.filter.get() && this.filter.get().trim(); |
||||
const offset = this.offset.get(); |
||||
if (filter) { |
||||
const regex = { $regex: filter, $options: 'i' }; |
||||
return this.loadSounds({ name: regex }, offset); |
||||
} |
||||
return this.loadSounds({}, offset); |
||||
}); |
||||
}); |
||||
|
||||
Template.adminSounds.onRendered(() => |
||||
Tracker.afterFlush(function() { |
||||
SideNav.setFlex('adminFlex'); |
||||
SideNav.openFlex(); |
||||
}), |
||||
); |
||||
|
||||
Template.adminSounds.events({ |
||||
'keydown #sound-filter'(e) { |
||||
// stop enter key
|
||||
if (e.which === 13) { |
||||
e.stopPropagation(); |
||||
e.preventDefault(); |
||||
} |
||||
}, |
||||
'keyup #sound-filter'(e, t) { |
||||
e.stopPropagation(); |
||||
e.preventDefault(); |
||||
t.filter.set(e.currentTarget.value); |
||||
t.offset.set(0); |
||||
}, |
||||
'click .icon-play-circled'(e, t) { |
||||
e.preventDefault(); |
||||
e.stopPropagation(); |
||||
CustomSounds.play(this._id); |
||||
const audio = document.getElementById(t.isPlayingId.get()); |
||||
if (audio) { |
||||
audio.pause(); |
||||
} |
||||
document.getElementById(this._id).onended = () => { |
||||
t.isPlayingId.set(''); |
||||
this.onended = null; |
||||
}; |
||||
t.isPlayingId.set(this._id); |
||||
}, |
||||
'click .icon-pause-circled'(e, t) { |
||||
e.preventDefault(); |
||||
e.stopPropagation(); |
||||
const audio = document.getElementById(this._id); |
||||
if (audio && !audio.paused) { |
||||
audio.pause(); |
||||
} |
||||
t.isPlayingId.set(''); |
||||
}, |
||||
'click .icon-reset-circled'(e) { |
||||
e.preventDefault(); |
||||
e.stopPropagation(); |
||||
const audio = document.getElementById(this._id); |
||||
if (audio) { |
||||
audio.currentTime = 0; |
||||
} |
||||
}, |
||||
}); |
||||
@ -1,11 +0,0 @@ |
||||
import { BlazeLayout } from 'meteor/kadira:blaze-layout'; |
||||
|
||||
import { registerAdminRoute } from '../../../ui-admin/client'; |
||||
|
||||
registerAdminRoute('/custom-sounds', { |
||||
name: 'custom-sounds', |
||||
async action(/* params*/) { |
||||
await import('./views'); |
||||
BlazeLayout.render('main', { center: 'adminSounds' }); |
||||
}, |
||||
}); |
||||
@ -1,25 +0,0 @@ |
||||
<template name="soundEdit"> |
||||
{{#requiresPermission 'manage-sounds'}} |
||||
<div class="about clearfix"> |
||||
<form class="edit-form" autocomplete="off"> |
||||
{{#if sound}} |
||||
<h3>{{sound.name}}</h3> |
||||
{{else}} |
||||
<h3>{{_ "Custom_Sound_Add"}}</h3> |
||||
{{/if}} |
||||
<div class="input-line"> |
||||
<label for="name">{{_ "Name"}}</label> |
||||
<input type="text" id="name" autocomplete="off" value="{{sound.name}}"> |
||||
</div> |
||||
<div class="input-line"> |
||||
<label for="image">{{_ "Sound_File_mp3"}}</label> |
||||
<input id="image" type="file" accept="audio/mp3,audio/mpeg,audio/x-mpeg,audio/mpeg3,audio/x-mpeg-3,.mp3"/> |
||||
</div> |
||||
<nav> |
||||
<button class='button button-block cancel' type="button"><span>{{_ "Cancel"}}</span></button> |
||||
<button class='button button-block primary save'><span>{{_ "Save"}}</span></button> |
||||
</nav> |
||||
</form> |
||||
</div> |
||||
{{/requiresPermission}} |
||||
</template> |
||||
@ -1,155 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { Template } from 'meteor/templating'; |
||||
import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; |
||||
import toastr from 'toastr'; |
||||
import s from 'underscore.string'; |
||||
|
||||
import { t, handleError } from '../../../utils'; |
||||
|
||||
Template.soundEdit.helpers({ |
||||
sound() { |
||||
return Template.instance().sound; |
||||
}, |
||||
|
||||
name() { |
||||
return this.name || this._id; |
||||
}, |
||||
}); |
||||
|
||||
Template.soundEdit.events({ |
||||
'click .cancel'(e, t) { |
||||
e.stopPropagation(); |
||||
e.preventDefault(); |
||||
delete Template.instance().soundFile; |
||||
t.cancel(t.find('form')); |
||||
}, |
||||
|
||||
'submit form'(e, t) { |
||||
e.stopPropagation(); |
||||
e.preventDefault(); |
||||
t.save(e.currentTarget); |
||||
}, |
||||
|
||||
'change input[type=file]'(ev) { |
||||
const e = ev.originalEvent != null ? ev.originalEvent : ev; |
||||
let { files } = e.target; |
||||
if (e.target.files == null || files.length === 0) { |
||||
if (e.dataTransfer.files != null) { |
||||
files = e.dataTransfer.files; |
||||
} else { |
||||
files = []; |
||||
} |
||||
} |
||||
|
||||
// using let x of y here seems to have incompatibility with some phones
|
||||
for (const file in files) { |
||||
if (files.hasOwnProperty(file)) { |
||||
Template.instance().soundFile = files[file]; |
||||
} |
||||
} |
||||
}, |
||||
}); |
||||
|
||||
Template.soundEdit.onCreated(function() { |
||||
if (this.data != null) { |
||||
this.sound = this.data.sound; |
||||
} else { |
||||
this.sound = undefined; |
||||
this.data.tabBar.showGroup('custom-sounds'); |
||||
} |
||||
this.onSuccess = Template.currentData().onSuccess; |
||||
this.cancel = (form, name) => { |
||||
form.reset(); |
||||
this.data.tabBar.close(); |
||||
if (this.sound) { |
||||
this.data.back(name); |
||||
} |
||||
}; |
||||
|
||||
this.getSoundData = () => { |
||||
const soundData = {}; |
||||
if (this.sound != null) { |
||||
soundData._id = this.sound._id; |
||||
soundData.previousName = this.sound.name; |
||||
soundData.extension = this.sound.extension; |
||||
soundData.previousExtension = this.sound.extension; |
||||
} |
||||
soundData.name = s.trim(this.$('#name').val()); |
||||
soundData.newFile = false; |
||||
return soundData; |
||||
}; |
||||
|
||||
this.validate = () => { |
||||
const soundData = this.getSoundData(); |
||||
|
||||
const errors = []; |
||||
if (!soundData.name) { |
||||
errors.push('Name'); |
||||
} |
||||
|
||||
if (!soundData._id) { |
||||
if (!this.soundFile) { |
||||
errors.push('Sound_File_mp3'); |
||||
} |
||||
} |
||||
|
||||
for (const error of errors) { |
||||
toastr.error(TAPi18n.__('error-the-field-is-required', { field: TAPi18n.__(error) })); |
||||
} |
||||
|
||||
if (this.soundFile) { |
||||
if (!/audio\/mp3/.test(this.soundFile.type) && !/audio\/mpeg/.test(this.soundFile.type) && !/audio\/x-mpeg/.test(this.soundFile.type)) { |
||||
errors.push('FileType'); |
||||
toastr.error(TAPi18n.__('error-invalid-file-type')); |
||||
} |
||||
} |
||||
|
||||
return errors.length === 0; |
||||
}; |
||||
|
||||
this.save = (form) => { |
||||
if (this.validate()) { |
||||
const soundData = this.getSoundData(); |
||||
|
||||
if (this.soundFile) { |
||||
soundData.newFile = true; |
||||
soundData.extension = this.soundFile.name.split('.').pop(); |
||||
soundData.type = this.soundFile.type; |
||||
} |
||||
|
||||
Meteor.call('insertOrUpdateSound', soundData, (error, result) => { |
||||
if (result) { |
||||
soundData._id = result; |
||||
soundData.random = Math.round(Math.random() * 1000); |
||||
|
||||
if (this.soundFile) { |
||||
toastr.info(TAPi18n.__('Uploading_file')); |
||||
|
||||
const reader = new FileReader(); |
||||
reader.readAsBinaryString(this.soundFile); |
||||
reader.onloadend = () => { |
||||
Meteor.call('uploadCustomSound', reader.result, this.soundFile.type, soundData, (uploadError/* , data*/) => { |
||||
if (uploadError != null) { |
||||
handleError(uploadError); |
||||
console.log(uploadError); |
||||
} |
||||
}, |
||||
); |
||||
delete this.soundFile; |
||||
toastr.success(TAPi18n.__('File_uploaded')); |
||||
}; |
||||
} |
||||
|
||||
toastr.success(t('Custom_Sound_Saved_Successfully')); |
||||
this.onSuccess(); |
||||
|
||||
this.cancel(form, soundData.name); |
||||
} |
||||
|
||||
if (error) { |
||||
handleError(error); |
||||
} |
||||
}); |
||||
} |
||||
}; |
||||
}); |
||||
@ -1,19 +0,0 @@ |
||||
<template name="soundInfo"> |
||||
{{#if editingSound}} |
||||
{{> soundEdit (soundToEdit)}} |
||||
{{else}} |
||||
{{#with sound}} |
||||
<div class="about clearfix"> |
||||
<div class="info"> |
||||
<h3 title="{{name}}">{{name}}</h3> |
||||
</div> |
||||
</div> |
||||
{{/with}} |
||||
<nav> |
||||
{{#if hasPermission 'manage-sounds'}} |
||||
<button class='button button-block danger delete'><span><i class='icon-trash'></i> {{_ "Delete"}}</span></button> |
||||
<button class='button button-block primary edit-sound'><span><i class='icon-edit'></i> {{_ "Edit"}}</span></button> |
||||
{{/if}} |
||||
</nav> |
||||
{{/if}} |
||||
</template> |
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue