Version bump
This commit is contained in:
commit
edfd3967ab
7
.gitattributes
vendored
7
.gitattributes
vendored
@ -1,11 +1,14 @@
|
||||
# Set default behaviour, in case users don't have core.autocrlf set.
|
||||
* text=auto
|
||||
|
||||
# Explicitly declare text files we want to always be normalized and converted
|
||||
# Treat email fixtures as binary files so CRLF are not converted to LF.
|
||||
*.eml binary
|
||||
|
||||
# Explicitly declare text files we want to always be normalized and converted
|
||||
# to native line endings on checkout.
|
||||
*.yml text
|
||||
|
||||
# Custom for Visual Studio, very unlikely, but lets keep it
|
||||
# Custom for Visual Studio, very unlikely, but lets keep it
|
||||
*.cs diff=csharp
|
||||
*.sln merge=union
|
||||
*.csproj merge=union
|
||||
|
||||
6
Brewfile
6
Brewfile
@ -1,11 +1,5 @@
|
||||
# Install development dependencies on Mac OS X using Homebrew (http://mxcl.github.com/homebrew)
|
||||
|
||||
# add this repo to Homebrew's sources
|
||||
tap 'homebrew/dupes'
|
||||
|
||||
# install the gcc compiler required for ruby
|
||||
brew 'apple-gcc42'
|
||||
|
||||
# you probably already have git installed; ensure that it is the latest version
|
||||
brew 'git'
|
||||
|
||||
|
||||
18
Gemfile
18
Gemfile
@ -2,8 +2,7 @@ source 'https://rubygems.org'
|
||||
# if there is a super emergency and rubygems is playing up, try
|
||||
#source 'http://production.cf.rubygems.org'
|
||||
|
||||
# does not install in linux ATM, so hack this for now
|
||||
gem 'bootsnap', require: false
|
||||
gem 'bootsnap', require: false, platform: :mri
|
||||
|
||||
def rails_master?
|
||||
ENV["RAILS_MASTER"] == '1'
|
||||
@ -36,7 +35,7 @@ gem 'redis-namespace'
|
||||
|
||||
gem 'active_model_serializers', '~> 0.8.3'
|
||||
|
||||
gem 'onebox', '1.8.38'
|
||||
gem 'onebox', '1.8.42'
|
||||
|
||||
gem 'http_accept_language', '~>2.0.5', require: false
|
||||
|
||||
@ -49,9 +48,10 @@ gem 'message_bus'
|
||||
|
||||
gem 'rails_multisite'
|
||||
|
||||
gem 'fast_xs'
|
||||
gem 'fast_xs', platform: :mri
|
||||
|
||||
gem 'fast_xor'
|
||||
# may move to xorcist post: https://github.com/fny/xorcist/issues/4
|
||||
gem 'fast_xor', platform: :mri
|
||||
|
||||
gem 'fastimage'
|
||||
|
||||
@ -141,7 +141,7 @@ end
|
||||
# this is an optional gem, it provides a high performance replacement
|
||||
# to String#blank? a method that is called quite frequently in current
|
||||
# ActiveRecord, this may change in the future
|
||||
gem 'fast_blank'
|
||||
gem 'fast_blank', platform: :mri
|
||||
|
||||
# this provides a very efficient lru cache
|
||||
gem 'lru_redux'
|
||||
@ -155,7 +155,7 @@ gem 'htmlentities', require: false
|
||||
gem 'flamegraph', require: false
|
||||
gem 'rack-mini-profiler', require: false
|
||||
|
||||
gem 'unicorn', require: false
|
||||
gem 'unicorn', require: false, platform: :mri
|
||||
gem 'puma', require: false
|
||||
gem 'rbtrace', require: false, platform: :mri
|
||||
gem 'gc_tracer', require: false, platform: :mri
|
||||
@ -175,9 +175,13 @@ gem 'logster'
|
||||
|
||||
gem 'sassc', require: false
|
||||
|
||||
gem 'rotp'
|
||||
gem 'rqrcode'
|
||||
|
||||
if ENV["IMPORT"] == "1"
|
||||
gem 'mysql2'
|
||||
gem 'redcarpet'
|
||||
gem 'sqlite3', '~> 1.3.13'
|
||||
gem 'ruby-bbcode-to-md', github: 'nlalonde/ruby-bbcode-to-md'
|
||||
gem 'reverse_markdown'
|
||||
end
|
||||
|
||||
29
Gemfile.lock
29
Gemfile.lock
@ -63,9 +63,9 @@ GEM
|
||||
coderay (>= 1.0.0)
|
||||
erubis (>= 2.6.6)
|
||||
rack (>= 0.9.0)
|
||||
binding_of_caller (0.7.2)
|
||||
binding_of_caller (0.8.0)
|
||||
debug_inspector (>= 0.0.1)
|
||||
bootsnap (1.0.0)
|
||||
bootsnap (1.1.8)
|
||||
msgpack (~> 1.0)
|
||||
builder (3.2.3)
|
||||
bullet (5.5.1)
|
||||
@ -73,13 +73,14 @@ GEM
|
||||
uniform_notifier (~> 1.10.0)
|
||||
byebug (9.0.6)
|
||||
certified (1.0.0)
|
||||
chunky_png (1.3.8)
|
||||
coderay (1.1.2)
|
||||
concurrent-ruby (1.0.5)
|
||||
connection_pool (2.2.1)
|
||||
cppjieba_rb (0.3.0)
|
||||
crack (0.4.3)
|
||||
safe_yaml (~> 1.0.0)
|
||||
crass (1.0.2)
|
||||
crass (1.0.3)
|
||||
debug_inspector (0.0.3)
|
||||
diff-lcs (1.3)
|
||||
discourse-qunit-rails (0.0.11)
|
||||
@ -183,14 +184,14 @@ GEM
|
||||
metaclass (~> 0.0.1)
|
||||
mock_redis (0.17.3)
|
||||
moneta (1.0.0)
|
||||
msgpack (1.1.0)
|
||||
multi_json (1.12.1)
|
||||
msgpack (1.2.4)
|
||||
multi_json (1.13.1)
|
||||
multi_xml (0.6.0)
|
||||
multipart-post (2.0.0)
|
||||
mustache (1.0.5)
|
||||
nokogiri (1.8.2)
|
||||
mini_portile2 (~> 2.3.0)
|
||||
nokogumbo (1.4.13)
|
||||
nokogumbo (1.5.0)
|
||||
nokogiri
|
||||
oauth (0.5.1)
|
||||
oauth2 (1.3.1)
|
||||
@ -199,7 +200,7 @@ GEM
|
||||
multi_json (~> 1.3)
|
||||
multi_xml (~> 0.5)
|
||||
rack (>= 1.2, < 3)
|
||||
oj (3.1.0)
|
||||
oj (3.4.0)
|
||||
omniauth (1.6.1)
|
||||
hashie (>= 3.4.6, < 3.6.0)
|
||||
rack (>= 1.6.2, < 3)
|
||||
@ -228,8 +229,7 @@ GEM
|
||||
omniauth-twitter (1.3.0)
|
||||
omniauth-oauth (~> 1.1)
|
||||
rack
|
||||
onebox (1.8.38)
|
||||
fast_blank (>= 1.0.0)
|
||||
onebox (1.8.42)
|
||||
htmlentities (~> 4.3)
|
||||
moneta (~> 1.0)
|
||||
multi_json (~> 1.11)
|
||||
@ -298,6 +298,9 @@ GEM
|
||||
redis (~> 3.0, >= 3.0.4)
|
||||
request_store (1.3.2)
|
||||
rinku (2.0.2)
|
||||
rotp (3.3.0)
|
||||
rqrcode (0.10.1)
|
||||
chunky_png (~> 1.0)
|
||||
rspec (3.6.0)
|
||||
rspec-core (~> 3.6.0)
|
||||
rspec-expectations (~> 3.6.0)
|
||||
@ -338,10 +341,10 @@ GEM
|
||||
nokogiri (>= 1.6.0)
|
||||
ruby_dep (1.5.0)
|
||||
safe_yaml (1.0.4)
|
||||
sanitize (4.5.0)
|
||||
sanitize (4.6.0)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.4.4)
|
||||
nokogumbo (~> 1.4.1)
|
||||
nokogumbo (~> 1.4)
|
||||
sass (3.4.24)
|
||||
sassc (1.11.2)
|
||||
bundler
|
||||
@ -461,7 +464,7 @@ DEPENDENCIES
|
||||
omniauth-oauth2
|
||||
omniauth-openid
|
||||
omniauth-twitter
|
||||
onebox (= 1.8.38)
|
||||
onebox (= 1.8.42)
|
||||
openid-redis-store
|
||||
pg (~> 0.21.0)
|
||||
pry-nav
|
||||
@ -479,6 +482,8 @@ DEPENDENCIES
|
||||
redis
|
||||
redis-namespace
|
||||
rinku
|
||||
rotp
|
||||
rqrcode
|
||||
rspec
|
||||
rspec-html-matchers
|
||||
rspec-rails
|
||||
|
||||
@ -57,7 +57,6 @@ Plus *lots* of Ruby Gems, a complete list of which is at [/master/Gemfile](https
|
||||
## Contributing
|
||||
|
||||
[](https://travis-ci.org/discourse/discourse)
|
||||
[](https://codeclimate.com/github/discourse/discourse)
|
||||
|
||||
Discourse is **100% free** and **open source**. We encourage and support an active, healthy community that
|
||||
accepts contributions from the public – including you!
|
||||
|
||||
@ -1,96 +1,10 @@
|
||||
import BufferedContent from 'discourse/mixins/buffered-content';
|
||||
import SiteSetting from 'admin/models/site-setting';
|
||||
import { propertyNotEqual } from 'discourse/lib/computed';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import { categoryLinkHTML } from 'discourse/helpers/category-link';
|
||||
|
||||
const CustomTypes = ['bool', 'enum', 'list', 'url_list', 'host_list', 'category_list', 'value_list'];
|
||||
|
||||
export default Ember.Component.extend(BufferedContent, {
|
||||
classNameBindings: [':row', ':setting', 'setting.overridden', 'typeClass'],
|
||||
content: Ember.computed.alias('setting'),
|
||||
dirty: propertyNotEqual('buffered.value', 'setting.value'),
|
||||
validationMessage: null,
|
||||
|
||||
@computed("setting", "buffered.value")
|
||||
preview(setting, value) {
|
||||
// A bit hacky, but allows us to use helpers
|
||||
if (setting.get('setting') === 'category_style') {
|
||||
let category = this.site.get('categories.firstObject');
|
||||
if (category) {
|
||||
return categoryLinkHTML(category, {
|
||||
categoryStyle: value
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let preview = setting.get('preview');
|
||||
if (preview) {
|
||||
return new Handlebars.SafeString("<div class='preview'>" + preview.replace(/\{\{value\}\}/g, value) + "</div>");
|
||||
}
|
||||
},
|
||||
|
||||
@computed('componentType')
|
||||
typeClass(componentType) {
|
||||
return componentType.replace(/\_/g, '-');
|
||||
},
|
||||
|
||||
@computed("setting.setting")
|
||||
settingName(setting) {
|
||||
return setting.replace(/\_/g, ' ');
|
||||
},
|
||||
|
||||
@computed("setting.type")
|
||||
componentType(type) {
|
||||
return CustomTypes.indexOf(type) !== -1 ? type : 'string';
|
||||
},
|
||||
|
||||
@computed("typeClass")
|
||||
componentName(typeClass) {
|
||||
return "site-settings/" + typeClass;
|
||||
},
|
||||
|
||||
_watchEnterKey: function() {
|
||||
const self = this;
|
||||
this.$().on("keydown.site-setting-enter", ".input-setting-string", function (e) {
|
||||
if (e.keyCode === 13) { // enter key
|
||||
self._save();
|
||||
}
|
||||
});
|
||||
}.on('didInsertElement'),
|
||||
|
||||
_removeBindings: function() {
|
||||
this.$().off("keydown.site-setting-enter");
|
||||
}.on("willDestroyElement"),
|
||||
import SettingComponent from 'admin/mixins/setting-component';
|
||||
|
||||
export default Ember.Component.extend(BufferedContent, SettingComponent, {
|
||||
_save() {
|
||||
const setting = this.get('buffered'),
|
||||
action = SiteSetting.update(setting.get('setting'), setting.get('value'));
|
||||
action.then(() => {
|
||||
this.set('validationMessage', null);
|
||||
this.commitBuffer();
|
||||
}).catch((e) => {
|
||||
if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) {
|
||||
this.set('validationMessage', e.jqXHR.responseJSON.errors[0]);
|
||||
} else {
|
||||
this.set('validationMessage', I18n.t('generic_error'));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
save() {
|
||||
this._save();
|
||||
},
|
||||
|
||||
resetDefault() {
|
||||
this.set('buffered.value', this.get('setting.default'));
|
||||
this._save();
|
||||
},
|
||||
|
||||
cancel() {
|
||||
this.rollbackBuffer();
|
||||
}
|
||||
const setting = this.get('buffered');
|
||||
return SiteSetting.update(setting.get('setting'), setting.get('value'));
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@ -6,7 +6,7 @@ export default Ember.Component.extend({
|
||||
enabled: {
|
||||
get(value) {
|
||||
if (Ember.isEmpty(value)) { return false; }
|
||||
return value === "true";
|
||||
return value.toString() === "true";
|
||||
},
|
||||
set(value) {
|
||||
this.set("value", value ? "true" : "false");
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
import BufferedContent from 'discourse/mixins/buffered-content';
|
||||
import SettingComponent from 'admin/mixins/setting-component';
|
||||
|
||||
export default Ember.Component.extend(BufferedContent, SettingComponent, {
|
||||
layoutName: 'admin/templates/components/site-setting',
|
||||
_save() {
|
||||
return this.get('model').saveSettings(this.get('setting.setting'), this.get('buffered.value'));
|
||||
}
|
||||
});
|
||||
@ -6,11 +6,22 @@ export default Ember.Controller.extend({
|
||||
section: null,
|
||||
|
||||
targets: [
|
||||
{id: 0, name: I18n.t('admin.customize.theme.common')},
|
||||
{id: 1, name: I18n.t('admin.customize.theme.desktop')},
|
||||
{id: 2, name: I18n.t('admin.customize.theme.mobile')}
|
||||
{ id: 0, name: 'common' },
|
||||
{ id: 1, name: 'desktop' },
|
||||
{ id: 2, name: 'mobile' },
|
||||
{ id: 3, name: 'settings' }
|
||||
],
|
||||
|
||||
fieldsForTarget: function (target) {
|
||||
const common = ["scss", "head_tag", "header", "after_header", "body_tag", "footer"];
|
||||
switch(target) {
|
||||
case "common": return [...common, "embedded_scss"];
|
||||
case "desktop": return common;
|
||||
case "mobile": return common;
|
||||
case "settings": return ["yaml"];
|
||||
}
|
||||
},
|
||||
|
||||
@computed('onlyOverridden')
|
||||
showCommon() {
|
||||
return this.shouldShow('common');
|
||||
@ -26,6 +37,11 @@ export default Ember.Controller.extend({
|
||||
return this.shouldShow('mobile');
|
||||
},
|
||||
|
||||
@computed('onlyOverridden', 'model.remote_theme')
|
||||
showSettings() {
|
||||
return this.shouldShow('settings') && !this.get('model.remote_theme');
|
||||
},
|
||||
|
||||
@observes('onlyOverridden')
|
||||
onlyOverriddenChanged() {
|
||||
if (this.get('onlyOverridden')) {
|
||||
@ -51,27 +67,19 @@ export default Ember.Controller.extend({
|
||||
currentTarget: 0,
|
||||
|
||||
setTargetName: function(name) {
|
||||
let target;
|
||||
switch(name) {
|
||||
case "common": target = 0; break;
|
||||
case "desktop": target = 1; break;
|
||||
case "mobile": target = 2; break;
|
||||
}
|
||||
|
||||
this.set("currentTarget", target);
|
||||
const target = this.get('targets').find(t => t.name === name);
|
||||
this.set("currentTarget", target && target.id);
|
||||
},
|
||||
|
||||
@computed("currentTarget")
|
||||
currentTargetName(target) {
|
||||
switch(parseInt(target)) {
|
||||
case 0: return "common";
|
||||
case 1: return "desktop";
|
||||
case 2: return "mobile";
|
||||
}
|
||||
currentTargetName(id) {
|
||||
const target = this.get('targets').find(t => t.id === parseInt(id, 10));
|
||||
return target && target.name;
|
||||
},
|
||||
|
||||
@computed("fieldName")
|
||||
activeSectionMode(fieldName) {
|
||||
if (fieldName === "yaml") return "yaml";
|
||||
return fieldName && fieldName.indexOf("scss") > -1 ? "scss" : "html";
|
||||
},
|
||||
|
||||
@ -96,15 +104,9 @@ export default Ember.Controller.extend({
|
||||
}
|
||||
},
|
||||
|
||||
@computed("currentTarget", "onlyOverridden")
|
||||
@computed("currentTargetName", "onlyOverridden")
|
||||
fields(target, onlyOverridden) {
|
||||
let fields = [
|
||||
"scss", "head_tag", "header", "after_header", "body_tag", "footer"
|
||||
];
|
||||
|
||||
if (parseInt(target) === 0) {
|
||||
fields.push("embedded_scss");
|
||||
}
|
||||
let fields = this.fieldsForTarget(target);
|
||||
|
||||
if (onlyOverridden) {
|
||||
const model = this.get("model");
|
||||
@ -155,5 +157,4 @@ export default Ember.Controller.extend({
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@ -2,6 +2,7 @@ import { default as computed } from 'ember-addons/ember-computed-decorators';
|
||||
import { url } from 'discourse/lib/computed';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
import showModal from 'discourse/lib/show-modal';
|
||||
import ThemeSettings from 'admin/models/theme-settings';
|
||||
|
||||
const THEME_UPLOAD_VAR = 2;
|
||||
|
||||
@ -30,7 +31,7 @@ export default Ember.Controller.extend({
|
||||
return text + ": " + localized.join(" , ");
|
||||
}
|
||||
};
|
||||
['common','desktop','mobile'].forEach(target=> {
|
||||
['common', 'desktop', 'mobile'].forEach(target => {
|
||||
descriptions.push(description(target));
|
||||
});
|
||||
return descriptions.reject(d=>Em.isBlank(d));
|
||||
@ -77,6 +78,16 @@ export default Ember.Controller.extend({
|
||||
return themes;
|
||||
},
|
||||
|
||||
@computed("model.settings")
|
||||
settings(settings) {
|
||||
return settings.map(setting => ThemeSettings.create(setting));
|
||||
},
|
||||
|
||||
@computed("settings")
|
||||
hasSettings(settings) {
|
||||
return settings.length > 0;
|
||||
},
|
||||
|
||||
downloadUrl: url('model.id', '/admin/themes/%@'),
|
||||
|
||||
actions: {
|
||||
|
||||
@ -19,6 +19,11 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
||||
|
||||
primaryGroupDirty: propertyNotEqual('originalPrimaryGroupId', 'model.primary_group_id'),
|
||||
|
||||
canDisableSecondFactor: Ember.computed.and(
|
||||
'model.second_factor_enabled',
|
||||
'model.can_disable_second_factor'
|
||||
),
|
||||
|
||||
automaticGroups: function() {
|
||||
return this.get("model.automaticGroups").map((g) => g.name).join(", ");
|
||||
}.property("model.automaticGroups"),
|
||||
@ -62,7 +67,16 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
||||
silence() { return this.get("model").silence(); },
|
||||
deleteAllPosts() { return this.get("model").deleteAllPosts(); },
|
||||
anonymize() { return this.get('model').anonymize(); },
|
||||
destroy() { return this.get('model').destroy(); },
|
||||
disableSecondFactor() { return this.get('model').disableSecondFactor(); },
|
||||
|
||||
destroy() {
|
||||
const postCount = this.get('model.post_count');
|
||||
if (postCount <= 5) {
|
||||
return this.get('model').destroy({ deletePosts: true });
|
||||
} else {
|
||||
return this.get('model').destroy();
|
||||
}
|
||||
},
|
||||
|
||||
viewActionLogs() {
|
||||
this.get('adminTools').showActionLogs(this, {
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
import { ajax } from 'discourse/lib/ajax';
|
||||
// import computed from 'ember-addons/ember-computed-decorators';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
local: Ember.computed.equal('selection', 'local'),
|
||||
remote: Ember.computed.equal('selection', 'remote'),
|
||||
selection: 'local',
|
||||
adminCustomizeThemes: Ember.inject.controller(),
|
||||
loading: false,
|
||||
|
||||
actions: {
|
||||
importTheme() {
|
||||
@ -24,11 +25,12 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
options.data = {remote: this.get('uploadUrl')};
|
||||
}
|
||||
|
||||
this.set('loading', true);
|
||||
ajax('/admin/themes/import', options).then(result=>{
|
||||
const theme = this.store.createRecord('theme',result.theme);
|
||||
this.get('adminCustomizeThemes').send('addTheme', theme);
|
||||
this.send('closeModal');
|
||||
});
|
||||
}).catch(popupAjaxError).finally(() => this.set('loading', false));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
98
app/assets/javascripts/admin/mixins/setting-component.js.es6
Normal file
98
app/assets/javascripts/admin/mixins/setting-component.js.es6
Normal file
@ -0,0 +1,98 @@
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import { categoryLinkHTML } from 'discourse/helpers/category-link';
|
||||
|
||||
const CustomTypes = ['bool', 'enum', 'list', 'url_list', 'host_list', 'category_list', 'value_list'];
|
||||
|
||||
export default Ember.Mixin.create({
|
||||
classNameBindings: [':row', ':setting', 'setting.overridden', 'typeClass'],
|
||||
content: Ember.computed.alias('setting'),
|
||||
validationMessage: null,
|
||||
|
||||
@computed("buffered.value", "setting.value")
|
||||
dirty(bufferVal, settingVal) {
|
||||
if (bufferVal === null || bufferVal === undefined) bufferVal = '';
|
||||
if (settingVal === null || settingVal === undefined) settingVal = '';
|
||||
|
||||
return bufferVal.toString() !== settingVal.toString();
|
||||
},
|
||||
|
||||
@computed("setting", "buffered.value")
|
||||
preview(setting, value) {
|
||||
// A bit hacky, but allows us to use helpers
|
||||
if (setting.get('setting') === 'category_style') {
|
||||
let category = this.site.get('categories.firstObject');
|
||||
if (category) {
|
||||
return categoryLinkHTML(category, {
|
||||
categoryStyle: value
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let preview = setting.get('preview');
|
||||
if (preview) {
|
||||
return new Handlebars.SafeString("<div class='preview'>" + preview.replace(/\{\{value\}\}/g, value) + "</div>");
|
||||
}
|
||||
},
|
||||
|
||||
@computed('componentType')
|
||||
typeClass(componentType) {
|
||||
return componentType.replace(/\_/g, '-');
|
||||
},
|
||||
|
||||
@computed("setting.setting")
|
||||
settingName(setting) {
|
||||
return setting.replace(/\_/g, ' ');
|
||||
},
|
||||
|
||||
@computed("setting.type")
|
||||
componentType(type) {
|
||||
return CustomTypes.indexOf(type) !== -1 ? type : 'string';
|
||||
},
|
||||
|
||||
@computed("typeClass")
|
||||
componentName(typeClass) {
|
||||
return "site-settings/" + typeClass;
|
||||
},
|
||||
|
||||
_watchEnterKey: function() {
|
||||
const self = this;
|
||||
this.$().on("keydown.setting-enter", ".input-setting-string", function (e) {
|
||||
if (e.keyCode === 13) { // enter key
|
||||
self._save();
|
||||
}
|
||||
});
|
||||
}.on('didInsertElement'),
|
||||
|
||||
_removeBindings: function() {
|
||||
this.$().off("keydown.setting-enter");
|
||||
}.on("willDestroyElement"),
|
||||
|
||||
_save() {
|
||||
Em.warn("You should define a `_save` method", { id: "admin.mixins.setting-component" });
|
||||
return Ember.RSVP.resolve();
|
||||
},
|
||||
|
||||
actions: {
|
||||
save() {
|
||||
this._save().then(() => {
|
||||
this.set('validationMessage', null);
|
||||
this.commitBuffer();
|
||||
}).catch(e => {
|
||||
if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) {
|
||||
this.set('validationMessage', e.jqXHR.responseJSON.errors[0]);
|
||||
} else {
|
||||
this.set('validationMessage', I18n.t('generic_error'));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
resetDefault() {
|
||||
this.set('buffered.value', this.get('setting.default'));
|
||||
this._save();
|
||||
},
|
||||
|
||||
cancel() {
|
||||
this.rollbackBuffer();
|
||||
}
|
||||
}
|
||||
});
|
||||
29
app/assets/javascripts/admin/mixins/setting-object.js.es6
Normal file
29
app/assets/javascripts/admin/mixins/setting-object.js.es6
Normal file
@ -0,0 +1,29 @@
|
||||
export default Ember.Mixin.create({
|
||||
overridden: function() {
|
||||
let val = this.get('value'),
|
||||
defaultVal = this.get('default');
|
||||
|
||||
if (val === null) val = '';
|
||||
if (defaultVal === null) defaultVal = '';
|
||||
|
||||
return val.toString() !== defaultVal.toString();
|
||||
}.property('value', 'default'),
|
||||
|
||||
validValues: function() {
|
||||
const vals = [],
|
||||
translateNames = this.get('translate_names');
|
||||
|
||||
this.get('valid_values').forEach(v => {
|
||||
if (v.name && v.name.length > 0 && translateNames) {
|
||||
vals.addObject({ name: I18n.t(v.name), value: v.value });
|
||||
} else {
|
||||
vals.addObject(v);
|
||||
}
|
||||
});
|
||||
return vals;
|
||||
}.property('valid_values'),
|
||||
|
||||
allowsNone: function() {
|
||||
if ( _.indexOf(this.get('valid_values'), '') >= 0 ) return 'admin.settings.none';
|
||||
}.property('valid_values')
|
||||
});
|
||||
@ -168,6 +168,14 @@ const AdminUser = Discourse.User.extend({
|
||||
}).catch(popupAjaxError);
|
||||
},
|
||||
|
||||
disableSecondFactor() {
|
||||
return ajax(`/admin/users/${this.get('id')}/disable_second_factor`, {
|
||||
type: 'PUT'
|
||||
}).then(() => {
|
||||
this.set('second_factor_enabled', false);
|
||||
}).catch(popupAjaxError);
|
||||
},
|
||||
|
||||
refreshBrowsers() {
|
||||
return ajax("/admin/users/" + this.get('id') + "/refresh_browsers", {
|
||||
type: 'POST'
|
||||
|
||||
@ -1,31 +1,7 @@
|
||||
import { ajax } from 'discourse/lib/ajax';
|
||||
const SiteSetting = Discourse.Model.extend({
|
||||
overridden: function() {
|
||||
let val = this.get('value'),
|
||||
defaultVal = this.get('default');
|
||||
import Setting from 'admin/mixins/setting-object';
|
||||
|
||||
if (val === null) val = '';
|
||||
if (defaultVal === null) defaultVal = '';
|
||||
|
||||
return val.toString() !== defaultVal.toString();
|
||||
}.property('value', 'default'),
|
||||
|
||||
validValues: function() {
|
||||
const vals = [],
|
||||
translateNames = this.get('translate_names');
|
||||
|
||||
this.get('valid_values').forEach(function(v) {
|
||||
if (v.name && v.name.length > 0) {
|
||||
vals.addObject(translateNames ? {name: I18n.t(v.name), value: v.value} : v);
|
||||
}
|
||||
});
|
||||
return vals;
|
||||
}.property('valid_values'),
|
||||
|
||||
allowsNone: function() {
|
||||
if ( _.indexOf(this.get('valid_values'), '') >= 0 ) return 'admin.site_settings.none';
|
||||
}.property('valid_values')
|
||||
});
|
||||
const SiteSetting = Discourse.Model.extend(Setting, {});
|
||||
|
||||
SiteSetting.reopenClass({
|
||||
findAll() {
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
import Setting from 'admin/mixins/setting-object';
|
||||
|
||||
export default Discourse.Model.extend(Setting, {});
|
||||
@ -2,6 +2,7 @@ import RestModel from 'discourse/models/rest';
|
||||
import { default as computed } from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
const THEME_UPLOAD_VAR = 2;
|
||||
const FIELDS_IDS = [0, 1, 5];
|
||||
|
||||
const Theme = RestModel.extend({
|
||||
|
||||
@ -14,13 +15,11 @@ const Theme = RestModel.extend({
|
||||
}
|
||||
|
||||
let hash = {};
|
||||
if (fields) {
|
||||
fields.forEach(field=>{
|
||||
if (!field.type_id || field.type_id < THEME_UPLOAD_VAR) {
|
||||
hash[this.getKey(field)] = field;
|
||||
}
|
||||
});
|
||||
}
|
||||
fields.forEach(field => {
|
||||
if (!field.type_id || FIELDS_IDS.includes(field.type_id)) {
|
||||
hash[this.getKey(field)] = field;
|
||||
}
|
||||
});
|
||||
return hash;
|
||||
},
|
||||
|
||||
@ -29,11 +28,11 @@ const Theme = RestModel.extend({
|
||||
if (!fields) {
|
||||
return [];
|
||||
}
|
||||
return fields.filter((f)=> f.target === 'common' && f.type_id === THEME_UPLOAD_VAR);
|
||||
return fields.filter(f => f.target === 'common' && f.type_id === THEME_UPLOAD_VAR);
|
||||
},
|
||||
|
||||
getKey(field){
|
||||
return field.target + " " + field.name;
|
||||
return `${field.target} ${field.name}`;
|
||||
},
|
||||
|
||||
hasEdited(target, name){
|
||||
@ -151,6 +150,11 @@ const Theme = RestModel.extend({
|
||||
.then(() => this.set("changed", false));
|
||||
},
|
||||
|
||||
saveSettings(name, value) {
|
||||
const settings = {};
|
||||
settings[name] = value;
|
||||
return this.save({ settings });
|
||||
}
|
||||
});
|
||||
|
||||
export default Theme;
|
||||
|
||||
@ -18,6 +18,11 @@ export default Ember.Route.extend({
|
||||
},
|
||||
|
||||
setupController(controller, wrapper) {
|
||||
const fields = controller.fieldsForTarget(wrapper.target);
|
||||
if (!fields.includes(wrapper.field_name)) {
|
||||
this.transitionTo('adminCustomizeThemes.edit', wrapper.model.id, wrapper.target, fields[0]);
|
||||
return;
|
||||
}
|
||||
controller.set("model", wrapper.model);
|
||||
controller.setTargetName(wrapper.target || "common");
|
||||
controller.set("fieldName", wrapper.field_name || "scss");
|
||||
|
||||
@ -10,5 +10,5 @@
|
||||
{{d-button class="cancel" action="cancel" icon="times"}}
|
||||
</div>
|
||||
{{else if setting.overridden}}
|
||||
{{d-button action="resetDefault" icon="undo" label="admin.site_settings.reset"}}
|
||||
{{d-button action="resetDefault" icon="undo" label="admin.settings.reset"}}
|
||||
{{/if}}
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
<div class='search controls'>
|
||||
<label>
|
||||
{{input type="checkbox" checked=onlyOverridden}}
|
||||
{{i18n 'admin.site_settings.show_overriden'}}
|
||||
{{i18n 'admin.settings.show_overriden'}}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -3,39 +3,47 @@
|
||||
<h2>{{i18n 'admin.customize.theme.edit_css_html'}} {{#link-to 'adminCustomizeThemes.show' model.id replace=true}}{{model.name}}{{/link-to}}</h2>
|
||||
|
||||
{{#if error}}
|
||||
<pre class='field-error'>{{error}}</pre>
|
||||
<pre class='field-error'>{{error}}</pre>
|
||||
{{/if}}
|
||||
|
||||
<div class='edit-main-nav'>
|
||||
<ul class='nav nav-pills target'>
|
||||
{{#if showCommon}}
|
||||
<li>
|
||||
{{#link-to 'adminCustomizeThemes.edit' model.id 'common' fieldName replace=true title=field.title}}
|
||||
{{i18n 'admin.customize.theme.common'}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
<li>
|
||||
{{#link-to 'adminCustomizeThemes.edit' model.id 'common' fieldName replace=true}}
|
||||
{{i18n 'admin.customize.theme.common'}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if showDesktop}}
|
||||
<li>
|
||||
{{#link-to 'adminCustomizeThemes.edit' model.id 'desktop' fieldName replace=true title=field.title}}
|
||||
{{i18n 'admin.customize.theme.desktop'}}
|
||||
{{d-icon 'desktop'}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
<li>
|
||||
{{#link-to 'adminCustomizeThemes.edit' model.id 'desktop' fieldName replace=true}}
|
||||
{{i18n 'admin.customize.theme.desktop'}}
|
||||
{{d-icon 'desktop'}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if showMobile}}
|
||||
<li class='mobile'>
|
||||
{{#link-to 'adminCustomizeThemes.edit' model.id 'mobile' fieldName replace=true title=field.title}}
|
||||
{{i18n 'admin.customize.theme.mobile'}}
|
||||
{{d-icon 'mobile'}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
<li class='mobile'>
|
||||
{{#link-to 'adminCustomizeThemes.edit' model.id 'mobile' fieldName replace=true}}
|
||||
{{i18n 'admin.customize.theme.mobile'}}
|
||||
{{d-icon 'mobile'}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
{{/if}}
|
||||
{{#if showSettings}}
|
||||
<li class='theme-settings'>
|
||||
{{#link-to 'adminCustomizeThemes.edit' model.id 'settings' fieldName replace=true}}
|
||||
{{i18n 'admin.customize.theme.settings'}}
|
||||
{{d-icon 'cog'}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
{{/if}}
|
||||
</ul>
|
||||
<div class='show-overidden'>
|
||||
<label>
|
||||
{{input type="checkbox" checked=onlyOverridden}}
|
||||
{{i18n 'admin.site_settings.show_overriden'}}
|
||||
{{i18n 'admin.settings.show_overriden'}}
|
||||
</label>
|
||||
</div>
|
||||
<div class='clearfix'></div>
|
||||
|
||||
@ -50,16 +50,16 @@
|
||||
|
||||
<h3>{{i18n "admin.customize.theme.css_html"}}</h3>
|
||||
{{#if hasEditedFields}}
|
||||
<p>{{i18n "admin.customize.theme.custom_sections"}}</p>
|
||||
<ul>
|
||||
{{#each editedDescriptions as |desc|}}
|
||||
<li>{{desc}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
<p>{{i18n "admin.customize.theme.custom_sections"}}</p>
|
||||
<ul>
|
||||
{{#each editedDescriptions as |desc|}}
|
||||
<li>{{desc}}</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{else}}
|
||||
<p>
|
||||
{{i18n "admin.customize.theme.edit_css_html_help"}}
|
||||
</p>
|
||||
<p>
|
||||
{{i18n "admin.customize.theme.edit_css_html_help"}}
|
||||
</p>
|
||||
{{/if}}
|
||||
<p>
|
||||
{{#if model.remote_theme}}
|
||||
@ -71,17 +71,17 @@
|
||||
{{/if}}
|
||||
{{#d-button action="editTheme" class="btn edit"}}{{i18n 'admin.customize.theme.edit_css_html'}}{{/d-button}}
|
||||
{{#if model.remote_theme}}
|
||||
<span class='status-message'>
|
||||
{{#if updatingRemote}}
|
||||
{{i18n 'admin.customize.theme.updating'}}
|
||||
{{else}}
|
||||
{{#if model.remote_theme.commits_behind}}
|
||||
{{i18n 'admin.customize.theme.commits_behind' count=model.remote_theme.commits_behind}}
|
||||
<span class='status-message'>
|
||||
{{#if updatingRemote}}
|
||||
{{i18n 'admin.customize.theme.updating'}}
|
||||
{{else}}
|
||||
{{i18n 'admin.customize.theme.up_to_date'}} {{format-date model.remote_theme.updated_at leaveAgo="true"}}
|
||||
{{#if model.remote_theme.commits_behind}}
|
||||
{{i18n 'admin.customize.theme.commits_behind' count=model.remote_theme.commits_behind}}
|
||||
{{else}}
|
||||
{{i18n 'admin.customize.theme.up_to_date'}} {{format-date model.remote_theme.updated_at leaveAgo="true"}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</span>
|
||||
</span>
|
||||
{{/if}}
|
||||
</p>
|
||||
|
||||
@ -105,6 +105,15 @@
|
||||
{{#d-button action="addUploadModal" icon="plus"}}{{i18n "admin.customize.theme.add"}}{{/d-button}}
|
||||
</p>
|
||||
|
||||
{{#if hasSettings}}
|
||||
<h3>{{i18n "admin.customize.theme.theme_settings"}}</h3>
|
||||
{{#d-section class="form-horizontal theme settings"}}
|
||||
{{#each settings as |setting|}}
|
||||
{{theme-setting setting=setting model=model class="theme-setting"}}
|
||||
{{/each}}
|
||||
{{/d-section}}
|
||||
{{/if}}
|
||||
|
||||
{{#if availableChildThemes}}
|
||||
<h3>{{i18n "admin.customize.theme.theme_components"}}</h3>
|
||||
{{#unless model.childThemes.length}}
|
||||
|
||||
@ -35,7 +35,7 @@
|
||||
<label for="owner-selector">{{i18n 'admin.groups.add_owners'}}</label>
|
||||
|
||||
{{user-selector usernames=model.ownerUsernames
|
||||
placeholderKey="admin.groups.selector_placeholder"
|
||||
placeholderKey="groups.selector_placeholder"
|
||||
id="owner-selector"}}
|
||||
|
||||
{{#if model.id}}
|
||||
|
||||
@ -58,7 +58,7 @@
|
||||
{{#unless item.editing}}
|
||||
{{d-button action="destroy" actionParam=item icon="trash-o" class="btn-danger"}}
|
||||
{{d-button action="edit" actionParam=item icon="pencil"}}
|
||||
{{#if isBlocked}}
|
||||
{{#if item.isBlocked}}
|
||||
{{d-button action="allow" actionParam=item icon="check" label="admin.logs.screened_ips.actions.do_nothing"}}
|
||||
{{else}}
|
||||
{{d-button action="block" actionParam=item icon="ban" label="admin.logs.screened_ips.actions.block"}}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{{#if filteredContent}}
|
||||
{{#d-section class="form-horizontal settings"}}
|
||||
{{#each filteredContent as |setting|}}
|
||||
{{site-setting setting=setting saveAction="saveSetting"}}
|
||||
{{site-setting setting=setting}}
|
||||
{{/each}}
|
||||
{{/d-section}}
|
||||
{{else}}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
<div class='search controls'>
|
||||
<label>
|
||||
{{input type="checkbox" checked=onlyOverridden}}
|
||||
{{i18n 'admin.site_settings.show_overriden'}}
|
||||
{{i18n 'admin.settings.show_overriden'}}
|
||||
</label>
|
||||
</div>
|
||||
<div class='controls'>
|
||||
|
||||
@ -156,6 +156,22 @@
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class='display-row'>
|
||||
<div class='field'>{{i18n 'user.second_factor.title'}}</div>
|
||||
<div class='value'>
|
||||
{{#if model.second_factor_enabled}}
|
||||
{{i18n "yes_value"}}
|
||||
{{else}}
|
||||
{{i18n "no_value"}}
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class='controls'>
|
||||
{{#if canDisableSecondFactor}}
|
||||
{{d-button action="disableSecondFactor" icon="unlock-alt" label="user.second_factor.disable"}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{{#if userFields}}
|
||||
|
||||
@ -98,6 +98,10 @@
|
||||
{{#if user.moderator}}
|
||||
{{d-icon "shield" title="admin.moderator" }}
|
||||
{{/if}}
|
||||
|
||||
{{#if user.second_factor_enabled}}
|
||||
{{d-icon "lock" title="admin.user.second_factor_enabled" }}
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
|
||||
@ -10,6 +10,12 @@
|
||||
{{watched-word-uploader uploading=uploading actionKey=actionNameKey done="uploadComplete"}}
|
||||
|
||||
<div class='clearfix'></div>
|
||||
<div>
|
||||
<label class="show-words-checkbox">
|
||||
{{input type="checkbox" checked=adminWatchedWords.showWords disabled=adminWatchedWords.disableShowWords}}
|
||||
{{i18n 'admin.watched_words.show_words'}}
|
||||
</label>
|
||||
</div>
|
||||
<div class="watched-words-list">
|
||||
{{#if showWordsList}}
|
||||
{{#each filteredContent as |word| }}
|
||||
|
||||
@ -1,10 +1,4 @@
|
||||
<div class='admin-controls'>
|
||||
<div class='search controls'>
|
||||
<label class="show-words-checkbox">
|
||||
{{input type="checkbox" checked=showWords disabled=disableShowWords}}
|
||||
{{i18n 'admin.watched_words.show_words'}}
|
||||
</label>
|
||||
</div>
|
||||
<div class='controls'>
|
||||
{{text-field value=filter placeholderKey="admin.watched_words.search" class="no-blur"}}
|
||||
{{d-button action="clearFilter" label="admin.watched_words.clear_filter"}}
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
// Stuff we need to load first
|
||||
//= require ./discourse/lib/utilities
|
||||
//= require ./discourse/lib/page-visible
|
||||
//= require ./discourse/lib/logout
|
||||
//= require ./discourse/lib/ajax
|
||||
//= require ./discourse/lib/text
|
||||
//= require ./discourse/lib/hash
|
||||
|
||||
@ -61,5 +61,11 @@ export default Ember.Component.extend({
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
showInserted() {
|
||||
this.sendAction('showInserted');
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
export default Ember.Component.extend({
|
||||
classNames: ["categories-and-top"]
|
||||
});
|
||||
@ -1,3 +1,3 @@
|
||||
export default Ember.Component.extend({
|
||||
classNames: ['latest-topic-list']
|
||||
tagName: ''
|
||||
});
|
||||
@ -38,11 +38,11 @@ export default Ember.Component.extend({
|
||||
<a class="post-link" href="${postLink.href}">${postLink.anchor}</a>
|
||||
${userAvatar}
|
||||
<span class="username">${userLink.anchor}</span>
|
||||
${iconHTML("mail-forward", { class: "reply-to-glyph" })}
|
||||
`;
|
||||
|
||||
if (originalUser) {
|
||||
editTitle += `
|
||||
${iconHTML("mail-forward", { class: "reply-to-glyph" })}
|
||||
${originalUser.avatar}
|
||||
<span class="original-username">${originalUser.username}</span>
|
||||
`;
|
||||
|
||||
@ -372,7 +372,13 @@ export default Ember.Component.extend({
|
||||
post.set('refreshedPost', true);
|
||||
}
|
||||
|
||||
$oneboxes.each((_, o) => load({ elem: o, refresh, ajax, categoryId: this.get('composer.category.id') }));
|
||||
$oneboxes.each((_, o) => load({
|
||||
elem: o,
|
||||
refresh,
|
||||
ajax,
|
||||
categoryId: this.get('composer.category.id'),
|
||||
topicId: this.get('composer.topic.id')
|
||||
}));
|
||||
},
|
||||
|
||||
_warnMentionedGroups($preview) {
|
||||
|
||||
@ -86,6 +86,7 @@ export default Ember.Component.extend({
|
||||
ajax,
|
||||
synchronous: true,
|
||||
categoryId: this.get('composer.category.id'),
|
||||
topicId: this.get('composer.topic.id')
|
||||
});
|
||||
|
||||
if (loadOnebox && loadOnebox.then) {
|
||||
|
||||
@ -14,11 +14,13 @@ export default Ember.Component.extend({
|
||||
|
||||
Ember.run.scheduleOnce('afterRender', this, this._afterFirstRender);
|
||||
this.appEvents.on('modal-body:flash', msg => this._flash(msg));
|
||||
this.appEvents.on('modal-body:clearFlash', () => this._clearFlash());
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super();
|
||||
this.appEvents.off('modal-body:flash');
|
||||
this.appEvents.off('modal-body:clearFlash');
|
||||
},
|
||||
|
||||
_afterFirstRender() {
|
||||
@ -45,10 +47,16 @@ export default Ember.Component.extend({
|
||||
);
|
||||
},
|
||||
|
||||
_clearFlash() {
|
||||
$('#modal-alert').hide().removeClass('alert-error', 'alert-success');
|
||||
},
|
||||
|
||||
_flash(msg) {
|
||||
$('#modal-alert').hide()
|
||||
.removeClass('alert-error', 'alert-success')
|
||||
.addClass(`alert alert-${msg.messageClass || 'success'}`).html(msg.text || '')
|
||||
.fadeIn();
|
||||
this._clearFlash();
|
||||
|
||||
$('#modal-alert')
|
||||
.addClass(`alert alert-${msg.messageClass || 'success'}`)
|
||||
.html(msg.text || '')
|
||||
.fadeIn();
|
||||
},
|
||||
});
|
||||
|
||||
@ -2,8 +2,7 @@ import { on, observes } from "ember-addons/ember-computed-decorators";
|
||||
import { findRawTemplate } from "discourse/lib/raw-templates";
|
||||
import { emojiUrlFor } from "discourse/lib/text";
|
||||
import KeyValueStore from "discourse/lib/key-value-store";
|
||||
import { emojis } from "pretty-text/emoji/data";
|
||||
import { extendedEmojiList, isSkinTonableEmoji } from "pretty-text/emoji";
|
||||
import { extendedEmojiList, isSkinTonableEmoji, emojiSearch } from "pretty-text/emoji";
|
||||
const { run } = Ember;
|
||||
|
||||
const keyValueStore = new KeyValueStore("discourse_emojis_");
|
||||
@ -205,10 +204,7 @@ export default Ember.Component.extend({
|
||||
this.$list.css("visibility", "visible");
|
||||
} else {
|
||||
const lowerCaseFilter = this.get("filter").toLowerCase();
|
||||
const filterableEmojis = emojis.concat(_.keys(extendedEmojiList()));
|
||||
const filteredCodes = _.filter(filterableEmojis, code => {
|
||||
return code.indexOf(lowerCaseFilter) > -1;
|
||||
}).slice(0, 30);
|
||||
const filteredCodes = emojiSearch(lowerCaseFilter, { maxResults: 30});
|
||||
this.$results.empty().html(
|
||||
_.map(filteredCodes, (code) => {
|
||||
const hasDiversity = isSkinTonableEmoji(code);
|
||||
|
||||
@ -13,8 +13,12 @@ export default Ember.Component.extend({
|
||||
},
|
||||
|
||||
actions: {
|
||||
externalLogin: function(provider) {
|
||||
this.sendAction('action', provider);
|
||||
emailLogin() {
|
||||
this.sendAction('emailLogin');
|
||||
},
|
||||
|
||||
externalLogin(provider) {
|
||||
this.sendAction('externalLogin', provider);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -11,7 +11,7 @@ export default Ember.Component.extend({
|
||||
}
|
||||
|
||||
Ember.run.schedule('afterRender', () => {
|
||||
$('#login-account-password, #login-account-name').keydown(e => {
|
||||
$('#login-account-password, #login-account-name, #login-second-factor').keydown(e => {
|
||||
if (e.keyCode === 13) {
|
||||
this.sendAction();
|
||||
}
|
||||
|
||||
@ -31,7 +31,7 @@ export default Ember.Component.extend({
|
||||
}
|
||||
|
||||
const unreadTopics = this.topicTrackingState.countUnread();
|
||||
const newTopics = this.topicTrackingState.countNew();
|
||||
const newTopics = this.currentUser ? this.topicTrackingState.countNew() : 0;
|
||||
|
||||
if (newTopics + unreadTopics > 0) {
|
||||
const hasBoth = unreadTopics > 0 && newTopics > 0;
|
||||
|
||||
@ -1,142 +0,0 @@
|
||||
import renderTag from 'discourse/lib/render-tag';
|
||||
|
||||
function formatTag(t) {
|
||||
return renderTag(t.id, {count: t.count, noHref: true});
|
||||
}
|
||||
|
||||
export default Ember.TextField.extend({
|
||||
classNameBindings: [':tag-chooser'],
|
||||
attributeBindings: ['tabIndex', 'placeholderKey', 'categoryId'],
|
||||
|
||||
init() {
|
||||
this._super();
|
||||
const tags = this.get('tags') || [];
|
||||
this.set('value', tags.join(", "));
|
||||
|
||||
if (this.get('allowCreate') !== false) {
|
||||
this.set('allowCreate', this.site.get('can_create_tag'));
|
||||
}
|
||||
|
||||
this.set('termMatchesForbidden', false);
|
||||
},
|
||||
|
||||
_valueChanged: function() {
|
||||
const tags = this.get('value').split(',').map(v => v.trim()).reject(v => v.length === 0).uniq();
|
||||
this.set('tags', tags);
|
||||
}.observes('value'),
|
||||
|
||||
_tagsChanged: function() {
|
||||
const $tagChooser = this.$(),
|
||||
val = this.get('value');
|
||||
|
||||
if ($tagChooser && val !== this.get('tags')) {
|
||||
if (this.get('tags')) {
|
||||
const data = this.get('tags').map((t) => {return {id: t, text: t};});
|
||||
$tagChooser.select2('data', data);
|
||||
} else {
|
||||
$tagChooser.select2('data', []);
|
||||
}
|
||||
}
|
||||
}.observes('tags'),
|
||||
|
||||
didInsertElement() {
|
||||
this._super();
|
||||
|
||||
const self = this;
|
||||
const filterRegexp = new RegExp(this.site.tags_filter_regexp, "g");
|
||||
|
||||
let limit = this.siteSettings.max_tags_per_topic;
|
||||
|
||||
if (this.get('unlimitedTagCount')) {
|
||||
limit = null;
|
||||
} else if (this.get('limit')) {
|
||||
limit = parseInt(this.get('limit'));
|
||||
}
|
||||
|
||||
this.$().select2({
|
||||
tags: true,
|
||||
placeholder: this.get('placeholder') === "" ? "" : I18n.t(this.get('placeholderKey') || 'tagging.choose_for_topic'),
|
||||
maximumInputLength: this.siteSettings.max_tag_length,
|
||||
maximumSelectionSize: limit,
|
||||
width: this.get('width') || 'resolve',
|
||||
initSelection(element, callback) {
|
||||
const data = [];
|
||||
|
||||
function splitVal(string, separator) {
|
||||
var val, i, l;
|
||||
if (string === null || string.length < 1) return [];
|
||||
val = string.split(separator);
|
||||
for (i = 0, l = val.length; i < l; i = i + 1) val[i] = $.trim(val[i]);
|
||||
return val;
|
||||
}
|
||||
|
||||
$(splitVal(element.val(), ",")).each(function () {
|
||||
data.push({
|
||||
id: this,
|
||||
text: this
|
||||
});
|
||||
});
|
||||
|
||||
callback(data);
|
||||
},
|
||||
createSearchChoice(term, data) {
|
||||
term = term.replace(filterRegexp, '').trim().toLowerCase();
|
||||
|
||||
// No empty terms, make sure the user has permission to create the tag
|
||||
if (!term.length || !self.get('allowCreate') || self.get('termMatchesForbidden')) return;
|
||||
|
||||
if ($(data).filter(function() {
|
||||
return this.text.localeCompare(term) === 0;
|
||||
}).length === 0) {
|
||||
return { id: term, text: term };
|
||||
}
|
||||
},
|
||||
createSearchChoicePosition(list, item) {
|
||||
// Search term goes on the bottom
|
||||
list.push(item);
|
||||
},
|
||||
formatSelection(data) {
|
||||
return data ? renderTag(this.text(data), {noHref: true}) : undefined;
|
||||
},
|
||||
formatSelectionCssClass() {
|
||||
return "discourse-tag-select2";
|
||||
},
|
||||
formatResult: formatTag,
|
||||
multiple: true,
|
||||
ajax: {
|
||||
quietMillis: 200,
|
||||
cache: true,
|
||||
url: Discourse.getURL("/tags/filter/search"),
|
||||
dataType: 'json',
|
||||
data: function (term) {
|
||||
const selectedTags = self.get('tags');
|
||||
const d = {
|
||||
q: term,
|
||||
limit: self.siteSettings.max_tag_search_results,
|
||||
categoryId: self.get('categoryId')
|
||||
};
|
||||
if (selectedTags) {
|
||||
d.selected_tags = selectedTags.slice(0,100);
|
||||
}
|
||||
if (!self.get('everyTag')) {
|
||||
d.filterForInput = true;
|
||||
}
|
||||
return d;
|
||||
},
|
||||
results: function (data) {
|
||||
if (self.siteSettings.tags_sort_alphabetically) {
|
||||
data.results = data.results.sort(function(a,b) { return a.id > b.id; });
|
||||
}
|
||||
self.set('termMatchesForbidden', data.forbidden ? true : false);
|
||||
return data;
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super();
|
||||
this.$().select2('destroy');
|
||||
}
|
||||
|
||||
});
|
||||
@ -1,86 +0,0 @@
|
||||
function renderTagGroup(tag) {
|
||||
return "<a class='discourse-tag'>" + Handlebars.Utils.escapeExpression(tag.text ? tag.text : tag) + "</a>";
|
||||
};
|
||||
|
||||
export default Ember.TextField.extend({
|
||||
classNameBindings: [':tag-chooser'],
|
||||
attributeBindings: ['tabIndex', 'placeholderKey', 'categoryId'],
|
||||
|
||||
_initValue: function() {
|
||||
const names = this.get('tagGroups') || [];
|
||||
this.set('value', names.join(","));
|
||||
}.on('init'),
|
||||
|
||||
_valueChanged: function() {
|
||||
const names = this.get('value').split(',').map(v => v.trim()).reject(v => v.length === 0).uniq();
|
||||
if ( this.get('tagGroups').join(',') !== this.get('value') ) {
|
||||
this.set('tagGroups', names);
|
||||
}
|
||||
}.observes('value'),
|
||||
|
||||
_tagGroupsChanged: function() {
|
||||
const $chooser = this.$(),
|
||||
val = this.get('value');
|
||||
|
||||
if ($chooser && val !== this.get('tagGroups')) {
|
||||
if (this.get('tagGroups')) {
|
||||
const data = this.get('tagGroups').map((t) => {return {id: t, text: t};});
|
||||
$chooser.select2('data', data);
|
||||
} else {
|
||||
$chooser.select2('data', []);
|
||||
}
|
||||
}
|
||||
}.observes('tagGroups'),
|
||||
|
||||
_initializeChooser: function() {
|
||||
const self = this;
|
||||
|
||||
this.$().select2({
|
||||
tags: true,
|
||||
placeholder: this.get('placeholderKey') ? I18n.t(this.get('placeholderKey')) : null,
|
||||
initSelection(element, callback) {
|
||||
const data = [];
|
||||
|
||||
function splitVal(string, separator) {
|
||||
var val, i, l;
|
||||
if (string === null || string.length < 1) return [];
|
||||
val = string.split(separator);
|
||||
for (i = 0, l = val.length; i < l; i = i + 1) val[i] = $.trim(val[i]);
|
||||
return val;
|
||||
}
|
||||
|
||||
$(splitVal(element.val(), ",")).each(function () {
|
||||
data.push({ id: this, text: this });
|
||||
});
|
||||
|
||||
callback(data);
|
||||
},
|
||||
formatSelection: function (data) {
|
||||
return data ? renderTagGroup(this.text(data)) : undefined;
|
||||
},
|
||||
formatSelectionCssClass: function(){
|
||||
return "discourse-tag-select2";
|
||||
},
|
||||
formatResult: renderTagGroup,
|
||||
multiple: true,
|
||||
ajax: {
|
||||
quietMillis: 200,
|
||||
cache: true,
|
||||
url: Discourse.getURL("/tag_groups/filter/search"),
|
||||
dataType: 'json',
|
||||
data: function (term) {
|
||||
return { q: term, limit: self.siteSettings.max_tag_search_results };
|
||||
},
|
||||
results: function (data) {
|
||||
data.results = data.results.sort(function(a,b) { return a.text > b.text; });
|
||||
return data;
|
||||
}
|
||||
},
|
||||
});
|
||||
}.on('didInsertElement'),
|
||||
|
||||
_destroyChooser: function() {
|
||||
this.$().select2('destroy');
|
||||
}.on('willDestroyElement')
|
||||
|
||||
});
|
||||
@ -28,6 +28,11 @@ export default Ember.Component.extend({
|
||||
return !this.site.mobileView && this.currentUser && this.currentUser.get('canManageTopic');
|
||||
},
|
||||
|
||||
showEditOnFooter: Ember.computed.and(
|
||||
'topic.isPrivateMessage',
|
||||
'site.can_tag_pms'
|
||||
),
|
||||
|
||||
@computed('topic.message_archived')
|
||||
archiveIcon: archived => archived ? '' : 'folder',
|
||||
|
||||
|
||||
@ -62,13 +62,14 @@ export default MountWidget.extend(Docking, {
|
||||
|
||||
this.dockBottom = false;
|
||||
if (posTop < topicTop) {
|
||||
this.dockAt = topicTop;
|
||||
this.dockAt = parseInt(topicTop, 10);
|
||||
} else if (pos > topicBottom + footerHeight) {
|
||||
this.dockAt = (topicBottom - timelineHeight) + footerHeight;
|
||||
this.dockAt = parseInt((topicBottom - timelineHeight) + footerHeight, 10);
|
||||
this.dockBottom = true;
|
||||
if (this.dockAt < 0) { this.dockAt = 0; }
|
||||
} else {
|
||||
this.dockAt = null;
|
||||
this.fastDockAt = parseInt(topicBottom - timelineHeight + footerHeight - offsetTop, 10);
|
||||
}
|
||||
|
||||
if (this.dockAt !== prev) {
|
||||
|
||||
@ -140,7 +140,8 @@ export default Ember.Controller.extend({
|
||||
return !this.site.mobileView &&
|
||||
this.site.get('can_tag_topics') &&
|
||||
canEditTitle &&
|
||||
!creatingPrivateMessage;
|
||||
!creatingPrivateMessage &&
|
||||
(!this.get('model.topic.isPrivateMessage') || this.site.get('can_tag_pms'));
|
||||
},
|
||||
|
||||
@computed('model.whisper', 'model.unlistTopic')
|
||||
|
||||
@ -42,7 +42,7 @@ const controllerOpts = {
|
||||
const tracker = this.topicTrackingState;
|
||||
|
||||
// Move inserted into topics
|
||||
this.get('content').loadBefore(tracker.get('newIncoming'));
|
||||
this.get('content').loadBefore(tracker.get('newIncoming'), true);
|
||||
tracker.resetTracking();
|
||||
return false;
|
||||
},
|
||||
|
||||
@ -29,45 +29,36 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
},
|
||||
|
||||
resetPassword() {
|
||||
return this._submit('/session/forgot_password', 'forgot_password.complete');
|
||||
},
|
||||
if (this.get('submitDisabled')) return false;
|
||||
this.set('disabled', true);
|
||||
|
||||
emailLogin() {
|
||||
return this._submit('/u/email-login', 'email_login.complete');
|
||||
this.clearFlash();
|
||||
|
||||
ajax('/session/forgot_password', {
|
||||
data: { login: this.get('accountEmailOrUsername').trim() },
|
||||
type: 'POST'
|
||||
}).then(data => {
|
||||
const accountEmailOrUsername = escapeExpression(this.get("accountEmailOrUsername"));
|
||||
const isEmail = accountEmailOrUsername.match(/@/);
|
||||
let key = `forgot_password.complete_${isEmail ? 'email' : 'username'}`;
|
||||
if (data.user_found) {
|
||||
this.set('offerHelp', I18n.t(`${key}_found`, {
|
||||
email: accountEmailOrUsername,
|
||||
username: accountEmailOrUsername
|
||||
}));
|
||||
} else {
|
||||
this.flash(I18n.t(`${key}_not_found`, {
|
||||
email: accountEmailOrUsername,
|
||||
username: accountEmailOrUsername
|
||||
}), 'error');
|
||||
}
|
||||
}).catch(e => {
|
||||
this.flash(extractError(e), 'error');
|
||||
}).finally(() => {
|
||||
this.set('disabled', false);
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
_submit(route, translationKey) {
|
||||
if (this.get('submitDisabled')) return false;
|
||||
this.set('disabled', true);
|
||||
|
||||
ajax(route, {
|
||||
data: { login: this.get('accountEmailOrUsername').trim() },
|
||||
type: 'POST'
|
||||
}).then(data => {
|
||||
const escaped = escapeExpression(this.get('accountEmailOrUsername'));
|
||||
const isEmail = this.get('accountEmailOrUsername').match(/@/);
|
||||
let key = `${translationKey}_${isEmail ? 'email' : 'username'}`;
|
||||
let extraClass;
|
||||
|
||||
if (data.user_found === true) {
|
||||
key += '_found';
|
||||
this.set('accountEmailOrUsername', '');
|
||||
this.set('offerHelp', I18n.t(key, { email: escaped, username: escaped }));
|
||||
} else {
|
||||
if (data.user_found === false) {
|
||||
key += '_not_found';
|
||||
extraClass = 'error';
|
||||
}
|
||||
|
||||
this.flash(I18n.t(key, { email: escaped, username: escaped }), extraClass);
|
||||
}
|
||||
}).catch(e => {
|
||||
this.flash(extractError(e), 'error');
|
||||
}).finally(() => {
|
||||
this.set('disabled', false);
|
||||
});
|
||||
|
||||
return false;
|
||||
},
|
||||
});
|
||||
|
||||
@ -4,6 +4,9 @@ import showModal from 'discourse/lib/show-modal';
|
||||
import { setting } from 'discourse/lib/computed';
|
||||
import { findAll } from 'discourse/models/login-method';
|
||||
import { escape } from 'pretty-text/sanitizer';
|
||||
import { escapeExpression } from 'discourse/lib/utilities';
|
||||
import { extractError } from 'discourse/lib/ajax-error';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
// This is happening outside of the app via popup
|
||||
const AuthErrors = [
|
||||
@ -23,14 +26,23 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
authenticate: null,
|
||||
loggingIn: false,
|
||||
loggedIn: false,
|
||||
processingEmailLink: false,
|
||||
showLoginButtons: true,
|
||||
|
||||
canLoginLocal: setting('enable_local_logins'),
|
||||
canLoginLocalWithEmail: setting('enable_local_logins_via_email'),
|
||||
loginRequired: Em.computed.alias('application.loginRequired'),
|
||||
|
||||
resetForm: function() {
|
||||
this.set('authenticate', null);
|
||||
this.set('loggingIn', false);
|
||||
this.set('loggedIn', false);
|
||||
this.setProperties({
|
||||
'authenticate': null,
|
||||
'loggingIn': false,
|
||||
'loggedIn': false,
|
||||
'secondFactorRequired': false,
|
||||
'showLoginButtons': true,
|
||||
});
|
||||
$("#credentials").show();
|
||||
$("#second-factor").hide();
|
||||
},
|
||||
|
||||
// Determines whether at least one login button is enabled
|
||||
@ -38,9 +50,10 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
return findAll(this.siteSettings).length > 0;
|
||||
}.property(),
|
||||
|
||||
loginButtonText: function() {
|
||||
return this.get('loggingIn') ? I18n.t('login.logging_in') : I18n.t('login.title');
|
||||
}.property('loggingIn'),
|
||||
@computed('loggingIn')
|
||||
loginButtonLabel(loggingIn) {
|
||||
return loggingIn ? 'login.logging_in' : 'login.title';
|
||||
},
|
||||
|
||||
loginDisabled: Em.computed.or('loggingIn', 'loggedIn'),
|
||||
|
||||
@ -54,6 +67,11 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
return this.get('loggingIn') || this.get('authenticate');
|
||||
}.property('loggingIn', 'authenticate'),
|
||||
|
||||
@computed('canLoginLocalWithEmail', 'loginName', 'processingEmailLink')
|
||||
showLoginWithEmailLink(canLoginLocalWithEmail, loginName, processingEmailLink) {
|
||||
return canLoginLocalWithEmail && !Ember.isEmpty(loginName) && !processingEmailLink;
|
||||
},
|
||||
|
||||
actions: {
|
||||
login() {
|
||||
const self = this;
|
||||
@ -67,13 +85,28 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
this.set('loggingIn', true);
|
||||
|
||||
ajax("/session", {
|
||||
data: { login: this.get('loginName'), password: this.get('loginPassword') },
|
||||
type: 'POST'
|
||||
type: 'POST',
|
||||
data: {
|
||||
login: this.get('loginName'),
|
||||
password: this.get('loginPassword'),
|
||||
second_factor_token: this.get('loginSecondFactor')
|
||||
},
|
||||
}).then(function (result) {
|
||||
// Successful login
|
||||
if (result && result.error) {
|
||||
self.set('loggingIn', false);
|
||||
if (result.reason === 'not_activated') {
|
||||
|
||||
if (result.reason === 'invalid_second_factor' && !self.get('secondFactorRequired')) {
|
||||
$('#modal-alert').hide();
|
||||
self.setProperties({
|
||||
'secondFactorRequired': true,
|
||||
'showLoginButtons': false,
|
||||
});
|
||||
|
||||
$("#credentials").hide();
|
||||
$("#second-factor").show();
|
||||
return;
|
||||
} else if (result.reason === 'not_activated') {
|
||||
self.send('showNotActivated', {
|
||||
username: self.get('loginName'),
|
||||
sentTo: escape(result.sent_to_email),
|
||||
@ -182,6 +215,37 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
const forgotPasswordController = this.get('forgotPassword');
|
||||
if (forgotPasswordController) { forgotPasswordController.set("accountEmailOrUsername", this.get("loginName")); }
|
||||
this.send("showForgotPassword");
|
||||
},
|
||||
|
||||
emailLogin() {
|
||||
if (this.get('processingEmailLink')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Ember.isEmpty(this.get('loginName'))){
|
||||
this.flash(I18n.t('login.blank_username'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
this.set('processingEmailLink', true);
|
||||
|
||||
ajax('/u/email-login', {
|
||||
data: { login: this.get('loginName').trim() },
|
||||
type: 'POST'
|
||||
}).then(data => {
|
||||
const loginName = escapeExpression(this.get('loginName'));
|
||||
const isEmail = loginName.match(/@/);
|
||||
let key = `email_login.complete_${isEmail ? 'email' : 'username'}`;
|
||||
if (data.user_found) {
|
||||
this.flash(I18n.t(`${key}_found`, { email: loginName, username: loginName }));
|
||||
} else {
|
||||
this.flash(I18n.t(`${key}_not_found`, { email: loginName, username: loginName }), 'error');
|
||||
}
|
||||
}).catch(e => {
|
||||
this.flash(extractError(e), 'error');
|
||||
}).finally(() => {
|
||||
this.set('processingEmailLink', false);
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@ -194,16 +258,28 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
}).property('authenticate'),
|
||||
|
||||
authenticationComplete(options) {
|
||||
|
||||
const self = this;
|
||||
function loginError(errorMsg, className) {
|
||||
function loginError(errorMsg, className, callback) {
|
||||
showModal('login');
|
||||
Ember.run.next(function() {
|
||||
|
||||
Ember.run.next(() => {
|
||||
callback();
|
||||
self.flash(errorMsg, className || 'success');
|
||||
self.set('authenticate', null);
|
||||
});
|
||||
}
|
||||
|
||||
if (options.omniauth_disallow_totp) {
|
||||
return loginError(I18n.t('login.omniauth_disallow_totp'), 'error', () => {
|
||||
this.setProperties({
|
||||
'loginName': options.email,
|
||||
'showLoginButtons': false,
|
||||
});
|
||||
|
||||
$('#login-account-password').focus();
|
||||
});
|
||||
}
|
||||
|
||||
for (let i=0; i<AuthErrors.length; i++) {
|
||||
const cond = AuthErrors[i];
|
||||
if (options[cond]) {
|
||||
|
||||
@ -8,6 +8,7 @@ import { userPath } from 'discourse/lib/url';
|
||||
export default Ember.Controller.extend(PasswordValidation, {
|
||||
isDeveloper: Ember.computed.alias('model.is_developer'),
|
||||
admin: Ember.computed.alias('model.admin'),
|
||||
secondFactorRequired: Ember.computed.alias('model.second_factor_required'),
|
||||
passwordRequired: true,
|
||||
errorMessage: null,
|
||||
successMessage: null,
|
||||
@ -32,7 +33,8 @@ export default Ember.Controller.extend(PasswordValidation, {
|
||||
url: userPath(`password-reset/${this.get('model.token')}.json`),
|
||||
type: 'PUT',
|
||||
data: {
|
||||
password: this.get('accountPassword')
|
||||
password: this.get('accountPassword'),
|
||||
second_factor_token: this.get('secondFactor')
|
||||
}
|
||||
}).then(result => {
|
||||
if (result.success) {
|
||||
@ -45,10 +47,22 @@ export default Ember.Controller.extend(PasswordValidation, {
|
||||
DiscourseURL.redirectTo(result.redirect_to || '/');
|
||||
}
|
||||
} else {
|
||||
if (result.errors && result.errors.password && result.errors.password.length > 0) {
|
||||
if (result.errors && result.errors.user_second_factor) {
|
||||
this.setProperties({
|
||||
secondFactorRequired: true,
|
||||
password: null,
|
||||
errorMessage: result.message
|
||||
});
|
||||
} else if (this.get('secondFactorRequired')) {
|
||||
this.setProperties({
|
||||
secondFactorRequired: false,
|
||||
errorMessage: null
|
||||
});
|
||||
} else if (result.errors && result.errors.password && result.errors.password.length > 0) {
|
||||
this.get('rejectedPasswords').pushObject(this.get('accountPassword'));
|
||||
this.get('rejectedPasswordsMessages').set(this.get('accountPassword'), result.errors.password[0]);
|
||||
}
|
||||
|
||||
if (result.message) {
|
||||
this.set('errorMessage', result.message);
|
||||
}
|
||||
|
||||
@ -40,6 +40,11 @@ export default Ember.Controller.extend(CanCheckEmails, PreferencesTabController,
|
||||
return !this.siteSettings.enable_sso && this.siteSettings.enable_local_logins;
|
||||
},
|
||||
|
||||
@computed("model.second_factor_enabled")
|
||||
secondFactorStatusClass(secondFactorEnabled) {
|
||||
return secondFactorEnabled ? 'tip good' : 'tip bad';
|
||||
},
|
||||
|
||||
actions: {
|
||||
save() {
|
||||
this.set('saved', false);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import PreferencesTabController from "discourse/mixins/preferences-tab-controller";
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
import { default as computed } from "ember-addons/ember-computed-decorators";
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
|
||||
export default Ember.Controller.extend(PreferencesTabController, {
|
||||
saveAttrNames: [
|
||||
@ -12,7 +12,7 @@ export default Ember.Controller.extend(PreferencesTabController, {
|
||||
|
||||
@computed("model.watchedCategories", "model.watchedFirstPostCategories", "model.trackedCategories", "model.mutedCategories")
|
||||
selectedCategories(watched, watchedFirst, tracked, muted) {
|
||||
return [].concat(watched, watchedFirst, tracked, muted);
|
||||
return [].concat(watched, watchedFirst, tracked, muted).filter(t => t);
|
||||
},
|
||||
|
||||
canSave: function() {
|
||||
|
||||
@ -0,0 +1,99 @@
|
||||
import { default as computed } from 'ember-addons/ember-computed-decorators';
|
||||
import { default as DiscourseURL, userPath } from 'discourse/lib/url';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
import { LOGIN_METHODS } from 'discourse/models/login-method';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
loading: false,
|
||||
resetPasswordLoading: false,
|
||||
resetPasswordProgress: '',
|
||||
password: null,
|
||||
secondFactorImage: null,
|
||||
secondFactorKey: null,
|
||||
showSecondFactorKey: false,
|
||||
errorMessage: null,
|
||||
newUsername: null,
|
||||
|
||||
loaded: Ember.computed.and('secondFactorImage', 'secondFactorKey'),
|
||||
|
||||
@computed('loading')
|
||||
submitButtonText(loading) {
|
||||
return loading ? 'loading' : 'submit';
|
||||
},
|
||||
|
||||
@computed
|
||||
displayOAuthWarning() {
|
||||
return LOGIN_METHODS.some(name => {
|
||||
return this.siteSettings[`enable_${name}_logins`];
|
||||
});
|
||||
},
|
||||
|
||||
toggleSecondFactor(enable) {
|
||||
if (!this.get('secondFactorToken')) return;
|
||||
this.set('loading', true);
|
||||
|
||||
this.get('content').toggleSecondFactor(this.get('secondFactorToken'), enable)
|
||||
.then(response => {
|
||||
if (response.error) {
|
||||
this.set('errorMessage', response.error);
|
||||
this.set('loading', false);
|
||||
return;
|
||||
}
|
||||
|
||||
this.set('errorMessage',null);
|
||||
DiscourseURL.redirectTo(userPath(`${this.get('content').username.toLowerCase()}/preferences`));
|
||||
})
|
||||
.catch(error => {
|
||||
this.set('loading', false);
|
||||
popupAjaxError(error);
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
confirmPassword() {
|
||||
if (!this.get('password')) return;
|
||||
this.set('loading', true);
|
||||
|
||||
this.get('content').loadSecondFactorCodes(this.get('password'))
|
||||
.then(response => {
|
||||
if(response.error) {
|
||||
this.set('errorMessage', response.error);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setProperties({
|
||||
errorMessage: null,
|
||||
secondFactorKey: response.key,
|
||||
secondFactorImage: response.qr,
|
||||
});
|
||||
})
|
||||
.catch(popupAjaxError)
|
||||
.finally(() => this.set('loading', false));
|
||||
},
|
||||
|
||||
resetPassword() {
|
||||
this.setProperties({
|
||||
resetPasswordLoading: true,
|
||||
resetPasswordProgress: ''
|
||||
});
|
||||
|
||||
return this.get('model').changePassword().then(() => {
|
||||
this.set('resetPasswordProgress', I18n.t('user.change_password.success'));
|
||||
})
|
||||
.catch(popupAjaxError)
|
||||
.finally(() => this.set('resetPasswordLoading', false));
|
||||
},
|
||||
|
||||
showSecondFactorKey() {
|
||||
this.set('showSecondFactorKey', true);
|
||||
},
|
||||
|
||||
enableSecondFactor() {
|
||||
this.toggleSecondFactor(true);
|
||||
},
|
||||
|
||||
disableSecondFactor() {
|
||||
this.toggleSecondFactor(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -1,8 +1,8 @@
|
||||
import PreferencesTabController from "discourse/mixins/preferences-tab-controller";
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
|
||||
export default Ember.Controller.extend(PreferencesTabController, {
|
||||
|
||||
saveAttrNames: [
|
||||
'muted_tags',
|
||||
'tracked_tags',
|
||||
@ -10,6 +10,11 @@ export default Ember.Controller.extend(PreferencesTabController, {
|
||||
'watching_first_post_tags'
|
||||
],
|
||||
|
||||
@computed("model.watched_tags", "model.watching_first_post_tags", "model.tracked_tags", "model.muted_tags")
|
||||
selectedTags(watched, watchedFirst, tracked, muted) {
|
||||
return [].concat(watched, watchedFirst, tracked, muted).filter(t => t);
|
||||
},
|
||||
|
||||
actions: {
|
||||
save() {
|
||||
this.set('saved', false);
|
||||
|
||||
@ -104,7 +104,7 @@ export default Ember.Controller.extend(BufferedContent, {
|
||||
|
||||
@computed('model.isPrivateMessage')
|
||||
canEditTags(isPrivateMessage) {
|
||||
return !isPrivateMessage && this.site.get('can_tag_topics');
|
||||
return this.site.get('can_tag_topics') && (!isPrivateMessage || this.site.get('can_tag_pms'));
|
||||
},
|
||||
|
||||
actions: {
|
||||
@ -265,6 +265,23 @@ export default Ember.Controller.extend(BufferedContent, {
|
||||
}
|
||||
},
|
||||
|
||||
editFirstPost() {
|
||||
const postStream = this.get('model.postStream');
|
||||
let firstPost = postStream.get('posts.firstObject');
|
||||
|
||||
if (firstPost.get('post_number') !== 1) {
|
||||
const postId = postStream.findPostIdForPostNumber(1);
|
||||
// try loading from identity map first
|
||||
firstPost = postStream.findLoadedPost(postId);
|
||||
if (firstPost === undefined) {
|
||||
return this.get('model.postStream').loadPost(postId).then(post => {
|
||||
this.send("editPost", post);
|
||||
});
|
||||
}
|
||||
}
|
||||
this.send("editPost", firstPost);
|
||||
},
|
||||
|
||||
// Post related methods
|
||||
replyToPost(post) {
|
||||
const composerController = this.get('composer');
|
||||
|
||||
@ -33,7 +33,6 @@ export default Ember.Controller.extend({
|
||||
return hasSelection && pmView !== "archive" && !archive;
|
||||
},
|
||||
|
||||
|
||||
bulkOperation(operation) {
|
||||
const selected = this.get('selected');
|
||||
var params = {type: operation};
|
||||
|
||||
@ -1,18 +1,59 @@
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
// Lists of topics on a user's page.
|
||||
export default Ember.Controller.extend({
|
||||
application: Ember.inject.controller(),
|
||||
|
||||
hideCategory: false,
|
||||
showPosters: false,
|
||||
newIncoming: [],
|
||||
incomingCount: 0,
|
||||
channel: null,
|
||||
|
||||
_showFooter: function() {
|
||||
this.set("application.showFooter", !this.get("model.canLoadMore"));
|
||||
}.observes("model.canLoadMore"),
|
||||
|
||||
@computed('incomingCount')
|
||||
hasIncoming(incomingCount) {
|
||||
return incomingCount > 0;
|
||||
},
|
||||
|
||||
subscribe(channel) {
|
||||
this.set('channel', channel);
|
||||
|
||||
this.messageBus.subscribe(channel, data => {
|
||||
if (this.get('newIncoming').indexOf(data.topic_id) === -1) {
|
||||
this.get('newIncoming').push(data.topic_id);
|
||||
this.incrementProperty('incomingCount');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
unsubscribe() {
|
||||
const channel = this.get('channel');
|
||||
if (channel) this.messageBus.unsubscribe(channel);
|
||||
this._resetTracking();
|
||||
this.set('channel', null);
|
||||
},
|
||||
|
||||
_resetTracking() {
|
||||
this.setProperties({
|
||||
"newIncoming": [],
|
||||
"incomingCount": 0
|
||||
});
|
||||
},
|
||||
|
||||
actions: {
|
||||
loadMore: function() {
|
||||
this.get('model').loadMore();
|
||||
}
|
||||
},
|
||||
|
||||
showInserted() {
|
||||
this.get('model').loadBefore(this.get('newIncoming'));
|
||||
this._resetTracking();
|
||||
return false;
|
||||
},
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
@ -36,7 +36,7 @@ function renderAvatar(user, options) {
|
||||
|
||||
if (!username || !avatarTemplate) { return ''; }
|
||||
|
||||
let formattedUsername = formatUsername(username);
|
||||
let displayName = Ember.get(user, 'name') || formatUsername(username);
|
||||
|
||||
let title = options.title;
|
||||
if (!title && !options.ignoreTitle) {
|
||||
@ -49,7 +49,7 @@ function renderAvatar(user, options) {
|
||||
// if a description has been provided
|
||||
if (description && description.length > 0) {
|
||||
// preprend the username before the description
|
||||
title = formattedUsername + " - " + description;
|
||||
title = displayName + " - " + description;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -57,7 +57,7 @@ function renderAvatar(user, options) {
|
||||
return avatarImg({
|
||||
size: options.imageSize,
|
||||
extraClasses: Em.get(user, 'extras') || options.extraClasses,
|
||||
title: title || formattedUsername,
|
||||
title: title || displayName,
|
||||
avatarTemplate: avatarTemplate
|
||||
});
|
||||
} else {
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import logout from 'discourse/lib/logout';
|
||||
|
||||
let _showingLogout = false;
|
||||
|
||||
// Subscribe to "logout" change events via the Message Bus
|
||||
export default {
|
||||
name: "logout",
|
||||
@ -7,14 +9,22 @@ export default {
|
||||
|
||||
initialize: function (container) {
|
||||
const messageBus = container.lookup('message-bus:main');
|
||||
const siteSettings = container.lookup('site-settings:main');
|
||||
const keyValueStore = container.lookup('key-value-store:main');
|
||||
|
||||
if (!messageBus) { return; }
|
||||
const callback = () => logout(siteSettings, keyValueStore);
|
||||
|
||||
messageBus.subscribe("/logout", function () {
|
||||
bootbox.dialog(I18n.t("logout"), {label: I18n.t("refresh"), callback}, {onEscape: callback, backdrop: 'static'});
|
||||
if (!_showingLogout) {
|
||||
|
||||
_showingLogout = true;
|
||||
|
||||
bootbox.dialog(I18n.t("logout"), {
|
||||
label: I18n.t("refresh"),
|
||||
callback: logout
|
||||
}, {
|
||||
onEscape: logout,
|
||||
backdrop: 'static'
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
import pageVisible from 'discourse/lib/page-visible';
|
||||
import logout from 'discourse/lib/logout';
|
||||
|
||||
let _trackView = false;
|
||||
let _transientHeader = null;
|
||||
let _showingLogout = false;
|
||||
|
||||
export function setTransientHeader(key, value) {
|
||||
_transientHeader = {key, value};
|
||||
@ -39,6 +41,10 @@ export function ajax() {
|
||||
|
||||
args.headers = args.headers || {};
|
||||
|
||||
if (Discourse.__container__.lookup('current-user:main')) {
|
||||
args.headers['Discourse-Logged-In'] = "true";
|
||||
}
|
||||
|
||||
if (_transientHeader) {
|
||||
args.headers[_transientHeader.key] = _transientHeader.value;
|
||||
_transientHeader = null;
|
||||
@ -54,7 +60,22 @@ export function ajax() {
|
||||
args.headers['Discourse-Visible'] = "true";
|
||||
}
|
||||
|
||||
let handleLogoff = function(xhr) {
|
||||
if (xhr.getResponseHeader('Discourse-Logged-Out') && !_showingLogout) {
|
||||
_showingLogout = true;
|
||||
bootbox.dialog(
|
||||
I18n.t("logout"), {label: I18n.t("refresh"), callback: logout},
|
||||
{
|
||||
onEscape: () => logout(),
|
||||
backdrop: 'static'
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
args.success = (data, textStatus, xhr) => {
|
||||
handleLogoff(xhr);
|
||||
|
||||
if (xhr.getResponseHeader('Discourse-Readonly')) {
|
||||
Ember.run(() => Discourse.Site.currentProp('isReadOnly', true));
|
||||
}
|
||||
@ -67,6 +88,8 @@ export function ajax() {
|
||||
};
|
||||
|
||||
args.error = (xhr, textStatus, errorThrown) => {
|
||||
handleLogoff(xhr);
|
||||
|
||||
// note: for bad CSRF we don't loop an extra request right away.
|
||||
// this allows us to eliminate the possibility of having a loop.
|
||||
if (xhr.status === 403 && xhr.responseText === "[\"BAD CSRF\"]") {
|
||||
|
||||
@ -26,7 +26,7 @@ export default {
|
||||
}
|
||||
|
||||
// don't track links in quotes or in elided part
|
||||
let tracking = $link.parents('aside.quote,.elided').length === 0;
|
||||
let tracking = $link.parents('aside.quote, .elided').length === 0;
|
||||
|
||||
let href = $link.attr('href') || $link.data('href');
|
||||
|
||||
@ -113,8 +113,10 @@ export default {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isInternal = DiscourseURL.isInternal(href);
|
||||
|
||||
// If we're on the same site, use the router and track via AJAX
|
||||
if (tracking && DiscourseURL.isInternal(href) && !$link.hasClass('attachment')) {
|
||||
if (tracking && isInternal && !$link.hasClass('attachment')) {
|
||||
ajax("/clicks/track", {
|
||||
data: {
|
||||
url: href,
|
||||
@ -128,9 +130,11 @@ export default {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Otherwise, use a custom URL with a redirect
|
||||
// consider CTRL+mouse-left-click / CMD+mouse-left-click or mouse-middle-click as well
|
||||
if (Discourse.User.currentProp('external_links_in_new_tab') || ((e.ctrlKey || e.metaKey) && (e.which === 1)) || (e.which === 2)) {
|
||||
const modifierLeftClicked = (e.ctrlKey || e.metaKey) && e.which === 1;
|
||||
const middleClicked = e.which === 2;
|
||||
const openExternalInNewTab = Discourse.User.currentProp('external_links_in_new_tab');
|
||||
|
||||
if (modifierLeftClicked || middleClicked || (!isInternal && openExternalInNewTab)) {
|
||||
window.open(destUrl, '_blank').focus();
|
||||
} else {
|
||||
DiscourseURL.redirectTo(destUrl);
|
||||
|
||||
@ -311,7 +311,7 @@ export function number(val) {
|
||||
formattedNumber = I18n.toNumber(val / 1000000, {precision: 1});
|
||||
return I18n.t("number.short.millions", {number: formattedNumber});
|
||||
} else if (val > 99999) {
|
||||
formattedNumber = I18n.toNumber(val / 1000, {precision: 0});
|
||||
formattedNumber = I18n.toNumber(Math.floor(val / 1000), {precision: 0});
|
||||
return I18n.t("number.short.thousands", {number: formattedNumber});
|
||||
} else if (val > 999) {
|
||||
formattedNumber = I18n.toNumber(val / 1000, {precision: 1});
|
||||
|
||||
@ -1,4 +1,10 @@
|
||||
export default function logout(siteSettings, keyValueStore) {
|
||||
if (!siteSettings || !keyValueStore) {
|
||||
const container = Discourse.__container__;
|
||||
siteSettings = siteSettings || container.lookup('site-settings:main');
|
||||
keyValueStore = keyValueStore || container.lookup('key-value-store:main');
|
||||
}
|
||||
|
||||
keyValueStore.abandonLocal();
|
||||
|
||||
const redirect = siteSettings.logout_redirect;
|
||||
|
||||
@ -3,7 +3,12 @@ export default function renderTag(tag, params) {
|
||||
tag = Handlebars.Utils.escapeExpression(tag);
|
||||
const classes = ['tag-' + tag, 'discourse-tag'];
|
||||
const tagName = params.tagName || "a";
|
||||
const href = (tagName === "a" && !params.noHref) ? " href='" + Discourse.getURL("/tags/" + tag) + "' " : "";
|
||||
let path;
|
||||
if (tagName === "a" && !params.noHref) {
|
||||
const current_user = Discourse.User.current();
|
||||
path = params.isPrivateMessage ? `/u/${current_user.username}/messages/tag/${tag}` : `/tags/${tag}`;
|
||||
}
|
||||
const href = path ? ` href='${Discourse.getURL(path)}' ` : "";
|
||||
|
||||
if (Discourse.SiteSettings.tag_style || params.style) {
|
||||
classes.push(params.style || Discourse.SiteSettings.tag_style);
|
||||
|
||||
@ -20,6 +20,7 @@ export function addTagsHtmlCallback(callback, options) {
|
||||
export default function(topic, params){
|
||||
let tags = topic.tags;
|
||||
let buffer = "";
|
||||
const isPrivateMessage = topic.get('isPrivateMessage');
|
||||
|
||||
if (params && params.mode === "list") {
|
||||
tags = topic.get("visibleListTags");
|
||||
@ -43,7 +44,7 @@ export default function(topic, params){
|
||||
buffer = "<div class='discourse-tags'>";
|
||||
if (tags) {
|
||||
for(let i=0; i<tags.length; i++){
|
||||
buffer += renderTag(tags[i]) + ' ';
|
||||
buffer += renderTag(tags[i], { isPrivateMessage }) + ' ';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -138,10 +138,12 @@ export default function transformPost(currentUser, site, post, prevPost, nextPos
|
||||
postAtts.topicCreatedAt = topic.created_at;
|
||||
postAtts.createdByUsername = createdBy.username;
|
||||
postAtts.createdByAvatarTemplate = createdBy.avatar_template;
|
||||
postAtts.createdByName = createdBy.name;
|
||||
|
||||
postAtts.lastPostUrl = topic.get('lastPostUrl');
|
||||
postAtts.lastPostUsername = details.last_poster.username;
|
||||
postAtts.lastPostAvatarTemplate = details.last_poster.avatar_template;
|
||||
postAtts.lastPostName = details.last_poster.name;
|
||||
postAtts.lastPostAt = topic.last_posted_at;
|
||||
|
||||
postAtts.topicReplyCount = topic.get('replyCount');
|
||||
|
||||
@ -203,7 +203,7 @@ export function validateUploadedFile(file, opts) {
|
||||
|
||||
// check that the uploaded file is authorized
|
||||
if (opts.allowStaffToUploadAnyFileInPm && opts.isPrivateMessage) {
|
||||
if (Discourse.User.current("staff")) {
|
||||
if (Discourse.User.currentProp('staff')) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -239,16 +239,28 @@ export function validateUploadedFile(file, opts) {
|
||||
|
||||
const IMAGES_EXTENSIONS_REGEX = /(png|jpe?g|gif|bmp|tiff?|svg|webp|ico)/i;
|
||||
|
||||
function extensionsToArray(exts) {
|
||||
return exts.toLowerCase()
|
||||
.replace(/[\s\.]+/g, "")
|
||||
.split("|")
|
||||
.filter(ext => ext.indexOf("*") === -1);
|
||||
}
|
||||
|
||||
function extensions() {
|
||||
return Discourse.SiteSettings.authorized_extensions
|
||||
.toLowerCase()
|
||||
.replace(/[\s\.]+/g, "")
|
||||
.split("|")
|
||||
.filter(ext => ext.indexOf("*") === -1);
|
||||
return extensionsToArray(Discourse.SiteSettings.authorized_extensions);
|
||||
}
|
||||
|
||||
function staffExtensions() {
|
||||
return extensionsToArray(Discourse.SiteSettings.authorized_extensions_for_staff);
|
||||
}
|
||||
|
||||
function imagesExtensions() {
|
||||
return extensions().filter(ext => IMAGES_EXTENSIONS_REGEX.test(ext));
|
||||
let exts = extensions().filter(ext => IMAGES_EXTENSIONS_REGEX.test(ext));
|
||||
if (Discourse.User.currentProp('staff')) {
|
||||
const staffExts = staffExtensions().filter(ext => IMAGES_EXTENSIONS_REGEX.test(ext));
|
||||
exts = _.union(exts, staffExts);
|
||||
}
|
||||
return exts;
|
||||
}
|
||||
|
||||
function extensionsRegex() {
|
||||
@ -259,7 +271,14 @@ function imagesExtensionsRegex() {
|
||||
return new RegExp("\\.(" + imagesExtensions().join("|") + ")$", "i");
|
||||
}
|
||||
|
||||
function staffExtensionsRegex() {
|
||||
return new RegExp("\\.(" + staffExtensions().join("|") + ")$", "i");
|
||||
}
|
||||
|
||||
function isAuthorizedFile(fileName) {
|
||||
if (Discourse.User.currentProp('staff') && staffExtensionsRegex().test(fileName)) {
|
||||
return true;
|
||||
}
|
||||
return extensionsRegex().test(fileName);
|
||||
}
|
||||
|
||||
@ -268,7 +287,8 @@ function isAuthorizedImage(fileName){
|
||||
}
|
||||
|
||||
export function authorizedExtensions() {
|
||||
return authorizesAllExtensions() ? "*" : extensions().join(", ");
|
||||
const exts = Discourse.User.currentProp('staff') ? [...extensions(), ...staffExtensions()] : extensions();
|
||||
return exts.filter(ext => ext.length > 0).join(", ");
|
||||
}
|
||||
|
||||
export function authorizedImagesExtensions() {
|
||||
@ -276,7 +296,9 @@ export function authorizedImagesExtensions() {
|
||||
}
|
||||
|
||||
export function authorizesAllExtensions() {
|
||||
return Discourse.SiteSettings.authorized_extensions.indexOf("*") >= 0;
|
||||
return Discourse.SiteSettings.authorized_extensions.indexOf("*") >= 0 || (
|
||||
Discourse.SiteSettings.authorized_extensions_for_staff.indexOf("*") >= 0 &&
|
||||
Discourse.User.currentProp('staff'));
|
||||
}
|
||||
|
||||
export function authorizesOneOrMoreExtensions() {
|
||||
@ -322,7 +344,7 @@ export function allowsImages() {
|
||||
}
|
||||
|
||||
export function allowsAttachments() {
|
||||
return authorizesAllExtensions() || extensions().length > imagesExtensions().length;
|
||||
return authorizesAllExtensions() || authorizedExtensions().split(", ").length > imagesExtensions().length;
|
||||
}
|
||||
|
||||
export function uploadLocation(url) {
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { propertyEqual, setting } from 'discourse/lib/computed';
|
||||
|
||||
export default Ember.Mixin.create({
|
||||
isOwnEmail: propertyEqual("model.id", "currentUser.id"),
|
||||
isCurrentUser: propertyEqual("model.id", "currentUser.id"),
|
||||
showEmailOnProfile: setting("show_email_on_profile"),
|
||||
canStaffCheckEmails: Em.computed.and("showEmailOnProfile", "currentUser.staff"),
|
||||
canAdminCheckEmails: Em.computed.alias("currentUser.admin"),
|
||||
canCheckEmails: Em.computed.or("isOwnEmail", "canStaffCheckEmails", "canAdminCheckEmails"),
|
||||
canCheckEmails: Em.computed.or("isCurrentUser", "canStaffCheckEmails", "canAdminCheckEmails"),
|
||||
});
|
||||
|
||||
@ -5,6 +5,10 @@ export default Ember.Mixin.create({
|
||||
this.appEvents.trigger('modal-body:flash', { text, messageClass });
|
||||
},
|
||||
|
||||
clearFlash() {
|
||||
this.appEvents.trigger('modal-body:clearFlash');
|
||||
},
|
||||
|
||||
showModal(...args) {
|
||||
return showModal(...args);
|
||||
},
|
||||
|
||||
@ -97,7 +97,7 @@ const Category = RestModel.extend({
|
||||
allow_badges: this.get('allow_badges'),
|
||||
custom_fields: this.get('custom_fields'),
|
||||
topic_template: this.get('topic_template'),
|
||||
suppress_from_homepage: this.get('suppress_from_homepage'),
|
||||
suppress_from_latest: this.get('suppress_from_latest'),
|
||||
all_topics_wiki: this.get('all_topics_wiki'),
|
||||
allowed_tags: this.get('allowed_tags'),
|
||||
allowed_tag_groups: this.get('allowed_tag_groups'),
|
||||
|
||||
@ -22,12 +22,21 @@ const LoginMethod = Ember.Object.extend({
|
||||
let methods;
|
||||
let preRegister;
|
||||
|
||||
export const LOGIN_METHODS = [
|
||||
"google_oauth2",
|
||||
"facebook",
|
||||
"twitter",
|
||||
"yahoo",
|
||||
"instagram",
|
||||
"github"
|
||||
];
|
||||
|
||||
export function findAll(siteSettings, capabilities, isMobileDevice) {
|
||||
if (methods) { return methods; }
|
||||
|
||||
methods = [];
|
||||
|
||||
[ "google_oauth2", "facebook", "cas", "twitter", "yahoo", "instagram", "github" ].forEach(name => {
|
||||
LOGIN_METHODS.forEach(name => {
|
||||
if (siteSettings["enable_" + name + "_logins"]) {
|
||||
const params = { name };
|
||||
if (name === "google_oauth2") {
|
||||
|
||||
@ -70,17 +70,17 @@ const TopicList = RestModel.extend({
|
||||
|
||||
|
||||
// loads topics with these ids "before" the current topics
|
||||
loadBefore(topic_ids) {
|
||||
loadBefore(topic_ids, storeInSession) {
|
||||
const topicList = this,
|
||||
topics = this.get('topics');
|
||||
|
||||
// refresh dupes
|
||||
topics.removeObjects(topics.filter(topic => topic_ids.indexOf(topic.get('id')) >= 0));
|
||||
|
||||
const url = `${Discourse.getURL("/")}${this.get('filter')}?topic_ids=${topic_ids.join(",")}`;
|
||||
const url = `${Discourse.getURL("/")}${this.get('filter')}.json?topic_ids=${topic_ids.join(",")}`;
|
||||
const store = this.store;
|
||||
|
||||
return ajax({ url }).then(result => {
|
||||
return ajax({ url, data: this.get("params") }).then(result => {
|
||||
let i = 0;
|
||||
topicList.forEachNew(TopicList.topicsFrom(store, result), function(t) {
|
||||
// highlight the first of the new topics so we can get a visual feedback
|
||||
@ -88,7 +88,7 @@ const TopicList = RestModel.extend({
|
||||
topics.insertAt(i,t);
|
||||
i++;
|
||||
});
|
||||
Discourse.Session.currentProp('topicList', topicList);
|
||||
if (storeInSession) Discourse.Session.currentProp('topicList', topicList);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { NotificationLevels } from 'discourse/lib/notification-levels';
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import { on } from "ember-addons/ember-computed-decorators";
|
||||
import { default as computed, on } from "ember-addons/ember-computed-decorators";
|
||||
import { defaultHomepage } from 'discourse/lib/utilities';
|
||||
import PreloadStore from 'preload-store';
|
||||
|
||||
@ -35,7 +34,7 @@ const TopicTrackingState = Discourse.Model.extend({
|
||||
tracker.incrementMessageCount();
|
||||
}
|
||||
|
||||
if (data.message_type === "new_topic" || data.message_type === "latest") {
|
||||
if (["new_topic", "latest"].includes(data.message_type)) {
|
||||
const muted_category_ids = Discourse.User.currentProp("muted_category_ids");
|
||||
if (_.include(muted_category_ids, data.payload.category_id)) {
|
||||
return;
|
||||
@ -55,7 +54,7 @@ const TopicTrackingState = Discourse.Model.extend({
|
||||
tracker.notify(data);
|
||||
}
|
||||
|
||||
if (data.message_type === "new_topic" || data.message_type === "unread" || data.message_type === "read") {
|
||||
if (["new_topic", "unread", "read"].includes(data.message_type)) {
|
||||
tracker.notify(data);
|
||||
const old = tracker.states["t" + data.topic_id];
|
||||
|
||||
@ -117,17 +116,17 @@ const TopicTrackingState = Discourse.Model.extend({
|
||||
}
|
||||
|
||||
if (filter === defaultHomepage()) {
|
||||
const suppressed_from_homepage_category_ids = Discourse.Site.currentProp("suppressed_from_homepage_category_ids");
|
||||
if (_.include(suppressed_from_homepage_category_ids, data.payload.category_id)) {
|
||||
const suppressed_from_latest_category_ids = Discourse.Site.currentProp("suppressed_from_latest_category_ids");
|
||||
if (_.include(suppressed_from_latest_category_ids, data.payload.category_id)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if ((filter === "all" || filter === "latest" || filter === "new") && data.message_type === "new_topic") {
|
||||
if (["all", "latest", "new"].includes(filter) && data.message_type === "new_topic") {
|
||||
this.addIncoming(data.topic_id);
|
||||
}
|
||||
|
||||
if ((filter === "all" || filter === "unread") && data.message_type === "unread") {
|
||||
if (["all", "unread"].includes(filter) && data.message_type === "unread") {
|
||||
const old = this.states["t" + data.topic_id];
|
||||
if(!old || old.highest_post_number === old.last_read_post_number) {
|
||||
this.addIncoming(data.topic_id);
|
||||
|
||||
@ -23,14 +23,16 @@ const User = RestModel.extend({
|
||||
hasPMs: Em.computed.gt("private_messages_stats.all", 0),
|
||||
hasStartedPMs: Em.computed.gt("private_messages_stats.mine", 0),
|
||||
hasUnreadPMs: Em.computed.gt("private_messages_stats.unread", 0),
|
||||
hasPosted: Em.computed.gt("post_count", 0),
|
||||
hasNotPosted: Em.computed.not("hasPosted"),
|
||||
canBeDeleted: Em.computed.and("can_be_deleted", "hasNotPosted"),
|
||||
|
||||
redirected_to_top: {
|
||||
reason: null,
|
||||
},
|
||||
|
||||
@computed("can_be_deleted", "post_count")
|
||||
canBeDeleted(canBeDeleted, postCount) {
|
||||
return canBeDeleted && postCount <= 5;
|
||||
},
|
||||
|
||||
@computed()
|
||||
stream() {
|
||||
return UserStream.create({ user: this });
|
||||
@ -304,6 +306,20 @@ const User = RestModel.extend({
|
||||
});
|
||||
},
|
||||
|
||||
loadSecondFactorCodes(password) {
|
||||
return ajax("/u/second_factors.json", {
|
||||
data: { password },
|
||||
type: 'POST'
|
||||
});
|
||||
},
|
||||
|
||||
toggleSecondFactor(token, enable) {
|
||||
return ajax("/u/second_factor.json", {
|
||||
data: { second_factor_token: token, enable },
|
||||
type: 'PUT'
|
||||
});
|
||||
},
|
||||
|
||||
loadUserAction(id) {
|
||||
const stream = this.get('stream');
|
||||
return ajax(`/user_actions/${id}.json`, { cache: 'false' }).then(result => {
|
||||
|
||||
@ -96,6 +96,7 @@ export default function() {
|
||||
this.route('archive');
|
||||
this.route('group', { path: 'group/:name'});
|
||||
this.route('groupArchive', { path: 'group/:name/archive'});
|
||||
this.route('tag', { path: 'tag/:id'});
|
||||
});
|
||||
|
||||
this.route('preferences', { resetNamespace: true }, function() {
|
||||
@ -110,6 +111,7 @@ export default function() {
|
||||
|
||||
this.route('username');
|
||||
this.route('email');
|
||||
this.route('second-factor');
|
||||
this.route('about', { path: '/about-me' });
|
||||
this.route('badgeTitle', { path: '/badge_title' });
|
||||
this.route('card-badge', { path: '/card-badge' });
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { emojiUnescape } from 'discourse/lib/text';
|
||||
|
||||
export default function (filter) {
|
||||
return Discourse.Route.extend({
|
||||
actions: {
|
||||
@ -20,6 +22,12 @@ export default function (filter) {
|
||||
// initialize "canLoadMore"
|
||||
model.set("canLoadMore", model.get("itemsLoaded") === 60);
|
||||
|
||||
model.get('content').forEach((item) => {
|
||||
if (item.get('title')) {
|
||||
item.set('title', emojiUnescape(Handlebars.Utils.escapeExpression(item.title)));
|
||||
}
|
||||
});
|
||||
|
||||
this.controllerFor("user-posts").set("model", model);
|
||||
},
|
||||
|
||||
|
||||
@ -1,10 +1,15 @@
|
||||
import UserTopicListRoute from "discourse/routes/user-topic-list";
|
||||
|
||||
// A helper to build a user topic list route
|
||||
export default (viewName, path) => {
|
||||
export default (viewName, path, channel) => {
|
||||
return UserTopicListRoute.extend({
|
||||
userActionType: Discourse.UserAction.TYPES.messages_received,
|
||||
|
||||
titleToken() {
|
||||
const key = viewName === "index" ? "inbox" : viewName;
|
||||
return [I18n.t(`user.messages.${key}`), I18n.t("user.private_messages")];
|
||||
},
|
||||
|
||||
actions: {
|
||||
didTransition() {
|
||||
this.controllerFor("user-topics-list")._showFooter();
|
||||
@ -19,6 +24,10 @@ export default (viewName, path) => {
|
||||
setupController() {
|
||||
this._super.apply(this, arguments);
|
||||
|
||||
if (channel) {
|
||||
this.controllerFor("user-topics-list").subscribe(`/private-messages/${channel}`);
|
||||
}
|
||||
|
||||
this.controllerFor("user-topics-list").setProperties({
|
||||
hideCategory: true,
|
||||
showPosters: true,
|
||||
@ -32,6 +41,8 @@ export default (viewName, path) => {
|
||||
},
|
||||
|
||||
deactivate() {
|
||||
this.controllerFor('user-topics-list').unsubscribe();
|
||||
|
||||
this.searchService.set(
|
||||
'searchContext',
|
||||
this.controllerFor("user").get("model.searchContext")
|
||||
|
||||
@ -91,7 +91,7 @@ export default function(filter, extras) {
|
||||
const topicOpts = {
|
||||
model,
|
||||
category: null,
|
||||
period: model.get('for_period') || (filter.indexOf('/') > 0 ? filter.split('/')[1] : ''),
|
||||
period: model.get('for_period') || (filter.indexOf('top/') >= 0 ? filter.split('/')[1] : ''),
|
||||
selected: [],
|
||||
expandGloballyPinned: true
|
||||
};
|
||||
|
||||
@ -12,20 +12,24 @@ const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, {
|
||||
this.render("discovery/categories", { outlet: "list-container" });
|
||||
},
|
||||
|
||||
model() {
|
||||
const style = !this.site.mobileView && this.siteSettings.desktop_category_page_style;
|
||||
const parentCategory = this.get("model.parentCategory");
|
||||
findCategories() {
|
||||
let style = !this.site.mobileView &&
|
||||
this.siteSettings.desktop_category_page_style;
|
||||
|
||||
let promise;
|
||||
let parentCategory = this.get("model.parentCategory");
|
||||
if (parentCategory) {
|
||||
promise = CategoryList.listForParent(this.store, parentCategory);
|
||||
return CategoryList.listForParent(this.store, parentCategory);
|
||||
} else if (style === "categories_and_latest_topics") {
|
||||
promise = this._loadCategoriesAndLatestTopics();
|
||||
} else {
|
||||
promise = CategoryList.list(this.store);
|
||||
return this._findCategoriesAndTopics('latest');
|
||||
} else if (style === "categories_and_top_topics") {
|
||||
return this._findCategoriesAndTopics('top');
|
||||
}
|
||||
|
||||
return promise.then(model => {
|
||||
return CategoryList.list(this.store);
|
||||
},
|
||||
|
||||
model() {
|
||||
return this.findCategories().then(model => {
|
||||
const tracking = this.topicTrackingState;
|
||||
if (tracking) {
|
||||
tracking.sync(model, "categories");
|
||||
@ -35,26 +39,31 @@ const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, {
|
||||
});
|
||||
},
|
||||
|
||||
_loadCategoriesAndLatestTopics() {
|
||||
const wrappedCategoriesList = PreloadStore.getAndRemove("categories_list");
|
||||
const topicListLatest = PreloadStore.getAndRemove("topic_list_latest");
|
||||
const categoriesList = wrappedCategoriesList && wrappedCategoriesList.category_list;
|
||||
if (categoriesList && topicListLatest) {
|
||||
return new Ember.RSVP.Promise(resolve => {
|
||||
const result = Ember.Object.create({
|
||||
categories: CategoryList.categoriesFrom(this.store, wrappedCategoriesList),
|
||||
topics: TopicList.topicsFrom(this.store, topicListLatest),
|
||||
_findCategoriesAndTopics(filter) {
|
||||
return Ember.RSVP.hash({
|
||||
wrappedCategoriesList: PreloadStore.getAndRemove("categories_list"),
|
||||
topicsList: PreloadStore.getAndRemove(`topic_list_${filter}`)
|
||||
}).then(hash => {
|
||||
let { wrappedCategoriesList, topicsList } = hash;
|
||||
let categoriesList = wrappedCategoriesList &&
|
||||
wrappedCategoriesList.category_list;
|
||||
|
||||
if (categoriesList && topicsList) {
|
||||
return Ember.Object.create({
|
||||
categories: CategoryList.categoriesFrom(
|
||||
this.store,
|
||||
wrappedCategoriesList
|
||||
),
|
||||
topics: TopicList.topicsFrom(this.store, topicsList),
|
||||
can_create_category: categoriesList.can_create_category,
|
||||
can_create_topic: categoriesList.can_create_topic,
|
||||
draft_key: categoriesList.draft_key,
|
||||
draft: categoriesList.draft,
|
||||
draft_sequence: categoriesList.draft_sequence
|
||||
});
|
||||
|
||||
resolve(result);
|
||||
});
|
||||
} else {
|
||||
return ajax("/categories_and_latest").then(result => {
|
||||
}
|
||||
// Otherwise, return the ajax result
|
||||
return ajax(`/categories_and_${filter}`).then(result => {
|
||||
return Ember.Object.create({
|
||||
categories: CategoryList.categoriesFrom(this.store, result),
|
||||
topics: TopicList.topicsFrom(this.store, result),
|
||||
@ -65,7 +74,7 @@ const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, {
|
||||
draft_sequence: result.category_list.draft_sequence
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
titleToken() {
|
||||
|
||||
@ -0,0 +1,15 @@
|
||||
import RestrictedUserRoute from "discourse/routes/restricted-user";
|
||||
|
||||
export default RestrictedUserRoute.extend({
|
||||
model() {
|
||||
return this.modelFor('user');
|
||||
},
|
||||
|
||||
renderTemplate() {
|
||||
return this.render({ into: 'user' });
|
||||
},
|
||||
|
||||
setupController(controller, model) {
|
||||
controller.setProperties({ model, newUsername: model.get('username') });
|
||||
}
|
||||
});
|
||||
@ -15,6 +15,10 @@ export default RestrictedUserRoute.extend({
|
||||
},
|
||||
|
||||
actions: {
|
||||
showTwoFactorModal() {
|
||||
showModal('second-factor-intro');
|
||||
},
|
||||
|
||||
showAvatarSelector() {
|
||||
showModal('avatar-selector');
|
||||
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
import createPMRoute from "discourse/routes/build-private-messages-route";
|
||||
|
||||
export default createPMRoute('archive', 'private-messages-archive');
|
||||
export default createPMRoute('archive', 'private-messages-archive', 'archive');
|
||||
|
||||
@ -1,26 +1,41 @@
|
||||
import createPMRoute from "discourse/routes/build-private-messages-route";
|
||||
|
||||
export default createPMRoute('groups', 'private-messages-groups').extend({
|
||||
model(params) {
|
||||
const username = this.modelFor("user").get("username_lower");
|
||||
return this.store.findFiltered("topicList", {
|
||||
filter: `topics/private-messages-group/${username}/${params.name}/archive`
|
||||
});
|
||||
},
|
||||
groupName: null,
|
||||
|
||||
afterModel(model) {
|
||||
const split = model.get("filter").split('/');
|
||||
const groupName = split[split.length-2];
|
||||
const groups = this.modelFor("user").get("groups");
|
||||
const group = _.first(groups.filterBy("name", groupName));
|
||||
this.controllerFor("user-private-messages").set("group", group);
|
||||
},
|
||||
titleToken() {
|
||||
const groupName = this.get('groupName');
|
||||
|
||||
setupController(controller, model) {
|
||||
this._super.apply(this, arguments);
|
||||
const split = model.get("filter").split('/');
|
||||
const group = split[split.length-2];
|
||||
this.controllerFor("user-private-messages").set("groupFilter", group);
|
||||
this.controllerFor("user-private-messages").set("archive", true);
|
||||
}
|
||||
if (groupName) {
|
||||
return [
|
||||
`${groupName.capitalize()} ${I18n.t('user.messages.archive')}`,
|
||||
I18n.t("user.private_messages")
|
||||
];
|
||||
};
|
||||
},
|
||||
|
||||
model(params) {
|
||||
const username = this.modelFor("user").get("username_lower");
|
||||
return this.store.findFiltered("topicList", {
|
||||
filter: `topics/private-messages-group/${username}/${params.name}/archive`
|
||||
});
|
||||
},
|
||||
|
||||
afterModel(model) {
|
||||
const split = model.get("filter").split('/');
|
||||
const groupName = split[split.length-2];
|
||||
this.set("groupName", groupName);
|
||||
const groups = this.modelFor("user").get("groups");
|
||||
const group = _.first(groups.filterBy("name", groupName));
|
||||
this.controllerFor("user-private-messages").set("group", group);
|
||||
},
|
||||
|
||||
setupController(controller, model) {
|
||||
this._super.apply(this, arguments);
|
||||
const split = model.get("filter").split('/');
|
||||
const group = split[split.length-2];
|
||||
this.controllerFor("user-private-messages").set("groupFilter", group);
|
||||
this.controllerFor("user-private-messages").set("archive", true);
|
||||
this.controllerFor("user-topics-list").subscribe(`/private-messages/group/${group}/archive`);
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,24 +1,33 @@
|
||||
import createPMRoute from "discourse/routes/build-private-messages-route";
|
||||
|
||||
export default createPMRoute('groups', 'private-messages-groups').extend({
|
||||
model(params) {
|
||||
const username = this.modelFor("user").get("username_lower");
|
||||
return this.store.findFiltered("topicList", {
|
||||
filter: `topics/private-messages-group/${username}/${params.name}`
|
||||
});
|
||||
},
|
||||
groupName: null,
|
||||
|
||||
afterModel(model) {
|
||||
const groupName = _.last(model.get("filter").split('/'));
|
||||
const groups = this.modelFor("user").get("groups");
|
||||
const group = _.first(groups.filterBy("name", groupName));
|
||||
this.controllerFor("user-private-messages").set("group", group);
|
||||
},
|
||||
titleToken() {
|
||||
const groupName = this.get('groupName');
|
||||
if (groupName) return [groupName.capitalize(), I18n.t("user.private_messages")];
|
||||
},
|
||||
|
||||
setupController(controller, model) {
|
||||
this._super.apply(this, arguments);
|
||||
const group = _.last(model.get("filter").split('/'));
|
||||
this.controllerFor("user-private-messages").set("groupFilter", group);
|
||||
this.controllerFor("user-private-messages").set("archive", false);
|
||||
}
|
||||
model(params) {
|
||||
const username = this.modelFor("user").get("username_lower");
|
||||
return this.store.findFiltered("topicList", {
|
||||
filter: `topics/private-messages-group/${username}/${params.name}`
|
||||
});
|
||||
},
|
||||
|
||||
afterModel(model) {
|
||||
const groupName = _.last(model.get("filter").split('/'));
|
||||
this.set("groupName", groupName);
|
||||
const groups = this.modelFor("user").get("groups");
|
||||
const group = _.first(groups.filterBy("name", groupName));
|
||||
this.controllerFor("user-private-messages").set("group", group);
|
||||
},
|
||||
|
||||
setupController(controller, model) {
|
||||
this._super.apply(this, arguments);
|
||||
const group = _.last(model.get("filter").split('/'));
|
||||
this.controllerFor("user-private-messages").set("groupFilter", group);
|
||||
this.controllerFor("user-private-messages").set("archive", false);
|
||||
this.controllerFor("user-topics-list").subscribe(`/private-messages/group/${group}`);
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
import createPMRoute from "discourse/routes/build-private-messages-route";
|
||||
|
||||
export default createPMRoute('index', 'private-messages');
|
||||
export default createPMRoute('index', 'private-messages', 'inbox');
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
import createPMRoute from "discourse/routes/build-private-messages-route";
|
||||
|
||||
export default createPMRoute('sent', 'private-messages-sent');
|
||||
export default createPMRoute('sent', 'private-messages-sent', 'sent');
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
import createPMRoute from "discourse/routes/build-private-messages-route";
|
||||
|
||||
export default createPMRoute('tags', 'private-messages-tags').extend({
|
||||
model(params) {
|
||||
const username = this.modelFor("user").get("username_lower");
|
||||
return this.store.findFiltered("topicList", {
|
||||
filter: `topics/private-messages-tag/${username}/${params.id}`
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -8,10 +8,11 @@
|
||||
<div class='title'>
|
||||
<h3>{{bg.badgeGrouping.displayName}}</h3>
|
||||
</div>
|
||||
|
||||
{{#each bg.badges as |b|}}
|
||||
{{badge-card badge=b filterUser=b.has_badge username=currentUser.username}}
|
||||
{{/each}}
|
||||
<div class="badge-group-list">
|
||||
{{#each bg.badges as |b|}}
|
||||
{{badge-card badge=b filterUser=b.has_badge username=currentUser.username}}
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
@ -37,6 +37,7 @@
|
||||
{{#if userBadges}}
|
||||
<div class="user-badges {{model.slug}}">
|
||||
{{#load-more selector=".badge-info" action="loadMore"}}
|
||||
<div class="badges-granted">
|
||||
{{#each userBadges as |ub|}}
|
||||
{{#user-info user=ub.user size="medium" class="badge-info" date=ub.granted_at}}
|
||||
<div class="granted-on">{{i18n 'badges.granted_on' date=(inline-date ub.granted_at)}}</div>
|
||||
@ -45,6 +46,7 @@
|
||||
{{/if}}
|
||||
{{/user-info}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/load-more}}
|
||||
|
||||
{{#unless canLoadMore}}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<p>{{i18n (concat "topics.bulk." title)}}</p>
|
||||
|
||||
<p>{{tag-chooser tags=tags categoryId=categoryId}}</p>
|
||||
<p>{{tag-chooser filterPlaceholder=null tags=tags categoryId=categoryId}}</p>
|
||||
|
||||
{{d-button action=action disabled=emptyTags label=(concat "topics.bulk." label)}}
|
||||
|
||||
@ -5,13 +5,15 @@
|
||||
<span class='check-display status-checked'>{{d-icon "check"}}</span>
|
||||
{{/if}}
|
||||
<div class='badge-contents'>
|
||||
<div class='badge-icon {{badge.badgeTypeClassName}}'>
|
||||
{{icon-or-image badge.icon}}
|
||||
</div>
|
||||
<div class='badge-info'>
|
||||
<div class='badge-info-item'>
|
||||
<h3><a href={{url}}>{{badge.name}}</a></h3>
|
||||
<div class='badge-summary'>{{{summary}}}</div>
|
||||
<a href={{url}} class="badge-link">
|
||||
<div class='badge-icon {{badge.badgeTypeClassName}}'>
|
||||
{{icon-or-image badge.icon}}
|
||||
</div>
|
||||
</div>
|
||||
<div class='badge-info'>
|
||||
<div class='badge-info-item'>
|
||||
<h3>{{badge.name}}</h3>
|
||||
<div class='badge-summary'>{{{summary}}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@ -1,4 +1,13 @@
|
||||
{{#conditional-loading-spinner condition=loading}}
|
||||
{{#if hasIncoming}}
|
||||
<div class="show-mores">
|
||||
<div class='alert alert-info clickable' {{action "showInserted"}}>
|
||||
{{count-i18n key="topic_count_" suffix="latest" count=incomingCount}}
|
||||
{{i18n 'click_to_show'}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if topics}}
|
||||
{{topic-list showParticipants=showParticipants
|
||||
showPosters=showPosters
|
||||
|
||||
@ -3,5 +3,5 @@
|
||||
</div>
|
||||
|
||||
<div class='column'>
|
||||
{{latest-topic-list topics=topics}}
|
||||
{{categories-topic-list topics=topics filter="latest" class="latest-topic-list"}}
|
||||
</div>
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
<div class='column categories'>
|
||||
{{categories-only categories=categories}}
|
||||
</div>
|
||||
|
||||
<div class='column'>
|
||||
{{categories-topic-list topics=topics filter="top" class="top-topic-list"}}
|
||||
</div>
|
||||
@ -11,6 +11,7 @@
|
||||
{{#if c.read_restricted}}
|
||||
{{d-icon 'lock'}}
|
||||
{{/if}}
|
||||
{{category-title-before category=c}}
|
||||
{{c.name}}
|
||||
</h3>
|
||||
</a>
|
||||
|
||||
@ -24,6 +24,7 @@
|
||||
<div class='subcategories'>
|
||||
{{#each c.subcategories as |s|}}
|
||||
<span class='subcategory'>
|
||||
{{category-title-before category=s}}
|
||||
{{category-link s hideParent="true"}}
|
||||
{{category-unread category=s}}
|
||||
</span>
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
<div class='table-heading' aria-role="heading" aria-level="2">
|
||||
{{i18n (concat "filters." filter ".title")}}
|
||||
</div>
|
||||
|
||||
{{#if topics}}
|
||||
{{#each topics as |t|}}
|
||||
{{latest-topic-list-item topic=t}}
|
||||
{{/each}}
|
||||
<div class="more-topics">
|
||||
<a href="/{{filter}}" class="btn pull-right">{{i18n "more"}}</a>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class='no-topics'>
|
||||
<h3>{{i18n (concat "topics.none." filter)}}</h3>
|
||||
</div>
|
||||
{{/if}}
|
||||
@ -0,0 +1 @@
|
||||
{{plugin-outlet name="category-title-before" noTags=true args=(hash category=category)}}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user