Version bump
This commit is contained in:
commit
23fa6ff325
@ -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/
|
||||
|
||||
@ -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
|
||||
|
||||
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
@ -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'
|
||||
|
||||
231
.rubocop.yml
231
.rubocop.yml
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
};
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
34
Gemfile
@ -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
|
||||
|
||||
166
Gemfile.lock
166
Gemfile.lock
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
25
app/assets/javascripts/activate-account.js
Normal file
25
app/assets/javascripts/activate-account.js
Normal 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);
|
||||
})();
|
||||
@ -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);
|
||||
})();
|
||||
122
app/assets/javascripts/admin/components/ace-editor.js
Normal file
122
app/assets/javascripts/admin/components/ace-editor.js
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
167
app/assets/javascripts/admin/components/admin-report-chart.js
Normal file
167
app/assets/javascripts/admin/components/admin-report-chart.js
Normal 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;
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
419
app/assets/javascripts/admin/components/admin-report.js
Normal file
419
app/assets/javascripts/admin/components/admin-report.js
Normal 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);
|
||||
}
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
73
app/assets/javascripts/admin/components/color-input.js
Normal file
73
app/assets/javascripts/admin/components/color-input.js
Normal 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());
|
||||
}
|
||||
});
|
||||
@ -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();
|
||||
});
|
||||
}
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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"
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
@ -1,8 +0,0 @@
|
||||
import Component from "@ember/component";
|
||||
export default Component.extend({
|
||||
actions: {
|
||||
onChange(value) {
|
||||
this.applyFilter(this.get("filter.id"), value);
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -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;
|
||||
}
|
||||
});
|
||||
@ -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;
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,3 @@
|
||||
import FilterComponent from "admin/components/report-filters/filter";
|
||||
|
||||
export default FilterComponent.extend();
|
||||
138
app/assets/javascripts/admin/components/resumable-upload.js
Normal file
138
app/assets/javascripts/admin/components/resumable-upload.js
Normal 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();
|
||||
}
|
||||
});
|
||||
@ -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();
|
||||
}
|
||||
});
|
||||
@ -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";
|
||||
}
|
||||
});
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
40
app/assets/javascripts/admin/components/site-text-summary.js
Normal file
40
app/assets/javascripts/admin/components/site-text-summary.js
Normal 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;
|
||||
}
|
||||
});
|
||||
@ -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
Reference in New Issue
Block a user