[FIX] Administration UI issues (#15934)

pull/15937/head
Tasso Evangelista 6 years ago committed by Guilherme Gazzo
parent 931e793a87
commit de46db8001
  1. 1
      .eslintignore
  2. 3
      .eslintrc
  3. 1
      app/lib/server/methods/removeOAuthService.js
  4. 10
      app/livechat/client/views/app/livechatAppearance.html
  5. 9
      app/livechat/client/views/app/livechatAppearance.js
  6. 1
      app/theme/client/index.js
  7. 1844
      app/theme/client/vendor/jscolor.js
  8. 2
      app/theme/server/variables.js
  9. 228
      app/ui-admin/client/admin.html
  10. 642
      app/ui-admin/client/admin.js
  11. 6
      app/ui-admin/client/index.js
  12. 3
      client/components/admin/settings/Section.js
  13. 4
      client/components/admin/settings/Setting.js
  14. 6
      client/components/admin/settings/SettingsState.js
  15. 10
      client/components/admin/settings/groups/AssetsGroupPage.js
  16. 80
      client/components/admin/settings/groups/OAuthGroupPage.js
  17. 21
      client/components/admin/settings/inputs/AssetSettingInput.js
  18. 6
      client/components/providers/TranslationProvider.js
  19. 1
      client/importPackages.js
  20. 268
      package-lock.json

@ -7,7 +7,6 @@ app/favico/favico.js
app/katex/client/katex/katex.min.js
packages/rocketchat-livechat/assets/rocketchat-livechat.min.js
packages/rocketchat-livechat/assets/rocket-livechat.js
app/theme/client/minicolors/jquery.minicolors.js
app/theme/client/vendor/
app/ui/client/lib/customEventPolyfill.js
app/ui/client/lib/Modernizr.js

@ -5,8 +5,7 @@
"__meteor_bootstrap__" : false,
"__meteor_runtime_config__" : false,
"Assets" : false,
"chrome" : false,
"jscolor" : false
"chrome" : false
},
"plugins": ["react"],
"rules": {

@ -40,5 +40,6 @@ Meteor.methods({
settings.removeById(`Accounts_OAuth_Custom-${ name }-roles_claim`);
settings.removeById(`Accounts_OAuth_Custom-${ name }-merge_roles`);
settings.removeById(`Accounts_OAuth_Custom-${ name }-merge_users`);
settings.removeById(`Accounts_OAuth_Custom-${ name }-show_button`);
},
});

@ -14,10 +14,7 @@
</div>
<div class="input-line">
<label for="color">{{_ "Title_bar_color"}}</label>
<div>
<input type="text" class="preview-settings colorpicker-input rc-input__element" name="color" id="color" value="{{color}}">
<span class="colorpicker-swatch border-component-color" style="background-color: {{color}}"></span>
</div>
<input type="color" class="preview-settings rc-input__element" name="color" id="color" value="{{color}}" />
</div>
<div class="input-line">
<label for="showAgentInfo">{{_ "Show_agent_Info"}}</label>
@ -63,10 +60,7 @@
</div>
<div class="input-line">
<label for="colorOffline">{{_ "Title_bar_color_offline"}}</label>
<div>
<input type="text" class="preview-settings colorpicker-input rc-input__element" name="colorOffline" id="colorOffline" value="{{colorOffline}}">
<span class="colorpicker-swatch border-component-color" style="background-color: {{colorOffline}}"></span>
</div>
<input type="color" class="preview-settings rc-input__element" name="colorOffline" id="colorOffline" value="{{colorOffline}}" />
</div>
<div class="input-line">
<label for="emailOffline">{{_ "Email_address_to_send_offline_messages"}}</label>

@ -1,4 +1,3 @@
/* eslint new-cap: ["error", { "newIsCapExceptions": ["jscolor"] }]*/
import { Meteor } from 'meteor/meteor';
import { ReactiveVar } from 'meteor/reactive-var';
import { Template } from 'meteor/templating';
@ -312,11 +311,3 @@ Template.livechatAppearance.events({
});
},
});
Template.livechatAppearance.onRendered(function() {
Meteor.setTimeout(() => {
$('.colorpicker-input').each((index, el) => {
new jscolor(el);
});
}, 500);
});

@ -1 +0,0 @@
import './vendor/jscolor';

File diff suppressed because it is too large Load Diff

@ -3,8 +3,6 @@ import { settings } from '../../settings';
// TODO: Define registers/getters/setters for packages to work with established
// heirarchy of colors instead of making duplicate definitions
// TODO: Settings pages to show simple separation of major/minor/addon colors
// TODO: Get major colours as swatches for minor colors in minicolors plugin
// TODO: Minicolors settings to use rgb for alphas, hex otherwise
// TODO: Add setting toggle to use defaults for minor colours and hide settings
// New colors, used for shades on solid backgrounds

@ -1,228 +0,0 @@
<template name="admin">
<section class="page-container page-home page-static page-settings">
{{#with group}}
{{#header sectionName=i18nLabel buttons=true}}
<div class="rc-header__section-button">
{{#if hasChanges}}
<button class="rc-button rc-button--cancel discard"><span>{{_ "Cancel"}}</span></button>
{{/if}}
<button class="rc-button rc-button--primary save" disabled="{{$not hasChanges}}"><span>{{_ "Save_changes"}}</span></button>
{{#if $eq _id 'OAuth'}}
<button class="rc-button rc-button--secondary refresh-oauth"><span>{{_ "Refresh_oauth_services"}}</span></button>
<button class="rc-button rc-button--secondary add-custom-oauth"><span>{{_ "Add_custom_oauth"}}</span></button>
{{/if}}
{{#if $eq _id 'Assets'}}
<button class="rc-button rc-button--secondary refresh-clients"><span>{{_ "Apply_and_refresh_all_clients"}}</span></button>
{{/if}}
</div>
{{/header}}
<div class="content">
{{#unless hasSettingPermission}}
<p>{{_ "You_are_not_authorized_to_view_this_page"}}</p>
{{else}}
{{#if description}}
<div class="info">
<p class="settings-description">{{description}}</p>
</div>
{{/if}}
<div class="page-settings rocket-form">
{{#each sections}}
<div class="section {{#if section}}section-collapsed{{/if}}">
{{#if section}}
<div class="section-title expand">
<div class="section-title-text">
{{translateSection section}}
</div>
<div class="section-title-right">
<button class="rc-button rc-button--nude"><i class="icon-angle-down"></i></button>
</div>
</div>
{{/if}}
<div class="section-content border-component-color">
{{#if section}}
{{#if sectionIsCustomOAuth section}}
<div class="section-helper">
{{#with callbackURL section}}
{{{_ "Custom_oauth_helper" .}}}
{{/with}}
</div>
{{/if}}
{{/if}}
{{#each settings}}
<div class="input-line double-col {{#if isSettingChanged _id}}setting-changed{{/if}}" {{isDisabled}}>
<label class="setting-label" title="{{_id}}">{{label}}</label>
<div class="setting-field">
{{#if $eq type 'string'}}
{{#if multiline}}
<textarea class="input-monitor rc-input__element" name="{{_id}}" rows="4" style="height: auto" {{isDisabled}} {{isReadonly}}>{{value}}</textarea>
{{else}}
<input class="input-monitor rc-input__element" type="text" name="{{_id}}" value="{{value}}" placeholder="{{placeholder}}" {{isDisabled}} {{isReadonly}} {{canAutocomplete}}/>
{{/if}}
{{/if}}
{{#if $eq type 'relativeUrl'}}
<input class="input-monitor rc-input__element" type="text" name="{{_id}}" value="{{relativeUrl value}}" placeholder="{{placeholder}}" {{isDisabled}} {{isReadonly}} {{canAutocomplete}}/>
{{/if}}
{{#if $eq type 'password'}}
<input class="input-monitor rc-input__element" type="password" name="{{_id}}" value="{{value}}" placeholder="{{placeholder}}" {{isDisabled}} {{isReadonly}} {{canAutocomplete}}/>
{{/if}}
{{#if $eq type 'int'}}
<input class="input-monitor rc-input__element" type="number" name="{{_id}}" value="{{value}}" placeholder="{{placeholder}}" {{isDisabled}} {{isReadonly}} {{canAutocomplete}}/>
{{/if}}
{{#if $eq type 'boolean'}}
<label><input class="input-monitor" type="radio" name="{{_id}}" value="1" checked="{{$eq value true}}" {{isDisabled}} {{isReadonly}} {{canAutocomplete}}/> {{_ "True"}}</label>
<label><input class="input-monitor" type="radio" name="{{_id}}" value="0" checked="{{$eq value false}}" {{isDisabled}} {{isReadonly}} {{canAutocomplete}}/> {{_ "False"}}</label>
{{/if}}
{{#if $eq type 'select'}}
<div class="rc-select">
<select class="input-monitor rc-select__element" name="{{_id}}" {{isDisabled}} {{isReadonly}}>
{{#each values}}
<option value="{{key}}" selected="{{selectedOption ../_id key}}">{{_ i18nLabel}}</option>
{{/each}}
</select>
{{> icon block="rc-select__arrow" icon="arrow-down" }}
</div>
{{/if}}
{{#if $eq type 'language'}}
<div class="rc-select">
<select class="input-monitor rc-select__element" name="{{_id}}" {{isDisabled}} {{isReadonly}}>
{{#each languages}}
<option value="{{key}}" selected="{{isAppLanguage key}}" dir="auto">{{name}}</option>
{{/each}}
</select>
{{> icon block="rc-select__arrow" icon="arrow-down" }}
</div>
{{/if}}
{{#if $eq type 'color'}}
<div class="horizontal">
{{#if $eq editor 'color'}}
<div class="flex-grow-1">
<input class="input-monitor rc-input__element colorpicker-input" type="text" name="{{_id}}" value="{{value}}" autocomplete="off" {{isDisabled}}/>
<span class="colorpicker-swatch border-component-color" style="background-color: {{value}}"></span>
</div>
{{/if}}
{{#if $eq editor 'expression'}}
<div class="flex-grow-1">
<input class="input-monitor rc-input__element" type="text" name="{{_id}}" value="{{value}}" {{isDisabled}} {{canAutocomplete}}/>
</div>
{{/if}}
<div class="color-editor ">
<select name="color-editor">
{{#each allowedTypes}}
<option value="{{.}}" selected="{{$eq ../editor .}}">{{_ .}}</option>
{{/each}}
</select>
</div>
</div>
<div class="settings-description">Variable name: {{getColorVariable _id}}</div>
{{/if}}
{{#if $eq type 'font'}}
<input class="input-monitor rc-input__element" type="text" name="{{_id}}" value="{{value}}" {{isDisabled}} {{isReadonly}} {{canAutocomplete}}/>
{{/if}}
{{#if $eq type 'code'}}
{{#if isDisabled.disabled}}
{{> CodeMirror name=_id options=(getEditorOptions true) code=(i18nDefaultValue) }}
{{else}}
<div class="code-mirror-box" data-editor-id="{{_id}}">
<div class="title">
{{label}}
</div>
{{> CodeMirror name=_id options=getEditorOptions code=value editorOnBlur=setEditorOnBlur}}
<div class="buttons">
<button class="rc-button rc-button--primary button-fullscreen">{{_ "Full_Screen"}}</button>
<button class="rc-button rc-button--primary button-restore">{{_ "Exit_Full_Screen"}}</button>
</div>
</div>
{{/if}}
{{/if}}
{{#if $eq type 'action'}}
{{#if hasChanges section}}
<span style="line-height: 40px" class="secondary-font-color">{{_ "Save_to_enable_this_action"}}</span>
{{else}}
<button type="button" class="rc-button rc-button--primary action" data-setting="{{_id}}" data-action="{{value}}" {{isDisabled}} >{{_ actionText}}</button>
{{/if}}
{{/if}}
{{#if $eq type 'asset'}}
{{#if value.url}}
<div class="settings-file-preview">
<div class="preview" style="background-image:url({{value.url}}?_dc={{random}});"></div>
<div class="action">
<button type="button" class="rc-button rc-button--cancel delete-asset"><i class="icon-trash"></i>{{_ 'Delete'}}</button>
</div>
</div>
{{else}}
<div class="settings-file-preview">
<div class="preview no-file background-transparent-light secondary-font-color"><i class="icon-upload"></i></div>
<div class="action">
<div class="rc-button rc-button--primary">{{_ 'Select_file'}}
<input type="file" accept="{{assetAccept fileConstraints}}" />
</div>
</div>
</div>
{{/if}}
{{/if}}
{{#if $eq type 'roomPick'}}
<div>
{{> inputAutocomplete settings=autocompleteRoom id=_id name=_id class="search autocomplete rc-input__element" autocomplete="off" disabled=isDisabled.disabled}}
<ul class="selected-rooms">
{{#each selectedRooms}}
<li class="remove-room" data-setting={{../_id}}>{{name}} <i class="icon-cancel"></i></li>
{{/each}}
</ul>
</div>
{{/if}}
{{#if description}}
<div class="settings-description secondary-font-color">{{{RocketChatMarkdownUnescape description}}}</div>
{{/if}}
{{#if alert}}
<div class="settings-alert pending-color pending-background pending-border"><i class="icon-attention"></i>{{{_ alert}}}</div>
{{/if}}
</div>
{{#if showResetButton}}
<button text="{{_ 'Reset'}}" data-setting="{{_id}}" class="reset-setting rc-button rc-button--cancel">
<i class="icon-ccw color-error-contrast"></i>
</button>
{{/if}}
</div>
{{/each}}
{{#unless $eq ../_id 'Assets'}}
<div class="input-line double-col">
<label class="setting-label">{{_ "Reset_section_settings"}}</label>
<div class="setting-field">
<button data-section="{{section}}" class="reset-group rc-button rc-button--cancel">
{{_ "Reset"}}
</button>
</div>
</div>
{{/unless}}
{{#if section}}
{{#if sectionIsCustomOAuth section}}
<div class="submit">
<button class="rc-button rc-button--cancel remove-custom-oauth"><span>{{_ "Remove_custom_oauth"}}</span></button>
</div>
{{/if}}
{{/if}}
</div>
</div>
{{/each}}
</div>
{{/unless}}
</div>
{{/with}}
</section>
</template>

@ -1,642 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';
import { ReactiveVar } from 'meteor/reactive-var';
import { Random } from 'meteor/random';
import { Tracker } from 'meteor/tracker';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { Template } from 'meteor/templating';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import _ from 'underscore';
import s from 'underscore.string';
import toastr from 'toastr';
import { settings } from '../../settings';
import { SideNav, modal } from '../../ui-utils';
import { t, handleError } from '../../utils';
import { PrivateSettingsCachedCollection } from './SettingsCachedCollection';
import { hasAtLeastOnePermission } from '../../authorization/client';
const TempSettings = new Mongo.Collection(null);
const getDefaultSetting = function(settingId) {
return settings.collectionPrivate.findOne({
_id: settingId,
});
};
const setFieldValue = function(settingId, value, type, editor) {
const input = $('.page-settings').find(`[name="${ settingId }"]`);
switch (type) {
case 'boolean':
$('.page-settings').find(`[name="${ settingId }"][value="${ Number(value) }"]`).prop('checked', true).change();
break;
case 'code':
input.next()[0].CodeMirror.setValue(value);
break;
case 'color':
editor = value && value[0] === '#' ? 'color' : 'expression';
input.parents('.horizontal').find('select[name="color-editor"]').val(editor).change();
input.val(value).change();
break;
case 'roomPick':
const selectedRooms = Template.instance().selectedRooms.get();
selectedRooms[settingId] = value;
Template.instance().selectedRooms.set(selectedRooms);
TempSettings.update({ _id: settingId }, { $set: { value, changed: JSON.stringify(settings.collectionPrivate.findOne(settingId).value) !== JSON.stringify(value) } });
break;
default:
input.val(value).change();
}
};
Template.admin.onCreated(function() {
if (settings.cachedCollectionPrivate == null) {
settings.cachedCollectionPrivate = new PrivateSettingsCachedCollection();
settings.collectionPrivate = settings.cachedCollectionPrivate.collection;
settings.cachedCollectionPrivate.init();
}
this.selectedRooms = new ReactiveVar({});
settings.collectionPrivate.find().observe({
added: (data) => {
const selectedRooms = this.selectedRooms.get();
if (data.type === 'roomPick') {
selectedRooms[data._id] = data.value;
this.selectedRooms.set(selectedRooms);
}
TempSettings.insert(data);
},
changed: (data) => {
const selectedRooms = this.selectedRooms.get();
if (data.type === 'roomPick') {
selectedRooms[data._id] = data.value;
this.selectedRooms.set(selectedRooms);
}
TempSettings.update(data._id, data);
},
removed: (data) => {
const selectedRooms = this.selectedRooms.get();
if (data.type === 'roomPick') {
delete selectedRooms[data._id];
this.selectedRooms.set(selectedRooms);
}
TempSettings.remove(data._id);
},
});
});
Template.admin.onDestroyed(function() {
TempSettings.remove({});
});
Template.admin.helpers({
hasSettingPermission() {
return hasAtLeastOnePermission(['view-privileged-setting', 'edit-privileged-setting', 'manage-selected-settings']);
},
languages() {
const languages = TAPi18n.getLanguages();
const result = Object.entries(languages)
.map(([key, language]) => ({ ...language, key: key.toLowerCase() }))
.sort((a, b) => a.key - b.key);
result.unshift({
name: 'Default',
en: 'Default',
key: '',
});
return result;
},
isAppLanguage(key) {
const languageKey = settings.get('Language');
return typeof languageKey === 'string' && languageKey.toLowerCase() === key;
},
group() {
const groupId = FlowRouter.getParam('group');
const group = settings.collectionPrivate.findOne({
_id: groupId,
type: 'group',
});
if (!group) {
return;
}
const rcSettings = settings.collectionPrivate.find({ group: groupId }, { sort: { section: 1, sorter: 1, i18nLabel: 1 } }).fetch();
const sections = {};
Object.keys(rcSettings).forEach((key) => {
const setting = rcSettings[key];
let i18nDefaultQuery;
if (setting.i18nDefaultQuery != null) {
if (_.isString(setting.i18nDefaultQuery)) {
i18nDefaultQuery = JSON.parse(setting.i18nDefaultQuery);
} else {
i18nDefaultQuery = setting.i18nDefaultQuery;
}
if (!_.isArray(i18nDefaultQuery)) {
i18nDefaultQuery = [i18nDefaultQuery];
}
Object.keys(i18nDefaultQuery).forEach((key) => {
const item = i18nDefaultQuery[key];
if (settings.collectionPrivate.findOne(item) != null) {
setting.value = TAPi18n.__(`${ setting._id }_Default`);
}
});
}
const settingSection = setting.section || '';
if (sections[settingSection] == null) {
sections[settingSection] = [];
}
sections[settingSection].push(setting);
});
group.sections = Object.keys(sections).map((key) => {
const value = sections[key];
return {
section: key,
settings: value,
};
});
return group;
},
i18nDefaultValue() {
return TAPi18n.__(`${ this._id }_Default`);
},
isDisabled() {
let enableQuery;
if (this.blocked) {
return {
disabled: 'disabled',
};
}
if (this.enableQuery == null) {
return {};
}
if (_.isString(this.enableQuery)) {
enableQuery = JSON.parse(this.enableQuery);
} else {
enableQuery = this.enableQuery;
}
if (!_.isArray(enableQuery)) {
enableQuery = [enableQuery];
}
let found = 0;
Object.keys(enableQuery).forEach((key) => {
const item = enableQuery[key];
if (TempSettings.findOne(item) != null) {
found++;
}
});
if (found === enableQuery.length) {
return {};
}
return {
disabled: 'disabled',
};
},
isReadonly() {
if (this.readonly === true) {
return {
readonly: 'readonly',
};
}
},
canAutocomplete() {
if (this.autocomplete === false) {
return {
autocomplete: 'off',
};
}
},
hasChanges(section) {
const group = FlowRouter.getParam('group');
const query = {
group,
changed: true,
};
if (section != null) {
if (section === '') {
query.$or = [
{
section: '',
}, {
section: {
$exists: false,
},
},
];
} else {
query.section = section;
}
}
return TempSettings.find(query).count() > 0;
},
isSettingChanged(id) {
return TempSettings.findOne({
_id: id,
}, {
fields: {
changed: 1,
},
}).changed;
},
translateSection(section) {
if (section.indexOf(':') > -1) {
return section;
}
return t(section);
},
label() {
const label = this.i18nLabel || this._id;
if (label) {
return TAPi18n.__(label);
}
},
description() {
let description;
if (this.i18nDescription) {
description = TAPi18n.__(this.i18nDescription);
}
if ((description != null) && description !== this.i18nDescription) {
return description;
}
},
sectionIsCustomOAuth(section) {
return /^Custom OAuth:\s.+/.test(section);
},
callbackURL(section) {
const id = s.strRight(section, 'Custom OAuth: ').toLowerCase();
return Meteor.absoluteUrl(`_oauth/${ id }`);
},
relativeUrl(url) {
return Meteor.absoluteUrl(url);
},
selectedOption(_id, val) {
const option = settings.collectionPrivate.findOne({ _id });
return option && option.value === val;
},
random() {
return Random.id();
},
getEditorOptions(readOnly = false) {
return {
lineNumbers: true,
mode: this.code || 'javascript',
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
foldGutter: true,
matchBrackets: true,
autoCloseBrackets: true,
matchTags: true,
showTrailingSpace: true,
highlightSelectionMatches: true,
readOnly,
};
},
setEditorOnBlur() {
return function(_id) {
if (!$(`.code-mirror-box[data-editor-id="${ _id }"] .CodeMirror`)[0]) {
return;
}
const codeMirror = $(`.code-mirror-box[data-editor-id="${ _id }"] .CodeMirror`)[0].CodeMirror;
if (codeMirror.changeAdded === true) {
return;
}
const onChange = function() {
const value = codeMirror.getValue();
TempSettings.update({ _id }, { $set: { value, changed: settings.collectionPrivate.findOne(_id).value !== value } });
};
const onChangeDelayed = _.debounce(onChange, 500);
codeMirror.on('change', onChangeDelayed);
codeMirror.changeAdded = true;
};
},
assetAccept(fileConstraints) {
if (fileConstraints.extensions && fileConstraints.extensions.length) {
return `.${ fileConstraints.extensions.join(', .') }`;
}
},
autocompleteRoom() {
return {
limit: 10,
// inputDelay: 300
rules: [
{
// @TODO maybe change this 'collection' and/or template
collection: 'CachedChannelList',
subscription: 'channelAndPrivateAutocomplete',
field: 'name',
template: Template.roomSearch,
noMatchTemplate: Template.roomSearchEmpty,
matchAll: true,
selector(match) {
return {
name: match,
};
},
sort: 'name',
},
],
};
},
selectedRooms() {
return Template.instance().selectedRooms.get()[this._id] || [];
},
getColorVariable(color) {
return color.replace(/theme-color-/, '@');
},
showResetButton() {
const setting = TempSettings.findOne({ _id: this._id }, { fields: { value: 1, packageValue: 1 } });
return !this.disableReset && !this.readonly && this.type !== 'asset' && setting.value !== setting.packageValue && !this.blocked;
},
});
Template.admin.events({
'change .input-monitor, keyup .input-monitor': _.throttle(function(e) {
let value = s.trim($(e.target).val());
switch (this.type) {
case 'int':
value = parseInt(value);
break;
case 'boolean':
value = value === '1';
break;
case 'color':
$(e.target).siblings('.colorpicker-swatch').css('background-color', value);
}
TempSettings.update({
_id: this._id,
}, {
$set: {
value,
changed: settings.collectionPrivate.findOne(this._id).value !== value,
},
});
}, 500),
'change select[name=color-editor]'(e) {
const value = s.trim($(e.target).val());
TempSettings.update({ _id: this._id }, { $set: { editor: value } });
settings.collectionPrivate.update({ _id: this._id }, { $set: { editor: value } });
},
'click .rc-header__section-button .discard'() {
const group = FlowRouter.getParam('group');
const query = {
group,
changed: true,
};
const rcSettings = TempSettings.find(query, {
fields: { _id: 1, value: 1, packageValue: 1 },
}).fetch();
rcSettings.forEach(function(setting) {
const oldSetting = settings.collectionPrivate.findOne({ _id: setting._id }, { fields: { value: 1, type: 1, editor: 1 } });
setFieldValue(setting._id, oldSetting.value, oldSetting.type, oldSetting.editor);
});
},
'click .reset-setting'(e) {
e.preventDefault();
let settingId = $(e.target).data('setting');
if (typeof settingId === 'undefined') {
settingId = $(e.target).parent().data('setting');
}
const defaultValue = getDefaultSetting(settingId);
setFieldValue(settingId, defaultValue.packageValue, defaultValue.type, defaultValue.editor);
},
'click .reset-group'(e) {
let rcSettings;
e.preventDefault();
const group = FlowRouter.getParam('group');
const section = $(e.target).data('section');
if (section === '') {
rcSettings = TempSettings.find({ group, section: { $exists: false } }, { fields: { _id: 1 } }).fetch();
} else {
rcSettings = TempSettings.find({ group, section }, { fields: { _id: 1 } }).fetch();
}
rcSettings.forEach(function(setting) {
const defaultValue = getDefaultSetting(setting._id);
setFieldValue(setting._id, defaultValue.packageValue, defaultValue.type, defaultValue.editor);
TempSettings.update({ _id: setting._id }, {
$set: {
value: defaultValue.packageValue,
changed: settings.collectionPrivate.findOne(setting._id).value !== defaultValue.packageValue,
},
});
});
},
'click .rc-header__section-button .save'() {
const group = FlowRouter.getParam('group');
const query = { group, changed: true };
const rcSettings = TempSettings.find(query, { fields: { _id: 1, value: 1, editor: 1 } }).fetch() || [];
if (rcSettings.length === 0) {
return;
}
const failedSettings = [];
settings.batchSet(rcSettings, (err) => {
if (err) {
// Handle error for every settings failed.
err.details.settingIds.forEach((settingId) => {
const error = Object.assign({}, err);
failedSettings.push(settingId);
error.details.settingIds = settingId;
handleError(error);
});
}
rcSettings.forEach((setting) => {
if (!failedSettings.includes(setting._id)) {
TempSettings.update({ _id: setting._id }, { $unset: { changed: 1 } });
}
});
if (rcSettings.some(({ _id }) => _id === 'Language')) {
const lng = Meteor.user().language
|| rcSettings.filter(({ _id }) => _id === 'Language').shift().value
|| 'en';
return TAPi18n._loadLanguage(lng).then(() => toastr.success(TAPi18n.__('Settings_updated', { lng })));
}
toastr.success(TAPi18n.__('Settings_updated'));
});
},
'click .rc-header__section-button .refresh-clients'() {
Meteor.call('refreshClients', function() {
toastr.success(TAPi18n.__('Clients_will_refresh_in_a_few_seconds'));
});
},
'click .rc-header__section-button .add-custom-oauth'() {
const config = {
title: TAPi18n.__('Add_custom_oauth'),
text: TAPi18n.__('Give_a_unique_name_for_the_custom_oauth'),
type: 'input',
showCancelButton: true,
closeOnConfirm: true,
inputPlaceholder: TAPi18n.__('Custom_oauth_unique_name'),
};
modal.open(config, function(inputValue) {
if (inputValue === false) {
return false;
}
if (inputValue === '') {
modal.showInputError(TAPi18n.__('Name_cant_be_empty'));
return false;
}
Meteor.call('addOAuthService', inputValue, function(err) {
if (err) {
handleError(err);
}
});
});
},
'click .rc-header__section-button .refresh-oauth'() {
toastr.info(TAPi18n.__('Refreshing'));
return Meteor.call('refreshOAuthService', function(err) {
if (err) {
return handleError(err);
}
return toastr.success(TAPi18n.__('Done'));
});
},
'click .remove-custom-oauth'() {
const name = this.section.replace('Custom OAuth: ', '');
const config = {
title: TAPi18n.__('Are_you_sure'),
type: 'warning',
showCancelButton: true,
confirmButtonColor: '#DD6B55',
confirmButtonText: TAPi18n.__('Yes_delete_it'),
cancelButtonText: TAPi18n.__('Cancel'),
closeOnConfirm: true,
};
modal.open(config, function() {
Meteor.call('removeOAuthService', name);
});
},
'click .delete-asset'() {
Meteor.call('unsetAsset', this.asset);
},
'change input[type=file]'(ev) {
const e = ev.originalEvent || ev;
let { files } = e.target;
if (!files || files.length === 0) {
if (e.dataTransfer && e.dataTransfer.files) {
files = e.dataTransfer.files;
} else {
files = [];
}
}
Object.keys(files).forEach((key) => {
const blob = files[key];
toastr.info(TAPi18n.__('Uploading_file'));
const reader = new FileReader();
reader.readAsBinaryString(blob);
reader.onloadend = () => Meteor.call('setAsset', reader.result, blob.type, this.asset, function(err) {
if (err != null) {
handleError(err);
console.log(err);
return;
}
return toastr.success(TAPi18n.__('File_uploaded'));
});
});
},
'click .expand'(e) {
const sectionTitle = e.currentTarget;
const section = sectionTitle.closest('.section');
const button = sectionTitle.querySelector('button');
const i = button.querySelector('i');
sectionTitle.classList.remove('expand');
sectionTitle.classList.add('collapse');
section.classList.remove('section-collapsed');
button.setAttribute('title', TAPi18n.__('Collapse'));
i.className = 'icon-angle-up';
$('.CodeMirror').each(function(index, codeMirror) {
codeMirror.CodeMirror.refresh();
});
},
'click .collapse'(e) {
const sectionTitle = e.currentTarget;
const section = sectionTitle.closest('.section');
const button = sectionTitle.querySelector('button');
const i = button.querySelector('i');
sectionTitle.classList.remove('collapse');
sectionTitle.classList.add('expand');
section.classList.add('section-collapsed');
button.setAttribute('title', TAPi18n.__('Expand'));
i.className = 'icon-angle-down';
},
'click button.action'() {
if (this.type !== 'action') {
return;
}
Meteor.call(this.value, function(err, data) {
if (err != null) {
err.details = _.extend(err.details || {}, {
errorTitle: 'Error',
});
handleError(err);
return;
}
const args = [data.message].concat(data.params);
toastr.success(TAPi18n.__.apply(TAPi18n, args), TAPi18n.__('Success'));
});
},
'click .button-fullscreen'() {
const codeMirrorBox = $(`.code-mirror-box[data-editor-id="${ this._id }"]`);
codeMirrorBox.addClass('code-mirror-box-fullscreen content-background-color');
codeMirrorBox.find('.CodeMirror')[0].CodeMirror.refresh();
},
'click .button-restore'() {
const codeMirrorBox = $(`.code-mirror-box[data-editor-id="${ this._id }"]`);
codeMirrorBox.removeClass('code-mirror-box-fullscreen content-background-color');
codeMirrorBox.find('.CodeMirror')[0].CodeMirror.refresh();
},
'autocompleteselect .autocomplete'(event, instance, doc) {
const selectedRooms = instance.selectedRooms.get();
selectedRooms[this.id] = (selectedRooms[this.id] || []).concat(doc);
instance.selectedRooms.set(selectedRooms);
const value = selectedRooms[this.id];
TempSettings.update({ _id: this.id }, { $set: { value, changed: JSON.stringify(settings.collectionPrivate.findOne(this.id).value) !== JSON.stringify(value) } });
event.currentTarget.value = '';
event.currentTarget.focus();
},
'click .remove-room'(event, instance) {
const docId = this._id;
const settingId = event.currentTarget.getAttribute('data-setting');
const selectedRooms = instance.selectedRooms.get();
selectedRooms[settingId] = _.reject(selectedRooms[settingId] || [], function(setting) {
return setting._id === docId;
});
instance.selectedRooms.set(selectedRooms);
const value = selectedRooms[settingId];
TempSettings.update({ _id: settingId }, {
$set: {
value,
changed: JSON.stringify(settings.collectionPrivate.findOne(settingId).value) !== JSON.stringify(value),
},
});
},
});
Template.admin.onRendered(function() {
Tracker.afterFlush(function() {
SideNav.setFlex('adminFlex');
SideNav.openFlex();
});
Tracker.autorun(function() {
const hasColor = TempSettings.find({
group: FlowRouter.getParam('group'),
type: 'color',
}, { fields: { _id: 1, editor: 1 } }).fetch().length;
if (hasColor) {
Meteor.setTimeout(function() {
$('.colorpicker-input').each(function(index, el) {
if (!el._jscLinkedInstance) {
new jscolor(el); //eslint-disable-line
}
});
}, 400);
}
});
});

@ -1,9 +1,3 @@
import './admin.html';
import './adminFlex.html';
import './admin';
import './adminFlex';
import './routes';
// import './users/adminUserChannels';
// import './users/adminUserChannels.html';

@ -18,14 +18,13 @@ export function Section({ children, groupId, hasReset = true, help, sectionName,
noncollapsible={solo || !section.name}
title={section.name && t(section.name)}
>
{help && <Paragraph hintColor>{help}</Paragraph>}
{help && <Paragraph><Text hintColor>{help}</Text></Paragraph>}
<FieldGroup>
{section.settings.map((settingId) => <Setting key={settingId} settingId={settingId} />)}
{hasReset && section.canReset && <Button
children={t('Reset_section_settings')}
className='reset-group'
danger
data-section={section.name}
ghost

@ -91,11 +91,11 @@ export function Setting({ settingId }) {
const {
_id,
disabled,
disableReset,
readonly,
type,
packageValue,
blocked,
i18nLabel,
i18nDescription,
alert,
@ -104,7 +104,7 @@ export function Setting({ settingId }) {
const label = (i18nLabel && t(i18nLabel)) || (_id || t(_id));
const hint = useMemo(() => t.has(i18nDescription) && <MarkdownText>{t(i18nDescription)}</MarkdownText>, [i18nDescription]);
const callout = useMemo(() => alert && <RawText>{t(alert)}</RawText>, [alert]);
const hasResetButton = !disableReset && !readonly && type !== 'asset' && value !== packageValue && !blocked;
const hasResetButton = !disableReset && !readonly && type !== 'asset' && value !== packageValue && !disabled;
return <MemoizedSetting
type={type}

@ -1,5 +1,6 @@
import { Meteor } from 'meteor/meteor';
import { Mongo } from 'meteor/mongo';
import { Tracker } from 'meteor/tracker';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import React, { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useReducer, useRef, useState } from 'react';
import toastr from 'toastr';
@ -310,10 +311,11 @@ export const useSection = (groupId, sectionName) => {
const canReset = useSelector((state) => filterSettings(state.settings).some(({ value, packageValue }) => value !== packageValue));
const settingsIds = useSelector((state) => filterSettings(state.settings).map(({ _id }) => _id), (a, b) => a.length === b.length && a.join() === b.join());
const { stateRef, hydrate } = useContext(SettingsContext);
const { stateRef, hydrate, isDisabled } = useContext(SettingsContext);
const reset = useEventCallback((filterSettings, { current: state }, hydrate) => {
const settings = filterSettings(state.settings);
const settings = filterSettings(state.settings)
.filter((setting) => Tracker.nonreactive(() => !isDisabled(setting))); // Ignore disabled settings
const persistedSettings = filterSettings(state.persistedSettings);
const changes = settings.map((setting) => {

@ -1,6 +1,8 @@
import { Button } from '@rocket.chat/fuselage';
import React from 'react';
import toastr from 'toastr';
import { call } from '../../../../../app/ui-utils/client/lib/callMethod';
import { useTranslation } from '../../../providers/TranslationProvider';
import { GroupPage } from '../GroupPage';
import { Section } from '../Section';
@ -9,8 +11,14 @@ export function AssetsGroupPage({ group }) {
const solo = group.sections.length === 1;
const t = useTranslation();
const handleApplyAndRefreshAllClientsButtonClick = () => {
call('refreshClients').then(() => {
toastr.success(t('Clients_will_refresh_in_a_few_seconds'));
});
};
return <GroupPage group={group} headerButtons={<>
<Button className='refresh-clients'>{t('Apply_and_refresh_all_clients')}</Button>
<Button onClick={handleApplyAndRefreshAllClientsButtonClick}>{t('Apply_and_refresh_all_clients')}</Button>
</>}>
{group.sections.map((sectionName) => <Section
key={sectionName}

@ -1,8 +1,11 @@
import { Button } from '@rocket.chat/fuselage';
import { Meteor } from 'meteor/meteor';
import React from 'react';
import toastr from 'toastr';
import s from 'underscore.string';
import { call } from '../../../../../app/ui-utils/client/lib/callMethod';
import { modal } from '../../../../../app/ui-utils/client/lib/modal';
import { RawText } from '../../../basic/RawText';
import { useTranslation } from '../../../providers/TranslationProvider';
import { GroupPage } from '../GroupPage';
@ -19,22 +22,69 @@ export function OAuthGroupPage({ group }) {
return Meteor.absoluteUrl(`_oauth/${ id }`);
};
const handleRefreshOAuthServicesButtonClick = () => {
toastr.info(t('Refreshing'));
call('refreshOAuthService').then(() => {
toastr.success(t('Done'));
});
};
const handleAddCustomOAuthButtonClick = () => {
modal.open({
title: t('Add_custom_oauth'),
text: t('Give_a_unique_name_for_the_custom_oauth'),
type: 'input',
showCancelButton: true,
closeOnConfirm: true,
inputPlaceholder: t('Custom_oauth_unique_name'),
}, (inputValue) => {
if (inputValue === false) {
return false;
}
if (inputValue === '') {
modal.showInputError(t('Name_cant_be_empty'));
return false;
}
call('addOAuthService', inputValue);
});
};
return <GroupPage group={group} headerButtons={<>
<Button className='refresh-oauth'>{t('Refresh_oauth_services')}</Button>
<Button className='add-custom-oauth'>{t('Add_custom_oauth')}</Button>
<Button onClick={handleRefreshOAuthServicesButtonClick}>{t('Refresh_oauth_services')}</Button>
<Button onClick={handleAddCustomOAuthButtonClick}>{t('Add_custom_oauth')}</Button>
</>}>
{group.sections.map((sectionName) => (sectionIsCustomOAuth(sectionName)
? <Section
key={sectionName}
groupId={group._id}
help={<RawText>{t('Custom_oauth_helper', callbackURL(sectionName))}</RawText>}
sectionName={sectionName}
solo={solo}
>
<div className='submit'>
<Button cancel className='remove-custom-oauth'>{t('Remove_custom_oauth')}</Button>
</div>
</Section>
: <Section key={sectionName} groupId={group._id} sectionName={sectionName} solo={solo} />))}
{group.sections.map((sectionName) => {
if (sectionIsCustomOAuth(sectionName)) {
const id = s.strRight(sectionName, 'Custom OAuth: ').toLowerCase();
const handleRemoveCustomOAuthButtonClick = () => {
modal.open({
title: t('Are_you_sure'),
type: 'warning',
showCancelButton: true,
confirmButtonColor: '#DD6B55',
confirmButtonText: t('Yes_delete_it'),
cancelButtonText: t('Cancel'),
closeOnConfirm: true,
}, () => {
call('removeOAuthService', id);
});
};
return <Section
key={sectionName}
groupId={group._id}
help={<RawText>{t('Custom_oauth_helper', callbackURL(sectionName))}</RawText>}
sectionName={sectionName}
solo={solo}
>
<div className='submit'>
<Button danger onClick={handleRemoveCustomOAuthButtonClick}>{t('Remove_custom_oauth')}</Button>
</div>
</Section>;
}
return <Section key={sectionName} groupId={group._id} sectionName={sectionName} solo={solo} />;
})}
</GroupPage>;
}

@ -1,11 +1,9 @@
import { Button, Icon, Label } from '@rocket.chat/fuselage';
import { Meteor } from 'meteor/meteor';
import { Random } from 'meteor/random';
import { TAPi18n } from 'meteor/rocketchat:tap-i18n';
import React from 'react';
import toastr from 'toastr';
import { handleError } from '../../../../../app/utils/client';
import { call } from '../../../../../app/ui-utils/client/lib/callMethod';
import { useTranslation } from '../../../providers/TranslationProvider';
export function AssetSettingInput({
@ -30,22 +28,19 @@ export function AssetSettingInput({
}
Object.values(files).forEach((blob) => {
toastr.info(TAPi18n.__('Uploading_file'));
toastr.info(t('Uploading_file'));
const reader = new FileReader();
reader.readAsBinaryString(blob);
reader.onloadend = () => Meteor.call('setAsset', reader.result, blob.type, asset, function(err) {
if (err != null) {
handleError(err);
console.log(err);
return;
}
return toastr.success(TAPi18n.__('File_uploaded'));
});
reader.onloadend = () =>
call('setAsset', reader.result, blob.type, asset)
.then(() => {
toastr.success(t('File_uploaded'));
});
});
};
const handleDeleteButtonClick = () => {
Meteor.call('unsetAsset', asset);
call('unsetAsset', asset);
};
return <>

@ -3,9 +3,7 @@ import { TAPi18n, TAPi18next } from 'meteor/rocketchat:tap-i18n';
import { useReactiveValue } from '../../hooks/useReactiveValue';
const translate = function(key) {
return key;
};
const translate = (key) => key;
translate.has = () => true;
@ -34,7 +32,7 @@ const createContextValue = (language) => {
});
};
const has = (key, { lng = language, ...options } = {}) => TAPi18next.exists(key, { ...options, lng });
const has = (key, { lng = language, ...options } = {}) => !!key && TAPi18next.exists(key, { ...options, lng });
translate.has = has;

@ -69,7 +69,6 @@ import '../app/slashcommands-topic/client';
import '../app/slashcommands-unarchiveroom/client';
import '../app/slider';
import '../app/spotify/client';
import '../app/theme/client';
import '../app/tokenpass/client';
import '../app/ui';
import '../app/ui-account';

268
package-lock.json generated

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save