Version bump

This commit is contained in:
Neil Lalonde 2016-05-04 14:31:36 -04:00
commit 8556622e2b
388 changed files with 9029 additions and 3618 deletions

2
.gitignore vendored
View File

@ -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
View 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"]
}

View File

@ -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

View File

@ -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

View File

@ -63,6 +63,6 @@ export default Ember.ArrayController.extend({
**/
hasMasterKey: function() {
return !!this.get('model').findBy('user', null);
}.property('model.@each')
}.property('model.[]')
});

View File

@ -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}")
});

View File

@ -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) {

View File

@ -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";

View File

@ -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.

View File

@ -27,7 +27,7 @@ export default Ember.Controller.extend(CanCheckEmails, {
});
}
return [];
}.property('model.user_fields.@each'),
}.property('model.user_fields.[]'),
actions: {
toggleTitleEdit() {

View File

@ -0,0 +1,2 @@
import AdminEmailLogs from 'admin/routes/admin-email-logs';
export default AdminEmailLogs.extend({ status: "bounced" });

View File

@ -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' });

View 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}}
&mdash;
{{/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}}

View File

@ -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}}

View File

@ -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}}

View File

@ -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");

View File

@ -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;

View File

@ -0,0 +1,7 @@
import RESTAdapter from 'discourse/adapters/rest';
export default RESTAdapter.extend({
pathFor(store, type, id) {
return "/tags/" + id + "/notifications";
}
});

View File

@ -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) {

View File

@ -46,7 +46,7 @@ export default Ember.Component.extend({
}
},
@observes('content.@each')
@observes('content.[]')
_rerenderOnChange() {
this.rerender();
},

View File

@ -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 {

View File

@ -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);

View File

@ -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()));
});
});
},

View File

@ -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'),
});

View File

@ -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);

View File

@ -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;

View File

@ -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']);
}

View File

@ -0,0 +1,8 @@
export default Ember.Component.extend({
actions: {
// TODO: When on Ember 1.13, use a closure action
loadMore() {
this.sendAction('loadMore');
}
}
});

View File

@ -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();
}
}
});

View File

@ -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');
}
}
});

View File

@ -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 };

View File

@ -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>');
}
});

View File

@ -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'));

View File

@ -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();
}
}
});

View File

@ -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) {

View File

@ -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);
}
}
});

View File

@ -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;
}

View File

@ -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; }

View File

@ -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');
}
}
});

View File

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

View File

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

View File

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

View File

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

View File

@ -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')
});

View 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());
}

View File

@ -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')
});

View File

@ -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;
}
});

View 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();
}
});

View File

@ -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);
}
});

View File

@ -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');
}
}
});

View File

@ -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();
}
});

View File

@ -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();
});
}
}
});

View File

@ -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);

View File

@ -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() {

View File

@ -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

View File

@ -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;

View File

@ -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);
});
}
}
}
});

View File

@ -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);
});
}
}
}
});

View File

@ -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);

View File

@ -1,3 +0,0 @@
import IndexController from 'discourse/controllers/group/index';
export default IndexController.extend({type: 'mentions'});

View File

@ -1,3 +0,0 @@
import IndexController from 'discourse/controllers/group/index';
export default IndexController.extend({type: 'topics'});

View File

@ -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;

View File

@ -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);

View File

@ -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');

View File

@ -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');
});
}
}
});

View File

@ -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']);
}
}
});

View 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 });
}
}
});

View File

@ -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');

View File

@ -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(),

View File

@ -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');

View File

@ -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();
}
}

View File

@ -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;
},

View File

@ -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) ||

View File

@ -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));
});

View File

@ -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++) {

View File

@ -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];

View File

@ -1,4 +1,4 @@
import { applyFlaggedProperties } from 'discourse/controllers/header';
import { applyFlaggedProperties } from 'discourse/components/site-header';
export default {
name: 'apply-flagged-properties',

View File

@ -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);
}

View File

@ -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'});

View File

@ -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);

View File

@ -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);
}
};

View File

@ -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,

View File

@ -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') ||

View File

@ -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 });
}

View 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);
});
}

View File

@ -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

View 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);
}
}

View File

@ -0,0 +1 @@
export const TAG_HASHTAG_POSTFIX = '::tag';

View File

@ -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;

View File

@ -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();

View File

@ -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');
}
};

View File

@ -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');

View File

@ -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 []; }

View File

@ -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;

View File

@ -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);

View File

@ -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({

View File

@ -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({

View File

@ -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 => {

View File

@ -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.'); }

View File

@ -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