[NEW] Rewrite admin pages (#17388)
Co-authored-by: Maria Eduarda Cunha <42151808+mariaeduardacunha@users.noreply.github.com> Co-authored-by: Tasso Evangelista <tasso.evangelista@rocket.chat> Co-authored-by: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Co-authored-by: Gabriel Henriques <gabriel.henriques@rocket.chat> Co-authored-by: Martin Schoeler <martin.schoeler@rocket.chat>pull/17533/head^2
parent
0b1b0c27bc
commit
ca665fae2a
@ -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,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> |
||||
@ -1,118 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { ReactiveVar } from 'meteor/reactive-var'; |
||||
import { Template } from 'meteor/templating'; |
||||
|
||||
import { modal } from '../../../ui-utils'; |
||||
import { t, handleError } from '../../../utils'; |
||||
|
||||
Template.soundInfo.helpers({ |
||||
name() { |
||||
const sound = Template.instance().sound.get(); |
||||
return sound.name; |
||||
}, |
||||
|
||||
sound() { |
||||
return Template.instance().sound.get(); |
||||
}, |
||||
|
||||
editingSound() { |
||||
return Template.instance().editingSound.get(); |
||||
}, |
||||
|
||||
soundToEdit() { |
||||
const instance = Template.instance(); |
||||
return { |
||||
tabBar: instance.data.tabBar, |
||||
data: instance.data.data, |
||||
sound: instance.sound.get(), |
||||
onSuccess: instance.onSuccess, |
||||
back(name) { |
||||
instance.editingSound.set(); |
||||
|
||||
if (name != null) { |
||||
const sound = instance.sound.get(); |
||||
if (sound.name != null && sound.name !== name) { |
||||
return instance.loadedName.set(name); |
||||
} |
||||
} |
||||
}, |
||||
}; |
||||
}, |
||||
}); |
||||
|
||||
Template.soundInfo.events({ |
||||
'click .delete'(e, instance) { |
||||
e.stopPropagation(); |
||||
e.preventDefault(); |
||||
const sound = instance.sound.get(); |
||||
if (sound != null) { |
||||
const { _id } = sound; |
||||
modal.open({ |
||||
title: t('Are_you_sure'), |
||||
text: t('Custom_Sound_Delete_Warning'), |
||||
type: 'warning', |
||||
showCancelButton: true, |
||||
confirmButtonColor: '#DD6B55', |
||||
confirmButtonText: t('Yes_delete_it'), |
||||
cancelButtonText: t('Cancel'), |
||||
closeOnConfirm: false, |
||||
html: false, |
||||
}, function() { |
||||
Meteor.call('deleteCustomSound', _id, (error/* , result*/) => { |
||||
if (error) { |
||||
handleError(error); |
||||
} else { |
||||
modal.open({ |
||||
title: t('Deleted'), |
||||
text: t('Custom_Sound_Has_Been_Deleted'), |
||||
type: 'success', |
||||
timer: 2000, |
||||
showConfirmButton: false, |
||||
}); |
||||
instance.onSuccess(); |
||||
instance.data.tabBar.showGroup('custom-sounds'); |
||||
instance.data.tabBar.close(); |
||||
} |
||||
}); |
||||
}); |
||||
} |
||||
}, |
||||
|
||||
'click .edit-sound'(e, instance) { |
||||
e.stopPropagation(); |
||||
e.preventDefault(); |
||||
|
||||
instance.editingSound.set(instance.sound.get()._id); |
||||
}, |
||||
}); |
||||
|
||||
Template.soundInfo.onCreated(function() { |
||||
this.sound = new ReactiveVar(); |
||||
|
||||
this.editingSound = new ReactiveVar(); |
||||
|
||||
this.loadedName = new ReactiveVar(); |
||||
this.onSuccess = Template.currentData().onSuccess; |
||||
|
||||
this.autorun(() => { |
||||
const data = Template.currentData(); |
||||
if (data && data.clear != null) { |
||||
this.clear = data.clear; |
||||
} |
||||
}); |
||||
|
||||
this.autorun(() => { |
||||
const data = Template.currentData().sound; |
||||
const sound = this.sound.get(); |
||||
if (sound && sound.name != null) { |
||||
this.loadedName.set(sound.name); |
||||
} else if (data.name != null) { |
||||
this.loadedName.set(data.name); |
||||
} |
||||
}); |
||||
|
||||
this.autorun(() => { |
||||
const data = Template.currentData().sound; |
||||
this.sound.set(data); |
||||
}); |
||||
}); |
||||
@ -1,11 +0,0 @@ |
||||
import { hasAtLeastOnePermission } from '../../../authorization'; |
||||
import { registerAdminSidebarItem } from '../../../ui-admin/client'; |
||||
|
||||
registerAdminSidebarItem({ |
||||
href: 'custom-sounds', |
||||
i18nLabel: 'Custom_Sounds', |
||||
icon: 'volume', |
||||
permissionGranted() { |
||||
return hasAtLeastOnePermission(['manage-sounds']); |
||||
}, |
||||
}); |
||||
@ -1,8 +0,0 @@ |
||||
import './adminSoundEdit.html'; |
||||
import './adminSoundInfo.html'; |
||||
import './adminSounds.html'; |
||||
import './adminSounds'; |
||||
import './soundEdit.html'; |
||||
import './soundEdit'; |
||||
import './soundInfo.html'; |
||||
import './soundInfo'; |
||||
@ -1,7 +1,4 @@ |
||||
import './notifications/deleteCustomSound'; |
||||
import './notifications/updateCustomSound'; |
||||
import './admin/route'; |
||||
import './admin/startup'; |
||||
import '../assets/stylesheets/customSoundsAdmin.css'; |
||||
|
||||
export { CustomSounds } from './lib/CustomSounds'; |
||||
|
||||
@ -1,132 +0,0 @@ |
||||
.emojiAdminPreview { |
||||
position: relative; |
||||
|
||||
overflow: hidden; |
||||
|
||||
width: 100%; |
||||
height: 100%; |
||||
|
||||
border-radius: 4px; |
||||
|
||||
& .emojiAdminPreview-image { |
||||
position: relative; |
||||
|
||||
display: block; |
||||
|
||||
width: 100%; |
||||
min-width: 20px; |
||||
height: 100%; |
||||
min-height: 20px; |
||||
|
||||
border-radius: 4px; |
||||
background-repeat: no-repeat; |
||||
background-position: center; |
||||
background-size: contain; |
||||
} |
||||
} |
||||
|
||||
.emoji-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,76 +0,0 @@ |
||||
<template name="adminEmoji"> |
||||
<div class="main-content-flex"> |
||||
<section class="page-container page-list flex-tab-main-content"> |
||||
{{> header sectionName="Custom_emoji"}} |
||||
<div class="content"> |
||||
{{#unless hasPermission 'manage-emoji'}} |
||||
<p>{{_ "You_are_not_authorized_to_view_this_page"}}</p> |
||||
{{else}} |
||||
<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="emoji-filter" type="text" class="rc-input__element" |
||||
placeholder="{{_ "Search"}}" autofocus dir="auto"> |
||||
</div> |
||||
</form> |
||||
<div class="results"> |
||||
{{{_ "Showing_results" customemoji.length}}} |
||||
</div> |
||||
{{#table fixed='true' onItemClick=onTableItemClick onScroll=onTableScroll onResize=onTableResize}} |
||||
<thead> |
||||
<tr> |
||||
<th width="50%"> |
||||
<div class="table-fake-th">{{_ "Name"}}</div> |
||||
</th> |
||||
<th width="50%"> |
||||
<div class="table-fake-th">{{_ "Aliases"}}</div> |
||||
</th> |
||||
</tr> |
||||
</thead> |
||||
<tbody> |
||||
{{#each customemoji}} |
||||
<tr> |
||||
<td width="50%"> |
||||
<div class="rc-table-wrapper"> |
||||
<div class="rc-table-info"> |
||||
<span class="rc-table-title"> |
||||
{{name}} |
||||
</span> |
||||
</div> |
||||
</div> |
||||
</td> |
||||
<td width="50%"> |
||||
<div class="rc-table-wrapper"> |
||||
<div class="rc-table-info"> |
||||
<span class="rc-table-title"> |
||||
{{aliases}} |
||||
</span> |
||||
</div> |
||||
</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}} |
||||
{{/unless}} |
||||
</div> |
||||
</section> |
||||
{{#with flexData}} |
||||
{{> flexTabBar}} |
||||
{{/with}} |
||||
</div> |
||||
</template> |
||||
@ -1,141 +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 { APIClient } from '../../../utils/client'; |
||||
|
||||
const LIST_SIZE = 50; |
||||
const DEBOUNCE_TIME_TO_SEARCH_IN_MS = 500; |
||||
|
||||
Template.adminEmoji.helpers({ |
||||
searchText() { |
||||
const instance = Template.instance(); |
||||
return instance.filter && instance.filter.get(); |
||||
}, |
||||
customemoji() { |
||||
return Template.instance().emojis.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 emojis = instance.emojis.get(); |
||||
if (instance.total.get() > emojis.length) { |
||||
instance.offset.set(instance.offset.get() + LIST_SIZE); |
||||
} |
||||
}; |
||||
}, |
||||
onTableItemClick() { |
||||
const instance = Template.instance(); |
||||
return function({ _id }) { |
||||
instance.tabBarData.set({ |
||||
emoji: instance.emojis.get().find((emoji) => emoji._id === _id), |
||||
onSuccess: instance.onSuccessCallback, |
||||
}); |
||||
instance.tabBar.open('admin-emoji-info'); |
||||
}; |
||||
}, |
||||
}); |
||||
|
||||
Template.adminEmoji.onCreated(async function() { |
||||
const instance = this; |
||||
this.emojis = new ReactiveVar([]); |
||||
this.offset = new ReactiveVar(0); |
||||
this.total = new ReactiveVar(0); |
||||
this.filter = new ReactiveVar(''); |
||||
this.query = new ReactiveVar({}); |
||||
this.isLoading = new ReactiveVar(false); |
||||
|
||||
this.tabBar = new RocketChatTabBar(); |
||||
this.tabBar.showGroup(FlowRouter.current().route.name); |
||||
this.tabBarData = new ReactiveVar(); |
||||
|
||||
TabBar.addButton({ |
||||
groups: ['emoji-custom'], |
||||
id: 'add-emoji', |
||||
i18nTitle: 'Custom_Emoji_Add', |
||||
icon: 'plus', |
||||
template: 'adminEmojiEdit', |
||||
order: 1, |
||||
}); |
||||
|
||||
TabBar.addButton({ |
||||
groups: ['emoji-custom'], |
||||
id: 'admin-emoji-info', |
||||
i18nTitle: 'Custom_Emoji_Info', |
||||
icon: 'customize', |
||||
template: 'adminEmojiInfo', |
||||
order: 2, |
||||
}); |
||||
this.onSuccessCallback = () => { |
||||
this.offset.set(0); |
||||
return this.loadEmojis(this.query.get(), this.offset.get()); |
||||
}; |
||||
this.tabBarData.set({ |
||||
onSuccess: instance.onSuccessCallback, |
||||
}); |
||||
|
||||
this.loadEmojis = _.debounce(async (query, offset) => { |
||||
this.isLoading.set(true); |
||||
const { emojis, total } = await APIClient.v1.get(`emoji-custom.all?count=${ LIST_SIZE }&offset=${ offset }&query=${ JSON.stringify(query) }`); |
||||
this.total.set(total); |
||||
if (offset === 0) { |
||||
this.emojis.set(emojis); |
||||
} else { |
||||
this.emojis.set(this.emojis.get().concat(emojis)); |
||||
} |
||||
this.isLoading.set(false); |
||||
}, DEBOUNCE_TIME_TO_SEARCH_IN_MS); |
||||
|
||||
this.autorun(() => { |
||||
this.filter.get(); |
||||
this.offset.set(0); |
||||
}); |
||||
|
||||
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.loadEmojis({ $or: [{ name: regex }, { aliases: regex }] }, offset); |
||||
} |
||||
return this.loadEmojis({}, offset); |
||||
}); |
||||
}); |
||||
|
||||
Template.adminEmoji.onRendered(() => |
||||
Tracker.afterFlush(function() { |
||||
SideNav.setFlex('adminFlex'); |
||||
SideNav.openFlex(); |
||||
}), |
||||
); |
||||
|
||||
Template.adminEmoji.events({ |
||||
'keydown #emoji-filter'(e) { |
||||
// stop enter key
|
||||
if (e.which === 13) { |
||||
e.stopPropagation(); |
||||
e.preventDefault(); |
||||
} |
||||
}, |
||||
|
||||
'keyup #emoji-filter'(e, t) { |
||||
e.stopPropagation(); |
||||
e.preventDefault(); |
||||
t.filter.set(e.currentTarget.value); |
||||
}, |
||||
}); |
||||
@ -1,7 +0,0 @@ |
||||
<template name="adminEmojiEdit"> |
||||
<div class="content"> |
||||
<div class="emoji-view"> |
||||
{{> emojiEdit .}} |
||||
</div> |
||||
</div> |
||||
</template> |
||||
@ -1,7 +0,0 @@ |
||||
<template name="adminEmojiInfo"> |
||||
<div class="content"> |
||||
<div class="emoji-view"> |
||||
{{> emojiInfo .}} |
||||
</div> |
||||
</div> |
||||
</template> |
||||
@ -1,31 +0,0 @@ |
||||
<template name="emojiEdit"> |
||||
{{#unless hasPermission 'manage-emoji'}} |
||||
<p>{{_ "You_are_not_authorized_to_view_this_page"}}</p> |
||||
{{else}} |
||||
<div class="about clearfix"> |
||||
<form class="edit-form" autocomplete="off"> |
||||
{{#if emoji}} |
||||
<h3>{{emoji.name}}</h3> |
||||
{{else}} |
||||
<h3>{{_ "Custom_Emoji_Add"}}</h3> |
||||
{{/if}} |
||||
<div class="input-line"> |
||||
<label for="name">{{_ "Name"}}</label> |
||||
<input type="text" id="name" autocomplete="off" value="{{emoji.name}}" class="content-background-color"> |
||||
</div> |
||||
<div class="input-line"> |
||||
<label for="aliases">{{_ "Aliases"}}</label> |
||||
<input type="text" id="aliases" autocomplete="off" value="{{emoji.aliases}}" class="content-background-color"> |
||||
</div> |
||||
<div class="input-line"> |
||||
<label for="image">{{_ "Image"}}</label> |
||||
<input id="image" type="file" /> |
||||
</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> |
||||
{{/unless}} |
||||
</template> |
||||
@ -1,158 +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.emojiEdit.helpers({ |
||||
emoji() { |
||||
return Template.instance().emoji; |
||||
}, |
||||
|
||||
name() { |
||||
return this.name || this._id; |
||||
}, |
||||
}); |
||||
|
||||
Template.emojiEdit.events({ |
||||
'click .cancel'(e, t) { |
||||
e.stopPropagation(); |
||||
e.preventDefault(); |
||||
delete Template.instance().emojiFile; |
||||
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 (files == null || files.length === 0) { |
||||
if (e.dataTransfer != null && 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().emojiFile = files[file]; |
||||
} |
||||
} |
||||
}, |
||||
}); |
||||
|
||||
Template.emojiEdit.onCreated(function() { |
||||
if (this.data != null) { |
||||
this.emoji = this.data.emoji; |
||||
} else { |
||||
this.emoji = undefined; |
||||
} |
||||
|
||||
this.tabBar = Template.currentData().tabBar; |
||||
this.onSuccess = Template.currentData().onSuccess; |
||||
|
||||
this.cancel = (form, name) => { |
||||
form.reset(); |
||||
this.tabBar.close(); |
||||
if (this.emoji) { |
||||
this.data.back(name); |
||||
} |
||||
}; |
||||
|
||||
this.getEmojiData = () => { |
||||
const emojiData = {}; |
||||
if (this.emoji != null) { |
||||
emojiData._id = this.emoji._id; |
||||
emojiData.previousName = this.emoji.name; |
||||
emojiData.extension = this.emoji.extension; |
||||
emojiData.previousExtension = this.emoji.extension; |
||||
} |
||||
emojiData.name = s.trim(this.$('#name').val()); |
||||
emojiData.aliases = s.trim(this.$('#aliases').val()); |
||||
emojiData.newFile = false; |
||||
return emojiData; |
||||
}; |
||||
|
||||
this.validate = () => { |
||||
const emojiData = this.getEmojiData(); |
||||
|
||||
const errors = []; |
||||
if (!emojiData.name) { |
||||
errors.push('Name'); |
||||
} |
||||
|
||||
if (!emojiData._id) { |
||||
if (!this.emojiFile) { |
||||
errors.push('Image'); |
||||
} |
||||
} |
||||
|
||||
for (const error of errors) { |
||||
toastr.error(TAPi18n.__('error-the-field-is-required', { field: TAPi18n.__(error) })); |
||||
} |
||||
|
||||
if (this.emojiFile) { |
||||
if (!/image\/.+/.test(this.emojiFile.type)) { |
||||
errors.push('FileType'); |
||||
toastr.error(TAPi18n.__('error-invalid-file-type')); |
||||
} |
||||
} |
||||
|
||||
return errors.length === 0; |
||||
}; |
||||
|
||||
this.save = (form) => { |
||||
if (this.validate()) { |
||||
const emojiData = this.getEmojiData(); |
||||
|
||||
if (this.emojiFile) { |
||||
emojiData.newFile = true; |
||||
emojiData.extension = this.emojiFile.name.split('.').pop(); |
||||
} |
||||
|
||||
Meteor.call('insertOrUpdateEmoji', emojiData, (error, result) => { |
||||
if (result) { |
||||
if (this.emojiFile) { |
||||
toastr.info(TAPi18n.__('Uploading_file')); |
||||
|
||||
const reader = new FileReader(); |
||||
reader.readAsBinaryString(this.emojiFile); |
||||
reader.onloadend = () => { |
||||
Meteor.call('uploadEmojiCustom', reader.result, this.emojiFile.type, emojiData, (uploadError/* , data*/) => { |
||||
if (uploadError != null) { |
||||
handleError(uploadError); |
||||
console.log(uploadError); |
||||
} |
||||
}, |
||||
); |
||||
delete this.emojiFile; |
||||
toastr.success(TAPi18n.__('File_uploaded')); |
||||
}; |
||||
} |
||||
|
||||
if (emojiData._id) { |
||||
toastr.success(t('Custom_Emoji_Updated_Successfully')); |
||||
} else { |
||||
toastr.success(t('Custom_Emoji_Added_Successfully')); |
||||
} |
||||
this.onSuccess(); |
||||
|
||||
this.cancel(form, emojiData.name); |
||||
} |
||||
|
||||
if (error) { |
||||
handleError(error); |
||||
} |
||||
}); |
||||
} |
||||
}; |
||||
}); |
||||
@ -1,23 +0,0 @@ |
||||
<template name="emojiInfo"> |
||||
{{#if editingEmoji}} |
||||
{{> emojiEdit (emojiToEdit)}} |
||||
{{else}} |
||||
{{#with emoji}} |
||||
<div class="about clearfix"> |
||||
<div class="thumb"> |
||||
{{> emojiPreview name=name extension=extension}} |
||||
</div> |
||||
<div class="info"> |
||||
<h3 title="{{name}}">{{name}}</h3> |
||||
<h3 title="{{aliases}}">{{aliases}}</h3> |
||||
</div> |
||||
</div> |
||||
{{/with}} |
||||
<nav> |
||||
{{#if hasPermission 'manage-emoji'}} |
||||
<button class='button button-block danger delete'><span><i class='icon-trash'></i> {{_ "Delete"}}</span></button> |
||||
<button class='button button-block primary edit-emoji'><span><i class='icon-edit'></i> {{_ "Edit"}}</span></button> |
||||
{{/if}} |
||||
</nav> |
||||
{{/if}} |
||||
</template> |
||||
@ -1,126 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { ReactiveVar } from 'meteor/reactive-var'; |
||||
import { Template } from 'meteor/templating'; |
||||
|
||||
import { handleError, t } from '../../../utils'; |
||||
import { modal } from '../../../ui-utils'; |
||||
|
||||
Template.emojiInfo.helpers({ |
||||
name() { |
||||
const emoji = Template.instance().emoji.get(); |
||||
return emoji.name; |
||||
}, |
||||
|
||||
aliases() { |
||||
const emoji = Template.instance().emoji.get(); |
||||
return emoji.aliases; |
||||
}, |
||||
|
||||
emoji() { |
||||
return Template.instance().emoji.get(); |
||||
}, |
||||
|
||||
editingEmoji() { |
||||
return Template.instance().editingEmoji.get(); |
||||
}, |
||||
|
||||
emojiToEdit() { |
||||
const instance = Template.instance(); |
||||
return { |
||||
tabBar: this.tabBar, |
||||
emoji: instance.emoji.get(), |
||||
onSuccess: instance.onSuccess, |
||||
back(name) { |
||||
instance.editingEmoji.set(); |
||||
|
||||
if (name != null) { |
||||
const emoji = instance.emoji.get(); |
||||
if (emoji != null && emoji.name != null && emoji.name !== name) { |
||||
return instance.loadedName.set(name); |
||||
} |
||||
} |
||||
}, |
||||
}; |
||||
}, |
||||
}); |
||||
|
||||
Template.emojiInfo.events({ |
||||
'click .thumb'(e) { |
||||
$(e.currentTarget).toggleClass('bigger'); |
||||
}, |
||||
|
||||
'click .delete'(e, instance) { |
||||
e.stopPropagation(); |
||||
e.preventDefault(); |
||||
const emoji = instance.emoji.get(); |
||||
if (emoji != null) { |
||||
const { _id } = emoji; |
||||
modal.open({ |
||||
title: t('Are_you_sure'), |
||||
text: t('Custom_Emoji_Delete_Warning'), |
||||
type: 'warning', |
||||
showCancelButton: true, |
||||
confirmButtonColor: '#DD6B55', |
||||
confirmButtonText: t('Yes_delete_it'), |
||||
cancelButtonText: t('Cancel'), |
||||
closeOnConfirm: false, |
||||
html: false, |
||||
}, function() { |
||||
Meteor.call('deleteEmojiCustom', _id, (error/* , result*/) => { |
||||
if (error) { |
||||
return handleError(error); |
||||
} |
||||
modal.open({ |
||||
title: t('Deleted'), |
||||
text: t('Custom_Emoji_Has_Been_Deleted'), |
||||
type: 'success', |
||||
timer: 2000, |
||||
showConfirmButton: false, |
||||
}); |
||||
instance.onSuccess(); |
||||
|
||||
instance.tabBar.close(); |
||||
}); |
||||
}); |
||||
} |
||||
}, |
||||
|
||||
'click .edit-emoji'(e, instance) { |
||||
e.stopPropagation(); |
||||
e.preventDefault(); |
||||
instance.editingEmoji.set(instance.emoji.get()._id); |
||||
}, |
||||
}); |
||||
|
||||
Template.emojiInfo.onCreated(function() { |
||||
this.emoji = new ReactiveVar(); |
||||
this.onSuccess = Template.currentData().onSuccess; |
||||
|
||||
this.editingEmoji = new ReactiveVar(); |
||||
|
||||
this.loadedName = new ReactiveVar(); |
||||
|
||||
this.tabBar = Template.currentData().tabBar; |
||||
|
||||
this.autorun(() => { |
||||
const data = Template.currentData(); |
||||
if (data != null && data.clear != null) { |
||||
this.clear = data.clear; |
||||
} |
||||
}); |
||||
|
||||
this.autorun(() => { |
||||
const data = Template.currentData().emoji; |
||||
const emoji = this.emoji.get(); |
||||
if (emoji != null && emoji.name != null) { |
||||
this.loadedName.set(emoji.name); |
||||
} else if (data != null && data.name != null) { |
||||
this.loadedName.set(data.name); |
||||
} |
||||
}); |
||||
|
||||
this.autorun(() => { |
||||
const data = Template.currentData().emoji; |
||||
this.emoji.set(data); |
||||
}); |
||||
}); |
||||
@ -1,5 +0,0 @@ |
||||
<template name="emojiPreview"> |
||||
<div class="emojiAdminPreview"> |
||||
<div class="emojiAdminPreview-image" data-emoji="{{this.name}}" style="background-image:url('{{emojiUrlFromName this.name this.extension}}');"></div> |
||||
</div> |
||||
</template> |
||||
@ -1,11 +0,0 @@ |
||||
import { BlazeLayout } from 'meteor/kadira:blaze-layout'; |
||||
|
||||
import { registerAdminRoute } from '../../../ui-admin/client'; |
||||
|
||||
registerAdminRoute('/emoji-custom', { |
||||
name: 'emoji-custom', |
||||
async action(/* params*/) { |
||||
await import('./views'); |
||||
BlazeLayout.render('main', { center: 'adminEmoji' }); |
||||
}, |
||||
}); |
||||
@ -1,9 +0,0 @@ |
||||
import './adminEmoji.html'; |
||||
import './adminEmoji'; |
||||
import './adminEmojiEdit.html'; |
||||
import './adminEmojiInfo.html'; |
||||
import './emojiEdit.html'; |
||||
import './emojiEdit'; |
||||
import './emojiInfo.html'; |
||||
import './emojiInfo'; |
||||
import './emojiPreview.html'; |
||||
@ -1,35 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
|
||||
import { CachedCollectionManager } from '../../ui-cached-collection'; |
||||
|
||||
class ImporterWebsocketReceiverDef { |
||||
constructor() { |
||||
this.streamer = new Meteor.Streamer('importers'); |
||||
|
||||
this.callbacks = []; |
||||
CachedCollectionManager.onLogin(() => { |
||||
this.streamer.on('progress', this.progressUpdated.bind(this)); |
||||
}); |
||||
} |
||||
|
||||
progressUpdated(progress) { |
||||
this.callbacks.forEach((c) => c(progress)); |
||||
} |
||||
|
||||
registerCallback(callback) { |
||||
if (typeof callback !== 'function') { |
||||
throw new Error('Callback must be a function.'); |
||||
} |
||||
|
||||
this.callbacks.push(callback); |
||||
} |
||||
|
||||
unregisterCallback(callback) { |
||||
const i = this.callbacks.indexOf(callback); |
||||
if (i >= 0) { |
||||
this.callbacks.splice(i, 1); |
||||
} |
||||
} |
||||
} |
||||
|
||||
export const ImporterWebsocketReceiver = new ImporterWebsocketReceiverDef(); |
||||
@ -1,115 +0,0 @@ |
||||
<template name="importOperationSummary"> |
||||
<span style="font-weight: 600;">{{_ "Import_Type"}}:</span> |
||||
<span>{{_ type}} [{{ importerKey}}]</span> |
||||
<br/> |
||||
|
||||
<span style="font-weight: 600;">{{_ "Last_Updated"}}:</span> |
||||
<span>{{ lastUpdated}}</span> |
||||
<br/> |
||||
|
||||
{{#if valid}} |
||||
<span style="font-weight: 600;">{{_ "Status"}}:</span> |
||||
{{else}} |
||||
<span style="font-weight: 600;">{{_ "Last_Status"}}:</span> |
||||
{{/if}} |
||||
|
||||
<span>{{ status}}</span> |
||||
<br/> |
||||
|
||||
{{#if file}} |
||||
<span style="font-weight: 600;">{{_ "File"}}:</span> |
||||
<span>{{ fileName}}</span> |
||||
<br/> |
||||
{{/if}} |
||||
|
||||
{{#if hasCounters}} |
||||
<span style="font-weight: 600;">{{_ "Counters"}}:</span> |
||||
<br/> |
||||
|
||||
<span>{{_ "Users"}}:</span> |
||||
<span>{{ userCount}}</span> |
||||
<br/> |
||||
<span>{{_ "Channels"}}:</span> |
||||
<span>{{ channelCount}}</span> |
||||
<br/> |
||||
<span>{{_ "Messages"}}:</span> |
||||
<span>{{ messageCount}}</span> |
||||
<br/> |
||||
<span>{{_ "Total"}}:</span> |
||||
<span>{{ totalCount}}</span> |
||||
<br/> |
||||
|
||||
<br/> |
||||
{{/if}} |
||||
|
||||
{{#if hasErrors}} |
||||
<h2>{{_ "Errors_and_Warnings"}}:</h2> |
||||
<br/> |
||||
|
||||
{{#if fileData.users}} |
||||
{{#each fileData.users}} |
||||
{{#if is_email_taken}} |
||||
<span style="color: red;">{{_ "Email_already_exists"}}:</span> |
||||
<span>{{ email}}</span> |
||||
<br/> |
||||
{{/if}} |
||||
|
||||
{{#if error}} |
||||
<span>{{ email}}:</span> |
||||
<span style="color: red;">{{ formatedError}}</span> |
||||
<br/> |
||||
{{/if}} |
||||
|
||||
{{/each}} |
||||
{{/if}} |
||||
|
||||
{{#if errors}} |
||||
<br/> |
||||
<br/> |
||||
<h3>{{_ "Not_Imported_Messages_Title"}}</h3> |
||||
<br/> |
||||
{{#each errors}} |
||||
<br/> |
||||
{{#if msg}} |
||||
{{#if msg.id}} |
||||
<span style="font-weight: 600;">{{_ "Message_Id"}}:</span> |
||||
<span>{{ msg.id}}</span> |
||||
<br/> |
||||
{{/if}} |
||||
{{#if msg.userId}} |
||||
<span style="font-weight: 600;">{{_ "Message_UserId"}}:</span> |
||||
<span>{{ msg.userId}}</span> |
||||
<br/> |
||||
{{/if}} |
||||
{{#if msg.ts}} |
||||
<span style="font-weight: 600;">{{_ "Message_Time"}}:</span> |
||||
<span>{{ messageTime}}</span> |
||||
<br/> |
||||
{{/if}} |
||||
{{/if}} |
||||
{{#if error}} |
||||
{{#if error.error}} |
||||
<span style="font-weight: 500;">{{_ "Error"}}:</span> |
||||
<span style="color: red;">{{ error.error}}:</span> |
||||
<br/> |
||||
{{/if}} |
||||
{{#if error.message}} |
||||
<span style="font-weight: 500;">{{_ "Message"}}:</span> |
||||
<span style="color: red;">{{ error.message}}:</span> |
||||
<br/> |
||||
{{/if}} |
||||
{{#if error.details}} |
||||
{{#if error.details.method}} |
||||
<span style="font-weight: 500;">{{_ "Method"}}:</span> |
||||
<span style="color: red;">{{ error.details.method}}:</span> |
||||
<br/> |
||||
{{/if}} |
||||
{{/if}} |
||||
{{/if}} |
||||
{{/each}} |
||||
{{/if}} |
||||
|
||||
<br/> |
||||
{{/if}} |
||||
|
||||
</template> |
||||
@ -1,41 +0,0 @@ |
||||
import toastr from 'toastr'; |
||||
|
||||
import { t } from '../../../utils'; |
||||
|
||||
function showToastrForErrorType(errorType, defaultErrorString) { |
||||
let errorCode; |
||||
|
||||
if (typeof errorType === 'string') { |
||||
errorCode = errorType; |
||||
} else if (typeof errorType === 'object') { |
||||
if (errorType.error && typeof errorType.error === 'string') { |
||||
errorCode = errorType.error; |
||||
} |
||||
} |
||||
|
||||
if (errorCode) { |
||||
const errorTranslation = t(errorCode); |
||||
if (errorTranslation !== errorCode) { |
||||
toastr.error(errorTranslation); |
||||
return; |
||||
} |
||||
} |
||||
|
||||
toastr.error(t(defaultErrorString || 'Failed_To_upload_Import_File')); |
||||
} |
||||
|
||||
function showException(error, defaultErrorString) { |
||||
console.error(error); |
||||
if (error && error.xhr && error.xhr.responseJSON) { |
||||
console.log(error.xhr.responseJSON); |
||||
|
||||
if (error.xhr.responseJSON.errorType) { |
||||
showToastrForErrorType(error.xhr.responseJSON.errorType, defaultErrorString); |
||||
return; |
||||
} |
||||
} |
||||
|
||||
toastr.error(t(defaultErrorString || 'Failed_To_upload_Import_File')); |
||||
} |
||||
|
||||
export const showImporterException = showException; |
||||
@ -1,25 +0,0 @@ |
||||
import { registerAdminRoute } from '../../ui-admin/client'; |
||||
|
||||
registerAdminRoute('/import', { |
||||
name: 'admin-import', |
||||
lazyRouteComponent: () => import('./components/ImportRoute'), |
||||
props: { page: 'history' }, |
||||
}); |
||||
|
||||
registerAdminRoute('/import/new/:importerKey?', { |
||||
name: 'admin-import-new', |
||||
lazyRouteComponent: () => import('./components/ImportRoute'), |
||||
props: { page: 'new' }, |
||||
}); |
||||
|
||||
registerAdminRoute('/import/prepare', { |
||||
name: 'admin-import-prepare', |
||||
lazyRouteComponent: () => import('./components/ImportRoute'), |
||||
props: { page: 'prepare' }, |
||||
}); |
||||
|
||||
registerAdminRoute('/import/progress', { |
||||
name: 'admin-import-progress', |
||||
lazyRouteComponent: () => import('./components/ImportRoute'), |
||||
props: { page: 'progress' }, |
||||
}); |
||||
@ -1,80 +0,0 @@ |
||||
<template name="adminInvites"> |
||||
<div class="main-content-flex"> |
||||
<section class="page-container page-list flex-tab-main-content"> |
||||
{{> header sectionName="Invites"}} |
||||
<div class="content"> |
||||
{{#unless hasPermission 'create-invite-links'}} |
||||
<p>{{_ "You_are_not_authorized_to_view_this_page"}}</p> |
||||
{{else}} |
||||
<div class="results"> |
||||
{{{_ "Showing_results" invites.length}}} |
||||
</div> |
||||
{{#table fixed='true' onItemClick=onTableItemClick onScroll=onTableScroll onResize=onTableResize}} |
||||
<thead> |
||||
<tr class="admin-table-row"> |
||||
<th class="content-background-color border-component-color" width="20%"> |
||||
<div class="table-fake-th">{{_ "Token"}}</div> |
||||
</th> |
||||
<th class="content-background-color border-component-color" width="35%"> |
||||
<div class="table-fake-th">{{_ "Created_at"}}</div> |
||||
</th> |
||||
<th class="content-background-color border-component-color" width="20%"> |
||||
<div class="table-fake-th">{{_ "Expiration"}}</div> |
||||
</th> |
||||
<th class="content-background-color border-component-color" width="10%"> |
||||
<div class="table-fake-th">{{_ "Uses"}}</div> |
||||
</th> |
||||
<th class="content-background-color border-component-color" width="15%"> |
||||
<div class="table-fake-th">{{_ "Uses_left"}}</div> |
||||
</th> |
||||
</tr> |
||||
</thead> |
||||
<tbody> |
||||
{{#each invites}} |
||||
<tr class="invites-info row-link admin-table-row"> |
||||
<td class="border-component-color"> |
||||
<div class="rc-table-wrapper"> |
||||
<div class="rc-table-info"> |
||||
<span class="rc-table-title">{{_id}}</span></div> |
||||
</div> |
||||
</td> |
||||
<td class="border-component-color"> |
||||
<div class="rc-table-wrapper"> |
||||
<div class="rc-table-info"> |
||||
<span class="rc-table-title">{{formatDateAndTime createdAt}}</span></div> |
||||
</div> |
||||
</td> |
||||
<td class="border-component-color"> |
||||
<div class="rc-table-wrapper"> |
||||
<div class="rc-table-info"> |
||||
<span class="rc-table-title">{{daysToExpire}}</span></div> |
||||
</div> |
||||
</td> |
||||
<td class="border-component-color"> |
||||
<div class="rc-table-wrapper"> |
||||
<div class="rc-table-info"> |
||||
<span class="rc-table-title">{{uses}}</span></div> |
||||
</div> |
||||
</td> |
||||
<td class="border-component-color"> |
||||
<div class="rc-table-wrapper"> |
||||
<div class="rc-table-info"> |
||||
<span class="rc-table-title">{{maxUsesLeft}}</span></div> |
||||
|
||||
|
||||
<button class="rc-apps-section__app-menu-trigger js-remove" data-id="{{_id}}"> |
||||
<svg class="rc-icon rc-icon--default-size rc-icon--default-size--menu" aria-hidden="true"> |
||||
<use xlink:href="#icon-cross"></use> |
||||
</svg> |
||||
</button> |
||||
</div> |
||||
</td> |
||||
</tr> |
||||
{{/each}} |
||||
</tbody> |
||||
{{/table}} |
||||
{{/unless}} |
||||
</div> |
||||
</section> |
||||
</div> |
||||
</template> |
||||
@ -1,93 +0,0 @@ |
||||
import { Template } from 'meteor/templating'; |
||||
import { ReactiveVar } from 'meteor/reactive-var'; |
||||
import moment from 'moment'; |
||||
import toastr from 'toastr'; |
||||
|
||||
import { t, APIClient } from '../../../utils'; |
||||
import { formatDateAndTime } from '../../../lib/client/lib/formatDate'; |
||||
import { modal } from '../../../ui-utils/client'; |
||||
|
||||
import './adminInvites.html'; |
||||
|
||||
Template.adminInvites.helpers({ |
||||
formatDateAndTime, |
||||
invites() { |
||||
return Template.instance().invites.get(); |
||||
}, |
||||
daysToExpire() { |
||||
const { expires, days } = this; |
||||
|
||||
if (days > 0) { |
||||
if (expires < Date.now()) { |
||||
return t('Expired'); |
||||
} |
||||
|
||||
return moment(expires).fromNow(true); |
||||
} |
||||
|
||||
return t('Never'); |
||||
}, |
||||
maxUsesLeft() { |
||||
const { maxUses, uses } = this; |
||||
|
||||
if (maxUses > 0) { |
||||
if (uses >= maxUses) { |
||||
return 0; |
||||
} |
||||
|
||||
return maxUses - uses; |
||||
} |
||||
|
||||
return t('Unlimited'); |
||||
}, |
||||
}); |
||||
|
||||
Template.adminInvites.onCreated(async function() { |
||||
const instance = this; |
||||
this.invites = new ReactiveVar([]); |
||||
|
||||
const result = await APIClient.v1.get('listInvites') || []; |
||||
|
||||
const invites = result.map((data) => ({ |
||||
...data, |
||||
createdAt: new Date(data.createdAt), |
||||
expires: data.expires ? new Date(data.expires) : '', |
||||
})); |
||||
|
||||
instance.invites.set(invites); |
||||
}); |
||||
|
||||
Template.adminInvites.events({ |
||||
async 'click .js-remove'(event, instance) { |
||||
event.stopPropagation(); |
||||
|
||||
modal.open({ |
||||
text: t('Are_you_sure_you_want_to_delete_this_record'), |
||||
type: 'warning', |
||||
showCancelButton: true, |
||||
confirmButtonColor: '#DD6B55', |
||||
confirmButtonText: t('Yes'), |
||||
cancelButtonText: t('No'), |
||||
closeOnConfirm: true, |
||||
html: false, |
||||
}, async (confirmed) => { |
||||
if (!confirmed) { |
||||
return; |
||||
} |
||||
|
||||
const { currentTarget } = event; |
||||
|
||||
const { id } = currentTarget.dataset; |
||||
|
||||
try { |
||||
await APIClient.v1.delete(`removeInvite/${ id }`); |
||||
|
||||
const invites = instance.invites.get() || []; |
||||
invites.splice(invites.findIndex((i) => i._id === id), 1); |
||||
instance.invites.set(invites); |
||||
} catch (e) { |
||||
toastr.error(t(e.error)); |
||||
} |
||||
}); |
||||
}, |
||||
}); |
||||
@ -1,11 +0,0 @@ |
||||
import { BlazeLayout } from 'meteor/kadira:blaze-layout'; |
||||
|
||||
import { registerAdminRoute } from '../../../ui-admin/client'; |
||||
|
||||
registerAdminRoute('/invites', { |
||||
name: 'invites', |
||||
async action(/* params */) { |
||||
await import('./adminInvites'); |
||||
BlazeLayout.render('main', { center: 'adminInvites' }); |
||||
}, |
||||
}); |
||||
@ -1,11 +0,0 @@ |
||||
import { hasAtLeastOnePermission } from '../../../authorization'; |
||||
import { registerAdminSidebarItem } from '../../../ui-admin/client'; |
||||
|
||||
registerAdminSidebarItem({ |
||||
href: 'invites', |
||||
i18nLabel: 'Invites', |
||||
icon: 'user-plus', |
||||
permissionGranted() { |
||||
return hasAtLeastOnePermission(['create-invite-links']); |
||||
}, |
||||
}); |
||||
@ -1,2 +0,0 @@ |
||||
import './admin/route'; |
||||
import './admin/startup'; |
||||
@ -1,34 +0,0 @@ |
||||
const foregroundColors = { |
||||
30: 'gray', |
||||
31: 'red', |
||||
32: 'lime', |
||||
33: 'yellow', |
||||
34: '#6B98FF', |
||||
35: '#FF00FF', |
||||
36: 'cyan', |
||||
37: 'white', |
||||
}; |
||||
|
||||
export const ansispan = (str: string) => { |
||||
str = str |
||||
.replace(/\s/g, ' ') |
||||
.replace(/(\\n|\n)/g, '<br>') |
||||
.replace(/>/g, '>') |
||||
.replace(/</g, '<') |
||||
.replace(/(.\d{8}-\d\d:\d\d:\d\d\.\d\d\d\(?.{0,2}\)?)/, '<span class="terminal-time">$1</span>') |
||||
.replace(/\033\[1m/g, '<strong>') |
||||
.replace(/\033\[22m/g, '</strong>') |
||||
.replace(/\033\[3m/g, '<em>') |
||||
.replace(/\033\[23m/g, '</em>') |
||||
.replace(/\033\[m/g, '</span>') |
||||
.replace(/\033\[0m/g, '</span>') |
||||
.replace(/\033\[39m/g, '</span>'); |
||||
return Object.entries(foregroundColors).reduce((str, [ansiCode, color]) => { |
||||
const span = `<span style="color: ${ color }">`; |
||||
return ( |
||||
str |
||||
.replace(new RegExp(`\\033\\[${ ansiCode }m`, 'g'), span) |
||||
.replace(new RegExp(`\\033\\[0;${ ansiCode }m`, 'g'), span) |
||||
); |
||||
}, str); |
||||
}; |
||||
@ -1,33 +0,0 @@ |
||||
import { BlazeLayout } from 'meteor/kadira:blaze-layout'; |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { Mongo } from 'meteor/mongo'; |
||||
|
||||
import { hasAllPermission } from '../../authorization'; |
||||
import { registerAdminRoute, registerAdminSidebarItem } from '../../ui-admin/client'; |
||||
import { t } from '../../utils'; |
||||
|
||||
export const stdout = new Mongo.Collection(null); |
||||
|
||||
Meteor.startup(function() { |
||||
registerAdminSidebarItem({ |
||||
href: 'admin-view-logs', |
||||
i18nLabel: 'View_Logs', |
||||
icon: 'post', |
||||
permissionGranted() { |
||||
return hasAllPermission('view-logs'); |
||||
}, |
||||
}); |
||||
}); |
||||
|
||||
registerAdminRoute('/view-logs', { |
||||
name: 'admin-view-logs', |
||||
async action() { |
||||
await import('./views/viewLogs'); |
||||
return BlazeLayout.render('main', { |
||||
center: 'pageSettingsContainer', |
||||
pageTitle: t('View_Logs'), |
||||
pageTemplate: 'viewLogs', |
||||
noScroll: true, |
||||
}); |
||||
}, |
||||
}); |
||||
@ -1,29 +0,0 @@ |
||||
.view-logs { |
||||
&__terminal { |
||||
overflow-y: scroll; |
||||
flex: 1; |
||||
|
||||
margin: 0; |
||||
margin-bottom: 0 !important; |
||||
padding: 8px 10px !important; |
||||
|
||||
color: var(--color-white); |
||||
border: none !important; |
||||
background-color: #444444 !important; |
||||
|
||||
font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; |
||||
font-weight: 500; |
||||
|
||||
&-line { |
||||
word-break: break-all; |
||||
} |
||||
|
||||
&-time { |
||||
color: #7f7f7f; |
||||
} |
||||
|
||||
.rtl & { |
||||
direction: ltr; |
||||
} |
||||
} |
||||
} |
||||
@ -1,17 +0,0 @@ |
||||
<template name="viewLogs"> |
||||
{{#if hasPermission}} |
||||
<div class="section view-logs__terminal js-terminal"> |
||||
{{#each logs}} |
||||
<div class="view-logs__terminal-line"> |
||||
{{{ansispan string}}} |
||||
</div> |
||||
{{/each}} |
||||
</div> |
||||
<div class="view-logs__new-logs js-new-logs not color-primary-action-color"> |
||||
<i class="icon-down-big"></i> |
||||
<span>{{_ "New_logs"}}</span> |
||||
</div> |
||||
{{else}} |
||||
{{_ "Not_authorized"}} |
||||
{{/if}} |
||||
</template> |
||||
@ -1,121 +0,0 @@ |
||||
import _ from 'underscore'; |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { Template } from 'meteor/templating'; |
||||
import { Tracker } from 'meteor/tracker'; |
||||
|
||||
import { ansispan } from '../ansispan'; |
||||
import { stdout } from '../viewLogs'; |
||||
import { hasAllPermission } from '../../../authorization'; |
||||
import { SideNav } from '../../../ui-utils/client'; |
||||
import './viewLogs.html'; |
||||
import './viewLogs.css'; |
||||
import { APIClient } from '../../../utils/client'; |
||||
|
||||
const stdoutStreamer = new Meteor.Streamer('stdout'); |
||||
|
||||
Template.viewLogs.onCreated(async function() { |
||||
const { queue } = await APIClient.v1.get('stdout.queue'); |
||||
(queue || []).forEach((item) => stdout.insert(item)); |
||||
stdoutStreamer.on('stdout', (item) => stdout.insert(item)); |
||||
this.atBottom = true; |
||||
}); |
||||
|
||||
Template.viewLogs.onDestroyed(() => { |
||||
stdout.remove({}); |
||||
stdoutStreamer.removeListener('stdout'); |
||||
}); |
||||
|
||||
Template.viewLogs.helpers({ |
||||
hasPermission() { |
||||
return hasAllPermission('view-logs'); |
||||
}, |
||||
logs() { |
||||
return stdout.find({}, { sort: { ts: 1 } }); |
||||
}, |
||||
ansispan, |
||||
}); |
||||
|
||||
Template.viewLogs.events({ |
||||
'click .new-logs'(event, instance) { |
||||
instance.atBottom = true; |
||||
instance.sendToBottomIfNecessary(); |
||||
}, |
||||
}); |
||||
|
||||
Template.viewLogs.onRendered(function() { |
||||
Tracker.afterFlush(() => { |
||||
SideNav.setFlex('adminFlex'); |
||||
SideNav.openFlex(); |
||||
}); |
||||
|
||||
const wrapper = this.find('.js-terminal'); |
||||
const newLogs = this.find('.js-new-logs'); |
||||
|
||||
this.isAtBottom = (scrollThreshold) => { |
||||
if (scrollThreshold == null) { |
||||
scrollThreshold = 0; |
||||
} |
||||
if (wrapper.scrollTop + scrollThreshold >= wrapper.scrollHeight - wrapper.clientHeight) { |
||||
newLogs.className = 'new-logs not'; |
||||
return true; |
||||
} |
||||
return false; |
||||
}; |
||||
|
||||
this.sendToBottom = () => { |
||||
wrapper.scrollTop = wrapper.scrollHeight - wrapper.clientHeight; |
||||
newLogs.className = 'new-logs not'; |
||||
}; |
||||
|
||||
this.checkIfScrollIsAtBottom = () => { |
||||
this.atBottom = this.isAtBottom(100); |
||||
}; |
||||
|
||||
this.sendToBottomIfNecessary = () => { |
||||
if (this.atBottom === true && this.isAtBottom() !== true) { |
||||
this.sendToBottom(); |
||||
} else if (this.atBottom === false) { |
||||
newLogs.className = 'new-logs'; |
||||
} |
||||
}; |
||||
|
||||
this.sendToBottomIfNecessaryDebounced = _.debounce(this.sendToBottomIfNecessary, 10); |
||||
this.sendToBottomIfNecessary(); |
||||
|
||||
if (window.MutationObserver) { |
||||
const observer = new MutationObserver((mutations) => { |
||||
mutations.forEach(() => this.sendToBottomIfNecessaryDebounced()); |
||||
}); |
||||
observer.observe(wrapper, { childList: true }); |
||||
} else { |
||||
wrapper.addEventListener('DOMSubtreeModified', () => this.sendToBottomIfNecessaryDebounced()); |
||||
} |
||||
|
||||
this.onWindowResize = () => { |
||||
Meteor.defer(() => this.sendToBottomIfNecessaryDebounced()); |
||||
}; |
||||
window.addEventListener('resize', this.onWindowResize); |
||||
|
||||
wrapper.addEventListener('mousewheel', () => { |
||||
this.atBottom = false; |
||||
Meteor.defer(() => this.checkIfScrollIsAtBottom()); |
||||
}); |
||||
|
||||
wrapper.addEventListener('wheel', () => { |
||||
this.atBottom = false; |
||||
Meteor.defer(() => this.checkIfScrollIsAtBottom()); |
||||
}); |
||||
|
||||
wrapper.addEventListener('touchstart', () => { |
||||
this.atBottom = false; |
||||
}); |
||||
|
||||
wrapper.addEventListener('touchend', () => { |
||||
Meteor.defer(() => this.checkIfScrollIsAtBottom()); |
||||
}); |
||||
|
||||
wrapper.addEventListener('scroll', () => { |
||||
this.atBottom = false; |
||||
Meteor.defer(() => this.checkIfScrollIsAtBottom()); |
||||
}); |
||||
}); |
||||
@ -1,17 +0,0 @@ |
||||
import { Box } from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
|
||||
import { useTranslation } from '../../../../client/contexts/TranslationContext'; |
||||
import { Page } from '../../../../client/components/basic/Page'; |
||||
|
||||
function NotAuthorizedPage() { |
||||
const t = useTranslation(); |
||||
|
||||
return <Page> |
||||
<Page.Content> |
||||
<Box is='p' textColor='default' textStyle='p1'>{t('You_are_not_authorized_to_view_this_page')}</Box> |
||||
</Page.Content> |
||||
</Page>; |
||||
} |
||||
|
||||
export default NotAuthorizedPage; |
||||
@ -1,22 +0,0 @@ |
||||
import { Subtitle } from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
|
||||
import { useTranslation } from '../../../../../client/contexts/TranslationContext'; |
||||
import { DescriptionList } from './DescriptionList'; |
||||
import { formatDate } from './formatters'; |
||||
|
||||
export function BuildEnvironmentSection({ info }) { |
||||
const t = useTranslation(); |
||||
const build = info && (info.compile || info.build); |
||||
|
||||
return <> |
||||
<Subtitle data-qa='build-env-title'>{t('Build_Environment')}</Subtitle> |
||||
<DescriptionList data-qa='build-env-list'> |
||||
<DescriptionList.Entry label={t('OS_Platform')}>{build.platform}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('OS_Arch')}>{build.arch}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('OS_Release')}>{build.osRelease}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Node_version')}>{build.nodeVersion}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Date')}>{formatDate(build.date)}</DescriptionList.Entry> |
||||
</DescriptionList> |
||||
</>; |
||||
} |
||||
@ -1,22 +0,0 @@ |
||||
import { Subtitle } from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
|
||||
import { useTranslation } from '../../../../../client/contexts/TranslationContext'; |
||||
import { DescriptionList } from './DescriptionList'; |
||||
|
||||
export function CommitSection({ info }) { |
||||
const t = useTranslation(); |
||||
const { commit = {} } = info; |
||||
|
||||
return <> |
||||
<Subtitle data-qa='commit-title'>{t('Commit')}</Subtitle> |
||||
<DescriptionList data-qa='commit-list'> |
||||
<DescriptionList.Entry label={t('Hash')}>{commit.hash}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Date')}>{commit.date}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Branch')}>{commit.branch}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Tag')}>{commit.tag}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Author')}>{commit.author}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Subject')}>{commit.subject}</DescriptionList.Entry> |
||||
</DescriptionList> |
||||
</>; |
||||
} |
||||
@ -1,16 +0,0 @@ |
||||
import React from 'react'; |
||||
|
||||
export const DescriptionList = ({ children, ...props }) => |
||||
<table className='statistics-table secondary-background-color' {...props}> |
||||
<tbody> |
||||
{children} |
||||
</tbody> |
||||
</table>; |
||||
|
||||
const Entry = ({ children, label, ...props }) => |
||||
<tr className='admin-table-row' {...props}> |
||||
<th className='content-background-color border-component-color'>{label}</th> |
||||
<td className='border-component-color'>{children}</td> |
||||
</tr>; |
||||
|
||||
DescriptionList.Entry = Entry; |
||||
@ -1,29 +0,0 @@ |
||||
import { Skeleton, Subtitle } from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
|
||||
import { useTranslation } from '../../../../../client/contexts/TranslationContext'; |
||||
import { formatDate, formatHumanReadableTime } from './formatters'; |
||||
import { DescriptionList } from './DescriptionList'; |
||||
|
||||
export function RocketChatSection({ info, statistics, isLoading }) { |
||||
const s = (fn) => (isLoading ? <Skeleton width='50%' /> : fn()); |
||||
const t = useTranslation(); |
||||
|
||||
const appsEngineVersion = info && info.marketplaceApiVersion; |
||||
|
||||
return <> |
||||
<Subtitle data-qa='rocket-chat-title'>{t('Rocket.Chat')}</Subtitle> |
||||
<DescriptionList data-qa='rocket-chat-list'> |
||||
<DescriptionList.Entry label={t('Version')}>{s(() => statistics.version)}</DescriptionList.Entry> |
||||
{appsEngineVersion && <DescriptionList.Entry label={t('Apps_Engine_Version')}>{appsEngineVersion}</DescriptionList.Entry>} |
||||
<DescriptionList.Entry label={t('DB_Migration')}>{s(() => statistics.migration.version)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('DB_Migration_Date')}>{s(() => formatDate(statistics.migration.lockedAt))}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Installed_at')}>{s(() => formatDate(statistics.installedAt))}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Uptime')}>{s(() => formatHumanReadableTime(statistics.process.uptime, t))}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Deployment_ID')}>{s(() => statistics.uniqueId)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('PID')}>{s(() => statistics.process.pid)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Running_Instances')}>{s(() => statistics.instanceCount)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('OpLog')}>{s(() => (statistics.oplogEnabled ? t('Enabled') : t('Disabled')))}</DescriptionList.Entry> |
||||
</DescriptionList> |
||||
</>; |
||||
} |
||||
@ -1,31 +0,0 @@ |
||||
import { Skeleton, Subtitle } from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
|
||||
import { useTranslation } from '../../../../../client/contexts/TranslationContext'; |
||||
import { useFormatMemorySize } from '../../../../ui/client/views/app/components/hooks'; |
||||
import { DescriptionList } from './DescriptionList'; |
||||
import { formatHumanReadableTime, formatCPULoad } from './formatters'; |
||||
|
||||
export function RuntimeEnvironmentSection({ statistics, isLoading }) { |
||||
const s = (fn) => (isLoading ? <Skeleton width='50%' /> : fn()); |
||||
const t = useTranslation(); |
||||
const formatMemorySize = useFormatMemorySize(); |
||||
|
||||
return <> |
||||
<Subtitle data-qa='runtime-env-title'>{t('Runtime_Environment')}</Subtitle> |
||||
<DescriptionList data-qa='runtime-env-list'> |
||||
<DescriptionList.Entry label={t('OS_Type')}>{s(() => statistics.os.type)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('OS_Platform')}>{s(() => statistics.os.platform)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('OS_Arch')}>{s(() => statistics.os.arch)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('OS_Release')}>{s(() => statistics.os.release)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Node_version')}>{s(() => statistics.process.nodeVersion)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Mongo_version')}>{s(() => statistics.mongoVersion)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Mongo_storageEngine')}>{s(() => statistics.mongoStorageEngine)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('OS_Uptime')}>{s(() => formatHumanReadableTime(statistics.os.uptime, t))}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('OS_Loadavg')}>{s(() => formatCPULoad(statistics.os.loadavg))}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('OS_Totalmem')}>{s(() => formatMemorySize(statistics.os.totalmem))}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('OS_Freemem')}>{s(() => formatMemorySize(statistics.os.freemem))}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('OS_Cpus')}>{s(() => statistics.os.cpus.length)}</DescriptionList.Entry> |
||||
</DescriptionList> |
||||
</>; |
||||
} |
||||
@ -1,51 +0,0 @@ |
||||
import { Subtitle, Skeleton } from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
|
||||
import { useTranslation } from '../../../../../client/contexts/TranslationContext'; |
||||
import { useFormatMemorySize } from '../../../../ui/client/views/app/components/hooks'; |
||||
import { DescriptionList } from './DescriptionList'; |
||||
|
||||
export function UsageSection({ statistics, isLoading }) { |
||||
const s = (fn) => (isLoading ? <Skeleton width='50%' /> : fn()); |
||||
const formatMemorySize = useFormatMemorySize(); |
||||
const t = useTranslation(); |
||||
|
||||
return <> |
||||
<Subtitle data-qa='usage-title'>{t('Usage')}</Subtitle> |
||||
<DescriptionList data-qa='usage-list'> |
||||
<DescriptionList.Entry label={t('Stats_Total_Users')}>{s(() => statistics.totalUsers)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Active_Users')}>{s(() => statistics.activeUsers)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Active_Guests')}>{s(() => statistics.activeGuests)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_App_Users')}>{s(() => statistics.appUsers)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Non_Active_Users')}>{s(() => statistics.nonActiveUsers)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Total_Connected_Users')}>{s(() => statistics.totalConnectedUsers)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Online_Users')}>{s(() => statistics.onlineUsers)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Away_Users')}>{s(() => statistics.awayUsers)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Offline_Users')}>{s(() => statistics.offlineUsers)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Total_Rooms')}>{s(() => statistics.totalRooms)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Total_Channels')}>{s(() => statistics.totalChannels)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Total_Private_Groups')}>{s(() => statistics.totalPrivateGroups)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Total_Direct_Messages')}>{s(() => statistics.totalDirect)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Total_Livechat_Rooms')}>{s(() => statistics.totalLivechat)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Total_Discussions')}>{s(() => statistics.totalDiscussions)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Total_Threads')}>{s(() => statistics.totalThreads)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Total_Messages')}>{s(() => statistics.totalMessages)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Total_Messages_Channel')}>{s(() => statistics.totalChannelMessages)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Total_Messages_PrivateGroup')}>{s(() => statistics.totalPrivateGroupMessages)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Total_Messages_Direct')}>{s(() => statistics.totalDirectMessages)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Total_Messages_Livechat')}>{s(() => statistics.totalLivechatMessages)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Total_Uploads')}>{s(() => statistics.uploadsTotal)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Total_Uploads_Size')}>{s(() => formatMemorySize(statistics.uploadsTotalSize))}</DescriptionList.Entry> |
||||
{statistics && statistics.apps && <> |
||||
<DescriptionList.Entry label={t('Stats_Total_Installed_Apps')}>{statistics.apps.totalInstalled}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Total_Active_Apps')}>{statistics.apps.totalActive}</DescriptionList.Entry> |
||||
</>} |
||||
<DescriptionList.Entry label={t('Stats_Total_Integrations')}>{s(() => statistics.integrations.totalIntegrations)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Total_Incoming_Integrations')}>{s(() => statistics.integrations.totalIncoming)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Total_Active_Incoming_Integrations')}>{s(() => statistics.integrations.totalIncomingActive)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Total_Outgoing_Integrations')}>{s(() => statistics.integrations.totalOutgoing)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Total_Active_Outgoing_Integrations')}>{s(() => statistics.integrations.totalOutgoingActive)}</DescriptionList.Entry> |
||||
<DescriptionList.Entry label={t('Stats_Total_Integrations_With_Script_Enabled')}>{s(() => statistics.integrations.totalWithScriptEnabled)}</DescriptionList.Entry> |
||||
</DescriptionList> |
||||
</>; |
||||
} |
||||
@ -1,43 +0,0 @@ |
||||
import moment from 'moment'; |
||||
import s from 'underscore.string'; |
||||
|
||||
export const formatNumber = (number) => s.numberFormat(number, 2); |
||||
|
||||
export const formatDate = (date) => { |
||||
if (!date) { |
||||
return null; |
||||
} |
||||
|
||||
return moment(date).format('LLL'); |
||||
}; |
||||
|
||||
export const formatHumanReadableTime = (time, t) => { |
||||
const days = Math.floor(time / 86400); |
||||
const hours = Math.floor((time % 86400) / 3600); |
||||
const minutes = Math.floor(((time % 86400) % 3600) / 60); |
||||
const seconds = Math.floor(((time % 86400) % 3600) % 60); |
||||
let out = ''; |
||||
if (days > 0) { |
||||
out += `${ days } ${ t('days') }, `; |
||||
} |
||||
if (hours > 0) { |
||||
out += `${ hours } ${ t('hours') }, `; |
||||
} |
||||
if (minutes > 0) { |
||||
out += `${ minutes } ${ t('minutes') }, `; |
||||
} |
||||
if (seconds > 0) { |
||||
out += `${ seconds } ${ t('seconds') }`; |
||||
} |
||||
return out; |
||||
}; |
||||
|
||||
export const formatCPULoad = (load) => { |
||||
if (!load) { |
||||
return null; |
||||
} |
||||
|
||||
const [oneMinute, fiveMinutes, fifteenMinutes] = load; |
||||
|
||||
return `${ formatNumber(oneMinute) }, ${ formatNumber(fiveMinutes) }, ${ formatNumber(fifteenMinutes) }`; |
||||
}; |
||||
@ -1,10 +0,0 @@ |
||||
import { useEffect } from 'react'; |
||||
|
||||
import { SideNav } from '../../../ui-utils/client/lib/SideNav'; |
||||
|
||||
export const useAdminSideNav = () => { |
||||
useEffect(() => { |
||||
SideNav.setFlex('adminFlex'); |
||||
SideNav.openFlex(); |
||||
}, []); |
||||
}; |
||||
@ -1,102 +0,0 @@ |
||||
<template name="adminRoomInfo"> |
||||
{{#with selectedRoom}} |
||||
<section class="contextual-bar__content"> |
||||
<div class="list-view channel-settings"> |
||||
<div class="title"> |
||||
<h2>{{_ "Room_Info"}}</h2> |
||||
</div> |
||||
<form> |
||||
<ul class="list clearfix"> |
||||
{{#if notDirect}} |
||||
<li> |
||||
<label>{{_ "Name"}}</label> |
||||
<div> |
||||
{{#if editing 'roomName'}} |
||||
<input type="text" name="roomName" value="{{roomName}}" class="content-background-color editing" /> |
||||
<button type="button" class="button cancel">{{_ "Cancel"}}</button> |
||||
<button type="button" class="button primary save">{{_ "Save"}}</button> |
||||
{{else}} |
||||
<span>{{roomName}}{{#if canEdit}} <i class="icon-pencil" data-edit="roomName"></i>{{/if}}</span> |
||||
{{/if}} |
||||
</div> |
||||
</li> |
||||
{{/if}} |
||||
{{#if roomOwner}} |
||||
<li> |
||||
<label>{{_ "Owner"}}</label> |
||||
<div> |
||||
<span>{{roomOwner}}</span> |
||||
</div> |
||||
</li> |
||||
{{/if}} |
||||
<li> |
||||
<label>{{_ "Topic"}}</label> |
||||
<div> |
||||
{{#if editing 'roomTopic'}} |
||||
<input type="text" name="roomTopic" value="{{roomTopic}}" class="content-background-color editing" /> |
||||
<button type="button" class="button cancel">{{_ "Cancel"}}</button> |
||||
<button type="button" class="button primary save">{{_ "Save"}}</button> |
||||
{{else}} |
||||
<span>{{{RocketChatMarkdown roomTopic}}}{{#if canEdit}} <i class="icon-pencil" data-edit="roomTopic"></i>{{/if}}</span> |
||||
{{/if}} |
||||
</div> |
||||
</li> |
||||
{{#if notDirect}} |
||||
<li> |
||||
<label>{{_ "Type"}}</label> |
||||
<div> |
||||
{{#if editing 'roomType'}} |
||||
<label><input type="radio" name="roomType" class="editing" value="c" checked="{{$eq roomType 'c'}}" /> {{_ "Channel"}}</label> |
||||
<label><input type="radio" name="roomType" value="p" checked="{{$eq roomType 'p'}}" /> {{_ "Private_Group"}}</label> |
||||
<button type="button" class="button cancel">{{_ "Cancel"}}</button> |
||||
<button type="button" class="button primary save">{{_ "Save"}}</button> |
||||
{{else}} |
||||
<span>{{roomTypeDescription}}{{#if canEdit}} <i class="icon-pencil" data-edit="roomType"></i>{{/if}}</span> |
||||
{{/if}} |
||||
</div> |
||||
</li> |
||||
{{/if}} |
||||
{{#if notDirect}} |
||||
<li> |
||||
<label>{{_ "Room_archivation_state"}}</label> |
||||
<div> |
||||
{{#if editing 'archivationState'}} |
||||
<label><input type="radio" name="archivationState" class="editing" value="true" checked="{{$eq archivationState true}}" /> {{_ "Room_archivation_state_true"}}</label> |
||||
<label><input type="radio" name="archivationState" value="false" checked="{{$neq archivationState true}}" /> {{_ "Room_archivation_state_false"}}</label> |
||||
<button type="button" class="button cancel">{{_ "Cancel"}}</button> |
||||
<button type="button" class="button primary save">{{_ "Save"}}</button> |
||||
{{else}} |
||||
<span>{{archivationStateDescription}}{{#if canEdit}} <i class="icon-pencil" data-edit="archivationState"></i>{{/if}}</span> |
||||
{{/if}} |
||||
</div> |
||||
</li> |
||||
{{/if}} |
||||
{{#if notDirect}} |
||||
<li> |
||||
<label>{{_ "Read_only_channel"}}</label> |
||||
<div> |
||||
{{#if editing 'readOnly'}} |
||||
<label><input type="radio" name="readOnly" class="editing" value="true" checked="{{$eq readOnly true}}" /> {{_ "True"}}</label> |
||||
<label><input type="radio" name="readOnly" value="false" checked="{{$neq readOnly true}}" /> {{_ "False"}}</label> |
||||
<button type="button" class="button cancel">{{_ "Cancel"}}</button> |
||||
<button type="button" class="button primary save">{{_ "Save"}}</button> |
||||
{{else}} |
||||
<span>{{readOnlyDescription}}{{#if canEdit}} <i class="icon-pencil" data-edit="readOnly"></i>{{/if}}</span> |
||||
{{/if}} |
||||
</div> |
||||
</li> |
||||
{{/if}} |
||||
{{#each channelSettings}} |
||||
{{> Template.dynamic template=template data=data}} |
||||
{{/each}} |
||||
</ul> |
||||
</form> |
||||
{{#if canDeleteRoom}} |
||||
<nav> |
||||
<button class='button danger delete'><span><i class='icon-trash'></i> {{_ "Delete"}}</span></button> |
||||
</nav> |
||||
{{/if}} |
||||
</div> |
||||
</section> |
||||
{{/with}} |
||||
</template> |
||||
@ -1,304 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { ReactiveVar } from 'meteor/reactive-var'; |
||||
import { Session } from 'meteor/session'; |
||||
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'; |
||||
import { call, modal } from '../../../ui-utils'; |
||||
import { hasAllPermission, hasAtLeastOnePermission } from '../../../authorization'; |
||||
import { ChannelSettings } from '../../../channel-settings'; |
||||
import { settings } from '../../../settings'; |
||||
import { callbacks } from '../../../callbacks'; |
||||
|
||||
Template.adminRoomInfo.helpers({ |
||||
selectedRoom() { |
||||
return Session.get('adminRoomsSelected'); |
||||
}, |
||||
canEdit() { |
||||
return hasAllPermission('edit-room', this.rid); |
||||
}, |
||||
editing(field) { |
||||
return Template.instance().editing.get() === field; |
||||
}, |
||||
notDirect() { |
||||
const room = Template.instance().room.get(); |
||||
return room && room.t !== 'd'; |
||||
}, |
||||
roomType() { |
||||
const room = Template.instance().room.get(); |
||||
return room && room.t; |
||||
}, |
||||
channelSettings() { |
||||
return ChannelSettings.getOptions(undefined, 'admin-room'); |
||||
}, |
||||
roomTypeDescription() { |
||||
const room = Template.instance().room.get(); |
||||
const roomType = room && room.t; |
||||
if (roomType === 'c') { |
||||
return t('Channel'); |
||||
} if (roomType === 'p') { |
||||
return t('Private_Group'); |
||||
} |
||||
}, |
||||
roomName() { |
||||
const room = Template.instance().room.get(); |
||||
return room && room.name; |
||||
}, |
||||
roomOwner() { |
||||
const roomOwner = Template.instance().roomOwner.get(); |
||||
return roomOwner && (roomOwner.name || roomOwner.username); |
||||
}, |
||||
roomTopic() { |
||||
const room = Template.instance().room.get(); |
||||
return room && room.topic; |
||||
}, |
||||
archivationState() { |
||||
const room = Template.instance().room.get(); |
||||
return room && room.archived; |
||||
}, |
||||
archivationStateDescription() { |
||||
const room = Template.instance().room.get(); |
||||
const archivationState = room && room.archived; |
||||
if (archivationState === true) { |
||||
return t('Room_archivation_state_true'); |
||||
} |
||||
return t('Room_archivation_state_false'); |
||||
}, |
||||
canDeleteRoom() { |
||||
const room = Template.instance().room.get(); |
||||
const roomType = room && room.t; |
||||
return (roomType != null) && hasAtLeastOnePermission(`delete-${ roomType }`); |
||||
}, |
||||
readOnly() { |
||||
const room = Template.instance().room.get(); |
||||
return room && room.ro; |
||||
}, |
||||
readOnlyDescription() { |
||||
const room = Template.instance().room.get(); |
||||
const readOnly = room && room.ro; |
||||
|
||||
if (readOnly === true) { |
||||
return t('True'); |
||||
} |
||||
return t('False'); |
||||
}, |
||||
}); |
||||
|
||||
Template.adminRoomInfo.events({ |
||||
'click .delete'(event, instance) { |
||||
modal.open({ |
||||
title: t('Are_you_sure'), |
||||
text: t('Delete_Room_Warning'), |
||||
type: 'warning', |
||||
showCancelButton: true, |
||||
confirmButtonColor: '#DD6B55', |
||||
confirmButtonText: t('Yes_delete_it'), |
||||
cancelButtonText: t('Cancel'), |
||||
closeOnConfirm: false, |
||||
html: false, |
||||
}, () => { |
||||
Meteor.call('eraseRoom', this.rid, function(error) { |
||||
if (error) { |
||||
handleError(error); |
||||
} else { |
||||
modal.open({ |
||||
title: t('Deleted'), |
||||
text: t('Room_has_been_deleted'), |
||||
type: 'success', |
||||
timer: 2000, |
||||
showConfirmButton: false, |
||||
}); |
||||
instance.onSuccess(); |
||||
instance.data.tabBar.close(); |
||||
} |
||||
}); |
||||
}); |
||||
}, |
||||
'keydown input[type=text]'(e, t) { |
||||
if (e.keyCode === 13) { |
||||
e.preventDefault(); |
||||
t.saveSetting(this.rid); |
||||
} |
||||
}, |
||||
'click [data-edit]'(e, t) { |
||||
e.preventDefault(); |
||||
t.editing.set($(e.currentTarget).data('edit')); |
||||
return setTimeout(function() { |
||||
t.$('input.editing').focus().select(); |
||||
}, 100); |
||||
}, |
||||
'click .cancel'(e, t) { |
||||
e.preventDefault(); |
||||
t.editing.set(); |
||||
}, |
||||
'click .save'(e, t) { |
||||
e.preventDefault(); |
||||
t.saveSetting(this.rid); |
||||
}, |
||||
}); |
||||
|
||||
Template.adminRoomInfo.onCreated(function() { |
||||
const instance = this; |
||||
const currentData = Template.currentData(); |
||||
this.editing = new ReactiveVar(); |
||||
this.room = new ReactiveVar(); |
||||
this.roomOwner = new ReactiveVar(); |
||||
this.onSuccess = Template.currentData().onSuccess; |
||||
|
||||
this.autorun(() => { |
||||
const { room } = Template.currentData(); |
||||
this.room.set(room); |
||||
}); |
||||
|
||||
this.validateRoomType = () => { |
||||
const type = this.$('input[name=roomType]:checked').val(); |
||||
if (type !== 'c' && type !== 'p') { |
||||
toastr.error(t('error-invalid-room-type', { type })); |
||||
} |
||||
return true; |
||||
}; |
||||
this.validateRoomName = (rid) => { |
||||
const { room } = currentData; |
||||
let nameValidation; |
||||
if (!hasAllPermission('edit-room', rid) || (room.t !== 'c' && room.t !== 'p')) { |
||||
toastr.error(t('error-not-allowed')); |
||||
return false; |
||||
} |
||||
name = $('input[name=roomName]').val(); |
||||
try { |
||||
nameValidation = new RegExp(`^${ settings.get('UTF8_Names_Validation') }$`); |
||||
} catch (_error) { |
||||
nameValidation = new RegExp('^[0-9a-zA-Z-_.]+$'); |
||||
} |
||||
if (!nameValidation.test(name)) { |
||||
toastr.error(t('error-invalid-room-name', { |
||||
room_name: s.escapeHTML(name), |
||||
})); |
||||
return false; |
||||
} |
||||
return true; |
||||
}; |
||||
this.validateRoomTopic = () => true; |
||||
this.saveSetting = (rid) => { |
||||
switch (this.editing.get()) { |
||||
case 'roomName': |
||||
if (this.validateRoomName(rid)) { |
||||
callbacks.run('roomNameChanged', currentData.room); |
||||
Meteor.call('saveRoomSettings', rid, 'roomName', this.$('input[name=roomName]').val(), function(err) { |
||||
if (err) { |
||||
return handleError(err); |
||||
} |
||||
toastr.success(TAPi18n.__('Room_name_changed_successfully')); |
||||
instance.onSuccess(); |
||||
instance.data.tabBar.close(); |
||||
}); |
||||
} |
||||
break; |
||||
case 'roomTopic': |
||||
if (this.validateRoomTopic(rid)) { |
||||
Meteor.call('saveRoomSettings', rid, 'roomTopic', this.$('input[name=roomTopic]').val(), function(err) { |
||||
if (err) { |
||||
return handleError(err); |
||||
} |
||||
toastr.success(TAPi18n.__('Room_topic_changed_successfully')); |
||||
callbacks.run('roomTopicChanged', currentData.room); |
||||
instance.onSuccess(); |
||||
instance.data.tabBar.close(); |
||||
}); |
||||
} |
||||
break; |
||||
case 'roomAnnouncement': |
||||
if (this.validateRoomTopic(rid)) { |
||||
Meteor.call('saveRoomSettings', rid, 'roomAnnouncement', this.$('input[name=roomAnnouncement]').val(), function(err) { |
||||
if (err) { |
||||
return handleError(err); |
||||
} |
||||
toastr.success(TAPi18n.__('Room_announcement_changed_successfully')); |
||||
callbacks.run('roomAnnouncementChanged', currentData.room); |
||||
instance.onSuccess(); |
||||
instance.data.tabBar.close(); |
||||
}); |
||||
} |
||||
break; |
||||
case 'roomType': |
||||
const val = this.$('input[name=roomType]:checked').val(); |
||||
if (this.validateRoomType(rid)) { |
||||
callbacks.run('roomTypeChanged', currentData.room); |
||||
const saveRoomSettings = function() { |
||||
Meteor.call('saveRoomSettings', rid, 'roomType', val, function(err) { |
||||
if (err) { |
||||
return handleError(err); |
||||
} |
||||
toastr.success(TAPi18n.__('Room_type_changed_successfully')); |
||||
instance.onSuccess(); |
||||
instance.data.tabBar.close(); |
||||
}); |
||||
}; |
||||
if (!currentData.room.default) { |
||||
return saveRoomSettings(); |
||||
} |
||||
modal.open({ |
||||
title: t('Room_default_change_to_private_will_be_default_no_more'), |
||||
type: 'warning', |
||||
showCancelButton: true, |
||||
confirmButtonColor: '#DD6B55', |
||||
confirmButtonText: t('Yes'), |
||||
cancelButtonText: t('Cancel'), |
||||
closeOnConfirm: true, |
||||
html: false, |
||||
}, function(confirmed) { |
||||
return !confirmed || saveRoomSettings(); |
||||
}); |
||||
} |
||||
break; |
||||
case 'archivationState': |
||||
const { room } = currentData; |
||||
if (this.$('input[name=archivationState]:checked').val() === 'true') { |
||||
if (room && room.archived !== true) { |
||||
Meteor.call('archiveRoom', rid, function(err) { |
||||
if (err) { |
||||
return handleError(err); |
||||
} |
||||
toastr.success(TAPi18n.__('Room_archived')); |
||||
callbacks.run('archiveRoom', currentData.room); |
||||
instance.onSuccess(); |
||||
instance.data.tabBar.close(); |
||||
}); |
||||
} |
||||
} else if ((room && room.archived) === true) { |
||||
Meteor.call('unarchiveRoom', rid, function(err) { |
||||
if (err) { |
||||
return handleError(err); |
||||
} |
||||
toastr.success(TAPi18n.__('Room_unarchived')); |
||||
callbacks.run('unarchiveRoom', currentData.room); |
||||
instance.onSuccess(); |
||||
instance.data.tabBar.close(); |
||||
}); |
||||
} |
||||
break; |
||||
case 'readOnly': |
||||
Meteor.call('saveRoomSettings', rid, 'readOnly', this.$('input[name=readOnly]:checked').val() === 'true', function(err) { |
||||
if (err) { |
||||
return handleError(err); |
||||
} |
||||
toastr.success(TAPi18n.__('Read_only_changed_successfully')); |
||||
instance.onSuccess(); |
||||
instance.data.tabBar.close(); |
||||
}); |
||||
} |
||||
this.editing.set(); |
||||
}; |
||||
|
||||
this.autorun(async () => { |
||||
this.roomOwner.set(null); |
||||
for (const { roles, u } of await call('getRoomRoles', Session.get('adminRoomsSelected').rid)) { |
||||
if (roles.includes('owner')) { |
||||
this.roomOwner.set(u); |
||||
} |
||||
} |
||||
}); |
||||
}); |
||||
@ -1,78 +0,0 @@ |
||||
<template name="adminRooms"> |
||||
<div class="main-content-flex"> |
||||
<section class="page-container page-list flex-tab-main-content"> |
||||
{{> header sectionName="Rooms" fixedHeight="true" fullpage="true"}} |
||||
<div class="content"> |
||||
{{#unless hasPermission 'view-room-administration'}} |
||||
<p>{{_ "You_are_not_authorized_to_view_this_page"}}</p> |
||||
{{else}} |
||||
<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="rooms-filter" type="text" class="rc-input__element" |
||||
placeholder="{{_ "Search"}}" autofocus dir="auto"> |
||||
</div> |
||||
<label><input type="checkbox" name="room-type" value="c"> {{_ "Channels"}}</label> |
||||
<label><input type="checkbox" name="room-type" value="d"> {{_ "Direct_Messages"}}</label> |
||||
<label><input type="checkbox" name="room-type" value="p"> {{_ "Private_Groups"}}</label> |
||||
<label><input type="checkbox" name="room-type" value="l"> {{_ "Omnichannel"}}</label> |
||||
<label><input type="checkbox" name="room-type" value="discussions"> {{_ "Discussions"}}</label> |
||||
</form> |
||||
<div class="results"> |
||||
{{{_ "Showing_results" roomCount}}} |
||||
</div> |
||||
{{#table fixed='true' onItemClick=onTableItemClick onScroll=onTableScroll onResize=onTableResize}} |
||||
<thead> |
||||
<tr> |
||||
<th width="30%"><div class="table-fake-th">{{_ "Name"}}</div></th> |
||||
<th width="20%"><div class="table-fake-th">{{_ "Type"}}</div></th> |
||||
<th width="20%"><div class="table-fake-th">{{_ "Users"}}</div></th> |
||||
<th width="10%"><div class="table-fake-th">{{_ "Msgs"}}</div></th> |
||||
<th width="10%"><div class="table-fake-th">{{_ "Default"}}</div></th> |
||||
<th width="10%"><div class="table-fake-th">{{_ "Featured"}}</div></th> |
||||
</tr> |
||||
</thead> |
||||
<tbody> |
||||
{{#each rooms}} |
||||
<tr> |
||||
<td width="30%"><div class="rc-table-wrapper"> |
||||
<div class="rc-table-avatar">{{> avatar url=url roomIcon="true"}}</div> |
||||
<div class="rc-table-info"> |
||||
<span class="rc-table-title"> |
||||
{{>icon icon=getIcon block="rc-table-icon"}} {{roomName}} |
||||
</span> |
||||
</div> |
||||
</div></td> |
||||
<td width="20%"><div class="rc-table-wrapper">{{type}}</div></td> |
||||
<td width="20%"><div class="rc-table-wrapper">{{usersCount}}</div></td> |
||||
<td width="10%"><div class="rc-table-wrapper">{{msgs}}</div></td> |
||||
<td width="10%"><div class="rc-table-wrapper">{{default}}</div></td> |
||||
<td width="10%"><div class="rc-table-wrapper">{{#if featured}}True{{else}}False{{/if}}</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" colspan="{{#if showLastMessage}}5{{else}}4{{/if}}">{{> loading}}</td> |
||||
</tr> |
||||
{{/if}} |
||||
</tbody> |
||||
{{/table}} |
||||
{{/unless}} |
||||
</div> |
||||
</section> |
||||
{{#with flexData}} |
||||
{{> flexTabBar}} |
||||
{{/with}} |
||||
</div> |
||||
</template> |
||||
@ -1,210 +0,0 @@ |
||||
import { Tracker } from 'meteor/tracker'; |
||||
import { ReactiveVar } from 'meteor/reactive-var'; |
||||
import { FlowRouter } from 'meteor/kadira:flow-router'; |
||||
import { Session } from 'meteor/session'; |
||||
import { Template } from 'meteor/templating'; |
||||
import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; |
||||
import _ from 'underscore'; |
||||
|
||||
import { SideNav, RocketChatTabBar, TabBar } from '../../../ui-utils'; |
||||
import { t, roomTypes } from '../../../utils'; |
||||
import { hasAllPermission } from '../../../authorization'; |
||||
import { ChannelSettings } from '../../../channel-settings'; |
||||
import { getAvatarURL } from '../../../utils/lib/getAvatarURL'; |
||||
import { APIClient } from '../../../utils/client'; |
||||
|
||||
const LIST_SIZE = 50; |
||||
const DEBOUNCE_TIME_TO_SEARCH_IN_MS = 500; |
||||
|
||||
Template.adminRooms.helpers({ |
||||
url() { |
||||
return this.t === 'd' ? getAvatarURL({ username: `@${ this.usernames[0] }` }) : roomTypes.getConfig(this.t).getAvatarPath(this); |
||||
}, |
||||
getIcon() { |
||||
return roomTypes.getIcon(this); |
||||
}, |
||||
roomName() { |
||||
return this.t === 'd' ? this.usernames.join(' x ') : roomTypes.getRoomName(this.t, this); |
||||
}, |
||||
searchText() { |
||||
const instance = Template.instance(); |
||||
return instance.filter && instance.filter.get(); |
||||
}, |
||||
rooms() { |
||||
return Template.instance().rooms.get(); |
||||
}, |
||||
isLoading() { |
||||
return Template.instance().isLoading.get(); |
||||
}, |
||||
roomCount() { |
||||
return Template.instance().rooms.get().length; |
||||
}, |
||||
type() { |
||||
return TAPi18n.__(roomTypes.getConfig(this.t).label); |
||||
}, |
||||
'default'() { |
||||
if (this.default) { |
||||
return t('True'); |
||||
} |
||||
return t('False'); |
||||
}, |
||||
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 rooms = instance.rooms.get(); |
||||
if (instance.total.get() > rooms.length) { |
||||
instance.offset.set(instance.offset.get() + LIST_SIZE); |
||||
} |
||||
}; |
||||
}, |
||||
onTableItemClick() { |
||||
const instance = Template.instance(); |
||||
return function(item) { |
||||
Session.set('adminRoomsSelected', { |
||||
rid: item._id, |
||||
}); |
||||
instance.tabBarData.set({ |
||||
room: instance.rooms.get().find((room) => room._id === item._id), |
||||
onSuccess: instance.onSuccessCallback, |
||||
}); |
||||
instance.tabBar.open('admin-room'); |
||||
}; |
||||
}, |
||||
|
||||
}); |
||||
|
||||
Template.adminRooms.onCreated(function() { |
||||
const instance = this; |
||||
this.offset = new ReactiveVar(0); |
||||
this.total = new ReactiveVar(0); |
||||
this.filter = new ReactiveVar(''); |
||||
this.types = new ReactiveVar([]); |
||||
this.rooms = new ReactiveVar([]); |
||||
this.ready = new ReactiveVar(true); |
||||
this.isLoading = new ReactiveVar(false); |
||||
this.tabBar = new RocketChatTabBar(); |
||||
this.tabBarData = new ReactiveVar(); |
||||
this.tabBar.showGroup(FlowRouter.current().route.name); |
||||
TabBar.addButton({ |
||||
groups: ['admin-rooms'], |
||||
id: 'admin-room', |
||||
i18nTitle: 'Room_Info', |
||||
icon: 'info-circled', |
||||
template: 'adminRoomInfo', |
||||
order: 7, |
||||
}); |
||||
|
||||
ChannelSettings.addOption({ |
||||
group: ['admin-room'], |
||||
id: 'make-default', |
||||
template: 'channelSettingsDefault', |
||||
data() { |
||||
return { |
||||
room: instance.tabBarData.get().room, |
||||
onSuccess: instance.tabBarData.get().onSuccess, |
||||
}; |
||||
}, |
||||
validation() { |
||||
return hasAllPermission('view-room-administration'); |
||||
}, |
||||
}); |
||||
|
||||
ChannelSettings.addOption({ |
||||
group: ['admin-room'], |
||||
id: 'make-featured', |
||||
template: 'channelSettingsFeatured', |
||||
data() { |
||||
return { |
||||
room: instance.tabBarData.get().room, |
||||
onSuccess: instance.tabBarData.get().onSuccess, |
||||
}; |
||||
}, |
||||
validation() { |
||||
return hasAllPermission('view-room-administration'); |
||||
}, |
||||
}); |
||||
|
||||
this.onSuccessCallback = () => { |
||||
instance.offset.set(0); |
||||
return instance.loadRooms(instance.types.get(), instance.filter.get(), instance.offset.get()); |
||||
}; |
||||
|
||||
this.tabBarData.set({ |
||||
onSuccess: instance.onSuccessCallback, |
||||
}); |
||||
|
||||
const mountTypesQueryParameter = (types) => types.reduce((acc, item) => { |
||||
acc += `types[]=${ item }&`; |
||||
return acc; |
||||
}, ''); |
||||
|
||||
this.loadRooms = _.debounce(async (types = [], filter, offset) => { |
||||
this.isLoading.set(true); |
||||
let url = `rooms.adminRooms?count=${ LIST_SIZE }&offset=${ offset }&${ mountTypesQueryParameter(types) }`; |
||||
if (filter) { |
||||
url += `filter=${ encodeURIComponent(filter) }`; |
||||
} |
||||
const { rooms, total } = await APIClient.v1.get(url); |
||||
this.total.set(total); |
||||
if (offset === 0) { |
||||
this.rooms.set(rooms); |
||||
} else { |
||||
this.rooms.set(this.rooms.get().concat(rooms)); |
||||
} |
||||
this.isLoading.set(false); |
||||
}, DEBOUNCE_TIME_TO_SEARCH_IN_MS); |
||||
|
||||
const allowedTypes = ['c', 'd', 'p']; |
||||
this.autorun(() => { |
||||
instance.filter.get(); |
||||
instance.types.get(); |
||||
instance.offset.set(0); |
||||
}); |
||||
this.autorun(() => { |
||||
const filter = instance.filter.get(); |
||||
const offset = instance.offset.get(); |
||||
let types = instance.types.get(); |
||||
if (types.length === 0) { |
||||
types = allowedTypes; |
||||
} |
||||
return this.loadRooms(types, filter, offset); |
||||
}); |
||||
this.getSearchTypes = function() { |
||||
return _.map($('[name=room-type]:checked'), function(input) { |
||||
return $(input).val(); |
||||
}); |
||||
}; |
||||
}); |
||||
|
||||
Template.adminRooms.onRendered(function() { |
||||
Tracker.afterFlush(function() { |
||||
SideNav.setFlex('adminFlex'); |
||||
SideNav.openFlex(); |
||||
}); |
||||
}); |
||||
|
||||
Template.adminRooms.events({ |
||||
'keydown #rooms-filter'(e) { |
||||
if (e.which === 13) { |
||||
e.stopPropagation(); |
||||
e.preventDefault(); |
||||
} |
||||
}, |
||||
'keyup #rooms-filter'(e, t) { |
||||
e.stopPropagation(); |
||||
e.preventDefault(); |
||||
t.filter.set(e.currentTarget.value); |
||||
}, |
||||
'change [name=room-type]'(e, t) { |
||||
t.types.set(t.getSearchTypes()); |
||||
}, |
||||
}); |
||||
@ -1,20 +0,0 @@ |
||||
<template name="channelSettingsDefault"> |
||||
{{#if canMakeDefault}} |
||||
<li> |
||||
<label>{{_ "Default"}}</label> |
||||
<div> |
||||
{{#if editing 'default'}} |
||||
<label><input type="radio" name="default" class="editing" value="true" checked="{{$eq roomDefault true}}" /> {{_ "True"}}</label> |
||||
<label><input type="radio" name="default" value="false" checked="{{$neq roomDefault true}}" /> {{_ "False"}}</label> |
||||
{{#if roomDefault}} |
||||
<label><input type="checkbox" name="favorite" checked="{{isFavorite}}" /> {{_ "Set_as_favorite"}}</label> |
||||
{{/if}} |
||||
<button type="button" class="button cancel">{{_ "Cancel"}}</button> |
||||
<button type="button" class="button primary save">{{_ "Save"}}</button> |
||||
{{else}} |
||||
<span>{{defaultDescription}} <i class="icon-pencil" data-edit="default"></i></span> |
||||
{{/if}} |
||||
</div> |
||||
</li> |
||||
{{/if}} |
||||
</template> |
||||
@ -1,84 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { ReactiveVar } from 'meteor/reactive-var'; |
||||
import { Template } from 'meteor/templating'; |
||||
import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; |
||||
import toastr from 'toastr'; |
||||
|
||||
import { t, handleError } from '../../../utils'; |
||||
|
||||
Template.channelSettingsDefault.helpers({ |
||||
canMakeDefault() { |
||||
const room = Template.instance().room.get(); |
||||
return room && room.t === 'c'; |
||||
}, |
||||
editing(field) { |
||||
return Template.instance().editing.get() === field; |
||||
}, |
||||
roomDefault() { |
||||
return Template.instance().isDefault.get(); |
||||
}, |
||||
isFavorite() { |
||||
return Template.instance().isFavorite.get(); |
||||
}, |
||||
defaultDescription() { |
||||
const { room, isDefault, isFavorite } = { room: Template.instance().room.get(), isDefault: Template.instance().isDefault.get(), isFavorite: Template.instance().isFavorite.get() }; |
||||
let description = t('False'); |
||||
|
||||
if (room && isDefault) { |
||||
description = t('True'); |
||||
|
||||
if (isFavorite) { |
||||
description += `, ${ t('Set_as_favorite') }`; |
||||
} |
||||
return description; |
||||
} |
||||
return description; |
||||
}, |
||||
}); |
||||
|
||||
Template.channelSettingsDefault.events({ |
||||
'change input[name=default]'(e, t) { |
||||
t.isDefault.set(e.currentTarget.value === 'true'); |
||||
}, |
||||
'change input[name=favorite]'(e, t) { |
||||
t.isFavorite.set(e.currentTarget.checked); |
||||
}, |
||||
'click [data-edit]'(e, t) { |
||||
e.preventDefault(); |
||||
t.editing.set($(e.currentTarget).data('edit')); |
||||
setTimeout(() => { |
||||
t.$('input.editing').focus().select(); |
||||
}, 100); |
||||
}, |
||||
'click .cancel'(e, t) { |
||||
e.preventDefault(); |
||||
t.editing.set(); |
||||
}, |
||||
'click .save'(e, t) { |
||||
e.preventDefault(); |
||||
|
||||
Meteor.call('saveRoomSettings', t.room.get()._id, { default: t.isDefault.get(), favorite: { defaultValue: t.isDefault.get(), favorite: t.isFavorite.get() } }, (err/* , result*/) => { |
||||
if (err) { |
||||
return handleError(err); |
||||
} |
||||
toastr.success(TAPi18n.__('Room_type_changed_successfully')); |
||||
}); |
||||
t.onSuccess(); |
||||
t.editing.set(); |
||||
}, |
||||
}); |
||||
|
||||
Template.channelSettingsDefault.onCreated(function() { |
||||
this.editing = new ReactiveVar(); |
||||
this.isDefault = new ReactiveVar(); |
||||
this.isFavorite = new ReactiveVar(); |
||||
this.room = new ReactiveVar(); |
||||
this.onSuccess = Template.currentData().onSuccess; |
||||
|
||||
this.autorun(() => { |
||||
const { room } = Template.currentData(); |
||||
this.isDefault.set(room && room.default); |
||||
this.isFavorite.set(room && room.favorite && room.default); |
||||
this.room.set(room); |
||||
}); |
||||
}); |
||||
@ -1,15 +0,0 @@ |
||||
<template name="channelSettingsFeatured"> |
||||
<li> |
||||
<label>{{_ "Featured"}}</label> |
||||
<div> |
||||
{{#if editing 'featured'}} |
||||
<label><input type="radio" name="featured" class="editing" value="true" checked="{{$eq roomFeatured true}}" /> {{_ "True"}}</label> |
||||
<label><input type="radio" name="featured" value="false" checked="{{$neq roomFeatured true}}" /> {{_ "False"}}</label> |
||||
<button type="button" class="button cancel">{{_ "Cancel"}}</button> |
||||
<button type="button" class="button primary save">{{_ "Save"}}</button> |
||||
{{else}} |
||||
<span>{{featuredDescription}} <i class="icon-pencil" data-edit="featured"></i></span> |
||||
{{/if}} |
||||
</div> |
||||
</li> |
||||
</template> |
||||
@ -1,65 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { ReactiveVar } from 'meteor/reactive-var'; |
||||
import { Template } from 'meteor/templating'; |
||||
import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; |
||||
import toastr from 'toastr'; |
||||
import './channelSettingsFeatured.html'; |
||||
|
||||
import { t, handleError } from '../../../utils'; |
||||
|
||||
Template.channelSettingsFeatured.helpers({ |
||||
editing(field) { |
||||
return Template.instance().editing.get() === field; |
||||
}, |
||||
roomFeatured() { |
||||
const room = Template.instance().room.get(); |
||||
|
||||
if (room) { |
||||
return room.featured; |
||||
} |
||||
}, |
||||
featuredDescription() { |
||||
const room = Template.instance().room.get(); |
||||
if (room && room.featured) { |
||||
return t('True'); |
||||
} |
||||
return t('False'); |
||||
}, |
||||
}); |
||||
|
||||
Template.channelSettingsFeatured.events({ |
||||
'click [data-edit]'(e, t) { |
||||
e.preventDefault(); |
||||
t.editing.set($(e.currentTarget).data('edit')); |
||||
setTimeout(() => { |
||||
t.$('input.editing').focus().select(); |
||||
}, 100); |
||||
}, |
||||
'click .cancel'(e, t) { |
||||
e.preventDefault(); |
||||
t.editing.set(); |
||||
}, |
||||
'click .save'(e, t) { |
||||
e.preventDefault(); |
||||
|
||||
Meteor.call('saveRoomSettings', Template.instance().room.get()._id, 'featured', $('input[name=featured]:checked').val(), (err/* , result*/) => { |
||||
if (err) { |
||||
return handleError(err); |
||||
} |
||||
toastr.success(TAPi18n.__('Room_changed_successfully')); |
||||
}); |
||||
t.onSuccess(); |
||||
t.editing.set(); |
||||
}, |
||||
}); |
||||
|
||||
Template.channelSettingsFeatured.onCreated(function() { |
||||
this.editing = new ReactiveVar(); |
||||
this.room = new ReactiveVar(); |
||||
this.onSuccess = Template.currentData().onSuccess; |
||||
|
||||
this.autorun(() => { |
||||
const { room } = Template.currentData(); |
||||
this.room.set(room); |
||||
}); |
||||
}); |
||||
@ -1,7 +0,0 @@ |
||||
import './adminRooms.html'; |
||||
import './adminRoomInfo.html'; |
||||
import './adminRoomInfo'; |
||||
import './channelSettingsDefault.html'; |
||||
import './channelSettingsDefault'; |
||||
import './channelSettingsFeatured'; |
||||
import './adminRooms'; |
||||
@ -1,67 +0,0 @@ |
||||
import { FlowRouter } from 'meteor/kadira:flow-router'; |
||||
import { BlazeLayout } from 'meteor/kadira:blaze-layout'; |
||||
import { Meteor } from 'meteor/meteor'; |
||||
|
||||
import { renderRouteComponent } from '../../../client/reactAdapters'; |
||||
|
||||
const routeGroup = FlowRouter.group({ |
||||
name: 'admin', |
||||
prefix: '/admin', |
||||
}); |
||||
|
||||
export const registerAdminRoute = (path, { lazyRouteComponent, props, action, ...options } = {}) => { |
||||
routeGroup.route(path, { |
||||
...options, |
||||
action: (params, queryParams) => { |
||||
if (action) { |
||||
action(params, queryParams); |
||||
return; |
||||
} |
||||
|
||||
renderRouteComponent(() => import('./components/AdministrationRouter'), { |
||||
template: 'main', |
||||
region: 'center', |
||||
propsFn: () => ({ lazyRouteComponent, ...options, params, queryParams, ...props }), |
||||
}); |
||||
}, |
||||
}); |
||||
}; |
||||
|
||||
registerAdminRoute('/', { |
||||
triggersEnter: [(context, redirect) => { |
||||
redirect('admin-info'); |
||||
}], |
||||
}); |
||||
|
||||
registerAdminRoute('/info', { |
||||
name: 'admin-info', |
||||
lazyRouteComponent: () => import('./components/info/InformationRoute'), |
||||
}); |
||||
|
||||
registerAdminRoute('/mailer', { |
||||
name: 'admin-mailer', |
||||
lazyRouteComponent: () => import('./components/mailer/MailerRoute'), |
||||
}); |
||||
|
||||
registerAdminRoute('/users', { |
||||
name: 'admin-users', |
||||
action: async () => { |
||||
await import('./users/views'); |
||||
BlazeLayout.render('main', { center: 'adminUsers' }); |
||||
}, |
||||
}); |
||||
|
||||
registerAdminRoute('/rooms', { |
||||
name: 'admin-rooms', |
||||
action: async () => { |
||||
await import('./rooms/views'); |
||||
BlazeLayout.render('main', { center: 'adminRooms' }); |
||||
}, |
||||
}); |
||||
|
||||
Meteor.startup(() => { |
||||
registerAdminRoute('/:group+', { |
||||
name: 'admin', |
||||
lazyRouteComponent: () => import('./components/settings/SettingsRoute'), |
||||
}); |
||||
}); |
||||
@ -1,10 +0,0 @@ |
||||
import { ReactiveVar } from 'meteor/reactive-var'; |
||||
import { Tracker } from 'meteor/tracker'; |
||||
|
||||
const items = new ReactiveVar([]); |
||||
|
||||
export const registerAdminSidebarItem = (itemOptions) => { |
||||
Tracker.nonreactive(() => items.set([...items.get(), itemOptions])); |
||||
}; |
||||
|
||||
export const getSidebarItems = () => items.get().filter((option) => !option.permissionGranted || option.permissionGranted()); |
||||
@ -1,31 +0,0 @@ |
||||
<template name="adminInviteUser"> |
||||
{{#if isAllowed}} |
||||
<div class="content"> |
||||
<div class="user-view"> |
||||
<div class="about clearfix"> |
||||
<form class="edit-form"> |
||||
<h3>{{_ "Send_invitation_email"}}</h3> |
||||
<div class="input-line"> |
||||
<label for="inviteEmails">{{_ "Send_invitation_email_info"}}</label> |
||||
<textarea id="inviteEmails" rows="3" style="height: auto" class="content-background-color"></textarea> |
||||
</div> |
||||
</form> |
||||
</div> |
||||
<nav> |
||||
<button class='button button-block cancel'><span>{{_ "Cancel"}}</span></button> |
||||
<button class='button button-block primary send' data-loading-text="{{_ "Please_wait"}}"><span>{{_ "Send"}}</span></button> |
||||
</nav> |
||||
{{#if inviteEmails.length}} |
||||
<div class="about clearfix" style="margin-top: 30px"> |
||||
<p style="color: #51a351"> {{_ "Send_invitation_email_success"}} </p> |
||||
<ul style="margin: 5px 10px"> |
||||
{{#each inviteEmails}} |
||||
<li style="margin-top: 5px">{{.}}</li> |
||||
{{/each}} |
||||
</ul> |
||||
</div> |
||||
{{/if}} |
||||
</div> |
||||
</div> |
||||
{{/if}} |
||||
</template> |
||||
@ -1,54 +0,0 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import { ReactiveVar } from 'meteor/reactive-var'; |
||||
import { Template } from 'meteor/templating'; |
||||
import _ from 'underscore'; |
||||
import toastr from 'toastr'; |
||||
|
||||
import { hasAtLeastOnePermission } from '../../../authorization'; |
||||
import { t, handleError } from '../../../utils'; |
||||
|
||||
Template.adminInviteUser.helpers({ |
||||
isAllowed() { |
||||
return hasAtLeastOnePermission('bulk-register-user'); |
||||
}, |
||||
inviteEmails() { |
||||
return Template.instance().inviteEmails.get(); |
||||
}, |
||||
}); |
||||
|
||||
Template.adminInviteUser.events({ |
||||
'click .send'(e, instance) { |
||||
const emails = $('#inviteEmails').val().split(/[\s,;]/); |
||||
const rfcMailPattern = /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; |
||||
const validEmails = _.compact(_.map(emails, function(email) { |
||||
if (rfcMailPattern.test(email)) { |
||||
return email; |
||||
} |
||||
})); |
||||
if (validEmails.length) { |
||||
Meteor.call('sendInvitationEmail', validEmails, function(error, result) { |
||||
if (result) { |
||||
instance.clearForm(); |
||||
instance.inviteEmails.set(validEmails); |
||||
} |
||||
if (error) { |
||||
handleError(error); |
||||
} |
||||
}); |
||||
} else { |
||||
toastr.error(t('Send_invitation_email_error')); |
||||
} |
||||
}, |
||||
'click .cancel'(e, instance) { |
||||
instance.clearForm(); |
||||
instance.inviteEmails.set([]); |
||||
Template.currentData().tabBar.close(); |
||||
}, |
||||
}); |
||||
|
||||
Template.adminInviteUser.onCreated(function() { |
||||
this.inviteEmails = new ReactiveVar([]); |
||||
this.clearForm = function() { |
||||
$('#inviteEmails').val(''); |
||||
}; |
||||
}); |
||||
@ -1,3 +0,0 @@ |
||||
<template name="adminUserEdit"> |
||||
{{> userEdit .}} |
||||
</template> |
||||
@ -1,9 +0,0 @@ |
||||
<template name="adminUserInfo"> |
||||
{{#if _id}} |
||||
{{> userInfo .}} |
||||
{{else}} |
||||
<section class="contextual-bar__content"> |
||||
<div>{{_ "Please_select_an_user"}}</div> |
||||
</section> |
||||
{{/if}} |
||||
</template> |
||||
@ -1,101 +0,0 @@ |
||||
<template name="adminUsers"> |
||||
<div class="main-content-flex"> |
||||
<section class="page-container page-list flex-tab-main-content"> |
||||
{{> header sectionName="Users"}} |
||||
<div class="content"> |
||||
{{#unless hasPermission 'view-user-administration'}} |
||||
<p>{{_ "You_are_not_authorized_to_view_this_page"}}</p> |
||||
{{else}} |
||||
<form class="search-form" role="form"> |
||||
<div class="rc-input__wrapper"> |
||||
<div class="rc-input__icon"> |
||||
{{#if isReady}} |
||||
{{> icon block="rc-input__icon-svg" icon="magnifier" }} |
||||
{{else}} |
||||
{{> loading }} |
||||
{{/if}} |
||||
</div> |
||||
<input id="users-filter" type="text" class="rc-input__element" |
||||
placeholder="{{_ "Search"}}" autofocus dir="auto"> |
||||
</div> |
||||
</form> |
||||
<div class="results"> |
||||
{{{_ "Showing_results" users.length}}} |
||||
</div> |
||||
{{#table fixed='true' onItemClick=onTableItemClick onScroll=onTableScroll onResize=onTableResize}} |
||||
<thead> |
||||
<tr> |
||||
<th width="34%"> |
||||
<div class="table-fake-th">{{_ "Name"}}</div> |
||||
</th> |
||||
<th width="33%"> |
||||
<div class="table-fake-th">{{_ "Username"}}</div> |
||||
</th> |
||||
<th width="33%"> |
||||
<div class="table-fake-th">{{_ "Email"}}</div> |
||||
</th> |
||||
<th width="33%"> |
||||
<div class="table-fake-th">{{_ "Roles"}}</div> |
||||
</th> |
||||
<th width="33%"> |
||||
<div class="table-fake-th">{{_ "Status"}}</div> |
||||
</th> |
||||
</tr> |
||||
</thead> |
||||
<tbody> |
||||
{{#each users}} |
||||
<tr class='user-info'> |
||||
<td width="30%"> |
||||
<div class="rc-table-wrapper"> |
||||
<div class="rc-table-avatar"> |
||||
{{> avatar username=username}} |
||||
</div> |
||||
<div class="rc-table-info"> |
||||
<span class="rc-table-title">{{name}}</span> |
||||
</div> |
||||
</div> |
||||
</td> |
||||
<td width="20%"> |
||||
<div class="rc-table-wrapper"> |
||||
<div class="rc-table-info"> |
||||
<span class="rc-table-title">{{username}}</span> |
||||
</div> |
||||
</div> |
||||
</td> |
||||
<td width="20%"> |
||||
<div class="rc-table-wrapper"> |
||||
<div class="rc-table-info"> |
||||
<span class="rc-table-title">{{emailAddress}}</span> |
||||
</div> |
||||
</div> |
||||
</td> |
||||
<td width="10%"> |
||||
<div class="rc-table-wrapper"> |
||||
<div class="rc-table-info"> |
||||
<span class="rc-table-title">{{roles}}</span> |
||||
</div> |
||||
</div> |
||||
</td> |
||||
<td width="20%"> |
||||
<div class="rc-table-wrapper">{{#if $not active}}{{_"deactivated"}}{{else}}{{status}}{{/if}}</div> |
||||
</td> |
||||
</tr> |
||||
{{else}} {{# with searchText}} |
||||
<tr class="table-no-click"> |
||||
<td>{{_ "No_results_found_for"}} {{.}}</td> |
||||
</tr> |
||||
{{/with}} {{/each}} {{#unless isReady}} |
||||
<tr class="table-no-click"> |
||||
<td class="table-loading-td" colspan="{{#if showLastMessage}}5{{else}}4{{/if}}">{{> loading}}</td> |
||||
</tr> |
||||
{{/unless}} |
||||
</tbody> |
||||
{{/table}} |
||||
{{/unless}} |
||||
</div> |
||||
</section> |
||||
{{#with flexData}} |
||||
{{> flexTabBar}} |
||||
{{/with}} |
||||
</div> |
||||
</template> |
||||
@ -1,183 +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 { SideNav, TabBar, RocketChatTabBar } from '../../../ui-utils'; |
||||
import { APIClient } from '../../../utils/client'; |
||||
|
||||
const USERS_COUNT = 50; |
||||
|
||||
Template.adminUsers.helpers({ |
||||
searchText() { |
||||
const instance = Template.instance(); |
||||
return instance.filter && instance.filter.get(); |
||||
}, |
||||
isReady() { |
||||
const instance = Template.instance(); |
||||
return instance.ready && instance.ready.get(); |
||||
}, |
||||
users() { |
||||
return Template.instance().users.get(); |
||||
}, |
||||
isLoading() { |
||||
const instance = Template.instance(); |
||||
if (!(instance.ready && instance.ready.get())) { |
||||
return 'btn-loading'; |
||||
} |
||||
}, |
||||
hasMore() { |
||||
const instance = Template.instance(); |
||||
const users = instance.users(); |
||||
if (instance.offset && instance.offset.get() && users && users.length) { |
||||
return instance.offset.get() === users.length; |
||||
} |
||||
}, |
||||
emailAddress() { |
||||
return _.map(this.emails, function(e) { return e.address; }).join(', '); |
||||
}, |
||||
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 instance.offset.set(instance.offset.get() + USERS_COUNT); |
||||
} |
||||
}; |
||||
}, |
||||
// onTableItemClick() {
|
||||
// const instance = Template.instance();
|
||||
// return function(item) {
|
||||
// Session.set('adminRoomsSelected', {
|
||||
// rid: item._id,
|
||||
// });
|
||||
// instance.tabBar.open('admin-room');
|
||||
// };
|
||||
// },
|
||||
}); |
||||
|
||||
Template.adminUsers.onCreated(function() { |
||||
const instance = this; |
||||
this.offset = new ReactiveVar(0); |
||||
this.filter = new ReactiveVar(''); |
||||
this.ready = new ReactiveVar(true); |
||||
this.tabBar = new RocketChatTabBar(); |
||||
this.tabBar.showGroup(FlowRouter.current().route.name); |
||||
this.tabBarData = new ReactiveVar(); |
||||
this.users = new ReactiveVar([]); |
||||
|
||||
TabBar.addButton({ |
||||
groups: ['admin-users'], |
||||
id: 'admin-user-info', |
||||
i18nTitle: 'User_Info', |
||||
icon: 'user', |
||||
template: 'adminUserInfo', |
||||
order: 3, |
||||
}); |
||||
|
||||
TabBar.addButton({ |
||||
groups: ['admin-users'], |
||||
id: 'invite-user', |
||||
i18nTitle: 'Invite_Users', |
||||
icon: 'send', |
||||
template: 'adminInviteUser', |
||||
order: 1, |
||||
}); |
||||
|
||||
TabBar.addButton({ |
||||
groups: ['admin-users'], |
||||
id: 'add-user', |
||||
i18nTitle: 'Add_User', |
||||
icon: 'plus', |
||||
template: 'adminUserEdit', |
||||
order: 2, |
||||
}); |
||||
|
||||
this.loadUsers = async (filter, offset) => { |
||||
this.ready.set(false); |
||||
|
||||
const query = { |
||||
$or: [ |
||||
{ 'emails.address': { $regex: filter, $options: 'i' } }, |
||||
{ username: { $regex: filter, $options: 'i' } }, |
||||
{ name: { $regex: filter, $options: 'i' } }, |
||||
], |
||||
}; |
||||
let url = `users.list?count=${ USERS_COUNT }&offset=${ offset }`; |
||||
if (filter) { |
||||
url += `&query=${ JSON.stringify(query) }`; |
||||
} |
||||
const { users } = await APIClient.v1.get(url); |
||||
if (offset === 0) { |
||||
this.users.set(users); |
||||
} else { |
||||
this.users.set(this.users.get().concat(users)); |
||||
} |
||||
this.ready.set(true); |
||||
}; |
||||
|
||||
this.autorun(async () => { |
||||
const filter = instance.filter.get(); |
||||
const offset = instance.offset.get(); |
||||
|
||||
this.loadUsers(filter, offset); |
||||
}); |
||||
}); |
||||
|
||||
Template.adminUsers.onRendered(function() { |
||||
Tracker.afterFlush(function() { |
||||
SideNav.setFlex('adminFlex'); |
||||
SideNav.openFlex(); |
||||
}); |
||||
}); |
||||
|
||||
const DEBOUNCE_TIME_FOR_SEARCH_USERS_IN_MS = 300; |
||||
|
||||
Template.adminUsers.events({ |
||||
'keydown #users-filter'(e) { |
||||
if (e.which === 13) { |
||||
e.stopPropagation(); |
||||
e.preventDefault(); |
||||
} |
||||
}, |
||||
'keyup #users-filter': _.debounce((e, t) => { |
||||
e.stopPropagation(); |
||||
e.preventDefault(); |
||||
t.filter.set(e.currentTarget.value); |
||||
t.offset.set(0); |
||||
}, DEBOUNCE_TIME_FOR_SEARCH_USERS_IN_MS), |
||||
'click .user-info'(e, instance) { |
||||
e.preventDefault(); |
||||
instance.tabBarData.set({ |
||||
...instance.users.get().find((user) => user._id === this._id), |
||||
onChange() { |
||||
const filter = instance.filter.get(); |
||||
const offset = instance.offset.get(); |
||||
|
||||
instance.loadUsers(filter, offset); |
||||
}, |
||||
}); |
||||
instance.tabBar.open('admin-user-info'); |
||||
}, |
||||
'click .info-tabs button'(e) { |
||||
e.preventDefault(); |
||||
$('.info-tabs button').removeClass('active'); |
||||
$(e.currentTarget).addClass('active'); |
||||
$('.user-info-content').hide(); |
||||
$($(e.currentTarget).attr('href')).show(); |
||||
}, |
||||
'click .load-more'(e, t) { |
||||
e.preventDefault(); |
||||
e.stopPropagation(); |
||||
t.offset.set(t.offset.get() + USERS_COUNT); |
||||
}, |
||||
}); |
||||
@ -1,6 +0,0 @@ |
||||
import './adminInviteUser.html'; |
||||
import './adminUserEdit.html'; |
||||
import './adminUserInfo.html'; |
||||
import './adminUsers.html'; |
||||
import './adminInviteUser'; |
||||
import './adminUsers'; |
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue