Version bump

This commit is contained in:
Neil Lalonde 2015-02-03 14:18:35 -05:00
commit adc1a2e9a5
331 changed files with 5910 additions and 2922 deletions

View File

@ -46,18 +46,18 @@ GEM
ember-source
execjs
handlebars-source (>= 1.0.0.rc.4)
better_errors (2.1.0)
better_errors (2.1.1)
coderay (>= 1.0.0)
erubis (>= 2.6.6)
rack (>= 0.9.0)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
builder (3.2.2)
celluloid (0.15.2)
timers (~> 1.1.0)
celluloid (0.16.0)
timers (~> 4.0.0)
certified (1.0.0)
coderay (1.1.0)
connection_pool (2.0.0)
connection_pool (2.1.1)
crass (0.2.1)
daemons (1.1.9)
debug_inspector (0.0.2)
@ -81,7 +81,7 @@ GEM
ember-source (1.9.0.beta.4)
handlebars-source (~> 2.0)
erubis (2.7.0)
eventmachine (1.0.4)
eventmachine (1.0.5)
excon (0.42.1)
execjs (2.2.2)
exifr (1.1.3)
@ -179,10 +179,11 @@ GEM
sorcerer (>= 0.3.7)
guess_html_encoding (0.0.9)
handlebars-source (2.0.0)
hashie (3.3.1)
hashie (3.3.2)
highline (1.6.21)
hike (1.2.3)
hiredis (0.5.2)
hitimes (1.2.2)
htmlentities (4.3.3)
i18n (0.6.11)
image_optim (0.9.1)
@ -198,7 +199,7 @@ GEM
jquery-rails (3.1.2)
railties (>= 3.0, < 5.0)
thor (>= 0.14, < 2.0)
json (1.8.1)
json (1.8.2)
jwt (1.0.0)
kgio (2.9.2)
librarian (0.1.2)
@ -212,20 +213,20 @@ GEM
mime-types (~> 1.16)
treetop (~> 1.4.8)
memory_profiler (0.0.4)
message_bus (1.0.5)
message_bus (1.0.6)
eventmachine
rack (>= 1.1.3)
redis
metaclass (0.0.4)
method_source (0.8.2)
mime-types (1.25.1)
mini_portile (0.6.1)
mini_portile (0.6.2)
minitest (5.4.2)
mocha (1.1.0)
metaclass (~> 0.0.1)
mock_redis (0.13.2)
moneta (0.8.0)
msgpack (0.5.8)
msgpack (0.5.10)
multi_json (1.10.1)
multi_xml (0.5.5)
multipart-post (2.0.0)
@ -234,7 +235,7 @@ GEM
net-ssh (>= 2.6.5)
net-ssh (2.9.1)
netrc (0.7.7)
nokogiri (1.6.5)
nokogiri (1.6.6.2)
mini_portile (~> 0.6.0)
nokogumbo (1.1.12)
nokogiri
@ -245,7 +246,7 @@ GEM
multi_json (~> 1.3)
multi_xml (~> 0.5)
rack (~> 1.2)
oj (2.11.1)
oj (2.11.4)
omniauth (1.2.2)
hashie (>= 1.2, < 4)
rack (~> 1.0)
@ -271,7 +272,7 @@ GEM
omniauth-twitter (1.0.1)
multi_json (~> 1.3)
omniauth-oauth (~> 1.0)
onebox (1.5.6)
onebox (1.5.11)
moneta (~> 0.7)
multi_json (~> 1.7)
mustache (~> 0.99)
@ -279,7 +280,7 @@ GEM
openid-redis-store (0.0.2)
redis
ruby-openid
pg (0.18.0)
pg (0.18.1)
polyglot (0.3.5)
progress (3.0.1)
pry (0.10.1)
@ -290,7 +291,7 @@ GEM
pry (>= 0.9.10, < 0.11.0)
pry-rails (0.3.2)
pry (>= 0.9.10)
puma (2.9.1)
puma (2.11.0)
rack (>= 1.1, < 2.0)
qunit-rails (0.0.7)
railties
@ -328,7 +329,7 @@ GEM
rb-fsevent (0.9.4)
rb-inotify (0.9.5)
ffi (>= 0.5.0)
rbtrace (0.4.5)
rbtrace (0.4.6)
ffi (>= 1.0.6)
msgpack (>= 0.4.3)
trollop (>= 1.16.2)
@ -388,9 +389,9 @@ GEM
shoulda-context (1.2.1)
shoulda-matchers (2.7.0)
activesupport (>= 3.0.0)
sidekiq (3.2.5)
celluloid (= 0.15.2)
connection_pool (>= 2.0.0)
sidekiq (3.3.1)
celluloid (>= 0.16.0)
connection_pool (>= 2.1.1)
json
redis (>= 3.0.6)
redis-namespace (>= 1.3.1)
@ -424,19 +425,20 @@ GEM
therubyracer (0.12.1)
libv8 (~> 3.16.14.0)
ref
thin (1.6.2)
daemons (>= 1.0.9)
eventmachine (>= 1.0.0)
rack (>= 1.0.0)
thin (1.6.3)
daemons (~> 1.0, >= 1.0.9)
eventmachine (~> 1.0)
rack (~> 1.0)
thor (0.19.1)
thread_safe (0.3.4)
tilt (1.4.1)
timecop (0.7.1)
timers (1.1.0)
timers (4.0.1)
hitimes
treetop (1.4.15)
polyglot
polyglot (>= 0.3.1)
trollop (2.0)
trollop (2.1.1)
tzinfo (1.2.2)
thread_safe (~> 0.1)
uglifier (2.6.0)

4
Vagrantfile vendored
View File

@ -3,8 +3,8 @@
# See https://github.com/discourse/discourse/blob/master/docs/VAGRANT.md
#
Vagrant.configure("2") do |config|
config.vm.box = 'discourse-0.9.9.13'
config.vm.box_url = "https://d3fvb7b7auiut8.cloudfront.net/discourse-0.9.9.13.box"
config.vm.box= "discourse/discourse-0.9.9.15.box"
config.vm.box_url = "https://vagrantcloud.com/discourse/discourse-0.9.9.15.box"
# Make this VM reachable on the host network as well, so that other
# VM's running other browsers can access our dev server.

View File

@ -1,7 +1,6 @@
export default Em.ObjectController.extend({
needs: ['adminGroups'],
needs: ['adminGroupsType'],
disableSave: false,
usernames: null,
currentPage: function() {
if (this.get("user_count") == 0) { return 0; }
@ -59,28 +58,29 @@ export default Em.ObjectController.extend({
},
addMembers: function() {
// TODO: should clear the input
if (Em.isEmpty(this.get("usernames"))) { return; }
this.get("model").addMembers(this.get("usernames"));
// clear the user selector
this.set("usernames", null);
},
save: function() {
var self = this,
group = this.get('model');
group = this.get('model'),
groupsController = this.get("controllers.adminGroupsType");
self.set('disableSave', true);
this.set('disableSave', true);
var promise;
if (group.get('id')) {
if (group.get("id")) {
promise = group.save();
} else {
promise = group.create().then(function() {
var groupsController = self.get('controllers.adminGroups');
groupsController.addObject(group);
});
}
promise.then(function() {
self.send('showGroup', group);
self.transitionToRoute("adminGroup", group);
}, function(e) {
var message = $.parseJSON(e.responseText).errors;
bootbox.alert(message);
@ -91,12 +91,13 @@ export default Em.ObjectController.extend({
destroy: function() {
var group = this.get('model'),
groupsController = this.get('controllers.adminGroups'),
groupsController = this.get('controllers.adminGroupsType'),
self = this;
bootbox.confirm(I18n.t("admin.groups.delete_confirm"), I18n.t("no_value"), I18n.t("yes_value"), function(result) {
if (result) {
self.set('disableSave', true);
this.set('disableSave', true);
bootbox.confirm(I18n.t("admin.groups.delete_confirm"), I18n.t("no_value"), I18n.t("yes_value"), function(confirmed) {
if (confirmed) {
group.destroy().then(function() {
groupsController.get('model').removeObject(group);
self.transitionToRoute('adminGroups.index');

View File

@ -0,0 +1,16 @@
export default Ember.ArrayController.extend({
sortProperties: ['name'],
refreshingAutoGroups: false,
actions: {
refreshAutoGroups: function(){
var self = this;
this.set('refreshingAutoGroups', true);
Discourse.ajax('/admin/groups/refresh_automatic_groups', {type: 'POST'}).then(function() {
self.transitionToRoute("adminGroupsType", "automatic").then(function() {
self.set('refreshingAutoGroups', false);
});
});
}
}
});

View File

@ -1,24 +0,0 @@
export default Ember.ArrayController.extend({
sortProperties: ['name'],
refreshingAutoGroups: false,
actions: {
refreshAutoGroups: function(){
var self = this,
groups = this.get('model');
self.set('refreshingAutoGroups', true);
this.transitionToRoute('adminGroups.index').then(function() {
Discourse.ajax('/admin/groups/refresh_automatic_groups', {type: 'POST'}).then(function() {
return Discourse.Group.findAll().then(function(newGroups) {
groups.clear();
groups.addObjects(newGroups);
}).finally(function() {
self.set('refreshingAutoGroups', false);
});
});
});
}
}
});

View File

@ -17,15 +17,18 @@ export default Ember.ObjectController.extend(BufferedContent, {
if (this.get('required')) {
ret.push(I18n.t('admin.user_fields.required.enabled'));
}
if (this.get('show_on_profile')) {
ret.push(I18n.t('admin.user_fields.show_on_profile.enabled'));
}
return ret.join(', ');
}.property('editable', 'required'),
}.property('editable', 'required', 'show_on_profile'),
actions: {
save: function() {
var self = this;
var attrs = this.get('buffered').getProperties('name', 'description', 'field_type', 'editable', 'required');
var attrs = this.get('buffered').getProperties('name', 'description', 'field_type', 'editable', 'required', 'show_on_profile');
this.get('model').save(attrs).then(function(res) {
self.set('model.id', res.user_field.id);

View File

@ -1,17 +1,20 @@
Discourse.AdminGroupRoute = Discourse.Route.extend({
export default Discourse.Route.extend({
model: function(params) {
var groups = this.modelFor('adminGroups'),
var groups = this.modelFor('adminGroupsType'),
group = groups.findProperty('name', params.name);
if (!group) { return this.transitionTo('adminGroups.index'); }
return group;
},
setupController: function(controller, model) {
controller.set("model", model);
// clear the user selector
controller.set("usernames", null);
// load the members of the group
model.findMembers();
}
});

View File

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

View File

@ -0,0 +1,17 @@
export default Discourse.Route.extend({
model: function(params) {
return Discourse.Group.findAll().then(function(groups) {
return groups.filterBy("type", params.type);
});
},
actions: {
newGroup: function() {
var self = this;
this.transitionTo("adminGroupsType", "custom").then(function() {
var group = Discourse.Group.create({ automatic: false, visible: true });
self.transitionTo("adminGroup", group);
})
}
}
});

View File

@ -1,5 +1,7 @@
export default function() {
this.resource('admin', function() {
export default {
resource: 'admin',
map: function() {
this.route('dashboard', { path: '/' });
this.resource('adminSiteSettings', { path: '/site_settings' }, function() {
this.resource('adminSiteSettingsCategory', { path: 'category/:category_id'} );
@ -40,8 +42,10 @@ export default function() {
this.route('screenedUrls', { path: '/screened_urls' });
});
this.resource('adminGroups', { path: '/groups'}, function() {
this.resource('adminGroup', { path: '/:name' });
this.resource('adminGroups', { path: '/groups' }, function() {
this.resource('adminGroupsType', { path: '/:type' }, function() {
this.resource('adminGroup', { path: '/:name' });
});
});
this.resource('adminUsers', { path: '/users' }, function() {
@ -51,13 +55,12 @@ export default function() {
});
this.resource('adminUsersList', { path: '/list' }, function() {
this.route('show', {path: '/:filter'});
this.route('show', { path: '/:filter' });
});
});
this.resource('adminBadges', { path: '/badges' }, function() {
this.route('show', { path: '/:badge_id' });
});
});
}
}
};

View File

@ -1,31 +0,0 @@
/**
Handles routes for admin groups
@class AdminGroupsRoute
@extends Discourse.Route
@namespace Discourse
@module Discourse
**/
Discourse.AdminGroupsRoute = Discourse.Route.extend({
model: function() {
return Discourse.Group.findAll();
},
actions: {
showGroup: function(g) {
// This hack is needed because the autocomplete plugin does not
// refresh properly when the underlying data changes. TODO should
// be to update the plugin so it works properly and remove this hack.
var self = this;
this.transitionTo('adminGroups.index').then(function() {
self.transitionTo('adminGroup', g);
});
},
newGroup: function(){
var group = Discourse.Group.create({ visible: true });
this.send('showGroup', group);
}
}
});

View File

@ -13,7 +13,7 @@
<li>{{#link-to 'adminBadges.index'}}{{i18n 'admin.badges.title'}}{{/link-to}}</li>
{{/if}}
{{#if currentUser.admin}}
<li>{{#link-to 'adminGroups.index'}}{{i18n 'admin.groups.title'}}{{/link-to}}</li>
<li>{{#link-to 'adminGroups'}}{{i18n 'admin.groups.title'}}{{/link-to}}</li>
{{/if}}
<li>{{#link-to 'adminEmail'}}{{i18n 'admin.email.title'}}{{/link-to}}</li>
<li>{{#link-to 'adminFlags'}}{{i18n 'admin.flags.title'}}{{/link-to}}</li>
@ -23,6 +23,7 @@
<li>{{#link-to 'admin.api'}}{{i18n 'admin.api.title'}}{{/link-to}}</li>
<li>{{#link-to 'admin.backups'}}{{i18n 'admin.backups.title'}}{{/link-to}}</li>
{{/if}}
{{plugin-outlet "admin-menu" tagName="li"}}
</ul>
<div class='boxed white admin-content'>

View File

@ -118,7 +118,7 @@
</p>
<p class="actions">
<small>{{i18n 'admin.dashboard.last_checked'}}: {{problemsTimestamp}}</small>
<button {{action "refreshProblems"}} class="btn btn-small"><i class="fa fa-refresh"></i>{{i18n 'admin.dashboard.refresh_problems'}}</button>
{{d-button action="refreshProblems" class="btn-small" icon="refresh" label="admin.dashboard.refresh_problems"}}
</p>
</div>
<div class="clearfix"></div>
@ -130,7 +130,7 @@
<div class="problem-messages">
<p>
{{i18n 'admin.dashboard.no_problems'}}
<button {{action "refreshProblems"}} class="btn btn-small"><i class="fa fa-refresh"></i>{{i18n 'admin.dashboard.refresh_problems'}}</button>
{{d-button action="refreshProblems" class="btn-small" icon="refresh" label="admin.dashboard.refresh_problems"}}
</p>
</div>
<div class="clearfix"></div>

View File

@ -43,6 +43,17 @@
{{combo-box name="alias" valueAttribute="value" value=alias_level content=aliasLevelOptions}}
</div>
{{#unless automatic}}
<div>
<label for="automatic_membership">{{i18n 'admin.groups.automatic_membership_email_domains'}}</label>
{{list-setting name="automatic_membership" settingValue=emailDomains}}
<label>
{{input type="checkbox" checked=automatic_membership_retroactive}}
{{i18n 'admin.groups.automatic_membership_retroactive'}}
</label>
</div>
{{/unless}}
<div class='buttons'>
<button {{action "save"}} {{bind-attr disabled="disableSave"}} class='btn btn-primary'>{{i18n 'admin.customize.save'}}</button>
{{#unless automatic}}

View File

@ -1,20 +1,11 @@
<div class='row groups'>
<div class='content-list span6'>
<h3>{{i18n 'admin.groups.edit'}}</h3>
<ul>
{{#each group in arrangedContent}}
<li>
<a href='#' {{action "showGroup" group}}>{{group.name}} <span class="count">{{group.userCountDisplay}}</span></a>
</li>
{{/each}}
<div class="admin-controls">
<div class="span15">
<ul class="nav nav-pills">
<li>{{#link-to "adminGroupsType" "custom"}}{{i18n 'admin.groups.custom'}}{{/link-to}}</li>
<li>{{#link-to "adminGroupsType" "automatic"}}{{i18n 'admin.groups.automatic'}}{{/link-to}}</li>
</ul>
<div class='controls'>
<button class='btn' {{bind-attr disabled="refreshingAutoGroups"}} {{action "refreshAutoGroups"}}><i class="fa fa-refresh"></i>{{i18n 'admin.groups.refresh'}}</button>
<button class='btn' {{action "newGroup"}}><i class="fa fa-plus"></i>{{i18n 'admin.groups.new'}}</button>
</div>
</div>
<div class='content-editor'>
{{outlet}}
</div>
</div>
<div class="admin-container">
{{outlet}}
</div>

View File

@ -1 +0,0 @@
{{i18n 'admin.groups.about'}}

View File

@ -0,0 +1,20 @@
<div class='row groups'>
<div class='content-list span6'>
<h3>{{i18n 'admin.groups.edit'}}</h3>
<ul>
{{#each group in controller}}
<li>
{{#link-to "adminGroup" group.type group.name}}{{group.name}} <span class="count">{{group.userCountDisplay}}</span>{{/link-to}}
</li>
{{/each}}
</ul>
<div class='controls'>
{{d-button action="newGroup" icon="plus" label="admin.groups.new"}}
{{d-button action="refreshAutoGroups" icon="refresh" label="admin.groups.refresh" disabled=refreshingAutoGroups}}
</div>
</div>
<div class='content-editor'>
{{outlet}}
</div>
</div>

View File

@ -3,7 +3,7 @@
<div>
{{i18n 'admin.dashboard.reports.start_date'}} {{input type="date" value=startDate}}
{{i18n 'admin.dashboard.reports.end_date'}} {{input type="date" value=endDate}}
<button {{action "refreshReport"}} class='btn btn-primary'>{{i18n 'admin.dashboard.reports.refresh_report'}}</button>
{{d-button action="refreshReport" class="btn-primary" label="admin.dashboard.reports.refresh_report" icon="refresh"}}
</div>
<div class='view-options'>

View File

@ -3,6 +3,7 @@
</div>
<div class="setting-value">
{{combo-box valueAttribute="value" content=validValues value=value none=allowsNone}}
{{view.preview}}
<div class='desc'>{{unbound description}}</div>
</div>
{{#if dirty}}

View File

@ -32,6 +32,11 @@
{{input type="checkbox" checked=f.buffered.required}} {{i18n 'admin.user_fields.required.title'}}
</label>
</div>
<div class='form-element'>
<label>
{{input type="checkbox" checked=f.buffered.show_on_profile}} {{i18n 'admin.user_fields.show_on_profile.title'}}
</label>
</div>
</div>
{{else}}
<div class="row">

View File

@ -9,6 +9,16 @@
Discourse.SiteSettingView = Discourse.View.extend(Discourse.ScrollTop, {
classNameBindings: [':row', ':setting', 'content.overridden'],
preview: function() {
var preview = this.get('content.preview');
if(preview){
return new Handlebars.SafeString("<div class='preview'>" +
preview.replace("{{value}}",this.get('content.value')) +
"</div>"
);
}
}.property('content.value'),
templateName: function() {
// If we're editing a boolean, show a checkbox
if (this.get('content.type') === 'bool') return 'admin/templates/site_settings/setting_bool';

View File

@ -24,6 +24,12 @@ window.Discourse = Ember.Application.createWithMixins(Discourse.Ajax, {
return u + url;
},
getURLWithCDN: function(url) {
url = this.getURL(url);
if (Discourse.CDN) { url = Discourse.CDN + url; }
return url;
},
Resolver: DiscourseResolver,
_titleChanged: function() {

View File

@ -14,9 +14,6 @@ export default Em.Component.extend(UploadMixin, {
uploadDone: function(data) {
var self = this;
// indicates the users is using an uploaded avatar
this.set("custom_avatar_upload_id", data.result.upload_id);
// display a warning whenever the image is not a square
this.set("imageIsNotASquare", data.result.width !== data.result.height);
@ -26,6 +23,11 @@ export default Em.Component.extend(UploadMixin, {
// 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(function(avatarTemplate) {
self.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)
self.set("custom_avatar_upload_id", data.result.upload_id);
});
// the upload is now done

View File

@ -1,13 +1,12 @@
/**
Renders a drop down for selecting a category
var get = Ember.get;
@class CategoryDropComponent
@extends Ember.Component
@namespace Discourse
@module Discourse
**/
export default Ember.Component.extend({
classNameBindings: ['category::no-category', 'categories:has-drop'],
classNameBindings: ['category::no-category', 'categories:has-drop','categoryStyle'],
categoryStyle: function(){
return Discourse.SiteSettings.category_style;
}.property(),
tagName: 'li',
iconClass: function() {
@ -44,11 +43,20 @@ export default Ember.Component.extend({
badgeStyle: function() {
var category = this.get('category');
if (category) {
return Discourse.HTML.categoryStyle(category);
} else {
return "background-color: #eee; color: #333";
var color = get(category, 'color'),
textColor = get(category, 'text_color');
if (color || textColor) {
var style = "";
if (color) { style += "background-color: #" + color + "; border-color: #" + color + ";"; }
if (textColor) { style += "color: #" + textColor + "; "; }
return style;
}
}
return "background-color: #eee; color: #333";
}.property('category'),
clickEventName: function() {
@ -81,24 +89,33 @@ export default Ember.Component.extend({
self.close();
});
$('html').on(this.get('clickEventName'), function(e) {
var $target = $(e.target),
closest = $target.closest($dropdown);
Em.run.next(function(){
self.$('.cat a').add('html').on(self.get('clickEventName'), function(e) {
var $target = $(e.target),
closest = $target.closest($dropdown);
return ($(e.currentTarget).hasClass('badge-category') || (closest.length && closest[0] === $dropdown)) ? true : self.close();
if ($(e.currentTarget).hasClass('badge-wrapper')){
self.close();
}
return ($(e.currentTarget).hasClass('badge-category') || (closest.length && closest[0] === $dropdown)) ? true : self.close();
});
});
}
},
close: function() {
removeEvents: function(){
$('html').off(this.get('clickEventName'));
this.$('a[data-drop-close]').off('click.category-drop');
},
close: function() {
this.removeEvents();
this.set('expanded', false);
},
willDestroyElement: function() {
$('html').off(this.get('clickEventName'));
this.$('a[data-drop-close]').off('click.category-drop');
this.removeEvents();
}
});

View File

@ -1,3 +1,5 @@
import { categoryBadgeHTML } from 'discourse/helpers/category-link';
export default Ember.Component.extend({
_initializeAutocomplete: function(){
@ -25,7 +27,7 @@ export default Ember.Component.extend({
},
template: template,
transformComplete: function(category) {
return Discourse.HTML.categoryBadge(category, {allowUncategorized: true});
return categoryBadgeHTML(category, {allowUncategorized: true});
}
});
}.on('didInsertElement')

View File

@ -7,7 +7,7 @@ export default Em.Component.extend({
categoryUrl = Discourse.getURL('/c/') + Discourse.Category.slugFor(category),
categoryName = Handlebars.Utils.escapeExpression(category.get('name'));
if (category.get('read_restricted')) { buffer.push("<i class='fa fa-group'></i>"); }
if (category.get('read_restricted')) { buffer.push("<i class='fa fa-lock'></i>"); }
buffer.push("<a href='" + categoryUrl + "'>");
buffer.push("<span class='category-name'>" + categoryName + "</span>");

View File

@ -0,0 +1,31 @@
import { iconHTML } from 'discourse/helpers/fa-icon';
export default Ember.Component.extend({
tagName: 'button',
classNameBindings: [':btn'],
attributeBindings: ['disabled', 'translatedTitle:title'],
translatedTitle: function() {
var label = this.get('label');
if (label) {
return I18n.t(this.get('label'));
}
}.property('label'),
render: function(buffer) {
var title = this.get('translatedTitle'),
icon = this.get('icon');
if (title || icon) {
if (icon) { buffer.push(iconHTML(icon) + ' '); }
if (title) { buffer.push(title); }
} else {
// If no label or icon is present, yield
return this._super();
}
},
click: function() {
this.sendAction("action", this.get("actionParam"));
}
});

View File

@ -0,0 +1,4 @@
export default Ember.Component.extend({
tagName: 'span',
classNameBindings: [':fa-stack'],
});

View File

@ -10,6 +10,10 @@ export default Ember.Component.extend(StringBuffer, {
notices.push(I18n.t("read_only_mode.enabled"));
}
if (this.siteSettings.disable_emails) {
notices.push(I18n.t("emails_are_disabled"));
}
if (Discourse.User.currentProp('admin') && this.siteSettings.show_create_topics_notice) {
var topic_count = 0,
post_count = 0;

View File

@ -18,6 +18,10 @@ export default Ember.Component.extend({
return !!this.get('changeSort');
}.property(),
skipHeader: function() {
return Discourse.Mobile.mobileView;
}.property(),
showLikes: function(){
return this.get('order') === "likes";
}.property('order'),

View File

@ -16,18 +16,15 @@ export default TextField.extend({
return selected;
}
var template = this.container.lookup('template:user-selector-autocomplete.raw');
$(this.get('element')).val(this.get('usernames')).autocomplete({
template: template,
this.$().val(this.get('usernames')).autocomplete({
template: this.container.lookup('template:user-selector-autocomplete.raw'),
disabled: this.get('disabled'),
single: this.get('single'),
allowAny: this.get('allowAny'),
dataSource: function(term) {
term = term.replace(/[^a-zA-Z0-9_]/, '');
return userSearch({
term: term,
term: term.replace(/[^a-zA-Z0-9_]/, ''),
topicId: self.get('topicId'),
exclude: excludedUsernames(),
includeGroups: includeGroups
@ -58,6 +55,15 @@ export default TextField.extend({
}
});
}.on('didInsertElement')
}.on('didInsertElement'),
// THIS IS A HUGE HACK TO SUPPORT CLEARING THE INPUT
_clearInput: function() {
if (arguments.length > 1) {
if (Em.isEmpty(this.get("usernames"))) {
this.$().parent().find("a").click();
}
}
}.observes("usernames")
});

View File

@ -39,7 +39,34 @@ export default DiscourseController.extend({
// Import a quote from the post
importQuote: function() {
this.get('model').importQuote();
var postStream = this.get('topic.postStream'),
postId = this.get('model.post.id');
// If there is no current post, use the first post id from the stream
if (!postId && postStream) {
postId = postStream.get('firstPostId');
}
// If we're editing a post, fetch the reply when importing a quote
if (this.get('model.editingPost')) {
var replyToPostNumber = this.get('model.post.reply_to_post_number');
if (replyToPostNumber) {
var replyPost = postStream.get('posts').findBy('post_number', replyToPostNumber);
if (replyPost) {
postId = replyPost.get('id');
}
}
}
if (postId) {
this.set('model.loading', true);
var composer = this;
return Discourse.Post.load(postId).then(function(post) {
var quote = Discourse.Quote.build(post, post.get("raw"))
composer.appendBlockAtCursor(quote);
composer.set('model.loading', false);
});
}
},
cancel: function() {
@ -212,7 +239,9 @@ export default DiscourseController.extend({
}
if ((!composer.get('replyingToTopic')) || (!Discourse.User.currentProp('disable_jump_reply'))) {
Discourse.URL.routeTo(opts.post.get('url'));
if (opts.post) {
Discourse.URL.routeTo(opts.post.get('url'));
}
}
}, function(error) {
composer.set('disableDrafts', false);

View File

@ -8,7 +8,6 @@ export default DiscoveryController.extend({
actions: {
refresh: function() {
var self = this;
// Don't refresh if we're still loading
if (this.get('controllers.discovery.loading')) { return; }
@ -17,7 +16,17 @@ export default DiscoveryController.extend({
// router and ember throws an error due to missing `handlerInfos`.
// Lesson learned: Don't call `loading` yourself.
this.set('controllers.discovery.loading', true);
Discourse.CategoryList.list('categories').then(function(list) {
var parentCategory = this.get('model.parentCategory');
var promise;
if (parentCategory) {
promise = Discourse.CategoryList.listForParent(parentCategory);
} else {
promise = Discourse.CategoryList.list();
}
var self = this;
promise.then(function(list) {
self.set('model', list);
self.send('loadingComplete');
});

View File

@ -50,6 +50,8 @@ var controllerOpts = {
// Lesson learned: Don't call `loading` yourself.
this.set('controllers.discovery.loading', true);
Discourse.TopicList.find(filter).then(function(list) {
Discourse.TopicList.hideUniformCategory(list, self.get('category'));
self.setProperties({ model: list, selected: [] });
var tracking = Discourse.TopicTrackingState.current();

View File

@ -1,5 +1,6 @@
import ModalFunctionality from 'discourse/mixins/modal-functionality';
import ObjectController from 'discourse/controllers/object';
import { categoryBadgeHTML } from 'discourse/helpers/category-link';
// Modal for editing / creating a category
export default ObjectController.extend(ModalFunctionality, {
@ -69,7 +70,7 @@ export default ObjectController.extend(ModalFunctionality, {
parent_category_id: parseInt(this.get('parent_category_id'),10),
read_restricted: this.get('model.read_restricted')
});
return Discourse.HTML.categoryBadge(c, {showParent: true, link: false});
return categoryBadgeHTML(c, {link: false});
}.property('parent_category_id', 'categoryName', 'color', 'text_color'),
// background colors are available as a pipe-separated string

View File

@ -1,5 +1,6 @@
import ModalFunctionality from 'discourse/mixins/modal-functionality';
import ObjectController from 'discourse/controllers/object';
import { categoryBadgeHTML } from 'discourse/helpers/category-link';
// This controller handles displaying of history
export default ObjectController.extend(ModalFunctionality, {
@ -22,14 +23,14 @@ export default ObjectController.extend(ModalFunctionality, {
hide: function(postId, postVersion) {
var self = this;
Discourse.Post.hideRevision(postId, postVersion).then(function (result) {
Discourse.Post.hideRevision(postId, postVersion).then(function () {
self.refresh(postId, postVersion);
});
},
show: function(postId, postVersion) {
var self = this;
Discourse.Post.showRevision(postId, postVersion).then(function (result) {
Discourse.Post.showRevision(postId, postVersion).then(function () {
self.refresh(postId, postVersion);
});
},
@ -65,47 +66,38 @@ export default ObjectController.extend(ModalFunctionality, {
displayingSideBySideMarkdown: Em.computed.equal("viewMode", "side_by_side_markdown"),
previousCategory: function() {
var changes = this.get("category_changes");
var changes = this.get("category_id_changes");
if (changes) {
var category = Discourse.Category.findById(changes["previous"]);
return Discourse.HTML.categoryBadge(category, { allowUncategorized: true });
return categoryBadgeHTML(category, { allowUncategorized: true });
}
}.property("category_changes"),
}.property("category_id_changes"),
currentCategory: function() {
var changes = this.get("category_changes");
var changes = this.get("category_id_changes");
if (changes) {
var category = Discourse.Category.findById(changes["current"]);
return Discourse.HTML.categoryBadge(category, { allowUncategorized: true });
return categoryBadgeHTML(category, { allowUncategorized: true });
}
}.property("category_changes"),
}.property("category_id_changes"),
wiki_diff: function() {
var changes = this.get("wiki_changes")
if (changes) {
return changes["current"] ?
'<span class="fa-stack"><i class="fa fa-pencil-square-o fa-stack-2x"></i></span>' :
'<span class="fa-stack"><i class="fa fa-pencil-square-o fa-stack-2x"></i><i class="fa fa-ban fa-stack-2x"></i></span>';
}
}.property("wiki_changes"),
wikiDisabled: function() {
var changes = this.get("wiki_changes");
return changes && !changes['current'];
}.property('wiki_changes'),
post_type_diff: function () {
var moderator = Discourse.Site.currentProp('post_types.moderator_action');
postTypeDisabled: function () {
var changes = this.get("post_type_changes");
if (changes) {
return changes["current"] == moderator ?
'<span class="fa-stack"><i class="fa fa-shield fa-stack-2x"></i></span>' :
'<span class="fa-stack"><i class="fa fa-shield fa-stack-2x"></i><i class="fa fa-ban fa-stack-2x"></i></span>';
}
return (changes && changes['current'] !== this.site.get('post_types.moderator_action'));
}.property("post_type_changes"),
title_diff: function() {
titleDiff: function() {
var viewMode = this.get("viewMode");
if (viewMode === "side_by_side_markdown") { viewMode = "side_by_side"; }
return this.get("title_changes." + viewMode);
}.property("viewMode", "title_changes"),
body_diff: function() {
bodyDiff: function() {
return this.get("body_changes." + this.get("viewMode"));
}.property("viewMode", "body_changes"),

View File

@ -160,6 +160,12 @@ export default DiscourseController.extend(ModalFunctionality, {
this.set('authenticate', null);
return;
}
if (options.suspended) {
this.send('showLogin');
this.flash(options.suspended_message, 'error');
this.set('authenticate', null);
return;
}
// Reload the page if we're authenticated
if (options.authenticated) {
if (window.location.pathname === Discourse.getURL('/login')) {

View File

@ -35,15 +35,19 @@ export default ObjectController.extend(CanCheckEmails, {
canEditName: Discourse.computed.setting('enable_names'),
canSelectTitle: function() {
return Discourse.SiteSettings.enable_badges && this.get('model.has_title_badges');
return this.siteSettings.enable_badges && this.get('model.has_title_badges');
}.property('model.badge_count'),
canChangePassword: function() {
return !Discourse.SiteSettings.enable_sso && Discourse.SiteSettings.enable_local_logins;
return !this.siteSettings.enable_sso && this.siteSettings.enable_local_logins;
}.property(),
canReceiveDigest: function() {
return !this.siteSettings.disable_digest_emails;
}.property(),
availableLocales: function() {
return Discourse.SiteSettings.available_locales.split('|').map( function(s) {
return this.siteSettings.available_locales.split('|').map( function(s) {
return {name: s, value: s};
});
}.property(),
@ -166,5 +170,3 @@ export default ObjectController.extend(CanCheckEmails, {
}
});

View File

@ -3,7 +3,9 @@ export default Em.ObjectController.extend({
actions: {
markFaqRead: function() {
Discourse.ajax("/users/read-faq", { method: "POST" });
if (Discourse.User.current()) {
Discourse.ajax("/users/read-faq", { method: "POST" });
}
}
}
});

View File

@ -42,6 +42,21 @@ export default ObjectController.extend(CanCheckEmails, {
return this.get('can_be_deleted') && this.get('can_delete_all_posts');
}.property('can_be_deleted', 'can_delete_all_posts'),
publicUserFields: function() {
var siteUserFields = this.site.get('user_fields');
if (!Ember.isEmpty(siteUserFields)) {
var userFields = this.get('user_fields');
return siteUserFields.filterProperty('show_on_profile', true).sortBy('id').map(function(uf) {
var val = userFields ? userFields[uf.get('id').toString()] : null;
if (Ember.isEmpty(val)) {
return null;
} else {
return Ember.Object.create({value: val, field: uf});
}
}).compact();
}
}.property('user_fields.@each.value'),
privateMessagesActive: Em.computed.equal('pmView', 'index'),
privateMessagesMineActive: Em.computed.equal('pmView', 'mine'),
privateMessagesUnreadActive: Em.computed.equal('pmView', 'unread'),

View File

@ -1,4 +1,5 @@
var esc = Handlebars.Utils.escapeExpression;
Discourse.BBCode.register('quote', {noWrap: true, singlePara: true}, function(contents, bbParams, options) {
var params = {'class': 'quote'},
username = null;

View File

@ -1,4 +1,70 @@
import registerUnbound from 'discourse/helpers/register-unbound';
import { iconHTML } from 'discourse/helpers/fa-icon';
var get = Em.get,
escapeExpression = Handlebars.Utils.escapeExpression;
function categoryStripe(color, classes) {
var style = color ? "style='background-color: #" + color + ";'" : "";
return "<span class='" + classes + "' " + style + "></span>";
}
export function categoryBadgeHTML(category, opts) {
opts = opts || {};
if ((!category) ||
(!opts.allowUncategorized &&
Em.get(category, 'id') === Discourse.Site.currentProp("uncategorized_category_id") &&
Discourse.SiteSettings.suppress_uncategorized_badge
)
) return "";
var description = get(category, 'description_text'),
restricted = get(category, 'read_restricted'),
url = Discourse.getURL("/c/") + Discourse.Category.slugFor(category),
href = (opts.link === false ? '' : url),
tagName = (opts.link === false || opts.link === "false" ? 'span' : 'a'),
extraClasses = (opts.extraClasses ? (' ' + opts.extraClasses) : ''),
color = get(category, 'color'),
html = "",
parentCat = null;
if (!opts.hideParent) {
parentCat = Discourse.Category.findById(get(category, 'parent_category_id'));
}
if (parentCat && parentCat !== category) {
html += categoryStripe(get(parentCat,'color'), "badge-category-parent-bg");
}
html += categoryStripe(color, "badge-category-bg");
var classNames = "badge-category clear-badge";
if (restricted) { classNames += " restricted"; }
var textColor = "#" + get(category, 'text_color');
html += "<span" + ' style="color: ' + textColor + ';" '+
'data-drop-close="true" class="' + classNames + '"' +
(description ? 'title="' + escapeExpression(description) + '" ' : '') +
">";
var name = escapeExpression(get(category, 'name'));
if (restricted) {
html += iconHTML('lock') + " " + name;
} else {
html += name;
}
html += "</span>";
if(href){
href = " href='" + href + "' ";
}
extraClasses = Discourse.SiteSettings.category_style ? Discourse.SiteSettings.category_style + extraClasses : extraClasses;
return "<" + tagName + " class='badge-wrapper " + extraClasses + "' " + href + ">" + html + "</" + tagName + ">";
}
export function categoryLinkHTML(category, options) {
var categoryOptions = {};
@ -9,12 +75,11 @@ export function categoryLinkHTML(category, options) {
if (options) {
if (options.allowUncategorized) { categoryOptions.allowUncategorized = true; }
if (options.showParent) { categoryOptions.showParent = true; }
if (options.onlyStripe) { categoryOptions.onlyStripe = true; }
if (options.link !== undefined) { categoryOptions.link = options.link; }
if (options.extraClasses) { categoryOptions.extraClasses = options.extraClasses; }
if (options.hideParent) { categoryOptions.hideParent = true; }
}
return new Handlebars.SafeString(Discourse.HTML.categoryBadge(category, categoryOptions));
return new Handlebars.SafeString(categoryBadgeHTML(category, categoryOptions));
}
registerUnbound('category-link', categoryLinkHTML);

View File

@ -1,12 +1,22 @@
Handlebars.registerHelper('fa-icon', function(icon, options) {
var labelKey;
if (options.hash) { labelKey = options.hash.label; }
import registerUnbound from 'discourse/helpers/register-unbound';
var html = "<i class='fa fa-" + icon + "'";
if (labelKey) { html += " aria-hidden='true'"; }
export function iconClasses(icon, modifier) {
var classes = "fa fa-" + icon;
if (modifier) { classes += " fa-" + modifier; }
return classes;
}
export function iconHTML(icon, label, modifier) {
var html = "<i class='" + iconClasses(icon, modifier) + "'";
if (label) { html += " aria-hidden='true'"; }
html += "></i>";
if (labelKey) {
html += "<span class='sr-only'>" + I18n.t(labelKey) + "</span>";
if (label) {
html += "<span class='sr-only'>" + I18n.t(label) + "</span>";
}
return new Handlebars.SafeString(html);
return html;
}
registerUnbound('fa-icon', function(icon, params) {
return new Handlebars.SafeString(iconHTML(icon, params.label, params.modifier));
});

View File

@ -88,20 +88,21 @@ export default function(connectionName, options) {
if (!_connectorCache) { buildConnectorCache(); }
if (_connectorCache[connectionName]) {
var view;
var viewClass;
var childViews = _connectorCache[connectionName];
// If there is more than one view, create a container. Otherwise
// just shove it in.
if (childViews.length > 1) {
view = Ember.ContainerView.extend({
viewClass = Ember.ContainerView.extend({
childViews: childViews
});
} else {
view = childViews[0];
viewClass = childViews[0];
}
delete options.fn; // we don't need the default template since we have a connector
return Ember.Handlebars.helpers.view.call(this, view, options);
return Ember.Handlebars.helpers.view.call(this, viewClass, options);
} else if (options.fn) {
// If a block is passed, render its content.
return Ember.Handlebars.helpers.view.call(this,

View File

@ -0,0 +1,10 @@
export default {
name: 'ie9-hacks',
initialize: function() {
if (!window) { return; }
// IE9 does not support a console object unless the developer tools are open
if (!window.console) { window.console = {}; }
if (!window.console.log) { window.console.log = Ember.K; }
}
};

View File

@ -1,11 +1,12 @@
import Session from 'discourse/models/session';
import AppEvents from 'discourse/lib/app-events';
export default {
name: "inject-objects",
initialize: function(container, application) {
// Inject appEvents everywhere
var appEvents = Ember.Object.createWithMixins(Ember.Evented);
var appEvents = AppEvents.create();
application.register('app-events:main', appEvents, { instantiate: false });
application.inject('controller', 'appEvents', 'app-events:main');

View File

@ -1676,7 +1676,7 @@
return querypart.replace(/\+/g, " "); // in the query string, a plus and a space are identical
});
link = decodeURIComponent(link); // unencode first, to prevent double encoding
link = encodeURI(link).replace(/'/g, '%27').replace(/\(/g, '%28').replace(/\)/g, '%29');
link = encodeURI(link).replace(/#/g, '%23').replace(/'/g, '%27').replace(/\(/g, '%28').replace(/\)/g, '%29');
link = link.replace(/\?.*$/, function (querypart) {
return querypart.replace(/\+/g, "%2b"); // since we replaced plus with spaces in the query part, all pluses that now appear where originally encoded
});

View File

@ -0,0 +1,23 @@
export default Ember.Object.extend(Ember.Evented);
var id = 1;
function newKey() {
return "_view_app_event_" + (id++);
}
export function createViewListener(eventName, cb) {
var extension = {};
extension[newKey()] = function() {
this.appEvents.on(eventName, this, cb);
}.on('didInsertElement');
extension[newKey()] = function() {
this.appEvents.off(eventName, this, cb);
}.on('willDestroyElement');
return extension;
}
export function listenForViewEvent(viewClass, eventName, cb) {
viewClass.reopen(createViewListener(eventName, cb));
}

View File

@ -40,34 +40,89 @@ var closeSelector = function(){
$('body, textarea').off('keydown.emoji');
};
var ungroupedIcons;
var ungroupedIcons, recentlyUsedIcons;
var initializeUngroupedIcons = function(){
ungroupedIcons = [];
var groupedIcons = {};
_.each(groups, function(group){
_.each(group.icons, function(icon){
groupedIcons[icon] = true;
});
});
var emojis = Discourse.Emoji.list();
_.each(emojis, function(emoji){
if(groupedIcons[emoji] !== true){
ungroupedIcons.push(emoji);
}
});
if(ungroupedIcons.length > 0){
groups.push({name: 'ungrouped', icons: ungroupedIcons});
}
};
try {
if (localStorage && !localStorage.emojiUsage) { localStorage.emojiUsage = "{}"; }
} catch(e){
/* localStorage can be disabled, or cookies disabled, do not crash script here
* TODO introduce a global wrapper for dealing with local storage
* */
}
var trackEmojiUsage = function(title){
var recent = JSON.parse(localStorage.emojiUsage);
if (!recent[title]) { recent[title] = { title: title, usage: 0 }; }
recent[title]["usage"]++;
localStorage.emojiUsage = JSON.stringify(recent);
// clear the cache
recentlyUsedIcons = null;
};
var initializeRecentlyUsedIcons = function(){
recentlyUsedIcons = [];
var usage = _.map(JSON.parse(localStorage.emojiUsage));
usage.sort(function(a,b){
if(a.usage > b.usage){
return -1;
}
if(b.usage > a.usage){
return 1;
}
return a.title.localeCompare(b.title);
});
var recent = _.take(usage, PER_ROW);
if(recent.length > 0){
_.each(recent, function(emoji){
recentlyUsedIcons.push(emoji.title);
});
var recentGroup = _.find(groups, {name: 'recent'});
if(!recentGroup){
recentGroup = {name: 'recent', icons: []};
groups.push(recentGroup);
}
recentGroup.icons = recentlyUsedIcons;
}
};
var toolbar = function(selected){
if(!ungroupedIcons){
ungroupedIcons = [];
var groupedIcons = {};
_.each(groups, function(group){
_.each(group.icons, function(icon){
groupedIcons[icon] = true;
});
});
var emojis = Discourse.Emoji.list();
_.each(emojis,function(emoji){
if(groupedIcons[emoji] !== true){
ungroupedIcons.push(emoji);
}
});
if(ungroupedIcons.length > 0){
groups.push({name: 'ungrouped', icons: ungroupedIcons});
}
}
if (!ungroupedIcons) { initializeUngroupedIcons(); }
if (!recentlyUsedIcons) { initializeRecentlyUsedIcons(); }
return _.map(groups, function(g, i){
var row = {src: Discourse.Emoji.urlFor(g.icons[0]), groupId: i};
if(i===selected){
var icon = g.name === "recent" ? "star2" : g.icons[0];
var row = {src: Discourse.Emoji.urlFor(icon), groupId: i};
if(i === selected){
row.selected = true;
}
return row;
@ -80,9 +135,11 @@ var bindEvents = function(page,offset){
var composerController = Discourse.__container__.lookup('controller:composer');
$('.emoji-page a').click(function(){
composerController.appendTextAtCursor(":" + $(this).attr('title') + ":", {space: true});
closeSelector();
return false;
var title = $(this).attr('title');
trackEmojiUsage(title)
composerController.appendTextAtCursor(":" + title + ":", {space: true});
closeSelector();
return false;
}).hover(function(){
var title = $(this).attr('title');
var html = "<img src='" + Discourse.Emoji.urlFor(title) + "' class='emoji'> <span>:" + title + ":<span>";

View File

@ -7,6 +7,7 @@ var emoji = <%= Emoji.standard.map(&:name).flatten.inspect %>;
var extendedEmoji = {};
Discourse.Dialect.registerEmoji = function(code, url) {
code = code.toLowerCase();
extendedEmoji[code] = url;
};
@ -53,13 +54,13 @@ var search = function(term, options) {
Discourse.Emoji.search = search;
var emojiHash = {};
emoji.forEach(function(code){
emojiHash[code] = true;
});
emoji.forEach(function(code){ emojiHash[code] = true; });
var urlFor = function(code) {
var url, set = Discourse.SiteSettings.emoji_set;
code = code.toLowerCase();
if(extendedEmoji.hasOwnProperty(code)) {
url = extendedEmoji[code];
}
@ -82,10 +83,12 @@ var urlFor = function(code) {
Discourse.Emoji.urlFor = urlFor;
Discourse.Emoji.exists = function(code){
return !!(extendedEmoji.hasOwnProperty(code) || emojiHash.hasOwnProperty(code));
code = code.toLowerCase();
return !!(extendedEmoji.hasOwnProperty(code) || emojiHash.hasOwnProperty(code));
}
function imageFor(code) {
code = code.toLowerCase();
var url = urlFor(code);
if (url) {
return ['img', { href: url, title: ':' + code + ':', 'class': 'emoji', alt: code }];
@ -145,7 +148,7 @@ Object.keys(translations).forEach(function (t) {
});
function escapeRegExp(s) {
return s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
return s.replace(/[-/\\^$*+?.()|[\]{}]/gi, '\\$&');
}
var translationColonRegexp = new RegExp(Object.keys(translationsWithColon).map(function (t) {

View File

@ -1,11 +1,3 @@
/**
Helpers to build HTML strings as well as custom fragments.
@class HTML
@namespace Discourse
@module Discourse
**/
var customizations = {};
Discourse.HTML = {
@ -15,9 +7,6 @@ Discourse.HTML = {
using `setCustomHTML(key, html)`. This is used by a handlebars helper to find
the HTML content it wants. It will also check the `PreloadStore` for any server
side preloaded HTML.
@method getCustomHTML
@param {String} key to lookup
**/
getCustomHTML: function(key) {
var c = customizations[key];
@ -31,104 +20,9 @@ Discourse.HTML = {
}
},
/**
Set a fragment of HTML by key. It can then be looked up with `getCustomHTML(key)`.
@method setCustomHTML
@param {String} key to store the html
@param {String} html fragment to store
**/
// Set a fragment of HTML by key. It can then be looked up with `getCustomHTML(key)`.
setCustomHTML: function(key, html) {
customizations[key] = html;
},
/**
Returns the CSS styles for a category
@method categoryStyle
@param {Discourse.Category} category the category whose link we want
**/
categoryStyle: function(category) {
var color = Em.get(category, 'color'),
textColor = Em.get(category, 'text_color');
if (!color && !textColor) { return; }
// Add the custom style if we need to
var style = "";
if (color) { style += "background-color: #" + color + "; "; }
if (textColor) { style += "color: #" + textColor + "; "; }
return style;
},
/**
Create a category badge
@method categoryBadge
@param {Discourse.Category} category the category whose link we want
@param {Object} opts The options for the category link
@param {Boolean} opts.allowUncategorized Whether we allow rendering of the uncategorized category (default false)
@param {Boolean} opts.showParent Whether to visually show whether category is a sub-category (default false)
@param {Boolean} opts.link Whether this category badge should link to the category (default true)
@param {String} opts.extraClasses add this string to the class attribute of the badge
@returns {String} the html category badge
**/
categoryBadge: function(category, opts) {
opts = opts || {};
if ((!category) ||
(!opts.allowUncategorized &&
Em.get(category, 'id') === Discourse.Site.currentProp("uncategorized_category_id") &&
Discourse.SiteSettings.suppress_uncategorized_badge
)
) return "";
var name = Em.get(category, 'name'),
description = Em.get(category, 'description_text'),
restricted = Em.get(category, 'read_restricted'),
url = Discourse.getURL("/c/") + Discourse.Category.slugFor(category),
elem = (opts.link === false ? 'span' : 'a'),
extraClasses = (opts.extraClasses ? (' ' + opts.extraClasses) : ''),
html = "<" + elem + " href=\"" + (opts.link === false ? '' : url) + "\" ",
categoryStyle;
// Parent stripe implies onlyStripe
if (opts.onlyStripe) { opts.showParent = true; }
html += "data-drop-close=\"true\" class=\"badge-category" + (restricted ? ' restricted' : '' ) +
(opts.onlyStripe ? ' clear-badge' : '') +
extraClasses + "\" ";
name = Handlebars.Utils.escapeExpression(name);
// Add description if we have it, without tags. Server has sanitized the description value.
if (description) html += "title=\"" + Handlebars.Utils.escapeExpression(description) + "\" ";
if (!opts.onlyStripe) {
categoryStyle = Discourse.HTML.categoryStyle(category);
if (categoryStyle) {
html += "style=\"" + categoryStyle + "\" ";
}
}
if (restricted) {
html += "><div><i class='fa fa-group'></i> " + name + "</div></" + elem + ">";
} else {
html += ">" + name + "</" + elem + ">";
}
if (opts.onlyStripe || (opts.showParent && category.get('parent_category_id'))) {
var parent = Discourse.Category.findById(category.get('parent_category_id'));
if (!parent) { parent = category; }
categoryStyle = Discourse.HTML.categoryStyle(opts.onlyStripe ? category : parent) || '';
html = "<span class='badge-wrapper'><" + elem + " class='badge-category-parent" + extraClasses + "' style=\"" + categoryStyle +
"\" href=\"" + (opts.link === false ? '' : url) + "\"><span class='category-name'>" +
(Em.get(parent, 'read_restricted') ? "<i class='fa fa-group'></i> " : "") +
Em.get(parent, 'name') + "</span></" + elem + ">" +
html + "</span>";
}
return html;
}
};

View File

@ -8,7 +8,6 @@ var PATH_BINDINGS = {
},
SELECTED_POST_BINDINGS = {
'b': 'toggleBookmark',
'd': 'deletePost',
'e': 'editPost',
'l': 'toggleLike',
@ -50,7 +49,8 @@ var PATH_BINDINGS = {
'ctrl+f': 'showBuiltinSearch',
'command+f': 'showBuiltinSearch',
'?': 'showHelpModal', // open keyboard shortcut help
'q': 'quoteReply'
'q': 'quoteReply',
'b': 'toggleBookmark'
};
@ -65,6 +65,11 @@ Discourse.KeyboardShortcuts = Ember.Object.createWithMixins({
_.each(FUNCTION_BINDINGS, this._bindToFunction, this);
},
toggleBookmark: function(){
this.sendToSelectedPost('toggleBookmark');
this.sendToTopicListItemView('toggleBookmark');
},
quoteReply: function(){
$('.topic-post.selected button.create').click();
// lazy but should work for now
@ -157,19 +162,32 @@ Discourse.KeyboardShortcuts = Ember.Object.createWithMixins({
Discourse.__container__.lookup('controller:application').send('showKeyboardShortcutsHelp');
},
_bindToSelectedPost: function(action, binding) {
sendToTopicListItemView: function(action){
var elem = $('tr.selected.topic-list-item.ember-view')[0];
if(elem){
var view = Ember.View.views[elem.id];
view.send(action);
}
},
sendToSelectedPost: function(action){
var container = this.container;
// TODO: We should keep track of the post without a CSS class
var selectedPostId = parseInt($('.topic-post.selected article.boxed').data('post-id'), 10);
if (selectedPostId) {
var topicController = container.lookup('controller:topic'),
post = topicController.get('postStream.posts').findBy('id', selectedPostId);
if (post) {
topicController.send(action, post);
}
}
},
_bindToSelectedPost: function(action, binding) {
var self = this;
this.keyTrapper.bind(binding, function() {
// TODO: We should keep track of the post without a CSS class
var selectedPostId = parseInt($('.topic-post.selected article.boxed').data('post-id'), 10);
if (selectedPostId) {
var topicController = container.lookup('controller:topic'),
post = topicController.get('postStream.posts').findBy('id', selectedPostId);
if (post) {
topicController.send(action, post);
}
}
self.sendToSelectedPost(action);
});
},
@ -244,9 +262,14 @@ Discourse.KeyboardShortcuts = Ember.Object.createWithMixins({
var $article = $articles.eq(index + direction);
if ($article.size() > 0) {
$articles.removeClass('selected');
$article.addClass('selected');
if($article.is('.topic-list-item')){
this.sendToTopicListItemView('select');
}
if ($article.is('.topic-post')) {
var tabLoc = $article.find('a.tabLoc');
if (tabLoc.length === 0) {

View File

@ -2,7 +2,7 @@
var jumpScheduled = false,
rewrites = [];
Discourse.URL = Em.Object.createWithMixins({
Discourse.URL = Ember.Object.createWithMixins({
// Used for matching a topic
TOPIC_REGEXP: /\/t\/([^\/]+)\/(\d+)\/?(\d+)?/,
@ -203,7 +203,6 @@ Discourse.URL = Em.Object.createWithMixins({
var container = Discourse.__container__,
topicController = container.lookup('controller:topic'),
topicProgressController = container.lookup('controller:topic-progress'),
opts = {},
postStream = topicController.get('postStream');
@ -211,16 +210,18 @@ Discourse.URL = Em.Object.createWithMixins({
if (path.match(/last$/)) { opts.nearPost = topicController.get('highest_post_number'); }
var closest = opts.nearPost || 1;
var self = this;
postStream.refresh(opts).then(function() {
topicController.setProperties({
currentPost: closest,
highlightOnInsert: closest,
enteredAt: new Date().getTime().toString()
});
var closestPost = postStream.closestPostForPostNumber(closest),
progress = postStream.progressIndexOfPost(closestPost);
topicProgressController.set('progressPosition', progress);
Discourse.PostView.considerHighlighting(topicController, closest);
progress = postStream.progressIndexOfPost(closestPost),
progressController = container.lookup('controller:topic-progress');
progressController.set('progressPosition', progress);
self.appEvents.trigger('post:highlight', closest);
}).then(function() {
Discourse.URL.jumpToPost(closest);
});

View File

@ -162,10 +162,13 @@ Discourse.Utilities = {
}
var upload = files[0];
var type = Discourse.Utilities.isAnImage(upload.name) ? 'image' : 'attachment';
// CHROME ONLY: if the image was pasted, sets its name to a default one
if (upload instanceof Blob && !(upload instanceof File) && upload.type === "image/png") { upload.name = "blob.png"; }
if (typeof Blob !== "undefined" && typeof File !== "undefined") {
if (upload instanceof Blob && !(upload instanceof File) && upload.type === "image/png") { upload.name = "blob.png"; }
}
var type = Discourse.Utilities.isAnImage(upload.name) ? 'image' : 'attachment';
return Discourse.Utilities.validateUploadedFile(upload, type, bypassNewUserRestriction);
},
@ -286,7 +289,7 @@ Discourse.Utilities = {
// deal with meaningful errors first
if (data.jqXHR) {
switch (data.jqXHR.status) {
// cancel from the user
// cancelled by the user
case 0: return;
// entity too large, usually returned from the web server

View File

@ -1,23 +1,23 @@
// Mix this in to a view that has a `categoryId` property to automatically
// Mix this in to a view that has a `categorySlug` property to automatically
// add it to the body as the view is entered / left / model is changed.
// This is used for keeping the `body` style in sync for the background image.
export default {
_observeOnce: function() { this.get('categoryId'); }.on('init'),
_enterView: function() { this.get('categorySlug'); }.on('init'),
_removeClasses: function() {
$('body').removeClass(function(idx, css) {
return (css.match(/\bcategory-\d+/g) || []).join(' ');
return (css.match(/\bcategory-\S+/g) || []).join(' ');
});
},
_categoryChanged: function() {
var categoryId = this.get('categoryId');
var categorySlug = this.get('categorySlug');
this._removeClasses();
if (categoryId) {
$('body').addClass('category-' + categoryId);
if (categorySlug) {
$('body').addClass('category-' + categorySlug);
}
}.observes('categoryId'),
}.observes('categorySlug'),
_leaveView: function() { this._removeClasses(); }.on('willDestroyElement')
};

View File

@ -63,6 +63,8 @@ Discourse.Ajax = Em.Mixin.create({
xhr.jqTextStatus = textStatus;
xhr.requestedUrl = url;
// TODO is this sequence correct? we are calling catch defined externally before
// the error that was defined inline, it should probably be in reverse
Ember.run(null, reject, xhr);
if (oldError) oldError(xhr);
};

View File

@ -11,17 +11,15 @@ export default Em.Mixin.create({
},
_initializeUploader: function() {
// NOTE: we can't cache this as fileupload replaces the input after upload
// cf. https://github.com/blueimp/jQuery-File-Upload/wiki/Frequently-Asked-Questions#why-is-the-file-input-field-cloned-and-replaced-after-each-selection
var $upload = this.$('input[type=file]'),
self = this;
var $upload = this.$(),
self = this,
csrf = Discourse.Session.currentProp("csrfToken");
$upload.fileupload({
url: this.get('uploadUrl'),
url: this.get('uploadUrl') + ".json?authenticity_token=" + encodeURIComponent(csrf),
dataType: "json",
fileInput: $upload,
dropZone: this.$(),
pasteZone: this.$()
dropZone: $upload,
pasteZone: $upload
});
$upload.on('fileuploadsubmit', function (e, data) {
@ -39,14 +37,20 @@ export default Em.Mixin.create({
});
$upload.on("fileuploaddone", function(e, data) {
if(data.result.url) {
self.uploadDone(data);
} else {
if (data.result.message) {
bootbox.alert(data.result.message);
if (data.result) {
if (data.result.url) {
self.uploadDone(data);
} else {
bootbox.alert(I18n.t('post.errors.upload'));
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'));
}
});
@ -60,12 +64,9 @@ export default Em.Mixin.create({
}.on('didInsertElement'),
_destroyUploader: function() {
this.$('input[type=file]').fileupload('destroy');
}.on('willDestroyElement'),
actions: {
selectFile: function() {
this.$('input[type=file]').click();
}
}
var $upload = this.$();
try { $upload.fileupload('destroy'); }
catch (e) { /* wasn't initialized yet */ }
$upload.off();
}.on('willDestroyElement')
});

View File

@ -4,16 +4,9 @@
//
// This is useful if you want to get around Ember's default
// behavior of not refreshing when navigating to the same place.
export default Em.Mixin.create({
_initURLRefresh: function() {
this.appEvents.on('url:refresh', this, '_urlRefresh');
}.on('didInsertElement'),
_tearDownURLRefresh: function() {
this.appEvents.off('url:refresh', this, '_urlRefresh');
}.on('willDestroyElement'),
import { createViewListener } from 'discourse/lib/app-events';
_urlRefresh: function() {
this.get('controller').send('refresh');
}
export default createViewListener('url:refresh', function() {
this.get('controller').send('refresh');
});

View File

@ -470,7 +470,7 @@ Discourse.Post.reopenClass({
loadRevision: function(postId, version) {
return Discourse.ajax("/posts/" + postId + "/revisions/" + version + ".json").then(function (result) {
return Em.Object.create(result);
return Ember.Object.create(result);
});
},

View File

@ -232,6 +232,7 @@ Discourse.Category.reopenClass({
},
findById: function(id) {
if (!id) { return; }
return Discourse.Category.idMap()[id];
},

View File

@ -1,11 +1,3 @@
/**
A data model for containing a list of categories
@class CategoryList
@extends Discourse.Model
@namespace Discourse
@module Discourse
**/
Discourse.CategoryList = Ember.ArrayProxy.extend({
init: function() {
this.set('content', []);
@ -50,7 +42,8 @@ Discourse.CategoryList.reopenClass({
var self = this;
return Discourse.ajax('/categories.json?parent_category_id=' + category.get('id')).then(function(result) {
return Discourse.CategoryList.create({
categories: self.categoriesFrom(result)
categories: self.categoriesFrom(result),
parentCategory: category
});
});
},

View File

@ -328,36 +328,6 @@ Discourse.Composer = Discourse.Model.extend({
Discourse.KeyValueStore.set({ key: 'composer.showPreview', value: this.get('showPreview') });
},
importQuote: function() {
var postStream = this.get('topic.postStream'),
postId = this.get('post.id');
if (!postId && postStream) {
postId = postStream.get('firstPostId');
}
// If we're editing a post, fetch the reply when importing a quote
if (this.get('editingPost')) {
var replyToPostNumber = this.get('post.reply_to_post_number');
if (replyToPostNumber) {
var replyPost = postStream.get('posts').findBy('post_number', replyToPostNumber);
if (replyPost) {
postId = replyPost.get('id');
}
}
}
// If there is no current post, use the post id from the stream
if (postId) {
this.set('loading', true);
var composer = this;
return Discourse.Post.load(postId).then(function(post) {
composer.appendText(Discourse.Quote.build(post, post.get('raw')));
composer.set('loading', false);
});
}
},
/*
Open a composer
@ -467,14 +437,19 @@ Discourse.Composer = Discourse.Model.extend({
editPost: function(opts) {
var post = this.get('post'),
oldCooked = post.get('cooked'),
composer = this;
self = this,
promise;
// Update the title if we've changed it
if (this.get('title') && post.get('post_number') === 1) {
// Update the title if we've changed it, otherwise consider it a
// successful resolved promise
if (this.get('title') &&
post.get('post_number') === 1 &&
this.get('topic.details.can_edit')) {
var topicProps = this.getProperties(Object.keys(_edit_topic_serializer));
Discourse.Topic.update(this.get('topic'), topicProps);
promise = Discourse.Topic.update(this.get('topic'), topicProps);
} else {
promise = Ember.RSVP.resolve();
}
post.setProperties({
@ -485,19 +460,19 @@ Discourse.Composer = Discourse.Model.extend({
});
this.set('composeState', CLOSED);
return new Ember.RSVP.Promise(function(resolve, reject) {
post.save(function(result) {
return promise.then(function() {
return post.save(function(result) {
post.updateFromPost(result);
composer.clearState();
}, function(error) {
self.clearState();
}).catch(function(error) {
var response = $.parseJSON(error.responseText);
if (response && response.errors) {
reject(response.errors[0]);
return(response.errors[0]);
} else {
reject(I18n.t('generic_error'));
return(I18n.t('generic_error'));
}
post.set('cooked', oldCooked);
composer.set('composeState', OPEN);
self.set('composeState', OPEN);
});
});
},
@ -530,9 +505,11 @@ Discourse.Composer = Discourse.Model.extend({
imageSizes: opts.imageSizes,
cooked: this.getCookedHtml(),
reply_count: 0,
name: currentUser.get('name'),
display_username: currentUser.get('name'),
username: currentUser.get('username'),
user_id: currentUser.get('id'),
user_title: currentUser.get('title'),
uploaded_avatar_id: currentUser.get('uploaded_avatar_id'),
user_custom_fields: currentUser.get('custom_fields'),
post_type: Discourse.Site.currentProp('post_types.regular'),

View File

@ -11,6 +11,15 @@ Discourse.Group = Discourse.Model.extend({
offset: 0,
user_count: 0,
emailDomains: function() {
var value = this.get("automatic_membership_email_domains");
return Em.isEmpty(value) ? "" : value;
}.property("automatic_membership_email_domains"),
type: function() {
return this.get("automatic") ? "automatic" : "custom";
}.property("automatic"),
userCountDisplay: function(){
var c = this.get('user_count');
// don't display zero its ugly
@ -20,7 +29,8 @@ Discourse.Group = Discourse.Model.extend({
findMembers: function() {
if (Em.isEmpty(this.get('name'))) { return ; }
var self = this, offset = Math.min(this.get("user_count"), Math.max(this.get("offset"), 0));
var self = this,
offset = Math.min(this.get("user_count"), Math.max(this.get("offset"), 0));
return Discourse.ajax('/groups/' + this.get('name') + '/members.json', {
data: {
@ -63,7 +73,9 @@ Discourse.Group = Discourse.Model.extend({
return {
name: this.get('name'),
alias_level: this.get('alias_level'),
visible: !!this.get('visible')
visible: !!this.get('visible'),
automatic_membership_email_domains: this.get('emailDomains'),
automatic_membership_retroactive: !!this.get('automatic_membership_retroactive')
};
},
@ -100,7 +112,7 @@ Discourse.Group = Discourse.Model.extend({
Discourse.Group.reopenClass({
findAll: function(opts){
return Discourse.ajax("/admin/groups.json", { data: opts }).then(function(groups){
return Discourse.ajax("/admin/groups.json", { data: opts }).then(function (groups){
return groups.map(function(g) { return Discourse.Group.create(g); });
});
},
@ -112,8 +124,8 @@ Discourse.Group.reopenClass({
},
find: function(name) {
return Discourse.ajax("/groups/" + name + ".json").then(function(g) {
return Discourse.Group.create(g.basic_group);
return Discourse.ajax("/groups/" + name + ".json").then(function (result) {
return Discourse.Group.create(result.basic_group);
});
}
});

View File

@ -188,15 +188,23 @@ Discourse.Topic = Discourse.Model.extend({
return Discourse.ajax('/t/' + this.get('id') + '/bookmark', {
type: 'PUT',
data: { bookmarked: self.get('bookmarked') }
}).then(null, function (error) {
self.toggleProperty('bookmarked');
if (self.get("postStream.firstPostPresent")) { firstPost.toggleProperty('bookmarked'); }
data: { bookmarked: self.get('bookmarked') },
error: function(error){
self.toggleProperty('bookmarked');
if (self.get("postStream.firstPostPresent")) { firstPost.toggleProperty('bookmarked'); }
if (error && error.responseText) {
bootbox.alert($.parseJSON(error.responseText).errors);
} else {
bootbox.alert(I18n.t('generic_error'));
var showGenericError = true;
if (error && error.responseText) {
try {
bootbox.alert($.parseJSON(error.responseText).errors);
showGenericError = false;
} catch(e){}
}
if(showGenericError){
bootbox.alert(I18n.t('generic_error'));
}
}
});
},

View File

@ -1,12 +1,3 @@
/**
A data model representing a list of topics
@class TopicList
@extends Discourse.Model
@namespace Discourse
@module Discourse
**/
function finderFor(filter, params) {
return function() {
var url = Discourse.getURL("/") + filter + ".json";
@ -264,6 +255,12 @@ Discourse.TopicList.reopenClass({
return PreloadStore.getAndRemove("topic_list_" + filter, finderFor(filter, params)).then(function(result) {
return Discourse.TopicList.from(result, filter, params);
});
},
// Sets `hideCategory` if all topics in the last have a particular category
hideUniformCategory: function(list, category) {
var hideCategory = !list.get('topics').any(function (t) { return t.get('category') !== category; });
list.set('hideCategory', hideCategory);
}
});

View File

@ -72,10 +72,9 @@ Discourse.User = Discourse.Model.extend({
@type {String}
**/
profileBackground: function() {
var background = this.get('profile_background');
if(Em.isEmpty(background) || !Discourse.SiteSettings.allow_profile_backgrounds) { return; }
return 'background-image: url(' + background + ')';
var url = this.get('profile_background');
if (Em.isEmpty(url) || !Discourse.SiteSettings.allow_profile_backgrounds) { return; }
return 'background-image: url(' + Discourse.getURLWithCDN(url) + ')';
}.property('profile_background'),
/**
@ -352,10 +351,13 @@ Discourse.User = Discourse.Model.extend({
Change avatar selection
*/
pickAvatar: function(uploadId) {
this.set("uploaded_avatar_id", uploadId);
var self = this;
return Discourse.ajax("/users/" + this.get("username_lower") + "/preferences/avatar/pick", {
type: 'PUT',
data: { upload_id: uploadId }
}).then(function(){
self.set('uploaded_avatar_id', uploadId);
});
},
@ -442,6 +444,7 @@ Discourse.User.reopenClass(Discourse.Singleton, {
avatarTemplate: function(username, uploadedAvatarId) {
var url;
if (uploadedAvatarId) {
url = "/user_avatar/" +
Discourse.BaseUrl +
@ -456,11 +459,7 @@ Discourse.User.reopenClass(Discourse.Singleton, {
Discourse.LetterAvatarVersion + ".png";
}
url = Discourse.getURL(url);
if (Discourse.CDN) {
url = Discourse.CDN + url;
}
return url;
return Discourse.getURLWithCDN(url);
},
/**

View File

@ -132,18 +132,13 @@ var ApplicationRoute = Discourse.Route.extend({
},
editCategory: function(category) {
var router = this;
var self = this;
if (category.get('isUncategorizedCategory')) {
Discourse.Route.showModal(router, 'editCategory', category);
router.controllerFor('editCategory').set('selectedTab', 'general');
} else {
Discourse.Category.reloadById(category.get('id')).then(function (c) {
Discourse.Site.current().updateCategory(c);
Discourse.Route.showModal(router, 'editCategory', c);
router.controllerFor('editCategory').set('selectedTab', 'general');
});
}
Discourse.Category.reloadById(category.get('id')).then(function (c) {
self.site.updateCategory(c);
Discourse.Route.showModal(self, 'editCategory', c);
self.controllerFor('editCategory').set('selectedTab', 'general');
});
},
/**

View File

@ -54,9 +54,7 @@ export default function(filter, params) {
extras = { cached: this.isPoppedState(transition) };
return Discourse.TopicList.list(listFilter, findOpts, extras).then(function(list) {
// If all the categories are the same, we can hide them
var hideCategory = !list.get('topics').find(function (t) { return t.get('category') !== model; });
list.set('hideCategory', hideCategory);
Discourse.TopicList.hideUniformCategory(list, model);
self.set('topics', list);
});
},

View File

@ -89,23 +89,53 @@ Discourse.Route.reopenClass({
},
mapRoutes: function() {
var resources = {};
// If a module is defined as `route-map` in discourse or a plugin, its routes
// will be built automatically. You can supply a `resource` property to
// automatically put it in that resource, such as `admin`. That way plugins
// can define admin routes.
Ember.keys(requirejs._eak_seen).forEach(function(key) {
if (/route-map$/.test(key)) {
var module = require(key, null, null, true);
if (!module || !module.default) { throw new Error(key + ' must export a route map.'); }
var mapObj = module.default;
if (typeof mapObj === 'function') {
mapObj = { resource: 'root', map: mapObj };
}
if (!resources[mapObj.resource]) { resources[mapObj.resource] = []; }
resources[mapObj.resource].push(mapObj.map);
}
});
Discourse.Router.map(function() {
var router = this;
// Do the root resources first
if (resources.root) {
resources.root.forEach(function(m) {
m.call(router);
});
delete resources.root;
}
// Apply other resources next
Object.keys(resources).forEach(function(r) {
router.resource(r, function() {
var res = this;
resources[r].forEach(function(m) {
m.call(res);
});
});
});
if (routeBuilder) {
Ember.warn("The Discourse `routeBuilder` is deprecated. Export a `route-map` instead");
routeBuilder.call(router);
}
// If a module is defined as `route-map` in discourse or a plugin, its routes
// will be built automatically.
Ember.keys(requirejs._eak_seen).forEach(function(key) {
if (/route-map$/.test(key)) {
var module = require(key, null, null, true);
if (!module) { throw new Error(key + ' must export a map function.'); }
module.default.call(router);
}
});
this.route('unknown', {path: '*path'});
});

View File

@ -45,15 +45,17 @@ export default RestrictedUserRoute.extend(ShowFooter, {
// sends the information to the server if it has changed
if (avatarSelector.get('selectedUploadId') !== user.get('uploaded_avatar_id')) {
user.pickAvatar(avatarSelector.get('selectedUploadId'));
user.pickAvatar(avatarSelector.get('selectedUploadId'))
.then(function(){
user.setProperties(avatarSelector.getProperties(
'system_avatar_upload_id',
'gravatar_avatar_upload_id',
'custom_avatar_upload_id'
));
});
}
// saves the data back
user.setProperties(avatarSelector.getProperties(
'system_avatar_upload_id',
'gravatar_avatar_upload_id',
'custom_avatar_upload_id'
));
avatarSelector.send('closeModal');
},

View File

@ -1,5 +1,4 @@
// This route is used for retrieving a topic based on params
export default Discourse.Route.extend({
setupController: function(controller, params) {
@ -15,6 +14,7 @@ export default Discourse.Route.extend({
// I sincerely hope no topic gets this many posts
if (params.nearPost === "last") { params.nearPost = 999999999; }
var self = this;
postStream.refresh(params).then(function () {
// TODO we are seeing errors where closest post is null and this is exploding
@ -28,13 +28,17 @@ export default Discourse.Route.extend({
topicController.setProperties({
currentPost: closest,
enteredAt: new Date().getTime().toString(),
highlightOnInsert: closest
});
topicProgressController.setProperties({
progressPosition: progress,
expanded: false
});
// Highlight our post after the next render
Ember.run.scheduleOnce('afterRender', function() {
self.appEvents.trigger('post:highlight', closest);
});
Discourse.URL.jumpToPost(closest);
if (topic.present('draft')) {

View File

@ -37,26 +37,37 @@
<th>&nbsp;</th>
<th>{{i18n 'about.stat.all_time'}}</th>
<th>{{i18n 'about.stat.last_7_days'}}</th>
<th>{{i18n 'about.stat.last_30_days'}}</th>
</tr>
<tr>
<td class='title'>{{i18n 'about.topic_count'}}</td>
<td>{{number stats.topic_count}}</td>
<td>{{number stats.topics_7_days}}</td>
<td>{{number stats.topics_30_days}}</td>
</tr>
<tr>
<td>{{i18n 'about.post_count'}}</td>
<td>{{number stats.post_count}}</td>
<td>{{number stats.posts_7_days}}</td>
<td>{{number stats.posts_30_days}}</td>
</tr>
<tr>
<td>{{i18n 'about.user_count'}}</td>
<td>{{number stats.user_count}}</td>
<td>{{number stats.users_7_days}}</td>
<td>{{number stats.users_30_days}}</td>
</tr>
<tr>
<td>{{i18n 'about.active_user_count'}}</td>
<td></td>
<td>{{number stats.active_users_7_days}}</td>
<td>{{number stats.active_users_30_days}}</td>
</tr>
<tr>
<td>{{i18n 'about.like_count'}}</td>
<td>{{number stats.like_count}}</td>
<td>{{number stats.likes_7_days}}</td>
<td>{{number stats.likes_30_days}}</td>
</tr>
</table>
</section>

View File

@ -1,7 +1,7 @@
<div class='autocomplete'>
<ul>
{{#each option in options}}
<li>{{category-link option allowUncategorized="true"}}</li>
<li><a>{{category-link option allowUncategorized="true" link="false"}}</a></li>
{{/each}}
</ul>
</div>

View File

@ -1,7 +1,7 @@
<input type="file" accept="image/*" style="display:none" />
<button class="btn" {{action "selectFile"}} {{bind-attr disabled="uploading"}} title="{{i18n 'user.change_avatar.upload_title'}}">
<i class="fa fa-picture-o"></i>&nbsp;{{uploadButtonText}}
</button>
<label class="btn" {{bind-attr disabled="uploading"}} title="{{i18n 'user.change_avatar.upload_title'}}">
{{fa-icon "picture-o"}}&nbsp;{{uploadButtonText}}
<input {{bind-attr disabled="uploading"}} type="file" accept="image/*" style="visibility: hidden; position: absolute;" />
</label>
{{#if uploading}}
<span>{{i18n 'upload_selector.uploading'}} {{view.uploadProgress}}%</span>
{{/if}}

View File

@ -1,27 +1,27 @@
{{#if category}}
<a href="#" {{action "expand"}} class="badge-category" {{bind-attr style="badgeStyle"}}>
<a href {{action "expand"}} class="badge-category" {{bind-attr style="badgeStyle"}}>
{{#if category.read_restricted}}
<i class='fa fa-group'></i>
{{fa-icon "lock"}}
{{/if}}
{{category.name}}
</a>
{{else}}
{{#if noSubcategories}}
<a href='#' {{action "expand"}} class='badge-category home' {{bind-attr style="badgeStyle"}}>{{i18n 'categories.no_subcategory'}}</a>
<a href {{action "expand"}} class='badge-category home' {{bind-attr style="badgeStyle"}}>{{i18n 'categories.no_subcategory'}}</a>
{{else}}
<a href='#' {{action "expand"}} class='badge-category home' {{bind-attr style="badgeStyle"}}>{{allCategoriesLabel}}</a>
<a href {{action "expand"}} class='badge-category home' {{bind-attr style="badgeStyle"}}>{{allCategoriesLabel}}</a>
{{/if}}
{{/if}}
{{#if categories}}
<a href='#' {{action "expand"}} {{bind-attr class="dropdownButtonClass" style="badgeStyle"}}><i {{bind-attr class="iconClass"}}></i></a>
<a href {{action "expand"}} {{bind-attr class="dropdownButtonClass" style="badgeStyle"}}><i {{bind-attr class="iconClass"}}></i></a>
<section {{bind-attr class="expanded::hidden :category-dropdown-menu"}} class='chooser'>
<div class='cat'><a {{bind-attr href=allCategoriesUrl}} data-drop-close="true" class='badge-category home'>{{allCategoriesLabel}}</a></div>
{{#if subCategory}}
<div class='cat'><a {{bind-attr href=noCategoriesUrl}} data-drop-close="true" class='badge-category home'>{{i18n 'categories.no_subcategory'}}</a></div>
<div class='cat'><a {{bind-attr href=noCategoriesUrl}} data-drop-close="true" class='badge-category home'>{{i18n 'categories.no_subcategory'}}</a></div>
{{/if}}
{{#if renderCategories}}
{{#each c in categories}}<div class='cat'>{{category-link c allowUncategorized=true}}</div>{{/each}}
{{#each c in categories}}<div class='cat'>{{category-link c allowUncategorized=true hideParent=subCategory}}</div>{{/each}}
{{/if}}
</section>
{{/if}}

View File

@ -0,0 +1,5 @@
{{fa-icon icon modifier="stack-2x"}}
{{#if disabled}}
{{fa-icon "ban" modifier="stack-2x"}}
{{/if}}

View File

@ -1,6 +1,6 @@
{{text-field name="name" placeholderKey="admin.emoji.name" value=name}}
<input type="file" accept=".png,.gif" style="display:none" />
<button {{bind-attr disabled="addDisabled"}} {{action "selectFile"}} class='btn btn-primary'>
<label class="btn btn-primary" {{bind-attr disabled="addDisabled"}}>
{{fa-icon "plus"}}
{{i18n 'admin.emoji.add'}}
</button>
<input {{bind-attr disabled="addDisabled"}} type="file" accept=".png,.gif" style="visibility: hidden; position: absolute;" />
</label>

View File

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

View File

@ -0,0 +1,6 @@
{{#if topic.category.parentCategory}}
{{bound-category-link topic.category.parentCategory}}
{{/if}}
{{bound-category-link topic.category hideParent=true}}
{{plugin-outlet "topic-category"}}

View File

@ -48,7 +48,7 @@ so I'm going to stop rendering it until we figure out what's up
{{#if showWarning}}
<div class='add-warning'>
<label>
{{input type="checkbox" checked=model.isWarning}}
{{input type="checkbox" checked=model.isWarning tabindex="3"}}
{{i18n "composer.add_warning"}}
</label>
</div>
@ -101,6 +101,7 @@ so I'm going to stop rendering it until we figure out what's up
{{#if currentUser}}
<div class='submit-panel'>
{{plugin-outlet "composer-fields-below"}}
<button {{action "save"}} tabindex="5" {{bind-attr class=":btn :btn-primary :create disableSubmit:disabled"}} title="{{i18n 'composer.title'}}">{{{model.saveIcon}}}{{model.saveText}}</button>
<a href='#' {{action "cancel"}} class='cancel' tabindex="6">{{i18n 'cancel'}}</a>
</div>

View File

@ -10,71 +10,71 @@
</thead>
<tbody>
{{#each c in categories}}
<tr data-category_id='{{unbound c.id}}' {{bind-attr class="c.description_excerpt:has-description:no-description c.logo_url:has-logo:no-logo"}}>
<td class='category' style="border-color: #{{unbound c.color}}">
<div>
<div class="pull-left">
{{category-title-link category=c}}
{{#if c.unreadTopics}}
<a href={{unbound c.unreadUrl}} class='badge new-posts badge-notification' title='{{i18n 'topic.unread_topics' count=c.unreadTopics}}'>{{i18n 'filters.unread.lower_title_with_count' count=c.unreadTopics}}</a>
{{/if}}
{{#if c.newTopics}}
<a href={{unbound c.newUrl}} class='badge new-posts badge-notification' title='{{i18n 'topic.new_topics' count=c.newTopics}}'>{{i18n 'filters.new.lower_title_with_count' count=c.newTopics}}</a>
{{/if}}
</div>
<div class="clearfix"></div>
</div>
{{#if c.description_excerpt}}
<div class="category-description">
{{{c.description_excerpt}}}
</div>
{{/if}}
{{#if c.subcategories}}
<div class='subcategories'>
{{#each s in c.subcategories}}
{{category-link s showParent="true" onlyStripe="true"}}
{{#if s.unreadTopics}}
<a href={{unbound s.unreadUrl}} class='badge new-posts badge-notification' title='{{i18n 'topic.unread_topics' count=s.unreadTopics}}'>{{unbound s.unreadTopics}}</a>
<tr data-category_id='{{unbound c.id}}' {{bind-attr class="c.description_excerpt:has-description:no-description c.logo_url:has-logo:no-logo"}}>
<td class='category' style="border-color: #{{unbound c.color}}">
<div>
<div class="pull-left">
{{category-title-link category=c}}
{{#if c.unreadTopics}}
<a href={{unbound c.unreadUrl}} class='badge new-posts badge-notification' title='{{i18n 'topic.unread_topics' count=c.unreadTopics}}'>{{i18n 'filters.unread.lower_title_with_count' count=c.unreadTopics}}</a>
{{/if}}
{{#if s.newTopics}}
<a href={{unbound s.newUrl}} class='badge new-posts badge-notification' title='{{i18n 'topic.new_topics' count=s.newTopics}}'>{{unbound s.newTopics}}</a>
{{#if c.newTopics}}
<a href={{unbound c.newUrl}} class='badge new-posts badge-notification' title='{{i18n 'topic.new_topics' count=c.newTopics}}'>{{i18n 'filters.new.lower_title_with_count' count=c.newTopics}}</a>
{{/if}}
{{/each}}
</div>
<div class="clearfix"></div>
</div>
{{/if}}
</td>
<td {{bind-attr class="c.archived :latest"}}>
{{#each f in c.featuredTopics}}
<div class="featured-topic">
{{topic-status topic=f}}
<a class='title' href="{{unbound f.lastUnreadUrl}}">{{{unbound f.fancy_title}}}</a>
{{topic-post-badges newPosts=f.totalUnread unseen=f.unseen url=f.lastUnreadUrl}}
{{#if c.description_excerpt}}
<div class="category-description">
{{{c.description_excerpt}}}
</div>
{{/if}}
{{#if c.subcategories}}
<div class='subcategories'>
{{#each s in c.subcategories}}
{{category-link s hideParent="true"}}
{{#if s.unreadTopics}}
<a href={{unbound s.unreadUrl}} class='badge new-posts badge-notification' title='{{i18n 'topic.unread_topics' count=s.unreadTopics}}'>{{unbound s.unreadTopics}}</a>
{{/if}}
{{#if s.newTopics}}
<a href={{unbound s.newUrl}} class='badge new-posts badge-notification' title='{{i18n 'topic.new_topics' count=s.newTopics}}'>{{unbound s.newTopics}}</a>
{{/if}}
{{/each}}
</div>
{{/if}}
</td>
<td {{bind-attr class="c.archived :latest"}}>
{{#each f in c.featuredTopics}}
<div class="featured-topic">
{{topic-status topic=f}}
<a class='title' href="{{unbound f.lastUnreadUrl}}">{{{unbound f.fancy_title}}}</a>
{{topic-post-badges newPosts=f.totalUnread unseen=f.unseen url=f.lastUnreadUrl}}
{{#if controller.latestTopicOnly}}
<div class='last-user-info'>
{{i18n 'categories.latest_by'}} <a href="{{{unbound f.lastPosterUrl}}}">{{unbound f.last_poster.username}}</a>
<a href="{{unbound f.lastPostUrl}}">{{format-age f.last_posted_at}}</a>
</div>
{{else}}
&nbsp;
<a href="{{unbound f.lastPostUrl}}" class="last-posted-at">{{format-age f.last_posted_at}}</a>
{{/if}}
</div>
{{/each}}
</td>
<td class='stats' {{bind-attr title="c.topicStatsTitle"}}>
<table class="categoryStats">
<tbody>
{{#each s in c.topicCountStats}}
<tr>
<td class="value">{{s.value}}</td>
<td class="unit"> / {{s.unit}}</td>
</tr>
{{#if controller.latestTopicOnly}}
<div class='last-user-info'>
{{i18n 'categories.latest_by'}} <a href="{{{unbound f.lastPosterUrl}}}">{{unbound f.last_poster.username}}</a>
<a href="{{unbound f.lastPostUrl}}">{{format-age f.last_posted_at}}</a>
</div>
{{else}}
&nbsp;
<a href="{{unbound f.lastPostUrl}}" class="last-posted-at">{{format-age f.last_posted_at}}</a>
{{/if}}
</div>
{{/each}}
</tbody>
</table>
</td>
</tr>
</td>
<td class='stats' {{bind-attr title="c.topicStatsTitle"}}>
<table class="categoryStats">
<tbody>
{{#each s in c.topicCountStats}}
<tr>
<td class="value">{{s.value}}</td>
<td class="unit"> / {{s.unit}}</td>
</tr>
{{/each}}
</tbody>
</table>
</td>
</tr>
{{/each}}
</tbody>
</table>

View File

@ -16,7 +16,7 @@
{{#if selected}}
<div id='bulk-select'>
<button class='btn no-text' {{action "showBulkActions"}}><i class="fa fa-wrench"></i></button>
{{d-button action="showBulkActions" icon="wrench" class="no-text"}}
</div>
{{/if}}
@ -37,6 +37,7 @@
{{#if hasTopics}}
{{topic-list
showTopicPostBadges=showTopicPostBadges
showPosters=true
currentUser=currentUser
canBulkSelect=canBulkSelect

View File

@ -10,9 +10,10 @@
<div class="topic-meta-data">
{{poster-name post=this}}
{{#if view.parentView.previousPost}}<a href='{{unbound url}}' class="post-info arrow" title="{{i18n 'topic.jump_reply_up'}}"><i class='fa fa-arrow-up'></i></a>{{/if}}
<div class='post-info post-date'>{{age-with-tooltip created_at}}</div>
{{#unless view.parentView.previousPost}}<a href='{{unbound url}}' class="post-info arrow" title="{{i18n 'topic.jump_reply_down'}}"><i class='fa fa-arrow-down'></i></a>{{/unless}}
<a href='{{unbound url}}'><div class='post-info post-date'>{{age-with-tooltip created_at}}</div></a>
</div>
{{{unbound cooked}}}
{{#unless view.parentView.previousPost}}<a href='{{unbound url}}' class="arrow" title="{{i18n 'topic.jump_reply_down'}}"><i class='fa fa-arrow-down'></i></a>{{/unless}}
</div>
</div>

View File

@ -4,7 +4,7 @@
<th colspan="3" class="seen">{{i18n 'last_seen'}}</th>
</tr>
{{#each m in members}}
<tr>
<tr>
<td class='avatar'>
{{avatar m imageSize="large"}}
</td>
@ -16,7 +16,6 @@
<span class='last-seen-at'>{{bound-date m.last_seen_at}}</span>
</td>
</tr>
</div>
{{/each}}
</table>
{{/if}}

View File

@ -6,13 +6,9 @@
<div class='panel clearfix'>
{{#unless currentUser}}
{{#if showSignUpButton}}
<button {{action "showCreateAccount"}} class='btn btn-primary btn-small sign-up-button'>
{{i18n 'sign_up'}}
</button>
{{d-button action="showCreateAccount" class="btn-primary btn-small sign-up-button" label="sign_up"}}
{{/if}}
<button {{action "showLogin"}} class='btn btn-primary btn-small login-button'>
{{fa-icon "user"}} {{i18n 'log_in'}}
</button>
{{d-button action="showLogin" class="btn-primary btn-small login-button" icon="user" label="log_in"}}
{{/unless}}
<ul class='icons clearfix' role='navigation'>
{{#if currentUser}}
@ -111,15 +107,10 @@
{{else}}
{{#if topic.errorLoading}}
<span class="error">{{topic.errorTitle}}</span>
{{else}}
<span class="loading">{{i18n 'topic.loading'}}</span>
{{/if}}
{{/if}}
</h1>
{{#if topic.category.parentCategory}}
{{bound-category-link topic.category.parentCategory onlyStripe="true"}}
{{/if}}
{{bound-category-link topic.category onlyStripe="true"}}
{{topic-category topic=topic}}
</div>
</div>
</div>

View File

@ -1 +1 @@
<td class='category'>{{category-link category showParent="true"}}</td>
<td class='category'>{{category-link category}}</td>

View File

@ -32,7 +32,7 @@
</div>
{{#unless controller.hideCategory}}
<div class='category'>
{{category-link t.category showParent="true"}}
{{category-link t.category}}
</div>
{{/unless}}
{{#if controller.showParticipants}}

View File

@ -46,7 +46,7 @@
<td>
<div class='subcategories'>
{{#each subcategory in c.subcategories}}
{{category-link subcategory showParent="true"}}
{{category-link subcategory}}
{{/each}}
</div>
</td>

View File

@ -14,7 +14,6 @@
{{#if hasTopics}}
{{topic-list
skipHeader=true
showPosters=true
currentUser=currentUser
hideCategory=hideCategory

View File

@ -11,7 +11,7 @@
<div class="topic-item-stats clearfix">
{{#unless controller.hideCategory}}
<div class='category'>
{{category-link content.category showParent="true"}}
{{category-link content.category}}
</div>
{{/unless}}

View File

@ -15,7 +15,7 @@
{{#if subCategories}}
<label>{{i18n 'categories.subcategories'}}</label>
{{#each s in subCategories}}
{{category-badge s}}
{{category-badge s hideParent="true"}}
{{/each}}
{{else}}
<label>{{i18n 'category.parent'}}</label>

View File

@ -40,12 +40,12 @@
&rarr; {{bound-avatar-template user_changes.current.avatar_template "small"}} {{user_changes.current.username}}
{{/if}}
{{#if wiki_changes}}
&mdash; {{{wiki_diff}}}
&mdash; {{disabled-icon icon="pencil-square-o" secondary=wikiDisabled}}
{{/if}}
{{#if post_type_changes}}
&mdash; {{{post_type_diff}}}
&mdash; {{disabled-icon icon="shield" disabled=postTypeDisabled}}
{{/if}}
{{#if category_changes}}
{{#if category_id_changes}}
&mdash; {{{previousCategory}}} &rarr; {{{currentCategory}}}
{{/if}}
{{/unless}}
@ -53,7 +53,7 @@
<div id="revisions" {{bind-attr class="hiddenClasses"}}>
{{#if title_changes}}
<div class="row">
<h2>{{{title_diff}}}</h2>
<h2>{{{titleDiff}}}</h2>
</div>
{{/if}}
{{#if site.mobileView}}
@ -65,22 +65,25 @@
{{/if}}
{{#if wiki_changes}}
<div class="row">
{{{wiki_diff}}}
{{disabled-icon icon="pencil-square-o" secondary=wikiDisabled}}
</div>
{{/if}}
{{#if post_type_changes}}
<div class="row">
{{{post_type_diff}}}
{{disabled-icon icon="shield" disabled=postTypeDisabled}}
</div>
{{/if}}
{{#if category_changes}}
{{#if category_id_changes}}
<div class="row">
{{{previousCategory}}} &rarr; {{{currentCategory}}}
</div>
{{/if}}
{{/if}}
{{plugin-outlet "post-revisions"}}
<div class="row">
{{{body_diff}}}
{{{bodyDiff}}}
</div>
</div>
</div>

View File

@ -46,7 +46,7 @@
{{#each c in categories itemController='site-map-category'}}
<li class="category">
{{category-link c allowUncategorized="true" showParent="true"}}
{{category-link c allowUncategorized="true"}}
{{#if c.unreadTotal}}
<a href={{unbound c.url}} class='badge badge-notification'>{{c.unreadTotal}}</a>

View File

@ -2,48 +2,48 @@
<ul>
<li>
<button {{action "toggleMultiSelect"}} class='btn btn-admin'>{{fa-icon "tasks"}} {{i18n 'topic.actions.multi_select'}}</button>
{{d-button action="toggleMultiSelect" icon="tasks" label="topic.actions.multi_select" class="btn-admin"}}
</li>
{{#if details.can_delete}}
<li>
<button {{action "deleteTopic"}} class='btn btn-admin btn-danger'>{{fa-icon "trash-o"}} {{i18n 'topic.actions.delete'}}</button>
{{d-button action="deleteTopic" icon="trash-o" label="topic.actions.delete" class="btn-admin btn-danger"}}
</li>
{{/if}}
{{#if showRecover}}
<li>
<button {{action "recoverTopic"}} class='btn btn-admin'>{{fa-icon "undo"}} {{i18n 'topic.actions.recover'}}</button>
{{d-button action="recoverTopic" icon="undo" label="topic.actions.recover" class="btn-admin"}}
</li>
{{/if}}
<li>
{{#if closed}}
<button {{action "toggleClosed"}} class='btn btn-admin'>{{fa-icon "unlock"}} {{i18n 'topic.actions.open'}}</button>
{{d-button action="toggleClosed" icon="unlock" label="topic.actions.open" class="btn-admin"}}
{{else}}
<button {{action "toggleClosed"}} class='btn btn-admin'>{{fa-icon "lock"}} {{i18n 'topic.actions.close'}}</button>
<button {{action "showAutoClose"}} class='btn btn-admin'>{{fa-icon "clock-o"}} {{i18n 'topic.actions.auto_close'}}</button>
{{d-button action="toggleClosed" icon="lock" label="topic.actions.close" class="btn-admin"}}
{{d-button action="showAutoClose" icon="clock-o" label="topic.actions.auto_close" class="btn-admin"}}
{{/if}}
</li>
{{#unless isPrivateMessage}}
<li>
{{#if isBanner}}
<button {{action "removeBanner"}} class='btn btn-admin'>{{fa-icon "bullhorn"}} {{i18n 'topic.actions.remove_banner'}}</button>
{{d-button action="removeBanner" icon="bullhorn" label="topic.actions.remove_banner" class="btn-admin"}}
{{else}}
{{#if visible}}
<button {{action "makeBanner"}} class='btn btn-admin'>{{fa-icon "bullhorn"}} {{i18n 'topic.actions.make_banner'}}</button>
{{d-button action="makeBanner" icon="bullhorn" label="topic.actions.make_banner" class="btn-admin"}}
{{/if}}
{{/if}}
</li>
<li>
{{#if pinned_at}}
<button {{action "togglePinned"}} class='btn btn-admin'>{{fa-icon "thumb-tack"}} {{i18n 'topic.actions.unpin'}}</button>
{{d-button action="togglePinned" icon="thumb-tack" label="topic.actions.unpin" class="btn-admin"}}
{{else}}
{{#if visible}}
<button {{action "togglePinned"}} class='btn btn-admin'>{{fa-icon "thumb-tack"}} {{i18n 'topic.actions.pin'}}</button>
<button {{action "togglePinnedGlobally"}} class='btn btn-admin'>{{fa-icon "thumb-tack"}} {{i18n 'topic.actions.pin_globally'}}</button>
{{d-button action="togglePinned" icon="thumb-tack" label="topic.actions.pin" class="btn-admin"}}
{{d-button action="togglePinnedGlobally" icon="thumb-tack" label="topic.actions.pin_globally" class="btn-admin"}}
{{/if}}
{{/if}}
</li>
@ -51,18 +51,19 @@
<li>
{{#if archived}}
<button {{action "toggleArchived"}} class='btn btn-admin'>{{fa-icon "folder"}} {{i18n 'topic.actions.unarchive'}}</button>
{{d-button action="toggleArchived" icon="folder" label="topic.actions.unarchive" class="btn-admin"}}
{{else}}
<button {{action "toggleArchived"}} class='btn btn-admin'>{{fa-icon "folder"}} {{i18n 'topic.actions.archive'}}</button>
{{d-button action="toggleArchived" icon="folder" label="topic.actions.archive" class="btn-admin"}}
{{/if}}
</li>
<li>
{{#if visible}}
<button {{action "toggleVisibility"}} class='btn btn-admin'>{{fa-icon "eye-slash"}} {{i18n 'topic.actions.invisible'}}</button>
{{d-button action="toggleVisibility" icon="eye-slash" label="topic.actions.invisible" class="btn-admin"}}
{{else}}
<button {{action "toggleVisibility"}} class='btn btn-admin'>{{fa-icon "eye"}} {{i18n 'topic.actions.visible'}}</button>
{{d-button action="toggleVisibility" icon="eye" label="topic.actions.visible" class="btn-admin"}}
{{/if}}
</li>
{{plugin-outlet "topic-admin-menu-buttons"}}
</ul>

View File

@ -1,6 +1,7 @@
<button {{action "enterTop"}} class='btn full no-text jump-top'>
<i class='fa fa-caret-up'></i> {{{topDate}}}
</button>
<button {{action "enterBottom"}} class='btn full no-text jump-bottom'>
{{{bottomDate}}} <i class='fa fa-caret-down'></i>
</button>
{{#d-button action="enterTop" class="full no-text jump-top"}}
{{fa-icon 'caret-up'}} {{{topDate}}}
{{/d-button}}
{{#d-button action="enterBottom" class="full no-text jump-button"}}
{{{bottomDate}}} {{fa-icon 'caret-down'}}
{{/d-button}}

View File

@ -1,16 +1,19 @@
{{#if expanded}}
<nav id='topic-progress-expanded'>
<button {{action "jumpTop"}} {{bind-attr disabled=jumpTopDisabled}} class='btn full no-text'>
<i class="fa fa-caret-up"></i>
{{i18n 'topic.progress.go_top'}}
</button>
{{d-button action="jumpTop"
disabled=jumpTopDisabled
class="full no-text"
icon="caret-up"
label="topic.progress.go_top"}}
<div class='jump-form'>
{{input value=toPostIndex}} <button {{action "jumpPost"}} class='btn'>{{i18n 'topic.progress.go'}}</button>
{{input value=toPostIndex}}
{{d-button action="jumpPost" label="topic.progress.go"}}
</div>
<button {{action "jumpBottom"}} {{bind-attr disabled=jumpBottomDisabled}} class='btn full no-text jump-bottom'>
{{i18n 'topic.progress.go_bottom'}}
<i class="fa fa-caret-down"></i>
</button>
{{d-button action="jumpBottom"
disabled=jumpBottomDisabled
class="full no-text jump-bottom"
icon="caret-down"
label="topic.progress.go_bottom"}}
</nav>
{{/if}}
<nav id='topic-progress' title="{{i18n 'topic.progress.title'}}" {{bind-attr class="hideProgress:hidden"}}>

View File

@ -22,8 +22,8 @@
{{plugin-outlet "edit-topic"}}
<button class='btn btn-primary btn-small no-text' {{action "finishedEditingTopic"}}>{{fa-icon "check"}}</button>
<button class='btn btn-small no-text' {{action "cancelEditingTopic"}}>{{fa-icon "times"}}</button>
{{d-button action="finishedEditingTopic" class="btn-primary btn-small no-text" icon="check"}}
{{d-button action="cancelEditingTopic" class="btn-small no-text" icon="times"}}
{{else}}
<h1>
{{#unless is_warning}}
@ -43,11 +43,7 @@
</h1>
{{#unless isPrivateMessage}}
{{#if category.parentCategory}}
{{bound-category-link category.parentCategory}}
{{/if}}
{{bound-category-link category}}
{{plugin-outlet "topic-category"}}
{{topic-category topic=model}}
{{/unless}}
{{/if}}
</div>
@ -118,10 +114,10 @@
<div>{{message}}</div>
{{#if noRetry}}
{{#unless currentUser}}
<button {{action "showLogin"}} class='btn btn-primary topic-retry'>{{fa-icon "user"}} {{i18n 'log_in'}}</button>
{{d-button action="showLogin" class="btn-primary topic-retry" icon="user" label="log_in"}}
{{/unless}}
{{else}}
<button class="btn btn-primary topic-retry" {{action "retryLoading"}}>{{fa-icon "refresh"}} {{i18n 'errors.buttons.again'}}</button>
{{d-button action="retryLoading" class="btn-primary topic-retry" icon="refresh" label="errors.buttons.again"}}
{{/if}}
</div>
{{loading-spinner condition=retrying}}

View File

@ -8,6 +8,6 @@
</li>
<li>{{#link-to 'userActivity.bookmarks' currentUser}}{{i18n 'user.bookmarks'}}{{/link-to}}</li>
<li>{{#link-to 'preferences' currentUser}}{{i18n 'user.preferences'}}{{/link-to}}</li>
<li><button {{action "logout"}} class='btn btn-danger right logout'><i class='fa fa-sign-out'></i>{{i18n 'user.log_out'}}</button></li>
<li>{{d-button action="logout" class="btn-danger right logout" icon="sign-out" label="user.log_out"}}</li>
</ul>
</section>

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