Version bump

This commit is contained in:
Neil Lalonde 2018-04-13 10:47:10 -04:00
commit 710af4b28c
663 changed files with 14810 additions and 6344 deletions

5
.gitignore vendored
View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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

View File

@ -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
View File

@ -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

View File

@ -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);
}
}
},

View File

@ -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 => {

View File

@ -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);
});
}

View File

@ -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);
}
}
);
}
}
});

View File

@ -1,4 +0,0 @@
export default Ember.Controller.extend({
adminGroupsBulk: Ember.inject.controller(),
bulkAddResponse: Ember.computed.alias('adminGroupsBulk.bulkAddResponse')
});

View File

@ -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);
});
}
}
});

View File

@ -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'}`;
}
});

View File

@ -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);
});
});
}
}
});

View File

@ -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'),

View File

@ -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'));

View File

@ -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'),

View File

@ -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');

View File

@ -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;
});
}

View File

@ -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;
}
});

View File

@ -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();
}
});

View File

@ -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 });
}
});

View File

@ -1,5 +0,0 @@
export default Discourse.Route.extend({
redirect: function() {
this.transitionTo("adminGroupsType", "custom");
}
});

View File

@ -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);
}
});

View File

@ -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')
});
}
});

View File

@ -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');

View File

@ -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'}}

View File

@ -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}}

View File

@ -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}}

View File

@ -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>

View File

@ -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}}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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}}

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -0,0 +1,7 @@
import RestAdapter from 'discourse/adapters/rest';
export default RestAdapter.extend({
appendQueryParams(path, findArgs) {
return this._super(path, findArgs, '.json');
},
});

View File

@ -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;

View File

@ -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({

View File

@ -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')

View File

@ -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);
}

View File

@ -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")

View File

@ -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'));
}
}

View File

@ -34,7 +34,7 @@ export default Ember.Component.extend({
if (flairHexColor) style += `color: #${flairHexColor};`;
return style;
return Ember.String.htmlSafe(style);
},
@computed('model.flairBackgroundHexColor')

View File

@ -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')

View File

@ -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));
}
},
});

View File

@ -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")

View File

@ -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")
});

View File

@ -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;

View File

@ -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'));
}
});

View File

@ -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;
}
});

View File

@ -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;
}
});

View File

@ -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));
},
});

View File

@ -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>");

View File

@ -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;

View File

@ -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() {

View File

@ -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);
}

View File

@ -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);
}
}));

View File

@ -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}">&nbsp;</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);
}

View File

@ -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

View File

@ -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);

View File

@ -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')

View File

@ -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')

View File

@ -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));
},
},
});

View File

@ -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);
});
}
}
});

View File

@ -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);
});
}
}
});

View File

@ -0,0 +1,3 @@
export default Ember.Controller.extend({
saving: null,
});

View File

@ -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;
},
});

View File

@ -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);
}
}
);
},
}
});

View File

@ -35,6 +35,10 @@ export default Ember.Controller.extend({
actions: {
loadMore() {
this.get('model').loadMore();
},
new() {
this.transitionToRoute("groups.new");
}
}
});

View File

@ -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));
},
}
});

View File

@ -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');

View File

@ -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);

View File

@ -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");

View File

@ -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);
},

View File

@ -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);
});

View File

@ -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();
};
});
}
});
}
}
};

View File

@ -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']
});
}
};

View File

@ -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");
}
},

View File

@ -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);
}
});

View File

@ -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;
}

View File

@ -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'
});

View File

@ -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,

View File

@ -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({

View File

@ -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);

View File

@ -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) {

View File

@ -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();

View File

@ -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();
}
});

View File

@ -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');
}
});

View File

@ -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');

View File

@ -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));

View File

@ -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();
}
});

View File

@ -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;

View File

@ -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