Version bump
This commit is contained in:
commit
d46b486633
@ -13,3 +13,5 @@ vendor/
|
||||
test/javascripts/test_helper.js
|
||||
test/javascripts/fixtures
|
||||
test/javascripts/helpers/assertions.js
|
||||
node_modules/
|
||||
dist/
|
||||
|
||||
@ -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
10
.gitignore
vendored
@ -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
|
||||
@ -179,6 +179,8 @@ RSpec/DescribedClassModuleWrapping:
|
||||
|
||||
RSpec/EmptyExampleGroup:
|
||||
Enabled: true
|
||||
Exclude:
|
||||
- 'spec/requests/api/*'
|
||||
|
||||
RSpec/EmptyLineAfterExample:
|
||||
Enabled: false # TODO
|
||||
|
||||
14
Gemfile
14
Gemfile
@ -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
|
||||
|
||||
66
Gemfile.lock
66
Gemfile.lock
@ -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
|
||||
|
||||
@ -66,7 +66,7 @@ Plus *lots* of Ruby Gems, a complete list of which is at [/master/Gemfile](https
|
||||
|
||||
## Contributing
|
||||
|
||||
[](https://travis-ci.org/discourse/discourse)
|
||||
[](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 – including you!
|
||||
|
||||
@ -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")) {
|
||||
|
||||
@ -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("|"));
|
||||
}
|
||||
});
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
},
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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>`;
|
||||
}
|
||||
|
||||
|
||||
@ -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");
|
||||
}
|
||||
});
|
||||
|
||||
@ -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");
|
||||
}
|
||||
});
|
||||
|
||||
@ -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));
|
||||
});
|
||||
|
||||
@ -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>"
|
||||
|
||||
@ -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"));
|
||||
});
|
||||
},
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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"}}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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}}
|
||||
|
||||
11
app/assets/javascripts/app-boot.js
Normal file
11
app/assets/javascripts/app-boot.js
Normal 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;
|
||||
})();
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
// ensure Discourse is added as a global
|
||||
(function() {
|
||||
window.Discourse = requirejs("discourse").default;
|
||||
})();
|
||||
@ -0,0 +1,3 @@
|
||||
export const INPUT_DELAY = 250;
|
||||
|
||||
export default { environment: Ember.testing ? "test" : "development" };
|
||||
@ -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));
|
||||
});
|
||||
@ -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));
|
||||
});
|
||||
94
app/assets/javascripts/discourse-common/addon/lib/helpers.js
Normal file
94
app/assets/javascripts/discourse-common/addon/lib/helpers.js
Normal 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);
|
||||
}
|
||||
@ -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;
|
||||
@ -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));
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -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);
|
||||
@ -0,0 +1,5 @@
|
||||
export default function extractValue(desc) {
|
||||
return (
|
||||
desc.value || (typeof desc.initializer === "function" && desc.initializer())
|
||||
);
|
||||
}
|
||||
@ -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);
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
export default function isDescriptor(item) {
|
||||
return (
|
||||
item &&
|
||||
typeof item === "object" &&
|
||||
"writable" in item &&
|
||||
"enumerable" in item &&
|
||||
"configurable" in item
|
||||
);
|
||||
}
|
||||
@ -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);
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
89
app/assets/javascripts/discourse-common/config/ember-try.js
Normal file
89
app/assets/javascripts/discourse-common/config/ember-try.js
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
};
|
||||
@ -1,3 +1,5 @@
|
||||
export const INPUT_DELAY = 250;
|
||||
"use strict";
|
||||
|
||||
export default { environment: Ember.testing ? "test" : "development" };
|
||||
module.exports = function(/* environment, appConfig */) {
|
||||
return {};
|
||||
};
|
||||
|
||||
@ -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();
|
||||
};
|
||||
@ -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));
|
||||
});
|
||||
@ -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));
|
||||
});
|
||||
12
app/assets/javascripts/discourse-common/index.js
Normal file
12
app/assets/javascripts/discourse-common/index.js
Normal file
@ -0,0 +1,12 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
name: require("./package").name,
|
||||
options: {
|
||||
autoImport: {
|
||||
alias: {
|
||||
handlebars: "handlebars/dist/cjs/handlebars.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -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);
|
||||
}
|
||||
@ -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;
|
||||
55
app/assets/javascripts/discourse-common/package.json
Normal file
55
app/assets/javascripts/discourse-common/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
0
app/assets/javascripts/discourse-common/vendor/.gitkeep
vendored
Normal file
0
app/assets/javascripts/discourse-common/vendor/.gitkeep
vendored
Normal file
10485
app/assets/javascripts/discourse-common/yarn.lock
Normal file
10485
app/assets/javascripts/discourse-common/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@ -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(
|
||||
|
||||
@ -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;
|
||||
202
app/assets/javascripts/discourse/app/app.js
Normal file
202
app/assets/javascripts/discourse/app/app.js
Normal 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
Reference in New Issue
Block a user