Version bump

This commit is contained in:
Neil Lalonde 2020-05-04 11:45:26 -04:00
commit d46b486633
No known key found for this signature in database
GPG Key ID: FF871CA9037D0A91
1785 changed files with 45918 additions and 28680 deletions

View File

@ -13,3 +13,5 @@ vendor/
test/javascripts/test_helper.js
test/javascripts/fixtures
test/javascripts/helpers/assertions.js
node_modules/
dist/

View File

@ -23,12 +23,6 @@ acc5cbdf8ecb9293a0fa9474ee73baf499c02428
# Rename wizard from es6 -> js
1ac02422011f89716ab27250d39b0e0212e03892
# Rename discourse-common es6 -> js
167503ca4824e37a2e93d74b3f50271556d0ba8e
# Rename ember-addons es6 -> js
16ba50bce362c1eefe1881f86c67bec66f493abb
# Rename some root files
11938d58d4b1bea1ff43306450da7b24f05db0a
@ -40,3 +34,6 @@ b66b277dc44bcd2122dc21965dab209c30636214
# DEV: enforces double quotes ember-template-lint
c4644c61d97c823b7dd940ffaf0967a104f4b58c
# Migrate to app directory
7a2e8d3ead63c7d99e1069fc7823e933f931ba85

10
.gitignore vendored
View File

@ -54,7 +54,7 @@ bootsnap-compile-cache/
!/plugins/discourse-narrative-bot
!/plugins/discourse-presence
!/plugins/discourse-local-dates
!/plugins/discourse-internet-explorer
!/plugins/discourse-unsupported-browser
/plugins/*/auto_generated/
/spec/fixtures/plugins/my_plugin/auto_generated
@ -122,7 +122,7 @@ vendor/bundle/*
*.swn
# ignore nodejs files
/node_modules
node_modules
/package-lock.json
/vendor/data/GeoLite2-City.mmdb
@ -132,3 +132,9 @@ vendor/bundle/*
# ignore auto-generated plugin js assets
/app/assets/javascripts/plugins/*
# ignore generated api documentation files
openapi/*
# ember-cli generated
dist

View File

@ -179,6 +179,8 @@ RSpec/DescribedClassModuleWrapping:
RSpec/EmptyExampleGroup:
Enabled: true
Exclude:
- 'spec/requests/api/*'
RSpec/EmptyLineAfterExample:
Enabled: false # TODO

14
Gemfile
View File

@ -118,7 +118,6 @@ gem 'rake'
gem 'thor', require: false
gem 'diffy', require: false
gem 'rinku'
gem 'sanitize'
gem 'sidekiq'
gem 'mini_scheduler'
@ -168,17 +167,17 @@ group :test, :development do
# we would like to upgrade it if possible
gem 'rb-inotify', '~> 0.9', require: RUBY_PLATFORM =~ /linux/i ? 'rb-inotify' : false
# TODO once 4.0.0 is released upgrade to it, at time of writing 3.9.0 is latest
gem 'rspec-rails', '4.0.0.beta2', require: false
gem 'rspec-rails'
gem 'shoulda-matchers', require: false
gem 'rspec-html-matchers'
gem 'pry-nav'
gem 'byebug', require: ENV['RM_INFO'].nil?, platform: :mri
gem 'rubocop', require: false
gem "rubocop-discourse", require: false
gem "rubocop-rspec", require: false
gem 'parallel_tests'
gem 'rswag-specs'
end
group :development do
@ -255,10 +254,3 @@ end
gem 'webpush', require: false
gem 'colored2', require: false
gem 'maxminddb'
# These are not direct dependencies, but we need to restrict
# versions for compatibility with https://github.com/discourse/discourse-zendesk-plugin
# These restrictions can be removed once the zendesk_api gem is updated
# for newer versions of hashie and faraday
gem 'hashie', '< 4.0.0', require: false # https://github.com/zendesk/zendesk_api_client_rb/pull/422
gem 'faraday', '< 1.0.0', require: false # https://github.com/zendesk/zendesk_api_client_rb/pull/421

View File

@ -61,12 +61,12 @@ GEM
aws-sdk-sns (1.22.0)
aws-sdk-core (~> 3, >= 3.71.0)
aws-sigv4 (~> 1.1)
aws-sigv4 (1.1.2)
aws-sigv4 (1.1.3)
aws-eventstream (~> 1.0, >= 1.0.2)
barber (0.12.2)
ember-source (>= 1.0, < 3.1)
execjs (>= 1.2, < 3)
better_errors (2.6.0)
better_errors (2.7.0)
coderay (>= 1.0.0)
erubi (>= 1.0.0)
rack (>= 0.9.0)
@ -78,7 +78,7 @@ GEM
bullet (6.1.0)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
byebug (11.1.2)
byebug (11.1.3)
cbor (0.5.9.6)
certified (1.0.0)
chunky_png (1.3.11)
@ -126,7 +126,7 @@ GEM
exifr (1.3.6)
fabrication (2.21.1)
fakeweb (1.3.0)
faraday (0.17.3)
faraday (1.0.1)
multipart-post (>= 1.2, < 3)
fast_blank (1.0.0)
fast_xor (1.1.3)
@ -142,7 +142,7 @@ GEM
activesupport (>= 4.2.0)
guess_html_encoding (0.0.11)
hashdiff (1.0.1)
hashie (3.6.0)
hashie (4.1.0)
highline (1.7.10)
hkdf (0.3.0)
htmlentities (4.3.4)
@ -158,6 +158,8 @@ GEM
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (2.3.0)
json-schema (2.8.1)
addressable (>= 2.4)
jwt (2.2.1)
kgio (2.11.3)
libv8 (7.3.492.27.1)
@ -182,9 +184,9 @@ GEM
mini_mime (>= 0.1.1)
maxminddb (0.1.22)
memory_profiler (0.9.14)
message_bus (2.2.4)
message_bus (3.1.0)
rack (>= 1.1.3)
method_source (0.9.2)
method_source (1.0.0)
mini_mime (1.0.2)
mini_portile2 (2.4.0)
mini_racer (0.2.10)
@ -251,15 +253,13 @@ GEM
parallel (1.19.1)
parallel_tests (2.32.0)
parallel
parser (2.7.1.1)
parser (2.7.1.2)
ast (~> 2.4.0)
pg (1.2.3)
progress (3.5.2)
pry (0.12.2)
coderay (~> 1.1.0)
method_source (~> 0.9.0)
pry-nav (0.3.0)
pry (>= 0.9.10, < 0.13.0)
pry (0.13.1)
coderay (~> 1.1)
method_source (~> 1.0)
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (4.0.4)
@ -292,7 +292,7 @@ GEM
rake (13.0.1)
rake-compiler (1.1.0)
rake
rb-fsevent (0.10.3)
rb-fsevent (0.10.4)
rb-inotify (0.10.1)
ffi (~> 1.0)
rbtrace (0.4.12)
@ -300,7 +300,7 @@ GEM
msgpack (>= 0.4.3)
optimist (>= 3.0.0)
rchardet (1.8.0)
redis (4.1.3)
redis (4.1.4)
redis-namespace (1.7.0)
redis (>= 3.0.4)
request_store (1.5.0)
@ -312,13 +312,13 @@ GEM
rqrcode (1.1.2)
chunky_png (~> 1.0)
rqrcode_core (~> 0.1)
rqrcode_core (0.1.1)
rqrcode_core (0.1.2)
rspec (3.9.0)
rspec-core (~> 3.9.0)
rspec-expectations (~> 3.9.0)
rspec-mocks (~> 3.9.0)
rspec-core (3.9.1)
rspec-support (~> 3.9.1)
rspec-core (3.9.2)
rspec-support (~> 3.9.3)
rspec-expectations (3.9.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
@ -328,15 +328,19 @@ GEM
rspec-mocks (3.9.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
rspec-rails (4.0.0.beta2)
rspec-rails (4.0.0)
actionpack (>= 4.2)
activesupport (>= 4.2)
railties (>= 4.2)
rspec-core (~> 3.8)
rspec-expectations (~> 3.8)
rspec-mocks (~> 3.8)
rspec-support (~> 3.8)
rspec-support (3.9.2)
rspec-core (~> 3.9)
rspec-expectations (~> 3.9)
rspec-mocks (~> 3.9)
rspec-support (~> 3.9)
rspec-support (3.9.3)
rswag-specs (2.3.1)
activesupport (>= 3.1, < 7.0)
json-schema (~> 2.2)
railties (>= 3.1, < 7.0)
rtlit (0.0.5)
rubocop (0.82.0)
jaro_winkler (~> 1.5.1)
@ -346,9 +350,10 @@ GEM
rexml
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 2.0)
rubocop-discourse (2.0.1)
rubocop-discourse (2.1.2)
rubocop (>= 0.69.0)
rubocop-rspec (1.38.1)
rubocop-rspec (>= 1.39.0)
rubocop-rspec (1.39.0)
rubocop (>= 0.68.1)
ruby-prof (1.3.2)
ruby-progressbar (1.10.1)
@ -405,7 +410,7 @@ GEM
unf_ext
unf_ext (0.0.7.7)
unicode-display_width (1.7.0)
unicorn (5.5.4)
unicorn (5.5.5)
kgio (~> 2.6)
raindrops (~> 0.7)
uniform_notifier (1.13.0)
@ -457,14 +462,12 @@ DEPENDENCIES
execjs
fabrication
fakeweb
faraday (< 1.0.0)
fast_blank
fast_xor
fast_xs
fastimage
flamegraph
gc_tracer
hashie (< 4.0.0)
highline (~> 1.7.0)
htmlentities
http_accept_language
@ -502,7 +505,6 @@ DEPENDENCIES
onebox
parallel_tests
pg
pry-nav
pry-rails
puma
r2
@ -523,7 +525,8 @@ DEPENDENCIES
rqrcode
rspec
rspec-html-matchers
rspec-rails (= 4.0.0.beta2)
rspec-rails
rswag-specs
rtlit
rubocop
rubocop-discourse
@ -531,7 +534,6 @@ DEPENDENCIES
ruby-prof
ruby-readability
rubyzip
sanitize
sassc (= 2.0.1)
sassc-rails
seed-fu

View File

@ -66,7 +66,7 @@ Plus *lots* of Ruby Gems, a complete list of which is at [/master/Gemfile](https
## Contributing
[![Build Status](https://api.travis-ci.org/discourse/discourse.svg?branch=master)](https://travis-ci.org/discourse/discourse)
[![Build Status](https://github.com/discourse/discourse/workflows/CI/badge.svg)](https://github.com/discourse/discourse/actions)
Discourse is **100% free** and **open source**. We encourage and support an active, healthy community that
accepts contributions from the public &ndash; including you!

View File

@ -1,7 +1,7 @@
import discourseComputed from "discourse-common/utils/decorators";
import { makeArray } from "discourse-common/lib/helpers";
import { alias, or, and, reads, equal, notEmpty } from "@ember/object/computed";
import EmberObject from "@ember/object";
import { alias, or, and, equal, notEmpty, not } from "@ember/object/computed";
import EmberObject, { computed, action } from "@ember/object";
import { next } from "@ember/runloop";
import Component from "@ember/component";
import ReportLoader from "discourse/lib/reports-loader";
@ -9,6 +9,7 @@ import { exportEntity } from "discourse/lib/export-csv";
import { outputExportResult } from "discourse/lib/export-result";
import Report, { SCHEMA_VERSION } from "admin/models/report";
import ENV from "discourse-common/config/environment";
import { isPresent } from "@ember/utils";
const TABLE_OPTIONS = {
perPage: 8,
@ -40,7 +41,12 @@ function collapseWeekly(data, average) {
}
export default Component.extend({
classNameBindings: ["isEnabled", "isLoading", "dasherizedDataSourceName"],
classNameBindings: [
"isVisible",
"isEnabled",
"isLoading",
"dasherizedDataSourceName"
],
classNames: ["admin-report"],
isEnabled: true,
disabledLabel: I18n.t("admin.dashboard.disabled"),
@ -62,6 +68,7 @@ export default Component.extend({
showDatesOptions: alias("model.dates_filtering"),
showRefresh: or("showDatesOptions", "model.available_filters.length"),
shouldDisplayTrend: and("showTrend", "model.prev_period"),
isVisible: not("isHidden"),
init() {
this._super(...arguments);
@ -69,8 +76,28 @@ export default Component.extend({
this._reports = [];
},
startDate: reads("filters.startDate"),
endDate: reads("filters.endDate"),
isHidden: computed("siteSettings.dashboard_hidden_reports", function() {
return (this.siteSettings.dashboard_hidden_reports || "")
.split("|")
.filter(Boolean)
.includes(this.dataSourceName);
}),
startDate: computed("filters.startDate", function() {
if (this.filters && isPresent(this.filters.startDate)) {
return moment(this.filters.startDate, "YYYY-MM-DD");
} else {
return moment();
}
}),
endDate: computed("filters.endDate", function() {
if (this.filters && isPresent(this.filters.endDate)) {
return moment(this.filters.endDate, "YYYY-MM-DD");
} else {
return moment();
}
}),
didReceiveAttrs() {
this._super(...arguments);
@ -126,39 +153,18 @@ export default Component.extend({
return `admin-report-${currentMode.replace(/_/g, "-")}`;
},
@discourseComputed("startDate")
normalizedStartDate(startDate) {
return startDate && typeof startDate.isValid === "function"
? moment
.utc(startDate.toISOString())
.locale("en")
.format("YYYYMMDD")
: moment(startDate)
.locale("en")
.format("YYYYMMDD");
},
@discourseComputed("endDate")
normalizedEndDate(endDate) {
return endDate && typeof endDate.isValid === "function"
? moment
.utc(endDate.toISOString())
.locale("en")
.format("YYYYMMDD")
: moment(endDate)
.locale("en")
.format("YYYYMMDD");
},
@discourseComputed(
"dataSourceName",
"normalizedStartDate",
"normalizedEndDate",
"startDate",
"endDate",
"filters.customFilters"
)
reportKey(dataSourceName, startDate, endDate, customFilters) {
if (!dataSourceName || !startDate || !endDate) return null;
startDate = startDate.toISOString(true).split("T")[0];
endDate = endDate.toISOString(true).split("T")[0];
let reportKey = "reports:";
reportKey += [
dataSourceName,
@ -179,74 +185,61 @@ export default Component.extend({
return reportKey;
},
actions: {
onChangeEndDate(date) {
const startDate = moment(this.startDate);
const newEndDate = moment(date).endOf("day");
@action
onChangeDateRange(range) {
this.send("refreshReport", {
startDate: range.from,
endDate: range.to
});
},
if (newEndDate.isSameOrAfter(startDate)) {
this.set("endDate", newEndDate.format("YYYY-MM-DD"));
} else {
this.set("endDate", startDate.endOf("day").format("YYYY-MM-DD"));
}
@action
applyFilter(id, value) {
let customFilters = this.get("filters.customFilters") || {};
this.send("refreshReport");
},
onChangeStartDate(date) {
const endDate = moment(this.endDate);
const newStartDate = moment(date).startOf("day");
if (newStartDate.isSameOrBefore(endDate)) {
this.set("startDate", newStartDate.format("YYYY-MM-DD"));
} else {
this.set("startDate", endDate.startOf("day").format("YYYY-MM-DD"));
}
this.send("refreshReport");
},
applyFilter(id, value) {
let customFilters = this.get("filters.customFilters") || {};
if (typeof value === "undefined") {
delete customFilters[id];
} else {
customFilters[id] = value;
}
this.attrs.onRefresh({
type: this.get("model.type"),
startDate: this.startDate,
endDate: this.endDate,
filters: customFilters
});
},
refreshReport() {
this.attrs.onRefresh({
type: this.get("model.type"),
startDate: this.startDate,
endDate: this.endDate,
filters: this.get("filters.customFilters")
});
},
exportCsv() {
const customFilters = this.get("filters.customFilters") || {};
exportEntity("report", {
name: this.get("model.type"),
start_date: this.startDate,
end_date: this.endDate,
category_id: customFilters.category,
group_id: customFilters.group
}).then(outputExportResult);
},
changeMode(mode) {
this.set("currentMode", mode);
if (typeof value === "undefined") {
delete customFilters[id];
} else {
customFilters[id] = value;
}
this.send("refreshReport", {
filters: customFilters
});
},
@action
refreshReport(options = {}) {
this.attrs.onRefresh({
type: this.get("model.type"),
startDate:
typeof options.startDate === "undefined"
? this.startDate
: options.startDate,
endDate:
typeof options.endDate === "undefined" ? this.endDate : options.endDate,
filters:
typeof options.filters === "undefined"
? this.get("filters.customFilters")
: options.filters
});
},
@action
exportCsv() {
const customFilters = this.get("filters.customFilters") || {};
exportEntity("report", {
name: this.get("model.type"),
start_date: this.startDate.toISOString(true).split("T")[0],
end_date: this.endDate.toISOString(true).split("T")[0],
category_id: customFilters.category,
group_id: customFilters.group
}).then(outputExportResult);
},
@action
changeMode(mode) {
this.set("currentMode", mode);
},
_computeReport() {
@ -276,10 +269,8 @@ export default Component.extend({
if (!this.startDate || !this.endDate) {
report = sort(filteredReports)[0];
} else {
const reportKey = this.reportKey;
report = sort(
filteredReports.filter(r => r.report_key.includes(reportKey))
filteredReports.filter(r => r.report_key.includes(this.reportKey))
)[0];
if (!report) return;
@ -339,15 +330,15 @@ export default Component.extend({
let payload = { data: { cache: true, facets } };
if (this.startDate) {
payload.data.start_date = moment
.utc(this.startDate, "YYYY-MM-DD")
.toISOString();
payload.data.start_date = moment(this.startDate)
.toISOString(true)
.split("T")[0];
}
if (this.endDate) {
payload.data.end_date = moment
.utc(this.endDate, "YYYY-MM-DD")
.toISOString();
payload.data.end_date = moment(this.endDate)
.toISOString(true)
.split("T")[0];
}
if (this.get("reportOptions.table.limit")) {

View File

@ -1,15 +1,17 @@
import discourseComputed from "discourse-common/utils/decorators";
import Component from "@ember/component";
import { action } from "@ember/object";
export default Component.extend({
@discourseComputed("value")
selectedTags: {
get(value) {
return value.split("|");
},
set(value) {
this.set("value", value.join("|"));
return value;
return value.split("|").filter(Boolean);
}
},
@action
changeSelectedTags(tags) {
this.set("value", tags.join("|"));
}
});

View File

@ -31,6 +31,7 @@ export default Controller.extend({
availableComponentsNames: mapBy("availableChildThemes", "name"),
availableActiveComponentsNames: mapBy("availableActiveChildThemes", "name"),
childThemesNames: mapBy("model.childThemes", "name"),
extraFiles: filterBy("model.theme_fields", "target", "extra_js"),
@discourseComputed("model.editedFields")
editedFieldsFormatted() {

View File

@ -23,9 +23,44 @@ export default Controller.extend(PeriodComputationMixin, {
@discourseComputed("siteSettings.dashboard_general_tab_activity_metrics")
activityMetrics(metrics) {
return (metrics || "").split("|").filter(m => m);
return (metrics || "").split("|").filter(Boolean);
},
hiddenReports: computed("siteSettings.dashboard_hidden_reports", function() {
return (this.siteSettings.dashboard_hidden_reports || "")
.split("|")
.filter(Boolean);
}),
isActivityMetricsVisible: computed(
"activityMetrics",
"hiddenReports",
function() {
return (
this.activityMetrics.length &&
this.activityMetrics.some(x => !this.hiddenReports.includes(x))
);
}
),
isSearchReportsVisible: computed("hiddenReports", function() {
return ["top_referred_topics", "trending_search"].some(
x => !this.hiddenReports.includes(x)
);
}),
isCommunityHealthVisible: computed("hiddenReports", function() {
return [
"consolidated_page_views",
"signups",
"topics",
"posts",
"dau_by_mau",
"daily_engaged_users",
"new_contributors"
].some(x => !this.hiddenReports.includes(x));
}),
@discourseComputed
activityMetricsFilters() {
return {

View File

@ -1,6 +1,7 @@
import discourseComputed from "discourse-common/utils/decorators";
import Controller from "@ember/controller";
import PeriodComputationMixin from "admin/mixins/period-computation";
import { computed } from "@ember/object";
export default Controller.extend(PeriodComputationMixin, {
@discourseComputed
@ -13,6 +14,16 @@ export default Controller.extend(PeriodComputationMixin, {
};
},
isModeratorsActivityVisible: computed(
"siteSettings.dashboard_hidden_reports",
function() {
return !(this.siteSettings.dashboard_hidden_reports || "")
.split("|")
.filter(Boolean)
.includes("moderators_activity");
}
),
@discourseComputed
userFlaggingRatioOptions() {
return {

View File

@ -8,17 +8,27 @@ const { get } = Ember;
export default Controller.extend({
filter: null,
@discourseComputed("model.[]", "filter")
@discourseComputed(
"model.[]",
"filter",
"siteSettings.dashboard_hidden_reports"
)
filterReports(reports, filter) {
if (filter) {
filter = filter.toLowerCase();
return reports.filter(report => {
reports = reports.filter(report => {
return (
(get(report, "title") || "").toLowerCase().indexOf(filter) > -1 ||
(get(report, "description") || "").toLowerCase().indexOf(filter) > -1
);
});
}
const hiddenReports = (this.siteSettings.dashboard_hidden_reports || "")
.split("|")
.filter(Boolean);
reports = reports.filter(report => !hiddenReports.includes(report.type));
return reports;
},

View File

@ -1,7 +1,7 @@
import discourseComputed from "discourse-common/utils/decorators";
import { inject } from "@ember/controller";
import Controller from "@ember/controller";
import Controller, { inject } from "@ember/controller";
import { setting } from "discourse/lib/computed";
import { computed } from "@ember/object";
import AdminDashboard from "admin/models/admin-dashboard";
import VersionCheck from "admin/models/version-check";
@ -18,6 +18,24 @@ export default Controller.extend({
return this.currentUser.get("admin") && (problemsLength || 0) > 0;
},
visibleTabs: computed("siteSettings.dashboard_visible_tabs", function() {
return (this.siteSettings.dashboard_visible_tabs || "")
.split("|")
.filter(Boolean);
}),
isModerationTabVisible: computed("visibleTabs", function() {
return this.visibleTabs.includes("moderation");
}),
isSecurityTabVisible: computed("visibleTabs", function() {
return this.visibleTabs.includes("security");
}),
isReportsTabVisible: computed("visibleTabs", function() {
return this.visibleTabs.includes("reports");
}),
fetchProblems() {
if (this.isLoadingProblems) return;

View File

@ -42,7 +42,7 @@ export default Controller.extend({
if (grant.post_id) {
i18nKey += "_post";
i18nParams.link = `<a href="/p/${grant.post_id}" data-auto-route="true">
${Handlebars.Utils.escapeExpression(grant.title)}
${escapeExpression(grant.title)}
</a>`;
}

View File

@ -2,6 +2,7 @@ import Controller, { inject as controller } from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import discourseComputed from "discourse-common/utils/decorators";
import { alias } from "@ember/object/computed";
import { action } from "@ember/object";
export default Controller.extend(ModalFunctionality, {
adminUserIndex: controller(),
@ -14,7 +15,10 @@ export default Controller.extend(ModalFunctionality, {
@discourseComputed("username", "targetUsername")
text(username, targetUsername) {
return `transfer @${username} to @${targetUsername}`;
return I18n.t(`admin.user.merge.confirmation.text`, {
username,
targetUsername
});
},
@discourseComputed("value", "text")
@ -22,14 +26,14 @@ export default Controller.extend(ModalFunctionality, {
return !value || text !== value;
},
actions: {
merge() {
this.adminUserIndex.send("merge", this.targetUsername);
this.send("closeModal");
},
@action
confirm() {
this.adminUserIndex.send("merge", this.targetUsername);
this.send("closeModal");
},
cancel() {
this.send("closeModal");
}
@action
close() {
this.send("closeModal");
}
});

View File

@ -2,6 +2,7 @@ import Controller, { inject as controller } from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import discourseComputed from "discourse-common/utils/decorators";
import { alias } from "@ember/object/computed";
import { action } from "@ember/object";
export default Controller.extend(ModalFunctionality, {
adminUserIndex: controller(),
@ -16,14 +17,14 @@ export default Controller.extend(ModalFunctionality, {
return !targetUsername || username === targetUsername;
},
actions: {
merge() {
this.send("closeModal");
this.adminUserIndex.send("showMergeConfirmation", this.targetUsername);
},
@action
showConfirmation() {
this.send("closeModal");
this.adminUserIndex.send("showMergeConfirmation", this.targetUsername);
},
cancel() {
this.send("closeModal");
}
@action
close() {
this.send("closeModal");
}
});

View File

@ -1,7 +1,8 @@
import { registerUnbound } from "discourse-common/lib/helpers";
import { renderIcon } from "discourse-common/lib/icon-library";
import { htmlSafe } from "@ember/template";
registerUnbound("check-icon", function(value) {
let icon = value ? "check" : "times";
return new Handlebars.SafeString(renderIcon("string", icon));
return htmlSafe(renderIcon("string", icon));
});

View File

@ -7,6 +7,7 @@ import Mixin from "@ember/object/mixin";
import showModal from "discourse/lib/show-modal";
import { Promise } from "rsvp";
import { ajax } from "discourse/lib/ajax";
import { htmlSafe } from "@ember/template";
const CUSTOM_TYPES = [
"bool",
@ -63,7 +64,7 @@ export default Mixin.create({
}
let preview = setting.get("preview");
if (preview) {
return new Handlebars.SafeString(
return htmlSafe(
"<div class='preview'>" +
preview.replace(/\{\{value\}\}/g, value) +
"</div>"

View File

@ -5,7 +5,7 @@ import { ajax } from "discourse/lib/ajax";
import { propertyNotEqual } from "discourse/lib/computed";
import { popupAjaxError } from "discourse/lib/ajax-error";
import Group from "discourse/models/group";
import { userPath } from "discourse/lib/url";
import DiscourseURL, { userPath } from "discourse/lib/url";
import { Promise } from "rsvp";
import User from "discourse/models/user";
@ -514,16 +514,16 @@ const AdminUser = User.extend({
formData["target_username"] = opts.targetUsername;
}
return ajax(`/admin/users/${user.get("id")}/merge.json`, {
return ajax(`/admin/users/${user.id}/merge.json`, {
type: "POST",
data: formData
})
.then(function(data) {
.then(data => {
if (data.merged) {
if (/^\/admin\/users\/list\//.test(location)) {
document.location = location;
DiscourseURL.redirectTo(location);
} else {
document.location = Discourse.getURL(
DiscourseURL.redirectTo(
`/admin/users/${data.user.id}/${data.user.username}`
);
}
@ -534,8 +534,8 @@ const AdminUser = User.extend({
}
}
})
.catch(function() {
AdminUser.find(user.get("id")).then(u => user.setProperties(u));
.catch(() => {
AdminUser.find(user.id).then(u => user.setProperties(u));
bootbox.alert(I18n.t("admin.user.merge_failed"));
});
},

View File

@ -13,8 +13,7 @@ export default DiscourseRoute.extend({
params.startDate =
params.start_date ||
moment
.utc()
moment()
.subtract(1, "day")
.subtract(1, "month")
.startOf("day")
@ -23,8 +22,7 @@ export default DiscourseRoute.extend({
params.endDate =
params.end_date ||
moment
.utc()
moment()
.endOf("day")
.format("YYYY-MM-DD");
delete params.end_date;
@ -56,9 +54,13 @@ export default DiscourseRoute.extend({
onParamsChange(params) {
const queryParams = {
type: params.type,
start_date: params.startDate,
start_date: params.startDate
? params.startDate.toISOString(true).split("T")[0]
: null,
filters: params.filters,
end_date: params.endDate
? params.endDate.toISOString(true).split("T")[0]
: null
};
this.transitionTo("adminReports.show", { queryParams });

View File

@ -1,205 +1,197 @@
{{#if isEnabled}}
{{#conditional-loading-section isLoading=isLoading}}
{{#if showHeader}}
<div class="header">
{{#if showTitle}}
<ul class="breadcrumb">
{{#if showAllReportsLink}}
<li class="item all-reports">
{{#link-to "admin.dashboardReports" class="report-url"}}
{{i18n "admin.dashboard.all_reports"}}
{{/link-to}}
</li>
{{#unless isHidden}}
{{#if isEnabled}}
{{#conditional-loading-section isLoading=isLoading}}
{{#if showHeader}}
<div class="header">
{{#if showTitle}}
<ul class="breadcrumb">
{{#if showAllReportsLink}}
<li class="item all-reports">
{{#link-to "admin.dashboardReports" class="report-url"}}
{{i18n "admin.dashboard.all_reports"}}
{{/link-to}}
</li>
{{#unless showNotFoundError}}
<li class="item separator">|</li>
{{/unless}}
{{/if}}
{{#unless showNotFoundError}}
<li class="item separator">|</li>
{{/unless}}
{{/if}}
{{#unless showNotFoundError}}
<li class="item report">
<a href={{model.reportUrl}} class="report-url">
{{model.title}}
</a>
{{#if model.description}}
{{#if model.description_link}}
<a target="_blank" rel="noopener noreferrer" href={{model.description_link}} class="info" data-tooltip={{model.description}}>
{{d-icon "question-circle"}}
</a>
{{else}}
<span class="info" data-tooltip={{model.description}}>
{{d-icon "question-circle"}}
</span>
{{/if}}
{{/if}}
</li>
{{/unless}}
</ul>
{{/if}}
{{#if shouldDisplayTrend}}
<div class="trend {{model.trend}}">
<span class="value" title={{model.trendTitle}}>
{{#if model.average}}
{{number model.currentAverage}}{{#if model.percent}}%{{/if}}
{{else}}
{{number model.currentTotal noTitle="true"}}{{#if model.percent}}%{{/if}}
{{/if}}
{{#if model.trendIcon}}
{{d-icon model.trendIcon class="icon"}}
{{/if}}
</span>
</div>
{{/if}}
</div>
{{/if}}
<div class="body">
<div class="main">
{{#if showError}}
{{#if showTimeoutError}}
<div class="alert alert-error report-alert timeout">
{{d-icon "exclamation-triangle"}}
<span>{{i18n "admin.dashboard.timeout_error"}}</span>
</div>
{{/if}}
{{#if showExceptionError}}
<div class="alert alert-error report-alert exception">
{{d-icon "exclamation-triangle"}}
<span>{{i18n "admin.dashboard.exception_error"}}</span>
</div>
{{/if}}
{{#if showNotFoundError}}
<div class="alert alert-error report-alert not-found">
{{d-icon "exclamation-triangle"}}
<span>{{i18n "admin.dashboard.not_found_error"}}</span>
</div>
{{/if}}
{{else}}
{{#if hasData}}
{{#if currentMode}}
{{component modeComponent model=model options=options}}
{{#if model.relatedReport}}
{{admin-report showFilteringUI=false dataSourceName=model.relatedReport.type}}
{{/if}}
{{/if}}
{{else}}
{{#if rateLimitationString}}
<div class="alert alert-error report-alert rate-limited">
{{d-icon "thermometer-three-quarters"}}
<span>{{rateLimitationString}}</span>
</div>
{{else}}
<div class="alert alert-info report-alert no-data">
{{d-icon "chart-pie"}}
{{#if model.reportUrl}}
<li class="item report">
<a href={{model.reportUrl}} class="report-url">
<span>
{{#if model.title}}
{{model.title}}
{{/if}}
{{i18n "admin.dashboard.reports.no_data"}}
</span>
{{model.title}}
</a>
{{#if model.description}}
{{#if model.description_link}}
<a target="_blank" rel="noopener noreferrer" href={{model.description_link}} class="info" data-tooltip={{model.description}}>
{{d-icon "question-circle"}}
</a>
{{else}}
<span class="info" data-tooltip={{model.description}}>
{{d-icon "question-circle"}}
</span>
{{/if}}
{{/if}}
</li>
{{/unless}}
</ul>
{{/if}}
{{#if shouldDisplayTrend}}
<div class="trend {{model.trend}}">
<span class="value" title={{model.trendTitle}}>
{{#if model.average}}
{{number model.currentAverage}}{{#if model.percent}}%{{/if}}
{{else}}
<span>{{i18n "admin.dashboard.reports.no_data"}}</span>
{{number model.currentTotal noTitle="true"}}{{#if model.percent}}%{{/if}}
{{/if}}
</div>
{{/if}}
{{/if}}
{{/if}}
</div>
{{#if showFilteringUI}}
<div class="filters">
{{#if showModes}}
<div class="modes">
{{#each displayedModes as |displayedMode|}}
{{d-button
action=(action "changeMode")
actionParam=displayedMode.mode
class=displayedMode.cssClass
icon=displayedMode.icon}}
{{/each}}
</div>
{{/if}}
{{#if showDatesOptions}}
<div class="control">
<span class="label">
{{i18n "admin.dashboard.reports.start_date"}}
{{#if model.trendIcon}}
{{d-icon model.trendIcon class="icon"}}
{{/if}}
</span>
<div class="input">
{{date-input
date=startDate
onChange=(action "onChangeStartDate")
}}
</div>
</div>
<div class="control">
<span class="label">
{{i18n "admin.dashboard.reports.end_date"}}
</span>
<div class="input">
{{date-input
date=endDate
onChange=(action "onChangeEndDate")
}}
</div>
</div>
{{/if}}
{{#each model.available_filters as |filter|}}
<div class="control">
<span class="label">
{{i18n (concat "admin.dashboard.reports.filters." filter.id ".label")}}
</span>
<div class="input">
{{component
(concat "report-filters/" filter.type)
model=model
filter=filter
applyFilter=(action "applyFilter")}}
</div>
</div>
{{/each}}
<div class="control">
<div class="input">
{{d-button
class="btn-default export-csv-btn"
action=(action "exportCsv")
label="admin.export_csv.button_text"
icon="download"}}
</div>
</div>
{{#if showRefresh}}
<div class="control">
<div class="input">
{{d-button
class="refresh-report-btn btn-primary"
action=(action "refreshReport")
label="admin.dashboard.reports.refresh_report"
icon="sync"}}
</div>
</div>
{{/if}}
</div>
{{/if}}
<div class="body">
<div class="main">
{{#if showError}}
{{#if showTimeoutError}}
<div class="alert alert-error report-alert timeout">
{{d-icon "exclamation-triangle"}}
<span>{{i18n "admin.dashboard.timeout_error"}}</span>
</div>
{{/if}}
{{#if showExceptionError}}
<div class="alert alert-error report-alert exception">
{{d-icon "exclamation-triangle"}}
<span>{{i18n "admin.dashboard.exception_error"}}</span>
</div>
{{/if}}
{{#if showNotFoundError}}
<div class="alert alert-error report-alert not-found">
{{d-icon "exclamation-triangle"}}
<span>{{i18n "admin.dashboard.not_found_error"}}</span>
</div>
{{/if}}
{{else}}
{{#if hasData}}
{{#if currentMode}}
{{component modeComponent model=model options=options}}
{{#if model.relatedReport}}
{{admin-report showFilteringUI=false dataSourceName=model.relatedReport.type}}
{{/if}}
{{/if}}
{{else}}
{{#if rateLimitationString}}
<div class="alert alert-error report-alert rate-limited">
{{d-icon "thermometer-three-quarters"}}
<span>{{rateLimitationString}}</span>
</div>
{{else}}
<div class="alert alert-info report-alert no-data">
{{d-icon "chart-pie"}}
{{#if model.reportUrl}}
<a href={{model.reportUrl}} class="report-url">
<span>
{{#if model.title}}
{{model.title}}
{{/if}}
{{i18n "admin.dashboard.reports.no_data"}}
</span>
</a>
{{else}}
<span>{{i18n "admin.dashboard.reports.no_data"}}</span>
{{/if}}
</div>
{{/if}}
{{/if}}
{{/if}}
</div>
{{#if showFilteringUI}}
<div class="filters">
{{#if showModes}}
<div class="modes">
{{#each displayedModes as |displayedMode|}}
{{d-button
action=(action "changeMode")
actionParam=displayedMode.mode
class=displayedMode.cssClass
icon=displayedMode.icon}}
{{/each}}
</div>
{{/if}}
{{#if showDatesOptions}}
<div class="control">
<span class="label">
{{i18n "admin.dashboard.reports.dates"}}
</span>
<div class="input">
{{date-time-input-range
from=startDate
to=endDate
onChange=(action "onChangeDateRange")
showFromTime=false
showToTime=false
}}
</div>
</div>
{{/if}}
{{#each model.available_filters as |filter|}}
<div class="control">
<span class="label">
{{i18n (concat "admin.dashboard.reports.filters." filter.id ".label")}}
</span>
<div class="input">
{{component
(concat "report-filters/" filter.type)
model=model
filter=filter
applyFilter=(action "applyFilter")}}
</div>
</div>
{{/each}}
<div class="control">
<div class="input">
{{d-button
class="btn-default export-csv-btn"
action=(action "exportCsv")
label="admin.export_csv.button_text"
icon="download"}}
</div>
</div>
{{#if showRefresh}}
<div class="control">
<div class="input">
{{d-button
class="refresh-report-btn btn-primary"
action=(action "refreshReport")
label="admin.dashboard.reports.refresh_report"
icon="sync"}}
</div>
</div>
{{/if}}
</div>
{{/if}}
</div>
{{/conditional-loading-section}}
{{else}}
<div class="alert alert-info">
{{html-safe disabledLabel}}
</div>
{{/conditional-loading-section}}
{{else}}
<div class="alert alert-info">
{{html-safe disabledLabel}}
</div>
{{/if}}
{{/if}}
{{/unless}}

View File

@ -1,3 +1,9 @@
{{tag-chooser tags=selectedTags allowCreate=false}}
{{tag-chooser
tags=selectedTags
onChange=(action "changeSelectedTags")
options=(hash
allowAny=false
)
}}
<div class="desc">{{html-safe setting.description}}</div>
{{setting-validation-message message=validationMessage}}

View File

@ -126,6 +126,15 @@
</span>
{{/if}}
</div>
{{else}}
<div class="control-unit">
<span class="heading">{{i18n "admin.customize.theme.creator"}}</span>
<span>
{{#user-link user=model.user}}
{{format-username model.user.username}}
{{/user-link}}
</span>
</div>
{{/if}}
{{#unless model.component}}
@ -219,6 +228,26 @@
{{#d-button action=(action "addUploadModal") class="btn-default" icon="plus"}}{{i18n "admin.customize.theme.add"}}{{/d-button}}
</div>
{{#if extraFiles.length}}
<div class="control-unit">
<div class="mini-title">{{i18n "admin.customize.theme.extra_files"}}</div>
<details>
<summary>
{{#if model.remote_theme}}
{{i18n "admin.customize.theme.extra_files_remote"}}
{{else}}
{{i18n "admin.customize.theme.extra_files_upload"}}
{{/if}}
</summary>
<ul>
{{#each extraFiles as |extraFile|}}
<li>{{extraFile.name}}</li>
{{/each}}
</ul>
</details>
</div>
{{/if}}
{{#if hasSettings}}
<div class="control-unit">
<div class="mini-title">{{i18n "admin.customize.theme.theme_settings"}}</div>

View File

@ -3,7 +3,7 @@
{{#if showVersionChecks}}
<div class="section-top">
<div class="version-checks">
{{version-checks versionCheck=versionCheck}}
{{version-checks versionCheck=versionCheck tagName=""}}
</div>
</div>
{{/if}}
@ -22,21 +22,30 @@
{{i18n "admin.dashboard.general_tab"}}
{{/link-to}}
</li>
<li class="navigation-item moderation">
{{#link-to "admin.dashboardModeration" class="navigation-link"}}
{{i18n "admin.dashboard.moderation_tab"}}
{{/link-to}}
</li>
<li class="navigation-item security">
{{#link-to "admin.dashboardSecurity" class="navigation-link"}}
{{i18n "admin.dashboard.security_tab"}}
{{/link-to}}
</li>
<li class="navigation-item reports">
{{#link-to "admin.dashboardReports" class="navigation-link"}}
{{i18n "admin.dashboard.reports_tab"}}
{{/link-to}}
</li>
{{#if isModerationTabVisible}}
<li class="navigation-item moderation">
{{#link-to "admin.dashboardModeration" class="navigation-link"}}
{{i18n "admin.dashboard.moderation_tab"}}
{{/link-to}}
</li>
{{/if}}
{{#if isSecurityTabVisible}}
<li class="navigation-item security">
{{#link-to "admin.dashboardSecurity" class="navigation-link"}}
{{i18n "admin.dashboard.security_tab"}}
{{/link-to}}
</li>
{{/if}}
{{#if isReportsTabVisible}}
<li class="navigation-item reports">
{{#link-to "admin.dashboardReports" class="navigation-link"}}
{{i18n "admin.dashboard.reports_tab"}}
{{/link-to}}
</li>
{{/if}}
</ul>
{{outlet}}

View File

@ -1,97 +1,101 @@
{{#conditional-loading-spinner condition=isLoading}}
{{plugin-outlet name="admin-dashboard-general-top"}}
<div class="community-health section">
<div class="period-section">
<div class="section-title">
<h2>
<a href={{get-url "/admin/dashboard/reports"}}>
{{i18n "admin.dashboard.community_health"}}
</a>
</h2>
{{period-chooser period=period action=(action "changePeriod") content=availablePeriods fullDay=true}}
</div>
{{#if isCommunityHealthVisible}}
<div class="community-health section">
<div class="period-section">
<div class="section-title">
<h2>
<a href={{get-url "/admin/dashboard/reports"}}>
{{i18n "admin.dashboard.community_health"}}
</a>
</h2>
{{period-chooser period=period action=(action "changePeriod") content=availablePeriods fullDay=true}}
</div>
<div class="section-body">
<div class="charts">
{{admin-report
dataSourceName="consolidated_page_views"
forcedModes="stacked-chart"
filters=filters}}
<div class="section-body">
<div class="charts">
{{admin-report
dataSourceName="consolidated_page_views"
forcedModes="stacked-chart"
filters=filters}}
{{admin-report
dataSourceName="signups"
showTrend=true
forcedModes="chart"
filters=filters}}
{{admin-report
dataSourceName="signups"
showTrend=true
forcedModes="chart"
filters=filters}}
{{admin-report
dataSourceName="topics"
showTrend=true
forcedModes="chart"
filters=filters}}
{{admin-report
dataSourceName="topics"
showTrend=true
forcedModes="chart"
filters=filters}}
{{admin-report
dataSourceName="posts"
showTrend=true
forcedModes="chart"
filters=filters}}
{{admin-report
dataSourceName="posts"
showTrend=true
forcedModes="chart"
filters=filters}}
{{admin-report
dataSourceName="dau_by_mau"
showTrend=true
forcedModes="chart"
filters=filters}}
{{admin-report
dataSourceName="dau_by_mau"
showTrend=true
forcedModes="chart"
filters=filters}}
{{admin-report
dataSourceName="daily_engaged_users"
showTrend=true
forcedModes="chart"
filters=filters}}
{{admin-report
dataSourceName="daily_engaged_users"
showTrend=true
forcedModes="chart"
filters=filters}}
{{admin-report
dataSourceName="new_contributors"
showTrend=true
forcedModes="chart"
filters=filters}}
{{admin-report
dataSourceName="new_contributors"
showTrend=true
forcedModes="chart"
filters=filters}}
</div>
</div>
</div>
</div>
</div>
{{/if}}
<div class="section-columns">
<div class="section-column">
{{#if activityMetrics.length}}
<div class="admin-report activity-metrics">
<div class="header">
<ul class="breadcrumb">
<li class="item report">
{{#link-to "adminReports" class="report-url"}}
{{i18n "admin.dashboard.activity_metrics"}}
{{/link-to}}
</li>
</ul>
</div>
<div class="report-body">
<div class="counters-list">
<div class="counters-header">
<div class="counters-cell"></div>
<div class="counters-cell">{{i18n "admin.dashboard.reports.today"}}</div>
<div class="counters-cell">{{i18n "admin.dashboard.reports.yesterday"}}</div>
<div class="counters-cell">{{i18n "admin.dashboard.reports.last_7_days"}}</div>
<div class="counters-cell">{{i18n "admin.dashboard.reports.last_30_days"}}</div>
</div>
{{#if isActivityMetricsVisible}}
{{#if activityMetrics.length}}
<div class="admin-report activity-metrics">
<div class="header">
<ul class="breadcrumb">
<li class="item report">
{{#link-to "adminReports" class="report-url"}}
{{i18n "admin.dashboard.activity_metrics"}}
{{/link-to}}
</li>
</ul>
</div>
<div class="report-body">
<div class="counters-list">
<div class="counters-header">
<div class="counters-cell"></div>
<div class="counters-cell">{{i18n "admin.dashboard.reports.today"}}</div>
<div class="counters-cell">{{i18n "admin.dashboard.reports.yesterday"}}</div>
<div class="counters-cell">{{i18n "admin.dashboard.reports.last_7_days"}}</div>
<div class="counters-cell">{{i18n "admin.dashboard.reports.last_30_days"}}</div>
</div>
{{#each activityMetrics as |metric|}}
{{admin-report
showHeader=false
filters=activityMetricsFilters
forcedModes="counters"
dataSourceName=metric}}
{{/each}}
{{#each activityMetrics as |metric|}}
{{admin-report
showHeader=false
filters=activityMetricsFilters
forcedModes="counters"
dataSourceName=metric}}
{{/each}}
</div>
</div>
</div>
</div>
{{/if}}
{{/if}}
<div class="user-metrics">
@ -130,20 +134,22 @@
</div>
</div>
<div class="section-column">
{{admin-report
filters=topReferredTopicsFilters
dataSourceName="top_referred_topics"
reportOptions=topReferredTopicsOptions}}
{{#if isSearchReportsVisible}}
<div class="section-column">
{{admin-report
filters=topReferredTopicsFilters
dataSourceName="top_referred_topics"
reportOptions=topReferredTopicsOptions}}
{{admin-report
dataSourceName="trending_search"
reportOptions=trendingSearchOptions
filters=trendingSearchFilters
isEnabled=logSearchQueriesEnabled
disabledLabel=trendingSearchDisabledLabel}}
{{html-safe (i18n "admin.dashboard.reports.trending_search.more" basePath=basePath)}}
</div>
{{admin-report
dataSourceName="trending_search"
reportOptions=trendingSearchOptions
filters=trendingSearchFilters
isEnabled=logSearchQueriesEnabled
disabledLabel=trendingSearchDisabledLabel}}
{{html-safe (i18n "admin.dashboard.reports.trending_search.more" basePath=basePath)}}
</div>
{{/if}}
</div>
{{plugin-outlet name="admin-dashboard-general-bottom"}}

View File

@ -1,27 +1,29 @@
<div class="sections">
{{plugin-outlet name="admin-dashboard-moderation-top"}}
<div class="moderators-activity section">
<div class="section-title">
<h2>
<a href={{get-url "/admin/reports/moderators_activity"}}>
{{i18n "admin.dashboard.moderators_activity"}}
</a>
</h2>
{{period-chooser
period=period
action=(action "changePeriod")
content=availablePeriods
fullDay=true}}
</div>
{{#if isModeratorsActivityVisible}}
<div class="moderators-activity section">
<div class="section-title">
<h2>
<a href={{get-url "/admin/reports/moderators_activity"}}>
{{i18n "admin.dashboard.moderators_activity"}}
</a>
</h2>
{{period-chooser
period=period
action=(action "changePeriod")
content=availablePeriods
fullDay=true}}
</div>
<div class="section-body">
{{admin-report
filters=filters
showHeader=false
dataSourceName="moderators_activity"}}
<div class="section-body">
{{admin-report
filters=filters
showHeader=false
dataSourceName="moderators_activity"}}
</div>
</div>
</div>
{{/if}}
<div class="main-section">
{{admin-report

View File

@ -7,14 +7,14 @@
<div class="modal-footer">
{{#d-button
class="btn-danger"
action=(action "merge")
action=(action "confirm")
icon="trash-alt"
disabled=mergeDisabled
}}
{{i18n "admin.user.merge.confirmation.transfer_and_delete" username=username}}
{{/d-button}}
{{d-button
action=(action "cancel")
action=(action "close")
label="admin.user.merge.confirmation.cancel"
}}
</div>

View File

@ -10,14 +10,14 @@
<div class="modal-footer">
{{#d-button
class="btn-primary"
action=(action "merge")
action=(action "showConfirmation")
icon="trash-alt"
disabled=mergeDisabled
}}
{{i18n "admin.user.merge.prompt.transfer_and_delete" username=username}}
{{/d-button}}
{{d-button
action=(action "cancel")
action=(action "close")
label="admin.user.merge.prompt.cancel"
}}
</div>

View File

@ -686,8 +686,8 @@
{{/if}}
{{#if model.can_be_merged}}
{{d-button label="admin.user.merge.transfer_and_delete"
icon="trash-alt"
{{d-button label="admin.user.merge.button"
icon="arrows-alt-h"
class="btn-danger"
action=(action "promptTargetUser")}}
{{/if}}

View File

@ -0,0 +1,11 @@
// discourse-skip-module
(function() {
if (window.unsupportedBrowser) {
throw "Unsupported browser detected";
}
let Discourse = requirejs("discourse/app").default;
// ensure Discourse is added as a global
window.Discourse = Discourse;
})();

View File

@ -1,104 +1,101 @@
//= require_tree ./ember-addons/utils
//= require_tree ./discourse-common
//= require ./ember-addons/decorator-alias
//= require ./ember-addons/macro-alias
//= require ./ember-addons/fmt
//= require_tree ./discourse-common/addon
//= require ./polyfills
//= require_tree ./select-kit
//= require ./discourse
//= require ./deprecated
//= require ./discourse/app/app
//= require ./app-boot
// Stuff we need to load first
//= require ./discourse/lib/to-markdown
//= require ./discourse/lib/utilities
//= require ./discourse/lib/user-presence
//= require ./discourse/lib/logout
//= require ./discourse/mixins/singleton
//= require ./discourse/models/rest
//= require ./discourse/models/session
//= require ./discourse/lib/ajax
//= require ./discourse/lib/text
//= require ./discourse/lib/hash
//= require ./discourse/lib/load-script
//= require ./discourse/lib/notification-levels
//= require ./discourse/services/app-events
//= require ./discourse/lib/offset-calculator
//= require ./discourse/lib/lock-on
//= require ./discourse/lib/url
//= require ./discourse/lib/debounce
//= require ./discourse/lib/quote
//= require ./discourse/lib/key-value-store
//= require ./discourse/lib/computed
//= require ./discourse/lib/formatter
//= require ./discourse/lib/text-direction
//= require ./discourse/lib/eyeline
//= require ./discourse/lib/show-modal
//= require ./discourse/mixins/scrolling
//= require ./discourse/lib/ajax-error
//= require ./discourse/models/result-set
//= require ./discourse/models/store
//= require ./discourse/models/action-summary
//= require ./discourse/models/permission-type
//= require ./discourse/models/category
//= require ./discourse/models/topic
//= require ./discourse/models/draft
//= require ./discourse/models/composer
//= require ./discourse/models/badge-grouping
//= require ./discourse/models/badge
//= require ./discourse/models/permission-type
//= require ./discourse/models/user-action-group
//= require ./discourse/models/trust-level
//= require ./discourse/lib/search
//= require ./discourse/lib/user-search
//= require ./discourse/lib/export-csv
//= require ./discourse/lib/autocomplete
//= require ./discourse/lib/after-transition
//= require ./discourse/lib/safari-hacks
//= require ./discourse/lib/put-cursor-at-end
//= require_tree ./discourse/adapters
//= require ./discourse/models/post-action-type
//= require ./discourse/models/post
//= require ./discourse/lib/posts-with-placeholders
//= require ./discourse/models/post-stream
//= require ./discourse/models/topic-details
//= require ./discourse/models/topic
//= require ./discourse/models/user-action
//= require ./discourse/models/draft
//= require ./discourse/models/composer
//= require ./discourse/models/user-badge
//= require_tree ./discourse/lib
//= require_tree ./discourse/mixins
//= require ./discourse/models/invite
//= require ./discourse/controllers/discovery-sortable
//= require ./discourse/controllers/navigation/default
//= require ./discourse/components/edit-category-panel
//= require ./discourse/lib/link-mentions
//= require ./discourse/components/site-header
//= require ./discourse/components/d-editor
//= require ./discourse/lib/screen-track
//= require ./discourse/routes/discourse
//= require ./discourse/routes/build-topic-route
//= require ./discourse/routes/restricted-user
//= require ./discourse/routes/user-topic-list
//= require ./discourse/routes/user-activity-stream
//= require ./discourse/routes/topic-from-params
//= require ./discourse/components/text-field
//= require ./discourse/components/conditional-loading-spinner
//= require ./discourse/helpers/user-avatar
//= require ./discourse/helpers/cold-age-class
//= require ./discourse/helpers/loading-spinner
//= require ./discourse/helpers/category-link
//= require ./discourse/lib/export-result
//= require ./discourse/mapping-router
//= require ./discourse/app/lib/to-markdown
//= require ./discourse/app/lib/utilities
//= require ./discourse/app/lib/user-presence
//= require ./discourse/app/lib/logout
//= require ./discourse/app/mixins/singleton
//= require ./discourse/app/models/rest
//= require ./discourse/app/models/session
//= require ./discourse/app/lib/ajax
//= require ./discourse/app/lib/text
//= require ./discourse/app/lib/hash
//= require ./discourse/app/lib/load-script
//= require ./discourse/app/lib/notification-levels
//= require ./discourse/app/services/app-events
//= require ./discourse/app/lib/offset-calculator
//= require ./discourse/app/lib/lock-on
//= require ./discourse/app/lib/url
//= require ./discourse/app/lib/debounce
//= require ./discourse/app/lib/quote
//= require ./discourse/app/lib/key-value-store
//= require ./discourse/app/lib/computed
//= require ./discourse/app/lib/formatter
//= require ./discourse/app/lib/text-direction
//= require ./discourse/app/lib/eyeline
//= require ./discourse/app/lib/show-modal
//= require ./discourse/app/mixins/scrolling
//= require ./discourse/app/lib/ajax-error
//= require ./discourse/app/models/result-set
//= require ./discourse/app/models/store
//= require ./discourse/app/models/action-summary
//= require ./discourse/app/models/permission-type
//= require ./discourse/app/models/category
//= require ./discourse/app/models/topic
//= require ./discourse/app/models/draft
//= require ./discourse/app/models/composer
//= require ./discourse/app/models/badge-grouping
//= require ./discourse/app/models/badge
//= require ./discourse/app/models/permission-type
//= require ./discourse/app/models/user-action-group
//= require ./discourse/app/models/trust-level
//= require ./discourse/app/lib/search
//= require ./discourse/app/lib/user-search
//= require ./discourse/app/lib/export-csv
//= require ./discourse/app/lib/autocomplete
//= require ./discourse/app/lib/after-transition
//= require ./discourse/app/lib/safari-hacks
//= require ./discourse/app/lib/put-cursor-at-end
//= require_tree ./discourse/app/adapters
//= require ./discourse/app/models/post-action-type
//= require ./discourse/app/models/post
//= require ./discourse/app/lib/posts-with-placeholders
//= require ./discourse/app/models/post-stream
//= require ./discourse/app/models/topic-details
//= require ./discourse/app/models/topic
//= require ./discourse/app/models/user-action
//= require ./discourse/app/models/draft
//= require ./discourse/app/models/composer
//= require ./discourse/app/models/user-badge
//= require_tree ./discourse/app/lib
//= require_tree ./discourse/app/mixins
//= require ./discourse/app/models/invite
//= require ./discourse/app/controllers/discovery-sortable
//= require ./discourse/app/controllers/navigation/default
//= require ./discourse/app/components/edit-category-panel
//= require ./discourse/app/lib/link-mentions
//= require ./discourse/app/components/site-header
//= require ./discourse/app/components/d-editor
//= require ./discourse/app/lib/screen-track
//= require ./discourse/app/routes/discourse
//= require ./discourse/app/routes/build-topic-route
//= require ./discourse/app/routes/restricted-user
//= require ./discourse/app/routes/user-topic-list
//= require ./discourse/app/routes/user-activity-stream
//= require ./discourse/app/routes/topic-from-params
//= require ./discourse/app/components/text-field
//= require ./discourse/app/components/conditional-loading-spinner
//= require ./discourse/app/helpers/user-avatar
//= require ./discourse/app/helpers/cold-age-class
//= require ./discourse/app/helpers/loading-spinner
//= require ./discourse/app/helpers/category-link
//= require ./discourse/app/lib/export-result
//= require ./discourse/app/mapping-router
//= require_tree ./discourse/controllers
//= require_tree ./discourse/models
//= require_tree ./discourse/components
//= require_tree ./discourse/raw-views
//= require_tree ./discourse/helpers
//= require_tree ./discourse/templates
//= require_tree ./discourse/routes
//= require_tree ./discourse/pre-initializers
//= require_tree ./discourse/initializers
//= require_tree ./discourse/services
//= require_tree ./discourse/widgets
//= require_tree ./discourse/app/controllers
//= require_tree ./discourse/app/models
//= require_tree ./discourse/app/components
//= require_tree ./discourse/app/raw-views
//= require_tree ./discourse/app/helpers
//= require_tree ./discourse/app/templates
//= require_tree ./discourse/app/routes
//= require_tree ./discourse/app/pre-initializers
//= require_tree ./discourse/app/initializers
//= require_tree ./discourse/app/services
//= require_tree ./discourse/app/widgets

View File

@ -1,3 +1,12 @@
if (!window.WeakMap || !window.Promise) {
window.unsupportedBrowser = true;
} else {
// Some implementations of `WeakMap.prototype.has` do not accept false
// values and Ember's `isClassicDecorator` sometimes does that (it only
// checks for `null` and `undefined`).
try {
new WeakMap().has(0);
} catch (err) {
window.unsupportedBrowser = true;
}
}

View File

@ -1,4 +0,0 @@
// ensure Discourse is added as a global
(function() {
window.Discourse = requirejs("discourse").default;
})();

View File

@ -0,0 +1,3 @@
export const INPUT_DELAY = 250;
export default { environment: Ember.testing ? "test" : "development" };

View File

@ -0,0 +1,7 @@
import { registerUnbound } from "discourse-common/lib/helpers";
import { renderIcon } from "discourse-common/lib/icon-library";
import { htmlSafe } from "@ember/template";
registerUnbound("d-icon", function(id, params) {
return htmlSafe(renderIcon("string", id, params));
});

View File

@ -0,0 +1,13 @@
import { registerUnbound } from "discourse-common/lib/helpers";
import { renderIcon } from "discourse-common/lib/icon-library";
import deprecated from "discourse-common/lib/deprecated";
import { htmlSafe } from "@ember/template";
export function iconHTML(id, params) {
return renderIcon("string", id, params);
}
registerUnbound("fa-icon", function(icon, params) {
deprecated("Use `{{d-icon}}` instead of `{{fa-icon}}");
return htmlSafe(iconHTML(icon, params));
});

View File

@ -0,0 +1,94 @@
import { get } from "@ember/object";
import Helper from "@ember/component/helper";
import RawHandlebars from "discourse-common/lib/raw-handlebars";
import { htmlSafe } from "@ember/template";
export function makeArray(obj) {
if (obj === null || obj === undefined) {
return [];
}
return Array.isArray(obj) ? obj : [obj];
}
export function htmlHelper(fn) {
return Helper.helper(function(...args) {
args =
args.length > 1 ? args[0].concat({ hash: args[args.length - 1] }) : args;
return htmlSafe(fn.apply(this, args) || "");
});
}
const _helpers = {};
function rawGet(ctx, property, options) {
if (options.types && options.data.view) {
var view = options.data.view;
return view.getStream
? view.getStream(property).value()
: view.getAttr(property);
} else {
return get(ctx, property);
}
}
export function registerHelper(name, fn) {
_helpers[name] = Helper.helper(fn);
}
export function findHelper(name) {
return _helpers[name] || _helpers[name.dasherize()];
}
export function registerHelpers(registry) {
Object.keys(_helpers).forEach(name => {
registry.register(`helper:${name}`, _helpers[name], { singleton: false });
});
}
function resolveParams(ctx, options) {
let params = {};
const hash = options.hash;
if (hash) {
if (options.hashTypes) {
Object.keys(hash).forEach(function(k) {
const type = options.hashTypes[k];
if (
type === "STRING" ||
type === "StringLiteral" ||
type === "SubExpression"
) {
params[k] = hash[k];
} else if (type === "ID" || type === "PathExpression") {
params[k] = rawGet(ctx, hash[k], options);
}
});
} else {
params = hash;
}
}
return params;
}
export function registerUnbound(name, fn) {
const func = function(...args) {
const options = args.pop();
const properties = args;
for (let i = 0; i < properties.length; i++) {
if (
options.types &&
(options.types[i] === "ID" || options.types[i] === "PathExpression")
) {
properties[i] = rawGet(this, properties[i], options);
}
}
return fn.call(this, ...properties, resolveParams(this, options));
};
_helpers[name] = Helper.extend({
compute: (params, args) => fn(...params, args)
});
RawHandlebars.registerHelper(name, func);
}

View File

@ -0,0 +1,132 @@
import Handlebars from "handlebars";
// This is a mechanism for quickly rendering templates which is Ember aware
// templates are highly compatible with Ember so you don't need to worry about calling "get"
// and discourseComputed properties function, additionally it uses stringParams like Ember does
const RawHandlebars = Handlebars.create();
function buildPath(blk, args) {
var result = {
type: "PathExpression",
data: false,
depth: blk.path.depth,
loc: blk.path.loc
};
// Server side precompile doesn't have jquery.extend
Object.keys(args).forEach(function(a) {
result[a] = args[a];
});
return result;
}
function replaceGet(ast) {
var visitor = new Handlebars.Visitor();
visitor.mutating = true;
visitor.MustacheStatement = function(mustache) {
if (!(mustache.params.length || mustache.hash)) {
mustache.params[0] = mustache.path;
mustache.path = buildPath(mustache, {
parts: ["get"],
original: "get",
strict: true,
falsy: true
});
}
return Handlebars.Visitor.prototype.MustacheStatement.call(this, mustache);
};
// rewrite `each x as |y|` as each y in x`
// This allows us to use the same syntax in all templates
visitor.BlockStatement = function(block) {
if (block.path.original === "each" && block.params.length === 1) {
var paramName = block.program.blockParams[0];
block.params = [
buildPath(block, { original: paramName }),
{ type: "CommentStatement", value: "in" },
block.params[0]
];
delete block.program.blockParams;
}
return Handlebars.Visitor.prototype.BlockStatement.call(this, block);
};
visitor.accept(ast);
}
if (Handlebars.Compiler) {
RawHandlebars.Compiler = function() {};
RawHandlebars.Compiler.prototype = Object.create(
Handlebars.Compiler.prototype
);
RawHandlebars.Compiler.prototype.compiler = RawHandlebars.Compiler;
RawHandlebars.JavaScriptCompiler = function() {};
RawHandlebars.JavaScriptCompiler.prototype = Object.create(
Handlebars.JavaScriptCompiler.prototype
);
RawHandlebars.JavaScriptCompiler.prototype.compiler =
RawHandlebars.JavaScriptCompiler;
RawHandlebars.JavaScriptCompiler.prototype.namespace = "RawHandlebars";
RawHandlebars.precompile = function(value, asObject) {
var ast = Handlebars.parse(value);
replaceGet(ast);
var options = {
knownHelpers: {
get: true
},
data: true,
stringParams: true
};
asObject = asObject === undefined ? true : asObject;
var environment = new RawHandlebars.Compiler().compile(ast, options);
return new RawHandlebars.JavaScriptCompiler().compile(
environment,
options,
undefined,
asObject
);
};
RawHandlebars.compile = function(string) {
var ast = Handlebars.parse(string);
replaceGet(ast);
// this forces us to rewrite helpers
var options = { data: true, stringParams: true };
var environment = new RawHandlebars.Compiler().compile(ast, options);
var templateSpec = new RawHandlebars.JavaScriptCompiler().compile(
environment,
options,
undefined,
true
);
var t = RawHandlebars.template(templateSpec);
t.isMethod = false;
return t;
};
}
export function template() {
return RawHandlebars.template.apply(this, arguments);
}
export function precompile() {
return RawHandlebars.precompile.apply(this, arguments);
}
export function compile() {
return RawHandlebars.compile.apply(this, arguments);
}
export default RawHandlebars;

View File

@ -0,0 +1,22 @@
import extractValue from "discourse-common/utils/extract-value";
export default function decoratorAlias(fn, errorMessage) {
return function(...params) {
// determine if user called as @discourseComputed('blah', 'blah') or @discourseComputed
if (params.length === 0) {
throw new Error(errorMessage);
} else {
return function(target, key, desc) {
return {
enumerable: desc.enumerable,
configurable: desc.configurable,
writable: desc.writable,
initializer: function() {
var value = extractValue(desc);
return fn.apply(null, params.concat(value));
}
};
};
}
};
}

View File

@ -0,0 +1,78 @@
import handleDescriptor from "discourse-common/utils/handle-descriptor";
import isDescriptor from "discourse-common/utils/is-descriptor";
import extractValue from "discourse-common/utils/extract-value";
import decoratorAlias from "discourse-common/utils/decorator-alias";
import macroAlias from "discourse-common/utils/macro-alias";
import { schedule, next } from "@ember/runloop";
export default function discourseComputedDecorator(...params) {
// determine if user called as @discourseComputed('blah', 'blah') or @discourseComputed
if (isDescriptor(params[params.length - 1])) {
return handleDescriptor(...arguments);
} else {
return function(/* target, key, desc */) {
return handleDescriptor(...arguments, params);
};
}
}
export function afterRender(target, name, descriptor) {
const originalFunction = descriptor.value;
descriptor.value = function() {
next(() => {
schedule("afterRender", () => {
if (this.element && !this.isDestroying && !this.isDestroyed) {
return originalFunction.apply(this, arguments);
}
});
});
};
}
export function readOnly(target, name, desc) {
return {
writable: false,
enumerable: desc.enumerable,
configurable: desc.configurable,
initializer: function() {
var value = extractValue(desc);
return value.readOnly();
}
};
}
/* eslint-disable */
export var on = decoratorAlias(Ember.on, "Can not `on` without event names");
export var observes = decoratorAlias(
Ember.observer,
"Can not `observe` without property names"
);
export var alias = macroAlias(Ember.computed.alias);
export var and = macroAlias(Ember.computed.and);
export var bool = macroAlias(Ember.computed.bool);
export var collect = macroAlias(Ember.computed.collect);
export var empty = macroAlias(Ember.computed.empty);
export var equal = macroAlias(Ember.computed.equal);
export var filter = macroAlias(Ember.computed.filter);
export var filterBy = macroAlias(Ember.computed.filterBy);
export var gt = macroAlias(Ember.computed.gt);
export var gte = macroAlias(Ember.computed.gte);
export var lt = macroAlias(Ember.computed.lt);
export var lte = macroAlias(Ember.computed.lte);
export var map = macroAlias(Ember.computed.map);
export var mapBy = macroAlias(Ember.computed.mapBy);
export var match = macroAlias(Ember.computed.match);
export var max = macroAlias(Ember.computed.max);
export var min = macroAlias(Ember.computed.min);
export var none = macroAlias(Ember.computed.none);
export var not = macroAlias(Ember.computed.not);
export var notEmpty = macroAlias(Ember.computed.notEmpty);
export var oneWay = macroAlias(Ember.computed.oneWay);
export var or = macroAlias(Ember.computed.or);
export var reads = macroAlias(Ember.computed.reads);
export var setDiff = macroAlias(Ember.computed.setDiff);
export var sort = macroAlias(Ember.computed.sort);
export var sum = macroAlias(Ember.computed.sum);
export var union = macroAlias(Ember.computed.union);
export var uniq = macroAlias(Ember.computed.uniq);

View File

@ -0,0 +1,5 @@
export default function extractValue(desc) {
return (
desc.value || (typeof desc.initializer === "function" && desc.initializer())
);
}

View File

@ -0,0 +1,71 @@
import { computed, get } from "@ember/object";
import extractValue from "./extract-value";
export default function handleDescriptor(target, key, desc, params = []) {
return {
enumerable: desc.enumerable,
configurable: desc.configurable,
writeable: desc.writeable,
initializer: function() {
let computedDescriptor;
if (desc.writable) {
var val = extractValue(desc);
if (typeof val === "object") {
let value = {};
if (val.get) {
value.get = callUserSuppliedGet(params, val.get);
}
if (val.set) {
value.set = callUserSuppliedSet(params, val.set);
}
computedDescriptor = value;
} else {
computedDescriptor = callUserSuppliedGet(params, val);
}
} else {
throw new Error(
"ember-computed-decorators does not support using getters and setters"
);
}
return computed.apply(null, params.concat(computedDescriptor));
}
};
}
function niceAttr(attr) {
const parts = attr.split(".");
let i;
for (i = 0; i < parts.length; i++) {
if (
parts[i] === "@each" ||
parts[i] === "[]" ||
parts[i].indexOf("{") !== -1
) {
break;
}
}
return parts.slice(0, i).join(".");
}
function callUserSuppliedGet(params, func) {
params = params.map(niceAttr);
return function() {
let paramValues = params.map(p => get(this, p));
return func.apply(this, paramValues);
};
}
function callUserSuppliedSet(params, func) {
params = params.map(niceAttr);
return function(key, value) {
let paramValues = params.map(p => get(this, p));
paramValues.unshift(value);
return func.apply(this, paramValues);
};
}

View File

@ -0,0 +1,9 @@
export default function isDescriptor(item) {
return (
item &&
typeof item === "object" &&
"writable" in item &&
"enumerable" in item &&
"configurable" in item
);
}

View File

@ -0,0 +1,24 @@
import isDescriptor from "discourse-common/utils/is-descriptor";
function handleDescriptor(target, property, desc, fn, params = []) {
return {
enumerable: desc.enumerable,
configurable: desc.configurable,
writable: desc.writable,
initializer: function() {
return fn(...params);
}
};
}
export default function macroAlias(fn) {
return function(...params) {
if (isDescriptor(params[params.length - 1])) {
return handleDescriptor(...params, fn);
} else {
return function(target, property, desc) {
return handleDescriptor(target, property, desc, fn, params);
};
}
};
}

View File

@ -0,0 +1,89 @@
"use strict";
const getChannelURL = require("ember-source-channel-url");
module.exports = async function() {
return {
useYarn: true,
scenarios: [
{
name: "ember-lts-3.8",
npm: {
devDependencies: {
"ember-source": "~3.8.0"
}
}
},
{
name: "ember-lts-3.12",
npm: {
devDependencies: {
"ember-source": "~3.12.0"
}
}
},
{
name: "ember-release",
npm: {
devDependencies: {
"ember-source": await getChannelURL("release")
}
}
},
{
name: "ember-beta",
npm: {
devDependencies: {
"ember-source": await getChannelURL("beta")
}
}
},
{
name: "ember-canary",
npm: {
devDependencies: {
"ember-source": await getChannelURL("canary")
}
}
},
// The default `.travis.yml` runs this scenario via `yarn test`,
// not via `ember try`. It's still included here so that running
// `ember try:each` manually or from a customized CI config will run it
// along with all the other scenarios.
{
name: "ember-default",
npm: {
devDependencies: {}
}
},
{
name: "ember-default-with-jquery",
env: {
EMBER_OPTIONAL_FEATURES: JSON.stringify({
"jquery-integration": true
})
},
npm: {
devDependencies: {
"@ember/jquery": "^0.5.1"
}
}
},
{
name: "ember-classic",
env: {
EMBER_OPTIONAL_FEATURES: JSON.stringify({
"application-template-wrapper": true,
"default-async-observers": false,
"template-only-glimmer-components": false
})
},
npm: {
ember: {
edition: "classic"
}
}
}
]
};
};

View File

@ -1,3 +1,5 @@
export const INPUT_DELAY = 250;
"use strict";
export default { environment: Ember.testing ? "test" : "development" };
module.exports = function(/* environment, appConfig */) {
return {};
};

View File

@ -0,0 +1,9 @@
"use strict";
const EmberAddon = require("ember-cli/lib/broccoli/ember-addon");
module.exports = function(defaults) {
let app = new EmberAddon(defaults, {});
return app.toTree();
};

View File

@ -1,6 +0,0 @@
import { registerUnbound } from "discourse-common/lib/helpers";
import { renderIcon } from "discourse-common/lib/icon-library";
registerUnbound("d-icon", function(id, params) {
return new Handlebars.SafeString(renderIcon("string", id, params));
});

View File

@ -1,12 +0,0 @@
import { registerUnbound } from "discourse-common/lib/helpers";
import { renderIcon } from "discourse-common/lib/icon-library";
import deprecated from "discourse-common/lib/deprecated";
export function iconHTML(id, params) {
return renderIcon("string", id, params);
}
registerUnbound("fa-icon", function(icon, params) {
deprecated("Use `{{d-icon}}` instead of `{{fa-icon}}");
return new Handlebars.SafeString(iconHTML(icon, params));
});

View File

@ -0,0 +1,12 @@
"use strict";
module.exports = {
name: require("./package").name,
options: {
autoImport: {
alias: {
handlebars: "handlebars/dist/cjs/handlebars.js"
}
}
}
};

View File

@ -1,93 +0,0 @@
import { get } from "@ember/object";
import Helper from "@ember/component/helper";
import RawHandlebars from "discourse-common/lib/raw-handlebars";
export function makeArray(obj) {
if (obj === null || obj === undefined) {
return [];
}
return Array.isArray(obj) ? obj : [obj];
}
export function htmlHelper(fn) {
return Helper.helper(function(...args) {
args =
args.length > 1 ? args[0].concat({ hash: args[args.length - 1] }) : args;
return new Handlebars.SafeString(fn.apply(this, args) || "");
});
}
const _helpers = {};
function rawGet(ctx, property, options) {
if (options.types && options.data.view) {
var view = options.data.view;
return view.getStream
? view.getStream(property).value()
: view.getAttr(property);
} else {
return get(ctx, property);
}
}
export function registerHelper(name, fn) {
_helpers[name] = Helper.helper(fn);
}
export function findHelper(name) {
return _helpers[name] || _helpers[name.dasherize()];
}
export function registerHelpers(registry) {
Object.keys(_helpers).forEach(name => {
registry.register(`helper:${name}`, _helpers[name], { singleton: false });
});
}
function resolveParams(ctx, options) {
let params = {};
const hash = options.hash;
if (hash) {
if (options.hashTypes) {
Object.keys(hash).forEach(function(k) {
const type = options.hashTypes[k];
if (
type === "STRING" ||
type === "StringLiteral" ||
type === "SubExpression"
) {
params[k] = hash[k];
} else if (type === "ID" || type === "PathExpression") {
params[k] = rawGet(ctx, hash[k], options);
}
});
} else {
params = hash;
}
}
return params;
}
export function registerUnbound(name, fn) {
const func = function(...args) {
const options = args.pop();
const properties = args;
for (let i = 0; i < properties.length; i++) {
if (
options.types &&
(options.types[i] === "ID" || options.types[i] === "PathExpression")
) {
properties[i] = rawGet(this, properties[i], options);
}
}
return fn.call(this, ...properties, resolveParams(this, options));
};
_helpers[name] = Helper.extend({
compute: (params, args) => fn(...params, args)
});
RawHandlebars.registerHelper(name, func);
}

View File

@ -1,131 +0,0 @@
// This is a mechanism for quickly rendering templates which is Ember aware
// templates are highly compatible with Ember so you don't need to worry about calling "get"
// and discourseComputed properties function, additionally it uses stringParams like Ember does
const RawHandlebars = Handlebars.create();
function buildPath(blk, args) {
var result = {
type: "PathExpression",
data: false,
depth: blk.path.depth,
loc: blk.path.loc
};
// Server side precompile doesn't have jquery.extend
Object.keys(args).forEach(function(a) {
result[a] = args[a];
});
return result;
}
function replaceGet(ast) {
var visitor = new Handlebars.Visitor();
visitor.mutating = true;
visitor.MustacheStatement = function(mustache) {
if (!(mustache.params.length || mustache.hash)) {
mustache.params[0] = mustache.path;
mustache.path = buildPath(mustache, {
parts: ["get"],
original: "get",
strict: true,
falsy: true
});
}
return Handlebars.Visitor.prototype.MustacheStatement.call(this, mustache);
};
// rewrite `each x as |y|` as each y in x`
// This allows us to use the same syntax in all templates
visitor.BlockStatement = function(block) {
if (block.path.original === "each" && block.params.length === 1) {
var paramName = block.program.blockParams[0];
block.params = [
buildPath(block, { original: paramName }),
{ type: "CommentStatement", value: "in" },
block.params[0]
];
delete block.program.blockParams;
}
return Handlebars.Visitor.prototype.BlockStatement.call(this, block);
};
visitor.accept(ast);
}
if (Handlebars.Compiler) {
RawHandlebars.Compiler = function() {};
RawHandlebars.Compiler.prototype = Object.create(
Handlebars.Compiler.prototype
);
RawHandlebars.Compiler.prototype.compiler = RawHandlebars.Compiler;
RawHandlebars.JavaScriptCompiler = function() {};
RawHandlebars.JavaScriptCompiler.prototype = Object.create(
Handlebars.JavaScriptCompiler.prototype
);
RawHandlebars.JavaScriptCompiler.prototype.compiler =
RawHandlebars.JavaScriptCompiler;
RawHandlebars.JavaScriptCompiler.prototype.namespace = "RawHandlebars";
RawHandlebars.precompile = function(value, asObject) {
var ast = Handlebars.parse(value);
replaceGet(ast);
var options = {
knownHelpers: {
get: true
},
data: true,
stringParams: true
};
asObject = asObject === undefined ? true : asObject;
var environment = new RawHandlebars.Compiler().compile(ast, options);
return new RawHandlebars.JavaScriptCompiler().compile(
environment,
options,
undefined,
asObject
);
};
RawHandlebars.compile = function(string) {
var ast = Handlebars.parse(string);
replaceGet(ast);
// this forces us to rewrite helpers
var options = { data: true, stringParams: true };
var environment = new RawHandlebars.Compiler().compile(ast, options);
var templateSpec = new RawHandlebars.JavaScriptCompiler().compile(
environment,
options,
undefined,
true
);
var t = RawHandlebars.template(templateSpec);
t.isMethod = false;
return t;
};
}
export function template() {
return RawHandlebars.template.apply(this, arguments);
}
export function precompile() {
return RawHandlebars.precompile.apply(this, arguments);
}
export function compile() {
return RawHandlebars.compile.apply(this, arguments);
}
export default RawHandlebars;

View File

@ -0,0 +1,55 @@
{
"name": "discourse-common",
"version": "1.0.0",
"description": "Shared code between discourse apps",
"author": "Discourse",
"license": "GPLv2",
"keywords": [
"ember-addon"
],
"repository": "",
"license": "",
"author": "",
"scripts": {
"build": "ember build",
"lint:hbs": "ember-template-lint .",
"lint:js": "eslint .",
"start": "ember serve"
},
"dependencies": {
"ember-cli-babel": "^7.13.0",
"ember-cli-htmlbars": "^4.2.0",
"ember-auto-import": "^1.5.3",
"handlebars": "^4.7.0"
},
"devDependencies": {
"@ember/optional-features": "^1.1.0",
"@glimmer/component": "^1.0.0",
"babel-eslint": "^10.0.3",
"broccoli-asset-rev": "^3.0.0",
"ember-cli": "~3.15.2",
"ember-cli-dependency-checker": "^3.2.0",
"ember-cli-eslint": "^5.1.0",
"ember-cli-inject-live-reload": "^2.0.1",
"ember-cli-sri": "^2.1.1",
"ember-cli-template-lint": "^1.0.0-beta.3",
"ember-cli-uglify": "^3.0.0",
"ember-disable-prototype-extensions": "^1.1.3",
"ember-export-application-global": "^2.0.1",
"ember-load-initializers": "^2.1.1",
"ember-maybe-import-regenerator": "^0.1.6",
"ember-resolver": "^7.0.0",
"ember-source": "~3.15.0",
"ember-source-channel-url": "^2.0.1",
"ember-try": "^1.4.0",
"eslint-plugin-ember": "^7.7.1",
"eslint-plugin-node": "^10.0.0",
"loader.js": "^4.7.0"
},
"engines": {
"node": "8.* || >= 10.*"
},
"ember": {
"edition": "octane"
}
}

View File

@ -1,80 +0,0 @@
import handleDescriptor from "ember-addons/utils/handle-descriptor";
import isDescriptor from "ember-addons/utils/is-descriptor";
import extractValue from "ember-addons/utils/extract-value";
import { schedule, next } from "@ember/runloop";
export default function discourseComputedDecorator(...params) {
// determine if user called as @discourseComputed('blah', 'blah') or @discourseComputed
if (isDescriptor(params[params.length - 1])) {
return handleDescriptor(...arguments);
} else {
return function(/* target, key, desc */) {
return handleDescriptor(...arguments, params);
};
}
}
export function afterRender(target, name, descriptor) {
const originalFunction = descriptor.value;
descriptor.value = function() {
next(() => {
schedule("afterRender", () => {
if (this.element && !this.isDestroying && !this.isDestroyed) {
return originalFunction.apply(this, arguments);
}
});
});
};
}
export function readOnly(target, name, desc) {
return {
writable: false,
enumerable: desc.enumerable,
configurable: desc.configurable,
initializer: function() {
var value = extractValue(desc);
return value.readOnly();
}
};
}
import decoratorAlias from "ember-addons/decorator-alias";
/* eslint-disable */
export var on = decoratorAlias(Ember.on, "Can not `on` without event names");
export var observes = decoratorAlias(
Ember.observer,
"Can not `observe` without property names"
);
import macroAlias from "ember-addons/macro-alias";
export var alias = macroAlias(Ember.computed.alias);
export var and = macroAlias(Ember.computed.and);
export var bool = macroAlias(Ember.computed.bool);
export var collect = macroAlias(Ember.computed.collect);
export var empty = macroAlias(Ember.computed.empty);
export var equal = macroAlias(Ember.computed.equal);
export var filter = macroAlias(Ember.computed.filter);
export var filterBy = macroAlias(Ember.computed.filterBy);
export var gt = macroAlias(Ember.computed.gt);
export var gte = macroAlias(Ember.computed.gte);
export var lt = macroAlias(Ember.computed.lt);
export var lte = macroAlias(Ember.computed.lte);
export var map = macroAlias(Ember.computed.map);
export var mapBy = macroAlias(Ember.computed.mapBy);
export var match = macroAlias(Ember.computed.match);
export var max = macroAlias(Ember.computed.max);
export var min = macroAlias(Ember.computed.min);
export var none = macroAlias(Ember.computed.none);
export var not = macroAlias(Ember.computed.not);
export var notEmpty = macroAlias(Ember.computed.notEmpty);
export var oneWay = macroAlias(Ember.computed.oneWay);
export var or = macroAlias(Ember.computed.or);
export var reads = macroAlias(Ember.computed.reads);
export var setDiff = macroAlias(Ember.computed.setDiff);
export var sort = macroAlias(Ember.computed.sort);
export var sum = macroAlias(Ember.computed.sum);
export var union = macroAlias(Ember.computed.union);
export var uniq = macroAlias(Ember.computed.uniq);

File diff suppressed because it is too large Load Diff

View File

@ -19,9 +19,7 @@ var define, requirejs;
default: Ember.ArrayProxy
},
"@ember/component": {
default: Ember.Component,
TextArea: Ember.TextArea,
TextField: Ember.TextField
default: Ember.Component
},
"@ember/controller": {
default: Ember.Controller,
@ -129,6 +127,12 @@ var define, requirejs;
"@ember/component/helper": {
default: Ember.Helper
},
"@ember/component/text-field": {
default: Ember.TextField
},
"@ember/component/text-area": {
default: Ember.TextArea
},
"@ember/error": {
default: Ember.error
},
@ -259,6 +263,14 @@ var define, requirejs;
function requireFrom(name, origin) {
name = transformForAliases(name);
if (name === "discourse") {
// eslint-disable-next-line no-console
console.log(
"discourse has been moved to `discourse/app` - please update your code"
);
name = "discourse/app";
}
if (name === "discourse/models/input-validation") {
// eslint-disable-next-line no-console
console.log(

View File

@ -1,223 +0,0 @@
/*global Mousetrap:true*/
import Application from "@ember/application";
import EmberObject, { computed } from "@ember/object";
import { buildResolver } from "discourse-common/resolver";
import discourseComputed, { observes } from "discourse-common/utils/decorators";
import FocusEvent from "discourse-common/mixins/focus-event";
import deprecated from "discourse-common/lib/deprecated";
if (window.unsupportedBrowser) {
throw "Unsupported browser detected";
}
const _pluginCallbacks = [];
const Discourse = Application.extend(FocusEvent, {
rootElement: "#main",
_docTitle: document.title,
RAW_TEMPLATES: {},
__widget_helpers: {},
customEvents: {
paste: "paste"
},
reset() {
this._super(...arguments);
Mousetrap.reset();
},
getURL(url) {
if (!url) return url;
// if it's a non relative URL, return it.
if (url !== "/" && !/^\/[^\/]/.test(url)) return url;
if (url[0] !== "/") url = "/" + url;
if (url.startsWith(Discourse.BaseUri)) return url;
return Discourse.BaseUri + url;
},
getURLWithCDN(url) {
url = Discourse.getURL(url);
// only relative urls
if (Discourse.CDN && /^\/[^\/]/.test(url)) {
url = Discourse.CDN + url;
} else if (Discourse.S3CDN) {
url = url.replace(Discourse.S3BaseUrl, Discourse.S3CDN);
}
return url;
},
Resolver: buildResolver("discourse"),
@observes("_docTitle", "hasFocus", "contextCount", "notificationCount")
_titleChanged() {
let title = this._docTitle || Discourse.SiteSettings.title;
// if we change this we can trigger changes on document.title
// only set if changed.
if ($("title").text() !== title) {
$("title").text(title);
}
let displayCount = this.displayCount;
let dynamicFavicon = this.currentUser && this.currentUser.dynamic_favicon;
if (displayCount > 0 && !dynamicFavicon) {
title = `(${displayCount}) ${title}`;
}
document.title = title;
},
@discourseComputed("contextCount", "notificationCount")
displayCount() {
return this.currentUser &&
this.currentUser.get("title_count_mode") === "notifications"
? this.notificationCount
: this.contextCount;
},
@observes("contextCount", "notificationCount")
faviconChanged() {
if (this.currentUser && this.currentUser.get("dynamic_favicon")) {
let url = Discourse.SiteSettings.site_favicon_url;
// Since the favicon is cached on the browser for a really long time, we
// append the favicon_url as query params to the path so that the cache
// is not used when the favicon changes.
if (/^http/.test(url)) {
url = Discourse.getURL("/favicon/proxied?" + encodeURIComponent(url));
}
var displayCount = this.displayCount;
new window.Favcount(url).set(displayCount);
}
},
updateContextCount(count) {
this.set("contextCount", count);
},
updateNotificationCount(count) {
if (!this.hasFocus) {
this.set("notificationCount", count);
}
},
incrementBackgroundContextCount() {
if (!this.hasFocus) {
this.set("backgroundNotify", true);
this.set("contextCount", (this.contextCount || 0) + 1);
}
},
@observes("hasFocus")
resetCounts() {
if (this.hasFocus && this.backgroundNotify) {
this.set("contextCount", 0);
}
this.set("backgroundNotify", false);
if (this.hasFocus) {
this.set("notificationCount", 0);
}
},
authenticationComplete(options) {
// TODO, how to dispatch this to the controller without the container?
const loginController = Discourse.__container__.lookup("controller:login");
return loginController.authenticationComplete(options);
},
// Start up the Discourse application by running all the initializers we've defined.
start() {
$("noscript").remove();
Object.keys(requirejs._eak_seen).forEach(function(key) {
if (/\/pre\-initializers\//.test(key)) {
const module = requirejs(key, null, null, true);
if (!module) {
throw new Error(key + " must export an initializer.");
}
const init = module.default;
const oldInitialize = init.initialize;
init.initialize = function() {
oldInitialize.call(this, Discourse.__container__, Discourse);
};
Discourse.initializer(init);
}
});
Object.keys(requirejs._eak_seen).forEach(function(key) {
if (/\/initializers\//.test(key)) {
const module = requirejs(key, null, null, true);
if (!module) {
throw new Error(key + " must export an initializer.");
}
const init = module.default;
const oldInitialize = init.initialize;
init.initialize = function() {
oldInitialize.call(this, Discourse.__container__, Discourse);
};
Discourse.instanceInitializer(init);
}
});
// Plugins that are registered via `<script>` tags.
const withPluginApi = requirejs("discourse/lib/plugin-api").withPluginApi;
let initCount = 0;
_pluginCallbacks.forEach(function(cb) {
Discourse.instanceInitializer({
name: "_discourse_plugin_" + ++initCount,
after: "inject-objects",
initialize() {
withPluginApi(cb.version, cb.code);
}
});
});
},
@discourseComputed("currentAssetVersion", "desiredAssetVersion")
requiresRefresh(currentAssetVersion, desiredAssetVersion) {
return desiredAssetVersion && currentAssetVersion !== desiredAssetVersion;
},
_registerPluginCode(version, code) {
_pluginCallbacks.push({ version, code });
},
assetVersion: computed({
get() {
return this.currentAssetVersion;
},
set(key, val) {
if (val) {
if (this.currentAssetVersion) {
this.set("desiredAssetVersion", val);
} else {
this.set("currentAssetVersion", val);
}
}
return this.currentAssetVersion;
}
})
}).create();
Object.defineProperty(Discourse, "Model", {
get() {
deprecated("Use an `@ember/object` instead of Discourse.Model", {
since: "2.4.0",
dropFrom: "2.5.0"
});
return EmberObject;
}
});
export default Discourse;

View File

@ -0,0 +1,202 @@
/*global Mousetrap:true*/
import Application from "@ember/application";
import { computed } from "@ember/object";
import { buildResolver } from "discourse-common/resolver";
import discourseComputed, { observes } from "discourse-common/utils/decorators";
import FocusEvent from "discourse-common/mixins/focus-event";
const _pluginCallbacks = [];
const Discourse = Application.extend(FocusEvent, {
rootElement: "#main",
_docTitle: document.title,
RAW_TEMPLATES: {},
__widget_helpers: {},
customEvents: {
paste: "paste"
},
reset() {
this._super(...arguments);
Mousetrap.reset();
},
getURL(url) {
if (!url) return url;
// if it's a non relative URL, return it.
if (url !== "/" && !/^\/[^\/]/.test(url)) return url;
if (url[0] !== "/") url = "/" + url;
if (url.startsWith(Discourse.BaseUri)) return url;
return Discourse.BaseUri + url;
},
getURLWithCDN(url) {
url = Discourse.getURL(url);
// only relative urls
if (Discourse.CDN && /^\/[^\/]/.test(url)) {
url = Discourse.CDN + url;
} else if (Discourse.S3CDN) {
url = url.replace(Discourse.S3BaseUrl, Discourse.S3CDN);
}
return url;
},
Resolver: buildResolver("discourse"),
@observes("_docTitle", "hasFocus", "contextCount", "notificationCount")
_titleChanged() {
let title = this._docTitle || Discourse.SiteSettings.title;
let displayCount = this.displayCount;
let dynamicFavicon = this.currentUser && this.currentUser.dynamic_favicon;
if (displayCount > 0 && !dynamicFavicon) {
title = `(${displayCount}) ${title}`;
}
document.title = title;
},
@discourseComputed("contextCount", "notificationCount")
displayCount() {
return this.currentUser &&
this.currentUser.get("title_count_mode") === "notifications"
? this.notificationCount
: this.contextCount;
},
@observes("contextCount", "notificationCount")
faviconChanged() {
if (this.currentUser && this.currentUser.get("dynamic_favicon")) {
let url = Discourse.SiteSettings.site_favicon_url;
// Since the favicon is cached on the browser for a really long time, we
// append the favicon_url as query params to the path so that the cache
// is not used when the favicon changes.
if (/^http/.test(url)) {
url = Discourse.getURL("/favicon/proxied?" + encodeURIComponent(url));
}
var displayCount = this.displayCount;
new window.Favcount(url).set(displayCount);
}
},
updateContextCount(count) {
this.set("contextCount", count);
},
updateNotificationCount(count) {
if (!this.hasFocus) {
this.set("notificationCount", count);
}
},
incrementBackgroundContextCount() {
if (!this.hasFocus) {
this.set("backgroundNotify", true);
this.set("contextCount", (this.contextCount || 0) + 1);
}
},
@observes("hasFocus")
resetCounts() {
if (this.hasFocus && this.backgroundNotify) {
this.set("contextCount", 0);
}
this.set("backgroundNotify", false);
if (this.hasFocus) {
this.set("notificationCount", 0);
}
},
authenticationComplete(options) {
// TODO, how to dispatch this to the controller without the container?
const loginController = Discourse.__container__.lookup("controller:login");
return loginController.authenticationComplete(options);
},
// Start up the Discourse application by running all the initializers we've defined.
start() {
$("noscript").remove();
Object.keys(requirejs._eak_seen).forEach(function(key) {
if (/\/pre\-initializers\//.test(key)) {
const module = requirejs(key, null, null, true);
if (!module) {
throw new Error(key + " must export an initializer.");
}
const init = module.default;
const oldInitialize = init.initialize;
init.initialize = function() {
oldInitialize.call(this, Discourse.__container__, Discourse);
};
Discourse.initializer(init);
}
});
Object.keys(requirejs._eak_seen).forEach(function(key) {
if (/\/initializers\//.test(key)) {
const module = requirejs(key, null, null, true);
if (!module) {
throw new Error(key + " must export an initializer.");
}
const init = module.default;
const oldInitialize = init.initialize;
init.initialize = function() {
oldInitialize.call(this, Discourse.__container__, Discourse);
};
Discourse.instanceInitializer(init);
}
});
// Plugins that are registered via `<script>` tags.
const withPluginApi = requirejs("discourse/lib/plugin-api").withPluginApi;
let initCount = 0;
_pluginCallbacks.forEach(function(cb) {
Discourse.instanceInitializer({
name: "_discourse_plugin_" + ++initCount,
after: "inject-objects",
initialize() {
withPluginApi(cb.version, cb.code);
}
});
});
},
@discourseComputed("currentAssetVersion", "desiredAssetVersion")
requiresRefresh(currentAssetVersion, desiredAssetVersion) {
return desiredAssetVersion && currentAssetVersion !== desiredAssetVersion;
},
_registerPluginCode(version, code) {
_pluginCallbacks.push({ version, code });
},
assetVersion: computed({
get() {
return this.currentAssetVersion;
},
set(key, val) {
if (val) {
if (this.currentAssetVersion) {
this.set("desiredAssetVersion", val);
} else {
this.set("currentAssetVersion", val);
}
}
return this.currentAssetVersion;
}
})
}).create();
export default Discourse;

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