Version bump

This commit is contained in:
Neil Lalonde 2015-03-02 18:31:18 -05:00
commit 9e96152788
275 changed files with 3721 additions and 2019 deletions

View File

@ -14,11 +14,13 @@ matrix:
env: "RAILS_MASTER=1"
- rvm: 2.1
env: "RAILS_MASTER=1"
- rvm: rbx-2
fast_finish: true
rvm:
- 2.0.0
- 2.1
- rbx-2
services:
- redis-server

View File

@ -11,31 +11,31 @@ GEM
execjs (~> 2.0)
6to5-source (3.3.7)
CFPropertyList (2.2.8)
actionmailer (4.1.8)
actionpack (= 4.1.8)
actionview (= 4.1.8)
actionmailer (4.1.9)
actionpack (= 4.1.9)
actionview (= 4.1.9)
mail (~> 2.5, >= 2.5.4)
actionpack (4.1.8)
actionview (= 4.1.8)
activesupport (= 4.1.8)
actionpack (4.1.9)
actionview (= 4.1.9)
activesupport (= 4.1.9)
rack (~> 1.5.2)
rack-test (~> 0.6.2)
actionpack-action_caching (1.1.1)
actionpack (>= 4.0.0, < 5.0)
actionview (4.1.8)
activesupport (= 4.1.8)
actionview (4.1.9)
activesupport (= 4.1.9)
builder (~> 3.1)
erubis (~> 2.7.0)
active_model_serializers (0.8.2)
activemodel (>= 3.0)
activemodel (4.1.8)
activesupport (= 4.1.8)
activemodel (4.1.9)
activesupport (= 4.1.9)
builder (~> 3.1)
activerecord (4.1.8)
activemodel (= 4.1.8)
activesupport (= 4.1.8)
activerecord (4.1.9)
activemodel (= 4.1.9)
activesupport (= 4.1.9)
arel (~> 5.0.0)
activesupport (4.1.8)
activesupport (4.1.9)
i18n (~> 0.6, >= 0.6.9)
json (~> 1.7, >= 1.7.7)
minitest (~> 5.1)
@ -66,9 +66,7 @@ GEM
debug_inspector (0.0.2)
diff-lcs (1.2.5)
docile (1.1.5)
dotenv (0.11.1)
dotenv-deployment (~> 0.0.2)
dotenv-deployment (0.0.2)
dotenv (1.0.2)
email_reply_parser (0.5.8)
ember-data-source (0.14)
ember-source
@ -171,8 +169,8 @@ GEM
fog-xml (0.1.1)
fog-core
nokogiri (~> 1.5, >= 1.5.11)
foreman (0.75.0)
dotenv (~> 0.11.1)
foreman (0.77.0)
dotenv (~> 1.0.2)
thor (~> 0.19.1)
formatador (0.2.5)
fspath (2.0.6)
@ -181,13 +179,13 @@ GEM
sorcerer (>= 0.3.7)
guess_html_encoding (0.0.9)
handlebars-source (2.0.0)
hashie (3.3.2)
hashie (3.4.0)
highline (1.6.21)
hike (1.2.3)
hiredis (0.6.0)
hitimes (1.2.2)
htmlentities (4.3.3)
i18n (0.6.11)
i18n (0.7.0)
image_optim (0.9.1)
exifr (~> 1.1.3)
fspath (~> 2.0.5)
@ -209,8 +207,8 @@ GEM
thor (~> 0.15)
libv8 (3.16.14.7)
listen (0.7.3)
logster (0.1.6)
lru_redux (0.8.1)
logster (0.1.7)
lru_redux (0.8.4)
mail (2.5.4)
mime-types (~> 1.16)
treetop (~> 1.4.8)
@ -274,7 +272,7 @@ GEM
omniauth-twitter (1.0.1)
multi_json (~> 1.3)
omniauth-oauth (~> 1.0)
onebox (1.5.12)
onebox (1.5.13)
moneta (~> 0.7)
multi_json (~> 1.7)
mustache (~> 0.99)
@ -298,30 +296,30 @@ GEM
qunit-rails (0.0.7)
railties
rack (1.5.2)
rack-mini-profiler (0.9.2)
rack-mini-profiler (0.9.3)
rack (>= 1.1.3)
rack-openid (1.3.1)
rack (>= 1.1.0)
ruby-openid (>= 2.1.8)
rack-protection (1.5.3)
rack
rack-test (0.6.2)
rack-test (0.6.3)
rack (>= 1.0)
rails (4.1.8)
actionmailer (= 4.1.8)
actionpack (= 4.1.8)
actionview (= 4.1.8)
activemodel (= 4.1.8)
activerecord (= 4.1.8)
activesupport (= 4.1.8)
rails (4.1.9)
actionmailer (= 4.1.9)
actionpack (= 4.1.9)
actionview (= 4.1.9)
activemodel (= 4.1.9)
activerecord (= 4.1.9)
activesupport (= 4.1.9)
bundler (>= 1.3.0, < 2.0)
railties (= 4.1.8)
railties (= 4.1.9)
sprockets-rails (~> 2.0)
rails-observers (0.1.2)
activemodel (~> 4.0)
railties (4.1.8)
actionpack (= 4.1.8)
activesupport (= 4.1.8)
railties (4.1.9)
actionpack (= 4.1.9)
activesupport (= 4.1.9)
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
raindrops (0.13.0)
@ -391,7 +389,7 @@ GEM
shoulda-context (1.2.1)
shoulda-matchers (2.7.0)
activesupport (>= 3.0.0)
sidekiq (3.3.1)
sidekiq (3.3.2)
celluloid (>= 0.16.0)
connection_pool (>= 2.1.1)
json
@ -434,7 +432,7 @@ GEM
thor (0.19.1)
thread_safe (0.3.4)
tilt (1.4.1)
timecop (0.7.1)
timecop (0.7.3)
timers (4.0.1)
hitimes
treetop (1.4.15)

View File

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

View File

@ -13,6 +13,42 @@ export default Ember.ArrayController.extend({
sortProperties: ['granted_at'],
sortAscending: false,
groupedBadges: function(){
const badges = this.get('model');
var grouped = _.groupBy(badges, badge => badge.badge_id);
var expanded = [];
const expandedBadges = badges.get('expandedBadges');
_(grouped).each(function(badges){
var lastGranted = badges[0].granted_at;
_.each(badges, function(badge) {
lastGranted = lastGranted < badge.granted_at ? badge.granted_at : lastGranted;
});
if(badges.length===1 || _.include(expandedBadges, badges[0].badge.id)){
_.each(badges, badge => expanded.push(badge));
return;
}
var result = {
badge: badges[0].badge,
granted_at: lastGranted,
badges: badges,
count: badges.length,
grouped: true
};
expanded.push(result);
});
return _(expanded).sortBy(group => group.granted_at).reverse().value();
}.property('model', 'model.@each', 'model.expandedBadges.@each'),
/**
Array of badges that have not been granted to this user.
@ -45,6 +81,12 @@ export default Ember.ArrayController.extend({
actions: {
expandGroup: function(userBadge){
const model = this.get('model');
model.set('expandedBadges', model.get('expandedBadges') || []);
model.get('expandedBadges').pushObject(userBadge.badge.id);
},
/**
Grant the selected badge to the user.
@ -53,7 +95,8 @@ export default Ember.ArrayController.extend({
**/
grantBadge: function(badgeId) {
var self = this;
Discourse.UserBadge.grant(badgeId, this.get('user.username')).then(function(userBadge) {
Discourse.UserBadge.grant(badgeId, this.get('user.username'), this.get('badgeReason')).then(function(userBadge) {
self.set('badgeReason', '');
self.pushObject(userBadge);
Ember.run.next(function() {
// Update the selected badge ID after the combobox has re-rendered.

View File

@ -1,5 +1,4 @@
import ModalFunctionality from 'discourse/mixins/modal-functionality';
import ObjectController from 'discourse/controllers/object';
export default ObjectController.extend(ModalFunctionality, {
@ -7,14 +6,16 @@ export default ObjectController.extend(ModalFunctionality, {
newSelected: Ember.computed.equal('selectedTab', 'new'),
onShow: function() {
this.selectNew();
this.send("selectNew");
},
selectNew: function() {
this.set('selectedTab', 'new');
},
actions: {
selectNew: function() {
this.set('selectedTab', 'new');
},
selectPrevious: function() {
this.set('selectedTab', 'previous');
selectPrevious: function() {
this.set('selectedTab', 'previous');
}
}
});

View File

@ -92,9 +92,9 @@ Discourse.Report = Discourse.Model.extend({
icon: function() {
switch( this.get('type') ) {
case 'flags':
return 'fa-flag';
return 'flag';
case 'likes':
return 'fa-heart';
return 'heart';
default:
return null;
}

View File

@ -1,8 +1,4 @@
Discourse.AdminRoute = Discourse.Route.extend({
renderTemplate: function() {
this.render('admin/templates/admin');
},
titleToken: function() {
return I18n.t('admin_title');
}

View File

@ -8,7 +8,7 @@ Discourse.AdminUserRoute = Discourse.Route.extend({
},
renderTemplate: function() {
this.render({into: 'admin/templates/admin'});
this.render({into: 'admin'});
},
afterModel: function(adminUser) {

View File

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

View File

@ -16,7 +16,7 @@
</tr>
</thead>
{{#unless loading}}
{{ render 'admin/templates/reports/trust_levels_report' users_by_trust_level tagName="tbody" }}
{{ render 'admin/templates/reports/trust_levels_report' users_by_trust_level tagName="tbody"}}
{{/unless}}
</table>
</div>
@ -52,13 +52,13 @@
</thead>
{{#unless loading}}
{{ render 'admin/templates/reports/per_day_counts_report' visits tagName="tbody"}}
{{ render 'admin_report_counts' signups }}
{{ render 'admin_report_counts' topics }}
{{ render 'admin_report_counts' posts }}
{{ render 'admin_report_counts' likes }}
{{ render 'admin_report_counts' flags }}
{{ render 'admin_report_counts' bookmarks }}
{{ render 'admin_report_counts' emails }}
{{admin-report-counts report=signups}}
{{admin-report-counts report=topics}}
{{admin-report-counts report=posts}}
{{admin-report-counts report=likes}}
{{admin-report-counts report=flags}}
{{admin-report-counts report=bookmarks}}
{{admin-report-counts report=emails}}
{{/unless}}
</table>
</div>
@ -76,10 +76,10 @@
</tr>
</thead>
{{#unless loading}}
{{ render 'admin_report_counts' page_view_anon_reqs }}
{{ render 'admin_report_counts' page_view_logged_in_reqs }}
{{ render 'admin_report_counts' page_view_crawler_reqs }}
{{ render 'admin_report_counts' page_view_total_reqs }}
{{admin-report-counts report=page_view_anon_reqs}}
{{admin-report-counts report=page_view_logged_in_reqs}}
{{admin-report-counts report=page_view_crawler_reqs}}
{{admin-report-counts report=page_view_total_reqs}}
{{/unless}}
</table>
</div>
@ -98,11 +98,11 @@
</tr>
</thead>
{{#unless loading}}
{{ render 'admin_report_counts' user_to_user_private_messages }}
{{ render 'admin_report_counts' system_private_messages }}
{{ render 'admin_report_counts' notify_moderators_private_messages }}
{{ render 'admin_report_counts' notify_user_private_messages }}
{{ render 'admin_report_counts' moderator_warning_private_messages }}
{{admin-report-counts report=user_to_user_private_messages}}
{{admin-report-counts report=system_private_messages}}
{{admin-report-counts report=notify_moderators_private_messages}}
{{admin-report-counts report=notify_user_private_messages}}
{{admin-report-counts report=moderator_warning_private_messages}}
{{/unless}}
</table>
</div>
@ -145,12 +145,12 @@
</tr>
</thead>
{{#unless loading}}
{{ render 'admin_report_counts' http_2xx_reqs }}
{{ render 'admin_report_counts' http_3xx_reqs}}
{{ render 'admin_report_counts' http_4xx_reqs}}
{{ render 'admin_report_counts' http_5xx_reqs}}
{{ render 'admin_report_counts' http_background_reqs }}
{{ render 'admin_report_counts' http_total_reqs }}
{{admin-report-counts report=http_2xx_reqs}}
{{admin-report-counts report=http_3xx_reqs}}
{{admin-report-counts report=http_4xx_reqs}}
{{admin-report-counts report=http_5xx_reqs}}
{{admin-report-counts report=http_background_reqs}}
{{admin-report-counts report=http_total_reqs}}
{{/unless}}
</table>
</div>

View File

@ -1,19 +1,73 @@
<section class="field">
<b>{{i18n 'admin.customize.css'}}</b>:
{{#if stylesheet}}
({{i18n 'character_count' count=stylesheet.length}})
{{/if}}
<br/>
{{textarea value=stylesheet class="plain"}}
</section>
<section class="field">
<b>{{i18n 'admin.customize.header'}}</b>:
{{#if header}}
({{i18n 'character_count' count=header.length}})
{{/if}}
<br/>
{{textarea value=header class="plain"}}
</section>
<section class="field">
<b>{{i18n 'admin.customize.enabled'}}</b>: {{enabled}}
</section>
{{#if stylesheet}}
<section class="field">
<b>{{i18n 'admin.customize.css'}}</b>: ({{i18n 'character_count' count=stylesheet.length}})
<br/>
{{textarea value=stylesheet class="plain"}}
</section>
{{/if}}
{{#if mobile_stylesheet}}
<section class="field">
<b>{{fa-icon "mobile"}}&nbsp;{{i18n 'admin.customize.css'}}</b>: ({{i18n 'character_count' count=mobile_stylesheet.length}})
<br/>
{{textarea value=mobile_stylesheet class="plain"}}
</section>
{{/if}}
{{#if header}}
<section class="field">
<b>{{i18n 'admin.customize.header'}}</b>: ({{i18n 'character_count' count=header.length}})
<br/>
{{textarea value=header class="plain"}}
</section>
{{/if}}
{{#if mobile_header}}
<section class="field">
<b>{{fa-icon "mobile"}}&nbsp;{{i18n 'admin.customize.header'}}</b>: ({{i18n 'character_count' count=mobile_header.length}})
<br/>
{{textarea value=mobile_header class="plain"}}
</section>
{{/if}}
{{#if top}}
<section class="field">
<b>{{i18n 'admin.customize.top'}}</b>: ({{i18n 'character_count' count=top.length}})
<br/>
{{textarea value=top class="plain"}}
</section>
{{/if}}
{{#if mobile_top}}
<section class="field">
<b>{{fa-icon "mobile"}}&nbsp;{{i18n 'admin.customize.top'}}</b>: ({{i18n 'character_count' count=mobile_top.length}})
<br/>
{{textarea value=mobile_top class="plain"}}
</section>
{{/if}}
{{#if footer}}
<section class="field">
<b>{{i18n 'admin.customize.footer'}}</b>: ({{i18n 'character_count' count=footer.length}})
<br/>
{{textarea value=footer class="plain"}}
</section>
{{/if}}
{{#if mobile_footer}}
<section class="field">
<b>{{fa-icon "mobile"}}&nbsp;{{i18n 'admin.customize.footer'}}</b>: ({{i18n 'character_count' count=mobile_footer.length}})
<br/>
{{textarea value=mobile_footer class="plain"}}
</section>
{{/if}}
{{#if head_tag}}
<section class="field">
<b>{{fa-icon "file-text-o"}}&nbsp;{{i18n 'admin.customize.head_tag.text'}}</b>: ({{i18n 'character_count' count=head_tag.length}})
<br/>
{{textarea value=head_tag class="plain"}}
</section>
{{/if}}
{{#if body_tag}}
<section class="field">
<b>{{fa-icon "file-text-o"}}&nbsp;{{i18n 'admin.customize.body_tag.text'}}</b>: ({{i18n 'character_count' count=body_tag.length}})
<br/>
{{textarea value=body_tag class="plain"}}
</section>
{{/if}}

View File

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

View File

@ -3,6 +3,7 @@
</div>
<div class="setting-value">
{{list-setting settingValue=value choices=choices settingName=setting}}
<div {{bind-attr class=":validation-error validationMessage::hidden"}}><i class='fa fa-times'></i> {{validationMessage}}</div>
<div class='desc'>{{{unbound description}}}</div>
</div>
{{#if dirty}}

View File

@ -9,40 +9,53 @@
{{#loading-spinner condition=loading}}
<div class='admin-container user-badges'>
<h2>{{i18n 'admin.badges.grant_badge'}}</h2>
<br>
{{#if noBadges}}
<p>{{i18n 'admin.badges.no_badges'}}</p>
{{else}}
<br>
<form class="form-horizontal">
<div>
<label>{{i18n 'admin.badges.badge'}}</label>
{{combo-box valueAttribute="id" value=controller.selectedBadgeId content=controller.grantableBadges}}
</div>
<label>
<label>{{i18n 'admin.badges.reason'}}</label>
{{input type="text" value=badgeReason}}<br><small>{{i18n 'admin.badges.reason_help'}}</small>
</label>
<button class='btn btn-primary' {{action "grantBadge" controller.selectedBadgeId}}>{{i18n 'admin.badges.grant'}}</button>
</form>
{{/if}}
<br>
<br>
<h2>{{i18n 'admin.badges.granted_badges'}}</h2>
<br>
<table>
<table id='user-badges'>
<tr>
<th>{{i18n 'admin.badges.badge'}}</th>
<th>{{i18n 'admin.badges.granted_by'}}</th>
<th class='reason'>{{i18n 'admin.badges.reason'}}</th>
<th>{{i18n 'admin.badges.granted_at'}}</th>
<th></th>
</tr>
{{#each}}
{{#each userBadge in groupedBadges}}
<tr>
<td>{{user-badge badge=badge}}</td>
<td>{{user-badge badge=userBadge.badge count=userBadge.count}}</td>
<td>
{{#link-to 'adminUser' badge.granted_by}}
{{avatar granted_by imageSize="tiny"}}
{{granted_by.username}}
{{#link-to 'adminUser' userBadge.badge.granted_by}}
{{avatar userBadge.granted_by imageSize="tiny"}}
{{userBadge.granted_by.username}}
{{/link-to}}
</td>
<td>{{age-with-tooltip granted_at}}</td>
<td class='reason'>
{{#if userBadge.postUrl}}
<a href="{{unbound userBadge.postUrl}}">{{userBadge.topic_title}}</a>
{{/if}}
</td>
<td>{{age-with-tooltip userBadge.granted_at}}</td>
<td>
<button class='btn' {{action "revokeBadge" this}}>{{i18n 'admin.badges.revoke'}}</button>
{{#if userBadge.grouped}}
<button class='btn' {{action "expandGroup" userBadge}}>{{{i18n 'admin.badges.expand'}}}</button>
{{else}}
<button class='btn btn-danger' {{action "revokeBadge" userBadge}}>{{i18n 'admin.badges.revoke'}}</button>
{{/if}}
</td>
</tr>
{{else}}

View File

@ -14,7 +14,7 @@
{{#unless loading}}
<tbody>
<td class="title">{{i18n 'admin.dashboard.version'}}</td>
<td class="version-number"><a {{bind-attr href="versionCheck.gitLink"}} target="_blank">{{ versionCheck.installed_version }}</a></td>
<td class="version-number"><a {{bind-attr href="versionCheck.gitLink"}} target="_blank">{{ versionCheck.installed_describe }}</a></td>
{{#if versionCheck.noCheckPerformed}}
<td class="version-number">&nbsp;</td>

View File

@ -1,4 +1,4 @@
Discourse.AdminBackupsView = Discourse.View.extend({
export default Discourse.View.extend({
classNames: ["admin-backups"],
_hijackDownloads: function() {

View File

@ -0,0 +1,12 @@
export default Discourse.View.extend({
_disableCustomStylesheets: function() {
if (this.session.get("disableCustomCSS")) {
$("link.custom-css").attr("rel", "");
this.session.set("disableCustomCSS", false);
}
}.on("willInsertElement"),
_enableCustomStylesheets: function() {
$("link.custom-css").attr("rel", "stylesheet");
}.on("willDestroyElement")
});

View File

@ -1,3 +0,0 @@
Discourse.AdminApiView = Discourse.View.extend({
templateName: 'admin/templates/api'
});

View File

@ -1,13 +0,0 @@
/**
The default view in the admin section
@class AdminDashboardView
@extends Discourse.View
@namespace Discourse
@module Discourse
**/
Discourse.AdminDashboardView = Discourse.View.extend({
templateName: 'admin/templates/dashboard'
});

View File

@ -1,4 +0,0 @@
Discourse.AdminReportCountsView = Discourse.View.extend({
templateName: 'admin/templates/reports/summed_counts_report',
tagName: 'tbody'
});

View File

@ -1,12 +1,4 @@
/**
A view to display a site setting with edit controls
@class SiteSettingView
@extends Discourse.View
@namespace Discourse
@module Discourse
**/
Discourse.SiteSettingView = Discourse.View.extend(Discourse.ScrollTop, {
export default Discourse.View.extend(Discourse.ScrollTop, {
classNameBindings: [':row', ':setting', 'content.overridden'],
preview: function() {

View File

@ -126,3 +126,6 @@ window.Discourse = Ember.Application.createWithMixins(Discourse.Ajax, {
}.property()
});
// TODO: Remove this, it is in for backwards compatibiltiy with plugins
Discourse.HasCurrentUser = {};

View File

@ -1,29 +1,52 @@
const ADMIN_MODELS = ['plugin'];
function plural(type) {
return type + 's';
}
function pathFor(type) {
const path = "/" + plural(type);
if (ADMIN_MODELS.indexOf(type) !== -1) {
return "/admin/" + path;
}
return path;
}
const _identityMap = {};
const RestModel = Ember.Object.extend({
update(attrs) {
const self = this;
return this.store.update(this.get('__type'), this.get('id'), attrs).then(function(result) {
self.setProperties(attrs);
return result;
});
}
});
export default Ember.Object.extend({
serverName(type) {
return Ember.String.underscore(type + 's');
},
pathFor(type, id) {
let path = "/" + this.serverName(type);
if (ADMIN_MODELS.indexOf(type) !== -1) { path = "/admin/" + path; }
if (id) { path += "/" + id; }
return path;
},
findAll(type) {
var self = this;
return Discourse.ajax(pathFor(type)).then(function(result) {
return result[plural(type)].map(obj => self._hydrate(type, obj));
return Discourse.ajax(this.pathFor(type)).then(function(result) {
return result[self.serverName(type)].map(obj => self._hydrate(type, obj));
});
},
find(type, id) {
var self = this;
return Discourse.ajax(this.pathFor(type, id)).then(function(result) {
return self._hydrate(type, result[self.serverName(type)]);
});
},
update(type, id, attrs) {
const data = {};
data[this.serverName(type)] = attrs;
return Discourse.ajax(this.pathFor(type, id), { method: 'PUT', data });
},
_hydrate(type, obj) {
if (!obj) { throw "Can't hydrate " + type + " of `null`"; }
if (!obj.id) { throw "Can't hydrate " + type + " without an `id`"; }
@ -37,7 +60,10 @@ export default Ember.Object.extend({
return existing;
}
const klass = this.container.lookupFactory('model:' + type) || Ember.Object;
obj.store = this;
obj.__type = type;
const klass = this.container.lookupFactory('model:' + type) || RestModel;
const model = klass.create(obj);
_identityMap[type][obj.id] = model;
return model;

View File

@ -1,18 +1,11 @@
/**
A breadcrumb including category drop downs
@class BreadCrumbsComponent
@extends Ember.Component
@namespace Discourse
@module Discourse
**/
// A breadcrumb including category drop downs
export default Ember.Component.extend({
classNames: ['category-breadcrumb'],
tagName: 'ol',
parentCategory: Em.computed.alias('category.parentCategory'),
parentCategories: Em.computed.filter('categories', function(c) {
if (c.id === Discourse.Site.currentProp("uncategorized_category_id") && !Discourse.SiteSettings.allow_uncategorized_topics) {
if (c.id === this.site.get("uncategorized_category_id") && !this.siteSettings.allow_uncategorized_topics) {
// Don't show "uncategorized" if allow_uncategorized_topics setting is false.
return false;
}

View File

@ -1,16 +1,13 @@
import NotificationsButton from 'discourse/views/notifications-button';
import NotificationsButton from 'discourse/components/notifications-button';
export default NotificationsButton.extend({
classNames: ['notification-options', 'category-notification-menu'],
buttonIncludesText: false,
longDescriptionBinding: null,
hidden: Em.computed.alias('category.deleted'),
notificationLevels: Discourse.Category.NotificationLevel,
notificationLevel: Em.computed.alias('category.notification_level'),
i18nPrefix: 'category.notifications',
i18nPostfix: '',
clicked: function(id) {
clicked(id) {
this.get('category').setNotification(id);
}
});

View File

@ -1,13 +1,13 @@
import StringBuffer from 'discourse/mixins/string-buffer';
export default Discourse.View.extend(StringBuffer, {
export default Ember.Component.extend(StringBuffer, {
classNameBindings: [':btn-group', 'hidden'],
rerenderTriggers: ['text', 'longDescription'],
_bindClick: function() {
// If there's a click handler, call it
if (this.clicked) {
var self = this;
const self = this;
this.$().on('click.dropdown-button', 'ul li', function(e) {
e.preventDefault();
if ($(e.currentTarget).data('id') !== self.get('activeItem')) {
@ -23,7 +23,7 @@ export default Discourse.View.extend(StringBuffer, {
this.$().off('click.dropdown-button', 'ul li');
}.on('willDestroyElement'),
renderString: function(buffer) {
renderString(buffer) {
buffer.push("<h4 class='title'>" + this.get('title') + "</h4>");
buffer.push("<button class='btn standard dropdown-toggle' data-toggle='dropdown'>");
@ -31,27 +31,24 @@ export default Discourse.View.extend(StringBuffer, {
buffer.push("</button>");
buffer.push("<ul class='dropdown-menu'>");
var contents = this.get('dropDownContent');
const contents = this.get('dropDownContent');
if (contents) {
var self = this;
const self = this;
contents.forEach(function(row) {
var id = row.id,
title = row.title,
iconClass = row.styleClasses,
description = row.description,
className = (self.get('activeItem') === id ? 'disabled': '');
const id = row.id,
className = (self.get('activeItem') === id ? 'disabled': '');
buffer.push("<li data-id=\"" + id + "\" class=\"" + className + "\"><a href>");
buffer.push("<span class='icon " + iconClass + "'></span>");
buffer.push("<div><span class='title'>" + title + "</span>");
buffer.push("<span>" + description + "</span></div>");
buffer.push("<span class='icon " + row.styleClasses + "'></span>");
buffer.push("<div><span class='title'>" + row.title + "</span>");
buffer.push("<span>" + row.description + "</span></div>");
buffer.push("</a></li>");
});
}
buffer.push("</ul>");
var desc = this.get('longDescription');
const desc = this.get('longDescription');
if (desc) {
buffer.push("<p>");
buffer.push(desc);

View File

@ -0,0 +1,11 @@
export default Ember.Component.extend({
classNameBindings: [':featured-topic'],
click(e) {
const $target = $(e.target);
if ($target.closest('.last-posted-at').length) {
this.sendAction('action', {topic: this.get('topic'), position: $target.offset()});
return false;
}
}
});

View File

@ -1,11 +1,11 @@
import DropdownButtonView from 'discourse/views/dropdown-button';
import DropdownButton from 'discourse/components/dropdown-button';
import NotificationLevels from 'discourse/lib/notification-levels';
export default DropdownButtonView.extend({
const NotificationsButton = DropdownButton.extend({
classNames: ['notification-options'],
title: '',
buttonIncludesText: true,
activeItem: Em.computed.alias('notificationLevel'),
notificationLevels: [],
i18nPrefix: '',
i18nPostfix: '',
watchingClasses: 'fa fa-exclamation-circle watching',
@ -21,15 +21,14 @@ export default DropdownButtonView.extend({
}.property(),
dropDownContent: function() {
var contents = [],
prefix = this.get('i18nPrefix'),
postfix = this.get('i18nPostfix'),
levels = this.get('notificationLevels');
const contents = [],
prefix = this.get('i18nPrefix'),
postfix = this.get('i18nPostfix');
_.each(this.get('options'), function(pair) {
if (postfix === '_pm' && pair[1] === 'regular') { return; }
contents.push({
id: levels[pair[0]],
id: NotificationLevels[pair[0]],
title: I18n.t(prefix + '.' + pair[1] + postfix + '.title'),
description: I18n.t(prefix + '.' + pair[1] + postfix + '.description'),
styleClasses: pair[2]
@ -40,21 +39,20 @@ export default DropdownButtonView.extend({
}.property(),
text: function() {
var self = this,
prefix = this.get('i18nPrefix'),
postfix = this.get('i18nPostfix'),
levels = this.get('notificationLevels');
const self = this,
prefix = this.get('i18nPrefix'),
postfix = this.get('i18nPostfix');
var key = (function() {
const key = (function() {
switch (this.get('notificationLevel')) {
case levels.WATCHING: return 'watching';
case levels.TRACKING: return 'tracking';
case levels.MUTED: return 'muted';
case NotificationLevels.WATCHING: return 'watching';
case NotificationLevels.TRACKING: return 'tracking';
case NotificationLevels.MUTED: return 'muted';
default: return 'regular';
}
}).call(this);
var icon = (function() {
const icon = (function() {
switch (key) {
case 'watching': return '<i class="' + self.watchingClasses + '"></i>&nbsp;';
case 'tracking': return '<i class="' + self.trackingClasses + '"></i>&nbsp;';
@ -65,8 +63,11 @@ export default DropdownButtonView.extend({
return icon + ( this.get('buttonIncludesText') ? I18n.t(prefix + '.' + key + postfix + ".title") : '') + "<span class='caret'></span>";
}.property('notificationLevel'),
clicked: function(/* id */) {
clicked(/* id */) {
// sub-class needs to implement this
}
});
export default NotificationsButton;
export { NotificationLevels };

View File

@ -1,22 +1,20 @@
import DropdownButtonView from 'discourse/views/dropdown-button';
import DropdownButton from 'discourse/components/dropdown-button';
export default DropdownButtonView.extend({
export default DropdownButton.extend({
descriptionKey: 'help',
classNames: ['pinned-options'],
title: '',
longDescription: function(){
var topic = this.get('topic');
var globally = topic.get('pinned_globally') ? '_globally' : '';
var key = 'topic_statuses.' + (topic.get('pinned') ? 'pinned' + globally : 'unpinned') + '.help';
const topic = this.get('topic');
const globally = topic.get('pinned_globally') ? '_globally' : '';
const key = 'topic_statuses.' + (topic.get('pinned') ? 'pinned' + globally : 'unpinned') + '.help';
return I18n.t(key);
}.property('topic.pinned'),
topic: Em.computed.alias('controller.model'),
target: Em.computed.alias('topic'),
hidden: function(){
var topic = this.get('topic');
const topic = this.get('topic');
return topic.get('deleted') || (!topic.get('pinned') && !topic.get('unpinned'));
}.property('topic.pinned', 'topic.deleted', 'topic.unpinned'),
@ -25,7 +23,7 @@ export default DropdownButtonView.extend({
}.property('topic.pinned'),
dropDownContent: function() {
var globally = this.get('topic.pinned_globally') ? '_globally' : '';
const globally = this.get('topic.pinned_globally') ? '_globally' : '';
return [
{id: 'pinned',
title: I18n.t('topic_statuses.pinned' + globally + '.title'),
@ -39,15 +37,15 @@ export default DropdownButtonView.extend({
}.property(),
text: function() {
var globally = this.get('topic.pinned_globally') ? '_globally' : '';
var state = this.get('topic.pinned') ? 'pinned' + globally : 'unpinned';
const globally = this.get('topic.pinned_globally') ? '_globally' : '';
const state = this.get('topic.pinned') ? 'pinned' + globally : 'unpinned';
return '<span class="fa fa-thumb-tack' + (state === 'unpinned' ? ' unpinned' : "") + '"></span> ' +
I18n.t('topic_statuses.' + state + '.title') + "<span class='caret'></span>";
}.property('topic.pinned', 'topic.unpinned'),
clicked: function(id) {
var topic = this.get('topic');
clicked(id) {
const topic = this.get('topic');
if(id==='unpinned'){
topic.clearPin();
} else {

View File

@ -0,0 +1,16 @@
import NotificationsButton from 'discourse/components/notifications-button';
export default NotificationsButton.extend({
longDescription: Em.computed.alias('topic.details.notificationReasonText'),
hidden: Em.computed.alias('topic.deleted'),
notificationLevel: Em.computed.alias('topic.details.notification_level'),
i18nPrefix: 'topic.notifications',
i18nPostfix: function() {
return this.get('topic.isPrivateMessage') ? '_pm' : '';
}.property('topic.isPrivateMessage'),
clicked(id) {
this.get('topic.details').updateNotifications(id);
}
});

View File

@ -1,9 +1,13 @@
import ObjectController from 'discourse/controllers/object';
export default ObjectController.extend({
faqOverriden: Ember.computed.gt('siteSettings.faq_url.length', 0),
contactInfo: function() {
if (Discourse.SiteSettings.contact_email) {
return I18n.t('about.contact_info', {contact_email: Discourse.SiteSettings.contact_email});
if (this.siteSettings.contact_url) {
return I18n.t('about.contact_info', {contact_info: "<a href='"+ this.siteSettings.contact_url +"' target='_blank'>"+ this.siteSettings.contact_url +"</a>"});
} else if (this.siteSettings.contact_email) {
return I18n.t('about.contact_info', {contact_info: this.siteSettings.contact_email});
} else {
return null;
}

View File

@ -48,6 +48,10 @@ export default ObjectController.extend({
_showFooter: function() {
this.set("controllers.application.showFooter", !this.get("canLoadMore"));
}.observes("canLoadMore")
}.observes("canLoadMore"),
showLongDescription: function(){
return window.location.search.match("long-description");
}.property('userBadges')
});

View File

@ -1,3 +1,5 @@
import NotificationLevels from 'discourse/lib/notification-levels';
// Support for changing the notification level of various topics
export default Em.Controller.extend({
needs: ['topic-bulk-actions'],
@ -5,9 +7,9 @@ export default Em.Controller.extend({
notificationLevels: function() {
var result = [];
Object.keys(Discourse.Topic.NotificationLevel).forEach(function(k) {
Object.keys(NotificationLevels).forEach(function(k) {
result.push({
id: Discourse.Topic.NotificationLevel[k].toString(),
id: NotificationLevels[k].toString(),
name: I18n.t('topic.notifications.' + k.toLowerCase() + ".title"),
description: I18n.t('topic.notifications.' + k.toLowerCase() + ".description")
});

View File

@ -5,50 +5,31 @@ export default Ember.ArrayController.extend({
// Whether we've checked our messages
checkedMessages: false,
/**
Initialize the controller
**/
init: function() {
init() {
this._super();
this.reset();
},
actions: {
/**
Closes and hides a message.
@method closeMessage
@params {Object} message The message to dismiss
**/
closeMessage: function(message) {
closeMessage(message) {
this.removeObject(message);
},
hideMessage: function(message) {
var messagesByTemplate = this.get('messagesByTemplate'),
templateName = message.get('templateName');
// kind of hacky but the visibility depends on this
messagesByTemplate[templateName] = undefined;
hideMessage(message) {
this.removeObject(message);
}
},
// kind of hacky but the visibility depends on this
this.get('messagesByTemplate')[message.get('templateName')] = undefined;
},
/**
Displays a new message
popup(message) {
let messagesByTemplate = this.get('messagesByTemplate');
const templateName = message.get('templateName');
@method popup
@params {Object} msg The message to display
**/
popup: function(msg) {
var messagesByTemplate = this.get('messagesByTemplate'),
templateName = msg.get('templateName'),
existing = messagesByTemplate[templateName];
if (!existing) {
this.pushObject(msg);
messagesByTemplate[templateName] = msg;
}
if (!messagesByTemplate[templateName]) {
this.pushObject(message);
messagesByTemplate[templateName] = message;
}
},
},
/**
@ -56,11 +37,13 @@ export default Ember.ArrayController.extend({
@method reset
**/
reset: function() {
reset() {
this.clear();
this.set('messagesByTemplate', {});
this.set('queuedForTyping', []);
this.set('checkedMessages', false);
this.setProperties({
messagesByTemplate: {},
queuedForTyping: [],
checkedMessages: false
});
},
/**
@ -69,11 +52,8 @@ export default Ember.ArrayController.extend({
@method typedReply
**/
typedReply: function() {
var self = this;
this.get('queuedForTyping').forEach(function (msg) {
self.popup(msg);
});
typedReply() {
this.get('queuedForTyping').forEach(msg => this.popup(msg));
},
/**
@ -82,11 +62,11 @@ export default Ember.ArrayController.extend({
@method queryFor
@params {Discourse.Composer} composer The composer model
**/
queryFor: function(composer) {
queryFor(composer) {
if (this.get('checkedMessages')) { return; }
var self = this,
queuedForTyping = self.get('queuedForTyping');
const self = this;
let queuedForTyping = self.get('queuedForTyping');
Discourse.ComposerMessage.find(composer).then(function (messages) {
self.set('checkedMessages', true);

View File

@ -29,18 +29,18 @@ export default DiscourseController.extend({
actions: {
// Toggle the reply view
toggle: function() {
toggle() {
this.toggle();
},
togglePreview: function() {
togglePreview() {
this.get('model').togglePreview();
},
// Import a quote from the post
importQuote: function() {
var postStream = this.get('topic.postStream'),
postId = this.get('model.post.id');
importQuote() {
const postStream = this.get('topic.postStream');
let postId = this.get('model.post.id');
// If there is no current post, use the first post id from the stream
if (!postId && postStream) {
@ -49,9 +49,9 @@ export default DiscourseController.extend({
// 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');
const replyToPostNumber = this.get('model.post.reply_to_post_number');
if (replyToPostNumber) {
var replyPost = postStream.get('posts').findBy('post_number', replyToPostNumber);
const replyPost = postStream.get('posts').findBy('post_number', replyToPostNumber);
if (replyPost) {
postId = replyPost.get('id');
}
@ -60,34 +60,34 @@ export default DiscourseController.extend({
if (postId) {
this.set('model.loading', true);
var composer = this;
const composer = this;
return Discourse.Post.load(postId).then(function(post) {
var quote = Discourse.Quote.build(post, post.get("raw"));
const quote = Discourse.Quote.build(post, post.get("raw"));
composer.appendBlockAtCursor(quote);
composer.set('model.loading', false);
});
}
},
cancel: function() {
cancel() {
this.cancelComposer();
},
save: function() {
save() {
this.save();
},
displayEditReason: function() {
displayEditReason() {
this.set("showEditReason", true);
},
hitEsc: function() {
hitEsc() {
if (this.get('model.viewOpen')) {
this.shrink();
}
},
openIfDraft: function() {
openIfDraft() {
if (this.get('model.viewDraft')) {
this.set('model.composeState', Discourse.Composer.OPEN);
}
@ -95,35 +95,33 @@ export default DiscourseController.extend({
},
updateDraftStatus: function() {
var c = this.get('model');
updateDraftStatus() {
const c = this.get('model');
if (c) { c.updateDraftStatus(); }
},
appendText: function(text, opts) {
var c = this.get('model');
appendText(text, opts) {
const c = this.get('model');
if (c) {
opts = opts || {};
var wmd = $('#wmd-input');
var val = wmd.val() || '';
var position = opts.position === "cursor" ? wmd.caret() : val.length;
const wmd = $('#wmd-input'),
val = wmd.val() || '',
position = opts.position === "cursor" ? wmd.caret() : val.length,
caret = c.appendText(text, position, opts);
var caret = c.appendText(text, position, opts);
if(wmd[0]){
Em.run.next(function(){
Discourse.Utilities.setCaretPosition(wmd[0], caret);
});
if (wmd[0]) {
Em.run.next(() => Discourse.Utilities.setCaretPosition(wmd[0], caret));
}
}
},
appendTextAtCursor: function(text, opts) {
appendTextAtCursor(text, opts) {
opts = opts || {};
opts.position = "cursor";
this.appendText(text, opts);
},
appendBlockAtCursor: function(text, opts) {
appendBlockAtCursor(text, opts) {
opts = opts || {};
opts.position = "cursor";
opts.block = true;
@ -135,7 +133,7 @@ export default DiscourseController.extend({
}.property(),
toggle: function() {
toggle() {
this.closeAutocomplete();
switch (this.get('model.composeState')) {
case Discourse.Composer.OPEN:
@ -158,9 +156,9 @@ export default DiscourseController.extend({
return this.get('model.loading');
}.property('model.loading'),
save: function(force) {
var composer = this.get('model'),
self = this;
save(force) {
const composer = this.get('model'),
self = this;
// Clear the warning state if we're not showing the checkbox anymore
if (!this.get('showWarning')) {
@ -168,7 +166,7 @@ export default DiscourseController.extend({
}
if(composer.get('cantSubmitPost')) {
var now = Date.now();
const now = Date.now();
this.setProperties({
'view.showTitleTip': now,
'view.showCategoryTip': now,
@ -182,12 +180,12 @@ export default DiscourseController.extend({
// for now handle a very narrow use case
// if we are replying to a topic AND not on the topic pop the window up
if (!force && composer.get('replyingToTopic')) {
var topic = this.get('topic');
const topic = this.get('topic');
if (!topic || topic.get('id') !== composer.get('topic.id'))
{
var message = I18n.t("composer.posting_not_on_topic");
const message = I18n.t("composer.posting_not_on_topic");
var buttons = [{
let buttons = [{
"label": I18n.t("composer.cancel"),
"class": "cancel",
"link": true
@ -231,7 +229,7 @@ export default DiscourseController.extend({
opts = opts || {};
self.close();
var currentUser = Discourse.User.current();
const currentUser = Discourse.User.current();
if (composer.get('creatingTopic')) {
currentUser.set('topic_count', currentUser.get('topic_count') + 1);
} else {
@ -255,7 +253,7 @@ export default DiscourseController.extend({
@method checkReplyLength
**/
checkReplyLength: function() {
checkReplyLength() {
if (this.present('model.reply')) {
// Notify the composer messages controller that a reply has been typed. Some
// messages only appear after typing.
@ -269,55 +267,52 @@ export default DiscourseController.extend({
@method findSimilarTopics
**/
findSimilarTopics: function() {
findSimilarTopics() {
// We don't care about similar topics unless creating a topic
if (!this.get('model.creatingTopic')) return;
if (!this.get('model.creatingTopic')) { return; }
var body = this.get('model.reply'),
title = this.get('model.title'),
self = this,
message;
let body = this.get('model.reply');
const title = this.get('model.title');
// Ensure the fields are of the minimum length
if (body.length < Discourse.SiteSettings.min_body_similar_length ||
title.length < Discourse.SiteSettings.min_title_similar_length) { return; }
if (body.length < Discourse.SiteSettings.min_body_similar_length) { return; }
if (title.length < Discourse.SiteSettings.min_title_similar_length) { return; }
// TODO pass the 200 in from somewhere
body = body.substr(0, 200);
// Done search over and over
if((title + body) === this.get('lastSimilaritySearch')) { return; }
if ((title + body) === this.get('lastSimilaritySearch')) { return; }
this.set('lastSimilaritySearch', title + body);
var messageController = this.get('controllers.composer-messages'),
similarTopics = this.get('similarTopics');
const messageController = this.get('controllers.composer-messages'),
similarTopics = this.get('similarTopics');
let message = this.get('similarTopicsMessage');
if (!message) {
message = Discourse.ComposerMessage.create({
templateName: 'composer/similar_topics',
extraClass: 'similar-topics'
});
this.set('similarTopicsMessage', message);
}
Discourse.Topic.findSimilarTo(title, body).then(function (newTopics) {
similarTopics.clear();
similarTopics.pushObjects(newTopics);
if (similarTopics.get('length') > 0) {
message = Discourse.ComposerMessage.create({
templateName: 'composer/similar_topics',
similarTopics: similarTopics,
extraClass: 'similar-topics'
});
self.set('similarTopicsMessage', message);
messageController.popup(message);
} else {
message = self.get('similarTopicsMessage');
if (message) {
messageController.send('hideMessage', message);
}
message.set('similarTopics', similarTopics);
messageController.send("popup", message);
} else if (message) {
messageController.send("hideMessage", message);
}
});
},
saveDraft: function() {
var model = this.get('model');
saveDraft() {
const model = this.get('model');
if (model) { model.saveDraft(); }
},
@ -331,7 +326,7 @@ export default DiscourseController.extend({
@param {Discourse.Topic} [opts.topic] The topic we're replying to
@param {String} [opts.quote] If we're opening a reply from a quote, the quote we're making
**/
open: function(opts) {
open(opts) {
opts = opts || {};
if (!opts.draftKey) {
@ -345,15 +340,17 @@ export default DiscourseController.extend({
this.set('scopedCategoryId', opts.categoryId);
}
var composerMessages = this.get('controllers.composer-messages'),
self = this,
composerModel = this.get('model');
const composerMessages = this.get('controllers.composer-messages'),
self = this;
let composerModel = this.get('model');
this.setProperties({ showEditReason: false, editReason: null });
composerMessages.reset();
// If we want a different draft than the current composer, close it and clear our model.
if (composerModel && opts.draftKey !== composerModel.draftKey &&
if (composerModel &&
opts.draftKey !== composerModel.draftKey &&
composerModel.composeState === Discourse.Composer.DRAFT) {
this.close();
composerModel = null;
@ -396,7 +393,7 @@ export default DiscourseController.extend({
},
// Given a potential instance and options, set the model for this composer.
_setModel: function(composerModel, opts) {
_setModel(composerModel, opts) {
if (opts.draft) {
composerModel = Discourse.Composer.loadDraft(opts.draftKey, opts.draftSequence, opts.draft);
if (composerModel) {
@ -411,26 +408,25 @@ export default DiscourseController.extend({
composerModel.set('composeState', Discourse.Composer.OPEN);
composerModel.set('isWarning', false);
var composerMessages = this.get('controllers.composer-messages');
composerMessages.queryFor(composerModel);
this.get('controllers.composer-messages').queryFor(composerModel);
},
// View a new reply we've made
viewNewReply: function() {
viewNewReply() {
Discourse.URL.routeTo(this.get('createdPost.url'));
this.close();
return false;
},
destroyDraft: function() {
var key = this.get('model.draftKey');
destroyDraft() {
const key = this.get('model.draftKey');
if (key) {
Discourse.Draft.clear(key, this.get('model.draftSequence'));
}
},
cancelComposer: function() {
var self = this;
cancelComposer() {
const self = this;
return new Ember.RSVP.Promise(function (resolve) {
if (self.get('model.hasMetaData') || self.get('model.replyDirty')) {
@ -454,7 +450,7 @@ export default DiscourseController.extend({
},
shrink: function() {
shrink() {
if (this.get('model.replyDirty')) {
this.collapse();
} else {
@ -462,12 +458,12 @@ export default DiscourseController.extend({
}
},
collapse: function() {
collapse() {
this.saveDraft();
this.set('model.composeState', Discourse.Composer.DRAFT);
},
close: function() {
close() {
this.setProperties({
model: null,
'view.showTitleTip': false,
@ -476,11 +472,11 @@ export default DiscourseController.extend({
});
},
closeAutocomplete: function() {
closeAutocomplete() {
$('#wmd-input').autocomplete({ cancel: true });
},
showOptions: function() {
showOptions() {
var _ref;
return (_ref = this.get('controllers.modal')) ? _ref.show(Discourse.ArchetypeOptionsModalView.create({
archetype: this.get('model.archetype'),

View File

@ -1 +1 @@
export default Ember.Controller.extend(Discourse.Presence, Discourse.HasCurrentUser);
export default Ember.Controller.extend(Discourse.Presence);

View File

@ -310,12 +310,26 @@ export default DiscourseController.extend(ModalFunctionality, {
});
}
if (!this.blank('accountUsername') && this.get('accountPassword') === this.get('accountUsername')) {
return Discourse.InputValidation.create({
failed: true,
reason: I18n.t('user.password.same_as_username')
});
}
if (!this.blank('accountEmail') && this.get('accountPassword') === this.get('accountEmail')) {
return Discourse.InputValidation.create({
failed: true,
reason: I18n.t('user.password.same_as_email')
});
}
// Looks good!
return Discourse.InputValidation.create({
ok: true,
reason: I18n.t('user.password.ok')
});
}.property('accountPassword', 'rejectedPasswords.@each'),
}.property('accountPassword', 'rejectedPasswords.@each', 'accountUsername', 'accountEmail'),
fetchConfirmationValue: function() {
var createAccountController = this;

View File

@ -14,7 +14,7 @@ export default ObjectController.extend({
this.set("controllers.application.showFooter", this.get("loadedAllItems"));
}.observes("loadedAllItems"),
showMoreUrl: function(period) {
showMoreUrl(period) {
var url = '', category = this.get('category');
if (category) {
url = '/c/' + Discourse.Category.slugFor(category) + (this.get('noSubcategories') ? '/none' : '') + '/l';
@ -24,12 +24,12 @@ export default ObjectController.extend({
},
periods: function() {
var self = this,
periods = [];
Discourse.Site.currentProp('periods').forEach(function(p) {
const self = this,
periods = [];
this.site.get('periods').forEach(function(p) {
periods.pushObject(TopPeriod.create({ id: p,
showMoreUrl: self.showMoreUrl(p),
periods: periods }));
periods }));
});
return periods;
}.property('category', 'noSubcategories'),

View File

@ -7,7 +7,8 @@ export default DiscoveryController.extend({
showPostsColumn: Em.computed.empty('withLogo'),
actions: {
refresh: function() {
refresh() {
// Don't refresh if we're still loading
if (this.get('controllers.discovery.loading')) { return; }
@ -17,15 +18,11 @@ export default DiscoveryController.extend({
// Lesson learned: Don't call `loading` yourself.
this.set('controllers.discovery.loading', true);
var parentCategory = this.get('model.parentCategory');
var promise;
if (parentCategory) {
promise = Discourse.CategoryList.listForParent(parentCategory);
} else {
promise = Discourse.CategoryList.list();
}
const parentCategory = this.get('model.parentCategory');
const promise = parentCategory ? Discourse.CategoryList.listForParent(parentCategory) :
Discourse.CategoryList.list();
var self = this;
const self = this;
promise.then(function(list) {
self.set('model', list);
self.send('loadingComplete');

View File

@ -1,5 +1,6 @@
import DiscoveryController from 'discourse/controllers/discovery';
import { queryParams } from 'discourse/controllers/discovery-sortable';
import NotificationLevels from 'discourse/lib/notification-levels';
var controllerOpts = {
needs: ['discovery'],
@ -86,7 +87,7 @@ var controllerOpts = {
operation = { type: 'dismiss_posts' };
} else {
operation = { type: 'change_notification_level',
notification_level_id: Discourse.Topic.NotificationLevel.REGULAR };
notification_level_id: NotificationLevels.REGULAR };
}
var promise;

View File

@ -17,13 +17,13 @@ export default ObjectController.extend(ModalFunctionality, {
_.each(this.get("actions_summary"),function(a) {
var actionSummary;
a.flagTopic = self.get('model');
a.actionType = Discourse.Site.current().topicFlagTypeById(a.id);
a.actionType = self.site.topicFlagTypeById(a.id);
actionSummary = Discourse.ActionSummary.create(a);
lookup.set(a.actionType.get('name_key'), actionSummary);
});
this.set('topicActionByName', lookup);
return Discourse.Site.currentProp('topic_flag_types').filter(function(item) {
return this.site.get('topic_flag_types').filter(function(item) {
return _.any(self.get("actions_summary"), function(a) {
return (a.id === item.get('id') && a.can_act);
});

View File

@ -16,10 +16,6 @@ export default DiscourseController.extend(ModalFunctionality, {
this.set('loggedIn', false);
},
site: function() {
return Discourse.Site.current();
}.property(),
/**
Determines whether at least one login button is enabled
**/
@ -168,6 +164,12 @@ export default DiscourseController.extend(ModalFunctionality, {
this.set('authenticate', null);
return;
}
if (options.admin_not_allowed_from_ip_address) {
this.send('showLogin');
this.flash(I18n.t('login.admin_not_allowed_from_ip_address'), 'success');
this.set('authenticate', null);
return;
}
if (options.not_allowed_from_ip_address) {
this.send('showLogin');
this.flash(I18n.t('login.not_allowed_from_ip_address'), 'success');

View File

@ -5,7 +5,7 @@ var INVITED_TYPE= 8;
export default ObjectController.extend({
scope: function () {
return "notifications." + Discourse.Site.currentProp("notificationLookup")[this.get("notification_type")];
return "notifications." + this.site.get("notificationLookup")[this.get("notification_type")];
}.property("notification_type"),
username: Em.computed.alias("data.display_username"),

View File

@ -1,4 +1,4 @@
export default Ember.ArrayController.extend(Discourse.HasCurrentUser, {
export default Ember.ArrayController.extend({
needs: ['header'],
loadingNotifications: Em.computed.alias('controllers.header.loadingNotifications')
});

View File

@ -1 +1 @@
export default Ember.ObjectController.extend(Discourse.Presence, Discourse.HasCurrentUser);
export default Ember.ObjectController.extend(Discourse.Presence);

View File

@ -1,4 +1,4 @@
export default Ember.ObjectController.extend(Discourse.HasCurrentUser, {
export default Ember.ObjectController.extend({
needs: ['site-map'],
unreadTotal: function() {

View File

@ -1,4 +1,4 @@
export default Ember.ArrayController.extend(Discourse.HasCurrentUser, {
export default Ember.ArrayController.extend({
needs: ['application'],
showBadgesLink: function(){return Discourse.SiteSettings.enable_badges;}.property(),

View File

@ -435,10 +435,10 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedCon
post.toggleProperty('wiki');
},
togglePostType: function (post) {
togglePostType(post) {
// the request to the server is made in an observer in the post class
var regular = Discourse.Site.currentProp('post_types.regular'),
moderator = Discourse.Site.currentProp('post_types.moderator_action');
const regular = this.site.get('post_types.regular'),
moderator = this.site.get('post_types.moderator_action');
if (post.get("post_type") === moderator) {
post.set("post_type", regular);

View File

@ -71,7 +71,7 @@ export default ObjectController.extend({
this.setProperties({ user: null, userLoading: username, cardTarget: target });
var self = this;
Discourse.User.findByUsername(username).then(function (user) {
Discourse.User.findByUsername(username, {stats: false}).then(function (user) {
user = Discourse.User.create(user);
self.setProperties({ user: user, avatar: user, visible: true});
self.appEvents.trigger('usercard:shown');

View File

@ -1,8 +1,8 @@
export default Ember.ArrayController.extend(Discourse.HasCurrentUser, {
export default Ember.ArrayController.extend({
showAdminLinks: Em.computed.alias("currentUser.staff"),
actions: {
logout: function() {
logout() {
Discourse.logout();
return false;
}

View File

@ -1,7 +1,7 @@
import ObjectController from 'discourse/controllers/object';
// Lists of topics on a user's page.
export default ObjectController.extend(Discourse.HasCurrentUser, {
export default ObjectController.extend({
needs: ["application", "user"],
hideCategory: false,
showParticipants: false,

View File

@ -171,6 +171,11 @@ export default Ember.DefaultResolver.extend({
const compTemplate = Ember.TEMPLATES['admin/templates/' + decamelized];
if (compTemplate) { return compTemplate; }
}
if (decamelized === "javascripts/admin") {
return Ember.TEMPLATES['admin/templates/admin'];
}
if (decamelized.indexOf('admin') === 0 || decamelized.indexOf('javascripts/admin') === 0) {
decamelized = decamelized.replace(/^admin\_/, 'admin/templates/');
decamelized = decamelized.replace(/^admin\./, 'admin/templates/');

View File

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

View File

@ -42,9 +42,10 @@ export default {
app.inject('view', 'session', 'session:main');
app.inject('model', 'session', 'session:main');
// Inject currentUser. Components only for now to prevent any breakage
app.register('current-user:main', Discourse.User.current(), { instantiate: false });
app.inject('component', 'currentUser', 'current-user:main');
app.inject('route', 'currentUser', 'current-user:main');
app.inject('controller', 'currentUser', 'current-user:main');
app.register('store:main', Store);
app.inject('route', 'store', 'store:main');

View File

@ -1672,14 +1672,7 @@
// sure the URL and the optinal title are "nice".
function properlyEncoded(linkdef) {
return linkdef.replace(/^\s*(.*?)(?:\s+"(.+)")?\s*$/, function (wholematch, link, title) {
link = link.replace(/\?.*$/, function (querypart) {
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, '%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
});
link = link.replace(/ /g, '%20').replace(/'/g, '%27').replace(/\(/g, '%28').replace(/\)/g, '%29');
if (title) {
title = title.trim ? title.trim() : title.replace(/^\s*/, "").replace(/\s*$/, "");
title = title.replace(/"/g, "quot;").replace(/\(/g, "&#40;").replace(/\)/g, "&#41;").replace(/</g, "&lt;").replace(/>/g, "&gt;");

View File

@ -5,6 +5,7 @@
@namespace Discourse
@module Discourse
**/
Discourse.ClickTrack = {
/**
@ -43,15 +44,11 @@ Discourse.ClickTrack = {
if (!ownLink) {
var $badge = $('span.badge', $link);
if ($badge.length === 1) {
// don't update counts in category badge
if ($link.closest('.badge-category').length === 0) {
// nor in oneboxes (except when we force it)
if (($link.closest(".onebox-result").length === 0 && $link.closest('.onebox-body').length === 0) || $link.hasClass("track-link")) {
var html = $badge.html();
if (/^\d+$/.test(html)) {
$badge.html(parseInt(html, 10) + 1);
}
}
// don't update counts in category badge nor in oneboxes (except when we force it)
if ($link.hasClass("track-link") ||
$link.closest('.badge-category,.onebox-result,.onebox-body').length === 0) {
var html = $badge.html();
if (/^\d+$/.test(html)) { $badge.html(parseInt(html, 10) + 1); }
}
}
}

View File

@ -0,0 +1,6 @@
export default {
WATCHING: 3,
TRACKING: 2,
REGULAR: 1,
MUTED: 0
};

View File

@ -313,6 +313,7 @@ Discourse.Utilities = {
} else {
return new Ember.RSVP.Promise(function(resolve) {
var image = document.createElement("img");
image.crossOrigin = 'Anonymous';
// this event will be fired as soon as the image is loaded
image.onload = function(e) {
var img = e.target;

View File

@ -1,12 +0,0 @@
/**
This mixin provides a `currentUser` property that can be used to retrieve information
about the currently logged in user. It is mostly useful to controllers so it can be
exposted to templates.
**/
Discourse.HasCurrentUser = Em.Mixin.create({
currentUser: function() {
return Discourse.User.current();
}.property().volatile()
});

View File

@ -192,13 +192,6 @@ var _uncategorized;
Discourse.Category.reopenClass({
NotificationLevel: {
WATCHING: 3,
TRACKING: 2,
REGULAR: 1,
MUTED: 0
},
findUncategorized: function() {
_uncategorized = _uncategorized || Discourse.Category.list().findBy('id', Discourse.Site.currentProp('uncategorized_category_id'));
return _uncategorized;

View File

@ -1,9 +1,9 @@
Discourse.Site = Discourse.Model.extend({
const Site = Discourse.Model.extend({
isReadOnly: Em.computed.alias('is_readonly'),
notificationLookup: function() {
var result = [];
const result = [];
_.each(this.get('notification_types'), function(v,k) {
result[v] = k;
});
@ -11,23 +11,23 @@ Discourse.Site = Discourse.Model.extend({
}.property('notification_types'),
flagTypes: function() {
var postActionTypes = this.get('post_action_types');
const postActionTypes = this.get('post_action_types');
if (!postActionTypes) return [];
return postActionTypes.filterProperty('is_flag', true);
}.property('post_action_types.@each'),
categoriesByCount: Em.computed.sort('categories', function(a, b) {
categoriesByCount: Ember.computed.sort('categories', function(a, b) {
return (b.get('topic_count') || 0) - (a.get('topic_count') || 0);
}),
// Sort subcategories under parents
sortedCategories: function() {
var cats = this.get('categoriesByCount'),
const cats = this.get('categoriesByCount'),
result = [],
remaining = {};
cats.forEach(function(c) {
var parentCategoryId = parseInt(c.get('parent_category_id'), 10);
const parentCategoryId = parseInt(c.get('parent_category_id'), 10);
if (!parentCategoryId) {
result.pushObject(c);
} else {
@ -37,7 +37,7 @@ Discourse.Site = Discourse.Model.extend({
});
Ember.keys(remaining).forEach(function(parentCategoryId) {
var category = result.findBy('id', parseInt(parentCategoryId, 10)),
const category = result.findBy('id', parseInt(parentCategoryId, 10)),
index = result.indexOf(category);
if (index !== -1) {
@ -48,16 +48,16 @@ Discourse.Site = Discourse.Model.extend({
return result;
}.property(),
postActionTypeById: function(id) {
postActionTypeById(id) {
return this.get("postActionByIdLookup.action" + id);
},
topicFlagTypeById: function(id) {
topicFlagTypeById(id) {
return this.get("topicFlagByIdLookup.action" + id);
},
updateCategory: function(newCategory) {
var existingCategory = this.get('categories').findProperty('id', Em.get(newCategory, 'id'));
updateCategory(newCategory) {
const existingCategory = this.get('categories').findProperty('id', Em.get(newCategory, 'id'));
if (existingCategory) {
// Don't update null permissions
if (newCategory.permission === null) { delete newCategory.permission; }
@ -67,20 +67,15 @@ Discourse.Site = Discourse.Model.extend({
}
});
Discourse.Site.reopenClass(Discourse.Singleton, {
Site.reopenClass(Discourse.Singleton, {
/**
The current singleton will retrieve its attributes from the `PreloadStore`.
@method createCurrent
@returns {Discourse.Site} the site
**/
createCurrent: function() {
// The current singleton will retrieve its attributes from the `PreloadStore`.
createCurrent() {
return Discourse.Site.create(PreloadStore.get('site'));
},
create: function() {
var result = this._super.apply(this, arguments);
create() {
const result = this._super.apply(this, arguments);
if (result.categories) {
result.categoriesById = {};
@ -107,7 +102,7 @@ Discourse.Site.reopenClass(Discourse.Singleton, {
if (result.post_action_types) {
result.postActionByIdLookup = Em.Object.create();
result.post_action_types = _.map(result.post_action_types,function(p) {
var actionType = Discourse.PostActionType.create(p);
const actionType = Discourse.PostActionType.create(p);
result.postActionByIdLookup.set("action" + p.id, actionType);
return actionType;
});
@ -116,7 +111,7 @@ Discourse.Site.reopenClass(Discourse.Singleton, {
if (result.topic_flag_types) {
result.topicFlagByIdLookup = Em.Object.create();
result.topic_flag_types = _.map(result.topic_flag_types,function(p) {
var actionType = Discourse.PostActionType.create(p);
const actionType = Discourse.PostActionType.create(p);
result.topicFlagByIdLookup.set("action" + p.id, actionType);
return actionType;
});
@ -138,4 +133,4 @@ Discourse.Site.reopenClass(Discourse.Singleton, {
}
});
export default Site;

View File

@ -2,5 +2,11 @@ export default Ember.Object.extend({
findAll(type) {
const adapter = this.container.lookup('adapter:' + type) || this.container.lookup('adapter:rest');
return adapter.findAll(type);
},
find(type, id) {
const adapter = this.container.lookup('adapter:' + type) || this.container.lookup('adapter:rest');
return adapter.find(type, id);
}
});

View File

@ -1,16 +1,18 @@
function isNew(topic){
import NotificationLevels from 'discourse/lib/notification-levels';
function isNew(topic) {
return topic.last_read_post_number === null &&
((topic.notification_level !== 0 && !topic.notification_level) ||
topic.notification_level >= Discourse.Topic.NotificationLevel.TRACKING);
topic.notification_level >= NotificationLevels.TRACKING);
}
function isUnread(topic){
function isUnread(topic) {
return topic.last_read_post_number !== null &&
topic.last_read_post_number < topic.highest_post_number &&
topic.notification_level >= Discourse.Topic.NotificationLevel.TRACKING;
topic.notification_level >= NotificationLevels.TRACKING;
}
Discourse.TopicTrackingState = Discourse.Model.extend({
const TopicTrackingState = Discourse.Model.extend({
messageCount: 0,
_setup: function() {
@ -19,17 +21,17 @@ Discourse.TopicTrackingState = Discourse.Model.extend({
this.states = {};
}.on('init'),
establishChannels: function() {
var tracker = this;
establishChannels() {
const tracker = this;
var process = function(data){
const process = function(data){
if (data.message_type === "delete") {
tracker.removeTopic(data.topic_id);
tracker.incrementMessageCount();
}
if (data.message_type === "new_topic" || data.message_type === "latest") {
var ignored_categories = Discourse.User.currentProp("muted_category_ids");
const ignored_categories = Discourse.User.currentProp("muted_category_ids");
if(_.include(ignored_categories, data.payload.category_id)){
return;
}
@ -41,7 +43,7 @@ Discourse.TopicTrackingState = Discourse.Model.extend({
if (data.message_type === "new_topic" || data.message_type === "unread" || data.message_type === "read") {
tracker.notify(data);
var old = tracker.states["t" + data.topic_id];
const old = tracker.states["t" + data.topic_id];
if(!_.isEqual(old, data.payload)){
tracker.states["t" + data.topic_id] = data.payload;
@ -52,32 +54,32 @@ Discourse.TopicTrackingState = Discourse.Model.extend({
Discourse.MessageBus.subscribe("/new", process);
Discourse.MessageBus.subscribe("/latest", process);
var currentUser = Discourse.User.current();
const currentUser = Discourse.User.current();
if(currentUser) {
Discourse.MessageBus.subscribe("/unread/" + currentUser.id, process);
}
},
updateSeen: function(topicId, highestSeen) {
updateSeen(topicId, highestSeen) {
if(!topicId || !highestSeen) { return; }
var state = this.states["t" + topicId];
const state = this.states["t" + topicId];
if(state && (!state.last_read_post_number || state.last_read_post_number < highestSeen)) {
state.last_read_post_number = highestSeen;
this.incrementMessageCount();
}
},
notify: function(data){
notify(data){
if (!this.newIncoming) { return; }
var filter = this.get("filter");
const filter = this.get("filter");
if ((filter === "all" || filter === "latest" || filter === "new") && data.message_type === "new_topic" ) {
this.addIncoming(data.topic_id);
}
if ((filter === "all" || filter === "unread") && data.message_type === "unread") {
var old = this.states["t" + data.topic_id];
const old = this.states["t" + data.topic_id];
if(!old || old.highest_post_number === old.last_read_post_number) {
this.addIncoming(data.topic_id);
}
@ -90,47 +92,47 @@ Discourse.TopicTrackingState = Discourse.Model.extend({
this.set("incomingCount", this.newIncoming.length);
},
addIncoming: function(topicId) {
addIncoming(topicId) {
if(this.newIncoming.indexOf(topicId) === -1){
this.newIncoming.push(topicId);
}
},
resetTracking: function(){
resetTracking(){
this.newIncoming = [];
this.set("incomingCount", 0);
},
// track how many new topics came for this filter
trackIncoming: function(filter) {
trackIncoming(filter) {
this.newIncoming = [];
this.set("filter", filter);
this.set("incomingCount", 0);
},
hasIncoming: function(){
var count = this.get('incomingCount');
const count = this.get('incomingCount');
return count && count > 0;
}.property('incomingCount'),
removeTopic: function(topic_id) {
removeTopic(topic_id) {
delete this.states["t" + topic_id];
},
// If we have a cached topic list, we can update it from our tracking
// information.
updateTopics: function(topics) {
updateTopics(topics) {
if (Em.isEmpty(topics)) { return; }
var states = this.states;
const states = this.states;
topics.forEach(function(t) {
var state = states['t' + t.get('id')];
const state = states['t' + t.get('id')];
if (state) {
var lastRead = t.get('last_read_post_number');
const lastRead = t.get('last_read_post_number');
if (lastRead !== state.last_read_post_number) {
var postsCount = t.get('posts_count'),
newPosts = postsCount - state.highest_post_number,
const postsCount = t.get('posts_count');
let newPosts = postsCount - state.highest_post_number,
unread = postsCount - state.last_read_post_number;
if (newPosts < 0) { newPosts = 0; }
@ -151,16 +153,16 @@ Discourse.TopicTrackingState = Discourse.Model.extend({
});
},
sync: function(list, filter) {
var tracker = this,
sync(list, filter) {
const tracker = this,
states = tracker.states;
if (!list || !list.topics) { return; }
// compensate for delayed "new" topics
// client side we know they are not new, server side we think they are
for (var i=list.topics.length-1; i>=0; i--) {
var state = states["t"+ list.topics[i].id];
for (let i=list.topics.length-1; i>=0; i--) {
const state = states["t"+ list.topics[i].id];
if (state && state.last_read_post_number > 0) {
if (filter === "new") {
list.topics.splice(i, 1);
@ -172,7 +174,7 @@ Discourse.TopicTrackingState = Discourse.Model.extend({
}
list.topics.forEach(function(topic){
var row = tracker.states["t" + topic.id] || {};
const row = tracker.states["t" + topic.id] || {};
row.topic_id = topic.id;
row.notification_level = topic.notification_level;
@ -199,7 +201,7 @@ Discourse.TopicTrackingState = Discourse.Model.extend({
// Correct missing states, safeguard in case message bus is corrupt
if((filter === "new" || filter === "unread") && !list.more_topics_url){
var ids = {};
const ids = {};
list.topics.forEach(function(r){
ids["t" + r.id] = true;
});
@ -224,11 +226,11 @@ Discourse.TopicTrackingState = Discourse.Model.extend({
this.incrementMessageCount();
},
incrementMessageCount: function() {
incrementMessageCount() {
this.set("messageCount", this.get("messageCount") + 1);
},
countNew: function(category_id){
countNew(category_id){
return _.chain(this.states)
.where(isNew)
.where(function(topic){ return topic.category_id === category_id || !category_id;})
@ -236,8 +238,8 @@ Discourse.TopicTrackingState = Discourse.Model.extend({
.length;
},
resetNew: function() {
var self = this;
resetNew() {
const self = this;
Object.keys(this.states).forEach(function (id) {
if (self.states[id].last_read_post_number === null) {
delete self.states[id];
@ -245,7 +247,7 @@ Discourse.TopicTrackingState = Discourse.Model.extend({
});
},
countUnread: function(category_id){
countUnread(category_id){
return _.chain(this.states)
.where(isUnread)
.where(function(topic){ return topic.category_id === category_id || !category_id;})
@ -253,19 +255,19 @@ Discourse.TopicTrackingState = Discourse.Model.extend({
.length;
},
countCategory: function(category_id) {
var count = 0;
countCategory(category_id) {
let sum = 0;
_.each(this.states, function(topic){
if (topic.category_id === category_id) {
count += (topic.last_read_post_number === null ||
sum += (topic.last_read_post_number === null ||
topic.last_read_post_number < topic.highest_post_number) ? 1 : 0;
}
});
return count;
return sum;
},
lookupCount: function(name, category){
var categoryName = category ? Em.get(category, "name") : null;
lookupCount(name, category){
let categoryName = category ? Em.get(category, "name") : null;
if(name === "new") {
return this.countNew(categoryName);
} else if(name === "unread") {
@ -277,9 +279,9 @@ Discourse.TopicTrackingState = Discourse.Model.extend({
}
}
},
loadStates: function (data) {
loadStates(data) {
// not exposed
var states = this.states;
const states = this.states;
if(data) {
_.each(data,function(topic){
@ -290,19 +292,21 @@ Discourse.TopicTrackingState = Discourse.Model.extend({
});
Discourse.TopicTrackingState.reopenClass({
createFromStates: function(data){
var instance = Discourse.TopicTrackingState.create();
TopicTrackingState.reopenClass({
createFromStates(data){
const instance = Discourse.TopicTrackingState.create();
instance.loadStates(data);
instance.establishChannels();
return instance;
},
current: function(){
current(){
if (!this.tracker) {
var data = PreloadStore.get('topicTrackingStates');
const data = PreloadStore.get('topicTrackingStates');
this.tracker = this.createFromStates(data);
PreloadStore.remove('topicTrackingStates');
}
return this.tracker;
}
});
export default TopicTrackingState;

View File

@ -26,6 +26,10 @@ const Topic = Discourse.Model.extend({
return PostStream.create({topic: this});
}.property(),
replyCount: function() {
return this.get('posts_count') - 1;
}.property('posts_count'),
details: function() {
return TopicDetails.create({topic: this});
}.property(),
@ -411,7 +415,6 @@ Topic.reopenClass({
});
return Discourse.ajax(topic.get('url'), { type: 'PUT', data: props }).then(function(result) {
// The title can be cleaned up server side
props.title = result.basic_topic.title;
props.fancy_title = result.basic_topic.fancy_title;

View File

@ -292,11 +292,11 @@ Discourse.User = Discourse.Model.extend({
return this.get('stats').rejectProperty('isPM');
}.property('stats.@each.isPM'),
findDetails: function() {
findDetails: function(options) {
var user = this;
return PreloadStore.getAndRemove("user_" + user.get('username'), function() {
return Discourse.ajax("/users/" + user.get('username') + '.json');
return Discourse.ajax("/users/" + user.get('username') + '.json', {data: options});
}).then(function (json) {
if (!Em.isEmpty(json.user.stats)) {
@ -468,9 +468,9 @@ Discourse.User.reopenClass(Discourse.Singleton, {
@method findByUsername
@returns {Promise} a promise that resolves to a `Discourse.User`
**/
findByUsername: function(username) {
findByUsername: function(username, options) {
var user = Discourse.User.create({username: username});
return user.findDetails();
return user.findDetails(options);
},
/**

View File

@ -7,6 +7,11 @@
@module Discourse
**/
Discourse.UserBadge = Discourse.Model.extend({
postUrl: function() {
if(this.get('topic_title')) {
return "/t/-/" + this.get('topic_id') + "/" + this.get('post_number');
}
}.property(), // avoid the extra bindings for now
/**
Revoke this badge.
@ -93,7 +98,7 @@ Discourse.UserBadge.reopenClass({
@returns {Promise} a promise that resolves to an array of `Discourse.UserBadge`.
**/
findByUsername: function(username, options) {
var url = "/users/" + username + "/badges_json.json";
var url = "/user-badges/" + username + ".json";
if (options && options.grouped) {
url += "?grouped=true";
}
@ -128,12 +133,13 @@ Discourse.UserBadge.reopenClass({
@param {String} username username of the user to be granted the badge.
@returns {Promise} a promise that resolves to an instance of `Discourse.UserBadge`.
**/
grant: function(badgeId, username) {
grant: function(badgeId, username, reason) {
return Discourse.ajax("/user_badges", {
type: "POST",
data: {
username: username,
badge_id: badgeId
badge_id: badgeId,
reason: reason
}
}).then(function(json) {
return Discourse.UserBadge.createFromJson(json);

View File

@ -28,8 +28,7 @@ export default function(filter, params) {
category: model,
filterMode: filterMode,
noSubcategories: params && params.no_subcategories,
canEditCategory: model.get('can_edit'),
canChangeCategoryNotificationLevel: Discourse.User.current()
canEditCategory: model.get('can_edit')
});
},

View File

@ -2,7 +2,7 @@ import { queryParams } from 'discourse/controllers/discovery-sortable';
// A helper to build a topic route for a filter
function filterQueryParams(params, defaultParams) {
var findOpts = defaultParams || {};
const findOpts = defaultParams || {};
if (params) {
Ember.keys(queryParams).forEach(function(opt) {
if (params[opt]) { findOpts[opt] = params[opt]; }
@ -16,47 +16,47 @@ export default function(filter, extras) {
return Discourse.Route.extend({
queryParams: queryParams,
beforeModel: function() {
beforeModel() {
this.controllerFor('navigation/default').set('filterMode', filter);
},
model: function(data, transition) {
model(data, transition) {
// attempt to stop early cause we need this to be called before .sync
Discourse.ScreenTrack.current().stop();
var findOpts = filterQueryParams(transition.queryParams),
extras = { cached: this.isPoppedState(transition) };
const findOpts = filterQueryParams(transition.queryParams),
extras = { cached: this.isPoppedState(transition) };
return Discourse.TopicList.list(filter, findOpts, extras);
},
titleToken: function() {
titleToken() {
if (filter === Discourse.Utilities.defaultHomepage()) { return; }
var filterText = I18n.t('filters.' + filter.replace('/', '.') + '.title', {count: 0});
const filterText = I18n.t('filters.' + filter.replace('/', '.') + '.title', {count: 0});
return I18n.t('filters.with_topics', {filter: filterText});
},
setupController: function(controller, model, trans) {
setupController(controller, model, trans) {
if (trans) {
controller.setProperties(Em.getProperties(trans, _.keys(queryParams).map(function(v){
return 'queryParams.' + v;
})));
}
var periods = this.controllerFor('discovery').get('periods'),
periodId = model.get('for_period') || (filter.indexOf('/') > 0 ? filter.split('/')[1] : '');
const periods = this.controllerFor('discovery').get('periods'),
periodId = model.get('for_period') || (filter.indexOf('/') > 0 ? filter.split('/')[1] : '');
var topicOpts = {
model: model,
const topicOpts = {
model,
category: null,
period: periods.findBy('id', periodId),
selected: [],
expandGloballyPinned: true
};
var params = model.get('params');
const params = model.get('params');
if (params && Object.keys(params).length) {
topicOpts.order = params.order;
topicOpts.ascending = params.ascending;
@ -67,7 +67,7 @@ export default function(filter, extras) {
this.controllerFor('navigation/default').set('canCreateTopic', model.get('can_create_topic'));
},
renderTemplate: function() {
renderTemplate() {
this.render('navigation/default', { outlet: 'navigation-bar' });
this.render('discovery/topics', { controller: 'discovery/topics', outlet: 'list-container' });
}

View File

@ -1,22 +1,22 @@
import ShowFooter from "discourse/mixins/show-footer";
Discourse.DiscoveryCategoriesRoute = Discourse.Route.extend(Discourse.OpenComposer, ShowFooter, {
renderTemplate: function() {
renderTemplate() {
this.render('navigation/categories', { outlet: 'navigation-bar' });
this.render('discovery/categories', { outlet: 'list-container' });
},
beforeModel: function() {
beforeModel() {
this.controllerFor('navigation/categories').set('filterMode', 'categories');
},
model: function() {
model() {
// TODO: Remove this and ensure server side does not supply `topic_list`
// if default page is categories
PreloadStore.remove("topic_list");
return Discourse.CategoryList.list('categories').then(function(list) {
var tracking = Discourse.TopicTrackingState.current();
const tracking = Discourse.TopicTrackingState.current();
if (tracking) {
tracking.sync(list, 'categories');
tracking.trackIncoming('categories');
@ -25,11 +25,12 @@ Discourse.DiscoveryCategoriesRoute = Discourse.Route.extend(Discourse.OpenCompos
});
},
titleToken: function() {
titleToken() {
if (Discourse.Utilities.defaultHomepage() === "categories") { return; }
return I18n.t('filters.categories.title');
},
setupController: function(controller, model) {
setupController(controller, model) {
controller.set('model', model);
// Only show either the Create Category or Create Topic button
@ -40,22 +41,19 @@ Discourse.DiscoveryCategoriesRoute = Discourse.Route.extend(Discourse.OpenCompos
},
actions: {
createCategory: function() {
var groups = Discourse.Site.current().groups;
var everyone_group = groups.findBy('id', 0);
var group_names = groups.map(function(group) {
return group.name;
});
createCategory() {
const groups = this.site.groups,
everyoneName = groups.findBy('id', 0).name;
Discourse.Route.showModal(this, 'editCategory', Discourse.Category.create({
color: 'AB9364', text_color: 'FFFFFF', group_permissions: [{group_name: everyone_group.name, permission_type: 1}],
available_groups: group_names,
color: 'AB9364', text_color: 'FFFFFF', group_permissions: [{group_name: everyoneName, permission_type: 1}],
available_groups: groups.map(g => g.name),
allow_badges: true
}));
this.controllerFor('editCategory').set('selectedTab', 'general');
},
createTopic: function() {
createTopic() {
this.openComposer(this.controllerFor('discovery/categories'));
}
}

View File

@ -1,82 +1,97 @@
<div class='container body-page'>
<section class='about'>
<h2>{{i18n 'about.title' title=title}}</h2>
<p>{{description}}</p>
</section>
<div class='container'>
<div class='contents clearfix body-page'>
{{#if admins}}
<section class='about admins'>
<h3>{{i18n 'about.our_admins'}}</h3>
{{#each a in admins}}
{{user-small user=a}}
{{/each}}
<div class='clearfix'></div>
<ul class="nav-pills">
<li class="nav-item-about">{{#link-to 'about' class="active"}}{{i18n 'about.simple_title'}}{{/link-to}}</li>
{{#if faqOverriden}}
<li class="nav-item-guidelines">{{#link-to 'guidelines'}}{{i18n 'guidelines'}}{{/link-to}}</li>
<li class="nav-item-faq">{{#link-to 'faq'}}{{i18n 'faq'}}{{/link-to}}</li>
{{else}}
<li class="nav-item-faq">{{#link-to 'faq'}}{{i18n 'faq'}}{{/link-to}}</li>
{{/if}}
<li class="nav-item-tos">{{#link-to 'tos'}}{{i18n 'terms_of_service'}}{{/link-to}}</li>
<li class="nav-item-privacy">{{#link-to 'privacy'}}{{i18n 'privacy'}}{{/link-to}}</li>
</ul>
<section class='about'>
<h2>{{i18n 'about.title' title=title}}</h2>
<p>{{description}}</p>
</section>
{{/if}}
{{#if moderators}}
<section class='about moderators'>
<h3>{{i18n 'about.our_moderators'}}</h3>
{{#if admins}}
<section class='about admins'>
<h3>{{i18n 'about.our_admins'}}</h3>
<div class='users'>
{{#each m in moderators}}
{{user-small user=m}}
{{#each a in admins}}
{{user-small user=a}}
{{/each}}
</div>
<div class='clearfix'></div>
<div class='clearfix'></div>
</section>
{{/if}}
{{#if moderators}}
<section class='about moderators'>
<h3>{{i18n 'about.our_moderators'}}</h3>
<div class='users'>
{{#each m in moderators}}
{{user-small user=m}}
{{/each}}
</div>
<div class='clearfix'></div>
</section>
{{/if}}
<section class='about stats'>
<h3>{{i18n 'about.stats'}}</h3>
<table class='table'>
<tr>
<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>&mdash;</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>
{{/if}}
<section class='about stats'>
<h3>{{i18n 'about.stats'}}</h3>
<table class='table'>
<tr>
<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>&mdash;</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>
{{#if contactInfo}}
<section class='about contact'>
<h3>{{i18n 'about.contact'}}</h3>
<p>{{contactInfo}}</p>
</section>
{{/if}}
{{#if contactInfo}}
<section class='about contact'>
<h3>{{i18n 'about.contact'}}</h3>
<p>{{{contactInfo}}}</p>
</section>
{{/if}}
</div>
</div>

View File

@ -17,6 +17,12 @@
</tbody>
</table>
{{#if showLongDescription}}
<div class='long-description banner'>
{{{long_description}}}
</div>
{{/if}}
{{#if userBadges}}
<div class={{unbound layoutClass}}>
{{#each ub in userBadges}}

View File

@ -1,17 +0,0 @@
<div>
<button class='btn standard dropdown-toggle' data-toggle='dropdown'>
<i {{bind-attr class=view.icon}}></i>
<span class='caret'></span>
</button>
<ul class='notification-dropdown-menu'>
{{#each level in view.dropDownContent}}
<li data-id="{{level.id}}">
<a {{action "setNotification" level.id}} href="#">
<span class="title"><i {{bind-attr class=level.styleClasses}}></i>&nbsp;{{level.title}}</span><span>{{level.description}}</span>
</a>
</li>
{{/each}}
</ul>
</div>

View File

@ -0,0 +1,13 @@
{{topic-status topic=topic}}
<a class='title' href="{{unbound topic.lastUnreadUrl}}">{{{unbound topic.fancy_title}}}</a>
{{topic-post-badges newPosts=topic.totalUnread unseen=topic.unseen url=topic.lastUnreadUrl}}
{{#if latestTopicOnly}}
<div class='last-user-info'>
{{i18n 'categories.latest_by'}} <a href="{{{unbound topic.lastPosterUrl}}}">{{unbound topic.last_poster.username}}</a>
<a href="{{unbound topic.lastPostUrl}}">{{format-age topic.last_posted_at}}</a>
</div>
{{else}}
&nbsp;
<a href class="last-posted-at">{{format-age topic.last_posted_at}}</a>
{{/if}}

View File

@ -1,6 +1,6 @@
<label>{{{field.name}}}</label>
<div class='controls'>
{{input value=value}}
{{input value=value maxlength=site.user_field_max_length}}
{{#if field.required}}<span class='required'>*</span>{{/if}}
<p>{{{field.description}}}</p>
</div>

View File

@ -1,4 +1,9 @@
{{#link-to 'user' user.username}}
{{avatar user imageSize="tiny"}}
{{user.username}}
{{/link-to}}
<div class="user-image">
{{#link-to 'user' user.username}}{{avatar user imageSize="large"}}{{/link-to}}
</div>
<div class="user-detail">
<span class="username">{{#link-to 'user' user.username}}{{user.username}}{{/link-to}}</span>
<span class="name">{{user.name}}</span>
<span class="title">{{user.title}}</span>
</div>

View File

@ -1,4 +1,4 @@
<a href='#' {{action "closeMessage" this}} class='close'><i class='fa fa-times-circle'></i></a>
<a href='#' {{action "closeMessage" this}} class='close'>{{fa-icon "times-circle"}}</a>
<h3>{{i18n 'composer.similar_topics'}}</h3>
<ul class='topics'>

View File

@ -16,10 +16,10 @@
<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>
<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>
<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>
@ -45,21 +45,7 @@
</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>
{{featured-topic topic=f latestTopicOnly=latestTopicOnly action="showTopicEntrance"}}
{{/each}}
</td>
<td class='stats' {{bind-attr title="c.topicStatsTitle"}}>

View File

@ -1,3 +1,3 @@
<{{view.tagName}} class='num posts-map posts {{view.likesHeat}}' title='{{view.title}}'>
<a href class='posts-map badge-posts {{view.likesHeat}}'>{{topic.posts_count}}</a>
<a href class='posts-map badge-posts {{view.likesHeat}}'>{{topic.replyCount}}</a>
</{{view.tagName}}>

View File

@ -15,7 +15,7 @@
{{#if t.hasExcerpt}}
<div class="topic-excerpt">
{{t.excerpt}}
{{{t.excerpt}}}
{{#if t.excerptTruncated}}
{{#unless t.canClearPin}}<a href="{{unbound t.url}}">{{i18n 'read_more'}}</a>{{/unless}}
{{/if}}

View File

@ -10,8 +10,8 @@
{{custom-html "extraNavItem"}}
</ul>
{{#if canChangeCategoryNotificationLevel}}
{{view 'category-notifications-button' category=category}}
{{#if currentUser}}
{{category-notifications-button category=category}}
{{/if}}
{{#if canCreateTopic}}

View File

@ -10,7 +10,7 @@
{{#if showPosters}}
{{raw "topic-list-header-column" order='posters' name='users'}}
{{/if}}
{{raw "topic-list-header-column" sortable=sortable number='true' order='posts' name='posts'}}
{{raw "topic-list-header-column" sortable=sortable number='true' order='posts' name='replies'}}
{{#if showParticipants}}
{{raw "topic-list-header-column" order='participants' name='users'}}
{{/if}}

View File

@ -145,6 +145,10 @@
</div>
</div>
{{#each uf in userFields}}
{{user-field field=uf.field value=uf.value}}
{{/each}}
<div class="control-group pref-location">
<label class="control-label">{{i18n 'user.location'}}</label>
<div class="controls">
@ -212,10 +216,6 @@
{{plugin-outlet "user_custom_preferences"}}
</div>
{{#each uf in userFields}}
{{user-field field=uf.field value=uf.value}}
{{/each}}
<div class="control-group category">
<label class="control-label">{{i18n 'user.categories_settings'}}</label>
<div class="controls category-controls">

View File

@ -3,7 +3,8 @@
import userSearch from 'discourse/lib/user-search';
import afterTransition from 'discourse/lib/after-transition';
var ComposerView = Discourse.View.extend(Ember.Evented, {
const ComposerView = Discourse.View.extend(Ember.Evented, {
_lastKeyTimeout: null,
templateName: 'composer',
elementId: 'reply-control',
classNameBindings: ['model.creatingPrivateMessage:private-message',
@ -48,12 +49,12 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
Ember.run.scheduleOnce('afterRender', this, 'refreshPreview');
}.observes('model.reply', 'model.hidePreview'),
focusIn: function() {
var controller = this.get('controller');
focusIn() {
const controller = this.get('controller');
if (controller) controller.updateDraftStatus();
},
movePanels: function(sizePx) {
movePanels(sizePx) {
$('#main-outlet').css('padding-bottom', sizePx);
$('.composer-popup').css('bottom', sizePx);
// signal the progress bar it should move!
@ -61,14 +62,14 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
},
resize: function() {
var self = this;
const self = this;
Em.run.scheduleOnce('afterRender', function() {
var h = $('#reply-control').height() || 0;
const h = $('#reply-control').height() || 0;
self.movePanels.apply(self, [h + "px"]);
// Figure out the size of the fields
var $fields = self.$('.composer-fields'),
pos = $fields.position();
const $fields = self.$('.composer-fields');
let pos = $fields.position();
if (pos) {
self.$('.wmd-controls').css('top', $fields.height() + pos.top + 5);
@ -83,17 +84,19 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
});
}.observes('model.composeState', 'model.action'),
keyUp: function() {
var controller = this.get('controller');
keyUp() {
const controller = this.get('controller');
controller.checkReplyLength();
var lastKeyUp = new Date();
const lastKeyUp = new Date();
this.set('lastKeyUp', lastKeyUp);
// One second from now, check to see if the last key was hit when
// we recorded it. If it was, the user paused typing.
var self = this;
Em.run.later(function() {
const self = this;
Ember.run.cancel(this._lastKeyTimeout);
this._lastKeyTimeout = Ember.run.later(function() {
if (lastKeyUp !== self.get('lastKeyUp')) return;
// Search for similar topics if the user pauses typing
@ -101,7 +104,7 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
}, 1000);
},
keyDown: function(e) {
keyDown(e) {
if (e.which === 27) {
// ESC
this.get('controller').send('hitEsc');
@ -114,12 +117,12 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
},
_enableResizing: function() {
var $replyControl = $('#reply-control'),
const $replyControl = $('#reply-control'),
self = this;
$replyControl.DivResizer({
resize: this.resize.bind(self),
onDrag: function (sizePx) { self.movePanels.apply(self, [sizePx]); }
onDrag(sizePx) { self.movePanels.apply(self, [sizePx]); }
});
afterTransition($replyControl, this.resize.bind(self));
this.ensureMaximumDimensionForImagesInPreview();
@ -130,14 +133,14 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
this.set('controller.view', null);
}.on('willDestroyElement'),
ensureMaximumDimensionForImagesInPreview: function() {
ensureMaximumDimensionForImagesInPreview() {
// This enforce maximum dimensions of images in the preview according
// to the current site settings.
// For interactivity, we immediately insert the locally cooked version
// of the post into the stream when the user hits reply. We therefore also
// need to enforce these rules on the .cooked version.
// Meanwhile, the server is busy post-processing the post and generating thumbnails.
var style = Discourse.Mobile.mobileView ?
const style = Discourse.Mobile.mobileView ?
'max-width: 100%; height: auto;' :
'max-width:' + Discourse.SiteSettings.max_image_width + 'px;' +
'max-height:' + Discourse.SiteSettings.max_image_height + 'px;';
@ -145,17 +148,17 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
$('<style>#wmd-preview img:not(.thumbnail), .cooked img:not(.thumbnail) {' + style + '}</style>').appendTo('head');
},
click: function() {
click() {
this.get('controller').send('openIfDraft');
},
// Called after the preview renders. Debounced for performance
afterRender: function() {
var $wmdPreview = $('#wmd-preview');
afterRender() {
const $wmdPreview = $('#wmd-preview');
if ($wmdPreview.length === 0) return;
var post = this.get('model.post'),
refresh = false;
const post = this.get('model.post');
let refresh = false;
// If we are editing a post, we'll refresh its contents once. This is a feature that
// allows a user to refresh its contents once.
@ -175,17 +178,17 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
this.trigger('previewRefreshed', $wmdPreview);
},
_applyEmojiAutocomplete: function() {
_applyEmojiAutocomplete() {
if (!this.siteSettings.enable_emoji) { return; }
var template = this.container.lookup('template:emoji-selector-autocomplete.raw');
const template = this.container.lookup('template:emoji-selector-autocomplete.raw');
$('#wmd-input').autocomplete({
template: template,
key: ":",
transformComplete: function(v){ return v.code + ":"; },
dataSource: function(term){
transformComplete(v) { return v.code + ":"; },
dataSource(term){
return new Ember.RSVP.Promise(function(resolve) {
var full = ":" + term;
const full = ":" + term;
term = term.toLowerCase();
if (term === "") {
@ -196,7 +199,7 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
return resolve([Discourse.Emoji.translations[full]]);
}
var options = Discourse.Emoji.search(term, {maxResults: 5});
const options = Discourse.Emoji.search(term, {maxResults: 5});
return resolve(options);
}).then(function(list) {
@ -208,10 +211,11 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
});
},
initEditor: function() {
initEditor() {
// not quite right, need a callback to pass in, meaning this gets called once,
// but if you start replying to another topic it will get the avatars wrong
var $wmdInput, editor, self = this;
let $wmdInput, editor;
const self = this;
this.wmdInput = $wmdInput = $('#wmd-input');
if ($wmdInput.length === 0 || $wmdInput.data('init') === true) return;
@ -219,11 +223,11 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
ComposerView.trigger("initWmdEditor");
this._applyEmojiAutocomplete();
var template = this.container.lookup('template:user-selector-autocomplete.raw');
const template = this.container.lookup('template:user-selector-autocomplete.raw');
$wmdInput.data('init', true);
$wmdInput.autocomplete({
template: template,
dataSource: function(term) {
dataSource(term) {
return userSearch({
term: term,
topicId: self.get('controller.controllers.topic.model.id'),
@ -231,7 +235,7 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
});
},
key: "@",
transformComplete: function(v) {
transformComplete(v) {
if (v.username) {
return v.username;
} else {
@ -241,10 +245,10 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
});
this.editor = editor = Discourse.Markdown.createEditor({
lookupAvatarByPostNumber: function(postNumber) {
var posts = self.get('controller.controllers.topic.postStream.posts');
lookupAvatarByPostNumber(postNumber) {
const posts = self.get('controller.controllers.topic.postStream.posts');
if (posts) {
var quotedPost = posts.findProperty("post_number", postNumber);
const quotedPost = posts.findProperty("post_number", postNumber);
if (quotedPost) {
return Discourse.Utilities.tinyAvatar(quotedPost.get("avatar_template"));
}
@ -273,7 +277,7 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
this.set('editor', this.editor);
this.loadingChanged();
var saveDraft = Discourse.debounce((function() {
const saveDraft = Discourse.debounce((function() {
return self.get('controller').saveDraft();
}), 2000);
@ -282,7 +286,7 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
return true;
});
var $replyTitle = $('#reply-title');
const $replyTitle = $('#reply-title');
$replyTitle.keyup(function() {
saveDraft();
@ -305,9 +309,9 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
// in case it's still bound somehow
this._unbindUploadTarget();
var $uploadTarget = $('#reply-control'),
csrf = Discourse.Session.currentProp('csrfToken'),
cancelledByTheUser;
const $uploadTarget = $('#reply-control'),
csrf = Discourse.Session.currentProp('csrfToken');
let cancelledByTheUser;
// NOTE: we need both the .json extension and the CSRF token as a query parameter for IE9
$uploadTarget.fileupload({
@ -318,7 +322,7 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
// submit - this event is triggered for each upload
$uploadTarget.on('fileuploadsubmit', function (e, data) {
var result = Discourse.Utilities.validateUploadedFiles(data.files);
const result = Discourse.Utilities.validateUploadedFiles(data.files);
// reset upload status when everything is ok
if (result) self.setProperties({ uploadProgress: 0, isUploading: true });
return result;
@ -331,7 +335,7 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
self.get('controller').send('closeModal');
// NOTE: IE9 doesn't support XHR
if (data["xhr"]) {
var jqHXR = data.xhr();
const jqHXR = data.xhr();
if (jqHXR) {
// need to wait for the link to show up in the DOM
Em.run.schedule('afterRender', function() {
@ -351,7 +355,7 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
// progress all
$uploadTarget.on('fileuploadprogressall', function (e, data) {
var progress = parseInt(data.loaded / data.total * 100, 10);
const progress = parseInt(data.loaded / data.total * 100, 10);
self.set('uploadProgress', progress);
});
@ -360,7 +364,7 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
if (!cancelledByTheUser) {
// make sure we have a url
if (data.result.url) {
var markdown = Discourse.Utilities.getUploadMarkdown(data.result);
const markdown = Discourse.Utilities.getUploadMarkdown(data.result);
// appends a space at the end of the inserted markdown
self.addMarkdown(markdown + " ");
self.set('isUploading', false);
@ -385,7 +389,7 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
// Firefox. This is pretty dangerous because it can potentially break
// Ctrl+v to paste so we should be conservative about what browsers this runs
// in.
var uaMatch = navigator.userAgent.match(/Firefox\/(\d+)\.\d/);
const uaMatch = navigator.userAgent.match(/Firefox\/(\d+)\.\d/);
if (uaMatch && parseInt(uaMatch[1]) >= 24) {
self.$().append( Ember.$("<div id='contenteditable' contenteditable='true' style='height: 0; width: 0; overflow: hidden'></div>") );
self.$("textarea").off('keydown.contenteditable');
@ -395,18 +399,18 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
// after we switch focus, probably because it is being executed too late.
if ((event.ctrlKey || event.metaKey) && (event.keyCode === 86)) {
// Save the current textarea selection.
var textarea = self.$("textarea")[0],
const textarea = self.$("textarea")[0],
selectionStart = textarea.selectionStart,
selectionEnd = textarea.selectionEnd;
// Focus the contenteditable div.
var contentEditableDiv = self.$('#contenteditable');
const contentEditableDiv = self.$('#contenteditable');
contentEditableDiv.focus();
// The paste doesn't finish immediately and we don't have any onpaste
// event, so wait for 100ms which _should_ be enough time.
setTimeout(function() {
var pastedImg = contentEditableDiv.find('img');
const pastedImg = contentEditableDiv.find('img');
if ( pastedImg.length === 1 ) {
pastedImg.remove();
@ -414,11 +418,11 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
// For restoring the selection.
textarea.focus();
var textareaContent = $(textarea).val(),
const textareaContent = $(textarea).val(),
startContent = textareaContent.substring(0, selectionStart),
endContent = textareaContent.substring(selectionEnd);
var restoreSelection = function(pastedText) {
const restoreSelection = function(pastedText) {
$(textarea).val( startContent + pastedText + endContent );
textarea.selectionStart = selectionStart + pastedText.length;
textarea.selectionEnd = textarea.selectionStart;
@ -435,20 +439,20 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
// to a Blob and upload that, but if it is a regular URL that
// operation is prevented for security purposes. When we get a regular
// URL let's just create an <img> tag for the image.
var imageSrc = pastedImg.attr('src');
const imageSrc = pastedImg.attr('src');
if (imageSrc.match(/^data:image/)) {
// Restore the cursor position, and remove any selected text.
restoreSelection("");
// Create a Blob to upload.
var image = new Image();
const image = new Image();
image.onload = function() {
// Create a new canvas.
var canvas = document.createElementNS('http://www.w3.org/1999/xhtml', 'canvas');
const canvas = document.createElementNS('http://www.w3.org/1999/xhtml', 'canvas');
canvas.height = image.height;
canvas.width = image.width;
var ctx = canvas.getContext('2d');
const ctx = canvas.getContext('2d');
ctx.drawImage(image, 0, 0);
canvas.toBlob(function(blob) {
@ -488,8 +492,8 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
}, 400);
},
addMarkdown: function(text) {
var ctrl = $('#wmd-input').get(0),
addMarkdown(text) {
const ctrl = $('#wmd-input').get(0),
caretPosition = Discourse.Utilities.caretPosition(ctrl),
current = this.get('model.reply');
this.set('model.reply', current.substring(0, caretPosition) + text + current.substring(caretPosition, current.length));
@ -500,10 +504,10 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
},
// Uses javascript to get the image sizes from the preview, if present
imageSizes: function() {
var result = {};
imageSizes() {
const result = {};
$('#wmd-preview img').each(function(i, e) {
var $img = $(e),
const $img = $(e),
src = $img.prop('src');
if (src && src.length) {
@ -513,12 +517,12 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
return result;
},
childDidInsertElement: function() {
childDidInsertElement() {
return this.initEditor();
},
childWillDestroyElement: function() {
var self = this;
childWillDestroyElement() {
const self = this;
this._unbindUploadTarget();
@ -532,9 +536,9 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
},
titleValidation: function() {
var titleLength = this.get('model.titleLength'),
missingChars = this.get('model.missingTitleCharacters'),
reason;
const titleLength = this.get('model.titleLength'),
missingChars = this.get('model.missingTitleCharacters');
let reason;
if( titleLength < 1 ){
reason = I18n.t('composer.error.title_missing');
} else if( missingChars > 0 ) {
@ -555,9 +559,9 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
}.property('model.categoryId'),
replyValidation: function() {
var replyLength = this.get('model.replyLength'),
missingChars = this.get('model.missingReplyCharacters'),
reason;
const replyLength = this.get('model.replyLength'),
missingChars = this.get('model.missingReplyCharacters');
let reason;
if( replyLength < 1 ){
reason = I18n.t('composer.error.post_missing');
} else if( missingChars > 0 ) {
@ -569,8 +573,8 @@ var ComposerView = Discourse.View.extend(Ember.Evented, {
}
}.property('model.reply', 'model.replyLength', 'model.missingReplyCharacters', 'model.minimumPostLength'),
_unbindUploadTarget: function() {
var $uploadTarget = $('#reply-control');
_unbindUploadTarget() {
const $uploadTarget = $('#reply-control');
try { $uploadTarget.fileupload('destroy'); }
catch (e) { /* wasn't initialized yet */ }
$uploadTarget.off();

View File

@ -1,26 +1,11 @@
export default Ember.ContainerView.extend(Discourse.Presence, {
/**
Attaches a view and wires up the container properly
@method attachViewWithArgs
@param {Object} viewArgs The arguments to pass when creating the view
@param {Class} viewClass The view class we want to create
**/
attachViewWithArgs: function(viewArgs, viewClass) {
attachViewWithArgs(viewArgs, viewClass) {
if (!viewClass) { viewClass = Ember.View.extend(); }
var view = this.createChildView(viewClass, viewArgs);
this.pushObject(view);
this.pushObject(this.createChildView(viewClass, viewArgs));
},
/**
Attaches a view with no arguments and wires up the container properly
@method attachViewClass
@param {Class} viewClass The view class we want to add
**/
attachViewClass: function(viewClass) {
attachViewClass(viewClass) {
this.attachViewWithArgs(null, viewClass);
}
});

View File

@ -11,7 +11,7 @@ export default Ember.Object.extend({
title: function() {
return I18n.messageFormat('posts_likes_MF', {
count: this.get('topic.posts_count'),
count: this.get('topic.replyCount'),
ratio: this.get('ratioText')
}).trim();
}.property(),

View File

@ -48,12 +48,12 @@ var PostView = Discourse.GroupedView.extend(Ember.Evented, {
Em.run.scheduleOnce('afterRender', this, '_cookedWasChanged');
}.observes('post.cooked'),
_cookedWasChanged: function() {
_cookedWasChanged() {
this.trigger('postViewUpdated', this.$());
this._insertQuoteControls();
},
mouseUp: function(e) {
mouseUp(e) {
if (this.get('controller.multiSelect') && (e.metaKey || e.ctrlKey)) {
this.get('controller').toggledSelectedPost(this.get('post'));
}
@ -74,7 +74,7 @@ var PostView = Discourse.GroupedView.extend(Ember.Evented, {
repliesShown: Em.computed.gt('post.replies.length', 0),
_updateQuoteElements: function($aside, desc) {
_updateQuoteElements($aside, desc) {
var navLink = "",
quoteTitle = I18n.t("post.follow_quote"),
postNumber = $aside.data('post');
@ -108,7 +108,7 @@ var PostView = Discourse.GroupedView.extend(Ember.Evented, {
$('.quote-controls', $aside).html(expandContract + navLink);
},
_toggleQuote: function($aside) {
_toggleQuote($aside) {
if (this.get('expanding')) { return; }
this.set('expanding', true);
@ -151,23 +151,29 @@ var PostView = Discourse.GroupedView.extend(Ember.Evented, {
},
// Show how many times links have been clicked on
_showLinkCounts: function() {
var self = this,
link_counts = this.get('post.link_counts');
_showLinkCounts() {
const self = this,
link_counts = this.get('post.link_counts');
if (!link_counts) return;
if (!link_counts) { return; }
link_counts.forEach(function(lc) {
if (!lc.clicks || lc.clicks < 1) return;
if (!lc.clicks || lc.clicks < 1) { return; }
self.$(".cooked a[href]").each(function() {
var link = $(this);
if ((!lc.internal || lc.url[0] === "/") && link.attr('href') === lc.url) {
// don't display badge counts on category badge
if (link.closest('.badge-category').length === 0 && ((link.closest(".onebox-result").length === 0 && link.closest('.onebox-body').length === 0) || link.hasClass("track-link"))) {
link.append("<span class='badge badge-notification clicks' title='" +
I18n.t("topic_map.clicks", {count: lc.clicks}) +
"'>" + Discourse.Formatter.number(lc.clicks) + "</span>");
const $link = $(this),
href = $link.attr('href');
let valid = !lc.internal && href === lc.url;
// this might be an attachment
if (lc.internal) { valid = href.indexOf(lc.url) >= 0; }
if (valid) {
// don't display badge counts on category badge & oneboxes (unless when explicitely stated)
if ($link.hasClass("track-link") ||
$link.closest('.badge-category,.onebox-result,.onebox-body').length === 0) {
$link.append("<span class='badge badge-notification clicks' title='" + I18n.t("topic_map.clicks", {count: lc.clicks}) + "'>" + Discourse.Formatter.number(lc.clicks) + "</span>");
}
}
});
@ -176,7 +182,7 @@ var PostView = Discourse.GroupedView.extend(Ember.Evented, {
actions: {
// Toggle the replies this post is a reply to
toggleReplyHistory: function(post) {
toggleReplyHistory(post) {
var replyHistory = post.get('replyHistory'),
topicController = this.get('controller'),
@ -227,7 +233,7 @@ var PostView = Discourse.GroupedView.extend(Ember.Evented, {
},
// Add the quote controls to a post
_insertQuoteControls: function() {
_insertQuoteControls() {
var self = this,
$quotes = this.$('aside.quote');

View File

@ -5,23 +5,24 @@ import BookmarkButton from 'discourse/views/bookmark-button';
import ShareButton from 'discourse/views/share-button';
import InviteReplyButton from 'discourse/views/invite-reply-button';
import ReplyButton from 'discourse/views/reply-button';
import PinnedButton from 'discourse/views/pinned-button';
import TopicNotificationsButton from 'discourse/views/topic-notifications-button';
import PinnedButton from 'discourse/components/pinned-button';
import TopicNotificationsButton from 'discourse/components/topic-notifications-button';
import DiscourseContainerView from 'discourse/views/container';
export default DiscourseContainerView.extend({
elementId: 'topic-footer-buttons',
topicBinding: 'controller.content',
init: function() {
init() {
this._super();
this.createButtons();
},
// Add the buttons below a topic
createButtons: function() {
var topic = this.get('topic');
createButtons() {
const topic = this.get('topic');
if (Discourse.User.current()) {
const viewArgs = {topic};
if (Discourse.User.currentProp("staff")) {
this.attachViewClass(TopicAdminMenuButton);
}
@ -39,8 +40,8 @@ export default DiscourseContainerView.extend({
if (this.get('topic.details.can_create_post')) {
this.attachViewClass(ReplyButton);
}
this.attachViewClass(PinnedButton);
this.attachViewClass(TopicNotificationsButton);
this.attachViewWithArgs(viewArgs, PinnedButton);
this.attachViewWithArgs(viewArgs, TopicNotificationsButton);
this.trigger('additionalButtons', this);
} else {

View File

@ -1,20 +0,0 @@
import NotificationsButton from 'discourse/views/notifications-button';
export default NotificationsButton.extend({
longDescriptionBinding: 'topic.details.notificationReasonText',
topic: Em.computed.alias('controller.model'),
target: Em.computed.alias('topic'),
hidden: Em.computed.alias('topic.deleted'),
notificationLevels: Discourse.Topic.NotificationLevel,
notificationLevel: Em.computed.alias('topic.details.notification_level'),
isPrivateMessage: Em.computed.alias('topic.isPrivateMessage'),
i18nPrefix: 'topic.notifications',
i18nPostfix: function() {
return this.get('isPrivateMessage') ? '_pm' : '';
}.property('isPrivateMessage'),
clicked: function(id) {
this.get('topic.details').updateNotifications(id);
}
});

View File

@ -51,8 +51,8 @@ var TopicView = Discourse.View.extend(AddCategoryClass, Discourse.Scrolling, {
var $target = $(e.target);
if ($target.hasClass('mention') || $target.parents('.expanded-embed').length) { return false; }
return Discourse.ClickTrack.trackClick(e);
return Discourse.ClickTrack.trackClick(e);
});
}.on('didInsertElement'),
@ -126,7 +126,7 @@ var TopicView = Discourse.View.extend(AddCategoryClass, Discourse.Scrolling, {
var opts = { latestLink: "<a href=\"" + Discourse.getURL("/latest") + "\">" + I18n.t("topic.view_latest_topics") + "</a>" },
category = this.get('controller.content.category');
if(Em.get(category, 'id') === Discourse.Site.currentProp("uncategorized_category_id")) {
if(category && Em.get(category, 'id') === Discourse.Site.currentProp("uncategorized_category_id")) {
category = null;
}

View File

@ -6,7 +6,7 @@ var clickOutsideEventName = "mousedown.outside-user-card",
export default Discourse.View.extend(CleansUp, {
elementId: 'user-card',
classNameBindings: ['controller.visible::hidden', 'controller.showBadges', 'controller.hasCardBadgeImage'],
classNameBindings: ['controller.visible:show', 'controller.showBadges', 'controller.hasCardBadgeImage'],
allowBackgrounds: Discourse.computed.setting('allow_profile_backgrounds'),
addBackground: function() {

View File

@ -522,7 +522,7 @@ I18n.enable_verbose_localization = function(){
if (!_.isEmpty(value)) {
message += ", parameters: " + JSON.stringify(value);
}
//window.console.log(message);
Em.Logger.info(message);
}
return t.apply(I18n, [scope, value]) + " (t" + current + ")";
};

View File

@ -10,6 +10,7 @@
//
// Stuff we need to load first
//= require ./discourse/lib/notification-levels
//= require ./discourse/lib/app-events
//= require ./discourse/helpers/i18n
//= require ./discourse/helpers/fa-icon
@ -42,9 +43,9 @@
//= require ./discourse/views/flag
//= require ./discourse/views/combo-box
//= require ./discourse/views/button
//= require ./discourse/views/dropdown-button
//= require ./discourse/views/notifications-button
//= require ./discourse/views/topic-notifications-button
//= require ./discourse/components/dropdown-button
//= require ./discourse/components/notifications-button
//= require ./discourse/components/topic-notifications-button
//= require ./discourse/views/pagedown-preview
//= require ./discourse/views/composer
//= require ./discourse/routes/discourse_route

View File

@ -1440,3 +1440,9 @@ tr.not-activated {
.preview {
margin-top: 5px;
}
table#user-badges {
.reason {
max-width: 200px;
}
}

View File

@ -159,7 +159,7 @@
.badge-category {
padding: 4px 10px;
display: inline-block;
line-height: 24px;
margin-bottom: 10px;
}
.category-dropdown-menu .badge-category {

View File

@ -6,13 +6,43 @@ section.about {
}
.user-small {
padding: 5px;
width: 200px;
padding: 8px;
width: 205px;
height: 60px;
float: left;
img {
.user-image {
float: left;
padding-right: 4px;
}
.user-detail {
float: left;
width: 70%;
span {
float: left;
width: 90%;
padding-left: 5px;
}
.username a {
font-weight: bold;
font-size: 16px;
color: scale-color($primary, $lightness: 30%);
}
.name {
font-size: 13px;
color: scale-color($primary, $lightness: 30%);
}
.title {
font-size: 13px;
color: scale-color($primary, $lightness: 50%);
}
}
}
p {

View File

@ -651,15 +651,15 @@ button {
/* start state */
.mfp-content {
opacity: 0;
transition: all 0.2s ease-in-out;
-webkit-transform: scale(0.8);
-ms-transform: scale(0.8);
transform: scale(0.8);
transition: all .2s;
-webkit-transform: scale(.8);
-ms-transform: scale(.8);
transform: scale(.8);
}
&.mfp-bg {
opacity: 0;
transition: all 0.3s ease-out;
transition: all .3s ease-out;
}
/* animate in */
@ -679,9 +679,9 @@ button {
&.mfp-removing {
.mfp-content {
-webkit-transform: scale(0.8);
-ms-transform: scale(0.8);
transform: scale(0.8);
-webkit-transform: scale(.8);
-ms-transform: scale(.8);
transform: scale(.8);
opacity: 0;
}
&.mfp-bg {

View File

@ -179,7 +179,11 @@ kbd
margin: 0 .1em;
padding: .1em .6em;
* * { display: none; }
// don't allow more than 3 nested elements to prevent FF from crashing
// cf. http://what.thedailywtf.com/t/nested-elements/7927
// 3 levels are needed to prevent highlighted words being hidden
// cf. https://meta.discourse.org/t/word-disappears-when-searched-and-in-details-summary-kbd-b/25741
* * * { display: none; }
}
// we assume blockquotes have their own margins, so all blockquotes

View File

@ -159,3 +159,8 @@ table.badges-listing {
text-align: left;
}
}
.long-description.banner {
width: 88%;
margin-bottom: 20px;
}

View File

@ -132,12 +132,21 @@ header .title-wrapper .bar .badge-category {
.category-breadcrumb li.bar > .badge-category {
background: dark-light-diff($primary, $secondary, 95%, -65%) !important;
line-height: 24px;
&:not(.home):first-child {
border-left-width: 5px;
border-left-style: solid;
}
}
.category-breadcrumb .box > a.badge-category {
margin-bottom: 0;
height: 24px;
// TODO clean this up
padding-top: 6px !important;
padding-bottom: 0 !important;
}
.category-dropdown-menu .cat .badge-wrapper.box {
width: 110%;
}

View File

@ -2,7 +2,7 @@
// Banner
// --------------------------------------------------
#banner {
#banner, .banner {
padding: 10px;
border-radius: 5px;
background: scale-color($tertiary, $lightness: 90%);

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