Version bump

This commit is contained in:
Neil Lalonde 2019-06-10 13:10:51 -04:00
commit 449d21b88c
433 changed files with 8686 additions and 2270 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 "![" + alt + "](" + src + ")";
return `![${alt}](${src}${title ? ` "${title}"` : ""})`;
}
return "";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -312,7 +312,7 @@ export default Ember.Object.extend({
obj[subType] = hydrated;
delete obj[k];
} else {
obj[subType] = null;
Ember.set(obj, subType, null);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,7 +8,6 @@
</div>
<div class="control-group">
<label class="control-label"></label>
<div class="controls">
{{combo-box
value=selectedUserBadgeId

View File

@ -1 +1,5 @@
{{input type="text" class="date-picker" placeholder=placeholder value=value}}
{{input
type=inputType
class="date-picker"
placeholder=placeholder
value=value}}

View File

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

View File

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

View File

@ -0,0 +1,6 @@
{{#if value }}
<div class={{classes}}>
<div class='name'>{{name}}</div>
<div class='value'>{{value}}</div>
</div>
{{/if}}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
{{#each popular_components as |theme|}}
<a class="popular-theme-item" href="{{theme.meta_url}}" target="_blank">
{{theme.name}}
</a>
{{/each}}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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