Version bump

This commit is contained in:
Neil Lalonde 2015-04-13 15:00:51 -04:00
commit 8c3d3060d1
335 changed files with 11037 additions and 4898 deletions

View File

@ -14,7 +14,6 @@
"moduleForComponent",
"Pretender",
"sandbox",
"integration",
"controllerFor",
"test",
"ok",

View File

@ -272,7 +272,7 @@ GEM
omniauth-twitter (1.0.1)
multi_json (~> 1.3)
omniauth-oauth (~> 1.0)
onebox (1.5.13)
onebox (1.5.16)
moneta (~> 0.7)
multi_json (~> 1.7)
mustache (~> 0.99)

View File

@ -10,12 +10,12 @@ To learn more about the philosophy and goals of the project, [visit **discourse.
## Screenshots
[![](https://raw.githubusercontent.com/discourse/discourse-docimages/master/readme/boing-boing-latest-small2.png)](http://bbs.boingboing.net)
[![](https://raw.githubusercontent.com/discourse/discourse-docimages/master/readme/how-to-geek-profile-small2.png)](http://discuss.howtogeek.com)
[![](https://raw.githubusercontent.com/discourse/discourse-docimages/master/readme/new-relic-categories-small2.png)](http://discuss.newrelic.com)
[![](https://raw.githubusercontent.com/discourse/discourse-docimages/master/readme/turtle-rock-topic-small2.jpg)](https://talk.turtlerockstudios.com/)
[![](https://raw.githubusercontent.com/discourse/discourse-docimages/master/readme/nexus-7-mobile-discourse-small3.png)](http://discuss.atom.io)
[![](https://raw.githubusercontent.com/discourse/discourse-docimages/master/readme/iphone-5s-mobile-discourse-small4.png)](http://discourse.soylent.me)
[![](http://www.discourse.org/images/readme/boingboing-1-3-beta.png)](http://bbs.boingboing.net)
[![](http://www.discourse.org/images/readme/howtogeek-1-3-beta.png)](http://discuss.howtogeek.com)
[![](http://www.discourse.org/images/readme/newrelic-1-3-beta.png)](http://discuss.newrelic.com)
[![](http://www.discourse.org/images/readme/turtlerock-1-3-beta.png)](https://talk.turtlerockstudios.com/)
[![](http://www.discourse.org/images/readme/android-tablet-discourse-1-3-beta.png?v=3)](http://discuss.atom.io)
[![](http://www.discourse.org/images/readme/iphone-6-discourse-1-3-beta.png?v=3)](http://discourse.soylent.me)
Browse [lots more notable Discourse instances](http://www.discourse.org/faq/customers/).

Binary file not shown.

BIN
app/assets/fonts/fontawesome-webfont.eot Executable file → Normal file

Binary file not shown.

989
app/assets/fonts/fontawesome-webfont.svg Executable file → Normal file

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 248 KiB

After

Width:  |  Height:  |  Size: 306 KiB

BIN
app/assets/fonts/fontawesome-webfont.ttf Executable file → Normal file

Binary file not shown.

BIN
app/assets/fonts/fontawesome-webfont.woff Executable file → Normal file

Binary file not shown.

Binary file not shown.

View File

@ -21,7 +21,8 @@ export default Ember.Component.extend({
tokenSeparators: ["|"],
tags : this.get("choices") || [],
width: 'off',
dropdownCss: this.get("choices") ? {} : {display: 'none'}
dropdownCss: this.get("choices") ? {} : {display: 'none'},
selectOnBlur: this.get("choices") ? false : true
};
var settingName = this.get('settingName');

View File

@ -14,6 +14,7 @@ Discourse.ResumableUploadComponent = Ember.Component.extend(Discourse.StringBuff
tagName: "button",
classNames: ["btn", "ru"],
classNameBindings: ["isUploading"],
attributeBindings: ["translatedTitle:title"],
resumable: null,
@ -22,6 +23,11 @@ Discourse.ResumableUploadComponent = Ember.Component.extend(Discourse.StringBuff
rerenderTriggers: ['isUploading', 'progress'],
translatedTitle: function() {
const title = this.get('title');
return title ? I18n.t(title) : this.get('text');
}.property('title', 'text'),
text: function() {
if (this.get("isUploading")) {
return this.get("progress") + " %";

View File

@ -1,49 +1,24 @@
export default Ember.ArrayController.extend({
needs: ["adminBackups"],
status: Em.computed.alias("controllers.adminBackups"),
uploadText: function() { return I18n.t("admin.backups.upload.text"); }.property(),
readOnlyModeDisabled: Em.computed.alias("status.isOperationRunning"),
isOperationRunning: Em.computed.alias("status.isOperationRunning"),
restoreDisabled: Em.computed.alias("status.restoreDisabled"),
uploadLabel: function() { return I18n.t("admin.backups.upload.label"); }.property(),
restoreTitle: function() {
if (!this.get('status.allowRestore')) {
return I18n.t("admin.backups.operations.restore.is_disabled");
return "admin.backups.operations.restore.is_disabled";
} else if (this.get("status.isOperationRunning")) {
return I18n.t("admin.backups.operation_already_running");
return "admin.backups.operations.is_running";
} else {
return I18n.t("admin.backups.operations.restore.title");
return "admin.backups.operations.restore.title";
}
}.property("status.isOperationRunning"),
destroyDisabled: Em.computed.alias("status.isOperationRunning"),
destroyTitle: function() {
if (this.get("status.isOperationRunning")) {
return I18n.t("admin.backups.operation_already_running");
} else {
return I18n.t("admin.backups.operations.destroy.title");
}
}.property("status.isOperationRunning"),
readOnlyModeTitle: function() { return this._readOnlyModeI18n("title"); }.property("site.isReadOnly"),
readOnlyModeText: function() { return this._readOnlyModeI18n("text"); }.property("site.isReadOnly"),
_readOnlyModeI18n: function(value) {
var action = this.site.get("isReadOnly") ? "disable" : "enable";
return I18n.t("admin.backups.read_only." + action + "." + value);
},
}.property("status.{allowRestore,isOperationRunning}"),
actions: {
/**
Toggle read-only mode
@method toggleReadOnlyMode
**/
toggleReadOnlyMode: function() {
toggleReadOnlyMode() {
var self = this;
if (!this.site.get("isReadOnly")) {
bootbox.confirm(
@ -64,7 +39,7 @@ export default Ember.ArrayController.extend({
},
_toggleReadOnlyMode: function(enable) {
_toggleReadOnlyMode(enable) {
var site = this.site;
Discourse.ajax("/admin/backups/readonly", {
type: "PUT",

View File

@ -25,31 +25,31 @@ export default Em.ObjectController.extend({
}.property(),
actions: {
next: function() {
next() {
if (this.get("showingLast")) { return; }
var group = this.get("model"),
offset = Math.min(group.get("offset") + group.get("limit"), group.get("user_count"));
const group = this.get("model"),
offset = Math.min(group.get("offset") + group.get("limit"), group.get("user_count"));
group.set("offset", offset);
return group.findMembers();
},
previous: function() {
previous() {
if (this.get("showingFirst")) { return; }
var group = this.get("model"),
offset = Math.max(group.get("offset") - group.get("limit"), 0);
const group = this.get("model"),
offset = Math.max(group.get("offset") - group.get("limit"), 0);
group.set("offset", offset);
return group.findMembers();
},
removeMember: function(member) {
var self = this,
message = I18n.t("admin.groups.delete_member_confirm", { username: member.get("username"), group: this.get("name") });
removeMember(member) {
const self = this,
message = I18n.t("admin.groups.delete_member_confirm", { username: member.get("username"), group: this.get("name") });
return bootbox.confirm(message, I18n.t("no_value"), I18n.t("yes_value"), function(confirm) {
if (confirm) {
self.get("model").removeMember(member);
@ -57,57 +57,49 @@ export default Em.ObjectController.extend({
});
},
addMembers: function() {
addMembers() {
if (Em.isEmpty(this.get("usernames"))) { return; }
this.get("model").addMembers(this.get("usernames"));
// clear the user selector
this.set("usernames", null);
},
save: function() {
var self = this,
group = this.get('model'),
groupsController = this.get("controllers.adminGroupsType");
save() {
const group = this.get('model'),
groupsController = this.get("controllers.adminGroupsType");
this.set('disableSave', true);
var promise;
if (group.get("id")) {
promise = group.save();
} else {
promise = group.create().then(function() {
groupsController.addObject(group);
});
}
promise.then(function() {
self.transitionToRoute("adminGroup", group);
}, function(e) {
var message = $.parseJSON(e.responseText).errors;
bootbox.alert(message);
}).finally(function() {
self.set('disableSave', false);
});
let promise = group.get("id") ? group.save() : group.create().then(() => groupsController.addObject(group));
promise.then(() => this.transitionToRoute("adminGroup", group))
.catch(e => bootbox.alert($.parseJSON(e.responseText).errors))
.finally(() => this.set('disableSave', false));
},
destroy: function() {
var group = this.get('model'),
groupsController = this.get('controllers.adminGroupsType'),
self = this;
destroy() {
const group = this.get('model'),
groupsController = this.get('controllers.adminGroupsType'),
self = this;
this.set('disableSave', true);
bootbox.confirm(I18n.t("admin.groups.delete_confirm"), I18n.t("no_value"), I18n.t("yes_value"), function(confirmed) {
if (confirmed) {
group.destroy().then(function() {
groupsController.get('model').removeObject(group);
self.transitionToRoute('adminGroups.index');
}, function() {
bootbox.alert(I18n.t("admin.groups.delete_failed"));
}).finally(function() {
bootbox.confirm(
I18n.t("admin.groups.delete_confirm"),
I18n.t("no_value"),
I18n.t("yes_value"),
function(confirmed) {
if (confirmed) {
group.destroy().then(() => {
groupsController.get('model').removeObject(group);
self.transitionToRoute('adminGroups.index');
}).catch(() => bootbox.alert(I18n.t("admin.groups.delete_failed")))
.finally(() => self.set('disableSave', false));
} else {
self.set('disableSave', false);
});
}
}
});
);
}
}
});

View File

@ -1,6 +1,9 @@
export default Ember.ArrayController.extend({
sortProperties: ['name'],
refreshingAutoGroups: false,
isAuto: function(){
return this.get('type') === 'automatic';
}.property('type'),
actions: {
refreshAutoGroups: function(){

View File

@ -1,10 +1,16 @@
export default Discourse.Route.extend({
model(params) {
this.set("type", params.type);
return Discourse.Group.findAll().then(function(groups) {
return groups.filterBy("type", params.type);
});
},
setupController(controller, model){
controller.set("type", this.get("type"));
controller.set("model", model);
},
actions: {
newGroup() {
const self = this;

View File

@ -9,7 +9,7 @@
{{#if canRollback}}
{{d-button action="rollback"
class="btn-rollback"
label="admin.backups.operations.rollback.text"
label="admin.backups.operations.rollback.label"
title="admin.backups.operations.rollback.title"
icon="ambulance"
disabled=rollbackDisabled}}
@ -18,13 +18,13 @@
{{d-button action="cancelOperation"
class="btn-danger"
title="admin.backups.operations.cancel.title"
label="admin.backups.operations.cancel.text"
label="admin.backups.operations.cancel.label"
icon="times"}}
{{else}}
{{d-button action="startBackup"
class="btn-primary"
title="admin.backups.operations.backup.title"
label="admin.backups.operations.backup.text"
label="admin.backups.operations.backup.label"
icon="rocket"}}
{{/if}}
</div>

View File

@ -4,8 +4,12 @@
<th width="10%">{{i18n 'admin.backups.columns.size'}}</th>
<th>
<div class="pull-right">
{{resumable-upload target="/admin/backups/upload" success="uploadSuccess" error="uploadError" uploadText=uploadText}}
<button {{action "toggleReadOnlyMode"}} class="btn" {{bind-attr disabled="readOnlyModeDisabled" title="readOnlyModeTitle"}}><i class="fa fa-eye"></i>{{readOnlyModeText}}</button>
{{resumable-upload target="/admin/backups/upload" success="uploadSuccess" error="uploadError" uploadText=uploadLabel title="admin.backups.upload.title"}}
{{#if site.isReadOnly}}
{{d-button icon="eye" action="toggleReadOnlyMode" disabled=isOperationRunning title="admin.backups.read_only.disable.title" label="admin.backups.read_only.disable.label"}}
{{else}}
{{d-button icon="eye" action="toggleReadOnlyMode" disabled=isOperationRunning title="admin.backups.read_only.enable.title" label="admin.backups.read_only.enable.label"}}
{{/if}}
</div>
</th>
</tr>
@ -15,9 +19,14 @@
<td>{{human-size backup.size}}</td>
<td>
<div class="pull-right">
<a {{bind-attr href="backup.link"}} class="btn download" title="{{i18n 'admin.backups.operations.download.title'}}"><i class="fa fa-download"></i>{{i18n 'admin.backups.operations.download.text'}}</a>
<button {{action "destroyBackup" backup}} class="btn btn-danger no-text" {{bind-attr disabled="destroyDisabled" title="destroyTitle"}}><i class="fa fa-trash-o"></i></button>
<button {{action "startRestore" backup}} class="btn" {{bind-attr disabled="restoreDisabled" title="restoreTitle"}}><i class="fa fa-play"></i>{{i18n 'admin.backups.operations.restore.text'}}</button>
<a {{bind-attr href="backup.link"}} class="btn download" title="{{i18n 'admin.backups.operations.download.title'}}">{{fa-icon "download"}}{{i18n 'admin.backups.operations.download.label'}}</a>
{{#if isOperationRunning}}
{{d-button icon="trash-o" action="destroyBackup" actionParam=backup class="btn-danger no-text" disabled="true" title="admin.backups.operations.is_running"}}
{{d-button icon="play" action="startRestore" actionParam=backup disabled=restoreDisabled title=restoreTitle label="admin.backups.operations.restore.label"}}
{{else}}
{{d-button icon="trash-o" action="destroyBackup" actionParam=backup class="btn-danger no-text" title="admin.backups.operations.destroy.title"}}
{{d-button icon="play" action="startRestore" actionParam=backup disabled=restoreDisabled title=restoreTitle label="admin.backups.operations.restore.label"}}
{{/if}}
</div>
</td>
</tr>

View File

@ -33,13 +33,15 @@
<li class='toggle-mobile'>
<a {{bind-attr class="view.mobile:active"}} {{action "toggleMobile" target="view"}}>{{fa-icon "mobile"}}</a>
</li>
<li class='toggle-maximize'><a {{action "toggleMaximize" target="view"}}>
{{#if view.maximized}}
{{fa-icon "compress"}}
{{else}}
{{fa-icon "expand"}}
{{/if}}
</a>
<li class='toggle-maximize'>
<a {{action "toggleMaximize" target="view"}}>
{{#if view.maximized}}
{{fa-icon "compress"}}
{{else}}
{{fa-icon "expand"}}
{{/if}}
</a>
</li>
</ul>
</div>

View File

@ -38,6 +38,15 @@
</label>
</div>
{{#unless automatic}}
<div>
<label for="primary_group">
{{input type="checkbox" checked=primary_group}}
{{i18n 'admin.groups.primary_group'}}
</label>
</div>
{{/unless}}
<div>
<label for="alias">{{i18n 'groups.alias_levels.title'}}</label>
{{combo-box name="alias" valueAttribute="value" value=alias_level content=aliasLevelOptions}}
@ -52,6 +61,13 @@
{{i18n 'admin.groups.automatic_membership_retroactive'}}
</label>
</div>
<div>
<label for="title">
{{i18n 'admin.groups.default_title'}}
</label>
{{input value=title}}
</div>
{{/unless}}
<div class='buttons'>

View File

@ -1 +1 @@
{{avatar member imageSize="small"}} {{member.username}} {{#unless automatic}}<a class='remove' {{action "removeMember" member}}>{{fa-icon "times"}}</a>{{/unless}}
<a href='{{unbound member.adminPath}}'>{{avatar member imageSize="small"}}</a> {{member.username}} {{#unless automatic}}<a class='remove' {{action "removeMember" member}}>{{fa-icon "times"}}</a>{{/unless}}

View File

@ -1,6 +1,6 @@
{{#admin-nav}}
{{admin-nav-item route='adminGroupsType' routeParam='custom' label='admin.groups.custom.label'}}
{{admin-nav-item route='adminGroupsType' routeParam='automatic' label='admin.groups.automatic.label'}}
{{admin-nav-item route='adminGroupsType' routeParam='custom' label='admin.groups.custom'}}
{{admin-nav-item route='adminGroupsType' routeParam='automatic' label='admin.groups.automatic'}}
{{/admin-nav}}
<div class="admin-container">

View File

@ -10,7 +10,9 @@
</ul>
<div class='controls'>
{{d-button action="newGroup" icon="plus" label="admin.groups.new"}}
{{#if controller.isAuto}}
{{d-button action="refreshAutoGroups" icon="refresh" label="admin.groups.refresh" disabled=refreshingAutoGroups}}
{{/if}}
</div>
</div>

View File

@ -2,7 +2,7 @@
{{i18n 'admin.logs.screened_emails.description'}}
<button class="btn pull-right" {{action "exportScreenedEmailList"}} title="{{i18n 'admin.export_csv.button_title.screened_email'}}">{{fa-icon "download"}}{{i18n 'admin.export_csv.button_text'}}</button>
</p>
</br>
<br>
{{#loading-spinner condition=loading}}
{{#if model.length}}

View File

@ -2,7 +2,7 @@
{{i18n 'admin.logs.screened_urls.description'}}
<button class="btn pull-right" {{action "exportScreenedUrlList"}} title="{{i18n 'admin.export_csv.button_title.screened_url'}}">{{fa-icon "download"}}{{i18n 'admin.export_csv.button_text'}}</button>
</p>
</br>
<br>
{{#loading-spinner condition=loading}}
{{#if model.length}}

View File

@ -31,7 +31,7 @@
<div class="pull-right">
{{d-button action="exportStaffActionLogs" label="admin.export_csv.button_text" icon="download"}}
</div>
</br>
<br>
<div class="staff-action-logs-instructions" {{bind-attr class=":staff-action-logs-instructions showInstructions::invisible"}}>
{{i18n 'admin.logs.staff_actions.instructions'}}

View File

@ -83,29 +83,6 @@
</div>
</div>
{{#if currentUser.admin}}
<div class='display-row'>
<div class='field'>{{i18n 'admin.groups.automatic.title'}}</div>
<div class='value'>{{automaticGroups}}</div>
</div>
<div class='display-row'>
<div class='field'>{{i18n 'admin.groups.custom.title'}}</div>
<div class='value'>
{{admin-group-selector selected=customGroups available=availableGroups}}
</div>
<div class='controls'>
{{#if customGroups}}
{{i18n 'admin.groups.primary'}}
{{combo-box content=customGroups value=primary_group_id nameProperty="name" none="admin.groups.no_primary"}}
{{/if}}
{{#if primaryGroupDirty}}
{{d-button icon="check" class="ok no-text" action="savePrimaryGroup"}}
{{d-button icon="times" class="cancel no-text" action="resetPrimaryGroup"}}
{{/if}}
</div>
</div>
{{/if}}
<div class='display-row'>
<div class='field'>{{i18n 'user.ip_address.title'}}</div>
<div class='value'>{{ip_address}}</div>
@ -159,7 +136,6 @@
</section>
{{/if}}
<section class='details'>
<h1>{{i18n 'admin.user.permissions'}}</h1>
@ -355,6 +331,33 @@
</div>
</section>
<section class='details'>
<h1>{{i18n 'admin.groups.title'}}</h1>
{{#if currentUser.admin}}
<div class='display-row'>
<div class='field'>{{i18n 'admin.groups.automatic'}}</div>
<div class='value'>{{automaticGroups}}</div>
</div>
<div class='display-row'>
<div class='field'>{{i18n 'admin.groups.custom'}}</div>
<div class='value'>
{{admin-group-selector selected=customGroups available=availableGroups}}
</div>
<div class='controls'>
{{#if customGroups}}
{{i18n 'admin.groups.primary'}}
{{combo-box content=customGroups value=primary_group_id nameProperty="name" none="admin.groups.no_primary"}}
{{/if}}
{{#if primaryGroupDirty}}
{{d-button icon="check" class="ok no-text" action="savePrimaryGroup"}}
{{d-button icon="times" class="cancel no-text" action="resetPrimaryGroup"}}
{{/if}}
</div>
</div>
{{/if}}
</section>
<section class='details'>
<h1>{{i18n 'admin.user.activity'}}</h1>

View File

@ -13,7 +13,7 @@
</ul>
</div>
<div class="pull-right">
{{d-button action="sendInvites" title="admin.invite.button_title" icon="envelope" label="admin.invite.button_text"}}
{{d-button action="sendInvites" title="admin.invite.button_title" icon="user-plus" label="admin.invite.button_text"}}
{{d-button action="exportUsers" title="admin.export_csv.button_title.user" icon="download" label="admin.export_csv.button_text"}}
</div>
</div>

View File

@ -4,7 +4,7 @@ export default Ember.Object.extend({
pathFor(store, type, findArgs) {
let path = "/" + Ember.String.underscore(store.pluralize(type));
if (ADMIN_MODELS.indexOf(type) !== -1) { path = "/admin/" + path; }
if (ADMIN_MODELS.indexOf(type) !== -1) { path = "/admin" + path; }
if (findArgs) {
if (typeof findArgs === "object") {

View File

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

View File

@ -6,11 +6,10 @@ export default Ember.TextArea.extend({
}.property('placeholderKey'),
_signalParentInsert: function() {
return this.get('parentView').childDidInsertElement(this);
this.get('parentView').childDidInsertElement(this);
}.on('didInsertElement'),
_signalParentDestroy: function() {
return this.get('parentView').childWillDestroyElement(this);
this.get('parentView').childWillDestroyElement(this);
}.on('willDestroyElement')
});

View File

@ -4,8 +4,14 @@ import { iconHTML } from 'discourse/helpers/fa-icon';
export default Ember.Component.extend(StringBuffer, {
tagName: 'th',
classNames: ['sortable'],
attributeBindings: ['title'],
rerenderTriggers: ['order', 'asc'],
title: function() {
const labelKey = 'directory.' + this.get('field');
return I18n.t(labelKey + '_long', { defaultValue: I18n.t(labelKey) });
}.property('field'),
renderString(buffer) {
const icon = this.get('icon');

View File

@ -4,18 +4,22 @@ export default Ember.Component.extend(StringBuffer, {
rerenderTriggers: ['site.isReadOnly'],
renderString: function(buffer) {
var notices = [];
let notices = [];
if (this.site.get("isReadOnly")) {
notices.push(I18n.t("read_only_mode.enabled"));
notices.push([I18n.t("read_only_mode.enabled"), 'alert-read-only']);
}
if (this.siteSettings.disable_emails) {
notices.push(I18n.t("emails_are_disabled"));
notices.push([I18n.t("emails_are_disabled"), 'alert-emails-disabled']);
}
if (this.siteSettings.enable_s3_uploads) {
notices.push([I18n.t("s3_deprecation_warning"), 'alert-s3-deprecation']);
}
if (Discourse.User.currentProp('admin') && this.siteSettings.show_create_topics_notice) {
var topic_count = 0,
let topic_count = 0,
post_count = 0;
_.each(this.site.get('categories'), function(c) {
if (!c.get('read_restricted')) {
@ -24,18 +28,16 @@ export default Ember.Component.extend(StringBuffer, {
}
});
if (topic_count < 5 || post_count < this.siteSettings.tl1_requires_read_posts) {
notices.push(I18n.t("too_few_topics_notice", {posts: this.siteSettings.tl1_requires_read_posts}));
notices.push([I18n.t("too_few_topics_notice", { posts: this.siteSettings.tl1_requires_read_posts }), 'alert-too-few-topics']);
}
}
if (!_.isEmpty(this.siteSettings.global_notice)) {
notices.push(this.siteSettings.global_notice);
notices.push([this.siteSettings.global_notice, 'alert-global-notice']);
}
if (notices.length > 0) {
buffer.push(_.map(notices, function(text) {
return "<div class='row'><div class='alert alert-info'>" + text + "</div></div>";
}).join(""));
buffer.push(_.map(notices, n => "<div class='row'><div class='alert alert-info " + n[1] + "'>" + n[0] + "</div></div>").join(""));
}
}
});

View File

@ -10,14 +10,14 @@ export default Ember.Component.extend(StringBuffer, {
title: function() {
var categoryName = this.get('content.categoryName'),
name = this.get('content.name'),
extra;
extra = {};
if (categoryName) {
extra = { categoryName: categoryName };
name = "category";
extra.categoryName = categoryName;
}
return I18n.t("filters." + name + ".help", extra);
}.property("content.name"),
return I18n.t("filters." + name.replace("/", ".") + ".help", extra);
}.property("content.{categoryName,name}"),
active: function() {
return this.get('content.filterMode') === this.get('filterMode') ||
@ -33,11 +33,11 @@ export default Ember.Component.extend(StringBuffer, {
name = 'category';
extra.categoryName = Discourse.Formatter.toTitleCase(categoryName);
}
return I18n.t("filters." + name + ".title", extra);
}.property('content.count'),
return I18n.t("filters." + name.replace("/", ".") + ".title", extra);
}.property('content.{categoryName,name,count}'),
renderString: function(buffer) {
var content = this.get('content');
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>");

View File

@ -1,3 +1,4 @@
import { iconHTML } from 'discourse/helpers/fa-icon';
import StringBuffer from 'discourse/mixins/string-buffer';
export default Ember.Component.extend(StringBuffer, {
@ -5,14 +6,16 @@ export default Ember.Component.extend(StringBuffer, {
rerenderTriggers: ['topic.archived', 'topic.closed', 'topic.pinned', 'topic.visible', 'topic.unpinned', 'topic.is_warning'],
click: function() {
var topic = this.get('topic');
click(e) {
if ($(e.target).hasClass('fa-thumb-tack')) {
const topic = this.get('topic');
// only pin unpin for now
if (topic.get('pinned')) {
topic.clearPin();
} else {
topic.rePin();
// only pin unpin for now
if (topic.get('pinned')) {
topic.clearPin();
} else {
topic.rePin();
}
}
return false;
@ -22,24 +25,26 @@ export default Ember.Component.extend(StringBuffer, {
return Discourse.User.current() && !this.get('disableActions');
}.property('disableActions'),
renderString: function(buffer) {
renderString(buffer) {
const self = this;
var self = this;
var renderIconIf = function(conditionProp, name, key, actionable) {
const renderIconIf = function(conditionProp, name, key, actionable) {
if (!self.get(conditionProp)) { return; }
var title = Handlebars.Utils.escapeExpression(I18n.t("topic_statuses." + key + ".help"));
var startTag = actionable ? "a href" : "span";
var endTag = actionable ? "a" : "span";
buffer.push("<" + startTag + " title='" + title + "' class='topic-status'><i class='fa fa-" + name + "'></i></" + endTag + ">");
const title = Handlebars.Utils.escapeExpression(I18n.t("topic_statuses." + key + ".help")),
startTag = actionable ? "a href" : "span",
endTag = actionable ? "a" : "span",
iconArgs = key === 'unpinned' ? { 'class': 'unpinned' } : null,
icon = iconHTML(name, iconArgs);
buffer.push("<" + startTag + " title='" + title + "' class='topic-status'>" + icon + "</" + endTag + ">");
};
renderIconIf('topic.is_warning', 'envelope', 'warning');
renderIconIf('topic.closed', 'lock', 'locked');
renderIconIf('topic.archived', 'lock', 'archived');
renderIconIf('topic.pinned', 'thumb-tack', 'pinned', self.get("canAct") );
renderIconIf('topic.unpinned', 'thumb-tack unpinned', 'unpinned', self.get("canAct"));
renderIconIf('topic.pinned', 'thumb-tack', 'pinned', this.get("canAct") );
renderIconIf('topic.unpinned', 'thumb-tack', 'unpinned', this.get("canAct"));
renderIconIf('topic.invisible', 'eye-slash', 'invisible');
}
});

View File

@ -57,6 +57,10 @@ export default TextField.extend({
});
}.on('didInsertElement'),
_removeAutocomplete: function() {
this.$().autocomplete('destroy');
}.on('willDestroyElement'),
// THIS IS A HUGE HACK TO SUPPORT CLEARING THE INPUT
_clearInput: function() {
if (arguments.length > 1) {

View File

@ -1,7 +1,7 @@
export default Ember.Component.extend({
classNames: ['user-small'],
userPath: Discourse.computed.url('username', '/users/%@'),
userPath: Discourse.computed.url('user.username', '/users/%@'),
name: function() {
const name = this.get('user.name');

View File

@ -3,16 +3,21 @@ import DiscourseController from 'discourse/controllers/controller';
export default DiscourseController.extend(ModalFunctionality, {
uploadedAvatarTemplate: null,
saveDisabled: Em.computed.alias("uploading"),
hasUploadedAvatar: Em.computed.or('uploadedAvatarTemplate', 'custom_avatar_upload_id'),
selectedUploadId: function() {
switch (this.get("selected")) {
case "system": return this.get("system_avatar_upload_id");
case "gravatar": return this.get("gravatar_avatar_upload_id");
default: return this.get("custom_avatar_upload_id");
}
switch (this.get("selected")) {
case "system": return this.get("system_avatar_upload_id");
case "gravatar": return this.get("gravatar_avatar_upload_id");
default: return this.get("custom_avatar_upload_id");
}
}.property('selected', 'system_avatar_upload_id', 'gravatar_avatar_upload_id', 'custom_avatar_upload_id'),
allowImageUpload: function() {
return Discourse.Utilities.allowsImages();
}.property(),
actions: {
useUploadedAvatar() { this.set("selected", "uploaded"); },
useGravatar() { this.set("selected", "gravatar"); },

View File

@ -348,7 +348,7 @@ export default DiscourseController.extend({
// If we show the subcategory list, scope the categories drop down to
// the category we opened the composer with.
if (Discourse.SiteSettings.show_subcategory_list) {
if (this.siteSettings.show_subcategory_list && opts.draftKey !== 'reply_as_new_topic') {
this.set('scopedCategoryId', opts.categoryId);
}

View File

@ -69,12 +69,20 @@ export default DiscourseController.extend(ModalFunctionality, {
return I18n.t('user.password.instructions', {count: Discourse.SiteSettings.min_password_length});
}.property(),
// Validate the name. It's not required.
nameInstructions: function() {
return I18n.t(Discourse.SiteSettings.full_name_required ? 'user.name.instructions_required' : 'user.name.instructions');
}.property(),
// Validate the name.
nameValidation: function() {
if (this.get('accountPasswordConfirm') === 0) {
this.fetchConfirmationValue();
}
if (Discourse.SiteSettings.full_name_required && this.blank('accountName')) {
return Discourse.InputValidation.create({ failed: true });
}
return Discourse.InputValidation.create({ok: true});
}.property('accountName'),

View File

@ -1,47 +0,0 @@
import ModalFunctionality from 'discourse/mixins/modal-functionality';
import ObjectController from 'discourse/controllers/object';
export default ObjectController.extend(ModalFunctionality, {
modalClass: 'invite',
isAdmin: function(){
return Discourse.User.currentProp("admin");
}.property(),
onShow: function(){
this.set('controllers.modal.modalClass', 'invite-modal');
this.set('emailOrUsername', '');
},
disabled: function() {
if (this.get('saving')) return true;
return this.blank('emailOrUsername');
}.property('emailOrUsername', 'saving'),
buttonTitle: function() {
if (this.get('saving')) return I18n.t('topic.inviting');
return I18n.t('topic.invite_private.action');
}.property('saving'),
actions: {
invite: function() {
if (this.get('disabled')) return;
var self = this;
this.setProperties({saving: true, error: false});
// Invite the user to the private message
this.get('model').createInvite(this.get('emailOrUsername')).then(function(result) {
self.setProperties({saving: true, finished: true});
if(result && result.user) {
self.get('model.details.allowed_users').pushObject(result.user);
}
}).catch(function() {
self.setProperties({error: true, saving: false});
});
return false;
}
}
});

View File

@ -6,103 +6,104 @@ export default ObjectController.extend(ModalFunctionality, {
// If this isn't defined, it will proxy to the user model on the preferences
// page which is wrong.
email: null,
emailOrUsername: null,
isAdmin: function(){
return Discourse.User.currentProp("admin");
}.property(),
/**
Can we submit the form?
@property disabled
**/
disabled: function() {
if (this.get('saving')) return true;
if (this.blank('email')) return true;
if (!Discourse.Utilities.emailValid(this.get('email'))) return true;
if (this.blank('emailOrUsername')) return true;
if (!this.get('invitingToTopic') && !Discourse.Utilities.emailValid(this.get('emailOrUsername'))) return true;
if (this.get('model.details.can_invite_to')) return false;
if (this.get('isPrivateTopic') && this.blank('groupNames')) return true;
return false;
}.property('email', 'isPrivateTopic', 'groupNames', 'saving'),
}.property('emailOrUsername', 'invitingToTopic', 'isPrivateTopic', 'groupNames', 'saving'),
/**
The current text for the invite button
@property buttonTitle
**/
buttonTitle: function() {
if (this.get('saving')) return I18n.t('topic.inviting');
return I18n.t('topic.invite_reply.action');
return this.get('saving') ? I18n.t('topic.inviting') : I18n.t('topic.invite_reply.action');
}.property('saving'),
/**
We are inviting to a topic if the model isn't the current user. The current user would
mean we are inviting to the forum in general.
@property invitingToTopic
**/
// We are inviting to a topic if the model isn't the current user.
// The current user would mean we are inviting to the forum in general.
invitingToTopic: function() {
return this.get('model') !== Discourse.User.current();
}.property('model'),
/**
Is Private Topic? (i.e. visible only to specific group members)
@property isPrivateTopic
**/
// Is Private Topic? (i.e. visible only to specific group members)
isPrivateTopic: Em.computed.and('invitingToTopic', 'model.category.read_restricted'),
/**
Instructional text for the modal.
isMessage: Em.computed.equal('model.archetype', 'private_message'),
@property inviteInstructions
**/
// Allow Existing Members? (username autocomplete)
allowExistingMembers: function() {
return this.get('invitingToTopic') && !this.get('isPrivateTopic');
}.property('invitingToTopic', 'isPrivateTopic'),
// Show Groups? (add invited user to private group)
showGroups: function() {
return this.get('isAdmin') && (Discourse.Utilities.emailValid(this.get('emailOrUsername')) || this.get('isPrivateTopic') || !this.get('invitingToTopic')) && !Discourse.SiteSettings.enable_sso;
}.property('isAdmin', 'emailOrUsername', 'isPrivateTopic', 'invitingToTopic'),
// Instructional text for the modal.
inviteInstructions: function() {
if (this.get('invitingToTopic')) {
return I18n.t('topic.invite_reply.to_topic');
if (Discourse.SiteSettings.enable_sso) {
// inviting existing user when SSO enabled
return I18n.t('topic.invite_reply.sso_enabled');
} else if (this.get('isMessage')) {
// inviting to a message
return I18n.t('topic.invite_private.email_or_username');
} else if (this.get('invitingToTopic')) {
// when inviting to topic, display instructions based on provided entity
if (this.blank('emailOrUsername')) {
return I18n.t('topic.invite_reply.to_topic_blank');
} else if (Discourse.Utilities.emailValid(this.get('emailOrUsername'))) {
return I18n.t('topic.invite_reply.to_topic_email');
} else {
return I18n.t('topic.invite_reply.to_topic_username');
}
} else {
// inviting to forum
return I18n.t('topic.invite_reply.to_forum');
}
}.property('invitingToTopic'),
}.property('isMessage', 'invitingToTopic', 'emailOrUsername'),
/**
Instructional text for the group selection.
@property groupInstructions
**/
// Instructional text for the group selection.
groupInstructions: function() {
if (this.get('isPrivateTopic')) {
return I18n.t('topic.automatically_add_to_groups_required');
} else {
return I18n.t('topic.automatically_add_to_groups_optional');
}
return this.get('isPrivateTopic') ?
I18n.t('topic.automatically_add_to_groups_required') :
I18n.t('topic.automatically_add_to_groups_optional');
}.property('isPrivateTopic'),
/**
Function to find groups.
**/
groupFinder: function(term) {
groupFinder(term) {
return Discourse.Group.findAll({search: term, ignore_automatic: true});
},
/**
The "success" text for when the invite was created.
@property successMessage
**/
successMessage: function() {
return I18n.t('topic.invite_reply.success', { email: this.get('email') });
}.property('email'),
if (this.get('isMessage')) {
return I18n.t('topic.invite_private.success');
} else if ( Discourse.Utilities.emailValid(this.get('emailOrUsername')) ) {
return I18n.t('topic.invite_reply.success_email', { emailOrUsername: this.get('emailOrUsername') });
} else {
return I18n.t('topic.invite_reply.success_username');
}
}.property('isMessage', 'emailOrUsername'),
/**
Reset the modal to allow a new user to be invited.
errorMessage: function() {
return this.get('isMessage') ? I18n.t('topic.invite_private.error') : I18n.t('topic.invite_reply.error');
}.property('isMessage'),
@method reset
**/
reset: function() {
placeholderKey: function() {
return Discourse.SiteSettings.enable_sso ?
'topic.invite_reply.username_placeholder' :
'topic.invite_private.email_or_username_placeholder';
}.property(),
// Reset the modal to allow a new user to be invited.
reset() {
this.setProperties({
email: null,
emailOrUsername: null,
groupNames: null,
error: false,
saving: false,
@ -112,34 +113,26 @@ export default ObjectController.extend(ModalFunctionality, {
actions: {
/**
Create the invite and update the modal accordingly.
@method createInvite
**/
createInvite: function() {
createInvite() {
if (this.get('disabled')) { return; }
var self = this;
var groupNames = this.get('groupNames');
var userInvitedController = this.get('controllers.user-invited');
const groupNames = this.get('groupNames'),
userInvitedController = this.get('controllers.user-invited');
this.setProperties({ saving: true, error: false });
this.get('model').createInvite(this.get('email'), groupNames).then(function() {
self.setProperties({ saving: false, finished: true });
if (!self.get('invitingToTopic')) {
Discourse.Invite.findInvitedBy(Discourse.User.current()).then(function (invite_model) {
userInvitedController.set('model', invite_model);
userInvitedController.set('totalInvites', invite_model.invites.length);
});
}
}).catch(function() {
self.setProperties({ saving: false, error: true });
});
return false;
return this.get('model').createInvite(this.get('emailOrUsername'), groupNames).then(result => {
this.setProperties({ saving: false, finished: true });
if (!this.get('invitingToTopic')) {
Discourse.Invite.findInvitedBy(Discourse.User.current()).then(invite_model => {
userInvitedController.set('model', invite_model);
userInvitedController.set('totalInvites', invite_model.invites.length);
});
} else if (this.get('isMessage') && result && result.user) {
this.get('model.details.allowed_users').pushObject(result.user);
}
}).catch(() => this.setProperties({ saving: false, error: true }));
}
}
});

View File

@ -1,11 +1,26 @@
import DiscourseController from 'discourse/controllers/controller';
export default DiscourseController.extend({
needs: ['discovery', 'discovery/topics'],
categories: function() {
return Discourse.Category.list();
}.property(),
navItems: function() {
return Discourse.NavItem.buildList();
}.property()
return Discourse.NavItem.buildList(null, {filterMode: this.get('filterMode')});
}.property('filterMode'),
isSearch: Em.computed.equal('filterMode', 'search'),
searchTerm: Em.computed.alias('controllers.discovery/topics.model.params.search'),
actions: {
search: function(){
var discovery = this.get('controllers.discovery/topics');
var model = discovery.get('model');
discovery.set('search', this.get("searchTerm"));
model.refreshSort();
}
}
});

View File

@ -39,6 +39,10 @@ export default ObjectController.extend(CanCheckEmails, {
canEditName: Discourse.computed.setting('enable_names'),
nameInstructions: function() {
return I18n.t(Discourse.SiteSettings.full_name_required ? 'user.name.instructions_required' : 'user.name.instructions');
}.property(),
canSelectTitle: function() {
return this.siteSettings.enable_badges && this.get('model.has_title_badges');
}.property('model.badge_count'),

View File

@ -19,8 +19,11 @@ export default DiscourseController.extend({
// anonymous users cannot "quote-reply"
if (!this.currentUser) return;
// don't display the "quote-reply" button if we can't at least reply as a new topic
if (!this.get('controllers.topic.model.details.can_reply_as_new_topic')) return;
// don't display the "quote-reply" button if we can't reply
const topicDetails = this.get('controllers.topic.model.details');
if (!(topicDetails.get('can_reply_as_new_topic') || topicDetails.get('can_create_post'))) {
return;
}
const selection = window.getSelection();
// no selections

View File

@ -427,8 +427,8 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedCon
}).then(function() {
return Em.isEmpty(quotedText) ? Discourse.Post.loadQuote(post.get('id')) : quotedText;
}).then(function(q) {
const postUrl = "" + location.protocol + "//" + location.host + (post.get('url')),
postLink = "[" + self.get('title') + "](" + postUrl + ")";
const postUrl = "" + location.protocol + "//" + location.host + post.get('url'),
postLink = "[" + Handlebars.escapeExpression(self.get('title')) + "](" + postUrl + ")";
composerController.appendText(I18n.t("post.continue_discussion", { postLink: postLink }) + "\n\n" + q);
});
},

View File

@ -1,10 +1,25 @@
export default Ember.ArrayController.extend({
showAdminLinks: Em.computed.alias("currentUser.staff"),
allowAnon: function(){
return this.siteSettings.allow_anonymous_posting &&
(this.get("currentUser.trust_level") >= this.siteSettings.anonymous_posting_min_trust_level ||
this.get("isAnon"));
}.property(),
isAnon: function(){
return this.get("currentUser.is_anonymous");
}.property(),
actions: {
logout() {
Discourse.logout();
return false;
},
toggleAnon() {
Discourse.ajax("/users/toggle-anon", {method: 'POST'}).then(function(){
window.location.reload();
});
}
}
});

View File

@ -1,9 +1,11 @@
import { iconHTML } from 'discourse/helpers/fa-icon';
const TITLE_SUBS = { yearly: 'this_year',
monthly: 'this_month',
daily: 'today',
all: 'all' };
const TITLE_SUBS = {
all: 'all_time',
yearly: 'this_year',
monthly: 'this_month',
daily: 'today',
};
export default Ember.Handlebars.makeBoundHelper(function (period) {
const title = I18n.t('filters.top.' + (TITLE_SUBS[period] || 'this_week'));

View File

@ -1,20 +1,17 @@
import registerUnbound from 'discourse/helpers/register-unbound';
import { iconHTML } from 'discourse/helpers/fa-icon';
const Safe = Handlebars.SafeString;
registerUnbound('user-status', function(user) {
export default Ember.Handlebars.makeBoundHelper(function(user, args) {
if (!user) { return; }
var name = Handlebars.Utils.escapeExpression(user.get('name'));
const name = Handlebars.Utils.escapeExpression(user.get('name'));
const currentUser = args.hash.currentUser;
if(Discourse.User.currentProp("admin") || Discourse.User.currentProp("moderator")) {
if(user.get('admin')) {
var adminDesc = I18n.t('user.admin', {user: name});
return new Safe('<i class="fa fa-shield" title="' + adminDesc + '" alt="' + adminDesc + '"></i>');
}
if (currentUser && user.get('admin') && currentUser.get('staff')) {
return new Safe(iconHTML('shield', { label: I18n.t('user.admin', { user: name }) }));
}
if(user.get('moderator')){
var modDesc = I18n.t('user.moderator', {user: name});
return new Safe('<i class="fa fa-shield" title="' + modDesc + '" alt="' + modDesc + '"></i>');
if (user.get('moderator')) {
return new Safe(iconHTML('shield', { label: I18n.t('user.moderator', { user: name }) }));
}
});

View File

@ -1282,19 +1282,6 @@
}
});
// Auto-indent on shift-enter
util.addEvent(inputBox, "keyup", function (key) {
if (key.shiftKey && !key.ctrlKey && !key.metaKey) {
var keyCode = key.charCode || key.keyCode;
// Character 13 is Enter
if (keyCode === 13) {
var fakeButton = {};
fakeButton.textOp = bindCommand("doAutoindent");
doClick(fakeButton);
}
}
});
// Perform the button's action.
@ -1542,57 +1529,83 @@
};
commandProto.doBold = function (chunk, postProcessing) {
return this.doBorI(chunk, postProcessing, 2, this.getString("boldexample"));
return this.doSurroundLines(chunk, postProcessing, 2, this.getString("boldexample"));
};
commandProto.doItalic = function (chunk, postProcessing) {
return this.doBorI(chunk, postProcessing, 1, this.getString("italicexample"));
return this.doSurroundLines(chunk, postProcessing, 1, this.getString("italicexample"));
};
// chunk: The selected region that will be enclosed with */**
commandProto.doSurroundLines = function(realChunk, postProcessing, nStars, fallbackText) {
realChunk.trimWhitespace();
// Look for stars before and after, absorb them into the selection.
var starsBefore = /(\**$)/.exec(realChunk.before)[0];
var starsAfter = /(^\**)/.exec(realChunk.after)[0];
realChunk.before = realChunk.before.replace(/(\**$)/, "");
realChunk.after = realChunk.after.replace(/(^\**)/, "");
var lines = (starsBefore + realChunk.selection + starsAfter).split("\n");
// Don't show the fallback text if more than one line is selected,
// it's probably a break between paragraphs.
if (lines.length > 1) {
fallbackText = "";
}
for(var i=0; i<lines.length; i++) {
// Split before, selection and after up.
var lineMatch = lines[i].match(/^(\s*\**)(.*?)(\**\s*)$/);
var newChunk = new Chunks();
newChunk.before = lineMatch[1];
newChunk.selection = lineMatch[2];
newChunk.after = lineMatch[3];
this.doSurroundLine(newChunk, postProcessing, nStars, fallbackText);
if (lines.length > 1) {
lines[i] = newChunk.before + newChunk.selection + newChunk.after;
} else {
realChunk.startTag = newChunk.before;
realChunk.endTag = newChunk.after;
lines[i] = newChunk.selection;
}
}
realChunk.selection = lines.join("\n");
};
// chunk: The selected region that will be enclosed with * or **
// nStars: 1 for italics, 2 for bold
// insertText: If you just click the button without highlighting text, this gets inserted
commandProto.doBorI = function (chunk, postProcessing, nStars, insertText) {
// Get rid of whitespace and fixup newlines.
// fallbackText: If you just click the button without highlighting text, this gets inserted
commandProto.doSurroundLine = function (chunk, postProcessing, nStars, fallbackText) {
// Get rid of whitespace
chunk.trimWhitespace();
chunk.selection = chunk.selection.replace(/\n{2,}/g, "\n");
// Look for stars before and after. Is the chunk already marked up?
// note that these regex matches cannot fail
var starsBefore = /(\**$)/.exec(chunk.before)[0];
var starsAfter = /(^\**)/.exec(chunk.after)[0];
var prevStars = Math.min(starsBefore.length, starsAfter.length);
var minStars = Math.min(chunk.before.length, chunk.after.length);
// Remove stars if we have to since the button acts as a toggle.
if ((prevStars >= nStars) && (prevStars != 2 || nStars != 1)) {
if ((minStars >= nStars) && (minStars != 2 || nStars != 1)) {
chunk.before = chunk.before.replace(re("[*]{" + nStars + "}$", ""), "");
chunk.after = chunk.after.replace(re("^[*]{" + nStars + "}", ""), "");
}
else if (!chunk.selection && starsAfter) {
// It's not really clear why this code is necessary. It just moves
// some arbitrary stuff around.
chunk.after = chunk.after.replace(/^([*_]*)/, "");
chunk.before = chunk.before.replace(/(\s?)$/, "");
var whitespace = re.$1;
chunk.before = chunk.before + starsAfter + whitespace;
}
else {
// In most cases, if you don't have any selected text and click the button
// you'll get a selected, marked up region with the default text inserted.
if (!chunk.selection && !starsAfter) {
chunk.selection = insertText;
if (!chunk.selection && !chunk.after) {
chunk.selection = fallbackText;
}
// Add the true markup.
var markup = nStars <= 1 ? "*" : "**"; // shouldn't the test be = ?
chunk.before = chunk.before + markup;
chunk.after = markup + chunk.after;
// Only operate if it's not a blank line
if (chunk.selection) {
// Add the true markup.
var markup = nStars === 1 ? "*" : "**";
chunk.before = chunk.before + markup;
chunk.after = markup + chunk.after;
}
}
return;
};
commandProto.stripLinkDefs = function (text, defsToAdd) {
@ -1765,51 +1778,6 @@
}
};
// When making a list, hitting shift-enter will put your cursor on the next line
// at the current indent level.
commandProto.doAutoindent = function (chunk, postProcessing) {
var commandMgr = this,
fakeSelection = false;
chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}([*+-]|\d+[.])[ \t]*\n$/, "\n\n");
chunk.before = chunk.before.replace(/(\n|^)[ ]{0,3}>[ \t]*\n$/, "\n\n");
chunk.before = chunk.before.replace(/(\n|^)[ \t]+\n$/, "\n\n");
// There's no selection, end the cursor wasn't at the end of the line:
// The user wants to split the current list item / code line / blockquote line
// (for the latter it doesn't really matter) in two. Temporarily select the
// (rest of the) line to achieve this.
if (!chunk.selection && !/^[ \t]*(?:\n|$)/.test(chunk.after)) {
chunk.after = chunk.after.replace(/^[^\n]*/, function (wholeMatch) {
chunk.selection = wholeMatch;
return "";
});
fakeSelection = true;
}
if (/(\n|^)[ ]{0,3}([*+-]|\d+[.])[ \t]+.*\n$/.test(chunk.before)) {
if (commandMgr.doList) {
commandMgr.doList(chunk);
}
}
if (/(\n|^)[ ]{0,3}>[ \t]+.*\n$/.test(chunk.before)) {
if (commandMgr.doBlockquote) {
commandMgr.doBlockquote(chunk);
}
}
if (/(\n|^)(\t|[ ]{4,}).*\n$/.test(chunk.before)) {
if (commandMgr.doCode) {
commandMgr.doCode(chunk);
}
}
if (fakeSelection) {
chunk.after = chunk.selection + chunk.after;
chunk.selection = "";
}
};
commandProto.doBlockquote = function (chunk, postProcessing) {
chunk.selection = chunk.selection.replace(/^(\n*)([^\r]+?)(\n*)$/,

View File

@ -33,11 +33,23 @@ var keys = {
};
let inputTimeout;
export default function(options) {
var autocompletePlugin = this;
if (this.length === 0) return;
if (options === 'destroy') {
Ember.run.cancel(inputTimeout);
$(this).off('keypress.autocomplete')
.off('keydown.autocomplete')
.off('paste.autocomplete');
return;
}
if (options && options.cancel && this.data("closeAutocomplete")) {
this.data("closeAutocomplete")();
return this;
@ -252,13 +264,13 @@ export default function(options) {
closeAutocomplete();
});
$(this).on('paste', function() {
$(this).on('paste.autocomplete', function() {
_.delay(function(){
me.trigger("keydown");
}, 50);
});
$(this).keypress(function(e) {
$(this).on('keypress.autocomplete', function(e) {
var caretPosition, term;
// keep hunting backwards till you hit a the @ key
@ -277,7 +289,7 @@ export default function(options) {
}
});
$(this).keydown(function(e) {
$(this).on('keydown.autocomplete', function(e) {
var c, caretPosition, i, initial, next, prev, prevIsGood, stopFound, term, total, userToComplete;
if(e.ctrlKey || e.altKey || e.metaKey){
@ -286,7 +298,9 @@ export default function(options) {
if(options.allowAny){
// saves us wiring up a change event as well, keypress is while its pressed
_.delay(function(){
Ember.run.cancel(inputTimeout);
inputTimeout = Ember.run.later(function(){
if(inputSelectedItems.length === 0) {
inputSelectedItems.push("");
}
@ -299,7 +313,7 @@ export default function(options) {
}
}
},50);
}, 50);
}
if (!options.key) {
@ -332,7 +346,7 @@ export default function(options) {
// ESC
if (e.which === keys.esc) {
if (completeStart !== null) {
if (div !== null) {
closeAutocomplete();
return false;
}

View File

@ -1,23 +1,10 @@
/**
Used for tracking when the user clicks on a link
export default {
trackClick(e) {
// cancel click if triggered as part of selection.
if (Discourse.Utilities.selectedText() !== "") { return false; }
@class ClickTrack
@namespace Discourse
@module Discourse
**/
Discourse.ClickTrack = {
/**
Track a click on a link
@method trackClick
@param {jQuery.Event} e The click event that occurred
**/
trackClick: function(e) {
if (Discourse.Utilities.selectedText()!=="") return false; //cancle click if triggered as part of selection.
var $link = $(e.currentTarget);
if ($link.hasClass('lightbox')) return true;
if ($link.hasClass('lightbox')) { return true; }
var href = $link.attr('href') || $link.data('href'),
$article = $link.closest('article'),
@ -25,9 +12,7 @@ Discourse.ClickTrack = {
topicId = $('#topic').data('topic-id'),
userId = $link.data('user-id');
if (!href || href.trim().length === 0){
return;
}
if (!href || href.trim().length === 0) { return; }
if (!userId) userId = $article.data('user-id');

View File

@ -261,6 +261,9 @@ var number = function(val) {
val = parseInt(val, 10);
if (isNaN(val)) val = 0;
if (val > 999999) {
return (val / 1000000).toFixed(1) + "M";
}
if (val > 999) {
return (val / 1000).toFixed(1) + "K";
}

View File

@ -3,7 +3,7 @@ import loadScript from 'discourse/lib/load-script';
export default function($elem) {
$("a.lightbox", $elem).each(function(i, e) {
loadScript("/javascripts/jquery.magnific-popup-min.js").then(function() {
var $e = $(e);
const $e = $(e);
// do not lightbox spoiled images
if ($e.parents(".spoiler").length > 0 || $e.parents(".spoiled").length > 0) { return; }
@ -14,30 +14,30 @@ export default function($elem) {
mainClass: "mfp-zoom-in",
callbacks: {
open: function() {
var wrap = this.wrap,
img = this.currItem.img,
maxHeight = img.css("max-height");
open() {
const wrap = this.wrap,
img = this.currItem.img,
maxHeight = img.css("max-height");
wrap.on("click.pinhandler", "img", function() {
wrap.toggleClass("mfp-force-scrollbars");
img.css("max-height", wrap.hasClass("mfp-force-scrollbars") ? "none" : maxHeight);
});
},
beforeClose: function() {
beforeClose() {
this.wrap.off("click.pinhandler");
this.wrap.removeClass("mfp-force-scrollbars");
}
},
image: {
titleSrc: function(item) {
var href = item.el.data("download-href") || item.src;
return [
item.el.attr("title"),
$("span.informations", item.el).text().replace('x', '&times;'),
'<a class="image-source-link" href="' + href + '">' + I18n.t("lightbox.download") + '</a>'
].join(' &middot; ');
titleSrc(item) {
const href = item.el.data("download-href") || item.src;
let src = [item.el.attr("title"), $("span.informations", item.el).text().replace('x', '&times;')];
if (!Discourse.SiteSettings.prevent_anons_from_downloading_files || Discourse.User.current()) {
src.push('<a class="image-source-link" href="' + href + '">' + I18n.t("lightbox.download") + '</a>');
}
return src.join(' &middot; ');
}
}

View File

@ -16,42 +16,88 @@ function positioningWorkaround($fixedElement) {
const fixedElement = $fixedElement[0];
var done = false;
var blurredNow = function(evt) {
if (!done && _.include($(document.activeElement).parents(), fixedElement)) {
// something in focus so skip
return;
}
done = true;
fixedElement.style.position = '';
fixedElement.style.top = '';
if (evt) {
evt.target.removeEventListener('blur', blurred);
}
};
var blurred = _.debounce(blurredNow, 250);
var positioningHack = function(evt){
const self = this;
done = false;
if (fixedElement.style.position !== 'absolute') {
evt.preventDefault();
fixedElement.style.position = 'absolute';
fixedElement.style.top = (window.scrollY + $('.d-header').height() + 10) + 'px';
// we need this, otherwise changing focus means we never clear
self.addEventListener('blur', blurred);
if (fixedElement.style.position === 'absolute') {
if (this !== document.activeElement) {
evt.preventDefault();
self.focus();
}
return;
}
var blurred = function() {
if (_.include($(document.activeElement).parents(), fixedElement)) {
// something in focus so skip
fixedElement.style.position = 'absolute';
// get out of the way while opening keyboard
fixedElement.style.top = '0px';
var iPadOffset = 0;
if (window.innerHeight > window.innerWidth && navigator.userAgent.match(/iPad/)) {
// there is no way to get virtual keyboard height
iPadOffset = 640 - $(fixedElement).height();
}
var oldScrollY = 0;
var positionElement = function(){
if (done) {
return;
}
fixedElement.style.position = '';
fixedElement.style.top = '';
self.removeEventListener('blur', blurred);
if (Math.abs(oldScrollY - window.scrollY) < 20) {
return;
}
oldScrollY = window.scrollY;
fixedElement.style.top = window.scrollY + iPadOffset + 'px';
};
blurred = _.debounce(blurred, 300);
// position once, correctly, after keyboard is shown
setTimeout(positionElement, 500);
if (this !== document.activeElement) {
self.focus();
}
self.addEventListener('blur', blurred);
evt.preventDefault();
self.focus();
};
function attachTouchStart(elem, fn) {
if (!$(elem).data('listening')) {
elem.addEventListener('touchstart', fn);
$(elem).data('listening', true);
}
}
const checkForInputs = _.debounce(function(){
$fixedElement.find('button,a').each(function(){
attachTouchStart(this, function(evt){
done = true;
$(document.activeElement).blur();
evt.preventDefault();
$(this).click();
});
});
$fixedElement.find('input,textarea').each(function(){
if (!$(this).data('listening')) {
this.addEventListener('touchstart', positioningHack);
$(this).data('listening', true);
}
attachTouchStart(this, positioningHack);
});
}, 100);

View File

@ -15,7 +15,7 @@ function searchForTerm(term, opts) {
};
}
return Discourse.ajax('/search/', { data: data }).then(function(results){
return Discourse.ajax('/search/query', { data: data }).then(function(results){
// Topics might not be included
if (!results.topics) { results.topics = []; }
if (!results.users) { results.users = []; }

View File

@ -12,4 +12,6 @@ export default (name, model) => {
if (controller.onShow) { controller.onShow(); }
controller.set('flashMessage', null);
}
return controller;
};

View File

@ -256,7 +256,12 @@ Discourse.Utilities = {
@param {String} path The path
**/
isAnImage: function(path) {
return (/\.(png|jpg|jpeg|gif|bmp|tif|tiff|svg|webp)$/i).test(path);
return (/\.(png|jpe?g|gif|bmp|tiff?|svg|webp)$/i).test(path);
},
allowsImages: function() {
return Discourse.Utilities.authorizesAllExtensions() ||
(/(png|jpe?g|gif|bmp|tiff?|svg|webp)/i).test(Discourse.Utilities.authorizedExtensions());
},
/**
@ -266,7 +271,7 @@ Discourse.Utilities = {
**/
allowsAttachments: function() {
return Discourse.Utilities.authorizesAllExtensions() ||
!(/((png|jpg|jpeg|gif|bmp|tif|tiff|svg|webp)(,\s)?)+$/i).test(Discourse.Utilities.authorizedExtensions());
!(/((png|jpe?g|gif|bmp|tiff?|svg|webp)(,\s)?)+$/i).test(Discourse.Utilities.authorizedExtensions());
},
displayErrorForUpload: function(data) {

View File

@ -0,0 +1,21 @@
// Mix this in to a view that has a `archetype` property to automatically
// add it to the body as the view is entered / left / model is changed.
// This is used for keeping the `body` style in sync for the background image.
export default {
_init: function() { this.get('archetype'); }.on('init'),
_cleanUp() {
$('body').removeClass((_, css) => (css.match(/\barchetype-\S+/g) || []).join(' '));
},
_archetypeChanged: function() {
const archetype = this.get('archetype');
this._cleanUp();
if (archetype) {
$('body').addClass('archetype-' + archetype);
}
}.observes('archetype'),
_willDestroyElement: function() { this._cleanUp(); }.on('willDestroyElement')
};

View File

@ -4,14 +4,12 @@
export default {
_enterView: function() { this.get('categoryFullSlug'); }.on('init'),
_removeClasses: function() {
$('body').removeClass(function(idx, css) {
return (css.match(/\bcategory-\S+/g) || []).join(' ');
});
_removeClasses() {
$('body').removeClass((_, css) => (css.match(/\bcategory-\S+/g) || []).join(' '));
},
_categoryChanged: function() {
var categoryFullSlug = this.get('categoryFullSlug');
const categoryFullSlug = this.get('categoryFullSlug');
this._removeClasses();
if (categoryFullSlug) {

View File

@ -86,7 +86,7 @@ const Composer = Discourse.Model.extend({
const replyUsername = post.get('reply_to_user.username');
const replyAvatarTemplate = post.get('reply_to_user.avatar_template');
if (replyUsername && replyAvatarTemplate && this.get('action') === EDIT) {
postDescription += " " + I18n.t("post.in_reply_to") + " " + Discourse.Utilities.tinyAvatar(replyAvatarTemplate) + " " + replyUsername;
postDescription += " <i class='fa fa-mail-forward reply-to-glyph'></i> " + Discourse.Utilities.tinyAvatar(replyAvatarTemplate) + " " + replyUsername;
}
}
}
@ -542,7 +542,6 @@ const Composer = Discourse.Model.extend({
// It's no longer a new post
createdPost.set('newPost', false);
topic.set('draft_sequence', result.draft_sequence);
topic.set('details.auto_close_at', result.topic_auto_close_at);
postStream.commitPost(createdPost);
addedToStream = true;
} else {

View File

@ -61,7 +61,9 @@ const Group = Discourse.Model.extend({
alias_level: this.get('alias_level'),
visible: !!this.get('visible'),
automatic_membership_email_domains: this.get('emailDomains'),
automatic_membership_retroactive: !!this.get('automatic_membership_retroactive')
automatic_membership_retroactive: !!this.get('automatic_membership_retroactive'),
title: this.get('title'),
primary_group: !!this.get('primary_group')
};
},

View File

@ -84,7 +84,15 @@ Discourse.NavItem.reopenClass({
args = args || {};
if (category) { args.category = category }
return Discourse.SiteSettings.top_menu.split("|").map(function(i) {
var items = Discourse.SiteSettings.top_menu.split("|");
if (args.filterMode && !_.some(items, function(i){
return i.indexOf(args.filterMode) !== -1;
})) {
items.push(args.filterMode);
}
return items.map(function(i) {
return Discourse.NavItem.fromText(i, args);
}).filter(function(i) {
return i !== null && !(category && i.get("name").indexOf("categor") === 0);

View File

@ -624,9 +624,6 @@ const PostStream = Ember.Object.extend({
return existing;
}
// Update the auto_close_at value of the topic
this.set("topic.details.auto_close_at", post.get("topic_auto_close_at"));
post.set('topic', this.get('topic'));
postIdentityMap.set(post.get('id'), post);

View File

@ -41,8 +41,13 @@ Discourse.TopicList = Discourse.Model.extend({
var self = this,
params = this.get('params');
params.order = order;
params.ascending = ascending;
params.order = order || params.order;
if (ascending === undefined) {
params.ascending = ascending;
} else {
params.ascending = ascending;
}
this.set('loaded', false);
var finder = finderFor(this.get('filter'), params);

View File

@ -157,20 +157,21 @@ const ApplicationRoute = Discourse.Route.extend({
const returnPath = encodeURIComponent(window.location.pathname);
window.location = Discourse.getURL('/session/sso?return_path=' + returnPath);
} else {
this._autoLogin('login', () => this.controllerFor('login').resetForm());
this._autoLogin('login', 'login-modal', () => this.controllerFor('login').resetForm());
}
},
handleShowCreateAccount() {
this._autoLogin('createAccount');
this._autoLogin('createAccount', 'create-account');
},
_autoLogin(modal, notAuto) {
_autoLogin(modal, modalClass, notAuto) {
const methods = Em.get('Discourse.LoginMethod.all');
if (!this.siteSettings.enable_local_logins && methods.length === 1) {
this.controllerFor('login').send('externalLogin', methods[0]);
} else {
showModal(modal);
this.controllerFor('modal').set('modalClass', modalClass);
if (notAuto) { notAuto(); }
}
},

View File

@ -9,7 +9,8 @@ const DiscoveryRoute = Discourse.Route.extend(Discourse.ScrollTop, Discourse.Ope
redirect: function() { return this.redirectIfLoginRequired(); },
beforeModel: function(transition) {
if (transition.targetName.indexOf("discovery.top") === -1 &&
if (transition.intent.url === "/" &&
transition.targetName.indexOf("discovery.top") === -1 &&
Discourse.User.currentProp("should_be_redirected_to_top")) {
Discourse.User.currentProp("should_be_redirected_to_top", false);
this.replaceWith("discovery.top");

View File

@ -56,6 +56,7 @@ export default RestrictedUserRoute.extend(ShowFooter, {
'gravatar_avatar_upload_id',
'custom_avatar_upload_id'
));
bootbox.alert(I18n.t("user.change_avatar.cache_notice"));
});
}

View File

@ -69,16 +69,6 @@ const TopicRoute = Discourse.Route.extend(ShowFooter, {
this.controllerFor('invite').reset();
},
showPrivateInvite() {
showModal('invitePrivate', this.modelFor('topic'));
this.controllerFor('invitePrivate').setProperties({
email: null,
error: false,
saving: false,
finished: false
});
},
showHistory(post) {
showModal('history', post);
this.controllerFor('history').refresh(post.get("id"), "latest");

View File

@ -19,6 +19,5 @@
</label>
</div>
{{/each}}
</ul>
{{/if}}
{{/if}}

View File

@ -0,0 +1,6 @@
{{#if category.unreadTopics}}
<a href="{{unbound category.unreadUrl}}" class='badge new-posts badge-notification' title='{{i18n 'topic.unread_topics' count=category.unreadTopics}}'>{{i18n 'filters.unread.lower_title_with_count' count=category.unreadTopics}}</a>
{{/if}}
{{#if category.newTopics}}
<a href="{{unbound category.newUrl}}" class='badge new-posts badge-notification' title='{{i18n 'topic.new_topics' count=ctegory.newTopics}}'>{{i18n 'filters.new.lower_title_with_count' count=category.newTopics}}</a>
{{/if}}

View File

@ -62,7 +62,7 @@
<tbody>
{{#each a in other_accounts}}
<tr>
<td>{{#link-to "adminUser" a}}{{avatar a usernamePath="user.username" imageSize="small"}}&nbsp;{{username}}{{/link-to}}</td>
<td>{{#link-to "adminUser" a}}{{avatar a usernamePath="user.username" imageSize="small"}}&nbsp;{{a.username}}{{/link-to}}</td>
<td>{{a.trustLevel.id}}</td>
<td>{{a.time_read}}</td>
<td>{{a.topics_entered}}</td>

View File

@ -1,3 +1,4 @@
<div id='wmd-button-bar'></div>
{{textarea value=value elementId="wmd-input"}}
<div id='wmd-preview' {{bind-attr class=":preview value::hidden"}}>
</div>

View File

@ -22,7 +22,7 @@ so I'm going to stop rendering it until we figure out what's up
{{plugin-outlet "composer-open"}}
<div class='reply-to'>
{{{model.actionTitle}}}:
{{{model.actionTitle}}}
{{#if canEdit}}
{{#if showEditReason}}
<div class="edit-reason-input">

View File

@ -15,12 +15,9 @@
<div>
<div class="pull-left">
{{category-title-link category=c}}
{{#if c.unreadTopics}}
<a href="{{unbound c.unreadUrl}}" class='badge new-posts badge-notification' title='{{i18n 'topic.unread_topics' count=c.unreadTopics}}'>{{i18n 'filters.unread.lower_title_with_count' count=c.unreadTopics}}</a>
{{/if}}
{{#if c.newTopics}}
<a href="{{unbound c.newUrl}}" class='badge new-posts badge-notification' title='{{i18n 'topic.new_topics' count=c.newTopics}}'>{{i18n 'filters.new.lower_title_with_count' count=c.newTopics}}</a>
{{/if}}
{{#unless c.logo_url}}
{{category-unread category=c}}
{{/unless}}
</div>
<div class="clearfix"></div>
</div>
@ -28,17 +25,16 @@
<div class="category-description">
{{{c.description_excerpt}}}
</div>
{{#if c.logo_url}}
{{category-unread category=c}}
{{/if}}
{{/if}}
{{#if c.subcategories}}
<div class='subcategories'>
{{#each s in c.subcategories}}
{{category-link s hideParent="true"}}
{{#if s.unreadTopics}}
<a href={{unbound s.unreadUrl}} class='badge new-posts badge-notification' title='{{i18n 'topic.unread_topics' count=s.unreadTopics}}'>{{unbound s.unreadTopics}}</a>
{{/if}}
{{#if s.newTopics}}
<a href={{unbound s.newUrl}} class='badge new-posts badge-notification' title='{{i18n 'topic.new_topics' count=s.newTopics}}'>{{unbound s.newTopics}}</a>
{{/if}}
{{category-unread category=s}}
{{/each}}
</div>
{{/if}}

View File

@ -13,14 +13,14 @@
<ul class='icons clearfix' role='navigation'>
{{#if currentUser}}
<li class='notifications'>
<a class='icon' href="#" {{action "showNotifications" target="view"}} data-notifications="notifications-dropdown" id='user-notifications' title='{{i18n 'notifications.title'}}'>
<a class='icon' href {{action "showNotifications" target="view"}} data-notifications="notifications-dropdown" id='user-notifications' title='{{i18n 'notifications.title'}}'>
{{fa-icon "comment" label="notifications.title"}}
</a>
{{#if currentUser.unread_notifications}}
<a href='#' class='badge-notification unread-notifications'>{{currentUser.unread_notifications}}</a>
<a href class='badge-notification unread-notifications'>{{currentUser.unread_notifications}}</a>
{{/if}}
{{#if currentUser.unread_private_messages}}
<a href='#' class='badge-notification unread-private-messages'>{{currentUser.unread_private_messages}}</a>
<a href class='badge-notification unread-private-messages'>{{currentUser.unread_private_messages}}</a>
{{/if}}
</li>
{{/if}}

View File

@ -37,7 +37,7 @@
</div>
</td>
<td class='num posts'>{{number t.posts_count}}</td>
<td class='num age'><span class="{{cold-age-class t.created_at}}" title='{{raw-date t.created_at}}'>{{{format-age t.created_at}}}</span></td>
<td class='num age'><span class="{{cold-age-class t.last_posted_at}}" title='{{raw-date t.last_posted_at}}'>{{{format-age t.last_posted_at}}}</span></td>
</tr>
{{/each}}

View File

@ -9,24 +9,27 @@
<label class="radio" for="gravatar">{{bound-avatar controller "large" gravatar_avatar_upload_id}} {{{i18n 'user.change_avatar.gravatar'}}} {{email}}</label>
{{d-button action="refreshGravatar" title="user.change_avatar.refresh_gravatar_title" disabled=gravatarRefreshDisabled class="no-text" icon="refresh"}}
</div>
<div>
<input type="radio" id="uploaded_avatar" name="avatar" value="uploaded" {{action "useUploadedAvatar"}}>
<label class="radio" for="uploaded_avatar">
{{#if hasUploadedAvatar}}
{{#if uploadedAvatarTemplate}}
{{bound-avatar-template uploadedAvatarTemplate "large"}}
{{#if allowImageUpload}}
<div>
<input type="radio" id="uploaded_avatar" name="avatar" value="uploaded" {{action "useUploadedAvatar"}}>
<label class="radio" for="uploaded_avatar">
{{#if hasUploadedAvatar}}
{{#if uploadedAvatarTemplate}}
{{bound-avatar-template uploadedAvatarTemplate "large"}}
{{else}}
{{bound-avatar controller "large" custom_avatar_upload_id}} {{i18n 'user.change_avatar.uploaded_avatar'}}
{{/if}}
{{else}}
{{bound-avatar controller "large" custom_avatar_upload_id}} {{i18n 'user.change_avatar.uploaded_avatar'}}
{{i18n 'user.change_avatar.uploaded_avatar_empty'}}
{{/if}}
{{else}}
{{i18n 'user.change_avatar.uploaded_avatar_empty'}}
{{/if}}
</label>
{{avatar-uploader username=username
uploadedAvatarTemplate=uploadedAvatarTemplate
custom_avatar_upload_id=custom_avatar_upload_id
done="useUploadedAvatar"}}
</div>
</label>
{{avatar-uploader username=username
uploadedAvatarTemplate=uploadedAvatarTemplate
custom_avatar_upload_id=custom_avatar_upload_id
uploading=uploading
done="useUploadedAvatar"}}
</div>
{{/if}}
</div>
</div>

View File

@ -42,7 +42,7 @@
</tr>
<tr class="instructions">
<td></td>
<td><label>{{i18n 'user.name.instructions'}}</label></td>
<td><label>{{nameInstructions}}</label></td>
</tr>
{{#if passwordRequired}}

View File

@ -27,15 +27,14 @@
{{#if showDescription}}
<section class='field'>
<label>{{i18n 'category.description'}}</label>
{{#if description}}
{{description}}
{{{description}}}
{{else}}
{{i18n 'category.no_description'}}
{{/if}}
{{#if topic_url}}
<br/>
<button class="btn btn-small" {{action "showCategoryTopic"}}><i class="fa fa-pencil"></i>{{i18n 'category.change_in_category_topic'}}</button>
{{d-button class="btn-small" action="showCategoryTopic" icon="pencil" label="category.change_in_category_topic"}}
{{/if}}
</section>
{{/if}}

View File

@ -2,7 +2,7 @@
{{#if pinned_at}}
<div class="feature-section">
<div class="button">
{{d-button action="unpin" icon="thumb-tack" label="topic.actions.unpin" class="btn-primary"}}
{{d-button action="unpin" icon="thumb-tack" label="topic.feature.unpin" class="btn-primary"}}
</div>
<div class="desc">
<p>{{{unPinMessage}}}</p>
@ -26,7 +26,7 @@
{{else}}
<div class="feature-section">
<div class="button">
{{d-button action="pin" icon="thumb-tack" label="topic.actions.pin" class="btn-primary"}}
{{d-button action="pin" icon="thumb-tack" label="topic.feature.pin" class="btn-primary"}}
</div>
<div class="desc">
<p>{{{pinMessage}}}</p>
@ -41,7 +41,7 @@
<hr>
<div class="feature-section">
<div class="button">
{{d-button action="pinGlobally" icon="thumb-tack" label="topic.actions.pin_globally" class="btn-primary"}}
{{d-button action="pinGlobally" icon="thumb-tack" label="topic.feature.pin_globally" class="btn-primary"}}
</div>
<div class="desc">
<p>{{i18n "topic.feature_topic.pin_globally"}}</p>
@ -58,9 +58,9 @@
<div class="feature-section">
<div class="button">
{{#if isBanner}}
{{d-button action="removeBanner" icon="thumb-tack" label="topic.actions.remove_banner" class="btn-primary"}}
{{d-button action="removeBanner" icon="thumb-tack" label="topic.feature.remove_banner" class="btn-primary"}}
{{else}}
{{d-button action="makeBanner" icon="thumb-tack" label="topic.actions.make_banner" class="btn-primary"}}
{{d-button action="makeBanner" icon="thumb-tack" label="topic.feature.make_banner" class="btn-primary"}}
{{/if}}
</div>
<div class="desc">

View File

@ -2,18 +2,19 @@
{{#if error}}
<div class="alert alert-error">
<button class="close" data-dismiss="alert">×</button>
{{i18n 'topic.invite_reply.error'}}
{{errorMessage}}
</div>
{{/if}}
{{#if finished}}
{{{successMessage}}}
{{else}}
<label>{{inviteInstructions}}</label>
{{text-field value=email placeholderKey="topic.invite_reply.email_placeholder"}}
{{#if isAdmin}}
{{#if allowExistingMembers}}
{{user-selector single="true" allowAny=true excludeCurrentUser="true" usernames=emailOrUsername includeGroups="true" placeholderKey=placeholderKey}}
{{else}}
{{text-field value=emailOrUsername placeholderKey="topic.invite_reply.email_placeholder"}}
{{/if}}
{{#if showGroups}}
<label>{{{groupInstructions}}}</label>
{{group-selector groupFinder=groupFinder groupNames=groupNames placeholderKey="topic.invite_private.group_name"}}
{{/if}}
@ -21,9 +22,8 @@
</div>
<div class="modal-footer">
{{#if finished}}
<button class='btn btn-primary' {{action "closeModal"}}>{{i18n 'close'}}</button>
{{d-button class="btn-primary" action="closeModal" label="close"}}
{{else}}
<button class='btn btn-primary' {{bind-attr disabled="disabled"}} {{action "createInvite"}}><i class="fa fa-envelope"></i>{{buttonTitle}}</button>
<button class='btn btn-primary' {{bind-attr disabled="disabled"}} {{action "createInvite"}}>{{fa-icon "user-plus"}}{{buttonTitle}}</button>
{{/if}}
</div>

View File

@ -1,23 +0,0 @@
<div class="modal-body">
{{#if error}}
<div class="alert alert-error">
<button class="close" data-dismiss="alert">×</button>
{{i18n 'topic.invite_private.error'}}
</div>
{{/if}}
{{#if finished}}
{{i18n 'topic.invite_private.success'}}
{{else}}
<label>{{i18n 'topic.invite_private.email_or_username'}}</label>
{{user-selector single="true" allowAny=true usernames=emailOrUsername includeGroups="true" placeholderKey="topic.invite_private.email_or_username_placeholder"}}
{{/if}}
</div>
<div class="modal-footer">
{{#if finished}}
<button class='btn btn-primary' {{action "closeModal"}}>{{i18n 'close'}}</button>
{{else}}
<button class='btn btn-primary' {{bind-attr disabled="disabled"}} {{action "invite"}}>{{buttonTitle}}</button>
{{/if}}
</div>

View File

@ -1,23 +1,19 @@
<div class="modal-outer-container">
<div class="modal-middle-container">
<div class="modal-inner-container">
<div class="modal-header">
<a class="close" {{action "closeModal"}}><i class='fa fa-times'></i></a>
<a class="close" {{action "closeModal"}}>{{fa-icon "times"}}</a>
<h3>{{title}}</h3>
<div class="clearfix"></div>
</div>
<div id='modal-alert'></div>
{{outlet "modalBody"}}
{{#each error in errors}}
<div class="alert alert-error">
<button class="close" data-dismiss="alert">×</button>
{{error}}
</div>
{{/each}}
</div>
</div>
</div>

View File

@ -1,3 +1,9 @@
{{#if isSearch}}
<div class="search row">
{{input type="text" value=searchTerm class="input-xxlarge search" action="search"}}
<button {{action "search"}} class="btn btn-primary"><i class='fa fa-search'></i></button>
</div>
{{else}}
{{bread-crumbs categories=categories}}
<ul class="nav nav-pills" id='navigation-bar'>
@ -10,3 +16,4 @@
{{#if canCreateTopic}}
<button id="create-topic" class='btn btn-default' {{action "createTopic"}}><i class='fa fa-plus'></i>{{i18n 'topic.create'}}</button>
{{/if}}
{{/if}}

View File

@ -1,35 +1,32 @@
{{view 'search-text-field' value=term searchContextEnabled=searchContextEnabled searchContext=searchContext id="search-term"}}
<div class='search-context'>
{{view "search-text-field" value=term searchContextEnabled=searchContextEnabled searchContext=searchContext id="search-term"}}
<div class="search-context">
{{#if searchContext}}
<label>
{{input type="checkbox" name="searchContext" checked=searchContextEnabled}} {{searchContextDescription}}
{{input type="checkbox" name="searchContext" checked=searchContextEnabled}} {{searchContextDescription}}
</label>
{{/if}}
<a href="#" class="show-help" {{action "showSearchHelp" bubbles=false}}>{{i18n 'show_help'}}</a>
<a href class="show-help" {{action "showSearchHelp" bubbles=false}}>{{i18n "show_help"}}</a>
</div>
{{#if loading}}
<div class='searching'>{{loading-spinner}}</div>
<div class="searching">{{loading-spinner}}</div>
{{else}}
<div class="results">
{{#unless noResults}}
{{#if noResults}}
<div class="no-results">
{{i18n "search.no_results"}}
</div>
{{else}}
{{#each resultType in content.resultTypes}}
<ul>
<li class='heading row'>
{{resultType.name}}
</li>
{{view 'search-results-type' type=resultType.type displayType=resultType.displayType content=resultType.results}}
<li class="heading row">{{resultType.name}}</li>
{{view "search-results-type" type=resultType.type displayType=resultType.displayType content=resultType.results}}
</ul>
<div class='no-results'>
{{#if resultType.more}}
<a href='#' class='filter' {{action "moreOfType" resultType.type bubbles=false}}>{{i18n 'show_more'}} <i class="fa fa-chevron-down"></i></a>
{{else}}
{{/if}}
</div>
<div class="no-results">
{{#if resultType.more}}
<a href class="filter" {{action "moreOfType" resultType.type bubbles=false}}>{{i18n "show_more"}} {{fa-icon "chevron-down"}}</a>
{{/if}}
</div>
{{/each}}
{{else}}
<div class='no-results'>
{{i18n 'search.no_results'}}
</div>
{{/unless}}
{{/if}}
</div>
{{/if}}

View File

@ -22,7 +22,9 @@
</li>
{{/if}}
<li>{{#link-to 'users'}}{{i18n "directory.title"}}{{/link-to}}</li>
{{#if siteSettings.enable_user_directory}}
<li>{{#link-to 'users'}}{{i18n "directory.title"}}{{/link-to}}</li>
{{/if}}
{{plugin-outlet "site-map-links"}}

View File

@ -1,11 +1,11 @@
<span class='topic-post-badges'>
{{#if unread ~}}
<a href='{{url}}' class='badge badge-notification unread' title='{{view.unreadTitle}}'>{{unread}}</a>
<a href='{{url}}' class='badge badge-notification unread' title='{{i18n "topic.unread_posts" count=unread}}'>{{unread}}</a>
{{/if }}
{{#if newPosts ~}}
<a href='{{url}}' class='badge badge-notification new-posts' title='{{view.newTitle}}'>{{newPosts}}</a>
<a href='{{url}}' class='badge badge-notification new-posts' title='{{i18n "topic.total_unread_posts" count=newPosts}}'>{{newPosts}}</a>
{{/if}}
{{#if unseen ~}}
<a href='{{url}}' class='badge badge-notification new-topic' title='{{view.unseenTitle}}'>{{i18n 'filters.new.lower_title'}}</a>
<a href='{{url}}' class='badge badge-notification new-topic' title='{{i18n "topic.new"}}'>{{i18n 'filters.new.lower_title'}}</a>
{{/if}}
</span>

View File

@ -16,7 +16,7 @@
{{autofocus-text-field id='edit-title' value=buffered.title maxLength=maxTitleLength}}
{{else}}
{{autofocus-text-field id='edit-title' value=buffered.title maxLength=maxTitleLength}}
</br>
<br>
{{category-chooser valueAttribute="id" value=buffered.category_id source=buffered.category_id}}
{{/if}}

View File

@ -5,7 +5,7 @@
<div class="names">
<span>
<h1 {{bind-attr class="staff new_user"}}>
{{#link-to 'user' user}}{{username}} {{user-status user}}{{/link-to}}
{{#link-to 'user' user}}{{username}} {{user-status user currentUser=currentUser}}{{/link-to}}
</h1>
{{#if user.name}}

View File

@ -8,6 +8,9 @@
</li>
<li>{{#link-to 'userActivity.bookmarks' currentUser}}{{i18n 'user.bookmarks'}}{{/link-to}}</li>
<li>{{#link-to 'preferences' currentUser}}{{i18n 'user.preferences'}}{{/link-to}}</li>
{{#if allowAnon}}
<li><a href {{action toggleAnon}}>{{#if isAnon}}{{i18n 'switch_from_anon'}}{{else}}{{i18n 'switch_to_anon'}}{{/if}}</a></li>
{{/if}}
<li>{{d-button action="logout" class="btn-danger right logout" icon="sign-out" label="user.log_out"}}</li>
</ul>
</section>

View File

@ -62,7 +62,7 @@
{{#if invite.reinvited}}
{{i18n 'user.invited.reinvited'}}
{{else}}
<button class='btn' {{action "reinvite" invite}}><i class="fa fa-envelope"></i>{{i18n 'user.invited.reinvite'}}</button>
<button class='btn' {{action "reinvite" invite}}><i class="fa fa-user-plus"></i>{{i18n 'user.invited.reinvite'}}</button>
{{/if}}
</td>
{{/if}}

View File

@ -32,7 +32,7 @@
{{/if}}
</div>
<div class='instructions'>
{{i18n 'user.name.instructions'}}
{{nameInstructions}}
</div>
</div>
{{/if}}
@ -62,7 +62,7 @@
</div>
{{else}}
<div class="controls">
{{d-button action="checkEmail" actionParam=this title="admin.users.check_email.title" icon="envelope-o" label="admin.users.check_email.text"}}
{{d-button action="checkEmail" actionParam=model title="admin.users.check_email.title" icon="envelope-o" label="admin.users.check_email.text"}}
</div>
{{/if}}
</div>

View File

@ -55,13 +55,13 @@
<li>{{#link-to 'preferences' class="btn right"}}{{fa-icon "cog"}}{{i18n 'user.preferences'}}{{/link-to}}</li>
{{/if}}
{{#if canInviteToForum}}
<li>{{#link-to 'user.invited' class="btn right"}}{{fa-icon "envelope-o"}}{{i18n 'user.invited.title'}}{{/link-to}}</li>
<li>{{#link-to 'user.invited' class="btn right"}}{{fa-icon "user-plus"}}{{i18n 'user.invited.title'}}{{/link-to}}</li>
{{/if}}
</ul>
</section>
<div class="primary-textual">
<h1>{{username}} {{user-status model}}</h1>
<h1>{{username}} {{user-status model currentUser=currentUser}}</h1>
<h2>{{name}}</h2>
{{#if title}}
<h3>{{title}}</h3>

View File

@ -18,6 +18,8 @@
{{directory-toggle field="topic_count" order=order asc=asc}}
{{directory-toggle field="post_count" order=order asc=asc}}
{{directory-toggle field="topics_entered" order=order asc=asc}}
{{directory-toggle field="posts_read" order=order asc=asc}}
{{directory-toggle field="days_visited" order=order asc=asc}}
{{#if showTimeRead}}
<th>{{i18n "directory.time_read"}}</th>
{{/if}}
@ -31,6 +33,8 @@
<td>{{number item.model.topic_count}}</td>
<td>{{number item.model.post_count}}</td>
<td>{{number item.model.topics_entered}}</td>
<td>{{number item.model.posts_read}}</td>
<td>{{number item.model.days_visited}}</td>
{{#if showTimeRead}}
<td><span class='time-read'>{{unbound item.model.time_read}}</span></td>
{{/if}}

View File

@ -9,11 +9,11 @@ export default ComboboxView.extend({
castInteger: true,
content: function() {
var scopedCategoryId = this.get('scopedCategoryId');
let scopedCategoryId = this.get('scopedCategoryId');
// Always scope to the parent of a category, if present
if (scopedCategoryId) {
var scopedCat = Discourse.Category.findById(scopedCategoryId);
const scopedCat = Discourse.Category.findById(scopedCategoryId);
scopedCategoryId = scopedCat.get('parent_category_id') || scopedCat.get('id');
}
@ -41,13 +41,13 @@ export default ComboboxView.extend({
}
}.property(),
template: function(item) {
template(item) {
var category;
let category;
// If we have no id, but text with the uncategorized name, we can use that badge.
if (Ember.isEmpty(item.id)) {
var uncat = Discourse.Category.findUncategorized();
const uncat = Discourse.Category.findUncategorized();
if (uncat && uncat.get('name') === item.text) {
category = uncat;
}
@ -56,15 +56,16 @@ export default ComboboxView.extend({
}
if (!category) return item.text;
var result = categoryBadgeHTML(category, {link: false, allowUncategorized: true, hideParent: true}),
parentCategoryId = category.get('parent_category_id');
let result = categoryBadgeHTML(category, {link: false, allowUncategorized: true, hideParent: true});
const parentCategoryId = category.get('parent_category_id');
if (parentCategoryId) {
result = categoryBadgeHTML(Discourse.Category.findById(parentCategoryId), {link: false}) + "&nbsp;" + result;
}
result += " <span class='topic-count'>&times; " + category.get('topic_count') + "</span>";
var description = category.get('description');
const description = category.get('description');
// TODO wtf how can this be null?;
if (description && description !== 'null') {
result += '<div class="category-desc">' +

View File

@ -125,7 +125,6 @@ const ComposerView = Discourse.View.extend(Ember.Evented, {
this.set('controller.view', this);
positioningWorkaround(this.$());
}.on('didInsertElement'),
_unlinkView: function() {
@ -520,19 +519,25 @@ const ComposerView = Discourse.View.extend(Ember.Evented, {
},
childDidInsertElement() {
return this.initEditor();
this.initEditor();
// Disable links in the preview
$('#wmd-preview').on('click.preview', (e) => {
e.preventDefault();
return false;
});
},
childWillDestroyElement() {
const self = this;
this._unbindUploadTarget();
Em.run.next(function() {
$('#wmd-preview').off('click.preview');
Em.run.next(() => {
$('#main-outlet').css('padding-bottom', 0);
// need to wait a bit for the "slide down" transition of the composer
Em.run.later(function() {
self.appEvents.trigger("composer:closed");
Em.run.later(() => {
this.appEvents.trigger("composer:closed");
}, 400);
});
},

View File

@ -8,15 +8,13 @@ export default ModalBodyView.extend({
}.property('controller.flagTopic'),
selectedChanged: function() {
const self = this;
Em.run.next(() => {
this.$("input[type='radio']").prop('checked', false);
Em.run.next(function() {
self.$("input[type='radio']").prop('checked', false);
const nameKey = self.get('controller.selected.name_key');
const nameKey = this.get('controller.selected.name_key');
if (!nameKey) { return; }
self.$('#radio_' + nameKey).prop('checked', 'true');
this.$('#radio_' + nameKey).prop('checked', 'true');
});
}.observes('controller.selected.name_key')
});

View File

@ -125,6 +125,7 @@ export default Discourse.View.extend({
$(document).unbind('touchmove.discourse-dock');
this.$('a.unread-private-messages, a.unread-notifications, a[data-notifications]').off('click.notifications');
this.$('a[data-dropdown]').off('click.dropdown');
$('body').off('keydown.header');
}.on('willDestroyElement'),
_setup: function() {

View File

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

View File

@ -6,11 +6,11 @@ export default ButtonView.extend({
attributeBindings: ['disabled'],
disabled: Em.computed.or('controller.archived', 'controller.closed', 'controller.deleted'),
renderIcon: function(buffer) {
renderIcon(buffer) {
buffer.push("<i class='fa fa-users'></i>");
},
click: function() {
return this.get('controller').send('showInvite');
click() {
this.get('controller').send('showInvite');
}
});

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