diff --git a/.eslintrc b/.eslintrc
index bfbe34ea65..218cb72b81 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -41,11 +41,12 @@
"visible":true,
"invisible":true,
"asyncRender":true,
- "selectDropdown":true,
"selectKit":true,
"expandSelectKit":true,
"collapseSelectKit":true,
- "selectKitSelectRow":true,
+ "selectKitSelectRowByValue":true,
+ "selectKitSelectRowByName":true,
+ "selectKitSelectRowByIndex":true,
"selectKitSelectNoneRow":true,
"selectKitFillInFilter":true,
"asyncTestDiscourse":true,
diff --git a/.travis.yml b/.travis.yml
index e4f402f801..73ae46e5b0 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -21,8 +21,11 @@ addons:
matrix:
fast_finish: true
+ allow_failures:
+ - rvm: 2.5.0
rvm:
+ - 2.5.0
- 2.4.2
- 2.3.4
@@ -45,6 +48,7 @@ before_install:
- git clone --depth=1 https://github.com/discourse/discourse-canned-replies.git plugins/discourse-canned-replies
- git clone --depth=1 https://github.com/discourse/discourse-chat-integration.git plugins/discourse-chat-integration
- git clone --depth=1 https://github.com/discourse/discourse-assign.git plugins/discourse-assign
+ - git clone --depth=1 https://github.com/discourse/discourse-patreon.git plugins/discourse-patreon
- export PATH=$HOME/.yarn/bin:$PATH
install:
@@ -67,7 +71,7 @@ script:
bundle exec rake db:create db:migrate
if [ '$QUNIT_RUN' == '1' ]; then
- bundle exec rake qunit:test['400000']
+ bundle exec rake qunit:test['400000'] && \
bundle exec rake plugin:spec
else
bundle exec rspec && bundle exec rake plugin:spec
diff --git a/Gemfile b/Gemfile
index f242cd5692..e558ffb47c 100644
--- a/Gemfile
+++ b/Gemfile
@@ -36,7 +36,7 @@ gem 'redis-namespace'
gem 'active_model_serializers', '~> 0.8.3'
-gem 'onebox', '1.8.30'
+gem 'onebox', '1.8.33'
gem 'http_accept_language', '~>2.0.5', require: false
diff --git a/Gemfile.lock b/Gemfile.lock
index a763d7c89d..529aa37e43 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -232,7 +232,7 @@ GEM
omniauth-twitter (1.3.0)
omniauth-oauth (~> 1.1)
rack
- onebox (1.8.30)
+ onebox (1.8.33)
fast_blank (>= 1.0.0)
htmlentities (~> 4.3)
moneta (~> 1.0)
@@ -469,7 +469,7 @@ DEPENDENCIES
omniauth-oauth2
omniauth-openid
omniauth-twitter
- onebox (= 1.8.30)
+ onebox (= 1.8.33)
openid-redis-store
pg
pry-nav
diff --git a/app/assets/javascripts/admin/controllers/modals/admin-add-upload.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-add-upload.js.es6
index a26d927873..7bbdaa3de5 100644
--- a/app/assets/javascripts/admin/controllers/modals/admin-add-upload.js.es6
+++ b/app/assets/javascripts/admin/controllers/modals/admin-add-upload.js.es6
@@ -16,7 +16,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
@computed('name')
nameValid(name) {
- return name && name.match(/\A[a-z_][a-z0-9_-]*\z/i);
+ return name && name.match(/^[a-z_][a-z0-9_-]*$/i);
},
@observes('name')
diff --git a/app/assets/javascripts/admin/routes/admin-backups.js.es6 b/app/assets/javascripts/admin/routes/admin-backups.js.es6
index 6d1a425190..92ac4652fa 100644
--- a/app/assets/javascripts/admin/routes/admin-backups.js.es6
+++ b/app/assets/javascripts/admin/routes/admin-backups.js.es6
@@ -68,7 +68,7 @@ export default Discourse.Route.extend({
function(confirmed) {
if (confirmed) {
backup.destroy().then(function() {
- self.controllerFor("adminBackupsIndex").removeObject(backup);
+ self.controllerFor("adminBackupsIndex").get('model').removeObject(backup);
});
}
}
diff --git a/app/assets/javascripts/admin/templates/admin.hbs b/app/assets/javascripts/admin/templates/admin.hbs
index 01f9ffb673..af4aafde94 100644
--- a/app/assets/javascripts/admin/templates/admin.hbs
+++ b/app/assets/javascripts/admin/templates/admin.hbs
@@ -20,7 +20,9 @@
{{#if currentUser.admin}}
{{nav-item route='adminCustomize' label='admin.customize.title'}}
{{nav-item route='adminApi' label='admin.api.title'}}
- {{nav-item route='admin.backups' label='admin.backups.title'}}
+ {{#if siteSettings.enable_backups}}
+ {{nav-item route='admin.backups' label='admin.backups.title'}}
+ {{/if}}
{{/if}}
{{nav-item route='adminPlugins' label='admin.plugins.title'}}
{{plugin-outlet name="admin-menu" connectorTagName="li"}}
diff --git a/app/assets/javascripts/admin/templates/customize-themes-show.hbs b/app/assets/javascripts/admin/templates/customize-themes-show.hbs
index 6f8ac614aa..7f339d63d7 100644
--- a/app/assets/javascripts/admin/templates/customize-themes-show.hbs
+++ b/app/assets/javascripts/admin/templates/customize-themes-show.hbs
@@ -1,5 +1,5 @@
-
+
{{#if editingName}}
{{text-field value=model.name autofocus="true"}}
{{d-button action="finishedEditingName" class="btn-primary btn-small submit-edit" icon="check"}}
@@ -7,7 +7,7 @@
{{else}}
{{model.name}} {{d-icon "pencil"}}
{{/if}}
-
+
{{#if model.remote_theme}}
diff --git a/app/assets/javascripts/discourse/components/badge-button.js.es6 b/app/assets/javascripts/discourse/components/badge-button.js.es6
index c32e1f1cbb..58b0d1358c 100644
--- a/app/assets/javascripts/discourse/components/badge-button.js.es6
+++ b/app/assets/javascripts/discourse/components/badge-button.js.es6
@@ -1,6 +1,6 @@
export default Ember.Component.extend({
tagName: 'span',
- classNameBindings: [':user-badge', 'badge.badgeTypeClassName'],
+ classNameBindings: [':user-badge', 'badge.badgeTypeClassName', 'badge.enabled::disabled'],
title: function(){
return $("
"+this.get('badge.description')+"
").text();
}.property('badge.description'),
diff --git a/app/assets/javascripts/discourse/components/composer-editor.js.es6 b/app/assets/javascripts/discourse/components/composer-editor.js.es6
index 6d56b6b915..968e1ade9a 100644
--- a/app/assets/javascripts/discourse/components/composer-editor.js.es6
+++ b/app/assets/javascripts/discourse/components/composer-editor.js.es6
@@ -1,5 +1,5 @@
import userSearch from 'discourse/lib/user-search';
-import { default as computed, on } from 'ember-addons/ember-computed-decorators';
+import { default as computed, observes, on } from 'ember-addons/ember-computed-decorators';
import { linkSeenMentions, fetchUnseenMentions } from 'discourse/lib/link-mentions';
import { linkSeenCategoryHashtags, fetchUnseenCategoryHashtags } from 'discourse/lib/link-category-hashtags';
import { linkSeenTagHashtags, fetchUnseenTagHashtags } from 'discourse/lib/link-tag-hashtag';
@@ -36,6 +36,18 @@ export default Ember.Component.extend({
return `[${I18n.t('uploading')}]() `;
},
+ @observes('composer.uploadCancelled')
+ _cancelUpload() {
+ if (!this.get('composer.uploadCancelled')) { return; }
+ this.set('composer.uploadCancelled', false);
+
+ if (this._xhr) {
+ this._xhr._userCancelled = true;
+ this._xhr.abort();
+ }
+ this._resetUpload(true);
+ },
+
@computed
markdownOptions() {
return {
@@ -363,7 +375,7 @@ export default Ember.Component.extend({
const $e = $(e);
var name = $e.data('name');
if (found.indexOf(name) === -1){
- this.sendAction('groupsMentioned', [{name: name, user_count: $e.data('mentionable-user-count')}]);
+ this.sendAction('groupsMentioned', [{name: name, user_count: $e.data('mentionable-user-count'), max_mentions: $e.data('max-mentions')}]);
found.push(name);
}
});
@@ -401,7 +413,9 @@ export default Ember.Component.extend({
},
_resetUpload(removePlaceholder) {
- this._validUploads--;
+ if (this._validUploads > 0) {
+ this._validUploads--;
+ }
if (this._validUploads === 0) {
this.setProperties({ uploadProgress: 0, isUploading: false, isCancellable: false });
}
@@ -493,7 +507,7 @@ export default Ember.Component.extend({
this._xhr = null;
if (!userCancelled) {
- displayErrorForUpload(data.jqXHR.responseJSON);
+ displayErrorForUpload(data);
}
});
@@ -624,14 +638,6 @@ export default Ember.Component.extend({
this.sendAction('importQuote', toolbarEvent);
},
- cancelUpload() {
- if (this._xhr) {
- this._xhr._userCancelled = true;
- this._xhr.abort();
- }
- this._resetUpload(true);
- },
-
onExpandPopupMenuOptions(toolbarEvent) {
const selected = toolbarEvent.selected;
toolbarEvent.selectText(selected.start, selected.end - selected.start);
diff --git a/app/assets/javascripts/discourse/components/d-editor.js.es6 b/app/assets/javascripts/discourse/components/d-editor.js.es6
index f7ff2e51c1..3d357ec225 100644
--- a/app/assets/javascripts/discourse/components/d-editor.js.es6
+++ b/app/assets/javascripts/discourse/components/d-editor.js.es6
@@ -279,6 +279,7 @@ export default Ember.Component.extend({
const markdownOptions = this.get('markdownOptions') || {};
cookAsync(value, markdownOptions).then(cooked => {
+ if (this.get('isDestroyed')) { return; }
this.set('preview', cooked);
Ember.run.scheduleOnce('afterRender', () => {
if (this._state !== "inDOM") { return; }
@@ -632,7 +633,8 @@ export default Ember.Component.extend({
if (rows.length > 1) {
const columns = rows.map(r => r.split("\t").length);
- const isTable = columns.reduce((a, b) => a && columns[0] === b && b > 1);
+ const isTable = columns.reduce((a, b) => a && columns[0] === b && b > 1) &&
+ !(columns[0] === 2 && rows[0].split("\t")[0].match(/^•$|^\d+.$/)); // to skip tab delimited lists
if (isTable) {
const splitterRow = [...Array(columns[0])].map(() => "---").join("\t");
@@ -662,8 +664,6 @@ export default Ember.Component.extend({
if (table) {
this.appEvents.trigger('composer:insert-text', table);
handled = true;
- } else if (html && html.includes("urn:schemas-microsoft-com:office:word")) {
- html = ""; // use plain text data for microsoft word
}
}
diff --git a/app/assets/javascripts/discourse/components/latest-topic-list-item.js.es6 b/app/assets/javascripts/discourse/components/latest-topic-list-item.js.es6
index d8fd285316..6a78bf877d 100644
--- a/app/assets/javascripts/discourse/components/latest-topic-list-item.js.es6
+++ b/app/assets/javascripts/discourse/components/latest-topic-list-item.js.es6
@@ -1,7 +1,21 @@
-import { showEntrance } from "discourse/components/topic-list-item";
+import { showEntrance, navigateToTopic } from "discourse/components/topic-list-item";
export default Ember.Component.extend({
- click: showEntrance,
attributeBindings: ['topic.id:data-topic-id'],
- classNameBindings: [':latest-topic-list-item', 'topic.archived', 'topic.visited']
+ classNameBindings: [':latest-topic-list-item', 'topic.archived', 'topic.visited'],
+
+ showEntrance,
+ navigateToTopic,
+
+ click(e) {
+ // for events undefined has a different meaning than false
+ if (this.showEntrance(e) === false) {
+ return false;
+ }
+
+ return this.unhandledRowClick(e, this.get('topic'));
+ },
+
+ // Can be overwritten by plugins to handle clicks on other parts of the row
+ unhandledRowClick() { },
});
diff --git a/app/assets/javascripts/discourse/components/topic-list-item.js.es6 b/app/assets/javascripts/discourse/components/topic-list-item.js.es6
index 7793fbd1b9..f9b3cd38b7 100644
--- a/app/assets/javascripts/discourse/components/topic-list-item.js.es6
+++ b/app/assets/javascripts/discourse/components/topic-list-item.js.es6
@@ -20,6 +20,12 @@ export function showEntrance(e) {
}
}
+export function navigateToTopic(topic, href) {
+ this.appEvents.trigger('header:update-topic', topic);
+ DiscourseURL.routeTo(href || topic.get('url'));
+ return false;
+}
+
export default Ember.Component.extend(bufferedRender({
rerenderTriggers: ['bulkSelectEnabled', 'topic.pinned'],
tagName: 'tr',
@@ -107,8 +113,10 @@ export default Ember.Component.extend(bufferedRender({
return false;
}.property(),
+ showEntrance,
+
click(e) {
- const result = showEntrance.call(this, e);
+ const result = this.showEntrance(e);
if (result === false) { return result; }
const topic = this.get('topic');
@@ -124,19 +132,23 @@ export default Ember.Component.extend(bufferedRender({
}
if (target.hasClass('raw-topic-link')) {
- if (wantsNewWindow(e)) { return true; }
-
- this.appEvents.trigger('header:update-topic', topic);
- DiscourseURL.routeTo(target.attr('href'));
- return false;
+ if (wantsNewWindow(e)) { return true; }
+ return this.navigateToTopic(topic, target.attr('href'));
}
if (target.closest('a.topic-status').length === 1) {
this.get('topic').togglePinnedForUser();
return false;
}
+
+ return this.unhandledRowClick(e, topic);
},
+ navigateToTopic,
+
+ // Can be overwritten by plugins to handle clicks on other parts of the row
+ unhandledRowClick() { },
+
highlight(opts = { isLastViewedTopic: false }) {
const $topic = this.$();
$topic
diff --git a/app/assets/javascripts/discourse/components/user-card-contents.js.es6 b/app/assets/javascripts/discourse/components/user-card-contents.js.es6
index 238cd97495..b7d3b4d31f 100644
--- a/app/assets/javascripts/discourse/components/user-card-contents.js.es6
+++ b/app/assets/javascripts/discourse/components/user-card-contents.js.es6
@@ -115,8 +115,7 @@ export default Ember.Component.extend(CleansUp, CanCheckEmails, {
return false;
}
- // XSS protection (should be encapsulated)
- username = username.toString().replace(/[^A-Za-z0-9_\.\-]/g, "");
+ username = Ember.Handlebars.Utils.escapeExpression(username.toString());
// Don't show on mobile
if (this.site.mobileView) {
diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6
index 20b8736c6f..d08f59c9f4 100644
--- a/app/assets/javascripts/discourse/controllers/composer.js.es6
+++ b/app/assets/javascripts/discourse/controllers/composer.js.es6
@@ -49,6 +49,10 @@ function loadDraft(store, opts) {
const _popupMenuOptionsCallbacks = [];
+export function clearPopupMenuOptionsCallback() {
+ _popupMenuOptionsCallbacks.length = 0;
+}
+
export function addPopupMenuOptionsCallback(callback) {
_popupMenuOptionsCallbacks.push(callback);
}
@@ -220,6 +224,10 @@ export default Ember.Controller.extend({
},
actions: {
+ cancelUpload() {
+ this.set('model.uploadCancelled', true);
+ },
+
onPopupMenuAction(action) {
this.send(action);
},
@@ -378,11 +386,21 @@ export default Ember.Controller.extend({
groupsMentioned(groups) {
if (!this.get('model.creatingPrivateMessage') && !this.get('model.topic.isPrivateMessage')) {
groups.forEach(group => {
- const body = I18n.t('composer.group_mentioned', {
- group: "@" + group.name,
- count: group.user_count,
- group_link: Discourse.getURL(`/groups/${group.name}/members`)
- });
+ let body;
+
+ if (group.max_mentions < group.user_count) {
+ body = I18n.t('composer.group_mentioned_limit', {
+ group: "@" + group.name,
+ max: group.max_mentions,
+ group_link: Discourse.getURL(`/groups/${group.name}/members`)
+ });
+ } else {
+ body = I18n.t('composer.group_mentioned', {
+ group: "@" + group.name,
+ count: group.user_count,
+ group_link: Discourse.getURL(`/groups/${group.name}/members`)
+ });
+ }
this.appEvents.trigger('composer-messages:create', {
extraClass: 'custom-body',
diff --git a/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 b/app/assets/javascripts/discourse/controllers/full-page-search.js.es6
index cdbc957b15..741e74a897 100644
--- a/app/assets/javascripts/discourse/controllers/full-page-search.js.es6
+++ b/app/assets/javascripts/discourse/controllers/full-page-search.js.es6
@@ -151,6 +151,12 @@ export default Ember.Controller.extend({
this.set("application.showFooter", !this.get("loading"));
},
+ @computed('resultCount', 'noSortQ')
+ resultCountLabel(count, term) {
+ const plus = (count % 50 === 0 ? "+" : "");
+ return I18n.t('search.result_count', {count, plus, term});
+ },
+
@observes('model.posts.length')
resultCountChanged() {
this.set("resultCount", this.get("model.posts.length"));
diff --git a/app/assets/javascripts/discourse/controllers/preferences/account.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/account.js.es6
index 00aeb2f8f0..9dc6083f15 100644
--- a/app/assets/javascripts/discourse/controllers/preferences/account.js.es6
+++ b/app/assets/javascripts/discourse/controllers/preferences/account.js.es6
@@ -10,6 +10,7 @@ export default Ember.Controller.extend(CanCheckEmails, PreferencesTabController,
saveAttrNames: ['name'],
canEditName: setting('enable_names'),
+ canSaveUser: true,
newNameInput: null,
diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6
index a2a8392ba5..80209d7b73 100644
--- a/app/assets/javascripts/discourse/controllers/topic.js.es6
+++ b/app/assets/javascripts/discourse/controllers/topic.js.es6
@@ -272,7 +272,7 @@ export default Ember.Controller.extend(BufferedContent, {
const quoteState = this.get('quoteState');
const postStream = this.get('model.postStream');
- if (!postStream) return;
+ if (!postStream || !topic || !topic.get('details.can_create_post')) { return; }
const quotedPost = postStream.findLoadedPost(quoteState.postId);
const quotedText = Quote.build(quotedPost, quoteState.buffer);
diff --git a/app/assets/javascripts/discourse/initializers/localization.js.es6 b/app/assets/javascripts/discourse/initializers/localization.js.es6
index 5d3ce55f98..2603ddaf4b 100644
--- a/app/assets/javascripts/discourse/initializers/localization.js.es6
+++ b/app/assets/javascripts/discourse/initializers/localization.js.es6
@@ -55,7 +55,9 @@ export default {
node = node[segs[i]];
}
- node[segs[segs.length-1]] = v;
+ if (typeof node === "object") {
+ node[segs[segs.length-1]] = v;
+ }
});
}
diff --git a/app/assets/javascripts/discourse/lib/click-track.js.es6 b/app/assets/javascripts/discourse/lib/click-track.js.es6
index 648b49299b..6247973d45 100644
--- a/app/assets/javascripts/discourse/lib/click-track.js.es6
+++ b/app/assets/javascripts/discourse/lib/click-track.js.es6
@@ -26,7 +26,7 @@ export default {
}
// don't track links in quotes or in elided part
- if ($link.parents('aside.quote,.elided').length) { return true; }
+ let tracking = $link.parents('aside.quote,.elided').length === 0;
let href = $link.attr('href') || $link.data('href');
@@ -39,26 +39,31 @@ export default {
const userId = $link.data('user-id') || $article.data('user-id');
const ownLink = userId && (userId === Discourse.User.currentProp('id'));
- let trackingUrl = Discourse.getURL('/clicks/track?url=' + encodeURIComponent(href));
+ let destUrl = href;
- if (postId && !$link.data('ignore-post-id')) {
- trackingUrl += "&post_id=" + encodeURI(postId);
- }
- if (topicId) {
- trackingUrl += "&topic_id=" + encodeURI(topicId);
- }
+ if (tracking) {
- // Update badge clicks unless it's our own
- if (!ownLink) {
- const $badge = $('span.badge', $link);
- if ($badge.length === 1) {
- // don't update counts in category badge nor in oneboxes (except when we force it)
- if (isValidLink($link)) {
- const html = $badge.html();
- const key = `${new Date().toLocaleDateString()}-${postId}-${href}`;
- if (/^\d+$/.test(html) && !sessionStorage.getItem(key)) {
- sessionStorage.setItem(key, true);
- $badge.html(parseInt(html, 10) + 1);
+ destUrl = Discourse.getURL('/clicks/track?url=' + encodeURIComponent(href));
+
+ if (postId && !$link.data('ignore-post-id')) {
+ destUrl += "&post_id=" + encodeURI(postId);
+ }
+ if (topicId) {
+ destUrl += "&topic_id=" + encodeURI(topicId);
+ }
+
+ // Update badge clicks unless it's our own
+ if (!ownLink) {
+ const $badge = $('span.badge', $link);
+ if ($badge.length === 1) {
+ // don't update counts in category badge nor in oneboxes (except when we force it)
+ if (isValidLink($link)) {
+ const html = $badge.html();
+ const key = `${new Date().toLocaleDateString()}-${postId}-${href}`;
+ if (/^\d+$/.test(html) && !sessionStorage.getItem(key)) {
+ sessionStorage.setItem(key, true);
+ $badge.html(parseInt(html, 10) + 1);
+ }
}
}
}
@@ -66,12 +71,12 @@ export default {
// If they right clicked, change the destination href
if (e.which === 3) {
- $link.attr('href', Discourse.SiteSettings.track_external_right_clicks ? trackingUrl : href);
+ $link.attr('href', Discourse.SiteSettings.track_external_right_clicks ? destUrl : href);
return true;
}
// if they want to open in a new tab, do an AJAX request
- if (wantsNewWindow(e)) {
+ if (tracking && wantsNewWindow(e)) {
ajax("/clicks/track", {
data: {
url: href,
@@ -109,7 +114,7 @@ export default {
}
// If we're on the same site, use the router and track via AJAX
- if (DiscourseURL.isInternal(href) && !$link.hasClass('attachment')) {
+ if (tracking && DiscourseURL.isInternal(href) && !$link.hasClass('attachment')) {
ajax("/clicks/track", {
data: {
url: href,
@@ -125,9 +130,9 @@ export default {
// Otherwise, use a custom URL with a redirect
if (Discourse.User.currentProp('external_links_in_new_tab')) {
- window.open(trackingUrl, '_blank').focus();
+ window.open(destUrl, '_blank').focus();
} else {
- DiscourseURL.redirectTo(trackingUrl);
+ DiscourseURL.redirectTo(destUrl);
}
return false;
diff --git a/app/assets/javascripts/discourse/lib/formatter.js.es6 b/app/assets/javascripts/discourse/lib/formatter.js.es6
index b26b270b2c..dcce95ca99 100644
--- a/app/assets/javascripts/discourse/lib/formatter.js.es6
+++ b/app/assets/javascripts/discourse/lib/formatter.js.es6
@@ -310,8 +310,10 @@ export function number(val) {
if (val > 999999) {
formattedNumber = I18n.toNumber(val / 1000000, {precision: 1});
return I18n.t("number.short.millions", {number: formattedNumber});
- }
- if (val > 999) {
+ } else if (val > 99999) {
+ formattedNumber = I18n.toNumber(val / 1000, {precision: 0});
+ return I18n.t("number.short.thousands", {number: formattedNumber});
+ } else if (val > 999) {
formattedNumber = I18n.toNumber(val / 1000, {precision: 1});
return I18n.t("number.short.thousands", {number: formattedNumber});
}
diff --git a/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 b/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6
index b011f8bab0..c3a7ecd27c 100644
--- a/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6
+++ b/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6
@@ -169,7 +169,9 @@ export default {
},
createTopic() {
- this.container.lookup('controller:composer').open({action: Composer.CREATE_TOPIC, draftKey: Composer.CREATE_TOPIC});
+ if (this.currentUser && this.currentUser.can_create_topic) {
+ this.container.lookup('controller:composer').open({action: Composer.CREATE_TOPIC, draftKey: Composer.CREATE_TOPIC});
+ }
},
pinUnpinTopic() {
diff --git a/app/assets/javascripts/discourse/lib/link-mentions.js.es6 b/app/assets/javascripts/discourse/lib/link-mentions.js.es6
index 688207c5b9..8aa802f80f 100644
--- a/app/assets/javascripts/discourse/lib/link-mentions.js.es6
+++ b/app/assets/javascripts/discourse/lib/link-mentions.js.es6
@@ -2,13 +2,15 @@ import { ajax } from 'discourse/lib/ajax';
import { userPath } from 'discourse/lib/url';
import { formatUsername } from 'discourse/lib/utilities';
+let maxGroupMention;
+
function replaceSpan($e, username, opts) {
let extra = "";
let extraClass = "";
if (opts && opts.group) {
if (opts.mentionable) {
- extra = `data-name='${username}' data-mentionable-user-count='${opts.mentionable.user_count}'`;
+ extra = `data-name='${username}' data-mentionable-user-count='${opts.mentionable.user_count}' data-max-mentions='${maxGroupMention}'`;
extraClass = "notify";
}
$e.replaceWith(``);
@@ -61,6 +63,7 @@ export function fetchUnseenMentions(usernames, topic_id) {
r.valid_groups.forEach(vg => foundGroups[vg] = true);
r.mentionable_groups.forEach(mg => mentionableGroups[mg.name] = mg);
r.cannot_see.forEach(cs => cannotSee[cs] = true);
+ maxGroupMention = r.max_users_notified_per_group_mention;
usernames.forEach(u => checked[u] = true);
return r;
});
diff --git a/app/assets/javascripts/discourse/lib/to-markdown.js.es6 b/app/assets/javascripts/discourse/lib/to-markdown.js.es6
index 8f81536f22..2fd90c59e0 100644
--- a/app/assets/javascripts/discourse/lib/to-markdown.js.es6
+++ b/app/assets/javascripts/discourse/lib/to-markdown.js.es6
@@ -2,17 +2,23 @@ import parseHTML from 'discourse/helpers/parse-html';
const trimLeft = text => text.replace(/^\s+/,"");
const trimRight = text => text.replace(/\s+$/,"");
+const countPipes = text => text.replace(/\\\|/,"").match(/\|/g).length;
class Tag {
- constructor(name, prefix = "", suffix = "") {
+ constructor(name, prefix = "", suffix = "", inline = false) {
this.name = name;
this.prefix = prefix;
this.suffix = suffix;
+ this.inline = inline;
}
decorate(text) {
if (this.prefix || this.suffix) {
- return [this.prefix, text, this.suffix].join("");
+ text = [this.prefix, text, this.suffix].join("");
+ }
+
+ if (this.inline) {
+ text = " " + text + " ";
}
return text;
@@ -30,7 +36,7 @@ class Tag {
static blocks() {
return ["address", "article", "aside", "dd", "div", "dl", "dt", "fieldset", "figcaption", "figure",
- "footer", "form", "header", "hgroup", "hr", "main", "nav", "p", "pre", "section", "ul"];
+ "footer", "form", "header", "hgroup", "hr", "main", "nav", "p", "pre", "section"];
}
static headings() {
@@ -38,25 +44,26 @@ class Tag {
}
static emphases() {
- return [ ["b", "**"], ["strong", "**"], ["i", "_"], ["em", "_"], ["s", "~~"], ["strike", "~~"] ];
+ return [ ["b", "**"], ["strong", "**"], ["i", "*"], ["em", "*"], ["s", "~~"], ["strike", "~~"] ];
}
static slices() {
- return ["dt", "dd", "tr", "thead", "tbody", "tfoot"];
+ return ["dt", "dd", "thead", "tbody", "tfoot"];
}
static trimmable() {
- return [...Tag.blocks(), ...Tag.headings(), ...Tag.slices(), "li", "td", "th", "br", "hr", "blockquote", "table", "ol"];
+ return [...Tag.blocks(), ...Tag.headings(), ...Tag.slices(), "li", "td", "th", "br", "hr", "blockquote", "table", "ol", "tr", "ul"];
}
static block(name, prefix, suffix) {
return class extends Tag {
constructor() {
super(name, prefix, suffix);
+ this.gap = "\n\n";
}
decorate(text) {
- return `\n\n${this.prefix}${text}${this.suffix}\n\n`;
+ return `${this.gap}${this.prefix}${text}${this.suffix}${this.gap}`;
}
};
}
@@ -69,18 +76,21 @@ class Tag {
static emphasis(name, decorator) {
return class extends Tag {
constructor() {
- super(name, decorator, decorator);
+ super(name, decorator, decorator, true);
}
decorate(text) {
- text = text.trim();
-
if (text.includes("\n")) {
this.prefix = `<${this.name}>`;
this.suffix = `${this.name}>`;
}
- return super.decorate(text);
+ let space = text.match(/^\s/) || [""];
+ this.prefix = space[0] + this.prefix;
+ space = text.match(/\s$/) || [""];
+ this.suffix = this.suffix + space[0];
+
+ return super.decorate(text.trim());
}
};
}
@@ -109,7 +119,7 @@ class Tag {
static link() {
return class extends Tag {
constructor() {
- super("a");
+ super("a", "", "", true);
}
decorate(text) {
@@ -128,7 +138,7 @@ class Tag {
static image() {
return class extends Tag {
constructor() {
- super("img");
+ super("img", "", "", true);
}
toMarkdown() {
@@ -143,7 +153,8 @@ class Tag {
const height = attr.height || pAttr.height;
if (width && height) {
- alt = `${alt}|${width}x${height}`;
+ const pipe = this.element.parentNames.includes("table") ? "\\|" : "|";
+ alt = `${alt}${pipe}${width}x${height}`;
}
return "";
@@ -178,14 +189,10 @@ class Tag {
toMarkdown() {
const text = this.element.innerMarkdown().trim();
- if (text.includes("\n") || text.includes("[![")) {
+ if (text.includes("\n")) {
throw "Unsupported format inside Markdown table cells";
}
- if (!this.element.next) {
- this.suffix = "|";
- }
-
return this.decorate(text);
}
};
@@ -194,7 +201,7 @@ class Tag {
static li() {
return class extends Tag.slice("li", "\n") {
decorate(text) {
- const indent = this.element.filterParentNames(["ol", "ul"]).slice(1).map(() => " ").join("");
+ const indent = this.element.filterParentNames(["ol", "ul"]).slice(1).map(() => "\t").join("");
return super.decorate(`${indent}* ${trimLeft(text)}`);
}
};
@@ -210,6 +217,8 @@ class Tag {
if (this.element.parentNames.includes("pre")) {
this.prefix = '\n\n```\n';
this.suffix = '\n```\n\n';
+ } else {
+ this.inline = true;
}
text = $('
').html(text).text();
@@ -234,19 +243,45 @@ class Tag {
static table() {
return class extends Tag.block("table") {
decorate(text) {
- text = super.decorate(text);
- const splitterRow = text.split("|\n")[0].match(/\|/g).map(() => "| --- ").join("") + "|\n";
- text = text.replace("|\n", "|\n" + splitterRow).replace(/\|\n{2,}\|/g, "|\n|");
+ text = super.decorate(text).replace(/\|\n{2,}\|/g, "|\n|");
+ const rows = text.trim().split("\n");
+ const pipeCount = countPipes(rows[0]);
+ const isValid = rows.length > 1 &&
+ pipeCount > 2 &&
+ rows.reduce((a, c) => a && countPipes(c) <= pipeCount);
+
+ if (!isValid) {
+ throw "Unsupported table format for Markdown conversion";
+ }
+
+ const splitterRow = [...Array(pipeCount-1)].map(() => "| --- ").join("") + "|\n";
+ text = text.replace("|\n", "|\n" + splitterRow);
+
return text;
}
};
}
+ static list(name) {
+ return class extends Tag.block(name) {
+ decorate(text) {
+ let smallGap = "";
+
+ if (this.element.filterParentNames(["li"]).length) {
+ this.gap = "";
+ smallGap = "\n";
+ }
+
+ return smallGap + super.decorate(trimRight(text));
+ }
+ };
+ }
+
static ol() {
- return class extends Tag.block("ol") {
+ return class extends Tag.list("ol") {
decorate(text) {
text = "\n" + text;
- const bullet = text.match(/\n *\*/)[0];
+ const bullet = text.match(/\n\t*\*/)[0];
for (let i = parseInt(this.element.attributes.start || 1); text.includes(bullet); i++) {
text = text.replace(bullet, bullet.replace("*", `${i}.`));
@@ -257,6 +292,17 @@ class Tag {
};
}
+ static tr() {
+ return class extends Tag.slice("tr", "|\n") {
+ decorate(text) {
+ if (!this.element.next) {
+ this.suffix = "|";
+ }
+ return `${text}${this.suffix}`;
+ }
+ };
+ }
+
}
const tags = [
@@ -267,7 +313,7 @@ const tags = [
Tag.cell("td"), Tag.cell("th"),
Tag.replace("br", "\n"), Tag.replace("hr", "\n---\n"), Tag.replace("head", ""),
Tag.keep("ins"), Tag.keep("del"), Tag.keep("small"), Tag.keep("big"),
- Tag.li(), Tag.link(), Tag.image(), Tag.code(), Tag.blockquote(), Tag.table(),, Tag.ol(),
+ Tag.li(), Tag.link(), Tag.image(), Tag.code(), Tag.blockquote(), Tag.table(), Tag.tr(), Tag.ol(), Tag.list("ul"),
];
class Element {
@@ -364,6 +410,19 @@ class Element {
}
}
+function trimUnwantedSpaces(html) {
+ const body = html.match(/]*>([\s\S]*?)<\/body>/);
+ html = body ? body[1] : html;
+ html = html.replace(/\r|\n| /g, " ");
+
+ let match;
+ while (match = html.match(/<[^\s>]+[^>]*>\s{2,}<[^\s>]+[^>]*>/)) {
+ html = html.replace(match[0], match[0].replace(/>\s{2,}, "> <"));
+ }
+
+ return html;
+}
+
function putPlaceholders(html) {
const codeRegEx = /
]*>([\s\S]*?)<\/code>/gi;
const origHtml = html;
@@ -379,7 +438,7 @@ function putPlaceholders(html) {
match = codeRegEx.exec(origHtml);
}
- const elements = parseHTML(html);
+ const elements = parseHTML(trimUnwantedSpaces(html));
return { elements, placeholders };
}
@@ -395,7 +454,7 @@ export default function toMarkdown(html) {
const { elements, placeholders } = putPlaceholders(html);
let markdown = Element.parse(elements).trim();
markdown = markdown.replace(/^/, "").replace(/<\/b>$/, "").trim(); // fix for google doc copy paste
- markdown = markdown.replace(/\r/g, "").replace(/\n \n/g, "\n\n").replace(/\n{3,}/g, "\n\n");
+ markdown = markdown.replace(/\n +/g, "\n").replace(/ +\n/g, "\n").replace(/ {2,}/g, " ").replace(/\n{3,}/g, "\n\n").replace(/\t/g, " ");
return replacePlaceholders(markdown, placeholders);
} catch(err) {
return "";
diff --git a/app/assets/javascripts/discourse/lib/utilities.js.es6 b/app/assets/javascripts/discourse/lib/utilities.js.es6
index dde738d9a8..33558befac 100644
--- a/app/assets/javascripts/discourse/lib/utilities.js.es6
+++ b/app/assets/javascripts/discourse/lib/utilities.js.es6
@@ -334,27 +334,27 @@ export function getUploadMarkdown(upload) {
}
export function displayErrorForUpload(data) {
- // deal with meaningful errors first
if (data.jqXHR) {
switch (data.jqXHR.status) {
// cancelled by the user
- case 0: return;
+ case 0:
+ return;
- // entity too large, usually returned from the web server
+ // entity too large, usually returned from the web server
case 413:
- var type = uploadTypeFromFileName(data.files[0].name);
- var maxSizeKB = Discourse.SiteSettings['max_' + type + '_size_kb'];
- bootbox.alert(I18n.t('post.errors.file_too_large', { max_size_kb: maxSizeKB }));
- return;
+ const type = uploadTypeFromFileName(data.files[0].name);
+ const max_size_kb = Discourse.SiteSettings[`max_${type}_size_kb`];
+ bootbox.alert(I18n.t('post.errors.file_too_large', { max_size_kb }));
+ return;
- // the error message is provided by the server
+ // the error message is provided by the server
case 422:
- if (data.jqXHR.responseJSON.message) {
- bootbox.alert(data.jqXHR.responseJSON.message);
- } else {
- bootbox.alert(data.jqXHR.responseJSON.join("\n"));
- }
- return;
+ if (data.jqXHR.responseJSON.message) {
+ bootbox.alert(data.jqXHR.responseJSON.message);
+ } else {
+ bootbox.alert(data.jqXHR.responseJSON.join("\n"));
+ }
+ return;
}
} else if (data.errors && data.errors.length > 0) {
bootbox.alert(data.errors.join("\n"));
diff --git a/app/assets/javascripts/discourse/mixins/upload.js.es6 b/app/assets/javascripts/discourse/mixins/upload.js.es6
index 1dc55eeb85..eec73d8c3d 100644
--- a/app/assets/javascripts/discourse/mixins/upload.js.es6
+++ b/app/assets/javascripts/discourse/mixins/upload.js.es6
@@ -56,7 +56,7 @@ export default Em.Mixin.create({
});
$upload.on("fileuploadfail", (e, data) => {
- displayErrorForUpload(data.jqXHR.responseJSON);
+ displayErrorForUpload(data);
reset();
});
}.on("didInsertElement"),
diff --git a/app/assets/javascripts/discourse/routes/build-private-messages-route.js.es6 b/app/assets/javascripts/discourse/routes/build-private-messages-route.js.es6
index 63fa6d56e8..b20e9e1b3d 100644
--- a/app/assets/javascripts/discourse/routes/build-private-messages-route.js.es6
+++ b/app/assets/javascripts/discourse/routes/build-private-messages-route.js.es6
@@ -32,7 +32,10 @@ export default (viewName, path) => {
},
deactivate() {
- this.searchService.set('contextType', 'private_messages');
+ this.searchService.set(
+ 'searchContext',
+ this.controllerFor("user").get("model.searchContext")
+ );
}
});
};
diff --git a/app/assets/javascripts/discourse/routes/topic.js.es6 b/app/assets/javascripts/discourse/routes/topic.js.es6
index 5086722534..9cc709d197 100644
--- a/app/assets/javascripts/discourse/routes/topic.js.es6
+++ b/app/assets/javascripts/discourse/routes/topic.js.es6
@@ -20,11 +20,11 @@ const TopicRoute = Discourse.Route.extend({
titleToken() {
const model = this.modelFor('topic');
if (model) {
- const result = model.get('unicode_title') ? model.get('unicode_title') : model.get('title'),
+ const result = model.get('unicode_title') || model.get('title'),
cat = model.get('category');
// Only display uncategorized in the title tag if it was renamed
- if (cat && !(cat.get('isUncategorizedCategory') && cat.get('name').toLowerCase() === "uncategorized")) {
+ if (this.siteSettings.topic_page_title_includes_category && cat && !(cat.get('isUncategorizedCategory') && cat.get('name').toLowerCase() === "uncategorized")) {
let catName = cat.get('name');
const parentCategory = cat.get('parentCategory');
diff --git a/app/assets/javascripts/discourse/templates/components/d-editor.hbs b/app/assets/javascripts/discourse/templates/components/d-editor.hbs
index 6029c6395b..6133b28f8f 100644
--- a/app/assets/javascripts/discourse/templates/components/d-editor.hbs
+++ b/app/assets/javascripts/discourse/templates/components/d-editor.hbs
@@ -17,7 +17,7 @@
{{toolbar-popup-menu-options
onPopupMenuAction=onPopupMenuAction
onExpand=(action b.action b)
- title="composer.options"
+ title=b.title
headerIcon=b.icon
class=b.className
content=popupMenuOptions}}
diff --git a/app/assets/javascripts/discourse/templates/components/edit-category-security.hbs b/app/assets/javascripts/discourse/templates/components/edit-category-security.hbs
index fb0a13c213..ad078cda2f 100644
--- a/app/assets/javascripts/discourse/templates/components/edit-category-security.hbs
+++ b/app/assets/javascripts/discourse/templates/components/edit-category-security.hbs
@@ -18,7 +18,7 @@
{{#if category.availableGroups}}
{{combo-box class="available-groups"
allowInitialValueMutation=true
- allowsContentReplacement=true
+ allowContentReplacement=true
content=category.availableGroups
value=selectedGroup}}
{{combo-box allowInitialValueMutation=true
diff --git a/app/assets/javascripts/discourse/templates/full-page-search.hbs b/app/assets/javascripts/discourse/templates/full-page-search.hbs
index 8f7839ace2..01edd14023 100644
--- a/app/assets/javascripts/discourse/templates/full-page-search.hbs
+++ b/app/assets/javascripts/discourse/templates/full-page-search.hbs
@@ -51,7 +51,7 @@
- {{{i18n "search.result_count" count=resultCount term=noSortQ}}}
+ {{{resultCountLabel}}}
diff --git a/app/assets/javascripts/discourse/templates/preferences/account.hbs b/app/assets/javascripts/discourse/templates/preferences/account.hbs
index cded185e59..38569dd9bd 100644
--- a/app/assets/javascripts/discourse/templates/preferences/account.hbs
+++ b/app/assets/javascripts/discourse/templates/preferences/account.hbs
@@ -3,13 +3,15 @@
{{model.username}}
{{#if model.can_edit_username}}
- {{#link-to "preferences.username" class="btn btn-small pad-left no-text"}}
+ {{#link-to "preferences.username" class="btn btn-small btn-icon pad-left no-text"}}
{{d-icon "pencil"}} {{/link-to}}
{{/if}}
-
- {{{i18n 'user.username.short_instructions' username=model.username}}}
-
+ {{#if siteSettings.enable_mentions}}
+
+ {{{i18n 'user.username.short_instructions' username=model.username}}}
+
+ {{/if}}
{{#if canEditName}}
@@ -35,7 +37,7 @@
{{model.email}}
{{#if model.can_edit_email}}
- {{#link-to "preferences.email" class="btn btn-small pad-left no-text"}}{{d-icon "pencil"}}{{/link-to}}
+ {{#link-to "preferences.email" class="btn btn-small btn-icon pad-left no-text"}}{{d-icon "pencil"}}{{/link-to}}
{{/if}}
@@ -82,22 +84,24 @@
{{i18n 'user.title.title'}}
{{model.title}}
- {{#link-to "preferences.badgeTitle" class="btn btn-small pad-left no-text"}}{{d-icon "pencil"}}{{/link-to}}
+ {{#link-to "preferences.badgeTitle" class="btn btn-small btn-icon pad-left no-text"}}{{d-icon "pencil"}}{{/link-to}}
{{/if}}
-{{plugin-outlet name="user-preferences-account" args=(hash model=model)}}
+{{plugin-outlet name="user-preferences-account" args=(hash model=model save=(action "save"))}}
{{plugin-outlet name="user-custom-controls" args=(hash model=model)}}
-