Version bump

This commit is contained in:
Neil Lalonde 2020-04-22 10:51:48 -04:00
commit 23fa6ff325
No known key found for this signature in database
GPG Key ID: FF871CA9037D0A91
2985 changed files with 92915 additions and 79032 deletions

View File

@ -3,7 +3,7 @@ app/assets/javascripts/main_include_admin.js
app/assets/javascripts/vendor.js
app/assets/javascripts/locales/i18n.js
app/assets/javascripts/ember-addons/
app/assets/javascripts/discourse/lib/autosize.js.es6
app/assets/javascripts/discourse/lib/autosize.js
lib/javascripts/locale/
lib/javascripts/messageformat.js
lib/highlight_js/

View File

@ -7,3 +7,36 @@
# DEV: enforces no self-closing-void-elements
dafd3c3b47f116c6c1dc56cb18df614c11747733
# Rename many `.js.es6` files to `.js`
032205e2029cbf82dc8f05b459fb93adf2503c60
# Rename admin app es6 -> js
181758e3248b14ad8b53abe063da8dc6a82d3089
# Rename pretty-text from es6 -> js
c15056650647e8650288f973d9038500dc9cf7bb
# Rename select kit from es6 -> js
acc5cbdf8ecb9293a0fa9474ee73baf499c02428
# Rename wizard from es6 -> js
1ac02422011f89716ab27250d39b0e0212e03892
# Rename discourse-common es6 -> js
167503ca4824e37a2e93d74b3f50271556d0ba8e
# Rename ember-addons es6 -> js
16ba50bce362c1eefe1881f86c67bec66f493abb
# Rename some root files
11938d58d4b1bea1ff43306450da7b24f05db0a
# The last remaining ES6
aabeb17aab4e73de5ef56753ab22ef5d416d2932
# DEV: enforces block-indentation of ember-template-lint rules
b66b277dc44bcd2122dc21965dab209c30636214
# DEV: enforces double quotes ember-template-lint
c4644c61d97c823b7dd940ffaf0967a104f4b58c

View File

@ -132,7 +132,7 @@ jobs:
if: env.BUILD_TYPE == 'LINT'
run: |
yarn prettier -v
yarn prettier --list-different "app/assets/stylesheets/**/*.scss" "app/assets/javascripts/**/*.es6" "test/javascripts/**/*.es6" "plugins/**/*.scss" "plugins/**/*.es6"
yarn prettier --list-different "app/assets/stylesheets/**/*.scss" "app/assets/javascripts/**/*.js" "app/assets/javascripts/**/*.es6" "test/javascripts/**/*.es6" "plugins/**/*.scss" "plugins/**/*.es6"
- name: Core RSpec
if: env.BUILD_TYPE == 'BACKEND' && env.TARGET == 'CORE'

View File

@ -1,8 +1,9 @@
require:
- rubocop-discourse
- rubocop-rspec
AllCops:
TargetRubyVersion: 2.4
TargetRubyVersion: 2.6
DisabledByDefault: true
Exclude:
- "db/schema.rb"
@ -12,6 +13,14 @@ AllCops:
- "public/**/*"
- "plugins/**/gems/**/*"
Discourse:
Enabled: true
Discourse/NoChdir:
Exclude:
- 'spec/**/*' # Specs are run sequentially, so chdir can be used
- 'plugins/*/spec/**/*'
# Prefer &&/|| over and/or.
Style/AndOr:
Enabled: true
@ -77,7 +86,7 @@ Layout/SpaceInsideParens:
Enabled: true
# Detect hard tabs, no hard tabs.
Layout/Tab:
Layout/IndentationStyle:
Enabled: true
# Blank lines should not have any spaces.
@ -127,15 +136,6 @@ Style/Semicolon:
Style/RedundantReturn:
Enabled: true
DiscourseCops/NoChdir:
Enabled: true
Exclude:
- 'spec/**/*' # Specs are run sequentially, so chdir can be used
- 'plugins/*/spec/**/*'
DiscourseCops/NoURIEscapeEncode:
Enabled: true
Style/GlobalVars:
Enabled: true
Severity: warning
@ -144,3 +144,212 @@ Style/GlobalVars:
- 'script/**/*'
- 'spec/**/*.rb'
- 'plugins/*/spec/**/*'
# Specs
RSpec/AnyInstance:
Enabled: false # To be decided
RSpec/AroundBlock:
Enabled: true
RSpec/BeforeAfterAll:
Enabled: false # To be decided
RSpec/ContextMethod:
Enabled: false # TODO
RSpec/ContextWording:
Enabled: false # To be decided
RSpec/DescribeClass:
Enabled: false # To be decided
RSpec/DescribeMethod:
Enabled: true
RSpec/DescribeSymbol:
Enabled: false # To be decided
RSpec/DescribedClass:
Enabled: false # To be decided
RSpec/DescribedClassModuleWrapping:
Enabled: false # To be decided
RSpec/EmptyExampleGroup:
Enabled: true
RSpec/EmptyLineAfterExample:
Enabled: false # TODO
RSpec/EmptyLineAfterExampleGroup:
Enabled: false # TODO
RSpec/EmptyLineAfterFinalLet:
Enabled: false # TODO
RSpec/EmptyLineAfterHook:
Enabled: false # TODO
RSpec/EmptyLineAfterSubject:
Enabled: false # TODO
RSpec/ExampleLength:
Enabled: false # To be decided
RSpec/ExampleWithoutDescription:
Enabled: true
RSpec/ExampleWording:
Enabled: false # TODO
RSpec/ExpectActual:
Enabled: true
RSpec/ExpectChange:
Enabled: false # To be decided
RSpec/ExpectInHook:
Enabled: false # To be decided
RSpec/ExpectOutput:
Enabled: true
RSpec/FilePath:
Enabled: false # To be decided
RSpec/Focus:
Enabled: true
RSpec/HookArgument:
Enabled: false # TODO
RSpec/HooksBeforeExamples:
Enabled: false # TODO
RSpec/ImplicitBlockExpectation:
Enabled: true
RSpec/ImplicitExpect:
Enabled: false # To be decided
RSpec/ImplicitSubject:
Enabled: false # To be decided
RSpec/InstanceSpy:
Enabled: true
RSpec/InstanceVariable:
Enabled: false # TODO
RSpec/InvalidPredicateMatcher:
Enabled: true
RSpec/ItBehavesLike:
Enabled: true
RSpec/IteratedExpectation:
Enabled: false # To be decided
RSpec/LeadingSubject:
Enabled: false # TODO
RSpec/LeakyConstantDeclaration:
Enabled: false # To be decided
RSpec/LetBeforeExamples:
Enabled: false # TODO
RSpec/LetSetup:
Enabled: false # TODO
RSpec/MessageChain:
Enabled: true
RSpec/MessageSpies:
Enabled: true
RSpec/MissingExampleGroupArgument:
Enabled: true
RSpec/MultipleDescribes:
Enabled: false # TODO
RSpec/MultipleSubjects:
Enabled: true
RSpec/NamedSubject:
Enabled: false # To be decided
RSpec/NestedGroups:
Enabled: false # To be decided
RSpec/OverwritingSetup:
Enabled: true
RSpec/ReceiveCounts:
Enabled: true
RSpec/ReceiveNever:
Enabled: true
RSpec/RepeatedDescription:
Enabled: false # TODO
RSpec/RepeatedExample:
Enabled: false # TODO
RSpec/RepeatedExampleGroupBody:
Enabled: false # TODO
RSpec/RepeatedExampleGroupDescription:
Enabled: false # TODO
RSpec/ReturnFromStub:
Enabled: true
RSpec/ScatteredSetup:
Enabled: false # TODO
RSpec/SharedContext:
Enabled: true
RSpec/SharedExamples:
Enabled: true
RSpec/SingleArgumentMessageChain:
Enabled: true
RSpec/SubjectStub:
Enabled: true
RSpec/UnspecifiedException:
Enabled: true
RSpec/VerifiedDoubles:
Enabled: true
RSpec/VoidExpect:
Enabled: true
RSpec/Yield:
Enabled: true
Capybara/CurrentPathExpectation:
Enabled: true
Capybara/FeatureMethods:
Enabled: true
FactoryBot/AttributeDefinedStatically:
Enabled: true
FactoryBot/CreateList:
Enabled: true
FactoryBot/FactoryClassName:
Enabled: true
Rails/HttpStatus:
Enabled: true

View File

@ -1,11 +1,56 @@
module.exports = {
// extends: "recommended",
extends: "recommended",
ignore: ["**/*.raw"],
// Pending:
// "eol-last": "always",
rules: {
"block-indentation": true,
"deprecated-render-helper": true,
"require-valid-alt-text": false,
"linebreak-style": true,
"link-rel-noopener": "strict",
"no-abstract-roles": true,
"no-args-paths": true,
"no-attrs-in-components": true,
"no-debugger": true,
"no-duplicate-attributes": true,
"no-extra-mut-helper-argument": true,
"no-html-comments": true,
"no-index-component-invocation": true,
"no-inline-styles": false,
"no-input-block": true,
"no-input-tagname": true,
"no-implicit-this": false,
"no-invalid-interactive": true,
"no-invalid-link-text": true,
"no-invalid-meta": true,
"no-invalid-role": true,
"no-log": true,
"no-negated-condition": true,
"no-nested-interactive": true,
"no-multiple-empty-lines": true,
"no-obsolete-elements": true,
"no-outlet-outside-routes": true,
"no-partial": true,
"no-positive-tabindex": false,
"no-quoteless-attributes": true,
"no-shadowed-elements": true,
"no-trailing-spaces": true,
"no-triple-curlies": true,
"no-unbound": true,
"no-unnecessary-concat": true,
"no-unnecessary-component-helper": true,
"no-unused-block-params": true,
quotes: "double",
"require-button-type": true,
"require-iframe-title": true,
"require-valid-alt-text": false,
"self-closing-void-elements": true,
"table-groups": true,
"simple-unless": true,
"style-concatenation": true,
"no-invalid-interactive": true
"table-groups": true,
"link-href-attributes": false
}
};

View File

@ -62,12 +62,6 @@ source_file = plugins/discourse-details/config/locales/server.en.yml
source_lang = en
type = YML
[discourse-org.corepluginnginx-performance-reportserveryml]
file_filter = plugins/discourse-nginx-performance-report/config/locales/server.<lang>.yml
source_file = plugins/discourse-nginx-performance-report/config/locales/server.en.yml
source_lang = en
type = YML
[discourse-org.core-plugin-local-dates-client-yml]
file_filter = plugins/discourse-local-dates/config/locales/client.<lang>.yml
source_file = plugins/discourse-local-dates/config/locales/client.en.yml

View File

@ -4,7 +4,7 @@ if github.pr_json && (github.pr_json["additions"] || 0) > 250 || (github.pr_json
warn("This pull request is big! We prefer smaller PRs whenever possible, as they are easier to review. Can this be split into a few smaller PRs?")
end
prettier_offenses = `yarn --silent prettier --list-different "app/assets/stylesheets/**/*.scss" "app/assets/javascripts/**/*.es6" "test/javascripts/**/*.es6"`.split("\n")
prettier_offenses = `yarn --silent prettier --list-different "app/assets/stylesheets/**/*.scss" "app/assets/javascripts/**/*.js" "app/assets/javascripts/**/*.es6" "test/javascripts/**/*.es6"`.split("\n")
unless prettier_offenses.empty?
fail(%{
@ -22,9 +22,9 @@ end
files = (git.added_files + git.modified_files)
.select { |path| !path.start_with?("plugins/") }
.select { |path| path.end_with?("es6") || path.end_with?("rb") }
.select { |path| path.end_with?("es6") || path.end_with?("js") || path.end_with?("rb") }
js_files = files.select { |path| path.end_with?(".js.es6") }
js_files = files.select { |path| path.end_with?(".js.es6") || path.end_with?(".js") }
js_test_files = js_files.select { |path| path.end_with?("-test.js.es6") }
super_offenses = []

34
Gemfile
View File

@ -18,16 +18,18 @@ else
# this allows us to include the bits of rails we use without pieces we do not.
#
# To issue a rails update bump the version number here
gem 'actionmailer', '6.0.1'
gem 'actionpack', '6.0.1'
gem 'actionview', '6.0.1'
gem 'activemodel', '6.0.1'
gem 'activerecord', '6.0.1'
gem 'activesupport', '6.0.1'
gem 'railties', '6.0.1'
gem 'actionmailer', '6.0.2.2'
gem 'actionpack', '6.0.2.2'
gem 'actionview', '6.0.2.2'
gem 'activemodel', '6.0.2.2'
gem 'activerecord', '6.0.2.2'
gem 'activesupport', '6.0.2.2'
gem 'railties', '6.0.2.2'
gem 'sprockets-rails'
end
gem 'json'
# TODO: At the moment Discourse does not work with Sprockets 4, we would need to correct internals
# This is a desired upgrade we should get to.
gem 'sprockets', '3.7.2'
@ -120,9 +122,6 @@ gem 'sanitize'
gem 'sidekiq'
gem 'mini_scheduler'
# for sidekiq web
gem 'tilt', require: false
gem 'execjs', require: false
gem 'mini_racer'
@ -161,10 +160,7 @@ group :test, :development do
gem 'listen', require: false
gem 'certified', require: false
gem 'fabrication', require: false
# TODO: upgrading to 1.10.1 cause it breaks our test suite.
# We want our test suite fixed though to support this upgrade.
gem 'mocha', '1.8.0', require: false
gem 'mocha', require: false
gem 'rb-fsevent', require: RUBY_PLATFORM =~ /darwin/i ? 'rb-fsevent' : false
@ -181,6 +177,7 @@ group :test, :development do
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'
end
@ -208,7 +205,7 @@ gem 'htmlentities', require: false
# we are open to it. by deferring require to the initializer we can configure discourse installs without it
gem 'flamegraph', require: false
gem 'rack-mini-profiler', require: false
gem 'rack-mini-profiler', require: ['enable_rails_patches']
gem 'unicorn', require: false, platform: :mri
gem 'puma', require: false
@ -258,3 +255,10 @@ end
gem 'webpush', require: false
gem 'colored2', require: false
gem 'maxminddb'
# These are not direct dependencies, but we need to restrict
# versions for compatibility with https://github.com/discourse/discourse-zendesk-plugin
# These restrictions can be removed once the zendesk_api gem is updated
# for newer versions of hashie and faraday
gem 'hashie', '< 4.0.0', require: false # https://github.com/zendesk/zendesk_api_client_rb/pull/422
gem 'faraday', '< 1.0.0', require: false # https://github.com/zendesk/zendesk_api_client_rb/pull/421

View File

@ -1,21 +1,21 @@
GEM
remote: https://rubygems.org/
specs:
actionmailer (6.0.1)
actionpack (= 6.0.1)
actionview (= 6.0.1)
activejob (= 6.0.1)
actionmailer (6.0.2.2)
actionpack (= 6.0.2.2)
actionview (= 6.0.2.2)
activejob (= 6.0.2.2)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (6.0.1)
actionview (= 6.0.1)
activesupport (= 6.0.1)
rack (~> 2.0)
actionpack (6.0.2.2)
actionview (= 6.0.2.2)
activesupport (= 6.0.2.2)
rack (~> 2.0, >= 2.0.8)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actionview (6.0.1)
activesupport (= 6.0.1)
actionview (6.0.2.2)
activesupport (= 6.0.2.2)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
@ -24,15 +24,15 @@ GEM
actionview (>= 6.0.a)
active_model_serializers (0.8.4)
activemodel (>= 3.0)
activejob (6.0.1)
activesupport (= 6.0.1)
activejob (6.0.2.2)
activesupport (= 6.0.2.2)
globalid (>= 0.3.6)
activemodel (6.0.1)
activesupport (= 6.0.1)
activerecord (6.0.1)
activemodel (= 6.0.1)
activesupport (= 6.0.1)
activesupport (6.0.1)
activemodel (6.0.2.2)
activesupport (= 6.0.2.2)
activerecord (6.0.2.2)
activemodel (= 6.0.2.2)
activesupport (= 6.0.2.2)
activesupport (6.0.2.2)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 0.7, < 2)
minitest (~> 5.1)
@ -40,28 +40,28 @@ GEM
zeitwerk (~> 2.2)
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
annotate (3.1.0)
annotate (3.1.1)
activerecord (>= 3.2, < 7.0)
rake (>= 10.4, < 14.0)
ast (2.4.0)
aws-eventstream (1.0.3)
aws-partitions (1.272.0)
aws-sdk-core (3.89.1)
aws-eventstream (~> 1.0, >= 1.0.2)
aws-eventstream (1.1.0)
aws-partitions (1.298.0)
aws-sdk-core (3.94.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.239.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.29.0)
aws-sdk-kms (1.30.0)
aws-sdk-core (~> 3, >= 3.71.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.60.2)
aws-sdk-s3 (1.62.0)
aws-sdk-core (~> 3, >= 3.83.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.1)
aws-sdk-sns (1.21.0)
aws-sdk-sns (1.22.0)
aws-sdk-core (~> 3, >= 3.71.0)
aws-sigv4 (~> 1.1)
aws-sigv4 (1.1.1)
aws-sigv4 (1.1.2)
aws-eventstream (~> 1.0, >= 1.0.2)
barber (0.12.2)
ember-source (>= 1.0, < 3.1)
@ -78,7 +78,7 @@ GEM
bullet (6.1.0)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
byebug (11.1.1)
byebug (11.1.2)
cbor (0.5.9.6)
certified (1.0.0)
chunky_png (1.3.11)
@ -86,9 +86,9 @@ GEM
colored2 (3.1.2)
concurrent-ruby (1.1.6)
connection_pool (2.2.2)
cose (0.11.0)
cose (1.0.0)
cbor (~> 0.5.9)
openssl-signature_algorithm (~> 0.3.0)
openssl-signature_algorithm (~> 0.4.0)
cppjieba_rb (0.3.3)
crack (0.4.3)
safe_yaml (~> 1.0.0)
@ -124,7 +124,7 @@ GEM
excon (0.72.0)
execjs (2.7.0)
exifr (1.3.6)
fabrication (2.21.0)
fabrication (2.21.1)
fakeweb (1.3.0)
faraday (0.17.3)
multipart-post (>= 1.2, < 3)
@ -157,6 +157,7 @@ GEM
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (2.3.0)
jwt (2.2.1)
kgio (2.11.3)
libv8 (7.3.492.27.1)
@ -171,8 +172,8 @@ GEM
logstash-event (1.2.02)
logstash-logger (0.26.1)
logstash-event (~> 1.2)
logster (2.7.1)
loofah (2.4.0)
logster (2.8.0)
loofah (2.5.0)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
lru_redux (1.1.0)
@ -181,23 +182,21 @@ GEM
mini_mime (>= 0.1.1)
maxminddb (0.1.22)
memory_profiler (0.9.14)
message_bus (2.2.3)
message_bus (2.2.4)
rack (>= 1.1.3)
metaclass (0.0.4)
method_source (0.9.2)
mini_mime (1.0.2)
mini_portile2 (2.4.0)
mini_racer (0.2.9)
libv8 (>= 6.9.411)
mini_racer (0.2.10)
libv8 (> 7.3)
mini_scheduler (0.12.2)
sidekiq
mini_sql (0.2.4)
mini_sql (0.2.5)
mini_suffix (0.3.0)
ffi (~> 1.9)
minitest (5.14.0)
mocha (1.8.0)
metaclass (~> 0.0.1)
mock_redis (0.22.0)
mocha (1.11.2)
mock_redis (0.23.0)
msgpack (1.3.3)
multi_json (1.14.1)
multi_xml (0.6.0)
@ -209,15 +208,15 @@ GEM
nokogumbo (2.0.2)
nokogiri (~> 1.8, >= 1.8.4)
oauth (0.5.4)
oauth2 (1.4.2)
oauth2 (1.4.4)
faraday (>= 0.8, < 2.0)
jwt (>= 1.0, < 3.0)
multi_json (~> 1.3)
multi_xml (~> 0.5)
rack (>= 1.2, < 3)
oj (3.10.5)
omniauth (1.9.0)
hashie (>= 3.4.6, < 3.7.0)
oj (3.10.6)
omniauth (1.9.1)
hashie (>= 3.4.6)
rack (>= 1.6.2, < 3)
omniauth-facebook (6.0.0)
omniauth-oauth2 (~> 1.2)
@ -240,21 +239,21 @@ GEM
omniauth-twitter (1.4.0)
omniauth-oauth (~> 1.1)
rack
onebox (1.9.26)
onebox (1.9.27.1)
addressable (~> 2.7.0)
htmlentities (~> 4.3)
multi_json (~> 1.11)
mustache
nokogiri (~> 1.7)
sanitize
openssl-signature_algorithm (0.3.0)
optimist (3.0.0)
openssl-signature_algorithm (0.4.0)
optimist (3.0.1)
parallel (1.19.1)
parallel_tests (2.31.0)
parallel_tests (2.32.0)
parallel
parser (2.7.0.4)
parser (2.7.1.1)
ast (~> 2.4.0)
pg (1.2.2)
pg (1.2.3)
progress (3.5.2)
pry (0.12.2)
coderay (~> 1.1.0)
@ -263,12 +262,12 @@ GEM
pry (>= 0.9.10, < 0.13.0)
pry-rails (0.3.9)
pry (>= 0.10.4)
public_suffix (4.0.3)
public_suffix (4.0.4)
puma (4.3.3)
nio4r (~> 2.0)
r2 (0.2.7)
rack (2.0.8)
rack-mini-profiler (1.1.6)
rack-mini-profiler (2.0.1)
rack (>= 1.2.0)
rack-protection (2.0.8.1)
rack
@ -279,12 +278,12 @@ GEM
nokogiri (>= 1.6)
rails-html-sanitizer (1.3.0)
loofah (~> 2.3)
rails_multisite (2.1.0)
rails_multisite (2.1.1)
activerecord (> 5.0, < 7)
railties (> 5.0, < 7)
railties (6.0.1)
actionpack (= 6.0.1)
activesupport (= 6.0.1)
railties (6.0.2.2)
actionpack (= 6.0.2.2)
activesupport (= 6.0.2.2)
method_source
rake (>= 0.8.7)
thor (>= 0.20.3, < 2.0)
@ -296,7 +295,7 @@ GEM
rb-fsevent (0.10.3)
rb-inotify (0.10.1)
ffi (~> 1.0)
rbtrace (0.4.11)
rbtrace (0.4.12)
ffi (>= 1.0.6)
msgpack (>= 0.4.3)
optimist (>= 3.0.0)
@ -320,7 +319,7 @@ GEM
rspec-mocks (~> 3.9.0)
rspec-core (3.9.1)
rspec-support (~> 3.9.1)
rspec-expectations (3.9.0)
rspec-expectations (3.9.1)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.9.0)
rspec-html-matchers (0.9.2)
@ -339,22 +338,24 @@ GEM
rspec-support (~> 3.8)
rspec-support (3.9.2)
rtlit (0.0.5)
rubocop (0.80.1)
rubocop (0.82.0)
jaro_winkler (~> 1.5.1)
parallel (~> 1.10)
parser (>= 2.7.0.1)
rainbow (>= 2.2.2, < 4.0)
rexml
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 1.7)
rubocop-discourse (1.0.2)
unicode-display_width (>= 1.4.0, < 2.0)
rubocop-discourse (2.0.1)
rubocop (>= 0.69.0)
ruby-prof (1.3.0)
rubocop-rspec (1.38.1)
rubocop (>= 0.68.1)
ruby-prof (1.3.2)
ruby-progressbar (1.10.1)
ruby-readability (0.7.0)
guess_html_encoding (>= 0.0.4)
nokogiri (>= 1.6.0)
rubyzip (2.2.0)
rubyzip (2.3.0)
safe_yaml (1.0.5)
sanitize (5.1.0)
crass (~> 1.0.2)
@ -374,7 +375,7 @@ GEM
activesupport (>= 3.1)
shoulda-matchers (4.3.0)
activesupport (>= 4.2.0)
sidekiq (6.0.5)
sidekiq (6.0.7)
connection_pool (>= 2.2.2)
rack (~> 2.0)
rack-protection (>= 2.0.0)
@ -396,19 +397,19 @@ GEM
thor (1.0.1)
thread_safe (0.3.6)
tilt (2.0.10)
tzinfo (1.2.6)
tzinfo (1.2.7)
thread_safe (~> 0.1)
uglifier (4.2.0)
execjs (>= 0.3.0, < 3)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.6)
unicode-display_width (1.6.1)
unicorn (5.5.3)
unf_ext (0.0.7.7)
unicode-display_width (1.7.0)
unicorn (5.5.4)
kgio (~> 2.6)
raindrops (~> 0.7)
uniform_notifier (1.13.0)
webmock (3.8.2)
webmock (3.8.3)
addressable (>= 2.3.6)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
@ -416,20 +417,20 @@ GEM
hkdf (~> 0.2)
jwt (~> 2.0)
yaml-lint (0.0.10)
zeitwerk (2.2.2)
zeitwerk (2.3.0)
PLATFORMS
ruby
DEPENDENCIES
actionmailer (= 6.0.1)
actionpack (= 6.0.1)
actionview (= 6.0.1)
actionmailer (= 6.0.2.2)
actionpack (= 6.0.2.2)
actionview (= 6.0.2.2)
actionview_precompiler
active_model_serializers (~> 0.8.3)
activemodel (= 6.0.1)
activerecord (= 6.0.1)
activesupport (= 6.0.1)
activemodel (= 6.0.2.2)
activerecord (= 6.0.2.2)
activesupport (= 6.0.2.2)
addressable
annotate
aws-sdk-s3
@ -456,15 +457,18 @@ 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
json
listen
lograge
logstash-event
@ -482,7 +486,7 @@ DEPENDENCIES
mini_sql
mini_suffix
minitest
mocha (= 1.8.0)
mocha
mock_redis
multi_json
mustache
@ -506,7 +510,7 @@ DEPENDENCIES
rack-mini-profiler
rack-protection
rails_multisite
railties (= 6.0.1)
railties (= 6.0.2.2)
rake
rb-fsevent
rb-inotify (~> 0.9)
@ -523,6 +527,7 @@ DEPENDENCIES
rtlit
rubocop
rubocop-discourse
rubocop-rspec
ruby-prof
ruby-readability
rubyzip
@ -539,7 +544,6 @@ DEPENDENCIES
stackprof
test-prof
thor
tilt
uglifier
unf
unicorn
@ -548,4 +552,4 @@ DEPENDENCIES
yaml-lint
BUNDLED WITH
2.1.1
2.1.4

View File

@ -34,7 +34,7 @@ To get your environment setup, follow the community setup guide for your operati
If you're familiar with how Rails works and are comfortable setting up your own environment, you can also try out the [**Discourse Advanced Developer Guide**](docs/DEVELOPER-ADVANCED.md), which is aimed primarily at Ubuntu and macOS environments.
Before you get started, ensure you have the following minimum versions: [Ruby 2.5+](https://www.ruby-lang.org/en/downloads/), [PostgreSQL 10+](https://www.postgresql.org/download/), [Redis 2.6+](https://redis.io/download). If you're having trouble, please see our [**TROUBLESHOOTING GUIDE**](docs/TROUBLESHOOTING.md) first!
Before you get started, ensure you have the following minimum versions: [Ruby 2.6+](https://www.ruby-lang.org/en/downloads/), [PostgreSQL 10+](https://www.postgresql.org/download/), [Redis 4.0+](https://redis.io/download). If you're having trouble, please see our [**TROUBLESHOOTING GUIDE**](docs/TROUBLESHOOTING.md) first!
## Setting up Discourse
@ -93,7 +93,7 @@ The original Discourse code contributors can be found in [**AUTHORS.MD**](docs/A
## Copyright / License
Copyright 2014 - 2019 Civilized Discourse Construction Kit, Inc.
Copyright 2014 - 2020 Civilized Discourse Construction Kit, Inc.
Licensed under the GNU General Public License Version 2.0 (or later);
you may not use this work except in compliance with the License.

View File

@ -0,0 +1,25 @@
// discourse-skip-module
(function() {
setTimeout(function() {
const $activateButton = $("#activate-account-button");
$activateButton.on("click", function() {
$activateButton.prop("disabled", true);
const hpPath = document.getElementById("data-activate-account").dataset
.path;
$.ajax(hpPath)
.then(function(hp) {
$("#password_confirmation").val(hp.value);
$("#challenge").val(
hp.challenge
.split("")
.reverse()
.join("")
);
$("#activate-account-form").submit();
})
.fail(function() {
$activateButton.prop("disabled", false);
});
});
}, 50);
})();

View File

@ -1,24 +0,0 @@
(function() {
setTimeout(function() {
const $activateButton = $("#activate-account-button");
$activateButton.on("click", function() {
$activateButton.prop("disabled", true);
const hpPath = document.getElementById("data-activate-account").dataset
.path;
$.ajax(hpPath)
.then(function(hp) {
$("#password_confirmation").val(hp.value);
$("#challenge").val(
hp.challenge
.split("")
.reverse()
.join("")
);
$("#activate-account-form").submit();
})
.fail(function() {
$activateButton.prop("disabled", false);
});
});
}, 50);
})();

View File

@ -0,0 +1,122 @@
import Component from "@ember/component";
import loadScript from "discourse/lib/load-script";
import { observes } from "discourse-common/utils/decorators";
import { on } from "@ember/object/evented";
export default Component.extend({
mode: "css",
classNames: ["ace-wrapper"],
_editor: null,
_skipContentChangeEvent: null,
disabled: false,
@observes("editorId")
editorIdChanged() {
if (this.autofocus) {
this.send("focus");
}
},
@observes("content")
contentChanged() {
const content = this.content || "";
if (this._editor && !this._skipContentChangeEvent) {
this._editor.getSession().setValue(content);
}
},
@observes("mode")
modeChanged() {
if (this._editor && !this._skipContentChangeEvent) {
this._editor.getSession().setMode("ace/mode/" + this.mode);
}
},
@observes("disabled")
disabledStateChanged() {
this.changeDisabledState();
},
changeDisabledState() {
const editor = this._editor;
if (editor) {
const disabled = this.disabled;
editor.setOptions({
readOnly: disabled,
highlightActiveLine: !disabled,
highlightGutterLine: !disabled
});
editor.container.parentNode.setAttribute("data-disabled", disabled);
}
},
_destroyEditor: on("willDestroyElement", function() {
if (this._editor) {
this._editor.destroy();
this._editor = null;
}
if (this.appEvents) {
// xxx: don't run during qunit tests
this.appEvents.off("ace:resize", this, "resize");
}
$(window).off("ace:resize");
}),
resize() {
if (this._editor) {
this._editor.resize();
}
},
didInsertElement() {
this._super(...arguments);
loadScript("/javascripts/ace/ace.js").then(() => {
window.ace.require(["ace/ace"], loadedAce => {
if (!this.element || this.isDestroying || this.isDestroyed) {
return;
}
const editor = loadedAce.edit(this.element.querySelector(".ace"));
editor.setTheme("ace/theme/chrome");
editor.setShowPrintMargin(false);
editor.setOptions({ fontSize: "14px" });
editor.getSession().setMode("ace/mode/" + this.mode);
editor.on("change", () => {
this._skipContentChangeEvent = true;
this.set("content", editor.getSession().getValue());
this._skipContentChangeEvent = false;
});
editor.$blockScrolling = Infinity;
editor.renderer.setScrollMargin(10, 10);
this.element.setAttribute("data-editor", editor);
this._editor = editor;
this.changeDisabledState();
$(window)
.off("ace:resize")
.on("ace:resize", () => this.appEvents.trigger("ace:resize"));
if (this.appEvents) {
// xxx: don't run during qunit tests
this.appEvents.on("ace:resize", this, "resize");
}
if (this.autofocus) {
this.send("focus");
}
});
});
},
actions: {
focus() {
if (this._editor) {
this._editor.focus();
this._editor.navigateFileEnd();
}
}
}
});

View File

@ -1,124 +0,0 @@
import Component from "@ember/component";
import loadScript from "discourse/lib/load-script";
import { observes } from "discourse-common/utils/decorators";
import { on } from "@ember/object/evented";
export default Component.extend({
mode: "css",
classNames: ["ace-wrapper"],
_editor: null,
_skipContentChangeEvent: null,
disabled: false,
@observes("editorId")
editorIdChanged() {
if (this.autofocus) {
this.send("focus");
}
},
@observes("content")
contentChanged() {
const content = this.content || "";
if (this._editor && !this._skipContentChangeEvent) {
this._editor.getSession().setValue(content);
}
},
@observes("mode")
modeChanged() {
if (this._editor && !this._skipContentChangeEvent) {
this._editor.getSession().setMode("ace/mode/" + this.mode);
}
},
@observes("disabled")
disabledStateChanged() {
this.changeDisabledState();
},
changeDisabledState() {
const editor = this._editor;
if (editor) {
const disabled = this.disabled;
editor.setOptions({
readOnly: disabled,
highlightActiveLine: !disabled,
highlightGutterLine: !disabled
});
editor.container.parentNode.setAttribute("data-disabled", disabled);
}
},
_destroyEditor: on("willDestroyElement", function() {
if (this._editor) {
this._editor.destroy();
this._editor = null;
}
if (this.appEvents) {
// xxx: don't run during qunit tests
this.appEvents.off("ace:resize", this, this.resize);
}
$(window).off("ace:resize");
}),
resize() {
if (this._editor) {
this._editor.resize();
}
},
didInsertElement() {
this._super(...arguments);
loadScript("/javascripts/ace/ace.js").then(() => {
window.ace.require(["ace/ace"], loadedAce => {
if (!this.element || this.isDestroying || this.isDestroyed) {
return;
}
const editor = loadedAce.edit(this.element.querySelector(".ace"));
editor.setTheme("ace/theme/chrome");
editor.setShowPrintMargin(false);
editor.setOptions({ fontSize: "14px" });
editor.getSession().setMode("ace/mode/" + this.mode);
editor.on("change", () => {
this._skipContentChangeEvent = true;
this.set("content", editor.getSession().getValue());
this._skipContentChangeEvent = false;
});
editor.$blockScrolling = Infinity;
editor.renderer.setScrollMargin(10, 10);
this.element.setAttribute("data-editor", editor);
this._editor = editor;
this.changeDisabledState();
$(window)
.off("ace:resize")
.on("ace:resize", () => {
this.appEvents.trigger("ace:resize");
});
if (this.appEvents) {
// xxx: don't run during qunit tests
this.appEvents.on("ace:resize", this, "resize");
}
if (this.autofocus) {
this.send("focus");
}
});
});
},
actions: {
focus() {
if (this._editor) {
this._editor.focus();
this._editor.navigateFileEnd();
}
}
}
});

View File

@ -0,0 +1,167 @@
import { makeArray } from "discourse-common/lib/helpers";
import { debounce, schedule } from "@ember/runloop";
import Component from "@ember/component";
import { number } from "discourse/lib/formatter";
import loadScript from "discourse/lib/load-script";
export default Component.extend({
classNames: ["admin-report-chart"],
limit: 8,
total: 0,
init() {
this._super(...arguments);
this.resizeHandler = () =>
debounce(this, this._scheduleChartRendering, 500);
},
didInsertElement() {
this._super(...arguments);
$(window).on("resize.chart", this.resizeHandler);
},
willDestroyElement() {
this._super(...arguments);
$(window).off("resize.chart", this.resizeHandler);
this._resetChart();
},
didReceiveAttrs() {
this._super(...arguments);
debounce(this, this._scheduleChartRendering, 100);
},
_scheduleChartRendering() {
schedule("afterRender", () => {
this._renderChart(
this.model,
this.element && this.element.querySelector(".chart-canvas")
);
});
},
_renderChart(model, chartCanvas) {
if (!chartCanvas) return;
const context = chartCanvas.getContext("2d");
const chartData = makeArray(model.get("chartData") || model.get("data"));
const prevChartData = makeArray(
model.get("prevChartData") || model.get("prev_data")
);
const labels = chartData.map(d => d.x);
const data = {
labels,
datasets: [
{
data: chartData.map(d => Math.round(parseFloat(d.y))),
backgroundColor: prevChartData.length
? "transparent"
: model.secondary_color,
borderColor: model.primary_color,
pointRadius: 3,
borderWidth: 1,
pointBackgroundColor: model.primary_color,
pointBorderColor: model.primary_color
}
]
};
if (prevChartData.length) {
data.datasets.push({
data: prevChartData.map(d => Math.round(parseFloat(d.y))),
borderColor: model.primary_color,
borderDash: [5, 5],
backgroundColor: "transparent",
borderWidth: 1,
pointRadius: 0
});
}
loadScript("/javascripts/Chart.min.js").then(() => {
this._resetChart();
if (!this.element) {
return;
}
this._chart = new window.Chart(context, this._buildChartConfig(data));
});
},
_buildChartConfig(data) {
return {
type: "line",
data,
options: {
tooltips: {
callbacks: {
title: tooltipItem =>
moment(tooltipItem[0].xLabel, "YYYY-MM-DD").format("LL")
}
},
legend: {
display: false
},
responsive: true,
maintainAspectRatio: false,
responsiveAnimationDuration: 0,
animation: {
duration: 0
},
layout: {
padding: {
left: 0,
top: 0,
right: 0,
bottom: 0
}
},
scales: {
yAxes: [
{
display: true,
ticks: {
userCallback: label => {
if (Math.floor(label) === label) return label;
},
callback: label => number(label),
sampleSize: 5,
maxRotation: 25,
minRotation: 25
}
}
],
xAxes: [
{
display: true,
gridLines: { display: false },
type: "time",
time: {
parser: "YYYY-MM-DD"
},
ticks: {
sampleSize: 5,
maxRotation: 50,
minRotation: 50
}
}
]
}
}
};
},
_resetChart() {
if (this._chart) {
this._chart.destroy();
this._chart = null;
}
}
});

View File

@ -1,168 +0,0 @@
import { makeArray } from "discourse-common/lib/helpers";
import { debounce } from "@ember/runloop";
import { schedule } from "@ember/runloop";
import Component from "@ember/component";
import { number } from "discourse/lib/formatter";
import loadScript from "discourse/lib/load-script";
export default Component.extend({
classNames: ["admin-report-chart"],
limit: 8,
total: 0,
init() {
this._super(...arguments);
this.resizeHandler = () =>
debounce(this, this._scheduleChartRendering, 500);
},
didInsertElement() {
this._super(...arguments);
$(window).on("resize.chart", this.resizeHandler);
},
willDestroyElement() {
this._super(...arguments);
$(window).off("resize.chart", this.resizeHandler);
this._resetChart();
},
didReceiveAttrs() {
this._super(...arguments);
debounce(this, this._scheduleChartRendering, 100);
},
_scheduleChartRendering() {
schedule("afterRender", () => {
this._renderChart(
this.model,
this.element && this.element.querySelector(".chart-canvas")
);
});
},
_renderChart(model, chartCanvas) {
if (!chartCanvas) return;
const context = chartCanvas.getContext("2d");
const chartData = makeArray(model.get("chartData") || model.get("data"));
const prevChartData = makeArray(
model.get("prevChartData") || model.get("prev_data")
);
const labels = chartData.map(d => d.x);
const data = {
labels,
datasets: [
{
data: chartData.map(d => Math.round(parseFloat(d.y))),
backgroundColor: prevChartData.length
? "transparent"
: model.secondary_color,
borderColor: model.primary_color,
pointRadius: 3,
borderWidth: 1,
pointBackgroundColor: model.primary_color,
pointBorderColor: model.primary_color
}
]
};
if (prevChartData.length) {
data.datasets.push({
data: prevChartData.map(d => Math.round(parseFloat(d.y))),
borderColor: model.primary_color,
borderDash: [5, 5],
backgroundColor: "transparent",
borderWidth: 1,
pointRadius: 0
});
}
loadScript("/javascripts/Chart.min.js").then(() => {
this._resetChart();
if (!this.element) {
return;
}
this._chart = new window.Chart(context, this._buildChartConfig(data));
});
},
_buildChartConfig(data) {
return {
type: "line",
data,
options: {
tooltips: {
callbacks: {
title: tooltipItem =>
moment(tooltipItem[0].xLabel, "YYYY-MM-DD").format("LL")
}
},
legend: {
display: false
},
responsive: true,
maintainAspectRatio: false,
responsiveAnimationDuration: 0,
animation: {
duration: 0
},
layout: {
padding: {
left: 0,
top: 0,
right: 0,
bottom: 0
}
},
scales: {
yAxes: [
{
display: true,
ticks: {
userCallback: label => {
if (Math.floor(label) === label) return label;
},
callback: label => number(label),
sampleSize: 5,
maxRotation: 25,
minRotation: 25
}
}
],
xAxes: [
{
display: true,
gridLines: { display: false },
type: "time",
time: {
parser: "YYYY-MM-DD"
},
ticks: {
sampleSize: 5,
maxRotation: 50,
minRotation: 50
}
}
]
}
}
};
},
_resetChart() {
if (this._chart) {
this._chart.destroy();
this._chart = null;
}
}
});

View File

@ -0,0 +1,155 @@
import { makeArray } from "discourse-common/lib/helpers";
import { debounce, schedule } from "@ember/runloop";
import Component from "@ember/component";
import { number } from "discourse/lib/formatter";
import loadScript from "discourse/lib/load-script";
export default Component.extend({
classNames: ["admin-report-chart", "admin-report-stacked-chart"],
init() {
this._super(...arguments);
this.resizeHandler = () =>
debounce(this, this._scheduleChartRendering, 500);
},
didInsertElement() {
this._super(...arguments);
$(window).on("resize.chart", this.resizeHandler);
},
willDestroyElement() {
this._super(...arguments);
$(window).off("resize.chart", this.resizeHandler);
this._resetChart();
},
didReceiveAttrs() {
this._super(...arguments);
debounce(this, this._scheduleChartRendering, 100);
},
_scheduleChartRendering() {
schedule("afterRender", () => {
if (!this.element) {
return;
}
this._renderChart(
this.model,
this.element.querySelector(".chart-canvas")
);
});
},
_renderChart(model, chartCanvas) {
if (!chartCanvas) return;
const context = chartCanvas.getContext("2d");
const chartData = makeArray(model.get("chartData") || model.get("data"));
const data = {
labels: chartData[0].data.mapBy("x"),
datasets: chartData.map(cd => {
return {
label: cd.label,
stack: "pageviews-stack",
data: cd.data.map(d => Math.round(parseFloat(d.y))),
backgroundColor: cd.color
};
})
};
loadScript("/javascripts/Chart.min.js").then(() => {
this._resetChart();
this._chart = new window.Chart(context, this._buildChartConfig(data));
});
},
_buildChartConfig(data) {
return {
type: "bar",
data,
options: {
responsive: true,
maintainAspectRatio: false,
responsiveAnimationDuration: 0,
hover: { mode: "index" },
animation: {
duration: 0
},
tooltips: {
mode: "index",
intersect: false,
callbacks: {
beforeFooter: tooltipItem => {
let total = 0;
tooltipItem.forEach(
item => (total += parseInt(item.yLabel || 0, 10))
);
return `= ${total}`;
},
title: tooltipItem =>
moment(tooltipItem[0].xLabel, "YYYY-MM-DD").format("LL")
}
},
layout: {
padding: {
left: 0,
top: 0,
right: 0,
bottom: 0
}
},
scales: {
yAxes: [
{
stacked: true,
display: true,
ticks: {
userCallback: label => {
if (Math.floor(label) === label) return label;
},
callback: label => number(label),
sampleSize: 5,
maxRotation: 25,
minRotation: 25
}
}
],
xAxes: [
{
display: true,
gridLines: { display: false },
type: "time",
offset: true,
time: {
parser: "YYYY-MM-DD",
minUnit: "day"
},
ticks: {
sampleSize: 5,
maxRotation: 50,
minRotation: 50
}
}
]
}
}
};
},
_resetChart() {
if (this._chart) {
this._chart.destroy();
this._chart = null;
}
}
});

View File

@ -1,156 +0,0 @@
import { makeArray } from "discourse-common/lib/helpers";
import { debounce } from "@ember/runloop";
import { schedule } from "@ember/runloop";
import Component from "@ember/component";
import { number } from "discourse/lib/formatter";
import loadScript from "discourse/lib/load-script";
export default Component.extend({
classNames: ["admin-report-chart", "admin-report-stacked-chart"],
init() {
this._super(...arguments);
this.resizeHandler = () =>
debounce(this, this._scheduleChartRendering, 500);
},
didInsertElement() {
this._super(...arguments);
$(window).on("resize.chart", this.resizeHandler);
},
willDestroyElement() {
this._super(...arguments);
$(window).off("resize.chart", this.resizeHandler);
this._resetChart();
},
didReceiveAttrs() {
this._super(...arguments);
debounce(this, this._scheduleChartRendering, 100);
},
_scheduleChartRendering() {
schedule("afterRender", () => {
if (!this.element) {
return;
}
this._renderChart(
this.model,
this.element.querySelector(".chart-canvas")
);
});
},
_renderChart(model, chartCanvas) {
if (!chartCanvas) return;
const context = chartCanvas.getContext("2d");
const chartData = makeArray(model.get("chartData") || model.get("data"));
const data = {
labels: chartData[0].data.mapBy("x"),
datasets: chartData.map(cd => {
return {
label: cd.label,
stack: "pageviews-stack",
data: cd.data.map(d => Math.round(parseFloat(d.y))),
backgroundColor: cd.color
};
})
};
loadScript("/javascripts/Chart.min.js").then(() => {
this._resetChart();
this._chart = new window.Chart(context, this._buildChartConfig(data));
});
},
_buildChartConfig(data) {
return {
type: "bar",
data,
options: {
responsive: true,
maintainAspectRatio: false,
responsiveAnimationDuration: 0,
hover: { mode: "index" },
animation: {
duration: 0
},
tooltips: {
mode: "index",
intersect: false,
callbacks: {
beforeFooter: tooltipItem => {
let total = 0;
tooltipItem.forEach(
item => (total += parseInt(item.yLabel || 0, 10))
);
return `= ${total}`;
},
title: tooltipItem =>
moment(tooltipItem[0].xLabel, "YYYY-MM-DD").format("LL")
}
},
layout: {
padding: {
left: 0,
top: 0,
right: 0,
bottom: 0
}
},
scales: {
yAxes: [
{
stacked: true,
display: true,
ticks: {
userCallback: label => {
if (Math.floor(label) === label) return label;
},
callback: label => number(label),
sampleSize: 5,
maxRotation: 25,
minRotation: 25
}
}
],
xAxes: [
{
display: true,
gridLines: { display: false },
type: "time",
offset: true,
time: {
parser: "YYYY-MM-DD",
minUnit: "day"
},
ticks: {
sampleSize: 5,
maxRotation: 50,
minRotation: 50
}
}
]
}
}
};
},
_resetChart() {
if (this._chart) {
this._chart.destroy();
this._chart = null;
}
}
});

View File

@ -0,0 +1,419 @@
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 { next } from "@ember/runloop";
import Component from "@ember/component";
import ReportLoader from "discourse/lib/reports-loader";
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";
const TABLE_OPTIONS = {
perPage: 8,
total: true,
limit: 20,
formatNumbers: true
};
const CHART_OPTIONS = {};
function collapseWeekly(data, average) {
let aggregate = [];
let bucket, i;
let offset = data.length % 7;
for (i = offset; i < data.length; i++) {
if (bucket && i % 7 === offset) {
if (average) {
bucket.y = parseFloat((bucket.y / 7.0).toFixed(2));
}
aggregate.push(bucket);
bucket = null;
}
bucket = bucket || { x: data[i].x, y: 0 };
bucket.y += data[i].y;
}
return aggregate;
}
export default Component.extend({
classNameBindings: ["isEnabled", "isLoading", "dasherizedDataSourceName"],
classNames: ["admin-report"],
isEnabled: true,
disabledLabel: I18n.t("admin.dashboard.disabled"),
isLoading: false,
rateLimitationString: null,
dataSourceName: null,
report: null,
model: null,
reportOptions: null,
forcedModes: null,
showAllReportsLink: false,
filters: null,
startDate: null,
endDate: null,
showTrend: false,
showHeader: true,
showTitle: true,
showFilteringUI: false,
showDatesOptions: alias("model.dates_filtering"),
showRefresh: or("showDatesOptions", "model.available_filters.length"),
shouldDisplayTrend: and("showTrend", "model.prev_period"),
init() {
this._super(...arguments);
this._reports = [];
},
startDate: reads("filters.startDate"),
endDate: reads("filters.endDate"),
didReceiveAttrs() {
this._super(...arguments);
if (this.report) {
this._renderReport(this.report, this.forcedModes, this.currentMode);
} else if (this.dataSourceName) {
this._fetchReport();
}
},
showError: or("showTimeoutError", "showExceptionError", "showNotFoundError"),
showNotFoundError: equal("model.error", "not_found"),
showTimeoutError: equal("model.error", "timeout"),
showExceptionError: equal("model.error", "exception"),
hasData: notEmpty("model.data"),
@discourseComputed("dataSourceName", "model.type")
dasherizedDataSourceName(dataSourceName, type) {
return (dataSourceName || type || "undefined").replace(/_/g, "-");
},
@discourseComputed("dataSourceName", "model.type")
dataSource(dataSourceName, type) {
dataSourceName = dataSourceName || type;
return `/admin/reports/${dataSourceName}`;
},
@discourseComputed("displayedModes.length")
showModes(displayedModesLength) {
return displayedModesLength > 1;
},
@discourseComputed("currentMode", "model.modes", "forcedModes")
displayedModes(currentMode, reportModes, forcedModes) {
const modes = forcedModes ? forcedModes.split(",") : reportModes;
return makeArray(modes).map(mode => {
const base = `btn-default mode-btn ${mode}`;
const cssClass = currentMode === mode ? `${base} is-current` : base;
return {
mode,
cssClass,
icon: mode === "table" ? "table" : "signal"
};
});
},
@discourseComputed("currentMode")
modeComponent(currentMode) {
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",
"filters.customFilters"
)
reportKey(dataSourceName, startDate, endDate, customFilters) {
if (!dataSourceName || !startDate || !endDate) return null;
let reportKey = "reports:";
reportKey += [
dataSourceName,
ENV.environment === "test" ? "start" : startDate.replace(/-/g, ""),
ENV.environment === "test" ? "end" : endDate.replace(/-/g, ""),
"[:prev_period]",
this.get("reportOptions.table.limit"),
// Convert all filter values to strings to ensure unique serialization
customFilters
? JSON.stringify(customFilters, (k, v) => (k ? `${v}` : v))
: null,
SCHEMA_VERSION
]
.filter(x => x)
.map(x => x.toString())
.join(":");
return reportKey;
},
actions: {
onChangeEndDate(date) {
const startDate = moment(this.startDate);
const newEndDate = moment(date).endOf("day");
if (newEndDate.isSameOrAfter(startDate)) {
this.set("endDate", newEndDate.format("YYYY-MM-DD"));
} else {
this.set("endDate", startDate.endOf("day").format("YYYY-MM-DD"));
}
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);
}
},
_computeReport() {
if (!this.element || this.isDestroying || this.isDestroyed) {
return;
}
if (!this._reports || !this._reports.length) {
return;
}
// on a slow network _fetchReport could be called multiple times between
// T and T+x, and all the ajax responses would occur after T+(x+y)
// to avoid any inconsistencies we filter by period and make sure
// the array contains only unique values
let filteredReports = this._reports.uniqBy("report_key");
let report;
const sort = r => {
if (r.length > 1) {
return r.findBy("type", this.dataSourceName);
} else {
return r;
}
};
if (!this.startDate || !this.endDate) {
report = sort(filteredReports)[0];
} else {
const reportKey = this.reportKey;
report = sort(
filteredReports.filter(r => r.report_key.includes(reportKey))
)[0];
if (!report) return;
}
if (report.error === "not_found") {
this.set("showFilteringUI", false);
}
this._renderReport(report, this.forcedModes, this.currentMode);
},
_renderReport(report, forcedModes, currentMode) {
const modes = forcedModes ? forcedModes.split(",") : report.modes;
currentMode = currentMode || (modes ? modes[0] : null);
this.setProperties({
model: report,
currentMode,
options: this._buildOptions(currentMode)
});
},
_fetchReport() {
this._super(...arguments);
this.setProperties({ isLoading: true, rateLimitationString: null });
next(() => {
let payload = this._buildPayload(["prev_period"]);
const callback = response => {
if (!this.element || this.isDestroying || this.isDestroyed) {
return;
}
this.set("isLoading", false);
if (response === 429) {
this.set(
"rateLimitationString",
I18n.t("admin.dashboard.too_many_requests")
);
} else if (response === 500) {
this.set("model.error", "exception");
} else if (response) {
this._reports.push(this._loadReport(response));
this._computeReport();
}
};
ReportLoader.enqueue(this.dataSourceName, payload.data, callback);
});
},
_buildPayload(facets) {
let payload = { data: { cache: true, facets } };
if (this.startDate) {
payload.data.start_date = moment
.utc(this.startDate, "YYYY-MM-DD")
.toISOString();
}
if (this.endDate) {
payload.data.end_date = moment
.utc(this.endDate, "YYYY-MM-DD")
.toISOString();
}
if (this.get("reportOptions.table.limit")) {
payload.data.limit = this.get("reportOptions.table.limit");
}
if (this.get("filters.customFilters")) {
payload.data.filters = this.get("filters.customFilters");
}
return payload;
},
_buildOptions(mode) {
if (mode === "table") {
const tableOptions = JSON.parse(JSON.stringify(TABLE_OPTIONS));
return EmberObject.create(
Object.assign(tableOptions, this.get("reportOptions.table") || {})
);
} else {
const chartOptions = JSON.parse(JSON.stringify(CHART_OPTIONS));
return EmberObject.create(
Object.assign(chartOptions, this.get("reportOptions.chart") || {})
);
}
},
_loadReport(jsonReport) {
Report.fillMissingDates(jsonReport, { filledField: "chartData" });
if (jsonReport.chartData && jsonReport.modes[0] === "stacked_chart") {
jsonReport.chartData = jsonReport.chartData.map(chartData => {
if (chartData.length > 40) {
return {
data: collapseWeekly(chartData.data),
req: chartData.req,
label: chartData.label,
color: chartData.color
};
} else {
return chartData;
}
});
} else if (jsonReport.chartData && jsonReport.chartData.length > 40) {
jsonReport.chartData = collapseWeekly(
jsonReport.chartData,
jsonReport.average
);
}
if (jsonReport.prev_data) {
Report.fillMissingDates(jsonReport, {
filledField: "prevChartData",
dataField: "prev_data",
starDate: jsonReport.prev_startDate,
endDate: jsonReport.prev_endDate
});
if (jsonReport.prevChartData && jsonReport.prevChartData.length > 40) {
jsonReport.prevChartData = collapseWeekly(
jsonReport.prevChartData,
jsonReport.average
);
}
}
return Report.create(jsonReport);
}
});

View File

@ -1,421 +0,0 @@
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 { next } from "@ember/runloop";
import Component from "@ember/component";
import ReportLoader from "discourse/lib/reports-loader";
import { exportEntity } from "discourse/lib/export-csv";
import { outputExportResult } from "discourse/lib/export-result";
import { isNumeric } from "discourse/lib/utilities";
import Report, { SCHEMA_VERSION } from "admin/models/report";
import ENV from "discourse-common/config/environment";
const TABLE_OPTIONS = {
perPage: 8,
total: true,
limit: 20,
formatNumbers: true
};
const CHART_OPTIONS = {};
function collapseWeekly(data, average) {
let aggregate = [];
let bucket, i;
let offset = data.length % 7;
for (i = offset; i < data.length; i++) {
if (bucket && i % 7 === offset) {
if (average) {
bucket.y = parseFloat((bucket.y / 7.0).toFixed(2));
}
aggregate.push(bucket);
bucket = null;
}
bucket = bucket || { x: data[i].x, y: 0 };
bucket.y += data[i].y;
}
return aggregate;
}
export default Component.extend({
classNameBindings: ["isEnabled", "isLoading", "dasherizedDataSourceName"],
classNames: ["admin-report"],
isEnabled: true,
disabledLabel: I18n.t("admin.dashboard.disabled"),
isLoading: false,
rateLimitationString: null,
dataSourceName: null,
report: null,
model: null,
reportOptions: null,
forcedModes: null,
showAllReportsLink: false,
filters: null,
startDate: null,
endDate: null,
showTrend: false,
showHeader: true,
showTitle: true,
showFilteringUI: false,
showDatesOptions: alias("model.dates_filtering"),
showRefresh: or("showDatesOptions", "model.available_filters.length"),
shouldDisplayTrend: and("showTrend", "model.prev_period"),
init() {
this._super(...arguments);
this._reports = [];
},
startDate: reads("filters.startDate"),
endDate: reads("filters.endDate"),
didReceiveAttrs() {
this._super(...arguments);
if (this.report) {
this._renderReport(this.report, this.forcedModes, this.currentMode);
} else if (this.dataSourceName) {
this._fetchReport();
}
},
showError: or("showTimeoutError", "showExceptionError", "showNotFoundError"),
showNotFoundError: equal("model.error", "not_found"),
showTimeoutError: equal("model.error", "timeout"),
showExceptionError: equal("model.error", "exception"),
hasData: notEmpty("model.data"),
@discourseComputed("dataSourceName", "model.type")
dasherizedDataSourceName(dataSourceName, type) {
return (dataSourceName || type || "undefined").replace(/_/g, "-");
},
@discourseComputed("dataSourceName", "model.type")
dataSource(dataSourceName, type) {
dataSourceName = dataSourceName || type;
return `/admin/reports/${dataSourceName}`;
},
@discourseComputed("displayedModes.length")
showModes(displayedModesLength) {
return displayedModesLength > 1;
},
@discourseComputed("currentMode", "model.modes", "forcedModes")
displayedModes(currentMode, reportModes, forcedModes) {
const modes = forcedModes ? forcedModes.split(",") : reportModes;
return makeArray(modes).map(mode => {
const base = `btn-default mode-btn ${mode}`;
const cssClass = currentMode === mode ? `${base} is-current` : base;
return {
mode,
cssClass,
icon: mode === "table" ? "table" : "signal"
};
});
},
@discourseComputed("currentMode")
modeComponent(currentMode) {
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",
"filters.customFilters"
)
reportKey(dataSourceName, startDate, endDate, customFilters) {
if (!dataSourceName || !startDate || !endDate) return null;
let reportKey = "reports:";
reportKey += [
dataSourceName,
ENV.environment === "test" ? "start" : startDate.replace(/-/g, ""),
ENV.environment === "test" ? "end" : endDate.replace(/-/g, ""),
"[:prev_period]",
this.get("reportOptions.table.limit"),
customFilters
? JSON.stringify(customFilters, (key, value) =>
isNumeric(value) ? value.toString() : value
)
: null,
SCHEMA_VERSION
]
.filter(x => x)
.map(x => x.toString())
.join(":");
return reportKey;
},
actions: {
onChangeEndDate(date) {
const startDate = moment(this.startDate);
const newEndDate = moment(date).endOf("day");
if (newEndDate.isSameOrAfter(startDate)) {
this.set("endDate", newEndDate.format("YYYY-MM-DD"));
} else {
this.set("endDate", startDate.endOf("day").format("YYYY-MM-DD"));
}
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);
}
},
_computeReport() {
if (!this.element || this.isDestroying || this.isDestroyed) {
return;
}
if (!this._reports || !this._reports.length) {
return;
}
// on a slow network _fetchReport could be called multiple times between
// T and T+x, and all the ajax responses would occur after T+(x+y)
// to avoid any inconsistencies we filter by period and make sure
// the array contains only unique values
let filteredReports = this._reports.uniqBy("report_key");
let report;
const sort = r => {
if (r.length > 1) {
return r.findBy("type", this.dataSourceName);
} else {
return r;
}
};
if (!this.startDate || !this.endDate) {
report = sort(filteredReports)[0];
} else {
const reportKey = this.reportKey;
report = sort(
filteredReports.filter(r => r.report_key.includes(reportKey))
)[0];
if (!report) return;
}
if (report.error === "not_found") {
this.set("showFilteringUI", false);
}
this._renderReport(report, this.forcedModes, this.currentMode);
},
_renderReport(report, forcedModes, currentMode) {
const modes = forcedModes ? forcedModes.split(",") : report.modes;
currentMode = currentMode || (modes ? modes[0] : null);
this.setProperties({
model: report,
currentMode,
options: this._buildOptions(currentMode)
});
},
_fetchReport() {
this._super(...arguments);
this.setProperties({ isLoading: true, rateLimitationString: null });
next(() => {
let payload = this._buildPayload(["prev_period"]);
const callback = response => {
if (!this.element || this.isDestroying || this.isDestroyed) {
return;
}
this.set("isLoading", false);
if (response === 429) {
this.set(
"rateLimitationString",
I18n.t("admin.dashboard.too_many_requests")
);
} else if (response === 500) {
this.set("model.error", "exception");
} else if (response) {
this._reports.push(this._loadReport(response));
this._computeReport();
}
};
ReportLoader.enqueue(this.dataSourceName, payload.data, callback);
});
},
_buildPayload(facets) {
let payload = { data: { cache: true, facets } };
if (this.startDate) {
payload.data.start_date = moment
.utc(this.startDate, "YYYY-MM-DD")
.toISOString();
}
if (this.endDate) {
payload.data.end_date = moment
.utc(this.endDate, "YYYY-MM-DD")
.toISOString();
}
if (this.get("reportOptions.table.limit")) {
payload.data.limit = this.get("reportOptions.table.limit");
}
if (this.get("filters.customFilters")) {
payload.data.filters = this.get("filters.customFilters");
}
return payload;
},
_buildOptions(mode) {
if (mode === "table") {
const tableOptions = JSON.parse(JSON.stringify(TABLE_OPTIONS));
return EmberObject.create(
Object.assign(tableOptions, this.get("reportOptions.table") || {})
);
} else {
const chartOptions = JSON.parse(JSON.stringify(CHART_OPTIONS));
return EmberObject.create(
Object.assign(chartOptions, this.get("reportOptions.chart") || {})
);
}
},
_loadReport(jsonReport) {
Report.fillMissingDates(jsonReport, { filledField: "chartData" });
if (jsonReport.chartData && jsonReport.modes[0] === "stacked_chart") {
jsonReport.chartData = jsonReport.chartData.map(chartData => {
if (chartData.length > 40) {
return {
data: collapseWeekly(chartData.data),
req: chartData.req,
label: chartData.label,
color: chartData.color
};
} else {
return chartData;
}
});
} else if (jsonReport.chartData && jsonReport.chartData.length > 40) {
jsonReport.chartData = collapseWeekly(
jsonReport.chartData,
jsonReport.average
);
}
if (jsonReport.prev_data) {
Report.fillMissingDates(jsonReport, {
filledField: "prevChartData",
dataField: "prev_data",
starDate: jsonReport.prev_startDate,
endDate: jsonReport.prev_endDate
});
if (jsonReport.prevChartData && jsonReport.prevChartData.length > 40) {
jsonReport.prevChartData = collapseWeekly(
jsonReport.prevChartData,
jsonReport.average
);
}
}
return Report.create(jsonReport);
}
});

View File

@ -0,0 +1,73 @@
import { schedule } from "@ember/runloop";
import Component from "@ember/component";
import { computed, action } from "@ember/object";
import loadScript, { loadCSS } from "discourse/lib/load-script";
import { observes } from "discourse-common/utils/decorators";
/**
An input field for a color.
@param hexValue is a reference to the color's hex value.
@param brightnessValue is a number from 0 to 255 representing the brightness of the color. See ColorSchemeColor.
@params valid is a boolean indicating if the input field is a valid color.
**/
export default Component.extend({
classNames: ["color-picker"],
onlyHex: true,
styleSelection: true,
maxlength: computed("onlyHex", function() {
return this.onlyHex ? 6 : null;
}),
@action
onHexInput(color) {
this.attrs.onChangeColor && this.attrs.onChangeColor(color || "");
},
@observes("hexValue", "brightnessValue", "valid")
hexValueChanged: function() {
const hex = this.hexValue;
let text = this.element.querySelector("input.hex-input");
this.attrs.onChangeColor && this.attrs.onChangeColor(hex);
if (this.valid) {
this.styleSelection &&
text.setAttribute(
"style",
"color: " +
(this.brightnessValue > 125 ? "black" : "white") +
"; background-color: #" +
hex +
";"
);
if (this.pickerLoaded) {
$(this.element.querySelector(".picker")).spectrum({
color: "#" + hex
});
}
} else {
this.styleSelection && text.setAttribute("style", "");
}
},
didInsertElement() {
loadScript("/javascripts/spectrum.js").then(() => {
loadCSS("/javascripts/spectrum.css").then(() => {
schedule("afterRender", () => {
$(this.element.querySelector(".picker"))
.spectrum({ color: "#" + this.hexValue })
.on("change.spectrum", (me, color) => {
this.set("hexValue", color.toHexString().replace("#", ""));
});
this.set("pickerLoaded", true);
});
});
});
schedule("afterRender", () => this.hexValueChanged());
}
});

View File

@ -1,58 +0,0 @@
import { schedule } from "@ember/runloop";
import Component from "@ember/component";
import loadScript, { loadCSS } from "discourse/lib/load-script";
import { observes } from "discourse-common/utils/decorators";
/**
An input field for a color.
@param hexValue is a reference to the color's hex value.
@param brightnessValue is a number from 0 to 255 representing the brightness of the color. See ColorSchemeColor.
@params valid is a boolean indicating if the input field is a valid color.
**/
export default Component.extend({
classNames: ["color-picker"],
@observes("hexValue", "brightnessValue", "valid")
hexValueChanged: function() {
var hex = this.hexValue;
let text = this.element.querySelector("input.hex-input");
if (this.valid) {
text.setAttribute(
"style",
"color: " +
(this.brightnessValue > 125 ? "black" : "white") +
"; background-color: #" +
hex +
";"
);
if (this.pickerLoaded) {
$(this.element.querySelector(".picker")).spectrum({
color: "#" + this.hexValue
});
}
} else {
text.setAttribute("style", "");
}
},
didInsertElement() {
loadScript("/javascripts/spectrum.js").then(() => {
loadCSS("/javascripts/spectrum.css").then(() => {
schedule("afterRender", () => {
$(this.element.querySelector(".picker"))
.spectrum({ color: "#" + this.hexValue })
.on("change.spectrum", (me, color) => {
this.set("hexValue", color.toHexString().replace("#", ""));
});
this.set("pickerLoaded", true);
});
});
});
schedule("afterRender", () => {
this.hexValueChanged();
});
}
});

View File

@ -0,0 +1,16 @@
import { action } from "@ember/object";
import FilterComponent from "admin/components/report-filters/filter";
export default FilterComponent.extend({
checked: false,
didReceiveAttrs() {
this._super(...arguments);
this.set("checked", !!this.filter.default);
},
@action
onChange() {
this.applyFilter(this.filter.id, !this.checked || undefined);
}
});

View File

@ -0,0 +1,12 @@
import { action } from "@ember/object";
import { readOnly } from "@ember/object/computed";
import FilterComponent from "admin/components/report-filters/filter";
export default FilterComponent.extend({
category: readOnly("filter.default"),
@action
onChange(categoryId) {
this.applyFilter(this.filter.id, categoryId || undefined);
}
});

View File

@ -1,16 +0,0 @@
import { readOnly } from "@ember/object/computed";
import FilterComponent from "admin/components/report-filters/filter";
export default FilterComponent.extend({
classNames: ["category-filter"],
layoutName: "admin/templates/components/report-filters/category",
category: readOnly("filter.default"),
actions: {
onChange(categoryId) {
this.applyFilter(this.get("filter.id"), categoryId || undefined);
}
}
});

View File

@ -1,7 +0,0 @@
import FilterComponent from "admin/components/report-filters/filter";
export default FilterComponent.extend({
classNames: ["file-extension-filter"],
layoutName: "admin/templates/components/report-filters/file-extension"
});

View File

@ -0,0 +1,9 @@
import Component from "@ember/component";
import { action } from "@ember/object";
export default Component.extend({
@action
onChange(value) {
this.applyFilter(this.filter.id, value);
}
});

View File

@ -1,8 +0,0 @@
import Component from "@ember/component";
export default Component.extend({
actions: {
onChange(value) {
this.applyFilter(this.get("filter.id"), value);
}
}
});

View File

@ -0,0 +1,18 @@
import { computed } from "@ember/object";
import FilterComponent from "admin/components/report-filters/filter";
export default FilterComponent.extend({
classNames: ["group-filter"],
@computed
get groupOptions() {
return (this.site.groups || []).map(group => {
return { name: group["name"], value: group["id"] };
});
},
@computed("filter.default")
get groupId() {
return this.filter.default ? parseInt(this.filter.default, 10) : null;
}
});

View File

@ -1,20 +0,0 @@
import FilterComponent from "admin/components/report-filters/filter";
import discourseComputed from "discourse-common/utils/decorators";
export default FilterComponent.extend({
classNames: ["group-filter"],
layoutName: "admin/templates/components/report-filters/group",
@discourseComputed()
groupOptions() {
return (this.site.groups || []).map(group => {
return { name: group["name"], value: group["id"] };
});
},
@discourseComputed("filter.default")
groupId(filterDefault) {
return filterDefault ? parseInt(filterDefault, 10) : null;
}
});

View File

@ -0,0 +1,3 @@
import FilterComponent from "admin/components/report-filters/filter";
export default FilterComponent.extend();

View File

@ -0,0 +1,138 @@
import { later, schedule } from "@ember/runloop";
import Component from "@ember/component";
import { iconHTML } from "discourse-common/lib/icon-library";
import discourseComputed, { on } from "discourse-common/utils/decorators";
/*global Resumable:true */
/**
Example usage:
{{resumable-upload
target="/admin/backups/upload"
success=(action "successAction")
error=(action "errorAction")
uploadText="UPLOAD"
}}
**/
export default Component.extend({
tagName: "button",
classNames: ["btn", "ru"],
classNameBindings: ["isUploading"],
attributeBindings: ["translatedTitle:title"],
resumable: null,
isUploading: false,
progress: 0,
rerenderTriggers: ["isUploading", "progress"],
uploadingIcon: null,
progressBar: null,
@on("init")
_initialize() {
this.resumable = new Resumable({
target: Discourse.getURL(this.target),
maxFiles: 1, // only 1 file at a time
headers: {
"X-CSRF-Token": document.querySelector("meta[name='csrf-token']")
.content
}
});
this.resumable.on("fileAdded", () => {
// automatically upload the selected file
this.resumable.upload();
// mark as uploading
later(() => {
this.set("isUploading", true);
this._updateIcon();
});
});
this.resumable.on("fileProgress", file => {
// update progress
later(() => {
this.set("progress", parseInt(file.progress() * 100, 10));
this._updateProgressBar();
});
});
this.resumable.on("fileSuccess", file => {
later(() => {
// mark as not uploading anymore
this._reset();
// fire an event to allow the parent route to reload its model
this.success(file.fileName);
});
});
this.resumable.on("fileError", (file, message) => {
later(() => {
// mark as not uploading anymore
this._reset();
// fire an event to allow the parent route to display the error message
this.error(file.fileName, message);
});
});
},
@on("didInsertElement")
_assignBrowse() {
schedule("afterRender", () => this.resumable.assignBrowse($(this.element)));
},
@on("willDestroyElement")
_teardown() {
if (this.resumable) {
this.resumable.cancel();
this.resumable = null;
}
},
@discourseComputed("title", "text")
translatedTitle(title, text) {
return title ? I18n.t(title) : text;
},
@discourseComputed("isUploading", "progress")
text(isUploading, progress) {
if (isUploading) {
return progress + " %";
} else {
return this.uploadText;
}
},
didReceiveAttrs() {
this._super(...arguments);
this._updateIcon();
},
click() {
if (this.isUploading) {
this.resumable.cancel();
later(() => this._reset());
return false;
} else {
return true;
}
},
_updateIcon() {
const icon = this.isUploading ? "times" : "upload";
this.set("uploadingIcon", `${iconHTML(icon)}`.htmlSafe());
},
_updateProgressBar() {
const pb = `${"width:" + this.progress + "%"}`.htmlSafe();
this.set("progressBar", pb);
},
_reset() {
this.setProperties({ isUploading: false, progress: 0 });
this._updateIcon();
this._updateProgressBar();
}
});

View File

@ -1,139 +0,0 @@
import { schedule } from "@ember/runloop";
import { later } from "@ember/runloop";
import Component from "@ember/component";
import { iconHTML } from "discourse-common/lib/icon-library";
import discourseComputed, { on } from "discourse-common/utils/decorators";
/*global Resumable:true */
/**
Example usage:
{{resumable-upload
target="/admin/backups/upload"
success=(action "successAction")
error=(action "errorAction")
uploadText="UPLOAD"
}}
**/
export default Component.extend({
tagName: "button",
classNames: ["btn", "ru"],
classNameBindings: ["isUploading"],
attributeBindings: ["translatedTitle:title"],
resumable: null,
isUploading: false,
progress: 0,
rerenderTriggers: ["isUploading", "progress"],
uploadingIcon: null,
progressBar: null,
@on("init")
_initialize() {
this.resumable = new Resumable({
target: Discourse.getURL(this.target),
maxFiles: 1, // only 1 file at a time
headers: {
"X-CSRF-Token": document.querySelector("meta[name='csrf-token']")
.content
}
});
this.resumable.on("fileAdded", () => {
// automatically upload the selected file
this.resumable.upload();
// mark as uploading
later(() => {
this.set("isUploading", true);
this._updateIcon();
});
});
this.resumable.on("fileProgress", file => {
// update progress
later(() => {
this.set("progress", parseInt(file.progress() * 100, 10));
this._updateProgressBar();
});
});
this.resumable.on("fileSuccess", file => {
later(() => {
// mark as not uploading anymore
this._reset();
// fire an event to allow the parent route to reload its model
this.success(file.fileName);
});
});
this.resumable.on("fileError", (file, message) => {
later(() => {
// mark as not uploading anymore
this._reset();
// fire an event to allow the parent route to display the error message
this.error(file.fileName, message);
});
});
},
@on("didInsertElement")
_assignBrowse() {
schedule("afterRender", () => this.resumable.assignBrowse($(this.element)));
},
@on("willDestroyElement")
_teardown() {
if (this.resumable) {
this.resumable.cancel();
this.resumable = null;
}
},
@discourseComputed("title", "text")
translatedTitle(title, text) {
return title ? I18n.t(title) : text;
},
@discourseComputed("isUploading", "progress")
text(isUploading, progress) {
if (isUploading) {
return progress + " %";
} else {
return this.uploadText;
}
},
didReceiveAttrs() {
this._super(...arguments);
this._updateIcon();
},
click() {
if (this.isUploading) {
this.resumable.cancel();
later(() => this._reset());
return false;
} else {
return true;
}
},
_updateIcon() {
const icon = this.isUploading ? "times" : "upload";
this.set("uploadingIcon", `${iconHTML(icon)}`.htmlSafe());
},
_updateProgressBar() {
const pb = `${"width:" + this.progress + "%"}`.htmlSafe();
this.set("progressBar", pb);
},
_reset() {
this.setProperties({ isUploading: false, progress: 0 });
this._updateIcon();
this._updateProgressBar();
}
});

View File

@ -1,14 +0,0 @@
import discourseComputed from "discourse-common/utils/decorators";
import { or } from "@ember/object/computed";
import Component from "@ember/component";
export default Component.extend({
classNames: ["controls"],
buttonDisabled: or("model.isSaving", "saveDisabled"),
@discourseComputed("model.isSaving")
savingText(saving) {
return saving ? "saving" : "save";
}
});

View File

@ -0,0 +1,49 @@
import Component from "@ember/component";
import { computed, action } from "@ember/object";
function RGBToHex(rgb) {
// Choose correct separator
let sep = rgb.indexOf(",") > -1 ? "," : " ";
// Turn "rgb(r,g,b)" into [r,g,b]
rgb = rgb
.substr(4)
.split(")")[0]
.split(sep);
let r = (+rgb[0]).toString(16),
g = (+rgb[1]).toString(16),
b = (+rgb[2]).toString(16);
if (r.length === 1) r = "0" + r;
if (g.length === 1) g = "0" + g;
if (b.length === 1) b = "0" + b;
return "#" + r + g + b;
}
export default Component.extend({
valid: computed("value", function() {
let value = this.value.toLowerCase();
let testColor = new Option().style;
testColor.color = value;
if (!testColor.color && !value.startsWith("#")) {
value = `#${value}`;
testColor = new Option().style;
testColor.color = value;
}
let hexifiedColor = RGBToHex(testColor.color);
if (hexifiedColor.includes("NaN")) {
hexifiedColor = testColor.color;
}
return testColor.color && hexifiedColor === value;
}),
@action
onChangeColor(color) {
this.set("value", color);
}
});

View File

@ -0,0 +1,40 @@
import Component from "@ember/component";
import { on } from "discourse-common/utils/decorators";
import highlightHTML from "discourse/lib/highlight-html";
export default Component.extend({
classNames: ["site-text"],
classNameBindings: ["siteText.overridden"],
@on("didInsertElement")
highlightTerm() {
const term = this._searchTerm();
if (term) {
highlightHTML(
this.element.querySelector(".site-text-id, .site-text-value"),
term,
{
className: "text-highlight"
}
);
}
$(this.element.querySelector(".site-text-value")).ellipsis();
},
click() {
this.editAction(this.siteText);
},
_searchTerm() {
const regex = this.searchRegex;
const siteText = this.siteText;
if (regex && siteText) {
const matches = siteText.value.match(new RegExp(regex, "i"));
if (matches) return matches[0];
}
return this.term;
}
});

View File

@ -1,37 +0,0 @@
import Component from "@ember/component";
import { on } from "discourse-common/utils/decorators";
export default Component.extend({
classNames: ["site-text"],
classNameBindings: ["siteText.overridden"],
@on("didInsertElement")
highlightTerm() {
const term = this._searchTerm();
if (term) {
$(
this.element.querySelector(".site-text-id, .site-text-value")
).highlight(term, {
className: "text-highlight"
});
}
$(this.element.querySelector(".site-text-value")).ellipsis();
},
click() {
this.editAction(this.siteText);
},
_searchTerm() {
const regex = this.searchRegex;
const siteText = this.siteText;
if (regex && siteText) {
const matches = siteText.value.match(new RegExp(regex, "i"));
if (matches) return matches[0];
}
return this.term;
}
});

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