Version bump

This commit is contained in:
Neil Lalonde 2015-07-01 17:12:23 -04:00
commit a4ae8570ea
214 changed files with 6847 additions and 2003 deletions

View File

@ -25,8 +25,6 @@ else
gem 'seed-fu', '~> 2.3.3'
end
gem 'actionpack-action_caching'
# Rails 4.1.6+ will relax the mail gem version requirement to `~> 2.5, >= 2.5.4`.
# However, mail gem 2.6.x currently does not work with discourse because of the
# reference to `Mail::RFC2822Parser` in `lib/email.rb`. This ensure discourse

View File

@ -15,8 +15,6 @@ GEM
activesupport (= 4.1.10)
rack (~> 1.5.2)
rack-test (~> 0.6.2)
actionpack-action_caching (1.1.1)
actionpack (>= 4.0.0, < 5.0)
actionview (4.1.10)
activesupport (= 4.1.10)
builder (~> 3.1)
@ -208,7 +206,7 @@ GEM
omniauth-twitter (1.0.1)
multi_json (~> 1.3)
omniauth-oauth (~> 1.0)
onebox (1.5.20)
onebox (1.5.21)
moneta (~> 0.7)
multi_json (~> 1.7)
mustache (~> 0.99)
@ -392,7 +390,6 @@ PLATFORMS
ruby
DEPENDENCIES
actionpack-action_caching
active_model_serializers (~> 0.8.3)
annotate
aws-sdk

View File

@ -1,3 +1,5 @@
import showModal from 'discourse/lib/show-modal';
/**
This controller supports interface for creating custom CSS skins in Discourse.
@ -21,6 +23,10 @@ export default Ember.ArrayController.extend({
this.set('selectedItem', item);
},
importModal: function() {
showModal('upload-customization');
},
/**
Select a given style

View File

@ -4,24 +4,26 @@ export default Ember.ObjectController.extend({
viewingBarChart: Em.computed.equal('viewMode', 'barChart'),
startDate: null,
endDate: null,
categoryId: null,
refreshing: false,
actions: {
refreshReport: function() {
var self = this;
this.set('refreshing', true);
Discourse.Report.find(this.get('type'), this.get('startDate'), this.get('endDate')).then(function(r) {
self.set('model', r);
}).finally(function() {
self.set('refreshing', false);
});
refreshReport() {
this.set("refreshing", true);
Discourse.Report.find(
this.get("type"),
this.get("startDate"),
this.get("endDate"),
this.get("categoryId")
).then(m => this.set("model", m)
).finally(() => this.set("refreshing", false));
},
viewAsTable: function() {
viewAsTable() {
this.set('viewMode', 'table');
},
viewAsBarChart: function() {
viewAsBarChart() {
this.set('viewMode', 'barChart');
}
}

View File

@ -1,9 +1,11 @@
Discourse.Report = Discourse.Model.extend({
import round from "discourse/lib/round";
const Report = Discourse.Model.extend({
reportUrl: function() {
return("/admin/reports/" + this.get('type'));
}.property('type'),
valueAt: function(numDaysAgo) {
valueAt(numDaysAgo) {
if (this.data) {
var wantedDate = moment().subtract(numDaysAgo, 'days').format('YYYY-MM-DD');
var item = this.data.find( function(d) { return d.x === wantedDate; } );
@ -14,7 +16,7 @@ Discourse.Report = Discourse.Model.extend({
return 0;
},
sumDays: function(startDaysAgo, endDaysAgo) {
sumDays(startDaysAgo, endDaysAgo) {
if (this.data) {
var earliestDate = moment().subtract(endDaysAgo, 'days').startOf('day');
var latestDate = moment().subtract(startDaysAgo, 'days').startOf('day');
@ -25,7 +27,7 @@ Discourse.Report = Discourse.Model.extend({
sum += datum.y;
}
});
return sum;
return round(sum, -2);
}
},
@ -100,7 +102,7 @@ Discourse.Report = Discourse.Model.extend({
}
}.property('type'),
percentChangeString: function(val1, val2) {
percentChangeString(val1, val2) {
var val = ((val1 - val2) / val2) * 100;
if( isNaN(val) || !isFinite(val) ) {
return null;
@ -111,7 +113,7 @@ Discourse.Report = Discourse.Model.extend({
}
},
changeTitle: function(val1, val2, prevPeriodString) {
changeTitle(val1, val2, prevPeriodString) {
var title = '';
var percentChange = this.percentChangeString(val1, val2);
if( percentChange ) {
@ -139,26 +141,29 @@ Discourse.Report = Discourse.Model.extend({
});
Discourse.Report.reopenClass({
find: function(type, startDate, endDate) {
Report.reopenClass({
return Discourse.ajax("/admin/reports/" + type, {data: {
start_date: startDate,
end_date: endDate
}}).then(function (json) {
find(type, startDate, endDate, categoryId) {
return Discourse.ajax("/admin/reports/" + type, {
data: {
start_date: startDate,
end_date: endDate,
category_id: categoryId
}
}).then(json => {
// Add a percent field to each tuple
var maxY = 0;
json.report.data.forEach(function (row) {
let maxY = 0;
json.report.data.forEach(row => {
if (row.y > maxY) maxY = row.y;
});
if (maxY > 0) {
json.report.data.forEach(function (row) {
row.percentage = Math.round((row.y / maxY) * 100);
});
json.report.data.forEach(row => row.percentage = Math.round((row.y / maxY) * 100));
}
var model = Discourse.Report.create({type: type});
const model = Discourse.Report.create({ type: type });
model.setProperties(json.report);
return model;
});
}
});
export default Report;

View File

@ -78,13 +78,18 @@ Discourse.SiteCustomization = Discourse.Model.extend({
siteCustomization.set('savingStatus', I18n.t('saved'));
siteCustomization.set('saving',false);
siteCustomization.startTrackingChanges();
return siteCustomization;
});
},
destroy: function() {
if (!this.id) return;
return Discourse.ajax("/admin/site_customizations/" + this.id, { type: 'DELETE' });
}
},
download_url: function() {
return Discourse.getURL('/admin/site_customizations/' + this.id);
}.property('id')
});
var SiteCustomizations = Ember.ArrayProxy.extend({

View File

@ -12,8 +12,9 @@ export default Discourse.Route.extend({
if (versionChecks) {
c.set('versionCheck', Discourse.VersionCheck.create(d.version_check));
}
_.each(d.reports,function(report){
c.set(report.type, Discourse.Report.create(report));
['global_reports', 'page_view_reports', 'private_message_reports', 'http_reports', 'user_reports'].forEach(name => {
c.set(name, d[name].map(r => Discourse.Report.create(r)));
});
var topReferrers = d.top_referrers;

View File

@ -8,12 +8,14 @@
<button {{action "newCustomization"}} class='btn'>
{{fa-icon "plus"}}{{i18n 'admin.customize.new'}}
</button>
{{d-button action="importModal" icon="upload" label="admin.customize.import"}}
</div>
{{#if selectedItem}}
<div {{bind-attr class=":current-style view.maximized:maximized"}}>
<div class='wrapper'>
{{text-field class="style-name" value=selectedItem.name}}
<a class="btn export" download target="_blank" href={{selectedItem.download_url}}>{{fa-icon "download"}} {{i18n 'admin.export_json.button_text'}}</a>
<div class='admin-controls'>
<ul class="nav nav-pills">

View File

@ -17,7 +17,9 @@
</thead>
<tbody>
{{#unless loading}}
{{admin-report-trust-level-counts report=users_by_trust_level}}
{{#each r in user_reports}}
{{admin-report-trust-level-counts report=r}}
{{/each}}
{{/unless}}
</tbody>
</table>
@ -26,15 +28,15 @@
<div class="dashboard-stats totals">
<table>
<tr>
<td class="title"><i class='fa fa-shield'></i> {{i18n 'admin.dashboard.admins'}}</td>
<td class="title">{{fa-icon "shield"}} {{i18n 'admin.dashboard.admins'}}</td>
<td class="value">{{#link-to 'adminUsersList.show' 'admins'}}{{admins}}{{/link-to}}</td>
<td class="title"><i class='fa fa-ban'></i> {{i18n 'admin.dashboard.suspended'}}</td>
<td class="title">{{fa-icon "ban"}} {{i18n 'admin.dashboard.suspended'}}</td>
<td class="value">{{#link-to 'adminUsersList.show' 'suspended'}}{{suspended}}{{/link-to}}</td>
</tr>
<tr>
<td class="title"><i class='fa fa-shield'></i> {{i18n 'admin.dashboard.moderators'}}</td>
<td class="title">{{fa-icon "shield"}} {{i18n 'admin.dashboard.moderators'}}</td>
<td class="value">{{#link-to 'adminUsersList.show' 'moderators'}}{{moderators}}{{/link-to}}</td>
<td class="title"><i class='fa fa-ban'></i> {{i18n 'admin.dashboard.blocked'}}</td>
<td class="title">{{fa-icon "ban"}} {{i18n 'admin.dashboard.blocked'}}</td>
<td class="value">{{#link-to 'adminUsersList.show' 'blocked'}}{{blocked}}{{/link-to}}</td>
</tr>
</table>
@ -54,14 +56,9 @@
</thead>
<tbody>
{{#unless loading}}
{{admin-report-counts report=visits}}
{{admin-report-counts report=signups}}
{{admin-report-counts report=topics}}
{{admin-report-counts report=posts}}
{{admin-report-counts report=likes}}
{{admin-report-counts report=flags}}
{{admin-report-counts report=bookmarks}}
{{admin-report-counts report=emails}}
{{#each r in global_reports}}
{{admin-report-counts report=r}}
{{/each}}
{{/unless}}
</tbody>
</table>
@ -81,21 +78,19 @@
</thead>
<tbody>
{{#unless loading}}
{{admin-report-counts report=page_view_anon_reqs}}
{{admin-report-counts report=page_view_logged_in_reqs}}
{{admin-report-counts report=page_view_crawler_reqs}}
{{admin-report-counts report=page_view_total_reqs}}
{{#each r in page_view_reports}}
{{admin-report-counts report=r}}
{{/each}}
{{/unless}}
</tbody>
</table>
</div>
<div class="dashboard-stats">
<table class="table table-condensed table-hover">
<thead>
<tr>
<th class="title" title="{{i18n 'admin.dashboard.private_messages_title'}}"><i class="fa fa-envelope"></i> {{i18n 'admin.dashboard.private_messages_short'}}</th>
<th class="title" title="{{i18n 'admin.dashboard.private_messages_title'}}">{{fa-icon "enveloppe"}} {{i18n 'admin.dashboard.private_messages_short'}}</th>
<th>{{i18n 'admin.dashboard.reports.today'}}</th>
<th>{{i18n 'admin.dashboard.reports.yesterday'}}</th>
<th>{{i18n 'admin.dashboard.reports.last_7_days'}}</th>
@ -105,11 +100,9 @@
</thead>
<tbody>
{{#unless loading}}
{{admin-report-counts report=user_to_user_private_messages}}
{{admin-report-counts report=system_private_messages}}
{{admin-report-counts report=notify_moderators_private_messages}}
{{admin-report-counts report=notify_user_private_messages}}
{{admin-report-counts report=moderator_warning_private_messages}}
{{#each r in private_message_reports}}
{{admin-report-counts report=r}}
{{/each}}
{{/unless}}
</tbody>
</table>
@ -154,12 +147,9 @@
</thead>
<tbody>
{{#unless loading}}
{{admin-report-counts report=http_2xx_reqs}}
{{admin-report-counts report=http_3xx_reqs}}
{{admin-report-counts report=http_4xx_reqs}}
{{admin-report-counts report=http_5xx_reqs}}
{{admin-report-counts report=http_background_reqs}}
{{admin-report-counts report=http_total_reqs}}
{{#each r in http_reports}}
{{admin-report-counts report=r}}
{{/each}}
{{/unless}}
</tbody>
</table>
@ -176,7 +166,7 @@
{{#if foundProblems}}
<div class="dashboard-stats detected-problems">
<div class="look-here"><i class="fa fa-exclamation-triangle"></i></div>
<div class="look-here">{{fa-icon "exclamation-triangle"}}</div>
<div class="problem-messages">
<p {{bind-attr class="loadingProblems:invisible"}}>
{{i18n 'admin.dashboard.problems_found'}}

View File

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

View File

@ -1,3 +1,3 @@
{{value-list value=buffered.value addKey="admin.site_settings.add_url"}}
{{value-list values=buffered.value addKey="admin.site_settings.add_url"}}
{{setting-validation-message message=validationMessage}}
<div class='desc'>{{{unbound setting.description}}}</div>

View File

@ -2,8 +2,8 @@ import RestAdapter from 'discourse/adapters/rest';
export default function buildPluginAdapter(pluginName) {
return RestAdapter.extend({
pathFor(store, type) {
return "/admin/plugins/" + pluginName + this._super(store, type);
pathFor(store, type, findArgs) {
return "/admin/plugins/" + pluginName + this._super(store, type, findArgs);
}
});
}

View File

@ -1,6 +1,6 @@
// A breadcrumb including category drop downs
export default Ember.Component.extend({
classNames: ['category-breadcrumb'],
classNameBindings: ['hidden:hidden',':category-breadcrumb'],
tagName: 'ol',
parentCategory: Em.computed.alias('category.parentCategory'),
@ -12,6 +12,10 @@ export default Ember.Component.extend({
return !c.get('parentCategory');
}),
hidden: function(){
return Discourse.Mobile.mobileView && !this.get('category');
}.property('category'),
firstCategory: function() {
return this.get('parentCategory') || this.get('category');
}.property('parentCategory', 'category'),
@ -29,6 +33,11 @@ export default Ember.Component.extend({
return this.get('categories').filter(function (c) {
return c.get('parentCategory') === firstCategory;
});
}.property('firstCategory', 'hideSubcategories')
}.property('firstCategory', 'hideSubcategories'),
render: function(buffer) {
if (this.get('hidden')) { return; }
this._super(buffer);
}
});

View File

@ -14,8 +14,12 @@ export default Ember.Component.extend({
return result;
},
realNameProperty: function() {
return this.get('nameProperty') || 'name';
}.property('nameProperty'),
render(buffer) {
const nameProperty = this.get('nameProperty') || 'name',
const nameProperty = this.get('realNameProperty'),
none = this.get('none');
// Add none option if required
@ -64,16 +68,22 @@ export default Ember.Component.extend({
o.selected = !!$(o).attr('selected');
});
// observer for item names changing (optional)
if (this.get('nameChanges')) {
this.addObserver('content.@each.' + this.get('realNameProperty'), this.rerender);
}
$elem.select2({formatResult: this.comboTemplate, minimumResultsForSearch: 5, width: 'resolve'});
const castInteger = this.get('castInteger');
$elem.on("change", function (e) {
let val = $(e.target).val();
if (val.length && castInteger) {
if (val && val.length && castInteger) {
val = parseInt(val, 10);
}
self.set('value', val);
});
$elem.trigger('change');
}.on('didInsertElement'),
_destroyDropdown: function() {

View File

@ -0,0 +1,103 @@
export default Em.Component.extend({
fileInput: null,
loading: false,
expectedRootObjectName: null,
hover: 0,
classNames: ['json-uploader'],
_initialize: function() {
const $this = this.$();
const self = this;
const $fileInput = $this.find('#js-file-input');
this.set('fileInput', $fileInput[0]);
$fileInput.on('change', function() {
self.fileSelected(this.files);
});
$this.on('dragover', function(e) {
if (e.preventDefault) e.preventDefault();
return false;
});
$this.on('dragenter', function(e) {
if (e.preventDefault) e.preventDefault();
self.set('hover', self.get('hover') + 1);
return false;
});
$this.on('dragleave', function(e) {
if (e.preventDefault) e.preventDefault();
self.set('hover', self.get('hover') - 1);
return false;
});
$this.on('drop', function(e) {
if (e.preventDefault) e.preventDefault();
self.set('hover', 0);
self.fileSelected(e.dataTransfer.files);
return false;
});
}.on('didInsertElement'),
accept: function() {
return ".json,application/json,application/x-javascript,text/json" + (this.get('extension') ? "," + this.get('extension') : "");
}.property('extension'),
setReady: function() {
let parsed;
try {
parsed = JSON.parse(this.get('value'));
} catch (e) {
this.set('ready', false);
return;
}
const rootObject = parsed[this.get('expectedRootObjectName')];
if (rootObject !== null && rootObject !== undefined) {
this.set('ready', true);
} else {
this.set('ready', false);
}
}.observes('destination', 'expectedRootObjectName'),
actions: {
selectFile: function() {
const $fileInput = $(this.get('fileInput'));
$fileInput.click();
}
},
fileSelected(fileList) {
const self = this;
let files = [];
for (let i = 0; i < fileList.length; i++) {
files[i] = fileList[i];
}
const fileNameRegex = /\.(json|txt)$/;
files = files.filter(function(file) {
if (fileNameRegex.test(file.name)) {
return true;
}
if (file.type === "text/plain") {
return true;
}
return false;
});
const firstFile = fileList[0];
this.set('loading', true);
let reader = new FileReader();
reader.onload = function(evt) {
self.set('value', evt.target.result);
self.set('loading', false);
};
reader.readAsText(firstFile);
}
});

View File

@ -0,0 +1,56 @@
export default Ember.Component.extend({
tagName: 'ul',
classNameBindings: [':nav', ':nav-pills'],
id: 'navigation-bar',
selectedNavItem: function(){
const filterMode = this.get('filterMode'),
navItems = this.get('navItems');
var item = navItems.find(function(item){
return item.get('filterMode').indexOf(filterMode) === 0;
});
return item || navItems[0];
}.property('filterMode'),
closedNav: function(){
if (!this.get('expanded')) {
this.ensureDropClosed();
}
}.observes('expanded'),
ensureDropClosed: function(){
if (!this.get('expanded')) {
this.set('expanded',false);
}
$(window).off('click.navigation-bar');
Discourse.URL.appEvents.off('dom:clean', this, this.ensureDropClosed);
},
actions: {
toggleDrop: function(){
this.set('expanded', !this.get('expanded'));
var self = this;
if (this.get('expanded')) {
Discourse.URL.appEvents.on('dom:clean', this, this.ensureDropClosed);
Em.run.next(function() {
if (!self.get('expanded')) { return; }
self.$('.drop a').on('click', function(){
self.$('.drop').hide();
self.set('expanded', false);
return true;
});
$(window).on('click.navigation-bar', function() {
self.set('expanded', false);
return true;
});
});
}
}
}
});

View File

@ -24,25 +24,13 @@ export default Ember.Component.extend(StringBuffer, {
this.get('filterMode').indexOf(this.get('content.filterMode')) === 0;
}.property('content.filterMode', 'filterMode'),
name: function() {
var categoryName = this.get('content.categoryName'),
name = this.get('content.name'),
extra = { count: this.get('content.count') || 0 };
if (categoryName) {
name = 'category';
extra.categoryName = Discourse.Formatter.toTitleCase(categoryName);
}
return I18n.t("filters." + name.replace("/", ".") + ".title", extra);
}.property('content.{categoryName,name,count}'),
renderString(buffer) {
const content = this.get('content');
buffer.push("<a href='" + content.get('href') + "'>");
if (content.get('hasIcon')) {
buffer.push("<span class='" + content.get('name') + "'></span>");
}
buffer.push(this.get('name'));
buffer.push(this.get('content.displayName'));
buffer.push("</a>");
}
});

View File

@ -1,5 +1,5 @@
export default Ember.Component.extend({
classNameBindings: [':gap', 'gap::hidden'],
classNameBindings: [':gap', ':jagged-border', 'gap::hidden'],
initGaps: function(){
this.set('loading', false);

View File

@ -0,0 +1,2 @@
import SearchResult from 'discourse/components/search-result';
export default SearchResult.extend();

View File

@ -0,0 +1,2 @@
import SearchResult from 'discourse/components/search-result';
export default SearchResult.extend();

View File

@ -0,0 +1,2 @@
import SearchResult from 'discourse/components/search-result';
export default SearchResult.extend();

View File

@ -0,0 +1,2 @@
import SearchResult from 'discourse/components/search-result';
export default SearchResult.extend();

View File

@ -0,0 +1,11 @@
export default Ember.Component.extend({
tagName: 'ul',
_highlightOnInsert: function() {
const term = this.get('controller.term');
if(!_.isEmpty(term)) {
this.$('.blurb').highlight(term.split(/\s+/), {className: 'search-highlight'});
this.$('.topic-title').highlight(term.split(/\s+/), {className: 'search-highlight'} );
}
}.on('didInsertElement')
});

View File

@ -0,0 +1,7 @@
import TextField from 'discourse/components/text-field';
export default TextField.extend({
placeholder: function() {
return this.get('searchContextEnabled') ? "" : I18n.t('search.title');
}.property('searchContextEnabled')
});

View File

@ -332,7 +332,7 @@ export default Ember.ObjectController.extend(Presence, {
this.set('similarTopicsMessage', message);
}
this.store.find('topic', {similar: {title, raw: body}}).then(function(newTopics) {
this.store.find('similar-topic', {title, raw: body}).then(function(newTopics) {
similarTopics.clear();
similarTopics.pushObjects(newTopics.get('content'));

View File

@ -21,8 +21,7 @@ export default DiscourseController.extend(ModalFunctionality, {
maxUsernameLength: Discourse.computed.setting('max_username_length'),
minUsernameLength: Discourse.computed.setting('min_username_length'),
resetForm: function() {
resetForm() {
// We wrap the fields in a structure so we can assign a value
this.setProperties({
accountName: '',
@ -49,11 +48,11 @@ export default DiscourseController.extend(ModalFunctionality, {
if (this.get('passwordValidation.failed')) return true;
// Validate required fields
var userFields = this.get('userFields');
let userFields = this.get('userFields');
if (userFields) { userFields = userFields.filterProperty('field.required'); }
if (!Ember.isEmpty(userFields)) {
var anyEmpty = userFields.any(function(uf) {
var val = uf.get('value');
const anyEmpty = userFields.any(function(uf) {
const val = uf.get('value');
return !val || Ember.isEmpty(val);
});
if (anyEmpty) { return true; }
@ -61,6 +60,9 @@ export default DiscourseController.extend(ModalFunctionality, {
return false;
}.property('passwordRequired', 'nameValidation.failed', 'emailValidation.failed', 'usernameValidation.failed', 'passwordValidation.failed', 'formSubmitted', 'userFields.@each.value'),
usernameRequired: Ember.computed.not('authOptions.omit_username'),
passwordRequired: function() {
return this.blank('authOptions.auth_provider');
}.property('authOptions.auth_provider'),
@ -89,7 +91,7 @@ export default DiscourseController.extend(ModalFunctionality, {
// Check the email address
emailValidation: function() {
// If blank, fail without a reason
var email;
let email;
if (this.blank('accountEmail')) {
return Discourse.InputValidation.create({
failed: true
@ -149,7 +151,7 @@ export default DiscourseController.extend(ModalFunctionality, {
}.observes('emailValidation', 'accountEmail'),
fetchExistingUsername: Discourse.debounce(function() {
var self = this;
const self = this;
Discourse.User.checkUsername(null, this.get('accountEmail')).then(function(result) {
if (result.suggestion && (self.blank('accountUsername') || self.get('accountUsername') === self.get('authOptions.username'))) {
self.set('accountUsername', result.suggestion);
@ -225,7 +227,7 @@ export default DiscourseController.extend(ModalFunctionality, {
},
checkUsernameAvailability: Discourse.debounce(function() {
var _this = this;
const _this = this;
if (this.shouldCheckUsernameMatch()) {
return Discourse.User.checkUsername(this.get('accountUsername'), this.get('accountEmail')).then(function(result) {
_this.set('globalNicknameExists', false);
@ -275,30 +277,23 @@ export default DiscourseController.extend(ModalFunctionality, {
// Actually wait for the async name check before we're 100% sure we're good to go
usernameValidation: function() {
var basicValidation, uniqueUsername;
basicValidation = this.get('basicUsernameValidation');
uniqueUsername = this.get('uniqueUsernameValidation');
if (uniqueUsername) {
return uniqueUsername;
}
return basicValidation;
const basicValidation = this.get('basicUsernameValidation');
const uniqueUsername = this.get('uniqueUsernameValidation');
return uniqueUsername ? uniqueUsername : basicValidation;
}.property('uniqueUsernameValidation', 'basicUsernameValidation'),
usernameNeedsToBeValidatedWithEmail: function() {
usernameNeedsToBeValidatedWithEmail() {
return( this.get('globalNicknameExists') || false );
},
// Validate the password
passwordValidation: function() {
var password;
if (!this.get('passwordRequired')) {
return Discourse.InputValidation.create({
ok: true
});
return Discourse.InputValidation.create({ ok: true });
}
// If blank, fail without a reason
password = this.get("accountPassword");
const password = this.get("accountPassword");
if (this.blank('accountPassword')) {
return Discourse.InputValidation.create({ failed: true });
}
@ -339,8 +334,8 @@ export default DiscourseController.extend(ModalFunctionality, {
});
}.property('accountPassword', 'rejectedPasswords.@each', 'accountUsername', 'accountEmail'),
fetchConfirmationValue: function() {
var createAccountController = this;
fetchConfirmationValue() {
const createAccountController = this;
return Discourse.ajax('/users/hp.json').then(function (json) {
createAccountController.set('accountPasswordConfirm', json.value);
createAccountController.set('accountChallenge', json.challenge.split("").reverse().join(""));
@ -348,12 +343,12 @@ export default DiscourseController.extend(ModalFunctionality, {
},
actions: {
externalLogin: function(provider) {
externalLogin(provider) {
this.get('controllers.login').send('externalLogin', provider);
},
createAccount: function() {
var self = this,
createAccount() {
const self = this,
attrs = this.getProperties('accountName', 'accountEmail', 'accountPassword', 'accountUsername', 'accountPasswordConfirm', 'accountChallenge'),
userFields = this.get('userFields');
@ -369,7 +364,7 @@ export default DiscourseController.extend(ModalFunctionality, {
return Discourse.User.createAccount(attrs).then(function(result) {
if (result.success) {
// Trigger the browser's password manager using the hidden static login form:
var $hidden_login_form = $('#hidden-login-form');
const $hidden_login_form = $('#hidden-login-form');
$hidden_login_form.find('input[name=username]').val(attrs.accountUsername);
$hidden_login_form.find('input[name=password]').val(attrs.accountPassword);
$hidden_login_form.find('input[name=redirect]').val(Discourse.getURL('/users/account-created'));
@ -397,7 +392,7 @@ export default DiscourseController.extend(ModalFunctionality, {
_createUserFields: function() {
if (!this.site) { return; }
var userFields = this.site.get('user_fields');
let userFields = this.site.get('user_fields');
if (userFields) {
userFields = userFields.map(function(f) {
return Ember.Object.create({

View File

@ -7,7 +7,8 @@ export var queryParams = {
status: { replace: true },
state: { replace: true },
search: { replace: true },
max_posts: { replace: true }
max_posts: { replace: true },
q: { replace: true }
};
// Basic controller options

View File

@ -2,7 +2,7 @@ import DiscoveryController from 'discourse/controllers/discovery';
import { queryParams } from 'discourse/controllers/discovery-sortable';
import BulkTopicSelection from 'discourse/mixins/bulk-topic-selection';
var controllerOpts = {
const controllerOpts = {
needs: ['discovery'],
period: null,
@ -16,20 +16,42 @@ var controllerOpts = {
expandGloballyPinned: false,
expandAllPinned: false,
isSearch: Em.computed.equal('model.filter', 'search'),
searchTerm: function(){
return this.get('model.params.q');
}.property('isSearch,model.params,model'),
actions: {
changeSort: function(sortBy) {
if (sortBy === this.get('order')) {
this.toggleProperty('ascending');
changeSort(sortBy) {
if (this.get('isSearch')) {
let term = this.get('searchTerm');
let order;
if (sortBy === 'activity') { order = 'latest'; }
if (sortBy === 'views') { order = 'views'; }
if (order && term.indexOf("order:" + order) === -1) {
term = term.replace(/order:[a-z]+/, '');
term = term.trim() + " order:" + order;
this.set('model.params.q', term);
this.get('model').refreshSort();
}
} else {
this.setProperties({ order: sortBy, ascending: false });
if (sortBy === this.get('order')) {
this.toggleProperty('ascending');
} else {
this.setProperties({ order: sortBy, ascending: false });
}
this.get('model').refreshSort(sortBy, this.get('ascending'));
}
this.get('model').refreshSort(sortBy, this.get('ascending'));
},
// Show newly inserted topics
showInserted: function() {
var tracker = Discourse.TopicTrackingState.current();
const tracker = Discourse.TopicTrackingState.current();
// Move inserted into topics
this.get('content').loadBefore(tracker.get('newIncoming'));
@ -38,7 +60,7 @@ var controllerOpts = {
},
refresh: function() {
var filter = this.get('model.filter'),
const filter = this.get('model.filter'),
self = this;
this.setProperties({ order: 'default', ascending: false });
@ -57,7 +79,7 @@ var controllerOpts = {
self.setProperties({ model: list });
self.resetSelected();
var tracking = Discourse.TopicTrackingState.current();
const tracking = Discourse.TopicTrackingState.current();
if (tracking) {
tracking.sync(list, filter);
}
@ -68,7 +90,7 @@ var controllerOpts = {
resetNew: function() {
var self = this;
const self = this;
Discourse.TopicTrackingState.current().resetNew();
Discourse.Topic.resetNew().then(function() {
@ -113,11 +135,11 @@ var controllerOpts = {
footerMessage: function() {
if (!this.get('allLoaded')) { return; }
var category = this.get('category');
const category = this.get('category');
if( category ) {
return I18n.t('topics.bottom.category', {category: category.get('name')});
} else {
var split = (this.get('model.filter') || '').split('/');
const split = (this.get('model.filter') || '').split('/');
if (this.get('model.topics.length') === 0) {
return I18n.t("topics.none." + split[0], {
category: split[1]
@ -133,7 +155,7 @@ var controllerOpts = {
footerEducation: function() {
if (!this.get('allLoaded') || this.get('model.topics.length') > 0 || !Discourse.User.current()) { return; }
var split = (this.get('model.filter') || '').split('/');
const split = (this.get('model.filter') || '').split('/');
if (split[0] !== 'new' && split[0] !== 'unread') { return; }

View File

@ -47,7 +47,7 @@ export default DiscourseController.extend(ModalFunctionality, {
actions: {
login: function() {
var self = this;
const self = this;
if(this.blank('loginName') || this.blank('loginPassword')){
self.flash(I18n.t('login.blank_username_or_password'), 'error');
@ -69,14 +69,15 @@ export default DiscourseController.extend(ModalFunctionality, {
sentTo: result.sent_to_email,
currentEmail: result.current_email
});
} else {
self.flash(result.error, 'error');
}
self.flash(result.error, 'error');
} else {
self.set('loggedIn', true);
// Trigger the browser's password manager using the hidden static login form:
var $hidden_login_form = $('#hidden-login-form');
var destinationUrl = $.cookie('destination_url');
var shouldRedirectToUrl = self.session.get("shouldRedirectToUrl");
const $hidden_login_form = $('#hidden-login-form');
const destinationUrl = $.cookie('destination_url');
const shouldRedirectToUrl = self.session.get("shouldRedirectToUrl");
$hidden_login_form.find('input[name=username]').val(self.get('loginName'));
$hidden_login_form.find('input[name=password]').val(self.get('loginPassword'));
if (self.get('loginRequired') && destinationUrl) {
@ -102,22 +103,22 @@ export default DiscourseController.extend(ModalFunctionality, {
},
externalLogin: function(loginMethod){
var name = loginMethod.get("name");
var customLogin = loginMethod.get("customLogin");
const name = loginMethod.get("name");
const customLogin = loginMethod.get("customLogin");
if(customLogin){
customLogin();
} else {
this.set('authenticate', name);
var left = this.get('lastX') - 400;
var top = this.get('lastY') - 200;
const left = this.get('lastX') - 400;
const top = this.get('lastY') - 200;
var height = loginMethod.get("frameHeight") || 400;
var width = loginMethod.get("frameWidth") || 800;
var w = window.open(Discourse.getURL("/auth/" + name), "_blank",
const height = loginMethod.get("frameHeight") || 400;
const width = loginMethod.get("frameWidth") || 800;
const w = window.open(Discourse.getURL("/auth/" + name), "_blank",
"menubar=no,status=no,height=" + height + ",width=" + width + ",left=" + left + ",top=" + top);
var self = this;
var timer = setInterval(function() {
const self = this;
const timer = setInterval(function() {
if(!w || w.closed) {
clearInterval(timer);
self.set('authenticate', null);
@ -127,10 +128,10 @@ export default DiscourseController.extend(ModalFunctionality, {
},
createAccount: function() {
var createAccountController = this.get('controllers.createAccount');
const createAccountController = this.get('controllers.createAccount');
if (createAccountController) {
createAccountController.resetForm();
var loginName = this.get('loginName');
const loginName = this.get('loginName');
if (loginName && loginName.indexOf('@') > 0) {
createAccountController.set("accountEmail", loginName);
} else {
@ -141,7 +142,7 @@ export default DiscourseController.extend(ModalFunctionality, {
},
forgotPassword: function() {
var forgotPasswordController = this.get('controllers.forgotPassword');
const forgotPasswordController = this.get('controllers.forgotPassword');
if (forgotPasswordController) { forgotPasswordController.set("accountEmailOrUsername", this.get("loginName")); }
this.send("showForgotPassword");
}
@ -149,13 +150,13 @@ export default DiscourseController.extend(ModalFunctionality, {
authMessage: (function() {
if (this.blank('authenticate')) return "";
var method = Discourse.get('LoginMethod.all').findProperty("name", this.get("authenticate"));
const method = Discourse.get('LoginMethod.all').findProperty("name", this.get("authenticate"));
if(method){
return method.get('message');
}
}).property('authenticate'),
authenticationComplete: function(options) {
authenticationComplete(options) {
const self = this;
function loginError(errorMsg, className) {
@ -187,12 +188,12 @@ export default DiscourseController.extend(ModalFunctionality, {
return;
}
var createAccountController = this.get('controllers.createAccount');
const createAccountController = this.get('controllers.createAccount');
createAccountController.setProperties({
accountEmail: options.email,
accountUsername: options.username,
accountName: options.name,
authOptions: Em.Object.create(options)
authOptions: Ember.Object.create(options)
});
showModal('createAccount');
}

View File

@ -13,13 +13,13 @@ export default DiscourseController.extend({
isSearch: Em.computed.equal('filterMode', 'search'),
searchTerm: Em.computed.alias('controllers.discovery/topics.model.params.search'),
searchTerm: Em.computed.alias('controllers.discovery/topics.model.params.q'),
actions: {
search: function(){
var discovery = this.get('controllers.discovery/topics');
var model = discovery.get('model');
discovery.set('search', this.get("searchTerm"));
discovery.set('q', this.get('searchTerm'));
model.refreshSort();
}
}

View File

@ -4,6 +4,10 @@ import DiscourseController from 'discourse/controllers/controller';
export default DiscourseController.extend(ModalFunctionality, {
emailSent: false,
onShow() {
this.set("emailSent", false);
},
actions: {
sendActivationEmail: function() {
Discourse.ajax('/users/action/send_activation_email', {data: {username: this.get('username')}, type: 'POST'});

View File

@ -88,7 +88,13 @@ export default Em.Controller.extend(Presence, {
actions: {
moreOfType: function(type) {
this.set('typeFilter', type);
if (type === 'topic' && (!this.get('searchContextEnabled') || this.get('searchContext.type') !== 'topic')) {
var term = this.get('term');
// TODO in topic and in category special handling
Discourse.URL.routeTo("/search?q=" + encodeURIComponent(term));
} else {
this.set('typeFilter', type);
}
},
cancelType: function() {

View File

@ -0,0 +1,52 @@
import ModalFunctionality from 'discourse/mixins/modal-functionality';
export default Ember.Controller.extend(ModalFunctionality, {
notReady: Em.computed.not('ready'),
needs: ['admin-customize-css-html'],
title: "hi",
ready: function() {
let parsed;
try {
parsed = JSON.parse(this.get('customizationFile'));
} catch (e) {
return false;
}
return !!parsed["site_customization"];
}.property('customizationFile'),
actions: {
createCustomization: function() {
const self = this;
const object = JSON.parse(this.get('customizationFile')).site_customization;
// Slight fixup before creating object
object.enabled = false;
delete object.id;
delete object.key;
const customization = Discourse.SiteCustomization.create(object);
this.set('loading', true);
customization.save().then(function(customization) {
self.send('closeModal');
self.set('loading', false);
const parentController = self.get('controllers.admin-customize-css-html');
parentController.pushObject(customization);
parentController.set('selectedItem', customization);
}).catch(function(xhr) {
self.set('loading', false);
if (xhr.responseJSON) {
bootbox.alert(xhr.responseJSON.errors.join("<br>"));
} else {
bootbox.alert(I18n.t('generic_error'));
}
});
}
}
});

View File

@ -89,6 +89,10 @@ export default Ember.DefaultResolver.extend({
return this.customResolve(parsedName) || this._super(parsedName);
},
resolveModel(parsedName) {
return this.customResolve(parsedName) || this._super(parsedName);
},
resolveView(parsedName) {
return this.findLoadingView(parsedName) || this.customResolve(parsedName) || this._super(parsedName);
},

View File

@ -4,21 +4,13 @@ var safe = Handlebars.SafeString;
var registerUnbound = require('discourse/helpers/register-unbound', null, null, true).default;
var avatarTemplate = require('discourse/lib/avatar-template', null, null, true).default;
/**
Bound avatar helper.
@method bound-avatar
@for Handlebars
**/
Em.Handlebars.helper('bound-avatar', function(user, size, uploadId) {
if (Em.isEmpty(user)) {
return new safe("<div class='avatar-placeholder'></div>");
}
var username = Em.get(user, 'username');
if (arguments.length < 4) { uploadId = Em.get(user, 'uploaded_avatar_id'); }
var avatar = Em.get(user, 'avatar_template') || avatarTemplate(username, uploadId);
return new safe(Discourse.Utilities.avatarImg({ size: size, avatarTemplate: avatar }));

View File

@ -1,5 +1,3 @@
import { iconHTML } from 'discourse/helpers/fa-icon';
const TITLE_SUBS = {
all: 'all_time',
yearly: 'this_year',
@ -7,7 +5,26 @@ const TITLE_SUBS = {
daily: 'today',
};
export default Ember.Handlebars.makeBoundHelper(function (period) {
export default Ember.Handlebars.makeBoundHelper(function (period, options) {
const title = I18n.t('filters.top.' + (TITLE_SUBS[period] || 'this_week'));
return new Handlebars.SafeString(iconHTML('calendar-o') + " " + title);
if (options.hash.showDateRange) {
var dateString = "";
switch(period) {
case 'yearly':
dateString = moment().subtract(1, 'year').format(I18n.t('dates.full_with_year_no_time'));
break;
case 'weekly':
dateString = moment().subtract(1, 'week').format(I18n.t('dates.full_no_year_no_time')) + " - " + moment().format(I18n.t('dates.full_no_year_no_time'));
break;
case 'monthly':
dateString = moment().subtract(1, 'month').format(I18n.t('dates.full_no_year_no_time')) + " - " + moment().format(I18n.t('dates.full_no_year_no_time'));
break;
case 'daily':
dateString = moment().format(I18n.t('dates.full_no_year_no_time'));
break;
}
return new Handlebars.SafeString(title + " <span class='top-date-string'>" + dateString + "</span>");
} else {
return new Handlebars.SafeString(title);
}
});

View File

@ -2,5 +2,6 @@ import registerUnbound from 'discourse/helpers/register-unbound';
registerUnbound('topic-link', function(topic) {
var title = topic.get('fancyTitle');
return new Handlebars.SafeString("<a href='" + topic.get('lastUnreadUrl') + "' class='title'>" + title + "</a>");
var url = topic.linked_post_number ? topic.urlForPostNumber(topic.linked_post_number) : topic.get('lastUnreadUrl');
return new Handlebars.SafeString("<a href='" + url + "' class='title'>" + title + "</a>");
});

View File

@ -64,6 +64,10 @@ export default {
$(this).data('orig', this.href);
}
const orig = $(this).data('orig');
if (!me.hash) {
window.__uniq = window.__uniq || 1;
me.hash = window.__uniq++;
}
this.href = orig + (orig.indexOf('?') >= 0 ? "&hash=" : "?hash=") + me.hash;
}
});

View File

@ -28,7 +28,7 @@ function extractError(error) {
if (parsedJSON) {
if (parsedJSON.errors && parsedJSON.errors.length > 0) {
parsedError = parsedJSON.errors[0];
parsedError = parsedJSON.errors.join("<br>");
} else if (parsedJSON.error) {
parsedError = parsedJSON.error;
} else if (parsedJSON.failed) {
@ -36,6 +36,12 @@ function extractError(error) {
}
}
if (!parsedError) {
if (error.status && error.status >= 400) {
parsedError = error.status + " " + error.statusText;
}
}
return parsedError || I18n.t('generic_error');
}

View File

@ -86,6 +86,7 @@ var translations = {
':-D' : 'smiley',
':|' : 'expressionless',
':-|' : 'expressionless',
':/' : 'confused',
";P" : 'stuck_out_tongue_winking_eye',
";-P" : 'stuck_out_tongue_winking_eye',
":$" : 'blush',

View File

@ -188,7 +188,7 @@ relativeAgeMediumSpan = function(distance, leaveAgo) {
};
switch(true){
case(distanceInMinutes >= 1 && distanceInMinutes <= 56):
case(distanceInMinutes >= 1 && distanceInMinutes <= 55):
formatted = t("x_minutes", {count: distanceInMinutes});
break;
case(distanceInMinutes >= 56 && distanceInMinutes <= 89):
@ -258,14 +258,18 @@ relativeAge = function(date, options) {
};
var number = function(val) {
var formattedNumber;
val = parseInt(val, 10);
if (isNaN(val)) val = 0;
if (val > 999999) {
return (val / 1000000).toFixed(1) + "M";
formattedNumber = I18n.toNumber(val / 1000000, {precision: 1});
return I18n.t("number.short.millions", {number: formattedNumber});
}
if (val > 999) {
return (val / 1000).toFixed(1) + "K";
formattedNumber = I18n.toNumber(val / 1000, {precision: 1});
return I18n.t("number.short.thousands", {number: formattedNumber});
}
return val.toString();
};

View File

@ -59,6 +59,7 @@ Discourse.KeyboardShortcuts = Ember.Object.createWithMixins({
bindEvents: function(keyTrapper, container) {
this.keyTrapper = keyTrapper;
this.container = container;
this._stopCallback();
_.each(PATH_BINDINGS, this._bindToPath, this);
_.each(CLICK_BINDINGS, this._bindToClick, this);
@ -128,6 +129,11 @@ Discourse.KeyboardShortcuts = Ember.Object.createWithMixins({
},
showBuiltinSearch: function() {
if ($('#search-dropdown').is(':visible')) {
this._toggleSearch(false);
return true;
}
var currentPath = this.container.lookup('controller:application').get('currentPath'),
blacklist = [ /^discovery\.categories/ ],
whitelist = [ /^topic\./ ],
@ -137,10 +143,14 @@ Discourse.KeyboardShortcuts = Ember.Object.createWithMixins({
// If we're viewing a topic, only intercept search if there are cloaked posts
if (showSearch && currentPath.match(/^topic\./)) {
showSearch = $('.cooked').length < this.container.lookup('controller:topic').get('postStream.stream.length');
}
return showSearch ? this.showSearch(true) : true;
if (showSearch) {
this._toggleSearch(true);
return false;
}
return true;
},
createTopic: function() {
@ -155,11 +165,8 @@ Discourse.KeyboardShortcuts = Ember.Object.createWithMixins({
Discourse.__container__.lookup('controller:topic-progress').send('toggleExpansion', {highlight: true});
},
showSearch: function(selectContext) {
$('#search-button').click();
if(selectContext) {
Discourse.__container__.lookup('controller:search').set('searchContextEnabled', true);
}
showSearch: function() {
this._toggleSearch(false);
return false;
},
@ -304,22 +311,32 @@ Discourse.KeyboardShortcuts = Ember.Object.createWithMixins({
tabLoc.focus();
}
var rgx = new RegExp("post-cloak-(\\d+)").exec($article.parent()[0].id);
if (rgx === null || typeof rgx[1] === 'undefined') {
this._scrollList($article, direction);
} else {
Discourse.URL.jumpToPost(rgx[1]);
}
this._scrollList($article, direction);
}
},
_scrollList: function($article) {
// Try to keep the article on screen
var scrollPos = $article.position().top - ($(window).height() * 0.5);
var pos = $article.offset();
var height = $article.height();
var scrollTop = $(window).scrollTop();
var windowHeight = $(window).height();
// skip if completely on screen
if (pos.top > scrollTop && (pos.top + height) < (scrollTop + windowHeight)) {
return;
}
var scrollPos = (pos.top + (height/2)) - (windowHeight * 0.5);
if (scrollPos < 0) { scrollPos = 0; }
$('html, body').scrollTop(scrollPos);
if (this._scrollAnimation) {
this._scrollAnimation.stop();
}
this._scrollAnimation = $("html, body").animate({ scrollTop: scrollPos + "px"}, 100);
},
_findArticles: function() {
var $topicList = $('.topic-list'),
$topicArea = $('.posts-wrapper');
@ -340,5 +357,24 @@ Discourse.KeyboardShortcuts = Ember.Object.createWithMixins({
if(index >= 0 && index < $sections.length){
$sections.eq(index).find('a').click();
}
}
},
_stopCallback: function() {
var oldStopCallback = this.keyTrapper.stopCallback;
this.keyTrapper.stopCallback = function(e, element, combo) {
if ((combo === 'ctrl+f' || combo === 'command+f') && element.id === 'search-term') {
return false;
}
return oldStopCallback(e, element, combo);
};
},
_toggleSearch: function(selectContext) {
$('#search-button').click();
if (selectContext) {
Discourse.__container__.lookup('controller:search').set('searchContextEnabled', true);
}
},
});

View File

@ -1,4 +1,4 @@
import decimalAdjust from "discourse/plugins/poll/lib/decimal-adjust";
import decimalAdjust from "discourse/lib/decimal-adjust";
export default function(value, exp) {
return decimalAdjust("round", value, exp);

View File

@ -4,7 +4,7 @@ function searchForTerm(term, opts) {
if (!opts) opts = {};
// Only include the data we have
var data = { term: term, include_blurbs: 'true' };
const data = { term: term, include_blurbs: 'true' };
if (opts.typeFilter) data.type_filter = opts.typeFilter;
if (opts.searchForId) data.search_for_id = true;
@ -22,7 +22,7 @@ function searchForTerm(term, opts) {
if (!results.posts) { results.posts = []; }
if (!results.categories) { results.categories = []; }
var topicMap = {};
const topicMap = {};
results.topics = results.topics.map(function(topic){
topic = Topic.create(topic);
topicMap[topic.id] = topic;
@ -44,23 +44,23 @@ function searchForTerm(term, opts) {
return Discourse.Category.list().findProperty('id', category.id);
}).compact();
var r = results.grouped_search_result;
const r = results.grouped_search_result;
results.resultTypes = [];
// TODO: consider refactoring front end to take a better structure
[['topic','posts'],['user','users'],['category','categories']].forEach(function(pair){
var type = pair[0], name = pair[1];
if(results[name].length > 0) {
const type = pair[0], name = pair[1];
if (results[name].length > 0) {
results.resultTypes.push({
results: results[name],
displayType: (opts.searchContext && opts.searchContext.type === 'topic' && type === 'topic') ? 'post' : type,
type: type,
componentName: "search-result-" + ((opts.searchContext && opts.searchContext.type === 'topic' && type === 'topic') ? 'post' : type),
type,
more: r['more_' + name]
});
}
});
var noResults = !!(results.topics.length === 0 &&
const noResults = !!(results.topics.length === 0 &&
results.posts.length === 0 &&
results.users.length === 0 &&
results.categories.length === 0);

View File

@ -109,7 +109,6 @@ Discourse.URL = Ember.Object.createWithMixins({
@param {String} path The path we are routing to.
**/
routeTo: function(path) {
if (Em.isEmpty(path)) { return; }
if (Discourse.get('requiresRefresh')) {

View File

@ -9,6 +9,24 @@
Discourse.NavItem = Discourse.Model.extend({
displayName: function() {
var categoryName = this.get('categoryName'),
name = this.get('name'),
count = this.get('count') || 0;
if (name === 'latest' && !Discourse.Mobile.mobileView) {
count = 0;
}
var extra = { count: count };
if (categoryName) {
name = 'category';
extra.categoryName = Discourse.Formatter.toTitleCase(categoryName);
}
return I18n.t("filters." + name.replace("/", ".") + ".title", extra);
}.property('categoryName,name,count'),
topicTrackingState: function() {
return Discourse.TopicTrackingState.current();
}.property(),

View File

@ -6,16 +6,20 @@ function calcDayDiff(p1, p2) {
const date = p1.get('created_at');
if (date) {
if (p2) {
const lastDate = p2.get('created_at');
if (lastDate) {
const delta = new Date(date).getTime() - new Date(lastDate).getTime();
const days = Math.round(delta / (1000 * 60 * 60 * 24));
const numDiff = p1.get('post_number') - p2.get('post_number');
if (numDiff === 1) {
const lastDate = p2.get('created_at');
if (lastDate) {
const delta = new Date(date).getTime() - new Date(lastDate).getTime();
const days = Math.round(delta / (1000 * 60 * 60 * 24));
p1.set('daysSincePrevious', days);
p1.set('daysSincePrevious', days);
}
}
}
}
}
const PostStream = RestModel.extend({
loading: Em.computed.or('loadingAbove', 'loadingBelow', 'loadingFilter', 'stagingPost'),
notLoading: Em.computed.not('loading'),
@ -137,7 +141,13 @@ const PostStream = RestModel.extend({
toggleSummary() {
this.get('userFilters').clear();
this.toggleProperty('summary');
return this.refresh();
const self = this;
return this.refresh().then(function() {
if (self.get('summary')) {
self.jumpToSecondVisible();
}
});
},
toggleDeleted() {
@ -145,17 +155,33 @@ const PostStream = RestModel.extend({
return this.refresh();
},
jumpToSecondVisible() {
const posts = this.get('posts');
if (posts.length > 1) {
const secondPostNum = posts[1].get('post_number');
Discourse.URL.jumpToPost(secondPostNum);
}
},
// Filter the stream to a particular user.
toggleParticipant(username) {
const userFilters = this.get('userFilters');
this.set('summary', false);
this.set('show_deleted', true);
let jump = false;
if (userFilters.contains(username)) {
userFilters.removeObject(username);
} else {
userFilters.addObject(username);
jump = true;
}
return this.refresh();
const self = this;
return this.refresh().then(function() {
if (jump) {
self.jumpToSecondVisible();
}
});
},
/**

View File

@ -50,6 +50,12 @@ const Post = RestModel.extend({
return Discourse.Utilities.postUrl(this.get('topic.slug') || this.get('topic_slug'), this.get('topic_id'), this.get('post_number'));
}.property('post_number', 'topic_id', 'topic.slug'),
// Don't drop the /1
urlWithNumber: function() {
const url = this.get('url');
return (this.get('post_number') === 1) ? url + "/1" : url;
}.property('post_number', 'url'),
usernameUrl: Discourse.computed.url('username', '/users/%@'),
showUserReplyTab: function() {

View File

@ -37,8 +37,13 @@ function findAndRemoveMap(type, id) {
flushMap();
export default Ember.Object.extend({
_plurals: {},
pluralize(thing) {
return thing + "s";
return this._plurals[thing] || thing + "s";
},
addPluralization(thing, plural) {
this._plurals[thing] = plural;
},
findAll(type) {
@ -143,10 +148,13 @@ export default Ember.Object.extend({
return this.container.lookup('adapter:' + type) || this.container.lookup('adapter:rest');
},
_lookupSubType(subType, id, root) {
_lookupSubType(subType, type, id, root) {
// cheat: we know we already have categories in memory
if (subType === 'category') {
// TODO: topics do their own resolving of `category_id`
// to category. That should either respect this or be
// removed.
if (subType === 'category' && type !== 'topic') {
return Discourse.Category.findById(id);
}
@ -172,13 +180,13 @@ export default Ember.Object.extend({
}
},
_hydrateEmbedded(obj, root) {
_hydrateEmbedded(type, obj, root) {
const self = this;
Object.keys(obj).forEach(function(k) {
const m = /(.+)\_id$/.exec(k);
if (m) {
const subType = m[1];
const hydrated = self._lookupSubType(subType, obj[k], root);
const hydrated = self._lookupSubType(subType, type, obj[k], root);
if (hydrated) {
obj[subType] = hydrated;
delete obj[k];
@ -196,7 +204,7 @@ export default Ember.Object.extend({
// Experimental: If serialized with a certain option we'll wire up embedded objects
// automatically.
if (root.__rest_serializer === "1") {
this._hydrateEmbedded(obj, root);
this._hydrateEmbedded(type, obj, root);
}
const existing = fromMap(type, obj.id);

View File

@ -40,8 +40,8 @@ const TopicList = RestModel.extend({
},
refreshSort: function(order, ascending) {
const self = this,
params = this.get('params') || {};
const self = this;
var params = this.get('params') || {};
params.order = order || params.order;
@ -51,7 +51,14 @@ const TopicList = RestModel.extend({
params.ascending = ascending;
}
if (params.q) {
// search is unique, nothing else allowed with it
params = {q: params.q};
}
this.set('loaded', false);
this.set('params', params);
const store = this.store;
store.findFiltered('topicList', {filter: this.get('filter'), params}).then(function(tl) {
const newTopics = tl.get('topics'),

View File

@ -266,6 +266,12 @@ const TopicTrackingState = Discourse.Model.extend({
},
lookupCount(name, category){
if (name === "latest") {
return this.lookupCount("new", category) +
this.lookupCount("unread", category);
}
let categoryName = category ? Em.get(category, "name") : null;
if(name === "new") {
return this.countNew(categoryName);
@ -278,6 +284,7 @@ const TopicTrackingState = Discourse.Model.extend({
}
}
},
loadStates(data) {
// not exposed
const states = this.states;

View File

@ -363,8 +363,7 @@ const Topic = RestModel.extend({
);
},
excerptNotEmpty: Em.computed.notEmpty('excerpt'),
hasExcerpt: Em.computed.and('pinned', 'excerptNotEmpty'),
hasExcerpt: Em.computed.notEmpty('excerpt'),
excerptTruncated: function() {
const e = this.get('excerpt');

View File

@ -16,6 +16,7 @@ function findTopicList(store, filter, filterParams, extras) {
extras = extras || {};
return new Ember.RSVP.Promise(function(resolve) {
const session = Discourse.Session.current();
if (extras.cached) {
@ -80,7 +81,6 @@ export default function(filter, extras) {
},
model(data, transition) {
// attempt to stop early cause we need this to be called before .sync
Discourse.ScreenTrack.current().stop();
@ -115,8 +115,12 @@ export default function(filter, extras) {
const params = model.get('params');
if (params && Object.keys(params).length) {
topicOpts.order = params.order;
topicOpts.ascending = params.ascending;
if (params.order !== undefined) {
topicOpts.order = params.order;
}
if (params.ascending !== undefined) {
topicOpts.ascending = params.ascending;
}
}
this.controllerFor('discovery/topics').setProperties(topicOpts);

View File

@ -1,22 +1,22 @@
var INDEX_STREAM_ROUTES = ["user.deletedPosts", "user.flaggedPosts", "userActivity.index"];
const INDEX_STREAM_ROUTES = ["user.deletedPosts", "user.flaggedPosts", "userActivity.index"];
export default Discourse.Route.extend({
titleToken: function() {
var model = this.modelFor('user');
var username = model.get('username');
titleToken() {
const model = this.modelFor('user');
const username = model.get('username');
if (username) {
return [I18n.t("user.profile"), username];
}
},
actions: {
logout: function() {
logout() {
Discourse.logout();
},
composePrivateMessage: function(user, post) {
var recipient = user ? user.get('username') : '',
composePrivateMessage(user, post) {
const recipient = user ? user.get('username') : '',
reply = post ? window.location.protocol + "//" + window.location.host + post.get("url") : null;
return this.controllerFor('composer').open({
@ -28,18 +28,18 @@ export default Discourse.Route.extend({
});
},
willTransition: function(transition) {
willTransition(transition) {
// will reset the indexStream when transitioning to routes that aren't "indexStream"
// otherwise the "header" will jump
var isIndexStream = ~INDEX_STREAM_ROUTES.indexOf(transition.targetName);
const isIndexStream = ~INDEX_STREAM_ROUTES.indexOf(transition.targetName);
this.controllerFor('user').set('indexStream', isIndexStream);
return true;
}
},
model: function(params) {
model(params) {
// If we're viewing the currently logged in user, return that object instead
var currentUser = Discourse.User.current();
const currentUser = this.currentUser;
if (currentUser && (params.username.toLowerCase() === currentUser.get('username_lower'))) {
return currentUser;
}
@ -47,34 +47,38 @@ export default Discourse.Route.extend({
return Discourse.User.create({username: params.username});
},
afterModel: function() {
var user = this.modelFor('user');
afterModel() {
const user = this.modelFor('user');
const self = this;
return user.findDetails().then(function() {
return user.findStaffInfo();
}).catch(function() {
return self.replaceWith('/404');
});
},
serialize: function(model) {
serialize(model) {
if (!model) return {};
return { username: (Em.get(model, 'username') || '').toLowerCase() };
},
setupController: function(controller, user) {
setupController(controller, user) {
controller.set('model', user);
// Add a search context
this.controllerFor('search').set('searchContext', user.get('searchContext'));
},
activate: function() {
activate() {
this._super();
var user = this.modelFor('user');
const user = this.modelFor('user');
this.messageBus.subscribe("/users/" + user.get('username_lower'), function(data) {
user.loadUserAction(data);
});
},
deactivate: function() {
deactivate() {
this._super();
this.messageBus.unsubscribe("/users/" + this.modelFor('user').get('username_lower'));

View File

@ -3,7 +3,10 @@
{{topic-list
showParticipants=showParticipants
hideCategory=hideCategory
topics=topics}}
topics=topics
expandExcerpts=expandExcerpts
searchTerm=searchTerm
}}
{{else}}
<div class='alert alert-info'>
{{i18n 'choose_topic.none_found'}}

View File

@ -0,0 +1,12 @@
<div class="jsfu-shade-container">
<div class="jsfu-file">
<input id="js-file-input" type="file" style="display:none;" accept={{accept}}>
{{d-button class="fileSelect" action="selectFile" class="" icon="upload" label="upload_selector.select_file"}}
{{conditional-loading-spinner condition=loading size="small"}}
</div>
<div class="jsfu-separator">{{i18n "alternation"}}</div>
<div class="jsfu-paste">
{{textarea value=value}}
</div>
<div class="jsfu-shade {{if hover '' 'hidden'}}"><span class="text">{{fa-icon "upload"}}</span></div>
</div>

View File

@ -0,0 +1,4 @@
{{#each navItem in navItems}}
{{navigation-item content=navItem filterMode=filterMode}}
{{/each}}
{{custom-html "extraNavItem"}}

View File

@ -1,4 +1,4 @@
<h2>{{period-title period}}</h2>
<h2>{{period-title period showDateRange=true}}</h2>
<button>{{fa-icon "caret-down"}}</button>
<div id='period-popup' {{bind-attr class="showPeriods::hidden :period-popup"}}>

View File

@ -0,0 +1,7 @@
{{#each results as |result|}}
<li>
<a href='{{unbound result.url}}'>
{{category-badge result}}
</a>
</li>
{{/each}}

View File

@ -0,0 +1,12 @@
{{#each results as |result|}}
<a class='search-link' href='{{unbound result.urlWithNumber}}'>
<span class='topic'>
{{i18n 'search.post_format' post_number=result.post_number username=result.username}}
</span>
{{#unless site.mobileView}}
<span class='blurb'>
{{{unbound result.blurb}}}
</span>
{{/unless}}
</a>
{{/each}}

View File

@ -0,0 +1,14 @@
{{#each results as |result|}}
<li>
<a class='search-link' href='{{unbound result.url}}'>
<span class='topic'>
{{topic-status topic=result.topic disableActions=true}}<span class='topic-title'>{{unbound result.topic.title}}</span>{{category-badge result.topic.category}}
</span>
{{#unless site.mobileView}}
<span class='blurb'>
{{format-age result.created_at}} - {{{unbound result.blurb}}}
</span>
{{/unless}}
</a>
</li>
{{/each}}

View File

@ -0,0 +1,8 @@
{{#each results as |result|}}
<li>
<a href='{{unbound result.path}}'>
{{avatar result imageSize="small"}}
{{unbound result.username}}
</a>
</li>
{{/each}}

View File

@ -2,11 +2,5 @@
<h3>{{i18n 'composer.similar_topics'}}</h3>
<ul class='topics'>
{{#each similarTopics as |t|}}
<li>
{{topic-status topic=t}}
{{topic-link t}}
{{category-link t.category}}
</li>
{{/each}}
{{search-result-topic results=similarTopics}}
</ul>

View File

@ -46,6 +46,8 @@
selected=selected
expandGloballyPinned=expandGloballyPinned
expandAllPinned=expandAllPinned
expandExcerpts=isSearch
searchTerm=searchTerm
topics=model.topics}}
{{/if}}
</div>

View File

@ -0,0 +1,13 @@
<li>
<a {{action "toggleDrop"}}>
{{selectedNavItem.displayName}}
<i class='fa fa-caret-down'></i>
</a>
</li>
{{#if expanded}}
<ul class='drop'>
{{#each navItem in navItems}}
{{navigation-item content=navItem filterMode=filterMode}}
{{/each}}
</ul>
{{/if}}

View File

@ -21,17 +21,19 @@
<td><label>{{i18n 'user.email.instructions'}}</label></td>
</tr>
<tr class="input">
<td class="label"><label for='new-account-username'>{{i18n 'user.username.title'}}</label></td>
<td>
{{input value=accountUsername id="new-account-username" name="username" maxlength=maxUsernameLength}}
&nbsp;{{input-tip validation=usernameValidation id="username-validation"}}
</td>
</tr>
<tr class="instructions">
<td></td>
<td><label>{{i18n 'user.username.instructions'}}</label></td>
</tr>
{{#if usernameRequired}}
<tr class="input">
<td class="label"><label for='new-account-username'>{{i18n 'user.username.title'}}</label></td>
<td>
{{input value=accountUsername id="new-account-username" name="username" maxlength=maxUsernameLength}}
&nbsp;{{input-tip validation=usernameValidation id="username-validation"}}
</td>
</tr>
<tr class="instructions">
<td></td>
<td><label>{{i18n 'user.username.instructions'}}</label></td>
</tr>
{{/if}}
<tr class="input">
<td style="width:80px" class="label"><label for='new-account-name'>{{i18n 'user.name.title'}}</label></td>
@ -46,20 +48,20 @@
</tr>
{{#if passwordRequired}}
<tr class="input">
<td class="label"><label for='new-account-password'>{{i18n 'user.password.title'}}</label></td>
<tr class="input">
<td class="label"><label for='new-account-password'>{{i18n 'user.password.title'}}</label></td>
<td>
{{password-field value=accountPassword type="password" id="new-account-password" capsLockOn=capsLockOn}}
&nbsp;{{input-tip validation=passwordValidation}}
</td>
</tr>
<tr class="instructions">
<td></td>
<td>
{{password-field value=accountPassword type="password" id="new-account-password" capsLockOn=capsLockOn}}
&nbsp;{{input-tip validation=passwordValidation}}
<label>{{passwordInstructions}}</label>
<div {{bind-attr class=":caps-lock-warning capsLockOn::invisible"}}><i class="fa fa-exclamation-triangle"></i> {{i18n 'login.caps_lock_warning'}}</div>
</td>
</tr>
<tr class="instructions">
<td></td>
<td>
<label>{{passwordInstructions}}</label>
<div {{bind-attr class=":caps-lock-warning capsLockOn::invisible"}}><i class="fa fa-exclamation-triangle"></i> {{i18n 'login.caps_lock_warning'}}</div>
</td>
</tr>
</tr>
{{/if}}
<tr class="password-confirmation">

View File

@ -0,0 +1,8 @@
<form {{action "dummy" on="submit"}}>
<div class='modal-body'>
{{json-file-uploader value=customizationFile extension=".dcstyle.json"}}
</div>
<div class="modal-footer">
{{d-button class='btn-primary' action='createCustomization' type='submit' disabled=notReady icon="plus" label='admin.customize.import'}}
</div>
</form>

View File

@ -1,11 +1,6 @@
{{bread-crumbs categories=categories}}
<ul class="nav nav-pills" id='navigation-bar'>
{{#each navItem in navItems}}
{{navigation-item content=navItem filterMode=filterMode}}
{{/each}}
{{custom-html "extraNavItem"}}
</ul>
{{navigation-bar navItems=navItems filterMode=filterMode}}
{{#if canCreateCategory}}
<button class='btn btn-default' {{action "createCategory"}}><i class='fa fa-plus'></i>{{i18n 'category.create'}}</button>

View File

@ -3,12 +3,8 @@
noSubcategories=noSubcategories
hideSubcategories=showingSubcategoryList}}
<ul class="nav nav-pills" id='navigation-bar'>
{{#each navItem in navItems}}
{{navigation-item content=navItem filterMode=filterMode}}
{{/each}}
{{custom-html "extraNavItem"}}
</ul>
{{navigation-bar navItems=navItems filterMode=filterMode}}
{{#if currentUser}}
{{category-notifications-button category=category}}

View File

@ -6,12 +6,7 @@
{{else}}
{{bread-crumbs categories=categories}}
<ul class="nav nav-pills" id='navigation-bar'>
{{#each navItem in navItems}}
{{navigation-item content=navItem filterMode=filterMode}}
{{/each}}
{{custom-html "extraNavItem"}}
</ul>
{{navigation-bar navItems=navItems filterMode=filterMode}}
{{#if canCreateTopic}}
<button id="create-topic" class='btn btn-default' {{action "createTopic"}}><i class='fa fa-plus'></i>{{i18n 'topic.create'}}</button>

View File

@ -1,4 +1,6 @@
{{view "search-text-field" value=term searchContextEnabled=searchContextEnabled searchContext=searchContext id="search-term"}}
{{plugin-outlet "above-search"}}
{{search-text-field value=term searchContextEnabled=searchContextEnabled id="search-term"}}
<div class="search-context">
{{#if searchContext}}
<label>
@ -16,10 +18,10 @@
{{i18n "search.no_results"}}
</div>
{{else}}
{{#each resultType in content.resultTypes}}
{{#each content.resultTypes as |resultType|}}
<ul>
<li class="heading row">{{resultType.name}}</li>
{{view "search-results-type" type=resultType.type displayType=resultType.displayType content=resultType.results}}
{{component resultType.componentName results=resultType.results term=term}}
</ul>
<div class="no-results">
{{#if resultType.more}}

View File

@ -1,3 +0,0 @@
<a href='{{unbound url}}'>
{{category-badge this}}
</a>

View File

@ -1,10 +0,0 @@
<a class='search-link' href='{{unbound url}}'>
<span class='topic'>
{{i18n 'search.post_format' post_number=post_number username=username}}
</span>
{{#unless controller.site.mobileView}}
<span class='blurb'>
{{{unbound blurb}}}
</span>
{{/unless}}
</a>

View File

@ -1,10 +0,0 @@
<a class='search-link' href='{{unbound url}}'>
<span class='topic'>
{{topic-status topic=topic disableActions=true}}<span class='topic-title'>{{unbound topic.title}}</span>{{category-badge topic.category}}
</span>
{{#unless controller.site.mobileView}}
<span class='blurb'>
{{format-age created_at}} - {{{unbound blurb}}}
</span>
{{/unless}}
</a>

View File

@ -1,4 +0,0 @@
<a href='{{unbound path}}'>
{{avatar this imageSize="small"}}
{{unbound username}}
</a>

View File

@ -17,7 +17,7 @@
{{/if}}
<li>
{{#link-to "discovery.latest" class="latest-topics-link"}}
{{i18n 'filters.latest.title'}}
{{i18n 'filters.latest.title.zero'}}
{{/link-to}}
</li>
{{#if showBadgesLink}}

View File

@ -1,12 +1,12 @@
{{#if controller.visible}}
<div class="card-content">
<a href {{action "showUser"}}>{{bound-avatar avatar "huge"}}</a>
<a href={{user.path}} {{action "showUser"}}>{{bound-avatar avatar "huge"}}</a>
<div class="names">
<span>
<h1 class="{{staff}} {{new_user}}">
<a href {{action "showUser"}}>{{username}} {{user-status user currentUser=currentUser}}</a>
<a href={{user.path}} {{action "showUser"}}>{{username}} {{user-status user currentUser=currentUser}}</a>
</h1>
{{#if user.name}}
@ -18,7 +18,7 @@
{{/if}}
{{#if showName}}
<h2><a href {{action "showUser"}}>{{name}}</a></h2>
<h2><a href={{user.path}} {{action "showUser"}}>{{name}}</a></h2>
{{/if}}
</span>
</div>

View File

@ -5,9 +5,12 @@ export default ModalBodyView.extend({
classNames: ['avatar-selector'],
title: I18n.t('user.change_avatar.title'),
// *HACK* used to select the proper radio button, cause {{action}}
// stops the default behavior
// *HACK* used to select the proper radio button, because {{action}} stops the default behavior
selectedChanged: function() {
Em.run.next(() => $('input:radio[name="avatar"]').val([this.get('controller.selected')]) );
}.observes('controller.selected')
Em.run.next(() => $('input:radio[name="avatar"]').val([this.get('controller.selected')]));
}.observes('controller.selected').on("didInsertElement"),
_focusSelectedButton: function() {
Em.run.next(() => $('input:radio[value="' + this.get('controller.selected') + '"]').focus());
}.on("didInsertElement")
});

View File

@ -1,15 +0,0 @@
export default Ember.CollectionView.extend({
tagName: 'ul',
itemViewClass: Discourse.GroupedView.extend({
tagName: 'li',
classNameBindings: ['selected'],
templateName: Discourse.computed.fmt('parentView.displayType', "search/%@_result")
}),
didInsertElement: function(){
var term = this.get('controller.term');
if(!_.isEmpty(term)) {
this.$('.blurb').highlight(term.split(/\s+/), {className: 'search-highlight'});
this.$('.topic-title').highlight(term.split(/\s+/), {className: 'search-highlight'} );
}
}
});

View File

@ -1,27 +0,0 @@
/**
This is a text field that supports a dynamic placeholder based on search context.
@class SearchTextField
@extends Discourse.TextField
@namespace Discourse
@module Discourse
**/
import TextField from 'discourse/components/text-field';
export default TextField.extend({
/**
A dynamic placeholder for the search field based on our context
@property placeholder
**/
placeholder: function() {
if(this.get('searchContextEnabled')){
return "";
}
return I18n.t('search.title');
}.property('searchContextEnabled')
});

View File

@ -2,5 +2,11 @@ export default Discourse.View.extend({
tagName: 'div',
classNames: ['d-dropdown'],
elementId: 'search-dropdown',
templateName: 'search'
templateName: 'search',
keyDown: function(e){
var term = this.get('controller.term');
if (e.which === 13 && term && term.length > 2) {
this.get('controller').send('moreOfType', 'topic');
}
}
});

View File

@ -65,6 +65,10 @@ export default Discourse.View.extend(StringBuffer, {
},
expandPinned: function() {
if (this.get('controller.searchTerm')) {
return true;
}
const pinned = this.get('topic.pinned');
if (!pinned) {
return false;
@ -130,6 +134,22 @@ export default Discourse.View.extend(StringBuffer, {
this.set('topic.highlight', false);
this.highlight();
}
var term = this.get('controller.searchTerm');
const self = this;
if (term) {
var terms = term.split(/\s+/);
// .main-link a is omitted cause a bit clowny
var excerpt = self.$('.topic-excerpt');
// some sane wrapping
excerpt.text(excerpt.text().replace(/\S{40,}/g, function(match){
return match.replace(/(\S)/g, "$1\u200B");
}));
terms.forEach(function(word) {
excerpt.highlight(word, {element: 'b', className: 'search-highlight'});
});
}
}.on('didInsertElement')
});

View File

@ -0,0 +1,6 @@
import ModalBodyView from "discourse/views/modal-body";
export default ModalBodyView.extend({
templateName: 'modal/upload-customization',
title: I18n.t('admin.customize.import_title')
});

View File

@ -84,18 +84,31 @@ export default Discourse.View.extend(CleansUp, {
},
_willShow(target) {
const rtl = ($('html').css('direction')) === 'rtl';
if (!target) { return; }
const width = this.$().width();
Ember.run.schedule('afterRender', () => {
if (target) {
let position = target.offset();
if (position) {
position.left += target.width() + 10;
const overage = ($(window).width() - 50) - (position.left + width);
if (overage < 0) {
position.left += overage;
position.top += target.height() + 48;
if (rtl) { // The site direction is rtl
position.right = $(window).width() - position.left + 10;
position.left = 'auto';
let overage = ($(window).width() - 50) - (position.right + width);
if (overage < 0) {
position.right += overage;
position.top += target.height() + 48;
}
} else { // The site direction is ltr
position.left += target.width() + 10;
let overage = ($(window).width() - 50) - (position.left + width);
if (overage < 0) {
position.left += overage;
position.top += target.height() + 48;
}
}
position.top -= $('#main-outlet').offset().top;

View File

@ -45,6 +45,7 @@
//= require ./discourse/views/cloaked
//= require ./discourse/components/combo-box
//= require ./discourse/views/button
//= require ./discourse/components/search-result
//= require ./discourse/components/dropdown-button
//= require ./discourse/components/notifications-button
//= require ./discourse/components/topic-notifications-button

View File

@ -545,6 +545,9 @@ section.details {
.preview-link {
margin-left: 15px;
}
.export {
float: right;
}
padding-left: 10px;
width: 70%;
.style-name {

View File

@ -232,6 +232,13 @@ ol.category-breadcrumb {
h2 {
float: left;
margin: 5px 0 10px;
.top-date-string {
color: scale-color($primary, $lightness: 60%);
font-weight: normal;
font-size: 0.7em;
text-transform: uppercase;
}
}
button {

View File

@ -138,6 +138,55 @@
.raw-email-textarea {
height: 300px;
}
.json-uploader {
.jsfu-shade-container {
display: table-row;
width: 100%;
height: 100%;
position: relative;
}
.jsfu-shade {
z-index: 1;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
.text {
color: rgb(255,255,255);
position: absolute;
top: 40%;
font-size: 36px;
text-align: center;
line-height: 38px;
margin-left: auto;
margin-right: auto;
left: 0;
right: 0;
}
}
.jsfu-file {
display: table-cell;
vertical-align: middle;
min-width: 120px;
}
.jsfu-separator {
vertical-align: middle;
display: table-cell;
font-size: 36px;
padding-left: 10px;
padding-right: 10px;
}
.jsfu-paste {
display: table-cell;
width: 100%;
textarea {
margin-bottom: 0;
margin-top: 4px;
}
}
}
}
.password-confirmation {
display: none;

View File

@ -160,7 +160,7 @@ aside.quote {
pre {
code {
white-space: pre-wrap;
word-wrap: normal;
display: block;
padding: 5px 10px;
color: $primary;
@ -197,3 +197,39 @@ blockquote > *:first-child {
blockquote > *:last-child {
margin-bottom: 0 !important;
}
.gap {
background-color: dark-light-diff($primary, $secondary, 90%, -60%);
padding: 5px 0;
color: lighten($primary, 60%);
cursor: pointer;
text-align: center;
&.jagged-border {
background-image:
linear-gradient(
135deg,
$secondary 50%, rgba(255,255,255,0) 50%
),
linear-gradient(
-135deg,
$secondary 50%, rgba(255,255,255,0) 50%
),
linear-gradient(
45deg,
$secondary 50%, rgba(255,255,255,0) 50%
),
linear-gradient(
-45deg,
$secondary 50%, rgba(255,255,255,0) 50%
);
background-position:
top center, top center,
bottom center, bottom center;
background-size: .9em .9em;
background-repeat: repeat-x;
padding: 20px 0;
margin-bottom: 20px;
}
}

View File

@ -171,12 +171,12 @@
}
li.bar>.badge-category {
background: dark-light-diff($primary, $secondary, 95%, -65%) !important;
background: dark-light-diff($primary, $secondary, 90%, -65%) !important;
color: $primary !important;
}
li.bullet>.badge-category {
background: dark-light-diff($primary, $secondary, 95%, -65%) !important;
background: dark-light-diff($primary, $secondary, 90%, -65%) !important;
color: $primary !important;
.badge-category-bg {

View File

@ -59,7 +59,7 @@
li {
font-weight: normal;
margin-top: 3px;
margin-top: 8px;
}
}
}
@ -74,6 +74,17 @@
.posts-count {
background-color: scale-color($tertiary, $lightness: -40%);
}
ul {
list-style: none;
margin: 0;
padding: 0;
}
.search-link {
.fa, .blurb {
color: scale-color($tertiary, $lightness: -40%);
}
}
}
.composer-popup:nth-of-type(2) {
@ -288,6 +299,7 @@
}
.submit-panel {
width: 28%;
position: absolute;
display: block;
bottom: 8px;

View File

@ -54,6 +54,7 @@ and (max-width : 570px) {
display: block;
word-wrap: break-word;
font-size: 11px;
line-height: 1.3em;
.search-highlight {
color: scale-color($primary, $lightness: 25%);
}

View File

@ -242,7 +242,10 @@
// Misc. stuff
// --------------------------------------------------
#list-area .top-lists h2 { margin: 5px 0 10px; }
#list-area .top-lists h2 {
cursor: pointer;
margin: 5px 0 10px;
}
#list-area {
h2 {

View File

@ -97,6 +97,7 @@ nav.post-controls {
.show-replies {
margin-left: 0;
font-size: inherit;
span.badge-posts {color: scale-color($primary, $lightness: 60%);}
&:hover {
background: dark-light-diff($primary, $secondary, 90%, -65%);
@ -104,6 +105,7 @@ nav.post-controls {
}
i {
margin-left: 5px;
font-size: 90%;
}
}
@ -563,6 +565,7 @@ a.mention {
padding: 2px 4px;
color: $primary;
background: dark-light-diff($primary, $secondary, 90%, -60%);
border-radius: 8px;
}
@ -709,11 +712,6 @@ $topic-avatar-width: 45px;
}
.gap {
background-color: dark-light-diff($primary, $secondary, 90%, -60%);
padding: 5px 0;
color: lighten($primary, 30%);
cursor: pointer;
text-align: center;
width: calc(#{$topic-avatar-width} + #{$topic-body-width} + 2 * #{$topic-body-width-padding});
}

View File

@ -6,6 +6,9 @@
// --------------------------------------------------
.list-controls {
.category-breadcrumb.hidden {
display: none;
}
margin: 5px;
.nav {
float: left;
@ -17,6 +20,37 @@
.btn {
float: right;
margin-left: 8px;
margin-top: 5px;
}
.nav-pills {
position: relative;
}
.nav-pills .drop {
border: 1px solid dark-light-diff($primary, $secondary, 90%, -65%);
position: absolute;
z-index: 1000;
background-color: $secondary;
padding: 0 10px 10px 10px;
width: 150px;
top: 100%;
margin: 0;
li {
list-style-type: none;
margin-left: 0;
margin-top: 5px;
padding-top: 10px;
a {
width: 100%;
display: inline-block;
}
}
}
.nav-pills > li {
background: dark-light-diff($primary, $secondary, 90%, -65%);
i.fa-caret-down {
margin-left: 8px;
}
}
}

View File

@ -1,17 +1,14 @@
.gap {
background-color: dark-light-diff($primary, $secondary, 90%, -60%);
padding: 5px 15px;
color: $primary;
text-align: center;
margin: 10px 0;
/* may not need this */
}
.time-gap {
border-bottom: 1px solid dark-light-diff($primary, $secondary, 90%, -60%);
color: lighten($primary, 50%);
padding-bottom: 3px;
margin-bottom: 10px;
text-transform: uppercase;
font-weight: bold;
.topic-avatar {
margin: 0 5px 0 10px;
}
@ -28,9 +25,6 @@ span.badge-posts {
.show-replies {
display: none;
i {
margin-left: 5px;
}
}
nav.post-controls {

View File

@ -13,7 +13,12 @@ class Admin::ReportsController < Admin::AdminController
end_date = start_date + 1.month
end_date = Time.parse(params[:end_date]) if params[:end_date].present?
report = Report.find(report_type, {start_date: start_date, end_date: end_date})
category_id = if params.has_key?(:category_id)
params[:category_id].blank? ? SiteSetting.uncategorized_category_id : params[:category_id].to_i
end
report = Report.find(report_type, start_date: start_date, end_date: end_date, category_id: category_id)
raise Discourse::NotFound if report.blank?
render_json_dump(report: report)

View File

@ -2,6 +2,8 @@ class Admin::SiteCustomizationsController < Admin::AdminController
before_filter :enable_customization
skip_before_filter :check_xhr, only: [:show]
def index
@site_customizations = SiteCustomization.order(:name)
@ -48,6 +50,26 @@ class Admin::SiteCustomizationsController < Admin::AdminController
end
end
def show
@site_customization = SiteCustomization.find(params[:id])
respond_to do |format|
format.json do
check_xhr
render json: SiteCustomizationSerializer.new(@site_customization)
end
format.any(:html, :text) do
raise RenderEmpty.new if request.xhr?
response.headers['Content-Disposition'] = "attachment; filename=#{@site_customization.name.parameterize}.dcstyle.json"
response.sending_file = true
render json: SiteCustomizationSerializer.new(@site_customization)
end
end
end
private
def site_customization_params

View File

@ -269,7 +269,8 @@ class ApplicationController < ActionController::Base
find_opts[:active] = true unless opts[:include_inactive]
User.find_by(find_opts)
elsif params[:external_id]
SingleSignOnRecord.find_by(external_id: params[:external_id]).try(:user)
external_id = params[:external_id].gsub(/\.json$/, '')
SingleSignOnRecord.find_by(external_id: external_id).try(:user)
end
raise Discourse::NotFound if user.blank?

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