Version bump
This commit is contained in:
commit
8556622e2b
2
.gitignore
vendored
2
.gitignore
vendored
@ -4,8 +4,6 @@
|
||||
# or operating system, you probably want to add a global ignore instead:
|
||||
# git config --global core.excludesfile ~/.gitignore_global
|
||||
|
||||
tags
|
||||
|
||||
.DS_Store
|
||||
._.DS_Store
|
||||
dump.rdb
|
||||
|
||||
5
.mention-bot
Normal file
5
.mention-bot
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"maxReviewers": 2,
|
||||
"message": "Thanks @pullRequester for your pull request :+1:. By analyzing the blame information on this pull request, I identified @reviewers to be potential reviewers.",
|
||||
"requiredOrgs": ["discourse"]
|
||||
}
|
||||
4
Gemfile
4
Gemfile
@ -52,7 +52,7 @@ gem 'ember-source', '1.12.2'
|
||||
gem 'barber'
|
||||
gem 'babel-transpiler'
|
||||
|
||||
gem 'message_bus', '2.0.0.beta.5'
|
||||
gem 'message_bus', '2.0.0.beta.8'
|
||||
|
||||
gem 'rails_multisite'
|
||||
|
||||
@ -61,7 +61,7 @@ gem 'fast_xs'
|
||||
gem 'fast_xor'
|
||||
|
||||
# while we sort out https://github.com/sdsykes/fastimage/pull/46
|
||||
gem 'fastimage_discourse', require: 'fastimage'
|
||||
gem 'discourse_fastimage', require: 'fastimage'
|
||||
gem 'aws-sdk', require: false
|
||||
gem 'excon', require: false
|
||||
gem 'unf', require: false
|
||||
|
||||
10
Gemfile.lock
10
Gemfile.lock
@ -73,6 +73,7 @@ GEM
|
||||
diff-lcs (1.2.5)
|
||||
discourse-qunit-rails (0.0.9)
|
||||
railties
|
||||
discourse_fastimage (2.0.0)
|
||||
docile (1.1.5)
|
||||
domain_name (0.5.25)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
@ -107,7 +108,6 @@ GEM
|
||||
rake
|
||||
rake-compiler
|
||||
fast_xs (0.8.0)
|
||||
fastimage_discourse (1.6.6)
|
||||
ffi (1.9.10)
|
||||
flamegraph (0.1.0)
|
||||
fast_stack
|
||||
@ -156,7 +156,7 @@ GEM
|
||||
mail (2.6.4)
|
||||
mime-types (>= 1.16, < 4)
|
||||
memory_profiler (0.9.6)
|
||||
message_bus (2.0.0.beta.5)
|
||||
message_bus (2.0.0.beta.8)
|
||||
rack (>= 1.1.3)
|
||||
metaclass (0.0.4)
|
||||
method_source (0.8.2)
|
||||
@ -213,7 +213,7 @@ GEM
|
||||
omniauth-twitter (1.2.1)
|
||||
json (~> 1.3)
|
||||
omniauth-oauth (~> 1.1)
|
||||
onebox (1.5.38)
|
||||
onebox (1.5.39)
|
||||
htmlentities (~> 4.3.4)
|
||||
moneta (~> 0.8)
|
||||
multi_json (~> 1.11)
|
||||
@ -413,6 +413,7 @@ DEPENDENCIES
|
||||
byebug
|
||||
certified
|
||||
discourse-qunit-rails
|
||||
discourse_fastimage
|
||||
email_reply_trimmer (= 0.1.3)
|
||||
ember-rails (= 0.18.5)
|
||||
ember-source (= 1.12.2)
|
||||
@ -422,7 +423,6 @@ DEPENDENCIES
|
||||
fast_blank
|
||||
fast_xor
|
||||
fast_xs
|
||||
fastimage_discourse
|
||||
flamegraph
|
||||
foreman
|
||||
gctools
|
||||
@ -437,7 +437,7 @@ DEPENDENCIES
|
||||
lru_redux
|
||||
mail
|
||||
memory_profiler
|
||||
message_bus (= 2.0.0.beta.5)
|
||||
message_bus (= 2.0.0.beta.8)
|
||||
mime-types
|
||||
minitest
|
||||
mocha
|
||||
|
||||
@ -63,6 +63,6 @@ export default Ember.ArrayController.extend({
|
||||
**/
|
||||
hasMasterKey: function() {
|
||||
return !!this.get('model').findBy('user', null);
|
||||
}.property('model.@each')
|
||||
}.property('model.[]')
|
||||
|
||||
});
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
import AdminEmailLogsController from 'admin/controllers/admin-email-logs';
|
||||
import debounce from 'discourse/lib/debounce';
|
||||
import EmailLog from 'admin/models/email-log';
|
||||
|
||||
export default AdminEmailLogsController.extend({
|
||||
filterEmailLogs: debounce(function() {
|
||||
EmailLog.findAll(this.get("filter")).then(logs => this.set("model", logs));
|
||||
}, 250).observes("filter.{user,address,type,skipped_reason}")
|
||||
});
|
||||
@ -3,7 +3,8 @@ export default Ember.ArrayController.extend({
|
||||
|
||||
actions: {
|
||||
emojiUploaded(emoji) {
|
||||
this.pushObject(Em.Object.create(emoji));
|
||||
emoji.url += "?t=" + new Date().getTime();
|
||||
this.pushObject(Ember.Object.create(emoji));
|
||||
},
|
||||
|
||||
destroy(emoji) {
|
||||
|
||||
@ -26,6 +26,11 @@ export default Ember.Controller.extend({
|
||||
return arr.concat(this.site.groups.map((i) => {return {name: i['name'], value: i['id']};}));
|
||||
},
|
||||
|
||||
@computed('model.type')
|
||||
showCategoryOptions(modelType) {
|
||||
return !modelType.match(/_private_messages$/);
|
||||
},
|
||||
|
||||
@computed('model.type')
|
||||
showGroupOptions(modelType) {
|
||||
return modelType === "visits" || modelType === "signups" || modelType === "profile_views";
|
||||
|
||||
@ -40,7 +40,7 @@ export default Ember.ArrayController.extend({
|
||||
return _(expanded).sortBy(group => group.granted_at).reverse().value();
|
||||
|
||||
|
||||
}.property('model', 'model.@each', 'model.expandedBadges.@each'),
|
||||
}.property('model', 'model.[]', 'model.expandedBadges.[]'),
|
||||
|
||||
/**
|
||||
Array of badges that have not been granted to this user.
|
||||
@ -62,7 +62,7 @@ export default Ember.ArrayController.extend({
|
||||
});
|
||||
|
||||
return _.sortBy(badges, badge => badge.get('name'));
|
||||
}.property('badges.@each', 'model.@each'),
|
||||
}.property('badges.[]', 'model.[]'),
|
||||
|
||||
/**
|
||||
Whether there are any badges that can be granted.
|
||||
|
||||
@ -27,7 +27,7 @@ export default Ember.Controller.extend(CanCheckEmails, {
|
||||
});
|
||||
}
|
||||
return [];
|
||||
}.property('model.user_fields.@each'),
|
||||
}.property('model.user_fields.[]'),
|
||||
|
||||
actions: {
|
||||
toggleTitleEdit() {
|
||||
|
||||
@ -0,0 +1,2 @@
|
||||
import AdminEmailLogs from 'admin/routes/admin-email-logs';
|
||||
export default AdminEmailLogs.extend({ status: "bounced" });
|
||||
@ -10,6 +10,7 @@ export default {
|
||||
this.resource('adminEmail', { path: '/email'}, function() {
|
||||
this.route('sent');
|
||||
this.route('skipped');
|
||||
this.route('bounced');
|
||||
this.route('received');
|
||||
this.route('rejected');
|
||||
this.route('previewDigest', { path: '/preview-digest' });
|
||||
|
||||
49
app/assets/javascripts/admin/templates/email-bounced.hbs
Normal file
49
app/assets/javascripts/admin/templates/email-bounced.hbs
Normal file
@ -0,0 +1,49 @@
|
||||
{{#load-more selector=".email-list tr" action="loadMore"}}
|
||||
<table class='table email-list'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{i18n 'admin.email.time'}}</th>
|
||||
<th>{{i18n 'admin.email.user'}}</th>
|
||||
<th>{{i18n 'admin.email.to_address'}}</th>
|
||||
<th>{{i18n 'admin.email.email_type'}}</th>
|
||||
<th>{{i18n 'admin.email.skipped_reason'}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tr class="filters">
|
||||
<td>{{i18n 'admin.email.logs.filters.title'}}</td>
|
||||
<td>{{text-field value=filter.user placeholderKey="admin.email.logs.filters.user_placeholder"}}</td>
|
||||
<td>{{text-field value=filter.address placeholderKey="admin.email.logs.filters.address_placeholder"}}</td>
|
||||
<td>{{text-field value=filter.type placeholderKey="admin.email.logs.filters.type_placeholder"}}</td>
|
||||
<td>{{text-field value=filter.skipped_reason placeholderKey="admin.email.logs.filters.skipped_reason_placeholder"}}</td>
|
||||
</tr>
|
||||
|
||||
{{#each l in model}}
|
||||
<tr>
|
||||
<td>{{format-date l.created_at}}</td>
|
||||
<td>
|
||||
{{#if l.user}}
|
||||
{{#link-to 'adminUser' l.user}}{{avatar l.user imageSize="tiny"}}{{/link-to}}
|
||||
{{#link-to 'adminUser' l.user}}{{l.user.username}}{{/link-to}}
|
||||
{{else}}
|
||||
—
|
||||
{{/if}}
|
||||
</td>
|
||||
<td><a href='mailto:{{unbound l.to_address}}'>{{l.to_address}}</a></td>
|
||||
<td>{{l.email_type}}</td>
|
||||
<td>
|
||||
{{#if l.post_url}}
|
||||
<a href="{{l.post_url}}">{{l.skipped_reason}}</a>
|
||||
{{else}}
|
||||
{{l.skipped_reason}}
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr><td colspan="5">{{i18n 'admin.email.logs.none'}}</td></tr>
|
||||
{{/each}}
|
||||
|
||||
</table>
|
||||
{{/load-more}}
|
||||
|
||||
{{conditional-loading-spinner condition=loading}}
|
||||
@ -4,6 +4,7 @@
|
||||
{{nav-item route='adminCustomizeEmailTemplates' label='admin.email.templates'}}
|
||||
{{nav-item route='adminEmail.sent' label='admin.email.sent'}}
|
||||
{{nav-item route='adminEmail.skipped' label='admin.email.skipped'}}
|
||||
{{nav-item route='adminEmail.bounced' label='admin.email.bounced'}}
|
||||
{{nav-item route='adminEmail.received' label='admin.email.received'}}
|
||||
{{nav-item route='adminEmail.rejected' label='admin.email.rejected'}}
|
||||
{{/admin-nav}}
|
||||
|
||||
@ -3,7 +3,9 @@
|
||||
<div class="admin-reports-filter">
|
||||
{{i18n 'admin.dashboard.reports.start_date'}} {{date-picker-past value=startDate}}
|
||||
{{i18n 'admin.dashboard.reports.end_date'}} {{date-picker-past value=endDate}}
|
||||
{{combo-box valueAttribute="value" content=categoryOptions value=categoryId}}
|
||||
{{#if showCategoryOptions}}
|
||||
{{combo-box valueAttribute="value" content=categoryOptions value=categoryId}}
|
||||
{{/if}}
|
||||
{{#if showGroupOptions}}
|
||||
{{combo-box valueAttribute="value" content=groupOptions value=groupId}}
|
||||
{{/if}}
|
||||
|
||||
@ -27,7 +27,7 @@ export default Ember.View.extend({
|
||||
// force rerender
|
||||
this.rerender();
|
||||
}
|
||||
}, 150).observes("controller.model.@each"),
|
||||
}, 150).observes("controller.model.[]"),
|
||||
|
||||
render(buffer) {
|
||||
const formattedLogs = this.get("formattedLogs");
|
||||
|
||||
@ -8,9 +8,10 @@ define('ember', ['exports'], function(__exports__) {
|
||||
|
||||
var _pluginCallbacks = [];
|
||||
|
||||
window.Discourse = Ember.Application.createWithMixins(Discourse.Ajax, {
|
||||
window.Discourse = Ember.Application.extend(Discourse.Ajax, {
|
||||
rootElement: '#main',
|
||||
_docTitle: document.title,
|
||||
__TAGS_INCLUDED__: true,
|
||||
|
||||
getURL: function(url) {
|
||||
if (!url) return url;
|
||||
@ -106,7 +107,7 @@ window.Discourse = Ember.Application.createWithMixins(Discourse.Ajax, {
|
||||
|
||||
$('noscript').remove();
|
||||
|
||||
Ember.keys(requirejs._eak_seen).forEach(function(key) {
|
||||
Object.keys(requirejs._eak_seen).forEach(function(key) {
|
||||
if (/\/pre\-initializers\//.test(key)) {
|
||||
var module = require(key, null, null, true);
|
||||
if (!module) { throw new Error(key + ' must export an initializer.'); }
|
||||
@ -114,7 +115,7 @@ window.Discourse = Ember.Application.createWithMixins(Discourse.Ajax, {
|
||||
}
|
||||
});
|
||||
|
||||
Ember.keys(requirejs._eak_seen).forEach(function(key) {
|
||||
Object.keys(requirejs._eak_seen).forEach(function(key) {
|
||||
if (/\/initializers\//.test(key)) {
|
||||
var module = require(key, null, null, true);
|
||||
if (!module) { throw new Error(key + ' must export an initializer.'); }
|
||||
@ -167,7 +168,7 @@ window.Discourse = Ember.Application.createWithMixins(Discourse.Ajax, {
|
||||
return this.get("currentAssetVersion");
|
||||
}
|
||||
})
|
||||
});
|
||||
}).create();
|
||||
|
||||
function RemovedObject(name) {
|
||||
this._removedName = name;
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
import RESTAdapter from 'discourse/adapters/rest';
|
||||
|
||||
export default RESTAdapter.extend({
|
||||
pathFor(store, type, id) {
|
||||
return "/tags/" + id + "/notifications";
|
||||
}
|
||||
});
|
||||
@ -13,7 +13,7 @@ export default Ember.Component.extend({
|
||||
|
||||
_topicListChanged: function() {
|
||||
this._initFromTopicList(this.get('topicList'));
|
||||
}.observes('topicList.@each'),
|
||||
}.observes('topicList.[]'),
|
||||
|
||||
_initFromTopicList(topicList) {
|
||||
if (topicList !== null) {
|
||||
|
||||
@ -46,7 +46,7 @@ export default Ember.Component.extend({
|
||||
}
|
||||
},
|
||||
|
||||
@observes('content.@each')
|
||||
@observes('content.[]')
|
||||
_rerenderOnChange() {
|
||||
this.rerender();
|
||||
},
|
||||
|
||||
@ -2,6 +2,7 @@ import userSearch from 'discourse/lib/user-search';
|
||||
import { default as computed, 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 { fetchUnseenTagHashtags, linkSeenTagHashtags } from 'discourse/lib/link-tag-hashtag';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNames: ['wmd-controls'],
|
||||
@ -27,6 +28,22 @@ export default Ember.Component.extend({
|
||||
return showPreview ? I18n.t('composer.hide_preview') : I18n.t('composer.show_preview');
|
||||
},
|
||||
|
||||
_renderUnseenTagHashtags($preview, unseen) {
|
||||
fetchUnseenTagHashtags(unseen).then(() => {
|
||||
linkSeenTagHashtags($preview);
|
||||
});
|
||||
},
|
||||
|
||||
@on('previewRefreshed')
|
||||
paintTagHashtags($preview) {
|
||||
if (!this.siteSettings.tagging_enabled) { return; }
|
||||
|
||||
const unseenTagHashtags = linkSeenTagHashtags($preview);
|
||||
if (unseenTagHashtags.length) {
|
||||
Ember.run.debounce(this, this._renderUnseenTagHashtags, $preview, unseenTagHashtags, 500);
|
||||
}
|
||||
},
|
||||
|
||||
@computed
|
||||
markdownOptions() {
|
||||
return {
|
||||
|
||||
@ -3,9 +3,10 @@ import loadScript from 'discourse/lib/load-script';
|
||||
import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators';
|
||||
import { showSelector } from "discourse/lib/emoji/emoji-toolbar";
|
||||
import Category from 'discourse/models/category';
|
||||
import { SEPARATOR as categoryHashtagSeparator,
|
||||
categoryHashtagTriggerRule
|
||||
} from 'discourse/lib/category-hashtags';
|
||||
import { categoryHashtagTriggerRule } from 'discourse/lib/category-hashtags';
|
||||
import { TAG_HASHTAG_POSTFIX } from 'discourse/lib/tag-hashtags';
|
||||
import { search as searchCategoryTag } from 'discourse/lib/category-tag-search';
|
||||
import { SEPARATOR } from 'discourse/lib/category-hashtags';
|
||||
|
||||
// Our head can be a static string or a function that returns a string
|
||||
// based on input (like for numbered lists).
|
||||
@ -217,7 +218,7 @@ export default Ember.Component.extend({
|
||||
const mouseTrap = Mousetrap(this.$('.d-editor-input')[0]);
|
||||
|
||||
const shortcuts = this.get('toolbar.shortcuts');
|
||||
Ember.keys(shortcuts).forEach(sc => {
|
||||
Object.keys(shortcuts).forEach(sc => {
|
||||
const button = shortcuts[sc];
|
||||
mouseTrap.bind(sc, () => {
|
||||
this.send(button.action, button);
|
||||
@ -243,7 +244,7 @@ export default Ember.Component.extend({
|
||||
this.appEvents.off('composer:insert-text');
|
||||
|
||||
const mouseTrap = this._mouseTrap;
|
||||
Ember.keys(this.get('toolbar.shortcuts')).forEach(sc => mouseTrap.unbind(sc));
|
||||
Object.keys(this.get('toolbar.shortcuts')).forEach(sc => mouseTrap.unbind(sc));
|
||||
this.$('.d-editor-preview').off('click.preview');
|
||||
},
|
||||
|
||||
@ -278,17 +279,22 @@ export default Ember.Component.extend({
|
||||
Ember.run.debounce(this, this._updatePreview, 30);
|
||||
},
|
||||
|
||||
_applyCategoryHashtagAutocomplete(container, $editorInput) {
|
||||
const template = container.lookup('template:category-group-autocomplete.raw');
|
||||
_applyCategoryHashtagAutocomplete(container) {
|
||||
const template = container.lookup('template:category-tag-autocomplete.raw');
|
||||
const siteSettings = this.siteSettings;
|
||||
|
||||
$editorInput.autocomplete({
|
||||
this.$('.d-editor-input').autocomplete({
|
||||
template: template,
|
||||
key: '#',
|
||||
transformComplete(category) {
|
||||
return Category.slugFor(category, categoryHashtagSeparator);
|
||||
transformComplete(obj) {
|
||||
if (obj.model) {
|
||||
return Category.slugFor(obj.model, SEPARATOR);
|
||||
} else {
|
||||
return `${obj.text}${TAG_HASHTAG_POSTFIX}`;
|
||||
}
|
||||
},
|
||||
dataSource(term) {
|
||||
return Category.search(term);
|
||||
return searchCategoryTag(term, siteSettings);
|
||||
},
|
||||
triggerRule(textarea, opts) {
|
||||
return categoryHashtagTriggerRule(textarea, opts);
|
||||
|
||||
@ -9,24 +9,28 @@ export default Em.Component.extend({
|
||||
@on("didInsertElement")
|
||||
_loadDatePicker() {
|
||||
const input = this.$(".date-picker")[0];
|
||||
const container = $("#" + this.get("containerId"))[0];
|
||||
|
||||
loadScript("/javascripts/pikaday.js").then(() => {
|
||||
let default_opts = {
|
||||
field: input,
|
||||
container: this.$()[0],
|
||||
format: "YYYY-MM-DD",
|
||||
firstDay: moment.localeData().firstDayOfWeek(),
|
||||
i18n: {
|
||||
previousMonth: I18n.t('dates.previous_month'),
|
||||
nextMonth: I18n.t('dates.next_month'),
|
||||
months: moment.months(),
|
||||
weekdays: moment.weekdays(),
|
||||
weekdaysShort: moment.weekdaysShort()
|
||||
},
|
||||
onSelect: date => this.set("value", moment(date).format("YYYY-MM-DD"))
|
||||
};
|
||||
Ember.run.next(() => {
|
||||
let default_opts = {
|
||||
field: input,
|
||||
container: container || this.$()[0],
|
||||
bound: container === undefined,
|
||||
format: "YYYY-MM-DD",
|
||||
firstDay: moment.localeData().firstDayOfWeek(),
|
||||
i18n: {
|
||||
previousMonth: I18n.t('dates.previous_month'),
|
||||
nextMonth: I18n.t('dates.next_month'),
|
||||
months: moment.months(),
|
||||
weekdays: moment.weekdays(),
|
||||
weekdaysShort: moment.weekdaysShort()
|
||||
},
|
||||
onSelect: date => this.set("value", moment(date).format("YYYY-MM-DD"))
|
||||
};
|
||||
|
||||
this._picker = new Pikaday(_.merge(default_opts, this._opts()));
|
||||
this._picker = new Pikaday(_.merge(default_opts, this._opts()));
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
export default Ember.Component.extend({
|
||||
tagName: 'a',
|
||||
classNameBindings: [':discourse-tag', 'style', 'tagClass'],
|
||||
attributeBindings: ['href'],
|
||||
|
||||
tagClass: function() {
|
||||
return "tag-" + this.get('tagRecord.id');
|
||||
}.property('tagRecord.id'),
|
||||
|
||||
href: function() {
|
||||
return '/tags/' + this.get('tagRecord.id');
|
||||
}.property('tagRecord.id'),
|
||||
});
|
||||
@ -3,7 +3,9 @@ import { on } from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
const CATEGORIES_LIST_BODY_CLASS = "categories-list";
|
||||
|
||||
export default Ember.View.extend(UrlRefresh, {
|
||||
export default Ember.Component.extend(UrlRefresh, {
|
||||
classNames: ['contents'],
|
||||
|
||||
@on("didInsertElement")
|
||||
addBodyClass() {
|
||||
$('body').addClass(CATEGORIES_LIST_BODY_CLASS);
|
||||
@ -1,27 +1,13 @@
|
||||
import UrlRefresh from 'discourse/mixins/url-refresh';
|
||||
import LoadMore from "discourse/mixins/load-more";
|
||||
import { on, observes } from "ember-addons/ember-computed-decorators";
|
||||
import LoadMore from "discourse/mixins/load-more";
|
||||
import UrlRefresh from 'discourse/mixins/url-refresh';
|
||||
|
||||
export default Ember.View.extend(LoadMore, UrlRefresh, {
|
||||
const DiscoveryTopicsListComponent = Ember.Component.extend(UrlRefresh, LoadMore, {
|
||||
classNames: ['contents'],
|
||||
eyelineSelector: '.topic-list-item',
|
||||
|
||||
actions: {
|
||||
loadMore() {
|
||||
const self = this;
|
||||
Discourse.notifyTitle(0);
|
||||
this.get('controller').loadMoreTopics().then(hasMoreResults => {
|
||||
Ember.run.schedule('afterRender', () => self.saveScrollPosition());
|
||||
if (!hasMoreResults) {
|
||||
this.get('eyeline').flushRest();
|
||||
} else if ($(window).height() >= $(document).height()) {
|
||||
this.send("loadMore");
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@on("didInsertElement")
|
||||
@observes("controller.model")
|
||||
@observes("model")
|
||||
_readjustScrollPosition() {
|
||||
const scrollTo = this.session.get('topicListScrollPosition');
|
||||
if (scrollTo && scrollTo >= 0) {
|
||||
@ -31,19 +17,33 @@ export default Ember.View.extend(LoadMore, UrlRefresh, {
|
||||
}
|
||||
},
|
||||
|
||||
@observes("controller.topicTrackingState.incomingCount")
|
||||
@observes("incomingCount")
|
||||
_updateTitle() {
|
||||
Discourse.notifyTitle(this.get('controller.topicTrackingState.incomingCount'));
|
||||
Discourse.notifyTitle(this.get('incomingCount'));
|
||||
},
|
||||
|
||||
// Remember where we were scrolled to
|
||||
saveScrollPosition() {
|
||||
this.session.set('topicListScrollPosition', $(window).scrollTop());
|
||||
},
|
||||
|
||||
// When the topic list is scrolled
|
||||
scrolled() {
|
||||
this._super();
|
||||
this.saveScrollPosition();
|
||||
},
|
||||
|
||||
actions: {
|
||||
loadMore() {
|
||||
Discourse.notifyTitle(0);
|
||||
this.get('model').loadMore().then(hasMoreResults => {
|
||||
Ember.run.schedule('afterRender', () => this.saveScrollPosition());
|
||||
if (!hasMoreResults) {
|
||||
this.get('eyeline').flushRest();
|
||||
} else if ($(window).height() >= $(document).height()) {
|
||||
this.send("loadMore");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export default DiscoveryTopicsListComponent;
|
||||
@ -17,6 +17,14 @@ export default Ember.Component.extend(StringBuffer, {
|
||||
notices.push([I18n.t("emails_are_disabled"), 'alert-emails-disabled']);
|
||||
}
|
||||
|
||||
if (this.currentUser && this.currentUser.get('staff') && this.siteSettings.bootstrap_mode_enabled) {
|
||||
if (this.siteSettings.bootstrap_mode_min_users > 0) {
|
||||
notices.push([I18n.t("bootstrap_mode_enabled", {min_users: this.siteSettings.bootstrap_mode_min_users}), 'alert-bootstrap-mode']);
|
||||
} else {
|
||||
notices.push([I18n.t("bootstrap_mode_disabled"), 'alert-bootstrap-mode']);
|
||||
}
|
||||
}
|
||||
|
||||
if (!_.isEmpty(this.siteSettings.global_notice)) {
|
||||
notices.push([this.siteSettings.global_notice, 'alert-global-notice']);
|
||||
}
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
export default Ember.Component.extend({
|
||||
actions: {
|
||||
// TODO: When on Ember 1.13, use a closure action
|
||||
loadMore() {
|
||||
this.sendAction('loadMore');
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -1,76 +0,0 @@
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import mobile from 'discourse/lib/mobile';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNames: ['hamburger-panel'],
|
||||
|
||||
@computed('currentUser.read_faq')
|
||||
prioritizeFaq(readFaq) {
|
||||
// If it's a custom FAQ never prioritize it
|
||||
return Ember.isEmpty(this.siteSettings.faq_url) && !readFaq;
|
||||
},
|
||||
|
||||
@computed()
|
||||
showKeyboardShortcuts() {
|
||||
return !this.site.mobileView && !this.capabilities.touch;
|
||||
},
|
||||
|
||||
@computed()
|
||||
showMobileToggle() {
|
||||
return this.site.mobileView || (this.siteSettings.enable_mobile_theme && this.capabilities.touch);
|
||||
},
|
||||
|
||||
@computed()
|
||||
mobileViewLinkTextKey() {
|
||||
return this.site.mobileView ? "desktop_view" : "mobile_view";
|
||||
},
|
||||
|
||||
@computed()
|
||||
faqUrl() {
|
||||
return this.siteSettings.faq_url ? this.siteSettings.faq_url : Discourse.getURL('/faq');
|
||||
},
|
||||
|
||||
_lookupCount(type) {
|
||||
const state = this.get('topicTrackingState');
|
||||
return state ? state.lookupCount(type) : 0;
|
||||
},
|
||||
|
||||
@computed('topicTrackingState.messageCount')
|
||||
newCount() {
|
||||
return this._lookupCount('new');
|
||||
},
|
||||
|
||||
@computed('topicTrackingState.messageCount')
|
||||
unreadCount() {
|
||||
return this._lookupCount('unread');
|
||||
},
|
||||
|
||||
@computed()
|
||||
categories() {
|
||||
const hideUncategorized = !this.siteSettings.allow_uncategorized_topics;
|
||||
const showSubcatList = this.siteSettings.show_subcategory_list;
|
||||
const isStaff = Discourse.User.currentProp('staff');
|
||||
|
||||
return Discourse.Category.list().reject((c) => {
|
||||
if (showSubcatList && c.get('parent_category_id')) { return true; }
|
||||
if (hideUncategorized && c.get('isUncategorizedCategory') && !isStaff) { return true; }
|
||||
return false;
|
||||
});
|
||||
},
|
||||
|
||||
@computed()
|
||||
showUserDirectoryLink() {
|
||||
if (!this.siteSettings.enable_user_directory) return false;
|
||||
if (this.siteSettings.hide_user_profiles_from_public && !this.currentUser) return false;
|
||||
return true;
|
||||
},
|
||||
|
||||
actions: {
|
||||
keyboardShortcuts() {
|
||||
this.sendAction('showKeyboardAction');
|
||||
},
|
||||
toggleMobileView() {
|
||||
mobile.toggleMobileView();
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -1,34 +0,0 @@
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
tagName: 'li',
|
||||
classNameBindings: [':header-dropdown-toggle', 'active'],
|
||||
|
||||
@computed('showUser', 'path')
|
||||
href(showUser, path) {
|
||||
return showUser ? this.currentUser.get('path') : Discourse.getURL(path);
|
||||
},
|
||||
|
||||
active: Ember.computed.alias('toggleVisible'),
|
||||
|
||||
actions: {
|
||||
toggle() {
|
||||
|
||||
if (this.siteSettings.login_required && !this.currentUser) {
|
||||
this.sendAction('loginAction');
|
||||
} else {
|
||||
if (this.site.mobileView && this.get('mobileAction')) {
|
||||
this.sendAction('mobileAction');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.get('action')) {
|
||||
this.sendAction('action');
|
||||
} else {
|
||||
this.toggleProperty('toggleVisible');
|
||||
}
|
||||
}
|
||||
this.appEvents.trigger('dropdowns:closeAll');
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -1,54 +1,3 @@
|
||||
import DiscourseURL from 'discourse/lib/url';
|
||||
|
||||
const TopicCategoryComponent = Ember.Component.extend({
|
||||
needsSecondRow: Ember.computed.gt('secondRowItems.length', 0),
|
||||
secondRowItems: function() { return []; }.property(),
|
||||
|
||||
pmPath: function() {
|
||||
var currentUser = this.get('currentUser');
|
||||
return currentUser && currentUser.pmPath(this.get('topic'));
|
||||
}.property('topic'),
|
||||
|
||||
showPrivateMessageGlyph: function() {
|
||||
return !this.get('topic.is_warning') && this.get('topic.isPrivateMessage');
|
||||
}.property('topic.is_warning', 'topic.isPrivateMessage'),
|
||||
|
||||
actions: {
|
||||
jumpToTopPost() {
|
||||
const topic = this.get('topic');
|
||||
if (topic) {
|
||||
DiscourseURL.routeTo(topic.get('firstPostUrl'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
let id = 0;
|
||||
|
||||
// Allow us (and plugins) to register themselves as needing a second
|
||||
// row in the header. If there is at least one thing in the second row
|
||||
// the style changes to accomodate it.
|
||||
function needsSecondRowIf(prop, cb) {
|
||||
const rowId = "_second_row_" + (id++),
|
||||
methodHash = {};
|
||||
|
||||
methodHash[id] = function() {
|
||||
const secondRowItems = this.get('secondRowItems'),
|
||||
propVal = this.get(prop);
|
||||
if (cb.call(this, propVal)) {
|
||||
secondRowItems.addObject(rowId);
|
||||
} else {
|
||||
secondRowItems.removeObject(rowId);
|
||||
}
|
||||
}.observes(prop).on('init');
|
||||
|
||||
TopicCategoryComponent.reopen(methodHash);
|
||||
export function needsSecondRowIf() {
|
||||
Ember.warn("DEPRECATION: `needsSecondRowIf` is deprecated. Use widget hooks on `header-second-row`");
|
||||
}
|
||||
|
||||
needsSecondRowIf('topic.category', function(cat) {
|
||||
return cat && (!cat.get('isUncategorizedCategory') || !this.siteSettings.suppress_uncategorized_badge);
|
||||
});
|
||||
|
||||
export default TopicCategoryComponent;
|
||||
export { needsSecondRowIf };
|
||||
|
||||
@ -1,58 +0,0 @@
|
||||
import DiscourseURL from 'discourse/lib/url';
|
||||
import { iconHTML } from 'discourse/helpers/fa-icon';
|
||||
import { observes } from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
widget: 'home-logo',
|
||||
showMobileLogo: null,
|
||||
linkUrl: null,
|
||||
classNames: ['title'],
|
||||
|
||||
init() {
|
||||
this._super();
|
||||
this.showMobileLogo = this.site.mobileView && !Ember.isEmpty(this.siteSettings.mobile_logo_url);
|
||||
this.linkUrl = this.get('targetUrl') || '/';
|
||||
},
|
||||
|
||||
@observes('minimized')
|
||||
_updateLogo() {
|
||||
// On mobile we don't minimize the logo
|
||||
if (!this.site.mobileView) {
|
||||
this.rerender();
|
||||
}
|
||||
},
|
||||
|
||||
click(e) {
|
||||
// if they want to open in a new tab, let it so
|
||||
if (e.shiftKey || e.metaKey || e.ctrlKey || e.which === 2) { return true; }
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
DiscourseURL.routeTo(this.linkUrl);
|
||||
return false;
|
||||
},
|
||||
|
||||
render(buffer) {
|
||||
const { siteSettings } = this;
|
||||
const logoUrl = siteSettings.logo_url || '';
|
||||
const title = siteSettings.title;
|
||||
|
||||
buffer.push(`<a href="${this.linkUrl}" data-auto-route="true">`);
|
||||
if (!this.site.mobileView && this.get('minimized')) {
|
||||
const logoSmallUrl = siteSettings.logo_small_url || '';
|
||||
if (logoSmallUrl.length) {
|
||||
buffer.push(`<img id='site-logo' class="logo-small" src="${logoSmallUrl}" width="33" height="33" alt="${title}">`);
|
||||
} else {
|
||||
buffer.push(iconHTML('home'));
|
||||
}
|
||||
} else if (this.showMobileLogo) {
|
||||
buffer.push(`<img id="site-logo" class="logo-big" src="${siteSettings.mobile_logo_url}" alt="${title}">`);
|
||||
} else if (logoUrl.length) {
|
||||
buffer.push(`<img id="site-logo" class="logo-big" src="${logoUrl}" alt="${title}">`);
|
||||
} else {
|
||||
buffer.push(`<h2 id="site-text-logo" class="text-logo">${title}</h2>`);
|
||||
}
|
||||
buffer.push('</a>');
|
||||
}
|
||||
|
||||
});
|
||||
@ -1,8 +1,6 @@
|
||||
import LoadMore from "discourse/mixins/load-more";
|
||||
|
||||
export default Ember.Component.extend(LoadMore, {
|
||||
_viaComponent: true,
|
||||
|
||||
init() {
|
||||
this._super();
|
||||
this.set('eyelineSelector', this.get('selector'));
|
||||
|
||||
@ -1,224 +0,0 @@
|
||||
import { default as computed, on, observes } from 'ember-addons/ember-computed-decorators';
|
||||
import { headerHeight } from 'discourse/views/header';
|
||||
|
||||
const PANEL_BODY_MARGIN = 30;
|
||||
const mutationSupport = !Ember.testing && !!window['MutationObserver'];
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNameBindings: [':menu-panel', 'visible::hidden', 'viewMode'],
|
||||
_lastVisible: false,
|
||||
|
||||
showClose: Ember.computed.equal('viewMode', 'slide-in'),
|
||||
|
||||
_layoutComponent() {
|
||||
if (!this.get('visible')) { return; }
|
||||
|
||||
const $window = $(window);
|
||||
let width = this.get('maxWidth') || 300;
|
||||
const windowWidth = parseInt($window.width());
|
||||
|
||||
if ((windowWidth - width) < 50) {
|
||||
width = windowWidth - 50;
|
||||
}
|
||||
|
||||
const viewMode = this.get('viewMode');
|
||||
const $panelBody = this.$('.panel-body');
|
||||
let contentHeight = parseInt(this.$('.panel-body-contents').height());
|
||||
|
||||
// We use a mutationObserver to check for style changes, so it's important
|
||||
// we don't set it if it doesn't change. Same goes for the $panelBody!
|
||||
const style = this.$().prop('style');
|
||||
|
||||
if (viewMode === 'drop-down') {
|
||||
const $buttonPanel = $('header ul.icons');
|
||||
if ($buttonPanel.length === 0) { return; }
|
||||
|
||||
// These values need to be set here, not in the css file - this is to deal with the
|
||||
// possibility of the window being resized and the menu changing from .slide-in to .drop-down.
|
||||
if (style.top !== '100%' || style.height !== 'auto') {
|
||||
this.$().css({ top: '100%', height: 'auto' });
|
||||
}
|
||||
|
||||
// adjust panel height
|
||||
const fullHeight = parseInt($window.height());
|
||||
const offsetTop = this.$().offset().top;
|
||||
const scrollTop = $window.scrollTop();
|
||||
|
||||
if (contentHeight + (offsetTop - scrollTop) + PANEL_BODY_MARGIN > fullHeight) {
|
||||
contentHeight = fullHeight - (offsetTop - scrollTop) - PANEL_BODY_MARGIN;
|
||||
}
|
||||
if ($panelBody.height() !== contentHeight) {
|
||||
$panelBody.height(contentHeight);
|
||||
}
|
||||
$('body').addClass('drop-down-visible');
|
||||
} else {
|
||||
const menuTop = headerHeight();
|
||||
|
||||
let height;
|
||||
const winHeight = $(window).height() - 16;
|
||||
if ((menuTop + contentHeight) < winHeight) {
|
||||
height = contentHeight + "px";
|
||||
} else {
|
||||
height = winHeight - menuTop;
|
||||
}
|
||||
|
||||
if ($panelBody.prop('style').height !== '100%') {
|
||||
$panelBody.height('100%');
|
||||
}
|
||||
if (style.top !== menuTop + "px" || style.height !== height) {
|
||||
this.$().css({ top: menuTop + "px", height });
|
||||
}
|
||||
$('body').removeClass('drop-down-visible');
|
||||
}
|
||||
|
||||
this.$().width(width);
|
||||
},
|
||||
|
||||
@computed('force')
|
||||
viewMode() {
|
||||
const force = this.get('force');
|
||||
if (force) { return force; }
|
||||
|
||||
const headerWidth = $('#main-outlet .container').width() || 1100;
|
||||
const screenWidth = $(window).width();
|
||||
const remaining = parseInt((screenWidth - headerWidth) / 2);
|
||||
|
||||
return (remaining < 50) ? 'slide-in' : 'drop-down';
|
||||
},
|
||||
|
||||
@observes('viewMode', 'visible')
|
||||
_visibleChanged() {
|
||||
if (this.get('visible')) {
|
||||
// Allow us to hook into things being shown
|
||||
if (!this._lastVisible) {
|
||||
Ember.run.scheduleOnce('afterRender', () => this.sendAction('onVisible'));
|
||||
this._lastVisible = true;
|
||||
}
|
||||
|
||||
$('html').on('click.close-menu-panel', (e) => {
|
||||
const $target = $(e.target);
|
||||
if ($target.closest('.header-dropdown-toggle').length > 0) { return; }
|
||||
if ($target.closest('.menu-panel').length > 0) { return; }
|
||||
this.hide();
|
||||
});
|
||||
this.performLayout();
|
||||
this._watchSizeChanges();
|
||||
|
||||
// iOS does not handle scroll events well
|
||||
if (!this.capabilities.isIOS) {
|
||||
$(window).on('scroll.discourse-menu-panel', () => this.performLayout());
|
||||
}
|
||||
} else if (this._lastVisible) {
|
||||
this._lastVisible = false;
|
||||
Ember.run.scheduleOnce('afterRender', () => this.sendAction('onHidden'));
|
||||
$('html').off('click.close-menu-panel');
|
||||
$(window).off('scroll.discourse-menu-panel');
|
||||
this._stopWatchingSize();
|
||||
$('body').removeClass('drop-down-visible');
|
||||
}
|
||||
},
|
||||
|
||||
@computed()
|
||||
showKeyboardShortcuts() {
|
||||
return !this.site.mobileView && !this.capabilities.touch;
|
||||
},
|
||||
|
||||
@computed()
|
||||
showMobileToggle() {
|
||||
return this.site.mobileView || (this.siteSettings.enable_mobile_theme && this.capabilities.touch);
|
||||
},
|
||||
|
||||
@computed()
|
||||
mobileViewLinkTextKey() {
|
||||
return this.site.mobileView ? "desktop_view" : "mobile_view";
|
||||
},
|
||||
|
||||
@computed()
|
||||
faqUrl() {
|
||||
return this.siteSettings.faq_url ? this.siteSettings.faq_url : Discourse.getURL('/faq');
|
||||
},
|
||||
|
||||
performLayout() {
|
||||
Ember.run.scheduleOnce('afterRender', this, this._layoutComponent);
|
||||
},
|
||||
|
||||
_watchSizeChanges() {
|
||||
if (mutationSupport) {
|
||||
this._observer.disconnect();
|
||||
this._observer.observe(this.element, { childList: true, subtree: true, characterData: true, attributes: true });
|
||||
} else {
|
||||
clearInterval(this._resizeInterval);
|
||||
this._resizeInterval = setInterval(() => {
|
||||
Ember.run(() => {
|
||||
const $panelBodyContents = this.$('.panel-body-contents');
|
||||
if ($panelBodyContents && $panelBodyContents.length) {
|
||||
const contentHeight = parseInt($panelBodyContents.height());
|
||||
if (contentHeight !== this._lastHeight) { this.performLayout(); }
|
||||
this._lastHeight = contentHeight;
|
||||
}
|
||||
});
|
||||
}, 500);
|
||||
}
|
||||
},
|
||||
|
||||
_stopWatchingSize() {
|
||||
if (mutationSupport) {
|
||||
this._observer.disconnect();
|
||||
} else {
|
||||
clearInterval(this._resizeInterval);
|
||||
}
|
||||
},
|
||||
|
||||
@on('didInsertElement')
|
||||
_bindEvents() {
|
||||
this.$().on('click.discourse-menu-panel', 'a', e => {
|
||||
if (e.metaKey || e.ctrlKey || e.shiftKey) { return; }
|
||||
const $target = $(e.target);
|
||||
if ($target.data('ember-action') || $target.closest('.search-link').length > 0) { return; }
|
||||
this.hide();
|
||||
});
|
||||
|
||||
this.appEvents.on('dropdowns:closeAll', this, this.hide);
|
||||
this.appEvents.on('dom:clean', this, this.hide);
|
||||
|
||||
$('body').on('keydown.discourse-menu-panel', e => {
|
||||
if (e.which === 27) {
|
||||
this.hide();
|
||||
}
|
||||
});
|
||||
|
||||
$(window).on('resize.discourse-menu-panel', () => {
|
||||
this.propertyDidChange('viewMode');
|
||||
this.performLayout();
|
||||
});
|
||||
|
||||
if (mutationSupport) {
|
||||
this._observer = new MutationObserver(() => {
|
||||
Ember.run.debounce(this, this.performLayout, 50);
|
||||
});
|
||||
}
|
||||
|
||||
this.propertyDidChange('viewMode');
|
||||
},
|
||||
|
||||
@on('willDestroyElement')
|
||||
_removeEvents() {
|
||||
this.appEvents.off('dom:clean', this, this.hide);
|
||||
this.appEvents.off('dropdowns:closeAll', this, this.hide);
|
||||
this.$().off('click.discourse-menu-panel');
|
||||
$('body').off('keydown.discourse-menu-panel');
|
||||
$('html').off('click.close-menu-panel');
|
||||
$(window).off('resize.discourse-menu-panel');
|
||||
$(window).off('scroll.discourse-menu-panel');
|
||||
},
|
||||
|
||||
hide() {
|
||||
this.set('visible', false);
|
||||
},
|
||||
|
||||
actions: {
|
||||
close() {
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -1,5 +1,6 @@
|
||||
import { keyDirty } from 'discourse/widgets/widget';
|
||||
import { diff, patch } from 'virtual-dom';
|
||||
import { WidgetClickHook } from 'discourse/widgets/click-hook';
|
||||
import { WidgetClickHook } from 'discourse/widgets/hooks';
|
||||
import { renderedKey, queryRegistry } from 'discourse/widgets/widget';
|
||||
|
||||
const _cleanCallbacks = {};
|
||||
@ -13,13 +14,20 @@ export default Ember.Component.extend({
|
||||
_rootNode: null,
|
||||
_timeout: null,
|
||||
_widgetClass: null,
|
||||
_afterRender: null,
|
||||
_renderCallback: null,
|
||||
_childEvents: null,
|
||||
|
||||
init() {
|
||||
this._super();
|
||||
const name = this.get('widget');
|
||||
|
||||
this._widgetClass = queryRegistry(name) || this.container.lookupFactory(`widget:${name}`);
|
||||
|
||||
if (!this._widgetClass) {
|
||||
console.error(`Error: Could not find widget: ${name}`);
|
||||
}
|
||||
|
||||
this._childEvents = [];
|
||||
this._connected = [];
|
||||
},
|
||||
|
||||
@ -42,50 +50,68 @@ export default Ember.Component.extend({
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._childEvents.forEach(evt => this.appEvents.off(evt));
|
||||
Ember.run.cancel(this._timeout);
|
||||
},
|
||||
|
||||
afterRender() {
|
||||
},
|
||||
|
||||
beforePatch() {
|
||||
},
|
||||
|
||||
afterPatch() {
|
||||
},
|
||||
|
||||
eventDispatched(eventName, key, refreshArg) {
|
||||
const onRefresh = Ember.String.camelize(eventName.replace(/:/, '-'));
|
||||
keyDirty(key, { onRefresh, refreshArg });
|
||||
this.queueRerender();
|
||||
},
|
||||
|
||||
dispatch(eventName, key) {
|
||||
this._childEvents.push(eventName);
|
||||
this.appEvents.on(eventName, refreshArg => {
|
||||
this.eventDispatched(eventName, key, refreshArg);
|
||||
});
|
||||
},
|
||||
|
||||
queueRerender(callback) {
|
||||
if (callback && !this._afterRender) {
|
||||
this._afterRender = callback;
|
||||
if (callback && !this._renderCallback) {
|
||||
this._renderCallback = callback;
|
||||
}
|
||||
|
||||
Ember.run.scheduleOnce('render', this, this.rerenderWidget);
|
||||
},
|
||||
|
||||
buildArgs() {
|
||||
},
|
||||
|
||||
rerenderWidget() {
|
||||
Ember.run.cancel(this._timeout);
|
||||
if (this._rootNode) {
|
||||
if (!this._widgetClass) { return; }
|
||||
|
||||
const t0 = new Date().getTime();
|
||||
|
||||
const args = this.get('args') || this.buildArgs();
|
||||
const opts = { model: this.get('model') };
|
||||
const newTree = new this._widgetClass(this.get('args'), this.container, opts);
|
||||
const newTree = new this._widgetClass(args, this.container, opts);
|
||||
|
||||
newTree._emberView = this;
|
||||
const patches = diff(this._tree || this._rootNode, newTree);
|
||||
|
||||
const $body = $(document);
|
||||
const prevHeight = $body.height();
|
||||
const prevScrollTop = $body.scrollTop();
|
||||
|
||||
this.beforePatch();
|
||||
this._rootNode = patch(this._rootNode, patches);
|
||||
|
||||
const height = $body.height();
|
||||
const scrollTop = $body.scrollTop();
|
||||
|
||||
// This hack is for when swapping out many cloaked views at once
|
||||
// when using keyboard navigation. It could suddenly move the
|
||||
// scroll
|
||||
if (prevHeight === height && scrollTop !== prevScrollTop) {
|
||||
$body.scrollTop(prevScrollTop);
|
||||
}
|
||||
this.afterPatch();
|
||||
|
||||
this._tree = newTree;
|
||||
|
||||
if (this._afterRender) {
|
||||
this._afterRender();
|
||||
this._afterRender = null;
|
||||
if (this._renderCallback) {
|
||||
this._renderCallback();
|
||||
this._renderCallback = null;
|
||||
}
|
||||
this.afterRender();
|
||||
|
||||
renderedKey('*');
|
||||
if (this.profileWidget) {
|
||||
|
||||
@ -1,105 +0,0 @@
|
||||
const LIKED_TYPE = 5;
|
||||
const INVITED_TYPE = 8;
|
||||
const GROUP_SUMMARY_TYPE = 16;
|
||||
|
||||
export default Ember.Component.extend({
|
||||
tagName: 'li',
|
||||
classNameBindings: ['notification.read', 'notification.is_warning'],
|
||||
|
||||
name: function() {
|
||||
var notificationType = this.get("notification.notification_type");
|
||||
var lookup = this.site.get("notificationLookup");
|
||||
return lookup[notificationType];
|
||||
}.property("notification.notification_type"),
|
||||
|
||||
scope: function() {
|
||||
if (this.get("name") === "custom") {
|
||||
return this.get("notification.data.message");
|
||||
} else {
|
||||
return "notifications." + this.get("name");
|
||||
}
|
||||
}.property("name"),
|
||||
|
||||
url: function() {
|
||||
const it = this.get('notification');
|
||||
const badgeId = it.get("data.badge_id");
|
||||
if (badgeId) {
|
||||
var badgeSlug = it.get("data.badge_slug");
|
||||
|
||||
if (!badgeSlug) {
|
||||
const badgeName = it.get("data.badge_name");
|
||||
badgeSlug = badgeName.replace(/[^A-Za-z0-9_]+/g, '-').toLowerCase();
|
||||
}
|
||||
|
||||
var username = it.get('data.username');
|
||||
username = username ? "?username=" + username.toLowerCase() : "";
|
||||
return Discourse.getURL('/badges/' + badgeId + '/' + badgeSlug + username);
|
||||
}
|
||||
|
||||
const topicId = it.get('topic_id');
|
||||
if (topicId) {
|
||||
return Discourse.Utilities.postUrl(it.get("slug"), topicId, it.get("post_number"));
|
||||
}
|
||||
|
||||
if (it.get('notification_type') === INVITED_TYPE) {
|
||||
return Discourse.getURL('/users/' + it.get('data.display_username'));
|
||||
}
|
||||
|
||||
if (it.get('data.group_id')) {
|
||||
return Discourse.getURL('/users/' + it.get('data.username') + '/messages/group/' + it.get('data.group_name'));
|
||||
}
|
||||
|
||||
}.property("notification.data.{badge_id,badge_name,display_username}", "model.slug", "model.topic_id", "model.post_number"),
|
||||
|
||||
description: function() {
|
||||
const badgeName = this.get("notification.data.badge_name");
|
||||
if (badgeName) { return Discourse.Utilities.escapeExpression(badgeName); }
|
||||
|
||||
const title = this.get('notification.data.topic_title');
|
||||
return Ember.isEmpty(title) ? "" : Discourse.Utilities.escapeExpression(title);
|
||||
}.property("notification.data.{badge_name,topic_title}"),
|
||||
|
||||
_markRead: function(){
|
||||
this.$('a').click(() => {
|
||||
this.set('notification.read', true);
|
||||
Discourse.setTransientHeader("Discourse-Clear-Notifications", this.get('notification.id'));
|
||||
if (document && document.cookie) {
|
||||
document.cookie = `cn=${this.get('notification.id')}; expires=Fri, 31 Dec 9999 23:59:59 GMT`;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}.on('didInsertElement'),
|
||||
|
||||
render(buffer) {
|
||||
const notification = this.get('notification');
|
||||
// since we are reusing views now sometimes this can be unset
|
||||
if (!notification) { return; }
|
||||
const description = this.get('description');
|
||||
const username = notification.get('data.display_username');
|
||||
var text;
|
||||
if (notification.get('notification_type') === GROUP_SUMMARY_TYPE) {
|
||||
const count = notification.get('data.inbox_count');
|
||||
const group_name = notification.get('data.group_name');
|
||||
text = I18n.t(this.get('scope'), {count, group_name});
|
||||
} else if (notification.get('notification_type') === LIKED_TYPE && notification.get("data.count") > 1) {
|
||||
const count = notification.get('data.count') - 2;
|
||||
const username2 = notification.get('data.username2');
|
||||
if (count===0) {
|
||||
text = I18n.t('notifications.liked_2', {description, username, username2});
|
||||
} else {
|
||||
text = I18n.t('notifications.liked_many', {description, username, username2, count});
|
||||
}
|
||||
}
|
||||
else {
|
||||
text = I18n.t(this.get('scope'), {description, username});
|
||||
}
|
||||
text = Discourse.Emoji.unescape(text);
|
||||
|
||||
const url = this.get('url');
|
||||
if (url) {
|
||||
buffer.push('<a href="' + url + '" alt="' + I18n.t('notifications.alt.' + this.get("name")) + '">' + text + '</a>');
|
||||
} else {
|
||||
buffer.push(text);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -5,10 +5,10 @@ export default Em.Component.extend({
|
||||
return I18n.t(this.get('labelKey'));
|
||||
}.property('labelKey'),
|
||||
|
||||
click() {
|
||||
change() {
|
||||
const warning = this.get('warning');
|
||||
|
||||
if (warning && !this.get('checked')) {
|
||||
if (warning && this.get('checked')) {
|
||||
this.sendAction('warning');
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -37,6 +37,25 @@ export default MountWidget.extend({
|
||||
'searchService');
|
||||
}).volatile(),
|
||||
|
||||
beforePatch() {
|
||||
const $body = $(document);
|
||||
this.prevHeight = $body.height();
|
||||
this.prevScrollTop = $body.scrollTop();
|
||||
},
|
||||
|
||||
afterPatch() {
|
||||
const $body = $(document);
|
||||
const height = $body.height();
|
||||
const scrollTop = $body.scrollTop();
|
||||
|
||||
// This hack is for when swapping out many cloaked views at once
|
||||
// when using keyboard navigation. It could suddenly move the
|
||||
// scroll
|
||||
if (this.prevHeight === height && scrollTop !== this.prevScrollTop) {
|
||||
$body.scrollTop(this.prevScrollTop);
|
||||
}
|
||||
},
|
||||
|
||||
scrolled() {
|
||||
if (this.isDestroyed || this.isDestroying) { return; }
|
||||
if (isWorkaroundActive()) { return; }
|
||||
|
||||
@ -1,162 +0,0 @@
|
||||
import {searchForTerm, searchContextDescription, isValidSearchTerm } from 'discourse/lib/search';
|
||||
import DiscourseURL from 'discourse/lib/url';
|
||||
import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
|
||||
import showModal from 'discourse/lib/show-modal';
|
||||
|
||||
let _dontSearch = false;
|
||||
export default Ember.Component.extend({
|
||||
searchService: Ember.inject.service('search'),
|
||||
classNames: ['search-menu'],
|
||||
typeFilter: null,
|
||||
|
||||
@observes('searchService.searchContext')
|
||||
contextChanged: function() {
|
||||
if (this.get('searchService.searchContextEnabled')) {
|
||||
_dontSearch = true;
|
||||
this.set('searchService.searchContextEnabled', false);
|
||||
_dontSearch = false;
|
||||
}
|
||||
},
|
||||
|
||||
@computed('searchService.searchContext', 'searchService.term', 'searchService.searchContextEnabled')
|
||||
fullSearchUrlRelative(searchContext, term, searchContextEnabled) {
|
||||
|
||||
if (searchContextEnabled && Ember.get(searchContext, 'type') === 'topic') {
|
||||
return null;
|
||||
}
|
||||
|
||||
let url = '/search?q=' + encodeURIComponent(this.get('searchService.term'));
|
||||
if (searchContextEnabled) {
|
||||
if (searchContext.id.toString().toLowerCase() === this.get('currentUser.username_lower') &&
|
||||
searchContext.type === "private_messages"
|
||||
) {
|
||||
url += ' in:private';
|
||||
} else {
|
||||
url += encodeURIComponent(" " + searchContext.type + ":" + searchContext.id);
|
||||
}
|
||||
}
|
||||
|
||||
return url;
|
||||
},
|
||||
|
||||
@computed('fullSearchUrlRelative')
|
||||
fullSearchUrl(fullSearchUrlRelative) {
|
||||
if (fullSearchUrlRelative) {
|
||||
return Discourse.getURL(fullSearchUrlRelative);
|
||||
}
|
||||
},
|
||||
|
||||
@computed('searchService.searchContext')
|
||||
searchContextDescription(ctx) {
|
||||
return searchContextDescription(Em.get(ctx, 'type'), Em.get(ctx, 'user.username') || Em.get(ctx, 'category.name'));
|
||||
},
|
||||
|
||||
@observes('searchService.searchContextEnabled')
|
||||
searchContextEnabledChanged() {
|
||||
if (_dontSearch) { return; }
|
||||
this.newSearchNeeded();
|
||||
},
|
||||
|
||||
// If we need to perform another search
|
||||
@observes('searchService.term', 'typeFilter')
|
||||
newSearchNeeded() {
|
||||
this.set('noResults', false);
|
||||
const term = this.get('searchService.term');
|
||||
if (isValidSearchTerm(term)) {
|
||||
this.set('loading', true);
|
||||
Ember.run.debounce(this, 'searchTerm', term, this.get('typeFilter'), 400);
|
||||
} else {
|
||||
this.setProperties({ content: null });
|
||||
}
|
||||
this.set('selectedIndex', 0);
|
||||
},
|
||||
|
||||
searchTerm(term, typeFilter) {
|
||||
// for cancelling debounced search
|
||||
if (this._cancelSearch){
|
||||
this._cancelSearch = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._search) {
|
||||
this._search.abort();
|
||||
}
|
||||
|
||||
const searchContext = this.get('searchService.searchContextEnabled') ? this.get('searchService.searchContext') : null;
|
||||
this._search = searchForTerm(term, { typeFilter, searchContext, fullSearchUrl: this.get('fullSearchUrl') });
|
||||
|
||||
this._search.then((content) => {
|
||||
this.setProperties({ noResults: !content, content });
|
||||
}).finally(() => {
|
||||
this.set('loading', false);
|
||||
this._search = null;
|
||||
});
|
||||
},
|
||||
|
||||
@computed('typeFilter', 'loading')
|
||||
showCancelFilter(typeFilter, loading) {
|
||||
if (loading) { return false; }
|
||||
return !Ember.isEmpty(typeFilter);
|
||||
},
|
||||
|
||||
@observes('searchService.term')
|
||||
termChanged() {
|
||||
this.cancelTypeFilter();
|
||||
},
|
||||
|
||||
actions: {
|
||||
fullSearch() {
|
||||
const self = this;
|
||||
|
||||
if (this._search) {
|
||||
this._search.abort();
|
||||
}
|
||||
|
||||
// maybe we are debounced and delayed
|
||||
// stop that as well
|
||||
this._cancelSearch = true;
|
||||
Em.run.later(function() {
|
||||
self._cancelSearch = false;
|
||||
}, 400);
|
||||
|
||||
const url = this.get('fullSearchUrlRelative');
|
||||
if (url) {
|
||||
DiscourseURL.routeTo(url);
|
||||
}
|
||||
},
|
||||
|
||||
moreOfType(type) {
|
||||
this.set('typeFilter', type);
|
||||
},
|
||||
|
||||
cancelType() {
|
||||
this.cancelTypeFilter();
|
||||
},
|
||||
|
||||
showedSearch() {
|
||||
$('#search-term').focus().select();
|
||||
},
|
||||
|
||||
showSearchHelp() {
|
||||
// TODO: @EvitTrout how do we get a loading indicator here?
|
||||
Discourse.ajax("/static/search_help.html", { dataType: 'html' }).then((model) => {
|
||||
showModal('searchHelp', { model });
|
||||
});
|
||||
},
|
||||
|
||||
cancelHighlight() {
|
||||
this.set('searchService.highlightTerm', null);
|
||||
}
|
||||
},
|
||||
|
||||
cancelTypeFilter() {
|
||||
this.set('typeFilter', null);
|
||||
},
|
||||
|
||||
keyDown(e) {
|
||||
if (e.which === 13 && isValidSearchTerm(this.get('searchService.term'))) {
|
||||
this.set('visible', false);
|
||||
this.send('fullSearch');
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -1,2 +0,0 @@
|
||||
import SearchResult from 'discourse/components/search-result';
|
||||
export default SearchResult.extend();
|
||||
@ -1,2 +0,0 @@
|
||||
import SearchResult from 'discourse/components/search-result';
|
||||
export default SearchResult.extend();
|
||||
@ -1,2 +0,0 @@
|
||||
import SearchResult from 'discourse/components/search-result';
|
||||
export default SearchResult.extend();
|
||||
@ -1,2 +0,0 @@
|
||||
import SearchResult from 'discourse/components/search-result';
|
||||
export default SearchResult.extend();
|
||||
@ -1,11 +0,0 @@
|
||||
export default Ember.Component.extend({
|
||||
tagName: 'ul',
|
||||
|
||||
_highlightOnInsert: function() {
|
||||
const term = this.get('controller.term');
|
||||
if(!_.isEmpty(term)) {
|
||||
this.$('.blurb').highlight(term.split(/\s+/), {className: 'search-highlight'});
|
||||
this.$('.topic-title').highlight(term.split(/\s+/), {className: 'search-highlight'} );
|
||||
}
|
||||
}.on('didInsertElement')
|
||||
});
|
||||
196
app/assets/javascripts/discourse/components/site-header.js.es6
Normal file
196
app/assets/javascripts/discourse/components/site-header.js.es6
Normal file
@ -0,0 +1,196 @@
|
||||
import MountWidget from 'discourse/components/mount-widget';
|
||||
import { observes } from 'ember-addons/ember-computed-decorators';
|
||||
|
||||
const _flagProperties = [];
|
||||
function addFlagProperty(prop) {
|
||||
_flagProperties.pushObject(prop);
|
||||
}
|
||||
|
||||
const PANEL_BODY_MARGIN = 30;
|
||||
|
||||
const SiteHeaderComponent = MountWidget.extend({
|
||||
widget: 'header',
|
||||
docAt: null,
|
||||
dockedHeader: null,
|
||||
_topic: null,
|
||||
|
||||
// profileWidget: true,
|
||||
// classNameBindings: ['editingTopic'],
|
||||
|
||||
@observes('currentUser.unread_notifications', 'currentUser.unread_private_messages')
|
||||
_notificationsChanged() {
|
||||
this.queueRerender();
|
||||
},
|
||||
|
||||
examineDockHeader() {
|
||||
const $body = $('body');
|
||||
|
||||
// Check the dock after the current run loop. While rendering,
|
||||
// it's much slower to calculate `outlet.offset()`
|
||||
Ember.run.next(() => {
|
||||
if (this.docAt === null) {
|
||||
const outlet = $('#main-outlet');
|
||||
if (!(outlet && outlet.length === 1)) return;
|
||||
this.docAt = outlet.offset().top;
|
||||
}
|
||||
|
||||
const offset = window.pageYOffset || $('html').scrollTop();
|
||||
if (offset >= this.docAt) {
|
||||
if (!this.dockedHeader) {
|
||||
$body.addClass('docked');
|
||||
this.dockedHeader = true;
|
||||
}
|
||||
} else {
|
||||
if (this.dockedHeader) {
|
||||
$body.removeClass('docked');
|
||||
this.dockedHeader = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
setTopic(topic) {
|
||||
this._topic = topic;
|
||||
this.queueRerender();
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
this._super();
|
||||
$(window).bind('scroll.discourse-dock', () => this.examineDockHeader());
|
||||
$(document).bind('touchmove.discourse-dock', () => this.examineDockHeader());
|
||||
$(window).on('resize.discourse-menu-panel', () => this.afterRender());
|
||||
|
||||
this.appEvents.on('header:show-topic', topic => this.setTopic(topic));
|
||||
this.appEvents.on('header:hide-topic', () => this.setTopic(null));
|
||||
|
||||
this.dispatch('notifications:changed', 'user-notifications');
|
||||
this.dispatch('header:keyboard-trigger', 'header');
|
||||
|
||||
this.appEvents.on('dom:clean', () => {
|
||||
// For performance, only trigger a re-render if any menu panels are visible
|
||||
if (this.$('.menu-panel').length) {
|
||||
this.eventDispatched('dom:clean', 'header');
|
||||
}
|
||||
});
|
||||
|
||||
this.examineDockHeader();
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super();
|
||||
$(window).unbind('scroll.discourse-dock');
|
||||
$(document).unbind('touchmove.discourse-dock');
|
||||
$('body').off('keydown.header');
|
||||
this.appEvents.off('notifications:changed');
|
||||
$(window).off('resize.discourse-menu-panel');
|
||||
|
||||
this.appEvents.off('header:show-topic');
|
||||
this.appEvents.off('header:hide-topic');
|
||||
this.appEvents.off('dom:clean');
|
||||
},
|
||||
|
||||
buildArgs() {
|
||||
return {
|
||||
flagCount: _flagProperties.reduce((prev, cur) => prev + (this.get(cur) || 0), 0),
|
||||
topic: this._topic,
|
||||
canSignUp: this.get('canSignUp')
|
||||
};
|
||||
},
|
||||
|
||||
afterRender() {
|
||||
const $menuPanels = $('.menu-panel');
|
||||
if ($menuPanels.length === 0) { return; }
|
||||
|
||||
const $window = $(window);
|
||||
const windowWidth = parseInt($window.width());
|
||||
|
||||
|
||||
const headerWidth = $('#main-outlet .container').width() || 1100;
|
||||
const remaining = parseInt((windowWidth - headerWidth) / 2);
|
||||
const viewMode = (remaining < 50) ? 'slide-in' : 'drop-down';
|
||||
|
||||
$menuPanels.each((idx, panel) => {
|
||||
const $panel = $(panel);
|
||||
let width = parseInt($panel.attr('data-max-width') || 300);
|
||||
if ((windowWidth - width) < 50) {
|
||||
width = windowWidth - 50;
|
||||
}
|
||||
|
||||
$panel.removeClass('drop-down').removeClass('slide-in').addClass(viewMode);
|
||||
|
||||
const $panelBody = $('.panel-body', $panel);
|
||||
let contentHeight = parseInt($('.panel-body-contents', $panel).height());
|
||||
|
||||
// We use a mutationObserver to check for style changes, so it's important
|
||||
// we don't set it if it doesn't change. Same goes for the $panelBody!
|
||||
const style = $panel.prop('style');
|
||||
|
||||
if (viewMode === 'drop-down') {
|
||||
const $buttonPanel = $('header ul.icons');
|
||||
if ($buttonPanel.length === 0) { return; }
|
||||
|
||||
// These values need to be set here, not in the css file - this is to deal with the
|
||||
// possibility of the window being resized and the menu changing from .slide-in to .drop-down.
|
||||
if (style.top !== '100%' || style.height !== 'auto') {
|
||||
$panel.css({ top: '100%', height: 'auto' });
|
||||
}
|
||||
|
||||
// adjust panel height
|
||||
const fullHeight = parseInt($window.height());
|
||||
const offsetTop = $panel.offset().top;
|
||||
const scrollTop = $window.scrollTop();
|
||||
|
||||
if (contentHeight + (offsetTop - scrollTop) + PANEL_BODY_MARGIN > fullHeight) {
|
||||
contentHeight = fullHeight - (offsetTop - scrollTop) - PANEL_BODY_MARGIN;
|
||||
}
|
||||
if ($panelBody.height() !== contentHeight) {
|
||||
$panelBody.height(contentHeight);
|
||||
}
|
||||
$('body').addClass('drop-down-visible');
|
||||
} else {
|
||||
const menuTop = headerHeight();
|
||||
|
||||
let height;
|
||||
const winHeight = $(window).height() - 16;
|
||||
if ((menuTop + contentHeight) < winHeight) {
|
||||
height = contentHeight + "px";
|
||||
} else {
|
||||
height = winHeight - menuTop;
|
||||
}
|
||||
|
||||
if ($panelBody.prop('style').height !== '100%') {
|
||||
$panelBody.height('100%');
|
||||
}
|
||||
if (style.top !== menuTop + "px" || style.height !== height) {
|
||||
$panel.css({ top: menuTop + "px", height });
|
||||
}
|
||||
$('body').removeClass('drop-down-visible');
|
||||
}
|
||||
|
||||
$panel.width(width);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default SiteHeaderComponent;
|
||||
|
||||
function applyFlaggedProperties() {
|
||||
const args = _flagProperties.slice();
|
||||
args.push(function() {
|
||||
this.queueRerender();
|
||||
}.on('init'));
|
||||
|
||||
SiteHeaderComponent.reopen({ _flagsChanged: Ember.observer.apply(this, args) });
|
||||
}
|
||||
|
||||
addFlagProperty('currentUser.site_flagged_posts_count');
|
||||
addFlagProperty('currentUser.post_queue_new_count');
|
||||
|
||||
export { addFlagProperty, applyFlaggedProperties };
|
||||
|
||||
export function headerHeight() {
|
||||
const $header = $('header.d-header');
|
||||
const headerOffset = $header.offset();
|
||||
const headerOffsetTop = (headerOffset) ? headerOffset.top : 0;
|
||||
return parseInt($header.outerHeight() + headerOffsetTop - $(window).scrollTop());
|
||||
}
|
||||
@ -0,0 +1,97 @@
|
||||
import renderTag from 'discourse/lib/render-tag';
|
||||
|
||||
function formatTag(t) {
|
||||
return renderTag(t.id, {count: t.count});
|
||||
}
|
||||
|
||||
export default Ember.TextField.extend({
|
||||
classNameBindings: [':tag-chooser'],
|
||||
attributeBindings: ['tabIndex'],
|
||||
|
||||
_setupTags: function() {
|
||||
const tags = this.get('tags') || [];
|
||||
this.set('value', tags.join(", "));
|
||||
}.on('init'),
|
||||
|
||||
_valueChanged: function() {
|
||||
const tags = this.get('value').split(',').map(v => v.trim()).reject(v => v.length === 0).uniq();
|
||||
this.set('tags', tags);
|
||||
}.observes('value'),
|
||||
|
||||
_initializeTags: function() {
|
||||
const site = this.site,
|
||||
self = this,
|
||||
filterRegexp = new RegExp(this.site.tags_filter_regexp, "g");
|
||||
|
||||
this.$().select2({
|
||||
tags: true,
|
||||
placeholder: I18n.t('tagging.choose_for_topic'),
|
||||
maximumInputLength: this.siteSettings.max_tag_length,
|
||||
maximumSelectionSize: this.siteSettings.max_tags_per_topic,
|
||||
initSelection(element, callback) {
|
||||
const data = [];
|
||||
|
||||
function splitVal(string, separator) {
|
||||
var val, i, l;
|
||||
if (string === null || string.length < 1) return [];
|
||||
val = string.split(separator);
|
||||
for (i = 0, l = val.length; i < l; i = i + 1) val[i] = $.trim(val[i]);
|
||||
return val;
|
||||
}
|
||||
|
||||
$(splitVal(element.val(), ",")).each(function () {
|
||||
data.push({
|
||||
id: this,
|
||||
text: this
|
||||
});
|
||||
});
|
||||
|
||||
callback(data);
|
||||
},
|
||||
createSearchChoice: function(term, data) {
|
||||
term = term.replace(filterRegexp, '').trim();
|
||||
|
||||
// No empty terms, make sure the user has permission to create the tag
|
||||
if (!term.length || !site.get('can_create_tag')) { return; }
|
||||
|
||||
if ($(data).filter(function() {
|
||||
return this.text.localeCompare(term) === 0;
|
||||
}).length === 0) {
|
||||
return { id: term, text: term };
|
||||
}
|
||||
},
|
||||
createSearchChoicePosition: function(list, item) {
|
||||
// Search term goes on the bottom
|
||||
list.push(item);
|
||||
},
|
||||
formatSelection: function (data) {
|
||||
return data ? renderTag(this.text(data)) : undefined;
|
||||
},
|
||||
formatSelectionCssClass: function(){
|
||||
return "discourse-tag-select2";
|
||||
},
|
||||
formatResult: formatTag,
|
||||
multiple: true,
|
||||
ajax: {
|
||||
quietMillis: 200,
|
||||
cache: true,
|
||||
url: Discourse.getURL("/tags/filter/search"),
|
||||
dataType: 'json',
|
||||
data: function (term) {
|
||||
return { q: term, limit: self.siteSettings.max_tag_search_results };
|
||||
},
|
||||
results: function (data) {
|
||||
if (self.siteSettings.tags_sort_alphabetically) {
|
||||
data.results = data.results.sort(function(a,b) { return a.id > b.id; });
|
||||
}
|
||||
return data;
|
||||
}
|
||||
},
|
||||
});
|
||||
}.on('didInsertElement'),
|
||||
|
||||
_destroyTags: function() {
|
||||
this.$().select2('destroy');
|
||||
}.on('willDestroyElement')
|
||||
|
||||
});
|
||||
@ -0,0 +1,29 @@
|
||||
import DiscourseURL from 'discourse/lib/url';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
tagName: 'a',
|
||||
classNameBindings: [':tag-badge-wrapper', ':badge-wrapper', ':bullet', 'tagClass'],
|
||||
attributeBindings: ['href'],
|
||||
|
||||
href: function() {
|
||||
var url = '/tags';
|
||||
if (this.get('category')) {
|
||||
url += this.get('category.url');
|
||||
}
|
||||
return url + '/' + this.get('tagId');
|
||||
}.property('tagId', 'category'),
|
||||
|
||||
tagClass: function() {
|
||||
return "tag-" + this.get('tagId');
|
||||
}.property('tagId'),
|
||||
|
||||
render(buffer) {
|
||||
buffer.push(Handlebars.Utils.escapeExpression(this.get('tagId')));
|
||||
},
|
||||
|
||||
click(e) {
|
||||
e.preventDefault();
|
||||
DiscourseURL.routeTo(this.get('href'));
|
||||
return true;
|
||||
}
|
||||
});
|
||||
113
app/assets/javascripts/discourse/components/tag-drop.js.es6
Normal file
113
app/assets/javascripts/discourse/components/tag-drop.js.es6
Normal file
@ -0,0 +1,113 @@
|
||||
import { setting } from 'discourse/lib/computed';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNameBindings: [':tag-drop', 'tag::no-category', 'tags:has-drop','categoryStyle','tagClass'],
|
||||
categoryStyle: setting('category_style'), // match the category-drop style
|
||||
currentCategory: Em.computed.or('secondCategory', 'firstCategory'),
|
||||
showFilterByTag: setting('show_filter_by_tag'),
|
||||
showTagDropdown: Em.computed.and('showFilterByTag', 'tags'),
|
||||
tagId: null,
|
||||
|
||||
tagName: 'li',
|
||||
|
||||
tags: function() {
|
||||
if (this.siteSettings.tags_sort_alphabetically && Discourse.Site.currentProp('top_tags')) {
|
||||
return Discourse.Site.currentProp('top_tags').sort();
|
||||
} else {
|
||||
return Discourse.Site.currentProp('top_tags');
|
||||
}
|
||||
}.property('site.top_tags'),
|
||||
|
||||
iconClass: function() {
|
||||
if (this.get('expanded')) { return "fa fa-caret-down"; }
|
||||
return "fa fa-caret-right";
|
||||
}.property('expanded'),
|
||||
|
||||
tagClass: function() {
|
||||
if (this.get('tagId')) {
|
||||
return "tag-" + this.get('tagId');
|
||||
} else {
|
||||
return "tag_all";
|
||||
}
|
||||
}.property('tagId'),
|
||||
|
||||
allTagsUrl: function() {
|
||||
if (this.get('currentCategory')) {
|
||||
return this.get('currentCategory.url') + "?allTags=1";
|
||||
} else {
|
||||
return "/";
|
||||
}
|
||||
}.property('firstCategory', 'secondCategory'),
|
||||
|
||||
allTagsLabel: function() {
|
||||
return I18n.t("tagging.selector_all_tags");
|
||||
}.property('tag'),
|
||||
|
||||
dropdownButtonClass: function() {
|
||||
var result = 'badge-category category-dropdown-button';
|
||||
if (Em.isNone(this.get('tag'))) {
|
||||
result += ' home';
|
||||
}
|
||||
return result;
|
||||
}.property('tag'),
|
||||
|
||||
clickEventName: function() {
|
||||
return "click.tag-drop-" + (this.get('tag') || "all");
|
||||
}.property('tag'),
|
||||
|
||||
actions: {
|
||||
expand: function() {
|
||||
var self = this;
|
||||
|
||||
if(!this.get('renderTags')){
|
||||
this.set('renderTags',true);
|
||||
Em.run.next(function(){
|
||||
self.send('expand');
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.get('expanded')) {
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.get('tags')) {
|
||||
this.set('expanded', true);
|
||||
}
|
||||
var $dropdown = this.$()[0];
|
||||
|
||||
this.$('a[data-drop-close]').on('click.tag-drop', function() {
|
||||
self.close();
|
||||
});
|
||||
|
||||
Em.run.next(function(){
|
||||
self.$('.cat a').add('html').on(self.get('clickEventName'), function(e) {
|
||||
var $target = $(e.target),
|
||||
closest = $target.closest($dropdown);
|
||||
|
||||
if ($(e.currentTarget).hasClass('badge-wrapper')){
|
||||
self.close();
|
||||
}
|
||||
|
||||
return ($(e.currentTarget).hasClass('badge-category') || (closest.length && closest[0] === $dropdown)) ? true : self.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
removeEvents: function(){
|
||||
$('html').off(this.get('clickEventName'));
|
||||
this.$('a[data-drop-close]').off('click.tag-drop');
|
||||
},
|
||||
|
||||
close: function() {
|
||||
this.removeEvents();
|
||||
this.set('expanded', false);
|
||||
},
|
||||
|
||||
willDestroyElement: function() {
|
||||
this.removeEvents();
|
||||
}
|
||||
|
||||
});
|
||||
@ -0,0 +1,11 @@
|
||||
import NotificationsButton from 'discourse/components/notifications-button';
|
||||
|
||||
export default NotificationsButton.extend({
|
||||
classNames: ['notification-options', 'tag-notification-menu'],
|
||||
buttonIncludesText: false,
|
||||
i18nPrefix: 'tagging.notifications',
|
||||
|
||||
clicked(id) {
|
||||
this.sendAction('action', id);
|
||||
}
|
||||
});
|
||||
@ -1,104 +0,0 @@
|
||||
import { url } from 'discourse/lib/computed';
|
||||
import { default as computed, observes } from 'ember-addons/ember-computed-decorators';
|
||||
import { headerHeight } from 'discourse/views/header';
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNames: ['user-menu'],
|
||||
notifications: null,
|
||||
loadingNotifications: false,
|
||||
notificationsPath: url('currentUser.path', '%@/notifications'),
|
||||
bookmarksPath: url('currentUser.path', '%@/activity/bookmarks'),
|
||||
messagesPath: url('currentUser.path', '%@/messages'),
|
||||
preferencesPath: url('currentUser.path', '%@/preferences'),
|
||||
|
||||
@computed('allowAnon', 'isAnon')
|
||||
showEnableAnon(allowAnon, isAnon) { return allowAnon && !isAnon; },
|
||||
|
||||
@computed('allowAnon', 'isAnon')
|
||||
showDisableAnon(allowAnon, isAnon) { return allowAnon && isAnon; },
|
||||
|
||||
@observes('visible')
|
||||
_loadNotifications() {
|
||||
if (this.get("visible")) {
|
||||
this.refreshNotifications();
|
||||
}
|
||||
},
|
||||
|
||||
@observes('currentUser.lastNotificationChange')
|
||||
_resetCachedNotifications() {
|
||||
const visible = this.get('visible');
|
||||
|
||||
if (!Discourse.get("hasFocus")) {
|
||||
this.set('visible', false);
|
||||
this.set('notifications', null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (visible) {
|
||||
this.refreshNotifications();
|
||||
} else {
|
||||
this.set('notifications', null);
|
||||
}
|
||||
},
|
||||
|
||||
refreshNotifications() {
|
||||
if (this.get('loadingNotifications')) { return; }
|
||||
|
||||
// estimate (poorly) the amount of notifications to return
|
||||
var limit = Math.round(($(window).height() - headerHeight()) / 55);
|
||||
// we REALLY don't want to be asking for negative counts of notifications
|
||||
// less than 5 is also not that useful
|
||||
if (limit < 5) { limit = 5; }
|
||||
if (limit > 40) { limit = 40; }
|
||||
|
||||
// TODO: It's a bit odd to use the store in a component, but this one really
|
||||
// wants to reach out and grab notifications
|
||||
const store = this.container.lookup('store:main');
|
||||
const stale = store.findStale('notification', {recent: true, limit }, {cacheKey: 'recent-notifications'});
|
||||
|
||||
if (stale.hasResults) {
|
||||
const results = stale.results;
|
||||
var content = results.get('content');
|
||||
|
||||
// we have to truncate to limit, otherwise we will render too much
|
||||
if (content && (content.length > limit)) {
|
||||
content = content.splice(0, limit);
|
||||
results.set('content', content);
|
||||
results.set('totalRows', limit);
|
||||
}
|
||||
|
||||
this.set('notifications', results);
|
||||
} else {
|
||||
this.set('loadingNotifications', true);
|
||||
}
|
||||
|
||||
stale.refresh().then((notifications) => {
|
||||
this.set('currentUser.unread_notifications', 0);
|
||||
this.set('notifications', notifications);
|
||||
}).catch(() => {
|
||||
this.set('notifications', null);
|
||||
}).finally(() => {
|
||||
this.set('loadingNotifications', false);
|
||||
});
|
||||
},
|
||||
|
||||
@computed()
|
||||
allowAnon() {
|
||||
return this.siteSettings.allow_anonymous_posting &&
|
||||
(this.get("currentUser.trust_level") >= this.siteSettings.anonymous_posting_min_trust_level ||
|
||||
this.get("isAnon"));
|
||||
},
|
||||
|
||||
isAnon: Ember.computed.alias('currentUser.is_anonymous'),
|
||||
|
||||
actions: {
|
||||
toggleAnon() {
|
||||
Discourse.ajax("/users/toggle-anon", {method: 'POST'}).then(function(){
|
||||
window.location.reload();
|
||||
});
|
||||
},
|
||||
logout() {
|
||||
this.sendAction('logoutAction');
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,16 @@
|
||||
import MountWidget from 'discourse/components/mount-widget';
|
||||
import { observes } from "ember-addons/ember-computed-decorators";
|
||||
|
||||
export default MountWidget.extend({
|
||||
widget: 'user-notifications-large',
|
||||
|
||||
init() {
|
||||
this._super();
|
||||
this.args = { notifications: this.get('notifications') };
|
||||
},
|
||||
|
||||
@observes('notifications.length')
|
||||
_triggerRefresh() {
|
||||
this.queueRerender();
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,57 @@
|
||||
import LoadMore from "discourse/mixins/load-more";
|
||||
import ClickTrack from 'discourse/lib/click-track';
|
||||
|
||||
export default Ember.Component.extend(LoadMore, {
|
||||
loading: false,
|
||||
eyelineSelector: '.user-stream .item',
|
||||
classNames: ['user-stream'],
|
||||
|
||||
_scrollTopOnModelChange: function() {
|
||||
Em.run.schedule('afterRender', () => $(document).scrollTop(0));
|
||||
}.observes('stream.user.id'),
|
||||
|
||||
_inserted: function() {
|
||||
this.bindScrolling({name: 'user-stream-view'});
|
||||
|
||||
$(window).on('resize.discourse-on-scroll', () => this.scrolled());
|
||||
|
||||
this.$().on('mouseup.discourse-redirect', '.excerpt a', function(e) {
|
||||
// bypass if we are selecting stuff
|
||||
const selection = window.getSelection && window.getSelection();
|
||||
if (selection.type === "Range" || selection.rangeCount > 0) {
|
||||
if (Discourse.Utilities.selectedText() !== "") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const $target = $(e.target);
|
||||
if ($target.hasClass('mention') || $target.parents('.expanded-embed').length) { return false; }
|
||||
|
||||
return ClickTrack.trackClick(e);
|
||||
});
|
||||
|
||||
}.on('didInsertElement'),
|
||||
|
||||
// This view is being removed. Shut down operations
|
||||
_destroyed: function() {
|
||||
this.unbindScrolling('user-stream-view');
|
||||
$(window).unbind('resize.discourse-on-scroll');
|
||||
|
||||
// Unbind link tracking
|
||||
this.$().off('mouseup.discourse-redirect', '.excerpt a');
|
||||
|
||||
}.on('willDestroyElement'),
|
||||
|
||||
actions: {
|
||||
loadMore() {
|
||||
if (this.get('loading')) { return; }
|
||||
|
||||
this.set('loading', true);
|
||||
const stream = this.get('stream');
|
||||
stream.findItems().then(() => {
|
||||
this.set('loading', false);
|
||||
this.get('eyeline').flushRest();
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -81,6 +81,14 @@ export default Ember.Controller.extend({
|
||||
this.set('similarTopics', []);
|
||||
}.on('init'),
|
||||
|
||||
@computed('model.canEditTitle', 'model.creatingPrivateMessage')
|
||||
canEditTags(canEditTitle, creatingPrivateMessage) {
|
||||
return !this.site.mobileView &&
|
||||
this.site.get('can_tag_topics') &&
|
||||
canEditTitle &&
|
||||
!creatingPrivateMessage;
|
||||
},
|
||||
|
||||
@computed('model.action')
|
||||
canWhisper(action) {
|
||||
const currentUser = this.currentUser;
|
||||
@ -386,7 +394,7 @@ export default Ember.Controller.extend({
|
||||
let message = this.get('similarTopicsMessage');
|
||||
if (!message) {
|
||||
message = Discourse.ComposerMessage.create({
|
||||
templateName: 'composer/similar_topics',
|
||||
templateName: 'composer/similar-topics',
|
||||
extraClass: 'similar-topics'
|
||||
});
|
||||
this.set('similarTopicsMessage', message);
|
||||
|
||||
@ -127,7 +127,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
failed: true,
|
||||
reason: I18n.t('user.email.invalid')
|
||||
});
|
||||
}.property('accountEmail', 'rejectedEmails.@each'),
|
||||
}.property('accountEmail', 'rejectedEmails.[]'),
|
||||
|
||||
emailValidated: function() {
|
||||
return this.get('authOptions.email') === this.get("accountEmail") && this.get('authOptions.email_valid');
|
||||
@ -326,7 +326,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
ok: true,
|
||||
reason: I18n.t('user.password.ok')
|
||||
});
|
||||
}.property('accountPassword', 'rejectedPasswords.@each', 'accountUsername', 'accountEmail', 'isDeveloper'),
|
||||
}.property('accountPassword', 'rejectedPasswords.[]', 'accountUsername', 'accountEmail', 'isDeveloper'),
|
||||
|
||||
@on('init')
|
||||
fetchConfirmationValue() {
|
||||
|
||||
@ -12,7 +12,7 @@ export var queryParams = {
|
||||
// Basic controller options
|
||||
var controllerOpts = {
|
||||
needs: ['discovery/topics'],
|
||||
queryParams: Ember.keys(queryParams),
|
||||
queryParams: Object.keys(queryParams),
|
||||
};
|
||||
|
||||
// Aliases for the values
|
||||
|
||||
@ -138,14 +138,11 @@ const controllerOpts = {
|
||||
return I18n.t("topics.none.educate." + split[0], {
|
||||
userPrefsUrl: Discourse.getURL("/users/") + (Discourse.User.currentProp("username_lower")) + "/preferences"
|
||||
});
|
||||
}.property('allLoaded', 'model.topics.length'),
|
||||
}.property('allLoaded', 'model.topics.length')
|
||||
|
||||
loadMoreTopics() {
|
||||
return this.get('model').loadMore();
|
||||
}
|
||||
};
|
||||
|
||||
Ember.keys(queryParams).forEach(function(p) {
|
||||
Object.keys(queryParams).forEach(function(p) {
|
||||
// If we don't have a default value, initialize it to null
|
||||
if (typeof controllerOpts[p] === 'undefined') {
|
||||
controllerOpts[p] = null;
|
||||
|
||||
@ -0,0 +1,27 @@
|
||||
import { fmt } from 'discourse/lib/computed';
|
||||
|
||||
export default Ember.ArrayController.extend({
|
||||
needs: ['group'],
|
||||
loading: false,
|
||||
emptyText: fmt('type', 'groups.empty.%@'),
|
||||
|
||||
actions: {
|
||||
loadMore() {
|
||||
|
||||
if (this.get('loading')) { return; }
|
||||
this.set('loading', true);
|
||||
const posts = this.get('model');
|
||||
if (posts && posts.length) {
|
||||
const beforePostId = posts[posts.length-1].get('id');
|
||||
const group = this.get('controllers.group.model');
|
||||
|
||||
const opts = { beforePostId, type: this.get('type') };
|
||||
group.findPosts(opts).then(newPosts => {
|
||||
posts.addObjects(newPosts);
|
||||
this.set('loading', false);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,28 +0,0 @@
|
||||
/**
|
||||
Handles displaying posts within a group
|
||||
**/
|
||||
export default Ember.ArrayController.extend({
|
||||
needs: ['group'],
|
||||
loading: false,
|
||||
|
||||
actions: {
|
||||
loadMore: function() {
|
||||
|
||||
if (this.get('loading')) { return; }
|
||||
this.set('loading', true);
|
||||
var posts = this.get('model'),
|
||||
self = this;
|
||||
if (posts && posts.length) {
|
||||
var lastPostId = posts[posts.length-1].get('id'),
|
||||
group = this.get('controllers.group.model');
|
||||
|
||||
var opts = {beforePostId: lastPostId, type: this.get('type')};
|
||||
group.findPosts(opts).then(function(newPosts) {
|
||||
posts.addObjects(newPosts);
|
||||
self.set('loading', false);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,12 +1,13 @@
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import Group from 'discourse/models/group';
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
loading: false,
|
||||
limit: null,
|
||||
offset: null,
|
||||
|
||||
@computed('model.owners.@each')
|
||||
@computed('model.owners.[]')
|
||||
isOwner(owners) {
|
||||
if (this.get('currentUser.admin')) {
|
||||
return true;
|
||||
@ -30,10 +31,7 @@ export default Ember.Controller.extend({
|
||||
},
|
||||
|
||||
loadMore() {
|
||||
const Group = require('discourse/models/group').default;
|
||||
|
||||
if (this.get("loading")) { return; }
|
||||
// we've reached the end
|
||||
if (this.get("model.members.length") >= this.get("model.user_count")) { return; }
|
||||
|
||||
this.set("loading", true);
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
import IndexController from 'discourse/controllers/group/index';
|
||||
|
||||
export default IndexController.extend({type: 'mentions'});
|
||||
@ -1,3 +0,0 @@
|
||||
import IndexController from 'discourse/controllers/group/index';
|
||||
|
||||
export default IndexController.extend({type: 'topics'});
|
||||
@ -1,77 +1,6 @@
|
||||
import DiscourseURL from 'discourse/lib/url';
|
||||
import { addFlagProperty as realAddFlagProperty } from 'discourse/components/site-header';
|
||||
|
||||
const HeaderController = Ember.Controller.extend({
|
||||
topic: null,
|
||||
showExtraInfo: null,
|
||||
hamburgerVisible: false,
|
||||
searchVisible: false,
|
||||
userMenuVisible: false,
|
||||
needs: ['application'],
|
||||
|
||||
canSignUp: Em.computed.alias('controllers.application.canSignUp'),
|
||||
|
||||
showSignUpButton: function() {
|
||||
return this.get('canSignUp') && !this.get('showExtraInfo');
|
||||
}.property('canSignUp', 'showExtraInfo'),
|
||||
|
||||
showStarButton: function() {
|
||||
return Discourse.User.current() && !this.get('topic.isPrivateMessage');
|
||||
}.property('topic.isPrivateMessage'),
|
||||
|
||||
|
||||
actions: {
|
||||
toggleSearch() {
|
||||
this.toggleProperty('searchVisible');
|
||||
},
|
||||
showUserMenu() {
|
||||
if (!this.get('userMenuVisible')) {
|
||||
this.appEvents.trigger('dropdowns:closeAll');
|
||||
this.set('userMenuVisible', true);
|
||||
}
|
||||
},
|
||||
|
||||
fullPageSearch() {
|
||||
const searchService = this.container.lookup('search-service:main');
|
||||
const context = searchService.get('searchContext');
|
||||
var params = "";
|
||||
|
||||
if (context) {
|
||||
params = `?context=${context.type}&context_id=${context.id}&skip_context=true`;
|
||||
}
|
||||
|
||||
DiscourseURL.routeTo('/search' + params);
|
||||
},
|
||||
toggleMenuPanel(visibleProp) {
|
||||
this.toggleProperty(visibleProp);
|
||||
this.appEvents.trigger('dropdowns:closeAll');
|
||||
},
|
||||
|
||||
toggleStar() {
|
||||
const topic = this.get('topic');
|
||||
if (topic) topic.toggleStar();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Allow plugins to add to the sum of "flags" above the site map
|
||||
const _flagProperties = [];
|
||||
function addFlagProperty(prop) {
|
||||
_flagProperties.pushObject(prop);
|
||||
export function addFlagProperty(prop) {
|
||||
Ember.warn("importing `addFlagProperty` is deprecated. Use the PluginAPI instead");
|
||||
realAddFlagProperty(prop);
|
||||
}
|
||||
|
||||
function applyFlaggedProperties() {
|
||||
const args = _flagProperties.slice();
|
||||
args.push(function() {
|
||||
let sum = 0;
|
||||
_flagProperties.forEach((fp) => sum += (this.get(fp) || 0));
|
||||
return sum;
|
||||
});
|
||||
HeaderController.reopen({ flaggedPostsCount: Ember.computed.apply(this, args) });
|
||||
}
|
||||
|
||||
addFlagProperty('currentUser.site_flagged_posts_count');
|
||||
addFlagProperty('currentUser.post_queue_new_count');
|
||||
|
||||
export { addFlagProperty, applyFlaggedProperties };
|
||||
export default HeaderController;
|
||||
|
||||
@ -3,6 +3,15 @@ import { categoryBadgeHTML } from 'discourse/helpers/category-link';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import { propertyGreaterThan, propertyLessThan } from 'discourse/lib/computed';
|
||||
|
||||
function customTagArray(fieldName) {
|
||||
return function() {
|
||||
var val = this.get(fieldName);
|
||||
if (!val) { return val; }
|
||||
if (!Array.isArray(val)) { val = [val]; }
|
||||
return val;
|
||||
}.property(fieldName);
|
||||
}
|
||||
|
||||
// This controller handles displaying of history
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
loading: true,
|
||||
@ -13,6 +22,9 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
if (this.site.mobileView) { this.set("viewMode", "inline"); }
|
||||
}.on("init"),
|
||||
|
||||
previousTagChanges: customTagArray('model.tags_changes.previous'),
|
||||
currentTagChanges: customTagArray('model.tags_changes.current'),
|
||||
|
||||
refresh(postId, postVersion) {
|
||||
this.set("loading", true);
|
||||
|
||||
|
||||
@ -27,7 +27,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
|
||||
**/
|
||||
hasAtLeastOneLoginButton: function() {
|
||||
return Em.get("Discourse.LoginMethod.all").length > 0;
|
||||
}.property("Discourse.LoginMethod.all.@each"),
|
||||
}.property("Discourse.LoginMethod.all.[]"),
|
||||
|
||||
loginButtonText: function() {
|
||||
return this.get('loggingIn') ? I18n.t('login.logging_in') : I18n.t('login.title');
|
||||
|
||||
@ -0,0 +1,25 @@
|
||||
import ModalFunctionality from 'discourse/mixins/modal-functionality';
|
||||
import BufferedContent from 'discourse/mixins/buffered-content';
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, BufferedContent, {
|
||||
|
||||
renameDisabled: function() {
|
||||
const filterRegexp = new RegExp(this.site.tags_filter_regexp, "g"),
|
||||
newId = this.get('buffered.id').replace(filterRegexp, '').trim();
|
||||
|
||||
return (newId.length === 0) || (newId === this.get('model.id'));
|
||||
}.property('buffered.id', 'id'),
|
||||
|
||||
actions: {
|
||||
performRename() {
|
||||
const tag = this.get('model'),
|
||||
self = this;
|
||||
tag.update({ id: this.get('buffered.id') }).then(function() {
|
||||
self.send('closeModal');
|
||||
self.transitionToRoute('tags.show', tag.get('id'));
|
||||
}).catch(function() {
|
||||
self.flash(I18n.t('generic_error'), 'error');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,15 @@
|
||||
export default Ember.Controller.extend({
|
||||
sortProperties: ['count:desc', 'id'],
|
||||
|
||||
sortedTags: Ember.computed.sort('model', 'sortProperties'),
|
||||
|
||||
actions: {
|
||||
sortByCount() {
|
||||
this.set('sortProperties', ['count:desc', 'id']);
|
||||
},
|
||||
|
||||
sortById() {
|
||||
this.set('sortProperties', ['id']);
|
||||
}
|
||||
}
|
||||
});
|
||||
133
app/assets/javascripts/discourse/controllers/tags-show.js.es6
Normal file
133
app/assets/javascripts/discourse/controllers/tags-show.js.es6
Normal file
@ -0,0 +1,133 @@
|
||||
import BulkTopicSelection from "discourse/mixins/bulk-topic-selection";
|
||||
import { default as NavItem, extraNavItemProperties, customNavItemHref } from 'discourse/models/nav-item';
|
||||
|
||||
if (extraNavItemProperties) {
|
||||
extraNavItemProperties(function(text, opts) {
|
||||
if (opts && opts.tagId) {
|
||||
return {tagId: opts.tagId};
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (customNavItemHref) {
|
||||
customNavItemHref(function(navItem) {
|
||||
if (navItem.get('tagId')) {
|
||||
var name = navItem.get('name');
|
||||
|
||||
if ( !Discourse.Site.currentProp('filters').contains(name) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var path = "/tags/",
|
||||
category = navItem.get("category");
|
||||
|
||||
if(category){
|
||||
path += "c/";
|
||||
path += Discourse.Category.slugFor(category);
|
||||
if (navItem.get('noSubcategories')) { path += '/none'; }
|
||||
path += "/";
|
||||
}
|
||||
|
||||
path += navItem.get('tagId') + "/l/";
|
||||
return path + name.replace(' ', '-');
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export default Ember.Controller.extend(BulkTopicSelection, {
|
||||
needs: ["application"],
|
||||
|
||||
tag: null,
|
||||
list: null,
|
||||
canAdminTag: Ember.computed.alias("currentUser.staff"),
|
||||
filterMode: null,
|
||||
navMode: 'latest',
|
||||
loading: false,
|
||||
canCreateTopic: false,
|
||||
order: 'default',
|
||||
ascending: false,
|
||||
status: null,
|
||||
state: null,
|
||||
search: null,
|
||||
max_posts: null,
|
||||
q: null,
|
||||
|
||||
queryParams: ['order', 'ascending', 'status', 'state', 'search', 'max_posts', 'q'],
|
||||
|
||||
navItems: function() {
|
||||
return NavItem.buildList(this.get('category'), {tagId: this.get('tag.id'), filterMode: this.get('filterMode')});
|
||||
}.property('category', 'tag.id', 'filterMode'),
|
||||
|
||||
showTagFilter: function() {
|
||||
return Discourse.SiteSettings.show_filter_by_tag;
|
||||
}.property('category'),
|
||||
|
||||
categories: function() {
|
||||
return Discourse.Category.list();
|
||||
}.property(),
|
||||
|
||||
showAdminControls: function() {
|
||||
return this.get('canAdminTag') && !this.get('category');
|
||||
}.property('canAdminTag', 'category'),
|
||||
|
||||
loadMoreTopics() {
|
||||
return this.get("list").loadMore();
|
||||
},
|
||||
|
||||
_showFooter: function() {
|
||||
this.set("controllers.application.showFooter", !this.get("list.canLoadMore"));
|
||||
}.observes("list.canLoadMore"),
|
||||
|
||||
footerMessage: function() {
|
||||
if (this.get('loading') || this.get('list.topics.length') !== 0) { return; }
|
||||
|
||||
if (this.get('list.topics.length') === 0) {
|
||||
return I18n.t('tagging.topics.none.' + this.get('navMode'), {tag: this.get('tag.id')});
|
||||
} else {
|
||||
return I18n.t('tagging.topics.bottom.' + this.get('navMode'), {tag: this.get('tag.id')});
|
||||
}
|
||||
}.property('navMode', 'list.topics.length', 'loading'),
|
||||
|
||||
actions: {
|
||||
changeSort(sortBy) {
|
||||
if (sortBy === this.get('order')) {
|
||||
this.toggleProperty('ascending');
|
||||
} else {
|
||||
this.setProperties({ order: sortBy, ascending: false });
|
||||
}
|
||||
this.send('invalidateModel');
|
||||
},
|
||||
|
||||
refresh() {
|
||||
const self = this;
|
||||
// TODO: this probably doesn't work anymore
|
||||
return this.store.findFiltered('topicList', {filter: 'tags/' + this.get('tag.id')}).then(function(list) {
|
||||
self.set("list", list);
|
||||
self.resetSelected();
|
||||
});
|
||||
},
|
||||
|
||||
deleteTag() {
|
||||
const self = this;
|
||||
bootbox.confirm(I18n.t("tagging.delete_confirm"), function(result) {
|
||||
if (!result) { return; }
|
||||
|
||||
self.get("tag").destroyRecord().then(function() {
|
||||
self.transitionToRoute("tags.index");
|
||||
}).catch(function() {
|
||||
bootbox.alert(I18n.t("generic_error"));
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
changeTagNotification(id) {
|
||||
const tagNotification = this.get("tagNotification");
|
||||
tagNotification.update({ notification_level: id });
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -14,11 +14,15 @@ addBulkButton('archiveTopics', 'archive_topics');
|
||||
addBulkButton('showNotificationLevel', 'notification_level');
|
||||
addBulkButton('resetRead', 'reset_read');
|
||||
addBulkButton('unlistTopics', 'unlist_topics');
|
||||
addBulkButton('showTagTopics', 'change_tags');
|
||||
|
||||
// Modal for performing bulk actions on topics
|
||||
export default Ember.ArrayController.extend(ModalFunctionality, {
|
||||
tags: null,
|
||||
buttonRows: null,
|
||||
|
||||
emptyTags: Ember.computed.empty('tags'),
|
||||
|
||||
onShow() {
|
||||
this.set('controllers.modal.modalClass', 'topic-bulk-actions-modal small');
|
||||
|
||||
@ -73,6 +77,15 @@ export default Ember.ArrayController.extend(ModalFunctionality, {
|
||||
},
|
||||
|
||||
actions: {
|
||||
showTagTopics() {
|
||||
this.set('tags', '');
|
||||
this.send('changeBulkTemplate', 'bulk-tag');
|
||||
},
|
||||
|
||||
changeTags() {
|
||||
this.performAndRefresh({type: 'change_tags', tags: this.get('tags')});
|
||||
},
|
||||
|
||||
showChangeCategory() {
|
||||
this.send('changeBulkTemplate', 'modal/bulk_change_category');
|
||||
this.set('controllers.modal.modalClass', 'topic-bulk-actions-modal full');
|
||||
|
||||
@ -9,7 +9,7 @@ import Composer from 'discourse/models/composer';
|
||||
import DiscourseURL from 'discourse/lib/url';
|
||||
|
||||
export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||
needs: ['header', 'modal', 'composer', 'quote-button', 'topic-progress', 'application'],
|
||||
needs: ['modal', 'composer', 'quote-button', 'topic-progress', 'application'],
|
||||
multiSelect: false,
|
||||
allPostsSelected: false,
|
||||
editingTopic: false,
|
||||
@ -108,6 +108,11 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||
return post => this.postSelected(post);
|
||||
}.property(),
|
||||
|
||||
@computed('model.isPrivateMessage')
|
||||
canEditTags(isPrivateMessage) {
|
||||
return !isPrivateMessage && this.site.get('can_tag_topics');
|
||||
},
|
||||
|
||||
actions: {
|
||||
|
||||
fillGapBefore(args) {
|
||||
@ -472,11 +477,6 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||
this.get('content').toggleStatus('archived');
|
||||
},
|
||||
|
||||
// Toggle the star on the topic
|
||||
toggleStar() {
|
||||
this.get('content').toggleStar();
|
||||
},
|
||||
|
||||
clearPin() {
|
||||
this.get('content').clearPin();
|
||||
},
|
||||
@ -509,7 +509,7 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||
}).then(q => {
|
||||
const postUrl = `${location.protocol}//${location.host}${post.get('url')}`;
|
||||
const postLink = `[${Handlebars.escapeExpression(self.get('model.title'))}](${postUrl})`;
|
||||
composerController.get('model').appendText(`${I18n.t("post.continue_discussion", { postLink })}\n\n${q}`);
|
||||
composerController.get('model').prependText(`${I18n.t("post.continue_discussion", { postLink })}\n\n${q}`, {new_line: true});
|
||||
});
|
||||
},
|
||||
|
||||
@ -545,6 +545,14 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||
changePostOwner(post) {
|
||||
this.get('selectedPosts').addObject(post);
|
||||
this.send('changeOwner');
|
||||
},
|
||||
|
||||
convertToPublicTopic() {
|
||||
this.get('content').convertTopic("public");
|
||||
},
|
||||
|
||||
convertToPrivateMessage() {
|
||||
this.get('content').convertTopic("private");
|
||||
}
|
||||
},
|
||||
|
||||
@ -625,10 +633,6 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, {
|
||||
return false;
|
||||
},
|
||||
|
||||
showStarButton: function() {
|
||||
return Discourse.User.current() && !this.get('model.isPrivateMessage');
|
||||
}.property('model.isPrivateMessage'),
|
||||
|
||||
loadingHTML: function() {
|
||||
return spinnerHTML;
|
||||
}.property(),
|
||||
|
||||
@ -49,7 +49,7 @@ export default Ember.Controller.extend({
|
||||
|
||||
moreBadgesCount: function() {
|
||||
return this.get('user.badge_count') - this.get('user.featured_user_badges.length');
|
||||
}.property('user.badge_count', 'user.featured_user_badges.@each'),
|
||||
}.property('user.badge_count', 'user.featured_user_badges.[]'),
|
||||
|
||||
hasCardBadgeImage: function() {
|
||||
const img = this.get('user.card_badge.image');
|
||||
|
||||
@ -5,18 +5,16 @@ export default Ember.ArrayController.extend({
|
||||
this.set("controllers.application.showFooter", !this.get("model.canLoadMore"));
|
||||
}.observes("model.canLoadMore"),
|
||||
|
||||
showDismissButton: Ember.computed.gt('user.total_unread_notifications', 0),
|
||||
|
||||
currentPath: Em.computed.alias('controllers.application.currentPath'),
|
||||
|
||||
actions: {
|
||||
resetNew: function() {
|
||||
resetNew() {
|
||||
Discourse.ajax('/notifications/mark-read', { method: 'PUT' }).then(() => {
|
||||
this.setEach('read', true);
|
||||
});
|
||||
},
|
||||
|
||||
loadMore: function() {
|
||||
loadMore() {
|
||||
this.get('model').loadMore();
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,7 +15,7 @@ export default Ember.Controller.extend({
|
||||
Discourse.User.currentProp('can_send_private_messages');
|
||||
}.property('controllers.user.viewingSelf'),
|
||||
|
||||
@computed('selected.@each', 'bulkSelectEnabled')
|
||||
@computed('selected.[]', 'bulkSelectEnabled')
|
||||
hasSelection(selected, bulkSelectEnabled){
|
||||
return bulkSelectEnabled && selected && selected.length > 0;
|
||||
},
|
||||
|
||||
@ -70,7 +70,7 @@ export default Ember.DefaultResolver.extend({
|
||||
// If we end with the name we want, use it. This allows us to define components within plugins.
|
||||
const suffix = parsedName.type + 's/' + parsedName.fullNameWithoutType,
|
||||
dashed = Ember.String.dasherize(suffix),
|
||||
moduleName = Ember.keys(requirejs.entries).find(function(e) {
|
||||
moduleName = Object.keys(requirejs.entries).find(function(e) {
|
||||
return (e.indexOf(suffix, e.length - suffix.length) !== -1) ||
|
||||
(e.indexOf(dashed, e.length - dashed.length) !== -1);
|
||||
});
|
||||
@ -151,11 +151,13 @@ export default Ember.DefaultResolver.extend({
|
||||
const withoutType = parsedName.fullNameWithoutType,
|
||||
slashedType = withoutType.replace(/\./g, '/'),
|
||||
decamelized = withoutType.decamelize(),
|
||||
dashed = decamelized.replace(/\./g, '-').replace(/\_/g, '-'),
|
||||
templates = Ember.TEMPLATES;
|
||||
|
||||
return this._super(parsedName) ||
|
||||
templates[slashedType] ||
|
||||
templates[withoutType] ||
|
||||
templates[dashed] ||
|
||||
templates[decamelized.replace(/\./, '/')] ||
|
||||
templates[decamelized.replace(/\_/, '/')] ||
|
||||
this.findAdminTemplate(parsedName) ||
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
import registerUnbound from 'discourse/helpers/register-unbound';
|
||||
import renderTag from 'discourse/lib/render-tag';
|
||||
|
||||
export default registerUnbound('discourse-tag', function(name, params) {
|
||||
return new Handlebars.SafeString(renderTag(name, params));
|
||||
});
|
||||
@ -60,7 +60,7 @@ function findOutlets(collection, callback) {
|
||||
|
||||
const disabledPlugins = Discourse.Site.currentProp('disabled_plugins') || [];
|
||||
|
||||
Ember.keys(collection).forEach(function(res) {
|
||||
Object.keys(collection).forEach(function(res) {
|
||||
if (res.indexOf("/connectors/") !== -1) {
|
||||
// Skip any disabled plugins
|
||||
for (let i=0; i<disabledPlugins.length; i++) {
|
||||
|
||||
@ -6,7 +6,7 @@ function resolveParams(ctx, options) {
|
||||
|
||||
if (hash) {
|
||||
if (options.hashTypes) {
|
||||
Ember.keys(hash).forEach(function(k) {
|
||||
Object.keys(hash).forEach(function(k) {
|
||||
const type = options.hashTypes[k];
|
||||
if (type === "STRING" || type === "StringLiteral") {
|
||||
params[k] = hash[k];
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { applyFlaggedProperties } from 'discourse/controllers/header';
|
||||
import { applyFlaggedProperties } from 'discourse/components/site-header';
|
||||
|
||||
export default {
|
||||
name: 'apply-flagged-properties',
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
export function autoLoadModules() {
|
||||
Ember.keys(requirejs.entries).forEach(entry => {
|
||||
Object.keys(requirejs.entries).forEach(entry => {
|
||||
if ((/\/helpers\//).test(entry)) {
|
||||
require(entry, null, null, true);
|
||||
}
|
||||
|
||||
@ -10,7 +10,8 @@ export default {
|
||||
siteSettings = container.lookup('site-settings:main'),
|
||||
bus = container.lookup('message-bus:main'),
|
||||
keyValueStore = container.lookup('key-value-store:main'),
|
||||
store = container.lookup('store:main');
|
||||
store = container.lookup('store:main'),
|
||||
appEvents = container.lookup('app-events:main');
|
||||
|
||||
// clear old cached notifications, we used to store in local storage
|
||||
// TODO 2017 delete this line
|
||||
@ -30,7 +31,7 @@ export default {
|
||||
});
|
||||
}
|
||||
|
||||
bus.subscribe("/notification/" + user.get('id'), function(data) {
|
||||
bus.subscribe(`/notification/${user.get('id')}`, function(data) {
|
||||
const oldUnread = user.get('unread_notifications');
|
||||
const oldPM = user.get('unread_private_messages');
|
||||
|
||||
@ -38,7 +39,7 @@ export default {
|
||||
user.set('unread_private_messages', data.unread_private_messages);
|
||||
|
||||
if (oldUnread !== data.unread_notifications || oldPM !== data.unread_private_messages) {
|
||||
user.set('lastNotificationChange', new Date());
|
||||
appEvents.trigger('notifications:changed');
|
||||
}
|
||||
|
||||
const stale = store.findStale('notification', {}, {cacheKey: 'recent-notifications'});
|
||||
|
||||
@ -1,25 +1 @@
|
||||
|
||||
var id = 1;
|
||||
function newKey() {
|
||||
return "_view_app_event_" + (id++);
|
||||
}
|
||||
|
||||
function createViewListener(eventName, cb) {
|
||||
var extension = {};
|
||||
extension[newKey()] = function() {
|
||||
this.appEvents.on(eventName, this, cb);
|
||||
}.on('didInsertElement');
|
||||
|
||||
extension[newKey()] = function() {
|
||||
this.appEvents.off(eventName, this, cb);
|
||||
}.on('willDestroyElement');
|
||||
|
||||
return extension;
|
||||
}
|
||||
|
||||
function listenForViewEvent(viewClass, eventName, cb) {
|
||||
viewClass.reopen(createViewListener(eventName, cb));
|
||||
}
|
||||
|
||||
export { listenForViewEvent, createViewListener };
|
||||
export default Ember.Object.extend(Ember.Evented);
|
||||
|
||||
@ -0,0 +1,65 @@
|
||||
import { CANCELLED_STATUS } from 'discourse/lib/autocomplete';
|
||||
import Category from 'discourse/models/category';
|
||||
|
||||
var cache = {};
|
||||
var cacheTime;
|
||||
var oldSearch;
|
||||
|
||||
function updateCache(term, results) {
|
||||
cache[term] = results;
|
||||
cacheTime = new Date();
|
||||
return results;
|
||||
}
|
||||
|
||||
function searchTags(term, categories, limit) {
|
||||
return new Ember.RSVP.Promise((resolve) => {
|
||||
const clearPromise = setTimeout(() => {
|
||||
resolve(CANCELLED_STATUS);
|
||||
}, 5000);
|
||||
|
||||
const debouncedSearch = _.debounce((q, cats, resultFunc) => {
|
||||
oldSearch = $.ajax(Discourse.getURL("/tags/filter/search"), {
|
||||
type: 'GET',
|
||||
cache: true,
|
||||
data: { limit: limit, q }
|
||||
});
|
||||
|
||||
var returnVal = CANCELLED_STATUS;
|
||||
|
||||
oldSearch.then((r) => {
|
||||
var tags = r.results.map((tag) => { return { text: tag.text, count: tag.count }; });
|
||||
returnVal = cats.concat(tags);
|
||||
}).always(() => {
|
||||
oldSearch = null;
|
||||
resultFunc(returnVal);
|
||||
});
|
||||
}, 300);
|
||||
|
||||
debouncedSearch(term, categories, (result) => {
|
||||
clearTimeout(clearPromise);
|
||||
resolve(updateCache(term, result));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export function search(term, siteSettings) {
|
||||
if (oldSearch) {
|
||||
oldSearch.abort();
|
||||
oldSearch = null;
|
||||
}
|
||||
|
||||
if ((new Date() - cacheTime) > 30000) cache = {};
|
||||
const cached = cache[term];
|
||||
if (cached) return cached;
|
||||
|
||||
const limit = 5;
|
||||
var categories = Category.search(term, { limit });
|
||||
var numOfCategories = categories.length;
|
||||
categories = categories.map((category) => { return { model: category }; });
|
||||
|
||||
if (numOfCategories !== limit && siteSettings.tagging_enabled) {
|
||||
return searchTags(term, categories, limit - numOfCategories);
|
||||
} else {
|
||||
return updateCache(term, categories);
|
||||
}
|
||||
};
|
||||
@ -1,4 +1,5 @@
|
||||
import DiscourseURL from 'discourse/lib/url';
|
||||
import { wantsNewWindow } from 'discourse/lib/intercept-click';
|
||||
|
||||
export function isValidLink($link) {
|
||||
return ($link.hasClass("track-link") ||
|
||||
@ -11,15 +12,24 @@ export default {
|
||||
if (Discourse.Utilities.selectedText() !== "") { return false; }
|
||||
|
||||
var $link = $(e.currentTarget);
|
||||
if ($link.hasClass('lightbox') || $link.hasClass('mention-group') || $link.hasClass('no-track-link')) { return true; }
|
||||
|
||||
// don't track lightboxes, group mentions or links with disabled tracking
|
||||
if ($link.hasClass('lightbox') || $link.hasClass('mention-group') ||
|
||||
$link.hasClass('no-track-link') || $link.hasClass('hashtag')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// don't track links in quotes or in elided part
|
||||
if ($link.parents('aside.quote,.elided').length) { return true; }
|
||||
|
||||
var href = $link.attr('href') || $link.data('href'),
|
||||
$article = $link.closest('article'),
|
||||
$article = $link.closest('article,.excerpt,#revisions'),
|
||||
postId = $article.data('post-id'),
|
||||
topicId = $('#topic').data('topic-id'),
|
||||
topicId = $('#topic').data('topic-id') || $article.data('topic-id'),
|
||||
userId = $link.data('user-id');
|
||||
|
||||
if (!href || href.trim().length === 0) { return; }
|
||||
if (!href || href.trim().length === 0) { return false; }
|
||||
if (href.indexOf("mailto:") === 0) { return true; }
|
||||
|
||||
if (!userId) userId = $article.data('user-id');
|
||||
|
||||
@ -52,7 +62,7 @@ export default {
|
||||
}
|
||||
|
||||
// if they want to open in a new tab, do an AJAX request
|
||||
if (e.shiftKey || e.metaKey || e.ctrlKey || e.which === 2) {
|
||||
if (wantsNewWindow(e)) {
|
||||
Discourse.ajax("/clicks/track", {
|
||||
data: {
|
||||
url: href,
|
||||
|
||||
@ -1,12 +1,16 @@
|
||||
import DiscourseURL from 'discourse/lib/url';
|
||||
|
||||
export function wantsNewWindow(e) {
|
||||
return (e.isDefaultPrevented() || e.shiftKey || e.metaKey || e.ctrlKey || (e.button && e.button !== 0));
|
||||
}
|
||||
|
||||
/**
|
||||
Discourse does some server side rendering of HTML, such as the `cooked` contents of
|
||||
posts. The downside of this in an Ember app is the links will not go through the router.
|
||||
This jQuery code intercepts clicks on those links and routes them properly.
|
||||
**/
|
||||
export default function interceptClick(e) {
|
||||
if (e.isDefaultPrevented() || e.shiftKey || e.metaKey || e.ctrlKey) { return; }
|
||||
if (wantsNewWindow(e)) { return; }
|
||||
|
||||
const $currentTarget = $(e.currentTarget),
|
||||
href = $currentTarget.attr('href');
|
||||
@ -18,6 +22,7 @@ export default function interceptClick(e) {
|
||||
$currentTarget.data('auto-route') ||
|
||||
$currentTarget.data('share-url') ||
|
||||
$currentTarget.data('user-card') ||
|
||||
$currentTarget.hasClass('widget-link') ||
|
||||
$currentTarget.hasClass('mention') ||
|
||||
(!$currentTarget.hasClass('d-link') && $currentTarget.hasClass('ember-view')) ||
|
||||
$currentTarget.hasClass('lightbox') ||
|
||||
|
||||
@ -10,8 +10,8 @@ const bindings = {
|
||||
'.': {click: '.alert.alert-info.clickable', anonymous: true}, // show incoming/updated topics
|
||||
'b': {handler: 'toggleBookmark'},
|
||||
'c': {handler: 'createTopic'},
|
||||
'ctrl+f': {handler: 'showBuiltinSearch', anonymous: true},
|
||||
'command+f': {handler: 'showBuiltinSearch', anonymous: true},
|
||||
'ctrl+f': {handler: 'showPageSearch', anonymous: true},
|
||||
'command+f': {handler: 'showPageSearch', anonymous: true},
|
||||
'd': {postAction: 'deletePost'},
|
||||
'e': {postAction: 'editPost'},
|
||||
'end': {handler: 'goToLastPost', anonymous: true},
|
||||
@ -142,32 +142,10 @@ export default {
|
||||
this._changeSection(-1);
|
||||
},
|
||||
|
||||
showBuiltinSearch() {
|
||||
if (this.container.lookup('controller:header').get('searchVisible')) {
|
||||
this.toggleSearch();
|
||||
return true;
|
||||
}
|
||||
|
||||
this.searchService.set('searchContextEnabled', false);
|
||||
|
||||
const currentPath = this.container.lookup('controller:application').get('currentPath'),
|
||||
blacklist = [ /^discovery\.categories/ ],
|
||||
whitelist = [ /^topic\./ ],
|
||||
check = function(regex) { return !!currentPath.match(regex); };
|
||||
let showSearch = whitelist.any(check) && !blacklist.any(check);
|
||||
|
||||
// If we're viewing a topic, only intercept search if there are cloaked posts
|
||||
if (showSearch && currentPath.match(/^topic\./)) {
|
||||
showSearch = $('.topic-post .cooked, .small-action:not(.time-gap)').length < this.container.lookup('controller:topic').get('model.postStream.stream.length');
|
||||
}
|
||||
|
||||
if (showSearch) {
|
||||
this.searchService.set('searchContextEnabled', true);
|
||||
this.toggleSearch();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
showPageSearch(event) {
|
||||
Ember.run(() => {
|
||||
this.appEvents.trigger('header:keyboard-trigger', {type: 'page-search', event});
|
||||
});
|
||||
},
|
||||
|
||||
createTopic() {
|
||||
@ -182,17 +160,16 @@ export default {
|
||||
this.container.lookup('controller:topic-progress').send('toggleExpansion', {highlight: true});
|
||||
},
|
||||
|
||||
toggleSearch() {
|
||||
this.container.lookup('controller:header').send('toggleSearch');
|
||||
return false;
|
||||
toggleSearch(event) {
|
||||
this.appEvents.trigger('header:keyboard-trigger', {type: 'search', event});
|
||||
},
|
||||
|
||||
toggleHamburgerMenu() {
|
||||
this.container.lookup('controller:header').send('toggleMenuPanel', 'hamburgerVisible');
|
||||
toggleHamburgerMenu(event) {
|
||||
this.appEvents.trigger('header:keyboard-trigger', {type: 'hamburger', event});
|
||||
},
|
||||
|
||||
showCurrentUser() {
|
||||
this.container.lookup('controller:header').send('toggleMenuPanel', 'userMenuVisible');
|
||||
showCurrentUser(event) {
|
||||
this.appEvents.trigger('header:keyboard-trigger', {type: 'user', event});
|
||||
},
|
||||
|
||||
showHelpModal() {
|
||||
@ -226,7 +203,13 @@ export default {
|
||||
const post = topicController.get('model.postStream.posts').findBy('id', selectedPostId);
|
||||
if (post) {
|
||||
// TODO: Use ember closure actions
|
||||
const result = topicController._actions[action].call(topicController, post);
|
||||
let actionMethod = topicController._actions[action];
|
||||
if (!actionMethod) {
|
||||
const topicRoute = container.lookup('route:topic');
|
||||
actionMethod = topicRoute._actions[action];
|
||||
}
|
||||
|
||||
const result = actionMethod.call(topicController, post);
|
||||
if (result && result.then) {
|
||||
this.appEvents.trigger('post-stream:refresh', { id: selectedPostId });
|
||||
}
|
||||
|
||||
52
app/assets/javascripts/discourse/lib/link-tag-hashtag.js.es6
Normal file
52
app/assets/javascripts/discourse/lib/link-tag-hashtag.js.es6
Normal file
@ -0,0 +1,52 @@
|
||||
import { replaceSpan } from 'discourse/lib/category-hashtags';
|
||||
import { TAG_HASHTAG_POSTFIX } from 'discourse/lib/tag-hashtags';
|
||||
|
||||
const validTagHashtags = {};
|
||||
const checkedTagHashtags = [];
|
||||
const testedClass = 'tag-hashtag-tested';
|
||||
|
||||
function updateFound($hashtags, tagValues) {
|
||||
Ember.run.schedule('afterRender', () => {
|
||||
$hashtags.each((index, hashtag) => {
|
||||
const tagValue = tagValues[index];
|
||||
const link = validTagHashtags[tagValue];
|
||||
const $hashtag = $(hashtag);
|
||||
|
||||
if (link) {
|
||||
replaceSpan($hashtag, tagValue, link);
|
||||
} else if (checkedTagHashtags.indexOf(tagValue) !== -1) {
|
||||
$hashtag.addClass(testedClass);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function linkSeenTagHashtags($elem) {
|
||||
const $hashtags = $(`span.hashtag:not(.${testedClass})`, $elem);
|
||||
const unseen = [];
|
||||
|
||||
if ($hashtags.length) {
|
||||
const tagValues = $hashtags.map((_, hashtag) => {
|
||||
return $(hashtag).text().substr(1).replace(`${TAG_HASHTAG_POSTFIX}`, "");
|
||||
});
|
||||
|
||||
if (tagValues.length) {
|
||||
_.uniq(tagValues).forEach((tagValue) => {
|
||||
if (checkedTagHashtags.indexOf(tagValue) === -1) unseen.push(tagValue);
|
||||
});
|
||||
}
|
||||
updateFound($hashtags, tagValues);
|
||||
}
|
||||
|
||||
return unseen;
|
||||
};
|
||||
|
||||
export function fetchUnseenTagHashtags(tagValues) {
|
||||
return Discourse.ajax("/tags/check", { data: { tag_values: tagValues } })
|
||||
.then((response) => {
|
||||
response.valid.forEach((tag) => {
|
||||
validTagHashtags[tag.value] = tag.url;
|
||||
});
|
||||
checkedTagHashtags.push.apply(checkedTagHashtags, tagValues);
|
||||
});
|
||||
}
|
||||
@ -9,6 +9,7 @@ import { createWidget, decorateWidget, changeSetting } from 'discourse/widgets/w
|
||||
import { onPageChange } from 'discourse/lib/page-tracker';
|
||||
import { preventCloak } from 'discourse/widgets/post-stream';
|
||||
import { h } from 'virtual-dom';
|
||||
import { addFlagProperty } from 'discourse/components/site-header';
|
||||
|
||||
class PluginApi {
|
||||
constructor(version, container) {
|
||||
@ -48,7 +49,7 @@ class PluginApi {
|
||||
|
||||
if (!opts.onlyStream) {
|
||||
decorate(ComposerEditor, 'previewRefreshed', callback);
|
||||
decorate(this.container.lookupFactory('view:user-stream'), 'didInsertElement', callback);
|
||||
decorate(this.container.lookupFactory('component:user-stream'), 'didInsertElement', callback);
|
||||
}
|
||||
}
|
||||
|
||||
@ -284,11 +285,20 @@ class PluginApi {
|
||||
createWidget(name, args) {
|
||||
return createWidget(name, args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a property that can be summed for calculating the flag counter
|
||||
**/
|
||||
addFlagProperty(property) {
|
||||
return addFlagProperty(property);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let _pluginv01;
|
||||
function getPluginApi(version) {
|
||||
if (version === "0.1" || version === "0.2" || version === "0.3") {
|
||||
version = parseFloat(version);
|
||||
if (version <= 0.4) {
|
||||
if (!_pluginv01) {
|
||||
_pluginv01 = new PluginApi(version, Discourse.__container__);
|
||||
}
|
||||
@ -299,7 +309,7 @@ function getPluginApi(version) {
|
||||
}
|
||||
|
||||
/**
|
||||
* withPluginApi(version, apiCode, noApi)
|
||||
* withPluginApi(version, apiCodeCallback, opts)
|
||||
*
|
||||
* Helper to version our client side plugin API. Pass the version of the API that your
|
||||
* plugin is coded against. If that API is available, the `apiCodeCallback` function will
|
||||
|
||||
37
app/assets/javascripts/discourse/lib/render-tag.js.es6
Normal file
37
app/assets/javascripts/discourse/lib/render-tag.js.es6
Normal file
@ -0,0 +1,37 @@
|
||||
import { h } from 'virtual-dom';
|
||||
|
||||
export default function renderTag(tag, params) {
|
||||
params = params || {};
|
||||
tag = Handlebars.Utils.escapeExpression(tag);
|
||||
const classes = ['tag-' + tag, 'discourse-tag'];
|
||||
const tagName = params.tagName || "a";
|
||||
const href = tagName === "a" ? " href='" + Discourse.getURL("/tags/" + tag) + "' " : "";
|
||||
|
||||
if (Discourse.SiteSettings.tag_style || params.style) {
|
||||
classes.push(params.style || Discourse.SiteSettings.tag_style);
|
||||
}
|
||||
|
||||
let val = "<" + tagName + href + " class='" + classes.join(" ") + "'>" + tag + "</" + tagName + ">";
|
||||
|
||||
if (params.count) {
|
||||
val += " <span class='discourse-tag-count'>x" + params.count + "</span>";
|
||||
}
|
||||
|
||||
return val;
|
||||
};
|
||||
|
||||
export function tagNode(tag, params) {
|
||||
const classes = ['tag-' + tag, 'discourse-tag'];
|
||||
const tagName = params.tagName || "a";
|
||||
|
||||
if (Discourse.SiteSettings.tag_style || params.style) {
|
||||
classes.push(params.style || Discourse.SiteSettings.tag_style);
|
||||
}
|
||||
|
||||
if (tagName === 'a') {
|
||||
const href = Discourse.getURL(`/tags/${tag}`);
|
||||
return h(tagName, { className: classes.join(' '), attributes: { href } }, tag);
|
||||
} else {
|
||||
return h(tagName, { className: classes.join(' ') }, tag);
|
||||
}
|
||||
}
|
||||
1
app/assets/javascripts/discourse/lib/tag-hashtags.js.es6
Normal file
1
app/assets/javascripts/discourse/lib/tag-hashtags.js.es6
Normal file
@ -0,0 +1 @@
|
||||
export const TAG_HASHTAG_POSTFIX = '::tag';
|
||||
@ -2,7 +2,7 @@
|
||||
let _jumpScheduled = false;
|
||||
const rewrites = [];
|
||||
|
||||
const DiscourseURL = Ember.Object.createWithMixins({
|
||||
const DiscourseURL = Ember.Object.extend({
|
||||
|
||||
// Used for matching a topic
|
||||
TOPIC_REGEXP: /\/t\/([^\/]+)\/(\d+)\/?(\d+)?/,
|
||||
@ -327,7 +327,6 @@ const DiscourseURL = Ember.Object.createWithMixins({
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
}).create();
|
||||
|
||||
export default DiscourseURL;
|
||||
|
||||
@ -5,14 +5,6 @@ import { on } from 'ember-addons/ember-computed-decorators';
|
||||
// Provides the ability to load more items for a view which is scrolled to the bottom.
|
||||
export default Ember.Mixin.create(Ember.ViewTargetActionSupport, Scrolling, {
|
||||
|
||||
init() {
|
||||
this._super();
|
||||
if (!this._viaComponent) {
|
||||
console.warn('Using `LoadMore` as a view mixin is deprecated. Use `{{load-more}}` instead');
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
scrolled() {
|
||||
const eyeline = this.get('eyeline');
|
||||
return eyeline && eyeline.update();
|
||||
|
||||
@ -1,12 +1,16 @@
|
||||
// A Mixin that a view can use to listen for 'url:refresh' when
|
||||
// it is on screen, and will send an action to the controller to
|
||||
// refresh its data.
|
||||
// it is on screen, and will send an action to refresh its data.
|
||||
//
|
||||
// This is useful if you want to get around Ember's default
|
||||
// behavior of not refreshing when navigating to the same place.
|
||||
export default {
|
||||
didInsertElement() {
|
||||
this._super();
|
||||
this.appEvents.on('url:refresh', () => this.sendAction('refresh'));
|
||||
},
|
||||
|
||||
import { createViewListener } from 'discourse/lib/app-events';
|
||||
|
||||
export default createViewListener('url:refresh', function() {
|
||||
this.get('controller').send('refresh');
|
||||
});
|
||||
willDestroyElement() {
|
||||
this._super();
|
||||
this.appEvents.off('url:refresh');
|
||||
}
|
||||
};
|
||||
|
||||
@ -28,12 +28,14 @@ const CLOSED = 'closed',
|
||||
archetype: 'archetypeId',
|
||||
target_usernames: 'targetUsernames',
|
||||
typing_duration_msecs: 'typingTime',
|
||||
composer_open_duration_msecs: 'composerTime'
|
||||
composer_open_duration_msecs: 'composerTime',
|
||||
tags: 'tags'
|
||||
},
|
||||
|
||||
_edit_topic_serializer = {
|
||||
title: 'topic.title',
|
||||
categoryId: 'topic.category.id'
|
||||
categoryId: 'topic.category.id',
|
||||
tags: 'topic.tags'
|
||||
};
|
||||
|
||||
const Composer = RestModel.extend({
|
||||
@ -358,6 +360,15 @@ const Composer = RestModel.extend({
|
||||
return before.length + text.length;
|
||||
},
|
||||
|
||||
prependText(text, opts) {
|
||||
const reply = (this.get('reply') || '');
|
||||
|
||||
if (opts && opts.new_line && reply.length > 0) {
|
||||
text = text.trim() + "\n\n";
|
||||
}
|
||||
this.set('reply', text + reply);
|
||||
},
|
||||
|
||||
applyTopicTemplate(oldCategoryId, categoryId) {
|
||||
if (this.get('action') !== CREATE_TOPIC) { return; }
|
||||
let reply = this.get('reply');
|
||||
|
||||
@ -40,7 +40,7 @@ export default RestModel.extend({
|
||||
notLoading: Ember.computed.not('loading'),
|
||||
filteredPostsCount: Ember.computed.alias("stream.length"),
|
||||
|
||||
@computed('posts.@each')
|
||||
@computed('posts.[]')
|
||||
hasPosts() {
|
||||
return this.get('posts.length') > 0;
|
||||
},
|
||||
@ -53,7 +53,7 @@ export default RestModel.extend({
|
||||
canAppendMore: Ember.computed.and('notLoading', 'hasPosts', 'lastPostNotLoaded'),
|
||||
canPrependMore: Ember.computed.and('notLoading', 'hasPosts', 'firstPostNotLoaded'),
|
||||
|
||||
@computed('hasLoadedData', 'firstPostId', 'posts.@each')
|
||||
@computed('hasLoadedData', 'firstPostId', 'posts.[]')
|
||||
firstPostPresent(hasLoadedData, firstPostId) {
|
||||
if (!hasLoadedData) { return false; }
|
||||
return !!this.get('posts').findProperty('id', firstPostId);
|
||||
@ -101,7 +101,7 @@ export default RestModel.extend({
|
||||
Returns the window of posts above the current set in the stream, bound to the top of the stream.
|
||||
This is the collection we'll ask for when scrolling upwards.
|
||||
**/
|
||||
@computed('posts.@each', 'stream.@each')
|
||||
@computed('posts.[]', 'stream.[]')
|
||||
previousWindow() {
|
||||
// If we can't find the last post loaded, bail
|
||||
const firstPost = _.first(this.get('posts'));
|
||||
@ -121,7 +121,7 @@ export default RestModel.extend({
|
||||
Returns the window of posts below the current set in the stream, bound by the bottom of the
|
||||
stream. This is the collection we use when scrolling downwards.
|
||||
**/
|
||||
@computed('posts.lastObject', 'stream.@each')
|
||||
@computed('posts.lastObject', 'stream.[]')
|
||||
nextWindow(lastLoadedPost) {
|
||||
// If we can't find the last post loaded, bail
|
||||
if (!lastLoadedPost) { return []; }
|
||||
|
||||
@ -108,7 +108,7 @@ const Post = RestModel.extend({
|
||||
// Put the metaData into the request
|
||||
if (metaData) {
|
||||
data.meta_data = {};
|
||||
Ember.keys(metaData).forEach(function(key) { data.meta_data[key] = metaData.get(key); });
|
||||
Object.keys(metaData).forEach(function(key) { data.meta_data[key] = metaData.get(key); });
|
||||
}
|
||||
|
||||
return data;
|
||||
|
||||
@ -15,7 +15,7 @@ const Site = RestModel.extend({
|
||||
return result;
|
||||
},
|
||||
|
||||
@computed("post_action_types.@each")
|
||||
@computed("post_action_types.[]")
|
||||
flagTypes() {
|
||||
const postActionTypes = this.get('post_action_types');
|
||||
if (!postActionTypes) return [];
|
||||
@ -26,7 +26,7 @@ const Site = RestModel.extend({
|
||||
categoriesByCount: Ember.computed.sort('categories', 'topicCountDesc'),
|
||||
|
||||
// Sort subcategories under parents
|
||||
@computed("categoriesByCount", "categories.@each")
|
||||
@computed("categoriesByCount", "categories.[]")
|
||||
sortedCategories(cats) {
|
||||
const result = [],
|
||||
remaining = {};
|
||||
@ -41,7 +41,7 @@ const Site = RestModel.extend({
|
||||
}
|
||||
});
|
||||
|
||||
Ember.keys(remaining).forEach(parentCategoryId => {
|
||||
Object.keys(remaining).forEach(parentCategoryId => {
|
||||
const category = result.findBy('id', parseInt(parentCategoryId, 10)),
|
||||
index = result.indexOf(category);
|
||||
|
||||
|
||||
@ -4,6 +4,7 @@ import { propertyEqual } from 'discourse/lib/computed';
|
||||
import { longDate } from 'discourse/lib/formatter';
|
||||
import computed from 'ember-addons/ember-computed-decorators';
|
||||
import ActionSummary from 'discourse/models/action-summary';
|
||||
import { popupAjaxError } from 'discourse/lib/ajax-error';
|
||||
|
||||
export function loadTopicView(topic, args) {
|
||||
const topicId = topic.get('id');
|
||||
@ -32,7 +33,7 @@ const Topic = RestModel.extend({
|
||||
return poster && poster.user;
|
||||
},
|
||||
|
||||
@computed('posters.@each')
|
||||
@computed('posters.[]')
|
||||
lastPoster(posters) {
|
||||
var user;
|
||||
if (posters && posters.length > 0) {
|
||||
@ -72,6 +73,24 @@ const Topic = RestModel.extend({
|
||||
return this.store.createRecord('postStream', {id: this.get('id'), topic: this});
|
||||
}.property(),
|
||||
|
||||
@computed('tags')
|
||||
visibleListTags(tags) {
|
||||
if (!tags || !Discourse.SiteSettings.suppress_overlapping_tags_in_list) {
|
||||
return tags;
|
||||
}
|
||||
|
||||
const title = this.get('title');
|
||||
const newTags = [];
|
||||
|
||||
tags.forEach(function(tag){
|
||||
if (title.toLowerCase().indexOf(tag) === -1 || Discourse.SiteSettings.staff_tags.indexOf(tag) !== -1) {
|
||||
newTags.push(tag);
|
||||
}
|
||||
});
|
||||
|
||||
return newTags;
|
||||
},
|
||||
|
||||
replyCount: function() {
|
||||
return this.get('posts_count') - 1;
|
||||
}.property('posts_count'),
|
||||
@ -428,8 +447,13 @@ const Topic = RestModel.extend({
|
||||
}).finally(()=>this.set('archiving', false));
|
||||
|
||||
return promise;
|
||||
}
|
||||
},
|
||||
|
||||
convertTopic(type) {
|
||||
return Discourse.ajax(`/t/${this.get('id')}/convert-topic/${type}`, {type: 'PUT'}).then(() => {
|
||||
window.location.reload();
|
||||
}).catch(popupAjaxError);
|
||||
}
|
||||
});
|
||||
|
||||
Topic.reopenClass({
|
||||
|
||||
@ -147,10 +147,10 @@ const UserAction = RestModel.extend({
|
||||
}
|
||||
return rval;
|
||||
}.property("childGroups",
|
||||
"childGroups.likes.items", "childGroups.likes.items.@each",
|
||||
"childGroups.stars.items", "childGroups.stars.items.@each",
|
||||
"childGroups.edits.items", "childGroups.edits.items.@each",
|
||||
"childGroups.bookmarks.items", "childGroups.bookmarks.items.@each"),
|
||||
"childGroups.likes.items", "childGroups.likes.items.[]",
|
||||
"childGroups.stars.items", "childGroups.stars.items.[]",
|
||||
"childGroups.edits.items", "childGroups.edits.items.[]",
|
||||
"childGroups.bookmarks.items", "childGroups.bookmarks.items.[]"),
|
||||
|
||||
switchToActing() {
|
||||
this.setProperties({
|
||||
|
||||
@ -136,7 +136,7 @@ const User = RestModel.extend({
|
||||
},
|
||||
|
||||
copy() {
|
||||
return Discourse.User.create(this.getProperties(Ember.keys(this)));
|
||||
return Discourse.User.create(this.getProperties(Object.keys(this)));
|
||||
},
|
||||
|
||||
save() {
|
||||
@ -229,7 +229,7 @@ const User = RestModel.extend({
|
||||
ua.action_type === UserAction.TYPES.topics;
|
||||
},
|
||||
|
||||
@computed("groups.@each")
|
||||
@computed("groups.[]")
|
||||
displayGroups() {
|
||||
const groups = this.get('groups');
|
||||
const filtered = groups.filter(group => {
|
||||
|
||||
@ -15,7 +15,7 @@ export function mapRoutes() {
|
||||
// will be built automatically. You can supply a `resource` property to
|
||||
// automatically put it in that resource, such as `admin`. That way plugins
|
||||
// can define admin routes.
|
||||
Ember.keys(requirejs._eak_seen).forEach(function(key) {
|
||||
Object.keys(requirejs._eak_seen).forEach(function(key) {
|
||||
if (/route-map$/.test(key)) {
|
||||
var module = require(key, null, null, true);
|
||||
if (!module || !module.default) { throw new Error(key + ' must export a route map.'); }
|
||||
|
||||
@ -118,4 +118,16 @@ export default function() {
|
||||
this.resource('queued-posts', { path: '/queued-posts' });
|
||||
|
||||
this.route('full-page-search', {path: '/search'});
|
||||
|
||||
this.resource('tags', function() {
|
||||
this.route('show', {path: '/:tag_id'});
|
||||
this.route('showCategory', {path: '/c/:category/:tag_id'});
|
||||
this.route('showParentCategory', {path: '/c/:parent_category/:category/:tag_id'});
|
||||
|
||||
Discourse.Site.currentProp('filters').forEach(filter => {
|
||||
this.route('show' + filter.capitalize(), {path: '/:tag_id/l/' + filter});
|
||||
this.route('showCategory' + filter.capitalize(), {path: '/c/:category/:tag_id/l/' + filter});
|
||||
this.route('showParentCategory' + filter.capitalize(), {path: '/c/:parent_category/:category/:tag_id/l/' + filter});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user