Version bump
This commit is contained in:
commit
449d21b88c
13
.eslintrc
13
.eslintrc
@ -16,14 +16,11 @@
|
||||
"_": true,
|
||||
"andThen": true,
|
||||
"asyncRender": true,
|
||||
"asyncTestDiscourse": true,
|
||||
"Blob": true,
|
||||
"bootbox": true,
|
||||
"click": true,
|
||||
"waitUntil": true,
|
||||
"getSettledState": true,
|
||||
"collapseSelectKit": true,
|
||||
"controllerFor": true,
|
||||
"count": true,
|
||||
"currentPath": true,
|
||||
"currentRouteName": true,
|
||||
@ -32,11 +29,9 @@
|
||||
"Discourse": true,
|
||||
"Ember": true,
|
||||
"exists": true,
|
||||
"expandSelectKit": true,
|
||||
"File": true,
|
||||
"fillIn": true,
|
||||
"find": true,
|
||||
"fixture": true,
|
||||
"Handlebars": true,
|
||||
"hasModule": true,
|
||||
"I18n": true,
|
||||
@ -53,14 +48,6 @@
|
||||
"requirejs": true,
|
||||
"RSVP": true,
|
||||
"sandbox": true,
|
||||
"selectKit": true,
|
||||
"selectKitFillInFilter": true,
|
||||
"selectKitSelectNoneRow": true,
|
||||
"selectKitSelectRowByIndex": true,
|
||||
"selectKitSelectRowByName": true,
|
||||
"selectKitSelectRowByValue": true,
|
||||
"setTextareaSelection": true,
|
||||
"getTextareaSelection": true,
|
||||
"sinon": true,
|
||||
"test": true,
|
||||
"triggerEvent": true,
|
||||
|
||||
6
Gemfile
6
Gemfile
@ -46,7 +46,7 @@ gem 'redis-namespace'
|
||||
|
||||
gem 'active_model_serializers', '~> 0.8.3'
|
||||
|
||||
gem 'onebox', '1.8.90'
|
||||
gem 'onebox', '1.8.92'
|
||||
|
||||
gem 'http_accept_language', '~>2.0.5', require: false
|
||||
|
||||
@ -89,8 +89,7 @@ gem 'omniauth-github'
|
||||
|
||||
gem 'omniauth-oauth2', require: false
|
||||
|
||||
# pinned until we test verified email change in the gem
|
||||
gem 'omniauth-google-oauth2', '0.6.0'
|
||||
gem 'omniauth-google-oauth2'
|
||||
|
||||
gem 'oj'
|
||||
gem 'pg'
|
||||
@ -145,6 +144,7 @@ group :test, :development do
|
||||
gem 'byebug', require: ENV['RM_INFO'].nil?
|
||||
gem 'rubocop', require: false
|
||||
gem 'parallel_tests'
|
||||
gem 'diffy', require: false
|
||||
end
|
||||
|
||||
group :development do
|
||||
|
||||
14
Gemfile.lock
14
Gemfile.lock
@ -90,6 +90,7 @@ GEM
|
||||
crass (1.0.4)
|
||||
debug_inspector (0.0.3)
|
||||
diff-lcs (1.3)
|
||||
diffy (3.3.0)
|
||||
discourse-ember-source (3.8.0.1)
|
||||
discourse_image_optim (0.26.2)
|
||||
exifr (~> 1.2, >= 1.2.2)
|
||||
@ -151,7 +152,7 @@ GEM
|
||||
railties (>= 4.2.0)
|
||||
thor (>= 0.14, < 2.0)
|
||||
json (2.2.0)
|
||||
jwt (2.1.0)
|
||||
jwt (2.2.1)
|
||||
kgio (2.11.2)
|
||||
libv8 (7.3.492.27.1)
|
||||
listen (3.1.5)
|
||||
@ -196,7 +197,7 @@ GEM
|
||||
msgpack (1.2.10)
|
||||
multi_json (1.13.1)
|
||||
multi_xml (0.6.0)
|
||||
multipart-post (2.0.0)
|
||||
multipart-post (2.1.1)
|
||||
mustache (1.1.0)
|
||||
nokogiri (1.10.3)
|
||||
mini_portile2 (~> 2.4.0)
|
||||
@ -218,7 +219,7 @@ GEM
|
||||
omniauth-github (1.3.0)
|
||||
omniauth (~> 1.5)
|
||||
omniauth-oauth2 (>= 1.4.0, < 2.0)
|
||||
omniauth-google-oauth2 (0.6.0)
|
||||
omniauth-google-oauth2 (0.7.0)
|
||||
jwt (>= 2.0)
|
||||
omniauth (>= 1.1.1)
|
||||
omniauth-oauth2 (>= 1.5)
|
||||
@ -237,7 +238,7 @@ GEM
|
||||
omniauth-twitter (1.4.0)
|
||||
omniauth-oauth (~> 1.1)
|
||||
rack
|
||||
onebox (1.8.90)
|
||||
onebox (1.8.92)
|
||||
htmlentities (~> 4.3)
|
||||
moneta (~> 1.0)
|
||||
multi_json (~> 1.11)
|
||||
@ -435,6 +436,7 @@ DEPENDENCIES
|
||||
certified
|
||||
colored2
|
||||
cppjieba_rb
|
||||
diffy
|
||||
discourse-ember-source (~> 3.8.0)
|
||||
discourse_image_optim
|
||||
email_reply_trimmer (~> 0.1)
|
||||
@ -479,12 +481,12 @@ DEPENDENCIES
|
||||
omniauth
|
||||
omniauth-facebook
|
||||
omniauth-github
|
||||
omniauth-google-oauth2 (= 0.6.0)
|
||||
omniauth-google-oauth2
|
||||
omniauth-instagram
|
||||
omniauth-oauth2
|
||||
omniauth-openid
|
||||
omniauth-twitter
|
||||
onebox (= 1.8.90)
|
||||
onebox (= 1.8.92)
|
||||
openid-redis-store
|
||||
parallel_tests
|
||||
pg
|
||||
|
||||
@ -31,6 +31,21 @@ export default Ember.Component.extend({
|
||||
return reportTotal && total && twoColumns;
|
||||
},
|
||||
|
||||
@computed("model.{average,data}", "totalsForSample.1.value", "twoColumns")
|
||||
showAverage(model, sampleTotalValue, hasTwoColumns) {
|
||||
return (
|
||||
model.average &&
|
||||
model.data.length > 0 &&
|
||||
sampleTotalValue &&
|
||||
hasTwoColumns
|
||||
);
|
||||
},
|
||||
|
||||
@computed("totalsForSample.1.value", "model.data.length")
|
||||
averageForSample(totals, count) {
|
||||
return (totals / count).toFixed(0);
|
||||
},
|
||||
|
||||
@computed("model.data.length")
|
||||
showSortingUI(dataLength) {
|
||||
return dataLength >= 5;
|
||||
|
||||
@ -1,51 +1,63 @@
|
||||
import { exportEntity } from "discourse/lib/export-csv";
|
||||
import { outputExportResult } from "discourse/lib/export-result";
|
||||
import StaffActionLog from "admin/models/staff-action-log";
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import {
|
||||
default as computed,
|
||||
on
|
||||
} from "ember-addons/ember-computed-decorators";
|
||||
|
||||
export default Ember.Controller.extend({
|
||||
loading: false,
|
||||
filters: null,
|
||||
userHistoryActions: [],
|
||||
model: null,
|
||||
nextPage: 0,
|
||||
lastPage: null,
|
||||
|
||||
filtersExists: Ember.computed.gt("filterCount", 0),
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
|
||||
this.userHistoryActions = [];
|
||||
},
|
||||
|
||||
filterActionIdChanged: function() {
|
||||
const filterActionId = this.filterActionId;
|
||||
if (filterActionId) {
|
||||
this._changeFilters({
|
||||
action_name: filterActionId,
|
||||
action_id: this.userHistoryActions.findBy("id", filterActionId)
|
||||
.action_id
|
||||
});
|
||||
}
|
||||
}.observes("filterActionId"),
|
||||
showTable: Ember.computed.gt("model.length", 0),
|
||||
|
||||
@computed("filters.action_name")
|
||||
actionFilter(name) {
|
||||
if (name) {
|
||||
return I18n.t("admin.logs.staff_actions.actions." + name);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
return name ? I18n.t("admin.logs.staff_actions.actions." + name) : null;
|
||||
},
|
||||
|
||||
showInstructions: Ember.computed.gt("model.length", 0),
|
||||
@on("init")
|
||||
resetFilters() {
|
||||
this.setProperties({
|
||||
filters: Ember.Object.create(),
|
||||
model: [],
|
||||
nextPage: 0,
|
||||
lastPage: null
|
||||
});
|
||||
this.scheduleRefresh();
|
||||
},
|
||||
|
||||
_changeFilters(props) {
|
||||
this.filters.setProperties(props);
|
||||
this.setProperties({
|
||||
model: [],
|
||||
nextPage: 0,
|
||||
lastPage: null
|
||||
});
|
||||
this.scheduleRefresh();
|
||||
},
|
||||
|
||||
_refresh() {
|
||||
if (this.lastPage && this.nextPage >= this.lastPage) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.set("loading", true);
|
||||
|
||||
var filters = this.filters,
|
||||
params = {},
|
||||
count = 0;
|
||||
const page = this.nextPage;
|
||||
let filters = this.filters;
|
||||
let params = { page };
|
||||
let count = 0;
|
||||
|
||||
// Don't send null values
|
||||
Object.keys(filters).forEach(function(k) {
|
||||
var val = filters.get(k);
|
||||
Object.keys(filters).forEach(k => {
|
||||
let val = filters.get(k);
|
||||
if (val) {
|
||||
params[k] = val;
|
||||
count += 1;
|
||||
@ -55,42 +67,49 @@ export default Ember.Controller.extend({
|
||||
|
||||
StaffActionLog.findAll(params)
|
||||
.then(result => {
|
||||
this.set("model", result.staff_action_logs);
|
||||
this.setProperties({
|
||||
model: this.model.concat(result.staff_action_logs),
|
||||
nextPage: page + 1
|
||||
});
|
||||
|
||||
if (result.staff_action_logs.length === 0) {
|
||||
this.set("lastPage", page);
|
||||
}
|
||||
|
||||
if (this.userHistoryActions.length === 0) {
|
||||
let actionTypes = result.user_history_actions.map(action => {
|
||||
return {
|
||||
id: action.id,
|
||||
action_id: action.action_id,
|
||||
name: I18n.t("admin.logs.staff_actions.actions." + action.id),
|
||||
name_raw: action.id
|
||||
};
|
||||
});
|
||||
actionTypes = _.sortBy(actionTypes, row => row.name);
|
||||
this.set("userHistoryActions", actionTypes);
|
||||
this.set(
|
||||
"userHistoryActions",
|
||||
result.user_history_actions
|
||||
.map(action => ({
|
||||
id: action.id,
|
||||
action_id: action.action_id,
|
||||
name: I18n.t("admin.logs.staff_actions.actions." + action.id),
|
||||
name_raw: action.id
|
||||
}))
|
||||
.sort((a, b) => (a.name > b.name ? 1 : -1))
|
||||
);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
this.set("loading", false);
|
||||
});
|
||||
.finally(() => this.set("loading", false));
|
||||
},
|
||||
|
||||
scheduleRefresh() {
|
||||
Ember.run.scheduleOnce("afterRender", this, this._refresh);
|
||||
},
|
||||
|
||||
resetFilters: function() {
|
||||
this.set("filters", Ember.Object.create());
|
||||
this.scheduleRefresh();
|
||||
}.on("init"),
|
||||
|
||||
_changeFilters: function(props) {
|
||||
this.filters.setProperties(props);
|
||||
this.scheduleRefresh();
|
||||
},
|
||||
|
||||
actions: {
|
||||
clearFilter: function(key) {
|
||||
var changed = {};
|
||||
filterActionIdChanged(filterActionId) {
|
||||
if (filterActionId) {
|
||||
this._changeFilters({
|
||||
action_name: filterActionId,
|
||||
action_id: this.userHistoryActions.findBy("id", filterActionId)
|
||||
.action_id
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
clearFilter(key) {
|
||||
let changed = {};
|
||||
|
||||
// Special case, clear all action related stuff
|
||||
if (key === "actionFilter") {
|
||||
@ -109,7 +128,7 @@ export default Ember.Controller.extend({
|
||||
this.resetFilters();
|
||||
},
|
||||
|
||||
filterByAction: function(logItem) {
|
||||
filterByAction(logItem) {
|
||||
this._changeFilters({
|
||||
action_name: logItem.get("action_name"),
|
||||
action_id: logItem.get("action"),
|
||||
@ -117,20 +136,24 @@ export default Ember.Controller.extend({
|
||||
});
|
||||
},
|
||||
|
||||
filterByStaffUser: function(acting_user) {
|
||||
filterByStaffUser(acting_user) {
|
||||
this._changeFilters({ acting_user: acting_user.username });
|
||||
},
|
||||
|
||||
filterByTargetUser: function(target_user) {
|
||||
filterByTargetUser(target_user) {
|
||||
this._changeFilters({ target_user: target_user.username });
|
||||
},
|
||||
|
||||
filterBySubject: function(subject) {
|
||||
filterBySubject(subject) {
|
||||
this._changeFilters({ subject: subject });
|
||||
},
|
||||
|
||||
exportStaffActionLogs: function() {
|
||||
exportStaffActionLogs() {
|
||||
exportEntity("staff_action").then(outputExportResult);
|
||||
},
|
||||
|
||||
loadMore() {
|
||||
this._refresh();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -6,104 +6,10 @@ import {
|
||||
observes
|
||||
} from "ember-addons/ember-computed-decorators";
|
||||
import { THEMES, COMPONENTS } from "admin/models/theme";
|
||||
import { POPULAR_THEMES } from "discourse-common/helpers/popular-themes";
|
||||
|
||||
const MIN_NAME_LENGTH = 4;
|
||||
|
||||
// TODO: use a central repository for themes/components
|
||||
const POPULAR_THEMES = [
|
||||
{
|
||||
name: "Graceful",
|
||||
value: "https://github.com/discourse/graceful",
|
||||
preview: "https://theme-creator.discourse.org/theme/awesomerobot/graceful",
|
||||
description: "A light and graceful theme for Discourse.",
|
||||
meta_url:
|
||||
"https://meta.discourse.org/t/a-graceful-theme-for-discourse/93040"
|
||||
},
|
||||
{
|
||||
name: "Material Design Theme",
|
||||
value: "https://github.com/discourse/material-design-stock-theme",
|
||||
preview: "https://newmaterial.trydiscourse.com",
|
||||
description:
|
||||
"Inspired by Material Design, this theme comes with several color palettes (incl. a dark one).",
|
||||
meta_url: "https://meta.discourse.org/t/material-design-stock-theme/47142"
|
||||
},
|
||||
{
|
||||
name: "Minima",
|
||||
value: "https://github.com/discourse/minima",
|
||||
preview: "https://theme-creator.discourse.org/theme/awesomerobot/minima",
|
||||
description: "A minimal theme with reduced UI elements and focus on text.",
|
||||
meta_url:
|
||||
"https://meta.discourse.org/t/minima-a-minimal-theme-for-discourse/108178"
|
||||
},
|
||||
{
|
||||
name: "Sam's Simple Theme",
|
||||
value: "https://github.com/discourse/discourse-simple-theme",
|
||||
preview: "https://theme-creator.discourse.org/theme/sam/simple",
|
||||
description:
|
||||
"Simplified front page design with classic colors and typography.",
|
||||
meta_url:
|
||||
"https://meta.discourse.org/t/sams-personal-minimal-topic-list-design/23552"
|
||||
},
|
||||
{
|
||||
name: "Vincent",
|
||||
value: "https://github.com/discourse/discourse-vincent-theme",
|
||||
preview: "https://theme-creator.discourse.org/theme/awesomerobot/vincent",
|
||||
description: "An elegant dark theme with a few color palettes.",
|
||||
meta_url: "https://meta.discourse.org/t/discourse-vincent-theme/76662"
|
||||
},
|
||||
{
|
||||
name: "Alternative Logos",
|
||||
value: "https://github.com/discourse/discourse-alt-logo",
|
||||
description: "Add alternative logos for dark / light themes.",
|
||||
meta_url:
|
||||
"https://meta.discourse.org/t/alternative-logo-for-dark-themes/88502",
|
||||
component: true
|
||||
},
|
||||
{
|
||||
name: "Brand Header Theme Component",
|
||||
value: "https://github.com/discourse/discourse-brand-header",
|
||||
description:
|
||||
"Add an extra top header with your logo, navigation links and social icons.",
|
||||
meta_url: "https://meta.discourse.org/t/brand-header-theme-component/77977",
|
||||
component: true
|
||||
},
|
||||
{
|
||||
name: "Custom Header Links",
|
||||
value: "https://github.com/discourse/discourse-custom-header-links",
|
||||
preview:
|
||||
"https://theme-creator.discourse.org/theme/Johani/custom-header-links",
|
||||
description: "Easily add custom text-based links to the header.",
|
||||
meta_url: "https://meta.discourse.org/t/custom-header-links/90588",
|
||||
component: true
|
||||
},
|
||||
{
|
||||
name: "Category Banners",
|
||||
value: "https://github.com/discourse/discourse-category-banners",
|
||||
preview:
|
||||
"https://theme-creator.discourse.org/theme/awesomerobot/discourse-category-banners",
|
||||
description:
|
||||
"Show banners on category pages using your existing category details.",
|
||||
meta_url: "https://meta.discourse.org/t/discourse-category-banners/86241",
|
||||
component: true
|
||||
},
|
||||
{
|
||||
name: "Hamburger Theme Selector",
|
||||
value: "https://github.com/discourse/discourse-hamburger-theme-selector",
|
||||
description:
|
||||
"Displays a theme selector in the hamburger menu provided there is more than one user-selectable theme.",
|
||||
meta_url: "https://meta.discourse.org/t/hamburger-theme-selector/61210",
|
||||
component: true
|
||||
},
|
||||
{
|
||||
name: "Header submenus",
|
||||
value: "https://github.com/discourse/discourse-header-submenus",
|
||||
preview: "https://theme-creator.discourse.org/theme/Johani/header-submenus",
|
||||
description: "Lets you build a header menu with submenus (dropdowns).",
|
||||
meta_url: "https://meta.discourse.org/t/header-submenus/94584",
|
||||
component: true
|
||||
}
|
||||
];
|
||||
|
||||
export default Ember.Controller.extend(ModalFunctionality, {
|
||||
popular: Ember.computed.equal("selection", "popular"),
|
||||
local: Ember.computed.equal("selection", "local"),
|
||||
|
||||
@ -48,6 +48,18 @@
|
||||
<td class="admin-report-table-cell number y">{{number model.total}}</td>
|
||||
</tr>
|
||||
{{/if}}
|
||||
|
||||
{{#if showAverage}}
|
||||
<tr class="total-row">
|
||||
<td colspan="2">
|
||||
{{i18n 'admin.dashboard.reports.average_for_sample'}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="admin-report-table-row">
|
||||
<td class="admin-report-table-cell date x">—</td>
|
||||
<td class="admin-report-table-cell number y">{{number averageForSample}}</td>
|
||||
</tr>
|
||||
{{/if}}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
@ -30,7 +30,7 @@
|
||||
{{/if}}
|
||||
</div>
|
||||
{{else}}
|
||||
{{i18n "admin.logs.staff_actions.filter"}} {{combo-box content=userHistoryActions value=filterActionId none="admin.logs.staff_actions.all"}}
|
||||
{{i18n "admin.logs.staff_actions.filter"}} {{combo-box content=userHistoryActions value=filterActionId none="admin.logs.staff_actions.all" onSelect=(action "filterActionIdChanged")}}
|
||||
{{/if}}
|
||||
|
||||
{{d-button class="btn-default" action=(action "exportStaffActionLogs") label="admin.export_csv.button_text" icon="download"}}
|
||||
@ -38,67 +38,71 @@
|
||||
<div class="clearfix"></div>
|
||||
|
||||
{{#staff-actions}}
|
||||
{{#conditional-loading-spinner condition=loading}}
|
||||
|
||||
<table class='table staff-logs grid'>
|
||||
{{#load-more selector=".staff-logs tr" action=(action "loadMore")}}
|
||||
{{#if showTable}}
|
||||
<table class='table staff-logs grid'>
|
||||
|
||||
<thead>
|
||||
<th>{{i18n 'admin.logs.staff_actions.staff_user'}}</th>
|
||||
<th>{{i18n 'admin.logs.action'}}</th>
|
||||
<th>{{i18n 'admin.logs.staff_actions.subject'}}</th>
|
||||
<th>{{i18n 'admin.logs.staff_actions.when'}}</th>
|
||||
<th>{{i18n 'admin.logs.staff_actions.details'}}</th>
|
||||
<th>{{i18n 'admin.logs.staff_actions.context'}}</th>
|
||||
</thead>
|
||||
<thead>
|
||||
<th>{{i18n 'admin.logs.staff_actions.staff_user'}}</th>
|
||||
<th>{{i18n 'admin.logs.action'}}</th>
|
||||
<th>{{i18n 'admin.logs.staff_actions.subject'}}</th>
|
||||
<th>{{i18n 'admin.logs.staff_actions.when'}}</th>
|
||||
<th>{{i18n 'admin.logs.staff_actions.details'}}</th>
|
||||
<th>{{i18n 'admin.logs.staff_actions.context'}}</th>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tbody>
|
||||
{{#each model as |item|}}
|
||||
<tr class='admin-list-item'>
|
||||
<td class="staff-users">
|
||||
<div class="staff-user">
|
||||
{{#if item.acting_user}}
|
||||
{{#link-to 'adminUser' item.acting_user}}{{avatar item.acting_user imageSize="tiny"}}{{/link-to}}
|
||||
<a {{action "filterByStaffUser" item.acting_user}}>{{item.acting_user.username}}</a>
|
||||
{{else}}
|
||||
<span class="deleted-user" title="{{i18n 'admin.user.deleted'}}">
|
||||
{{d-icon "far-trash-alt"}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</td>
|
||||
<td class="col value action">
|
||||
<a {{action "filterByAction" item}}>{{item.actionName}}</a>
|
||||
</td>
|
||||
<td class="col value subject">
|
||||
<div class="subject">
|
||||
|
||||
{{#each model as |item|}}
|
||||
<tr class='admin-list-item'>
|
||||
<td class="staff-users">
|
||||
<div class="staff-user">
|
||||
{{#if item.acting_user}}
|
||||
{{#link-to 'adminUser' item.acting_user}}{{avatar item.acting_user imageSize="tiny"}}{{/link-to}}
|
||||
<a {{action "filterByStaffUser" item.acting_user}}>{{item.acting_user.username}}</a>
|
||||
{{else}}
|
||||
<span class="deleted-user" title="{{i18n 'admin.user.deleted'}}">
|
||||
{{d-icon "far-trash-alt"}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</div>
|
||||
</td>
|
||||
<td class="col value action">
|
||||
<a {{action "filterByAction" item}}>{{item.actionName}}</a>
|
||||
</td>
|
||||
<td class="col value subject">
|
||||
<div class="subject">
|
||||
{{#if item.target_user}}
|
||||
{{#link-to 'adminUser' item.target_user}}{{avatar item.target_user imageSize="tiny"}}{{/link-to}}
|
||||
<a {{action "filterByTargetUser" item.target_user}}>{{item.target_user.username}}</a>
|
||||
{{/if}}
|
||||
{{#if item.subject}}
|
||||
<a {{action "filterBySubject" item.subject}} title={{item.subject}}>{{item.subject}}</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
</td>
|
||||
<td class="col value created-at">{{age-with-tooltip item.created_at}}</td>
|
||||
<td class="col value details">
|
||||
{{{item.formattedDetails}}}
|
||||
{{#if item.useCustomModalForDetails}}
|
||||
<a {{action "showCustomDetailsModal" item}}>{{d-icon "info-circle"}} {{i18n 'admin.logs.staff_actions.show'}}</a>
|
||||
{{/if}}
|
||||
{{#if item.useModalForDetails}}
|
||||
<a {{action "showDetailsModal" item}}>{{d-icon "info-circle"}} {{i18n 'admin.logs.staff_actions.show'}}</a>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class="col value context">{{item.context}}</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
|
||||
{{#if item.target_user}}
|
||||
{{#link-to 'adminUser' item.target_user}}{{avatar item.target_user imageSize="tiny"}}{{/link-to}}
|
||||
<a {{action "filterByTargetUser" item.target_user}}>{{item.target_user.username}}</a>
|
||||
{{/if}}
|
||||
{{#if item.subject}}
|
||||
<a {{action "filterBySubject" item.subject}} title={{item.subject}}>{{item.subject}}</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
</td>
|
||||
<td class="col value created-at">{{age-with-tooltip item.created_at}}</td>
|
||||
<td class="col value details">
|
||||
{{{item.formattedDetails}}}
|
||||
{{#if item.useCustomModalForDetails}}
|
||||
<a {{action "showCustomDetailsModal" item}}>{{d-icon "info-circle"}} {{i18n 'admin.logs.staff_actions.show'}}</a>
|
||||
{{/if}}
|
||||
{{#if item.useModalForDetails}}
|
||||
<a {{action "showDetailsModal" item}}>{{d-icon "info-circle"}} {{i18n 'admin.logs.staff_actions.show'}}</a>
|
||||
{{/if}}
|
||||
</td>
|
||||
<td class="col value context">{{item.context}}</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
{{i18n 'search.no_results'}}
|
||||
{{/each}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{/conditional-loading-spinner}}
|
||||
</table>
|
||||
{{else}}
|
||||
{{i18n 'search.no_results'}}
|
||||
{{/if}}
|
||||
|
||||
{{conditional-loading-spinner condition=loading}}
|
||||
{{/load-more}}
|
||||
|
||||
{{/staff-actions}}
|
||||
|
||||
@ -0,0 +1,119 @@
|
||||
export const POPULAR_THEMES = [
|
||||
{
|
||||
name: "Graceful",
|
||||
value: "https://github.com/discourse/graceful",
|
||||
preview: "https://theme-creator.discourse.org/theme/awesomerobot/graceful",
|
||||
description: "A light and graceful theme for Discourse.",
|
||||
meta_url:
|
||||
"https://meta.discourse.org/t/a-graceful-theme-for-discourse/93040"
|
||||
},
|
||||
{
|
||||
name: "Material Design Theme",
|
||||
value: "https://github.com/discourse/material-design-stock-theme",
|
||||
preview: "https://newmaterial.trydiscourse.com",
|
||||
description:
|
||||
"Inspired by Material Design, this theme comes with several color palettes (incl. a dark one).",
|
||||
meta_url: "https://meta.discourse.org/t/material-design-stock-theme/47142"
|
||||
},
|
||||
{
|
||||
name: "Minima",
|
||||
value: "https://github.com/discourse/minima",
|
||||
preview: "https://theme-creator.discourse.org/theme/awesomerobot/minima",
|
||||
description: "A minimal theme with reduced UI elements and focus on text.",
|
||||
meta_url:
|
||||
"https://meta.discourse.org/t/minima-a-minimal-theme-for-discourse/108178"
|
||||
},
|
||||
{
|
||||
name: "Sam's Simple Theme",
|
||||
value: "https://github.com/discourse/discourse-simple-theme",
|
||||
preview: "https://theme-creator.discourse.org/theme/sam/simple",
|
||||
description:
|
||||
"Simplified front page design with classic colors and typography.",
|
||||
meta_url:
|
||||
"https://meta.discourse.org/t/sams-personal-minimal-topic-list-design/23552"
|
||||
},
|
||||
{
|
||||
name: "Vincent",
|
||||
value: "https://github.com/discourse/discourse-vincent-theme",
|
||||
preview: "https://theme-creator.discourse.org/theme/awesomerobot/vincent",
|
||||
description: "An elegant dark theme with a few color palettes.",
|
||||
meta_url: "https://meta.discourse.org/t/discourse-vincent-theme/76662"
|
||||
},
|
||||
{
|
||||
name: "Brand Header",
|
||||
value: "https://github.com/discourse/discourse-brand-header",
|
||||
description:
|
||||
"Add an extra top header with your logo, navigation links and social icons.",
|
||||
meta_url: "https://meta.discourse.org/t/brand-header-theme-component/77977",
|
||||
component: true
|
||||
},
|
||||
{
|
||||
name: "Custom Header Links",
|
||||
value: "https://github.com/discourse/discourse-custom-header-links",
|
||||
preview:
|
||||
"https://theme-creator.discourse.org/theme/Johani/custom-header-links",
|
||||
description: "Easily add custom text-based links to the header.",
|
||||
meta_url: "https://meta.discourse.org/t/custom-header-links/90588",
|
||||
component: true
|
||||
},
|
||||
{
|
||||
name: "Category Banners",
|
||||
value: "https://github.com/discourse/discourse-category-banners",
|
||||
preview:
|
||||
"https://theme-creator.discourse.org/theme/awesomerobot/discourse-category-banners",
|
||||
description:
|
||||
"Show banners on category pages using your existing category details.",
|
||||
meta_url: "https://meta.discourse.org/t/discourse-category-banners/86241",
|
||||
component: true
|
||||
},
|
||||
{
|
||||
name: "Kanban Board",
|
||||
value: "https://github.com/discourse/discourse-kanban-theme",
|
||||
preview: "https://theme-creator.discourse.org/theme/david/kanban",
|
||||
description: "Display and organize topics using a Kanban board interface.",
|
||||
meta_url:
|
||||
"https://meta.discourse.org/t/kanban-board-theme-component/118164",
|
||||
component: true
|
||||
},
|
||||
{
|
||||
name: "Hamburger Theme Selector",
|
||||
value: "https://github.com/discourse/discourse-hamburger-theme-selector",
|
||||
description:
|
||||
"Displays a theme selector in the hamburger menu provided there is more than one user-selectable theme.",
|
||||
meta_url: "https://meta.discourse.org/t/hamburger-theme-selector/61210",
|
||||
component: true
|
||||
},
|
||||
{
|
||||
name: "Header Submenus",
|
||||
value: "https://github.com/discourse/discourse-header-submenus",
|
||||
preview: "https://theme-creator.discourse.org/theme/Johani/header-submenus",
|
||||
description: "Lets you build a header menu with submenus (dropdowns).",
|
||||
meta_url: "https://meta.discourse.org/t/header-submenus/94584",
|
||||
component: true
|
||||
},
|
||||
{
|
||||
name: "Alternative Logos",
|
||||
value: "https://github.com/discourse/discourse-alt-logo",
|
||||
description: "Add alternative logos for dark / light themes.",
|
||||
meta_url:
|
||||
"https://meta.discourse.org/t/alternative-logo-for-dark-themes/88502",
|
||||
component: true
|
||||
},
|
||||
{
|
||||
name: "Automatic Table of Contents",
|
||||
value: "https://github.com/discourse/DiscoTOC",
|
||||
description:
|
||||
"Generates an interactive table of contents on the sidebar of your topic with a simple click in the composer.",
|
||||
meta_url:
|
||||
"https://meta.discourse.org/t/discotoc-automatic-table-of-contents/111143",
|
||||
component: true
|
||||
},
|
||||
{
|
||||
name: "Easy Responsive Footer",
|
||||
value: "https://github.com/discourse/Discourse-easy-footer",
|
||||
preview: "https://theme-creator.discourse.org/theme/Johani/easy-footer",
|
||||
description: "Add a fully responsive footer without writing any HTML.",
|
||||
meta_url: "https://meta.discourse.org/t/easy-responsive-footer/95818",
|
||||
component: true
|
||||
}
|
||||
];
|
||||
@ -4,6 +4,7 @@ import {
|
||||
default as computed
|
||||
} from "ember-addons/ember-computed-decorators";
|
||||
import { findRawTemplate } from "discourse/lib/raw-templates";
|
||||
const { makeArray } = Ember;
|
||||
|
||||
export default Ember.Component.extend({
|
||||
@computed("placeholderKey")
|
||||
@ -13,43 +14,40 @@ export default Ember.Component.extend({
|
||||
|
||||
@observes("badgeNames")
|
||||
_update() {
|
||||
if (this.canReceiveUpdates === "true")
|
||||
if (this.canReceiveUpdates === "true") {
|
||||
this._initializeAutocomplete({ updateData: true });
|
||||
}
|
||||
},
|
||||
|
||||
@on("didInsertElement")
|
||||
_initializeAutocomplete(opts) {
|
||||
var self = this;
|
||||
var selectedBadges;
|
||||
let selectedBadges;
|
||||
|
||||
self.$("input").autocomplete({
|
||||
$(this.element.querySelector("input")).autocomplete({
|
||||
allowAny: false,
|
||||
items: _.isArray(this.badgeNames) ? this.badgeNames : [this.badgeNames],
|
||||
items: makeArray(this.badgeNames),
|
||||
single: this.single,
|
||||
updateData: opts && opts.updateData ? opts.updateData : false,
|
||||
onChangeItems: function(items) {
|
||||
template: findRawTemplate("badge-selector-autocomplete"),
|
||||
|
||||
onChangeItems(items) {
|
||||
selectedBadges = items;
|
||||
self.set("badgeNames", items.join(","));
|
||||
this.set("badgeNames", items.join(","));
|
||||
},
|
||||
transformComplete: function(g) {
|
||||
|
||||
transformComplete(g) {
|
||||
return g.name;
|
||||
},
|
||||
dataSource: function(term) {
|
||||
return self
|
||||
.get("badgeFinder")(term)
|
||||
.then(function(badges) {
|
||||
if (!selectedBadges) {
|
||||
return badges;
|
||||
}
|
||||
|
||||
return badges.filter(function(badge) {
|
||||
return !selectedBadges.any(function(s) {
|
||||
return s === badge.name;
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
template: findRawTemplate("badge-selector-autocomplete")
|
||||
dataSource(term) {
|
||||
return this.badgeFinder(term).then(badges => {
|
||||
if (!selectedBadges) return badges;
|
||||
|
||||
return badges.filter(
|
||||
badge => !selectedBadges.any(s => s === badge.name)
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -13,16 +13,19 @@ export default Ember.Component.extend(BadgeSelectController, {
|
||||
|
||||
const badge_id = this.selectedUserBadgeId || 0;
|
||||
|
||||
ajax(this.get("user.path") + "/preferences/badge_title", {
|
||||
ajax(this.currentUser.path + "/preferences/badge_title", {
|
||||
type: "PUT",
|
||||
data: { user_badge_id: badge_id }
|
||||
}).then(
|
||||
() => {
|
||||
this.setProperties({
|
||||
saved: true,
|
||||
saving: false,
|
||||
"user.title": this.get("selectedUserBadge.badge.name")
|
||||
saving: false
|
||||
});
|
||||
this.currentUser.set(
|
||||
"title",
|
||||
this.get("selectedUserBadge.badge.name")
|
||||
);
|
||||
},
|
||||
() => {
|
||||
bootbox.alert(I18n.t("generic_error"));
|
||||
|
||||
@ -694,7 +694,7 @@ export default Ember.Component.extend({
|
||||
|
||||
const matchingHandler = uploadHandlers.find(matcher);
|
||||
if (data.files.length === 1 && matchingHandler) {
|
||||
if (!matchingHandler.method(data.files[0])) {
|
||||
if (!matchingHandler.method(data.files[0], this)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -35,42 +35,13 @@ export default Ember.Component.extend({
|
||||
}
|
||||
});
|
||||
|
||||
this.appEvents.on("modal:body-shown", data => {
|
||||
if (this.isDestroying || this.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.fixed) {
|
||||
this.$().removeClass("hidden");
|
||||
}
|
||||
|
||||
if (data.title) {
|
||||
this.set("title", I18n.t(data.title));
|
||||
} else if (data.rawTitle) {
|
||||
this.set("title", data.rawTitle);
|
||||
}
|
||||
|
||||
if (data.subtitle) {
|
||||
this.set("subtitle", I18n.t(data.subtitle));
|
||||
} else if (data.rawSubtitle) {
|
||||
this.set("subtitle", data.rawSubtitle);
|
||||
} else {
|
||||
// if no subtitle provided, makes sure the previous subtitle
|
||||
// of another modal is not used
|
||||
this.set("subtitle", null);
|
||||
}
|
||||
|
||||
if ("dismissable" in data) {
|
||||
this.set("dismissable", data.dismissable);
|
||||
} else {
|
||||
this.set("dismissable", true);
|
||||
}
|
||||
});
|
||||
this.appEvents.on("modal:body-shown", this, "_modalBodyShown");
|
||||
},
|
||||
|
||||
@on("willDestroyElement")
|
||||
cleanUp() {
|
||||
$("html").off("keydown.discourse-modal");
|
||||
this.appEvents.off("modal:body-shown", this, "_modalBodyShown");
|
||||
},
|
||||
|
||||
mouseDown(e) {
|
||||
@ -87,5 +58,37 @@ export default Ember.Component.extend({
|
||||
// the backdrop and makes it unclickable.
|
||||
$(".modal-header a.close").click();
|
||||
}
|
||||
},
|
||||
|
||||
_modalBodyShown(data) {
|
||||
if (this.isDestroying || this.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.fixed) {
|
||||
this.$().removeClass("hidden");
|
||||
}
|
||||
|
||||
if (data.title) {
|
||||
this.set("title", I18n.t(data.title));
|
||||
} else if (data.rawTitle) {
|
||||
this.set("title", data.rawTitle);
|
||||
}
|
||||
|
||||
if (data.subtitle) {
|
||||
this.set("subtitle", I18n.t(data.subtitle));
|
||||
} else if (data.rawSubtitle) {
|
||||
this.set("subtitle", data.rawSubtitle);
|
||||
} else {
|
||||
// if no subtitle provided, makes sure the previous subtitle
|
||||
// of another modal is not used
|
||||
this.set("subtitle", null);
|
||||
}
|
||||
|
||||
if ("dismissable" in data) {
|
||||
this.set("dismissable", data.dismissable);
|
||||
} else {
|
||||
this.set("dismissable", true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -8,18 +8,31 @@ import {
|
||||
export default Ember.Component.extend({
|
||||
classNames: ["date-picker-wrapper"],
|
||||
_picker: null,
|
||||
value: null,
|
||||
|
||||
@computed("site.mobileView")
|
||||
inputType(mobileView) {
|
||||
return mobileView ? "date" : "text";
|
||||
},
|
||||
|
||||
@on("didInsertElement")
|
||||
_loadDatePicker() {
|
||||
const input = this.$(".date-picker")[0];
|
||||
const container = $("#" + this.containerId)[0];
|
||||
const container = this.element.querySelector(`#${this.containerId}`);
|
||||
|
||||
if (this.site.mobileView) {
|
||||
this._loadNativePicker(container);
|
||||
} else {
|
||||
this._loadPikadayPicker(container);
|
||||
}
|
||||
},
|
||||
|
||||
_loadPikadayPicker(container) {
|
||||
loadScript("/javascripts/pikaday.js").then(() => {
|
||||
Ember.run.next(() => {
|
||||
let default_opts = {
|
||||
field: input,
|
||||
container: container || this.$()[0],
|
||||
bound: container === undefined,
|
||||
const default_opts = {
|
||||
field: this.element.querySelector(".date-picker"),
|
||||
container: container || this.element,
|
||||
bound: container === null,
|
||||
format: "YYYY-MM-DD",
|
||||
firstDay: 1,
|
||||
i18n: {
|
||||
@ -29,24 +42,39 @@ export default Ember.Component.extend({
|
||||
weekdays: moment.weekdays(),
|
||||
weekdaysShort: moment.weekdaysShort()
|
||||
},
|
||||
onSelect: date => {
|
||||
const formattedDate = moment(date).format("YYYY-MM-DD");
|
||||
|
||||
if (this.attrs.onSelect) {
|
||||
this.attrs.onSelect(formattedDate);
|
||||
}
|
||||
|
||||
if (!this.element || this.isDestroying || this.isDestroyed) return;
|
||||
|
||||
this.set("value", formattedDate);
|
||||
}
|
||||
onSelect: date => this._handleSelection(date)
|
||||
};
|
||||
|
||||
this._picker = new Pikaday(_.merge(default_opts, this._opts()));
|
||||
this._picker = new Pikaday(Object.assign(default_opts, this._opts()));
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
_loadNativePicker(container) {
|
||||
const wrapper = container || this.element;
|
||||
const picker = wrapper.querySelector("input.date-picker");
|
||||
picker.onchange = () => this._handleSelection(picker.value);
|
||||
picker.hide = () => {
|
||||
/* do nothing for native */
|
||||
};
|
||||
picker.destroy = () => {
|
||||
/* do nothing for native */
|
||||
};
|
||||
this._picker = picker;
|
||||
},
|
||||
|
||||
_handleSelection(value) {
|
||||
const formattedDate = moment(value).format("YYYY-MM-DD");
|
||||
|
||||
if (!this.element || this.isDestroying || this.isDestroyed) return;
|
||||
|
||||
this._picker && this._picker.hide();
|
||||
|
||||
if (this.onSelect) {
|
||||
this.onSelect(formattedDate);
|
||||
}
|
||||
},
|
||||
|
||||
@on("willDestroyElement")
|
||||
_destroy() {
|
||||
if (this._picker) {
|
||||
|
||||
@ -3,7 +3,6 @@ import {
|
||||
observes
|
||||
} from "ember-addons/ember-computed-decorators";
|
||||
import { FORMAT } from "select-kit/components/future-date-input-selector";
|
||||
|
||||
import { PUBLISH_TO_CATEGORY_STATUS_TYPE } from "discourse/controllers/edit-topic-timer";
|
||||
|
||||
export default Ember.Component.extend({
|
||||
@ -22,26 +21,37 @@ export default Ember.Component.extend({
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
|
||||
const input = this.input;
|
||||
|
||||
if (input) {
|
||||
if (this.input) {
|
||||
if (this.basedOnLastPost) {
|
||||
this.set("selection", "set_based_on_last_post");
|
||||
} else {
|
||||
this.set("selection", "pick_date_and_time");
|
||||
const datetime = moment(input);
|
||||
this.set("date", datetime.toDate());
|
||||
this.set("time", datetime.format("HH:mm"));
|
||||
const datetime = moment(this.input);
|
||||
this.setProperties({
|
||||
selection: "pick_date_and_time",
|
||||
date: datetime.format("YYYY-MM-DD"),
|
||||
time: datetime.format("HH:mm")
|
||||
});
|
||||
this._updateInput();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
timeInputDisabled: Ember.computed.empty("date"),
|
||||
|
||||
@observes("date", "time")
|
||||
_updateInput() {
|
||||
const date = moment(this.date).format("YYYY-MM-DD");
|
||||
const time = (this.time && ` ${this.time}`) || "";
|
||||
this.set("input", moment(`${date}${time}`).format(FORMAT));
|
||||
if (!this.date) {
|
||||
this.set("time", null);
|
||||
}
|
||||
|
||||
const time = this.time ? ` ${this.time}` : "";
|
||||
const dateTime = moment(`${this.date}${time}`);
|
||||
|
||||
if (dateTime.isValid()) {
|
||||
this.set("input", dateTime.format(FORMAT));
|
||||
} else {
|
||||
this.set("input", null);
|
||||
}
|
||||
},
|
||||
|
||||
@observes("isBasedOnLastPost")
|
||||
@ -72,6 +82,8 @@ export default Ember.Component.extend({
|
||||
},
|
||||
|
||||
didReceiveAttrs() {
|
||||
this._super(...arguments);
|
||||
|
||||
if (this.label) this.set("displayLabel", I18n.t(this.label));
|
||||
},
|
||||
|
||||
|
||||
@ -87,18 +87,36 @@ export default Ember.Component.extend(
|
||||
|
||||
@on("didInsertElement")
|
||||
_setupLogsNotice() {
|
||||
LogsNotice.current().addObserver("hidden", () => {
|
||||
this.rerenderBuffer();
|
||||
});
|
||||
this._boundRerenderBuffer = Ember.run.bind(this, this.rerenderBuffer);
|
||||
LogsNotice.current().addObserver("hidden", this._boundRerenderBuffer);
|
||||
|
||||
this.$().on("click.global-notice", ".alert-logs-notice .close", () => {
|
||||
LogsNotice.currentProp("text", "");
|
||||
});
|
||||
this._boundResetCurrentProp = Ember.run.bind(
|
||||
this,
|
||||
this._resetCurrentProp
|
||||
);
|
||||
$(this.element).on(
|
||||
"click.global-notice",
|
||||
".alert-logs-notice .close",
|
||||
this._boundResetCurrentProp
|
||||
);
|
||||
},
|
||||
|
||||
@on("willDestroyElement")
|
||||
_teardownLogsNotice() {
|
||||
this.$().off("click.global-notice");
|
||||
if (this._boundResetCurrentProp) {
|
||||
$(this.element).off("click.global-notice", this._boundResetCurrentProp);
|
||||
}
|
||||
|
||||
if (this._boundRerenderBuffer) {
|
||||
LogsNotice.current().removeObserver(
|
||||
"hidden",
|
||||
this._boundRerenderBuffer
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
_resetCurrentProp() {
|
||||
LogsNotice.currentProp("text", "");
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@ -61,7 +61,7 @@ export default Ember.Component.extend({
|
||||
willDestroyElement() {
|
||||
this._dispatched.forEach(evt => {
|
||||
const [eventName, caller] = evt;
|
||||
this.appEvents.off(eventName, caller);
|
||||
this.appEvents.off(eventName, this, caller);
|
||||
});
|
||||
Ember.run.cancel(this._timeout);
|
||||
},
|
||||
@ -84,7 +84,7 @@ export default Ember.Component.extend({
|
||||
const caller = refreshArg =>
|
||||
this.eventDispatched(eventName, key, refreshArg);
|
||||
this._dispatched.push([eventName, caller]);
|
||||
this.appEvents.on(eventName, caller);
|
||||
this.appEvents.on(eventName, this, caller);
|
||||
},
|
||||
|
||||
queueRerender(callback) {
|
||||
|
||||
@ -4,6 +4,8 @@ import TextField from "discourse/components/text-field";
|
||||
import { applySearchAutocomplete } from "discourse/lib/search";
|
||||
|
||||
export default TextField.extend({
|
||||
autocomplete: "discourse",
|
||||
|
||||
@computed("searchService.searchContextEnabled")
|
||||
placeholder(searchContextEnabled) {
|
||||
return searchContextEnabled ? "" : I18n.t("search.full_page_title");
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import { wantsNewWindow } from "discourse/lib/intercept-click";
|
||||
import { longDateNoYear } from "discourse/lib/formatter";
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import {
|
||||
default as computed,
|
||||
on
|
||||
} from "ember-addons/ember-computed-decorators";
|
||||
import Sharing from "discourse/lib/sharing";
|
||||
import { nativeShare } from "discourse/lib/pwa-utils";
|
||||
|
||||
@ -148,19 +151,24 @@ export default Ember.Component.extend({
|
||||
this._showUrl($target, url);
|
||||
},
|
||||
|
||||
@on("init")
|
||||
_setupHandlers() {
|
||||
this._boundMouseDownHandler = Ember.run.bind(this, this._mouseDownHandler);
|
||||
this._boundClickHandler = Ember.run.bind(this, this._clickHandler);
|
||||
this._boundKeydownHandler = Ember.run.bind(this, this._keydownHandler);
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
const $html = $("html");
|
||||
$html.on("mousedown.outside-share-link", this._mouseDownHandler.bind(this));
|
||||
|
||||
$html.on(
|
||||
"click.discourse-share-link",
|
||||
"button[data-share-url], .post-info .post-date[data-share-url]",
|
||||
this._clickHandler.bind(this)
|
||||
);
|
||||
|
||||
$html.on("keydown.share-view", this._keydownHandler);
|
||||
$("html")
|
||||
.on("mousedown.outside-share-link", this._boundMouseDownHandler)
|
||||
.on(
|
||||
"click.discourse-share-link",
|
||||
"button[data-share-url], .post-info .post-date[data-share-url]",
|
||||
this._boundClickHandler
|
||||
)
|
||||
.on("keydown.share-view", this._boundKeydownHandler);
|
||||
|
||||
this.appEvents.on("share:url", this._shareUrlHandler);
|
||||
},
|
||||
@ -169,9 +177,9 @@ export default Ember.Component.extend({
|
||||
this._super(...arguments);
|
||||
|
||||
$("html")
|
||||
.off("click.discourse-share-link", this._clickHandler)
|
||||
.off("mousedown.outside-share-link", this._mouseDownHandler)
|
||||
.off("keydown.share-view", this._keydownHandler);
|
||||
.off("click.discourse-share-link", this._boundClickHandler)
|
||||
.off("mousedown.outside-share-link", this._boundMouseDownHandler)
|
||||
.off("keydown.share-view", this._boundKeydownHandler);
|
||||
|
||||
this.appEvents.off("share:url", this._shareUrlHandler);
|
||||
},
|
||||
|
||||
@ -58,7 +58,7 @@ export default Ember.Component.extend(CleansUp, {
|
||||
|
||||
_setCSS() {
|
||||
const pos = this._position;
|
||||
const $self = this.$();
|
||||
const $self = $(this.element);
|
||||
const width = $self.width();
|
||||
const height = $self.height();
|
||||
pos.left = parseInt(pos.left) - width / 2;
|
||||
@ -74,8 +74,7 @@ export default Ember.Component.extend(CleansUp, {
|
||||
_show(data) {
|
||||
this._position = data.position;
|
||||
|
||||
this.set("topic", data.topic);
|
||||
this.set("visible", true);
|
||||
this.setProperties({ topic: data.topic, visible: true });
|
||||
|
||||
Ember.run.scheduleOnce("afterRender", this, this._setCSS);
|
||||
|
||||
@ -85,7 +84,7 @@ export default Ember.Component.extend(CleansUp, {
|
||||
const $target = $(e.target);
|
||||
if (
|
||||
$target.prop("id") === "topic-entrance" ||
|
||||
this.$().has($target).length !== 0
|
||||
$(this.element).has($target).length !== 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@ -94,8 +93,7 @@ export default Ember.Component.extend(CleansUp, {
|
||||
},
|
||||
|
||||
cleanUp() {
|
||||
this.set("topic", null);
|
||||
this.set("visible", false);
|
||||
this.setProperties({ topic: null, visible: false });
|
||||
$("html").off("mousedown.topic-entrance");
|
||||
},
|
||||
|
||||
|
||||
@ -32,6 +32,8 @@ export default Ember.Component.extend({
|
||||
|
||||
canInviteTo: Ember.computed.alias("topic.details.can_invite_to"),
|
||||
|
||||
canDefer: Ember.computed.alias("currentUser.enable_defer"),
|
||||
|
||||
inviteDisabled: Ember.computed.or(
|
||||
"topic.archived",
|
||||
"topic.closed",
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import computed from "ember-addons/ember-computed-decorators";
|
||||
import {
|
||||
on,
|
||||
default as computed
|
||||
} from "ember-addons/ember-computed-decorators";
|
||||
|
||||
var ButtonBackBright = {
|
||||
const ButtonBackBright = {
|
||||
classes: "btn-primary",
|
||||
action: "back",
|
||||
key: "errors.buttons.back"
|
||||
@ -28,11 +31,13 @@ export default Ember.Controller.extend({
|
||||
lastTransition: null,
|
||||
|
||||
@computed
|
||||
isNetwork: function() {
|
||||
isNetwork() {
|
||||
// never made it on the wire
|
||||
if (this.get("thrown.readyState") === 0) return true;
|
||||
|
||||
// timed out
|
||||
if (this.get("thrown.jqTextStatus") === "timeout") return true;
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
@ -47,9 +52,10 @@ export default Ember.Controller.extend({
|
||||
networkFixed: false,
|
||||
loading: false,
|
||||
|
||||
_init: function() {
|
||||
@on("init")
|
||||
_init() {
|
||||
this.set("loading", false);
|
||||
}.on("init"),
|
||||
},
|
||||
|
||||
@computed("isNetwork", "isServer", "isUnknown")
|
||||
reason() {
|
||||
@ -99,16 +105,16 @@ export default Ember.Controller.extend({
|
||||
},
|
||||
|
||||
actions: {
|
||||
back: function() {
|
||||
back() {
|
||||
window.history.back();
|
||||
},
|
||||
|
||||
tryLoading: function() {
|
||||
tryLoading() {
|
||||
this.set("loading", true);
|
||||
var self = this;
|
||||
Ember.run.schedule("afterRender", function() {
|
||||
self.get("lastTransition").retry();
|
||||
self.set("loading", false);
|
||||
|
||||
Ember.run.schedule("afterRender", () => {
|
||||
this.lastTransition.retry();
|
||||
this.set("loading", false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ import { iconHTML } from "discourse-common/lib/icon-library";
|
||||
import CanCheckEmails from "discourse/mixins/can-check-emails";
|
||||
import { default as computed } from "ember-addons/ember-computed-decorators";
|
||||
import PreferencesTabController from "discourse/mixins/preferences-tab-controller";
|
||||
import { setting } from "discourse/lib/computed";
|
||||
import { propertyNotEqual, setting } from "discourse/lib/computed";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import showModal from "discourse/lib/show-modal";
|
||||
import { findAll } from "discourse/models/login-method";
|
||||
@ -40,9 +40,7 @@ export default Ember.Controller.extend(
|
||||
),
|
||||
|
||||
reset() {
|
||||
this.setProperties({
|
||||
passwordProgress: null
|
||||
});
|
||||
this.set("passwordProgress", null);
|
||||
},
|
||||
|
||||
@computed()
|
||||
@ -54,10 +52,7 @@ export default Ember.Controller.extend(
|
||||
);
|
||||
},
|
||||
|
||||
@computed("model.availableTitles")
|
||||
canSelectTitle(availableTitles) {
|
||||
return availableTitles.length > 0;
|
||||
},
|
||||
canSelectTitle: Ember.computed.gt("model.availableTitles.length", 0),
|
||||
|
||||
@computed("model.is_anonymous")
|
||||
canChangePassword(isAnonymous) {
|
||||
@ -86,15 +81,10 @@ export default Ember.Controller.extend(
|
||||
};
|
||||
});
|
||||
|
||||
return result.filter(value => {
|
||||
return value.account || value.method.get("can_connect");
|
||||
});
|
||||
return result.filter(value => value.account || value.method.can_connect);
|
||||
},
|
||||
|
||||
@computed("model.id")
|
||||
disableConnectButtons(userId) {
|
||||
return userId !== this.get("currentUser.id");
|
||||
},
|
||||
disableConnectButtons: propertyNotEqual("model.id", "currentUser.id"),
|
||||
|
||||
@computed(
|
||||
"model.second_factor_enabled",
|
||||
@ -129,25 +119,23 @@ export default Ember.Controller.extend(
|
||||
: tokens.slice(0, DEFAULT_AUTH_TOKENS_COUNT);
|
||||
},
|
||||
|
||||
@computed("model.user_auth_tokens")
|
||||
canShowAllAuthTokens(tokens) {
|
||||
return tokens.length > DEFAULT_AUTH_TOKENS_COUNT;
|
||||
},
|
||||
canShowAllAuthTokens: Ember.computed.gt(
|
||||
"model.user_auth_tokens.length",
|
||||
DEFAULT_AUTH_TOKENS_COUNT
|
||||
),
|
||||
|
||||
actions: {
|
||||
save() {
|
||||
this.set("saved", false);
|
||||
|
||||
const model = this.model;
|
||||
this.model.setProperties({
|
||||
name: this.newNameInput,
|
||||
title: this.newTitleInput
|
||||
});
|
||||
|
||||
model.set("name", this.newNameInput);
|
||||
model.set("title", this.newTitleInput);
|
||||
|
||||
return model
|
||||
return this.model
|
||||
.save(this.saveAttrNames)
|
||||
.then(() => {
|
||||
this.set("saved", true);
|
||||
})
|
||||
.then(() => this.set("saved", true))
|
||||
.catch(popupAjaxError);
|
||||
},
|
||||
|
||||
@ -178,17 +166,14 @@ export default Ember.Controller.extend(
|
||||
|
||||
delete() {
|
||||
this.set("deleting", true);
|
||||
const self = this,
|
||||
message = I18n.t("user.delete_account_confirm"),
|
||||
const message = I18n.t("user.delete_account_confirm"),
|
||||
model = this.model,
|
||||
buttons = [
|
||||
{
|
||||
label: I18n.t("cancel"),
|
||||
class: "d-modal-cancel",
|
||||
link: true,
|
||||
callback: () => {
|
||||
this.set("deleting", false);
|
||||
}
|
||||
callback: () => this.set("deleting", false)
|
||||
},
|
||||
{
|
||||
label:
|
||||
@ -197,14 +182,15 @@ export default Ember.Controller.extend(
|
||||
class: "btn btn-danger",
|
||||
callback() {
|
||||
model.delete().then(
|
||||
function() {
|
||||
bootbox.alert(I18n.t("user.deleted_yourself"), function() {
|
||||
window.location.pathname = Discourse.getURL("/");
|
||||
});
|
||||
() => {
|
||||
bootbox.alert(
|
||||
I18n.t("user.deleted_yourself"),
|
||||
() => (window.location.pathname = Discourse.getURL("/"))
|
||||
);
|
||||
},
|
||||
function() {
|
||||
() => {
|
||||
bootbox.alert(I18n.t("user.delete_yourself_not_allowed"));
|
||||
self.set("deleting", false);
|
||||
this.set("deleting", false);
|
||||
}
|
||||
);
|
||||
}
|
||||
@ -214,25 +200,23 @@ export default Ember.Controller.extend(
|
||||
},
|
||||
|
||||
revokeAccount(account) {
|
||||
const model = this.model;
|
||||
this.set("revoking", true);
|
||||
model
|
||||
|
||||
this.model
|
||||
.revokeAssociatedAccount(account.name)
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
model.get("associated_accounts").removeObject(account);
|
||||
this.model.associated_accounts.removeObject(account);
|
||||
} else {
|
||||
bootbox.alert(result.message);
|
||||
}
|
||||
})
|
||||
.catch(popupAjaxError)
|
||||
.finally(() => {
|
||||
this.set("revoking", false);
|
||||
});
|
||||
.finally(() => this.set("revoking", false));
|
||||
},
|
||||
|
||||
toggleShowAllAuthTokens() {
|
||||
this.set("showAllAuthTokens", !this.showAllAuthTokens);
|
||||
this.toggleProperty("showAllAuthTokens");
|
||||
},
|
||||
|
||||
revokeAuthToken(token) {
|
||||
|
||||
@ -29,6 +29,11 @@ export default Ember.Controller.extend(PreferencesTabController, {
|
||||
return this.get("currentUser.id") === this.get("model.id");
|
||||
},
|
||||
|
||||
@computed("siteSettings.remove_muted_tags_from_latest")
|
||||
hideMutedTags() {
|
||||
return this.siteSettings.remove_muted_tags_from_latest !== "never";
|
||||
},
|
||||
|
||||
canSave: Ember.computed.or("canSee", "currentUser.admin"),
|
||||
|
||||
actions: {
|
||||
|
||||
@ -31,6 +31,7 @@ export default Ember.Controller.extend(PreferencesTabController, {
|
||||
"external_links_in_new_tab",
|
||||
"dynamic_favicon",
|
||||
"enable_quoting",
|
||||
"enable_defer",
|
||||
"automatically_unpin_topics",
|
||||
"allow_private_messages",
|
||||
"homepage_id",
|
||||
|
||||
@ -7,7 +7,8 @@ export default Ember.Controller.extend({
|
||||
"status",
|
||||
"category_id",
|
||||
"topic_id",
|
||||
"username"
|
||||
"username",
|
||||
"sort_order"
|
||||
],
|
||||
type: null,
|
||||
status: "pending",
|
||||
@ -17,6 +18,7 @@ export default Ember.Controller.extend({
|
||||
topic_id: null,
|
||||
filtersExpanded: false,
|
||||
username: "",
|
||||
sort_order: "priority",
|
||||
|
||||
init(...args) {
|
||||
this._super(...args);
|
||||
@ -44,6 +46,18 @@ export default Ember.Controller.extend({
|
||||
});
|
||||
},
|
||||
|
||||
@computed
|
||||
sortOrders() {
|
||||
return ["priority", "priority_asc", "created_at", "created_at_asc"].map(
|
||||
order => {
|
||||
return {
|
||||
id: order,
|
||||
name: I18n.t(`review.filters.orders.${order}`)
|
||||
};
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
@computed
|
||||
statuses() {
|
||||
return [
|
||||
@ -86,7 +100,8 @@ export default Ember.Controller.extend({
|
||||
priority: this.filterPriority,
|
||||
status: this.filterStatus,
|
||||
category_id: this.filterCategoryId,
|
||||
username: this.filterUsername
|
||||
username: this.filterUsername,
|
||||
sort_order: this.filterSortOrder
|
||||
});
|
||||
this.send("refreshRoute");
|
||||
},
|
||||
|
||||
@ -108,22 +108,32 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.appEvents.on("post:show-revision", (postNumber, revision) => {
|
||||
const post = this.model.get("postStream").postForPostNumber(postNumber);
|
||||
if (!post) {
|
||||
return;
|
||||
}
|
||||
|
||||
Ember.run.scheduleOnce("afterRender", () => {
|
||||
this.send("showHistory", post, revision);
|
||||
});
|
||||
});
|
||||
this.appEvents.on("post:show-revision", this, "_showRevision");
|
||||
|
||||
this.setProperties({
|
||||
selectedPostIds: [],
|
||||
quoteState: new QuoteState()
|
||||
});
|
||||
},
|
||||
|
||||
willDestroy() {
|
||||
this._super(...arguments);
|
||||
|
||||
this.appEvents.off("post:show-revision", this, "_showRevision");
|
||||
},
|
||||
|
||||
_showRevision(postNumber, revision) {
|
||||
const post = this.model.get("postStream").postForPostNumber(postNumber);
|
||||
if (!post) {
|
||||
return;
|
||||
}
|
||||
|
||||
Ember.run.scheduleOnce("afterRender", () => {
|
||||
this.send("showHistory", post, revision);
|
||||
});
|
||||
},
|
||||
|
||||
showCategoryChooser: Ember.computed.not("model.isPrivateMessage"),
|
||||
|
||||
gotoInbox(name) {
|
||||
@ -405,6 +415,27 @@ export default Ember.Controller.extend(bufferedProperty("model"), {
|
||||
}
|
||||
},
|
||||
|
||||
deferTopic() {
|
||||
const screenTrack = Discourse.__container__.lookup("screen-track:main");
|
||||
const currentUser = this.currentUser;
|
||||
const topic = this.model;
|
||||
|
||||
screenTrack.reset();
|
||||
screenTrack.stop();
|
||||
const goToPath = topic.get("isPrivateMessage")
|
||||
? currentUser.pmPath(topic)
|
||||
: "/";
|
||||
ajax("/t/" + topic.get("id") + "/timings.json?last=1", { type: "DELETE" })
|
||||
.then(() => {
|
||||
const highestSeenByTopic = Discourse.Session.currentProp(
|
||||
"highestSeenByTopic"
|
||||
);
|
||||
highestSeenByTopic[topic.get("id")] = null;
|
||||
DiscourseURL.routeTo(goToPath);
|
||||
})
|
||||
.catch(popupAjaxError);
|
||||
},
|
||||
|
||||
editFirstPost() {
|
||||
const postStream = this.get("model.postStream");
|
||||
let firstPost = postStream.get("posts.firstObject");
|
||||
|
||||
@ -5,27 +5,32 @@ export default {
|
||||
name: "avatar-select",
|
||||
|
||||
initialize(container) {
|
||||
const siteSettings = container.lookup("site-settings:main");
|
||||
const appEvents = container.lookup("app-events:main");
|
||||
this.selectAvatarsEnabled = container.lookup(
|
||||
"site-settings:main"
|
||||
).select_avatars_enabled;
|
||||
|
||||
appEvents.on("show-avatar-select", user => {
|
||||
const avatarTemplate = user.get("avatar_template");
|
||||
let selected = "uploaded";
|
||||
container
|
||||
.lookup("app-events:main")
|
||||
.on("show-avatar-select", this, "_showAvatarSelect");
|
||||
},
|
||||
|
||||
if (avatarTemplate === user.get("system_avatar_template")) {
|
||||
selected = "system";
|
||||
} else if (avatarTemplate === user.get("gravatar_avatar_template")) {
|
||||
selected = "gravatar";
|
||||
}
|
||||
_showAvatarSelect(user) {
|
||||
const avatarTemplate = user.avatar_template;
|
||||
let selected = "uploaded";
|
||||
|
||||
const modal = showModal("avatar-selector");
|
||||
modal.setProperties({ user, selected });
|
||||
if (avatarTemplate === user.system_avatar_template) {
|
||||
selected = "system";
|
||||
} else if (avatarTemplate === user.gravatar_avatar_template) {
|
||||
selected = "gravatar";
|
||||
}
|
||||
|
||||
if (siteSettings.selectable_avatars_enabled) {
|
||||
ajax("/site/selectable-avatars.json").then(avatars =>
|
||||
modal.set("selectableAvatars", avatars)
|
||||
);
|
||||
}
|
||||
});
|
||||
const modal = showModal("avatar-selector");
|
||||
modal.setProperties({ user, selected });
|
||||
|
||||
if (this.selectAvatarsEnabled) {
|
||||
ajax("/site/selectable-avatars.json").then(avatars =>
|
||||
modal.set("selectableAvatars", avatars)
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -4,16 +4,20 @@ export default {
|
||||
after: "message-bus",
|
||||
|
||||
initialize(container) {
|
||||
const appEvents = container.lookup("app-events:main");
|
||||
const user = container.lookup("current-user:main");
|
||||
|
||||
if (!user) return; // must be logged in
|
||||
if (!window.ExperimentalBadge) return; // must have the Badging API
|
||||
|
||||
appEvents.on("notifications:changed", () => {
|
||||
let notifications =
|
||||
user.get("unread_notifications") + user.get("unread_private_messages");
|
||||
window.ExperimentalBadge.set(notifications);
|
||||
});
|
||||
const user = container.lookup("current-user:main");
|
||||
if (!user) return; // must be logged in
|
||||
|
||||
this.notifications =
|
||||
user.unread_notifications + user.unread_private_messages;
|
||||
|
||||
container
|
||||
.lookup("app-events:main")
|
||||
.on("notifications:changed", this, "_updateBadge");
|
||||
},
|
||||
|
||||
_updateBadge() {
|
||||
window.ExperimentalBadge.set(this.notifications);
|
||||
}
|
||||
};
|
||||
|
||||
@ -3,16 +3,18 @@ export default {
|
||||
after: "message-bus",
|
||||
|
||||
initialize(container) {
|
||||
const appEvents = container.lookup("app-events:main");
|
||||
const user = container.lookup("current-user:main");
|
||||
|
||||
if (!user) return; // must be logged in
|
||||
|
||||
appEvents.on("notifications:changed", () => {
|
||||
let notifications =
|
||||
user.get("unread_notifications") + user.get("unread_private_messages");
|
||||
this.notifications =
|
||||
user.unread_notifications + user.unread_private_messages;
|
||||
|
||||
Discourse.updateNotificationCount(notifications);
|
||||
});
|
||||
container
|
||||
.lookup("app-events:main")
|
||||
.on("notifications:changed", this, "_updateTitle");
|
||||
},
|
||||
|
||||
_updateTitle() {
|
||||
Discourse.updateNotificationCount(this.notifications);
|
||||
}
|
||||
};
|
||||
|
||||
@ -130,6 +130,9 @@ export default {
|
||||
"archiveTitle",
|
||||
"toggleArchiveMessage"
|
||||
],
|
||||
dropdown() {
|
||||
return this.site.mobileView;
|
||||
},
|
||||
displayed() {
|
||||
return this.canArchive;
|
||||
}
|
||||
@ -148,5 +151,20 @@ export default {
|
||||
return this.showEditOnFooter;
|
||||
}
|
||||
});
|
||||
|
||||
registerTopicFooterButton({
|
||||
id: "defer",
|
||||
icon: "circle",
|
||||
priority: 300,
|
||||
label: "topic.defer.title",
|
||||
title: "topic.defer.help",
|
||||
action: "deferTopic",
|
||||
displayed() {
|
||||
return this.canDefer;
|
||||
},
|
||||
dropdown() {
|
||||
return this.site.mobileView;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -103,17 +103,13 @@ export function endWith() {
|
||||
const args = Array.prototype.slice.call(arguments, 0);
|
||||
const substring = args.pop();
|
||||
const computed = Ember.computed(function() {
|
||||
const self = this;
|
||||
return _.every(
|
||||
args.map(function(a) {
|
||||
return self.get(a);
|
||||
}),
|
||||
function(s) {
|
||||
return args
|
||||
.map(a => this.get(a))
|
||||
.every(s => {
|
||||
const position = s.length - substring.length,
|
||||
lastIndex = s.lastIndexOf(substring);
|
||||
return lastIndex !== -1 && lastIndex === position;
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
return computed.property.apply(computed, args);
|
||||
}
|
||||
@ -128,5 +124,5 @@ export function endWith() {
|
||||
export function setting(name) {
|
||||
return Ember.computed(function() {
|
||||
return Discourse.SiteSettings[name];
|
||||
}).property();
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,14 +1,6 @@
|
||||
import { defaultHomepage } from "discourse/lib/utilities";
|
||||
|
||||
/**
|
||||
@module Discourse
|
||||
*/
|
||||
|
||||
const get = Ember.get,
|
||||
set = Ember.set;
|
||||
let popstateFired = false;
|
||||
const supportsHistoryState = window.history && "state" in window.history;
|
||||
|
||||
const popstateCallbacks = [];
|
||||
|
||||
/**
|
||||
@ -21,7 +13,9 @@ const popstateCallbacks = [];
|
||||
*/
|
||||
const DiscourseLocation = Ember.Object.extend({
|
||||
init() {
|
||||
set(this, "location", get(this, "location") || window.location);
|
||||
this._super(...arguments);
|
||||
|
||||
this.set("location", this.location || window.location);
|
||||
this.initState();
|
||||
},
|
||||
|
||||
@ -33,18 +27,17 @@ const DiscourseLocation = Ember.Object.extend({
|
||||
@method initState
|
||||
*/
|
||||
initState() {
|
||||
const history = get(this, "history") || window.history;
|
||||
const history = this.history || window.history;
|
||||
if (history && history.scrollRestoration) {
|
||||
history.scrollRestoration = "manual";
|
||||
}
|
||||
|
||||
set(this, "history", history);
|
||||
this.set("history", history);
|
||||
|
||||
let url = this.formatURL(this.getURL());
|
||||
const loc = get(this, "location");
|
||||
|
||||
if (loc && loc.hash) {
|
||||
url += loc.hash;
|
||||
if (this.location && this.location.hash) {
|
||||
url += this.location.hash;
|
||||
}
|
||||
|
||||
this.replaceState(url);
|
||||
@ -66,12 +59,11 @@ const DiscourseLocation = Ember.Object.extend({
|
||||
@method getURL
|
||||
*/
|
||||
getURL() {
|
||||
const location = get(this, "location");
|
||||
let url = location.pathname;
|
||||
let url = this.location.pathname;
|
||||
|
||||
url = url.replace(new RegExp(`^${Discourse.BaseUri}`), "");
|
||||
|
||||
const search = location.search || "";
|
||||
const search = this.location.search || "";
|
||||
url += search;
|
||||
return url;
|
||||
},
|
||||
@ -124,9 +116,7 @@ const DiscourseLocation = Ember.Object.extend({
|
||||
@method getState
|
||||
*/
|
||||
getState() {
|
||||
return supportsHistoryState
|
||||
? get(this, "history").state
|
||||
: this._historyState;
|
||||
return supportsHistoryState ? this.history.state : this._historyState;
|
||||
},
|
||||
|
||||
/**
|
||||
@ -138,13 +128,13 @@ const DiscourseLocation = Ember.Object.extend({
|
||||
@param path {String}
|
||||
*/
|
||||
pushState(path) {
|
||||
const state = { path: path };
|
||||
const state = { path };
|
||||
|
||||
// store state if browser doesn't support `history.state`
|
||||
if (!supportsHistoryState) {
|
||||
this._historyState = state;
|
||||
} else {
|
||||
get(this, "history").pushState(state, null, path);
|
||||
this.history.pushState(state, null, path);
|
||||
}
|
||||
|
||||
// used for webkit workaround
|
||||
@ -160,13 +150,13 @@ const DiscourseLocation = Ember.Object.extend({
|
||||
@param path {String}
|
||||
*/
|
||||
replaceState(path) {
|
||||
const state = { path: path };
|
||||
const state = { path };
|
||||
|
||||
// store state if browser doesn't support `history.state`
|
||||
if (!supportsHistoryState) {
|
||||
this._historyState = state;
|
||||
} else {
|
||||
get(this, "history").replaceState(state, null, path);
|
||||
this.history.replaceState(state, null, path);
|
||||
}
|
||||
|
||||
// used for webkit workaround
|
||||
@ -183,21 +173,18 @@ const DiscourseLocation = Ember.Object.extend({
|
||||
@param callback {Function}
|
||||
*/
|
||||
onUpdateURL(callback) {
|
||||
const guid = Ember.guidFor(this),
|
||||
self = this;
|
||||
const guid = Ember.guidFor(this);
|
||||
|
||||
$(window).on(`popstate.ember-location-${guid}`, () => {
|
||||
const url = this.getURL();
|
||||
|
||||
Ember.$(window).on("popstate.ember-location-" + guid, function() {
|
||||
// Ignore initial page load popstate event in Chrome
|
||||
if (!popstateFired) {
|
||||
popstateFired = true;
|
||||
if (self.getURL() === self._previousURL) {
|
||||
return;
|
||||
}
|
||||
if (url === this._previousURL) return;
|
||||
}
|
||||
const url = self.getURL();
|
||||
popstateCallbacks.forEach(function(cb) {
|
||||
cb(url);
|
||||
});
|
||||
|
||||
popstateCallbacks.forEach(cb => cb(url));
|
||||
callback(url);
|
||||
});
|
||||
},
|
||||
@ -211,7 +198,7 @@ const DiscourseLocation = Ember.Object.extend({
|
||||
@param url {String}
|
||||
*/
|
||||
formatURL(url) {
|
||||
let rootURL = get(this, "rootURL");
|
||||
let rootURL = this.rootURL;
|
||||
|
||||
if (url !== "") {
|
||||
rootURL = rootURL.replace(/\/$/, "");
|
||||
@ -225,9 +212,10 @@ const DiscourseLocation = Ember.Object.extend({
|
||||
},
|
||||
|
||||
willDestroy() {
|
||||
const guid = Ember.guidFor(this);
|
||||
this._super(...arguments);
|
||||
|
||||
Ember.$(window).off("popstate.ember-location-" + guid);
|
||||
const guid = Ember.guidFor(this);
|
||||
$(window).off(`popstate.ember-location-${guid}`);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -53,10 +53,21 @@ function show(image) {
|
||||
copyImg.style.position = "absolute";
|
||||
copyImg.style.top = `${image.offsetTop}px`;
|
||||
copyImg.style.left = `${image.offsetLeft}px`;
|
||||
copyImg.style.width = imageData.width;
|
||||
copyImg.style.height = imageData.height;
|
||||
copyImg.className = imageData.className;
|
||||
|
||||
let inOnebox = false;
|
||||
for (let element = image; element; element = element.parentElement) {
|
||||
if (element.classList.contains("onebox")) {
|
||||
inOnebox = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!inOnebox) {
|
||||
copyImg.style.width = `${imageData.width}px`;
|
||||
copyImg.style.height = `${imageData.height}px`;
|
||||
}
|
||||
|
||||
image.parentNode.insertBefore(copyImg, image);
|
||||
} else {
|
||||
image.classList.remove("d-lazyload-hidden");
|
||||
|
||||
@ -44,7 +44,7 @@ import { addComposerUploadHandler } from "discourse/components/composer-editor";
|
||||
import { addCategorySortCriteria } from "discourse/components/edit-category-settings";
|
||||
|
||||
// If you add any methods to the API ensure you bump up this number
|
||||
const PLUGIN_API_VERSION = "0.8.30";
|
||||
const PLUGIN_API_VERSION = "0.8.31";
|
||||
|
||||
class PluginApi {
|
||||
constructor(version, container) {
|
||||
@ -809,7 +809,7 @@ class PluginApi {
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* addComposerUploadHandler(["mp4", "mov"], (file) => {
|
||||
* addComposerUploadHandler(["mp4", "mov"], (file, editor) => {
|
||||
* console.log("Handling upload for", file.name);
|
||||
* })
|
||||
*/
|
||||
|
||||
@ -26,7 +26,8 @@ export default class {
|
||||
// Create an interval timer if we don't have one.
|
||||
if (!this._interval) {
|
||||
this._interval = setInterval(() => this.tick(), 1000);
|
||||
$(window).on("scroll.screentrack", this.scrolled.bind(this));
|
||||
this._boundScrolled = Ember.run.bind(this, this.scrolled);
|
||||
$(window).on("scroll.screentrack", this._boundScrolled);
|
||||
}
|
||||
|
||||
this._topicId = topicId;
|
||||
@ -39,7 +40,10 @@ export default class {
|
||||
return;
|
||||
}
|
||||
|
||||
$(window).off("scroll.screentrack", this.scrolled);
|
||||
if (this._boundScrolled) {
|
||||
$(window).off("scroll.screentrack", this._boundScrolled);
|
||||
}
|
||||
|
||||
this.tick();
|
||||
this.flush();
|
||||
this.reset();
|
||||
|
||||
@ -241,6 +241,7 @@ export class Tag {
|
||||
let alt = attr.alt || pAttr.alt || "";
|
||||
const width = attr.width || pAttr.width;
|
||||
const height = attr.height || pAttr.height;
|
||||
const title = attr.title;
|
||||
|
||||
if (width && height) {
|
||||
const pipe = this.element.parentNames.includes("table")
|
||||
@ -249,7 +250,7 @@ export class Tag {
|
||||
alt = `${alt}${pipe}${width}x${height}`;
|
||||
}
|
||||
|
||||
return "";
|
||||
return ``;
|
||||
}
|
||||
|
||||
return "";
|
||||
|
||||
@ -55,7 +55,7 @@ export function transformBasicPost(post) {
|
||||
reviewableScorePendingCount: post.reviewable_score_pending_count,
|
||||
version: post.version,
|
||||
canRecoverTopic: false,
|
||||
canDeletedTopic: false,
|
||||
canDeleteTopic: false,
|
||||
canViewEditHistory: post.can_view_edit_history,
|
||||
canWiki: post.can_wiki,
|
||||
showLike: false,
|
||||
@ -234,12 +234,10 @@ export default function transformPost(
|
||||
|
||||
// Show a "Flag to delete" message if not staff and you can't
|
||||
// otherwise delete it.
|
||||
postAtts.showFlagDelete = (
|
||||
postAtts.showFlagDelete =
|
||||
!postAtts.canDelete &&
|
||||
postAtts.yours &&
|
||||
(currentUser && !currentUser.staff)
|
||||
);
|
||||
|
||||
(currentUser && !currentUser.staff);
|
||||
} else {
|
||||
postAtts.canRecover = postAtts.isDeleted && postAtts.canRecover;
|
||||
postAtts.canDelete =
|
||||
|
||||
@ -127,10 +127,16 @@ export default Ember.Mixin.create({
|
||||
|
||||
this.appEvents.on(previewClickEvent, this, "_previewClick");
|
||||
|
||||
this.appEvents.on(`topic-header:trigger-${id}`, (username, $target) => {
|
||||
this.setProperties({ isFixed: true, isDocked: true });
|
||||
return this._show(username, $target);
|
||||
});
|
||||
this.appEvents.on(
|
||||
`topic-header:trigger-${id}`,
|
||||
this,
|
||||
"_topicHeaderTrigger"
|
||||
);
|
||||
},
|
||||
|
||||
_topicHeaderTrigger(username, $target) {
|
||||
this.setProperties({ isFixed: true, isDocked: true });
|
||||
return this._show(username, $target);
|
||||
},
|
||||
|
||||
_bindMobileScroll() {
|
||||
@ -281,7 +287,14 @@ export default Ember.Mixin.create({
|
||||
$("#main")
|
||||
.off(clickDataExpand)
|
||||
.off(clickMention);
|
||||
|
||||
this.appEvents.off(previewClickEvent, this, "_previewClick");
|
||||
|
||||
this.appEvents.off(
|
||||
`topic-header:trigger-${this.elementId}`,
|
||||
this,
|
||||
"_topicHeaderTrigger"
|
||||
);
|
||||
},
|
||||
|
||||
keyUp(e) {
|
||||
|
||||
@ -7,20 +7,20 @@ export default Ember.Mixin.create({
|
||||
uniqueUsernameValidation: null,
|
||||
|
||||
maxUsernameLength: setting("max_username_length"),
|
||||
|
||||
minUsernameLength: setting("min_username_length"),
|
||||
|
||||
fetchExistingUsername: debounce(function() {
|
||||
const self = this;
|
||||
Discourse.User.checkUsername(null, this.accountEmail).then(function(
|
||||
result
|
||||
) {
|
||||
Discourse.User.checkUsername(null, this.accountEmail).then(result => {
|
||||
if (
|
||||
result.suggestion &&
|
||||
(Ember.isEmpty(self.get("accountUsername")) ||
|
||||
self.get("accountUsername") === self.get("authOptions.username"))
|
||||
(Ember.isEmpty(this.accountUsername) ||
|
||||
this.accountUsername === this.get("authOptions.username"))
|
||||
) {
|
||||
self.set("accountUsername", result.suggestion);
|
||||
self.set("prefilledUsername", result.suggestion);
|
||||
this.setProperties({
|
||||
accountUsername: result.suggestion,
|
||||
prefilledUsername: result.suggestion
|
||||
});
|
||||
}
|
||||
});
|
||||
}, 500),
|
||||
@ -38,9 +38,7 @@ export default Ember.Mixin.create({
|
||||
|
||||
// If blank, fail without a reason
|
||||
if (Ember.isEmpty(accountUsername)) {
|
||||
return InputValidation.create({
|
||||
failed: true
|
||||
});
|
||||
return InputValidation.create({ failed: true });
|
||||
}
|
||||
|
||||
// If too short
|
||||
@ -67,7 +65,7 @@ export default Ember.Mixin.create({
|
||||
});
|
||||
},
|
||||
|
||||
shouldCheckUsernameAvailability: function() {
|
||||
shouldCheckUsernameAvailability() {
|
||||
return (
|
||||
!Ember.isEmpty(this.accountUsername) &&
|
||||
this.accountUsername.length >= this.minUsernameLength
|
||||
|
||||
@ -11,23 +11,14 @@ const Badge = RestModel.extend({
|
||||
return Discourse.getURL(`/badges/${this.id}/${this.slug}`);
|
||||
},
|
||||
|
||||
/**
|
||||
Update this badge with the response returned by the server on save.
|
||||
|
||||
@method updateFromJson
|
||||
@param {Object} json The JSON response returned by the server
|
||||
**/
|
||||
updateFromJson: function(json) {
|
||||
const self = this;
|
||||
updateFromJson(json) {
|
||||
if (json.badge) {
|
||||
Object.keys(json.badge).forEach(function(key) {
|
||||
self.set(key, json.badge[key]);
|
||||
});
|
||||
Object.keys(json.badge).forEach(key => this.set(key, json.badge[key]));
|
||||
}
|
||||
if (json.badge_types) {
|
||||
json.badge_types.forEach(function(badgeType) {
|
||||
if (badgeType.id === self.get("badge_type_id")) {
|
||||
self.set("badge_type", Object.create(badgeType));
|
||||
json.badge_types.forEach(badgeType => {
|
||||
if (badgeType.id === this.badge_type_id) {
|
||||
this.set("badge_type", Object.create(badgeType));
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -36,77 +27,57 @@ const Badge = RestModel.extend({
|
||||
@computed("badge_type.name")
|
||||
badgeTypeClassName(type) {
|
||||
type = type || "";
|
||||
return "badge-type-" + type.toLowerCase();
|
||||
return `badge-type-${type.toLowerCase()}`;
|
||||
},
|
||||
|
||||
/**
|
||||
Save and update the badge from the server's response.
|
||||
|
||||
@method save
|
||||
@returns {Promise} A promise that resolves to the updated `Badge`
|
||||
**/
|
||||
save: function(data) {
|
||||
save(data) {
|
||||
let url = "/admin/badges",
|
||||
requestType = "POST";
|
||||
const self = this;
|
||||
type = "POST";
|
||||
|
||||
if (this.id) {
|
||||
// We are updating an existing badge.
|
||||
url += "/" + this.id;
|
||||
requestType = "PUT";
|
||||
url += `/${this.id}`;
|
||||
type = "PUT";
|
||||
}
|
||||
|
||||
return ajax(url, {
|
||||
type: requestType,
|
||||
data: data
|
||||
})
|
||||
.then(function(json) {
|
||||
self.updateFromJson(json);
|
||||
return self;
|
||||
return ajax(url, { type, data })
|
||||
.then(json => {
|
||||
this.updateFromJson(json);
|
||||
return this;
|
||||
})
|
||||
.catch(function(error) {
|
||||
.catch(error => {
|
||||
throw new Error(error);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
Destroy the badge.
|
||||
|
||||
@method destroy
|
||||
@returns {Promise} A promise that resolves to the server response
|
||||
**/
|
||||
destroy: function() {
|
||||
destroy() {
|
||||
if (this.newBadge) return Ember.RSVP.resolve();
|
||||
return ajax("/admin/badges/" + this.id, {
|
||||
|
||||
return ajax(`/admin/badges/${this.id}`, {
|
||||
type: "DELETE"
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Badge.reopenClass({
|
||||
/**
|
||||
Create `Badge` instances from the server JSON response.
|
||||
|
||||
@method createFromJson
|
||||
@param {Object} json The JSON returned by the server
|
||||
@returns Array or instance of `Badge` depending on the input JSON
|
||||
**/
|
||||
createFromJson: function(json) {
|
||||
createFromJson(json) {
|
||||
// Create BadgeType objects.
|
||||
const badgeTypes = {};
|
||||
if ("badge_types" in json) {
|
||||
json.badge_types.forEach(function(badgeTypeJson) {
|
||||
badgeTypes[badgeTypeJson.id] = Ember.Object.create(badgeTypeJson);
|
||||
});
|
||||
json.badge_types.forEach(
|
||||
badgeTypeJson =>
|
||||
(badgeTypes[badgeTypeJson.id] = Ember.Object.create(badgeTypeJson))
|
||||
);
|
||||
}
|
||||
|
||||
const badgeGroupings = {};
|
||||
if ("badge_groupings" in json) {
|
||||
json.badge_groupings.forEach(function(badgeGroupingJson) {
|
||||
badgeGroupings[badgeGroupingJson.id] = BadgeGrouping.create(
|
||||
badgeGroupingJson
|
||||
);
|
||||
});
|
||||
json.badge_groupings.forEach(
|
||||
badgeGroupingJson =>
|
||||
(badgeGroupings[badgeGroupingJson.id] = BadgeGrouping.create(
|
||||
badgeGroupingJson
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
// Create Badge objects.
|
||||
@ -116,13 +87,12 @@ Badge.reopenClass({
|
||||
} else if (json.badges) {
|
||||
badges = json.badges;
|
||||
}
|
||||
badges = badges.map(function(badgeJson) {
|
||||
badges = badges.map(badgeJson => {
|
||||
const badge = Badge.create(badgeJson);
|
||||
badge.set("badge_type", badgeTypes[badge.get("badge_type_id")]);
|
||||
badge.set(
|
||||
"badge_grouping",
|
||||
badgeGroupings[badge.get("badge_grouping_id")]
|
||||
);
|
||||
badge.setProperties({
|
||||
badge_type: badgeTypes[badge.badge_type_id],
|
||||
badge_grouping: badgeGroupings[badge.badge_grouping_id]
|
||||
});
|
||||
return badge;
|
||||
});
|
||||
|
||||
@ -133,35 +103,21 @@ Badge.reopenClass({
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
Find all `Badge` instances that have been defined.
|
||||
|
||||
@method findAll
|
||||
@returns {Promise} a promise that resolves to an array of `Badge`
|
||||
**/
|
||||
findAll: function(opts) {
|
||||
findAll(opts) {
|
||||
let listable = "";
|
||||
if (opts && opts.onlyListable) {
|
||||
listable = "?only_listable=true";
|
||||
}
|
||||
return ajax("/badges.json" + listable, { data: opts }).then(function(
|
||||
badgesJson
|
||||
) {
|
||||
return Badge.createFromJson(badgesJson);
|
||||
});
|
||||
|
||||
return ajax(`/badges.json${listable}`, { data: opts }).then(badgesJson =>
|
||||
Badge.createFromJson(badgesJson)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
Returns a `Badge` that has the given ID.
|
||||
|
||||
@method findById
|
||||
@param {Number} id ID of the badge
|
||||
@returns {Promise} a promise that resolves to a `Badge`
|
||||
**/
|
||||
findById: function(id) {
|
||||
return ajax("/badges/" + id).then(function(badgeJson) {
|
||||
return Badge.createFromJson(badgeJson);
|
||||
});
|
||||
findById(id) {
|
||||
return ajax(`/badges/${id}`).then(badgeJson =>
|
||||
Badge.createFromJson(badgeJson)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@ const Group = RestModel.extend({
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
|
||||
this.owners = [];
|
||||
this.set("owners", []);
|
||||
},
|
||||
|
||||
hasOwners: Ember.computed.notEmpty("owners"),
|
||||
@ -50,7 +50,7 @@ const Group = RestModel.extend({
|
||||
|
||||
return Group.loadMembers(this.name, offset, this.limit, params).then(
|
||||
result => {
|
||||
var ownerIds = {};
|
||||
const ownerIds = {};
|
||||
result.owners.forEach(owner => (ownerIds[owner.id] = true));
|
||||
|
||||
this.setProperties({
|
||||
@ -70,29 +70,26 @@ const Group = RestModel.extend({
|
||||
},
|
||||
|
||||
removeOwner(member) {
|
||||
var self = this;
|
||||
return ajax("/admin/groups/" + this.id + "/owners.json", {
|
||||
return ajax(`/admin/groups/${this.id}/owners.json`, {
|
||||
type: "DELETE",
|
||||
data: { user_id: member.get("id") }
|
||||
}).then(function() {
|
||||
data: { user_id: member.id }
|
||||
}).then(() => {
|
||||
// reload member list
|
||||
self.findMembers();
|
||||
this.findMembers();
|
||||
});
|
||||
},
|
||||
|
||||
removeMember(member, params) {
|
||||
return ajax("/groups/" + this.id + "/members.json", {
|
||||
return ajax(`/groups/${this.id}/members.json`, {
|
||||
type: "DELETE",
|
||||
data: { user_id: member.get("id") }
|
||||
}).then(() => {
|
||||
this.findMembers(params);
|
||||
});
|
||||
data: { user_id: member.id }
|
||||
}).then(() => this.findMembers(params));
|
||||
},
|
||||
|
||||
addMembers(usernames, filter) {
|
||||
return ajax("/groups/" + this.id + "/members.json", {
|
||||
return ajax(`/groups/${this.id}/members.json`, {
|
||||
type: "PUT",
|
||||
data: { usernames: usernames }
|
||||
data: { usernames }
|
||||
}).then(response => {
|
||||
if (filter) {
|
||||
this._filterMembers(response);
|
||||
@ -105,7 +102,7 @@ const Group = RestModel.extend({
|
||||
addOwners(usernames, filter) {
|
||||
return ajax(`/admin/groups/${this.id}/owners.json`, {
|
||||
type: "PUT",
|
||||
data: { group: { usernames: usernames } }
|
||||
data: { group: { usernames } }
|
||||
}).then(response => {
|
||||
if (filter) {
|
||||
this._filterMembers(response);
|
||||
@ -125,30 +122,27 @@ const Group = RestModel.extend({
|
||||
},
|
||||
|
||||
@computed("flair_bg_color")
|
||||
flairBackgroundHexColor() {
|
||||
return this.flair_bg_color
|
||||
? this.flair_bg_color.replace(new RegExp("[^0-9a-fA-F]", "g"), "")
|
||||
flairBackgroundHexColor(flairBgColor) {
|
||||
return flairBgColor
|
||||
? flairBgColor.replace(new RegExp("[^0-9a-fA-F]", "g"), "")
|
||||
: null;
|
||||
},
|
||||
|
||||
@computed("flair_color")
|
||||
flairHexColor() {
|
||||
return this.flair_color
|
||||
? this.flair_color.replace(new RegExp("[^0-9a-fA-F]", "g"), "")
|
||||
flairHexColor(flairColor) {
|
||||
return flairColor
|
||||
? flairColor.replace(new RegExp("[^0-9a-fA-F]", "g"), "")
|
||||
: null;
|
||||
},
|
||||
|
||||
@computed("mentionable_level")
|
||||
canEveryoneMention(mentionableLevel) {
|
||||
return mentionableLevel === "99";
|
||||
},
|
||||
canEveryoneMention: Ember.computed.equal("mentionable_level", 99),
|
||||
|
||||
@computed("visibility_level")
|
||||
isPrivate(visibilityLevel) {
|
||||
return visibilityLevel !== 0;
|
||||
},
|
||||
|
||||
@observes("visibility_level", "canEveryoneMention")
|
||||
@observes("isPrivate", "canEveryoneMention")
|
||||
_updateAllowMembershipRequests() {
|
||||
if (this.isPrivate || !this.canEveryoneMention) {
|
||||
this.set("allow_membership_requests", false);
|
||||
@ -158,8 +152,7 @@ const Group = RestModel.extend({
|
||||
@observes("visibility_level")
|
||||
_updatePublic() {
|
||||
if (this.isPrivate) {
|
||||
this.set("public", false);
|
||||
this.set("allow_membership_requests", false);
|
||||
this.setProperties({ public: false, allow_membership_requests: false });
|
||||
}
|
||||
},
|
||||
|
||||
@ -170,9 +163,7 @@ const Group = RestModel.extend({
|
||||
messageable_level: this.messageable_level,
|
||||
visibility_level: this.visibility_level,
|
||||
automatic_membership_email_domains: this.emailDomains,
|
||||
automatic_membership_retroactive: !!this.get(
|
||||
"automatic_membership_retroactive"
|
||||
),
|
||||
automatic_membership_retroactive: !!this.automatic_membership_retroactive,
|
||||
title: this.title,
|
||||
primary_group: !!this.primary_group,
|
||||
grant_trust_level: this.grant_trust_level,
|
||||
@ -223,7 +214,7 @@ const Group = RestModel.extend({
|
||||
if (!this.id) {
|
||||
return;
|
||||
}
|
||||
return ajax("/admin/groups/" + this.id, { type: "DELETE" });
|
||||
return ajax(`/admin/groups/${this.id}`, { type: "DELETE" });
|
||||
},
|
||||
|
||||
findLogs(offset, filters) {
|
||||
@ -239,13 +230,13 @@ const Group = RestModel.extend({
|
||||
|
||||
findPosts(opts) {
|
||||
opts = opts || {};
|
||||
|
||||
const type = opts.type || "posts";
|
||||
const data = {};
|
||||
|
||||
var data = {};
|
||||
if (opts.beforePostId) {
|
||||
data.before_post_id = opts.beforePostId;
|
||||
}
|
||||
|
||||
if (opts.categoryId) {
|
||||
data.category_id = parseInt(opts.categoryId);
|
||||
}
|
||||
@ -271,21 +262,21 @@ const Group = RestModel.extend({
|
||||
requestMembership(reason) {
|
||||
return ajax(`/groups/${this.name}/request_membership`, {
|
||||
type: "POST",
|
||||
data: { reason: reason }
|
||||
data: { reason }
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Group.reopenClass({
|
||||
findAll(opts) {
|
||||
return ajax("/groups/search.json", { data: opts }).then(groups => {
|
||||
return groups.map(g => Group.create(g));
|
||||
});
|
||||
return ajax("/groups/search.json", { data: opts }).then(groups =>
|
||||
groups.map(g => Group.create(g))
|
||||
);
|
||||
},
|
||||
|
||||
loadMembers(name, offset, limit, params) {
|
||||
return ajax("/groups/" + name + "/members.json", {
|
||||
data: _.extend(
|
||||
return ajax(`/groups/${name}/members.json`, {
|
||||
data: Object.assign(
|
||||
{
|
||||
limit: limit || 50,
|
||||
offset: offset || 0
|
||||
|
||||
@ -12,21 +12,18 @@ const Invite = Discourse.Model.extend({
|
||||
},
|
||||
|
||||
reinvite() {
|
||||
const self = this;
|
||||
return ajax("/invites/reinvite", {
|
||||
type: "POST",
|
||||
data: { email: this.email }
|
||||
})
|
||||
.then(function() {
|
||||
self.set("reinvited", true);
|
||||
})
|
||||
.then(() => this.set("reinvited", true))
|
||||
.catch(popupAjaxError);
|
||||
}
|
||||
});
|
||||
|
||||
Invite.reopenClass({
|
||||
create() {
|
||||
var result = this._super.apply(this, arguments);
|
||||
const result = this._super.apply(this, arguments);
|
||||
if (result.user) {
|
||||
result.user = Discourse.User.create(result.user);
|
||||
}
|
||||
@ -34,37 +31,27 @@ Invite.reopenClass({
|
||||
},
|
||||
|
||||
findInvitedBy(user, filter, search, offset) {
|
||||
if (!user) {
|
||||
return Ember.RSVP.resolve();
|
||||
}
|
||||
if (!user) Ember.RSVP.resolve();
|
||||
|
||||
var data = {};
|
||||
if (!Ember.isNone(filter)) {
|
||||
data.filter = filter;
|
||||
}
|
||||
if (!Ember.isNone(search)) {
|
||||
data.search = search;
|
||||
}
|
||||
const data = {};
|
||||
if (!Ember.isNone(filter)) data.filter = filter;
|
||||
if (!Ember.isNone(search)) data.search = search;
|
||||
data.offset = offset || 0;
|
||||
|
||||
return ajax(userPath(user.get("username_lower") + "/invited.json"), {
|
||||
return ajax(userPath(`${user.username_lower}/invited.json`), {
|
||||
data
|
||||
}).then(function(result) {
|
||||
result.invites = result.invites.map(function(i) {
|
||||
return Invite.create(i);
|
||||
});
|
||||
|
||||
}).then(result => {
|
||||
result.invites = result.invites.map(i => Invite.create(i));
|
||||
return Ember.Object.create(result);
|
||||
});
|
||||
},
|
||||
|
||||
findInvitedCount(user) {
|
||||
if (!user) {
|
||||
return Ember.RSVP.resolve();
|
||||
}
|
||||
return ajax(
|
||||
userPath(user.get("username_lower") + "/invited_count.json")
|
||||
).then(result => Ember.Object.create(result.counts));
|
||||
if (!user) Ember.RSVP.resolve();
|
||||
|
||||
return ajax(userPath(`${user.username_lower}/invited_count.json`)).then(
|
||||
result => Ember.Object.create(result.counts)
|
||||
);
|
||||
},
|
||||
|
||||
reinviteAll() {
|
||||
|
||||
@ -13,7 +13,7 @@ const LoginMethod = Ember.Object.extend({
|
||||
|
||||
@computed
|
||||
message() {
|
||||
return this.message_override || I18n.t("login." + this.name + ".message");
|
||||
return this.message_override || I18n.t(`login.${this.name}.message`);
|
||||
},
|
||||
|
||||
doLogin({ reconnect = false, fullScreenLogin = true } = {}) {
|
||||
@ -23,7 +23,7 @@ const LoginMethod = Ember.Object.extend({
|
||||
if (customLogin) {
|
||||
customLogin();
|
||||
} else {
|
||||
let authUrl = this.custom_url || Discourse.getURL("/auth/" + name);
|
||||
let authUrl = this.custom_url || Discourse.getURL(`/auth/${name}`);
|
||||
|
||||
if (reconnect) {
|
||||
authUrl += "?reconnect=true";
|
||||
@ -45,7 +45,7 @@ const LoginMethod = Ember.Object.extend({
|
||||
authUrl += "display=popup";
|
||||
}
|
||||
|
||||
const w = window.open(
|
||||
const windowState = window.open(
|
||||
authUrl,
|
||||
"_blank",
|
||||
"menubar=no,status=no,height=" +
|
||||
@ -57,11 +57,11 @@ const LoginMethod = Ember.Object.extend({
|
||||
",top=" +
|
||||
top
|
||||
);
|
||||
const self = this;
|
||||
const timer = setInterval(function() {
|
||||
if (!w || w.closed) {
|
||||
|
||||
const timer = setInterval(() => {
|
||||
if (!windowState || windowState.closed) {
|
||||
clearInterval(timer);
|
||||
self.set("authenticate", null);
|
||||
this.set("authenticate", null);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
@ -72,18 +72,16 @@ const LoginMethod = Ember.Object.extend({
|
||||
let methods;
|
||||
|
||||
export function findAll() {
|
||||
if (methods) {
|
||||
return methods;
|
||||
}
|
||||
if (methods) return methods;
|
||||
|
||||
methods = [];
|
||||
|
||||
Discourse.Site.currentProp("auth_providers").forEach(provider => {
|
||||
methods.pushObject(LoginMethod.create(provider));
|
||||
});
|
||||
Discourse.Site.currentProp("auth_providers").forEach(provider =>
|
||||
methods.pushObject(LoginMethod.create(provider))
|
||||
);
|
||||
|
||||
// exclude FA icon for Google, uses custom SVG
|
||||
methods.forEach(m => m.set("isGoogle", m.get("name") === "google_oauth2"));
|
||||
methods.forEach(m => m.set("isGoogle", m.name === "google_oauth2"));
|
||||
|
||||
return methods;
|
||||
}
|
||||
|
||||
@ -20,7 +20,7 @@ const Post = RestModel.extend({
|
||||
@computed("url")
|
||||
shareUrl(url) {
|
||||
const user = Discourse.User.current();
|
||||
const userSuffix = user ? "?u=" + user.get("username_lower") : "";
|
||||
const userSuffix = user ? `?u=${user.username_lower}` : "";
|
||||
|
||||
if (this.firstPost) {
|
||||
return this.get("topic.url") + userSuffix;
|
||||
@ -55,24 +55,21 @@ const Post = RestModel.extend({
|
||||
},
|
||||
|
||||
@computed("post_number", "topic_id", "topic.slug")
|
||||
url(postNr, topicId, slug) {
|
||||
url(post_number, topic_id, topicSlug) {
|
||||
return postUrl(
|
||||
slug || this.topic_slug,
|
||||
topicId || this.get("topic.id"),
|
||||
postNr
|
||||
topicSlug || this.topic_slug,
|
||||
topic_id || this.get("topic.id"),
|
||||
post_number
|
||||
);
|
||||
},
|
||||
|
||||
// Don't drop the /1
|
||||
@computed("post_number", "url")
|
||||
urlWithNumber(postNumber, baseUrl) {
|
||||
return postNumber === 1 ? baseUrl + "/1" : baseUrl;
|
||||
return postNumber === 1 ? `${baseUrl}/1` : baseUrl;
|
||||
},
|
||||
|
||||
@computed("username")
|
||||
usernameUrl(username) {
|
||||
return userPath(username);
|
||||
},
|
||||
@computed("username") usernameUrl: userPath,
|
||||
|
||||
topicOwner: propertyEqual("topic.details.created_by.id", "user_id"),
|
||||
|
||||
@ -81,9 +78,7 @@ const Post = RestModel.extend({
|
||||
data[field] = value;
|
||||
|
||||
return ajax(`/posts/${this.id}/${field}`, { type: "PUT", data })
|
||||
.then(() => {
|
||||
this.set(field, value);
|
||||
})
|
||||
.then(() => this.set(field, value))
|
||||
.catch(popupAjaxError);
|
||||
},
|
||||
|
||||
@ -102,9 +97,9 @@ const Post = RestModel.extend({
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.site.get("flagTypes").filter(item => {
|
||||
return this.get(`actionByName.${item.get("name_key")}.can_act`);
|
||||
});
|
||||
return this.site.flagTypes.filter(item =>
|
||||
this.get(`actionByName.${item.name_key}.can_act`)
|
||||
);
|
||||
},
|
||||
|
||||
afterUpdate(res) {
|
||||
@ -131,9 +126,9 @@ const Post = RestModel.extend({
|
||||
// Put the metaData into the request
|
||||
if (metaData) {
|
||||
data.meta_data = {};
|
||||
Object.keys(metaData).forEach(function(key) {
|
||||
data.meta_data[key] = metaData.get(key);
|
||||
});
|
||||
Object.keys(metaData).forEach(
|
||||
key => (data.meta_data[key] = metaData[key])
|
||||
);
|
||||
}
|
||||
|
||||
return data;
|
||||
@ -194,7 +189,7 @@ const Post = RestModel.extend({
|
||||
|
||||
// Moderators can delete posts. Users can only trigger a deleted at message, unless delete_removed_posts_after is 0.
|
||||
if (
|
||||
deletedBy.get("staff") ||
|
||||
deletedBy.staff ||
|
||||
Discourse.SiteSettings.delete_removed_posts_after === 0
|
||||
) {
|
||||
this.setProperties({
|
||||
@ -260,10 +255,9 @@ const Post = RestModel.extend({
|
||||
is already found in an identity map.
|
||||
**/
|
||||
updateFromPost(otherPost) {
|
||||
const self = this;
|
||||
Object.keys(otherPost).forEach(function(key) {
|
||||
Object.keys(otherPost).forEach(key => {
|
||||
let value = otherPost[key],
|
||||
oldValue = self[key];
|
||||
oldValue = this[key];
|
||||
|
||||
if (!value) {
|
||||
value = null;
|
||||
@ -282,28 +276,27 @@ const Post = RestModel.extend({
|
||||
}
|
||||
|
||||
if (!skip) {
|
||||
self.set(key, value);
|
||||
this.set(key, value);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
expandHidden() {
|
||||
return ajax("/posts/" + this.id + "/cooked.json").then(result => {
|
||||
return ajax(`/posts/${this.id}/cooked.json`).then(result => {
|
||||
this.setProperties({ cooked: result.cooked, cooked_hidden: false });
|
||||
});
|
||||
},
|
||||
|
||||
rebake() {
|
||||
return ajax("/posts/" + this.id + "/rebake", { type: "PUT" });
|
||||
return ajax(`/posts/${this.id}/rebake`, { type: "PUT" });
|
||||
},
|
||||
|
||||
unhide() {
|
||||
return ajax("/posts/" + this.id + "/unhide", { type: "PUT" });
|
||||
return ajax(`/posts/${this.id}/unhide`, { type: "PUT" });
|
||||
},
|
||||
|
||||
toggleBookmark() {
|
||||
const self = this;
|
||||
let bookmarkedTopic;
|
||||
|
||||
this.toggleProperty("bookmarked");
|
||||
@ -316,13 +309,11 @@ const Post = RestModel.extend({
|
||||
// need to wait to hear back from server (stuff may not be loaded)
|
||||
|
||||
return Discourse.Post.updateBookmark(this.id, this.bookmarked)
|
||||
.then(function(result) {
|
||||
self.set("topic.bookmarked", result.topic_bookmarked);
|
||||
})
|
||||
.catch(function(error) {
|
||||
self.toggleProperty("bookmarked");
|
||||
.then(result => this.set("topic.bookmarked", result.topic_bookmarked))
|
||||
.catch(error => {
|
||||
this.toggleProperty("bookmarked");
|
||||
if (bookmarkedTopic) {
|
||||
self.set("topic.bookmarked", false);
|
||||
this.set("topic.bookmarked", false);
|
||||
}
|
||||
throw new Error(error);
|
||||
});
|
||||
@ -348,7 +339,7 @@ Post.reopenClass({
|
||||
const lookup = Ember.Object.create();
|
||||
|
||||
// this area should be optimized, it is creating way too many objects per post
|
||||
json.actions_summary = json.actions_summary.map(function(a) {
|
||||
json.actions_summary = json.actions_summary.map(a => {
|
||||
a.actionType = Discourse.Site.current().postActionTypeById(a.id);
|
||||
a.count = a.count || 0;
|
||||
const actionSummary = ActionSummary.create(a);
|
||||
@ -366,13 +357,14 @@ Post.reopenClass({
|
||||
if (json && json.reply_to_user) {
|
||||
json.reply_to_user = Discourse.User.create(json.reply_to_user);
|
||||
}
|
||||
|
||||
return json;
|
||||
},
|
||||
|
||||
updateBookmark(postId, bookmarked) {
|
||||
return ajax("/posts/" + postId + "/bookmark", {
|
||||
return ajax(`/posts/${postId}/bookmark`, {
|
||||
type: "PUT",
|
||||
data: { bookmarked: bookmarked }
|
||||
data: { bookmarked }
|
||||
});
|
||||
},
|
||||
|
||||
@ -391,27 +383,27 @@ Post.reopenClass({
|
||||
},
|
||||
|
||||
loadRevision(postId, version) {
|
||||
return ajax("/posts/" + postId + "/revisions/" + version + ".json").then(
|
||||
result => Ember.Object.create(result)
|
||||
return ajax(`/posts/${postId}/revisions/${version}.json`).then(result =>
|
||||
Ember.Object.create(result)
|
||||
);
|
||||
},
|
||||
|
||||
hideRevision(postId, version) {
|
||||
return ajax("/posts/" + postId + "/revisions/" + version + "/hide", {
|
||||
return ajax(`/posts/${postId}/revisions/${version}/hide`, {
|
||||
type: "PUT"
|
||||
});
|
||||
},
|
||||
|
||||
showRevision(postId, version) {
|
||||
return ajax("/posts/" + postId + "/revisions/" + version + "/show", {
|
||||
return ajax(`/posts/${postId}/revisions/${version}/show`, {
|
||||
type: "PUT"
|
||||
});
|
||||
},
|
||||
|
||||
loadQuote(postId) {
|
||||
return ajax("/posts/" + postId + ".json").then(result => {
|
||||
return ajax(`/posts/${postId}.json`).then(result => {
|
||||
const post = Discourse.Post.create(result);
|
||||
return Quote.build(post, post.get("raw"), { raw: true, full: true });
|
||||
return Quote.build(post, post.raw, { raw: true, full: true });
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@ -10,8 +10,13 @@ export const IGNORED = 3;
|
||||
export const DELETED = 4;
|
||||
|
||||
export default RestModel.extend({
|
||||
@computed("type")
|
||||
humanType(type) {
|
||||
@computed("type", "topic")
|
||||
humanType(type, topic) {
|
||||
// Display "Queued Topic" if the post will create a topic
|
||||
if (type === "ReviewableQueuedPost" && !topic) {
|
||||
type = "ReviewableQueuedTopic";
|
||||
}
|
||||
|
||||
return I18n.t(`review.types.${type.underscore()}.title`, {
|
||||
defaultValue: ""
|
||||
});
|
||||
|
||||
@ -312,7 +312,7 @@ export default Ember.Object.extend({
|
||||
obj[subType] = hydrated;
|
||||
delete obj[k];
|
||||
} else {
|
||||
obj[subType] = null;
|
||||
Ember.set(obj, subType, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,15 +1,17 @@
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import RestModel from "discourse/models/rest";
|
||||
import Model from "discourse/models/model";
|
||||
import { getOwner } from "discourse-common/lib/get-owner";
|
||||
|
||||
// Whether to show the category badge in topic lists
|
||||
function displayCategoryInList(site, category) {
|
||||
if (category) {
|
||||
if (category.get("has_children")) {
|
||||
if (category.has_children) {
|
||||
return true;
|
||||
}
|
||||
let draftCategoryId = site.get("shared_drafts_category_id");
|
||||
if (draftCategoryId && category.get("id") === draftCategoryId) {
|
||||
|
||||
const draftCategoryId = site.shared_drafts_category_id;
|
||||
if (draftCategoryId && category.id === draftCategoryId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -25,7 +27,7 @@ const TopicList = RestModel.extend({
|
||||
forEachNew(topics, callback) {
|
||||
const topicIds = [];
|
||||
|
||||
this.topics.forEach(topic => (topicIds[topic.get("id")] = true));
|
||||
this.topics.forEach(topic => (topicIds[topic.id] = true));
|
||||
|
||||
topics.forEach(topic => {
|
||||
if (!topicIds[topic.id]) {
|
||||
@ -68,30 +70,27 @@ const TopicList = RestModel.extend({
|
||||
moreUrl += "?" + params;
|
||||
}
|
||||
|
||||
const self = this;
|
||||
this.set("loadingMore", true);
|
||||
|
||||
const store = this.store;
|
||||
return ajax({ url: moreUrl }).then(function(result) {
|
||||
return ajax({ url: moreUrl }).then(result => {
|
||||
let topicsAdded = 0;
|
||||
|
||||
if (result) {
|
||||
// the new topics loaded from the server
|
||||
const newTopics = TopicList.topicsFrom(store, result);
|
||||
const topics = self.get("topics");
|
||||
const newTopics = TopicList.topicsFrom(this.store, result);
|
||||
|
||||
self.forEachNew(newTopics, function(t) {
|
||||
this.forEachNew(newTopics, t => {
|
||||
t.set("highlight", topicsAdded++ === 0);
|
||||
topics.pushObject(t);
|
||||
this.topics.pushObject(t);
|
||||
});
|
||||
|
||||
self.setProperties({
|
||||
this.setProperties({
|
||||
loadingMore: false,
|
||||
more_topics_url: result.topic_list.more_topics_url
|
||||
});
|
||||
|
||||
Discourse.Session.currentProp("topicList", self);
|
||||
return self.get("more_topics_url");
|
||||
Discourse.Session.currentProp("topicList", this);
|
||||
return this.more_topics_url;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
@ -102,37 +101,32 @@ const TopicList = RestModel.extend({
|
||||
|
||||
// loads topics with these ids "before" the current topics
|
||||
loadBefore(topic_ids, storeInSession) {
|
||||
const topicList = this,
|
||||
topics = this.topics;
|
||||
|
||||
// refresh dupes
|
||||
topics.removeObjects(
|
||||
topics.filter(topic => topic_ids.indexOf(topic.get("id")) >= 0)
|
||||
this.topics.removeObjects(
|
||||
this.topics.filter(topic => topic_ids.indexOf(topic.id) >= 0)
|
||||
);
|
||||
|
||||
const url = `${Discourse.getURL("/")}${this.get(
|
||||
"filter"
|
||||
)}.json?topic_ids=${topic_ids.join(",")}`;
|
||||
const store = this.store;
|
||||
const url = `${Discourse.getURL("/")}${
|
||||
this.filter
|
||||
}.json?topic_ids=${topic_ids.join(",")}`;
|
||||
|
||||
return ajax({ url, data: this.params }).then(result => {
|
||||
let i = 0;
|
||||
topicList.forEachNew(TopicList.topicsFrom(store, result), function(t) {
|
||||
this.forEachNew(TopicList.topicsFrom(this.store, result), t => {
|
||||
// highlight the first of the new topics so we can get a visual feedback
|
||||
t.set("highlight", true);
|
||||
topics.insertAt(i, t);
|
||||
this.topics.insertAt(i, t);
|
||||
i++;
|
||||
});
|
||||
if (storeInSession) Discourse.Session.currentProp("topicList", topicList);
|
||||
|
||||
if (storeInSession) Discourse.Session.currentProp("topicList", this);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
TopicList.reopenClass({
|
||||
topicsFrom(store, result, opts) {
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
if (!result) return;
|
||||
|
||||
opts = opts || {};
|
||||
let listKey = opts.listKey || "topics";
|
||||
@ -143,9 +137,9 @@ TopicList.reopenClass({
|
||||
users = Model.extractByKey(result.users, Discourse.User),
|
||||
groups = Model.extractByKey(result.primary_groups, Ember.Object);
|
||||
|
||||
return result.topic_list[listKey].map(function(t) {
|
||||
return result.topic_list[listKey].map(t => {
|
||||
t.category = categories.findBy("id", t.category_id);
|
||||
t.posters.forEach(function(p) {
|
||||
t.posters.forEach(p => {
|
||||
p.user = users[p.user_id];
|
||||
p.extraClasses = p.extras;
|
||||
if (p.primary_group_id) {
|
||||
@ -157,11 +151,11 @@ TopicList.reopenClass({
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (t.participants) {
|
||||
t.participants.forEach(function(p) {
|
||||
p.user = users[p.user_id];
|
||||
});
|
||||
t.participants.forEach(p => (p.user = users[p.user_id]));
|
||||
}
|
||||
|
||||
return store.createRecord("topic", t);
|
||||
});
|
||||
},
|
||||
@ -188,7 +182,7 @@ TopicList.reopenClass({
|
||||
},
|
||||
|
||||
find(filter, params) {
|
||||
const store = Discourse.__container__.lookup("service:store");
|
||||
const store = getOwner(this).lookup("service:store");
|
||||
return store.findFiltered("topicList", { filter, params });
|
||||
},
|
||||
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { on } from "ember-addons/ember-computed-decorators";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { url } from "discourse/lib/computed";
|
||||
import UserAction from "discourse/models/user-action";
|
||||
@ -5,13 +6,14 @@ import UserAction from "discourse/models/user-action";
|
||||
export default Discourse.Model.extend({
|
||||
loaded: false,
|
||||
|
||||
_initialize: function() {
|
||||
@on("init")
|
||||
_initialize() {
|
||||
this.setProperties({
|
||||
itemsLoaded: 0,
|
||||
canLoadMore: true,
|
||||
content: []
|
||||
});
|
||||
}.on("init"),
|
||||
},
|
||||
|
||||
url: url(
|
||||
"user.username_lower",
|
||||
@ -40,7 +42,6 @@ export default Discourse.Model.extend({
|
||||
},
|
||||
|
||||
findItems() {
|
||||
const self = this;
|
||||
if (this.loading || !this.canLoadMore) {
|
||||
return Ember.RSVP.reject();
|
||||
}
|
||||
@ -48,21 +49,17 @@ export default Discourse.Model.extend({
|
||||
this.set("loading", true);
|
||||
|
||||
return ajax(this.url, { cache: false })
|
||||
.then(function(result) {
|
||||
.then(result => {
|
||||
if (result) {
|
||||
const posts = result.map(function(post) {
|
||||
return UserAction.create(post);
|
||||
});
|
||||
self.get("content").pushObjects(posts);
|
||||
self.setProperties({
|
||||
const posts = result.map(post => UserAction.create(post));
|
||||
this.content.pushObjects(posts);
|
||||
this.setProperties({
|
||||
loaded: true,
|
||||
itemsLoaded: self.get("itemsLoaded") + posts.length,
|
||||
itemsLoaded: this.itemsLoaded + posts.length,
|
||||
canLoadMore: posts.length > 0
|
||||
});
|
||||
}
|
||||
})
|
||||
.finally(function() {
|
||||
self.set("loading", false);
|
||||
});
|
||||
.finally(() => this.set("loading", false));
|
||||
}
|
||||
});
|
||||
|
||||
@ -271,6 +271,7 @@ const User = RestModel.extend({
|
||||
"email_previous_replies",
|
||||
"dynamic_favicon",
|
||||
"enable_quoting",
|
||||
"enable_defer",
|
||||
"automatically_unpin_topics",
|
||||
"digest_after_minutes",
|
||||
"new_topic_duration_minutes",
|
||||
@ -338,6 +339,7 @@ const User = RestModel.extend({
|
||||
const userProps = Ember.getProperties(
|
||||
this.user_option,
|
||||
"enable_quoting",
|
||||
"enable_defer",
|
||||
"external_links_in_new_tab",
|
||||
"dynamic_favicon"
|
||||
);
|
||||
|
||||
@ -30,15 +30,16 @@ export default Discourse.Route.extend({
|
||||
},
|
||||
|
||||
afterModel(model, transition) {
|
||||
const username =
|
||||
const usernameFromParams =
|
||||
transition.to.queryParams && transition.to.queryParams.username;
|
||||
|
||||
const userBadgesGrant = UserBadge.findByBadgeId(model.get("id"), {
|
||||
username
|
||||
username: usernameFromParams
|
||||
}).then(userBadges => {
|
||||
this.userBadgesGrant = userBadges;
|
||||
});
|
||||
|
||||
const username = this.currentUser && this.currentUser.username_lower;
|
||||
const userBadgesAll = UserBadge.findByUsername(username).then(
|
||||
userBadges => {
|
||||
this.userBadgesAll = userBadges;
|
||||
|
||||
@ -11,16 +11,16 @@ export default Discourse.Route.extend(OpenComposer, {
|
||||
},
|
||||
|
||||
beforeModel(transition) {
|
||||
const user = Discourse.User;
|
||||
const url = transition.intent.url;
|
||||
|
||||
if (
|
||||
(transition.intent.url === "/" ||
|
||||
transition.intent.url === "/latest" ||
|
||||
transition.intent.url === "/categories") &&
|
||||
(url === "/" || url === "/latest" || url === "/categories") &&
|
||||
transition.targetName.indexOf("discovery.top") === -1 &&
|
||||
Discourse.User.currentProp("should_be_redirected_to_top")
|
||||
user.currentProp("should_be_redirected_to_top")
|
||||
) {
|
||||
Discourse.User.currentProp("should_be_redirected_to_top", false);
|
||||
const period =
|
||||
Discourse.User.currentProp("redirect_to_top.period") || "all";
|
||||
user.currentProp("should_be_redirected_to_top", false);
|
||||
const period = user.currentProp("redirected_to_top.period") || "all";
|
||||
this.replaceWith(`discovery.top${period.capitalize()}`);
|
||||
}
|
||||
},
|
||||
|
||||
@ -20,7 +20,8 @@ export default Discourse.Route.extend({
|
||||
filterCategoryId: meta.category_id,
|
||||
filterPriority: meta.priority,
|
||||
reviewableTypes: meta.reviewable_types,
|
||||
filterUsername: meta.username
|
||||
filterUsername: meta.username,
|
||||
filterSortOrder: meta.sort_order
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@ -17,9 +17,8 @@ export default Discourse.Route.extend({
|
||||
params = params || {};
|
||||
params.track_visit = true;
|
||||
|
||||
const self = this,
|
||||
topic = this.modelFor("topic"),
|
||||
postStream = topic.get("postStream"),
|
||||
const topic = this.modelFor("topic"),
|
||||
postStream = topic.postStream,
|
||||
topicController = this.controllerFor("topic"),
|
||||
composerController = this.controllerFor("composer");
|
||||
|
||||
@ -32,7 +31,7 @@ export default Discourse.Route.extend({
|
||||
|
||||
postStream
|
||||
.refresh(params)
|
||||
.then(function() {
|
||||
.then(() => {
|
||||
// TODO we are seeing errors where closest post is null and this is exploding
|
||||
// we need better handling and logging for this condition.
|
||||
|
||||
@ -40,22 +39,20 @@ export default Discourse.Route.extend({
|
||||
const closestPost = postStream.closestPostForPostNumber(
|
||||
params.nearPost || 1
|
||||
);
|
||||
const closest = closestPost.get("post_number");
|
||||
const closest = closestPost.post_number;
|
||||
|
||||
topicController.setProperties({
|
||||
"model.currentPost": closest,
|
||||
enteredIndex: topic
|
||||
.get("postStream")
|
||||
.progressIndexOfPost(closestPost),
|
||||
enteredIndex: topic.postStream.progressIndexOfPost(closestPost),
|
||||
enteredAt: new Date().getTime().toString()
|
||||
});
|
||||
|
||||
topicController.subscribe();
|
||||
|
||||
// Highlight our post after the next render
|
||||
Ember.run.scheduleOnce("afterRender", function() {
|
||||
self.appEvents.trigger("post:highlight", closest);
|
||||
});
|
||||
Ember.run.scheduleOnce("afterRender", () =>
|
||||
this.appEvents.trigger("post:highlight", closest)
|
||||
);
|
||||
|
||||
const opts = {};
|
||||
if (document.location.hash && document.location.hash.length) {
|
||||
@ -63,13 +60,13 @@ export default Discourse.Route.extend({
|
||||
}
|
||||
DiscourseURL.jumpToPost(closest, opts);
|
||||
|
||||
if (!Ember.isEmpty(topic.get("draft"))) {
|
||||
if (!Ember.isEmpty(topic.draft)) {
|
||||
composerController.open({
|
||||
draft: Draft.getLocal(topic.get("draft_key"), topic.get("draft")),
|
||||
draftKey: topic.get("draft_key"),
|
||||
draftSequence: topic.get("draft_sequence"),
|
||||
topic: topic,
|
||||
ignoreIfChanged: true
|
||||
draft: Draft.getLocal(topic.draft_key, topic.draft),
|
||||
draftKey: topic.draft_key,
|
||||
draftSequence: topic.draft_sequence,
|
||||
ignoreIfChanged: true,
|
||||
topic
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
export default Discourse.Route.extend({
|
||||
titleToken() {
|
||||
const username = this.modelFor("user").get("username");
|
||||
const username = this.modelFor("user").username;
|
||||
if (username) {
|
||||
return [I18n.t("user.profile"), username];
|
||||
}
|
||||
@ -32,12 +32,11 @@ export default Discourse.Route.extend({
|
||||
|
||||
model(params) {
|
||||
// If we're viewing the currently logged in user, return that object instead
|
||||
const currentUser = this.currentUser;
|
||||
if (
|
||||
currentUser &&
|
||||
params.username.toLowerCase() === currentUser.get("username_lower")
|
||||
this.currentUser &&
|
||||
params.username.toLowerCase() === this.currentUser.username_lower
|
||||
) {
|
||||
return currentUser;
|
||||
return this.currentUser;
|
||||
}
|
||||
|
||||
return Discourse.User.create({ username: params.username });
|
||||
@ -45,43 +44,38 @@ export default Discourse.Route.extend({
|
||||
|
||||
afterModel() {
|
||||
const user = this.modelFor("user");
|
||||
const self = this;
|
||||
|
||||
return user
|
||||
.findDetails()
|
||||
.then(function() {
|
||||
return user.findStaffInfo();
|
||||
})
|
||||
.catch(function() {
|
||||
return self.replaceWith("/404");
|
||||
});
|
||||
.then(() => user.findStaffInfo())
|
||||
.catch(() => this.replaceWith("/404"));
|
||||
},
|
||||
|
||||
serialize(model) {
|
||||
if (!model) return {};
|
||||
return { username: (Ember.get(model, "username") || "").toLowerCase() };
|
||||
|
||||
return { username: (model.username || "").toLowerCase() };
|
||||
},
|
||||
|
||||
setupController(controller, user) {
|
||||
controller.set("model", user);
|
||||
this.searchService.set("searchContext", user.get("searchContext"));
|
||||
this.searchService.set("searchContext", user.searchContext);
|
||||
},
|
||||
|
||||
activate() {
|
||||
this._super(...arguments);
|
||||
|
||||
const user = this.modelFor("user");
|
||||
this.messageBus.subscribe("/u/" + user.get("username_lower"), function(
|
||||
data
|
||||
) {
|
||||
user.loadUserAction(data);
|
||||
});
|
||||
this.messageBus.subscribe(`/u/${user.username_lower}`, data =>
|
||||
user.loadUserAction(data)
|
||||
);
|
||||
},
|
||||
|
||||
deactivate() {
|
||||
this._super(...arguments);
|
||||
this.messageBus.unsubscribe(
|
||||
"/u/" + this.modelFor("user").get("username_lower")
|
||||
);
|
||||
|
||||
const user = this.modelFor("user");
|
||||
this.messageBus.unsubscribe(`/u/${user.username_lower}`);
|
||||
|
||||
// Remove the search context
|
||||
this.searchService.set("searchContext", null);
|
||||
|
||||
@ -8,7 +8,6 @@
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<label class="control-label"></label>
|
||||
<div class="controls">
|
||||
{{combo-box
|
||||
value=selectedUserBadgeId
|
||||
|
||||
@ -1 +1,5 @@
|
||||
{{input type="text" class="date-picker" placeholder=placeholder value=value}}
|
||||
{{input
|
||||
type=inputType
|
||||
class="date-picker"
|
||||
placeholder=placeholder
|
||||
value=value}}
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
|
||||
<div class="control-group">
|
||||
{{d-icon "far-clock"}}
|
||||
{{input type="time" value=time}}
|
||||
{{input placeholder="--:--" type="time" value=time disabled=timeInputDisabled}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
|
||||
@ -27,6 +27,9 @@
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{{plugin-outlet name="groups-form-membership-below-automatic"
|
||||
args=(hash model=model)}}
|
||||
|
||||
<div class="control-group">
|
||||
<label class="control-label">{{i18n "admin.groups.manage.membership.trust_level"}}</label>
|
||||
<label for="grant_trust_level">{{i18n 'admin.groups.manage.membership.trust_levels_title'}}</label>
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
{{#if value }}
|
||||
<div class={{classes}}>
|
||||
<div class='name'>{{name}}</div>
|
||||
<div class='value'>{{value}}</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
@ -12,9 +12,8 @@
|
||||
<div class='post-contents-wrapper'>
|
||||
{{reviewable-created-by user=reviewable.target_created_by tagName=''}}
|
||||
<div class='post-contents'>
|
||||
{{reviewable-created-by-name user=reviewable.target_created_by tagName=''}}
|
||||
{{reviewable-post-header reviewable=reviewable createdBy=reviewable.target_created_by tagName=''}}
|
||||
<div class='post-body'>
|
||||
|
||||
{{#if reviewable.blank_post}}
|
||||
<p>{{i18n "review.deleted_post"}}</p>
|
||||
{{else}}
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
<div class='reviewable-post-header'>
|
||||
{{reviewable-created-by-name user=createdBy tagName=''}}
|
||||
{{#if reviewable.reply_to_post_number}}
|
||||
<a href={{concat reviewable.topic_url "/" reviewable.reply_to_post_number}} class='reviewable-reply-to'>
|
||||
{{d-icon "share"}}
|
||||
<span>{{i18n "review.in_reply_to"}}</span>
|
||||
</a>
|
||||
{{/if}}
|
||||
</div>
|
||||
@ -1,5 +1,6 @@
|
||||
{{#reviewable-topic-link reviewable=reviewable tagName=''}}
|
||||
<div class="title-text">{{i18n "review.new_topic"}}
|
||||
<div class="title-text">
|
||||
{{d-icon "plus-square" title="review.new_topic"}}
|
||||
{{reviewable.payload.title}}
|
||||
</div>
|
||||
{{category-badge reviewable.category}}
|
||||
@ -10,7 +11,7 @@
|
||||
{{reviewable-created-by user=reviewable.created_by tagName=''}}
|
||||
|
||||
<div class='post-contents'>
|
||||
{{reviewable-created-by-name user=reviewable.created_by tagName=''}}
|
||||
{{reviewable-post-header reviewable=reviewable createdBy=reviewable.created_by tagName=''}}
|
||||
|
||||
<div class='post-body'>
|
||||
{{cook-text reviewable.payload.raw}}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<div class='post-topic'>
|
||||
{{#if reviewable.topic}}
|
||||
{{topic-status topic=reviewable.topic}}
|
||||
<a href={{reviewable.topic_url}} class='title-text'>{{reviewable.topic.title}}</a>
|
||||
<a href={{reviewable.target_url}} class='title-text'>{{reviewable.topic.title}}</a>
|
||||
{{category-badge reviewable.category}}
|
||||
{{reviewable-tags tags=reviewable.topic_tags tagName=''}}
|
||||
{{else if (has-block)}}
|
||||
|
||||
@ -12,21 +12,19 @@
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
{{#if reviewable.payload.name}}
|
||||
<div class='reviewable-user-details name'>
|
||||
<div class='name'>{{i18n "review.user.name"}}</div>
|
||||
<div class='value'>{{reviewable.payload.name}}</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
<div class='reviewable-user-details email'>
|
||||
<div class='name'>{{i18n "review.user.email"}}</div>
|
||||
<div class='value'>{{reviewable.payload.email}}</div>
|
||||
</div>
|
||||
|
||||
{{reviewable-field classes='reviewable-user-details name'
|
||||
name=(i18n 'review.user.name')
|
||||
value=reviewable.payload.name}}
|
||||
|
||||
{{reviewable-field classes='reviewable-user-details email'
|
||||
name=(i18n 'review.user.email')
|
||||
value=reviewable.payload.email}}
|
||||
|
||||
{{#each userFields as |f|}}
|
||||
<div class='reviewable-user-details user-field'>
|
||||
<div class='name'>{{f.name}}</div>
|
||||
<div class='value'>{{f.value}}</div>
|
||||
</div>
|
||||
{{reviewable-field classes='reviewable-user-details user-field'
|
||||
name=f.name
|
||||
value=f.value}}
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
|
||||
@ -64,8 +64,10 @@
|
||||
removeMember=(action "removeMember")
|
||||
makeOwner=(action "makeOwner")
|
||||
removeOwner=(action "removeOwner")
|
||||
member=m}}
|
||||
member=m
|
||||
group=model}}
|
||||
{{/if}}
|
||||
{{!-- group parameter is used by plugins --}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
<p>{{i18n "topic.feature_topic.pin_note"}}</p>
|
||||
{{/if}}
|
||||
<p>{{{unPinMessage}}}</p>
|
||||
<p>{{d-button action=(action "unpin") icon="thumb-tack" label="topic.feature.unpin" class="btn-primary"}}</p>
|
||||
<p>{{d-button action=(action "unpin") icon="thumbtack" label="topic.feature.unpin" class="btn-primary"}}</p>
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
@ -61,7 +61,7 @@
|
||||
</p>
|
||||
{{/if}}
|
||||
<p>
|
||||
{{d-button action=(action "pin") icon="thumb-tack" label="topic.feature.pin" class="btn-primary"}}
|
||||
{{d-button action=(action "pin") icon="thumbtack" label="topic.feature.pin" class="btn-primary"}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -105,7 +105,7 @@
|
||||
</p>
|
||||
{{/if}}
|
||||
<p>
|
||||
{{d-button action=(action "pinGlobally") icon="thumb-tack" label="topic.feature.pin_globally" class="btn-primary"}}
|
||||
{{d-button action=(action "pinGlobally") icon="thumbtack" label="topic.feature.pin_globally" class="btn-primary"}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -135,9 +135,9 @@
|
||||
</p>
|
||||
<p>
|
||||
{{#if model.isBanner}}
|
||||
{{d-button action=(action "removeBanner") icon="thumb-tack" label="topic.feature.remove_banner" class="btn-primary"}}
|
||||
{{d-button action=(action "removeBanner") icon="thumbtack" label="topic.feature.remove_banner" class="btn-primary"}}
|
||||
{{else}}
|
||||
{{d-button action=(action "makeBanner") icon="thumb-tack" label="topic.feature.make_banner" class="btn-primary"}}
|
||||
{{d-button action=(action "makeBanner") icon="thumbtack" label="topic.feature.make_banner" class="btn-primary"}}
|
||||
{{/if}}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -33,7 +33,7 @@
|
||||
<label>{{d-icon "d-muted"}} {{i18n 'user.muted_categories'}}</label>
|
||||
{{category-selector categories=model.mutedCategories blacklist=selectedCategories}}
|
||||
</div>
|
||||
<div class="instructions">{{i18n 'user.muted_categories_instructions'}}</div>
|
||||
<div class="instructions">{{i18n (if hideMutedTags 'user.muted_categories_instructions' 'user.muted_categories_instructions_dont_hide')}}</div>
|
||||
{{#if canSee}}
|
||||
<div class="controls">
|
||||
<a href="{{unbound model.mutedTopicsPath}}">{{i18n 'user.muted_topics_link'}}</a>
|
||||
|
||||
@ -47,17 +47,18 @@
|
||||
<div class="control-group other">
|
||||
<label class="control-label">{{i18n 'user.other_settings'}}</label>
|
||||
|
||||
{{preference-checkbox labelKey="user.external_links_in_new_tab" checked=model.user_option.external_links_in_new_tab}}
|
||||
{{preference-checkbox labelKey="user.enable_quoting" checked=model.user_option.enable_quoting}}
|
||||
{{preference-checkbox labelKey="user.external_links_in_new_tab" checked=model.user_option.external_links_in_new_tab class="pref-external-links"}}
|
||||
{{preference-checkbox labelKey="user.enable_quoting" checked=model.user_option.enable_quoting class="pref-enable-quoting"}}
|
||||
{{preference-checkbox labelKey="user.enable_defer" checked=model.user_option.enable_defer class="pref-defer-undread"}}
|
||||
{{#if siteSettings.automatically_unpin_topics}}
|
||||
{{preference-checkbox labelKey="user.automatically_unpin_topics" checked=model.user_option.automatically_unpin_topics}}
|
||||
{{preference-checkbox labelKey="user.automatically_unpin_topics" checked=model.user_option.automatically_unpin_topics class="pref-auto-unpin"}}
|
||||
{{/if}}
|
||||
{{preference-checkbox labelKey="user.hide_profile_and_presence" checked=model.user_option.hide_profile_and_presence}}
|
||||
{{preference-checkbox labelKey="user.hide_profile_and_presence" checked=model.user_option.hide_profile_and_presence class="pref-hide-profile"}}
|
||||
{{#if isiPad}}
|
||||
{{preference-checkbox labelKey="user.enable_physical_keyboard" checked=disableSafariHacks}}
|
||||
{{preference-checkbox labelKey="user.enable_physical_keyboard" checked=disableSafariHacks class="pref-safari-hacks"}}
|
||||
{{/if}}
|
||||
{{preference-checkbox labelKey="user.dynamic_favicon" checked=model.user_option.dynamic_favicon}}
|
||||
<div class='controls controls-dropdown'>
|
||||
{{preference-checkbox labelKey="user.dynamic_favicon" checked=model.user_option.dynamic_favicon class="pref-dynamic-favicon"}}
|
||||
<div class='controls controls-dropdown pref-page-title'>
|
||||
<label for="user-email-level">{{i18n 'user.title_count_mode.title'}}</label>
|
||||
{{combo-box valueAttribute="value"
|
||||
content=titleCountModes
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<label class="control-label">{{i18n 'user.users'}}</label>
|
||||
<div class="control-group">
|
||||
<div class="control-group user-mute">
|
||||
<div class="controls tracking-controls">
|
||||
<label>
|
||||
{{d-icon "d-muted" class="icon"}}
|
||||
@ -20,8 +20,8 @@
|
||||
</div>
|
||||
|
||||
{{#if ignoredEnabled}}
|
||||
<div class="control-group">
|
||||
<div class="controls tracking-controls">
|
||||
<div class="control-group user-ignore">
|
||||
<div class="controls tracking-controls user-notifications">
|
||||
<label>{{d-icon "eye-slash" class="icon"}} {{i18n 'user.ignored_users'}}</label>
|
||||
{{ignored-user-list model=model items=model.ignored_usernames saving=saved}}
|
||||
</div>
|
||||
|
||||
@ -55,6 +55,11 @@
|
||||
{{d-button label="review.show_all_topics" icon="times" action=(action "resetTopic")}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class='reviewable-filter sort-order'>
|
||||
{{i18n "review.order_by"}}
|
||||
{{combo-box value=filterSortOrder content=sortOrders}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class='reviewable-filters-actions'>
|
||||
|
||||
@ -301,6 +301,7 @@
|
||||
showFlagTopic=(route-action "showFlagTopic")
|
||||
toggleArchiveMessage=(action "toggleArchiveMessage")
|
||||
editFirstPost=(action "editFirstPost")
|
||||
deferTopic=(action "deferTopic")
|
||||
replyToPost=(action "replyToPost")}}
|
||||
{{else}}
|
||||
<div id="topic-footer-buttons">
|
||||
|
||||
@ -13,11 +13,6 @@ import { setTransientHeader } from "discourse/lib/ajax";
|
||||
import { userPath } from "discourse/lib/url";
|
||||
import { iconNode } from "discourse-common/lib/icon-library";
|
||||
|
||||
const LIKED_TYPE = 5;
|
||||
const INVITED_TYPE = 8;
|
||||
const GROUP_SUMMARY_TYPE = 16;
|
||||
export const LIKED_CONSOLIDATED_TYPE = 19;
|
||||
|
||||
createWidget("notification-item", {
|
||||
tagName: "li",
|
||||
|
||||
@ -35,6 +30,7 @@ createWidget("notification-item", {
|
||||
url() {
|
||||
const attrs = this.attrs;
|
||||
const data = attrs.data;
|
||||
const notificationTypes = this.site.notification_types;
|
||||
|
||||
const badgeId = data.badge_id;
|
||||
if (badgeId) {
|
||||
@ -58,11 +54,11 @@ createWidget("notification-item", {
|
||||
return postUrl(attrs.slug, topicId, attrs.post_number);
|
||||
}
|
||||
|
||||
if (attrs.notification_type === INVITED_TYPE) {
|
||||
if (attrs.notification_type === notificationTypes.invitee_accepted) {
|
||||
return userPath(data.display_username);
|
||||
}
|
||||
|
||||
if (attrs.notification_type === LIKED_CONSOLIDATED_TYPE) {
|
||||
if (attrs.notification_type === notificationTypes.liked_consolidated) {
|
||||
return userPath(
|
||||
`${this.attrs.username ||
|
||||
this.currentUser
|
||||
@ -95,7 +91,10 @@ createWidget("notification-item", {
|
||||
|
||||
let title;
|
||||
|
||||
if (this.attrs.notification_type === LIKED_CONSOLIDATED_TYPE) {
|
||||
if (
|
||||
this.attrs.notification_type ===
|
||||
this.site.notification_types.liked_consolidated
|
||||
) {
|
||||
title = I18n.t("notifications.liked_consolidated_description", {
|
||||
count: parseInt(data.count)
|
||||
});
|
||||
@ -112,7 +111,9 @@ createWidget("notification-item", {
|
||||
const scope =
|
||||
notName === "custom" ? data.message : `notifications.${notName}`;
|
||||
|
||||
if (notificationType === GROUP_SUMMARY_TYPE) {
|
||||
const notificationTypes = this.site.notification_types;
|
||||
|
||||
if (notificationType === notificationTypes.group_message_summary) {
|
||||
const count = data.inbox_count;
|
||||
const group_name = data.group_name;
|
||||
return I18n.t(scope, { count, group_name });
|
||||
@ -121,7 +122,7 @@ createWidget("notification-item", {
|
||||
const username = formatUsername(data.display_username);
|
||||
const description = this.description();
|
||||
|
||||
if (notificationType === LIKED_TYPE && data.count > 1) {
|
||||
if (notificationType === notificationTypes.liked && data.count > 1) {
|
||||
const count = data.count - 2;
|
||||
const username2 = formatUsername(data.username2);
|
||||
|
||||
@ -147,13 +148,26 @@ createWidget("notification-item", {
|
||||
html(attrs) {
|
||||
const notificationType = attrs.notification_type;
|
||||
const lookup = this.site.get("notificationLookup");
|
||||
const notName = lookup[notificationType];
|
||||
const notificationName = lookup[notificationType];
|
||||
|
||||
let { data } = attrs;
|
||||
let infoKey = notName === "custom" ? data.message : notName;
|
||||
let text = emojiUnescape(this.text(notificationType, notName));
|
||||
let infoKey =
|
||||
notificationName === "custom" ? data.message : notificationName;
|
||||
let text = emojiUnescape(this.text(notificationType, notificationName));
|
||||
let icon = iconNode(`notification.${infoKey}`);
|
||||
|
||||
let title;
|
||||
|
||||
if (notificationName) {
|
||||
if (notificationName === "custom") {
|
||||
title = data.title ? I18n.t(data.title) : "";
|
||||
} else {
|
||||
title = I18n.t(`notifications.titles.${notificationName}`);
|
||||
}
|
||||
} else {
|
||||
title = "";
|
||||
}
|
||||
|
||||
// We can use a `<p>` tag here once other languages have fixed their HTML
|
||||
// translations.
|
||||
let html = new RawHtml({ html: `<div>${text}</div>` });
|
||||
@ -162,7 +176,11 @@ createWidget("notification-item", {
|
||||
|
||||
const href = this.url();
|
||||
return href
|
||||
? h("a", { attributes: { href, "data-auto-route": true } }, contents)
|
||||
? h(
|
||||
"a",
|
||||
{ attributes: { href, title, "data-auto-route": true } },
|
||||
contents
|
||||
)
|
||||
: contents;
|
||||
},
|
||||
|
||||
|
||||
@ -131,39 +131,47 @@ I18n.interpolate = function(message, options) {
|
||||
|
||||
I18n.translate = function(scope, options) {
|
||||
options = this.prepareOptions(options);
|
||||
options.needsPluralization = typeof options.count === "number";
|
||||
options.ignoreMissing = !this.noFallbacks;
|
||||
|
||||
var translation = this.lookup(scope, options);
|
||||
var translation = this.findTranslation(scope, options);
|
||||
|
||||
if (!this.noFallbacks) {
|
||||
if (!translation && this.fallbackLocale) {
|
||||
options.locale = this.fallbackLocale;
|
||||
translation = this.lookup(scope, options);
|
||||
translation = this.findTranslation(scope, options);
|
||||
}
|
||||
|
||||
options.ignoreMissing = false;
|
||||
|
||||
if (!translation && this.currentLocale() !== this.defaultLocale) {
|
||||
options.locale = this.defaultLocale;
|
||||
translation = this.lookup(scope, options);
|
||||
translation = this.findTranslation(scope, options);
|
||||
}
|
||||
|
||||
if (!translation && this.currentLocale() !== 'en') {
|
||||
options.locale = 'en';
|
||||
translation = this.lookup(scope, options);
|
||||
translation = this.findTranslation(scope, options);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof translation === "object") {
|
||||
if (typeof options.count === "number") {
|
||||
return this.pluralize(translation, scope, options);
|
||||
} else {
|
||||
return translation;
|
||||
}
|
||||
} else {
|
||||
return this.interpolate(translation, options);
|
||||
}
|
||||
return this.interpolate(translation, options);
|
||||
} catch (error) {
|
||||
return this.missingTranslation(scope);
|
||||
}
|
||||
};
|
||||
|
||||
I18n.findTranslation = function(scope, options) {
|
||||
var translation = this.lookup(scope, options);
|
||||
|
||||
if (translation && options.needsPluralization) {
|
||||
translation = this.pluralize(translation, scope, options);
|
||||
}
|
||||
|
||||
return translation;
|
||||
};
|
||||
|
||||
I18n.toNumber = function(number, options) {
|
||||
options = this.prepareOptions(
|
||||
options,
|
||||
@ -260,6 +268,8 @@ I18n.findAndTranslateValidNode = function(keys, translation) {
|
||||
};
|
||||
|
||||
I18n.pluralize = function(translation, scope, options) {
|
||||
if (typeof translation !== "object") return translation;
|
||||
|
||||
options = this.prepareOptions(options);
|
||||
var count = options.count.toString();
|
||||
|
||||
@ -268,9 +278,12 @@ I18n.pluralize = function(translation, scope, options) {
|
||||
var keys = ((typeof key === "object") && (key instanceof Array)) ? key : [key];
|
||||
|
||||
var message = this.findAndTranslateValidNode(keys, translation);
|
||||
if (message == null) message = this.missingTranslation(scope, keys[0]);
|
||||
|
||||
return this.interpolate(message, options);
|
||||
if (message !== null || options.ignoreMissing) {
|
||||
return message;
|
||||
}
|
||||
|
||||
return this.missingTranslation(scope, keys[0]);
|
||||
};
|
||||
|
||||
I18n.missingTranslation = function(scope, key) {
|
||||
|
||||
@ -236,7 +236,7 @@ function applyEmoji(
|
||||
|
||||
export function setup(helper) {
|
||||
helper.registerOptions((opts, siteSettings, state) => {
|
||||
opts.features.emoji = !!siteSettings.enable_emoji;
|
||||
opts.features.emoji = !state.disableEmojis && !!siteSettings.enable_emoji;
|
||||
opts.features.emojiShortcuts = !!siteSettings.enable_emoji_shortcuts;
|
||||
opts.features.inlineEmoji = !!siteSettings.enable_inline_emoji_translation;
|
||||
opts.emojiSet = siteSettings.emoji_set || "";
|
||||
|
||||
@ -29,7 +29,8 @@ export function buildOptions(state) {
|
||||
lookupUploadUrls,
|
||||
previewing,
|
||||
linkify,
|
||||
censoredWords
|
||||
censoredWords,
|
||||
disableEmojis
|
||||
} = state;
|
||||
|
||||
let features = {
|
||||
@ -76,7 +77,8 @@ export function buildOptions(state) {
|
||||
markdownIt: true,
|
||||
injectLineNumbersToPreview:
|
||||
siteSettings.enable_advanced_editor_preview_sync,
|
||||
previewing
|
||||
previewing,
|
||||
disableEmojis
|
||||
};
|
||||
|
||||
// note, this will mutate options due to the way the API is designed
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
export default Ember.Mixin.create({
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
import { on } from "ember-addons/ember-computed-decorators";
|
||||
|
||||
const { bind } = Ember.run;
|
||||
|
||||
export default Ember.Mixin.create({
|
||||
@on("init")
|
||||
_initKeys() {
|
||||
this.keys = {
|
||||
TAB: 9,
|
||||
ENTER: 13,
|
||||
@ -13,27 +16,64 @@ export default Ember.Mixin.create({
|
||||
RIGHT: 39,
|
||||
A: 65
|
||||
};
|
||||
|
||||
this._boundMouseDownHandler = bind(this, this._mouseDownHandler);
|
||||
this._boundFocusHeaderHandler = bind(this, this._focusHeaderHandler);
|
||||
this._boundKeydownHeaderHandler = bind(this, this._keydownHeaderHandler);
|
||||
this._boundKeypressHeaderHandler = bind(this, this._keypressHeaderHandler);
|
||||
this._boundChangeFilterInputHandler = bind(
|
||||
this,
|
||||
this._changeFilterInputHandler
|
||||
);
|
||||
this._boundKeypressFilterInputHandler = bind(
|
||||
this,
|
||||
this._keypressFilterInputHandler
|
||||
);
|
||||
this._boundFocusoutFilterInputHandler = bind(
|
||||
this,
|
||||
this._focusoutFilterInputHandler
|
||||
);
|
||||
this._boundKeydownFilterInputHandler = bind(
|
||||
this,
|
||||
this._keydownFilterInputHandler
|
||||
);
|
||||
},
|
||||
|
||||
willDestroyElement() {
|
||||
this._super(...arguments);
|
||||
@on("didInsertElement")
|
||||
_setupEvents() {
|
||||
$(document).on("mousedown.select-kit", this._boundMouseDownHandler);
|
||||
|
||||
$(document).off("mousedown.select-kit", this._mouseDownHandler);
|
||||
this.$header()
|
||||
.on("blur.select-kit", this._boundBlurHeaderHandler)
|
||||
.on("focus.select-kit", this._boundFocusHeaderHandler)
|
||||
.on("keydown.select-kit", this._boundKeydownHeaderHandler)
|
||||
.on("keypress.select-kit", this._boundKeypressHeaderHandler);
|
||||
|
||||
this.$filterInput()
|
||||
.on("change.select-kit", this._boundChangeFilterInputHandler)
|
||||
.on("keypress.select-kit", this._boundKeypressFilterInputHandler)
|
||||
.on("focusout.select-kit", this._boundFocusoutFilterInputHandler)
|
||||
.on("keydown.select-kit", this._boundKeydownFilterInputHandler);
|
||||
},
|
||||
|
||||
@on("willDestroyElement")
|
||||
_cleanUpEvents() {
|
||||
$(document).off("mousedown.select-kit", this._boundMouseDownHandler);
|
||||
|
||||
if (this.$header().length) {
|
||||
this.$header()
|
||||
.off("blur.select-kit", this._blurHeaderHandler)
|
||||
.off("focus.select-kit", this._focusHeaderHandler)
|
||||
.off("keydown.select-kit", this._keydownHeaderHandler)
|
||||
.off("keypress.select-kit", this._keypressHeaderHandler);
|
||||
.off("blur.select-kit", this._boundBlurHeaderHandler)
|
||||
.off("focus.select-kit", this._boundFocusHeaderHandler)
|
||||
.off("keydown.select-kit", this._boundKeydownHeaderHandler)
|
||||
.off("keypress.select-kit", this._boundKeypressHeaderHandler);
|
||||
}
|
||||
|
||||
if (this.$filterInput().length) {
|
||||
this.$filterInput()
|
||||
.off("change.select-kit", this._changeFilterInputHandler)
|
||||
.off("keydown.select-kit", this._keydownFilterInputHandler)
|
||||
.off("keypress.select-kit", this._keypressFilterInputHandler)
|
||||
.off("focusout.select-kit", this._focusoutFilterInputHandler);
|
||||
.off("change.select-kit", this._boundChangeFilterInputHandler)
|
||||
.off("keypress.select-kit", this._boundKeypressFilterInputHandler)
|
||||
.off("focusout.select-kit", this._boundFocusoutFilterInputHandler)
|
||||
.off("keydown.select-kit", this._boundKeydownFilterInputHandler);
|
||||
}
|
||||
},
|
||||
|
||||
@ -145,24 +185,6 @@ export default Ember.Mixin.create({
|
||||
this.onFilterInputFocusout(event);
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
|
||||
$(document).on("mousedown.select-kit", this._mouseDownHandler.bind(this));
|
||||
|
||||
this.$header()
|
||||
.on("blur.select-kit", this._blurHeaderHandler.bind(this))
|
||||
.on("focus.select-kit", this._focusHeaderHandler.bind(this))
|
||||
.on("keydown.select-kit", this._keydownHeaderHandler.bind(this))
|
||||
.on("keypress.select-kit", this._keypressHeaderHandler.bind(this));
|
||||
|
||||
this.$filterInput()
|
||||
.on("change.select-kit", this._changeFilterInputHandler.bind(this))
|
||||
.on("keypress.select-kit", this._keypressFilterInputHandler.bind(this))
|
||||
.on("focusout.select-kit", this._focusoutFilterInputHandler.bind(this))
|
||||
.on("keydown.select-kit", this._keydownFilterInputHandler.bind(this));
|
||||
},
|
||||
|
||||
didPressTab(event) {
|
||||
if (this.$highlightedRow().length && this.isExpanded) {
|
||||
this.close(event);
|
||||
|
||||
@ -0,0 +1,29 @@
|
||||
import { POPULAR_THEMES } from "discourse-common/helpers/popular-themes";
|
||||
|
||||
export default Ember.Component.extend({
|
||||
classNames: ["popular-themes"],
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
|
||||
this.popular_components = this.selectedThemeComponents();
|
||||
},
|
||||
|
||||
selectedThemeComponents() {
|
||||
return this.shuffle()
|
||||
.filter(theme => theme.component)
|
||||
.slice(0, 5);
|
||||
},
|
||||
|
||||
shuffle() {
|
||||
let array = POPULAR_THEMES;
|
||||
|
||||
// https://stackoverflow.com/a/12646864
|
||||
for (let i = array.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[array[i], array[j]] = [array[j], array[i]];
|
||||
}
|
||||
|
||||
return array;
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,5 @@
|
||||
{{#each popular_components as |theme|}}
|
||||
<a class="popular-theme-item" href="{{theme.meta_url}}" target="_blank">
|
||||
{{theme.name}}
|
||||
</a>
|
||||
{{/each}}
|
||||
@ -6,28 +6,115 @@ body.crawler {
|
||||
z-index: z("max");
|
||||
background-color: #fff;
|
||||
padding: 10px;
|
||||
box-shadow: shadow("header");
|
||||
box-shadow: none;
|
||||
border-bottom: 1px solid $primary-low-mid;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
div.topic-list div[itemprop="itemListElement"] {
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #e9e9e9;
|
||||
.page-links a {
|
||||
padding: 0 4px;
|
||||
}
|
||||
}
|
||||
|
||||
div#main-outlet {
|
||||
div.post {
|
||||
word-break: break-word;
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
footer nav {
|
||||
margin: 50px 0;
|
||||
|
||||
.raw-topic-link {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
|
||||
.topic-list {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 3em;
|
||||
border-top: 1px solid $primary-low-mid;
|
||||
}
|
||||
}
|
||||
|
||||
.crawler-topic-title {
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.crawler-post {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 2em;
|
||||
padding-top: 1.5em;
|
||||
border-top: 1px solid $primary-low;
|
||||
}
|
||||
|
||||
.crawler-post-meta {
|
||||
margin-bottom: 1em;
|
||||
.creator {
|
||||
word-break: break-all;
|
||||
a {
|
||||
padding: 15px;
|
||||
font-weight: bold;
|
||||
}
|
||||
@include breakpoint(tablet) {
|
||||
display: inline-block;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.crawler-post-infos {
|
||||
color: $primary-high;
|
||||
display: inline-block;
|
||||
@include breakpoint(tablet, min-width) {
|
||||
float: right;
|
||||
}
|
||||
[itemprop="position"] {
|
||||
float: left;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
#breadcrumbs {
|
||||
margin-bottom: 0.5em;
|
||||
font-size: $font-up-1;
|
||||
> div {
|
||||
margin-bottom: 0.15em;
|
||||
}
|
||||
.badge-category-bg {
|
||||
background-color: $secondary-high;
|
||||
}
|
||||
.category-title {
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
.crawler-tags-list {
|
||||
span {
|
||||
display: block;
|
||||
margin-bottom: 0.15em;
|
||||
}
|
||||
}
|
||||
|
||||
.crawler-linkback-list {
|
||||
margin-top: 1em;
|
||||
a {
|
||||
display: block;
|
||||
padding: 0.5em 0;
|
||||
border-top: 1px solid $primary-low;
|
||||
}
|
||||
}
|
||||
|
||||
.crawler-nav {
|
||||
margin: 1em 0;
|
||||
ul {
|
||||
margin: 0;
|
||||
list-style-type: none;
|
||||
}
|
||||
li {
|
||||
display: inline-block;
|
||||
}
|
||||
a {
|
||||
display: inline-block;
|
||||
padding: 0.5em 1em 0.5em 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -185,6 +185,7 @@ input,
|
||||
select,
|
||||
textarea {
|
||||
color: $primary;
|
||||
caret-color: currentColor;
|
||||
|
||||
&[class*="span"] {
|
||||
float: none;
|
||||
@ -667,6 +668,7 @@ table {
|
||||
}
|
||||
|
||||
.auth-token-icon {
|
||||
color: $primary-medium;
|
||||
font-size: 2.25em;
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
@ -697,7 +699,7 @@ table {
|
||||
background: transparent;
|
||||
|
||||
.d-icon {
|
||||
color: $primary;
|
||||
color: $primary-high;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
}
|
||||
input.date-picker,
|
||||
input[type="time"] {
|
||||
width: 150px;
|
||||
width: 200px;
|
||||
text-align: left;
|
||||
}
|
||||
.radios {
|
||||
@ -49,6 +49,10 @@
|
||||
font-size: $font-up-1;
|
||||
}
|
||||
}
|
||||
|
||||
input[disabled] {
|
||||
background: $primary-low;
|
||||
}
|
||||
}
|
||||
.pika-single {
|
||||
position: absolute !important; /* inline JS styles */
|
||||
|
||||
@ -389,6 +389,23 @@
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
.reviewable-post-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
max-width: $topic-body-width;
|
||||
width: $topic-body-width;
|
||||
align-items: center;
|
||||
|
||||
.reviewable-reply-to {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: $primary-medium;
|
||||
font-size: 0.9em;
|
||||
.d-icon {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.post-contents {
|
||||
width: 100%;
|
||||
|
||||
@ -705,6 +705,7 @@ blockquote > *:last-child {
|
||||
font-weight: bold;
|
||||
font-size: $font-down-1;
|
||||
color: $primary-medium;
|
||||
min-width: 0; // Allows flex container to shrink
|
||||
|
||||
.custom-message {
|
||||
flex: 1 1 100%;
|
||||
@ -712,6 +713,8 @@ blockquote > *:last-child {
|
||||
font-weight: normal;
|
||||
font-size: $font-up-1;
|
||||
order: 12;
|
||||
word-break: break-word;
|
||||
min-width: 0; // Allows content like oneboxes to shrink
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@ -892,22 +892,3 @@ span.highlighted {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.crawler-post {
|
||||
.post-time {
|
||||
float: right;
|
||||
}
|
||||
.post-likes {
|
||||
float: right;
|
||||
}
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
#breadcrumbs {
|
||||
.badge-category-bg {
|
||||
background-color: $secondary-high;
|
||||
}
|
||||
.category-title {
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
@ -104,7 +104,7 @@
|
||||
&.show-preview {
|
||||
.d-editor-preview-wrapper {
|
||||
position: fixed;
|
||||
z-index: z("base") + 1;
|
||||
z-index: z("fullscreen");
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
@ -122,7 +122,7 @@
|
||||
position: fixed;
|
||||
right: 5px;
|
||||
bottom: 5px;
|
||||
z-index: z("base") + 2;
|
||||
z-index: z("fullscreen") + 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -464,6 +464,25 @@ body.wizard {
|
||||
}
|
||||
}
|
||||
|
||||
.wizard-step-themes-further-reading {
|
||||
.wizard-field .input-area {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.popular-themes {
|
||||
display: flex;
|
||||
a.popular-theme-item {
|
||||
background: #f9f9f9;
|
||||
padding: 8px;
|
||||
margin: 0px 4px;
|
||||
width: 25%;
|
||||
&:hover {
|
||||
background: #f3f3f3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.textarea-field {
|
||||
textarea {
|
||||
width: 100%;
|
||||
@ -591,6 +610,17 @@ body.wizard {
|
||||
.wizard-column-contents {
|
||||
padding: 1em !important;
|
||||
}
|
||||
.wizard-step-themes-further-reading {
|
||||
.popular-themes {
|
||||
a.popular-theme-item {
|
||||
width: 33.3%;
|
||||
&:nth-child(4),
|
||||
&:nth-child(5) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.emoji-preview img {
|
||||
width: 16px !important;
|
||||
height: 16px !important;
|
||||
|
||||
@ -130,7 +130,7 @@ class Admin::GroupsController < Admin::AdminController
|
||||
private
|
||||
|
||||
def group_params
|
||||
params.require(:group).permit(
|
||||
permitted = [
|
||||
:name,
|
||||
:mentionable_level,
|
||||
:messageable_level,
|
||||
@ -153,6 +153,10 @@ class Admin::GroupsController < Admin::AdminController
|
||||
:membership_request_template,
|
||||
:owner_usernames,
|
||||
:usernames
|
||||
)
|
||||
]
|
||||
custom_fields = Group.editable_group_custom_fields
|
||||
permitted << { custom_fields: custom_fields } unless custom_fields.blank?
|
||||
|
||||
params.require(:group).permit(permitted)
|
||||
end
|
||||
end
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
class Admin::StaffActionLogsController < Admin::AdminController
|
||||
|
||||
def index
|
||||
filters = params.slice(*UserHistory.staff_filters)
|
||||
filters = params.slice(*UserHistory.staff_filters + [:page, :limit])
|
||||
|
||||
staff_action_logs = UserHistory.staff_action_records(current_user, filters).to_a
|
||||
render json: StaffActionLogsSerializer.new({
|
||||
|
||||
@ -550,9 +550,9 @@ class Admin::UsersController < Admin::AdminController
|
||||
if post = Post.where(id: params[:post_id]).first
|
||||
case params[:post_action]
|
||||
when 'delete'
|
||||
PostDestroyer.new(current_user, post).destroy
|
||||
PostDestroyer.new(current_user, post).destroy if guardian.can_delete_post_or_topic?(post)
|
||||
when "delete_replies"
|
||||
PostDestroyer.delete_with_replies(current_user, post)
|
||||
PostDestroyer.delete_with_replies(current_user, post) if guardian.can_delete_post_or_topic?(post)
|
||||
when 'edit'
|
||||
revisor = PostRevisor.new(post)
|
||||
|
||||
|
||||
@ -79,7 +79,9 @@ class ApplicationController < ActionController::Base
|
||||
request.user_agent &&
|
||||
(request.content_type.blank? || request.content_type.include?('html')) &&
|
||||
!['json', 'rss'].include?(params[:format]) &&
|
||||
(has_escaped_fragment? || CrawlerDetection.crawler?(request.user_agent) || params.key?("print"))
|
||||
(has_escaped_fragment? || params.key?("print") ||
|
||||
CrawlerDetection.crawler?(request.user_agent, request.headers["HTTP_VIA"])
|
||||
)
|
||||
end
|
||||
|
||||
def perform_refresh_session
|
||||
@ -728,24 +730,30 @@ class ApplicationController < ActionController::Base
|
||||
# save original URL in a session so we can redirect after login
|
||||
session[:destination_url] = destination_url
|
||||
redirect_to path('/session/sso')
|
||||
return
|
||||
elsif params[:authComplete].present?
|
||||
redirect_to path("/login?authComplete=true")
|
||||
return
|
||||
else
|
||||
# save original URL in a cookie (javascript redirects after login in this case)
|
||||
cookies[:destination_url] = destination_url
|
||||
redirect_to path("/login")
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
if current_user &&
|
||||
!current_user.totp_enabled? &&
|
||||
check_totp = current_user &&
|
||||
!request.format.json? &&
|
||||
!is_api? &&
|
||||
((SiteSetting.enforce_second_factor == 'staff' && current_user.staff?) ||
|
||||
SiteSetting.enforce_second_factor == 'all')
|
||||
SiteSetting.enforce_second_factor == 'all') &&
|
||||
!current_user.totp_enabled?
|
||||
|
||||
if check_totp
|
||||
redirect_path = "#{GlobalSetting.relative_url_root}/u/#{current_user.username}/preferences/second-factor"
|
||||
if !request.fullpath.start_with?(redirect_path)
|
||||
redirect_to path(redirect_path)
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -545,6 +545,9 @@ class GroupsController < ApplicationController
|
||||
:automatic_membership_email_domains,
|
||||
:automatic_membership_retroactive
|
||||
])
|
||||
|
||||
custom_fields = Group.editable_group_custom_fields
|
||||
default_params << { custom_fields: custom_fields } unless custom_fields.blank?
|
||||
end
|
||||
|
||||
default_params
|
||||
|
||||
@ -172,24 +172,31 @@ class InvitesController < ApplicationController
|
||||
def upload_csv
|
||||
guardian.ensure_can_bulk_invite_to_forum!(current_user)
|
||||
|
||||
file = params[:file] || params[:files].first
|
||||
name = params[:name] || File.basename(file.original_filename, ".*")
|
||||
extension = File.extname(file.original_filename)
|
||||
hijack do
|
||||
begin
|
||||
file = params[:file] || params[:files].first
|
||||
|
||||
begin
|
||||
data = if extension.downcase == ".csv"
|
||||
path = Invite.create_csv(file, name)
|
||||
Jobs.enqueue(:bulk_invite, filename: "#{name}#{extension}", current_user_id: current_user.id)
|
||||
{ url: path }
|
||||
else
|
||||
failed_json.merge(errors: [I18n.t("bulk_invite.file_should_be_csv")])
|
||||
if File.read(file.tempfile).scan(/\n/).count.to_i > 50000
|
||||
return render json: failed_json.merge(errors: [I18n.t("bulk_invite.max_rows")]), status: 422
|
||||
end
|
||||
|
||||
invites = []
|
||||
CSV.foreach(file.tempfile) do |row|
|
||||
invite_hash = { email: row[0], groups: row[1], topic_id: row[2] }
|
||||
if invite_hash[:email].present?
|
||||
invites.push(invite_hash)
|
||||
end
|
||||
end
|
||||
if invites.present?
|
||||
Jobs.enqueue(:bulk_invite, invites: invites, current_user_id: current_user.id)
|
||||
render json: success_json
|
||||
else
|
||||
render json: failed_json.merge(errors: [I18n.t("bulk_invite.error")]), status: 422
|
||||
end
|
||||
rescue
|
||||
render json: failed_json.merge(errors: [I18n.t("bulk_invite.error")]), status: 422
|
||||
end
|
||||
rescue
|
||||
failed_json.merge(errors: [I18n.t("bulk_invite.error")])
|
||||
end
|
||||
MessageBus.publish("/uploads/csv", data.as_json, user_ids: [current_user.id])
|
||||
|
||||
render json: success_json
|
||||
end
|
||||
|
||||
def fetch_username
|
||||
|
||||
@ -433,24 +433,24 @@ class ListController < ApplicationController
|
||||
end
|
||||
|
||||
def self.best_period_with_topics_for(previous_visit_at, category_id = nil, default_period = SiteSetting.top_page_default_timeframe)
|
||||
best_periods_for(previous_visit_at, default_period.to_sym).each do |period|
|
||||
best_periods_for(previous_visit_at, default_period.to_sym).find do |period|
|
||||
top_topics = TopTopic.where("#{period}_score > 0")
|
||||
top_topics = top_topics.joins(:topic).where("topics.category_id = ?", category_id) if category_id
|
||||
top_topics = top_topics.limit(SiteSetting.topics_per_period_in_top_page)
|
||||
return period if top_topics.count == SiteSetting.topics_per_period_in_top_page
|
||||
top_topics.count == SiteSetting.topics_per_period_in_top_page
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
def self.best_periods_for(date, default_period = :all)
|
||||
date ||= 1.year.ago
|
||||
return [default_period, :all].uniq unless date
|
||||
|
||||
periods = []
|
||||
periods << default_period if :all != default_period
|
||||
periods << :daily if :daily != default_period && date > 8.days.ago
|
||||
periods << :weekly if :weekly != default_period && date > 35.days.ago
|
||||
periods << :monthly if :monthly != default_period && date > 180.days.ago
|
||||
periods << :yearly if :yearly != default_period
|
||||
periods << :daily if date > (1.week + 1.day).ago
|
||||
periods << :weekly if date > (1.month + 1.week).ago
|
||||
periods << :monthly if date > (3.months + 3.weeks).ago
|
||||
periods << :quarterly if date > (1.year + 1.month).ago
|
||||
periods << :yearly if date > 3.years.ago
|
||||
periods << :all
|
||||
periods
|
||||
end
|
||||
|
||||
|
||||
@ -26,7 +26,8 @@ class ReviewablesController < ApplicationController
|
||||
topic_id: topic_id,
|
||||
priority: params[:priority],
|
||||
username: params[:username],
|
||||
type: params[:type]
|
||||
type: params[:type],
|
||||
sort_order: params[:sort_order]
|
||||
}
|
||||
|
||||
total_rows = Reviewable.list_for(current_user, filters).count
|
||||
|
||||
@ -103,7 +103,21 @@ class SessionController < ApplicationController
|
||||
skip_before_action :check_xhr, only: [:become]
|
||||
|
||||
def become
|
||||
|
||||
raise Discourse::InvalidAccess if Rails.env.production?
|
||||
|
||||
if ENV['DISCOURSE_DEV_ALLOW_ANON_TO_IMPERSONATE'] != "1"
|
||||
render(content_type: 'text/plain', inline: <<~TEXT)
|
||||
To enable impersonating any user without typing passwords set the following ENV var
|
||||
|
||||
export DISCOURSE_DEV_ALLOW_ANON_TO_IMPERSONATE=1
|
||||
|
||||
You can do that in your bashrc of bash profile file or the script you use to launch the web server
|
||||
TEXT
|
||||
|
||||
return
|
||||
end
|
||||
|
||||
user = User.find_by_username(params[:session_id])
|
||||
raise "User #{params[:session_id]} not found" if user.blank?
|
||||
|
||||
|
||||
@ -142,7 +142,8 @@ class StaticController < ApplicationController
|
||||
file&.read || ""
|
||||
rescue => e
|
||||
AdminDashboardData.add_problem_message('dashboard.bad_favicon_url', 1800)
|
||||
Rails.logger.warn("Failed to fetch faivcon #{favicon.url}: #{e}\n#{e.backtrace}")
|
||||
Rails.logger.warn("Failed to fetch favicon #{favicon.url}: #{e}\n#{e.backtrace}")
|
||||
""
|
||||
ensure
|
||||
file&.unlink
|
||||
end
|
||||
|
||||
@ -27,6 +27,34 @@ class TagsController < ::ApplicationController
|
||||
@description_meta = I18n.t("tags.title")
|
||||
@title = @description_meta
|
||||
|
||||
show_all_tags = guardian.can_admin_tags? && guardian.is_admin?
|
||||
|
||||
if SiteSetting.tags_listed_by_group
|
||||
ungrouped_tags = Tag.where("tags.id NOT IN (SELECT tag_id FROM tag_group_memberships)")
|
||||
ungrouped_tags = ungrouped_tags.where("tags.topic_count > 0") unless show_all_tags
|
||||
|
||||
grouped_tag_counts = TagGroup.visible(guardian).order('name ASC').includes(:tags).map do |tag_group|
|
||||
{ id: tag_group.id, name: tag_group.name, tags: self.class.tag_counts_json(tag_group.tags) }
|
||||
end
|
||||
|
||||
@tags = self.class.tag_counts_json(ungrouped_tags)
|
||||
@extras = { tag_groups: grouped_tag_counts }
|
||||
else
|
||||
tags = show_all_tags ? Tag.all : Tag.where("tags.topic_count > 0")
|
||||
unrestricted_tags = DiscourseTagging.filter_visible(tags, guardian)
|
||||
|
||||
categories = Category.where("id IN (SELECT category_id FROM category_tags)")
|
||||
.where("id IN (?)", guardian.allowed_category_ids)
|
||||
.includes(:tags)
|
||||
|
||||
category_tag_counts = categories.map do |c|
|
||||
{ id: c.id, tags: self.class.tag_counts_json(c.tags) }
|
||||
end
|
||||
|
||||
@tags = self.class.tag_counts_json(unrestricted_tags)
|
||||
@extras = { categories: category_tag_counts }
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
|
||||
format.html do
|
||||
@ -34,37 +62,10 @@ class TagsController < ::ApplicationController
|
||||
end
|
||||
|
||||
format.json do
|
||||
show_all_tags = guardian.can_admin_tags? && guardian.is_admin?
|
||||
|
||||
if SiteSetting.tags_listed_by_group
|
||||
ungrouped_tags = Tag.where("tags.id NOT IN (SELECT tag_id FROM tag_group_memberships)")
|
||||
ungrouped_tags = ungrouped_tags.where("tags.topic_count > 0") unless show_all_tags
|
||||
|
||||
grouped_tag_counts = TagGroup.visible(guardian).order('name ASC').includes(:tags).map do |tag_group|
|
||||
{ id: tag_group.id, name: tag_group.name, tags: self.class.tag_counts_json(tag_group.tags) }
|
||||
end
|
||||
|
||||
render json: {
|
||||
tags: self.class.tag_counts_json(ungrouped_tags),
|
||||
extras: { tag_groups: grouped_tag_counts }
|
||||
}
|
||||
else
|
||||
tags = show_all_tags ? Tag.all : Tag.where("tags.topic_count > 0")
|
||||
unrestricted_tags = DiscourseTagging.filter_visible(tags, guardian)
|
||||
|
||||
categories = Category.where("id IN (SELECT category_id FROM category_tags)")
|
||||
.where("id IN (?)", guardian.allowed_category_ids)
|
||||
.includes(:tags)
|
||||
|
||||
category_tag_counts = categories.map do |c|
|
||||
{ id: c.id, tags: self.class.tag_counts_json(c.tags) }
|
||||
end
|
||||
|
||||
render json: {
|
||||
tags: self.class.tag_counts_json(unrestricted_tags),
|
||||
extras: { categories: category_tag_counts }
|
||||
}
|
||||
end
|
||||
render json: {
|
||||
tags: @tags,
|
||||
extras: @extras
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -98,7 +98,7 @@ class UploadsController < ApplicationController
|
||||
if Discourse.store.internal?
|
||||
send_file_local_upload(upload)
|
||||
else
|
||||
redirect_to upload.url
|
||||
redirect_to Discourse.store.url_for(upload)
|
||||
end
|
||||
else
|
||||
render_404
|
||||
|
||||
@ -352,6 +352,8 @@ class UsersController < ApplicationController
|
||||
return fail_with("login.reserved_username")
|
||||
end
|
||||
|
||||
params[:locale] ||= I18n.locale unless current_user
|
||||
|
||||
new_user_params = user_params
|
||||
user = User.unstage(new_user_params)
|
||||
user = User.new(new_user_params) if user.nil?
|
||||
@ -1259,8 +1261,7 @@ class UsersController < ApplicationController
|
||||
.permit(permitted, theme_ids: [])
|
||||
.reverse_merge(
|
||||
ip_address: request.remote_ip,
|
||||
registration_ip_address: request.remote_ip,
|
||||
locale: user_locale
|
||||
registration_ip_address: request.remote_ip
|
||||
)
|
||||
|
||||
if !UsernameCheckerService.is_developer?(result['email']) &&
|
||||
@ -1279,10 +1280,6 @@ class UsersController < ApplicationController
|
||||
attrs
|
||||
end
|
||||
|
||||
def user_locale
|
||||
I18n.locale
|
||||
end
|
||||
|
||||
def fail_with(key)
|
||||
render json: { success: false, message: I18n.t(key) }
|
||||
end
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user