Version bump
This commit is contained in:
commit
710af4b28c
5
.gitignore
vendored
5
.gitignore
vendored
@ -19,8 +19,6 @@ public/tombstone/*
|
||||
|
||||
# Ignore bundler config
|
||||
/.bundle
|
||||
/.vagrant
|
||||
/.vagrantfile
|
||||
/cache
|
||||
/coverage/*
|
||||
|
||||
@ -95,9 +93,6 @@ config/fog_credentials.yml
|
||||
script/download_db
|
||||
script/refresh_db
|
||||
|
||||
# temp directory for chef (used to configure vagrant VM)
|
||||
chef/tmp/*
|
||||
|
||||
# .procfile
|
||||
.procfile
|
||||
|
||||
|
||||
18
.tx/config
18
.tx/config
@ -50,6 +50,24 @@ source_file = plugins/discourse-presence/config/locales/server.en.yml
|
||||
source_lang = en
|
||||
type = YML
|
||||
|
||||
[discourse-org.coreplugindetailsclientyml]
|
||||
file_filter = plugins/discourse-details/config/locales/client.<lang>.yml
|
||||
source_file = plugins/discourse-details/config/locales/client.en.yml
|
||||
source_lang = en
|
||||
type = YML
|
||||
|
||||
[discourse-org.coreplugindetailsserveryml]
|
||||
file_filter = plugins/discourse-details/config/locales/server.<lang>.yml
|
||||
source_file = plugins/discourse-details/config/locales/server.en.yml
|
||||
source_lang = en
|
||||
type = YML
|
||||
|
||||
[discourse-org.corepluginnginx-performance-reportserveryml]
|
||||
file_filter = plugins/discourse-nginx-performance-report/config/locales/server.<lang>.yml
|
||||
source_file = plugins/discourse-nginx-performance-report/config/locales/server.en.yml
|
||||
source_lang = en
|
||||
type = YML
|
||||
|
||||
[discourse-org.403html]
|
||||
file_filter = public/403.<lang>.html
|
||||
source_file = public/403.html
|
||||
|
||||
5
Gemfile
5
Gemfile
@ -24,8 +24,7 @@ else
|
||||
gem 'seed-fu'
|
||||
end
|
||||
|
||||
gem 'mail'
|
||||
gem 'mime-types', require: 'mime/types/columnar'
|
||||
gem 'mail', require: false
|
||||
gem 'mini_mime'
|
||||
gem 'mini_suffix'
|
||||
|
||||
@ -59,7 +58,7 @@ gem 'aws-sdk-s3', require: false
|
||||
gem 'excon', require: false
|
||||
gem 'unf', require: false
|
||||
|
||||
gem 'email_reply_trimmer', '0.1.10'
|
||||
gem 'email_reply_trimmer', '0.1.11'
|
||||
|
||||
# Forked until https://github.com/toy/image_optim/pull/149 is merged
|
||||
gem 'discourse_image_optim', require: 'image_optim'
|
||||
|
||||
24
Gemfile.lock
24
Gemfile.lock
@ -91,7 +91,7 @@ GEM
|
||||
image_size (~> 1.5)
|
||||
in_threads (~> 1.3)
|
||||
progress (~> 3.0, >= 3.0.1)
|
||||
email_reply_trimmer (0.1.10)
|
||||
email_reply_trimmer (0.1.11)
|
||||
ember-data-source (2.2.1)
|
||||
ember-source (>= 1.8, < 3.0)
|
||||
ember-handlebars-template (0.7.5)
|
||||
@ -159,20 +159,17 @@ GEM
|
||||
logstash-logger (0.25.1)
|
||||
logstash-event (~> 1.2)
|
||||
logster (1.2.9)
|
||||
loofah (2.2.1)
|
||||
loofah (2.2.2)
|
||||
crass (~> 1.0.2)
|
||||
nokogiri (>= 1.5.9)
|
||||
lru_redux (1.1.0)
|
||||
mail (2.6.6)
|
||||
mime-types (>= 1.16, < 4)
|
||||
mail (2.7.0)
|
||||
mini_mime (>= 0.1.1)
|
||||
memory_profiler (0.9.10)
|
||||
message_bus (2.1.2)
|
||||
rack (>= 1.1.3)
|
||||
metaclass (0.0.4)
|
||||
method_source (0.8.2)
|
||||
mime-types (3.1)
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2016.0521)
|
||||
mini_mime (0.1.3)
|
||||
mini_portile2 (2.3.0)
|
||||
mini_racer (0.1.15)
|
||||
@ -256,21 +253,21 @@ GEM
|
||||
public_suffix (2.0.5)
|
||||
puma (3.9.1)
|
||||
r2 (0.2.6)
|
||||
rack (2.0.3)
|
||||
rack-mini-profiler (0.10.7)
|
||||
rack (2.0.4)
|
||||
rack-mini-profiler (1.0.0)
|
||||
rack (>= 1.2.0)
|
||||
rack-openid (1.3.1)
|
||||
rack (>= 1.1.0)
|
||||
ruby-openid (>= 2.1.8)
|
||||
rack-protection (2.0.0)
|
||||
rack-protection (2.0.1)
|
||||
rack
|
||||
rack-test (0.7.0)
|
||||
rack (>= 1.0, < 3)
|
||||
rails-dom-testing (2.0.3)
|
||||
activesupport (>= 4.2.0)
|
||||
nokogiri (>= 1.6)
|
||||
rails-html-sanitizer (1.0.3)
|
||||
loofah (~> 2.0)
|
||||
rails-html-sanitizer (1.0.4)
|
||||
loofah (~> 2.2, >= 2.2.2)
|
||||
rails_multisite (2.0.4)
|
||||
activerecord (> 4.2, < 6)
|
||||
railties (> 4.2, < 6)
|
||||
@ -417,7 +414,7 @@ DEPENDENCIES
|
||||
cppjieba_rb
|
||||
discourse-qunit-rails
|
||||
discourse_image_optim
|
||||
email_reply_trimmer (= 0.1.10)
|
||||
email_reply_trimmer (= 0.1.11)
|
||||
ember-handlebars-template (= 0.7.5)
|
||||
ember-rails (= 0.18.5)
|
||||
ember-source (= 2.13.3)
|
||||
@ -445,7 +442,6 @@ DEPENDENCIES
|
||||
mail
|
||||
memory_profiler
|
||||
message_bus
|
||||
mime-types
|
||||
mini_mime
|
||||
mini_racer
|
||||
mini_suffix
|
||||
|
||||
@ -22,7 +22,7 @@ Browse [lots more notable Discourse instances](https://www.discourse.org/custome
|
||||
|
||||
## Development
|
||||
|
||||
1. If you're **brand new to Ruby and Rails**, please see [**Discourse as Your First Rails App**](http://blog.discourse.org/2013/04/discourse-as-your-first-rails-app/) or our [**Discourse Vagrant Developer Guide**](docs/VAGRANT.md), which includes a development environment in a virtual machine.
|
||||
1. If you're **brand new to Ruby and Rails**, please see [**Discourse as Your First Rails App**](http://blog.discourse.org/2013/04/discourse-as-your-first-rails-app/).
|
||||
|
||||
2. If you're familiar with how Rails works and are comfortable setting up your own environment, use our [**Discourse Advanced Developer Guide**](docs/DEVELOPER-ADVANCED.md).
|
||||
|
||||
|
||||
48
Vagrantfile
vendored
48
Vagrantfile
vendored
@ -1,48 +0,0 @@
|
||||
# -*- mode: ruby -*-
|
||||
# vi: set ft=ruby :
|
||||
# See https://github.com/discourse/discourse/blob/master/docs/VAGRANT.md
|
||||
#
|
||||
Vagrant.configure("2") do |config|
|
||||
config.vm.box = 'discourse-16.04'
|
||||
config.vm.box_url = "https://www.dropbox.com/s/2132770g1e05c6d/discourse.box?dl=1"
|
||||
|
||||
# Make this VM reachable on the host network as well, so that other
|
||||
# VM's running other browsers can access our dev server.
|
||||
config.vm.network :private_network, ip: "192.168.10.200"
|
||||
|
||||
# Make it so that network access from the vagrant guest is able to
|
||||
# use SSH private keys that are present on the host without copying
|
||||
# them into the VM.
|
||||
config.ssh.forward_agent = true
|
||||
|
||||
config.vm.provider :virtualbox do |v|
|
||||
# This setting gives the VM 1024MB of RAM instead of the default 384.
|
||||
v.customize ["modifyvm", :id, "--memory", [ENV['DISCOURSE_VM_MEM'].to_i, 1024].max]
|
||||
|
||||
# Who has a single core cpu these days anyways?
|
||||
cpu_count = 2
|
||||
|
||||
# Determine the available cores in host system.
|
||||
# This mostly helps on linux, but it couldn't hurt on MacOSX.
|
||||
if RUBY_PLATFORM =~ /linux/
|
||||
cpu_count = `nproc`.to_i
|
||||
elsif RUBY_PLATFORM =~ /darwin/
|
||||
cpu_count = `sysctl -n hw.ncpu`.to_i
|
||||
end
|
||||
|
||||
# Assign additional cores to the guest OS.
|
||||
v.customize ["modifyvm", :id, "--cpus", cpu_count]
|
||||
v.customize ["modifyvm", :id, "--ioapic", "on"]
|
||||
|
||||
# This setting makes it so that network access from inside the vagrant guest
|
||||
# is able to resolve DNS using the hosts VPN connection.
|
||||
v.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]
|
||||
end
|
||||
|
||||
config.vm.network :forwarded_port, guest: 3000, host: 4000
|
||||
config.vm.network :forwarded_port, guest: 1080, host: 4080 # Mailcatcher
|
||||
|
||||
nfs_setting = RUBY_PLATFORM =~ /darwin/ || RUBY_PLATFORM =~ /linux/
|
||||
config.vm.synced_folder ".", "/vagrant", id: "vagrant-root"
|
||||
|
||||
end
|
||||
@ -5,6 +5,8 @@ export default Ember.Controller.extend({
|
||||
maximized: false,
|
||||
section: null,
|
||||
|
||||
editRouteName: 'adminCustomizeThemes.edit',
|
||||
|
||||
targets: [
|
||||
{ id: 0, name: 'common' },
|
||||
{ id: 1, name: 'desktop' },
|
||||
@ -52,7 +54,7 @@ export default Ember.Controller.extend({
|
||||
|
||||
let fields = this.get('model.theme_fields');
|
||||
let field = fields && fields.find(f => (f.target === target));
|
||||
this.replaceRoute('adminCustomizeThemes.edit', this.get('model.id'), target, field && field.name);
|
||||
this.replaceRoute(this.get('editRouteName'), this.get('model.id'), target, field && field.name);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -8,6 +8,8 @@ const THEME_UPLOAD_VAR = 2;
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
|
||||
editRouteName: 'adminCustomizeThemes.edit',
|
||||
|
||||
@computed("model", "allThemes")
|
||||
parentThemes(model, allThemes) {
|
||||
let parents = allThemes.filter(theme =>
|
||||
@ -142,7 +144,7 @@ export default Ember.Controller.extend({
|
||||
},
|
||||
|
||||
editTheme() {
|
||||
let edit = ()=>this.transitionToRoute('adminCustomizeThemes.edit', this.get('model.id'), 'common', 'scss');
|
||||
let edit = ()=>this.transitionToRoute(this.get('editRouteName'), this.get('model.id'), 'common', 'scss');
|
||||
|
||||
if (this.get("model.remote_theme")) {
|
||||
bootbox.confirm(I18n.t("admin.customize.theme.edit_confirm"), result => {
|
||||
|
||||
@ -19,6 +19,7 @@ export default Ember.Controller.extend({
|
||||
versionCheck: null,
|
||||
dashboardFetchedAt: null,
|
||||
showVersionChecks: setting('version_checks'),
|
||||
exceptionController: Ember.inject.controller('exception'),
|
||||
|
||||
@computed('problems.length')
|
||||
foundProblems(problemsLength) {
|
||||
@ -39,10 +40,10 @@ export default Ember.Controller.extend({
|
||||
|
||||
fetchDashboard() {
|
||||
if (!this.get('dashboardFetchedAt') || moment().subtract(30, 'minutes').toDate() > this.get('dashboardFetchedAt')) {
|
||||
this.set('dashboardFetchedAt', new Date());
|
||||
this.set('loading', true);
|
||||
const versionChecks = this.siteSettings.version_checks;
|
||||
AdminDashboard.find().then(d => {
|
||||
this.set('dashboardFetchedAt', new Date());
|
||||
if (versionChecks) {
|
||||
this.set('versionCheck', VersionCheck.create(d.version_check));
|
||||
}
|
||||
@ -56,6 +57,10 @@ export default Ember.Controller.extend({
|
||||
}
|
||||
|
||||
ATTRIBUTES.forEach(a => this.set(a, d[a]));
|
||||
}).catch(e => {
|
||||
this.get('exceptionController').set('thrown', e.jqXHR);
|
||||
this.replaceRoute('exception');
|
||||
}).finally(() => {
|
||||
this.set('loading', false);
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,110 +0,0 @@
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
adminGroupsType: Ember.inject.controller(),
|
||||
disableSave: false,
|
||||
savingStatus: '',
|
||||
|
||||
aliasLevelOptions: function() {
|
||||
return [
|
||||
{ name: I18n.t("groups.alias_levels.nobody"), value: 0 },
|
||||
{ name: I18n.t("groups.alias_levels.mods_and_admins"), value: 2 },
|
||||
{ name: I18n.t("groups.alias_levels.members_mods_and_admins"), value: 3 },
|
||||
{ name: I18n.t("groups.alias_levels.everyone"), value: 99 }
|
||||
];
|
||||
}.property(),
|
||||
|
||||
visibilityLevelOptions: function() {
|
||||
return [
|
||||
{ name: I18n.t("groups.visibility_levels.public"), value: 0 },
|
||||
{ name: I18n.t("groups.visibility_levels.members"), value: 1 },
|
||||
{ name: I18n.t("groups.visibility_levels.staff"), value: 2 },
|
||||
{ name: I18n.t("groups.visibility_levels.owners"), value: 3 }
|
||||
];
|
||||
}.property(),
|
||||
|
||||
trustLevelOptions: function() {
|
||||
return [
|
||||
{ name: I18n.t("groups.trust_levels.none"), value: 0 },
|
||||
{ name: 1, value: 1 }, { name: 2, value: 2 }, { name: 3, value: 3 }, { name: 4, value: 4 }
|
||||
];
|
||||
}.property(),
|
||||
|
||||
@computed('model.visibility_level', 'model.public_admission')
|
||||
disableMembershipRequestSetting(visibility_level, publicAdmission) {
|
||||
visibility_level = parseInt(visibility_level);
|
||||
return (visibility_level !== 0) || publicAdmission;
|
||||
},
|
||||
|
||||
@computed('model.visibility_level', 'model.allow_membership_requests')
|
||||
disablePublicSetting(visibility_level, allowMembershipRequests) {
|
||||
visibility_level = parseInt(visibility_level);
|
||||
return (visibility_level !== 0) || allowMembershipRequests;
|
||||
},
|
||||
|
||||
actions: {
|
||||
removeOwner(member) {
|
||||
const self = this,
|
||||
message = I18n.t("admin.groups.delete_owner_confirm", { username: member.get("username"), group: this.get("model.name") });
|
||||
return bootbox.confirm(message, I18n.t("no_value"), I18n.t("yes_value"), function(confirm) {
|
||||
if (confirm) {
|
||||
self.get("model").removeOwner(member);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
addOwners() {
|
||||
if (Em.isEmpty(this.get("model.ownerUsernames"))) { return; }
|
||||
this.get("model").addOwners(this.get("model.ownerUsernames")).catch(popupAjaxError);
|
||||
this.set("model.ownerUsernames", null);
|
||||
},
|
||||
|
||||
save() {
|
||||
const group = this.get('model'),
|
||||
groupsController = this.get("adminGroupsType"),
|
||||
groupType = groupsController.get("type");
|
||||
|
||||
this.set('disableSave', true);
|
||||
this.set('savingStatus', I18n.t('saving'));
|
||||
|
||||
let promise = group.get("id") ? group.save() : group.create().then(() => groupsController.get('model').addObject(group));
|
||||
|
||||
promise.then(() => {
|
||||
this.transitionToRoute("adminGroup", groupType, group.get('name'));
|
||||
this.set('savingStatus', I18n.t('saved'));
|
||||
}).catch(popupAjaxError)
|
||||
.finally(() => this.set('disableSave', false));
|
||||
},
|
||||
|
||||
destroy() {
|
||||
const group = this.get('model'),
|
||||
groupsController = this.get('adminGroupsType'),
|
||||
self = this;
|
||||
|
||||
if (!group.get('id')) {
|
||||
self.transitionToRoute('adminGroupsType.index', 'custom');
|
||||
return;
|
||||
}
|
||||
|
||||
this.set('disableSave', true);
|
||||
|
||||
bootbox.confirm(
|
||||
I18n.t("admin.groups.delete_confirm"),
|
||||
I18n.t("no_value"),
|
||||
I18n.t("yes_value"),
|
||||
function(confirmed) {
|
||||
if (confirmed) {
|
||||
group.destroy().then(() => {
|
||||
groupsController.get('model').removeObject(group);
|
||||
self.transitionToRoute('adminGroups.index');
|
||||
}).catch(() => bootbox.alert(I18n.t("admin.groups.delete_failed")))
|
||||
.finally(() => self.set('disableSave', false));
|
||||
} else {
|
||||
self.set('disableSave', false);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -1,4 +0,0 @@
|
||||
export default Ember.Controller.extend({
|
||||
adminGroupsBulk: Ember.inject.controller(),
|
||||
bulkAddResponse: Ember.computed.alias('adminGroupsBulk.bulkAddResponse')
|
||||
});
|
||||
@ -1,37 +0,0 @@
|
||||
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({
|
||||
users: null,
|
||||
groupId: null,
|
||||
saving: false,
|
||||
bulkAddResponse: null,
|
||||
|
||||
@computed('saving', 'users', 'groupId')
|
||||
buttonDisabled(saving, users, groupId) {
|
||||
return saving || !groupId || !users || !users.length;
|
||||
},
|
||||
|
||||
actions: {
|
||||
addToGroup() {
|
||||
if (this.get('saving')) { return; }
|
||||
|
||||
const users = this.get('users').split("\n")
|
||||
.uniq()
|
||||
.reject(x => x.length === 0);
|
||||
|
||||
this.set('saving', true);
|
||||
ajax('/admin/groups/bulk', {
|
||||
data: { users, group_id: this.get('groupId') },
|
||||
method: 'PUT'
|
||||
}).then(result => {
|
||||
this.set('bulkAddResponse', result);
|
||||
this.transitionToRoute('adminGroups.bulkComplete');
|
||||
}).catch(popupAjaxError).finally(() => {
|
||||
this.set('saving', false);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -1,11 +0,0 @@
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
adminGroupsType: Ember.inject.controller(),
|
||||
sortedGroups: Ember.computed.alias("adminGroupsType.sortedGroups"),
|
||||
|
||||
@computed("sortedGroups")
|
||||
messageKey(sortedGroups) {
|
||||
return `admin.groups.${sortedGroups.length > 0 ? 'none_selected' : 'no_custom_groups'}`;
|
||||
}
|
||||
});
|
||||
@ -1,20 +0,0 @@
|
||||
import { ajax } from 'discourse/lib/ajax';
|
||||
export default Ember.Controller.extend({
|
||||
sortedGroups: Ember.computed.sort('model', 'groupSorting'),
|
||||
groupSorting: ['name'],
|
||||
|
||||
refreshingAutoGroups: false,
|
||||
|
||||
isAuto: Ember.computed.equal('type', 'automatic'),
|
||||
|
||||
actions: {
|
||||
refreshAutoGroups() {
|
||||
this.set('refreshingAutoGroups', true);
|
||||
ajax('/admin/groups/refresh_automatic_groups', {type: 'POST'}).then(() => {
|
||||
this.transitionToRoute("adminGroupsType", "automatic").then(() => {
|
||||
this.set('refreshingAutoGroups', false);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -24,9 +24,13 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
||||
'model.can_disable_second_factor'
|
||||
),
|
||||
|
||||
automaticGroups: function() {
|
||||
return this.get("model.automaticGroups").map((g) => g.name).join(", ");
|
||||
}.property("model.automaticGroups"),
|
||||
@computed("model.automaticGroups")
|
||||
automaticGroups(automaticGroups) {
|
||||
return automaticGroups.map(group => {
|
||||
const name = Ember.String.htmlSafe(group.name);
|
||||
return `<a href="/groups/${name}">${name}</a>`;
|
||||
}).join(", ");
|
||||
},
|
||||
|
||||
userFields: function() {
|
||||
const siteUserFields = this.site.get('user_fields'),
|
||||
|
||||
@ -10,11 +10,11 @@ export default Ember.Controller.extend({
|
||||
return (this.get('adminWatchedWords.model') || []).findBy('nameKey', actionName);
|
||||
},
|
||||
|
||||
@computed('adminWatchedWords.model', 'actionNameKey')
|
||||
filteredContent() {
|
||||
if (!this.get('actionNameKey')) { return []; }
|
||||
@computed('actionNameKey', 'adminWatchedWords.model')
|
||||
filteredContent(actionNameKey) {
|
||||
if (!actionNameKey) { return []; }
|
||||
|
||||
const a = this.findAction(this.get('actionNameKey'));
|
||||
const a = this.findAction(actionNameKey);
|
||||
return a ? a.words : [];
|
||||
},
|
||||
|
||||
@ -23,6 +23,12 @@ export default Ember.Controller.extend({
|
||||
return I18n.t('admin.watched_words.action_descriptions.' + actionNameKey);
|
||||
},
|
||||
|
||||
@computed('actionNameKey', 'adminWatchedWords.model')
|
||||
wordCount(actionNameKey) {
|
||||
const a = this.findAction(actionNameKey);
|
||||
return a ? a.words.length : 0;
|
||||
},
|
||||
|
||||
actions: {
|
||||
recordAdded(arg) {
|
||||
const a = this.findAction(this.get('actionNameKey'));
|
||||
|
||||
@ -3,9 +3,13 @@ import { ajax } from 'discourse/lib/ajax';
|
||||
import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
|
||||
const THEME_FIELD_VARIABLE_TYPE_IDS = [2, 3, 4];
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
adminCustomizeThemesShow: Ember.inject.controller(),
|
||||
|
||||
uploadUrl: '/admin/themes/upload_asset',
|
||||
|
||||
onShow() {
|
||||
this.set('name', null);
|
||||
this.set('fileSelected', false);
|
||||
@ -14,9 +18,11 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
enabled: Em.computed.and('nameValid', 'fileSelected'),
|
||||
disabled: Em.computed.not('enabled'),
|
||||
|
||||
@computed('name')
|
||||
nameValid(name) {
|
||||
return name && name.match(/^[a-z_][a-z0-9_-]*$/i);
|
||||
@computed('name', 'adminCustomizeThemesShow.model.theme_fields')
|
||||
nameValid(name, themeFields) {
|
||||
return name &&
|
||||
name.match(/^[a-z_][a-z0-9_-]*$/i) &&
|
||||
!themeFields.some(tf => THEME_FIELD_VARIABLE_TYPE_IDS.includes(tf.type_id) && name === tf.name);
|
||||
},
|
||||
|
||||
@observes('name')
|
||||
@ -48,7 +54,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
|
||||
options.data.append('file', file);
|
||||
|
||||
ajax('/admin/themes/upload_asset', options).then(result => {
|
||||
ajax(this.get('uploadUrl'), options).then(result => {
|
||||
const upload = {
|
||||
upload_id: result.upload_id,
|
||||
name: this.get('name'),
|
||||
|
||||
@ -9,6 +9,8 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
selection: 'local',
|
||||
adminCustomizeThemes: Ember.inject.controller(),
|
||||
loading: false,
|
||||
keyGenUrl: '/admin/themes/generate_key_pair',
|
||||
importUrl: '/admin/themes/import',
|
||||
|
||||
checkPrivate: Ember.computed.match('uploadUrl', /^git/),
|
||||
|
||||
@ -17,7 +19,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
const checked = this.get('privateChecked');
|
||||
if (checked && !this._keyLoading) {
|
||||
this._keyLoading = true;
|
||||
ajax('/admin/themes/generate_key_pair', {method: 'POST'})
|
||||
ajax(this.get('keyGenUrl'), {method: 'POST'})
|
||||
.then(pair => {
|
||||
this.set('privateKey', pair.private_key);
|
||||
this.set('publicKey', pair.public_key);
|
||||
@ -52,7 +54,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
}
|
||||
|
||||
this.set('loading', true);
|
||||
ajax('/admin/themes/import', options).then(result=>{
|
||||
ajax(this.get('importUrl'), options).then(result=>{
|
||||
const theme = this.store.createRecord('theme',result.theme);
|
||||
this.get('adminCustomizeThemes').send('addTheme', theme);
|
||||
this.send('closeModal');
|
||||
|
||||
@ -2,6 +2,7 @@ import { ajax } from 'discourse/lib/ajax';
|
||||
import round from "discourse/lib/round";
|
||||
import { fmt } from 'discourse/lib/computed';
|
||||
import { fillMissingDates } from 'discourse/lib/utilities';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
const Report = Discourse.Model.extend({
|
||||
reportUrl: fmt("type", "/admin/reports/%@"),
|
||||
@ -42,7 +43,8 @@ const Report = Discourse.Model.extend({
|
||||
lastSevenDaysCount: function() { return this.valueFor(1, 7); }.property("data"),
|
||||
lastThirtyDaysCount: function() { return this.valueFor(1, 30); }.property("data"),
|
||||
|
||||
yesterdayTrend: function() {
|
||||
@computed('data')
|
||||
yesterdayTrend() {
|
||||
const yesterdayVal = this.valueAt(1);
|
||||
const twoDaysAgoVal = this.valueAt(2);
|
||||
if (yesterdayVal > twoDaysAgoVal) {
|
||||
@ -52,9 +54,10 @@ const Report = Discourse.Model.extend({
|
||||
} else {
|
||||
return "no-change";
|
||||
}
|
||||
}.property("data"),
|
||||
},
|
||||
|
||||
sevenDayTrend: function() {
|
||||
@computed('data')
|
||||
sevenDayTrend() {
|
||||
const currentPeriod = this.valueFor(1, 7);
|
||||
const prevPeriod = this.valueFor(8, 14);
|
||||
if (currentPeriod > prevPeriod) {
|
||||
@ -64,36 +67,39 @@ const Report = Discourse.Model.extend({
|
||||
} else {
|
||||
return "no-change";
|
||||
}
|
||||
}.property("data"),
|
||||
},
|
||||
|
||||
thirtyDayTrend: function() {
|
||||
if (this.get("prev30Days")) {
|
||||
@computed('prev30Days', 'data')
|
||||
thirtyDayTrend(prev30Days) {
|
||||
if (prev30Days) {
|
||||
const currentPeriod = this.valueFor(1, 30);
|
||||
if (currentPeriod > this.get("prev30Days")) {
|
||||
return "trending-up";
|
||||
} else if (currentPeriod < this.get("prev30Days")) {
|
||||
} else if (currentPeriod < prev30Days) {
|
||||
return "trending-down";
|
||||
}
|
||||
}
|
||||
return "no-change";
|
||||
}.property("data", "prev30Days"),
|
||||
},
|
||||
|
||||
icon: function() {
|
||||
switch (this.get("type")) {
|
||||
@computed('type')
|
||||
icon(type) {
|
||||
switch (type) {
|
||||
case "flags": return "flag";
|
||||
case "likes": return "heart";
|
||||
case "bookmarks": return "bookmark";
|
||||
default: return null;
|
||||
}
|
||||
}.property("type"),
|
||||
},
|
||||
|
||||
method: function() {
|
||||
if (this.get("type") === "time_to_first_response") {
|
||||
@computed('type')
|
||||
method(type) {
|
||||
if (type === "time_to_first_response") {
|
||||
return "average";
|
||||
} else {
|
||||
return "sum";
|
||||
}
|
||||
}.property("type"),
|
||||
},
|
||||
|
||||
percentChangeString(val1, val2) {
|
||||
const val = ((val1 - val2) / val2) * 100;
|
||||
@ -114,21 +120,31 @@ const Report = Discourse.Model.extend({
|
||||
return title;
|
||||
},
|
||||
|
||||
yesterdayCountTitle: function() {
|
||||
@computed('data')
|
||||
yesterdayCountTitle() {
|
||||
return this.changeTitle(this.valueAt(1), this.valueAt(2), "two days ago");
|
||||
}.property("data"),
|
||||
},
|
||||
|
||||
sevenDayCountTitle: function() {
|
||||
@computed('data')
|
||||
sevenDayCountTitle() {
|
||||
return this.changeTitle(this.valueFor(1, 7), this.valueFor(8, 14), "two weeks ago");
|
||||
}.property("data"),
|
||||
},
|
||||
|
||||
thirtyDayCountTitle: function() {
|
||||
return this.changeTitle(this.valueFor(1, 30), this.get("prev30Days"), "in the previous 30 day period");
|
||||
}.property("data"),
|
||||
@computed('prev30Days', 'data')
|
||||
thirtyDayCountTitle(prev30Days) {
|
||||
return this.changeTitle(this.valueFor(1, 30), prev30Days, "in the previous 30 day period");
|
||||
},
|
||||
|
||||
dataReversed: function() {
|
||||
return this.get("data").toArray().reverse();
|
||||
}.property("data")
|
||||
@computed('data')
|
||||
sortedData(data) {
|
||||
return this.get('xAxisIsDate') ? data.toArray().reverse() : data.toArray();
|
||||
},
|
||||
|
||||
@computed('data')
|
||||
xAxisIsDate() {
|
||||
if (!this.data[0]) return false;
|
||||
return this.data && this.data[0].x.match(/\d{4}-\d{1,2}-\d{1,2}/);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@ -145,13 +161,21 @@ Report.reopenClass({
|
||||
}).then(json => {
|
||||
// Add zero values for missing dates
|
||||
if (json.report.data.length > 0) {
|
||||
const startDateFormatted = moment(json.report.start_date).format('YYYY-MM-DD');
|
||||
const endDateFormatted = moment(json.report.end_date).format('YYYY-MM-DD');
|
||||
const startDateFormatted = moment(json.report.start_date).utc().format('YYYY-MM-DD');
|
||||
const endDateFormatted = moment(json.report.end_date).utc().format('YYYY-MM-DD');
|
||||
json.report.data = fillMissingDates(json.report.data, startDateFormatted, endDateFormatted);
|
||||
}
|
||||
|
||||
const model = Report.create({ type: type });
|
||||
model.setProperties(json.report);
|
||||
|
||||
if (json.report.related_report) {
|
||||
// TODO: fillMissingDates if xaxis is date
|
||||
const related = Report.create({ type: json.report.related_report.type });
|
||||
related.setProperties(json.report.related_report);
|
||||
model.set('relatedReport', related);
|
||||
}
|
||||
|
||||
return model;
|
||||
});
|
||||
}
|
||||
|
||||
@ -2,10 +2,11 @@ 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];
|
||||
|
||||
const Theme = RestModel.extend({
|
||||
|
||||
FIELDS_IDS: [0, 1],
|
||||
|
||||
@computed('theme_fields')
|
||||
themeFields(fields) {
|
||||
|
||||
@ -16,7 +17,7 @@ const Theme = RestModel.extend({
|
||||
|
||||
let hash = {};
|
||||
fields.forEach(field => {
|
||||
if (!field.type_id || FIELDS_IDS.includes(field.type_id)) {
|
||||
if (!field.type_id || this.get('FIELDS_IDS').includes(field.type_id)) {
|
||||
hash[this.getKey(field)] = field;
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,24 +0,0 @@
|
||||
import Group from 'discourse/models/group';
|
||||
|
||||
export default Discourse.Route.extend({
|
||||
|
||||
model(params) {
|
||||
if (params.name === 'new') {
|
||||
return Group.create({ automatic: false, visibility_level: 0 });
|
||||
}
|
||||
|
||||
const group = this.modelFor('adminGroupsType').findBy('name', params.name);
|
||||
|
||||
if (!group) { return this.transitionTo('adminGroups.index'); }
|
||||
|
||||
return group;
|
||||
},
|
||||
|
||||
setupController(controller, model) {
|
||||
controller.set("model", model);
|
||||
controller.set("model.usernames", null);
|
||||
controller.set("savingStatus", "");
|
||||
model.findMembers();
|
||||
}
|
||||
|
||||
});
|
||||
@ -1,13 +0,0 @@
|
||||
import Group from 'discourse/models/group';
|
||||
|
||||
export default Ember.Route.extend({
|
||||
model() {
|
||||
return Group.findAll().then(groups => {
|
||||
return groups.filter(g => !g.get('automatic'));
|
||||
});
|
||||
},
|
||||
|
||||
setupController(controller, groups) {
|
||||
controller.setProperties({ groups, groupId: null, users: null });
|
||||
}
|
||||
});
|
||||
@ -1,5 +0,0 @@
|
||||
export default Discourse.Route.extend({
|
||||
redirect: function() {
|
||||
this.transitionTo("adminGroupsType", "custom");
|
||||
}
|
||||
});
|
||||
@ -1,15 +0,0 @@
|
||||
import Group from 'discourse/models/group';
|
||||
|
||||
export default Discourse.Route.extend({
|
||||
model(params) {
|
||||
this.set("type", params.type);
|
||||
return Group.findAll().then(function(groups) {
|
||||
return groups.filterBy("type", params.type);
|
||||
});
|
||||
},
|
||||
|
||||
setupController(controller, model){
|
||||
controller.set("type", this.get("type"));
|
||||
controller.set("model", model);
|
||||
}
|
||||
});
|
||||
@ -12,8 +12,8 @@ export default Discourse.Route.extend({
|
||||
model: model,
|
||||
categoryId: (model.get('category_id') || 'all'),
|
||||
groupId: model.get('group_id'),
|
||||
startDate: moment(model.get('start_date')).format('YYYY-MM-DD'),
|
||||
endDate: moment(model.get('end_date')).format('YYYY-MM-DD')
|
||||
startDate: moment(model.get('start_date')).utc().format('YYYY-MM-DD'),
|
||||
endDate: moment(model.get('end_date')).utc().format('YYYY-MM-DD')
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -76,14 +76,6 @@ export default function() {
|
||||
});
|
||||
});
|
||||
|
||||
this.route('adminGroups', { path: '/groups', resetNamespace: true }, function() {
|
||||
this.route('bulk');
|
||||
this.route('bulkComplete', { path: 'bulk-complete' });
|
||||
this.route('adminGroupsType', { path: '/:type', resetNamespace: true }, function() {
|
||||
this.route('adminGroup', { path: '/:name', resetNamespace: true });
|
||||
});
|
||||
});
|
||||
|
||||
this.route('adminUsers', { path: '/users', resetNamespace: true }, function() {
|
||||
this.route('adminUser', { path: '/:user_id/:username', resetNamespace: true }, function() {
|
||||
this.route('badges');
|
||||
|
||||
@ -12,7 +12,6 @@
|
||||
{{nav-item route='adminBadges' label='admin.badges.title'}}
|
||||
{{/if}}
|
||||
{{#if currentUser.admin}}
|
||||
{{nav-item route='adminGroups' label='admin.groups.title'}}
|
||||
{{nav-item route='adminEmail' label='admin.email.title'}}
|
||||
{{/if}}
|
||||
{{nav-item route='adminFlags' label='admin.flags.title'}}
|
||||
|
||||
@ -0,0 +1,17 @@
|
||||
{{#if model.sortedData}}
|
||||
<table class="table report {{model.type}}">
|
||||
<tr>
|
||||
<th>{{model.xaxis}}</th>
|
||||
<th>{{model.yaxis}}</th>
|
||||
</tr>
|
||||
|
||||
{{#each model.sortedData as |row|}}
|
||||
<tr>
|
||||
<td class="x-value">{{row.x}}</td>
|
||||
<td>
|
||||
{{row.y}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</table>
|
||||
{{/if}}
|
||||
@ -3,3 +3,6 @@
|
||||
{{/if}}
|
||||
{{topic-status topic=flaggedPost.topic}}
|
||||
<a href='{{unbound flaggedPost.url}}'>{{{unbound flaggedPost.topic.fancyTitle}}}</a>
|
||||
{{#if flaggedPost.reply_count}}
|
||||
<span class="flagged-post-reply-count">{{i18n 'admin.flags.replies' count=flaggedPost.reply_count}}</span>
|
||||
{{/if}}
|
||||
|
||||
@ -1,173 +0,0 @@
|
||||
<form class="form-horizontal">
|
||||
|
||||
<div>
|
||||
{{#if model.automatic}}
|
||||
<h3>{{model.name}}</h3>
|
||||
{{else}}
|
||||
<label for="name">{{i18n 'groups.name'}}</label>
|
||||
{{text-field name="name" value=model.name placeholderKey="groups.name_placeholder"}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#unless model.automatic}}
|
||||
<div>
|
||||
<label for='full_name'>{{i18n 'groups.edit.full_name'}}</label>
|
||||
{{input type='text' name='full_name' value=model.full_name class='group-edit-full-name'}}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="bio">{{i18n 'groups.bio'}}</label>
|
||||
{{d-editor value=model.bio_raw}}
|
||||
</div>
|
||||
|
||||
{{#if model.hasOwners}}
|
||||
<div>
|
||||
<label for='owner-list'>{{i18n 'admin.groups.group_owners'}}</label>
|
||||
<div class="ac-wrap clearfix" id='owner-list'>
|
||||
{{#each model.owners as |member|}}
|
||||
{{group-member member=member removeAction="removeOwner"}}
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div>
|
||||
<label for="owner-selector">{{i18n 'admin.groups.add_owners'}}</label>
|
||||
|
||||
{{user-selector usernames=model.ownerUsernames
|
||||
placeholderKey="groups.selector_placeholder"
|
||||
id="owner-selector"}}
|
||||
|
||||
{{#if model.id}}
|
||||
{{d-button
|
||||
action="addOwners"
|
||||
class="add"
|
||||
icon="plus"
|
||||
label="admin.groups.add"}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/unless}}
|
||||
|
||||
<div>
|
||||
{{group-members-input model=model addButton=model.id}}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="visiblity">{{i18n 'groups.visibility_levels.title'}}</label>
|
||||
{{combo-box name="alias"
|
||||
valueAttribute="value"
|
||||
value=model.visibility_level
|
||||
content=visibilityLevelOptions
|
||||
castInteger=true}}
|
||||
</div>
|
||||
|
||||
{{#unless model.automatic}}
|
||||
<div>
|
||||
<label>
|
||||
{{input type="checkbox"
|
||||
checked=model.public_admission
|
||||
disabled=disablePublicSetting}}
|
||||
|
||||
{{i18n 'groups.public_admission'}}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
{{input type='checkbox'
|
||||
checked=model.public_exit}}
|
||||
|
||||
{{i18n 'groups.public_exit'}}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
{{input type="checkbox"
|
||||
checked=model.allow_membership_requests
|
||||
disabled=disableMembershipRequestSetting}}
|
||||
|
||||
{{i18n 'groups.allow_membership_requests'}}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{{#if model.allow_membership_requests}}
|
||||
<div>
|
||||
<label for="membership-request-template">
|
||||
{{i18n 'groups.membership_request_template'}}
|
||||
</label>
|
||||
|
||||
{{expanding-text-area name="membership-request-template"
|
||||
value=model.membership_request_template}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div>
|
||||
<label>
|
||||
{{input type="checkbox" checked=model.primary_group}}
|
||||
{{i18n 'admin.groups.primary_group'}}
|
||||
</label>
|
||||
</div>
|
||||
{{/unless}}
|
||||
|
||||
<div>
|
||||
<label for="alias">{{i18n 'groups.alias_levels.mentionable'}}</label>
|
||||
{{combo-box name="alias" valueAttribute="value" value=model.mentionable_level content=aliasLevelOptions}}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="alias">{{i18n 'groups.alias_levels.messageable'}}</label>
|
||||
{{combo-box name="alias" valueAttribute="value" value=model.messageable_level content=aliasLevelOptions}}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>{{i18n 'groups.notification_level'}}</label>
|
||||
{{notifications-button i18nPrefix='groups.notifications' value=model.default_notification_level}}
|
||||
<div class='clearfix'></div>
|
||||
</div>
|
||||
|
||||
{{#unless model.automatic}}
|
||||
<div>
|
||||
<label for="automatic_membership">{{i18n 'admin.groups.automatic_membership_email_domains'}}</label>
|
||||
{{list-setting name="automatic_membership" settingValue=model.emailDomains}}
|
||||
<label>
|
||||
{{input type="checkbox" checked=model.automatic_membership_retroactive}}
|
||||
{{i18n 'admin.groups.automatic_membership_retroactive'}}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="title">
|
||||
{{i18n 'admin.groups.default_title'}}
|
||||
</label>
|
||||
{{input value=model.title}}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="grant_trust_level">{{i18n 'groups.trust_levels.title'}}</label>
|
||||
{{combo-box name="grant_trust_level" valueAttribute="value" value=model.grant_trust_level content=trustLevelOptions}}
|
||||
</div>
|
||||
|
||||
{{#if siteSettings.email_in}}
|
||||
<label for="incoming_email">{{i18n 'admin.groups.incoming_email'}}</label>
|
||||
{{text-field name="incoming_email" value=model.incoming_email placeholderKey="admin.groups.incoming_email_placeholder"}}
|
||||
{{plugin-outlet name="group-email-in" args=(hash model=model)}}
|
||||
{{/if}}
|
||||
{{/unless}}
|
||||
|
||||
{{#unless model.automatic}}
|
||||
{{group-flair-inputs model=model}}
|
||||
{{/unless}}
|
||||
|
||||
{{plugin-outlet name="group-edit" args=(hash group=model)}}
|
||||
|
||||
<div class='buttons'>
|
||||
<button {{action "save"}} disabled={{disableSave}} class='btn btn-primary'>{{i18n 'admin.customize.save'}}</button>
|
||||
{{#unless model.automatic}}
|
||||
<button {{action "destroy"}} class='btn btn-danger'>{{d-icon "trash-o"}}{{i18n 'admin.customize.delete'}}</button>
|
||||
{{/unless}}
|
||||
|
||||
<span class="saving {{unless savingStatus 'hidden'}}">{{savingStatus}}</span>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
@ -1,11 +0,0 @@
|
||||
{{#if bulkAddResponse}}
|
||||
<p>{{{bulkAddResponse.message}}}</p>
|
||||
{{#if bulkAddResponse.users_not_added}}
|
||||
<p>{{i18n "admin.groups.bulk_complete_users_not_added"}}</p>
|
||||
{{#each bulkAddResponse.users_not_added as |user|}}
|
||||
{{user}}<br/>
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<p>{{i18n "admin.groups.bulk_complete"}}</p>
|
||||
{{/if}}
|
||||
@ -1,19 +0,0 @@
|
||||
<div class='groups-bulk'>
|
||||
<p>{{i18n "admin.groups.bulk_paste"}}</p>
|
||||
|
||||
<div class='control'>
|
||||
{{textarea value=users class="paste-users"}}
|
||||
</div>
|
||||
|
||||
<div class='control'>
|
||||
{{combo-box filterable=true content=groups value=groupId none="admin.groups.bulk_select"}}
|
||||
</div>
|
||||
|
||||
<div class='control'>
|
||||
{{d-button disabled=buttonDisabled
|
||||
class="btn-primary"
|
||||
action="addToGroup"
|
||||
icon="plus"
|
||||
label="admin.groups.bulk"}}
|
||||
</div>
|
||||
</div>
|
||||
@ -1,9 +0,0 @@
|
||||
<div class="groups-type-index">
|
||||
<p>{{i18n messageKey}}</p>
|
||||
|
||||
<div>
|
||||
{{#link-to 'adminGroup' 'new' class="btn"}}
|
||||
{{d-icon "plus"}} {{i18n 'admin.groups.new'}}
|
||||
{{/link-to}}
|
||||
</div>
|
||||
</div>
|
||||
@ -1,31 +0,0 @@
|
||||
<div class='row groups'>
|
||||
{{#if sortedGroups}}
|
||||
<div class='content-list'>
|
||||
<h3>{{i18n 'admin.groups.edit'}}</h3>
|
||||
<ul>
|
||||
{{#each sortedGroups as |group|}}
|
||||
<li>
|
||||
{{#link-to "adminGroup" group.type group.name}}{{group.name}}
|
||||
{{#if group.userCountDisplay}}
|
||||
<span class="count">{{number group.userCountDisplay}}</span>
|
||||
{{/if}}
|
||||
{{/link-to}}
|
||||
</li>
|
||||
{{/each}}
|
||||
</ul>
|
||||
<div class='controls'>
|
||||
{{#if isAuto}}
|
||||
{{d-button action="refreshAutoGroups" icon="refresh" label="admin.groups.refresh" disabled=refreshingAutoGroups}}
|
||||
{{else}}
|
||||
{{#link-to 'adminGroup' 'new' class="btn"}}
|
||||
{{d-icon "plus"}} {{i18n 'admin.groups.new'}}
|
||||
{{/link-to}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="content-body">
|
||||
{{outlet}}
|
||||
</div>
|
||||
</div>
|
||||
@ -1,9 +0,0 @@
|
||||
{{#admin-nav}}
|
||||
{{nav-item route='adminGroupsType' routeParam='custom' label='admin.groups.custom'}}
|
||||
{{nav-item route='adminGroupsType' routeParam='automatic' label='admin.groups.automatic'}}
|
||||
{{nav-item route='adminGroups.bulk' label='admin.groups.bulk'}}
|
||||
{{/admin-nav}}
|
||||
|
||||
<div class="admin-container">
|
||||
{{outlet}}
|
||||
</div>
|
||||
@ -14,6 +14,7 @@
|
||||
<table class="admin-plugins">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{{i18n "admin.plugins.name"}}</th>
|
||||
<th>{{i18n "admin.plugins.version"}}</th>
|
||||
<th>{{i18n "admin.plugins.enabled"}}</th>
|
||||
@ -23,6 +24,14 @@
|
||||
<tbody>
|
||||
{{#each model as |plugin|}}
|
||||
<tr>
|
||||
<td>
|
||||
{{#if plugin.is_official}}
|
||||
{{d-icon "check-circle"
|
||||
title="admin.plugins.official"
|
||||
class="admin-plugins-official-badge"}}
|
||||
{{/if}}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{{#if plugin.url}}
|
||||
<a href={{plugin.url}} target="_blank">{{plugin.name}}</a>
|
||||
@ -58,4 +67,3 @@
|
||||
{{/if}}
|
||||
|
||||
<p><a href="https://meta.discourse.org/t/install-a-plugin/19157">{{i18n "admin.plugins.howto"}}</a></p>
|
||||
|
||||
|
||||
@ -31,20 +31,10 @@
|
||||
{{#if viewingGraph}}
|
||||
{{admin-graph model=model}}
|
||||
{{else}}
|
||||
<table class='table report'>
|
||||
<tr>
|
||||
<th>{{model.xaxis}}</th>
|
||||
<th>{{model.yaxis}}</th>
|
||||
</tr>
|
||||
{{admin-table-report model=model}}
|
||||
{{/if}}
|
||||
|
||||
{{#each model.dataReversed as |row|}}
|
||||
<tr>
|
||||
<td>{{row.x}}</td>
|
||||
<td>
|
||||
{{row.y}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</table>
|
||||
{{#if model.relatedReport}}
|
||||
{{admin-table-report model=model.relatedReport}}
|
||||
{{/if}}
|
||||
{{/conditional-loading-spinner}}
|
||||
|
||||
@ -56,6 +56,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{plugin-outlet name="admin-user-below-names" args=(hash user=model) tagName='' connectorTagName=''}}
|
||||
|
||||
{{#if canCheckEmails}}
|
||||
<div class='display-row email'>
|
||||
<div class='field'>{{i18n 'user.email.title'}}</div>
|
||||
@ -199,8 +201,8 @@
|
||||
<div class='value'>
|
||||
{{#if model.approved}}
|
||||
{{i18n 'admin.user.approved_by'}}
|
||||
{{#link-to 'adminUser' approvedBy}}{{avatar model.approvedBy imageSize="small"}}{{/link-to}}
|
||||
{{#link-to 'adminUser' approvedBy}}{{model.approvedBy.username}}{{/link-to}}
|
||||
{{#link-to 'adminUser' model.approvedBy}}{{avatar model.approvedBy imageSize="small"}}{{/link-to}}
|
||||
{{#link-to 'adminUser' model.approvedBy}}{{model.approvedBy.username}}{{/link-to}}
|
||||
{{else}}
|
||||
{{i18n 'no_value'}}
|
||||
{{/if}}
|
||||
@ -351,8 +353,8 @@
|
||||
<div class='display-row highlight-danger suspension-info'>
|
||||
<div class='field'>{{i18n 'admin.user.suspended_by'}}</div>
|
||||
<div class='value'>
|
||||
{{#link-to 'adminUser' suspendedBy}}{{avatar model.suspendedBy imageSize="tiny"}}{{/link-to}}
|
||||
{{#link-to 'adminUser' suspendedBy}}{{model.suspendedBy.username}}{{/link-to}}
|
||||
{{#link-to 'adminUser' model.suspendedBy}}{{avatar model.suspendedBy imageSize="tiny"}}{{/link-to}}
|
||||
{{#link-to 'adminUser' model.suspendedBy}}{{model.suspendedBy.username}}{{/link-to}}
|
||||
</div>
|
||||
<div class='controls'>
|
||||
<b>{{i18n 'admin.user.suspend_reason'}}</b>:
|
||||
@ -396,8 +398,8 @@
|
||||
<div class='display-row highlight-danger silence-info'>
|
||||
<div class='field'>{{i18n 'admin.user.silenced_by'}}</div>
|
||||
<div class='value'>
|
||||
{{#link-to 'adminUser' silencedBy}}{{avatar model.silencedBy imageSize="tiny"}}{{/link-to}}
|
||||
{{#link-to 'adminUser' silencedBy}}{{model.silencedBy.username}}{{/link-to}}
|
||||
{{#link-to 'adminUser' model.silencedBy}}{{avatar model.silencedBy imageSize="tiny"}}{{/link-to}}
|
||||
{{#link-to 'adminUser' model.silencedBy}}{{model.silencedBy.username}}{{/link-to}}
|
||||
</div>
|
||||
<div class='controls'>
|
||||
<b>{{i18n 'admin.user.silence_reason'}}</b>:
|
||||
@ -413,7 +415,7 @@
|
||||
<h1>{{i18n 'admin.groups.title'}}</h1>
|
||||
<div class='display-row'>
|
||||
<div class='field'>{{i18n 'admin.groups.automatic'}}</div>
|
||||
<div class='value'>{{automaticGroups}}</div>
|
||||
<div class='value'>{{{automaticGroups}}}</div>
|
||||
</div>
|
||||
<div class='display-row'>
|
||||
<div class='field'>{{i18n 'admin.groups.custom'}}</div>
|
||||
|
||||
@ -22,6 +22,6 @@
|
||||
<div class="watched-word-box">{{admin-watched-word word=word action="recordRemoved"}}</div>
|
||||
{{/each}}
|
||||
{{else}}
|
||||
{{i18n 'admin.watched_words.word_count' count=model.words.length}}
|
||||
{{i18n 'admin.watched_words.word_count' count=wordCount}}
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
@ -18,13 +18,15 @@ RawHandlebars.helpers['get'] = function(context, options) {
|
||||
var firstContext = options.contexts[0];
|
||||
var val = firstContext[context];
|
||||
|
||||
if (context.indexOf('controller') === 0) {
|
||||
context = context.replace(/^controller\./, '');
|
||||
if (context.indexOf('controller.') === 0) {
|
||||
context = context.slice(context.indexOf('.') + 1);
|
||||
}
|
||||
|
||||
if (val && val.isDescriptor) { return Em.get(firstContext, context); }
|
||||
val = val === undefined ? Em.get(firstContext, context): val;
|
||||
return val;
|
||||
if (val && val.isDescriptor) {
|
||||
return Em.get(firstContext, context);
|
||||
}
|
||||
|
||||
return val === undefined ? Em.get(firstContext, context) : val;
|
||||
};
|
||||
|
||||
// adds compatability so this works with stringParams
|
||||
|
||||
7
app/assets/javascripts/discourse/adapters/group.js.es6
Normal file
7
app/assets/javascripts/discourse/adapters/group.js.es6
Normal file
@ -0,0 +1,7 @@
|
||||
import RestAdapter from 'discourse/adapters/rest';
|
||||
|
||||
export default RestAdapter.extend({
|
||||
appendQueryParams(path, findArgs) {
|
||||
return this._super(path, findArgs, '.json');
|
||||
},
|
||||
});
|
||||
@ -40,7 +40,7 @@ export default Ember.Object.extend({
|
||||
return "/";
|
||||
},
|
||||
|
||||
appendQueryParams(path, findArgs) {
|
||||
appendQueryParams(path, findArgs, extension) {
|
||||
if (findArgs) {
|
||||
if (typeof findArgs === "object") {
|
||||
const queryString = Object.keys(findArgs)
|
||||
@ -48,11 +48,11 @@ export default Ember.Object.extend({
|
||||
.map(k => k + "=" + encodeURIComponent(findArgs[k]));
|
||||
|
||||
if (queryString.length) {
|
||||
return path + "?" + queryString.join('&');
|
||||
return `${path}${extension ? extension : ''}?${queryString.join('&')}`;
|
||||
}
|
||||
} else {
|
||||
// It's serializable as a string if not an object
|
||||
return path + "/" + findArgs;
|
||||
return `${path}/${findArgs}${extension ? extension : ''}`;
|
||||
}
|
||||
}
|
||||
return path;
|
||||
|
||||
@ -1,11 +1,19 @@
|
||||
import { default as computed } from 'ember-addons/ember-computed-decorators';
|
||||
import { PRIVATE_MESSAGE, CREATE_TOPIC, CREATE_SHARED_DRAFT, REPLY, EDIT } from "discourse/models/composer";
|
||||
import {
|
||||
PRIVATE_MESSAGE,
|
||||
CREATE_TOPIC,
|
||||
CREATE_SHARED_DRAFT,
|
||||
REPLY,
|
||||
EDIT,
|
||||
EDIT_SHARED_DRAFT
|
||||
} from "discourse/models/composer";
|
||||
import { iconHTML } from 'discourse-common/lib/icon-library';
|
||||
|
||||
const TITLES = {
|
||||
[PRIVATE_MESSAGE]: 'topic.private_message',
|
||||
[CREATE_TOPIC]: 'topic.create_long',
|
||||
[CREATE_SHARED_DRAFT]: 'composer.create_shared_draft'
|
||||
[CREATE_SHARED_DRAFT]: 'composer.create_shared_draft',
|
||||
[EDIT_SHARED_DRAFT]: 'composer.edit_shared_draft'
|
||||
};
|
||||
|
||||
export default Ember.Component.extend({
|
||||
|
||||
@ -37,10 +37,19 @@ export default Ember.Component.extend({
|
||||
return `[${I18n.t('uploading')}]() `;
|
||||
},
|
||||
|
||||
@computed()
|
||||
replyPlaceholder() {
|
||||
const key = authorizesOneOrMoreImageExtensions() ? "reply_placeholder" : "reply_placeholder_no_images";
|
||||
return `composer.${key}`;
|
||||
@computed('composer.requiredCategoryMissing')
|
||||
replyPlaceholder(requiredCategoryMissing) {
|
||||
if (requiredCategoryMissing) {
|
||||
return 'composer.reply_placeholder_choose_category';
|
||||
} else {
|
||||
const key = authorizesOneOrMoreImageExtensions() ? "reply_placeholder" : "reply_placeholder_no_images";
|
||||
return `composer.${key}`;
|
||||
}
|
||||
},
|
||||
|
||||
@computed('composer.requiredCategoryMissing', 'composer.replyLength')
|
||||
disableTextarea(requiredCategoryMissing, replyLength) {
|
||||
return requiredCategoryMissing && replyLength === 0;
|
||||
},
|
||||
|
||||
@observes('composer.uploadCancelled')
|
||||
|
||||
@ -53,7 +53,11 @@ export default Ember.Component.extend({
|
||||
if (this.get('autoPosted') || !this.get('watchForLink')) { return; }
|
||||
|
||||
if (Ember.testing) {
|
||||
this._checkForUrl();
|
||||
Em.run.next(() =>
|
||||
// not ideal but we don't want to run this in current
|
||||
// runloop to avoid an error in console
|
||||
this._checkForUrl()
|
||||
);
|
||||
} else {
|
||||
Ember.run.debounce(this, this._checkForUrl, 500);
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ export default Ember.Component.extend({
|
||||
|
||||
tagName: 'button',
|
||||
classNameBindings: [':btn', 'noText', 'btnType'],
|
||||
attributeBindings: ['disabled', 'translatedTitle:title', 'tabindex'],
|
||||
attributeBindings: ['disabled', 'translatedTitle:title', 'translatedTitle:aria-label', 'tabindex'],
|
||||
|
||||
btnIcon: Ember.computed.notEmpty('icon'),
|
||||
|
||||
@ -23,7 +23,7 @@ export default Ember.Component.extend({
|
||||
|
||||
@computed("title")
|
||||
translatedTitle(title) {
|
||||
return title ? I18n.t(title) : this.get('translatedLabel');
|
||||
if (title) return I18n.t(title);
|
||||
},
|
||||
|
||||
@computed("label")
|
||||
|
||||
@ -750,6 +750,8 @@ export default Ember.Component.extend({
|
||||
},
|
||||
|
||||
toolbarButton(button) {
|
||||
if (this.get('disabled')) { return; }
|
||||
|
||||
const selected = this._getSelected(button.trimLeading);
|
||||
const toolbarEvent = {
|
||||
selected,
|
||||
@ -770,6 +772,8 @@ export default Ember.Component.extend({
|
||||
},
|
||||
|
||||
showLinkModal() {
|
||||
if (this.get('disabled')) { return; }
|
||||
|
||||
this._lastSel = this._getSelected();
|
||||
|
||||
if (this._lastSel) {
|
||||
@ -780,6 +784,8 @@ export default Ember.Component.extend({
|
||||
},
|
||||
|
||||
formatCode() {
|
||||
if (this.get('disabled')) { return; }
|
||||
|
||||
const sel = this._getSelected('', { lineVal: true });
|
||||
const selValue = sel.value;
|
||||
const hasNewLine = selValue.indexOf("\n") !== -1;
|
||||
@ -833,6 +839,7 @@ export default Ember.Component.extend({
|
||||
},
|
||||
|
||||
emoji() {
|
||||
if (this.get('disabled')) { return; }
|
||||
this.set('emojiPickerIsActive', !this.get('emojiPickerIsActive'));
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,7 +34,7 @@ export default Ember.Component.extend({
|
||||
|
||||
if (flairHexColor) style += `color: #${flairHexColor};`;
|
||||
|
||||
return style;
|
||||
return Ember.String.htmlSafe(style);
|
||||
},
|
||||
|
||||
@computed('model.flairBackgroundHexColor')
|
||||
|
||||
@ -5,7 +5,7 @@ export default Ember.Component.extend({
|
||||
|
||||
@computed('type')
|
||||
label(type) {
|
||||
return I18n.t(`groups.logs.${type}`);
|
||||
return I18n.t(`groups.manage.logs.${type}`);
|
||||
},
|
||||
|
||||
@computed('value', 'type')
|
||||
@ -0,0 +1,25 @@
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
saving: null,
|
||||
|
||||
@computed('saving')
|
||||
savingText(saving) {
|
||||
if (saving) return I18n.t("saving");
|
||||
return saving ? I18n.t("saving") : I18n.t("save");
|
||||
},
|
||||
|
||||
actions: {
|
||||
save() {
|
||||
this.set('saving', true);
|
||||
|
||||
return this.get('model').save()
|
||||
.then(() => {
|
||||
this.set("saved", true);
|
||||
})
|
||||
.catch(popupAjaxError)
|
||||
.finally(() => this.set('saving', false));
|
||||
}
|
||||
},
|
||||
});
|
||||
@ -5,7 +5,7 @@ import computed from "ember-addons/ember-computed-decorators";
|
||||
export default DropdownButton.extend({
|
||||
buttonExtraClasses: 'no-text',
|
||||
title: '',
|
||||
text: iconHTML('ellipsis-h'),
|
||||
text: iconHTML('wrench'),
|
||||
classNames: ['group-member-dropdown'],
|
||||
|
||||
@computed("member.owner")
|
||||
|
||||
@ -55,7 +55,7 @@ export default Ember.Component.extend({
|
||||
},
|
||||
|
||||
removeMember(member) {
|
||||
const message = I18n.t("groups.edit.delete_member_confirm",{
|
||||
const message = I18n.t("groups.manage.delete_member_confirm",{
|
||||
username: member.get("username"),
|
||||
group: this.get("model.name")
|
||||
});
|
||||
|
||||
@ -3,6 +3,8 @@ import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
import showModal from 'discourse/lib/show-modal';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNames: ["group-membership-button"],
|
||||
|
||||
@computed("model.public_admission", "userIsGroupUser")
|
||||
canJoinGroup(publicAdmission, userIsGroupUser) {
|
||||
return publicAdmission && !userIsGroupUser;
|
||||
|
||||
@ -1,25 +0,0 @@
|
||||
import DropdownSelectBox from "select-kit/components/dropdown-select-box";
|
||||
|
||||
export default DropdownSelectBox.extend({
|
||||
classNames: ["group-navigation-dropdown", "pull-right"],
|
||||
nameProperty: "label",
|
||||
headerIcon: ["bars"],
|
||||
showFullTitle: false,
|
||||
|
||||
computeContent() {
|
||||
const content = [];
|
||||
|
||||
content.push({
|
||||
id: "manageMembership",
|
||||
icon: "user-plus",
|
||||
label: I18n.t("groups.add_members.title"),
|
||||
description: I18n.t("groups.add_members.description"),
|
||||
});
|
||||
|
||||
return content;
|
||||
},
|
||||
|
||||
mutateValue(value) {
|
||||
this.get(value)(this.get('model'));
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,23 @@
|
||||
import { default as computed } from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
visibilityLevelOptions: [
|
||||
{ name: I18n.t("admin.groups.manage.interaction.visibility_levels.public"), value: 0 },
|
||||
{ name: I18n.t("admin.groups.manage.interaction.visibility_levels.members"), value: 1 },
|
||||
{ name: I18n.t("admin.groups.manage.interaction.visibility_levels.staff"), value: 2 },
|
||||
{ name: I18n.t("admin.groups.manage.interaction.visibility_levels.owners"), value: 3 }
|
||||
],
|
||||
|
||||
aliasLevelOptions: [
|
||||
{ name: I18n.t("groups.alias_levels.nobody"), value: 0 },
|
||||
{ name: I18n.t("groups.alias_levels.only_admins"), value: 1 },
|
||||
{ name: I18n.t("groups.alias_levels.mods_and_admins"), value: 2 },
|
||||
{ name: I18n.t("groups.alias_levels.members_mods_and_admins"), value: 3 },
|
||||
{ name: I18n.t("groups.alias_levels.everyone"), value: 99 }
|
||||
],
|
||||
|
||||
@computed('siteSettings.email_in', 'model.automatic', 'currentUser.admin')
|
||||
showEmailSettings(emailIn, automatic, isAdmin) {
|
||||
return emailIn && isAdmin && !automatic;
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,20 @@
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
trustLevelOptions: [
|
||||
{ name: I18n.t("admin.groups.manage.membership.trust_levels_none"), value: 0 },
|
||||
{ name: 1, value: 1 }, { name: 2, value: 2 }, { name: 3, value: 3 }, { name: 4, value: 4 }
|
||||
],
|
||||
|
||||
@computed('model.visibility_level', 'model.public_admission')
|
||||
disableMembershipRequestSetting(visibility_level, publicAdmission) {
|
||||
visibility_level = parseInt(visibility_level);
|
||||
return (visibility_level !== 0) || publicAdmission;
|
||||
},
|
||||
|
||||
@computed('model.visibility_level', 'model.allow_membership_requests')
|
||||
disablePublicSetting(visibility_level, allowMembershipRequests) {
|
||||
visibility_level = parseInt(visibility_level);
|
||||
return (visibility_level !== 0) || allowMembershipRequests;
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,89 @@
|
||||
import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
|
||||
import User from "discourse/models/user";
|
||||
import InputValidation from 'discourse/models/input-validation';
|
||||
import debounce from 'discourse/lib/debounce';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
disableSave: null,
|
||||
nameInput: null,
|
||||
|
||||
didInsertElement() {
|
||||
this._super();
|
||||
const name = this.get('model.name');
|
||||
|
||||
if (name) {
|
||||
this.set("nameInput", name);
|
||||
} else {
|
||||
this.set('disableSave', true);
|
||||
}
|
||||
},
|
||||
|
||||
@computed('basicNameValidation', 'uniqueNameValidation')
|
||||
nameValidation(basicNameValidation, uniqueNameValidation) {
|
||||
return uniqueNameValidation ? uniqueNameValidation : basicNameValidation;
|
||||
},
|
||||
|
||||
@observes("nameInput")
|
||||
_validateName() {
|
||||
name = this.get('nameInput');
|
||||
if (name === this.get('model.name')) return;
|
||||
|
||||
if (name === undefined) {
|
||||
return this._failedInputValidation();
|
||||
};
|
||||
|
||||
if (name === "") {
|
||||
this.set('uniqueNameValidation', null);
|
||||
return this._failedInputValidation(I18n.t('admin.groups.new.name.blank'));
|
||||
}
|
||||
|
||||
if (name.length < this.siteSettings.min_username_length) {
|
||||
return this._failedInputValidation(I18n.t('admin.groups.new.name.too_short'));
|
||||
}
|
||||
|
||||
if (name.length > this.siteSettings.max_username_length) {
|
||||
return this._failedInputValidation(I18n.t('admin.groups.new.name.too_long'));
|
||||
}
|
||||
|
||||
this.checkGroupName();
|
||||
|
||||
return this._failedInputValidation(I18n.t('admin.groups.new.name.checking'));
|
||||
},
|
||||
|
||||
checkGroupName: debounce(function() {
|
||||
name = this.get('nameInput');
|
||||
if (Ember.isEmpty(name)) return;
|
||||
|
||||
User.checkUsername(name).then(response => {
|
||||
const validationName = 'uniqueNameValidation';
|
||||
|
||||
if (response.available) {
|
||||
this.set(validationName, InputValidation.create({
|
||||
ok: true,
|
||||
reason: I18n.t('admin.groups.new.name.available')
|
||||
}));
|
||||
|
||||
this.set('disableSave', false);
|
||||
this.set('model.name', this.get('nameInput'));
|
||||
} else {
|
||||
let reason;
|
||||
|
||||
if (response.errors) {
|
||||
reason = response.errors.join(' ');
|
||||
} else {
|
||||
reason = I18n.t('admin.groups.new.name.not_available');
|
||||
}
|
||||
|
||||
this.set(validationName, this._failedInputValidation(reason));
|
||||
}
|
||||
});
|
||||
}, 500),
|
||||
|
||||
_failedInputValidation(reason) {
|
||||
this.set('disableSave', true);
|
||||
|
||||
const options = { failed: true };
|
||||
if (reason) options.reason = reason;
|
||||
this.set('basicNameValidation', InputValidation.create(options));
|
||||
},
|
||||
});
|
||||
@ -3,9 +3,9 @@ import { bufferedRender } from 'discourse-common/lib/buffered-render';
|
||||
|
||||
export default Ember.Component.extend(bufferedRender({
|
||||
tagName: 'li',
|
||||
classNameBindings: ['active', 'content.hasIcon:has-icon', 'content.classNames'],
|
||||
classNameBindings: ['active', 'content.hasIcon:has-icon', 'content.classNames', 'hidden'],
|
||||
attributeBindings: ['content.title:title'],
|
||||
hidden: Em.computed.not('content.visible'),
|
||||
hidden: false,
|
||||
rerenderTriggers: ['content.count'],
|
||||
|
||||
@computed("content.filterMode", "filterMode")
|
||||
@ -27,6 +27,12 @@ export default Ember.Component.extend(bufferedRender({
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.get('active') && this.currentUser && this.currentUser.trust_level > 0 && (content.get('name') === "new" || content.get('name') === "unread") && (content.get('count') < 1)) {
|
||||
this.set('hidden', true);
|
||||
} else {
|
||||
this.set('hidden', false);
|
||||
}
|
||||
|
||||
buffer.push(`<a href='${href}'>`);
|
||||
if (content.get('hasIcon')) {
|
||||
buffer.push("<span class='" + content.get('name') + "'></span>");
|
||||
|
||||
@ -54,8 +54,7 @@ export default MountWidget.extend({
|
||||
const scrollTop = $body.scrollTop();
|
||||
|
||||
// This hack is for when swapping out many cloaked views at once
|
||||
// when using keyboard navigation. It could suddenly move the
|
||||
// scroll
|
||||
// when using keyboard navigation. It could suddenly move the scroll
|
||||
if (this.prevHeight === height && scrollTop !== this.prevScrollTop) {
|
||||
$body.scrollTop(this.prevScrollTop);
|
||||
}
|
||||
@ -80,7 +79,7 @@ export default MountWidget.extend({
|
||||
const postsWrapperTop = $('.posts-wrapper').offset().top;
|
||||
const $posts = this.$('.onscreen-post, .cloaked-post');
|
||||
const viewportTop = windowTop - slack;
|
||||
const topView = findTopView($posts, viewportTop, postsWrapperTop, 0, $posts.length-1);
|
||||
const topView = findTopView($posts, viewportTop, postsWrapperTop, 0, $posts.length - 1);
|
||||
|
||||
let windowBottom = windowTop + windowHeight;
|
||||
let viewportBottom = windowBottom + slack;
|
||||
@ -93,7 +92,7 @@ export default MountWidget.extend({
|
||||
let percent = null;
|
||||
|
||||
const offset = offsetCalculator();
|
||||
const topCheck = Math.ceil(windowTop + offset);
|
||||
const topCheck = Math.ceil(windowTop + offset + 5);
|
||||
|
||||
// uncomment to debug the eyeline
|
||||
/*
|
||||
@ -102,7 +101,7 @@ export default MountWidget.extend({
|
||||
$('body').prepend('<div class="debug-eyeline"></div>');
|
||||
$eyeline = $('.debug-eyeline');
|
||||
}
|
||||
$eyeline.css({ height: '1px', width: '100%', backgroundColor: 'blue', position: 'absolute', top: `${topCheck}px` });
|
||||
$eyeline.css({ height: '5px', width: '100%', backgroundColor: 'blue', position: 'absolute', top: `${topCheck}px`, zIndex: 999999 });
|
||||
*/
|
||||
|
||||
let allAbove = true;
|
||||
@ -115,7 +114,7 @@ export default MountWidget.extend({
|
||||
if (!$post) { break; }
|
||||
|
||||
const viewTop = $post.offset().top;
|
||||
const postHeight = $post.height();
|
||||
const postHeight = $post.outerHeight(true);
|
||||
const viewBottom = Math.ceil(viewTop + postHeight);
|
||||
|
||||
allAbove = allAbove && (viewTop < topCheck);
|
||||
@ -170,7 +169,7 @@ export default MountWidget.extend({
|
||||
this.sendAction('topVisibleChanged', { post: first, refresh: topRefresh });
|
||||
}
|
||||
|
||||
const last = posts.objectAt(onscreen[onscreen.length-1]);
|
||||
const last = posts.objectAt(onscreen[onscreen.length - 1]);
|
||||
if (this._bottomVisible !== last) {
|
||||
this._bottomVisible = last;
|
||||
this.sendAction('bottomVisibleChanged', { post: last, refresh });
|
||||
@ -184,7 +183,7 @@ export default MountWidget.extend({
|
||||
}
|
||||
|
||||
if (percent !== null) {
|
||||
if (percent > 1.0) { percent = 1.0; }
|
||||
percent = Math.max(0.0, Math.min(1.0, percent));
|
||||
|
||||
if (changedPost || (this._currentPercent !== percent)) {
|
||||
this._currentPercent = percent;
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import { ajax } from 'discourse/lib/ajax';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
tagName: '',
|
||||
@ -13,10 +12,7 @@ export default Ember.Component.extend({
|
||||
|
||||
actions: {
|
||||
updateDestinationCategory(category) {
|
||||
ajax(`/t/${this.get('topic.id')}/shared-draft`, {
|
||||
method: 'PUT',
|
||||
data: { category_id: category.get('id') }
|
||||
});
|
||||
return this.get('topic').updateDestinationCategory(category.get('id'));
|
||||
},
|
||||
|
||||
publish() {
|
||||
|
||||
@ -33,6 +33,11 @@ export default Ember.Component.extend(bufferedRender({
|
||||
attributeBindings: ['data-topic-id'],
|
||||
'data-topic-id': Em.computed.alias('topic.id'),
|
||||
|
||||
@computed
|
||||
newDotText() {
|
||||
return (this.currentUser && this.currentUser.trust_level > 0) ? "" : I18n.t('filters.new.lower_title');
|
||||
},
|
||||
|
||||
actions: {
|
||||
toggleBookmark() {
|
||||
this.get('topic').toggleBookmark().finally(() => this.rerenderBuffer());
|
||||
@ -58,8 +63,15 @@ export default Ember.Component.extend(bufferedRender({
|
||||
classes.push('has-excerpt');
|
||||
}
|
||||
|
||||
if (topic.get('unseen')) {
|
||||
classes.push("unseen-topic");
|
||||
}
|
||||
|
||||
['liked', 'archived', 'bookmarked', 'pinned'].forEach(name => {
|
||||
if (topic.get('displayNewPosts')) {
|
||||
classes.push("new-posts");
|
||||
}
|
||||
|
||||
['liked', 'archived', 'bookmarked', 'pinned', 'closed'].forEach(name => {
|
||||
if (topic.get(name)) {
|
||||
classes.push(name);
|
||||
}
|
||||
|
||||
@ -13,9 +13,10 @@ export default Ember.Component.extend(bufferedRender({
|
||||
rerenderTriggers: ['url', 'unread', 'newPosts', 'unseen'],
|
||||
|
||||
buildBuffer(buffer) {
|
||||
const newDotText = (this.currentUser && this.currentUser.trust_level > 0) ? " " : I18n.t('filters.new.lower_title');
|
||||
const url = this.get('url');
|
||||
link(buffer, this.get('unread'), url, 'unread', 'unread_posts');
|
||||
link(buffer, this.get('newPosts'), url, 'new-posts', 'new_posts');
|
||||
link(buffer, this.get('unseen'), url, 'new-topic', 'new', I18n.t('filters.new.lower_title'));
|
||||
link(buffer, this.get('unseen'), url, 'new-topic', 'new', newDotText);
|
||||
}
|
||||
}));
|
||||
|
||||
@ -111,48 +111,48 @@ export default Ember.Component.extend({
|
||||
const style = `border-right-width: ${borderSize}; width: ${progressWidth}px`;
|
||||
$topicProgress.append(`<div class='bg' style="${style}"> </div>`);
|
||||
} else {
|
||||
$bg.css("border-right-width", borderSize).width(progressWidth);
|
||||
$bg.css("border-right-width", borderSize).width(progressWidth - 2);
|
||||
}
|
||||
},
|
||||
|
||||
_dock() {
|
||||
let maximumOffset = $('#topic-bottom').offset();
|
||||
let composerHeight = $('#reply-control').height() || 0;
|
||||
let $topicProgressWrapper = this.$();
|
||||
let offset = window.pageYOffset || $('html').scrollTop();
|
||||
const $topicProgressWrapper = this.$();
|
||||
if (!$topicProgressWrapper || $topicProgressWrapper.length === 0) return;
|
||||
|
||||
if (!$topicProgressWrapper || $topicProgressWrapper.length === 0) {
|
||||
return;
|
||||
}
|
||||
// on desktop, we want the topic-progress after the last post
|
||||
// on mobile, we want it right before the end of the last post
|
||||
const progressHeight = this.site.mobileView ? 0 : $("#topic-progress").outerHeight();
|
||||
|
||||
// Right position
|
||||
let $replyArea = $('#reply-control .reply-area');
|
||||
const maximumOffset = $('#topic-bottom').offset();
|
||||
const composerHeight = $('#reply-control').height() || 0;
|
||||
const offset = window.pageYOffset || $('html').scrollTop();
|
||||
|
||||
const $replyArea = $('#reply-control .reply-area');
|
||||
if ($replyArea && $replyArea.length) {
|
||||
let rightPos = $replyArea.offset().left;
|
||||
$topicProgressWrapper.css('right', `${rightPos}px`);
|
||||
$topicProgressWrapper.css('right', `${$replyArea.offset().left}px`);
|
||||
} else {
|
||||
$topicProgressWrapper.css('right', `1em`);
|
||||
}
|
||||
|
||||
let isDocked = false;
|
||||
if (maximumOffset) {
|
||||
let threshold = maximumOffset.top;
|
||||
let windowHeight = $(window).height();
|
||||
let headerHeight = $('header').outerHeight(true);
|
||||
const threshold = maximumOffset.top + progressHeight;
|
||||
const windowHeight = $(window).height();
|
||||
|
||||
if (this.capabilities.isIOS) {
|
||||
const headerHeight = $('header').outerHeight(true);
|
||||
isDocked = offset >= (threshold - windowHeight - headerHeight + composerHeight);
|
||||
} else {
|
||||
isDocked = offset >= (threshold - windowHeight + composerHeight);
|
||||
}
|
||||
}
|
||||
|
||||
let dockPos = $(document).height() - $('#topic-bottom').offset().top;
|
||||
const dockPos = $(document).height() - maximumOffset.top - progressHeight;
|
||||
if (composerHeight > 0) {
|
||||
if (isDocked) {
|
||||
$topicProgressWrapper.css('bottom', dockPos);
|
||||
} else {
|
||||
let height = composerHeight + "px";
|
||||
const height = composerHeight + "px";
|
||||
if ($topicProgressWrapper.css('bottom') !== height) {
|
||||
$topicProgressWrapper.css('bottom', height);
|
||||
}
|
||||
|
||||
@ -54,7 +54,7 @@ export default Ember.Component.extend(bufferedRender({
|
||||
}, options);
|
||||
}
|
||||
|
||||
buffer.push(I18n.t(this._noticeKey(), options));
|
||||
buffer.push(`<span>${I18n.t(this._noticeKey(), options)}</span>`);
|
||||
buffer.push('</h3>');
|
||||
|
||||
// TODO Sam: concerned this can cause a heavy rerender loop
|
||||
|
||||
@ -519,7 +519,7 @@ export default Ember.Controller.extend({
|
||||
if (result.responseJson.action === "create_post" || this.get('replyAsNewTopicDraft') || this.get('replyAsNewPrivateMessageDraft')) {
|
||||
this.destroyDraft();
|
||||
}
|
||||
if (this.get('model.action') === 'edit') {
|
||||
if (this.get('model.editingPost')) {
|
||||
this.appEvents.trigger('post-stream:refresh', { id: parseInt(result.responseJson.id) });
|
||||
if (result.responseJson.post.post_number === 1) {
|
||||
this.appEvents.trigger('header:update-topic', composer.get('topic'));
|
||||
@ -779,11 +779,19 @@ export default Ember.Controller.extend({
|
||||
|
||||
@computed('model.categoryId', 'lastValidatedAt')
|
||||
categoryValidation(categoryId, lastValidatedAt) {
|
||||
if( !this.siteSettings.allow_uncategorized_topics && !categoryId) {
|
||||
if(!this.siteSettings.allow_uncategorized_topics && !categoryId) {
|
||||
return InputValidation.create({ failed: true, reason: I18n.t('composer.error.category_missing'), lastShownAt: lastValidatedAt });
|
||||
}
|
||||
},
|
||||
|
||||
@computed('model.category', 'model.tags', 'lastValidatedAt')
|
||||
tagValidation(category, tags, lastValidatedAt) {
|
||||
const tagsArray = tags || [];
|
||||
if (this.site.get('can_tag_topics') && category && category.get('minimum_required_tags') > tagsArray.length) {
|
||||
return InputValidation.create({ failed: true, reason: I18n.t('composer.error.tags_missing', {count: category.get('minimum_required_tags')}), lastShownAt: lastValidatedAt });
|
||||
}
|
||||
},
|
||||
|
||||
collapse() {
|
||||
this._saveDraft();
|
||||
this.set('model.composeState', Composer.DRAFT);
|
||||
|
||||
@ -137,11 +137,12 @@ const controllerOpts = {
|
||||
footerEducation: function() {
|
||||
if (!this.get('allLoaded') || this.get('model.topics.length') > 0 || !this.currentUser) { return; }
|
||||
|
||||
const split = (this.get('model.filter') || '').split('/');
|
||||
const segments = (this.get('model.filter') || '').split('/');
|
||||
|
||||
if (split[0] !== 'new' && split[0] !== 'unread') { return; }
|
||||
const tab = segments[segments.length - 1];
|
||||
if (tab !== 'new' && tab !== 'unread') { return; }
|
||||
|
||||
return I18n.t("topics.none.educate." + split[0], {
|
||||
return I18n.t("topics.none.educate." + tab, {
|
||||
userPrefsUrl: userPath(`${this.currentUser.get('username_lower')}/preferences`)
|
||||
});
|
||||
}.property('allLoaded', 'model.topics.length')
|
||||
|
||||
@ -174,7 +174,7 @@ export default Ember.Controller.extend({
|
||||
|
||||
@computed('expanded')
|
||||
searchAdvancedIcon(expanded) {
|
||||
return iconHTML(expanded ? "caret-down" : "caret-right");
|
||||
return iconHTML(expanded ? "caret-down fa-fw" : "caret-right fa-fw");
|
||||
},
|
||||
|
||||
@computed('page')
|
||||
|
||||
@ -1,18 +1,20 @@
|
||||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import { extractError } from 'discourse/lib/ajax-error';
|
||||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
loading: false,
|
||||
setAsOwner: false,
|
||||
|
||||
@computed('model.usernames')
|
||||
disableAddButton(usernames) {
|
||||
return !usernames || !(usernames.length > 0);
|
||||
@computed('model.usernames', 'loading')
|
||||
disableAddButton(usernames, loading) {
|
||||
return loading || !usernames || !(usernames.length > 0);
|
||||
},
|
||||
|
||||
actions: {
|
||||
addMembers() {
|
||||
this.set('loading', true);
|
||||
|
||||
const model = this.get('model');
|
||||
const usernames = model.get('usernames');
|
||||
if (Em.isEmpty(usernames)) { return; }
|
||||
@ -30,10 +32,14 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
this.get('model.name'),
|
||||
{ queryParams: { filter: usernames } }
|
||||
);
|
||||
|
||||
model.set("usernames", null);
|
||||
this.send('closeModal');
|
||||
})
|
||||
.catch(error => this.flash(extractError(error), 'error'));
|
||||
.catch(error => {
|
||||
this.flash(extractError(error), 'error');
|
||||
})
|
||||
.finally(() => this.set('loading', false));
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -0,0 +1,45 @@
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import { extractError } from 'discourse/lib/ajax-error';
|
||||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
import { ajax } from 'discourse/lib/ajax';
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
loading: false,
|
||||
|
||||
@computed('input', 'loading', 'result')
|
||||
disableAddButton(input, loading, result) {
|
||||
return loading || Ember.isEmpty(input) || (input.length <= 0) || result;
|
||||
},
|
||||
|
||||
actions: {
|
||||
cancel() {
|
||||
this.set('result', null);
|
||||
},
|
||||
|
||||
add() {
|
||||
this.setProperties({
|
||||
loading: true,
|
||||
result: null,
|
||||
});
|
||||
|
||||
const users = this.get('input').split("\n")
|
||||
.uniq()
|
||||
.reject(x => x.length === 0);
|
||||
|
||||
ajax('/admin/groups/bulk', {
|
||||
data: { users, group_id: this.get('model.id') },
|
||||
method: 'PUT'
|
||||
}).then(result => {
|
||||
this.set('result', result);
|
||||
|
||||
if (result.users_not_added) {
|
||||
this.set('result.invalidUsers', result.users_not_added.join(", "));
|
||||
}
|
||||
}).catch(error => {
|
||||
this.flash(extractError(error), 'error');
|
||||
}).finally(() => {
|
||||
this.set('loading', false);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -1,23 +0,0 @@
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
@computed('saving')
|
||||
savingText(saving) {
|
||||
if (saving !== undefined) {
|
||||
return saving ? I18n.t('saving') : I18n.t('saved');
|
||||
}
|
||||
},
|
||||
|
||||
actions: {
|
||||
save() {
|
||||
this.set('saving', true);
|
||||
|
||||
this.get('model').save().catch(error => {
|
||||
popupAjaxError(error);
|
||||
}).finally(() => {
|
||||
this.set('saving', false);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,3 @@
|
||||
export default Ember.Controller.extend({
|
||||
saving: null,
|
||||
});
|
||||
@ -0,0 +1,25 @@
|
||||
import { default as computed } from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
application: Ember.inject.controller(),
|
||||
|
||||
@computed("model.automatic")
|
||||
tabs(automatic) {
|
||||
const defaultTabs = [
|
||||
{ route: 'group.manage.interaction', title: 'groups.manage.interaction.title' },
|
||||
{ route: 'group.manage.logs', title: 'groups.manage.logs.title' },
|
||||
];
|
||||
|
||||
if (!automatic) {
|
||||
defaultTabs.splice(0, 0,
|
||||
{ route: 'group.manage.profile', title: 'groups.manage.profile.title' }
|
||||
);
|
||||
|
||||
defaultTabs.splice(1, 0,
|
||||
{ route: 'group.manage.membership', title: 'groups.manage.membership.title' }
|
||||
);
|
||||
}
|
||||
|
||||
return defaultTabs;
|
||||
},
|
||||
});
|
||||
@ -13,9 +13,10 @@ export default Ember.Controller.extend({
|
||||
application: Ember.inject.controller(),
|
||||
counts: null,
|
||||
showing: 'members',
|
||||
destroying: null,
|
||||
|
||||
@computed('showMessages', 'model.user_count')
|
||||
tabs(showMessages, userCount) {
|
||||
@computed('showMessages', 'model.user_count', 'canManageGroup')
|
||||
tabs(showMessages, userCount, canManageGroup) {
|
||||
const membersTab = Tab.create({
|
||||
name: 'members',
|
||||
route: 'group.index',
|
||||
@ -36,15 +37,12 @@ export default Ember.Controller.extend({
|
||||
}));
|
||||
}
|
||||
|
||||
if (this.currentUser && this.currentUser.canManageGroup(this.model)) {
|
||||
defaultTabs.push(...[
|
||||
if (canManageGroup) {
|
||||
defaultTabs.push(
|
||||
Tab.create({
|
||||
name: 'edit', i18nKey: 'edit.title', icon: 'pencil'
|
||||
}),
|
||||
Tab.create({
|
||||
name: 'logs', i18nKey: 'logs.title', icon: 'list-alt'
|
||||
name: 'manage', i18nKey: 'manage.title', icon: 'wrench'
|
||||
})
|
||||
]);
|
||||
);
|
||||
}
|
||||
|
||||
return defaultTabs;
|
||||
@ -84,14 +82,40 @@ export default Ember.Controller.extend({
|
||||
return this.currentUser && messageable;
|
||||
},
|
||||
|
||||
@computed('model')
|
||||
canManageGroup(model) {
|
||||
return this.currentUser && this.currentUser.canManageGroup(model);
|
||||
@computed('model', 'model.automatic')
|
||||
canManageGroup(model, automatic) {
|
||||
return this.currentUser && (
|
||||
this.currentUser.canManageGroup(model) ||
|
||||
(this.currentUser.admin && automatic)
|
||||
);
|
||||
},
|
||||
|
||||
actions: {
|
||||
messageGroup() {
|
||||
this.send('createNewMessageViaParams', this.get('model.name'));
|
||||
}
|
||||
},
|
||||
|
||||
destroy() {
|
||||
const group = this.get('model');
|
||||
this.set('destroying', true);
|
||||
|
||||
bootbox.confirm(
|
||||
I18n.t("admin.groups.delete_confirm"),
|
||||
I18n.t("no_value"),
|
||||
I18n.t("yes_value"),
|
||||
confirmed => {
|
||||
if (confirmed) {
|
||||
group.destroy().then(() => {
|
||||
this.transitionToRoute('groups.index');
|
||||
}).catch(error => {
|
||||
Ember.Logger.error(error);
|
||||
bootbox.alert(I18n.t("admin.groups.delete_failed"));
|
||||
}).finally(() => this.set('destroying', false));
|
||||
} else {
|
||||
this.set('destroying', false);
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
@ -35,6 +35,10 @@ export default Ember.Controller.extend({
|
||||
actions: {
|
||||
loadMore() {
|
||||
this.get('model').loadMore();
|
||||
},
|
||||
|
||||
new() {
|
||||
this.transitionToRoute("groups.new");
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,17 @@
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
saving: null,
|
||||
|
||||
actions: {
|
||||
save() {
|
||||
this.set('saving', true);
|
||||
const group = this.get('model');
|
||||
|
||||
group.create().then(() => {
|
||||
this.transitionToRoute("group.members", group.name);
|
||||
}).catch(popupAjaxError)
|
||||
.finally(() => this.set('saving', false));
|
||||
},
|
||||
}
|
||||
});
|
||||
@ -6,10 +6,8 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
|
||||
actions: {
|
||||
jump() {
|
||||
let where = parseInt(this.get('postNumber'));
|
||||
if (where < 1) { where = 1; }
|
||||
const max = this.get('topic.postStream.filteredPostsCount');
|
||||
if (where > max) { where = max; }
|
||||
const max = this.get("topic.postStream.filteredPostsCount");
|
||||
const where = Math.min(max, Math.max(1, parseInt(this.get("postNumber"))));
|
||||
|
||||
this.jumpToIndex(where);
|
||||
this.send('closeModal');
|
||||
|
||||
@ -7,12 +7,13 @@ import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
|
||||
export default Ember.Controller.extend(CanCheckEmails, PreferencesTabController, {
|
||||
|
||||
saveAttrNames: ['name'],
|
||||
saveAttrNames: ['name', 'title'],
|
||||
|
||||
canEditName: setting('enable_names'),
|
||||
canSaveUser: true,
|
||||
|
||||
newNameInput: null,
|
||||
newTitleInput: null,
|
||||
|
||||
passwordProgress: null,
|
||||
|
||||
@ -30,9 +31,9 @@ export default Ember.Controller.extend(CanCheckEmails, PreferencesTabController,
|
||||
return I18n.t(this.siteSettings.full_name_required ? 'user.name.instructions_required' : 'user.name.instructions');
|
||||
},
|
||||
|
||||
@computed("model.has_title_badges")
|
||||
canSelectTitle(hasTitleBadges) {
|
||||
return this.siteSettings.enable_badges && hasTitleBadges;
|
||||
@computed('model.availableTitles')
|
||||
canSelectTitle(availableTitles) {
|
||||
return availableTitles.length > 0;
|
||||
},
|
||||
|
||||
@computed()
|
||||
@ -52,6 +53,7 @@ export default Ember.Controller.extend(CanCheckEmails, PreferencesTabController,
|
||||
const model = this.get('model');
|
||||
|
||||
model.set('name', this.get('newNameInput'));
|
||||
model.set('title', this.get('newTitleInput'));
|
||||
|
||||
return model.save(this.get('saveAttrNames')).then(() => {
|
||||
this.set('saved', true);
|
||||
|
||||
@ -409,16 +409,29 @@ export default Ember.Controller.extend(BufferedContent, {
|
||||
}
|
||||
|
||||
const composer = this.get("composer");
|
||||
let topic = this.get('model');
|
||||
const composerModel = composer.get("model");
|
||||
let editingFirst = composerModel && (post.get('firstPost') || composerModel.get('editingFirstPost'));
|
||||
|
||||
let editingSharedDraft = false;
|
||||
let draftsCategoryId = this.get('site.shared_drafts_category_id');
|
||||
if (draftsCategoryId && draftsCategoryId === topic.get('category.id')) {
|
||||
editingSharedDraft = post.get('firstPost');
|
||||
}
|
||||
|
||||
const opts = {
|
||||
post,
|
||||
action: Composer.EDIT,
|
||||
action: editingSharedDraft ? Composer.EDIT_SHARED_DRAFT : Composer.EDIT,
|
||||
draftKey: post.get("topic.draft_key"),
|
||||
draftSequence: post.get("topic.draft_sequence")
|
||||
};
|
||||
|
||||
if (editingSharedDraft) {
|
||||
opts.destinationCategoryId = topic.get('destination_category_id');
|
||||
}
|
||||
|
||||
// Cancel and reopen the composer for the first post
|
||||
if (composerModel && (post.get('firstPost') || composerModel.get('editingFirstPost'))) {
|
||||
if (editingFirst) {
|
||||
composer.cancelComposer().then(() => composer.open(opts));
|
||||
} else {
|
||||
composer.open(opts);
|
||||
@ -439,17 +452,17 @@ export default Ember.Controller.extend(BufferedContent, {
|
||||
},
|
||||
|
||||
jumpToIndex(index) {
|
||||
this._jumpToPostId(this.get('model.postStream.stream')[index - 1]);
|
||||
this._jumpToIndex(index);
|
||||
},
|
||||
|
||||
jumpToPostPrompt() {
|
||||
const postText = prompt(I18n.t('topic.progress.jump_prompt_long'));
|
||||
if (postText === null) { return; }
|
||||
|
||||
const postNumber = parseInt(postText, 10);
|
||||
if (postNumber === 0) { return; }
|
||||
const postIndex = parseInt(postText, 10);
|
||||
if (postIndex === 0) { return; }
|
||||
|
||||
this._jumpToPostId(this.get('model.postStream').findPostIdForPostNumber(postNumber));
|
||||
this._jumpToIndex(postIndex);
|
||||
},
|
||||
|
||||
jumpToPost(postNumber) {
|
||||
@ -742,6 +755,12 @@ export default Ember.Controller.extend(BufferedContent, {
|
||||
}
|
||||
},
|
||||
|
||||
_jumpToIndex(index) {
|
||||
const stream = this.get("model.postStream.stream");
|
||||
index = Math.max(1, Math.min(stream.length, index));
|
||||
this._jumpToPostId(stream[index - 1]);
|
||||
},
|
||||
|
||||
_jumpToPostId(postId) {
|
||||
if (!postId) {
|
||||
Ember.Logger.warn("jump-post code broken - requested an index outside the stream array");
|
||||
|
||||
@ -10,9 +10,15 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
||||
currentPath: Ember.computed.alias('application.currentPath'),
|
||||
adminTools: optionalService(),
|
||||
|
||||
@computed("content.username")
|
||||
@computed('model.username')
|
||||
viewingSelf(username) {
|
||||
return username === User.currentProp('username');
|
||||
let currentUser = this.currentUser;
|
||||
return currentUser && username === currentUser.get('username');
|
||||
},
|
||||
|
||||
@computed('viewingSelf')
|
||||
canExpandProfile(viewingSelf) {
|
||||
return viewingSelf;
|
||||
},
|
||||
|
||||
@computed('model.profileBackground')
|
||||
@ -91,6 +97,10 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
||||
},
|
||||
|
||||
actions: {
|
||||
collapseProfile() {
|
||||
this.set('forceExpand', false);
|
||||
},
|
||||
|
||||
expandProfile() {
|
||||
this.set('forceExpand', true);
|
||||
},
|
||||
|
||||
@ -0,0 +1,20 @@
|
||||
// A small helper to inject theme settings into
|
||||
// context objects of handlebars templates used
|
||||
// in themes
|
||||
|
||||
import { registerHelper } from 'discourse-common/lib/helpers';
|
||||
|
||||
function inject(context, key, value) {
|
||||
if (typeof value === "string") {
|
||||
value = value.replace(/\\u0022/g, '"');
|
||||
}
|
||||
|
||||
if (!context.get("themeSettings")) {
|
||||
context.set("themeSettings", {});
|
||||
}
|
||||
context.set(`themeSettings.${key}`, value);
|
||||
}
|
||||
|
||||
registerHelper('theme-setting-injector', function(arr, hash) {
|
||||
inject(hash.context, hash.key, hash.value);
|
||||
});
|
||||
@ -2,27 +2,25 @@ export default {
|
||||
name: 'register-service-worker',
|
||||
|
||||
initialize() {
|
||||
window.addEventListener('load', () => {
|
||||
const isSecured = (document.location.protocol === 'https:') ||
|
||||
(location.hostname === "localhost");
|
||||
const isSecured = (document.location.protocol === 'https:') ||
|
||||
(location.hostname === "localhost");
|
||||
|
||||
const isSupported= isSecured && ('serviceWorker' in navigator);
|
||||
const isSupported= isSecured && ('serviceWorker' in navigator);
|
||||
|
||||
if (isSupported) {
|
||||
if (Discourse.ServiceWorkerURL) {
|
||||
navigator.serviceWorker
|
||||
.register(`${Discourse.BaseUri}/${Discourse.ServiceWorkerURL}`)
|
||||
.catch(error => {
|
||||
Ember.Logger.info(`Failed to register Service Worker: ${error}`);
|
||||
});
|
||||
} else {
|
||||
navigator.serviceWorker.getRegistrations().then(registrations => {
|
||||
for(let registration of registrations) {
|
||||
registration.unregister();
|
||||
};
|
||||
if (isSupported) {
|
||||
if (Discourse.ServiceWorkerURL) {
|
||||
navigator.serviceWorker
|
||||
.register(`${Discourse.BaseUri}/${Discourse.ServiceWorkerURL}`)
|
||||
.catch(error => {
|
||||
Ember.Logger.info(`Failed to register Service Worker: ${error}`);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
navigator.serviceWorker.getRegistrations().then(registrations => {
|
||||
for(let registration of registrations) {
|
||||
registration.unregister();
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -21,7 +21,7 @@ export default {
|
||||
}
|
||||
|
||||
DiscourseURL.rewrite(/^\/u\/([^\/]+)\/?$/, "/u/$1/summary", {
|
||||
exceptions: ['/u/account-created', '/users/account-created']
|
||||
exceptions: ['/u/account-created', '/users/account-created', '/u/password-reset', '/users/password-reset']
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import DiscourseURL from 'discourse/lib/url';
|
||||
import Composer from 'discourse/models/composer';
|
||||
import { scrollTopFor } from 'discourse/lib/offset-calculator';
|
||||
import { minimumOffset } from 'discourse/lib/offset-calculator';
|
||||
|
||||
const bindings = {
|
||||
'!': {postAction: 'showFlags'},
|
||||
@ -311,52 +311,36 @@ export default {
|
||||
_moveSelection(direction) {
|
||||
const $articles = this._findArticles();
|
||||
|
||||
if (typeof $articles === 'undefined') {
|
||||
return;
|
||||
}
|
||||
if (typeof $articles === 'undefined') return;
|
||||
|
||||
const $selected = ($articles.filter('.selected').length !== 0)
|
||||
? $articles.filter('.selected')
|
||||
: $articles.filter('[data-islastviewedtopic=true]');
|
||||
|
||||
let index = $articles.index($selected);
|
||||
|
||||
if ($selected.length !== 0) { //boundries check
|
||||
// loop is not allowed
|
||||
if (direction === -1 && index === 0) { return; }
|
||||
if (direction === 1 && index === ($articles.length - 1) ) { return; }
|
||||
if ($selected.length !== 0) {
|
||||
if (direction === -1 && index === 0) return;
|
||||
if (direction === 1 && index === $articles.length - 1) return;
|
||||
}
|
||||
|
||||
// if nothing is selected go to the first post on screen
|
||||
// when nothing is selected
|
||||
if ($selected.length === 0) {
|
||||
const scrollTop = $(document).scrollTop();
|
||||
|
||||
index = 0;
|
||||
$articles.each(function() {
|
||||
const top = $(this).position().top;
|
||||
if (top >= scrollTop) {
|
||||
return false;
|
||||
}
|
||||
index += 1;
|
||||
});
|
||||
|
||||
if (index >= $articles.length) {
|
||||
index = $articles.length - 1;
|
||||
}
|
||||
|
||||
// select the first post with its top visible
|
||||
const offset = minimumOffset();
|
||||
index = $articles.toArray().findIndex(article => article.getBoundingClientRect().top > offset);
|
||||
direction = 0;
|
||||
}
|
||||
|
||||
const $article = $articles.eq(index + direction);
|
||||
|
||||
if ($article.length > 0) {
|
||||
|
||||
$articles.removeClass('selected');
|
||||
$article.addClass('selected');
|
||||
|
||||
if ($article.is('.topic-post')) {
|
||||
$('a.tabLoc', $article).focus();
|
||||
this._scrollToPost($article);
|
||||
|
||||
} else {
|
||||
this._scrollList($article, direction);
|
||||
}
|
||||
@ -364,8 +348,11 @@ export default {
|
||||
},
|
||||
|
||||
_scrollToPost($article) {
|
||||
const pos = $article.offset();
|
||||
$(window).scrollTop(scrollTopFor(pos.top));
|
||||
if ($article.find("#post_1").length > 0) {
|
||||
$(window).scrollTop(0);
|
||||
} else {
|
||||
$(window).scrollTop($article.offset().top - minimumOffset());
|
||||
}
|
||||
},
|
||||
|
||||
_scrollList($article) {
|
||||
@ -393,14 +380,13 @@ export default {
|
||||
|
||||
|
||||
_findArticles() {
|
||||
const $topicList = $('.topic-list'),
|
||||
$topicArea = $('.posts-wrapper');
|
||||
const $topicList = $(".topic-list");
|
||||
const $postsWrapper = $(".posts-wrapper");
|
||||
|
||||
if ($topicArea.length > 0) {
|
||||
return $('.posts-wrapper .topic-post, .topic-list tbody tr');
|
||||
}
|
||||
else if ($topicList.length > 0) {
|
||||
return $topicList.find('.topic-list-item');
|
||||
if ($postsWrapper.length > 0) {
|
||||
return $(".posts-wrapper .topic-post, .topic-list tbody tr");
|
||||
} else if ($topicList.length > 0) {
|
||||
return $topicList.find(".topic-list-item");
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import { scrollTopFor } from 'discourse/lib/offset-calculator';
|
||||
import { minimumOffset } from "discourse/lib/offset-calculator";
|
||||
|
||||
// Dear traveller, you are entering a zone where we are at war with the browser
|
||||
// the browser is insisting on positioning scrollTop per the location it was in
|
||||
// the past, we are insisting on it being where we want it to be
|
||||
// The hack is just to keep trying over and over to position the scrollbar (up to 1 minute)
|
||||
// Dear traveller, you are entering a zone where we are at war with the browser.
|
||||
// The browser is insisting on positioning scrollTop per the location it was in
|
||||
// the past, we are insisting on it being where we want it to be.
|
||||
// The hack is just to keep trying over and over to position the scrollbar (up to 1 second).
|
||||
//
|
||||
// The root cause is that a "refresh" on a topic page will almost never be at the
|
||||
// same position it was in the past, the URL points to the post at the top of the
|
||||
// page, so a refresh will try to bring that post into view causing drift
|
||||
// page, so a refresh will try to bring that post into view causing drift.
|
||||
//
|
||||
// Additionally if you loaded multiple batches of posts, on refresh they will not
|
||||
// be loaded.
|
||||
@ -18,29 +18,29 @@ import { scrollTopFor } from 'discourse/lib/offset-calculator';
|
||||
// 1. onbeforeunload ensure we are scrolled to the right spot
|
||||
// 2. give up on the scrollbar and implement it ourselves (something that will happen)
|
||||
|
||||
const LOCK_DURATION_MS = 1000;
|
||||
const SCROLL_EVENTS = "scroll.lock-on touchmove.lock-on mousedown.lock-on wheel.lock-on DOMMouseScroll.lock-on mousewheel.lock-on keyup.lock-on";
|
||||
const SCROLL_TYPES = ["mousedown", "mousewheel", "touchmove", "wheel"];
|
||||
|
||||
function within(threshold, x, y) {
|
||||
return Math.abs(x-y) < threshold;
|
||||
return Math.abs(x - y) < threshold;
|
||||
}
|
||||
|
||||
export default class LockOn {
|
||||
constructor(selector, options) {
|
||||
this.selector = selector;
|
||||
this.options = options || {};
|
||||
this.offsetTop = null;
|
||||
}
|
||||
|
||||
elementTop() {
|
||||
const selected = $(this.selector);
|
||||
if (selected && selected.offset && selected.offset()) {
|
||||
const result = selected.offset().top;
|
||||
return scrollTopFor(result);
|
||||
const $selected = $(this.selector);
|
||||
if ($selected && $selected.offset && $selected.offset()) {
|
||||
return $selected.offset().top - minimumOffset();
|
||||
}
|
||||
}
|
||||
|
||||
clearLock(interval) {
|
||||
$('body,html').off(SCROLL_EVENTS);
|
||||
$("body, html").off(SCROLL_EVENTS);
|
||||
clearInterval(interval);
|
||||
if (this.options.finished) {
|
||||
this.options.finished();
|
||||
@ -54,9 +54,7 @@ export default class LockOn {
|
||||
$(window).scrollTop(previousTop);
|
||||
|
||||
const interval = setInterval(() => {
|
||||
let top = this.elementTop();
|
||||
if (top < 0) { top = 0; }
|
||||
|
||||
const top = Math.max(0, this.elementTop());
|
||||
const scrollTop = $(window).scrollTop();
|
||||
|
||||
if (typeof(top) === "undefined" || isNaN(top)) {
|
||||
@ -68,20 +66,14 @@ export default class LockOn {
|
||||
previousTop = top;
|
||||
}
|
||||
|
||||
// We commit suicide after 3s just to clean up
|
||||
const nowTime = new Date().getTime();
|
||||
if (nowTime - startedAt > 1000) {
|
||||
// Commit suicide after a little while
|
||||
if (new Date().getTime() - startedAt > LOCK_DURATION_MS) {
|
||||
return this.clearLock(interval);
|
||||
}
|
||||
}, 50);
|
||||
|
||||
$('body,html').off(SCROLL_EVENTS).on(SCROLL_EVENTS, e => {
|
||||
if ( e.which > 0 ||
|
||||
e.type === "mousedown" ||
|
||||
e.type === "mousewheel" ||
|
||||
e.type === "touchmove" ||
|
||||
e.type === "wheel"
|
||||
) {
|
||||
$("body, html").off(SCROLL_EVENTS).on(SCROLL_EVENTS, e => {
|
||||
if (e.which > 0 || SCROLL_TYPES.includes(e.type)) {
|
||||
this.clearLock(interval);
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,50 +1,36 @@
|
||||
export function scrollTopFor(y) {
|
||||
return y - offsetCalculator(y);
|
||||
return y - offsetCalculator();
|
||||
}
|
||||
|
||||
export default function offsetCalculator(y) {
|
||||
const $header = $('header');
|
||||
const $container = $('.posts-wrapper');
|
||||
const containerOffset = $container.offset();
|
||||
|
||||
let titleHeight = 0;
|
||||
const scrollTop = y || $(window).scrollTop();
|
||||
|
||||
if (!containerOffset || scrollTop < containerOffset.top) {
|
||||
titleHeight = $('#topic-title').height() || 0;
|
||||
}
|
||||
|
||||
const rawWinHeight = $(window).height();
|
||||
const windowHeight = rawWinHeight - titleHeight;
|
||||
|
||||
const eyeTarget = (windowHeight / 10);
|
||||
const headerHeight = $header.outerHeight(true);
|
||||
const expectedOffset = titleHeight - ($header.find('.contents').height() || 0) + (eyeTarget * 2);
|
||||
const ideal = headerHeight + ((expectedOffset < 0) ? 0 : expectedOffset);
|
||||
|
||||
if ($container.length === 0) { return expectedOffset; }
|
||||
|
||||
const topPos = $container.offset().top;
|
||||
|
||||
const docHeight = $(document).height();
|
||||
let scrollPercent = Math.min((scrollTop / (docHeight-rawWinHeight)), 1.0);
|
||||
|
||||
let inter = topPos - scrollTop + ($container.height() * scrollPercent);
|
||||
if (inter < headerHeight + eyeTarget) {
|
||||
inter = headerHeight + eyeTarget;
|
||||
}
|
||||
|
||||
if (inter > ideal) {
|
||||
const bottom = $('#topic-bottom').offset().top;
|
||||
const switchPos = bottom - rawWinHeight - ideal;
|
||||
|
||||
if (scrollTop > switchPos) {
|
||||
const p = Math.max(Math.min((scrollTop + inter - switchPos) / rawWinHeight, 1.0), 0.0);
|
||||
return ((1 - p) * ideal) + (p * inter);
|
||||
} else {
|
||||
return ideal;
|
||||
}
|
||||
}
|
||||
|
||||
return inter;
|
||||
export function minimumOffset() {
|
||||
const $header = $("header.d-header");
|
||||
const headerHeight = $header.outerHeight(true) || 0;
|
||||
const headerPositionTop = $header.position().top;
|
||||
return headerHeight + headerPositionTop;
|
||||
}
|
||||
|
||||
export default function offsetCalculator() {
|
||||
const min = minimumOffset();
|
||||
|
||||
// on mobile, just use the header
|
||||
if ($("html").hasClass("mobile-view")) return min;
|
||||
|
||||
const $window = $(window);
|
||||
const windowHeight = $window.height();
|
||||
const documentHeight = $(document).height();
|
||||
const topicBottomOffsetTop = $("#topic-bottom").offset().top;
|
||||
|
||||
// the footer is bigger than the window, we can scroll down past the last post
|
||||
if (documentHeight - windowHeight > topicBottomOffsetTop) return min;
|
||||
|
||||
const scrollTop = $window.scrollTop();
|
||||
const visibleBottomHeight = scrollTop + windowHeight - topicBottomOffsetTop;
|
||||
|
||||
if (visibleBottomHeight > 0) {
|
||||
const bottomHeight = documentHeight - topicBottomOffsetTop;
|
||||
const offset = (windowHeight - bottomHeight) * visibleBottomHeight / bottomHeight;
|
||||
return Math.max(min, offset);
|
||||
}
|
||||
|
||||
return min;
|
||||
}
|
||||
|
||||
@ -108,7 +108,8 @@ const Category = RestModel.extend({
|
||||
num_featured_topics: this.get('num_featured_topics'),
|
||||
default_view: this.get('default_view'),
|
||||
subcategory_list_style: this.get('subcategory_list_style'),
|
||||
default_top_period: this.get('default_top_period')
|
||||
default_top_period: this.get('default_top_period'),
|
||||
minimum_required_tags: this.get('minimum_required_tags')
|
||||
},
|
||||
type: id ? 'PUT' : 'POST'
|
||||
});
|
||||
|
||||
@ -10,6 +10,7 @@ import { escapeExpression, tinyAvatar } from 'discourse/lib/utilities';
|
||||
export const
|
||||
CREATE_TOPIC = 'createTopic',
|
||||
CREATE_SHARED_DRAFT = 'createSharedDraft',
|
||||
EDIT_SHARED_DRAFT = 'editSharedDraft',
|
||||
PRIVATE_MESSAGE = 'privateMessage',
|
||||
NEW_PRIVATE_MESSAGE_KEY = 'new_private_message',
|
||||
REPLY = 'reply',
|
||||
@ -17,6 +18,10 @@ export const
|
||||
REPLY_AS_NEW_TOPIC_KEY = "reply_as_new_topic",
|
||||
REPLY_AS_NEW_PRIVATE_MESSAGE_KEY = "reply_as_new_private_message";
|
||||
|
||||
function isEdit(action) {
|
||||
return action === EDIT || action === EDIT_SHARED_DRAFT;
|
||||
}
|
||||
|
||||
const CLOSED = 'closed',
|
||||
SAVING = 'saving',
|
||||
OPEN = 'open',
|
||||
@ -52,11 +57,13 @@ const SAVE_LABELS = {
|
||||
[REPLY]: 'composer.reply',
|
||||
[CREATE_TOPIC]: 'composer.create_topic',
|
||||
[PRIVATE_MESSAGE]: 'composer.create_pm',
|
||||
[CREATE_SHARED_DRAFT]: 'composer.create_shared_draft'
|
||||
[CREATE_SHARED_DRAFT]: 'composer.create_shared_draft',
|
||||
[EDIT_SHARED_DRAFT]: 'composer.save_edit'
|
||||
};
|
||||
|
||||
const SAVE_ICONS = {
|
||||
[EDIT]: 'pencil',
|
||||
[EDIT_SHARED_DRAFT]: 'clipboard',
|
||||
[REPLY]: 'reply',
|
||||
[CREATE_TOPIC]: 'plus',
|
||||
[PRIVATE_MESSAGE]: 'envelope',
|
||||
@ -98,6 +105,11 @@ const Composer = RestModel.extend({
|
||||
return categoryId ? this.site.categories.findBy('id', categoryId) : null;
|
||||
},
|
||||
|
||||
@computed('category')
|
||||
minimumRequiredTags(category) {
|
||||
return (category && category.get('minimum_required_tags') > 0) ? category.get('minimum_required_tags') : null;
|
||||
},
|
||||
|
||||
creatingTopic: Em.computed.equal('action', CREATE_TOPIC),
|
||||
creatingSharedDraft: Em.computed.equal('action', CREATE_SHARED_DRAFT),
|
||||
creatingPrivateMessage: Em.computed.equal('action', PRIVATE_MESSAGE),
|
||||
@ -116,13 +128,14 @@ const Composer = RestModel.extend({
|
||||
|
||||
topicFirstPost: Em.computed.or('creatingTopic', 'editingFirstPost'),
|
||||
|
||||
editingPost: Em.computed.equal('action', EDIT),
|
||||
@computed('action')
|
||||
editingPost: isEdit,
|
||||
|
||||
replyingToTopic: Em.computed.equal('action', REPLY),
|
||||
|
||||
viewOpen: Em.computed.equal('composeState', OPEN),
|
||||
viewDraft: Em.computed.equal('composeState', DRAFT),
|
||||
|
||||
|
||||
composeStateChanged: function() {
|
||||
var oldOpen = this.get('composerOpened');
|
||||
|
||||
@ -212,7 +225,7 @@ const Composer = RestModel.extend({
|
||||
if (!this.site.mobileView) {
|
||||
const originalUserName = post.get('reply_to_user.username');
|
||||
const originalUserAvatar = post.get('reply_to_user.avatar_template');
|
||||
if (originalUserName && originalUserAvatar && action === EDIT) {
|
||||
if (originalUserName && originalUserAvatar && isEdit(action)) {
|
||||
options.originalUser = {
|
||||
username: originalUserName,
|
||||
avatar: tinyAvatar(originalUserAvatar)
|
||||
@ -238,31 +251,48 @@ const Composer = RestModel.extend({
|
||||
return options;
|
||||
},
|
||||
|
||||
// whether to disable the post button
|
||||
cantSubmitPost: function() {
|
||||
@computed
|
||||
isStaffUser() {
|
||||
const currentUser = Discourse.User.current();
|
||||
return currentUser && currentUser.get('staff');
|
||||
},
|
||||
|
||||
@computed('loading', 'canEditTitle', 'titleLength', 'targetUsernames', 'replyLength', 'categoryId', 'missingReplyCharacters', 'tags', 'topicFirstPost', 'minimumRequiredTags', 'isStaffUser')
|
||||
cantSubmitPost(loading, canEditTitle, titleLength, targetUsernames, replyLength, categoryId, missingReplyCharacters, tags, topicFirstPost, minimumRequiredTags, isStaffUser) {
|
||||
|
||||
// can't submit while loading
|
||||
if (this.get('loading')) return true;
|
||||
if (loading) return true;
|
||||
|
||||
// title is required when
|
||||
// - creating a new topic/private message
|
||||
// - editing the 1st post
|
||||
if (this.get('canEditTitle') && !this.get('titleLengthValid')) return true;
|
||||
if (canEditTitle && !this.get('titleLengthValid')) return true;
|
||||
|
||||
// reply is always required
|
||||
if (this.get('missingReplyCharacters') > 0) return true;
|
||||
if (missingReplyCharacters > 0) return true;
|
||||
|
||||
if (this.site.get('can_tag_topics') && !isStaffUser && topicFirstPost && minimumRequiredTags) {
|
||||
const tagsArray = tags || [];
|
||||
if (tagsArray.length < minimumRequiredTags) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.get("privateMessage")) {
|
||||
// need at least one user when sending a PM
|
||||
return this.get('targetUsernames') && (this.get('targetUsernames').trim() + ',').indexOf(',') === 0;
|
||||
return targetUsernames && (targetUsernames.trim() + ',').indexOf(',') === 0;
|
||||
} else {
|
||||
// has a category? (when needed)
|
||||
return this.get('canCategorize') &&
|
||||
!this.siteSettings.allow_uncategorized_topics &&
|
||||
!this.get('categoryId') &&
|
||||
!this.user.get('admin');
|
||||
return this.get('requiredCategoryMissing');
|
||||
}
|
||||
}.property('loading', 'canEditTitle', 'titleLength', 'targetUsernames', 'replyLength', 'categoryId', 'missingReplyCharacters'),
|
||||
},
|
||||
|
||||
@computed('canCategorize', 'categoryId')
|
||||
requiredCategoryMissing(canCategorize, categoryId) {
|
||||
return canCategorize && !categoryId &&
|
||||
!this.siteSettings.allow_uncategorized_topics &&
|
||||
!this.user.get('admin');
|
||||
},
|
||||
|
||||
titleLengthValid: function() {
|
||||
if (this.user.get('admin') && this.get('post.static_doc') && this.get('titleLength') > 0) return true;
|
||||
@ -471,11 +501,11 @@ const Composer = RestModel.extend({
|
||||
|
||||
const composer = this;
|
||||
if (!replyBlank &&
|
||||
((opts.reply || opts.action === EDIT) && this.get('replyDirty'))) {
|
||||
((opts.reply || isEdit(opts.action)) && this.get('replyDirty'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.action === REPLY && this.get('action') === EDIT) this.set('reply', '');
|
||||
if (opts.action === REPLY && isEdit(this.get('action'))) this.set('reply', '');
|
||||
if (!opts.draftKey) throw 'draft key is required';
|
||||
if (opts.draftSequence === null) throw 'draft sequence is required';
|
||||
|
||||
@ -527,11 +557,15 @@ const Composer = RestModel.extend({
|
||||
}
|
||||
|
||||
// If we are editing a post, load it.
|
||||
if (opts.action === EDIT && opts.post) {
|
||||
if (isEdit(opts.action) && opts.post) {
|
||||
|
||||
const topicProps = this.serialize(_edit_topic_serializer);
|
||||
topicProps.loading = true;
|
||||
|
||||
// When editing a shared draft, use its category
|
||||
if (opts.action === EDIT_SHARED_DRAFT && opts.destinationCategoryId) {
|
||||
topicProps.categoryId = opts.destinationCategoryId;
|
||||
}
|
||||
this.setProperties(topicProps);
|
||||
|
||||
this.store.find('post', opts.post.get('id')).then(function(post) {
|
||||
@ -591,21 +625,25 @@ const Composer = RestModel.extend({
|
||||
|
||||
// When you edit a post
|
||||
editPost(opts) {
|
||||
const post = this.get('post'),
|
||||
oldCooked = post.get('cooked'),
|
||||
self = this;
|
||||
let post = this.get('post');
|
||||
let oldCooked = post.get('cooked');
|
||||
let promise = Ember.RSVP.resolve();
|
||||
|
||||
let promise;
|
||||
|
||||
// Update the title if we've changed it, otherwise consider it a
|
||||
// successful resolved promise
|
||||
// Update the topic if we're editing the first post
|
||||
if (this.get('title') &&
|
||||
post.get('post_number') === 1 &&
|
||||
this.get('topic.details.can_edit')) {
|
||||
const topicProps = this.getProperties(Object.keys(_edit_topic_serializer));
|
||||
promise = Topic.update(this.get('topic'), topicProps);
|
||||
} else {
|
||||
promise = Ember.RSVP.resolve();
|
||||
|
||||
let topic = this.get('topic');
|
||||
|
||||
// If we're editing a shared draft, keep the original category
|
||||
if (this.get('action') === EDIT_SHARED_DRAFT) {
|
||||
let destinationCategoryId = topicProps.categoryId;
|
||||
promise = promise.then(() => topic.updateDestinationCategory(destinationCategoryId));
|
||||
topicProps.categoryId = topic.get('category.id');
|
||||
}
|
||||
promise = promise.then(() => Topic.update(topic, topicProps));
|
||||
}
|
||||
|
||||
const props = {
|
||||
@ -617,18 +655,18 @@ const Composer = RestModel.extend({
|
||||
|
||||
this.set('composeState', SAVING);
|
||||
|
||||
var rollback = throwAjaxError(function(){
|
||||
let rollback = throwAjaxError(() => {
|
||||
post.set('cooked', oldCooked);
|
||||
self.set('composeState', OPEN);
|
||||
this.set('composeState', OPEN);
|
||||
});
|
||||
|
||||
return promise.then(function() {
|
||||
return promise.then(() => {
|
||||
// rest model only sets props after it is saved
|
||||
post.set("cooked", props.cooked);
|
||||
return post.save(props).then(function(result) {
|
||||
self.clearState();
|
||||
return post.save(props).then(result => {
|
||||
this.clearState();
|
||||
return result;
|
||||
}).catch(function(error) {
|
||||
}).catch(error => {
|
||||
throw error;
|
||||
});
|
||||
}).catch(rollback);
|
||||
@ -867,6 +905,8 @@ Composer.reopenClass({
|
||||
|
||||
// The actions the composer can take
|
||||
CREATE_TOPIC,
|
||||
CREATE_SHARED_DRAFT,
|
||||
EDIT_SHARED_DRAFT,
|
||||
PRIVATE_MESSAGE,
|
||||
REPLY,
|
||||
EDIT,
|
||||
|
||||
@ -124,16 +124,21 @@ const Group = RestModel.extend({
|
||||
return mentionableLevel === '99';
|
||||
},
|
||||
|
||||
@computed("visibility_level")
|
||||
isPrivate(visibilityLevel) {
|
||||
return visibilityLevel !== 0;
|
||||
},
|
||||
|
||||
@observes("visibility_level", "canEveryoneMention")
|
||||
_updateAllowMembershipRequests() {
|
||||
if (this.get('visibility_level') !== 0 || !this.get('canEveryoneMention')) {
|
||||
if (this.get('isPrivate') || !this.get('canEveryoneMention')) {
|
||||
this.set ('allow_membership_requests', false);
|
||||
}
|
||||
},
|
||||
|
||||
@observes("visibility_level")
|
||||
_updatePublic() {
|
||||
if (this.get('visibility_level') !== 0) {
|
||||
if (this.get('isPrivate')) {
|
||||
this.set('public', false);
|
||||
this.set('allow_membership_requests', false);
|
||||
}
|
||||
@ -185,10 +190,7 @@ const Group = RestModel.extend({
|
||||
},
|
||||
|
||||
save() {
|
||||
const id = this.get('id');
|
||||
const url = this.get('is_group_owner') ? `/groups/${id}` : `/admin/groups/${id}`;
|
||||
|
||||
return ajax(url, {
|
||||
return ajax(`/groups/${this.get('id')}`, {
|
||||
type: "PUT",
|
||||
data: { group: this.asJSON() }
|
||||
});
|
||||
@ -250,10 +252,6 @@ Group.reopenClass({
|
||||
});
|
||||
},
|
||||
|
||||
find(name) {
|
||||
return ajax("/groups/" + name + ".json").then(result => Group.create(result.basic_group));
|
||||
},
|
||||
|
||||
loadMembers(name, offset, limit, params) {
|
||||
return ajax('/groups/' + name + '/members.json', {
|
||||
data: _.extend({
|
||||
|
||||
@ -107,6 +107,11 @@ export default Ember.Object.extend({
|
||||
var adapter = this.adapterFor(type);
|
||||
return adapter.find(this, type, findArgs, opts).then(result => {
|
||||
var hydrated = this._hydrateFindResults(result, type, findArgs, opts);
|
||||
|
||||
if (result.extras) {
|
||||
hydrated.set('extras', result.extras);
|
||||
}
|
||||
|
||||
if (adapter.cache) {
|
||||
const stale = adapter.findStale(this, type, findArgs, opts);
|
||||
hydrated = this._updateStale(stale, hydrated);
|
||||
|
||||
@ -24,7 +24,8 @@ const TagGroup = RestModel.extend({
|
||||
name: this.get('name'),
|
||||
tag_names: this.get('tag_names'),
|
||||
parent_tag_name: this.get('parent_tag_name') ? this.get('parent_tag_name') : undefined,
|
||||
one_per_topic: this.get('one_per_topic')
|
||||
one_per_topic: this.get('one_per_topic'),
|
||||
permissions: this.get('visible_only_to_staff') ? {"staff": "1"} : {"everyone": "1"}
|
||||
},
|
||||
type: isNew ? 'POST' : 'PUT'
|
||||
}).then(function(result) {
|
||||
|
||||
@ -488,6 +488,14 @@ const Topic = RestModel.extend({
|
||||
}).catch(popupAjaxError);
|
||||
},
|
||||
|
||||
updateDestinationCategory(categoryId) {
|
||||
this.set('destination_category_id', categoryId);
|
||||
return ajax(`/t/${this.get('id')}/shared-draft`, {
|
||||
method: 'PUT',
|
||||
data: { category_id: categoryId }
|
||||
});
|
||||
},
|
||||
|
||||
convertTopic(type) {
|
||||
return ajax(`/t/${this.get('id')}/convert-topic/${type}`, {type: 'PUT'}).then(() => {
|
||||
window.location.reload();
|
||||
|
||||
@ -218,6 +218,7 @@ const User = RestModel.extend({
|
||||
'website',
|
||||
'location',
|
||||
'name',
|
||||
'title',
|
||||
'locale',
|
||||
'custom_fields',
|
||||
'user_fields',
|
||||
@ -283,6 +284,12 @@ const User = RestModel.extend({
|
||||
}
|
||||
});
|
||||
|
||||
['muted_tags', 'tracked_tags', 'watched_tags', 'watching_first_post_tags'].forEach(prop => {
|
||||
if (fields === undefined || fields.includes(prop)) {
|
||||
data[prop] = this.get(prop) ? this.get(prop).join(',') : '';
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: We can remove this when migrated fully to rest model.
|
||||
this.set('isSaving', true);
|
||||
return ajax(userPath(`${this.get('username_lower')}.json`), {
|
||||
@ -563,6 +570,25 @@ const User = RestModel.extend({
|
||||
|
||||
canManageGroup(group) {
|
||||
return group.get('automatic') ? false : (this.get('admin') || group.get('is_group_owner'));
|
||||
},
|
||||
|
||||
@computed('groups.@each.title', 'badges.@each')
|
||||
availableTitles() {
|
||||
let titles = [];
|
||||
|
||||
_.each(this.get('groups'), group => {
|
||||
if (group.get('title')) {
|
||||
titles.push(group.get('title'));
|
||||
}
|
||||
});
|
||||
|
||||
_.each(this.get('badges'), badge => {
|
||||
if (badge.get('allow_title')) {
|
||||
titles.push(badge.get('name'));
|
||||
}
|
||||
});
|
||||
|
||||
return _.uniq(titles).sort();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -1,4 +1,11 @@
|
||||
import { default as computed } from "ember-addons/ember-computed-decorators";
|
||||
|
||||
export default Ember.Object.extend({
|
||||
postCountsPresent: Ember.computed.or('topic.unread', 'topic.displayNewPosts'),
|
||||
showBadges: Ember.computed.and('postBadgesEnabled', 'postCountsPresent')
|
||||
showBadges: Ember.computed.and('postBadgesEnabled', 'postCountsPresent'),
|
||||
|
||||
@computed
|
||||
newDotText() {
|
||||
return (this.currentUser && this.currentUser.trust_level > 0) ? "" : I18n.t('filters.new.lower_title');
|
||||
}
|
||||
});
|
||||
|
||||
@ -49,7 +49,9 @@ export default function() {
|
||||
this.route('categoryWithID', { path: '/c/:parentSlug/:slug/:id' });
|
||||
});
|
||||
|
||||
this.route('groups', { resetNamespace: true });
|
||||
this.route('groups', { resetNamespace: true }, function() {
|
||||
this.route("new", { path: "custom/new" });
|
||||
});
|
||||
|
||||
this.route('group', { path: '/groups/:name', resetNamespace: true }, function() {
|
||||
this.route('members');
|
||||
@ -60,8 +62,13 @@ export default function() {
|
||||
this.route('mentions');
|
||||
});
|
||||
|
||||
this.route('logs');
|
||||
this.route('edit');
|
||||
this.route('manage', function() {
|
||||
this.route('profile');
|
||||
this.route('membership');
|
||||
this.route('interaction');
|
||||
this.route('members');
|
||||
this.route('logs');
|
||||
});
|
||||
|
||||
this.route('messages', function() {
|
||||
this.route('inbox');
|
||||
|
||||
@ -17,7 +17,7 @@ const DiscourseRoute = Ember.Route.extend({
|
||||
refresh() {
|
||||
if (!this.refreshQueryWithoutTransition) { return this._super(); }
|
||||
|
||||
if (!this.router.router.activeTransition) {
|
||||
if (!this.router._routerMicrolib.activeTransition) {
|
||||
const controller = this.controller,
|
||||
model = controller.get('model'),
|
||||
params = this.controller.getProperties(Object.keys(this.queryParams));
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
export default Discourse.Route.extend({
|
||||
titleToken() {
|
||||
return I18n.t('groups.edit.title');
|
||||
},
|
||||
|
||||
model() {
|
||||
return this.modelFor('group');
|
||||
},
|
||||
|
||||
afterModel(group) {
|
||||
if (!this.currentUser || !this.currentUser.canManageGroup(group)) {
|
||||
this.transitionTo("group.members", group);
|
||||
}
|
||||
},
|
||||
|
||||
setupController(controller, model) {
|
||||
this.controllerFor('group-edit').setProperties({ model });
|
||||
this.controllerFor("group").set("showing", 'edit');
|
||||
model.findMembers();
|
||||
}
|
||||
});
|
||||
@ -1,3 +1,5 @@
|
||||
import showModal from 'discourse/lib/show-modal';
|
||||
|
||||
export default Discourse.Route.extend({
|
||||
titleToken() {
|
||||
return I18n.t('groups.members.title');
|
||||
@ -20,6 +22,14 @@ export default Discourse.Route.extend({
|
||||
},
|
||||
|
||||
actions: {
|
||||
showAddMembersModal() {
|
||||
showModal('group-add-members', { model: this.modelFor('group') });
|
||||
},
|
||||
|
||||
showBulkAddModal() {
|
||||
showModal('group-bulk-add', { model: this.modelFor('group') });
|
||||
},
|
||||
|
||||
didTransition() {
|
||||
this.controllerFor("group-index").set("filterInput", this._params.filter);
|
||||
return true;
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
export default Discourse.Route.extend({
|
||||
beforeModel() {
|
||||
this.transitionTo("group.manage.profile");
|
||||
}
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user