Version bump

This commit is contained in:
Neil Lalonde 2015-05-26 11:45:35 -04:00
commit 63f0bd0495
488 changed files with 11403 additions and 5911 deletions

22
.codeclimate.yml Normal file
View File

@ -0,0 +1,22 @@
languages:
Ruby: true
JavaScript: true
Python: false
PHP: false
exclude_paths:
- "app/assets/javascripts/defer/*"
- "app/assets/javascripts/discourse/lib/Markdown.Editor.js"
- "app/assets/javascripts/ember-addons/*"
- "lib/autospec/*"
- "lib/es6_module_transpiler/*"
- "lib/highlight_js/*"
- "lib/import/*"
- "lib/javascripts/*"
- "lib/tasks/*"
- "lib/*.js"
- "public/*"
- "script/*"
- "spec/*"
- "test/*"
- "vendor/*"

View File

@ -35,6 +35,7 @@
"exists",
"visible",
"invisible",
"asyncRender",
"selectDropdown",
"asyncTestDiscourse",
"fixture",

View File

@ -51,14 +51,14 @@ gem 'message_bus'
gem 'rails_multisite', path: 'vendor/gems/rails_multisite'
gem 'redcarpet', require: false
gem 'eventmachine'
gem 'fast_xs'
gem 'fast_xor'
# while we sort out https://github.com/sdsykes/fastimage/pull/46
gem 'fastimage_discourse', require: 'fastimage'
gem 'fog', '1.26.0', require: false
gem 'aws-sdk', require: false
gem 'excon', require: false
gem 'unf', require: false
gem 'email_reply_parser'
@ -96,7 +96,7 @@ gem 'sass'
gem 'sidekiq'
# for sidekiq web
gem 'sinatra', require: nil
gem 'sinatra', require: false
gem 'therubyracer'
gem 'thin', require: false
@ -124,7 +124,7 @@ group :test, :development do
gem 'certified', require: false
# later appears to break Fabricate(:topic, category: category)
gem 'fabrication', '2.9.8', require: false
gem 'qunit-rails'
gem 'discourse-qunit-rails', require: 'qunit-rails'
gem 'mocha', require: false
gem 'rb-fsevent', require: RUBY_PLATFORM =~ /darwin/i ? 'rb-fsevent' : false
gem 'rb-inotify', '~> 0.9', require: RUBY_PLATFORM =~ /linux/i ? 'rb-inotify' : false

View File

@ -6,7 +6,6 @@ PATH
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (2.2.8)
actionmailer (4.1.10)
actionpack (= 4.1.10)
actionview (= 4.1.10)
@ -41,6 +40,14 @@ GEM
activerecord (>= 2.3.0)
rake (~> 10.4.2, >= 10.4.2)
arel (5.0.1.20140414130214)
aws-sdk (2.0.45)
aws-sdk-resources (= 2.0.45)
aws-sdk-core (2.0.45)
builder (~> 3.0)
jmespath (~> 1.0)
multi_json (~> 1.0)
aws-sdk-resources (2.0.45)
aws-sdk-core (= 2.0.45)
babel-source (4.6.6)
babel-transpiler (0.6.0)
babel-source (>= 4.0, < 5)
@ -64,6 +71,8 @@ GEM
daemons (1.2.2)
debug_inspector (0.0.2)
diff-lcs (1.2.5)
discourse-qunit-rails (0.0.8)
railties
docile (1.1.5)
dotenv (1.0.2)
email_reply_parser (0.5.8)
@ -82,7 +91,7 @@ GEM
ember-source (1.11.3.1)
erubis (2.7.0)
eventmachine (1.0.7)
excon (0.44.4)
excon (0.45.3)
execjs (2.5.2)
exifr (1.1.3)
fabrication (2.9.8)
@ -99,79 +108,11 @@ GEM
fast_xs (0.8.0)
fastimage_discourse (1.6.6)
ffi (1.9.6)
fission (0.5.0)
CFPropertyList (~> 2.2)
flamegraph (0.1.0)
fast_stack
fog (1.26.0)
fog-atmos
fog-brightbox (~> 0.4)
fog-core (~> 1.27, >= 1.27.1)
fog-ecloud
fog-json
fog-profitbricks
fog-radosgw (>= 0.0.2)
fog-sakuracloud (>= 0.0.4)
fog-softlayer
fog-storm_on_demand
fog-terremark
fog-vmfusion
fog-voxel
fog-xml (~> 0.1.1)
ipaddress (~> 0.5)
nokogiri (~> 1.5, >= 1.5.11)
fog-atmos (0.1.0)
fog-core
fog-xml
fog-brightbox (0.7.1)
fog-core (~> 1.22)
fog-json
inflecto (~> 0.0.2)
fog-core (1.27.2)
builder
excon (~> 0.38)
formatador (~> 0.2)
mime-types
net-scp (~> 1.1)
net-ssh (>= 2.1.3)
fog-ecloud (0.0.2)
fog-core
fog-xml
fog-json (1.0.0)
multi_json (~> 1.0)
fog-profitbricks (0.0.1)
fog-core
fog-xml
nokogiri
fog-radosgw (0.0.3)
fog-core (>= 1.21.0)
fog-json
fog-xml (>= 0.0.1)
fog-sakuracloud (0.1.1)
fog-core
fog-json
fog-softlayer (0.3.26)
fog-core
fog-json
fog-storm_on_demand (0.1.0)
fog-core
fog-json
fog-terremark (0.0.3)
fog-core
fog-xml
fog-vmfusion (0.0.1)
fission
fog-core
fog-voxel (0.0.2)
fog-core
fog-xml
fog-xml (0.1.1)
fog-core
nokogiri (~> 1.5, >= 1.5.11)
foreman (0.77.0)
dotenv (~> 1.0.2)
thor (~> 0.19.1)
formatador (0.2.5)
fspath (2.0.6)
gctools (0.2.3)
given_core (3.5.4)
@ -193,8 +134,8 @@ GEM
progress (~> 3.0.0)
image_size (1.1.5)
in_threads (1.2.2)
inflecto (0.0.2)
ipaddress (0.8.0)
jmespath (1.0.2)
multi_json (~> 1.0)
jquery-rails (3.1.2)
railties (>= 3.0, < 5.0)
thor (>= 0.14, < 2.0)
@ -229,9 +170,6 @@ GEM
multi_xml (0.5.5)
multipart-post (2.0.0)
mustache (0.99.8)
net-scp (1.2.1)
net-ssh (>= 2.6.5)
net-ssh (2.9.2)
netrc (0.10.3)
nokogiri (1.6.6.2)
mini_portile (~> 0.6.0)
@ -270,7 +208,7 @@ GEM
omniauth-twitter (1.0.1)
multi_json (~> 1.3)
omniauth-oauth (~> 1.0)
onebox (1.5.18)
onebox (1.5.19)
moneta (~> 0.7)
multi_json (~> 1.7)
mustache (~> 0.99)
@ -291,8 +229,6 @@ GEM
pry (>= 0.9.10)
puma (2.11.1)
rack (>= 1.1, < 2.0)
qunit-rails (0.0.7)
railties
r2 (0.2.5)
rack (1.5.3)
rack-mini-profiler (0.9.3)
@ -459,15 +395,17 @@ DEPENDENCIES
actionpack-action_caching
active_model_serializers (~> 0.8.3)
annotate
aws-sdk
babel-transpiler
barber
better_errors
binding_of_caller
certified
discourse-qunit-rails
email_reply_parser
ember-rails
ember-source (= 1.11.3.1)
eventmachine
excon
fabrication (= 2.9.8)
fakeweb (~> 1.3.0)
fast_blank
@ -475,7 +413,6 @@ DEPENDENCIES
fast_xs
fastimage_discourse
flamegraph
fog (= 1.26.0)
foreman
gctools
handlebars-source (= 2.0.0)
@ -510,7 +447,6 @@ DEPENDENCIES
pry-nav
pry-rails
puma
qunit-rails
r2 (~> 0.2.5)
rack-mini-profiler
rack-protection

View File

@ -1,5 +1,4 @@
/*global ace:true */
/* global ace:true */
import loadScript from 'discourse/lib/load-script';
export default Ember.Component.extend({
@ -33,19 +32,21 @@ export default Ember.Component.extend({
const self = this;
loadScript("/javascripts/ace/ace.js", { scriptTag: true }).then(function() {
const editor = ace.edit(self.$('.ace')[0]);
ace.require(['ace/ace'], function(loadedAce) {
const editor = loadedAce.edit(self.$('.ace')[0]);
editor.setTheme("ace/theme/chrome");
editor.setShowPrintMargin(false);
editor.getSession().setMode("ace/mode/" + (self.get('mode')));
editor.on('change', function() {
self._skipContentChangeEvent = true;
self.set('content', editor.getSession().getValue());
self._skipContentChangeEvent = false;
editor.setTheme("ace/theme/chrome");
editor.setShowPrintMargin(false);
editor.getSession().setMode("ace/mode/" + self.get('mode'));
editor.on('change', function() {
self._skipContentChangeEvent = true;
self.set('content', editor.getSession().getValue());
self._skipContentChangeEvent = false;
});
self.$().data('editor', editor);
self._editor = editor;
});
self.$().data('editor', editor);
self._editor = editor;
});
}.on('didInsertElement')

View File

@ -1,3 +1,3 @@
export default Ember.Component.extend({
tagName: 'tbody'
tagName: 'tr'
});

View File

@ -0,0 +1,3 @@
export default Ember.Component.extend({
tagName: 'tr'
});

View File

@ -0,0 +1,3 @@
export default Ember.Component.extend({
tagName: 'tr'
});

View File

@ -1,9 +1,10 @@
import BufferedContent from 'discourse/mixins/buffered-content';
import ScrollTop from 'discourse/mixins/scroll-top';
import SiteSetting from 'admin/models/site-setting';
const CustomTypes = ['bool', 'enum', 'list', 'url_list'];
export default Ember.Component.extend(BufferedContent, Discourse.ScrollTop, {
export default Ember.Component.extend(BufferedContent, ScrollTop, {
classNameBindings: [':row', ':setting', 'setting.overridden', 'typeClass'],
content: Ember.computed.alias('setting'),
dirty: Discourse.computed.propertyNotEqual('buffered.value', 'setting.value'),
@ -65,8 +66,8 @@ export default Ember.Component.extend(BufferedContent, Discourse.ScrollTop, {
self.set('validationMessage', null);
self.commitBuffer();
}).catch(function(e) {
if (e.responseJSON && e.responseJSON.errors) {
self.set('validationMessage', e.responseJSON.errors[0]);
if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) {
self.set('validationMessage', e.jqXHR.responseJSON.errors[0]);
} else {
self.set('validationMessage', I18n.t('generic_error'));
}

View File

@ -1,3 +1,4 @@
import { popupAjaxError } from 'discourse/lib/ajax-error';
import BufferedContent from 'discourse/mixins/buffered-content';
export default Ember.ObjectController.extend(BufferedContent, {
@ -64,11 +65,9 @@ export default Ember.ObjectController.extend(BufferedContent, {
self.set('savingStatus', I18n.t('saved'));
}
}).catch(function(error) {
self.set('savingStatus', I18n.t('failed'));
self.send('saveError', error);
}).finally(function() {
}).catch(popupAjaxError).finally(function() {
self.set('saving', false);
self.set('savingStatus', '');
});
}
},

View File

@ -2,19 +2,24 @@ export default Ember.ArrayController.extend({
sortProperties: ["name"],
actions: {
emojiUploaded: function (emoji) {
this.pushObject(emoji);
emojiUploaded(emoji) {
this.pushObject(Em.Object.create(emoji));
},
destroy: function(emoji) {
var self = this;
return bootbox.confirm(I18n.t("admin.emoji.delete_confirm", { name: emoji.name }), I18n.t("no_value"), I18n.t("yes_value"), function (destroy) {
if (destroy) {
return Discourse.ajax("/admin/customize/emojis/" + emoji.name, { type: "DELETE" }).then(function() {
self.removeObject(emoji);
});
destroy(emoji) {
const self = this;
return bootbox.confirm(
I18n.t("admin.emoji.delete_confirm", { name: emoji.get("name") }),
I18n.t("no_value"),
I18n.t("yes_value"),
function(destroy) {
if (destroy) {
return Discourse.ajax("/admin/customize/emojis/" + emoji.get("name"), { type: "DELETE" }).then(function() {
self.removeObject(emoji);
});
}
}
});
);
}
}
});

View File

@ -1,3 +1,5 @@
import { popupAjaxError } from 'discourse/lib/ajax-error';
export default Em.ObjectController.extend({
needs: ['adminGroupsType'],
disableSave: false,
@ -73,7 +75,7 @@ export default Em.ObjectController.extend({
let promise = group.get("id") ? group.save() : group.create().then(() => groupsController.addObject(group));
promise.then(() => this.transitionToRoute("adminGroup", group))
.catch(e => bootbox.alert($.parseJSON(e.responseText).errors))
.catch(popupAjaxError)
.finally(() => this.set('disableSave', false));
},

View File

@ -2,7 +2,7 @@ import UserField from 'admin/models/user-field';
export default Ember.ArrayController.extend({
fieldTypes: null,
createDisabled: Em.computed.gte('model.length', 3),
createDisabled: Em.computed.gte('model.length', 20),
userFieldsDescription: function() {
return I18n.t('admin.user_fields.description');

View File

@ -9,7 +9,7 @@ export default ObjectController.extend(CanCheckEmails, {
showApproval: Discourse.computed.setting('must_approve_users'),
showBadges: Discourse.computed.setting('enable_badges'),
primaryGroupDirty: Discourse.computed.propertyNotEqual('originalPrimaryGroupId', 'primary_group_id'),
primaryGroupDirty: Discourse.computed.propertyNotEqual('originalPrimaryGroupId', 'model.primary_group_id'),
automaticGroups: function() {
return this.get("model.automaticGroups").map((g) => g.name).join(", ");
@ -17,7 +17,7 @@ export default ObjectController.extend(CanCheckEmails, {
userFields: function() {
const siteUserFields = this.site.get('user_fields'),
userFields = this.get('user_fields');
userFields = this.get('model.user_fields');
if (!Ember.isEmpty(siteUserFields)) {
return siteUserFields.map(function(uf) {
@ -26,7 +26,7 @@ export default ObjectController.extend(CanCheckEmails, {
});
}
return [];
}.property('user_fields.@each'),
}.property('model.user_fields.@each'),
actions: {
toggleTitleEdit() {
@ -67,16 +67,16 @@ export default ObjectController.extend(CanCheckEmails, {
return Discourse.ajax("/admin/users/" + this.get('id') + "/primary_group", {
type: 'PUT',
data: {primary_group_id: this.get('primary_group_id')}
data: {primary_group_id: this.get('model.primary_group_id')}
}).then(function () {
self.set('originalPrimaryGroupId', self.get('primary_group_id'));
self.set('originalPrimaryGroupId', self.get('model.primary_group_id'));
}).catch(function() {
bootbox.alert(I18n.t('generic_error'));
});
},
resetPrimaryGroup() {
this.set('primary_group_id', this.get('originalPrimaryGroupId'));
this.set('model.primary_group_id', this.get('originalPrimaryGroupId'));
},
regenerateApiKey() {

View File

@ -1,3 +1,5 @@
import { popupAjaxError } from 'discourse/lib/ajax-error';
const AdminUser = Discourse.User.extend({
customGroups: Em.computed.filter("groups", (g) => !g.automatic && Discourse.Group.create(g)),
@ -90,14 +92,7 @@ const AdminUser = Discourse.User.extend({
can_grant_admin: false,
can_revoke_admin: true
});
}).catch(function(e) {
let error;
if (e.responseJSON && e.responseJSON.error) {
error = e.responseJSON.error;
}
error = error || I18n.t('admin.user.grant_admin_failed', { error: "http: " + e.status + " - " + e.body });
bootbox.alert(error);
});
}).catch(popupAjaxError);
},
revokeModeration() {
@ -110,7 +105,7 @@ const AdminUser = Discourse.User.extend({
can_grant_moderation: true,
can_revoke_moderation: false
});
});
}).catch(popupAjaxError);
},
grantModeration() {
@ -123,14 +118,7 @@ const AdminUser = Discourse.User.extend({
can_grant_moderation: false,
can_revoke_moderation: true
});
}).catch(function(e) {
let error;
if (e.responseJSON && e.responseJSON.error) {
error = e.responseJSON.error;
}
error = error || I18n.t('admin.user.grant_moderation_failed', { error: "http: " + e.status + " - " + e.body });
bootbox.alert(error);
});
}).catch(popupAjaxError);
},
refreshBrowsers() {
@ -156,10 +144,6 @@ const AdminUser = Discourse.User.extend({
this.set('originalTrustLevel', this.get('trust_level'));
},
trustLevels: function() {
return Discourse.Site.currentProp('trustLevels');
}.property(),
dirty: Discourse.computed.propertyNotEqual('originalTrustLevel', 'trustLevel.id'),
saveTrustLevel() {
@ -243,7 +227,7 @@ const AdminUser = Discourse.User.extend({
type: 'POST',
data: { username_or_email: this.get('username') }
}).then(function() {
document.location = "/";
document.location = Discourse.getURL("/");
}).catch(function(e) {
if (e.status === 404) {
bootbox.alert(I18n.t('admin.impersonate.not_found'));
@ -321,9 +305,9 @@ const AdminUser = Discourse.User.extend({
}).then(function(data) {
if (data.success) {
if (data.username) {
document.location = "/admin/users/" + data.username;
document.location = Discourse.getURL("/admin/users/" + data.username);
} else {
document.location = "/admin/users/list/active";
document.location = Discourse.getURL("/admin/users/list/active");
}
} else {
bootbox.alert(I18n.t("admin.user.anonymize_failed"));
@ -386,7 +370,7 @@ const AdminUser = Discourse.User.extend({
if (/^\/admin\/users\/list\//.test(location)) {
document.location = location;
} else {
document.location = "/admin/users/list/active";
document.location = Discourse.getURL("/admin/users/list/active");
}
} else {
bootbox.alert(I18n.t("admin.user.delete_failed"));

View File

@ -1,13 +1,11 @@
<tr>
<td class="title">
{{#if report.icon}}
{{fa-icon report.icon}}
{{/if}}
<a {{bind-attr href="report.reportUrl"}}>{{report.title}}</a>
</td>
<td class="value">{{report.todayCount}}</td>
<td {{bind-attr class=":value report.yesterdayTrend"}} {{bind-attr title="report.yesterdayCountTitle"}}>{{report.yesterdayCount}} {{fa-icon "caret-up" class="up"}} {{fa-icon "caret-down" class="down"}}</td>
<td {{bind-attr class=":value report.sevenDayTrend"}} {{bind-attr title="report.sevenDayCountTitle"}}>{{report.lastSevenDaysCount}} {{fa-icon "caret-up" class="up"}} {{fa-icon "caret-down" class="down"}}</td>
<td {{bind-attr class=":value report.thirtyDayTrend"}} {{bind-attr title="report.thirtyDayCountTitle"}}>{{report.lastThirtyDaysCount}} {{fa-icon "caret-up" class="up"}} {{fa-icon "caret-down" class="down"}}</td>
<td class="value">{{report.total}}</td>
</tr>
<td class="title">
{{#if report.icon}}
{{fa-icon report.icon}}
{{/if}}
<a href="{{report.reportUrl}}">{{report.title}}</a>
</td>
<td class="value">{{report.todayCount}}</td>
<td {{bind-attr class=":value report.yesterdayTrend"}} title="{{report.yesterdayCountTitle}}">{{report.yesterdayCount}} {{fa-icon "caret-up" class="up"}} {{fa-icon "caret-down" class="down"}}</td>
<td {{bind-attr class=":value report.sevenDayTrend"}} title="{{report.sevenDayCountTitle}}">{{report.lastSevenDaysCount}} {{fa-icon "caret-up" class="up"}} {{fa-icon "caret-down" class="down"}}</td>
<td {{bind-attr class=":value report.thirtyDayTrend"}} title="{{report.thirtyDayCountTitle}}">{{report.lastThirtyDaysCount}} {{fa-icon "caret-up" class="up"}} {{fa-icon "caret-down" class="down"}}</td>
<td class="value">{{report.total}}</td>

View File

@ -0,0 +1,6 @@
<td class="title"><a href={{report.reportUrl}}>{{report.title}}</a></td>
<td class="value">{{report.todayCount}}</td>
<td class="value">{{report.yesterdayCount}}</td>
<td class="value">{{report.sevenDaysAgoCount}}</td>
<td class="value">{{report.thirtyDaysAgoCount}}</td>
<td class="value"></td>

View File

@ -0,0 +1,6 @@
<td class="title">{{report.title}}</td>
<td class="value">{{#link-to 'adminUsersList.show' 'newuser'}}{{value-at-tl report.data level="0"}}{{/link-to}}</td>
<td class="value">{{#link-to 'adminUsersList.show' 'basic'}}{{value-at-tl report.data level="1"}}{{/link-to}}</td>
<td class="value">{{#link-to 'adminUsersList.show' 'regular'}}{{value-at-tl report.data level="2"}}{{/link-to}}</td>
<td class="value">{{#link-to 'adminUsersList.show' 'leader'}}{{value-at-tl report.data level="3"}}{{/link-to}}</td>
<td class="value">{{#link-to 'adminUsersList.show' 'elder'}}{{value-at-tl report.data level="4"}}{{/link-to}}</td>

View File

@ -15,9 +15,11 @@
<th>4</th>
</tr>
</thead>
{{#unless loading}}
{{ render 'admin/templates/reports/trust_levels_report' users_by_trust_level tagName="tbody"}}
{{/unless}}
<tbody>
{{#unless loading}}
{{admin-report-trust-level-counts report=users_by_trust_level}}
{{/unless}}
</tbody>
</table>
</div>
@ -50,16 +52,18 @@
<th>{{i18n 'admin.dashboard.reports.all'}}</th>
</tr>
</thead>
{{#unless loading}}
{{ render 'admin/templates/reports/per_day_counts_report' visits tagName="tbody"}}
{{admin-report-counts report=signups}}
{{admin-report-counts report=topics}}
{{admin-report-counts report=posts}}
{{admin-report-counts report=likes}}
{{admin-report-counts report=flags}}
{{admin-report-counts report=bookmarks}}
{{admin-report-counts report=emails}}
{{/unless}}
<tbody>
{{#unless loading}}
{{admin-report-per-day-counts report=visits}}
{{admin-report-counts report=signups}}
{{admin-report-counts report=topics}}
{{admin-report-counts report=posts}}
{{admin-report-counts report=likes}}
{{admin-report-counts report=flags}}
{{admin-report-counts report=bookmarks}}
{{admin-report-counts report=emails}}
{{/unless}}
</tbody>
</table>
</div>
@ -75,12 +79,14 @@
<th>{{i18n 'admin.dashboard.reports.all'}}</th>
</tr>
</thead>
{{#unless loading}}
{{admin-report-counts report=page_view_anon_reqs}}
{{admin-report-counts report=page_view_logged_in_reqs}}
{{admin-report-counts report=page_view_crawler_reqs}}
{{admin-report-counts report=page_view_total_reqs}}
{{/unless}}
<tbody>
{{#unless loading}}
{{admin-report-counts report=page_view_anon_reqs}}
{{admin-report-counts report=page_view_logged_in_reqs}}
{{admin-report-counts report=page_view_crawler_reqs}}
{{admin-report-counts report=page_view_total_reqs}}
{{/unless}}
</tbody>
</table>
</div>
@ -97,13 +103,15 @@
<th>{{i18n 'admin.dashboard.reports.all'}}</th>
</tr>
</thead>
{{#unless loading}}
{{admin-report-counts report=user_to_user_private_messages}}
{{admin-report-counts report=system_private_messages}}
{{admin-report-counts report=notify_moderators_private_messages}}
{{admin-report-counts report=notify_user_private_messages}}
{{admin-report-counts report=moderator_warning_private_messages}}
{{/unless}}
<tbody>
{{#unless loading}}
{{admin-report-counts report=user_to_user_private_messages}}
{{admin-report-counts report=system_private_messages}}
{{admin-report-counts report=notify_moderators_private_messages}}
{{admin-report-counts report=notify_user_private_messages}}
{{admin-report-counts report=moderator_warning_private_messages}}
{{/unless}}
</tbody>
</table>
</div>
@ -118,14 +126,14 @@
</tr>
</thead>
<tbody>
{{#unless loading}}
{{#unless loading}}
<tr>
<td>{{i18n 'admin.dashboard.uploads'}}</td>
<td>{{disk_space.uploads_used}} ({{i18n 'admin.dashboard.space_free' size=disk_space.uploads_free}})</td>
<td><a href="/admin/backups">{{i18n 'admin.dashboard.backups'}}</a></td>
<td>{{disk_space.backups_used}} ({{i18n 'admin.dashboard.space_free' size=disk_space.backups_free}})</td>
</tr>
{{/unless}}
{{/unless}}
</tbody>
</table>
</div>
@ -144,20 +152,22 @@
<th>{{i18n 'admin.dashboard.reports.all'}}</th>
</tr>
</thead>
{{#unless loading}}
{{admin-report-counts report=http_2xx_reqs}}
{{admin-report-counts report=http_3xx_reqs}}
{{admin-report-counts report=http_4xx_reqs}}
{{admin-report-counts report=http_5xx_reqs}}
{{admin-report-counts report=http_background_reqs}}
{{admin-report-counts report=http_total_reqs}}
{{/unless}}
<tbody>
{{#unless loading}}
{{admin-report-counts report=http_2xx_reqs}}
{{admin-report-counts report=http_3xx_reqs}}
{{admin-report-counts report=http_4xx_reqs}}
{{admin-report-counts report=http_5xx_reqs}}
{{admin-report-counts report=http_background_reqs}}
{{admin-report-counts report=http_total_reqs}}
{{/unless}}
</tbody>
</table>
</div>
{{else}}
<div class="dashboard-stats">
<a href {{action showTrafficReport}}>{{i18n 'admin.dashboard.show_traffic_report'}}</a>
</div>
<div class="dashboard-stats">
<a href {{action 'showTrafficReport'}}>{{i18n 'admin.dashboard.show_traffic_report'}}</a>
</div>
{{/if}}
{{/unless}}
</div>

View File

@ -10,13 +10,13 @@
<br/>
<table>
<thead>
<tr>
<th>{{i18n "admin.plugins.name"}}</th>
<th>{{i18n "admin.plugins.version"}}</th>
</tr>
</thead>
<tbody>
<thead>
<tr>
<th>{{i18n "admin.plugins.name"}}</th>
<th>{{i18n "admin.plugins.version"}}</th>
</tr>
</thead>
{{#each plugin in controller}}
<tr>
<td>

View File

@ -1,8 +0,0 @@
<tr>
<td class="title"><a {{bind-attr href="reportUrl"}}>{{title}}</a></td>
<td class="value">{{todayCount}}</td>
<td class="value">{{yesterdayCount}}</td>
<td class="value">{{sevenDaysAgoCount}}</td>
<td class="value">{{thirtyDaysAgoCount}}</td>
<td class="value"></td>
</tr>

View File

@ -1,8 +0,0 @@
<tr>
<td class="title">{{title}}</td>
<td class="value">{{#link-to 'adminUsersList.show' 'newuser'}}{{value-at-tl data level="0"}}{{/link-to}}</td>
<td class="value">{{#link-to 'adminUsersList.show' 'basic'}}{{value-at-tl data level="1"}}{{/link-to}}</td>
<td class="value">{{#link-to 'adminUsersList.show' 'regular'}}{{value-at-tl data level="2"}}{{/link-to}}</td>
<td class="value">{{#link-to 'adminUsersList.show' 'leader'}}{{value-at-tl data level="3"}}{{/link-to}}</td>
<td class="value">{{#link-to 'adminUsersList.show' 'elder'}}{{value-at-tl data level="4"}}{{/link-to}}</td>
</tr>

View File

@ -1,12 +1,12 @@
<section {{bind-attr class=":details active::not-activated"}}>
<section class="details {{unless model.active 'not-activated'}}">
<div class='user-controls'>
{{#if active}}
{{#if model.active}}
{{#link-to 'user' model class="btn"}}
{{fa-icon "user"}}
{{i18n 'admin.user.show_public_profile'}}
{{/link-to}}
{{#if can_impersonate}}
{{#if model.can_impersonate}}
<button class='btn btn-danger' {{action "impersonate" target="content"}} title="{{i18n 'admin.impersonate.help'}}">
{{fa-icon "crosshairs"}}
{{i18n 'admin.impersonate.title'}}
@ -23,7 +23,7 @@
<div class='display-row username'>
<div class='field'>{{i18n 'user.username.title'}}</div>
<div class='value'>{{username}}</div>
<div class='value'>{{model.username}}</div>
<div class='controls'>
{{#link-to 'preferences.username' model class="btn"}}
{{fa-icon "pencil"}}
@ -36,11 +36,11 @@
<div class='display-row email'>
<div class='field'>{{i18n 'user.email.title'}}</div>
<div class='value'>
{{#unless active}}
{{#unless model.active}}
<div class='controls'>{{i18n 'admin.users.not_verified'}}</div>
{{/unless}}
{{#if email}}
<a href="mailto:{{unbound email}}">{{email}}</a>
{{#if model.email}}
<a href="mailto:{{unbound model.email}}">{{model.email}}</a>
{{else}}
<button class="btn" title="{{i18n 'admin.users.check_email.title'}}" {{action "checkEmail" this}}>{{fa-icon "envelope-o"}} {{i18n 'admin.users.check_email.text'}}</button>
{{/if}}
@ -50,8 +50,8 @@
<div class='display-row associations'>
<div class='field'>{{i18n 'user.associated_accounts'}}</div>
<div class='value'>
{{#if associated_accounts}}
{{associated_accounts}}
{{#if model.associated_accounts}}
{{model.associated_accounts}}
{{else}}
<button class="btn" title="{{i18n 'admin.users.check_email.title'}}" {{action "checkEmail" this}}>{{fa-icon "envelope-o"}} {{i18n 'admin.users.check_email.text'}}</button>
{{/if}}
@ -68,9 +68,9 @@
<div class='field'>{{i18n 'user.title.title'}}</div>
<div class='value'>
{{#if editingTitle}}
{{text-field value=title autofocus="autofocus"}}
{{text-field value=model.title autofocus="autofocus"}}
{{else}}
<span {{action "toggleTitleEdit"}}>{{title}}&nbsp;</span>
<span {{action "toggleTitleEdit"}}>{{model.title}}&nbsp;</span>
{{/if}}
</div>
<div class='controls'>
@ -85,23 +85,23 @@
<div class='display-row'>
<div class='field'>{{i18n 'user.ip_address.title'}}</div>
<div class='value'>{{ip_address}}</div>
<div class='value'>{{model.ip_address}}</div>
<div class='controls'>
{{#if currentUser.staff}}
<button class='btn' {{action "refreshBrowsers" target="content"}}>
{{i18n 'admin.user.refresh_browsers'}}
</button>
{{ip-lookup ip=ip_address userId=id}}
{{ip-lookup ip=model.ip_address userId=model.id}}
{{/if}}
</div>
</div>
<div class='display-row'>
<div class='field'>{{i18n 'user.registration_ip_address.title'}}</div>
<div class='value'>{{registration_ip_address}}</div>
<div class='value'>{{model.registration_ip_address}}</div>
<div class='controls'>
{{#if currentUser.staff}}
{{ip-lookup ip=registration_ip_address userId=id}}
{{ip-lookup ip=model.registration_ip_address userId=model.id}}
{{/if}}
</div>
</div>
@ -110,10 +110,10 @@
<div class='display-row'>
<div class='field'>{{i18n 'admin.badges.title'}}</div>
<div class='value'>
{{i18n 'badges.badge_count' count=badge_count}}
{{i18n 'badges.badge_count' count=model.badge_count}}
</div>
<div class='controls'>
{{#link-to 'adminUser.badges' this class="btn"}}{{fa-icon "certificate"}}{{i18n 'admin.badges.edit_badges'}}{{/link-to}}
{{#link-to 'adminUser.badges' model class="btn"}}{{fa-icon "certificate"}}{{i18n 'admin.badges.edit_badges'}}{{/link-to}}
</div>
</div>
{{/if}}
@ -169,26 +169,26 @@
<div class='display-row'>
<div class='field'>{{i18n 'admin.users.active'}}</div>
<div class='value'>
{{#if active}}
{{#if model.active}}
{{i18n 'yes_value'}}
{{else}}
{{i18n 'no_value'}}
{{/if}}
</div>
<div class='controls'>
{{#if active}}
{{#if can_deactivate}}
{{#if model.active}}
{{#if model.can_deactivate}}
<button class='btn' {{action "deactivate" target="content"}}>{{i18n 'admin.user.deactivate_account'}}</button>
{{i18n 'admin.user.deactivate_explanation'}}
{{/if}}
{{else}}
{{#if can_send_activation_email}}
{{#if model.can_send_activation_email}}
<button class='btn' {{action "sendActivationEmail" target="content"}}>
{{fa-icon "envelope"}}
{{i18n 'admin.user.send_activation_email'}}
</button>
{{/if}}
{{#if can_activate}}
{{#if model.can_activate}}
<button class='btn' {{action "activate" target="content"}}>
{{fa-icon "check"}}
{{i18n 'admin.user.activate'}}
@ -200,9 +200,9 @@
<div class='display-row'>
<div class='field'>{{i18n 'admin.api.key'}}</div>
{{#if api_key}}
{{#if model.api_key}}
<div class='long-value'>
{{api_key.key}}
{{model.api_key.key}}
{{d-button action="regenerateApiKey" icon="undo" label="admin.api.regenerate"}}
{{d-button action="revokeApiKey" icon="times" label="admin.api.revoke"}}
</div>
@ -218,15 +218,15 @@
<div class='display-row'>
<div class='field'>{{i18n 'admin.user.admin'}}</div>
<div class='value'>{{admin}}</div>
<div class='value'>{{model.admin}}</div>
<div class='controls'>
{{#if can_revoke_admin}}
{{#if model.can_revoke_admin}}
<button class='btn' {{action "revokeAdmin" target="content"}}>
{{fa-icon "shield"}}
{{i18n 'admin.user.revoke_admin'}}
</button>
{{/if}}
{{#if can_grant_admin}}
{{#if model.can_grant_admin}}
<button class='btn' {{action "grantAdmin" target="content"}}>
{{fa-icon "shield"}}
{{i18n 'admin.user.grant_admin'}}
@ -237,15 +237,15 @@
<div class='display-row'>
<div class='field'>{{i18n 'admin.user.moderator'}}</div>
<div class='value'>{{moderator}}</div>
<div class='value'>{{model.moderator}}</div>
<div class='controls'>
{{#if can_revoke_moderation}}
{{#if model.can_revoke_moderation}}
<button class='btn' {{action "revokeModeration" target="content"}}>
{{fa-icon "shield"}}
{{i18n 'admin.user.revoke_moderation'}}
</button>
{{/if}}
{{#if can_grant_moderation}}
{{#if model.can_grant_moderation}}
<button class='btn' {{action "grantModeration" target="content"}}>
{{fa-icon "shield"}}
{{i18n 'admin.user.grant_moderation'}}
@ -257,8 +257,8 @@
<div class='display-row'>
<div class='field'>{{i18n 'trust_level'}}</div>
<div class="value">
{{combo-box content=trustLevels value=trust_level nameProperty="detailedName"}}
{{#if dirty}}
{{combo-box content=site.trustLevels value=model.trust_level nameProperty="detailedName"}}
{{#if model.dirty}}
<div>
<button class='btn ok no-text' {{action "saveTrustLevel" target="content"}}>{{fa-icon "check"}}</button>
<button class='btn cancel no-text' {{action "restoreTrustLevel" target="content"}}>{{fa-icon "times"}}</button>
@ -273,17 +273,17 @@
<i title='{{i18n 'admin.user.trust_level_unlocked_tip'}}' class='fa fa-unlock'></i> <button class="btn" {{action "lockTrustLevel" true target="model"}}>{{i18n 'admin.user.lock_trust_level'}}</button>
{{/if}}
{{/if}}
{{#if tl3Requirements}}
{{#link-to 'adminUser.tl3Requirements' this class="btn"}}{{i18n 'admin.user.trust_level_3_requirements'}}{{/link-to}}
{{#if model.tl3Requirements}}
{{#link-to 'adminUser.tl3Requirements' model class="btn"}}{{i18n 'admin.user.trust_level_3_requirements'}}{{/link-to}}
{{/if}}
</div>
</div>
<div {{bind-attr class=":display-row isSuspended:highlight-danger"}}>
<div {{bind-attr class=":display-row model.isSuspended:highlight-danger"}}>
<div class='field'>{{i18n 'admin.user.suspended'}}</div>
<div class='value'>{{isSuspended}}</div>
<div class='value'>{{model.isSuspended}}</div>
<div class='controls'>
{{#if isSuspended}}
{{#if model.isSuspended}}
<button class='btn btn-danger' {{action "unsuspend" target="content"}}>
{{fa-icon "ban"}}
{{i18n 'admin.user.unsuspend'}}
@ -291,7 +291,7 @@
{{suspendDuration}}
{{i18n 'admin.user.suspended_explanation'}}
{{else}}
{{#if canSuspend}}
{{#if model.canSuspend}}
<button class='btn btn-danger' {{action "showSuspendModal" this}}>
{{fa-icon "ban"}}
{{i18n 'admin.user.suspend'}}
@ -302,7 +302,7 @@
</div>
</div>
{{#if isSuspended}}
{{#if model.isSuspended}}
<div class='display-row highlight-danger'>
<div class='field'>{{i18n 'admin.user.suspended_by'}}</div>
<div class='value'>
@ -316,11 +316,11 @@
</div>
{{/if}}
<div {{bind-attr class=":display-row blocked:highlight-danger"}}>
<div {{bind-attr class=":display-row model.blocked:highlight-danger"}}>
<div class='field'>{{i18n 'admin.user.blocked'}}</div>
<div class='value'>{{blocked}}</div>
<div class='value'>{{model.blocked}}</div>
<div class='controls'>
{{#if blocked}}
{{#if model.blocked}}
<button class='btn' {{action "unblock" target="content"}}>
{{fa-icon "thumbs-o-up"}}
{{i18n 'admin.user.unblock'}}
@ -342,12 +342,12 @@
<div class='display-row'>
<div class='field'>{{i18n 'admin.groups.custom'}}</div>
<div class='value'>
{{admin-group-selector selected=customGroups available=availableGroups}}
{{admin-group-selector selected=model.customGroups available=availableGroups}}
</div>
<div class='controls'>
{{#if customGroups}}
{{#if model.customGroups}}
{{i18n 'admin.groups.primary'}}
{{combo-box content=customGroups value=primary_group_id nameProperty="name" none="admin.groups.no_primary"}}
{{combo-box content=model.customGroups value=model.primary_group_id nameProperty="name" none="admin.groups.no_primary"}}
{{/if}}
{{#if primaryGroupDirty}}
{{d-button icon="check" class="ok no-text" action="savePrimaryGroup"}}
@ -363,71 +363,71 @@
<div class='display-row'>
<div class='field'>{{i18n 'created'}}</div>
<div class='value'>{{{created_at_age}}}</div>
<div class='value'>{{{model.created_at_age}}}</div>
</div>
<div class='display-row'>
<div class='field'>{{i18n 'admin.users.last_emailed'}}</div>
<div class='value'>{{{last_emailed_age}}}</div>
<div class='value'>{{{model.last_emailed_age}}}</div>
</div>
<div class='display-row'>
<div class='field'>{{i18n 'last_seen'}}</div>
<div class='value'>{{{last_seen_age}}}</div>
<div class='value'>{{{model.last_seen_age}}}</div>
</div>
<div class='display-row'>
<div class='field'>{{i18n 'admin.user.like_count'}}</div>
<div class='value'>{{like_given_count}} / {{like_count}}</div>
<div class='value'>{{model.like_given_count}} / {{model.like_count}}</div>
</div>
<div class='display-row'>
<div class='field'>{{i18n 'admin.user.topics_entered'}}</div>
<div class='value'>{{topics_entered}}</div>
<div class='value'>{{model.topics_entered}}</div>
</div>
<div class='display-row'>
<div class='field'>{{i18n 'admin.user.post_count'}}</div>
<div class='value'>{{post_count}}</div>
<div class='value'>{{model.post_count}}</div>
<div class='controls'>
{{#if can_delete_all_posts}}
{{#if post_count}}
{{#if model.can_delete_all_posts}}
{{#if model.post_count}}
<button class='btn btn-danger' {{action "deleteAllPosts" target="content"}}>
{{fa-icon "trash-o"}}
{{i18n 'admin.user.delete_all_posts'}}
</button>
{{/if}}
{{else}}
{{deleteAllPostsExplanation}}
{{model.deleteAllPostsExplanation}}
{{/if}}
</div>
</div>
<div class='display-row'>
<div class='field'>{{i18n 'admin.user.posts_read_count'}}</div>
<div class='value'>{{posts_read_count}}</div>
<div class='value'>{{model.posts_read_count}}</div>
</div>
<div class='display-row'>
<div class='field'>{{i18n 'admin.user.warnings_received_count'}}</div>
<div class='value'>{{warnings_received_count}}</div>
<div class='value'>{{model.warnings_received_count}}</div>
</div>
<div class='display-row'>
<div class='field'>{{i18n 'admin.user.flags_given_received_count'}}</div>
<div class='value'>{{flags_given_count}} / {{flags_received_count}}</div>
<div class='value'>{{model.flags_given_count}} / {{model.flags_received_count}}</div>
</div>
<div class='display-row'>
<div class='field'>{{i18n 'admin.user.private_topics_count'}}</div>
<div class='value'>{{private_topics_count}}</div>
<div class='value'>{{model.private_topics_count}}</div>
</div>
<div class='display-row'>
<div class='field'>{{i18n 'admin.user.time_read'}}</div>
<div class='value'>{{{time_read}}}</div>
<div class='value'>{{{model.time_read}}}</div>
</div>
<div class='display-row'>
<div class='field'>{{i18n 'user.invited.days_visited'}}</div>
<div class='value'>{{{days_visited}}}</div>
<div class='value'>{{{model.days_visited}}}</div>
</div>
</section>
{{#if single_sign_on_record}}
{{#if model.single_sign_on_record}}
<section class='details'>
<h1>{{i18n 'admin.user.sso.title'}}</h1>
{{#with single_sign_on_record}}
{{#with model.single_sign_on_record}}
<div class='display-row'>
<div class='field'>{{i18n 'admin.user.sso.external_id'}}</div>
<div class='value'>{{external_id}}</div>
@ -455,28 +455,28 @@
<section>
<hr/>
<div class="pull-right">
{{#unless anonymizeForbidden}}
{{#unless model.anonymizeForbidden}}
{{d-button label="admin.user.anonymize"
icon="exclamation-triangle"
class="btn-danger"
disabled=anonymizeForbidden
disabled=model.anonymizeForbidden
action="anonymize"}}
{{/unless}}
{{#unless deleteForbidden}}
{{#unless model.deleteForbidden}}
{{d-button label="admin.user.delete"
icon="exclamation-triangle"
class="btn-danger"
disabled=deleteForbidden
disabled=model.deleteForbidden
action="destroy"}}
{{/unless}}
</div>
{{#if deleteExplanation}}
{{#if model.deleteExplanation}}
<div class="clearfix"></div>
<br/>
<div class="pull-right">
{{fa-icon "exclamation-triangle"}} {{deleteExplanation}}
{{fa-icon "exclamation-triangle"}} {{model.deleteExplanation}}
</div>
{{/if}}
</section>

View File

@ -1 +1,3 @@
export default Ember.View.extend(Discourse.ScrollTop);
import ScrollTop from 'discourse/mixins/scroll-top';
export default Ember.View.extend(ScrollTop);

View File

@ -1,4 +1,6 @@
export default Ember.View.extend(Discourse.ScrollTop, {
import ScrollTop from 'discourse/mixins/scroll-top';
export default Ember.View.extend(ScrollTop, {
_scrollOnModelChange: function() {
this._scrollTop();
}.observes('controller.model.id')

View File

@ -1,36 +1,21 @@
import UploadMixin from 'discourse/mixins/upload';
import UploadMixin from "discourse/mixins/upload";
export default Em.Component.extend(UploadMixin, {
type: 'avatar',
tagName: 'span',
type: "avatar",
tagName: "span",
imageIsNotASquare: false,
uploadUrl: Discourse.computed.url('username', '/users/%@/preferences/user_image'),
uploadButtonText: function() {
return this.get("uploading") ?
I18n.t("uploading") :
I18n.t("user.change_avatar.upload_picture");
return this.get("uploading") ? I18n.t("uploading") : I18n.t("user.change_avatar.upload_picture");
}.property("uploading"),
uploadDone(data) {
// display a warning whenever the image is not a square
this.set("imageIsNotASquare", data.result.width !== data.result.height);
// in order to be as much responsive as possible, we're cheating a bit here
// indeed, the server gives us back the url to the file we've just uploaded
// often, this file is not a square, so we need to crop it properly
// this will also capture the first frame of animated avatars when they're not allowed
Discourse.Utilities.cropAvatar(data.result.url, data.files[0].type).then(avatarTemplate => {
this.set("uploadedAvatarTemplate", avatarTemplate);
// indicates the users is using an uploaded avatar (must happen after cropping, otherwise
// we will attempt to load an invalid avatar and cache a redirect to old one, uploadedAvatarTemplate
// trumps over custom avatar upload id)
this.set("custom_avatar_upload_id", data.result.upload_id);
uploadDone(upload) {
this.setProperties({
imageIsNotASquare: upload.width !== upload.height,
uploadedAvatarTemplate: upload.url,
custom_avatar_upload_id: upload.id,
});
// the upload is now done
this.sendAction("done");
}
});

View File

@ -17,7 +17,7 @@ export default DiscourseContainerView.extend({
tagName: 'button',
attributeBindings: ['style', 'title'],
classNames: ['colorpicker'].concat( isUsed ? ['used-color'] : ['unused-color'] ),
style: 'background-color: #' + color + ';',
style: ('background-color: #' + color + ';').htmlSafe(),
title: isUsed ? I18n.t("category.already_used") : null,
click: function() {
self.set("value", color);

View File

@ -1,4 +1,4 @@
import UploadMixin from 'discourse/mixins/upload';
import UploadMixin from "discourse/mixins/upload";
export default Em.Component.extend(UploadMixin, {
type: "emoji",
@ -11,9 +11,9 @@ export default Em.Component.extend(UploadMixin, {
return Ember.isBlank(this.get("name")) ? {} : { name: this.get("name") };
}.property("name"),
uploadDone: function (data) {
uploadDone(upload) {
this.set("name", null);
this.sendAction("done", data.result);
this.sendAction("done", upload);
}
});

View File

@ -1,31 +1,21 @@
import UploadMixin from 'discourse/mixins/upload';
import UploadMixin from "discourse/mixins/upload";
export default Em.Component.extend(UploadMixin, {
classNames: ["image-uploader"],
backgroundStyle: function() {
var imageUrl = this.get('imageUrl');
const imageUrl = this.get("imageUrl");
if (Em.isNone(imageUrl)) { return; }
return ("background-image: url(" + imageUrl + ")").htmlSafe();
}.property("imageUrl"),
return "background-image: url(" + imageUrl + ")";
}.property('imageUrl'),
uploadDone: function(data) {
this.set('imageUrl', data.result.url);
uploadDone(upload) {
this.set("imageUrl", upload.url);
},
actions: {
trash: function() {
this.set('imageUrl', null);
// Do we want to signal the delete to the server right away?
if (this.get('instantDelete')) {
Discourse.ajax(this.get('uploadUrl'), {
type: 'DELETE',
data: { image_type: this.get('type') }
}).then(null, function() {
bootbox.alert(I18n.t('generic_error'));
});
}
trash() {
this.set("imageUrl", null);
}
}
});

View File

@ -62,7 +62,7 @@ export default Em.Component.extend(StringBuffer, {
buffer.push('</ul>');
}
if ((links.length <= MAX_SHOWN || !collapsed) && this.get('canReplyAsNewTopic')) {
if (this.get('canReplyAsNewTopic')) {
buffer.push("<a href class='reply-new'>" + iconHTML('plus') + I18n.t('post.reply_as_new_topic') + "</a>");
}
},

View File

@ -1,10 +1,12 @@
export default Ember.Component.extend({
_parse: function() {
this.$().find('hr').remove();
this.$().ellipsis();
Ember.run.next(null, () => {
this.$().find('hr').remove();
this.$().ellipsis();
});
}.on('didInsertElement'),
render: function(buffer) {
render(buffer) {
buffer.push(this.get('text'));
}
});

View File

@ -191,8 +191,9 @@ export default Ember.ObjectController.extend(Presence, {
// for now handle a very narrow use case
// if we are replying to a topic AND not on the topic pop the window up
if (!force && composer.get('replyingToTopic')) {
const topic = this.get('model.topic');
if (!topic || topic.get('id') !== composer.get('topic.id'))
const currentTopic = this.get('controllers.topic.model');
if (!currentTopic || currentTopic.get('id') !== composer.get('topic.id'))
{
const message = I18n.t("composer.posting_not_on_topic");
@ -202,12 +203,12 @@ export default Ember.ObjectController.extend(Presence, {
"link": true
}];
if (topic) {
if (currentTopic) {
buttons.push({
"label": I18n.t("composer.reply_here") + "<br/><div class='topic-title overflow-ellipsis'>" + Handlebars.Utils.escapeExpression(topic.get('title')) + "</div>",
"label": I18n.t("composer.reply_here") + "<br/><div class='topic-title overflow-ellipsis'>" + Handlebars.Utils.escapeExpression(currentTopic.get('title')) + "</div>",
"class": "btn btn-reply-here",
"callback": function() {
composer.set('topic', topic);
composer.set('topic', currentTopic);
composer.set('post', null);
self.save(true);
}

View File

@ -5,7 +5,10 @@ import { categoryBadgeHTML } from 'discourse/helpers/category-link';
// Modal for editing / creating a category
export default ObjectController.extend(ModalFunctionality, {
foregroundColors: ['FFFFFF', '000000'],
categoryUploadUrl: '/category/uploads',
editingPermissions: false,
selectedTab: null,
saving: false,
deleting: false,
parentCategories: function() {
return Discourse.Category.list().filter(function (c) {
@ -15,31 +18,31 @@ export default ObjectController.extend(ModalFunctionality, {
// We can change the parent if there are no children
subCategories: function() {
if (Em.isEmpty(this.get('id'))) { return null; }
return Discourse.Category.list().filterBy('parent_category_id', this.get('id'));
if (Em.isEmpty(this.get('model.id'))) { return null; }
return Discourse.Category.list().filterBy('parent_category_id', this.get('model.id'));
}.property('model.id'),
canSelectParentCategory: Em.computed.not('isUncategorizedCategory'),
canSelectParentCategory: Em.computed.not('model.isUncategorizedCategory'),
onShow: function() {
onShow() {
this.changeSize();
this.titleChanged();
},
changeSize: function() {
if (this.present('description')) {
if (this.present('model.description')) {
this.set('controllers.modal.modalClass', 'edit-category-modal full');
} else {
this.set('controllers.modal.modalClass', 'edit-category-modal small');
}
}.observes('description'),
}.observes('model.description'),
title: function() {
if (this.get('id')) {
if (this.get('model.id')) {
return I18n.t("category.edit_long") + " : " + this.get('model.name');
}
return I18n.t("category.create") + (this.get('model.name') ? (" : " + this.get('model.name')) : '');
}.property('id', 'model.name'),
}.property('model.id', 'model.name'),
titleChanged: function() {
this.set('controllers.modal.title', this.get('title'));
@ -47,10 +50,10 @@ export default ObjectController.extend(ModalFunctionality, {
disabled: function() {
if (this.get('saving') || this.get('deleting')) return true;
if (!this.get('name')) return true;
if (!this.get('color')) return true;
if (!this.get('model.name')) return true;
if (!this.get('model.color')) return true;
return false;
}.property('saving', 'name', 'color', 'deleting'),
}.property('saving', 'model.name', 'model.color', 'deleting'),
emailInEnabled: Discourse.computed.setting('email_in'),
@ -59,80 +62,82 @@ export default ObjectController.extend(ModalFunctionality, {
}.property('disabled', 'saving', 'deleting'),
colorStyle: function() {
return "background-color: #" + (this.get('color')) + "; color: #" + (this.get('text_color')) + ";";
}.property('color', 'text_color'),
return "background-color: #" + this.get('model.color') + "; color: #" + this.get('model.text_color') + ";";
}.property('model.color', 'model.text_color'),
categoryBadgePreview: function() {
var c = Discourse.Category.create({
name: this.get('categoryName'),
color: this.get('color'),
text_color: this.get('text_color'),
parent_category_id: parseInt(this.get('parent_category_id'),10),
read_restricted: this.get('model.read_restricted')
const model = this.get('model');
const c = Discourse.Category.create({
name: model.get('categoryName'),
color: model.get('color'),
text_color: model.get('text_color'),
parent_category_id: parseInt(model.get('parent_category_id'),10),
read_restricted: model.get('read_restricted')
});
return categoryBadgeHTML(c, {link: false});
}.property('parent_category_id', 'categoryName', 'color', 'text_color'),
}.property('model.parent_category_id', 'model.categoryName', 'model.color', 'model.text_color'),
// background colors are available as a pipe-separated string
backgroundColors: function() {
var categories = Discourse.Category.list();
const categories = Discourse.Category.list();
return Discourse.SiteSettings.category_colors.split("|").map(function(i) { return i.toUpperCase(); }).concat(
categories.map(function(c) { return c.color.toUpperCase(); }) ).uniq();
}.property('Discourse.SiteSettings.category_colors'),
usedBackgroundColors: function() {
var categories = Discourse.Category.list();
const categories = Discourse.Category.list();
var currentCat = this.get('model');
const currentCat = this.get('model');
return categories.map(function(c) {
// If editing a category, don't include its color:
return (currentCat.get('id') && currentCat.get('color').toUpperCase() === c.color.toUpperCase()) ? null : c.color.toUpperCase();
}, this).compact();
}.property('id', 'color'),
}.property('model.id', 'model.color'),
categoryName: function() {
var name = this.get('name') || "";
const name = this.get('name') || "";
return name.trim().length > 0 ? name : I18n.t("preview");
}.property('name'),
buttonTitle: function() {
if (this.get('saving')) return I18n.t("saving");
if (this.get('isUncategorizedCategory')) return I18n.t("save");
return (this.get('id') ? I18n.t("category.save") : I18n.t("category.create"));
}.property('saving', 'id'),
if (this.get('model.isUncategorizedCategory')) return I18n.t("save");
return (this.get('model.id') ? I18n.t("category.save") : I18n.t("category.create"));
}.property('saving', 'model.id'),
deleteButtonTitle: function() {
return I18n.t('category.delete');
}.property(),
showDescription: function() {
return !this.get('isUncategorizedCategory') && this.get('id');
}.property('isUncategorizedCategory', 'id'),
return !this.get('model.isUncategorizedCategory') && this.get('model.id');
}.property('model.isUncategorizedCategory', 'model.id'),
showPositionInput: Discourse.computed.setting('fixed_category_positions'),
actions: {
showCategoryTopic: function() {
showCategoryTopic() {
this.send('closeModal');
Discourse.URL.routeTo(this.get('topic_url'));
Discourse.URL.routeTo(this.get('model.topic_url'));
return false;
},
editPermissions: function(){
editPermissions() {
this.set('editingPermissions', true);
},
addPermission: function(group, permission_id){
this.get('model').addPermission({group_name: group + "", permission: Discourse.PermissionType.create({id: permission_id})});
addPermission(group, id) {
this.get('model').addPermission({group_name: group + "",
permission: Discourse.PermissionType.create({id})});
},
removePermission: function(permission){
removePermission(permission) {
this.get('model').removePermission(permission);
},
saveCategory: function() {
var self = this,
saveCategory() {
const self = this,
model = this.get('model'),
parentCategory = Discourse.Category.list().findBy('id', parseInt(model.get('parent_category_id'), 10));
@ -155,8 +160,8 @@ export default ObjectController.extend(ModalFunctionality, {
});
},
deleteCategory: function() {
var self = this;
deleteCategory() {
const self = this;
this.set('deleting', true);
this.send('hideModal');

View File

@ -3,6 +3,7 @@ import ObjectController from 'discourse/controllers/object';
// The basic controller for a group
export default ObjectController.extend({
counts: null,
showing: null,
// It would be nice if bootstrap marked action lists as selected when their links
// were 'active' not the `li` tags.

View File

@ -5,15 +5,13 @@ export default Ember.ObjectController.extend({
loadMore() {
if (this.get("loading")) { return; }
// we've reached the end
if (this.get("members.length") >= this.get("user_count")) { return; }
if (this.get("model.members.length") >= this.get("user_count")) { return; }
this.set("loading", true);
const self = this;
Discourse.Group.loadMembers(this.get("name"), this.get("members.length"), this.get("limit")).then(function (result) {
self.get("members").addObjects(result.members.map(member => Discourse.User.create(member)));
self.setProperties({
Discourse.Group.loadMembers(this.get("name"), this.get("model.members.length"), this.get("limit")).then(result => {
this.get("model.members").addObjects(result.members.map(member => Discourse.User.create(member)));
this.setProperties({
loading: false,
user_count: result.meta.total,
limit: result.meta.limit,

View File

@ -1,21 +0,0 @@
export default Em.ObjectController.extend({
byName: function() {
var result = "",
longName = this.get('user_long_name'),
title = this.get('user_title');
if (!Em.isEmpty(longName)) {
result += longName;
}
if (!Em.isEmpty(title)) {
if (result.length > 0) {
result += ", ";
}
result += title;
}
return result;
}.property()
});

View File

@ -16,12 +16,13 @@ export default ObjectController.extend(Presence, ModalFunctionality, {
disabled: function() {
if (this.get('saving')) return true;
if (this.blank('emailOrUsername')) return true;
const emailOrUsername = this.get('emailOrUsername').trim();
// when inviting to forum, email must be valid
if (!this.get('invitingToTopic') && !Discourse.Utilities.emailValid(this.get('emailOrUsername'))) return true;
if (!this.get('invitingToTopic') && !Discourse.Utilities.emailValid(emailOrUsername)) return true;
// normal users (not admin) can't invite users to private topic via email
if (!this.get('isAdmin') && this.get('isPrivateTopic') && Discourse.Utilities.emailValid(this.get('emailOrUsername'))) return true;
if (!this.get('isAdmin') && this.get('isPrivateTopic') && Discourse.Utilities.emailValid(emailOrUsername)) return true;
// when invting to private topic via email, group name must be specified
if (this.get('isPrivateTopic') && this.blank('groupNames') && Discourse.Utilities.emailValid(this.get('emailOrUsername'))) return true;
if (this.get('isPrivateTopic') && this.blank('groupNames') && Discourse.Utilities.emailValid(emailOrUsername)) return true;
if (this.get('model.details.can_invite_to')) return false;
return false;
}.property('isAdmin', 'emailOrUsername', 'invitingToTopic', 'isPrivateTopic', 'groupNames', 'saving'),
@ -135,7 +136,7 @@ export default ObjectController.extend(Presence, ModalFunctionality, {
this.setProperties({ saving: true, error: false });
return this.get('model').createInvite(this.get('emailOrUsername'), groupNames).then(result => {
return this.get('model').createInvite(this.get('emailOrUsername').trim(), groupNames).then(result => {
this.setProperties({ saving: false, finished: true });
if (!this.get('invitingToTopic')) {
Discourse.Invite.findInvitedBy(Discourse.User.current()).then(invite_model => {

View File

@ -1,5 +1,6 @@
import ObjectController from 'discourse/controllers/object';
import CanCheckEmails from 'discourse/mixins/can-check-emails';
import { popupAjaxError } from 'discourse/lib/ajax-error';
export default ObjectController.extend(CanCheckEmails, {
@ -10,8 +11,10 @@ export default ObjectController.extend(CanCheckEmails, {
editHistoryVisible: Discourse.computed.setting('edit_history_visible_to_public'),
selectedCategories: function(){
return [].concat(this.get("watchedCategories"), this.get("trackedCategories"), this.get("mutedCategories"));
}.property("watchedCategories", "trackedCategories", "mutedCategories"),
return [].concat(this.get("model.watchedCategories"),
this.get("model.trackedCategories"),
this.get("model.mutedCategories"));
}.property("model.watchedCategories", "model.trackedCategories", "model.mutedCategories"),
// By default we haven't saved anything
saved: false,
@ -21,7 +24,7 @@ export default ObjectController.extend(CanCheckEmails, {
userFields: function() {
let siteUserFields = this.site.get('user_fields');
if (!Ember.isEmpty(siteUserFields)) {
const userFields = this.get('user_fields');
const userFields = this.get('model.user_fields');
// Staff can edit fields that are not `editable`
if (!this.get('currentUser.staff')) {
@ -32,7 +35,7 @@ export default ObjectController.extend(CanCheckEmails, {
return Ember.Object.create({ value, field });
});
}
}.property('user_fields.@each.value'),
}.property('model.user_fields.@each.value'),
cannotDeleteAccount: Em.computed.not('can_delete_account'),
deleteDisabled: Em.computed.or('saving', 'deleting', 'cannotDeleteAccount'),
@ -84,19 +87,19 @@ export default ObjectController.extend(CanCheckEmails, {
{ name: I18n.t('user.new_topic_duration.last_here'), value: -2 }],
saveButtonText: function() {
return this.get('saving') ? I18n.t('saving') : I18n.t('save');
}.property('saving'),
return this.get('model.isSaving') ? I18n.t('saving') : I18n.t('save');
}.property('model.isSaving'),
imageUploadUrl: Discourse.computed.url('username', '/users/%@/preferences/user_image'),
passwordProgress: null,
actions: {
save() {
const self = this;
this.setProperties({ saving: true, saved: false });
this.set('saved', false);
const model = this.get('model'),
userFields = this.get('userFields');
const model = this.get('model');
const userFields = this.get('userFields');
// Update the user fields
if (!Ember.isEmpty(userFields)) {
@ -111,22 +114,12 @@ export default ObjectController.extend(CanCheckEmails, {
// Cook the bio for preview
model.set('name', this.get('newNameInput'));
return model.save().then(function() {
// model was saved
self.set('saving', false);
if (Discourse.User.currentProp('id') === model.get('id')) {
Discourse.User.currentProp('name', model.get('name'));
}
self.set('bio_cooked', Discourse.Markdown.cook(Discourse.Markdown.sanitize(self.get('bio_raw'))));
model.set('bio_cooked', Discourse.Markdown.cook(Discourse.Markdown.sanitize(model.get('bio_raw'))));
self.set('saved', true);
}, function(error) {
// model failed to save
self.set('saving', false);
if (error && error.responseText) {
alert($.parseJSON(error.responseText).errors[0]);
} else {
alert(I18n.t('generic_error'));
}
});
}).catch(popupAjaxError);
},
changePassword() {

View File

@ -2,6 +2,7 @@ export default Ember.ObjectController.extend({
needs: ['topic'],
progressPosition: null,
expanded: false,
toPostIndex: null,
actions: {
toggleExpansion: function(opts) {
@ -50,11 +51,11 @@ export default Ember.ObjectController.extend({
},
jumpTop: function() {
this.jumpTo(this.get('firstPostUrl'));
this.jumpTo(this.get('model.firstPostUrl'));
},
jumpBottom: function() {
this.jumpTo(this.get('lastPostUrl'));
this.jumpTo(this.get('model.lastPostUrl'));
}
},
@ -83,8 +84,8 @@ export default Ember.ObjectController.extend({
jumpBottomDisabled: function() {
return this.get('progressPosition') >= this.get('model.postStream.filteredPostsCount') ||
this.get('progressPosition') >= this.get('highest_post_number');
}.property('model.postStream.filteredPostsCount', 'highest_post_number', 'progressPosition'),
this.get('progressPosition') >= this.get('model.highest_post_number');
}.property('model.postStream.filteredPostsCount', 'model.highest_post_number', 'progressPosition'),
hideProgress: function() {
if (!this.get('model.postStream.loaded')) return true;
@ -95,14 +96,14 @@ export default Ember.ObjectController.extend({
hugeNumberOfPosts: function() {
return (this.get('model.postStream.filteredPostsCount') >= Discourse.SiteSettings.short_progress_text_threshold);
}.property('highest_post_number'),
}.property('model.highest_post_number'),
jumpToBottomTitle: function() {
if (this.get('hugeNumberOfPosts')) {
return I18n.t('topic.progress.jump_bottom_with_number', {post_number: this.get('highest_post_number')});
return I18n.t('topic.progress.jump_bottom_with_number', {post_number: this.get('model.highest_post_number')});
} else {
return I18n.t('topic.progress.jump_bottom');
}
}.property('hugeNumberOfPosts', 'highest_post_number')
}.property('hugeNumberOfPosts', 'model.highest_post_number')
});

View File

@ -16,6 +16,7 @@ export default ObjectController.extend(SelectedPostsCount, BufferedContent, {
loadedAllPosts: false,
enteredAt: null,
firstPostExpanded: false,
retrying: false,
maxTitleLength: Discourse.computed.setting('max_topic_title_length'),

View File

@ -35,57 +35,60 @@ export default Ember.Controller.extend({
show(username, postId, target) {
// XSS protection (should be encapsulated)
username = username.toString().replace(/[^A-Za-z0-9_]/g, "");
const url = "/users/" + username;
// Don't show on mobile
if (Discourse.Mobile.mobileView) {
const url = "/users/" + username;
Discourse.URL.routeTo(url);
return;
}
const currentUsername = this.get('username'),
wasVisible = this.get('visible'),
post = this.get('viewingTopic') && postId ? this.get('postStream').findLoadedPost(postId) : null;
this.setProperties({ avatar: null, post: post, username: username });
// If we click the avatar again, close it (unless its diff element on the screen).
if (target === this.get('cardTarget') && wasVisible) {
this.setProperties({ visible: false, username: null, cardTarget: null });
return;
}
wasVisible = this.get('visible'),
previousTarget = this.get('cardTarget'),
post = this.get('viewingTopic') && postId ? this.get('postStream').findLoadedPost(postId) : null;
if (username === currentUsername && this.get('userLoading') === username) {
// debounce
return;
}
this.set('topicPostCount', null);
if (wasVisible) {
this.close();
if (target === previousTarget) {
return; // Same target, close it without loading the new user card
}
}
this.setProperties({ user: null, userLoading: username, cardTarget: target });
this.setProperties({ username, userLoading: username, cardTarget: target, post });
const args = { stats: false };
args.include_post_count_for = this.get('controllers.topic.id');
const self = this;
return Discourse.User.findByUsername(username, args).then(function(user) {
args.include_post_count_for = this.get('controllers.topic.model.id');
return Discourse.User.findByUsername(username, args).then((user) => {
if (user.topic_post_count) {
self.set('topicPostCount', user.topic_post_count[args.include_post_count_for]);
this.set('topicPostCount', user.topic_post_count[args.include_post_count_for]);
}
user = Discourse.User.create(user);
self.setProperties({ user, avatar: user, visible: true});
self.appEvents.trigger('usercard:shown');
}).catch(function(error) {
self.close();
this.setProperties({ user, avatar: user, visible: true });
}).catch((error) => {
this.close();
throw error;
}).finally(function() {
self.set('userLoading', null);
}).finally(() => {
this.set('userLoading', null);
});
},
close() {
this.setProperties({ visible: false, cardTarget: null });
this.setProperties({
visible: false,
user: null,
username: null,
avatar: null,
userLoading: null,
cardTarget: null,
post: null,
topicPostCount: null
});
},
actions: {

View File

@ -2,12 +2,12 @@ import registerUnbound from 'discourse/helpers/register-unbound';
registerUnbound('link-domain', function(link) {
if (link) {
var internal = Em.get(link, 'internal'),
hasTitle = (!Em.isEmpty(Em.get(link, 'title')));
if (hasTitle && !internal) {
var domain = Em.get(link, 'domain');
if (!Em.isEmpty(domain)) {
var s = domain.split('.');
const hasTitle = (!Ember.isEmpty(Em.get(link, 'title')));
if (hasTitle) {
let domain = Ember.get(link, 'domain');
if (!Ember.isEmpty(domain)) {
const s = domain.split('.');
domain = s[s.length-2] + "." + s[s.length-1];
return new Handlebars.SafeString("<span class='domain'>" + domain + "</span>");
}

View File

@ -45,6 +45,6 @@ export default {
inject(app, 'currentUser', 'component', 'route', 'controller');
app.register('message-bus:main', window.MessageBus, { instantiate: false });
inject(app, 'messageBus', 'route', 'controller', 'view');
inject(app, 'messageBus', 'route', 'controller', 'view', 'component');
}
};

View File

@ -29,6 +29,8 @@ function extractError(error) {
if (parsedJSON) {
if (parsedJSON.errors && parsedJSON.errors.length > 0) {
parsedError = parsedJSON.errors[0];
} else if (parsedJSON.error) {
parsedError = parsedJSON.error;
} else if (parsedJSON.failed) {
parsedError = parsedJSON.message;
}

View File

@ -7,7 +7,6 @@ let lastAction = -1;
const focusTrackerKey = "focus-tracker";
const idleThresholdTime = 1000 * 10; // 10 seconds
let notificationTagName; // "discourse-notification-popup-" + Discourse.SiteSettings.title;
// Called from an initializer
function init(messageBus) {
@ -25,8 +24,6 @@ function init(messageBus) {
return;
}
if (!("Notification" in window)) {
Em.Logger.info('Discourse desktop notifications are disabled - not supported by browser');
return;
@ -55,8 +52,6 @@ function init(messageBus) {
// This function is only called if permission was granted
function setupNotifications() {
notificationTagName = "discourse-notification-popup-" + Discourse.SiteSettings.title;
window.addEventListener("storage", function(e) {
// note: This event only fires when other tabs setItem()
const key = e.key;
@ -108,13 +103,14 @@ function onNotification(data) {
const notificationBody = data.excerpt;
const notificationIcon = Discourse.SiteSettings.logo_small_url || Discourse.SiteSettings.logo_url;
const notificationTag = "discourse-notification-" + Discourse.SiteSettings.title + "-" + data.topic_id;
requestPermission().then(function() {
// This shows the notification!
const notification = new Notification(notificationTitle, {
body: notificationBody,
icon: notificationIcon,
tag: notificationTagName
tag: notificationTag
});
function clickEventHandler() {

View File

@ -20,7 +20,7 @@ const popstateCallbacks = [];
*/
const DiscourseLocation = Ember.Object.extend({
init: function() {
init() {
set(this, 'location', get(this, 'location') || window.location);
this.initState();
},
@ -32,11 +32,11 @@ const DiscourseLocation = Ember.Object.extend({
@method initState
*/
initState: function() {
initState() {
set(this, 'history', get(this, 'history') || window.history);
var url = this.formatURL(this.getURL()),
loc = get(this, 'location');
let url = this.formatURL(this.getURL());
const loc = get(this, 'location');
if (loc && loc.hash) {
url += loc.hash;
@ -60,15 +60,15 @@ const DiscourseLocation = Ember.Object.extend({
@method getURL
*/
getURL: function() {
var rootURL = (Discourse.BaseUri === undefined ? "/" : Discourse.BaseUri),
location = get(this, 'location'),
url = location.pathname;
getURL() {
const location = get(this, 'location');
let rootURL = (Discourse.BaseUri === undefined ? "/" : Discourse.BaseUri);
let url = location.pathname;
rootURL = rootURL.replace(/\/$/, '');
url = url.replace(rootURL, '');
var search = location.search || '';
const search = location.search || '';
url += search;
return url;
@ -82,8 +82,8 @@ const DiscourseLocation = Ember.Object.extend({
@method setURL
@param path {String}
*/
setURL: function(path) {
var state = this.getState();
setURL(path) {
const state = this.getState();
path = this.formatURL(path);
if (state && state.path !== path) {
@ -100,8 +100,8 @@ const DiscourseLocation = Ember.Object.extend({
@method replaceURL
@param path {String}
*/
replaceURL: function(path) {
var state = this.getState();
replaceURL(path) {
const state = this.getState();
path = this.formatURL(path);
if (state && state.path !== path) {
@ -114,11 +114,11 @@ const DiscourseLocation = Ember.Object.extend({
Get the current `history.state`
Polyfill checks for native browser support and falls back to retrieving
from a private _historyState variable
from a private _historyState constiable
@method getState
*/
getState: function() {
getState() {
return supportsHistoryState ? get(this, 'history').state : this._historyState;
},
@ -130,8 +130,8 @@ const DiscourseLocation = Ember.Object.extend({
@method pushState
@param path {String}
*/
pushState: function(path) {
var state = { path: path };
pushState(path) {
const state = { path: path };
// store state if browser doesn't support `history.state`
if (!supportsHistoryState) {
@ -152,8 +152,8 @@ const DiscourseLocation = Ember.Object.extend({
@method replaceState
@param path {String}
*/
replaceState: function(path) {
var state = { path: path };
replaceState(path) {
const state = { path: path };
// store state if browser doesn't support `history.state`
if (!supportsHistoryState) {
@ -175,8 +175,8 @@ const DiscourseLocation = Ember.Object.extend({
@method onUpdateURL
@param callback {Function}
*/
onUpdateURL: function(callback) {
var guid = Ember.guidFor(this),
onUpdateURL(callback) {
const guid = Ember.guidFor(this),
self = this;
Ember.$(window).on('popstate.ember-location-'+guid, function() {
@ -185,7 +185,7 @@ const DiscourseLocation = Ember.Object.extend({
popstateFired = true;
if (self.getURL() === self._previousURL) { return; }
}
var url = self.getURL();
const url = self.getURL();
popstateCallbacks.forEach(function(cb) {
cb(url);
});
@ -201,8 +201,8 @@ const DiscourseLocation = Ember.Object.extend({
@method formatURL
@param url {String}
*/
formatURL: function(url) {
var rootURL = get(this, 'rootURL');
formatURL(url) {
let rootURL = get(this, 'rootURL');
if (url !== '') {
rootURL = rootURL.replace(/\/$/, '');
@ -215,8 +215,8 @@ const DiscourseLocation = Ember.Object.extend({
return rootURL + url;
},
willDestroy: function() {
var guid = Ember.guidFor(this);
willDestroy() {
const guid = Ember.guidFor(this);
Ember.$(window).off('popstate.ember-location-'+guid);
}
@ -230,7 +230,7 @@ const DiscourseLocation = Ember.Object.extend({
**/
CloakedCollectionView.reopen({
_watchForPopState: function() {
var self = this,
const self = this,
cb = function() {
// Sam: This is a hack, but a very important one
// Due to the way we use replace state the back button works strangely

View File

@ -4,7 +4,8 @@ var PATH_BINDINGS = {
'g n': '/new',
'g u': '/unread',
'g c': '/categories',
'g t': '/top'
'g t': '/top',
'g b': '/bookmarks'
},
SELECTED_POST_BINDINGS = {

View File

@ -1,18 +1,23 @@
/* global assetPath */
const _loaded = {};
const _loading = {};
function loadWithTag(path, cb) {
const head = document.getElementsByTagName('head')[0];
let s = document.createElement('script');
s.src = path;
if (Ember.Test) { Ember.Test.pendingAjaxRequests++; }
head.appendChild(s);
s.onload = s.onreadystatechange = function(_, abort) {
if (Ember.Test) { Ember.Test.pendingAjaxRequests--; }
if (abort || !s.readyState || s.readyState === "loaded" || s.readyState === "complete") {
s = s.onload = s.onreadystatechange = null;
if (!abort) { cb(); }
if (!abort) {
Ember.run(null, cb);
}
}
};
}
@ -25,9 +30,20 @@ export default function loadScript(url, opts) {
// If we already loaded this url
if (_loaded[url]) { return resolve(); }
if (_loading[url]) { return _loading[url].then(resolve);}
var done;
_loading[url] = new Ember.RSVP.Promise(function(_done){
done = _done;
});
_loading[url].then(function(){
delete _loading[url];
});
const cb = function() {
_loaded[url] = true;
done();
resolve();
};

View File

@ -89,7 +89,7 @@ Discourse.URL = Ember.Object.createWithMixins({
Em.run.schedule('afterRender', function() {
var $elem = $(id);
if ($elem.length === 0) {
$elem = $("[name=" + id.replace('#', ''));
$elem = $("[name='" + id.replace('#', '') + "']");
}
if ($elem.length > 0) {
$('html,body').scrollTop($elem.offset().top - $('header').height() - 15);
@ -230,7 +230,7 @@ Discourse.URL = Ember.Object.createWithMixins({
var self = this;
postStream.refresh(opts).then(function() {
topicController.setProperties({
currentPost: closest,
'model.currentPost': closest,
enteredAt: new Date().getTime().toString()
});
var closestPost = postStream.closestPostForPostNumber(closest),

View File

@ -1,10 +1,3 @@
/**
General utility functions
@class Utilities
@namespace Discourse
@module Discourse
**/
Discourse.Utilities = {
translateSize: function(size) {
@ -195,22 +188,10 @@ Discourse.Utilities = {
return true;
},
/**
Determine whether all file extensions are authorized.
@method authorizesAllExtensions
**/
authorizesAllExtensions: function() {
return Discourse.SiteSettings.authorized_extensions.indexOf("*") >= 0;
},
/**
Check the extension of the file against the list of authorized extensions
@method isAuthorizedUpload
@param {File} file The file we want to upload
**/
isAuthorizedUpload: function(file) {
if (file && file.name) {
var extensions = _.chain(Discourse.SiteSettings.authorized_extensions.split("|"))
@ -222,11 +203,6 @@ Discourse.Utilities = {
return false;
},
/**
List the authorized extension for display
@method authorizedExtensions
**/
authorizedExtensions: function() {
return _.chain(Discourse.SiteSettings.authorized_extensions.split("|"))
.reject(function(extension) { return extension.indexOf("*") >= 0; })
@ -235,12 +211,6 @@ Discourse.Utilities = {
.join(", ");
},
/**
Get the markdown template for an upload (either an image or an attachment)
@method getUploadMarkdown
@param {Upload} upload The upload we want the markdown from
**/
getUploadMarkdown: function(upload) {
if (Discourse.Utilities.isAnImage(upload.original_filename)) {
return '<img src="' + upload.url + '" width="' + upload.width + '" height="' + upload.height + '">';
@ -249,12 +219,6 @@ Discourse.Utilities = {
}
},
/**
Check whether the path is refering to an image
@method isAnImage
@param {String} path The path
**/
isAnImage: function(path) {
return (/\.(png|jpe?g|gif|bmp|tiff?|svg|webp)$/i).test(path);
},
@ -264,11 +228,6 @@ Discourse.Utilities = {
(/(png|jpe?g|gif|bmp|tiff?|svg|webp)/i).test(Discourse.Utilities.authorizedExtensions());
},
/**
Determines whether we allow attachments or not
@method allowsAttachments
**/
allowsAttachments: function() {
return Discourse.Utilities.authorizesAllExtensions() ||
!(/((png|jpe?g|gif|bmp|tiff?|svg|webp)(,\s)?)+$/i).test(Discourse.Utilities.authorizedExtensions());
@ -296,60 +255,14 @@ Discourse.Utilities = {
}
return;
}
} else if (data.errors && data.errors.length > 0) {
bootbox.alert(data.errors.join("\n"));
return;
}
// otherwise, display a generic error message
bootbox.alert(I18n.t('post.errors.upload'));
},
/**
Crop an image to be used as avatar.
Simulate the "centered square thumbnail" generation done server-side.
Uses only the first frame of animated gifs when they are disabled.
@method cropAvatar
@param {String} url The url of the avatar
@param {String} fileType The file type of the uploaded file
@returns {Promise} a promise that will eventually be the cropped avatar.
**/
cropAvatar: function(url, fileType) {
if (Discourse.SiteSettings.allow_animated_avatars && fileType === "image/gif") {
// can't crop animated gifs... let the browser stretch the gif
return Ember.RSVP.resolve(url);
} else {
return new Ember.RSVP.Promise(function(resolve) {
var image = document.createElement("img");
image.crossOrigin = 'Anonymous';
// this event will be fired as soon as the image is loaded
image.onload = function(e) {
var img = e.target;
// computes the dimension & position (x, y) of the largest square we can fit in the image
var width = img.width, height = img.height, dimension, center, x, y;
if (width <= height) {
dimension = width;
center = height / 2;
x = 0;
y = center - (dimension / 2);
} else {
dimension = height;
center = width / 2;
x = center - (dimension / 2);
y = 0;
}
// set the size of the canvas to the maximum available size for avatars (browser will take care of downsizing the image)
var canvas = document.createElement("canvas");
var size = Discourse.Utilities.getRawSize(Discourse.Utilities.translateSize("huge"));
canvas.height = canvas.width = size;
// draw the image into the canvas
canvas.getContext("2d").drawImage(img, x, y, dimension, dimension, 0, 0, size, size);
// retrieve the image from the canvas
resolve(canvas.toDataURL(fileType));
};
// launch the onload event
image.src = url;
});
}
},
defaultHomepage: function() {
// the homepage is the first item of the 'top_menu' site setting
return Discourse.SiteSettings.top_menu.split("|")[0].split(",")[0];

View File

@ -0,0 +1,12 @@
function scrollTop() {
if (Discourse.URL.isJumpScheduled()) { return; }
Ember.run.schedule('afterRender', function() {
$(document).scrollTop(0);
});
}
export default Ember.Mixin.create({
_scrollTop: scrollTop.on('didInsertElement')
});
export { scrollTop };

View File

@ -1,8 +0,0 @@
Discourse.ScrollTop = Em.Mixin.create({
_scrollTop: function() {
if (Discourse.URL.isJumpScheduled()) { return; }
Em.run.schedule('afterRender', function() {
$(document).scrollTop(0);
});
}.on('didInsertElement')
});

View File

@ -2,27 +2,33 @@ export default Em.Mixin.create({
uploading: false,
uploadProgress: 0,
uploadDone: function() {
uploadDone() {
Em.warn("You should implement `uploadDone`");
},
deleteDone: function() {
Em.warn("You should implement `deleteDone`");
},
_initialize: function() {
const $upload = this.$(),
csrf = Discourse.Session.currentProp("csrfToken"),
uploadUrl = this.getWithDefault("uploadUrl", "/uploads"),
reset = () => this.setProperties({ uploading: false, uploadProgress: 0});
_initializeUploader: function() {
var $upload = this.$(),
self = this,
csrf = Discourse.Session.currentProp("csrfToken");
this.messageBus.subscribe("/uploads/" + this.get("type"), upload => {
if (upload && upload.url) {
this.uploadDone(upload);
} else {
Discourse.Utilities.displayErrorForUpload(upload);
}
reset();
});
$upload.fileupload({
url: this.get('uploadUrl') + ".json?authenticity_token=" + encodeURIComponent(csrf),
url: uploadUrl + ".json?authenticity_token=" + encodeURIComponent(csrf),
dataType: "json",
dropZone: $upload,
pasteZone: $upload
});
$upload.on("fileuploaddrop", function (e, data) {
$upload.on("fileuploaddrop", (e, data) => {
if (data.files.length > 10) {
bootbox.alert(I18n.t("post.errors.too_many_dragged_and_dropped_files"));
return false;
@ -31,51 +37,31 @@ export default Em.Mixin.create({
}
});
$upload.on('fileuploadsubmit', function (e, data) {
var isValid = Discourse.Utilities.validateUploadedFiles(data.files, true);
var form = { image_type: self.get('type') };
if (self.get("data")) { form = $.extend(form, self.get("data")); }
$upload.on("fileuploadsubmit", (e, data) => {
const isValid = Discourse.Utilities.validateUploadedFiles(data.files, true);
let form = { type: this.get("type") };
if (this.get("data")) { form = $.extend(form, this.get("data")); }
data.formData = form;
self.setProperties({ uploadProgress: 0, uploading: isValid });
this.setProperties({ uploadProgress: 0, uploading: isValid });
return isValid;
});
$upload.on("fileuploadprogressall", function(e, data) {
var progress = parseInt(data.loaded / data.total * 100, 10);
self.set("uploadProgress", progress);
$upload.on("fileuploadprogressall", (e, data) => {
const progress = parseInt(data.loaded / data.total * 100, 10);
this.set("uploadProgress", progress);
});
$upload.on("fileuploaddone", function(e, data) {
if (data.result) {
if (data.result.url) {
self.uploadDone(data);
} else {
if (data.result.message) {
bootbox.alert(data.result.message);
} else if (data.result.length > 0) {
bootbox.alert(data.result.join("\n"));
} else {
bootbox.alert(I18n.t('post.errors.upload'));
}
}
} else {
bootbox.alert(I18n.t('post.errors.upload'));
}
});
$upload.on("fileuploadfail", function(e, data) {
$upload.on("fileuploadfail", (e, data) => {
Discourse.Utilities.displayErrorForUpload(data);
reset();
});
}.on("didInsertElement"),
$upload.on("fileuploadalways", function() {
self.setProperties({ uploading: false, uploadProgress: 0});
});
}.on('didInsertElement'),
_destroyUploader: function() {
var $upload = this.$();
try { $upload.fileupload('destroy'); }
_destroy: function() {
this.messageBus.unsubscribe("/uploads/" + this.get("type"));
const $upload = this.$();
try { $upload.fileupload("destroy"); }
catch (e) { /* wasn't initialized yet */ }
$upload.off();
}.on('willDestroyElement')
}.on("willDestroyElement")
});

View File

@ -1,4 +1,7 @@
Discourse.ActionSummary = Discourse.Model.extend({
import RestModel from 'discourse/models/rest';
import { popupAjaxError } from 'discourse/lib/ajax-error';
export default RestModel.extend({
// Description for the action
description: function() {
@ -84,10 +87,9 @@ Discourse.ActionSummary = Discourse.Model.extend({
if (!self.get('flagTopic')) {
return post.updateActionsSummary(result);
}
}).catch(function (error) {
}).catch(function(error) {
popupAjaxError(error);
self.removeAction();
var message = $.parseJSON(error.responseText).errors;
bootbox.alert(message);
});
},

View File

@ -90,7 +90,7 @@ Discourse.Category = Discourse.Model.extend({
}.property("permissions"),
destroy: function() {
return Discourse.ajax("/categories/" + (this.get('slug') || this.get('id')), { type: 'DELETE' });
return Discourse.ajax("/categories/" + (this.get('id') || this.get('slug')), { type: 'DELETE' });
},
addPermission: function(permission){

View File

@ -26,12 +26,7 @@ Discourse.LoginMethod.reopenClass({
var methods = this.methods = Em.A();
/*
* enable_google_logins etc.
* */
[ "google",
"google_oauth2",
[ "google_oauth2",
"facebook",
"cas",
"twitter",
@ -42,7 +37,7 @@ Discourse.LoginMethod.reopenClass({
var params = {name: name};
if (name === "google" || name === "google_oauth2") {
if (name === "google_oauth2") {
params.frameWidth = 850;
params.frameHeight = 500;
} else if (name === "facebook") {

View File

@ -69,7 +69,7 @@ const User = RestModel.extend({
profileBackground: function() {
var url = this.get('profile_background');
if (Em.isEmpty(url) || !Discourse.SiteSettings.allow_profile_backgrounds) { return; }
return 'background-image: url(' + Discourse.getURLWithCDN(url) + ')';
return ('background-image: url(' + Discourse.getURLWithCDN(url) + ')').htmlSafe();
}.property('profile_background'),
/**
@ -171,27 +171,31 @@ const User = RestModel.extend({
@returns {Promise} the result of the operation
**/
save: function() {
var self = this,
data = this.getProperties('auto_track_topics_after_msecs',
'bio_raw',
'website',
'location',
'name',
'locale',
'email_digests',
'email_direct',
'email_always',
'email_private_messages',
'dynamic_favicon',
'digest_after_days',
'new_topic_duration_minutes',
'external_links_in_new_tab',
'mailing_list_mode',
'enable_quoting',
'disable_jump_reply',
'custom_fields',
'user_fields',
'muted_usernames');
const self = this,
data = this.getProperties(
'auto_track_topics_after_msecs',
'bio_raw',
'website',
'location',
'name',
'locale',
'email_digests',
'email_direct',
'email_always',
'email_private_messages',
'dynamic_favicon',
'digest_after_days',
'new_topic_duration_minutes',
'external_links_in_new_tab',
'mailing_list_mode',
'enable_quoting',
'disable_jump_reply',
'custom_fields',
'user_fields',
'muted_usernames',
'profile_background',
'card_background'
);
['muted','watched','tracked'].forEach(function(s){
var cats = self.get(s + 'Categories').map(function(c){ return c.get('id')});
@ -204,6 +208,8 @@ const User = RestModel.extend({
data['edit_history_public'] = this.get('edit_history_public');
}
// TODO: We can remove this when migrated fully to rest model.
this.set('isSaving', true);
return Discourse.ajax("/users/" + this.get('username_lower'), {
data: data,
type: 'PUT'
@ -212,6 +218,8 @@ const User = RestModel.extend({
var userProps = self.getProperties('enable_quoting', 'external_links_in_new_tab', 'dynamic_favicon');
Discourse.User.current().setProperties(userProps);
}).finally(() => {
this.set('isSaving', false);
});
},

View File

@ -2,11 +2,11 @@
The parent route for all discovery routes.
Handles the logic for showing the loading spinners.
**/
import ShowFooter from "discourse/mixins/show-footer";
import OpenComposer from "discourse/mixins/open-composer";
import { scrollTop } from 'discourse/mixins/scroll-top';
const DiscoveryRoute = Discourse.Route.extend(Discourse.ScrollTop, OpenComposer, ShowFooter, {
const DiscoveryRoute = Discourse.Route.extend(OpenComposer, ShowFooter, {
redirect: function() { return this.redirectIfLoginRequired(); },
beforeModel: function(transition) {
@ -27,7 +27,7 @@ const DiscoveryRoute = Discourse.Route.extend(Discourse.ScrollTop, OpenComposer,
loadingComplete: function() {
this.controllerFor('discovery').set('loading', false);
if (!this.session.get('topicListScrollPosition')) {
this._scrollTop();
scrollTop();
}
},

View File

@ -1,6 +1,7 @@
// A base route that allows us to redirect when access is restricted
import DiscourseRoute from 'discourse/routes/discourse';
export default Discourse.Route.extend({
// A base route that allows us to redirect when access is restricted
export default DiscourseRoute.extend({
afterModel() {
if (!this.modelFor('user').get('can_edit')) {

View File

@ -155,10 +155,7 @@ const TopicRoute = Discourse.Route.extend(ShowFooter, {
let topic = this.modelFor('topic');
if (topic && (topic.get('id') === parseInt(params.id, 10))) {
this.setupParams(topic, queryParams);
// If we have the existing model, refresh it
return topic.get('postStream').refresh().then(function() {
return topic;
});
return topic;
} else {
topic = this.store.createRecord('topic', _.omit(params, 'username_filters', 'filter'));
return this.setupParams(topic, queryParams);

View File

@ -1,7 +1,14 @@
export default Discourse.Route.extend({
beforeModel: function() {
this.replaceWith('userActivity');
// HACK: Something with the way the user card intercepts clicks seems to break how the
// transition into a user's activity works. This makes the back button work on mobile
// where there is no user card as well as desktop where there is.
if (Discourse.Mobile.mobileView) {
this.replaceWith('userActivity');
} else {
this.transitionTo('userActivity');
}
}
});

View File

@ -1,6 +1,6 @@
<div class="uploaded-image-preview" class="input-xxlarge" {{bind-attr style="backgroundStyle"}}>
<div class="uploaded-image-preview input-xxlarge" style={{backgroundStyle}}>
<div class="image-upload-controls">
<label class="btn pad-left no-text" {{bind-attr disabled="uploading"}}>
<label class="btn pad-left no-text {{if uploading 'disabled'}}">
{{fa-icon "picture-o"}}
<input {{bind-attr disabled="uploading"}} type="file" accept="image/*" style="visibility: hidden; position: absolute;" />
</label>

View File

@ -56,6 +56,7 @@
{{#unless mapCollapsed}}
<section class='avatars clearfix'>
<h3>{{i18n 'topic_map.participants_title'}}</h3>
{{#each p in details.participants}}
{{topic-participant participant=p}}
{{/each}}
@ -63,9 +64,9 @@
{{#if infoLinks}}
<section class='links'>
<h3>{{i18n 'topic_map.links_title'}}</h3>
<table class='topic-links'>
{{#each link in infoLinks}}
{{#each infoLinks as |link|}}
<tr>
<td>
<span class='badge badge-notification clicks' title='{{i18n 'topic_map.clicks' count=clicks}}'>{{link.clicks}}</span>

View File

@ -1,18 +1,6 @@
{{#if visible}}
{{!--
note this spinner is NEVER turned "off" when the composer is open
so I'm going to stop rendering it until we figure out what's up
<div class='composer-loading'>
{{loading-spinner}}
</div>
--}}
<div class='contents'>
{{render "composer-messages"}}
<div class='control'>
<a href class='toggler' {{action "toggle" bubbles=false}} title='{{i18n 'composer.toggler'}}'></a>

View File

@ -19,7 +19,7 @@
<section class='user-right groups'>
<section class='about group'>
<div class='details'>
<h1>{{name}}</h1>
<h1>{{model.name}}</h1>
</div>
</section>
{{outlet}}

View File

@ -1,18 +1,18 @@
<div class='user-stream'>
{{#each p in model itemController="group/post"}}
{{#each p in controller}}
<div class='item'>
<div class='clearfix info'>
{{#link-to 'user' p.user class="avatar-link"}}<div class='avatar-wrapper'>{{avatar p.user imageSize="large" extraClasses="actor" ignoreTitle="true"}}</div>{{/link-to}}
<a href="{{unbound p.user.userUrl}}" data-user-card="{{unbound p.user.username}}" class='avatar-link'><div class='avatar-wrapper'>{{avatar p.user imageSize="large" extraClasses="actor" ignoreTitle="true"}}</div></a>
<span class='time'>{{format-date p.created_at leaveAgo="true"}}</span>
<span class="title">
<a href="{{unbound p.url}}">{{unbound p.title}}</a>
</span>
<span class="category">{{category-link p.category}}</span>
{{#if p.byName}}
<span class="name">
{{unbound p.byName}}
</span>
{{/if}}
<div class="user-info">
{{#if p.user_long_name}}
{{p.user_long_name}}{{#if p.user_title}}, {{p.user_title}}{{/if}}
{{/if}}
</div>
</div>
<p class='excerpt'>
{{{unbound p.cooked}}}

View File

@ -16,13 +16,13 @@
{{topic-list
showPosters=true
currentUser=currentUser
hideCategory=hideCategory
topics=topics}}
hideCategory=model.hideCategory
topics=model.topics}}
{{/if}}
</div>
<footer class='topic-list-bottom'>
{{conditional-loading-spinner condition=loadingMore}}
{{conditional-loading-spinner condition=model.loadingMore}}
{{#if allLoaded}}
{{#if showDismissRead}}
<button title="{{i18n 'topics.bulk.dismiss_topics_tooltip'}}" id='dismiss-topics' class='btn dismiss-read' {{action "dismissRead" "topics"}}>{{i18n 'topics.bulk.dismiss_topics'}}</button>

View File

@ -17,8 +17,9 @@
{{#if uploadedAvatarTemplate}}
{{bound-avatar-template uploadedAvatarTemplate "large"}}
{{else}}
{{bound-avatar controller "large" custom_avatar_upload_id}} {{i18n 'user.change_avatar.uploaded_avatar'}}
{{bound-avatar controller "large" custom_avatar_upload_id}}
{{/if}}
{{i18n 'user.change_avatar.uploaded_avatar'}}
{{else}}
{{i18n 'user.change_avatar.uploaded_avatar_empty'}}
{{/if}}

View File

@ -2,11 +2,11 @@
<section class='field'>
<section class="field-item">
<label>{{i18n 'category.name'}}</label>
{{text-field value=name placeholderKey="category.name_placeholder" maxlength="50"}}
{{text-field value=model.name placeholderKey="category.name_placeholder" maxlength="50"}}
</section>
<section class="field-item">
<label>{{i18n 'category.slug'}}</label>
{{text-field value=slug placeholderKey="category.slug_placeholder" maxlength="255"}}
{{text-field value=model.slug placeholderKey="category.slug_placeholder" maxlength="255"}}
</section>
</section>
@ -19,7 +19,7 @@
{{/each}}
{{else}}
<label>{{i18n 'category.parent'}}</label>
{{category-chooser valueAttribute="id" value=parent_category_id categories=parentCategories rootNone=true}}
{{category-chooser valueAttribute="id" value=model.parent_category_id categories=parentCategories rootNone=true}}
{{/if}}
</section>
{{/if}}
@ -27,12 +27,12 @@
{{#if showDescription}}
<section class='field'>
<label>{{i18n 'category.description'}}</label>
{{#if description}}
{{{description}}}
{{#if model.description}}
{{{model.description}}}
{{else}}
{{i18n 'category.no_description'}}
{{/if}}
{{#if topic_url}}
{{#if model.topic_url}}
<br/>
{{d-button class="btn-small" action="showCategoryTopic" icon="pencil" label="category.change_in_category_topic"}}
{{/if}}
@ -46,14 +46,14 @@
<div class='input-prepend input-append' style="margin-top: 10px;">
<span class='color-title'>{{i18n 'category.background_color'}}:</span>
<span class='add-on'>#</span>{{text-field value=color placeholderKey="category.color_placeholder" maxlength="6"}}
{{color-picker colors=backgroundColors usedColors=usedBackgroundColors value=color}}
<span class='add-on'>#</span>{{text-field value=model.color placeholderKey="category.color_placeholder" maxlength="6"}}
{{color-picker colors=backgroundColors usedColors=usedBackgroundColors value=model.color}}
</div>
<div class='input-prepend input-append'>
<span class='color-title'>{{i18n 'category.foreground_color'}}:</span>
<span class='add-on'>#</span>{{text-field value=text_color placeholderKey="category.color_placeholder" maxlength="6"}}
{{color-picker colors=foregroundColors value=text_color}}
<span class='add-on'>#</span>{{text-field value=model.text_color placeholderKey="category.color_placeholder" maxlength="6"}}
{{color-picker colors=foregroundColors value=model.text_color}}
</div>
</div>
</section>

View File

@ -1,9 +1,9 @@
<section class='field'>
<label>{{i18n 'category.logo'}}</label>
{{image-uploader uploadUrl=categoryUploadUrl imageUrl=logo_url type="logo"}}
{{image-uploader imageUrl=model.logo_url type="category_logo"}}
</section>
<section class='field'>
<label>{{i18n 'category.background_image'}}</label>
{{image-uploader uploadUrl=categoryUploadUrl imageUrl=background_url type="background"}}
{{image-uploader imageUrl=model.background_url type="category_background"}}
</section>

View File

@ -1,6 +1,6 @@
<section class='field'>
<ul class='permission-list'>
{{#each p in permissions}}
{{#each model.permissions as |p|}}
<li>
<span class="name"><span class="badge-group">{{p.group_name}}</span></span>
{{{i18n "category.can"}}}

View File

@ -1,13 +1,13 @@
<section class='field'>
{{auto-close-form autoCloseTime=auto_close_hours
autoCloseBasedOnLastPost=auto_close_based_on_last_post
{{auto-close-form autoCloseTime=model.auto_close_hours
autoCloseBasedOnLastPost=model.auto_close_based_on_last_post
limited="true" }}
</section>
<section class='field'>
<div class="allow-badges">
<div>
{{input type="checkbox" checked=allow_badges}}
{{input type="checkbox" checked=model.allow_badges}}
{{i18n 'category.allow_badges_label'}}
</div>
</div>

View File

@ -1,7 +1,7 @@
<div {{bind-attr class="loading:invisible"}}>
<div>
<ul class="nav nav-pills">
{{edit-category-tab selectedTab=selectedTab tab="general"}}
{{#unless isUncategorizedCategory}}
{{#unless model.isUncategorizedCategory}}
{{edit-category-tab selectedTab=selectedTab tab="security"}}
{{/unless}}
{{edit-category-tab selectedTab=selectedTab tab="settings"}}
@ -16,11 +16,11 @@
<div class="modal-footer">
<button class='btn btn-primary' {{bind-attr disabled="disabled"}} {{action "saveCategory"}}>{{buttonTitle}}</button>
{{#if can_delete}}
{{#if model.can_delete}}
<button class='btn btn-danger pull-right' {{bind-attr disabled="deleteDisabled"}} {{action "deleteCategory"}}><i class="fa fa-trash-o"></i>{{deleteButtonTitle}}</button>
{{else}}
<div class="cannot_delete_reason">
{{{cannot_delete_reason}}}
{{{model.cannot_delete_reason}}}
</div>
{{/if}}
</div>

View File

@ -9,6 +9,7 @@
<li>{{{i18n 'keyboard_shortcuts_help.jump_to.unread'}}}</li>
<li>{{{i18n 'keyboard_shortcuts_help.jump_to.categories'}}}</li>
<li>{{{i18n 'keyboard_shortcuts_help.jump_to.top'}}}</li>
<li>{{{i18n 'keyboard_shortcuts_help.jump_to.bookmarks'}}}</li>
</ul>
<h4>{{i18n 'keyboard_shortcuts_help.navigation.title'}}</h4>
<ul>

View File

@ -8,7 +8,7 @@
<p>{{{i18n 'topic.merge_topic.instructions' count=selectedPostsCount}}}</p>
<form>
{{choose-topic selectedTopicId=selectedTopicId}}
{{view "choose-topic" selectedTopicId=selectedTopicId}}
</form>
</div>

View File

@ -19,7 +19,7 @@
{{/if}}
{{#if canEditCategory}}
{{d-button class="btn-default" action="editCategory" actionParam=category icon="wrench" label="category.edit_long"}}
{{d-button class="btn-default edit-category" action="editCategory" actionParam=category icon="wrench" label="category.edit_long"}}
{{/if}}
<section class='category-heading'>

View File

@ -72,6 +72,9 @@
<div {{bind-attr class="showUserReplyTab:avoid-tab view.repliesShown::contents :regular view.extraClass"}}>
<div class='cooked'>
{{{cooked}}}
{{#if firstPost}}
{{plugin-outlet "topic-after-cooked"}}
{{/if}}
</div>
{{#if cooked_hidden}}
<a href {{action "expandHidden" this}}>{{i18n 'post.show_hidden'}}</a>

View File

@ -61,11 +61,13 @@
label="queue.reject"
icon="times"
class="btn-danger reject"}}
{{d-button action="deleteUser"
disabled=ctrl.post.isSaving
label="queue.delete_user"
icon="trash"
class="btn-danger delete-user"}}
{{#if ctrl.post.can_delete_user}}
{{d-button action="deleteUser"
disabled=ctrl.post.isSaving
label="queue.delete_user"
icon="trash"
class="btn-danger delete-user"}}
{{/if}}
{{d-button action="edit"
disabled=ctrl.post.isSaving
label="queue.edit"

View File

@ -128,8 +128,8 @@
{{{model.notFoundHtml}}}
{{else}}
<div class="topic-error">
<div>{{message}}</div>
{{#if noRetry}}
<div>{{model.message}}</div>
{{#if model.noRetry}}
{{#unless currentUser}}
{{d-button action="showLogin" class="btn-primary topic-retry" icon="user" label="log_in"}}
{{/unless}}

View File

@ -47,7 +47,7 @@
<b>{{i18n 'user.suspended_reason'}}</b> {{user.suspend_reason}}
</div>
{{else}}
{{#if user.bio_cooked}}<div class='bio'>{{text-overflow class="overflow" text=user.bio_cooked}}</div>{{/if}}
{{#if user.bio_cooked}}<div class='bio'>{{text-overflow class="overflow" text=user.bio_excerpt}}</div>{{/if}}
{{/if}}
{{#if user.card_badge}}
@ -58,7 +58,9 @@
{{#if user}}
<div class="metadata">
<h3><span class='desc'>{{i18n 'last_post'}}</span> {{format-date user.last_posted_at leaveAgo="true"}}</h3>
{{#if user.last_posted_at}}
<h3><span class='desc'>{{i18n 'last_post'}}</span> {{format-date user.last_posted_at leaveAgo="true"}}</h3>
{{/if}}
<h3><span class='desc'>{{i18n 'joined'}}</span> {{format-date user.created_at leaveAgo="true"}}</h3>
</div>
{{/if}}

View File

@ -1,23 +1,23 @@
<section class='user-content'>
<section class='user-content user-preferences'>
<form class="form-horizontal">
<div class="control-group save-button" id='save-button-top'>
<div class="controls">
{{partial 'user/preferences/saveButton'}}
{{partial 'user/preferences/save-button'}}
</div>
</div>
<div class="control-group pref-username">
<label class="control-label">{{i18n 'user.username.title'}}</label>
<div class="controls">
<span class='static'>{{username}}</span>
{{#if can_edit_username}}
<span class='static'>{{model.username}}</span>
{{#if model.can_edit_username}}
{{#link-to "preferences.username" class="btn btn-small pad-left no-text"}}<i class="fa fa-pencil"></i>{{/link-to}}
{{/if}}
</div>
<div class='instructions'>
{{{i18n 'user.username.short_instructions' username=username}}}
{{{i18n 'user.username.short_instructions' username=model.username}}}
</div>
</div>
@ -25,7 +25,7 @@
<div class="control-group pref-name">
<label class="control-label">{{i18n 'user.name.title'}}</label>
<div class="controls">
{{#if can_edit_name}}
{{#if model.can_edit_name}}
{{text-field value=newNameInput classNames="input-xxlarge"}}
{{else}}
<span class='static'>{{name}}</span>
@ -41,7 +41,7 @@
<div class="control-group pref-title">
<label class="control-label">{{i18n 'user.title.title'}}</label>
<div class="controls">
<span class="static">{{title}}</span>
<span class="static">{{model.title}}</span>
{{#link-to "preferences.badgeTitle" class="btn btn-small pad-left no-text"}}{{fa-icon "pencil"}}{{/link-to}}
</div>
</div>
@ -53,7 +53,7 @@
{{#if model.email}}
<div class="controls">
<span class='static'>{{model.email}}</span>
{{#if can_edit_email}}
{{#if model.can_edit_email}}
{{#link-to "preferences.email" class="btn btn-small pad-left no-text"}}{{fa-icon "pencil"}}{{/link-to}}
{{/if}}
</div>
@ -74,7 +74,7 @@
<div class="controls">
<a href="#" {{action "changePassword"}} class='btn'>
{{fa-icon "envelope"}}
{{#if no_password}}
{{#if model.no_password}}
{{i18n 'user.change_password.set_password'}}
{{else}}
{{i18n 'user.change_password.action'}}
@ -104,10 +104,7 @@
<div class="control-group pref-profile-bg">
<label class="control-label">{{i18n 'user.change_profile_background.title'}}</label>
<div class="controls">
{{image-uploader uploadUrl=imageUploadUrl
imageUrl=profile_background
instantDelete="true"
type="profile_background"}}
{{image-uploader imageUrl=model.profile_background type="profile_background"}}
</div>
<div class='instructions'>
{{i18n 'user.change_profile_background.instructions'}}
@ -117,10 +114,7 @@
<div class="control-group pref-profile-bg">
<label class="control-label">{{i18n 'user.change_card_background.title'}}</label>
<div class="controls">
{{image-uploader uploadUrl=imageUploadUrl
imageUrl=card_background
instantDelete="true"
type="card_background"}}
{{image-uploader imageUrl=model.card_background type="card_background"}}
</div>
<div class='instructions'>
{{i18n 'user.change_card_background.instructions'}}
@ -132,7 +126,7 @@
<div class="control-group pref-locale">
<label class="control-label">{{i18n 'user.locale.title'}}</label>
<div class="controls">
{{combo-box valueAttribute="value" content=availableLocales value=locale none="user.locale.default"}}
{{combo-box valueAttribute="value" content=availableLocales value=model.locale none="user.locale.default"}}
</div>
<div class='instructions'>
{{i18n 'user.locale.instructions'}}
@ -143,7 +137,7 @@
<div class="control-group pref-bio">
<label class="control-label">{{i18n 'user.bio'}}</label>
<div class="controls bio-composer">
{{pagedown-editor value=bio_raw}}
{{pagedown-editor value=model.bio_raw}}
</div>
</div>
@ -154,22 +148,22 @@
<div class="control-group pref-location">
<label class="control-label">{{i18n 'user.location'}}</label>
<div class="controls">
{{input type="text" value=location class="input-xxlarge"}}
{{input type="text" value=model.location class="input-xxlarge" id='edit-location'}}
</div>
</div>
<div class="control-group pref-website">
<label class="control-label">{{i18n 'user.website'}}</label>
<div class="controls">
{{input type="text" value=website class="input-xxlarge"}}
{{input type="text" value=model.website class="input-xxlarge"}}
</div>
</div>
<div class="control-group pref-card-badge">
<label class="control-label">{{i18n 'user.card_badge.title'}}</label>
<div class="controls">
{{#if card_image_badge}}
{{icon-or-image card_image_badge}}
{{#if model.card_image_badge}}
{{icon-or-image model.card_image_badge}}
{{/if}}
{{#link-to "preferences.card-badge" class="btn btn-small pad-left no-text"}}{{fa-icon "pencil"}}{{/link-to}}
</div>
@ -178,17 +172,17 @@
<div class="control-group pref-email-settings">
<label class="control-label">{{i18n 'user.email_settings'}}</label>
{{#if canReceiveDigest}}
{{preference-checkbox labelKey="user.email_digests.title" checked=email_digests}}
{{#if email_digests}}
{{preference-checkbox labelKey="user.email_digests.title" checked=model.email_digests}}
{{#if model.email_digests}}
<div class='controls controls-dropdown'>
{{combo-box valueAttribute="value" content=digestFrequencies value=digest_after_days}}
{{combo-box valueAttribute="value" content=digestFrequencies value=model.digest_after_days}}
</div>
{{/if}}
{{/if}}
{{preference-checkbox labelKey="user.email_private_messages" checked=email_private_messages}}
{{preference-checkbox labelKey="user.email_direct" checked=email_direct}}
{{preference-checkbox labelKey="user.mailing_list_mode" checked=mailing_list_mode}}
{{preference-checkbox labelKey="user.email_always" checked=email_always}}
{{preference-checkbox labelKey="user.email_private_messages" checked=model.email_private_messages}}
{{preference-checkbox labelKey="user.email_direct" checked=model.email_direct}}
{{preference-checkbox labelKey="user.mailing_list_mode" checked=model.mailing_list_mode}}
{{preference-checkbox labelKey="user.email_always" checked=model.email_always}}
<div class='instructions'>
{{i18n 'user.email.frequency' count=siteSettings.email_time_window_mins}}
@ -200,20 +194,20 @@
<div class="controls controls-dropdown">
<label>{{i18n 'user.new_topic_duration.label'}}</label>
{{combo-box valueAttribute="value" content=considerNewTopicOptions value=new_topic_duration_minutes}}
{{combo-box valueAttribute="value" content=considerNewTopicOptions value=model.new_topic_duration_minutes}}
</div>
<div class="controls controls-dropdown">
<label>{{i18n 'user.auto_track_topics'}}</label>
{{combo-box valueAttribute="value" content=autoTrackDurations value=auto_track_topics_after_msecs}}
{{combo-box valueAttribute="value" content=autoTrackDurations value=model.auto_track_topics_after_msecs}}
</div>
{{preference-checkbox labelKey="user.external_links_in_new_tab" checked=external_links_in_new_tab}}
{{preference-checkbox labelKey="user.enable_quoting" checked=enable_quoting}}
{{preference-checkbox labelKey="user.dynamic_favicon" checked=dynamic_favicon}}
{{preference-checkbox labelKey="user.disable_jump_reply" checked=disable_jump_reply}}
{{preference-checkbox labelKey="user.external_links_in_new_tab" checked=model.external_links_in_new_tab}}
{{preference-checkbox labelKey="user.enable_quoting" checked=model.enable_quoting}}
{{preference-checkbox labelKey="user.dynamic_favicon" checked=model.dynamic_favicon}}
{{preference-checkbox labelKey="user.disable_jump_reply" checked=model.disable_jump_reply}}
{{#unless editHistoryVisible}}
{{preference-checkbox labelKey="user.edit_history_public" checked=edit_history_public}}
{{preference-checkbox labelKey="user.edit_history_public" checked=model.edit_history_public}}
{{/unless}}
{{plugin-outlet "user_custom_preferences"}}
@ -223,17 +217,17 @@
<label class="control-label">{{i18n 'user.categories_settings'}}</label>
<div class="controls category-controls">
<label>{{i18n 'user.watched_categories'}}</label>
{{category-group categories=watchedCategories blacklist=selectedCategories}}
{{category-group categories=model.watchedCategories blacklist=selectedCategories}}
</div>
<div class="instructions">{{i18n 'user.watched_categories_instructions'}}</div>
<div class="controls category-controls">
<label>{{i18n 'user.tracked_categories'}}</label>
{{category-group categories=trackedCategories blacklist=selectedCategories}}
{{category-group categories=model.trackedCategories blacklist=selectedCategories}}
</div>
<div class="instructions">{{i18n 'user.tracked_categories_instructions'}}</div>
<div class="controls category-controls">
<label>{{i18n 'user.muted_categories'}}</label>
{{category-group categories=mutedCategories blacklist=selectedCategories}}
{{category-group categories=model.mutedCategories blacklist=selectedCategories}}
</div>
<div class="instructions">{{i18n 'user.muted_categories_instructions'}}</div>
</div>
@ -242,24 +236,23 @@
<label class="control-label">{{i18n 'user.users'}}</label>
<div class="controls category-controls">
<label>{{i18n 'user.muted_users'}}</label>
{{user-selector excludeCurrentUser=true usernames=muted_usernames class="user-selector"}}
{{user-selector excludeCurrentUser=true usernames=model.muted_usernames class="user-selector"}}
</div>
<div class="instructions">{{i18n 'user.muted_users_instructions'}}</div>
</div>
<div class="control-group">
<div class="controls">
{{partial 'user/preferences/saveButton'}}
{{partial 'user/preferences/save-button'}}
</div>
</div>
{{#if canDeleteAccount}}
{{#if model.canDeleteAccount}}
<div class="control-group delete-account">
<hr/>
<div class="controls">
{{d-button action="delete" disabled="deleteDisabled" class="btn-danger" icon="trash-o" label="user.delete_account"}}
{{d-button action="delete" disabled=deleteDisabled class="btn-danger" icon="trash-o" label="user.delete_account"}}
</div>
</div>
{{/if}}
</form>
</section>

View File

@ -0,0 +1,7 @@
{{#d-button action="save" disabled=model.isSaving class="btn btn-primary save-user"}}
{{saveButtonText}}
{{/d-button}}
{{#if saved}}
<span class='saved-user'>{{i18n 'saved'}}</span>
{{/if}}

View File

@ -1,2 +0,0 @@
<button {{action "save"}} {{bind-attr disabled="saving"}} class="btn btn-primary">{{saveButtonText}}</button>
{{#if saved}}{{i18n 'saved'}}{{/if}}

View File

@ -67,7 +67,7 @@
<h3>{{model.title}}</h3>
{{/if}}
<h3>
{{#if model.location}}{{fa-icon "map-marker"}}{{model.location}}{{/if}}
{{#if model.location}}{{fa-icon "map-marker"}} {{model.location}}{{/if}}
{{#if websiteName}}
{{fa-icon "globe"}}
{{#if linkWebsite}}

View File

@ -1,2 +1,3 @@
export default Ember.View.extend(Discourse.ScrollTop);
import ScrollTop from 'discourse/mixins/scroll-top';
export default Ember.View.extend(ScrollTop);

View File

@ -306,78 +306,73 @@ const ComposerView = Discourse.View.extend(Ember.Evented, {
// in case it's still bound somehow
this._unbindUploadTarget();
const $uploadTarget = $('#reply-control'),
csrf = Discourse.Session.currentProp('csrfToken');
let cancelledByTheUser;
const $uploadTarget = $("#reply-control"),
csrf = Discourse.Session.currentProp("csrfToken"),
reset = () => this.setProperties({ uploadProgress: 0, isUploading: false });
var cancelledByTheUser;
this.messageBus.subscribe("/uploads/composer", upload => {
if (!cancelledByTheUser) {
if (upload && upload.url) {
const markdown = Discourse.Utilities.getUploadMarkdown(upload);
this.addMarkdown(markdown + " ");
} else {
Discourse.Utilities.displayErrorForUpload(upload);
}
}
// reset upload state
reset();
});
// NOTE: we need both the .json extension and the CSRF token as a query parameter for IE9
$uploadTarget.fileupload({
url: Discourse.getURL('/uploads.json?authenticity_token=' + encodeURIComponent(csrf)),
dataType: 'json',
pasteZone: $uploadTarget
url: Discourse.getURL("/uploads.json?authenticity_token=" + encodeURIComponent(csrf)),
dataType: "json",
pasteZone: $uploadTarget,
});
// submit - this event is triggered for each upload
$uploadTarget.on('fileuploadsubmit', function (e, data) {
const result = Discourse.Utilities.validateUploadedFiles(data.files);
// reset upload status when everything is ok
if (result) self.setProperties({ uploadProgress: 0, isUploading: true });
return result;
$uploadTarget.on("fileuploadsubmit", (e, data) => {
const isValid = Discourse.Utilities.validateUploadedFiles(data.files);
data.formData = { type: "composer" };
this.setProperties({ uploadProgress: 0, isUploading: isValid });
return isValid;
});
// send - this event is triggered when the upload request is about to start
$uploadTarget.on('fileuploadsend', function (e, data) {
cancelledByTheUser = false;
$uploadTarget.on("fileuploadsend", (e, data) => {
// hide the "file selector" modal
self.get('controller').send('closeModal');
// NOTE: IE9 doesn't support XHR
this.get("controller").send("closeModal");
// deal with cancellation
cancelledByTheUser = false;
if (data["xhr"]) {
const jqHXR = data.xhr();
if (jqHXR) {
// need to wait for the link to show up in the DOM
Em.run.schedule('afterRender', function() {
// bind on the click event on the cancel link
$('#cancel-file-upload').on('click', function() {
// cancel the upload
self.set('isUploading', false);
// NOTE: this might trigger a 'fileuploadfail' event with status = 0
if (jqHXR) { cancelledByTheUser = true; jqHXR.abort(); }
Em.run.schedule("afterRender", () => {
const $cancel = $("#cancel-file-upload");
$cancel.on("click", () => {
if (jqHXR) {
cancelledByTheUser = true;
// might trigger a "fileuploadfail" event with status = 0
jqHXR.abort();
// make sure we always reset the uploading status
reset();
}
// unbind
$(this).off('click');
$cancel.off("click");
});
});
}
}
});
// progress all
$uploadTarget.on('fileuploadprogressall', function (e, data) {
$uploadTarget.on("fileuploadprogressall", (e, data) => {
const progress = parseInt(data.loaded / data.total * 100, 10);
self.set('uploadProgress', progress);
this.set("uploadProgress", progress);
});
// done
$uploadTarget.on('fileuploaddone', function (e, data) {
$uploadTarget.on("fileuploadfail", (e, data) => {
reset();
if (!cancelledByTheUser) {
// make sure we have a url
if (data.result.url) {
const markdown = Discourse.Utilities.getUploadMarkdown(data.result);
// appends a space at the end of the inserted markdown
self.addMarkdown(markdown + " ");
self.set('isUploading', false);
} else {
// display the error message sent by the server
bootbox.alert(data.result.join("\n"));
}
}
});
// fail
$uploadTarget.on('fileuploadfail', function (e, data) {
// hide upload status
self.set('isUploading', false);
if (!cancelledByTheUser) {
// display an error message
Discourse.Utilities.displayErrorForUpload(data);
}
});
@ -538,6 +533,14 @@ const ComposerView = Discourse.View.extend(Ember.Evented, {
});
},
_unbindUploadTarget() {
this.messageBus.unsubscribe("/uploads/composer");
const $uploadTarget = $("#reply-controler");
try { $uploadTarget.fileupload("destroy"); }
catch (e) { /* wasn't initialized yet */ }
$uploadTarget.off();
},
titleValidation: function() {
const titleLength = this.get('model.titleLength'),
missingChars = this.get('model.missingTitleCharacters');
@ -580,13 +583,6 @@ const ComposerView = Discourse.View.extend(Ember.Evented, {
return Discourse.InputValidation.create({ failed: true, reason });
}
}.property('model.reply', 'model.replyLength', 'model.missingReplyCharacters', 'model.minimumPostLength'),
_unbindUploadTarget() {
const $uploadTarget = $('#reply-control');
try { $uploadTarget.fileupload('destroy'); }
catch (e) { /* wasn't initialized yet */ }
$uploadTarget.off();
}
});
RSVP.EventTarget.mixin(ComposerView);

View File

@ -1,6 +1,6 @@
import UrlRefresh from 'discourse/mixins/url-refresh';
export default Discourse.View.extend(UrlRefresh, Discourse.ScrollTop, {
export default Discourse.View.extend(UrlRefresh, {
_addBodyClass: function() {
$('body').addClass('categories-list');
}.on('didInsertElement'),

View File

@ -1,3 +1,4 @@
import UrlRefresh from 'discourse/mixins/url-refresh';
import ScrollTop from 'discourse/mixins/scroll-top';
export default Discourse.View.extend(Discourse.ScrollTop, UrlRefresh);
export default Discourse.View.extend(ScrollTop, UrlRefresh);

View File

@ -6,7 +6,7 @@ export default Discourse.View.extend(LoadMore, UrlRefresh, {
actions: {
loadMore() {
var self = this;
const self = this;
Discourse.notifyTitle(0);
this.get('controller').loadMoreTopics().then(function (hasMoreResults) {
Em.run.schedule('afterRender', function() {
@ -20,7 +20,7 @@ export default Discourse.View.extend(LoadMore, UrlRefresh, {
},
_readjustScrollPosition: function() {
var scrollTo = this.session.get('topicListScrollPosition');
const scrollTo = this.session.get('topicListScrollPosition');
if (typeof scrollTo !== "undefined") {
Em.run.schedule('afterRender', function() {

View File

@ -1,5 +1,6 @@
import ScrollTop from 'discourse/mixins/scroll-top';
import LoadMore from "discourse/mixins/load-more";
export default Discourse.View.extend(Discourse.ScrollTop, LoadMore, {
export default Discourse.View.extend(ScrollTop, LoadMore, {
eyelineSelector: '.user-stream .item',
});

View File

@ -1,5 +1,6 @@
import ScrollTop from 'discourse/mixins/scroll-top';
import LoadMore from "discourse/mixins/load-more";
export default Discourse.View.extend(Discourse.ScrollTop, LoadMore, {
export default Discourse.View.extend(ScrollTop, LoadMore, {
eyelineSelector: '.group-members tr',
});

View File

@ -44,7 +44,7 @@ Button.prototype.render = function(buffer) {
var hiddenButtons;
export default Discourse.View.extend(StringBuffer, {
var PostMenuView = Discourse.View.extend(StringBuffer, {
tagName: 'section',
classNames: ['post-menu-area', 'clearfix'],
@ -141,6 +141,13 @@ export default Discourse.View.extend(StringBuffer, {
visibleButtons.splice(visibleButtons.length - 1, 0, this.buttonForShowMoreActions(post));
}
var callbacks = PostMenuView._registerButtonCallbacks;
if (callbacks) {
_.each(callbacks, function(callback) {
callback.apply(self, [visibleButtons]);
});
}
buffer.push('<div class="actions">');
visibleButtons.forEach(function (b) {
b.render(buffer);
@ -374,3 +381,12 @@ export default Discourse.View.extend(StringBuffer, {
}
});
PostMenuView.reopenClass({
registerButton: function(callback){
this._registerButtonCallbacks = this._registerButtonCallbacks || [];
this._registerButtonCallbacks.push(callback);
}
});
export default PostMenuView;

View File

@ -1,9 +1,9 @@
var DAY = 60 * 50 * 1000;
const DAY = 60 * 50 * 1000;
var PostView = Discourse.GroupedView.extend(Ember.Evented, {
const PostView = Discourse.GroupedView.extend(Ember.Evented, {
classNames: ['topic-post', 'clearfix'],
templateName: 'post',
classNameBindings: ['postTypeClass',
classNameBindings: ['needsModeratorClass:moderator:regular',
'selected',
'post.hidden:post-hidden',
'post.deleted:deleted',
@ -14,11 +14,11 @@ var PostView = Discourse.GroupedView.extend(Ember.Evented, {
post: Ember.computed.alias('content'),
historyHeat: function() {
var updatedAt = this.get('post.updated_at');
const updatedAt = this.get('post.updated_at');
if (!updatedAt) { return; }
// Show heat on age
var rightNow = new Date().getTime(),
const rightNow = new Date().getTime(),
updatedAtDate = new Date(updatedAt).getTime();
if (updatedAtDate > (rightNow - DAY * Discourse.SiteSettings.history_hours_low)) return 'heatmap-high';
@ -26,12 +26,13 @@ var PostView = Discourse.GroupedView.extend(Ember.Evented, {
if (updatedAtDate > (rightNow - DAY * Discourse.SiteSettings.history_hours_high)) return 'heatmap-low';
}.property('post.updated_at'),
postTypeClass: function() {
return this.get('post.post_type') === Discourse.Site.currentProp('post_types.moderator_action') ? 'moderator' : 'regular';
needsModeratorClass: function() {
return (this.get('post.post_type') === this.site.get('post_types.moderator_action')) ||
(this.get('post.topic.is_warning') && this.get('post.firstPost'));
}.property('post.post_type'),
groupNameClass: function() {
var primaryGroupName = this.get('post.primary_group_name');
const primaryGroupName = this.get('post.primary_group_name');
if (primaryGroupName) {
return "group-" + primaryGroupName;
}
@ -40,7 +41,7 @@ var PostView = Discourse.GroupedView.extend(Ember.Evented, {
showExpandButton: function() {
if (this.get('controller.firstPostExpanded')) { return false; }
var post = this.get('post');
const post = this.get('post');
return post.get('post_number') === 1 && post.get('topic.expandable_first_post');
}.property('post.post_number', 'controller.firstPostExpanded'),
@ -76,14 +77,14 @@ var PostView = Discourse.GroupedView.extend(Ember.Evented, {
repliesShown: Em.computed.gt('post.replies.length', 0),
_updateQuoteElements($aside, desc) {
var navLink = "",
quoteTitle = I18n.t("post.follow_quote"),
postNumber = $aside.data('post');
let navLink = "";
const quoteTitle = I18n.t("post.follow_quote"),
postNumber = $aside.data('post');
if (postNumber) {
// If we have a topic reference
var topicId, topic;
let topicId, topic;
if (topicId = $aside.data('topic')) {
topic = this.get('controller.content');
@ -101,7 +102,7 @@ var PostView = Discourse.GroupedView.extend(Ember.Evented, {
}
}
// Only add the expand/contract control if it's not a full post
var expandContract = "";
let expandContract = "";
if (!$aside.data('full')) {
expandContract = "<i class='fa fa-" + desc + "' title='" + I18n.t("post.expand_collapse") + "'></i>";
$('.title', $aside).css('cursor', 'pointer');
@ -115,7 +116,7 @@ var PostView = Discourse.GroupedView.extend(Ember.Evented, {
$aside.data('expanded', !$aside.data('expanded'));
var self = this,
const self = this,
finished = function() {
self.set('expanding', false);
};
@ -123,23 +124,23 @@ var PostView = Discourse.GroupedView.extend(Ember.Evented, {
if ($aside.data('expanded')) {
this._updateQuoteElements($aside, 'chevron-up');
// Show expanded quote
var $blockQuote = $('blockquote', $aside);
const $blockQuote = $('blockquote', $aside);
$aside.data('original-contents',$blockQuote.html());
var originalText = $blockQuote.text().trim();
const originalText = $blockQuote.text().trim();
$blockQuote.html(I18n.t("loading"));
var topicId = this.get('post.topic_id');
let topicId = this.get('post.topic_id');
if ($aside.data('topic')) {
topicId = $aside.data('topic');
}
var postId = parseInt($aside.data('post'), 10);
const postId = parseInt($aside.data('post'), 10);
topicId = parseInt(topicId, 10);
Discourse.ajax("/posts/by_number/" + topicId + "/" + postId).then(function (result) {
// slightly double escape the cooked html to prevent jQuery from unescaping it
var escaped = result.cooked.replace("&", "&amp;");
var parsed = $(escaped);
const escaped = result.cooked.replace("&", "&amp;");
const parsed = $(escaped);
parsed.replaceText(originalText, "<span class='highlighted'>" + originalText + "</span>");
$blockQuote.showHtml(parsed, 'fast', finished);
});
@ -185,7 +186,7 @@ var PostView = Discourse.GroupedView.extend(Ember.Evented, {
// Toggle the replies this post is a reply to
toggleReplyHistory(post) {
var replyHistory = post.get('replyHistory'),
const replyHistory = post.get('replyHistory'),
topicController = this.get('controller'),
origScrollTop = $(window).scrollTop(),
replyPostNumber = this.get('post.reply_to_post_number'),
@ -197,8 +198,8 @@ var PostView = Discourse.GroupedView.extend(Ember.Evented, {
return;
}
var stream = topicController.get('postStream');
var offsetFromTop = this.$().position().top - $(window).scrollTop();
const stream = topicController.get('postStream');
const offsetFromTop = this.$().position().top - $(window).scrollTop();
if(Discourse.SiteSettings.experimental_reply_expansion) {
if(postNumber - replyPostNumber > 1) {
@ -213,7 +214,7 @@ var PostView = Discourse.GroupedView.extend(Ember.Evented, {
}
if (replyHistory.length > 0) {
var origHeight = this.$('.embedded-posts.top').height();
const origHeight = this.$('.embedded-posts.top').height();
replyHistory.clear();
Em.run.next(function() {
@ -235,17 +236,17 @@ var PostView = Discourse.GroupedView.extend(Ember.Evented, {
// Add the quote controls to a post
_insertQuoteControls() {
var self = this,
const self = this,
$quotes = this.$('aside.quote');
// Safety check - in some cases with cloackedView this seems to be `undefined`.
if (Em.isEmpty($quotes)) { return; }
$quotes.each(function(i, e) {
var $aside = $(e);
const $aside = $(e);
if ($aside.data('post')) {
self._updateQuoteElements($aside, 'chevron-down');
var $title = $('.title', $aside);
const $title = $('.title', $aside);
// Unless it's a full quote, allow click to expand
if (!($aside.data('full') || $title.data('has-quote-controls'))) {
@ -280,8 +281,8 @@ var PostView = Discourse.GroupedView.extend(Ember.Evented, {
}.on('didInsertElement'),
_applySearchHighlight: function() {
var highlight = this.get('controller.searchHighlight');
var cooked = this.$('.cooked');
const highlight = this.get('controller.searchHighlight');
const cooked = this.$('.cooked');
if(!cooked){ return; }

View File

@ -1,8 +1,9 @@
import isElementInViewport from "discourse/lib/is-element-in-viewport";
import ScrollTop from 'discourse/mixins/scroll-top';
var readFaq = false;
export default Ember.View.extend(Discourse.ScrollTop, {
export default Ember.View.extend(ScrollTop, {
_checkRead: function() {
const path = this.get('controller.model.path');

View File

@ -1 +1,3 @@
export default Ember.View.extend(Discourse.ScrollTop);
import ScrollTop from 'discourse/mixins/scroll-top';
export default Ember.View.extend(ScrollTop);

View File

@ -3,8 +3,8 @@ import CleansUp from 'discourse/mixins/cleans-up';
import afterTransition from 'discourse/lib/after-transition';
const clickOutsideEventName = "mousedown.outside-user-card",
clickDataExpand = "click.discourse-user-card",
clickMention = "click.discourse-user-mention";
clickDataExpand = "click.discourse-user-card",
clickMention = "click.discourse-user-mention";
export default Discourse.View.extend(CleansUp, {
elementId: 'user-card',
@ -27,54 +27,50 @@ export default Discourse.View.extend(CleansUp, {
}.observes('controller.user.card_background'),
_setup: function() {
const self = this;
afterTransition(self.$(), this._hide.bind(this));
afterTransition(this.$(), this._hide.bind(this));
$('html').off(clickOutsideEventName)
.on(clickOutsideEventName, function(e) {
if (self.get('controller.visible')) {
const $target = $(e.target);
if ($target.closest('[data-user-card]').data('userCard') ||
.on(clickOutsideEventName, (e) => {
if (this.get('controller.visible')) {
const $target = $(e.target);
if ($target.closest('[data-user-card]').data('userCard') ||
$target.closest('a.mention').length > 0 ||
$target.closest('#user-card').length > 0) {
return;
return;
}
this.get('controller').close();
}
self.get('controller').close();
return true;
});
const expand = (username, $target) => {
const postId = $target.parents('article').data('post-id'),
user = this.get('controller').show(username, postId, $target[0]);
if (user !== undefined) {
user.then( () => this._willShow($target) ).catch( () => this._hide() );
} else {
this._hide();
}
return true;
});
var expand = function(username, $target) {
const postId = $target.parents('article').data('post-id');
self.get('controller')
.show(username, postId, $target[0])
.then(function() {
self._willShow($target);
}).catch(function() {
self._hide();
});
return false;
};
$('#main-outlet').on(clickDataExpand, '[data-user-card]', function(e) {
$('#main-outlet').on(clickDataExpand, '[data-user-card]', (e) => {
if (e.ctrlKey || e.metaKey) { return; }
const $target = $(e.currentTarget),
username = $target.data('user-card');
username = $target.data('user-card');
return expand(username, $target);
});
$('#main-outlet').on(clickMention, 'a.mention', function(e) {
$('#main-outlet').on(clickMention, 'a.mention', (e) => {
if (e.ctrlKey || e.metaKey) { return; }
const $target = $(e.target),
username = $target.text().replace(/^@/, '');
username = $target.text().replace(/^@/, '');
return expand(username, $target);
});
this.appEvents.on('usercard:shown', this, '_shown');
}.on('didInsertElement'),
@ -89,9 +85,8 @@ export default Discourse.View.extend(CleansUp, {
_willShow(target) {
if (!target) { return; }
const self = this,
width = this.$().width();
Em.run.schedule('afterRender', function() {
const width = this.$().width();
Ember.run.schedule('afterRender', () => {
if (target) {
let position = target.offset();
if (position) {
@ -100,19 +95,20 @@ export default Discourse.View.extend(CleansUp, {
const overage = ($(window).width() - 50) - (position.left + width);
if (overage < 0) {
position.left += overage;
position.top += target.height() + 8;
position.top += target.height() + 48;
}
position.top -= $('#main-outlet').offset().top;
self.$().css(position);
this.$().css(position);
}
this.appEvents.trigger('usercard:shown');
}
});
},
_hide() {
if (!this.get('controller.visible')) {
this.$().css({ left: -9999, top: -9999 });
this.$().css({left: -9999, top: -9999});
}
},
@ -120,11 +116,19 @@ export default Discourse.View.extend(CleansUp, {
this.get('controller').close();
},
keyUp(e) {
if (e.keyCode === 27) { // ESC
const target = this.get('controller.cardTarget');
this.cleanUp();
target.focus();
}
},
_removeEvents: function() {
$('html').off(clickOutsideEventName);
$('#main').off(clickDataExpand)
.off(clickMention);
.off(clickMention);
this.appEvents.off('usercard:shown', this, '_shown');
}.on('willDestroyElement')

View File

@ -1,4 +1,6 @@
export default Ember.View.extend(Discourse.ScrollTop, {
import ScrollTop from 'discourse/mixins/scroll-top';
export default Ember.View.extend(ScrollTop, {
templateName: 'user/user',
userBinding: 'controller.content'
});

View File

@ -3,6 +3,7 @@ if Rails.env.development? || Rails.env.test?
require_asset ("ember-template-compiler.js")
require_asset ("ember.custom.debug.js")
else
require_asset ("ember-template-compiler.js")
require_asset ("ember.prod.js")
end
%>

View File

@ -0,0 +1,3 @@
//= depend_on 'client.bs_BA.yml'
//= require locales/i18n
<%= JsLocaleHelper.output_locale(:bs_BA) %>

View File

@ -107,7 +107,6 @@
.category .badge-notification {
background-color:transparent;
color: scale-color($primary, $lightness: 50%);
position: inherit;
}
.subcategories .badge {

Some files were not shown because too many files have changed in this diff Show More