Version bump

This commit is contained in:
Neil Lalonde 2018-01-31 12:18:54 -05:00
commit 557dde29c7
9109 changed files with 10427 additions and 6998 deletions

View File

@ -1,7 +1,5 @@
app/assets/javascripts/env.js
app/assets/javascripts/main_include.js
app/assets/javascripts/main_include_admin.js
app/assets/javascripts/pagedown_custom.js
app/assets/javascripts/vendor.js
app/assets/javascripts/locales/i18n.js
app/assets/javascripts/ember-addons/
@ -11,11 +9,9 @@ lib/javascripts/messageformat.js
lib/javascripts/moment.js
lib/javascripts/moment_locale/
lib/highlight_js/
plugins/**/lib/javascripts/locale
public/javascripts/
spec/phantom_js/smoke_test.js
vendor/
test/javascripts/test_helper.js
test/javascripts/test_helper.js
test/javascripts/fixtures
test/javascripts/helpers/assertions.js
app/assets/javascripts/ember-addons/

5
.gitignore vendored
View File

@ -117,9 +117,12 @@ bundler_stubs/*
vendor/bundle/*
*.db
#ignore jetbrains ide file
# ignore jetbrains ide file
*.iml
# vim swap
*.swn
# ignore nodejs files
/node_modules
/package-lock.json

View File

@ -21,8 +21,6 @@ addons:
matrix:
fast_finish: true
allow_failures:
- rvm: 2.5.0
rvm:
- 2.5.0

11
Gemfile
View File

@ -36,12 +36,12 @@ gem 'redis-namespace'
gem 'active_model_serializers', '~> 0.8.3'
gem 'onebox', '1.8.33'
gem 'onebox', '1.8.36'
gem 'http_accept_language', '~>2.0.5', require: false
gem 'ember-rails', '0.18.5'
gem 'ember-source'
gem 'ember-source', '2.13.3'
gem 'ember-handlebars-template', '0.7.5'
gem 'barber'
@ -53,14 +53,13 @@ gem 'fast_xs'
gem 'fast_xor'
# Forked until https://github.com/sdsykes/fastimage/pull/93 is merged
gem 'discourse_fastimage', require: 'fastimage'
gem 'fastimage'
gem 'aws-sdk-s3', require: false
gem 'excon', require: false
gem 'unf', require: false
gem 'email_reply_trimmer', '0.1.8'
gem 'email_reply_trimmer', '0.1.9'
# Forked until https://github.com/toy/image_optim/pull/149 is merged
gem 'discourse_image_optim', require: 'image_optim'
@ -83,7 +82,7 @@ gem 'omniauth-oauth2', require: false
gem 'omniauth-google-oauth2'
gem 'oj'
gem 'pg'
gem 'pg', '~> 0.21.0'
gem 'pry-rails', require: false
gem 'r2', '~> 0.2.5', require: false
gem 'rake'

View File

@ -74,7 +74,7 @@ GEM
uniform_notifier (~> 1.10.0)
byebug (9.0.6)
certified (1.0.0)
coderay (1.1.1)
coderay (1.1.2)
concurrent-ruby (1.0.5)
connection_pool (2.2.1)
cppjieba_rb (0.3.0)
@ -85,14 +85,13 @@ GEM
diff-lcs (1.3)
discourse-qunit-rails (0.0.11)
railties
discourse_fastimage (2.1.0)
discourse_image_optim (0.24.5)
exifr (~> 1.2, >= 1.2.2)
fspath (~> 3.0)
image_size (~> 1.5)
in_threads (~> 1.3)
progress (~> 3.0, >= 3.0.1)
email_reply_trimmer (0.1.8)
email_reply_trimmer (0.1.9)
ember-data-source (2.2.1)
ember-source (>= 1.8, < 3.0)
ember-handlebars-template (0.7.5)
@ -120,6 +119,7 @@ GEM
rake
rake-compiler
fast_xs (0.8.0)
fastimage (2.1.1)
ffi (1.9.18)
flamegraph (0.9.5)
foreman (0.84.0)
@ -144,8 +144,8 @@ GEM
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
jwt (1.5.6)
kgio (2.11.0)
libv8 (5.9.211.38.1)
kgio (2.11.1)
libv8 (6.3.292.48.1)
listen (3.1.5)
rb-fsevent (~> 0.9, >= 0.9.4)
rb-inotify (~> 0.9, >= 0.9.7)
@ -166,7 +166,7 @@ GEM
mail (2.6.6)
mime-types (>= 1.16, < 4)
memory_profiler (0.9.8)
message_bus (2.1.1)
message_bus (2.1.2)
rack (>= 1.1.3)
metaclass (0.0.4)
method_source (0.8.2)
@ -175,8 +175,8 @@ GEM
mime-types-data (3.2016.0521)
mini_mime (0.1.3)
mini_portile2 (2.3.0)
mini_racer (0.1.11)
libv8 (~> 5.7)
mini_racer (0.1.15)
libv8 (~> 6.3)
mini_suffix (0.3.0)
ffi (~> 1.9)
minitest (5.10.3)
@ -232,7 +232,7 @@ GEM
omniauth-twitter (1.3.0)
omniauth-oauth (~> 1.1)
rack
onebox (1.8.33)
onebox (1.8.36)
fast_blank (>= 1.0.0)
htmlentities (~> 4.3)
moneta (~> 1.0)
@ -246,7 +246,7 @@ GEM
parallel (1.12.0)
parser (2.4.0.0)
ast (~> 2.2)
pg (0.20.0)
pg (0.21.0)
powerpack (0.1.1)
progress (3.3.1)
pry (0.10.4)
@ -275,7 +275,7 @@ GEM
nokogiri (>= 1.6)
rails-html-sanitizer (1.0.3)
loofah (~> 2.0)
rails_multisite (1.1.2)
rails_multisite (2.0.2)
activerecord (> 4.2, < 6)
railties (> 4.2, < 6)
railties (5.1.4)
@ -293,7 +293,7 @@ GEM
rb-fsevent (0.9.8)
rb-inotify (0.9.8)
ffi (>= 0.5.0)
rbtrace (0.4.8)
rbtrace (0.4.10)
ffi (>= 1.0.6)
msgpack (>= 0.4.3)
trollop (>= 1.16.2)
@ -389,7 +389,7 @@ GEM
unf_ext
unf_ext (0.0.7.4)
unicode-display_width (1.3.0)
unicorn (5.3.1)
unicorn (5.4.0)
kgio (~> 2.6)
raindrops (~> 0.7)
uniform_notifier (1.10.0)
@ -420,12 +420,11 @@ DEPENDENCIES
certified
cppjieba_rb
discourse-qunit-rails
discourse_fastimage
discourse_image_optim
email_reply_trimmer (= 0.1.8)
email_reply_trimmer (= 0.1.9)
ember-handlebars-template (= 0.7.5)
ember-rails (= 0.18.5)
ember-source
ember-source (= 2.13.3)
excon
execjs
fabrication (= 2.9.8)
@ -433,6 +432,7 @@ DEPENDENCIES
fast_blank
fast_xor
fast_xs
fastimage
flamegraph
foreman
gc_tracer
@ -469,9 +469,9 @@ DEPENDENCIES
omniauth-oauth2
omniauth-openid
omniauth-twitter
onebox (= 1.8.33)
onebox (= 1.8.36)
openid-redis-store
pg
pg (~> 0.21.0)
pry-nav
pry-rails
puma
@ -509,4 +509,4 @@ DEPENDENCIES
webmock
BUNDLED WITH
1.16.0
1.16.1

View File

@ -0,0 +1,3 @@
export default Ember.Component.extend({
classNames: ['flag-user-lists']
});

View File

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

View File

@ -13,15 +13,18 @@ export default Ember.Component.extend({
'flaggedPost.deleted'
],
canAct: Ember.computed.alias('actableFilter'),
@computed('filter')
canAct(filter) {
actableFilter(filter) {
return filter === 'active';
},
removeAfter(promise) {
return promise.then(() => {
this.attrs.removePost();
}).catch(() => {
}).catch(error => {
if (error._discourse_displayed) { return; }
bootbox.alert(I18n.t("admin.flags.error"));
});
},

View File

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

View File

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

View File

@ -1,7 +1,7 @@
export default Ember.Controller.extend({
loading: false,
term: null,
period: "yearly",
period: "quarterly",
searchType: "all",
searchTypeOptions: [

View File

@ -1,8 +1,10 @@
import UserBadge from 'discourse/models/user-badge';
import GrantBadgeController from "discourse/mixins/grant-badge-controller";
export default Ember.Controller.extend({
export default Ember.Controller.extend(GrantBadgeController, {
adminUser: Ember.inject.controller(),
user: Ember.computed.alias('adminUser.model'),
userBadges: Ember.computed.alias('model'),
allBadges: Ember.computed.alias('badges'),
sortedBadges: Ember.computed.sort('model', 'badgeSortOrder'),
badgeSortOrder: ['granted_at:desc'],
@ -41,36 +43,6 @@ export default Ember.Controller.extend({
return _(expanded).sortBy(group => group.granted_at).reverse().value();
}.property('model', 'model.[]', 'model.expandedBadges.[]'),
/**
Array of badges that have not been granted to this user.
@property grantableBadges
@type {Boolean}
**/
grantableBadges: function() {
var granted = {};
this.get('model').forEach(function(userBadge) {
granted[userBadge.get('badge_id')] = true;
});
var badges = [];
this.get('badges').forEach(function(badge) {
if (badge.get('enabled') && (badge.get('multiple_grant') || !granted[badge.get('id')])) {
badges.push(badge);
}
});
return _.sortBy(badges, badge => badge.get('name'));
}.property('badges.[]', 'model.[]'),
/**
Whether there are any badges that can be granted.
@property noBadges
@type {Boolean}
**/
noBadges: Em.computed.empty('grantableBadges'),
actions: {
expandGroup: function(userBadge){
@ -79,21 +51,21 @@ export default Ember.Controller.extend({
model.get('expandedBadges').pushObject(userBadge.badge.id);
},
grantBadge(badgeId) {
UserBadge.grant(badgeId, this.get('user.username'), this.get('badgeReason')).then(userBadge => {
this.set('badgeReason', '');
this.get('model').pushObject(userBadge);
Ember.run.next(() => {
// Update the selected badge ID after the combobox has re-rendered.
const newSelectedBadge = this.get('grantableBadges')[0];
if (newSelectedBadge) {
this.set('selectedBadgeId', newSelectedBadge.get('id'));
}
grantBadge() {
this.grantBadge(this.get('selectedBadgeId'), this.get('user.username'), this.get('badgeReason'))
.then(() => {
this.set('badgeReason', '');
Ember.run.next(() => {
// Update the selected badge ID after the combobox has re-rendered.
const newSelectedBadge = this.get('grantableBadges')[0];
if (newSelectedBadge) {
this.set('selectedBadgeId', newSelectedBadge.get('id'));
}
});
}, function() {
// Failure
bootbox.alert(I18n.t('generic_error'));
});
}, function() {
// Failure
bootbox.alert(I18n.t('generic_error'));
});
},
revokeBadge(userBadge) {

View File

@ -64,6 +64,15 @@ export default Ember.Controller.extend(CanCheckEmails, {
anonymize() { return this.get('model').anonymize(); },
destroy() { return this.get('model').destroy(); },
viewActionLogs() {
this.get('adminTools').showActionLogs(this, {
target_user: this.get('model.username'),
});
},
showFlagsReceived() {
this.get('adminTools').showFlagsReceived(this.get('model'));
},
showSuspendModal() {
this.get('adminTools').showSuspendModal(this.get('model'));
},

View File

@ -1,5 +1,7 @@
import { ajax } from 'discourse/lib/ajax';
export default Ember.Controller.extend({
import ModalFunctionality from 'discourse/mixins/modal-functionality';
export default Ember.Controller.extend(ModalFunctionality, {
modelChanged: function(){
const model = this.get('model');

View File

@ -0,0 +1,17 @@
export default Ember.Controller.extend({
loadingFlags: null,
user: null,
onShow() {
this.set('loadingFlags', true);
this.store.findAll('flagged-post', {
filter: 'without_custom',
user_id: this.get('model.id')
}).then(result => {
this.set('loadingFlags', false);
console.log(result);
this.set('flaggedPosts', result);
});
}
});

View File

@ -22,6 +22,11 @@ export default Post.extend({
});
},
@computed('post_actions')
hasDisposedBy() {
return this.get('post_actions').some(action => action.disposed_by);
},
@computed('post_actions.@each.name_key')
flaggedForSpam() {
return this.get('post_actions').every(action => action.name_key === 'spam');

View File

@ -1,6 +1,7 @@
import { ajax } from 'discourse/lib/ajax';
import round from "discourse/lib/round";
import { fmt } from 'discourse/lib/computed';
import { fillMissingDates } from 'discourse/lib/utilities';
const Report = Discourse.Model.extend({
reportUrl: fmt("type", "/admin/reports/%@"),
@ -142,14 +143,13 @@ Report.reopenClass({
group_id: groupId
}
}).then(json => {
// Add a percent field to each tuple
let maxY = 0;
json.report.data.forEach(row => {
if (row.y > maxY) maxY = row.y;
});
if (maxY > 0) {
json.report.data.forEach(row => row.percentage = Math.round((row.y / maxY) * 100));
// Add zero values for missing dates
if (json.report.data.length > 0) {
const startDateFormatted = moment(json.report.start_date).format('YYYY-MM-DD');
const endDateFormatted = moment(json.report.end_date).format('YYYY-MM-DD');
json.report.data = fillMissingDates(json.report.data, startDateFormatted, endDateFormatted);
}
const model = Report.create({ type: type });
model.setProperties(json.report);
return model;

View File

@ -1,7 +1,15 @@
import Backup from 'admin/models/backup';
export default Ember.Route.extend({
activate() {
this.messageBus.subscribe("/admin/backups", backups => this.controller.set("model", backups));
},
model() {
return Backup.find();
},
deactivate() {
this.messageBus.unsubscribe("/admin/backups");
}
});

View File

@ -9,26 +9,24 @@ const LOG_CHANNEL = "/admin/backups/logs";
export default Discourse.Route.extend({
activate() {
this.messageBus.subscribe(LOG_CHANNEL, this._processLogMessage.bind(this));
},
_processLogMessage(log) {
if (log.message === "[STARTED]") {
this.controllerFor("adminBackups").set("model.isOperationRunning", true);
this.controllerFor("adminBackupsLogs").get('logs').clear();
} else if (log.message === "[FAILED]") {
this.controllerFor("adminBackups").set("model.isOperationRunning", false);
bootbox.alert(I18n.t("admin.backups.operations.failed", { operation: log.operation }));
} else if (log.message === "[SUCCESS]") {
Discourse.User.currentProp("hideReadOnlyAlert", false);
this.controllerFor("adminBackups").set("model.isOperationRunning", false);
if (log.operation === "restore") {
// redirect to homepage when the restore is done (session might be lost)
window.location.pathname = Discourse.getURL("/");
this.messageBus.subscribe(LOG_CHANNEL, (log) => {
if (log.message === "[STARTED]") {
this.controllerFor("adminBackups").set("model.isOperationRunning", true);
this.controllerFor("adminBackupsLogs").get('logs').clear();
} else if (log.message === "[FAILED]") {
this.controllerFor("adminBackups").set("model.isOperationRunning", false);
bootbox.alert(I18n.t("admin.backups.operations.failed", { operation: log.operation }));
} else if (log.message === "[SUCCESS]") {
Discourse.User.currentProp("hideReadOnlyAlert", false);
this.controllerFor("adminBackups").set("model.isOperationRunning", false);
if (log.operation === "restore") {
// redirect to homepage when the restore is done (session might be lost)
window.location.pathname = Discourse.getURL("/");
}
} else {
this.controllerFor("adminBackupsLogs").get('logs').pushObject(Em.Object.create(log));
}
} else {
this.controllerFor("adminBackupsLogs").get('logs').pushObject(Em.Object.create(log));
}
});
},
model() {
@ -122,12 +120,7 @@ export default Discourse.Route.extend({
},
uploadSuccess(filename) {
const self = this;
bootbox.alert(I18n.t("admin.backups.upload.success", { filename: filename }), function() {
Backup.find().then(function (backups) {
self.controllerFor("adminBackupsIndex").set("model", backups);
});
});
bootbox.alert(I18n.t("admin.backups.upload.success", { filename: filename }));
},
uploadError(filename, message) {

View File

@ -1,4 +1,6 @@
import { ajax } from 'discourse/lib/ajax';
import { fillMissingDates } from 'discourse/lib/utilities';
import { translateResults } from "discourse/lib/search";
export default Discourse.Route.extend({
queryParams: {
@ -15,6 +17,16 @@ export default Discourse.Route.extend({
search_type: params.searchType
}
}).then(json => {
// Add zero values for missing dates
if (json.term.data.length > 0) {
const startDate = (json.term.period === "all") ? moment(json.term.data[0].x).format('YYYY-MM-DD') : moment(json.term.start_date).format('YYYY-MM-DD');
const endDate = moment(json.term.end_date).format('YYYY-MM-DD');
json.term.data = fillMissingDates(json.term.data, startDate, endDate);
}
if (json.term.search_result) {
json.term.search_result = translateResults(json.term.search_result);
}
const model = Ember.Object.create({ type: "search_log_term" });
model.setProperties(json.term);
return model;

View File

@ -21,15 +21,5 @@ export default Discourse.Route.extend({
availableGroups: this._availableGroups,
model
});
},
actions: {
viewActionLogs(username) {
const controller = this.controllerFor('adminLogs.staffActionLogs');
this.transitionTo('adminLogs.staffActionLogs').then(() => {
controller.set('filters', Ember.Object.create());
controller._changeFilters({ target_user: username });
});
}
}
});

View File

@ -17,10 +17,26 @@ export default Ember.Service.extend({
this.siteSettings = getOwner(this).lookup('site-settings:main');
},
showActionLogs(target, filters) {
const controller = getOwner(target).lookup('controller:adminLogs.staffActionLogs');
target.transitionToRoute('adminLogs.staffActionLogs').then(() => {
controller.set('filters', Ember.Object.create());
controller._changeFilters(filters);
});
},
showFlagsReceived(user) {
showModal(`admin-flags-received`, { admin: true, model: user });
},
checkSpammer(userId) {
return AdminUser.find(userId).then(au => this.spammerDetails(au));
},
deleteUser(id) {
AdminUser.find(id).then(user => user.destroy({ deletePosts: true }));
},
spammerDetails(adminUser) {
return {
deleteUser: () => this._deleteSpammer(adminUser),
@ -39,11 +55,10 @@ export default Ember.Service.extend({
controller.set('post', opts.post);
}
let promise = user.adminUserView ?
return (user.adminUserView ?
Ember.RSVP.resolve(user) :
AdminUser.find(user.get('id'));
promise.then(loadedUser => {
AdminUser.find(user.get('id'))
).then(loadedUser => {
controller.setProperties({
user: loadedUser,
loadingUser: false,

View File

@ -4,11 +4,11 @@
{{/admin-form-row}}
{{#admin-form-row label="admin.user_fields.name"}}
{{input value=buffered.name class="user-field-name"}}
{{input value=buffered.name class="user-field-name" maxlength="255"}}
{{/admin-form-row}}
{{#admin-form-row label="admin.user_fields.description"}}
{{input value=buffered.description class="user-field-desc"}}
{{input value=buffered.description class="user-field-desc" maxlength="255"}}
{{/admin-form-row}}
{{#if bufferedFieldType.hasOptions}}

View File

@ -0,0 +1,32 @@
<div class='flagged-by'>
<div class='user-list-title'>
{{i18n "admin.flags.flagged_by"}}
</div>
<div class='flag-users'>
{{#each flaggedPost.post_actions as |postAction|}}
{{#flag-user user=postAction.user date=postAction.created_at}}
<div class='flagger-flag-type'>
{{post-action-title postAction.post_action_type_id postAction.name_key}}
</div>
{{/flag-user}}
{{/each}}
</div>
</div>
{{#if showResolvedBy}}
<div class='flagged-post-resolved-by'>
<div class='user-list-title'>
{{i18n "admin.flags.resolved_by"}}
</div>
<div class='flag-users'>
{{#each flaggedPost.post_actions as |postAction|}}
{{#flag-user user=postAction.disposed_by date=postAction.disposed_at}}
{{disposition-icon postAction.disposition}}
{{#if postAction.staff_took_action}}
{{d-icon "gavel" title="admin.flags.took_action"}}
{{/if}}
{{/flag-user}}
{{/each}}
</div>
</div>
{{/if}}

View File

@ -0,0 +1,5 @@
{{#if flaggedPost.topic.isPrivateMessage}}
<span class="private-message-glyph">{{d-icon "envelope"}}</span>
{{/if}}
{{topic-status topic=flaggedPost.topic}}
<a href='{{unbound flaggedPost.url}}'>{{{unbound flaggedPost.topic.fancyTitle}}}</a>

View File

@ -22,17 +22,15 @@
<div class="flagged-post-contents">
<div class='flagged-post-user-details'>
<a class='username' href={{user.path}} data-user-card={{flaggedPost.user.username}}>{{format-username flaggedPost.user.username}}</a>
{{plugin-outlet
name="flagged-post-controls"
tagName=""
args=(hash flaggedPost=flaggedPost actableFilter=actableFilter topic=topic)}}
</div>
<div class='flagged-post-excerpt'>
{{#unless hideTitle}}
<h3>
{{#if flaggedPost.topic.isPrivateMessage}}
<span class="private-message-glyph">{{d-icon "envelope"}}</span>
{{/if}}
{{topic-status topic=flaggedPost.topic}}
<a href='{{unbound flaggedPost.url}}'>{{{unbound flaggedPost.topic.fancyTitle}}}</a>
</h3>
{{flagged-post-title flaggedPost=flaggedPost}}
{{/unless}}
{{#if flaggedPost.postAuthorFlagged}}
{{#if expanded}}
@ -68,44 +66,11 @@
</div>
{{/each}}
<div class='flag-user-lists'>
<div class='flagged-by'>
<div class='user-list-title'>
{{i18n "admin.flags.flagged_by"}}
</div>
<div class='flag-users'>
{{#each flaggedPost.post_actions as |postAction|}}
{{#flag-user user=postAction.user date=postAction.created_at}}
<div class='flagger-flag-type'>
{{post-action-title postAction.post_action_type_id postAction.name_key}}
</div>
{{/flag-user}}
{{/each}}
</div>
</div>
{{#if showResolvedBy}}
<div class='flagged-post-resolved-by'>
<div class='user-list-title'>
{{i18n "admin.flags.resolved_by"}}
</div>
<div class='flag-users'>
{{#each flaggedPost.post_actions as |postAction|}}
{{#flag-user user=postAction.disposed_by date=postAction.disposed_at}}
{{disposition-icon postAction.disposition}}
{{#if postAction.staff_took_action}}
{{d-icon "gavel" title="admin.flags.took_action"}}
{{/if}}
{{/flag-user}}
{{/each}}
</div>
</div>
{{/if}}
</div>
{{flag-user-lists flaggedPost=flaggedPost showResolvedBy=showResolvedBy}}
{{#if suspended}}
<div class='suspended-message'>
The user was suspended for this post.
{{i18n "admin.flags.suspended_for_post"}}
</div>
{{/if}}
@ -133,12 +98,14 @@
{{d-button
class="defer-flag"
title="admin.flags.defer_flag_title"
title="admin.flags.ignore_flag_title"
action="defer"
icon="external-link"
label="admin.flags.defer_flag"}}
label="admin.flags.ignore_flag"}}
{{admin-delete-flag-dropdown post=flaggedPost removeAfter=(action "removeAfter")}}
{{admin-delete-flag-dropdown
post=flaggedPost
removeAfter=(action "removeAfter")}}
{{#unless suspended}}
{{d-button
@ -158,6 +125,6 @@
{{plugin-outlet
name="flagged-post-below-controls"
tagName=""
args=(hash flaggedPost=flaggedPost canAct=canAct)}}
args=(hash flaggedPost=flaggedPost canAct=canAct actableFilter=actableFilter)}}
</div>
</div>

View File

@ -5,6 +5,7 @@
{{flagged-post
flaggedPost=flaggedPost
filter=filter
topic=topic
showResolvedBy=showResolvedBy
removePost=(action "removePost" flaggedPost)
hideTitle=topic}}

View File

@ -0,0 +1,21 @@
<div class='reason-controls'>
<label>
<div class='silence-reason-label'>
{{{i18n 'admin.user.silence_reason_label'}}}
</div>
</label>
{{text-field
value=reason
class="silence-reason"
placeholderKey="admin.user.silence_reason_placeholder"}}
</div>
<label>
<div class='silence-message-label'>
{{i18n "admin.user.silence_message"}}
</div>
</label>
{{textarea
value=message
class="silence-message"
placeholder=(i18n "admin.user.silence_message_placeholder")}}

View File

@ -0,0 +1,25 @@
<div class='reason-controls'>
<label>
<div class='suspend-reason-label'>
{{#if siteSettings.hide_suspension_reasons}}
{{{i18n 'admin.user.suspend_reason_hidden_label'}}}
{{else}}
{{{i18n 'admin.user.suspend_reason_label'}}}
{{/if}}
</div>
</label>
{{text-field
value=reason
class="suspend-reason"
placeholderKey="admin.user.suspend_reason_placeholder"}}
</div>
<label>
<div class='suspend-message-label'>
{{i18n "admin.user.suspend_message"}}
</div>
</label>
{{textarea
value=message
class="suspend-message"
placeholder=(i18n "admin.user.suspend_message_placeholder")}}

View File

@ -224,7 +224,7 @@
<td class="title">
<div class="referred-topic-title">
<div class="overflow-ellipsis">
<a href="/t/{{unbound data.topic_slug}}/{{unbound data.topic_id}}">{{data.topic_title}}</a>
<a href="{{unbound data.topic_url}}">{{data.topic_title}}</a>
</div>
</div>
</td>

View File

@ -0,0 +1,14 @@
{{#d-modal-body rawTitle=(i18n "admin.user.flags_received_by" username=model.username)}}
{{#conditional-loading-spinner condition=loadingFlags}}
{{#each flaggedPosts as |flaggedPost|}}
<div class='received-flag flagged-post'>
<div class='flagged-post-excerpt'>
{{flagged-post-title flaggedPost=flaggedPost}}
</div>
{{flag-user-lists flaggedPost=flaggedPost showResolvedBy=flaggedPost.hasDisposedBy}}
</div>
{{else}}
{{i18n "admin.user.flags_received_none"}}
{{/each}}
{{/conditional-loading-spinner}}
{{/d-modal-body}}

View File

@ -11,28 +11,7 @@
</label>
</div>
<div class='reason-controls'>
<label>
<div class='silence-reason-label'>
{{{i18n 'admin.user.silence_reason_label'}}}
</div>
</label>
{{text-field
value=reason
class="silence-reason"
placeholderKey="admin.user.silence_reason_placeholder"}}
</div>
<label>
<div class='silence-message-label'>
{{i18n "admin.user.silence_message"}}
</div>
</label>
{{textarea
value=message
class="silence-message"
placeholder=(i18n "admin.user.silence_message_placeholder")}}
{{silence-details reason=reason message=message}}
{{/conditional-loading-spinner}}
@ -47,4 +26,4 @@
label="admin.user.silence"}}
{{d-modal-cancel close=(action "closeModal")}}
{{conditional-loading-spinner condition=loading size="small"}}
</div>
</div>

View File

@ -11,32 +11,8 @@
input=suspendUntil}}
</label>
</div>
{{suspension-details reason=reason message=message}}
<div class='reason-controls'>
<label>
<div class='suspend-reason-label'>
{{#if siteSettings.hide_suspension_reasons}}
{{{i18n 'admin.user.suspend_reason_hidden_label'}}}
{{else}}
{{{i18n 'admin.user.suspend_reason_label'}}}
{{/if}}
</div>
</label>
{{text-field
value=reason
class="suspend-reason"
placeholderKey="admin.user.suspend_reason_placeholder"}}
</div>
<label>
<div class='suspend-message-label'>
{{i18n "admin.user.suspend_message"}}
</div>
</label>
{{textarea
value=message
class="suspend-message"
placeholder=(i18n "admin.user.suspend_message_placeholder")}}
{{else}}
<div class='cant-suspend'>
{{i18n "admin.user.cant_suspend"}}

View File

@ -10,4 +10,53 @@
{{#conditional-loading-spinner condition=refreshing}}
{{admin-graph model=model}}
<br><br>
<h2> {{i18n "admin.logs.search_logs.header_search_results"}} </h2>
<br>
<div class='header-search-results'>
{{#each model.search_result.posts as |result|}}
<div class='fps-result'>
<div class='author'>
<a href={{result.userPath}} data-user-card="{{unbound result.username}}">
{{avatar result imageSize="large"}}
</a>
</div>
<div class='fps-topic'>
<div class='topic'>
<a class='search-link' href='{{unbound result.url}}'>
{{topic-status topic=result.topic disableActions=true}}<span class='topic-title'>{{#highlight-text highlight=term}}{{{unbound result.topic.fancyTitle}}}{{/highlight-text}}</span>
</a>
<div class='search-category'>
{{#if result.topic.category.parentCategory}}
{{category-link result.topic.category.parentCategory}}
{{/if}}
{{category-link result.topic.category hideParent=true}}
{{#each result.topic.tags as |tag|}}
{{discourse-tag tag}}
{{/each}}
</div>
</div>
<div class='blurb container'>
<span class='date'>
{{format-age result.created_at}}
{{#if result.blurb}}
-
{{/if}}
</span>
{{#if result.blurb}}
{{#highlight-text highlight=term}}
{{{unbound result.blurb}}}
{{/highlight-text}}
{{/if}}
</div>
</div>
</div>
{{/each}}
</div>
{{/conditional-loading-spinner}}

View File

@ -10,7 +10,7 @@
<div class='admin-container user-badges'>
<h2>{{i18n 'admin.badges.grant_badge'}}</h2>
<br>
{{#if noBadges}}
{{#if noGrantableBadges}}
<p>{{i18n 'admin.badges.no_badges'}}</p>
{{else}}
<form class="form-horizontal">
@ -22,7 +22,7 @@
<label>{{i18n 'admin.badges.reason'}}</label>
{{input type="text" value=badgeReason}}<br><small>{{i18n 'admin.badges.reason_help'}}</small>
</label>
<button class='btn btn-primary' {{action "grantBadge" selectedBadgeId}}>{{i18n 'admin.badges.grant'}}</button>
<button class='btn btn-primary' {{action "grantBadge"}}>{{i18n 'admin.badges.grant'}}</button>
</form>
{{/if}}

View File

@ -6,6 +6,7 @@
{{i18n 'admin.user.show_public_profile'}}
{{/link-to}}
{{/if}}
{{#if model.can_view_action_logs}}
{{d-button action="viewActionLogs" actionParam=model.username icon="list-alt" label="admin.user.action_logs"}}
{{/if}}
@ -466,7 +467,18 @@
</div>
<div class='display-row'>
<div class='field'>{{i18n 'admin.user.flags_given_received_count'}}</div>
<div class='value'>{{model.flags_given_count}} / {{model.flags_received_count}}</div>
<div class='value'>
{{model.flags_given_count}} / {{model.flags_received_count}}
</div>
<div class='controls'>
{{#if model.flags_received_count}}
{{d-button
action=(action "showFlagsReceived")
label="admin.user.show_flags_received"
icon="flag"
}}
{{/if}}
</div>
</div>
<div class='display-row'>
<div class='field'>{{i18n 'admin.user.private_topics_count'}}</div>

View File

@ -60,9 +60,9 @@
//= require ./discourse/models/user-action
//= require ./discourse/models/draft
//= require ./discourse/models/composer
//= require ./discourse/models/user-badge
//= require_tree ./discourse/mixins
//= require ./discourse/models/invite
//= require ./discourse/models/user-badge
//= require ./discourse/controllers/discovery-sortable
//= require ./discourse/controllers/navigation/default
//= require ./discourse/components/edit-category-panel

View File

@ -195,7 +195,8 @@ export function buildResolver(baseName) {
findAdminTemplate(parsedName) {
var decamelized = parsedName.fullNameWithoutType.decamelize();
if (decamelized.indexOf('components') === 0) {
const compTemplate = Ember.TEMPLATES['admin/templates/' + decamelized];
let comPath = `admin/templates/${decamelized}`;
const compTemplate = Ember.TEMPLATES[`javascripts/${comPath}`] || Ember.TEMPLATES[comPath];
if (compTemplate) { return compTemplate; }
}

View File

@ -1,152 +0,0 @@
import { setting } from 'discourse/lib/computed';
import computed from 'ember-addons/ember-computed-decorators';
var get = Ember.get;
export default Ember.Component.extend({
classNameBindings: ['category::no-category', 'categories:has-drop', 'categoryStyle'],
categoryStyle: setting('category_style'),
expanded: false,
tagName: 'li',
@computed('expanded')
expandIcon(expanded) {
return expanded ? 'd-drop-expanded' : 'd-drop-collapsed';
},
allCategoriesUrl: function() {
if (this.get('subCategory')) {
return this.get('parentCategory.url') || "/";
} else {
return "/";
}
}.property('parentCategory.url', 'subCategory'),
noCategoriesUrl: function() {
return this.get('parentCategory.url') + "/none";
}.property('parentCategory.url'),
allCategoriesLabel: function() {
if (this.get('subCategory')) {
return I18n.t('categories.all_subcategories', {categoryName: this.get('parentCategory.name')});
}
return I18n.t('categories.all');
}.property('category'),
dropdownButtonClass: function() {
let result = 'dropdown-header category-dropdown-button';
if (Em.isNone(this.get('category'))) {
result += ' home';
}
return result;
}.property('category'),
categoryColor: function() {
var category = this.get('category');
if (category) {
var color = get(category, 'color');
if (color) {
var style = "";
if (color) { style += "background-color: #" + color + ";"; }
return style.htmlSafe();
}
}
return "background-color: #eee;".htmlSafe();
}.property('category'),
badgeStyle: function() {
let category = this.get('category');
const categoryStyle = this.siteSettings.category_style;
if (categoryStyle === 'bullet') {
return;
}
if (category) {
let color = get(category, 'color');
let textColor = get(category, 'text_color');
if (color || textColor) {
let style = "";
if (color) {
if (categoryStyle === "bar") {
style += `border-color: #${color};`;
} else if (categoryStyle === "box") {
style += `background-color: #${color};`;
if (textColor) { style += "color: #" + textColor + "; "; }
}
}
return style.htmlSafe();
}
}
if (categoryStyle === 'box') {
return "background-color: #eee; color: #333".htmlSafe();
}
}.property('category'),
clickEventName: function() {
return "click.category-drop-" + (this.get('category.id') || "all");
}.property('category.id'),
actions: {
expand: function() {
var self = this;
if(!this.get('renderCategories')){
this.set('renderCategories',true);
Em.run.next(function(){
self.send('expand');
});
return;
}
if (this.get('expanded')) {
this.close();
return;
}
if (this.get('categories')) {
this.set('expanded', true);
}
var $dropdown = this.$()[0];
this.$('a[data-drop-close]').on('click.category-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.category-drop');
},
close: function() {
this.removeEvents();
this.set('expanded', false);
},
willDestroyElement: function() {
this.removeEvents();
}
});

View File

@ -13,6 +13,7 @@ import { tinyAvatar,
displayErrorForUpload,
getUploadMarkdown,
validateUploadedFiles,
authorizesOneOrMoreImageExtensions,
formatUsername,
clipboardData
} from 'discourse/lib/utilities';
@ -36,6 +37,12 @@ export default Ember.Component.extend({
return `[${I18n.t('uploading')}]() `;
},
@computed()
replyPlaceholder() {
const key = authorizesOneOrMoreImageExtensions() ? "reply_placeholder" : "reply_placeholder_no_images";
return `composer.${key}`;
},
@observes('composer.uploadCancelled')
_cancelUpload() {
if (!this.get('composer.uploadCancelled')) { return; }
@ -662,13 +669,15 @@ export default Ember.Component.extend({
unshift: true
});
toolbar.addButton({
id: 'upload',
group: 'insertions',
icon: 'upload',
title: 'upload',
sendAction: 'showUploadModal'
});
if (this.get('allowUpload')) {
toolbar.addButton({
id: 'upload',
group: 'insertions',
icon: 'upload',
title: 'upload',
sendAction: 'showUploadModal'
});
}
toolbar.addButton({
id: 'options',

View File

@ -1,5 +1,7 @@
import LinkLookup from 'discourse/lib/link-lookup';
let _messagesCache = {};
export default Ember.Component.extend({
classNameBindings: [':composer-popup-container', 'hidden'],
checkedMessages: false,
@ -165,7 +167,9 @@ export default Ember.Component.extend({
if (topicId) { args.topic_id = topicId; }
if (postId) { args.post_id = postId; }
composer.store.find('composer-message', args).then(messages => {
const cacheKey = `${args.composer_action}${args.topic_id}${args.post_id}`;
const processMessages = messages => {
if (this.isDestroying || this.isDestroyed) { return; }
// Checking composer messages on replies can give us a list of links to check for
@ -177,6 +181,15 @@ export default Ember.Component.extend({
this.set('checkedMessages', true);
const queuedForTyping = this.get('queuedForTyping');
messages.forEach(msg => msg.wait_for_typing ? queuedForTyping.addObject(msg) : this.send('popup', msg));
});
};
if (_messagesCache.cacheKey === cacheKey) {
processMessages(_messagesCache.messages);
} else {
composer.store.find('composer-message', args).then(messages => {
_messagesCache = {messages, cacheKey};
processMessages(messages);
});
}
}
});

View File

@ -2,6 +2,8 @@ import { getCustomHTML } from 'discourse/helpers/custom-html';
import { getOwner } from 'discourse-common/lib/get-owner';
export default Ember.Component.extend({
triggerAppEvent: null,
init() {
this._super();
const name = this.get('name');
@ -16,5 +18,19 @@ export default Ember.Component.extend({
this.set('layoutName', name);
}
}
},
didInsertElement() {
this._super();
if (this.get('triggerAppEvent') === 'true') {
this.appEvents.trigger(`inserted-custom-html:${this.get('name')}`);
}
},
willDestroyElement() {
this._super();
if (this.get('triggerAppEvent') === 'true') {
this.appEvents.trigger(`destroyed-custom-html:${this.get('name')}`);
}
}
});

View File

@ -254,6 +254,7 @@ export default Ember.Component.extend({
@on('willDestroyElement')
_shutDown() {
if (this.get('composerEvents')) {
this.appEvents.off('composer:insert-block');
this.appEvents.off('composer:insert-text');
this.appEvents.off('composer:replace-text');
}
@ -734,6 +735,11 @@ export default Ember.Component.extend({
showLinkModal() {
this._lastSel = this._getSelected();
if (this._lastSel) {
this.set("linkText", this._lastSel.value.trim());
}
this.set('insertLinkHidden', false);
},

View File

@ -4,6 +4,7 @@ import { emojiUrlFor } from "discourse/lib/text";
import KeyValueStore from "discourse/lib/key-value-store";
import { emojis } from "pretty-text/emoji/data";
import { extendedEmojiList, isSkinTonableEmoji } from "pretty-text/emoji";
const { run } = Ember;
const keyValueStore = new KeyValueStore("discourse_emojis_");
const EMOJI_USAGE = "emojiUsage";
@ -44,7 +45,7 @@ export default Ember.Component.extend({
this.set("selectedDiversity", keyValueStore.getObject(EMOJI_SELECTED_DIVERSITY) || 1);
this.set("recentEmojis", keyValueStore.getObject(EMOJI_USAGE) || []);
Ember.run.scheduleOnce("afterRender", this, function() {
run.scheduleOnce("afterRender", this, function() {
this._bindEvents();
this._sectionLoadingCheck();
this._loadCategoriesEmojis();
@ -83,13 +84,14 @@ export default Ember.Component.extend({
@on("didUpdateAttrs")
_setState() {
this.get("active") === true ? this.show() : this.close();
this.get("active") ? this.show() : this.close();
},
@observes("filter")
filterChanged() {
this.$filter.find(".clear-filter").toggle(!_.isEmpty(this.get("filter")));
Ember.run.debounce(this, this._filterEmojisList, 250);
const filterDelay = this.site.isMobileDevice ? 400 : 250;
run.debounce(this, this._filterEmojisList, filterDelay);
},
@observes("selectedDiversity")
@ -148,7 +150,7 @@ export default Ember.Component.extend({
_sectionLoadingCheck() {
this._checkTimeout = setTimeout(() => { this._sectionLoadingCheck(); }, 500);
Ember.run.throttle(this, this._checkVisibleSection, 100);
run.throttle(this, this._checkVisibleSection, 100);
},
_loadCategoriesEmojis() {
@ -200,7 +202,7 @@ export default Ember.Component.extend({
if (this.get("filter") === "") {
this.$filter.find("input[name='filter']").val("");
this.$results.empty().hide();
this.$list.show();
this.$list.css("visibility", "visible");
} else {
const lowerCaseFilter = this.get("filter").toLowerCase();
const filterableEmojis = emojis.concat(_.keys(extendedEmojiList()));
@ -217,7 +219,7 @@ export default Ember.Component.extend({
).show();
this._bindHover(this.$results);
this._bindEmojiClick(this.$results);
this.$list.hide();
this.$list.css("visibility", "hidden");
}
},
@ -239,7 +241,7 @@ export default Ember.Component.extend({
this.$picker.find(".category-icon").on("click", "button.emoji", (event) => {
this.set("filter", "");
this.$results.empty();
this.$list.show();
this.$list.css("visibility", "visible");
const section = $(event.currentTarget).data("section");
const $section = this.$list.find(`.section[data-section="${section}"]`);
@ -264,11 +266,11 @@ export default Ember.Component.extend({
_bindResizing() {
this.$(window).on("resize", () => {
Ember.run.throttle(this, this._positionPicker, 16);
run.throttle(this, this._positionPicker, 16);
});
$("#reply-control").on("div-resizing", () => {
Ember.run.throttle(this, this._positionPicker, 16);
run.throttle(this, this._positionPicker, 16);
});
},
@ -320,7 +322,7 @@ export default Ember.Component.extend({
_bindSectionsScroll() {
this.$list.on("scroll", () => {
this.scrollPosition = this.$list.scrollTop();
Ember.run.throttle(this, this._checkVisibleSection, 150);
run.throttle(this, this._checkVisibleSection, 150);
});
},
@ -415,12 +417,12 @@ export default Ember.Component.extend({
const mobilePositioning = options => {
let attributes = {
width: windowWidth - 12,
marginLeft: 5,
marginTop: -130,
width: windowWidth,
marginLeft: 0,
marginTop: "auto",
left: 0,
bottom: "",
top: "50%",
top: 0,
display: "flex"
};
@ -445,7 +447,7 @@ export default Ember.Component.extend({
this.$picker.css(_.merge(attributes, options));
};
if(Ember.testing || this.get("automaticPositioning") === false) {
if(Ember.testing || !this.get("automaticPositioning")) {
desktopPositioning();
return;
}
@ -510,7 +512,7 @@ export default Ember.Component.extend({
this.$list.scrollTop(yPosition);
// if we dont actually scroll we need to force it
if(yPosition === 0) {
if (yPosition === 0) {
this.$list.scroll();
}
},

View File

@ -7,11 +7,13 @@ export default Ember.Component.extend(bufferedRender({
rerenderTriggers: ['order', 'desc'],
buildBuffer(buffer) {
buffer.push("<span class='header-contents'>");
buffer.push(I18n.t(this.get('i18nKey')));
if (this.get('field') === this.get('order')) {
buffer.push(iconHTML(this.get('desc') ? 'chevron-down' : 'chevron-up'));
}
buffer.push("</span>");
},
click() {

View File

@ -1,46 +0,0 @@
import CleansUp from 'discourse/mixins/cleans-up';
export default Ember.Component.extend(CleansUp, {
classNames: 'period-chooser',
showPeriods: false,
cleanUp: function() {
this.set('showPeriods', false);
$('html').off('mousedown.top-period');
},
_clickToClose: function() {
const self = this;
$('html').off('mousedown.top-period').on('mousedown.top-period', function(e) {
const $target = $(e.target);
if (($target.prop('id') === 'topic-entrance') || (self.$().has($target).length !== 0)) {
return;
}
self.cleanUp();
});
},
click(e) {
if ($(e.target).closest('.period-popup').length) { return; }
if (!this.get('showPeriods')) {
if (!this.site.mobileView) {
const $chevron = this.$('.d-icon-caret-down');
this.$('#period-popup').css($chevron.position());
} else {
this.$('#period-popup').css({top: this.$().height()});
}
this.set('showPeriods', true);
this._clickToClose();
}
},
actions: {
changePeriod(p) {
this.cleanUp();
this.set('period', p);
this.sendAction('action', p);
}
}
});

View File

@ -139,7 +139,7 @@ const SiteHeaderComponent = MountWidget.extend(Docking, {
if ($panelBody.height() !== contentHeight) {
$panelBody.height(contentHeight);
}
$('body').addClass('drop-down-visible');
$('body').addClass('drop-down-mode');
} else {
const menuTop = headerHeight();
@ -157,7 +157,7 @@ const SiteHeaderComponent = MountWidget.extend(Docking, {
if (style.top !== menuTop + "px" || style.height !== height) {
$panel.css({ top: menuTop + "px", height });
}
$('body').removeClass('drop-down-visible');
$('body').removeClass('drop-down-mode');
}
$panel.width(width);

View File

@ -1,139 +0,0 @@
import { setting } from 'discourse/lib/computed';
import computed from 'ember-addons/ember-computed-decorators';
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',
@computed('site.top_tags')
tags(topTags) {
if (this.siteSettings.tags_sort_alphabetically && topTags) {
return topTags.sort();
} else {
return topTags;
}
},
@computed('expanded')
expandedIcon(expanded) {
return expanded ? 'd-drop-expanded' : 'd-drop-collapsed';
},
@computed('tagId')
tagClass() {
if (this.get('tagId')) {
return "tag-" + this.get('tagId');
} else {
return "tag_all";
}
},
@computed('firstCategory', 'secondCategory')
allTagsUrl() {
if (this.get('currentCategory')) {
return this.get('currentCategory.url') + "?allTags=1";
} else {
return "/";
}
},
@computed('tag')
allTagsLabel() {
return I18n.t("tagging.selector_all_tags");
},
@computed('tagId')
noTagsSelected() {
return this.get('tagId') === 'none';
},
@computed('firstCategory', 'secondCategory')
noTagsUrl() {
var url = '/tags';
if (this.get('currentCategory')) {
url += this.get('currentCategory.url');
}
return url + '/none';
},
@computed('tag')
noTagsLabel() {
return I18n.t("tagging.selector_no_tags");
},
@computed('tag')
dropdownButtonClass() {
let result = 'dropdown-header category-dropdown-button';
if (Em.isNone(this.get('tag'))) {
result += ' home';
}
return result;
},
@computed('tag')
clickEventName() {
return "click.tag-drop-" + (this.get('tag') || "all");
},
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

@ -6,6 +6,16 @@ export default Ember.Component.extend({
// Allow us to extend it
layoutName: 'components/topic-footer-buttons',
@computed('topic.isPrivateMessage')
canArchive(isPM) {
return this.siteSettings.enable_private_messages && isPM;
},
@computed('topic.isPrivateMessage')
showNotificationsButton(isPM) {
return (!isPM) || this.siteSettings.enable_private_messages;
},
@computed('topic.details.can_invite_to')
canInviteTo(result) {
return !this.site.mobileView && result;

View File

@ -1,5 +1,6 @@
export default Ember.Component.extend({
classNames: ['user-stat'],
type: 'number',
isNumber: Ember.computed.equal('type', 'number')
isNumber: Ember.computed.equal('type', 'number'),
isDuration: Ember.computed.equal('type', 'duration')
});

View File

@ -5,7 +5,7 @@ import Composer from 'discourse/models/composer';
import { default as computed, observes, on } from 'ember-addons/ember-computed-decorators';
import InputValidation from 'discourse/models/input-validation';
import { getOwner } from 'discourse-common/lib/get-owner';
import { escapeExpression } from 'discourse/lib/utilities';
import { escapeExpression, authorizesOneOrMoreExtensions } from 'discourse/lib/utilities';
import { emojiUnescape } from 'discourse/lib/text';
import { shortDate } from 'discourse/lib/formatter';
@ -70,6 +70,7 @@ export default Ember.Controller.extend({
scopedCategoryId: null,
lastValidatedAt: null,
isUploading: false,
allowUpload: false,
topic: null,
linkLookup: null,
showPreview: true,
@ -223,6 +224,11 @@ export default Ember.Controller.extend({
return emojiUnescape(escapeExpression(topic.get('title')));
},
@computed
allowUpload() {
return authorizesOneOrMoreExtensions();
},
actions: {
cancelUpload() {
this.set('model.uploadCancelled', true);

View File

@ -169,7 +169,7 @@ export default Ember.Controller.extend({
@computed('expanded', 'model.grouped_search_result.can_create_topic')
canCreateTopic(expanded, userCanCreateTopic) {
return this.currentUser && userCanCreateTopic && !this.site.mobileView && !expanded;
return this.currentUser && userCanCreateTopic && !expanded;
},
@computed('expanded')
@ -231,6 +231,7 @@ export default Ember.Controller.extend({
}
}else{
setTransient('lastSearch', { searchKey, model }, 5);
model.grouped_search_result = results.grouped_search_result;
this.set("model", model);
}
}).finally(() => {

View File

@ -0,0 +1,63 @@
import computed from "ember-addons/ember-computed-decorators";
import { extractError } from 'discourse/lib/ajax-error';
import ModalFunctionality from "discourse/mixins/modal-functionality";
import GrantBadgeController from "discourse/mixins/grant-badge-controller";
import Badge from 'discourse/models/badge';
import UserBadge from 'discourse/models/user-badge';
export default Ember.Controller.extend(ModalFunctionality, GrantBadgeController, {
topicController: Ember.inject.controller("topic"),
loading: true,
saving: false,
selectedBadgeId: null,
allBadges: [],
userBadges: [],
@computed('topicController.selectedPosts')
post() {
return this.get('topicController.selectedPosts')[0];
},
@computed('post')
badgeReason(post) {
const url = post.get('url');
const protocolAndHost = window.location.protocol + '//' + window.location.host;
return url.indexOf('/') === 0 ? protocolAndHost + url : url;
},
@computed("saving", "selectedBadgeGrantable")
buttonDisabled(saving, selectedBadgeGrantable) {
return saving || !selectedBadgeGrantable;
},
onShow() {
this.set('loading', true);
Ember.RSVP.all([Badge.findAll(), UserBadge.findByUsername(this.get('post.username'))])
.then(([allBadges, userBadges]) => {
this.setProperties({
'allBadges': allBadges,
'userBadges': userBadges,
'loading': false,
});
});
},
actions: {
grantBadge() {
this.set('saving', true);
this.grantBadge(this.get('selectedBadgeId'), this.get('post.username'), this.get('badgeReason'))
.then(newBadge => {
this.set('selectedBadgeId', null);
this.flash(I18n.t(
'badges.successfully_granted', { username: this.get('post.username'), badge: newBadge.get('badge.name') }
), 'success');
}, error => {
this.flash(extractError(error), 'error');
})
.finally(() => this.set('saving', false));
}
}
});

View File

@ -24,7 +24,7 @@ export default Ember.Controller.extend({
group.findPosts(opts).then(newPosts => {
posts.addObjects(newPosts);
if(newPosts.length === 0) {
if (newPosts.length === 0) {
this.set('canLoadMore', false);
}
}).finally(() => {

View File

@ -217,7 +217,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
// Reload the page if we're authenticated
if (options.authenticated) {
const destinationUrl = $.cookie('destination_url');
const destinationUrl = $.cookie('destination_url') || options.destination_url;
if (destinationUrl) {
// redirect client to the original URL
$.cookie('destination_url', null);

View File

@ -1,7 +1,20 @@
import computed from 'ember-addons/ember-computed-decorators';
export default Ember.Controller.extend({
sortProperties: ['count:desc', 'id'],
canAdminTags: Ember.computed.alias("currentUser.staff"),
groupedByCategory: Ember.computed.notEmpty('model.extras.categories'),
groupedByTagGroup: Ember.computed.notEmpty('model.extras.tag_groups'),
@computed('groupedByCategory', 'groupedByTagGroup')
otherTagsTitleKey(groupedByCategory, groupedByTagGroup) {
if (!groupedByCategory && !groupedByTagGroup) {
return 'tagging.all_tags';
} else {
return 'tagging.other_tags';
}
},
actions: {
sortByCount() {

View File

@ -119,8 +119,8 @@ export default Ember.Controller.extend(BulkTopicSelection, {
deleteTag() {
const self = this;
const topicsLength = this.get('list.topic_list.topics.length');
const confirmText = topicsLength === 0 ? I18n.t("tagging.delete_confirm_no_topics") : I18n.t("tagging.delete_confirm", {count: topicsLength});
const numTopics = this.get('list.topic_list.tags.firstObject.topic_count') || 0;
const confirmText = numTopics === 0 ? I18n.t("tagging.delete_confirm_no_topics") : I18n.t("tagging.delete_confirm", {count: numTopics});
bootbox.confirm(confirmText, function(result) {
if (!result) { return; }

View File

@ -518,6 +518,19 @@ export default Ember.Controller.extend(BufferedContent, {
this.send('changeOwner');
},
lockPost(post) {
return post.updatePostField('locked', true);
},
unlockPost(post) {
return post.updatePostField('locked', false);
},
grantBadge(post) {
this.set("selectedPostIds", [post.id]);
this.send('showGrantBadgeModal');
},
toggleParticipant(user) {
this.get("model.postStream")
.toggleParticipant(user.get("username"))
@ -767,9 +780,12 @@ export default Ember.Controller.extend(BufferedContent, {
return selectedPostsCount > 0 && (selectedAllPosts || selectedPosts.every(p => p.can_delete));
},
@computed('canMergeTopic', 'selectedAllPosts')
canSplitTopic(canMergeTopic, selectedAllPosts) {
return canMergeTopic && !selectedAllPosts;
@computed('canMergeTopic', 'selectedAllPosts', 'selectedPosts', 'selectedPosts.[]')
canSplitTopic(canMergeTopic, selectedAllPosts, selectedPosts) {
return canMergeTopic &&
!selectedAllPosts &&
selectedPosts.length > 0 &&
selectedPosts.sort((a, b) => a.post_number - b.post_number)[0].post_type === 1;
},
@computed('model.details.can_move_posts', 'selectedPostsCount')

View File

@ -1,12 +1,14 @@
import CanCheckEmails from 'discourse/mixins/can-check-emails';
import computed from 'ember-addons/ember-computed-decorators';
import User from 'discourse/models/user';
import optionalService from 'discourse/lib/optional-service';
export default Ember.Controller.extend(CanCheckEmails, {
indexStream: false,
application: Ember.inject.controller(),
userNotifications: Ember.inject.controller('user-notifications'),
currentPath: Ember.computed.alias('application.currentPath'),
adminTools: optionalService(),
@computed("content.username")
viewingSelf(username) {
@ -93,11 +95,15 @@ export default Ember.Controller.extend(CanCheckEmails, {
this.set('forceExpand', true);
},
adminDelete() {
// I really want this deferred, don't want to bring in all this code till used
const AdminUser = requirejs('admin/models/admin-user').default;
AdminUser.find(this.get('model.id')).then(user => user.destroy({deletePosts: true}));
showSuspensions() {
this.get('adminTools').showActionLogs(this, {
target_user: this.get('model.username'),
action_name: 'suspend_user'
});
},
adminDelete() {
this.get('adminTools').deleteUser(this.get('model.id'));
}
}
});

View File

@ -67,9 +67,9 @@ export function categoryBadgeHTML(category, opts) {
let categoryName = escapeExpression(get(category, 'name'));
if (restricted) {
html += iconHTML('lock') + " " + categoryName;
html += `${iconHTML('lock')}<span>${categoryName}</span>`;
} else {
html += categoryName;
html += `<span>${categoryName}</span>`;
}
html += "</span>";

View File

@ -4,31 +4,21 @@ export default {
name: 'localization',
after: 'inject-objects',
enableVerboseLocalization() {
let counter = 0;
let keys = {};
let t = I18n.t;
I18n.noFallbacks = true;
isVerboseLocalizationEnabled(container) {
const siteSettings = container.lookup('site-settings:main');
if (siteSettings.verbose_localization) return true;
I18n.t = I18n.translate = function(scope, value){
let current = keys[scope];
if (!current) {
current = keys[scope] = ++counter;
let message = "Translation #" + current + ": " + scope;
if (!_.isEmpty(value)) {
message += ", parameters: " + JSON.stringify(value);
}
Em.Logger.info(message);
}
return t.apply(I18n, [scope, value]) + " (#" + current + ")";
};
try {
return sessionStorage && sessionStorage.getItem("verbose_localization");
} catch (e) {
return false;
}
},
initialize(container) {
const siteSettings = container.lookup('site-settings:main');
if (siteSettings.verbose_localization) {
this.enableVerboseLocalization();
if (this.isVerboseLocalizationEnabled(container)) {
I18n.enableVerboseLocalization();
}
// Merge any overrides into our object

View File

@ -1,5 +1,5 @@
import { cleanDOM } from 'discourse/lib/clean-dom';
import { startPageTracking } from 'discourse/lib/page-tracker';
import { startPageTracking, googleTagManagerPageChanged } from 'discourse/lib/page-tracker';
import { viewTrackingRequired } from 'discourse/lib/ajax';
export default {
@ -35,15 +35,7 @@ export default {
// And Google Tag Manager too
if (typeof window.dataLayer !== 'undefined') {
appEvents.on('page:changed', data => {
window.dataLayer.push({
'event': 'virtualPageView',
'page': {
'title': data.title,
'url': data.url
}
});
});
appEvents.on('page:changed', googleTagManagerPageChanged);
}
}
};

View File

@ -55,4 +55,8 @@ export function throwAjaxError(undoCallback) {
export function popupAjaxError(error) {
bootbox.alert(extractError(error));
error._discourse_displayed = true;
// We re-throw in a catch to not swallow the exception
throw error;
}

View File

@ -276,7 +276,6 @@ export default function(options) {
}
if (Discourse.Site.currentProp('mobileView')) {
div.css('width', 'auto');
if ((me.height() / 2) >= pos.top) { vOffset = -23; }
if ((me.width() / 2) <= pos.left) { hOffset = -div.width(); }

View File

@ -129,7 +129,8 @@ export default {
}
// Otherwise, use a custom URL with a redirect
if (Discourse.User.currentProp('external_links_in_new_tab')) {
// consider CTRL+mouse-left-click / CMD+mouse-left-click or mouse-middle-click as well
if (Discourse.User.currentProp('external_links_in_new_tab') || ((e.ctrlKey || e.metaKey) && (e.which === 1)) || (e.which === 2)) {
window.open(destUrl, '_blank').focus();
} else {
DiscourseURL.redirectTo(destUrl);

View File

@ -12,6 +12,7 @@ const bindings = {
'.': {click: '.alert.alert-info.clickable', anonymous: true}, // show incoming/updated topics
'b': {handler: 'toggleBookmark'},
'c': {handler: 'createTopic'},
'C': {handler: 'focusComposer'},
'ctrl+f': {handler: 'showPageSearch', anonymous: true},
'command+f': {handler: 'showPageSearch', anonymous: true},
'ctrl+p': {handler: 'printTopic', anonymous: true},
@ -67,6 +68,12 @@ export default {
this.searchService = this.container.lookup('search-service:main');
this.appEvents = this.container.lookup('app-events:main');
this.currentUser = this.container.lookup('current-user:main');
let siteSettings = this.container.lookup('site-settings:main');
// Disable the shortcut if private messages are disabled
if (!siteSettings.enable_private_messages) {
delete bindings['g m'];
}
Object.keys(bindings).forEach(key => {
const binding = bindings[key];
@ -174,6 +181,15 @@ export default {
}
},
focusComposer() {
const composer = this.container.lookup('controller:composer');
if (composer.get('model.viewOpen')) {
setTimeout(() => $('textarea.d-editor-input').focus(), 0);
} else {
composer.send('openIfDraft');
}
},
pinUnpinTopic() {
this.container.lookup('controller:topic').togglePinnedState();
},

View File

@ -38,3 +38,23 @@ export function startPageTracking(router, appEvents) {
});
_started = true;
}
const _gtmPageChangedCallbacks = [];
export function addGTMPageChangedCallback(callback) {
_gtmPageChangedCallbacks.push(callback);
}
export function googleTagManagerPageChanged(data) {
let gtmData = {
'event': 'virtualPageView',
'page': {
'title': data.title,
'url': data.url
}
};
_.each(_gtmPageChangedCallbacks, callback => callback(gtmData));
window.dataLayer.push(gtmData);
}

View File

@ -22,9 +22,10 @@ import { registerIconRenderer, replaceIcon } from 'discourse-common/lib/icon-lib
import { addNavItem } from 'discourse/models/nav-item';
import { replaceFormatter } from 'discourse/lib/utilities';
import { modifySelectKit } from "select-kit/mixins/plugin-api";
import { addGTMPageChangedCallback } from 'discourse/lib/page-tracker';
// If you add any methods to the API ensure you bump up this number
const PLUGIN_API_VERSION = '0.8.13';
const PLUGIN_API_VERSION = '0.8.17';
class PluginApi {
constructor(version, container) {
@ -350,10 +351,24 @@ class PluginApi {
```
**/
onPageChange(fn) {
let appEvents = this.container.lookup('app-events:main');
appEvents.on('page:changed', data => fn(data.url, data.title));
this.onAppEvent('page:changed', data => fn(data.url, data.title));
}
/**
Listen for a triggered `AppEvent` from Discourse.
```javascript
api.onAppEvent('inserted-custom-html', () => {
console.log('a custom footer was rendered');
});
```
**/
onAppEvent(name, fn) {
let appEvents = this.container.lookup('app-events:main');
appEvents.on(name, fn);
}
/**
* Changes a setting associated with a widget. For example, if
* you wanted small avatars in the post stream:
@ -605,6 +620,20 @@ class PluginApi {
modifySelectKit(pluginApiKey) {
return modifySelectKit(pluginApiKey);
}
/**
*
* Registers a function that can inspect and modify the data that
* will be sent to Google Tag Manager when a page changed event is triggered.
*
* Example:
*
* addGTMPageChangedCallback( gtmData => gtmData.locale = I18n.currentLocale() )
*
*/
addGTMPageChangedCallback(fn) {
addGTMPageChangedCallback(fn);
}
}
let _pluginv01;

View File

@ -66,6 +66,7 @@ export default class {
this._totalTimings = {};
this._topicTime = 0;
this._onscreen = [];
this._inProgress = false;
}
scrolled() {
@ -108,6 +109,7 @@ export default class {
if (!$.isEmptyObject(newTimings)) {
if (this.currentUser) {
this._inProgress = true;
ajax('/topics/timings', {
data: {
timings: newTimings,
@ -128,6 +130,9 @@ export default class {
}).catch(e => {
const error = e.jqXHR;
if (error.status === 405 && error.responseJSON.error_type === "read_only") return;
}).finally(() => {
this._inProgress = false;
this._lastFlush = 0;
});
} else if (this._anonCallback) {
// Anonymous viewer - save to localStorage
@ -182,7 +187,7 @@ export default class {
return timings[postNumber] > 0 && !totalTimings[postNumber];
});
if (this._lastFlush > nextFlush || rush) {
if (!this._inProgress && (this._lastFlush > nextFlush || rush)) {
this.flush();
}

View File

@ -2,7 +2,7 @@ import parseHTML from 'discourse/helpers/parse-html';
const trimLeft = text => text.replace(/^\s+/,"");
const trimRight = text => text.replace(/\s+$/,"");
const countPipes = text => text.replace(/\\\|/,"").match(/\|/g).length;
const countPipes = text => (text.replace(/\\\|/,"").match(/\|/g) || []).length;
class Tag {
constructor(name, prefix = "", suffix = "", inline = false) {
@ -189,8 +189,14 @@ class Tag {
toMarkdown() {
const text = this.element.innerMarkdown().trim();
if (text.includes("\n")) {
throw "Unsupported format inside Markdown table cells";
if(text.includes("\n")) { // Unsupported format inside Markdown table cells
let e = this.element;
while(e = e.parent) {
if (e.name === "table") {
e.tag().invalid();
break;
}
}
}
return this.decorate(text);
@ -242,21 +248,38 @@ class Tag {
static table() {
return class extends Tag.block("table") {
constructor() {
super();
this.isValid = true;
}
invalid() {
this.isValid = false;
if (this.element.parentNames.includes("table")) {
let e = this.element;
while(e = e.parent) {
if (e.name === "table") {
e.tag().invalid();
break;
}
}
}
}
decorate(text) {
text = super.decorate(text).replace(/\|\n{2,}\|/g, "|\n|");
const rows = text.trim().split("\n");
const pipeCount = countPipes(rows[0]);
const isValid = rows.length > 1 &&
pipeCount > 2 &&
rows.reduce((a, c) => a && countPipes(c) <= pipeCount);
this.isValid = this.isValid && rows.length > 1 && pipeCount > 2 && rows.reduce((a, c) => a && countPipes(c) <= pipeCount); // Unsupported table format for Markdown conversion
if (!isValid) {
throw "Unsupported table format for Markdown conversion";
if (this.isValid) {
const splitterRow = [...Array(pipeCount-1)].map(() => "| --- ").join("") + "|\n";
text = text.replace("|\n", "|\n" + splitterRow);
} else {
text = text.replace(/\|/g, " ");
this.invalid();
}
const splitterRow = [...Array(pipeCount-1)].map(() => "| --- ").join("") + "|\n";
text = text.replace("|\n", "|\n" + splitterRow);
return text;
}
};

View File

@ -77,6 +77,7 @@ export function transformBasicPost(post) {
cooked_hidden: !!post.cooked_hidden,
expandablePost: false,
replyCount: post.reply_count,
locked: post.locked
};
_additionalAttributes.forEach(a => postAtts[a] = post[a]);

View File

@ -193,6 +193,8 @@ export function validateUploadedFiles(files, opts) {
}
export function validateUploadedFile(file, opts) {
if (!authorizesOneOrMoreExtensions()) return false;
opts = opts || {};
const name = file && file.name;
@ -277,6 +279,21 @@ export function authorizesAllExtensions() {
return Discourse.SiteSettings.authorized_extensions.indexOf("*") >= 0;
}
export function authorizesOneOrMoreExtensions() {
if (authorizesAllExtensions()) return true;
return Discourse.SiteSettings.authorized_extensions
.split("|")
.filter(ext => ext)
.length > 0;
}
export function authorizesOneOrMoreImageExtensions() {
if (authorizesAllExtensions()) return true;
return imagesExtensions().length > 0;
}
export function isAnImage(path) {
return (/\.(png|jpe?g|gif|bmp|tiff?|svg|webp|ico)$/i).test(path);
}
@ -291,13 +308,13 @@ function isGUID(value) {
function imageNameFromFileName(fileName) {
const split = fileName.split('.');
const name = split[split.length-2];
let name = split[split.length - 2];
if (exports.isAppleDevice() && isGUID(name)) {
return I18n.t('upload_selector.default_image_alt_text');
name = I18n.t('upload_selector.default_image_alt_text');
}
return name;
return encodeURIComponent(name);
}
export function allowsImages() {
@ -450,5 +467,25 @@ export function clipboardData(e, canUpload) {
return { clipboard, types, canUpload, canPasteHtml };
}
export function fillMissingDates(data, startDate, endDate) {
const startMoment = moment(startDate, "YYYY-MM-DD");
const endMoment = moment(endDate, "YYYY-MM-DD");
const countDays = endMoment.diff(startMoment, 'days');
let currentMoment = startMoment;
for (let i = 0; i <= countDays; i++) {
let date = (data[i]) ? moment(data[i].x, "YYYY-MM-DD") : null;
if (i === 0 && date.isAfter(startMoment)) {
data.splice(i, 0, { "x" : startMoment.format("YYYY-MM-DD"), 'y': 0 });
} else {
if (!date || date.isAfter(moment(currentMoment))) {
data.splice(i, 0, { "x" : currentMoment, 'y': 0 });
}
}
currentMoment = moment(currentMoment).add(1, "day").format("YYYY-MM-DD");
}
return data;
}
// This prevents a mini racer crash
export default {};

View File

@ -0,0 +1,38 @@
import computed from "ember-addons/ember-computed-decorators";
import UserBadge from 'discourse/models/user-badge';
export default Ember.Mixin.create({
@computed('allBadges.[]', 'userBadges.[]')
grantableBadges(allBadges, userBadges) {
const granted = userBadges.reduce((map, badge) => {
map[badge.get('badge_id')] = true;
return map;
}, {});
return allBadges
.filter(badge => {
return badge.get('enabled')
&& badge.get('manually_grantable')
&& (!granted[badge.get('id')] || badge.get('multiple_grant'));
})
.sort((a, b) => a.get('name').localeCompare(b.get('name')));
},
noGrantableBadges: Ember.computed.empty('grantableBadges'),
@computed('selectedBadgeId', 'grantableBadges')
selectedBadgeGrantable(selectedBadgeId, grantableBadges) {
return grantableBadges && grantableBadges.find(badge => badge.get('id') === selectedBadgeId);
},
grantBadge(selectedBadgeId, username, badgeReason) {
return UserBadge.grant(selectedBadgeId, username, badgeReason)
.then(newBadge => {
this.get('userBadges').pushObject(newBadge);
return newBadge;
}, error => {
throw error;
});
}
});

View File

@ -53,7 +53,7 @@ export default Ember.Object.extend({
results.forEach(result => {
result.title = I18n.t(`topic_statuses.${result.key}.help`);
if (!self.disableActions && (result.key === "pinned" ||result.key === "unpinned")) {
if (this.currentUser && (result.key === "pinned" || result.key === "unpinned")) {
result.openTag = 'a href';
result.closeTag = 'a';
} else {

View File

@ -30,20 +30,9 @@ export default Discourse.Route.extend({
afterModel(model, transition) {
const username = transition.queryParams && transition.queryParams.username;
const userBadgesGrant = UserBadge.findByBadgeId(model.get("id"), {username}).then(userBadges => {
return UserBadge.findByBadgeId(model.get("id"), {username}).then(userBadges => {
this.userBadgesGrant = userBadges;
});
const userBadgesAll = UserBadge.findByUsername(username).then(userBadges => {
this.userBadgesAll = userBadges;
});
const promises = {
userBadgesGrant,
userBadgesAll,
};
return Ember.RSVP.hash(promises);
},
titleToken() {

View File

@ -54,8 +54,8 @@ function findTopicList(store, tracking, filter, filterParams, extras) {
tracking.trackIncoming(list.filter);
}
Discourse.Session.currentProp('topicList', list);
if (list.topic_list && list.topic_list.tags) {
Discourse.Site.currentProp('top_tags', list.topic_list.tags);
if (list.topic_list && list.topic_list.top_tags) {
Discourse.Site.currentProp('top_tags', list.topic_list.top_tags);
}
return list;
});

View File

@ -36,7 +36,8 @@ export default Discourse.Route.extend({
return null;
}
}).then(results => {
const model = (results && translateResults(results)) || {};
const grouped_search_result = results ? results.grouped_search_result : {};
const model = (results && translateResults(results)) || { grouped_search_result };
setTransient('lastSearch', { searchKey, model }, 5);
return model;
});

View File

@ -12,8 +12,13 @@ export function buildGroupPage(type) {
},
setupController(controller, model) {
this.controllerFor('group-activity-posts').setProperties({ model, type, canLoadMore: true });
this.controllerFor("group").set("showing", type);
let loadedAll = model.length < 20;
this.controllerFor('group-activity-posts').setProperties({
model,
type,
canLoadMore: !loadedAll,
});
this.controllerFor('application').set('showFooter', loadedAll);
},
renderTemplate() {

View File

@ -4,10 +4,11 @@ import Group from 'discourse/models/group';
export default Discourse.Route.extend({
beforeModel(transition) {
const self = this;
const params = transition.queryParams;
if (this.currentUser) {
this.replaceWith("discovery.latest").then(e => {
if (self.currentUser) {
self.replaceWith("discovery.latest").then(e => {
if (params.username) {
// send a message to a user
User.findByUsername(params.username).then(user => {

View File

@ -85,6 +85,10 @@ const TopicRoute = Discourse.Route.extend({
this.controllerFor('modal').set('modalClass', 'history-modal');
},
showGrantBadgeModal() {
showModal('grant-badge', { model: this.modelFor('topic'), title: 'admin.badges.grant_badge' });
},
showRawEmail(model) {
showModal('raw-email', { model });
this.controllerFor('raw_email').loadRawEmail(model.get("id"));

View File

@ -23,7 +23,7 @@
{{plugin-outlet name="above-footer" args=(hash showFooter=showFooter)}}
{{#if showFooter}}
{{custom-html name="footer"}}
{{custom-html name="footer" triggerAppEvent="true"}}
{{/if}}
{{plugin-outlet name="below-footer" args=(hash showFooter=showFooter)}}

View File

@ -10,7 +10,10 @@
<div class="control-group">
<label class="control-label"></label>
<div class="controls">
{{combo-box value=selectedUserBadgeId nameProperty="badge.name" content=selectableUserBadges}}
{{combo-box
value=selectedUserBadgeId
nameProperty="badge.name"
content=selectableUserBadges}}
</div>
</div>

View File

@ -1,7 +1,14 @@
{{category-drop category=firstCategory categories=parentCategories}}
{{category-drop
category=firstCategory
categories=parentCategories}}
{{#if childCategories}}
{{category-drop category=secondCategory parentCategory=firstCategory categories=childCategories subCategory="true" noSubcategories=noSubcategories}}
{{category-drop
category=secondCategory
parentCategory=firstCategory
categories=childCategories
subCategory="true"
noSubcategories=noSubcategories}}
{{/if}}
{{#if siteSettings.tagging_enabled}}

View File

@ -2,14 +2,14 @@
<table class="category-list {{if showTopics 'with-topics'}}">
<thead>
<tr>
<th class="category">{{i18n 'categories.category'}}</th>
<th class="category"><span aria-role="heading" aria-level="2" id="categories-only-category">{{i18n 'categories.category'}}</span></th>
<th class="topics">{{i18n 'categories.topics'}}</th>
{{#if showTopics}}
<th class="latest">{{i18n 'categories.latest'}}</th>
{{/if}}
</tr>
</thead>
<tbody>
<tbody aria-labelledby="categories-only-category">
{{#each categories as |c|}}
<tr data-category-id={{c.id}} class="{{if c.description_excerpt 'has-description' 'no-description'}} {{if c.uploaded_logo.url 'has-logo' 'no-logo'}}">
<td class="category" style={{border-color c.color}}>

View File

@ -1,30 +0,0 @@
{{#if category}}
<a href {{action "expand"}} class="dropdown-header" style={{badgeStyle}} aria-label={{i18n 'categories.category_list'}} aria-expanded={{expanded}}>
<span class="badge-category-bg" style={{categoryColor}}></span>
{{#if category.read_restricted}}
{{d-icon "lock"}}
{{/if}}
<span class='d-label'>{{category.name}}</span>
</a>
{{else}}
{{#if noSubcategories}}
<a href {{action "expand"}} class='dropdown-header home' style={{badgeStyle}} aria-label={{i18n 'categories.category_list'}} aria-expanded={{expanded}}>{{i18n 'categories.no_subcategory'}}</a>
{{else}}
<a href {{action "expand"}} class='dropdown-header home' style={{badgeStyle}} aria-label={{i18n 'categories.category_list'}} aria-expanded={{expanded}}>{{allCategoriesLabel}}</a>
{{/if}}
{{/if}}
{{#if categories}}
<a href {{action "expand"}} class={{dropdownButtonClass}} style={{badgeStyle}}>
{{d-icon expandIcon}}
</a>
<section class="{{unless expanded 'hidden'}} category-dropdown-menu chooser">
<div class='cat'><a href={{allCategoriesUrl}} data-drop-close="true" class='badge-category home'>{{allCategoriesLabel}}</a></div>
{{#if subCategory}}
<div class='cat'><a href={{noCategoriesUrl}} data-drop-close="true" class='badge-category home'>{{i18n 'categories.no_subcategory'}}</a></div>
{{/if}}
{{#if renderCategories}}
{{#each categories as |c|}}<div class='cat'>{{category-link c allowUncategorized=true hideParent=subCategory}}</div>{{/each}}
{{/if}}
</section>
{{/if}}

View File

@ -0,0 +1,10 @@
<section class='field'>
<section class="field-item">
<label>{{i18n 'category.name'}}</label>
{{text-field value=category.name placeholderKey="category.name_placeholder" maxlength="50"}}
</section>
<section class="field-item">
<label>{{i18n 'category.slug'}}</label>
{{text-field value=category.slug placeholderKey="category.slug_placeholder" maxlength="255"}}
</section>
</section>

View File

@ -1,20 +1,22 @@
{{d-editor tabindex="4"
value=composer.reply
placeholder="composer.reply_placeholder"
previewUpdated="previewUpdated"
markdownOptions=markdownOptions
extraButtons="extraButtons"
importQuote="importQuote"
showUploadModal="showUploadModal"
togglePreview="togglePreview"
validation=validation
loading=composer.loading
forcePreview=forcePreview
composerEvents=true
onExpandPopupMenuOptions="onExpandPopupMenuOptions"
onPopupMenuAction=onPopupMenuAction
popupMenuOptions=popupMenuOptions}}
{{d-editor
tabindex="4"
value=composer.reply
placeholder=replyPlaceholder
previewUpdated="previewUpdated"
markdownOptions=markdownOptions
extraButtons="extraButtons"
importQuote="importQuote"
showUploadModal="showUploadModal"
togglePreview="togglePreview"
validation=validation
loading=composer.loading
forcePreview=forcePreview
composerEvents=true
onExpandPopupMenuOptions="onExpandPopupMenuOptions"
onPopupMenuAction=onPopupMenuAction
popupMenuOptions=popupMenuOptions
outletArgs=(hash composer=composer editorType="composer")}}
{{#if site.mobileView}}
<input type="file" id="mobile-uploader" multiple />
<input type="file" id="mobile-uploader" multiple>
{{/if}}

View File

@ -41,13 +41,14 @@
{{conditional-loading-spinner condition=loading}}
{{textarea tabindex=tabindex value=value class="d-editor-input" placeholder=placeholderTranslated}}
{{popup-input-tip validation=validation}}
{{plugin-outlet name="after-d-editor" tagName="" args=outletArgs}}
</div>
<div class="d-editor-preview-wrapper {{if forcePreview 'force-preview'}}">
<div class="d-editor-preview">
{{{preview}}}
</div>
{{plugin-outlet name="editor-preview" classNames="d-editor-plugin"}}
{{plugin-outlet name="editor-preview" classNames="d-editor-plugin"}}
</div>
</div>

View File

@ -1,14 +1,5 @@
<form>
<section class='field'>
<section class="field-item">
<label>{{i18n 'category.name'}}</label>
{{text-field value=category.name placeholderKey="category.name_placeholder" maxlength="50"}}
</section>
<section class="field-item">
<label>{{i18n 'category.slug'}}</label>
{{text-field value=category.slug placeholderKey="category.slug_placeholder" maxlength="255"}}
</section>
</section>
{{category-name-fields category=category tagName=""}}
{{#if canSelectParentCategory}}
<section class='field'>

View File

@ -1,4 +1,4 @@
<div class='table-heading'>
<div class='table-heading' aria-role="heading" aria-level="2">
{{i18n "filters.latest.title"}}
</div>

View File

@ -1,11 +0,0 @@
<h2>{{period-title period showDateRange=true}}</h2>
<button>{{d-icon "caret-down"}}</button>
<div id='period-popup' class="{{unless showPeriods 'hidden'}} period-popup">
<ul>
{{#each site.periods as |p|}}
<li><a href {{action "changePeriod" p}}>{{period-title p showDateRange=true}}</a></li>
{{/each}}
</ul>
</div>
<div class='clearfix'></div>

View File

@ -1,28 +0,0 @@
{{#if showTagDropdown}}
{{#if tagId}}
{{#if noTagsSelected}}
<a href {{action "expand"}} class="dropdown-header {{tagClass}} home">{{noTagsLabel}}</a>
{{else}}
<a href {{action "expand"}} class="dropdown-header {{tagClass}}">{{tagId}}</a>
{{/if}}
{{else}}
<a href {{action "expand"}} class="dropdown-header {{tagClass}} home">{{allTagsLabel}}</a>
{{/if}}
{{#if tags}}
<a href {{action "expand"}} class={{dropdownButtonClass}}>
{{d-icon expandedIcon}}
</a>
<section class="{{unless expanded 'hidden'}} category-dropdown-menu chooser">
<div class='cat'><a href={{allTagsUrl}} data-drop-close="true" class='dropdown-header home'>{{allTagsLabel}}</a></div>
<div class='cat'><a href={{noTagsUrl}} data-drop-close="true" class='dropdown-header home'>{{noTagsLabel}}</a></div>
{{#if renderTags}}
{{#each tags as |t|}}
<div class='cat'>
{{tag-drop-link tagId=t category=currentCategory}}
</div>
{{/each}}
{{/if}}
</section>
{{/if}}
{{/if}}

View File

@ -4,6 +4,9 @@
{{#if category}}
{{category-title-link category=category}}
{{/if}}
{{#if tagGroupName}}
<h3>{{tagGroupName}}</h3>
{{/if}}
{{#each sortedTags as |tag|}}
<div class='tag-box'>
{{#if tag.count}}

View File

@ -50,8 +50,8 @@
disabled=inviteDisabled}}
{{/if}}
{{#if topic.isPrivateMessage}}
{{d-button class="standard"
{{#if canArchive}}
{{d-button class="standard archive-topic"
title=archiveTitle
label=archiveLabel
icon=archiveIcon
@ -79,7 +79,9 @@
{{pinned-button pinned=topic.pinned topic=topic}}
</div>
{{topic-notifications-button notificationLevel=topic.details.notification_level topic=topic}}
{{#if showNotificationsButton}}
{{topic-notifications-button notificationLevel=topic.details.notification_level topic=topic}}
{{/if}}
{{plugin-outlet name="after-topic-footer-buttons"
args=(hash topic=topic)

View File

@ -1,3 +1,4 @@
<label class="control-label">{{{field.name}}}</label>
<div class='controls'>
<label class="control-label checkbox-label">{{input checked=value type="checkbox"}} {{{field.description}}}</label>
</div>

View File

@ -1,6 +1,8 @@
<span class='value'>
{{#if isNumber}}
{{number value}}
{{else if isDuration}}
{{format-duration value}}
{{else}}
{{value}}
{{/if}}

View File

@ -85,6 +85,7 @@
popupMenuOptions=popupMenuOptions
draftStatus=model.draftStatus
isUploading=isUploading
allowUpload=allowUpload
isCancellable=isCancellable
uploadProgress=uploadProgress
groupsMentioned="groupsMentioned"
@ -131,7 +132,9 @@
<div class="composer-bottom-right">
{{#if site.mobileView}}
<a class="mobile-file-upload {{if isUploading 'hidden'}}">{{i18n 'upload'}}</a>
{{#if allowUpload}}
<a class="mobile-file-upload {{if isUploading 'hidden'}}">{{i18n 'upload'}}</a>
{{/if}}
{{#if showPreview}}
{{d-button action='togglePreview' class='hide-preview' label='composer.hide_preview'}}

View File

@ -1,5 +1,5 @@
{{#if bulkSelectEnabled}}
<td class='star'>
<td class="bulk-select">
<input type="checkbox" class="bulk-select">
</td>
{{/if}}

View File

@ -7,5 +7,5 @@
{{user-stat value=item.posts_read label="directory.posts_read"}}
{{user-stat value=item.days_visited label="directory.days_visited"}}
{{#if showTimeRead}}
<div class='time-read'>{{unbound item.time_read}}</div>
{{user-stat value=item.time_read label="directory.time_read" type="duration"}}
{{/if}}

View File

@ -0,0 +1,15 @@
{{#d-modal-body class='grant-badge'}}
{{#conditional-loading-spinner condition=loading}}
{{#if noGrantableBadges}}
<p>{{i18n 'admin.badges.no_badges'}}</p>
{{else}}
<p>{{combo-box filterable=true value=selectedBadgeId content=grantableBadges none="badges.none"}}</p>
{{/if}}
{{/conditional-loading-spinner}}
{{/d-modal-body}}
<div class="modal-footer">
<button class='btn btn-primary' disabled={{buttonDisabled}} {{action "grantBadge"}}>
{{i18n 'admin.badges.grant'}}
</button>
</div>

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